rigjs
Version:
A multi-repos dev tool based on yarn and git.Rigjs is intended to be the simplest way to develop,share and deliver codes between different developers or different projects.
134 lines (123 loc) • 8.41 kB
text/typescript
import fs from 'fs';
import os from 'os';
import path from 'path';
import { findLatestTranscript, inspectTranscript, intakeTranscript, iterateTranscript, readTranscriptPage } from './transcript';
describe('Claude transcript reader', () => {
let root: string;
let transcript: string;
beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'rig-handoff-transcript-'));
transcript = path.join(root, 'session.jsonl');
const rows = [
{ type: 'custom-title', customTitle: 'Ship feature', sessionId: 's1' },
{ type: 'user', uuid: 'u1', parentUuid: null, timestamp: '2026-07-15T00:00:00Z', cwd: '/repo', gitBranch: 'main', sessionId: 's1', version: '2.1.161', message: { role: 'user', content: 'Implement it' } },
{ type: 'assistant', uuid: 'a1', parentUuid: 'u1', timestamp: '2026-07-15T00:00:01Z', cwd: '/repo', sessionId: 's1', message: { id: 'm1', role: 'assistant', model: 'claude-test', usage: { input_tokens: 10 }, content: [
{ type: 'thinking', thinking: 'private reasoning', signature: 'secret' },
{ type: 'text', text: 'Working now' },
{ type: 'tool_use', id: 't1', name: 'Edit', input: { file_path: '/repo/a.ts', new_string: 'x'.repeat(400) } },
] } },
{ type: 'assistant', uuid: 'a1b', parentUuid: 'a1', timestamp: '2026-07-15T00:00:01.250Z', cwd: '/repo', sessionId: 's1', message: { id: 'm1', role: 'assistant', model: 'claude-test', content: [
{ type: 'tool_use', id: 't2', name: 'Read', input: { file_path: '/repo/b.ts' } },
] } },
{ type: 'user', uuid: 'old1', parentUuid: 'a1', timestamp: '2026-07-15T00:00:01.500Z', cwd: '/repo', sessionId: 's1', isCompactSummary: true, message: { role: 'user', content: 'Abandoned branch summary' } },
{ type: 'user', uuid: 'u2', parentUuid: 'a1', timestamp: '2026-07-15T00:00:02Z', cwd: '/repo', sessionId: 's1', toolUseResult: { filePath: '/repo/a.ts', oldString: 'old', newString: 'new' }, message: { role: 'user', content: [
{ type: 'tool_result', tool_use_id: 't1', content: 'updated' },
] } },
{ type: 'user', uuid: 'u2b', parentUuid: 'a1b', timestamp: '2026-07-15T00:00:02.500Z', cwd: '/repo', sessionId: 's1', sourceToolAssistantUUID: 'a1b', toolUseResult: { filePath: '/repo/b.ts', type: 'text' }, message: { role: 'user', content: [
{ type: 'tool_result', tool_use_id: 't2', content: 'parallel result' },
] } },
{ type: 'user', uuid: 'u3', parentUuid: 'u2', timestamp: '2026-07-15T00:00:03Z', cwd: '/repo', sessionId: 's1', isCompactSummary: true, message: { role: 'user', content: 'Earlier context summary' } },
{ type: 'last-prompt', sessionId: 's1', lastPrompt: 'Continue publishing', leafUuid: 'u3' },
];
fs.writeFileSync(transcript, rows.map(row => JSON.stringify(row)).join('\n') + '\n');
});
afterEach(() => fs.rmSync(root, { recursive: true, force: true }));
test('streams lines and builds a useful manifest', () => {
expect(Array.from(iterateTranscript(transcript))).toHaveLength(9);
const manifest = inspectTranscript(transcript, { recent: 8, maxChars: 256 });
expect(manifest.lines).toBe(9);
expect(manifest.session).toEqual(expect.objectContaining({
sessionId: 's1',
cwd: '/repo',
title: 'Ship feature',
models: ['claude-test'],
}));
expect(manifest.tools).toEqual({ Edit: 1, Read: 1 });
expect(manifest.lastPrompt).toBe('Continue publishing');
expect(manifest.compactSummaries).toHaveLength(2);
expect(manifest.compactSummaries.find((entry: any) => entry.uuid === 'old1').branch).toBe('non-lineage');
expect(manifest.compactSummaries.find((entry: any) => entry.uuid === 'u3').branch).toBe('active');
expect(manifest.branch).toEqual(expect.objectContaining({ activeUuids: 6, nonLineageUuids: 1 }));
expect(manifest.recent.find((entry: any) => entry.uuid === 'old1'))
.toEqual(expect.objectContaining({ branch: 'non-lineage' }));
expect(manifest.recent.find((entry: any) => entry.uuid === 'u2b'))
.toEqual(expect.objectContaining({ branch: 'active' }));
});
test('pages by raw JSONL line and omits private thinking', () => {
const first = readTranscriptPage(transcript, { from: 1, limit: 3, maxChars: 256 });
expect(first.hasMore).toBe(true);
expect(first.nextLine).toBe(4);
expect(first.entries).toHaveLength(3);
const assistant = first.entries[2];
expect(assistant.content[0]).toEqual(expect.objectContaining({ type: 'thinking', omitted: true }));
expect(JSON.stringify(assistant)).not.toContain('private reasoning');
expect(JSON.stringify(assistant)).not.toContain('secret');
expect(JSON.stringify(assistant)).toContain('truncated');
const second = readTranscriptPage(transcript, { from: first.nextLine, limit: 20 });
expect(second.hasMore).toBe(false);
expect(second.entries.find((entry: any) => entry.uuid === 'u2').toolUseResult)
.toEqual(expect.objectContaining({ filePath: '/repo/a.ts', oldString: 'old' }));
expect(second.entries.find((entry: any) => entry.uuid === 'old1').branch).toBe('non-lineage');
expect(second.entries.find((entry: any) => entry.uuid === 'u2b').branch).toBe('active');
expect(second.entries.find((entry: any) => entry.uuid === 'u3').isCompactSummary).toBe(true);
});
test('intakes meaningful evidence newest-first without thinking or telemetry noise', () => {
fs.appendFileSync(transcript, [
{ type: 'user', uuid: 'control-u', parentUuid: 'u3', message: { role: 'user', content: [{ type: 'text', text: 'Continue from where you left off.' }] } },
{ type: 'assistant', uuid: 'control-a', parentUuid: 'control-u', message: { role: 'assistant', content: [{ type: 'text', text: 'No response requested.' }] } },
{ type: 'system', uuid: 'control-s', parentUuid: 'control-a', subtype: 'informational', level: 'warning', content: 'UserPromptExpansion operation blocked by hook' },
].map(row => JSON.stringify(row)).join('\n') + '\n');
const first = intakeTranscript(transcript, { limit: 2, maxChars: 256 });
expect(first.schemaVersion).toBe(2);
expect(first.lastPrompt).toBe('Continue publishing');
expect(first.checkpoint).toEqual(expect.objectContaining({
uuid: 'u3',
branch: 'active',
summary: 'Earlier context summary',
}));
expect(first.stats).toEqual(expect.objectContaining({
recoverableEntries: 5,
compactSummaries: 2,
omittedThinkingBlocks: 1,
}));
expect(first.page.direction).toBe('newest-to-oldest');
expect(first.page.entries.map((entry: any) => entry.uuid)).toEqual(['u2b', 'u2']);
expect(first.page.nextBeforeLine).toBe(6);
expect(first.branch.leafUuid).toBe('control-s');
expect(JSON.stringify(first)).not.toContain('private reasoning');
expect(JSON.stringify(first)).not.toContain('input_tokens');
expect(JSON.stringify(first)).not.toContain('No response requested');
expect(JSON.stringify(first)).not.toContain('operation blocked by hook');
expect(first.workspaceEvidence.editedFiles).toContainEqual({ path: '/repo/a.ts', line: 6 });
const second = intakeTranscript(transcript, { before: first.page.nextBeforeLine, limit: 2, maxChars: 256 });
expect(second.page.entries.map((entry: any) => entry.uuid)).toEqual(['a1b', 'a1']);
expect(second.page.nextBeforeLine).toBe(3);
const assistant = second.page.entries.find((entry: any) => entry.uuid === 'a1');
expect(assistant.content.map((block: any) => block.type)).toEqual(['text', 'tool_use']);
expect(assistant.model).toBeUndefined();
expect(assistant.usage).toBeUndefined();
const third = intakeTranscript(transcript, { before: second.page.nextBeforeLine, limit: 2 });
expect(third.page.entries.map((entry: any) => entry.uuid)).toEqual(['u1']);
expect(third.page.hasOlder).toBe(false);
expect(third.page.nextBeforeLine).toBeNull();
});
test('finds the latest transcript and filters by cwd', () => {
const other = path.join(root, 'other.jsonl');
fs.writeFileSync(other, JSON.stringify({ type: 'user', cwd: '/other', message: { content: 'x' } }) + '\n');
const future = new Date(Date.now() + 10000);
fs.utimesSync(other, future, future);
expect(findLatestTranscript(root)).toBe(other);
expect(findLatestTranscript(root, '/repo')).toBe(transcript);
expect(findLatestTranscript(root, '/missing')).toBeNull();
});
});