🐛 fix(vscode): 移交终审修正:schema 墓碑、顶层兜底、前置复查、分支保护、清单校验
Claude-Session: https://claude.ai/code/session_011cEyL6k351U2BzX1Qmygph
This commit is contained in:
1 parent
aa98dfb4e1
commit
0acfd04b54
10 files changed
+299
-220
No files matched your search
@@ -1,6 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { handoffAttachmentName, parseHandoffManifest } from './handoffManifest'
|
||||
|
||||
const UUID = '11111111-1111-4111-8111-111111111111'
|
||||
|
||||
const good = {
|
||||
version: 1,
|
||||
issue: 42,
|
||||
@@ -9,7 +11,7 @@ const good = {
|
||||
branch: 'feature/x',
|
||||
sessions: { implementSessionId: 'a' },
|
||||
profiles: {},
|
||||
claude: [{ id: 'a', kind: 'implement' }],
|
||||
claude: [{ id: UUID, kind: 'implement' }],
|
||||
codex: [],
|
||||
}
|
||||
|
||||
@@ -30,7 +32,13 @@ describe('parseHandoffManifest', () => {
|
||||
expect(() => parseHandoffManifest(JSON.stringify({ ...good, version: 2 }), 42)).toThrow(/version/)
|
||||
})
|
||||
it('claude 条目 kind 非法 → 抛错', () => {
|
||||
expect(() => parseHandoffManifest(JSON.stringify({ ...good, claude: [{ id: 'a', kind: 'x' }] }), 42)).toThrow(/claude/)
|
||||
expect(() => parseHandoffManifest(JSON.stringify({ ...good, claude: [{ id: UUID, kind: 'x' }] }), 42)).toThrow(/claude/)
|
||||
})
|
||||
it('claude 条目 id 非 UUID(路径穿越)→ 抛错', () => {
|
||||
expect(() => parseHandoffManifest(JSON.stringify({ ...good, claude: [{ id: '../x', kind: 'implement' }] }), 42)).toThrow(/claude/)
|
||||
})
|
||||
it('codex 条目 id 非 UUID → 抛错', () => {
|
||||
expect(() => parseHandoffManifest(JSON.stringify({ ...good, codex: [{ id: 'not-a-uuid', relPath: '2026/08/26/rollout-x.jsonl' }] }), 42)).toThrow(/codex/)
|
||||
})
|
||||
it('不是 JSON → 抛错', () => {
|
||||
expect(() => parseHandoffManifest('nope', 42)).toThrow()
|
||||
|
||||
@@ -39,6 +39,9 @@ export function handoffAttachmentName(issueNumber: number): string {
|
||||
|
||||
const KINDS: ReadonlySet<string> = new Set<ClaudeSessionKind>(['brainstorm', 'implement', 'test'])
|
||||
|
||||
// Claude / Codex 会话 id 都是 UUID;顺手挡掉路径穿越(如 `../x`)之类的伪造条目。
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
|
||||
function optString(v: unknown): string | undefined {
|
||||
return typeof v === 'string' && v.length > 0 ? v : undefined
|
||||
}
|
||||
@@ -63,7 +66,7 @@ export function parseHandoffManifest(json: string, expectedIssue: number): Hando
|
||||
const o = e as Record<string, unknown>
|
||||
const id = optString(o.id)
|
||||
const kind = optString(o.kind)
|
||||
if (!id || !kind || !KINDS.has(kind))
|
||||
if (!id || !kind || !KINDS.has(kind) || !UUID_RE.test(id))
|
||||
throw new Error(`handoff.json claude 条目非法:${JSON.stringify(e)}`)
|
||||
return { id, kind: kind as ClaudeSessionKind }
|
||||
})
|
||||
@@ -71,7 +74,7 @@ export function parseHandoffManifest(json: string, expectedIssue: number): Hando
|
||||
const o = e as Record<string, unknown>
|
||||
const id = optString(o.id)
|
||||
const relPath = optString(o.relPath)
|
||||
if (!id || !relPath || relPath.startsWith('/') || relPath.includes('..'))
|
||||
if (!id || !relPath || relPath.startsWith('/') || relPath.includes('..') || !UUID_RE.test(id))
|
||||
throw new Error(`handoff.json codex 条目非法:${JSON.stringify(e)}`)
|
||||
return { id, relPath }
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user