266 lines
8.0 KiB
JavaScript
266 lines
8.0 KiB
JavaScript
import test from 'brittle'
|
|
import path from 'path'
|
|
import {
|
|
bareOsQvacSkipFromEnv,
|
|
bareOsQvacFlattenTools,
|
|
bareOsQvacSanitizeHistory,
|
|
bareOsQvacDetectToolDialect,
|
|
bareOsQvacEmitAgentEvent,
|
|
bareOsQvacResolveDeviceConfig,
|
|
bareOsQvacBuildModelConfig,
|
|
bareOsQvacPickVulkanPin,
|
|
createBareOsQvacBridge
|
|
} from './lib/services/bare-os-qvac-host.mjs'
|
|
import { bareOsQvacHostIsDarwin } from './lib/services/bare-os-qvac-gpu-probe.mjs'
|
|
|
|
test('BARE_OS_SKIP_QVAC disables bridge', async (t) => {
|
|
t.ok(bareOsQvacSkipFromEnv({ BARE_OS_SKIP_QVAC: '1' }))
|
|
t.ok(bareOsQvacSkipFromEnv({ BARE_OS_SKIP_QVAC: 'true' }))
|
|
t.absent(bareOsQvacSkipFromEnv({}))
|
|
const bridge = createBareOsQvacBridge({ env: { BARE_OS_SKIP_QVAC: '1' } })
|
|
t.absent(bridge.available())
|
|
t.is(bridge.status().status, 'unavailable')
|
|
t.ok(bridge.status().cacheDir)
|
|
t.is(bridge.status().hdmsPath, '/mnt/models')
|
|
await t.exception(() => bridge.loadModel({ modelSrc: 'QWEN3_1_7B_INST_Q4' }))
|
|
})
|
|
|
|
test('resolve device config defaults to auto GPU', async (t) => {
|
|
const d = bareOsQvacResolveDeviceConfig({}, {})
|
|
t.is(d.device, 'gpu')
|
|
if (bareOsQvacHostIsDarwin()) {
|
|
// macOS: ggml-metal exposes one device and only offloads with n_gpu_layers
|
|
// > 0, so the default pins mainGpu=0 and offloads all layers.
|
|
t.is(d.mainGpu, 0)
|
|
t.is(d.gpuLayers, 999)
|
|
} else {
|
|
t.is(d.mainGpu, 'auto')
|
|
t.absent(d.gpuLayers)
|
|
}
|
|
t.absent(d.noCpuFallback)
|
|
|
|
const cpu = bareOsQvacResolveDeviceConfig({}, { BARE_OS_QVAC_DEVICE: 'cpu' })
|
|
t.is(cpu.device, 'cpu')
|
|
|
|
const idx = bareOsQvacResolveDeviceConfig({ mainGpu: '1' }, {})
|
|
t.is(idx.mainGpu, 1)
|
|
|
|
// Explicit gpu_layers is honored on every host (no darwin default override).
|
|
const explicit = bareOsQvacResolveDeviceConfig({ gpuLayers: 12 }, {})
|
|
t.is(explicit.gpuLayers, 12)
|
|
const explicitCpu = bareOsQvacResolveDeviceConfig(
|
|
{ gpuLayers: 12 },
|
|
{ BARE_OS_QVAC_DEVICE: 'cpu' }
|
|
)
|
|
t.is(explicitCpu.gpuLayers, 12)
|
|
|
|
const cfg = bareOsQvacBuildModelConfig({
|
|
tools: true,
|
|
ctxSize: 4096,
|
|
device: 'gpu',
|
|
mainGpu: 'dedicated'
|
|
})
|
|
t.is(cfg.device, 'gpu')
|
|
t.is(cfg['main-gpu'], 'dedicated')
|
|
t.is(cfg.ctx_size, 4096)
|
|
t.absent(cfg.no_mmap)
|
|
|
|
const cpuCfg = bareOsQvacBuildModelConfig({
|
|
tools: false,
|
|
ctxSize: 2048,
|
|
device: 'cpu',
|
|
mainGpu: 'dedicated'
|
|
})
|
|
t.is(cpuCfg.device, 'cpu')
|
|
t.is(cpuCfg.gpu_layers, 0)
|
|
t.absent(cpuCfg['main-gpu'])
|
|
t.absent(cpuCfg.no_mmap)
|
|
})
|
|
|
|
test('pick vulkan pin prefers highest-VRAM vulkaninfo device', async (t) => {
|
|
// Caller passes rank-sorted list (highest VRAM first).
|
|
const pin = bareOsQvacPickVulkanPin([
|
|
{
|
|
index: 1,
|
|
name: 'NVIDIA',
|
|
memoryBytes: 4 * 1024 * 1024 * 1024,
|
|
deviceType: 'DISCRETE',
|
|
source: 'vulkaninfo'
|
|
},
|
|
{
|
|
index: 0,
|
|
name: 'Intel',
|
|
memoryBytes: 512 * 1024 * 1024,
|
|
deviceType: 'INTEGRATED',
|
|
source: 'vulkaninfo'
|
|
}
|
|
])
|
|
t.is(pin.index, 1)
|
|
|
|
const nvidiaOnly = bareOsQvacPickVulkanPin([
|
|
{
|
|
index: 0,
|
|
name: 'NVIDIA GeForce RTX 3050 Ti',
|
|
memoryBytes: 4 * 1024 * 1024 * 1024,
|
|
deviceType: 'DISCRETE',
|
|
source: 'nvidia-smi'
|
|
}
|
|
])
|
|
t.is(nvidiaOnly.index, 0)
|
|
|
|
const amdOnly = bareOsQvacPickVulkanPin([
|
|
{
|
|
index: 0,
|
|
name: 'AMD Radeon RX 7900 XTX',
|
|
memoryBytes: 24 * 1024 * 1024 * 1024,
|
|
deviceType: 'DISCRETE',
|
|
source: 'sysfs'
|
|
}
|
|
])
|
|
t.is(amdOnly.index, 0)
|
|
t.ok(/AMD\/RADV/.test(amdOnly.note))
|
|
|
|
const softOnly = bareOsQvacPickVulkanPin([
|
|
{
|
|
index: 0,
|
|
name: 'llvmpipe (LLVM 22.1.8, 256 bits)',
|
|
memoryBytes: 8 * 1024 * 1024 * 1024,
|
|
deviceType: 'PHYSICAL_DEVICE_TYPE_CPU',
|
|
source: 'vulkaninfo'
|
|
}
|
|
])
|
|
t.is(softOnly.index, null)
|
|
})
|
|
|
|
test('flatten tools nested → flat', async (t) => {
|
|
const flat = bareOsQvacFlattenTools([
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'run_command',
|
|
description: 'Run shell',
|
|
parameters: { type: 'object', properties: { cmd: { type: 'string' } } }
|
|
}
|
|
},
|
|
{ type: 'function', name: 'already_flat', parameters: { type: 'object', properties: {} } }
|
|
])
|
|
t.is(flat.length, 2)
|
|
t.is(flat[0].name, 'run_command')
|
|
t.is(flat[1].name, 'already_flat')
|
|
})
|
|
|
|
test('flatten strips nested schema fields QVAC rejects', async (t) => {
|
|
const flat = bareOsQvacFlattenTools([
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'todo_write',
|
|
description: 'todos',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
todos: {
|
|
type: 'array',
|
|
items: { type: 'object', properties: { id: { type: 'string' } } }
|
|
}
|
|
},
|
|
required: ['todos']
|
|
}
|
|
}
|
|
}
|
|
])
|
|
t.is(flat[0].parameters.properties.todos.type, 'array')
|
|
t.absent(flat[0].parameters.properties.todos.items)
|
|
})
|
|
|
|
test('sanitize history converts null content + tool_calls to string', async (t) => {
|
|
const out = bareOsQvacSanitizeHistory([
|
|
{
|
|
role: 'assistant',
|
|
content: null,
|
|
tool_calls: [
|
|
{ function: { name: 'read_file', arguments: '{"path":"/x"}' } }
|
|
]
|
|
},
|
|
{ role: 'tool', tool_call_id: 'c1', content: 'ok' }
|
|
])
|
|
t.is(out[0].content.includes('read_file'), true)
|
|
t.is(out[1].content, 'ok')
|
|
t.is(bareOsQvacDetectToolDialect('QWEN3_5_2B_MULTIMODAL_Q4_K_M'), 'qwen35')
|
|
t.is(bareOsQvacDetectToolDialect('QWEN3_1_7B_INST_Q4'), 'hermes')
|
|
t.is(bareOsQvacDetectToolDialect('LFM2_1_2B_Q4_K_M'), 'pythonic')
|
|
t.is(bareOsQvacDetectToolDialect('DEEPSEEK_V3_2_CHAT'), 'dsml')
|
|
const withText = bareOsQvacSanitizeHistory([
|
|
{
|
|
role: 'assistant',
|
|
content: 'Working.',
|
|
tool_calls: [
|
|
{ function: { name: 'read_file', arguments: '{"path":"/x"}' } }
|
|
]
|
|
}
|
|
])
|
|
t.is(withText[0].content.includes('Working.'), true)
|
|
t.is(withText[0].content.includes('<tool_call>'), true)
|
|
})
|
|
|
|
test('emit agent events from QVAC stream shapes', async (t) => {
|
|
/** @type {Record<string, unknown>[]} */
|
|
const out = []
|
|
const state = { toolIndex: 0 }
|
|
bareOsQvacEmitAgentEvent({ type: 'contentDelta', text: 'hi' }, (e) => out.push(e), state)
|
|
bareOsQvacEmitAgentEvent({ type: 'thinkingDelta', text: 'think' }, (e) => out.push(e), state)
|
|
bareOsQvacEmitAgentEvent(
|
|
{ type: 'toolCall', call: { name: 'read_file', arguments: { path: '/x' }, id: 'c1' } },
|
|
(e) => out.push(e),
|
|
state
|
|
)
|
|
t.is(out[0].type, 'delta_content')
|
|
t.is(out[0].content, 'hi')
|
|
t.is(out[1].type, 'delta_reasoning')
|
|
t.is(out[2].type, 'delta_tool_calls')
|
|
t.is(/** @type {any} */ (out[2]).tool_calls[0].function.name, 'read_file')
|
|
t.ok(String(/** @type {any} */ (out[2]).tool_calls[0].function.arguments).includes('/x'))
|
|
})
|
|
|
|
test('toolCall emit increments toolIndex once per call', async (t) => {
|
|
const state = { toolIndex: 0 }
|
|
/** @type {Record<string, unknown>[]} */
|
|
const out = []
|
|
bareOsQvacEmitAgentEvent(
|
|
{ type: 'toolCall', call: { name: 'get_system_info', arguments: '{}', id: 'a' } },
|
|
(e) => out.push(e),
|
|
state
|
|
)
|
|
bareOsQvacEmitAgentEvent(
|
|
{ type: 'toolCall', call: { name: 'get_system_info', arguments: '{}', id: 'b' } },
|
|
(e) => out.push(e),
|
|
state
|
|
)
|
|
t.is(state.toolIndex, 2)
|
|
t.is(/** @type {any} */ (out[0]).tool_calls[0].index, 0)
|
|
t.is(/** @type {any} */ (out[1]).tool_calls[0].index, 1)
|
|
})
|
|
|
|
test('backendsDir resolves to real prebuilds (not app.bundle)', async (t) => {
|
|
const {
|
|
bareOsQvacNativeHostId,
|
|
bareOsQvacResolveBackendsDir
|
|
} = await import('./lib/services/bare-os-qvac-sdk-bind.mjs')
|
|
const host = bareOsQvacNativeHostId()
|
|
t.ok(/^(darwin|linux|win32)-/.test(host))
|
|
const dir = bareOsQvacResolveBackendsDir()
|
|
// Dev hosts may use package prebuilds, ~/.bare-os/qvac/backends, or the
|
|
// packed sidecar at ~/.local/share/bare-os/booter/qvac-backends.
|
|
if (dir) {
|
|
t.absent(/app\.bundle/i.test(dir))
|
|
t.ok(
|
|
dir.endsWith('prebuilds') ||
|
|
dir.includes(path.join('qvac', 'backends')) ||
|
|
dir.endsWith('qvac-backends') ||
|
|
dir.includes(path.join('qvac-backends'))
|
|
)
|
|
} else {
|
|
t.pass('no backends for this host (ok)')
|
|
}
|
|
})
|