mirror of
https://github.com/fancive/obsidian-parallel-reader.git
synced 2026-07-22 06:53:43 +00:00
fix: harden CLI backend smoke checks
Change-Id: I7b4388095412b155c056c42661cdf481bd601e4c
This commit is contained in:
parent
d00075779e
commit
714b6a368b
12 changed files with 317 additions and 46 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "parallel-reader",
|
||||
"name": "Parallel Reader",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.20",
|
||||
"minAppVersion": "1.8.7",
|
||||
"description": "AI-powered split-view reading: source note on the left, LLM-generated summary cards on the right with scroll-sync highlighting.",
|
||||
"author": "lancivez",
|
||||
|
|
|
|||
60
src/backend-test.ts
Normal file
60
src/backend-test.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
'use strict';
|
||||
|
||||
import { requestUrl } from 'obsidian';
|
||||
import { resolveCliPath, runCli, summarizeViaClaudeCode, summarizeViaCodex } from './cli';
|
||||
import type { RequestUrlFunction } from './provider-request';
|
||||
import { testApiBackend } from './providers';
|
||||
import { normalizeCliTimeoutMs } from './settings';
|
||||
import type { PluginSettings } from './types';
|
||||
|
||||
const CLI_TEST_SYSTEM =
|
||||
'Return only valid JSON with exactly one card: {"cards":[{"title":"CLI smoke","anchor":"parallel reader smoke anchor","gist":"ok","bullets":["ok"]}]}';
|
||||
const CLI_TEST_USER = 'parallel reader smoke anchor';
|
||||
const CLI_TEST_TIMEOUT_MS = 60000;
|
||||
|
||||
type SpawnImpl = Parameters<typeof runCli>[5];
|
||||
|
||||
export interface BackendTestDeps {
|
||||
requestUrlImpl?: RequestUrlFunction;
|
||||
spawnImpl?: SpawnImpl;
|
||||
}
|
||||
|
||||
function cliSmokeSettings(settings: PluginSettings): PluginSettings {
|
||||
return {
|
||||
...settings,
|
||||
cliTimeoutMs: Math.min(normalizeCliTimeoutMs(settings.cliTimeoutMs), CLI_TEST_TIMEOUT_MS),
|
||||
minCards: 1,
|
||||
maxCards: 1,
|
||||
maxDocChars: 1000,
|
||||
};
|
||||
}
|
||||
|
||||
export async function testBackend(settings: PluginSettings, deps: BackendTestDeps = {}): Promise<string> {
|
||||
if (settings.backend === 'claude-code') {
|
||||
const cmd = resolveCliPath('claude', settings.cliPath);
|
||||
const version = await runCli(cmd, ['--version'], '', 10000, undefined, deps.spawnImpl);
|
||||
const cards = await summarizeViaClaudeCode(
|
||||
CLI_TEST_SYSTEM,
|
||||
CLI_TEST_USER,
|
||||
cliSmokeSettings(settings),
|
||||
undefined,
|
||||
deps.spawnImpl,
|
||||
);
|
||||
return `claude @ ${cmd}\n${version.stdout.trim()}\nsmoke: ${cards.length} card`;
|
||||
}
|
||||
|
||||
if (settings.backend === 'codex') {
|
||||
const cmd = resolveCliPath('codex', settings.cliPath);
|
||||
const version = await runCli(cmd, ['--version'], '', 10000, undefined, deps.spawnImpl);
|
||||
const cards = await summarizeViaCodex(
|
||||
CLI_TEST_SYSTEM,
|
||||
CLI_TEST_USER,
|
||||
cliSmokeSettings(settings),
|
||||
undefined,
|
||||
deps.spawnImpl,
|
||||
);
|
||||
return `codex @ ${cmd}\n${version.stdout.trim()}\nsmoke: ${cards.length} card`;
|
||||
}
|
||||
|
||||
return testApiBackend(deps.requestUrlImpl || requestUrl, settings);
|
||||
}
|
||||
65
src/cli.ts
65
src/cli.ts
|
|
@ -89,6 +89,7 @@ export class CliProcessError extends Error {
|
|||
|
||||
const DIAG_STDERR_TAIL_CHARS = 1500;
|
||||
const DIAG_STDOUT_TAIL_CHARS = 1500;
|
||||
const EMPTY_MCP_CONFIG = '{"mcpServers":{}}';
|
||||
|
||||
function tail(text: string, max: number): string {
|
||||
if (text.length <= max) return text;
|
||||
|
|
@ -108,6 +109,38 @@ function utf8ByteLength(text: string): number {
|
|||
return Buffer.byteLength(text, 'utf8');
|
||||
}
|
||||
|
||||
function claudeResultText(stdout: string): string {
|
||||
const events: Array<Record<string, unknown>> = [];
|
||||
try {
|
||||
const parsed = JSON.parse(stdout);
|
||||
if (Array.isArray(parsed)) {
|
||||
events.push(...(parsed.filter((e) => e && typeof e === 'object') as Array<Record<string, unknown>>));
|
||||
} else if (parsed && typeof parsed === 'object') {
|
||||
events.push(parsed as Record<string, unknown>);
|
||||
}
|
||||
} catch {
|
||||
for (const line of stdout.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (parsed && typeof parsed === 'object') events.push(parsed as Record<string, unknown>);
|
||||
} catch {
|
||||
// Ignore non-JSON progress lines and keep scanning for result events.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
const event = events[i];
|
||||
if (event.type === 'result' && typeof event.result === 'string') return event.result;
|
||||
}
|
||||
const single = events[0];
|
||||
if (single && typeof single.result === 'string') return single.result;
|
||||
if (single && typeof single.content === 'string') return single.content;
|
||||
return '';
|
||||
}
|
||||
|
||||
export function runCli(
|
||||
cmd: string,
|
||||
args: string[],
|
||||
|
|
@ -308,7 +341,7 @@ export function runCli(
|
|||
try {
|
||||
childStdin.write(stdinText);
|
||||
childStdin.end();
|
||||
} catch (_e) {
|
||||
} catch {
|
||||
// Child may have exited before stdin was written.
|
||||
}
|
||||
} else {
|
||||
|
|
@ -332,38 +365,32 @@ export async function summarizeViaClaudeCode(
|
|||
const args = [
|
||||
'-p',
|
||||
'--output-format',
|
||||
'json',
|
||||
'stream-json',
|
||||
'--append-system-prompt',
|
||||
system,
|
||||
'--tools',
|
||||
'',
|
||||
'--disallowed-tools',
|
||||
'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch,TodoWrite,Task',
|
||||
'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch,TodoWrite,Task,LSP,NotebookEdit',
|
||||
'--no-session-persistence',
|
||||
'--disable-slash-commands',
|
||||
'--no-chrome',
|
||||
'--strict-mcp-config',
|
||||
'--mcp-config',
|
||||
EMPTY_MCP_CONFIG,
|
||||
];
|
||||
const claudeModel = (settings.model || '').trim();
|
||||
if (claudeModel) args.push('--model', claudeModel);
|
||||
const { stdout } = await runCli(cmd, args, user, settings.cliTimeoutMs, job, spawnImpl);
|
||||
|
||||
// --output-format json produces a JSON array of event objects.
|
||||
// Find the "result" entry and extract its .result text field.
|
||||
let resultText = '';
|
||||
try {
|
||||
const events = JSON.parse(stdout);
|
||||
if (Array.isArray(events)) {
|
||||
const resultEvent = events.find((e: Record<string, unknown>) => e.type === 'result');
|
||||
if (resultEvent && typeof resultEvent.result === 'string') {
|
||||
resultText = resultEvent.result;
|
||||
}
|
||||
} else if (events && typeof events === 'object') {
|
||||
// Older CLI versions return a single object
|
||||
resultText = events.result || events.content || '';
|
||||
}
|
||||
} catch (_) {
|
||||
const resultText = claudeResultText(stdout);
|
||||
if (!resultText && stdout.trim()) {
|
||||
console.warn(
|
||||
'[parallel-reader] claude CLI returned unexpected output. length=',
|
||||
stdout.length,
|
||||
'head=',
|
||||
stdout.slice(0, 80),
|
||||
);
|
||||
throw new Error(translate(settings, 'errorClaudeCliBadJson', { length: String(stdout.length) }));
|
||||
}
|
||||
|
||||
if (!resultText) {
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ export const STRINGS: Record<string, Record<string, string>> = {
|
|||
settingCliPathDesc: '留空则自动探测常见位置;Obsidian GUI 启动时不继承 shell PATH,必要时填绝对路径',
|
||||
settingCliPathPlaceholder: '例:/Users/you/bin/codex',
|
||||
settingCliTimeoutName: 'CLI 超时 (ms)',
|
||||
settingCliTimeoutDesc: 'CLI 调用的最大等待时间(毫秒),最小 1000,默认 120000',
|
||||
settingCliTimeoutDesc: 'CLI 调用的最大等待时间(毫秒),最小 1000,默认 300000',
|
||||
sectionQuickSetup: '快速配置',
|
||||
sectionReadingOutput: '阅读输出',
|
||||
sectionAdvancedConnection: '高级连接设置',
|
||||
|
|
@ -183,8 +183,9 @@ export const STRINGS: Record<string, Record<string, string>> = {
|
|||
settingCustomPromptPlaceholder: '留空使用内置 prompt',
|
||||
settingTestBackendName: '测试当前后端',
|
||||
settingTestBackendButton: '测试',
|
||||
settingTestBackendButtonRunning: '测试中...',
|
||||
settingTestBackendDescApi: '会发起一次最小 LLM 请求验证 API 设置',
|
||||
settingTestBackendDescCli: '调用 `<cli> --version` 验证 spawn 能找到二进制',
|
||||
settingTestBackendDescCli: '先调用 `<cli> --version`,再跑一次最小 JSON 生成,验证认证、输出解析和超时设置',
|
||||
backendTestFailed: '✗ 后端测试失败:{error}',
|
||||
settingExportFolderName: '导出文件夹',
|
||||
settingExportFolderDesc: '对照笔记生成位置(相对 Vault 根)',
|
||||
|
|
@ -360,7 +361,7 @@ export const STRINGS: Record<string, Record<string, string>> = {
|
|||
'Leave blank to auto-detect common paths. Obsidian launched from the GUI may not inherit shell PATH.',
|
||||
settingCliPathPlaceholder: 'Example: /Users/you/bin/codex',
|
||||
settingCliTimeoutName: 'CLI timeout (ms)',
|
||||
settingCliTimeoutDesc: 'Max wait time for CLI calls (ms). Minimum 1000, default 120000.',
|
||||
settingCliTimeoutDesc: 'Max wait time for CLI calls (ms). Minimum 1000, default 300000.',
|
||||
sectionQuickSetup: 'Quick setup',
|
||||
sectionReadingOutput: 'Reading output',
|
||||
sectionAdvancedConnection: 'Advanced connection',
|
||||
|
|
@ -401,8 +402,10 @@ export const STRINGS: Record<string, Record<string, string>> = {
|
|||
settingCustomPromptPlaceholder: 'Leave blank to use built-in prompt',
|
||||
settingTestBackendName: 'Test current backend',
|
||||
settingTestBackendButton: 'Test',
|
||||
settingTestBackendButtonRunning: 'Testing...',
|
||||
settingTestBackendDescApi: 'Sends a minimal LLM request to validate API settings.',
|
||||
settingTestBackendDescCli: 'Runs `<cli> --version` to verify the binary can be spawned.',
|
||||
settingTestBackendDescCli:
|
||||
'Runs `<cli> --version`, then a minimal JSON generation to validate auth, output parsing, and timeout behavior.',
|
||||
backendTestFailed: '✗ Backend test failed: {error}',
|
||||
settingExportFolderName: 'Export folder',
|
||||
settingExportFolderDesc: 'Parallel-note output location, relative to the Vault root.',
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
import { type App, Notice, type Plugin, PluginSettingTab, requestUrl, Setting } from 'obsidian';
|
||||
import { resolveCliPath, runCli } from './cli';
|
||||
import { testApiBackend } from './providers';
|
||||
import { type App, Notice, type Plugin, PluginSettingTab, Setting } from 'obsidian';
|
||||
import { testBackend } from './backend-test';
|
||||
import {
|
||||
API_AUTH_TYPES,
|
||||
API_FORMATS,
|
||||
|
|
@ -20,20 +19,6 @@ import {
|
|||
} from './settings';
|
||||
import type { PluginHost, PluginSettings } from './types';
|
||||
|
||||
async function testBackend(settings: PluginSettings) {
|
||||
if (settings.backend === 'claude-code') {
|
||||
const cmd = resolveCliPath('claude', settings.cliPath);
|
||||
const { stdout } = await runCli(cmd, ['--version'], '', 10000);
|
||||
return `claude @ ${cmd}\n${stdout.trim()}`;
|
||||
}
|
||||
if (settings.backend === 'codex') {
|
||||
const cmd = resolveCliPath('codex', settings.cliPath);
|
||||
const { stdout } = await runCli(cmd, ['--version'], '', 10000);
|
||||
return `codex @ ${cmd}\n${stdout.trim()}`;
|
||||
}
|
||||
return testApiBackend(requestUrl, settings);
|
||||
}
|
||||
|
||||
/** Detect whether the user has departed from preset defaults. If so we keep the
|
||||
* Advanced connection section open so they can find what they configured. */
|
||||
function shouldOpenAdvancedConnection(settings: PluginSettings): boolean {
|
||||
|
|
@ -224,11 +209,16 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
|
|||
.setDesc(isCliBacked ? this.tr('settingTestBackendDescCli') : this.tr('settingTestBackendDescApi'))
|
||||
.addButton((b) =>
|
||||
b.setButtonText(this.tr('settingTestBackendButton')).onClick(async () => {
|
||||
b.setDisabled(true);
|
||||
b.setButtonText(this.tr('settingTestBackendButtonRunning'));
|
||||
try {
|
||||
const result = await testBackend(this.plugin.settings);
|
||||
new Notice(`✓ ${result.slice(0, 180)}`, 8000);
|
||||
} catch (e: unknown) {
|
||||
new Notice(this.tr('backendTestFailed', { error: e instanceof Error ? e.message : String(e) }), 10000);
|
||||
} finally {
|
||||
b.setButtonText(this.tr('settingTestBackendButton'));
|
||||
b.setDisabled(false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const MAX_DOC_CHARS = 100000;
|
|||
export const PROMPT_VERSION = 2;
|
||||
export const CACHE_SCHEMA_VERSION = 2;
|
||||
export const DEFAULT_MAX_CACHE_ENTRIES = 100;
|
||||
export const DEFAULT_CLI_TIMEOUT_MS = 300000;
|
||||
export const MIN_STREAMING_TIMEOUT_MS = 1000;
|
||||
export const MIN_CLI_TIMEOUT_MS = 1000;
|
||||
export const PROMPT_LANGUAGES = {
|
||||
|
|
@ -44,7 +45,7 @@ export const DEFAULT_SETTINGS: PluginSettings = {
|
|||
customSystemPrompt: '',
|
||||
model: 'claude-sonnet-4-6',
|
||||
exportFolder: 'Reading/Articles',
|
||||
cliTimeoutMs: 120000,
|
||||
cliTimeoutMs: DEFAULT_CLI_TIMEOUT_MS,
|
||||
streaming: true,
|
||||
streamingTimeoutMs: 120000,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
export { default as ParallelReaderPlugin } from '../main';
|
||||
export { findLineForAnchor } from './anchor';
|
||||
export { testBackend } from './backend-test';
|
||||
export {
|
||||
batchProgressVars,
|
||||
createBatchRunState,
|
||||
|
|
|
|||
144
tests/backend-test.test.js
Normal file
144
tests/backend-test.test.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
const { assert, baseSettings, EventEmitter, openAiCardsResponse, t } = require('./test-setup');
|
||||
|
||||
function createFakeChild(stdoutText, stderrText = '', code = 0) {
|
||||
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.pid = 1234;
|
||||
child.kill = () => true;
|
||||
|
||||
process.nextTick(() => {
|
||||
if (stdoutText) child.stdout.emit('data', Buffer.from(stdoutText));
|
||||
if (stderrText) child.stderr.emit('data', Buffer.from(stderrText));
|
||||
child.emit('close', code, null);
|
||||
});
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
async function testClaudeBackendRunsVersionAndSmoke() {
|
||||
const calls = [];
|
||||
const streamJson = [
|
||||
JSON.stringify({ type: 'system', subtype: 'init', tools: ['LSP'] }),
|
||||
JSON.stringify({
|
||||
type: 'result',
|
||||
result: '{"cards":[{"title":"CLI smoke","anchor":"parallel reader smoke anchor","gist":"ok","bullets":["ok"]}]}',
|
||||
}),
|
||||
'',
|
||||
].join('\n');
|
||||
const spawnImpl = (cmd, args) => {
|
||||
calls.push({ cmd, args });
|
||||
if (args.includes('--version')) return createFakeChild('2.1.133 (Claude Code)\n');
|
||||
return createFakeChild(streamJson);
|
||||
};
|
||||
|
||||
const result = await t.testBackend(
|
||||
{
|
||||
...baseSettings,
|
||||
backend: 'claude-code',
|
||||
cliPath: '/usr/bin/claude',
|
||||
cliTimeoutMs: 300000,
|
||||
model: 'claude-sonnet-4-6',
|
||||
promptLanguage: 'zh',
|
||||
minCards: 5,
|
||||
maxCards: 15,
|
||||
maxDocChars: 100000,
|
||||
},
|
||||
{ spawnImpl },
|
||||
);
|
||||
|
||||
assert.match(result, /claude @ \/usr\/bin\/claude/, 'result shows resolved Claude command');
|
||||
assert.match(result, /2\.1\.133/, 'result includes CLI version output');
|
||||
assert.match(result, /smoke: 1 card/, 'result includes parsed smoke-card count');
|
||||
assert.deepStrictEqual(calls[0].args, ['--version'], 'first Claude call checks version');
|
||||
|
||||
const smokeArgs = calls[1].args;
|
||||
assert.strictEqual(smokeArgs[0], '-p', 'Claude smoke uses print mode');
|
||||
assert.strictEqual(
|
||||
smokeArgs[smokeArgs.indexOf('--output-format') + 1],
|
||||
'stream-json',
|
||||
'Claude smoke uses stream-json for incremental output',
|
||||
);
|
||||
assert.strictEqual(smokeArgs[smokeArgs.indexOf('--tools') + 1], '', 'Claude smoke disables tools');
|
||||
assert.ok(smokeArgs.includes('--strict-mcp-config'), 'Claude smoke ignores user/project MCP servers');
|
||||
assert.strictEqual(
|
||||
smokeArgs[smokeArgs.indexOf('--mcp-config') + 1],
|
||||
'{"mcpServers":{}}',
|
||||
'Claude smoke passes an empty MCP config',
|
||||
);
|
||||
}
|
||||
|
||||
async function testCodexBackendRunsVersionAndSmoke() {
|
||||
const calls = [];
|
||||
const resultJson =
|
||||
'{"cards":[{"title":"CLI smoke","anchor":"parallel reader smoke anchor","gist":"ok","bullets":["ok"]}]}';
|
||||
const spawnImpl = (cmd, args) => {
|
||||
calls.push({ cmd, args });
|
||||
if (args.includes('--version')) return createFakeChild('codex-cli 1.2.3\n');
|
||||
return createFakeChild(resultJson);
|
||||
};
|
||||
|
||||
const result = await t.testBackend(
|
||||
{
|
||||
...baseSettings,
|
||||
backend: 'codex',
|
||||
cliPath: '/usr/bin/codex',
|
||||
cliTimeoutMs: 300000,
|
||||
promptLanguage: 'zh',
|
||||
minCards: 5,
|
||||
maxCards: 15,
|
||||
maxDocChars: 100000,
|
||||
},
|
||||
{ spawnImpl },
|
||||
);
|
||||
|
||||
assert.match(result, /codex @ \/usr\/bin\/codex/, 'result shows resolved Codex command');
|
||||
assert.match(result, /codex-cli 1\.2\.3/, 'result includes CLI version output');
|
||||
assert.match(result, /smoke: 1 card/, 'result includes parsed smoke-card count');
|
||||
assert.deepStrictEqual(calls[0].args, ['--version'], 'first Codex call checks version');
|
||||
assert.strictEqual(calls[1].args[0], 'exec', 'Codex smoke runs through codex exec');
|
||||
assert.strictEqual(calls[1].args[calls[1].args.indexOf('--sandbox') + 1], 'read-only', 'Codex smoke is read-only');
|
||||
}
|
||||
|
||||
async function testApiBackendUsesInjectedRequestUrl() {
|
||||
let called = false;
|
||||
const result = await t.testBackend(
|
||||
{
|
||||
...baseSettings,
|
||||
apiHeaders: '',
|
||||
maxDocChars: 100000,
|
||||
promptLanguage: 'zh',
|
||||
minCards: 5,
|
||||
maxCards: 15,
|
||||
},
|
||||
{
|
||||
requestUrlImpl: async () => {
|
||||
called = true;
|
||||
return openAiCardsResponse([]);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.strictEqual(called, true, 'API backend test should use injected requestUrl');
|
||||
assert.strictEqual(result, 'OpenAI / OpenAI Chat Completions', 'API backend test reports provider and format');
|
||||
}
|
||||
|
||||
(async () => {
|
||||
await testClaudeBackendRunsVersionAndSmoke();
|
||||
await testCodexBackendRunsVersionAndSmoke();
|
||||
await testApiBackendUsesInjectedRequestUrl();
|
||||
console.log('backend-test tests passed');
|
||||
})().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -38,6 +38,7 @@
|
|||
"description": "Tests for provider protocols, CLI command contracts, exported test surfaces, and architecture invariants.",
|
||||
"files": [
|
||||
"architecture.test.js",
|
||||
"backend-test.test.js",
|
||||
"cli.test.js",
|
||||
"direct-providers.test.js",
|
||||
"providers.test.js",
|
||||
|
|
|
|||
|
|
@ -258,13 +258,55 @@ async function testClaudeCodeArgs() {
|
|||
// Verify key expected flags are present
|
||||
assert.ok(capturedArgs.includes('-p'), 'should include -p (print mode)');
|
||||
assert.ok(capturedArgs.includes('--output-format'), 'should include --output-format');
|
||||
assert.strictEqual(
|
||||
capturedArgs[capturedArgs.indexOf('--output-format') + 1],
|
||||
'stream-json',
|
||||
'claude output should stream JSON events',
|
||||
);
|
||||
assert.ok(capturedArgs.includes('--append-system-prompt'), 'should include --append-system-prompt');
|
||||
assert.ok(capturedArgs.includes('--tools'), 'should explicitly restrict Claude tools');
|
||||
assert.strictEqual(capturedArgs[capturedArgs.indexOf('--tools') + 1], '', 'Claude tools should be disabled');
|
||||
assert.ok(capturedArgs.includes('--disallowed-tools'), 'should include --disallowed-tools');
|
||||
assert.ok(capturedArgs.includes('--no-session-persistence'), 'should avoid session persistence for plugin calls');
|
||||
assert.ok(capturedArgs.includes('--strict-mcp-config'), 'should ignore user/project MCP servers');
|
||||
|
||||
// Verify --max-tokens is NOT present (it's not a valid claude CLI flag)
|
||||
assert.ok(!capturedArgs.includes('--max-tokens'), 'should NOT include --max-tokens');
|
||||
}
|
||||
|
||||
async function testClaudeCodeStreamJsonOutput() {
|
||||
const streamJson = [
|
||||
JSON.stringify({ type: 'system', subtype: 'init', tools: ['LSP'] }),
|
||||
JSON.stringify({
|
||||
type: 'result',
|
||||
result: '{"cards":[{"title":"T","anchor":"hello world test anchor","gist":"G","bullets":["B"]}]}',
|
||||
}),
|
||||
'',
|
||||
].join('\n');
|
||||
const fakeSpawn = () => {
|
||||
const child = createFakeChild();
|
||||
process.nextTick(() => {
|
||||
child.stdout.emit('data', Buffer.from(streamJson));
|
||||
child.emit('close', 0);
|
||||
});
|
||||
return child;
|
||||
};
|
||||
|
||||
const settings = {
|
||||
backend: 'claude-code',
|
||||
cliPath: '/usr/bin/claude',
|
||||
cliTimeoutMs: 5000,
|
||||
promptLanguage: 'zh',
|
||||
minCards: 5,
|
||||
maxCards: 15,
|
||||
maxDocChars: 100000,
|
||||
};
|
||||
|
||||
const cards = await t.summarizeViaClaudeCode('system prompt', 'user content', settings, undefined, fakeSpawn);
|
||||
assert.strictEqual(cards.length, 1, 'claude stream-json result event is parsed');
|
||||
assert.strictEqual(cards[0].title, 'T', 'parsed card title preserved');
|
||||
}
|
||||
|
||||
// ── summarizeViaCodex args validation ──
|
||||
|
||||
// Known flags/subcommands supported by `codex exec` (from `codex exec --help`).
|
||||
|
|
@ -519,6 +561,7 @@ assert.strictEqual(t.classifyGenerationError('string error'), 'unknown', 'string
|
|||
(async () => {
|
||||
await testRunCliEdgeCases();
|
||||
await testClaudeCodeArgs();
|
||||
await testClaudeCodeStreamJsonOutput();
|
||||
await testClaudeCodeArgsWithModel();
|
||||
await testCodexArgs();
|
||||
await testCodexArgsIgnoresClaudeDefaultModel();
|
||||
|
|
|
|||
|
|
@ -103,11 +103,11 @@ assert.strictEqual(t.normalizeCardCount('15', 5), 15, 'numeric string accepted')
|
|||
|
||||
// normalizeCliTimeoutMs (mirrors normalizeStreamingTimeoutMs)
|
||||
assert.strictEqual(t.normalizeCliTimeoutMs('60000'), 60000, 'cli timeout accepts numeric strings');
|
||||
assert.strictEqual(t.normalizeCliTimeoutMs(999), 120000, 'cli timeout below minimum falls back to default');
|
||||
assert.strictEqual(t.normalizeCliTimeoutMs('bad'), 120000, 'cli timeout rejects non-numeric values');
|
||||
assert.strictEqual(t.normalizeCliTimeoutMs(999), 300000, 'cli timeout below minimum falls back to default');
|
||||
assert.strictEqual(t.normalizeCliTimeoutMs('bad'), 300000, 'cli timeout rejects non-numeric values');
|
||||
assert.strictEqual(
|
||||
t.normalizeSettings({ ...baseSettings, cliTimeoutMs: 500 }).cliTimeoutMs,
|
||||
120000,
|
||||
300000,
|
||||
'normalizeSettings protects invalid cli timeout values',
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -18,5 +18,6 @@
|
|||
"1.0.16": "1.7.2",
|
||||
"1.0.17": "1.7.2",
|
||||
"1.0.18": "1.8.7",
|
||||
"1.0.19": "1.8.7"
|
||||
"1.0.19": "1.8.7",
|
||||
"1.0.20": "1.8.7"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue