Files
Codeman/test/edit-history-local-source.test.ts
T
RandalixandClaude Opus 5.5 de43657676 fix(edit-history): bounded image probe, linear path scan (review of #693)
- probeLocalPaths: every agent-printed candidate goes through
  probePathKind() first (no pastCap, bulk scan), sequentially; only a path
  that answered 'file' gets realpath + stat. A path on a mount that stopped
  answering no longer hangs the sync, a queued sync or remove().
- PATH_TOKEN_RE: branch 4 starts only at a token boundary (`:` excluded so
  URLs stay ignored), branch 1 stops at `:`. Linear in the line length
  (30,000 `#` took ~3 s before).
- Load older yields to the event loop between the files it parses.
- GET /edits answers unsupported without loading a store.
- CLAUDE.md paragraph trimmed to the rules; probe and regex rules added to
  architecture-invariants#edit-history-panel.

Tests: stall test through the real sync and probe with a stat that never
settles; time-budget tests with 30,000-character inputs and a 139-result
base64 chunk; route test asserts no store load for unsupported sessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 09:46:23 +01:00

166 lines
7.3 KiB
TypeScript

/**
* Local edit-source reader against a temp projects dir: the transcript lookup, byte-offset
* reads of the main transcript and its subagents, and the image probe.
*/
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { mkdirSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
findClaudeTranscript,
probeLocalPaths,
readClaudeEditSources,
resolveClaudeTranscript,
} from '../src/edit-history-local-source.js';
let root: string;
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'edit-src-'));
});
afterEach(() => {
rmSync(root, { recursive: true, force: true });
});
function put(rel: string, content: string | Buffer, mtimeSec?: number): string {
const path = join(root, rel);
mkdirSync(join(path, '..'), { recursive: true });
writeFileSync(path, content);
if (mtimeSec) utimesSync(path, mtimeSec, mtimeSec);
return path;
}
describe('findClaudeTranscript', () => {
it('returns null when no transcript exists under any id', async () => {
put('p/other.jsonl', 'x\n');
expect(await findClaudeTranscript(['sid'], root)).toBeNull();
expect(await findClaudeTranscript(['sid'], join(root, 'missing'))).toBeNull();
});
it('takes the first id that has a transcript, the newest one across project dirs', async () => {
put('a/sid.jsonl', 'old\n', 1000);
const newest = put('b/sid.jsonl', 'new\n', 2000);
put('c/second.jsonl', 'x\n');
expect(await findClaudeTranscript(['bad id!', 'sid', 'second'], root)).toBe(newest);
});
});
describe('resolveClaudeTranscript', () => {
it('takes the watcher path (what the hooks reported) as is', async () => {
expect(await resolveClaudeTranscript('/hooked/conv.jsonl', ['sid'], join(root, 'cfg'))).toBe('/hooked/conv.jsonl');
});
it("without one, looks under the session's own claude config dir", async () => {
const path = put('cfg/projects/p/sid.jsonl', 'x\n');
expect(await resolveClaudeTranscript(null, ['sid'], join(root, 'cfg'))).toBe(path);
expect(await resolveClaudeTranscript(undefined, ['other'], join(root, 'cfg'))).toBeNull();
});
});
describe('readClaudeEditSources', () => {
it('returns null for a missing file or a path that is not an absolute <id>.jsonl', async () => {
const path = put('p/sid.jsonl', 'x\n');
expect(await readClaudeEditSources(join(root, 'p/gone.jsonl'), [], 100)).toBeNull();
expect(await readClaudeEditSources('p/sid.jsonl', [], 100)).toBeNull();
expect(await readClaudeEditSources(path.replace('.jsonl', '.txt'), [], 100)).toBeNull();
expect(await readClaudeEditSources(put('p/bad id.jsonl', 'x\n'), [], 100)).toBeNull();
});
it('names the conversation after the file', async () => {
const res = await readClaudeEditSources(put('p/conv-1.jsonl', 'x\n'), [], 100);
expect(res?.transcriptId).toBe('conv-1');
});
it('reads a new file from its end within the budget, a requested one from its offset', async () => {
const path = put('p/sid.jsonl', '0123456789');
const fresh = await readClaudeEditSources(path, [], 4);
expect(fresh?.files).toEqual([{ relPath: 'main', size: 10, startOffset: 6, chunk: Buffer.from('6789') }]);
const cont = await readClaudeEditSources(path, [{ relPath: 'main', offset: 3, len: 2 }], 100);
expect(cont?.files[0]).toMatchObject({ startOffset: 3, chunk: Buffer.from('34') });
});
it('clamps an offset past the end to the size (a replaced, shorter file)', async () => {
const path = put('p/sid.jsonl', 'abc');
const res = await readClaudeEditSources(path, [{ relPath: 'main', offset: 50 }], 100);
expect(res?.files[0]).toMatchObject({ size: 3, startOffset: 3, chunk: Buffer.alloc(0) });
});
it('lists subagents with their meta for new ones only, and shares the budget main first', async () => {
const path = put('p/sid.jsonl', 'MAIN');
put('p/sid/subagents/agent-a1.jsonl', 'AAAA');
put('p/sid/subagents/agent-a1.meta.json', '{"description":"Fix it"}');
put('p/sid/subagents/agent-b2.jsonl', 'BBBB');
put('p/sid/subagents/agent-b2.meta.json', '{"description":"Other"}');
put('p/sid/subagents/notes.txt', 'ignored');
const res = await readClaudeEditSources(path, [{ relPath: 'subagents/agent-b2.jsonl', offset: 0 }], 6);
expect(res?.files.map((f) => [f.relPath, f.startOffset, f.chunk.toString()])).toEqual([
['main', 0, 'MAIN'],
['subagents/agent-a1.jsonl', 2, 'AA'],
['subagents/agent-b2.jsonl', 0, ''],
]);
expect(res?.metas).toEqual({ a1: '{"description":"Fix it"}' });
});
it('ignores invalid requests', async () => {
const path = put('p/sid.jsonl', 'abcdef');
const res = await readClaudeEditSources(
path,
[
{ relPath: '../etc/passwd', offset: 0 },
{ relPath: 'main', offset: -1 },
],
3
);
// The invalid main request is dropped, so main is read like a new file.
expect(res?.files[0]).toMatchObject({ startOffset: 3, chunk: Buffer.from('def') });
});
it('keeps multibyte bytes intact (byte offsets, not string lengths)', async () => {
const path = put('p/sid.jsonl', Buffer.from('äöü\n', 'utf8'));
const res = await readClaudeEditSources(path, [{ relPath: 'main', offset: 2 }], 100);
expect(res?.files[0].chunk).toEqual(Buffer.from('äöü\n', 'utf8').subarray(2));
});
it('with onlyRequested, reads the requested files and nothing else', async () => {
const path = put('p/sid.jsonl', 'MAIN');
put('p/sid/subagents/agent-a1.jsonl', 'AAAA');
put('p/sid/subagents/agent-b2.jsonl', 'BBBB');
const res = await readClaudeEditSources(path, [{ relPath: 'subagents/agent-b2.jsonl', offset: 1, len: 2 }], 100, {
onlyRequested: true,
});
expect(res?.files.map((f) => [f.relPath, f.startOffset, f.chunk.toString()])).toEqual([
['subagents/agent-b2.jsonl', 1, 'BB'],
]);
});
it('reads the end of a block that does not fit the budget', async () => {
const path = put('p/sid.jsonl', '0123456789');
const res = await readClaudeEditSources(path, [{ relPath: 'main', offset: 2, len: 6 }], 3);
expect(res?.files[0]).toMatchObject({ startOffset: 5, chunk: Buffer.from('567') });
});
});
describe('probeLocalPaths', () => {
it('reports size and mtime of files, expands ~/ against home, null for missing and directories', async () => {
put('home/img.png', 'PNG!', 1_700_000_000);
mkdirSync(join(root, 'dir'));
const res = await probeLocalPaths(['~/img.png', join(root, 'dir'), join(root, 'nope.png')], join(root, 'home'));
expect(res[0]).toEqual({ realPath: join(root, 'home/img.png'), kind: 'file', size: 4, mtimeMs: 1_700_000_000_000 });
expect(res[1]).toBeNull();
expect(res[2]).toBeNull();
});
it('touches a path only after the bounded probe saw a file there', async () => {
put('a.png', 'PNG!', 1_700_000_000);
put('b.png', 'PNG!', 1_700_000_000);
const asked: string[] = [];
const res = await probeLocalPaths([join(root, 'a.png'), join(root, 'b.png'), 'rel.png'], root, async (p) => {
asked.push(p);
return p.endsWith('a.png') ? 'unknown' : 'file';
});
// a.png exists, but its filesystem did not answer the bounded probe: no unbounded stat.
expect(res.map((r) => r?.realPath ?? null)).toEqual([null, join(root, 'b.png'), null]);
expect(asked).toEqual([join(root, 'a.png'), join(root, 'b.png')]); // relative: never probed
});
});