import { Buffer } from 'node:buffer' import { randomBytes } from 'node:crypto' import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { formatByteSize, HANDOFF_ATTACHMENT_MAX_BYTES, HandoffArchiveTooLargeError, packHandoff } from './handoffArchive' import { ensureHandoffFits, HandoffSourcesTooLargeError, measureHandoffSources } from './handoffSize' const tempDirs: string[] = [] afterEach(() => { while (tempDirs.length > 0) { const dir = tempDirs.pop() if (dir) rmSync(dir, { recursive: true, force: true }) } }) function tempDir(prefix: string): string { const dir = mkdtempSync(path.join(os.tmpdir(), prefix)) tempDirs.push(dir) return dir } const BRAIN = 'aaaaaaaa-0000-4000-8000-000000000001' const IMPL = 'bbbbbbbb-0000-4000-8000-000000000002' const REVIEW = '019dcb27-58a6-70a1-a5d1-bfc7f3ed9d0a' function fixture(implBytes: Buffer): { projectsRoot: string, codexRoot: string } { const projectsRoot = tempDir('handoff-size-projects-') const codexRoot = tempDir('handoff-size-codex-') const proj = path.join(projectsRoot, '-Users-chw-proj') const wt = path.join(projectsRoot, '-Users-chw-wt-x') mkdirSync(proj, { recursive: true }) mkdirSync(path.join(wt, IMPL, 'subagents'), { recursive: true }) writeFileSync(path.join(proj, `${BRAIN}.jsonl`), 'brain\n') writeFileSync(path.join(wt, `${IMPL}.jsonl`), implBytes) writeFileSync(path.join(wt, IMPL, 'subagents', 'agent-1.jsonl'), '{}\n') const day = path.join(codexRoot, '2026', '08', '26') mkdirSync(day, { recursive: true }) writeFileSync(path.join(day, `rollout-2026-08-26T01-02-03-${REVIEW}.jsonl`), 'review\n') return { projectsRoot, codexRoot } } function source(roots: { projectsRoot: string, codexRoot: string }) { return { src: { issueNumber: 448, from: 'chw', sessions: { sessionId: BRAIN, implementSessionId: IMPL, testSessionId: 'missing-0000-4000-8000-000000000000', reviewSessionId: REVIEW, }, profiles: {}, }, roots, } } describe('formatByteSize', () => { it('100MB 上限按整数 MB 显示', () => { expect(formatByteSize(HANDOFF_ATTACHMENT_MAX_BYTES)).toBe('100MB') }) }) describe('measureHandoffSources', () => { it('jsonl、会话目录、codex 加在一起,缺失的会话不出现', async () => { const roots = fixture(Buffer.from('impl\n')) const { src } = source(roots) const items = await measureHandoffSources(src, roots) expect(items).toEqual([ { role: '头脑风暴', bytes: Buffer.byteLength('brain\n') }, { role: '实施', bytes: Buffer.byteLength('impl\n') }, { role: '审查', bytes: Buffer.byteLength('review\n') }, ]) }) }) describe('ensureHandoffFits', () => { it('原始大小不超过上限时直接通过', async () => { const roots = fixture(Buffer.from('impl\n')) const { src } = source(roots) const items = await ensureHandoffFits(src, roots, { maxBytes: 1024 * 1024 }) expect(items.map(item => item.role)).toEqual(['头脑风暴', '实施', '审查']) }) it('压缩后超过上限就停,并说明还没开始移交', async () => { const roots = fixture(randomBytes(80 * 1024)) const { src } = source(roots) await expect(ensureHandoffFits(src, roots, { maxBytes: 1024 })).rejects.toBeInstanceOf(HandoffSourcesTooLargeError) try { await ensureHandoffFits(src, roots, { maxBytes: 1024 }) } catch (err) { expect(err).toBeInstanceOf(HandoffSourcesTooLargeError) expect((err as Error).message).toContain('没有开始移交') expect((err as Error).message).toContain('实施') expect((err as Error).message).toContain('1.0KB') } }) }) describe('packHandoff', () => { it('产出超过 maxBytes 就失败,不等到打完', async () => { const staging = tempDir('handoff-pack-') writeFileSync(path.join(staging, 'big.bin'), randomBytes(80 * 1024)) const out = path.join(tempDir('handoff-pack-out-'), 'bundle.tgz') await expect(packHandoff(staging, out, { maxBytes: 1024 })).rejects.toBeInstanceOf(HandoffArchiveTooLargeError) }) })