mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(codex): align local transcript message projection
Use semantic events as the visible text source and preserve completed plans as ordered plan proposal cards.
This commit is contained in:
@@ -459,7 +459,7 @@ describe('codexLocalLauncher', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('replays existing response_item chat messages when importing a Codex thread into a new Hapi session', async () => {
|
||||
it('replays semantic chat events once and keeps a same-turn preface before its plan', async () => {
|
||||
const transcriptPath = join(tempDir, 'codex-import-response-item-transcript.jsonl');
|
||||
const { session, userMessages, agentMessages, getUserActivityCount } = createSessionStub('default', undefined, '/tmp/worktree', null, true);
|
||||
let releaseRunBarrier: (() => void) | undefined;
|
||||
@@ -476,7 +476,23 @@ describe('codexLocalLauncher', () => {
|
||||
payload: {
|
||||
type: 'message',
|
||||
role: 'user',
|
||||
content: [{ type: 'input_text', text: 'old response_item user message' }]
|
||||
content: [{ type: 'input_text', text: 'visible user message' }]
|
||||
}
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'event_msg',
|
||||
payload: { type: 'user_message', message: 'visible user message' }
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'event_msg',
|
||||
payload: {
|
||||
type: 'item_completed',
|
||||
turn_id: 'turn-with-preface',
|
||||
item: {
|
||||
type: 'Plan',
|
||||
id: 'plan-1',
|
||||
text: '## Proposed plan\n\n1. Inspect\n2. Implement'
|
||||
}
|
||||
}
|
||||
}),
|
||||
JSON.stringify({
|
||||
@@ -484,15 +500,31 @@ describe('codexLocalLauncher', () => {
|
||||
payload: {
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [{ type: 'output_text', text: 'old response_item assistant message' }]
|
||||
content: [{
|
||||
type: 'output_text',
|
||||
text: 'visible assistant preface\n\n<proposed_plan>## Proposed plan\n\n1. Inspect\n2. Implement</proposed_plan>'
|
||||
}],
|
||||
internal_chat_message_metadata_passthrough: { turn_id: 'turn-with-preface' }
|
||||
}
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'event_msg',
|
||||
payload: {
|
||||
type: 'agent_message',
|
||||
message: 'visible assistant preface',
|
||||
phase: 'final_answer'
|
||||
}
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'event_msg',
|
||||
payload: { type: 'task_complete', turn_id: 'turn-with-preface' }
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'response_item',
|
||||
payload: {
|
||||
type: 'message',
|
||||
role: 'user',
|
||||
content: [{ type: 'input_image', image_url: 'data:image/png;base64,abc' }]
|
||||
content: [{ type: 'input_text', text: '<environment_context>hidden context</environment_context>' }]
|
||||
}
|
||||
})
|
||||
].join('\n') + '\n'
|
||||
@@ -511,13 +543,87 @@ describe('codexLocalLauncher', () => {
|
||||
}
|
||||
await launcherPromise;
|
||||
|
||||
expect(userMessages).toContain('old response_item user message');
|
||||
expect(getUserActivityCount()).toBe(1);
|
||||
expect(agentMessages).toContainEqual({
|
||||
expect(userMessages).toEqual(['visible user message']);
|
||||
expect(getUserActivityCount()).toBe(0);
|
||||
expect(agentMessages).toEqual([{
|
||||
type: 'message',
|
||||
message: 'old response_item assistant message',
|
||||
message: 'visible assistant preface',
|
||||
id: expect.any(String)
|
||||
}, {
|
||||
type: 'tool-call',
|
||||
name: 'ExitPlanMode',
|
||||
callId: 'codex-proposed-plan:plan-1',
|
||||
input: { plan: '## Proposed plan\n\n1. Inspect\n2. Implement' },
|
||||
id: 'plan-1'
|
||||
}, {
|
||||
type: 'tool-call-result',
|
||||
callId: 'codex-proposed-plan:plan-1',
|
||||
output: null,
|
||||
id: 'plan-1:result'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('replays a plan-only turn when the turn completes', async () => {
|
||||
const transcriptPath = join(tempDir, 'codex-import-plan-only-transcript.jsonl');
|
||||
const { session, agentMessages } = createSessionStub('default', undefined, '/tmp/worktree', null, true);
|
||||
let releaseRunBarrier: (() => void) | undefined;
|
||||
harness.runBarrier = new Promise((resolve) => {
|
||||
releaseRunBarrier = resolve;
|
||||
});
|
||||
|
||||
await writeFile(
|
||||
transcriptPath,
|
||||
[
|
||||
JSON.stringify({ type: 'session_meta', payload: { id: 'codex-thread-plan-only' } }),
|
||||
JSON.stringify({
|
||||
type: 'event_msg',
|
||||
payload: {
|
||||
type: 'item_completed',
|
||||
turn_id: 'turn-plan-only',
|
||||
item: { type: 'Plan', id: 'plan-only', text: '## Plan only' }
|
||||
}
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'response_item',
|
||||
payload: {
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [{ type: 'output_text', text: '<proposed_plan>## Plan only</proposed_plan>' }],
|
||||
internal_chat_message_metadata_passthrough: { turn_id: 'turn-plan-only' }
|
||||
}
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'event_msg',
|
||||
payload: { type: 'task_complete', turn_id: 'turn-plan-only' }
|
||||
})
|
||||
].join('\n') + '\n'
|
||||
);
|
||||
|
||||
const launcherPromise = codexLocalLauncher(session as never);
|
||||
await wait(50);
|
||||
|
||||
harness.sessionHookHandlers[0]?.('codex-thread-plan-only', {
|
||||
transcript_path: transcriptPath
|
||||
});
|
||||
await wait(300);
|
||||
|
||||
if (releaseRunBarrier) {
|
||||
releaseRunBarrier();
|
||||
}
|
||||
await launcherPromise;
|
||||
|
||||
expect(agentMessages).toEqual([{
|
||||
type: 'tool-call',
|
||||
name: 'ExitPlanMode',
|
||||
callId: 'codex-proposed-plan:plan-only',
|
||||
input: { plan: '## Plan only' },
|
||||
id: 'plan-only'
|
||||
}, {
|
||||
type: 'tool-call-result',
|
||||
callId: 'codex-proposed-plan:plan-only',
|
||||
output: null,
|
||||
id: 'plan-only:result'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('does not let a later non-clear hook replace the primary session', async () => {
|
||||
|
||||
Reference in New Issue
Block a user