照测试会话那条链路补一份 prototype:
- 消息与路由:session/start-prototype、session/resume-prototype、
issue/update-prototype-profile-path,都进只读门禁
- 会话:handleStartPrototypeSession / handleResumePrototypeSession,
终端名 issue-N-原型;cwd 有 worktree 用 worktree、没有退主仓库根
(原 resolveTestSessionCwd 改名 resolveWorktreeOrRootCwd 两种会话共用)
- 不要求有 PR,任何阶段都能起;前置工单锁仍然拦
- 提示词:新增 prompts/prototype.md,设置里可改,占位符 {issueNumber}
- profile:工单级 prototypeProfilePath > 设置里的原型默认 profile >
内置默认,与其它会话互不回退
- 状态字段 prototypeSessionId / prototypeProfilePath / prototypeTabOpen
贯通 gitea、youtrack、本机 state 与移交包
- 详情面板加「原型会话id」「原型配置文件」两行,设置面板加对应两项
Claude-Session: https://claude.ai/code/session_01Kgmb1LvrPmFX3NoUDB4zZC
201 lines
6.9 KiB
TypeScript
201 lines
6.9 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import {
|
||
canAcceptHandoff,
|
||
canStartHandoff,
|
||
checkWorktreePushed,
|
||
handoffAcceptedStateExtra,
|
||
handoffAcceptedUiPatch,
|
||
handoffLocalTombstones,
|
||
handoffStartedStateExtra,
|
||
handoffStartedUiPatch,
|
||
} from './handoff'
|
||
|
||
describe('canStartHandoff', () => {
|
||
it('gitea 工单、非 done、无待接管、无 assignee 或 assignee 含我 → true', () => {
|
||
expect(canStartHandoff({ column: 'in-progress' }, 'me')).toBe(true)
|
||
expect(canStartHandoff({ column: 'todo', source: 'gitea' }, 'me')).toBe(true)
|
||
expect(canStartHandoff({ column: 'todo', assignees: [] }, 'me')).toBe(true)
|
||
expect(canStartHandoff({ column: 'todo', assignees: ['me', 'x'] }, 'me')).toBe(true)
|
||
})
|
||
it('done / youtrack / 已在移交中 / 不是我负责 → false', () => {
|
||
expect(canStartHandoff({ column: 'done' }, 'me')).toBe(false)
|
||
expect(canStartHandoff({ column: 'review', source: 'youtrack' }, 'me')).toBe(false)
|
||
expect(canStartHandoff({ column: 'review', handoffAttachmentId: '9' }, 'me')).toBe(false)
|
||
expect(canStartHandoff({ column: 'todo', assignees: ['x'] }, 'me')).toBe(false)
|
||
expect(canStartHandoff({ column: 'todo', assignees: ['me'] }, undefined)).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',
|
||
})
|
||
})
|
||
it('本机墓碑:keepWorktreePath=false 时本机字段全清', () => {
|
||
expect(handoffLocalTombstones({ keepWorktreePath: false })).toEqual({
|
||
sessionId: '',
|
||
implementSessionId: '',
|
||
reviewSessionId: '',
|
||
testSessionId: '',
|
||
prototypeSessionId: '',
|
||
profilePath: '',
|
||
brainstormProfilePath: '',
|
||
testProfilePath: '',
|
||
prototypeProfilePath: '',
|
||
worktreePath: '',
|
||
prDiffFile: '',
|
||
})
|
||
})
|
||
it('本机墓碑:keepWorktreePath=true 时保留 worktreePath', () => {
|
||
const extra = handoffLocalTombstones({ keepWorktreePath: true })
|
||
expect(extra.worktreePath).toBeUndefined()
|
||
expect(extra).toEqual({
|
||
sessionId: '',
|
||
implementSessionId: '',
|
||
reviewSessionId: '',
|
||
testSessionId: '',
|
||
prototypeSessionId: '',
|
||
profilePath: '',
|
||
brainstormProfilePath: '',
|
||
testProfilePath: '',
|
||
prototypeProfilePath: '',
|
||
prDiffFile: '',
|
||
})
|
||
})
|
||
it('移交 UI patch 用 null 清字段并更新 assignees', () => {
|
||
expect(handoffStartedUiPatch(9, 'chw', 'me', { keepWorktreePath: false })).toEqual({
|
||
handoffAttachmentId: '9',
|
||
handoffFrom: 'chw',
|
||
assignees: ['me'],
|
||
sessionId: null,
|
||
implementSessionId: null,
|
||
reviewSessionId: null,
|
||
testSessionId: null,
|
||
prototypeSessionId: null,
|
||
profilePath: null,
|
||
brainstormProfilePath: null,
|
||
testProfilePath: null,
|
||
prototypeProfilePath: null,
|
||
prDiffFile: null,
|
||
worktreePath: null,
|
||
worktreeExists: false,
|
||
})
|
||
})
|
||
it('移交 UI patch:keepWorktreePath=true 时省略 worktreePath / worktreeExists', () => {
|
||
const patch = handoffStartedUiPatch(9, 'chw', 'me', { keepWorktreePath: true })
|
||
expect(patch).not.toHaveProperty('worktreePath')
|
||
expect(patch).not.toHaveProperty('worktreeExists')
|
||
expect(patch).toEqual({
|
||
handoffAttachmentId: '9',
|
||
handoffFrom: 'chw',
|
||
assignees: ['me'],
|
||
sessionId: null,
|
||
implementSessionId: null,
|
||
reviewSessionId: null,
|
||
testSessionId: null,
|
||
prototypeSessionId: null,
|
||
profilePath: null,
|
||
brainstormProfilePath: null,
|
||
testProfilePath: null,
|
||
prototypeProfilePath: null,
|
||
prDiffFile: null,
|
||
})
|
||
})
|
||
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,
|
||
})
|
||
})
|
||
it('接管 UI patch:无 worktreePath 时显式置 null,不留旧值', () => {
|
||
expect(handoffAcceptedUiPatch({
|
||
sessions: { sessionId: 's' },
|
||
profiles: {},
|
||
worktreeExists: false,
|
||
})).toEqual({
|
||
handoffAttachmentId: null,
|
||
handoffFrom: null,
|
||
sessionId: 's',
|
||
worktreePath: null,
|
||
worktreeExists: false,
|
||
})
|
||
})
|
||
it('接管 UI patch:带 reviewSessionFileExists 时一并下发,UI 不用等下次刷新才能点链接', () => {
|
||
expect(handoffAcceptedUiPatch({
|
||
sessions: { reviewSessionId: 'r' },
|
||
profiles: {},
|
||
worktreeExists: false,
|
||
reviewSessionFileExists: true,
|
||
})).toEqual({
|
||
handoffAttachmentId: null,
|
||
handoffFrom: null,
|
||
reviewSessionId: 'r',
|
||
worktreePath: null,
|
||
worktreeExists: false,
|
||
reviewSessionFileExists: true,
|
||
})
|
||
})
|
||
it('接管 UI patch:reviewSessionFileExists 未传时不出现在 patch 里', () => {
|
||
const patch = handoffAcceptedUiPatch({
|
||
sessions: { sessionId: 's' },
|
||
profiles: {},
|
||
worktreeExists: false,
|
||
})
|
||
expect(patch).not.toHaveProperty('reviewSessionFileExists')
|
||
})
|
||
})
|