Files
Codeman/test/agent-codex-identity.test.ts
T
Aamer Akhter 2832a037d6 fix(agent): verify staged text on a live codex pane and refuse popups
Stage now re-reads the pane after typing (every 50 ms, up to 1 s) and
issues a receipt only for a frame with the same generation, the input
revision its own write moved, and a composer hash equal to the
message's. Codex repaints 30 to 60 ms after the keys, so the first
capture was often stale and stage refused with the text already typed.
Every refusal after typing now says the message may be in the composer
and points at the Ctrl+U recovery. A visible-frame capture reads the
screen and the caret in one tmux invocation, with the old two calls as
the fallback.

Codex draws its suggestion popups above the composer and keeps the
footer, so a staged /stat verified and submit ran /status. Stage now
refuses a leading / or ! and a last word starting with @ or $ before
typing, and stage and submit refuse a frame with popup rows just above
the composer. The pattern and characters are registry data
(composerPopupLine, composerCommandPrefixes, composerCompletionSigils),
read from the codex 0.162.0 bottom_pane sources.

Submit waits until 250 ms after the staged keys: codex turns an Enter
within 120 ms of a typed burst into a newline. POST .../input answers
409 with its seq returned while a guarded submit holds the input lease,
instead of 200 for bytes it never wrote.

Also align the attribution memo comment with the code (a verified file
whose incremental scan fails is not re-walked, since the full walk
reads a superset), and list stage as a fromUser source in CLAUDE.md.
2026-10-25 09:22:35 -04:00

254 lines
11 KiB
TypeScript

/** @fileoverview Strict bounded native rollout attribution without viewer heuristics. */
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { mkdtemp, mkdir, writeFile, rm, symlink } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import {
__clearCodexAttributionMemo,
findExactCodexConversation,
resolveEffectiveCodexHome,
} from '../src/agent-codex-identity.js';
const roots: string[] = [];
afterEach(async () => {
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
});
const A = '11111111-1111-4111-8111-111111111111';
const B = '22222222-2222-4222-8222-222222222222';
async function fixture() {
const root = await mkdtemp(join(tmpdir(), 'agent-attribution-'));
roots.push(root);
const home = join(root, 'home');
await mkdir(join(home, 'sessions'), { recursive: true });
const options = { codexHome: home, sessionId: 'managed', workingDir: root };
const put = async (id: string, payload: object = {}, text?: string) => {
const path = join(home, 'sessions', `rollout-${id}.jsonl`);
await writeFile(
path,
text ??
JSON.stringify({
type: 'session_meta',
payload: { id, cwd: root, originator: 'codeman_managed', ...payload },
}) + '\n'
);
return path;
};
return { root, home, options, put };
}
it('requires one complete native session_meta matching the rollout ID, managed originator and canonical workspace', async () => {
const f = await fixture();
const path = await f.put(A);
expect(await findExactCodexConversation(f.options)).toEqual({ id: A, path });
await f.put(A, { id: B });
expect(await findExactCodexConversation(f.options)).toBeUndefined();
});
it('never chooses newest cwd files or an ambiguous managed originator', async () => {
const f = await fixture();
await f.put(A, { originator: 'codex_cli' });
expect(await findExactCodexConversation(f.options)).toBeUndefined();
await f.put(A);
await f.put(B);
expect(await findExactCodexConversation(f.options)).toBeUndefined();
});
it('allows an explicit resume identity but still requires native metadata and the workspace', async () => {
const f = await fixture();
const path = await f.put(A, { originator: 'original-launch' });
expect(await findExactCodexConversation({ ...f.options, resumeSessionId: A })).toEqual({ id: A, path });
await f.put(B);
expect(await findExactCodexConversation({ ...f.options, resumeSessionId: A })).toBeUndefined();
await f.put(A, { cwd: f.home });
expect(await findExactCodexConversation({ ...f.options, resumeSessionId: A })).toBeUndefined();
});
it.each(['partial', 'duplicate', 'oversized'] as const)('refuses %s metadata', async (kind) => {
const f = await fixture();
const row = JSON.stringify({ type: 'session_meta', payload: { id: A, cwd: f.root, originator: 'codeman_managed' } });
await f.put(
A,
{},
kind === 'partial'
? row
: kind === 'duplicate'
? row + '\n' + row + '\n'
: row.slice(0, -2) + ',"instructions":"' + 'x'.repeat(140000) + '"}}\n'
);
expect(await findExactCodexConversation(f.options)).toBeUndefined();
});
it('enforces traversal/read caps and ignores symlinked directories', async () => {
const f = await fixture();
await f.put(A);
expect(await findExactCodexConversation(f.options, { maxEntries: 0, maxBytes: 1048576 })).toBeUndefined();
expect(await findExactCodexConversation(f.options, { maxEntries: 8192, maxBytes: 128 })).toBeUndefined();
await mkdir(join(f.root, 'elsewhere'));
await symlink(join(f.root, 'elsewhere'), join(f.home, 'sessions', 'alias'));
expect((await findExactCodexConversation(f.options))?.id).toBe(A);
});
it('resolves per-session, process and default CODEX_HOME relative to the session workspace', () => {
expect(resolveEffectiveCodexHome('custom', '/tmp/work', { CODEX_HOME: '/other' })).toBe('/tmp/work/custom');
expect(resolveEffectiveCodexHome(undefined, '/tmp/work', { CODEX_HOME: 'process' })).toBe('/tmp/work/process');
expect(resolveEffectiveCodexHome(undefined, '/tmp/work', { HOME: '/tmp/user' })).toBe('/tmp/user/.codex');
});
it.each([
['trailing-space', 'codex-home ', 'codex-home'],
['whitespace-only', ' ', '.codex'],
])(
'keeps the raw %s CODEX_HOME namespace through the Session getter and attribution helper',
async (_label, configured, wrongHome) => {
const { Session } = await import('../src/session.js');
const f = await fixture();
const expectedHome = join(f.root, configured);
const siblingHome = join(f.root, wrongHome);
const record =
JSON.stringify({ type: 'session_meta', payload: { id: A, cwd: f.root, originator: 'codeman_managed' } }) + '\n';
for (const home of [expectedHome, siblingHome]) {
await mkdir(join(home, 'sessions'), { recursive: true });
await writeFile(join(home, 'sessions', `rollout-${A}.jsonl`), record);
}
const session = new Session({
id: 'managed',
workingDir: f.root,
mode: 'codex',
envOverrides: { CODEX_HOME: configured },
});
try {
const codexHome = resolveEffectiveCodexHome(session.getCodexHomeOverride(), session.workingDir, { HOME: f.root });
expect(await findExactCodexConversation({ ...f.options, codexHome })).toEqual({
id: A,
path: join(expectedHome, 'sessions', `rollout-${A}.jsonl`),
});
expect(session.getCodexHomeOverride()).toBe(configured);
} finally {
await session.stop(false);
}
}
);
it('attributes an owned rollout among 320 unrelated full-size metadata prefixes', async () => {
const f = await fixture();
const path = await f.put(A);
await Promise.all(
Array.from({ length: 320 }, (_, n) => {
const id = `10000000-1111-4111-8111-${String(n).padStart(12, '0')}`;
return f.put(id, { originator: 'codex_cli', instructions: 'x'.repeat(5000) });
})
);
expect(await findExactCodexConversation(f.options)).toEqual({ id: A, path });
expect(
await findExactCodexConversation(f.options, { maxEntries: 8192, maxBytes: 1048576, maxPrefixBytes: 4096 })
).toBeUndefined();
});
it.each(['workspace', 'metadata'] as const)(
'probes %s bounded past the cap before canonical filesystem reads',
async (where) => {
const { vi } = await import('vitest');
const probe = await import('../src/utils/bounded-path-probe.js');
const f = await fixture();
const alias = join(f.root, 'workspace-alias');
await symlink(f.root, alias);
await f.put(A, { cwd: where === 'metadata' ? alias : f.root });
const blocked = where === 'workspace' ? f.root : alias;
const spy = vi
.spyOn(probe, 'probePathKind')
.mockImplementation(async (path) => (path === blocked ? 'unknown' : 'directory'));
try {
expect(await findExactCodexConversation(f.options)).toBeUndefined();
expect(spy).toHaveBeenCalledWith(blocked, { pastCap: true });
} finally {
spy.mockRestore();
}
}
);
describe('remembered attribution on a large, never-pruned store', () => {
beforeEach(() => __clearCodexAttributionMemo());
const OWNED = '33333333-3333-4333-8333-333333333333';
const NEWER = '44444444-4444-4444-8444-444444444444';
/** codex's own layout: sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl */
async function dated(f: Awaited<ReturnType<typeof fixture>>, day: string, id: string, payload: object = {}) {
const dir = join(f.home, 'sessions', ...day.split('/'));
await mkdir(dir, { recursive: true });
const path = join(dir, `rollout-${day.replaceAll('/', '-')}T00-00-00-${id}.jsonl`);
await writeFile(
path,
JSON.stringify({
type: 'session_meta',
payload: { id, cwd: f.root, originator: 'codeman_managed', ...payload },
}) + '\n'
);
return path;
}
/** `count` unrelated 19 KiB rollouts (a real session_meta size) spread over older days. */
async function unrelated(f: Awaited<ReturnType<typeof fixture>>, count: number) {
const instructions = 'x'.repeat(19 * 1024);
for (let start = 0; start < count; start += 200) {
await Promise.all(
Array.from({ length: Math.min(200, count - start) }, (_, k) => {
const n = start + k;
const day = `2026/${String(1 + (n % 9)).padStart(2, '0')}/${String(1 + (n % 28)).padStart(2, '0')}`;
return dated(f, day, `10000000-1111-4111-8111-${String(n).padStart(12, '0')}`, {
originator: 'codex_cli',
instructions,
});
})
);
}
}
const small = { maxEntries: 20, maxBytes: 1048576 };
it('attributes one owned rollout beside 2,100 unrelated full-size rollouts', async () => {
const f = await fixture();
await unrelated(f, 2100);
const path = await dated(f, '2026/10/24', OWNED);
expect(await findExactCodexConversation(f.options)).toEqual({ id: OWNED, path });
}, 60_000);
it('re-verifies only the remembered file on a later call instead of re-walking the store', async () => {
const f = await fixture();
await unrelated(f, 300);
const path = await dated(f, '2026/10/24', OWNED);
expect(await findExactCodexConversation(f.options)).toEqual({ id: OWNED, path });
// 20 entries cannot cover 300 rollouts in 9 months of day directories: only the
// remembered head plus the directories not older than it fit.
expect(await findExactCodexConversation(f.options, small)).toEqual({ id: OWNED, path });
__clearCodexAttributionMemo();
expect(await findExactCodexConversation(f.options, small)).toBeUndefined();
});
it('still catches a newer owned rollout (codex /new) after remembering one', async () => {
const f = await fixture();
await unrelated(f, 50);
const path = await dated(f, '2026/10/24', OWNED);
expect(await findExactCodexConversation(f.options)).toEqual({ id: OWNED, path });
await dated(f, '2026/10/25', NEWER);
expect(await findExactCodexConversation(f.options, small)).toBeUndefined();
expect(await findExactCodexConversation(f.options)).toBeUndefined();
});
it('answers unattributed, without a full walk, when the remembered file verifies but its incremental scan runs out', async () => {
const f = await fixture();
const path = await dated(f, '2026/10/24', OWNED);
expect(await findExactCodexConversation(f.options)).toEqual({ id: OWNED, path });
// 30 rollouts in the remembered day itself: the incremental scan reads them all,
// so 20 entries cannot cover it, and a full walk (a superset) could not either.
await Promise.all(
Array.from({ length: 30 }, (_, n) =>
dated(f, '2026/10/24', `20000000-2222-4222-8222-${String(n).padStart(12, '0')}`, { originator: 'codex_cli' })
)
);
expect(await findExactCodexConversation(f.options, small)).toBeUndefined();
// The memo was dropped, so a call with room walks the store and attributes again.
expect(await findExactCodexConversation(f.options)).toEqual({ id: OWNED, path });
});
it('drops the remembered file when it is replaced, and walks again', async () => {
const f = await fixture();
const path = await dated(f, '2026/10/24', OWNED);
expect(await findExactCodexConversation(f.options)).toEqual({ id: OWNED, path });
await rm(path);
await dated(f, '2026/10/24', OWNED, { originator: 'codex_cli' });
expect(await findExactCodexConversation(f.options)).toBeUndefined();
const moved = await dated(f, '2026/10/23', NEWER);
expect(await findExactCodexConversation(f.options)).toEqual({ id: NEWER, path: moved });
});
});