test: cover cli edge cases

Change-Id: Ia6114ab7ba24e32f7f86ff76710a596b0c24682c
This commit is contained in:
wujunchen 2026-04-26 18:16:38 +08:00
parent 174fa7295d
commit 6dd7ef15a4
5 changed files with 83 additions and 22 deletions

40
main.js

File diff suppressed because one or more lines are too long

View file

@ -19,7 +19,7 @@ import {
import { serializeCacheFile, shouldConfirmRegenerate, touchCacheEntry } from './src/cache';
import { CacheManager } from './src/cache-manager';
import { activeIndexAfterCardDelete, removeCardAt, updateCardAt } from './src/cards';
import { resolveCliPath } from './src/cli';
import { resolveCliPath, runCli } from './src/cli';
import { cancellationNoticeKey, summarizeDocument } from './src/generation';
import {
classifyGenerationError,
@ -798,6 +798,7 @@ export const __test = {
requestBatchCancel,
removeCardAt,
resolveCliPath,
runCli,
serializeCacheFile,
shouldConfirmRegenerate,
shouldSkipBatchFile,

View file

@ -57,6 +57,7 @@ export function runCli(
stdinText: string,
timeoutMs: number,
job?: GenerationJob,
spawnImpl: typeof spawn = spawn,
): Promise<{ stdout: string; stderr: string }> {
return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
let child: ReturnType<typeof spawn>;
@ -75,7 +76,7 @@ export function runCli(
resolve(value);
};
try {
child = spawn(cmd, args, {
child = spawnImpl(cmd, args, {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,

View file

@ -67,6 +67,7 @@ assert.strictEqual(typeof t.addIconButton, 'function');
assert.strictEqual(typeof t.addTextButton, 'function');
assert.strictEqual(typeof t.copyToClipboard, 'function');
assert.strictEqual(typeof t.resolveCliPath, 'function');
assert.strictEqual(typeof t.runCli, 'function');
assert.strictEqual(typeof t.buildPrompts, 'function');
assert.strictEqual(typeof t.buildOpenAiChatBody, 'function');
assert.strictEqual(typeof t.extractJson, 'function');

View file

@ -1,4 +1,5 @@
const assert = require('assert');
const { EventEmitter } = require('events');
const Module = require('module');
const originalLoad = Module._load;
@ -677,8 +678,65 @@ try {
fsModule.existsSync = origExistsSync;
}
function createFakeChild() {
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.stdin = {
written: '',
ended: false,
write(value) {
this.written += value;
},
end() {
this.ended = true;
},
};
child.killedWith = null;
child.kill = (signal) => {
child.killedWith = signal;
return true;
};
return child;
}
async function testRunCliEdgeCases() {
const timeoutChild = createFakeChild();
await assert.rejects(
() => t.runCli('fake', [], '', 5, undefined, () => timeoutChild),
/CLI timed out \(5ms\)/,
'runCli rejects on timeout',
);
assert.strictEqual(timeoutChild.killedWith, 'SIGKILL', 'runCli kills timed out processes');
const cancelChild = createFakeChild();
let cancelHandler = null;
const cancelPromise = t.runCli(
'fake',
[],
'',
1000,
{ key: 'cancel.md', onCancel: (handler) => { cancelHandler = handler; } },
() => cancelChild,
);
cancelHandler();
await assert.rejects(
() => cancelPromise,
(err) => err && err.code === 'cancelled' && err.key === 'cancel.md',
'runCli rejects with GenerationJobCancelledError on cancellation',
);
assert.strictEqual(cancelChild.killedWith, 'SIGKILL', 'runCli kills cancelled processes');
await assert.rejects(
() => t.runCli('fake', [], '', 100, undefined, () => ({ stdout: null, stderr: null, stdin: null })),
/CLI process streams are unavailable/,
'runCli reports unavailable stdio streams',
);
}
// Wrap the tail in an async runner so module-level tests can include async cases.
(async () => {
await testRunCliEdgeCases();
await testCacheManagerMove();
await testSummarizeDocumentAnchorSorting();
console.log('modules tests passed');