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') }) })