Files
Marc ChanandGitHub b06f26a5fd test: strengthen e2e PR coverage (#796)
* test: strengthen e2e PR coverage

* fix: address e2e PR feedback

Generated-By: looper 0.6.1 (runner=fixer, agent=opencode)

* fix: address e2e PR feedback

Generated-By: looper 0.6.1 (runner=fixer, agent=opencode)

* ci: cache Windows packaged smoke builds

* test: fake additional agent runtimes

* fix: address e2e PR feedback

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix: address e2e PR feedback

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix: address e2e PR feedback

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix: address e2e PR feedback

Route tools-pack mac starts through a launch-time packaged config override so portable packaged smoke runs keep using the namespace runtime root that inspect and logs expect.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix: address e2e PR feedback

Fall back to the packaged app's embedded config when the build output config is missing so installed mac starts still work.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix: align packaged mac PR smoke with tools-pack runtime mode

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix: address e2e PR feedback

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix: address e2e PR feedback

Keep blake3-wasm out of the packaged mac daemon prebundle so the standalone runtime loads the Cloudflare asset hasher from node_modules instead of crashing in ESM.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix: address e2e PR feedback

Skip the portable mac launch override when the bundled packaged config is missing so installed fallback app targets can still boot with packaged defaults.

Add a regression test covering the missing-config start path.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(pack): remove duplicate mac prebundle dependency key
2026-05-08 16:48:10 +08:00

214 lines
8.4 KiB
TypeScript

import { chmod, mkdir, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
export type FakeAgentId =
| 'claude'
| 'codex'
| 'copilot'
| 'cursor-agent'
| 'deepseek'
| 'gemini'
| 'opencode'
| 'qoder'
| 'qwen';
type FakeAgentRuntime = {
agentId: FakeAgentId;
bin: string;
envKey: string;
env: Record<string, string>;
};
const AGENT_BIN_NAMES: Record<FakeAgentId, string> = {
claude: 'claude-e2e.js',
codex: 'codex-e2e.js',
copilot: 'copilot-e2e.js',
'cursor-agent': 'cursor-agent-e2e.js',
deepseek: 'deepseek-e2e.js',
gemini: 'gemini-e2e.js',
opencode: 'opencode-e2e.js',
qoder: 'qodercli-e2e.js',
qwen: 'qwen-e2e.js',
};
const AGENT_BIN_ENV_KEYS: Record<FakeAgentId, string> = {
claude: 'CLAUDE_BIN',
codex: 'CODEX_BIN',
copilot: 'COPILOT_BIN',
'cursor-agent': 'CURSOR_AGENT_BIN',
deepseek: 'DEEPSEEK_BIN',
gemini: 'GEMINI_BIN',
opencode: 'OPENCODE_BIN',
qoder: 'QODER_BIN',
qwen: 'QWEN_BIN',
};
export const FAKE_AGENT_RUNTIME_IDS: FakeAgentId[] = [
'claude',
'gemini',
'opencode',
'cursor-agent',
'qwen',
'qoder',
'copilot',
];
export async function createFakeAgentRuntimes(
runtimeIds: FakeAgentId[] = ['codex', ...FAKE_AGENT_RUNTIME_IDS],
): Promise<Record<FakeAgentId, FakeAgentRuntime>> {
const root = path.join(tmpdir(), `open-design-playwright-fake-agents-${process.pid}`);
await mkdir(root, { recursive: true });
const runtimes = {} as Record<FakeAgentId, FakeAgentRuntime>;
for (const agentId of runtimeIds) {
const script = path.join(root, AGENT_BIN_NAMES[agentId]);
const bin = process.platform === 'win32'
? script.replace(/\.js$/i, '.cmd')
: script;
await writeFile(script, renderFakeAgentScript(agentId), 'utf8');
if (process.platform === 'win32') {
await writeFile(bin, '@echo off\r\nnode "%~dp0%~n0.js" %*\r\n', 'utf8');
} else {
await chmod(bin, 0o755);
}
const envKey = AGENT_BIN_ENV_KEYS[agentId];
runtimes[agentId] = { agentId, bin, envKey, env: { [envKey]: bin } };
}
return runtimes;
}
function renderFakeAgentScript(agentId: FakeAgentId): string {
return `#!/usr/bin/env node
const agentId = ${JSON.stringify(agentId)};
const args = process.argv.slice(2);
if (args.includes('--version')) {
process.stdout.write(agentId + '-e2e 0.0.0\\n');
process.exitCode = 0;
} else if (agentId === 'claude' && args[0] === '-p' && args.includes('--help')) {
process.stdout.write('--add-dir --include-partial-messages\\n');
process.exitCode = 0;
} else if ((agentId === 'opencode' || agentId === 'cursor-agent') && args[0] === 'models') {
process.stdout.write('fake/default\\n');
process.exitCode = 0;
} else {
let prompt = '';
let emitted = false;
process.stdin.setEncoding('utf8');
process.stdin.resume();
process.stdin.on('data', (chunk) => { prompt += chunk; });
process.stdin.on('end', () => {
emitRun(prompt);
});
if (process.stdin.isTTY || agentId === 'deepseek') {
prompt = args.join(' ');
emitRun(prompt);
}
function emitRun(promptText) {
if (emitted) return;
emitted = true;
if (promptText.includes('Return an intentional daemon smoke failure')) {
emitFailure();
return;
}
const isChunked = promptText.includes('Create a chunked deterministic smoke artifact');
const isFollowUp = promptText.includes('Create a follow-up deterministic smoke artifact');
const isDefaultSmoke = promptText.includes('Create a deterministic smoke artifact');
const isRuntime = promptText.match(/Fake runtime smoke for ([a-z0-9-]+)/i);
const runtimeId = isRuntime ? isRuntime[1] : agentId;
const heading = isChunked ? 'Chunked Daemon Smoke' : isFollowUp ? 'Follow-up Daemon Smoke' : isDefaultSmoke ? 'Real Daemon Smoke' : 'Fake Agent Runtime ' + runtimeId;
const identifier = isChunked ? 'chunked-daemon-smoke' : isFollowUp ? 'follow-up-daemon-smoke' : isDefaultSmoke ? 'real-daemon-smoke' : 'fake-agent-runtime-' + runtimeId;
const text = isChunked ? 'Chunked through the daemon run path.' : isFollowUp ? 'Generated after an earlier daemon turn.' : isDefaultSmoke ? 'Generated through the daemon run path.' : 'Generated through fake ' + runtimeId + ' runtime.';
const html = '<!doctype html><html><body><main><h1>' + heading + '</h1><p>' + text + '</p></main></body></html>';
const artifact = '<artifact identifier="' + identifier + '" type="text/html" title="' + heading + '">' + html + '</artifact>';
emitSuccess(artifact, isChunked);
process.exitCode = 0;
}
function writeJson(value) {
process.stdout.write(JSON.stringify(value) + '\\n');
}
function emitSuccess(artifact, isChunked) {
const first = artifact.slice(0, Math.ceil(artifact.length / 2));
const second = artifact.slice(Math.ceil(artifact.length / 2));
switch (agentId) {
case 'codex':
writeJson({ type: 'thread.started' });
writeJson({ type: 'turn.started' });
if (isChunked) {
writeJson({ type: 'item.completed', item: { type: 'agent_message', text: first } });
writeJson({ type: 'item.completed', item: { type: 'agent_message', text: second } });
} else {
writeJson({ type: 'item.completed', item: { type: 'agent_message', text: artifact } });
}
writeJson({ type: 'turn.completed', usage: { input_tokens: 1, output_tokens: 1 } });
return;
case 'claude':
writeJson({ type: 'system', subtype: 'init', model: 'fake-claude', session_id: 'fake-session' });
writeJson({ type: 'assistant', message: { id: 'msg-1', content: [{ type: 'text', text: artifact }] } });
writeJson({ type: 'result', usage: { input_tokens: 1, output_tokens: 1 }, total_cost_usd: 0, duration_ms: 1, stop_reason: 'end_turn' });
return;
case 'gemini':
writeJson({ type: 'init', session_id: 'fake-gemini', model: 'fake-gemini' });
writeJson({ type: 'message', role: 'assistant', content: artifact, delta: true });
writeJson({ type: 'result', status: 'success', stats: { input_tokens: 1, output_tokens: 1, cached: 0, duration_ms: 1 } });
return;
case 'opencode':
writeJson({ type: 'step_start', sessionID: 'fake-opencode', part: { type: 'step-start' } });
writeJson({ type: 'text', sessionID: 'fake-opencode', part: { type: 'text', text: artifact } });
writeJson({ type: 'step_finish', sessionID: 'fake-opencode', part: { type: 'step-finish', tokens: { input: 1, output: 1 }, cost: 0 } });
return;
case 'cursor-agent':
writeJson({ type: 'system', subtype: 'init', model: 'fake-cursor' });
writeJson({ type: 'assistant', timestamp_ms: 1, message: { role: 'assistant', content: [{ type: 'text', text: artifact }] } });
writeJson({ type: 'result', duration_ms: 1, usage: { inputTokens: 1, outputTokens: 1, cacheReadTokens: 0, cacheWriteTokens: 0 } });
return;
case 'qoder':
writeJson({ type: 'system', subtype: 'init', qodercli_version: '0.0.0', model: 'fake-qoder', session_id: 'fake-qoder' });
writeJson({ type: 'assistant', message: { content: [{ type: 'text', text: artifact }] }, session_id: 'fake-qoder' });
writeJson({ type: 'result', subtype: 'success', duration_ms: 1, is_error: false, stop_reason: 'end_turn', total_cost_usd: 0, usage: { input_tokens: 1, output_tokens: 1 } });
return;
case 'copilot':
writeJson({ type: 'session.tools_updated', data: { model: 'fake-copilot' } });
writeJson({ type: 'assistant.turn_start', data: {} });
writeJson({ type: 'assistant.message_delta', data: { deltaContent: artifact } });
writeJson({ type: 'result', success: true, exitCode: 0, usage: { input_tokens: 1, output_tokens: 1, sessionDurationMs: 1 } });
return;
case 'qwen':
case 'deepseek':
process.stdout.write(artifact + '\\n');
return;
default:
process.stdout.write(artifact + '\\n');
}
}
function emitFailure() {
switch (agentId) {
case 'codex':
writeJson({ type: 'thread.started' });
writeJson({ type: 'turn.started' });
writeJson({ type: 'turn.failed', error: { message: 'intentional fake codex failure' } });
process.exitCode = 0;
return;
case 'opencode':
writeJson({ type: 'error', error: { data: { message: 'intentional fake opencode failure' } } });
process.exitCode = 0;
return;
case 'qoder':
writeJson({ type: 'assistant', message: { content: [] }, error: { message: 'intentional fake qoder failure' } });
process.exitCode = 0;
return;
default:
process.stderr.write('intentional fake ' + agentId + ' failure\\n');
process.exitCode = 1;
}
}
}
`;
}