Files
Codeman/test/agent-inspection.test.ts
T

181 lines
8.4 KiB
TypeScript

/** @fileoverview Bounded JSONL inspection: cursors, byte budgets and file generations. */
import { afterEach, beforeEach, expect, it } from 'vitest';
import { mkdtemp, writeFile, appendFile, rename, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { InspectionReader } from '../src/agent-inspection.js';
let dir: string;
let file: string;
let reader: InspectionReader;
const scope = { owner: 'alice', sessionId: 's', generation: 'pane-1', conversationId: 'thread-1' };
beforeEach(async () => {
dir = await mkdtemp(join(tmpdir(), 'inspect-'));
file = join(dir, 'rollout.jsonl');
reader = new InspectionReader();
});
afterEach(async () => {
await rm(dir, { recursive: true, force: true });
});
it('returns complete UTF8 records within the byte budget and resumes exactly', async () => {
const row = JSON.stringify({ text: 'é😀' }) + '\n';
await writeFile(file, row + row);
const first = await reader.read(file, scope, Buffer.byteLength(row));
expect(first.evidence).toBe(row);
expect(first.truncated).toBe(true);
await appendFile(file, row);
const next = await reader.read(file, scope, 1024, first.cursor);
expect(next.evidence).toBe(row);
expect(next.truncated).toBe(false);
});
it('does not consume partial final records', async () => {
await writeFile(file, '{"text":"hello');
const first = await reader.read(file, scope, 100);
expect(first.evidence).toBe('');
await appendFile(file, '"}\n');
const next = await reader.read(file, scope, 100, first.cursor);
expect(next.evidence).toBe('{"text":"hello"}\n');
});
it('reports an oversized incremental record without splitting it or advancing past it', async () => {
await writeFile(file, '{}\n');
const initial = await reader.read(file, scope, 100);
await appendFile(file, JSON.stringify({ text: 'x'.repeat(300000) }) + '\n');
const result = await reader.read(file, scope, 100, initial.cursor);
expect(result.evidence).toBe('');
expect(result.truncated).toBe(true);
expect(result.blockedReason).toBe('record-exceeds-scan-limit');
expect(result.scannedBytes).toBeLessThanOrEqual(262144);
});
it('rejects malformed, cross-owner and changed-session cursors', async () => {
await writeFile(file, '{}\n');
const first = await reader.read(file, scope, 100);
for (const changed of [
{ ...scope, owner: 'bob' },
{ ...scope, sessionId: 'other' },
{ ...scope, generation: 'new' },
{ ...scope, conversationId: 'new' },
]) {
await expect(reader.read(file, changed, 100, first.cursor)).rejects.toThrow(/cursor/i);
}
await expect(reader.read(file, scope, 100, 'garbage')).rejects.toThrow(/cursor/i);
});
it('rejects truncation, replacement and rewritten consumed evidence', async () => {
await writeFile(file, '{}\n{}\n');
const first = await reader.read(file, scope, 100);
await writeFile(file, '{}\n');
await expect(reader.read(file, scope, 100, first.cursor)).rejects.toThrow(/cursor/i);
await writeFile(file, '{}\n{}\n');
const second = await reader.read(file, scope, 100);
await writeFile(file, '[]\n[]\n');
await expect(reader.read(file, scope, 100, second.cursor)).rejects.toThrow(/cursor/i);
const third = await reader.read(file, scope, 100);
await rename(file, join(dir, 'old'));
await writeFile(file, '[]\n[]\n');
await expect(reader.read(file, scope, 100, third.cursor)).rejects.toThrow(/cursor/i);
});
it('expires cursors and bounds retained state', async () => {
await writeFile(file, '{}\n');
let now = 0;
reader = new InspectionReader(() => now, 2, 1000);
const first = await reader.read(file, scope, 100);
await reader.read(file, scope, 100);
await reader.read(file, scope, 100);
await expect(reader.read(file, scope, 100, first.cursor)).rejects.toThrow(/cursor/i);
const last = await reader.read(file, scope, 100);
now = 1001;
await expect(reader.read(file, scope, 100, last.cursor)).rejects.toThrow(/cursor/i);
});
it('bounds strict attribution itself and refuses an incomplete inventory', async () => {
const { mkdir } = await import('node:fs/promises');
const { findExactCodexConversation } = await import('../src/agent-codex-identity.js');
const home = join(dir, 'codex');
const sessions = join(home, 'sessions');
await mkdir(sessions, { recursive: true });
const id = '11111111-1111-4111-8111-111111111111';
await writeFile(
join(sessions, `rollout-${id}.jsonl`),
JSON.stringify({ type: 'session_meta', payload: { id, cwd: dir, originator: 'codeman_s' } }) + '\n'
);
await writeFile(join(sessions, 'other.txt'), 'other');
const options = { codexHome: home, sessionId: 's', workingDir: dir };
expect((await findExactCodexConversation(options))?.id).toBe(id);
expect(await findExactCodexConversation(options, { maxEntries: 1, maxBytes: 16384 })).toBeUndefined();
});
it('starts at recent complete records past oversized bootstrap evidence, labeling all omission', async () => {
const old = JSON.stringify({ system: 'x'.repeat(300000) }) + '\n';
const latest = JSON.stringify({ assistant: 'latest 😀' }) + '\n';
await writeFile(file, old + latest);
const result = await reader.read(file, scope, Buffer.byteLength(latest));
expect(result.evidence).toBe(latest);
expect(result.omittedBeforeBytes).toBe(Buffer.byteLength(old));
expect(result.truncated).toBe(true);
await appendFile(file, '{"assistant":"next"}\n');
expect((await reader.read(file, scope, 100, result.cursor)).evidence).toBe('{"assistant":"next"}\n');
});
it('attributes a real-sized session_meta row rather than requiring a tiny header', async () => {
const { mkdir } = await import('node:fs/promises');
const { findExactCodexConversation } = await import('../src/agent-codex-identity.js');
const home = join(dir, 'codex');
await mkdir(join(home, 'sessions'), { recursive: true });
const id = '11111111-1111-4111-8111-111111111111';
await writeFile(
join(home, 'sessions', `rollout-${id}.jsonl`),
JSON.stringify({
type: 'session_meta',
payload: { id, cwd: dir, originator: 'codeman_s', instructions: 'x'.repeat(20000) },
}) + '\n'
);
expect(
(
await findExactCodexConversation(
{ codexHome: home, sessionId: 's', workingDir: dir },
{ maxEntries: 8192, maxBytes: 1048576 }
)
)?.id
).toBe(id);
});
it('rejects same-size rewrites outside the bounded cursor anchors', async () => {
const { utimes } = await import('node:fs/promises');
const row = JSON.stringify({ text: 'a'.repeat(20000) }) + '\n';
await writeFile(file, row);
await utimes(file, 1, 1);
const first = await reader.read(file, scope, 30000);
await writeFile(file, row.slice(0, 10000) + 'b' + row.slice(10001));
await expect(reader.read(file, scope, 30000, first.cursor)).rejects.toThrow(/cursor/i);
});
it('keeps cursor capacity bounded across concurrent reads', async () => {
await writeFile(file, '{}\n');
reader = new InspectionReader(Date.now, 2);
const results = await Promise.all(Array.from({ length: 16 }, () => reader.read(file, scope, 100)));
const resumed = await Promise.allSettled(results.map((result) => reader.read(file, scope, 100, result.cursor)));
expect(resumed.filter((result) => result.status === 'fulfilled').length).toBeLessThanOrEqual(2);
});
it('keeps an incomplete UTF8 sequence pending until its JSONL record is complete', async () => {
const row = Buffer.from('{"text":"😀"}\n');
const split = row.indexOf(Buffer.from('😀')) + 2;
await writeFile(file, row.subarray(0, split));
const first = await reader.read(file, scope, 100);
expect(first.evidence).toBe('');
await appendFile(file, row.subarray(split));
expect((await reader.read(file, scope, 100, first.cursor)).evidence).toBe(row.toString('utf8'));
});
it('treats a full incremental page as pageable rather than a blocked oversized record', async () => {
await writeFile(file, '{}\n');
const initial = await reader.read(file, scope, 100);
await appendFile(file, '{}\n{}\n');
const page = await reader.read(file, scope, 3, initial.cursor);
expect(page.evidence).toBe('{}\n');
expect(page.truncated).toBe(true);
expect(page.blockedReason).toBeUndefined();
const next = await reader.read(file, scope, 3, page.cursor);
expect(next.evidence).toBe('{}\n');
});
it('still labels the first selected oversized tail record after omitted older bytes', async () => {
await writeFile(file, '{}\n' + JSON.stringify({ text: 'x'.repeat(100) }) + '\n');
const page = await reader.read(file, scope, 10);
expect(page.evidence).toBe('');
expect(page.blockedReason).toBe('record-exceeds-output-budget');
});