feat: support codex app goal command
This commit is contained in:
@@ -260,7 +260,16 @@ function nextMessage(messages, ws, predicate, timeoutMs = 15000) {
|
||||
clearInterval(timer);
|
||||
const recentTypes = messages.slice(-12).map((m) => m?.type).join(', ');
|
||||
const pendingTypes = messages.slice(0, 12).map((m) => m?.type).join(', ');
|
||||
reject(new Error(`Timed out waiting for expected WebSocket message (wsState=${ws.readyState}, callSite=${callSite}, pendingTypes=[${pendingTypes}], recentTypes=[${recentTypes}])`));
|
||||
const recentDetails = messages.slice(-6).map((m) => JSON.stringify({
|
||||
type: m?.type,
|
||||
sessionId: m?.sessionId,
|
||||
status: m?.status,
|
||||
clientMessageId: m?.clientMessageId,
|
||||
code: m?.code,
|
||||
text: typeof m?.text === 'string' ? m.text.slice(0, 120) : undefined,
|
||||
message: typeof m?.message === 'string' ? m.message.slice(0, 120) : undefined,
|
||||
})).join(' | ');
|
||||
reject(new Error(`Timed out waiting for expected WebSocket message (wsState=${ws.readyState}, callSite=${callSite}, pendingTypes=[${pendingTypes}], recentTypes=[${recentTypes}], recentDetails=[${recentDetails}])`));
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
@@ -432,6 +441,10 @@ function createFakeCodexConfig(homeDir, { model = 'gpt-5.5', reasoningEffort = '
|
||||
'[projects."/tmp/project-b"]',
|
||||
'trust_level = "trusted"',
|
||||
'',
|
||||
'[mcp_servers.reg-config]',
|
||||
'command = "node"',
|
||||
'args = ["regression-mcp.js"]',
|
||||
'',
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
@@ -634,6 +647,23 @@ async function main() {
|
||||
const slashMcpComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-slash-mcp');
|
||||
assert(slashMcpComposer.items.some((item) => item.kind === 'mcp' && item.name === 'ccweb_list_conversations'), 'Composer slash suggestions should include ccweb MCP tools');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-slash-mcp-config', trigger: '/', query: 'reg-config', sessionId: codexSession.sessionId, agent: 'codex' }));
|
||||
const slashMcpConfigComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-slash-mcp-config');
|
||||
assert(slashMcpConfigComposer.items.some((item) => item.kind === 'mcp' && item.itemType === 'server' && item.name === 'reg-config'), 'Composer slash suggestions should include MCP servers from Codex config');
|
||||
|
||||
const storedComposerFixture = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexSession.sessionId}.json`), 'utf8'));
|
||||
storedComposerFixture.messages.push({
|
||||
role: 'assistant',
|
||||
content: 'Runtime tools include mcp__regRuntime__inspect_schema and mcp:reg-state/query.',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
fs.writeFileSync(path.join(sessionsDir, `${codexSession.sessionId}.json`), JSON.stringify(storedComposerFixture, null, 2));
|
||||
|
||||
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-slash-mcp-runtime', trigger: '/', query: 'reg', sessionId: codexSession.sessionId, agent: 'codex' }));
|
||||
const slashMcpRuntimeComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-slash-mcp-runtime');
|
||||
assert(slashMcpRuntimeComposer.items.some((item) => item.kind === 'mcp' && item.itemType === 'server' && item.name === 'regRuntime'), 'Composer slash suggestions should include 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 include MCP servers from mcp:server labels');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-skill', trigger: '$', query: 'reg', sessionId: codexSession.sessionId, agent: 'codex' }));
|
||||
const skillComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-skill');
|
||||
assert(skillComposer.items.some((item) => item.kind === 'skill' && item.name === 'regression-skill'), 'Composer skill suggestions should include local Codex skill');
|
||||
@@ -972,6 +1002,9 @@ async function main() {
|
||||
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-codexapp-skill', trigger: '$', query: 'reg', sessionId: codexAppSession.sessionId, agent: 'codexapp' }));
|
||||
const codexAppSkillComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-codexapp-skill');
|
||||
assert(codexAppSkillComposer.items.some((item) => item.kind === 'skill' && item.name === 'regression-skill'), 'Codex App composer skill suggestions should include local Codex skill');
|
||||
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-codexapp-goal-slash', trigger: '/', query: 'go', sessionId: codexAppSession.sessionId, agent: 'codexapp' }));
|
||||
const codexAppGoalSlashComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-codexapp-goal-slash');
|
||||
assert(codexAppGoalSlashComposer.items.some((item) => item.kind === 'command' && item.name === '/goal'), 'Codex App composer slash suggestions should include /goal');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'message', text: 'codexapp collaboration default probe', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
const codexAppDefaultCollab = await nextMessage(messages, ws, (msg) => msg.type === 'text_delta' && msg.sessionId === codexAppSession.sessionId && /collaboration mode:/.test(msg.text || ''));
|
||||
@@ -982,6 +1015,27 @@ 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: '/goal improve benchmark coverage', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
const codexAppGoalSet = await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.sessionId === codexAppSession.sessionId && /Goal active/.test(msg.message || '') && /improve benchmark coverage/.test(msg.message || ''));
|
||||
assert(/Goal active/.test(codexAppGoalSet.message || ''), 'Codex App /goal should set an active goal');
|
||||
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 || ''));
|
||||
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 || ''));
|
||||
assert(/Goal paused/.test(codexAppGoalPause.message || ''), 'Codex App /goal pause should pause the current goal');
|
||||
ws.send(JSON.stringify({ type: 'message', text: '/goal resume', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
const codexAppGoalResume = await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.sessionId === codexAppSession.sessionId && /Goal active/.test(msg.message || ''));
|
||||
assert(/Goal active/.test(codexAppGoalResume.message || ''), 'Codex App /goal resume should resume the current goal');
|
||||
ws.send(JSON.stringify({ type: 'message', text: '/goal clear', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
const codexAppGoalClear = await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.sessionId === codexAppSession.sessionId && /Goal cleared/.test(msg.message || ''));
|
||||
assert(/Goal cleared/.test(codexAppGoalClear.message || ''), 'Codex App /goal clear should clear the current goal');
|
||||
ws.send(JSON.stringify({ type: 'message', text: '/goal', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
const codexAppGoalEmpty = await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.sessionId === codexAppSession.sessionId && /用法: \/goal <目标描述>/.test(msg.message || ''));
|
||||
assert(/\/goal <目标描述>/.test(codexAppGoalEmpty.message || ''), 'Codex App /goal should show usage when no goal exists');
|
||||
const storedCodexAppAfterGoal = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
|
||||
assert(!storedCodexAppAfterGoal.messages.some((message) => message.role === 'user' && /^\/goal/.test(String(message.content || ''))), 'Codex App /goal slash commands should not be persisted as normal user messages');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'message', text: 'codexapp runtime warning prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
const codexAppRuntimeWarning = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'system_message' &&
|
||||
@@ -1108,6 +1162,27 @@ async function main() {
|
||||
assert(/guided answer: A/.test(guidedDelta.text || ''), 'Codex App should continue after guided input response');
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
||||
|
||||
ws.send(JSON.stringify({ type: 'message', text: 'codexapp approval prompt', sessionId: codexAppSession.sessionId, mode: 'default', agent: 'codexapp' }));
|
||||
const approvalRequest = await nextMessage(messages, ws, (msg) => msg.type === 'codex_app_approval_request' && msg.sessionId === codexAppSession.sessionId);
|
||||
assert(approvalRequest.method === 'item/commandExecution/requestApproval', 'Codex App should forward command approval requests');
|
||||
assert(approvalRequest.itemId === 'approval-command-call', 'Codex App approval request should keep item id');
|
||||
assert(/echo approved/.test(JSON.stringify(approvalRequest.payload || {})), 'Codex App approval request should include command payload');
|
||||
ws.send(JSON.stringify({
|
||||
type: 'codex_app_approval_response',
|
||||
action: 'approve_session',
|
||||
sessionId: codexAppSession.sessionId,
|
||||
requestId: approvalRequest.requestId,
|
||||
}));
|
||||
const approvalSubmitted = await nextMessage(messages, ws, (msg) =>
|
||||
msg.type === 'system_message' &&
|
||||
msg.sessionId === codexAppSession.sessionId &&
|
||||
/本会话执行/.test(msg.message || '')
|
||||
);
|
||||
assert(/本会话执行/.test(approvalSubmitted.message || ''), 'Codex App should show approval confirmation hint');
|
||||
const approvalDelta = await nextMessage(messages, ws, (msg) => msg.type === 'text_delta' && msg.sessionId === codexAppSession.sessionId && /approval decision: acceptForSession/.test(msg.text || ''));
|
||||
assert(/approval decision: acceptForSession/.test(approvalDelta.text || ''), 'Codex App should continue after approval response');
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
||||
|
||||
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(150);
|
||||
|
||||
Reference in New Issue
Block a user