mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-11 12:12:19 -10:00
test(cli): pin the full replaceProcess predicate for one-shot launches
The verify lane's mutation run left M7 (jsonFd term) surviving and the broader predicate shape unpinned after the slash-command guard was reverted. Assert the replaceProcess value relaunchAppInChildProcess receives across the launch modes: plain and slash-command one-shot prompts replace the process; ACP, -i, file, json-fd, stream-json, and dual-output file inputs keep the supervisor. Mutation-checked: deleting the jsonFd term fails the json-fd row, forcing the expression true fails all six supervisor rows. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmuakc4l1ae
This commit is contained in:
1 parent
45d0ac3d29
commit
22dd3240aa
1 file changed
+123
-1
@@ -37,7 +37,7 @@ import type { CliArgs } from './config/config.js';
|
||||
import { type LoadedSettings } from './config/settings.js';
|
||||
import { appEvents, AppEvent } from './utils/events.js';
|
||||
import type { ChatRecord, Config } from '@qwen-code/qwen-code-core';
|
||||
import { ApprovalMode, OutputFormat, Storage } from '@qwen-code/qwen-code-core';
|
||||
import { ApprovalMode, InputFormat, OutputFormat, Storage } from '@qwen-code/qwen-code-core';
|
||||
import { EXTERNAL_TOOL_GUARD_REQUIRED_VALUE } from '@qwen-code/acp-bridge/externalToolGuard';
|
||||
|
||||
const mockPrepareFileWatchersForProcessExit = vi.hoisted(() => vi.fn());
|
||||
@@ -1267,6 +1267,128 @@ describe('llm.tsx main function', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Pins the process-replacement predicate in llm.tsx: a one-shot headless
|
||||
// prompt replaces the already-loaded process, while supervisor-backed
|
||||
// modes (ACP, -i, stream-json / file / json-fd input) keep the parent.
|
||||
// The slash-command row pins the post-revert contract: a headless
|
||||
// `/update` updates standalone installs in-process or prints manual
|
||||
// instructions and never emits a relaunch exit code, so it keeps the
|
||||
// execve optimization like any other one-shot prompt.
|
||||
describe('replaceProcess predicate', () => {
|
||||
const rows: Array<{
|
||||
label: string;
|
||||
argv: Partial<CliArgs>;
|
||||
dualOutputInputFile?: string;
|
||||
expected: boolean;
|
||||
}> = [
|
||||
{
|
||||
label: 'plain one-shot prompt',
|
||||
argv: { prompt: 'summarize this repository' },
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
label: 'headless slash-command prompt',
|
||||
argv: { prompt: '/update' },
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
label: 'acp mode',
|
||||
argv: { acp: true, prompt: 'hi' },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
label: 'interactive prompt (-i)',
|
||||
argv: { prompt: 'hi', promptInteractive: 'follow-up' },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
label: 'file input',
|
||||
argv: { prompt: 'hi', inputFile: 'input.txt' },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
label: 'json-fd input',
|
||||
argv: { prompt: 'hi', jsonFd: 3 },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
label: 'stream-json input',
|
||||
argv: { prompt: 'hi', inputFormat: InputFormat.STREAM_JSON },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
label: 'dual-output file input',
|
||||
argv: { prompt: 'hi' },
|
||||
dualOutputInputFile: 'session.jsonl',
|
||||
expected: false,
|
||||
},
|
||||
];
|
||||
|
||||
it.each(rows)(
|
||||
'passes replaceProcess=$expected to relaunchAppInChildProcess for $label',
|
||||
async ({ argv, dualOutputInputFile, expected }) => {
|
||||
const originalIsTTY = Object.getOwnPropertyDescriptor(
|
||||
process.stdin,
|
||||
'isTTY',
|
||||
);
|
||||
Object.defineProperty(process.stdin, 'isTTY', {
|
||||
value: true,
|
||||
configurable: true,
|
||||
});
|
||||
vi.stubEnv('QWEN_CODE_NO_RELAUNCH', '');
|
||||
|
||||
const { parseArguments } = await import('./config/config.js');
|
||||
const { loadSettings } = await import('./config/settings.js');
|
||||
const { loadSandboxConfig } = await import('./config/sandboxConfig.js');
|
||||
const { relaunchAppInChildProcess } = await import(
|
||||
'./utils/relaunch.js'
|
||||
);
|
||||
vi.mocked(parseArguments).mockResolvedValue(argv as CliArgs);
|
||||
vi.mocked(loadSandboxConfig).mockResolvedValue(undefined);
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
errors: [],
|
||||
merged: {
|
||||
advanced: {},
|
||||
security: { auth: {} },
|
||||
ui: {},
|
||||
dualOutput: dualOutputInputFile
|
||||
? { inputFile: dualOutputInputFile }
|
||||
: undefined,
|
||||
},
|
||||
setValue: vi.fn(),
|
||||
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
|
||||
migrationWarnings: [],
|
||||
getSystemHooks: () => undefined,
|
||||
getUserHooks: () => undefined,
|
||||
getProjectHooks: () => undefined,
|
||||
} as never);
|
||||
|
||||
let replaceProcess: boolean | undefined;
|
||||
vi.mocked(relaunchAppInChildProcess).mockImplementation(
|
||||
async (_memoryArgs, _extraArgs, options) => {
|
||||
replaceProcess = options?.replaceProcess;
|
||||
throw new Error('stop after replaceProcess check');
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await expect(main()).rejects.toThrow(
|
||||
'stop after replaceProcess check',
|
||||
);
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
if (originalIsTTY) {
|
||||
Object.defineProperty(process.stdin, 'isTTY', originalIsTTY);
|
||||
} else {
|
||||
delete (process.stdin as { isTTY?: unknown }).isTTY;
|
||||
}
|
||||
}
|
||||
|
||||
expect(replaceProcess).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Regression for #8653 (sandbox hop): getSandboxPassthroughEnvArgs
|
||||
// forwards the QWEN_CODE_SERVE stamp into the container, so the sandboxed
|
||||
// stage of a daemon-spawned ACP child must still scrub.
|
||||
|
||||
Reference in new issue
Block a user