chore: rebuild release package

This commit is contained in:
shiyue
2026-07-16 18:04:37 +08:00
parent 614e222f14
commit 5c3401292d
18 changed files with 1032 additions and 64 deletions

View File

@@ -564,7 +564,7 @@ function assertFrontendComposerMcpContract() {
assert(source.includes("className = 'msg-mentions'"), 'Frontend should render a dedicated mention strip container');
}
function assertFrontendSlashDraftPreservationContract() {
function assertComposerSlashRoutingContract() {
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
assert(source.includes('const pendingSlashDraftsByRequestId = new Map();'), 'Frontend should keep request-scoped slash drafts');
@@ -576,19 +576,67 @@ function assertFrontendSlashDraftPreservationContract() {
assert(source.includes("send({ type: 'message', text, sessionId: currentSessionId, mode: currentMode, agent: currentAgent, requestId });"), 'Frontend slash sends should carry requestId');
assert((source.match(/applyPendingSlashDraftResponse\(msg\);/g) || []).length >= 2, 'Frontend should handle slash draft restoration on system_message and error');
const frontendCommandsMatch = source.match(/const\s+SLASH_COMMANDS\s*=\s*\[([\s\S]*?)\n\s*\];/);
const serverCommandsMatch = serverSource.match(/const\s+COMPOSER_COMMANDS\s*=\s*\[([\s\S]*?)\n\s*\];/);
assert(frontendCommandsMatch && serverCommandsMatch, 'Frontend and server should both declare composer slash command lists');
const frontendCommandNames = Array.from(frontendCommandsMatch[1].matchAll(/\bcmd:\s*['"]([^'"]+)['"]/g), (match) => match[1]);
const serverCommandNames = Array.from(serverCommandsMatch[1].matchAll(/\bname:\s*['"]([^'"]+)['"]/g), (match) => match[1]);
assert(frontendCommandNames.length > 0, 'Frontend slash command list should not be empty');
assert(
JSON.stringify([...frontendCommandNames].sort()) === JSON.stringify([...serverCommandNames].sort()),
'Frontend and server should classify the same complete set of known slash commands'
);
const knownSlashSource = extractFunctionSource(source, 'isKnownSlashCommandText');
const knownSlashApi = new Function('SLASH_COMMANDS', `
${knownSlashSource}
return isKnownSlashCommandText;
`)(frontendCommandNames.map((cmd) => ({ cmd })));
frontendCommandNames.forEach((command) => {
assert(knownSlashApi(`${command.toUpperCase()} argument`), `Known slash command ${command} should match case-insensitively`);
});
assert(!knownSlashApi('/report/mcps?search') && !knownSlashApi('/help/topic'), 'Unknown slash paths should remain ordinary messages');
const tokenSource = extractFunctionSource(source, 'findActiveComposerToken');
const tokenApi = new Function(`
let msgInput = null;
${tokenSource}
return (value, cursor = value.length) => {
msgInput = { value, selectionStart: cursor };
return findActiveComposerToken();
};
`)();
assert(tokenApi('/rep')?.trigger === '/', 'Slash suggestions should trigger at the composer start');
assert(tokenApi('请查看 /rep')?.query === 'rep', 'Slash suggestions should trigger after whitespace');
assert(tokenApi('请查看\n/rep')?.query === 'rep', 'Slash suggestions should trigger at a new line');
assert(tokenApi('请查看\r\n/rep')?.query === 'rep', 'Slash suggestions should trigger after a CRLF line boundary');
assert(tokenApi('请查看 /rep', 3) === null, 'Slash suggestions should follow the cursor and ignore tokens after it');
assert(tokenApi('src/foo') === null, 'Slash suggestions should not trigger inside a path token');
assert(tokenApi('请查看 @file')?.trigger === '@' && tokenApi('请使用 $skill')?.trigger === '$', '@ and $ trigger behavior should remain unchanged');
const sendStart = source.indexOf('function sendMessage()');
const slashStart = source.indexOf("if (text.startsWith('/'))", sendStart);
const slashStart = source.indexOf('if (isKnownSlashCommandText(text))', sendStart);
const rememberStart = source.indexOf('rememberPendingSlashDraft(requestId, text, currentSessionId, currentAgent);', slashStart);
const modelPickerStart = source.indexOf("if (text === '/model' || text === '/model ')", slashStart);
const modePickerStart = source.indexOf("if (text === '/mode' || text === '/mode ')", slashStart);
assert(slashStart >= 0 && rememberStart > slashStart, 'Frontend should keep slash send branch explicit');
assert(slashStart >= 0 && rememberStart > slashStart, 'Frontend should reserve the slash send branch for known commands');
assert(modelPickerStart > slashStart && modelPickerStart < rememberStart, 'Frontend /model picker should stay local and clear normally');
assert(modePickerStart > slashStart && modePickerStart < rememberStart, 'Frontend /mode picker should stay local and clear normally');
assert(serverSource.includes('handleSlashCommand(ws, msg.text.trim(), msg.sessionId, msg.agent, msg);'), 'Server should pass client request metadata into slash handler');
assert(/const\s+handled\s*=\s*handleSlashCommand\(ws, msg\.text\.trim\(\), msg\.sessionId, msg\.agent, msg\);/.test(serverSource), 'Server should inspect slash text and retain the handled result');
assert(/if\s*\(!handled\)\s*handleMessage\(ws, msg/.test(serverSource), 'Server should pass unknown slash text into the ordinary message pipeline');
assert(serverSource.includes('function handleSlashCommand(ws, text, sessionId, fallbackAgent, source = {})'), 'Server slash handler should accept request metadata');
assert(serverSource.includes('wsSend(ws, attachClientRequestId(base, source));'), 'Server slash responses should echo requestId');
assert(serverSource.includes('preserveComposerDraft: true'), 'Server should explicitly mark slash failures as draft-preserving');
assert(/default:\s*\n\s*sendSlashSystemMessage\(`未知指令:[\s\S]*?\n\s*return false;/.test(serverSource), 'Unknown slash hints should not restore the composer and should report an unhandled command');
const serverSlashHandler = extractFunctionSource(serverSource, 'handleSlashCommand');
const serverHandledCommandNames = Array.from(serverSlashHandler.matchAll(/case\s+['"]([^'"]+)['"]\s*:/g), (match) => match[1]);
assert(
JSON.stringify([...serverCommandNames].sort()) === JSON.stringify([...serverHandledCommandNames].sort()),
'Every server-declared slash command should keep an explicit handler case'
);
const unknownGuardStart = serverSlashHandler.indexOf('if (!COMPOSER_COMMANDS.some((item) => item.name === cmd))');
const runningGuardStart = serverSlashHandler.indexOf('activeCodexAppTurns.has(sessionId)');
assert(unknownGuardStart >= 0 && unknownGuardStart < runningGuardStart, 'Unknown slash text should be classified before active Codex App command guards');
assert(serverSource.includes('wsSend(ws, attachClientRequestId({') && serverSource.includes('...(msg.preserveComposerDraft ? { preserveComposerDraft: true } : {})'), 'Server runtime errors should echo requestId and draft-preservation metadata');
}
@@ -670,6 +718,7 @@ function assertFrontendSubagentCardMetadataContract() {
const collabMergeEnd = source.indexOf(' function collabStateLabel(statusText)', collabMergeStart);
const childUpdateStart = source.indexOf(' function applyCcwebMcpChildAgentUpdate(msg)');
const childUpdateEnd = source.indexOf(' function getDeleteConfirmMessage(agent)', childUpdateStart);
const buildMsgElementSource = extractFunctionSource(source, 'buildMsgElement');
assert(collabMergeStart >= 0 && collabMergeEnd > collabMergeStart, 'Frontend should expose collab merge helpers');
assert(childUpdateStart >= 0 && childUpdateEnd > childUpdateStart, 'Frontend should define child-agent update handling before delete helpers');
const collabApi = new Function(`
@@ -693,16 +742,57 @@ function assertFrontendSubagentCardMetadataContract() {
updater(cachedSnapshot);
}
function updateToolCall() {}
function makeNode() {
return {
children: [],
appendChild(child) {
this.children.push(child);
return child;
},
insertBefore(child) {
this.children.unshift(child);
return child;
},
querySelector() {
return null;
},
};
}
function createMsgElement() {
const bubble = makeNode();
return {
bubble,
querySelector(selector) {
return selector === '.msg-bubble' ? bubble : null;
},
};
}
function createCcwebPromptElement() { return makeNode(); }
function isEmptyReasoningTool() { return false; }
function createToolCallElement(toolUseId, tool, done) {
return { ...makeNode(), toolUseId, tool, done };
}
function isGroupableToolCall() { return false; }
function _refreshGroupSummary() {}
function markSessionMessageElement() {}
const document = { createElement: () => makeNode() };
function shortChildAgentId(id) {
const value = String(id || '');
return value.length > 12 ? value.slice(0, 8) : value;
}
${source.slice(collabMergeStart, collabMergeEnd)}
${source.slice(childUpdateStart, childUpdateEnd)}
${buildMsgElementSource}
return {
toolKind,
collabStateTone,
mergeCollabAgentTools,
applyCcwebMcpChildAgentUpdate,
rememberCollabAgentState,
renderToolCallsForMessage: (toolCalls) => {
const el = buildMsgElement({ role: 'assistant', content: '', toolCalls });
return el.querySelector('.msg-bubble').children.map((node) => node.tool);
},
getCachedState: (id) => collabAgentStateCache.get(id),
hasCachedState: (id) => collabAgentStateCache.has(id),
cacheSize: () => collabAgentStateCache.size,
@@ -910,6 +1000,354 @@ function assertFrontendSubagentCardMetadataContract() {
});
assert(collabApi.cacheSize() === cacheSizeBeforeOrdinaryTool, 'Non-collab child updates should not write sub-agent state cache');
assert(!collabApi.hasCachedState('ordinary-child'), 'Non-collab tools should not cache child thread state');
const rawActivityThreadId = 'agent-thread-plan-reviewer-001';
const rawActivityPrompt = '请审查 Phase 8 的前端 helper 行为。';
const rawSubAgentActivity = (overrides = {}) => {
const input = {
kind: overrides.activityKind || 'started',
agentThreadId: overrides.agentThreadId || rawActivityThreadId,
agentPath: overrides.agentPath || '/root/plan_reviewer',
...(overrides.prompt === undefined ? { prompt: rawActivityPrompt } : {}),
...(overrides.prompt ? { prompt: overrides.prompt } : {}),
...(overrides.input || {}),
};
return {
id: overrides.id || `call_activity_${input.kind}`,
name: 'subAgentActivity',
kind: 'subAgentActivity',
input,
...(overrides.result !== undefined ? { result: overrides.result } : {}),
done: !!overrides.done,
};
};
const emptyWaitTool = {
id: 'call_wait_empty',
name: 'wait_agent',
kind: 'collab_agent_tool_call',
input: { tool: 'wait_agent', receiverThreadIds: [], agentsStates: {} },
result: JSON.stringify({ receiverThreadIds: [], agentsStates: {} }),
done: false,
};
const rawActivityMerge = collabApi.mergeCollabAgentTools([
rawSubAgentActivity(),
emptyWaitTool,
]);
assert(rawActivityMerge, 'Raw subAgentActivity with an empty wait should produce one merged collab card');
assert(
rawActivityMerge.input.receiverThreadIds.length === 1
&& rawActivityMerge.input.receiverThreadIds[0] === rawActivityThreadId,
'Raw subAgentActivity agentThreadId should be the sole receiverThreadId and must not fall back to the wait tool call id'
);
assert(
Object.keys(rawActivityMerge.input.agentsStates).length === 1
&& Object.prototype.hasOwnProperty.call(rawActivityMerge.input.agentsStates, rawActivityThreadId),
'Raw subAgentActivity agentThreadId should be the sole agentsStates key'
);
const rawActivityState = rawActivityMerge.input.agentsStates[rawActivityThreadId];
assert(rawActivityState.label === 'plan_reviewer', 'Raw subAgentActivity agentPath basename should become the readable card title');
assert(rawActivityState.role === '', 'Raw subAgentActivity agentPath should not be duplicated into role when no explicit role is present');
assert(!/^ID\s+call_/.test(rawActivityState.label || ''), 'Raw subAgentActivity title must not fall back to a tool call id');
assert(rawActivityState.status === 'running', 'Raw subAgentActivity started events should keep the child running');
assert(rawActivityMerge.input.status === 'running', 'Raw subAgentActivity aggregate status should match its running child state');
assert(rawActivityState.taskDescription === rawActivityPrompt, 'Raw subAgentActivity prompt should be preserved as taskDescription');
const explicitRoleMerge = collabApi.mergeCollabAgentTools([
rawSubAgentActivity({
id: 'call_activity_explicit_role',
agentThreadId: 'agent-thread-explicit-role',
input: { role: 'reviewer' },
}),
]);
assert(explicitRoleMerge.input.agentsStates['agent-thread-explicit-role'].label === 'plan_reviewer', 'Raw subAgentActivity agentPath basename should remain the readable title when role is explicit');
assert(explicitRoleMerge.input.agentsStates['agent-thread-explicit-role'].role === 'reviewer', 'Raw subAgentActivity should preserve an explicit role');
const completedEmptyWaitTool = {
...emptyWaitTool,
id: 'call_wait_completed_empty',
result: JSON.stringify({ status: 'completed', receiverThreadIds: [], agentsStates: {} }),
done: true,
};
const runningStartedWithCompletedWait = collabApi.mergeCollabAgentTools([
rawSubAgentActivity({
id: 'call_activity_started_done_completed',
activityKind: 'started',
done: true,
input: { status: 'completed' },
result: JSON.stringify({ status: 'completed' }),
}),
completedEmptyWaitTool,
]);
assert(runningStartedWithCompletedWait.input.receiverThreadIds.length === 1, 'Started raw activity plus completed empty wait should keep one child');
assert(runningStartedWithCompletedWait.input.agentsStates[rawActivityThreadId].status === 'running', 'Started raw activity should stay running even when the empty wait is completed');
assert(runningStartedWithCompletedWait.input.status === 'running', 'Merged collab status should stay running when the only child is running');
collabApi.rememberCollabAgentState(
'restore-running-child',
{ title: '恢复运行代理', status: 'running' },
'请保持运行态。',
0
);
const restoredRunningWithCompletedWait = collabApi.mergeCollabAgentTools([
completedEmptyWaitTool,
], {
restoreAgentIds: new Set(['restore-running-child']),
});
assert(restoredRunningWithCompletedWait.input.agentsStates['restore-running-child'].status === 'running', 'Empty completed wait should not overwrite restored running cache status');
assert(restoredRunningWithCompletedWait.input.status === 'running', 'Empty completed wait should not make a restored running child look completed in the aggregate header');
['started', 'interacted'].forEach((activityKind) => {
const threadId = `agent-thread-${activityKind}`;
const merged = collabApi.mergeCollabAgentTools([
rawSubAgentActivity({
id: `call_activity_${activityKind}_only`,
activityKind,
agentThreadId: threadId,
prompt: activityKind === 'started' ? `请处理 ${activityKind} 状态。` : '',
}),
]);
assert(merged, `Raw subAgentActivity ${activityKind} should be recognized as a collab display tool`);
assert(merged.input.receiverThreadIds.length === 1 && merged.input.receiverThreadIds[0] === threadId, `Raw ${activityKind} activity should use agentThreadId as receiverThreadId`);
assert(merged.input.agentsStates[threadId].status === 'running', `Raw ${activityKind} activity should map to running status`);
});
['completed', 'returned'].forEach((activityKind) => {
const threadId = `agent-thread-${activityKind}`;
const merged = collabApi.mergeCollabAgentTools([
rawSubAgentActivity({
id: `call_activity_${activityKind}_only`,
activityKind,
agentThreadId: threadId,
prompt: `请处理 ${activityKind} 状态。`,
done: true,
}),
]);
assert(merged, `Raw subAgentActivity ${activityKind} should be recognized as a collab display tool`);
assert(merged.input.receiverThreadIds.length === 1 && merged.input.receiverThreadIds[0] === threadId, `Raw ${activityKind} activity should use agentThreadId as receiverThreadId`);
assert(collabApi.collabStateTone(merged.input.agentsStates[threadId].status) === 'done', `Raw ${activityKind} activity should map to a completed tone`);
});
const completedWithoutPrompt = collabApi.mergeCollabAgentTools([
rawSubAgentActivity(),
rawSubAgentActivity({
id: 'call_activity_completed_without_prompt',
activityKind: 'completed',
prompt: '',
done: true,
}),
]);
assert(
completedWithoutPrompt.input.agentsStates[rawActivityThreadId].taskDescription === rawActivityPrompt,
'Raw subAgentActivity updates for the same thread should retain an earlier prompt as taskDescription'
);
assert(
collabApi.collabStateTone(completedWithoutPrompt.input.agentsStates[rawActivityThreadId].status) === 'done',
'Raw subAgentActivity completed updates should finish the same child card'
);
const noPromptThreadId = 'agent-thread-no-prompt';
const noPromptActivityMerge = collabApi.mergeCollabAgentTools([
rawSubAgentActivity({
id: 'call_activity_no_prompt',
agentThreadId: noPromptThreadId,
prompt: '',
}),
]);
assert(noPromptActivityMerge.input.agentsStates[noPromptThreadId].label === 'plan_reviewer', 'Raw subAgentActivity without prompt should still use agentPath as title');
assert(!noPromptActivityMerge.input.agentsStates[noPromptThreadId].taskDescription, 'Raw subAgentActivity without prompt should not fabricate taskDescription');
const renderCandidates = collabApi.renderToolCallsForMessage([
rawSubAgentActivity(),
emptyWaitTool,
]);
assert(renderCandidates.length === 1, 'Raw subAgentActivity plus empty wait should render only the merged collab card');
assert(renderCandidates[0].kind === 'collab_agent_tool_call', 'Raw subAgentActivity should render through the collab display tool');
assert(
!renderCandidates.some((tool) => tool?.id === 'call_activity_started' && tool?.kind === 'subAgentActivity'),
'Raw subAgentActivity should be filtered out of ordinary rendered tool rows'
);
const nameOnlyOrdinaryTool = {
id: 'call_name_only_subagent_activity',
name: 'subAgentActivity',
kind: 'command_execution',
input: { command: 'echo should-stay-ordinary' },
done: true,
};
assert(collabApi.toolKind(nameOnlyOrdinaryTool) === 'command_execution', 'Name-only subAgentActivity command tools without agentThreadId should stay ordinary tools');
assert(collabApi.mergeCollabAgentTools([nameOnlyOrdinaryTool]) === null, 'Name-only ordinary subAgentActivity tools should not produce a collab card');
const nameOnlyRenderCandidates = collabApi.renderToolCallsForMessage([nameOnlyOrdinaryTool]);
assert(nameOnlyRenderCandidates.length === 1 && nameOnlyRenderCandidates[0].kind === 'command_execution', 'Name-only ordinary subAgentActivity tools should not be filtered into a collab render candidate');
}
function assertCodexAppRuntimeSubAgentActivityContract() {
const { createCodexAppRuntime } = require(path.join(REPO_DIR, 'lib', 'codex-app-runtime'));
const sent = [];
const runtime = createCodexAppRuntime({
wsSend: (_ws, payload) => sent.push(payload),
loadSession: () => null,
saveSession: () => {},
});
const sessionId = 'runtime-subagent-session';
const threadId = 'agent-thread-runtime-001';
const prompt = '请审查 runtime subAgentActivity 结构。';
const entry = {
ws: {},
toolCalls: [],
fullText: '',
};
runtime.processCodexAppNotification(entry, {
method: 'item/started',
params: {
item: {
id: 'runtime-activity',
type: 'subAgentActivity',
kind: 'started',
agentThreadId: threadId,
agentPath: '/root/plan_reviewer',
prompt,
},
},
}, sessionId);
const started = sent.find((msg) => msg.type === 'tool_start' && msg.toolUseId === 'runtime-activity');
assert(started, 'Runtime subAgentActivity item/started should emit tool_start');
assert(started.sessionId === sessionId, 'Runtime subAgentActivity tool_start should carry session id');
assert(started.name === 'subAgentActivity', 'Runtime subAgentActivity tool_start should preserve activity name');
assert(started.kind === 'collab_agent_tool_call', 'Runtime subAgentActivity should surface as a collab agent tool call');
assert(started.input?.type === 'subAgentActivity', 'Runtime subAgentActivity input should preserve original type');
assert(started.input?.kind === 'started', 'Runtime subAgentActivity input should preserve activity kind');
assert(started.input?.agentThreadId === threadId, 'Runtime subAgentActivity input should preserve agentThreadId');
assert(started.input?.agentPath === '/root/plan_reviewer', 'Runtime subAgentActivity input should preserve agentPath');
assert(started.input?.prompt === prompt, 'Runtime subAgentActivity input should preserve prompt');
assert(started.input?.receiverThreadIds?.[0] === threadId, 'Runtime subAgentActivity input should expose receiverThreadIds');
assert(started.input?.agentsStates?.[threadId]?.label === 'plan_reviewer', 'Runtime subAgentActivity should derive title from agentPath basename');
assert(started.input?.agentsStates?.[threadId]?.role === '', 'Runtime subAgentActivity should not duplicate agentPath title into role');
assert(started.input?.agentsStates?.[threadId]?.taskDescription === prompt, 'Runtime subAgentActivity should copy prompt to taskDescription');
assert(started.input?.agentsStates?.[threadId]?.status === 'running', 'Runtime started subAgentActivity should map to running');
runtime.processCodexAppNotification(entry, {
method: 'item/completed',
params: {
item: {
id: 'runtime-activity',
type: 'subAgentActivity',
agentThreadId: threadId,
agentPath: '/root/plan_reviewer',
},
},
}, sessionId);
const completed = sent.find((msg) => msg.type === 'tool_end' && msg.toolUseId === 'runtime-activity');
assert(completed, 'Runtime subAgentActivity item/completed should emit tool_end');
assert(completed.kind === 'collab_agent_tool_call', 'Runtime completed subAgentActivity should keep collab tool kind');
assert(completed.name === 'subAgentActivity', 'Runtime completed subAgentActivity should keep activity name');
assert(completed.input?.type === 'subAgentActivity', 'Runtime completed subAgentActivity tool_end should carry input');
assert(completed.input?.kind === 'started', 'Runtime lifecycle-completed subAgentActivity input should inherit the started activity kind when completed omits kind');
assert(completed.input?.prompt === prompt, 'Runtime completed subAgentActivity input should retain the started prompt when completed omits it');
assert(completed.input?.receiverThreadIds?.[0] === threadId, 'Runtime completed subAgentActivity input should keep receiverThreadIds');
assert(completed.input?.agentsStates?.[threadId]?.taskDescription === prompt, 'Runtime completed subAgentActivity input should retain the started taskDescription');
assert(completed.input?.agentsStates?.[threadId]?.status === 'running', 'Runtime lifecycle-completed subAgentActivity input should keep child running when completed omits kind');
const completedResult = JSON.parse(completed.result);
assert(completedResult.type === 'subAgentActivity', 'Runtime completed subAgentActivity result should preserve original type');
assert(completedResult.kind === 'started', 'Runtime lifecycle-completed subAgentActivity result should inherit the started activity kind when completed omits kind');
assert(completedResult.prompt === prompt, 'Runtime completed subAgentActivity result should retain the started prompt when completed omits it');
assert(completedResult.receiverThreadIds?.[0] === threadId, 'Runtime completed subAgentActivity result should expose receiverThreadIds');
assert(completedResult.agentsStates?.[threadId]?.taskDescription === prompt, 'Runtime completed subAgentActivity result should retain the started taskDescription');
assert(completedResult.agentsStates?.[threadId]?.status === 'running', 'Runtime lifecycle-completed subAgentActivity result should keep child running when completed omits kind');
assert(entry.toolCalls[0]?.kind === 'collab_agent_tool_call', 'Runtime persisted tool call should keep collab tool kind');
assert(entry.toolCalls[0]?.input?.type === 'subAgentActivity', 'Runtime persisted tool call should keep subAgentActivity input type for routing recovery');
assert(entry.toolCalls[0]?.input?.prompt === prompt, 'Runtime persisted subAgentActivity input should retain the started prompt');
assert(entry.toolCalls[0]?.input?.kind === 'started', 'Runtime persisted lifecycle-completed subAgentActivity input should keep the started activity kind');
assert(entry.toolCalls[0]?.input?.agentsStates?.[threadId]?.status === 'running', 'Runtime persisted lifecycle-completed subAgentActivity input should keep child running');
const explicitCompletedSent = [];
const explicitCompletedRuntime = createCodexAppRuntime({
wsSend: (_ws, payload) => explicitCompletedSent.push(payload),
loadSession: () => null,
saveSession: () => {},
});
const explicitCompletedEntry = { ws: {}, toolCalls: [], fullText: '' };
const explicitCompletedThreadId = 'agent-thread-runtime-completed';
explicitCompletedRuntime.processCodexAppNotification(explicitCompletedEntry, {
method: 'item/started',
params: {
item: {
id: 'runtime-activity-completed',
type: 'subAgentActivity',
kind: 'started',
agentThreadId: explicitCompletedThreadId,
agentPath: '/root/plan_reviewer',
prompt,
},
},
}, sessionId);
explicitCompletedRuntime.processCodexAppNotification(explicitCompletedEntry, {
method: 'item/completed',
params: {
item: {
id: 'runtime-activity-completed',
type: 'subAgentActivity',
kind: 'completed',
agentThreadId: explicitCompletedThreadId,
agentPath: '/root/plan_reviewer',
},
},
}, sessionId);
const explicitCompleted = explicitCompletedSent.find((msg) => msg.type === 'tool_end' && msg.toolUseId === 'runtime-activity-completed');
assert(explicitCompleted.input?.kind === 'completed', 'Runtime explicit completed subAgentActivity input should preserve completed activity kind');
assert(explicitCompleted.input?.prompt === prompt, 'Runtime explicit completed subAgentActivity input should retain the started prompt when completed omits it');
assert(explicitCompleted.input?.agentsStates?.[explicitCompletedThreadId]?.taskDescription === prompt, 'Runtime explicit completed subAgentActivity input should retain the started taskDescription');
assert(explicitCompleted.input?.agentsStates?.[explicitCompletedThreadId]?.status === 'completed', 'Runtime explicit completed subAgentActivity input should map child status to completed');
const explicitCompletedResult = JSON.parse(explicitCompleted.result);
assert(explicitCompletedResult.kind === 'completed', 'Runtime explicit completed subAgentActivity result should preserve completed activity kind');
assert(explicitCompletedResult.prompt === prompt, 'Runtime explicit completed subAgentActivity result should retain the started prompt');
assert(explicitCompletedResult.agentsStates?.[explicitCompletedThreadId]?.taskDescription === prompt, 'Runtime explicit completed subAgentActivity result should retain the started taskDescription');
assert(explicitCompletedResult.agentsStates?.[explicitCompletedThreadId]?.status === 'completed', 'Runtime explicit completed subAgentActivity result should map child status to completed');
const explicitRoleSent = [];
const explicitRoleRuntime = createCodexAppRuntime({
wsSend: (_ws, payload) => explicitRoleSent.push(payload),
loadSession: () => null,
saveSession: () => {},
});
explicitRoleRuntime.processCodexAppNotification({ ws: {}, toolCalls: [], fullText: '' }, {
method: 'item/started',
params: {
item: {
id: 'runtime-activity-role',
type: 'subAgentActivity',
kind: 'started',
agentThreadId: 'agent-thread-runtime-role',
agentPath: '/root/plan_reviewer',
role: 'reviewer',
},
},
}, sessionId);
const explicitRoleStarted = explicitRoleSent.find((msg) => msg.type === 'tool_start' && msg.toolUseId === 'runtime-activity-role');
assert(explicitRoleStarted.input?.agentsStates?.['agent-thread-runtime-role']?.role === 'reviewer', 'Runtime subAgentActivity should preserve explicit role');
const reasoningSent = [];
const reasoningRuntime = createCodexAppRuntime({
wsSend: (_ws, payload) => reasoningSent.push(payload),
loadSession: () => null,
saveSession: () => {},
});
reasoningRuntime.processCodexAppNotification({ ws: {}, toolCalls: [], fullText: '' }, {
method: 'item/completed',
params: {
item: {
id: 'runtime-reasoning',
type: 'reasoning',
content: [{ text: '推理完成' }],
},
},
}, sessionId);
const reasoningEnd = reasoningSent.find((msg) => msg.type === 'tool_end' && msg.toolUseId === 'runtime-reasoning');
assert(reasoningEnd, 'Runtime reasoning item/completed should still emit tool_end');
assert(!Object.prototype.hasOwnProperty.call(reasoningEnd, 'input'), 'Runtime non-subAgentActivity reasoning tool_end should not gain input');
assert(!Object.prototype.hasOwnProperty.call(reasoningEnd, 'name'), 'Runtime non-subAgentActivity reasoning tool_end should not gain name');
}
function assertFrontendPrimaryCodexAppUiContract() {
@@ -1268,23 +1706,35 @@ async function main() {
const targetIndex = process.argv.indexOf('--target');
const regressionTarget = targetIndex >= 0 ? String(process.argv[targetIndex + 1] || '').trim() : String(process.env.CC_WEB_REGRESSION_TARGET || '').trim();
if (regressionTarget) {
if (regressionTarget !== 'codexapp-unrouted-routing') {
throw new Error(`Unknown regression target: ${regressionTarget}`);
if (regressionTarget === 'composer-slash-routing') {
assertComposerSlashRoutingContract();
console.log('Composer slash routing regression checks passed.');
return;
}
assertCodexAppUnroutedNotificationRoutingContract();
console.log('Codex App unrouted routing regression checks passed.');
return;
if (regressionTarget === 'codexapp-unrouted-routing') {
assertCodexAppUnroutedNotificationRoutingContract();
console.log('Codex App unrouted routing regression checks passed.');
return;
}
if (regressionTarget === 'subagent-card-metadata') {
assertFrontendSubagentCardMetadataContract();
assertCodexAppRuntimeSubAgentActivityContract();
console.log('Subagent card metadata regression checks passed.');
return;
}
throw new Error(`Unknown regression target: ${regressionTarget}`);
}
assertUnlimitedImageAttachmentsContract();
assertFrontendGenerationControlsContract();
assertFrontendComposerMcpContract();
assertFrontendSlashDraftPreservationContract();
assertComposerSlashRoutingContract();
assertFrontendCcwebPromptContract();
assertFrontendMarkdownLinkContract();
assertMockCodexAppPromptUserNotTextTriggered();
assertFrontendMcpReloadContract();
assertFrontendSubagentCardMetadataContract();
assertCodexAppRuntimeSubAgentActivityContract();
assertFrontendPrimaryCodexAppUiContract();
assertSetTitleMcpContract();
assertSessionSwitchResilienceContract();
@@ -1562,11 +2012,29 @@ async function main() {
assert(!slashMcpRuntimeComposer.items.some((item) => item.kind === 'mcp' && item.itemType === 'server' && item.name === 'regRuntime'), 'Composer slash suggestions should not infer MCP servers from session tool names');
assert(!slashMcpRuntimeComposer.items.some((item) => item.kind === 'mcp' && item.itemType === 'server' && item.name === 'reg-state'), 'Composer slash suggestions should not infer MCP servers from mcp:server labels');
ws.send(JSON.stringify({ type: 'message', text: '/grilling', sessionId: codexSession.sessionId, mode: 'plan', agent: 'codex', requestId: 'reg-unknown-slash-draft' }));
const unknownSlashAttachment = await uploadAttachment(port, token, {
filename: 'unknown-slash.png',
mime: 'image/png',
data: Buffer.from('unknown-slash-image'),
});
ws.send(JSON.stringify({
type: 'message',
text: '/report/mcps?search',
attachments: [unknownSlashAttachment],
sessionId: codexSession.sessionId,
mode: 'plan',
agent: 'codex',
requestId: 'reg-unknown-slash-draft',
}));
const unknownSlashDraft = await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.requestId === 'reg-unknown-slash-draft');
assert(unknownSlashDraft.sessionId === codexSession.sessionId, 'Unknown slash draft response should stay scoped to the active session');
assert(unknownSlashDraft.preserveComposerDraft === true, 'Unknown slash command should tell the frontend to restore the composer draft');
assert(/未知指令: \/grilling/.test(unknownSlashDraft.message || ''), 'Unknown slash command should still show the normal failure hint');
assert(!unknownSlashDraft.preserveComposerDraft, 'Unknown slash hints should not restore text that is continuing through ordinary send');
assert(/未知指令: \/report\/mcps\?search/.test(unknownSlashDraft.message || ''), 'Unknown slash text should still show the normal hint');
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexSession.sessionId);
const storedUnknownSlashSession = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexSession.sessionId}.json`), 'utf8'));
const storedUnknownSlashMessage = storedUnknownSlashSession.messages.find((message) => message.role === 'user' && message.content === '/report/mcps?search');
assert(storedUnknownSlashMessage, 'Unknown slash text should continue through the ordinary message pipeline');
assert(storedUnknownSlashMessage.attachments?.some((attachment) => attachment.filename === unknownSlashAttachment.filename), 'Unknown slash text should preserve ordinary message attachments');
ws.send(JSON.stringify({ type: 'message', text: '/help', sessionId: codexSession.sessionId, mode: 'plan', agent: 'codex', requestId: 'reg-help-slash-draft' }));
const helpSlashDraft = await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.requestId === 'reg-help-slash-draft');
@@ -2614,6 +3082,38 @@ async function main() {
ws.send(JSON.stringify({ type: 'message', text: 'slow codexapp prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
await nextMessage(messages, ws, (msg) => msg.type === 'session_list' && msg.sessions.some((s) => s.id === codexAppSession.sessionId && s.isRunning));
await sleep(500);
ws.send(JSON.stringify({
type: 'message',
text: '/runtime/report',
sessionId: codexAppSession.sessionId,
mode: 'yolo',
agent: 'codexapp',
clientMessageId: 'regression-unknown-slash-steer',
}));
const runningUnknownSlashHint = await nextMessage(messages, ws, (msg) => (
msg.type === 'system_message' &&
msg.sessionId === codexAppSession.sessionId &&
/未知指令: \/runtime\/report/.test(msg.message || '')
));
assert(!runningUnknownSlashHint.preserveComposerDraft, 'Running Codex App unknown slash hints should not restore a message that continues through steer');
await nextMessage(messages, ws, (msg) => (
msg.type === 'codex_app_steer_status' &&
msg.sessionId === codexAppSession.sessionId &&
msg.clientMessageId === 'regression-unknown-slash-steer' &&
msg.status === 'pending'
));
const runningUnknownSlashDelta = await nextMessage(messages, ws, (msg) => (
msg.type === 'text_delta' &&
msg.sessionId === codexAppSession.sessionId &&
/steer accepted: \/runtime\/report/.test(msg.text || '')
));
assert(/\/runtime\/report/.test(runningUnknownSlashDelta.text || ''), 'Running Codex App unknown slash text should continue through turn/steer');
await nextMessage(messages, ws, (msg) => (
msg.type === 'codex_app_steer_status' &&
msg.sessionId === codexAppSession.sessionId &&
msg.clientMessageId === 'regression-unknown-slash-steer' &&
msg.status === 'inserted'
));
ws.send(JSON.stringify({
type: 'message',
text: 'runtime steer insert',
@@ -2648,6 +3148,7 @@ async function main() {
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
storedCodexApp = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
assert(storedCodexApp.codexAppThreadId === codexAppThreadId, 'Codex App follow-up should resume the same app-server thread');
assert(storedCodexApp.messages.some((message) => message.role === 'user' && message.content === '/runtime/report'), 'Running Codex App unknown slash text should persist as ordinary user history');
assert(storedCodexApp.messages.some((message) => message.role === 'user' && message.content === 'runtime steer insert'), 'Codex App steer message should be persisted as user history');
assert(storedCodexApp.messages.some((message) => message.role === 'assistant' && /runtime steer insert/.test(String(message.content || ''))), 'Codex App steered assistant output should be persisted');