205 lines
6.1 KiB
JavaScript
205 lines
6.1 KiB
JavaScript
/**
|
|
* orchestrator/test-files.mjs
|
|
*
|
|
* IPC + CLI parity for text file sources (port of kbot/src/source.ts — text slice only; images later).
|
|
* Fixtures: packages/kbot/tests/test-data/files (path below is resolved from orchestrator/).
|
|
*
|
|
* Run: npm run test:files
|
|
*
|
|
* Env (optional live LLM step):
|
|
* KBOT_IPC_LLM — set 0/false/off to skip live kbot-ai (default: run when key available)
|
|
* KBOT_ROUTER, KBOT_IPC_MODEL — same as test-ipc
|
|
*
|
|
* CLI (npm run test:files -- --help):
|
|
* --fixtures <dir> Override fixture root (default: ../../tests/test-data/files)
|
|
*/
|
|
|
|
import { spawn } from 'node:child_process';
|
|
import { dirname, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import net from 'node:net';
|
|
import { existsSync, unlinkSync } from 'node:fs';
|
|
import yargs from 'yargs';
|
|
import { hideBin } from 'yargs/helpers';
|
|
|
|
import {
|
|
distExePath,
|
|
platform,
|
|
uds,
|
|
timeouts,
|
|
kbotAiPayloadFromEnv,
|
|
} from './presets.js';
|
|
import {
|
|
createAssert,
|
|
payloadObj,
|
|
ipcLlmEnabled,
|
|
createIpcClient,
|
|
pipeWorkerStderr,
|
|
} from './test-commons.js';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const EXE = distExePath(__dirname);
|
|
const stats = createAssert();
|
|
const { assert } = stats;
|
|
|
|
const defaultFixtures = resolve(__dirname, '../../tests/test-data/files');
|
|
|
|
function parseArgv() {
|
|
const y = yargs(hideBin(process.argv))
|
|
.scriptName('test-files')
|
|
.usage('$0 [options]\n\nText file source IPC tests (fixtures under packages/kbot/tests/test-data/files).')
|
|
.option('fixtures', {
|
|
type: 'string',
|
|
default: defaultFixtures,
|
|
describe: 'Directory used as kbot-ai `path` (project root for includes)',
|
|
})
|
|
.strict()
|
|
.help()
|
|
.alias('h', 'help');
|
|
return y.parseSync();
|
|
}
|
|
|
|
/**
|
|
* @param {import('node:net').Socket} socket
|
|
* @param {string} fixturesDir
|
|
*/
|
|
async function runFileSuite(socket, fixturesDir) {
|
|
const ipc = createIpcClient(socket);
|
|
ipc.attach();
|
|
|
|
const readyMsg = await ipc.readyPromise;
|
|
assert(readyMsg.type === 'ready', 'worker ready');
|
|
|
|
console.log('\n── Dry-run source attachment (no LLM) ──\n');
|
|
|
|
/** @param {Record<string, unknown>} payload */
|
|
async function dry(payload) {
|
|
const msg = await ipc.request({ type: 'kbot-ai', payload }, timeouts.ipcDefault);
|
|
assert(msg.type === 'job_result', `job_result (got ${msg.type})`);
|
|
const p = payloadObj(msg);
|
|
assert(p?.dry_run === true, 'dry_run flag');
|
|
assert(p?.status === 'success', 'status success');
|
|
assert(Array.isArray(p?.sources), 'sources array');
|
|
return p;
|
|
}
|
|
|
|
let p = await dry({
|
|
dry_run: true,
|
|
path: fixturesDir,
|
|
include: ['bubblesort.js'],
|
|
prompt: 'What function is defined? Reply one word.',
|
|
});
|
|
assert(
|
|
p.sources.some((s) => String(s).includes('bubblesort')),
|
|
'sources lists bubblesort.js',
|
|
);
|
|
assert(
|
|
/bubbleSort/i.test(String(p.prompt_preview || '')),
|
|
'prompt_preview contains bubbleSort',
|
|
);
|
|
|
|
p = await dry({
|
|
dry_run: true,
|
|
path: fixturesDir,
|
|
include: ['*.js'],
|
|
prompt: 'List algorithms.',
|
|
});
|
|
assert(p.sources.length >= 2, 'glob *.js yields at least 2 files');
|
|
const names = p.sources.map((s) => String(s).toLowerCase());
|
|
assert(names.some((n) => n.includes('bubblesort')), 'glob includes bubblesort.js');
|
|
assert(names.some((n) => n.includes('factorial')), 'glob includes factorial.js');
|
|
|
|
p = await dry({
|
|
dry_run: true,
|
|
path: fixturesDir,
|
|
include: ['glob/data.json'],
|
|
prompt: 'What is the title?',
|
|
});
|
|
assert(
|
|
String(p.prompt_preview || '').includes('Injection Barrel'),
|
|
'JSON fixture content in preview',
|
|
);
|
|
|
|
if (ipcLlmEnabled()) {
|
|
console.log('\n── Live LLM — single file prompt ──\n');
|
|
const base = kbotAiPayloadFromEnv();
|
|
const payload = {
|
|
...base,
|
|
path: fixturesDir,
|
|
include: ['bubblesort.js'],
|
|
prompt:
|
|
process.env.KBOT_FILES_LIVE_PROMPT ||
|
|
'What is the name of the sorting algorithm in the code? Reply with two words: bubble sort',
|
|
};
|
|
const msg = await ipc.request({ type: 'kbot-ai', payload }, timeouts.kbotAi);
|
|
assert(msg.type === 'job_result', 'live job_result');
|
|
const lp = payloadObj(msg);
|
|
assert(lp?.status === 'success', 'live status success');
|
|
const text = String(lp?.text || '');
|
|
assert(/bubble/i.test(text), 'assistant mentions bubble (file context worked)');
|
|
} else {
|
|
console.log('\n── Live LLM — skipped (KBOT_IPC_LLM off) ──\n');
|
|
}
|
|
|
|
const shutdownRes = await ipc.request({ type: 'shutdown' }, timeouts.ipcDefault);
|
|
assert(shutdownRes.type === 'shutdown_ack', 'shutdown ack');
|
|
}
|
|
|
|
async function run() {
|
|
const argv = parseArgv();
|
|
const fixturesDir = resolve(argv.fixtures);
|
|
|
|
if (!existsSync(EXE)) {
|
|
console.error(`Binary not found: ${EXE}`);
|
|
process.exit(1);
|
|
}
|
|
if (!existsSync(fixturesDir)) {
|
|
console.error(`Fixtures directory not found: ${fixturesDir}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`\n📁 test:files — fixtures: ${fixturesDir}\n`);
|
|
|
|
const CPP_UDS_ARG = uds.workerArg();
|
|
if (!platform.isWin && existsSync(CPP_UDS_ARG)) {
|
|
unlinkSync(CPP_UDS_ARG);
|
|
}
|
|
|
|
const workerProc = spawn(EXE, ['worker', '--uds', CPP_UDS_ARG], { stdio: 'pipe' });
|
|
pipeWorkerStderr(workerProc);
|
|
|
|
let socket;
|
|
for (let i = 0; i < timeouts.connectAttempts; i++) {
|
|
try {
|
|
await new Promise((res, rej) => {
|
|
socket = net.connect(uds.connectOpts(CPP_UDS_ARG));
|
|
socket.once('connect', res);
|
|
socket.once('error', rej);
|
|
});
|
|
break;
|
|
} catch {
|
|
if (i === timeouts.connectAttempts - 1) throw new Error('connect failed');
|
|
await new Promise((r) => setTimeout(r, timeouts.connectRetryMs));
|
|
}
|
|
}
|
|
|
|
try {
|
|
await runFileSuite(socket, fixturesDir);
|
|
} finally {
|
|
try {
|
|
socket?.destroy();
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
workerProc.kill();
|
|
}
|
|
|
|
console.log(`\nDone. Passed: ${stats.passed} Failed: ${stats.failed}\n`);
|
|
process.exit(stats.failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
run().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|