mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
refactor(codex): share the rollout format between the two Codex history readers
The Past Sessions scanner and the offline history reader each parsed Codex rollouts with their own shape helpers, their own CODEX_HOME resolution and different injected-context rules. Move the store location, the three message shapes and the injected-context rule into codex-rollout-format.ts. The scanner's behaviour is unchanged; the offline reader adopts its rule in the next commit.
This commit is contained in:
1 parent
e8ea8b0715
commit
803e7c7f85
3 files changed
+256
-53
No files matched your search
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* @fileoverview The Codex rollout shape helpers shared by the Past Sessions
|
||||
* scanner (codex-transcript.ts) and the offline history reader
|
||||
* (local-history.ts): store location, message shapes and the injected-context
|
||||
* rule. Both readers' own tests cover their use of these; this pins the shared
|
||||
* contract itself.
|
||||
*/
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
codexContentText,
|
||||
codexHomeDir,
|
||||
codexMessageFromLine,
|
||||
codexSessionsRoot,
|
||||
isCodexInjectedContext,
|
||||
} from '../src/codex-rollout-format.js';
|
||||
|
||||
const savedHome = process.env.CODEX_HOME;
|
||||
afterEach(() => {
|
||||
if (savedHome === undefined) delete process.env.CODEX_HOME;
|
||||
else process.env.CODEX_HOME = savedHome;
|
||||
});
|
||||
|
||||
describe('codex rollout format', () => {
|
||||
it('resolves the store from CODEX_HOME, else ~/.codex', () => {
|
||||
process.env.CODEX_HOME = '/custom/codex';
|
||||
expect(codexHomeDir()).toBe('/custom/codex');
|
||||
expect(codexSessionsRoot()).toBe(join('/custom/codex', 'sessions'));
|
||||
process.env.CODEX_HOME = '';
|
||||
expect(codexSessionsRoot()).toBe(join(process.env.HOME!, '.codex', 'sessions'));
|
||||
});
|
||||
|
||||
it('reads all three message shapes with their role, shape and phase', () => {
|
||||
expect(codexMessageFromLine({ type: 'event_msg', payload: { type: 'user_message', message: 'hi' } })).toEqual({
|
||||
role: 'user',
|
||||
text: 'hi',
|
||||
shape: 'event',
|
||||
phase: undefined,
|
||||
injectionProne: false,
|
||||
});
|
||||
expect(
|
||||
codexMessageFromLine({
|
||||
type: 'event_msg',
|
||||
payload: { type: 'agent_message', message: 'ok', phase: 'commentary' },
|
||||
})
|
||||
).toMatchObject({ role: 'assistant', shape: 'event', phase: 'commentary', injectionProne: false });
|
||||
expect(
|
||||
codexMessageFromLine({
|
||||
type: 'event_msg',
|
||||
payload: {
|
||||
type: 'item_completed',
|
||||
item: { type: 'AgentMessage', phase: 'final_answer', content: [{ type: 'Text', text: 'done' }] },
|
||||
},
|
||||
})
|
||||
).toMatchObject({ role: 'assistant', text: 'done', shape: 'item', phase: 'final_answer' });
|
||||
expect(
|
||||
codexMessageFromLine({
|
||||
type: 'response_item',
|
||||
payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'typed' }] },
|
||||
})
|
||||
).toMatchObject({ role: 'user', text: 'typed', shape: 'response', injectionProne: true });
|
||||
expect(
|
||||
codexMessageFromLine({
|
||||
type: 'response_item',
|
||||
payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'a' }] },
|
||||
})
|
||||
).toMatchObject({ role: 'assistant', shape: 'response', injectionProne: false });
|
||||
});
|
||||
|
||||
it('ignores every other line, empty text and malformed input', () => {
|
||||
for (const value of [
|
||||
null,
|
||||
42,
|
||||
'text',
|
||||
[],
|
||||
{ type: 'session_meta', payload: { id: 'x' } },
|
||||
{ type: 'response_item', payload: { type: 'message', role: 'developer', content: 'policy' } },
|
||||
{ type: 'response_item', payload: { type: 'reasoning', summary: [] } },
|
||||
{ type: 'event_msg', payload: { type: 'user_message', message: '' } },
|
||||
{ type: 'event_msg', payload: { type: 'item_completed', item: { type: 'CommandExecution' } } },
|
||||
])
|
||||
expect(codexMessageFromLine(value)).toBeNull();
|
||||
});
|
||||
|
||||
it('flattens only blocks that carry text', () => {
|
||||
expect(codexContentText('plain')).toBe('plain');
|
||||
expect(
|
||||
codexContentText([
|
||||
{ type: 'input_text', text: 'a' },
|
||||
{ type: 'input_image', image_url: 'x' },
|
||||
{ text: 'b' },
|
||||
null,
|
||||
])
|
||||
).toBe('a\nb');
|
||||
expect(codexContentText(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('treats a heading or a tag as injected context, wherever the wrapper name is new', () => {
|
||||
for (const text of [
|
||||
'# AGENTS.md instructions for /repo',
|
||||
'<environment_context>',
|
||||
'<recommended_plugins>',
|
||||
'<subagent_notification>',
|
||||
'<codex_internal_context source="goal">',
|
||||
'<image name=[Image #1]>',
|
||||
' <turn_aborted>',
|
||||
])
|
||||
expect(isCodexInjectedContext(text)).toBe(true);
|
||||
for (const text of ['fix the bug', 'You are a reviewer', '$skill run', 'a <b> tag later'])
|
||||
expect(isCodexInjectedContext(text)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user