feat: complete Codex App capabilities and rebuild release
This commit is contained in:
@@ -102,6 +102,7 @@ function ensureThread(threadId, params = {}) {
|
||||
capacityRetryAttempts: new Map(),
|
||||
reconnectRetryAttempts: new Map(),
|
||||
goal: null,
|
||||
threadStartWebSearchMode: params.config?.web_search || null,
|
||||
});
|
||||
}
|
||||
const thread = threads.get(id);
|
||||
@@ -537,9 +538,12 @@ function completeTurnWithoutTerminalNotification(thread, turnId, text) {
|
||||
thread.steers = [];
|
||||
}
|
||||
|
||||
function completeGoalBackgroundTurn(thread, objective) {
|
||||
function completeGoalBackgroundTurn(thread, objective, turnNumber = 1, totalTurns = 1) {
|
||||
const turnId = `goal-turn-${crypto.randomUUID()}`;
|
||||
const text = `Goal background output: ${objective}`;
|
||||
const itemId = `goal-agent-msg-${turnNumber}`;
|
||||
const text = turnNumber === 1
|
||||
? `Goal background output: ${objective}`
|
||||
: `Goal continuation output ${turnNumber}: ${objective}`;
|
||||
thread.activeTurnId = turnId;
|
||||
send({
|
||||
method: 'turn/started',
|
||||
@@ -553,7 +557,7 @@ function completeGoalBackgroundTurn(thread, objective) {
|
||||
params: {
|
||||
threadId: thread.id,
|
||||
turnId,
|
||||
itemId: 'goal-agent-msg',
|
||||
itemId,
|
||||
delta: text,
|
||||
},
|
||||
});
|
||||
@@ -564,13 +568,24 @@ function completeGoalBackgroundTurn(thread, objective) {
|
||||
turnId,
|
||||
completedAtMs: Date.now(),
|
||||
item: {
|
||||
id: 'goal-agent-msg',
|
||||
id: itemId,
|
||||
type: 'agentMessage',
|
||||
text,
|
||||
status: 'completed',
|
||||
},
|
||||
},
|
||||
});
|
||||
if (turnNumber >= totalTurns && thread.goal) {
|
||||
thread.goal = {
|
||||
...thread.goal,
|
||||
status: 'complete',
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
send({
|
||||
method: 'thread/goal/updated',
|
||||
params: { threadId: thread.id, goal: thread.goal },
|
||||
});
|
||||
}
|
||||
send({
|
||||
method: 'thread/tokenUsage/updated',
|
||||
params: {
|
||||
@@ -591,6 +606,9 @@ function completeGoalBackgroundTurn(thread, objective) {
|
||||
},
|
||||
});
|
||||
thread.activeTurnId = null;
|
||||
if (turnNumber < totalTurns) {
|
||||
setTimeout(() => completeGoalBackgroundTurn(thread, objective, turnNumber + 1, totalTurns), 120);
|
||||
}
|
||||
}
|
||||
|
||||
function requestClient(method, params, callback) {
|
||||
@@ -667,6 +685,9 @@ function completeMcpToolTurn(thread, turnId) {
|
||||
ok: true,
|
||||
currentConversationId: env.CC_WEB_SOURCE_SESSION_ID || urlSourceSessionId,
|
||||
sourceHopCount: env.CC_WEB_CROSS_HOP_COUNT || urlSourceHopCount,
|
||||
threadStartWebSearchMode: thread.threadStartWebSearchMode || null,
|
||||
threadConfigMethod: thread.lastThreadConfigMethod || null,
|
||||
webSearchMode: thread.config?.web_search || null,
|
||||
hasCcwebMcpConfig: Boolean(ccwebConfig),
|
||||
hasProjectMcpConfig: Boolean(projectConfig),
|
||||
ccwebType: ccwebConfig?.type || (ccwebConfig?.url ? 'streamable_http' : (ccwebConfig?.command ? 'stdio' : null)),
|
||||
@@ -1092,19 +1113,42 @@ function handleRequest(message) {
|
||||
}
|
||||
if (method === 'thread/start') {
|
||||
const thread = ensureThread(null, params);
|
||||
thread.lastThreadConfigMethod = 'thread/start';
|
||||
send({ id, result: { thread: threadPayload(thread), model: params.model || 'gpt-5.5', cwd: thread.cwd, modelProvider: 'mock', approvalPolicy: params.approvalPolicy || 'never', approvalsReviewer: 'user', sandbox: params.sandbox || 'danger-full-access' } });
|
||||
return;
|
||||
}
|
||||
if (method === 'thread/resume') {
|
||||
if (params.threadId && resumeMismatchThreads.delete(params.threadId)) {
|
||||
const thread = ensureThread(null, params);
|
||||
thread.lastThreadConfigMethod = 'thread/start';
|
||||
send({ id, result: { thread: threadPayload(thread), model: params.model || 'gpt-5.5', cwd: thread.cwd, modelProvider: 'mock', approvalPolicy: params.approvalPolicy || 'never', approvalsReviewer: 'user', sandbox: params.sandbox || 'danger-full-access' } });
|
||||
return;
|
||||
}
|
||||
const thread = ensureThread(params.threadId, params);
|
||||
thread.lastThreadConfigMethod = 'thread/resume';
|
||||
send({ id, result: { thread: threadPayload(thread), model: params.model || 'gpt-5.5', cwd: thread.cwd, modelProvider: 'mock', approvalPolicy: params.approvalPolicy || 'never', approvalsReviewer: 'user', sandbox: params.sandbox || 'danger-full-access' } });
|
||||
return;
|
||||
}
|
||||
if (method === 'thread/compact/start') {
|
||||
const thread = ensureThread(params.threadId, params);
|
||||
const compactTurnId = `app-compact-${crypto.randomUUID()}`;
|
||||
thread.lastCompactionTurnId = compactTurnId;
|
||||
send({
|
||||
method: 'thread/compacted',
|
||||
params: {
|
||||
threadId: thread.id,
|
||||
turnId: compactTurnId,
|
||||
},
|
||||
});
|
||||
send({
|
||||
id,
|
||||
result: {
|
||||
threadId: thread.id,
|
||||
turnId: compactTurnId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (method === 'thread/goal/get') {
|
||||
const thread = ensureThread(params.threadId, params);
|
||||
send({ id, result: { goal: thread.goal } });
|
||||
@@ -1130,10 +1174,11 @@ function handleRequest(message) {
|
||||
params: { threadId: thread.id, goal: thread.goal },
|
||||
});
|
||||
setTimeout(() => {
|
||||
send({ id, result: { goal: thread.goal } });
|
||||
if (params.objective) {
|
||||
setTimeout(() => completeGoalBackgroundTurn(thread, objective), 50);
|
||||
}
|
||||
send({ id, result: { goal: thread.goal } });
|
||||
if (params.objective) {
|
||||
const totalTurns = /improve benchmark coverage/i.test(objective) ? 2 : 1;
|
||||
setTimeout(() => completeGoalBackgroundTurn(thread, objective, 1, totalTurns), 50);
|
||||
}
|
||||
}, 250);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2444,6 +2444,47 @@ function assertCodexAppRuntimeSubAgentActivityContract() {
|
||||
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');
|
||||
|
||||
const webSearchSent = [];
|
||||
const webSearchRuntime = createCodexAppRuntime({
|
||||
wsSend: (_ws, payload) => webSearchSent.push(payload),
|
||||
loadSession: () => null,
|
||||
saveSession: () => {},
|
||||
});
|
||||
const webSearchEntry = { ws: {}, toolCalls: [], fullText: '' };
|
||||
webSearchRuntime.processCodexAppNotification(webSearchEntry, {
|
||||
method: 'item/started',
|
||||
params: {
|
||||
item: {
|
||||
id: 'runtime-web-search',
|
||||
type: 'webSearch',
|
||||
query: 'cc-web Web Search regression',
|
||||
},
|
||||
},
|
||||
}, sessionId);
|
||||
const webSearchStart = webSearchSent.find((msg) => msg.type === 'tool_start' && msg.toolUseId === 'runtime-web-search');
|
||||
assert(webSearchStart, 'Runtime webSearch item/started should emit tool_start');
|
||||
assert(webSearchStart.name === 'WebSearch', 'Runtime webSearch tool_start should map to WebSearch');
|
||||
assert(webSearchStart.kind === 'web_search', 'Runtime webSearch tool_start should map to web_search kind');
|
||||
assert(webSearchStart.input?.type === 'webSearch', 'Runtime webSearch input should preserve original item type');
|
||||
assert(webSearchStart.input?.query === 'cc-web Web Search regression', 'Runtime webSearch input should preserve query');
|
||||
|
||||
webSearchRuntime.processCodexAppNotification(webSearchEntry, {
|
||||
method: 'item/completed',
|
||||
params: {
|
||||
item: {
|
||||
id: 'runtime-web-search',
|
||||
type: 'webSearch',
|
||||
query: 'cc-web Web Search regression',
|
||||
results: [{ title: 'Result', url: 'https://example.com/result' }],
|
||||
},
|
||||
},
|
||||
}, sessionId);
|
||||
const webSearchEnd = webSearchSent.find((msg) => msg.type === 'tool_end' && msg.toolUseId === 'runtime-web-search');
|
||||
assert(webSearchEnd, 'Runtime webSearch item/completed should emit tool_end');
|
||||
assert(webSearchEnd.kind === 'web_search', 'Runtime webSearch tool_end should keep web_search kind');
|
||||
assert(webSearchEntry.toolCalls[0]?.name === 'WebSearch', 'Runtime persisted webSearch tool should use WebSearch name');
|
||||
assert(webSearchEntry.toolCalls[0]?.kind === 'web_search', 'Runtime persisted webSearch tool should use web_search kind');
|
||||
}
|
||||
|
||||
function assertCodexAppTransientReconnectContract() {
|
||||
@@ -2531,6 +2572,10 @@ function assertFrontendPrimaryCodexAppUiContract() {
|
||||
assert(source.includes("localStorage.setItem('cc-web-agent', currentAgent);"), 'Frontend should overwrite stale cc-web-agent storage with the primary UI agent');
|
||||
assert(source.includes('currentAgent = normalizeUiAgent(agent);'), 'setCurrentAgent should coerce ordinary UI agent changes back to Codex App');
|
||||
assert(source.includes('return sessions.filter((s) => isPrimaryUiAgent(s.agent));'), 'Session list should only expose Codex App sessions in ordinary UI');
|
||||
assert(source.includes('id="codex-enable-search"'), 'Codex settings should expose the Web Search toggle');
|
||||
assert(source.includes('仅作用于当前 Codex App 会话的原生联网搜索'), 'Codex Web Search setting copy should scope the toggle to Codex App native search');
|
||||
assert(source.includes('codexSearchToggle.checked = !!currentCodexConfig.enableSearch;'), 'Codex Web Search toggle should load the persisted setting');
|
||||
assert(source.includes('enableSearch: !!codexSearchToggle.checked'), 'Codex Web Search save should send the real toggle state');
|
||||
assert(
|
||||
/function applySessionSnapshot\(snapshot[\s\S]*?!isPrimaryUiAgent\(snapshotAgent\)[\s\S]*?return false;[\s\S]*?return true;/.test(source),
|
||||
'Frontend should reject legacy Claude/Codex snapshots from the current view'
|
||||
@@ -4001,6 +4046,76 @@ function assertCodexAppStaleRunningRecoveryContract() {
|
||||
);
|
||||
}
|
||||
|
||||
function assertCodexAppGoalLifecycleContract() {
|
||||
const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
|
||||
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
|
||||
const doneStart = frontendSource.indexOf("case 'done':");
|
||||
const doneEnd = frontendSource.indexOf("case 'system_message':", doneStart);
|
||||
const doneBlock = doneStart >= 0 && doneEnd > doneStart
|
||||
? frontendSource.slice(doneStart, doneEnd)
|
||||
: '';
|
||||
const finishBlock = extractFunctionSource(frontendSource, 'finishGenerating');
|
||||
const controlsBlock = extractFunctionSource(frontendSource, 'updateGenerationControls');
|
||||
const sendMessageBlock = extractFunctionSource(frontendSource, 'sendMessage');
|
||||
const runningBlock = extractFunctionSource(serverSource, 'isSessionRunning');
|
||||
const notificationBlock = extractFunctionSource(serverSource, 'handleCodexAppGoalNotification');
|
||||
const completeBlock = extractFunctionSource(serverSource, 'handleCodexAppTurnComplete');
|
||||
const pauseBlock = extractFunctionSource(serverSource, 'pauseCodexAppGoalForAbort');
|
||||
const abortBlock = extractFunctionSource(serverSource, 'handleAbort');
|
||||
const messageBlock = extractFunctionSource(serverSource, 'handleMessage');
|
||||
const deleteBlock = extractFunctionSource(serverSource, 'handleDeleteSession');
|
||||
|
||||
assert(
|
||||
doneBlock.includes('snapshot.isRunning = msg.goalActive === true')
|
||||
&& doneBlock.includes('finishGenerating(msg.sessionId, { keepRunning: msg.goalActive === true })'),
|
||||
'Frontend done handling should keep current and background Goal sessions running between turns'
|
||||
);
|
||||
assert(
|
||||
finishBlock.includes('options = {}')
|
||||
&& finishBlock.includes('const keepRunning = options.keepRunning === true')
|
||||
&& finishBlock.includes('setCurrentSessionRunningState(keepRunning)'),
|
||||
'Frontend finishGenerating should close one bubble without ending an active Goal'
|
||||
);
|
||||
assert(
|
||||
controlsBlock.includes('const runtimeBusy = isGenerating || currentSessionRunning')
|
||||
&& controlsBlock.includes('abortBtn.hidden = !runtimeBusy'),
|
||||
'Frontend should keep Stop available while an active Goal is between turns'
|
||||
);
|
||||
assert(
|
||||
sendMessageBlock.includes('if (currentSessionRunning && !isGenerating) return;'),
|
||||
'Frontend should not submit an ordinary message into an active Goal turn gap'
|
||||
);
|
||||
assert(
|
||||
runningBlock.includes('isCodexAppGoalActive(sessionId)')
|
||||
&& notificationBlock.includes("method !== 'thread/goal/updated'")
|
||||
&& notificationBlock.includes("method !== 'thread/goal/cleared'")
|
||||
&& notificationBlock.includes('updateCodexAppGoalState(matched.session, goal)'),
|
||||
'Server should maintain thread-level Goal state independently from active turns'
|
||||
);
|
||||
assert(
|
||||
completeBlock.includes('const goalActive = isCodexAppGoalActive(sessionId)')
|
||||
&& completeBlock.includes("{ type: 'done', sessionId, costUsd: null, goalActive }")
|
||||
&& completeBlock.includes('if (goalActive)')
|
||||
&& completeBlock.includes('broadcastSessionList()'),
|
||||
'Server turn completion should report active Goal state without sending final background completion'
|
||||
);
|
||||
assert(
|
||||
pauseBlock.includes("status: 'paused'")
|
||||
&& pauseBlock.includes("client.request('thread/goal/set'")
|
||||
&& abortBlock.includes('pauseCodexAppGoalForAbort(sessionId, ws)')
|
||||
&& abortBlock.includes('handleCodexAppAbortSession(sessionId, ws)'),
|
||||
'Stop should pause the Goal and interrupt any currently active turn'
|
||||
);
|
||||
assert(
|
||||
messageBlock.includes('!codexAppGoalStates.has(sessionId)')
|
||||
&& messageBlock.includes('loadSession(sessionId)')
|
||||
&& messageBlock.includes('isCodexAppGoalActive(sessionId)')
|
||||
&& messageBlock.includes('Goal 仍在持续运行')
|
||||
&& deleteBlock.includes('codexAppGoalStates.delete(sessionId)'),
|
||||
'Server should block ordinary Goal-gap turns and clean Goal state on deletion'
|
||||
);
|
||||
}
|
||||
|
||||
function assertRuntimeImageSendStaticContract() {
|
||||
const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
|
||||
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
|
||||
@@ -5954,6 +6069,11 @@ async function main() {
|
||||
console.log('Codex App stale running regression checks passed.');
|
||||
return;
|
||||
}
|
||||
if (regressionTarget === 'codexapp-goal-lifecycle') {
|
||||
assertCodexAppGoalLifecycleContract();
|
||||
console.log('Codex App Goal lifecycle regression checks passed.');
|
||||
return;
|
||||
}
|
||||
if (regressionTarget === 'session-switch-race') {
|
||||
assertSessionSwitchRaceContract();
|
||||
console.log('Session switch race regression checks passed.');
|
||||
@@ -6064,6 +6184,7 @@ async function main() {
|
||||
assertCcwebMcpChildUpdateCoalescingContract();
|
||||
assertTitleHistoryOutlineContract();
|
||||
assertSessionSwitchResilienceContract();
|
||||
assertCodexAppGoalLifecycleContract();
|
||||
assertSessionSwitchRaceContract();
|
||||
assertAdvancedSessionSearchContract();
|
||||
await assertAdvancedSearchTimeOrderingContract();
|
||||
@@ -6260,8 +6381,8 @@ async function main() {
|
||||
assert(codexConfigMsg.config.mode === 'custom', 'Codex config mode save/load failed');
|
||||
assert(codexConfigMsg.config.activeProfile === 'Regression Profile', 'Codex active profile save/load failed');
|
||||
assert(Array.isArray(codexConfigMsg.config.profiles) && codexConfigMsg.config.profiles[0]?.apiKey.includes('****'), 'Codex profile API key should be masked');
|
||||
assert(codexConfigMsg.config.supportsSearch === false, 'Codex config should expose unsupported search capability');
|
||||
assert(codexConfigMsg.config.enableSearch === false, 'Codex config should ignore unsupported search toggle');
|
||||
assert(codexConfigMsg.config.supportsSearch === true, 'Codex config should expose Codex App native search capability');
|
||||
assert(codexConfigMsg.config.enableSearch === true, 'Codex config should persist enabled Web Search');
|
||||
assert(codexConfigMsg.config.retry?.mode === 'limited', 'Codex retry mode should round-trip');
|
||||
assert(codexConfigMsg.config.retry?.intervalSeconds === 1, 'Codex retry interval should round-trip');
|
||||
assert(codexConfigMsg.config.retry?.maxAttempts === 2, 'Codex retry max attempts should round-trip');
|
||||
@@ -7215,6 +7336,13 @@ async function main() {
|
||||
assert(/"hasTopLevelEffort":false/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration turn should not duplicate effort at top level');
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
||||
|
||||
ws.send(JSON.stringify({ type: 'message', text: 'codexapp dynamic web search enabled prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
const codexAppEnabledSearchDynamicTool = await nextMessage(messages, ws, (msg) => msg.type === 'tool_end' && msg.sessionId === codexAppSession.sessionId && msg.toolUseId === 'mcp-ccweb-list');
|
||||
assert(/"threadStartWebSearchMode": "live"/.test(codexAppEnabledSearchDynamicTool.result || ''), 'Codex App thread/start should pass web_search=live when Web Search is enabled');
|
||||
assert(/"webSearchMode": "live"/.test(codexAppEnabledSearchDynamicTool.result || ''), 'Codex App thread config should keep web_search=live while Web Search is enabled');
|
||||
assert(!/"webSearchToolConfig"/.test(codexAppEnabledSearchDynamicTool.result || ''), 'Codex App should use top-level web_search instead of tools.web_search as the mode switch');
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'save_codex_config',
|
||||
config: {
|
||||
@@ -7229,6 +7357,7 @@ async function main() {
|
||||
msg.type === 'codex_config' && msg.config?.activeProfile === 'Regression Profile Updated'
|
||||
);
|
||||
assert(codexAppChangedConfig.config.mode === 'custom', 'Codex App config-change regression should save custom mode');
|
||||
assert(codexAppChangedConfig.config.enableSearch === false, 'Codex App config-change regression should persist disabled Web Search');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'message', text: 'codexapp after config change prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
const codexAppAfterConfigChange = await nextMessage(messages, ws, (msg) => (
|
||||
@@ -7239,6 +7368,13 @@ async function main() {
|
||||
assert(/codexapp after config change prompt/.test(codexAppAfterConfigChange.text || ''), 'Codex App should not reject a new turn after config signature changes');
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
||||
|
||||
ws.send(JSON.stringify({ type: 'message', text: 'codexapp dynamic after web search disabled prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
const codexAppDisabledSearchDynamicTool = await nextMessage(messages, ws, (msg) => msg.type === 'tool_end' && msg.sessionId === codexAppSession.sessionId && msg.toolUseId === 'mcp-ccweb-list');
|
||||
assert(/"threadConfigMethod": "thread\/resume"/.test(codexAppDisabledSearchDynamicTool.result || ''), 'Codex App existing thread should refresh config through thread/resume');
|
||||
assert(/"webSearchMode": "disabled"/.test(codexAppDisabledSearchDynamicTool.result || ''), 'Codex App thread/resume should pass web_search=disabled after Web Search is disabled');
|
||||
assert(!/"webSearchToolConfig"/.test(codexAppDisabledSearchDynamicTool.result || ''), 'Codex App should not use tools.web_search as the Web Search mode switch');
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
||||
|
||||
const codexAppRetryText = 'codexapp capacity retry prompt';
|
||||
ws.send(JSON.stringify({ type: 'message', text: codexAppRetryText, sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
const codexAppCapacityRetryNotice = await nextMessage(messages, ws, (msg) => (
|
||||
@@ -7346,13 +7482,45 @@ async function main() {
|
||||
/Goal background output: improve benchmark coverage/.test(msg.text || '')
|
||||
), 5000);
|
||||
assert(/Goal background output/.test(codexAppGoalBackgroundDelta.text || ''), 'Codex App /goal background turn should stream through the active session');
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId, 5000);
|
||||
const codexAppGoalFirstTurnDone = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'done' &&
|
||||
msg.sessionId === codexAppSession.sessionId &&
|
||||
msg.goalActive === true
|
||||
), 5000);
|
||||
assert(codexAppGoalFirstTurnDone.goalActive === true, 'Goal first turn completion should remain explicitly active');
|
||||
const codexAppGoalStillRunningList = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'session_list' &&
|
||||
Array.isArray(msg.sessions) &&
|
||||
msg.sessions.some((session) => session.id === codexAppSession.sessionId && session.isRunning)
|
||||
), 5000);
|
||||
assert(codexAppGoalStillRunningList.sessions.some((session) => session.id === codexAppSession.sessionId && session.isRunning), 'Goal should stay running between continuation turns');
|
||||
const codexAppGoalContinuationDelta = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'text_delta' &&
|
||||
msg.sessionId === codexAppSession.sessionId &&
|
||||
/Goal continuation output 2: improve benchmark coverage/.test(msg.text || '')
|
||||
), 5000);
|
||||
assert(/Goal continuation output 2/.test(codexAppGoalContinuationDelta.text || ''), 'Goal continuation turn should stream through the same session');
|
||||
const codexAppGoalFinalDone = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'done' &&
|
||||
msg.sessionId === codexAppSession.sessionId &&
|
||||
msg.goalActive !== true
|
||||
), 5000);
|
||||
assert(codexAppGoalFinalDone.goalActive !== true, 'Goal terminal turn should emit a final done event');
|
||||
const codexAppGoalIdleList = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'session_list' &&
|
||||
Array.isArray(msg.sessions) &&
|
||||
msg.sessions.some((session) => session.id === codexAppSession.sessionId && !session.isRunning)
|
||||
), 5000);
|
||||
assert(codexAppGoalIdleList.sessions.some((session) => session.id === codexAppSession.sessionId && !session.isRunning), 'Codex App /goal RPC should clear running state after app-server responds');
|
||||
assert(codexAppGoalIdleList.sessions.some((session) => session.id === codexAppSession.sessionId && !session.isRunning), 'Codex App Goal should become idle only after the Goal reaches a terminal status');
|
||||
const storedCodexAppGoalSession = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
|
||||
assert(
|
||||
storedCodexAppGoalSession.messages.some((message) => message.role === 'assistant' && /Goal background output: improve benchmark coverage/.test(String(message.content || ''))),
|
||||
'Goal first turn assistant output should be persisted'
|
||||
);
|
||||
assert(
|
||||
storedCodexAppGoalSession.messages.some((message) => message.role === 'assistant' && /Goal continuation output 2: improve benchmark coverage/.test(String(message.content || ''))),
|
||||
'Goal continuation assistant output should be persisted separately'
|
||||
);
|
||||
ws.send(JSON.stringify({
|
||||
type: 'message',
|
||||
text: '/goal improve benchmark coverage',
|
||||
@@ -7372,7 +7540,7 @@ async function main() {
|
||||
assert(!messages.some((msg) => msg.type === 'session_message' && msg.message?.id === codexAppGoalMessageId), 'Duplicate Goal command ids should not append another user bubble');
|
||||
assert(!messages.some((msg) => msg.type === 'text_delta' && /Goal background output: improve benchmark coverage/.test(msg.text || '')), 'Duplicate Goal command ids should not trigger another Goal RPC background turn');
|
||||
ws.send(JSON.stringify({ type: 'message', text: '/goal', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
const codexAppGoalShow = await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.sessionId === codexAppSession.sessionId && /Goal active/.test(msg.message || '') && /improve benchmark coverage/.test(msg.message || ''));
|
||||
const codexAppGoalShow = await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.sessionId === codexAppSession.sessionId && /Goal complete/.test(msg.message || '') && /improve benchmark coverage/.test(msg.message || ''));
|
||||
assert(/improve benchmark coverage/.test(codexAppGoalShow.message || ''), 'Codex App /goal should show the current goal');
|
||||
ws.send(JSON.stringify({ type: 'message', text: '/goal pause', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
const codexAppGoalPause = await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.sessionId === codexAppSession.sessionId && /Goal paused/.test(msg.message || ''));
|
||||
@@ -7432,6 +7600,28 @@ async function main() {
|
||||
assert(storedCodexApp.messages.some((message) => message.role === 'assistant' && /codexapp tool prompt/.test(String(message.content || ''))), 'Codex App assistant response should be persisted');
|
||||
assert((storedCodexApp.totalUsage?.inputTokens || 0) > 0, 'Codex App token usage should be persisted');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'message', text: '/compact', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
const codexAppCompactStart = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'system_message' &&
|
||||
msg.sessionId === codexAppSession.sessionId &&
|
||||
/正在执行 Codex App 原生 \/compact/.test(msg.message || '')
|
||||
), 5000);
|
||||
assert(/Codex App 原生 \/compact/.test(codexAppCompactStart.message || ''), 'Codex App /compact should announce native compaction');
|
||||
const codexAppCompactRunning = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'session_list' &&
|
||||
msg.sessions?.some((session) => session.id === codexAppSession.sessionId && session.isRunning)
|
||||
), 5000);
|
||||
assert(codexAppCompactRunning.sessions.some((session) => session.id === codexAppSession.sessionId && session.isRunning), 'Codex App /compact should mark the session running');
|
||||
const codexAppCompactDone = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'system_message' &&
|
||||
msg.sessionId === codexAppSession.sessionId &&
|
||||
/已执行 Codex App 原生 \/compact/.test(msg.message || '')
|
||||
), 10000);
|
||||
assert(/上下文压缩完成/.test(codexAppCompactDone.message || ''), 'Codex App /compact should complete through app-server');
|
||||
const storedCodexAppAfterCompact = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
|
||||
assert(storedCodexAppAfterCompact.codexAppThreadId === codexAppThreadId, 'Codex App /compact should keep the same thread');
|
||||
assert(!storedCodexAppAfterCompact.messages.some((message) => message.role === 'user' && message.content === '/compact'), 'Codex App /compact should not persist the slash command as a user message');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'message', text: 'codexapp huge output prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
const codexAppHugeTool = await nextMessage(messages, ws, (msg) => msg.type === 'tool_end' && msg.sessionId === codexAppSession.sessionId && msg.toolUseId === 'huge-tool');
|
||||
assert((codexAppHugeTool.result || '').length <= 33000, 'Codex App huge tool result should be capped before sending to the browser');
|
||||
|
||||
Reference in New Issue
Block a user