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:
jinjing.zzjandQwen-Coder committed 2026-09-21 09:42:23 +08:00
1 parent 45d0ac3d29
commit 22dd3240aa
1 file changed
+123 -1
+123 -1
View File
@@ -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.