✨ feat(vscode): handoff 纯决策与 state/UI payload
Claude-Session: https://claude.ai/code/session_011cEyL6k351U2BzX1Qmygph
This commit is contained in:
@@ -0,0 +1,111 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import {
|
||||||
|
canAcceptHandoff,
|
||||||
|
canStartHandoff,
|
||||||
|
checkWorktreePushed,
|
||||||
|
handoffAcceptedStateExtra,
|
||||||
|
handoffAcceptedUiPatch,
|
||||||
|
handoffStartedStateExtra,
|
||||||
|
handoffStartedUiPatch,
|
||||||
|
} from './handoff'
|
||||||
|
|
||||||
|
describe('canStartHandoff', () => {
|
||||||
|
it('gitea 工单、非 done、无待接管 → true', () => {
|
||||||
|
expect(canStartHandoff({ column: 'in-progress' })).toBe(true)
|
||||||
|
expect(canStartHandoff({ column: 'todo', source: 'gitea' })).toBe(true)
|
||||||
|
})
|
||||||
|
it('done / youtrack / 已在移交中 → false', () => {
|
||||||
|
expect(canStartHandoff({ column: 'done' })).toBe(false)
|
||||||
|
expect(canStartHandoff({ column: 'review', source: 'youtrack' })).toBe(false)
|
||||||
|
expect(canStartHandoff({ column: 'review', handoffAttachmentId: '9' })).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('canAcceptHandoff', () => {
|
||||||
|
it('有附件且 assignee 含我 → true', () => {
|
||||||
|
expect(canAcceptHandoff({ handoffAttachmentId: '9', assignees: ['me', 'x'] }, 'me')).toBe(true)
|
||||||
|
})
|
||||||
|
it('无附件 / 不是我的 / 身份未知 / youtrack → false', () => {
|
||||||
|
expect(canAcceptHandoff({ assignees: ['me'] }, 'me')).toBe(false)
|
||||||
|
expect(canAcceptHandoff({ handoffAttachmentId: '9', assignees: ['x'] }, 'me')).toBe(false)
|
||||||
|
expect(canAcceptHandoff({ handoffAttachmentId: '9', assignees: ['me'] }, undefined)).toBe(false)
|
||||||
|
expect(canAcceptHandoff({ source: 'youtrack', handoffAttachmentId: '9', assignees: ['me'] }, 'me')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('checkWorktreePushed', () => {
|
||||||
|
it('干净且 HEAD 与远端一致 → ok', () => {
|
||||||
|
expect(checkWorktreePushed({ statusPorcelain: '', localHead: 'abc', remoteHead: 'abc' })).toEqual({ ok: true })
|
||||||
|
})
|
||||||
|
it('有未提交改动 → 拒绝', () => {
|
||||||
|
const r = checkWorktreePushed({ statusPorcelain: ' M a.ts\n', localHead: 'abc', remoteHead: 'abc' })
|
||||||
|
expect(r.ok).toBe(false)
|
||||||
|
if (!r.ok)
|
||||||
|
expect(r.reason).toMatch(/未提交/)
|
||||||
|
})
|
||||||
|
it('本地领先远端 → 拒绝', () => {
|
||||||
|
const r = checkWorktreePushed({ statusPorcelain: '', localHead: 'abc', remoteHead: 'def' })
|
||||||
|
expect(r.ok).toBe(false)
|
||||||
|
if (!r.ok)
|
||||||
|
expect(r.reason).toMatch(/push/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('payloads', () => {
|
||||||
|
it('移交落盘:两个共享字段 + 本机字段全部墓碑', () => {
|
||||||
|
expect(handoffStartedStateExtra(9, 'chw')).toEqual({
|
||||||
|
handoffAttachmentId: '9',
|
||||||
|
handoffFrom: 'chw',
|
||||||
|
sessionId: '',
|
||||||
|
implementSessionId: '',
|
||||||
|
reviewSessionId: '',
|
||||||
|
testSessionId: '',
|
||||||
|
profilePath: '',
|
||||||
|
brainstormProfilePath: '',
|
||||||
|
testProfilePath: '',
|
||||||
|
worktreePath: '',
|
||||||
|
prDiffFile: '',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it('移交 UI patch 用 null 清字段并更新 assignees', () => {
|
||||||
|
expect(handoffStartedUiPatch(9, 'chw', 'me')).toEqual({
|
||||||
|
handoffAttachmentId: '9',
|
||||||
|
handoffFrom: 'chw',
|
||||||
|
assignees: ['me'],
|
||||||
|
sessionId: null,
|
||||||
|
implementSessionId: null,
|
||||||
|
reviewSessionId: null,
|
||||||
|
testSessionId: null,
|
||||||
|
worktreePath: null,
|
||||||
|
worktreeExists: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it('接管落盘:清两个共享字段,写本机 sid / profile / worktree', () => {
|
||||||
|
expect(handoffAcceptedStateExtra({
|
||||||
|
sessions: { implementSessionId: 'i', reviewSessionId: 'r' },
|
||||||
|
profiles: { profilePath: '/p.json' },
|
||||||
|
worktreePath: '/wt',
|
||||||
|
})).toEqual({
|
||||||
|
handoffAttachmentId: '',
|
||||||
|
handoffFrom: '',
|
||||||
|
implementSessionId: 'i',
|
||||||
|
reviewSessionId: 'r',
|
||||||
|
profilePath: '/p.json',
|
||||||
|
worktreePath: '/wt',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it('接管 UI patch', () => {
|
||||||
|
expect(handoffAcceptedUiPatch({
|
||||||
|
sessions: { sessionId: 's' },
|
||||||
|
profiles: {},
|
||||||
|
worktreePath: '/wt',
|
||||||
|
worktreeExists: true,
|
||||||
|
})).toEqual({
|
||||||
|
handoffAttachmentId: null,
|
||||||
|
handoffFrom: null,
|
||||||
|
sessionId: 's',
|
||||||
|
worktreePath: '/wt',
|
||||||
|
worktreeExists: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import type { HandoffProfiles, HandoffSessions } from '../../cc/handoffManifest'
|
||||||
|
import type { IssueColumn } from '../../gitea/types'
|
||||||
|
import { LOCAL_STATE_FIELDS } from '../../issues/localState'
|
||||||
|
|
||||||
|
export function canStartHandoff(issue: {
|
||||||
|
source?: 'gitea' | 'youtrack'
|
||||||
|
column: IssueColumn
|
||||||
|
handoffAttachmentId?: string
|
||||||
|
}): boolean {
|
||||||
|
return issue.source !== 'youtrack' && issue.column !== 'done' && !issue.handoffAttachmentId
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canAcceptHandoff(issue: {
|
||||||
|
source?: 'gitea' | 'youtrack'
|
||||||
|
handoffAttachmentId?: string
|
||||||
|
assignees?: string[]
|
||||||
|
}, me: string | undefined): boolean {
|
||||||
|
return issue.source !== 'youtrack'
|
||||||
|
&& !!issue.handoffAttachmentId
|
||||||
|
&& me !== undefined
|
||||||
|
&& (issue.assignees ?? []).includes(me)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移交前 worktree 必须「干净 + 全部 push」,否则接管方从远端重建会丢工作。 */
|
||||||
|
export function checkWorktreePushed(input: {
|
||||||
|
statusPorcelain: string
|
||||||
|
localHead: string
|
||||||
|
remoteHead: string
|
||||||
|
}): { ok: true } | { ok: false, reason: string } {
|
||||||
|
if (input.statusPorcelain.trim().length > 0)
|
||||||
|
return { ok: false, reason: '工作区有未提交改动,请先提交并 push' }
|
||||||
|
if (input.localHead.trim() !== input.remoteHead.trim())
|
||||||
|
return { ok: false, reason: '本地分支与远端不一致,请先 push' }
|
||||||
|
return { ok: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handoffStartedStateExtra(attachmentId: number, from: string): Record<string, unknown> {
|
||||||
|
const extra: Record<string, unknown> = { handoffAttachmentId: String(attachmentId), handoffFrom: from }
|
||||||
|
for (const f of LOCAL_STATE_FIELDS)
|
||||||
|
extra[f] = ''
|
||||||
|
return extra
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handoffStartedUiPatch(attachmentId: number, from: string, to: string): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
handoffAttachmentId: String(attachmentId),
|
||||||
|
handoffFrom: from,
|
||||||
|
assignees: [to],
|
||||||
|
sessionId: null,
|
||||||
|
implementSessionId: null,
|
||||||
|
reviewSessionId: null,
|
||||||
|
testSessionId: null,
|
||||||
|
worktreePath: null,
|
||||||
|
worktreeExists: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function definedEntries(o: Record<string, any>): Record<string, string> {
|
||||||
|
const out: Record<string, string> = {}
|
||||||
|
for (const [k, v] of Object.entries(o)) {
|
||||||
|
if (v)
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handoffAcceptedStateExtra(local: {
|
||||||
|
sessions: HandoffSessions
|
||||||
|
profiles: HandoffProfiles
|
||||||
|
worktreePath?: string
|
||||||
|
}): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
handoffAttachmentId: '',
|
||||||
|
handoffFrom: '',
|
||||||
|
...definedEntries(local.sessions),
|
||||||
|
...definedEntries(local.profiles),
|
||||||
|
...(local.worktreePath ? { worktreePath: local.worktreePath } : {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handoffAcceptedUiPatch(local: {
|
||||||
|
sessions: HandoffSessions
|
||||||
|
profiles: HandoffProfiles
|
||||||
|
worktreePath?: string
|
||||||
|
worktreeExists: boolean
|
||||||
|
}): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
handoffAttachmentId: null,
|
||||||
|
handoffFrom: null,
|
||||||
|
...definedEntries(local.sessions),
|
||||||
|
...definedEntries(local.profiles),
|
||||||
|
...(local.worktreePath ? { worktreePath: local.worktreePath } : {}),
|
||||||
|
worktreeExists: local.worktreeExists,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user