✨ feat(vscode): handoff 会话包 stage/pack/unpack/install

Claude-Session: https://claude.ai/code/session_011cEyL6k351U2BzX1Qmygph
This commit is contained in:
cruldra committed 2026-08-26 15:54:36 +08:00
1 parent 65cd404350
commit 332205a2fe
3 files changed
+229

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
import { existsSync, mkdirSync, mkdtempSync, readFileSync, 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 { packHandoff, unpackHandoff } from './handoffArchive'
import { installHandoffBundle, stageHandoffBundle } from './handoffBundle'
import { HANDOFF_MANIFEST_FILE, parseHandoffManifest } from './handoffManifest'
const tempDirs: string[] = []
afterEach(() => {
while (tempDirs.length > 0) {
const d = tempDirs.pop()
if (d)
rmSync(d, { recursive: true, force: true })
}
})
function tempDir(prefix: string): string {
const d = mkdtempSync(path.join(os.tmpdir(), prefix))
tempDirs.push(d)
return d
}
const BRAIN = 'aaaaaaaa-0000-4000-8000-000000000001'
const IMPL = 'bbbbbbbb-0000-4000-8000-000000000002'
const REVIEW = '019dcb27-58a6-70a1-a5d1-bfc7f3ed9d0a'
function senderRoots(): { projectsRoot: string, codexRoot: string } {
const projectsRoot = tempDir('sender-projects-')
const codexRoot = tempDir('sender-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`), 'impl\n')
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 }
}
describe('stageHandoffBundle', () => {
it('收集存在的会话,缺失的不进列表', async () => {
const roots = senderRoots()
const staging = tempDir('staging-')
const manifest = await stageHandoffBundle({
issueNumber: 7,
from: 'chw',
branch: 'feature/x',
sessions: { sessionId: BRAIN, implementSessionId: IMPL, testSessionId: 'missing-0000-4000-8000-000000000000', reviewSessionId: REVIEW },
profiles: { profilePath: '/Users/chw/p.json' },
}, staging, roots, new Date('2026-08-26T00:00:00Z'))
expect(manifest.claude).toEqual([{ id: BRAIN, kind: 'brainstorm' }, { id: IMPL, kind: 'implement' }])
expect(manifest.codex).toEqual([{ id: REVIEW, relPath: path.join('2026', '08', '26', `rollout-2026-08-26T01-02-03-${REVIEW}.jsonl`) }])
expect(manifest.sessions.testSessionId).toBe('missing-0000-4000-8000-000000000000')
expect(manifest.createdAt).toBe('2026-08-26T00:00:00.000Z')
expect(existsSync(path.join(staging, 'claude', `${IMPL}.jsonl`))).toBe(true)
expect(existsSync(path.join(staging, 'claude', IMPL, 'subagents', 'agent-1.jsonl'))).toBe(true)
expect(existsSync(path.join(staging, 'codex', manifest.codex[0].relPath))).toBe(true)
expect(parseHandoffManifest(readFileSync(path.join(staging, HANDOFF_MANIFEST_FILE), 'utf8'), 7)).toEqual(manifest)
})
})
describe('pack → unpack → install', () => {
it('接管方装到对应 project 目录与 codex 目录', async () => {
const roots = senderRoots()
const staging = tempDir('staging-')
const manifest = await stageHandoffBundle({
issueNumber: 7,
from: 'chw',
branch: 'feature/x',
sessions: { sessionId: BRAIN, implementSessionId: IMPL, reviewSessionId: REVIEW },
profiles: {},
}, staging, roots)
const archive = path.join(tempDir('archive-'), 'spx-handoff-issue-7.tgz')
await packHandoff(staging, archive)
expect(existsSync(archive)).toBe(true)
const extracted = tempDir('extracted-')
await unpackHandoff(archive, extracted)
const parsed = parseHandoffManifest(readFileSync(path.join(extracted, HANDOFF_MANIFEST_FILE), 'utf8'), 7)
expect(parsed).toEqual(manifest)
const projectsRoot = tempDir('receiver-projects-')
const codexRoot = tempDir('receiver-codex-')
const workspaceProjectsDir = path.join(projectsRoot, '-home-me-proj')
const worktreeProjectsDir = path.join(projectsRoot, '-home-me-wt-x')
// 本机残留的同 sid 副本必须被清掉
mkdirSync(path.join(projectsRoot, '-stale'), { recursive: true })
writeFileSync(path.join(projectsRoot, '-stale', `${IMPL}.jsonl`), 'stale\n')
await installHandoffBundle(extracted, parsed, { projectsRoot, codexRoot, workspaceProjectsDir, worktreeProjectsDir })
expect(readFileSync(path.join(workspaceProjectsDir, `${BRAIN}.jsonl`), 'utf8')).toBe('brain\n')
expect(readFileSync(path.join(worktreeProjectsDir, `${IMPL}.jsonl`), 'utf8')).toBe('impl\n')
expect(existsSync(path.join(worktreeProjectsDir, IMPL, 'subagents', 'agent-1.jsonl'))).toBe(true)
expect(existsSync(path.join(projectsRoot, '-stale', `${IMPL}.jsonl`))).toBe(false)
expect(readFileSync(path.join(codexRoot, parsed.codex[0].relPath), 'utf8')).toBe('review\n')
})
it('无 worktreeProjectsDir 时实施会话也装到工作区目录', async () => {
const roots = senderRoots()
const staging = tempDir('staging-')
const manifest = await stageHandoffBundle({ issueNumber: 7, from: 'chw', sessions: { implementSessionId: IMPL }, profiles: {} }, staging, roots)
const projectsRoot = tempDir('receiver-projects-')
const workspaceProjectsDir = path.join(projectsRoot, '-home-me-proj')
await installHandoffBundle(staging, manifest, { projectsRoot, codexRoot: tempDir('receiver-codex-'), workspaceProjectsDir })
expect(existsSync(path.join(workspaceProjectsDir, `${IMPL}.jsonl`))).toBe(true)
})
})