- 会话包传到 OSS 的 spx-handoff/<owner>/<repo>/issue-<N>-<时间>.tar.xz,上传时对象设成私有;不再挂 Gitea 工单附件 - 新命令「配置移交存储」:endpoint、bucket 存扩展设置,AccessKey 存密钥存储;没配时移交 / 接管直接提示,什么都不改 - 共享 state 字段 handoffAttachmentId 改名 handoffBundleKey(含 spx CLI 与 schema) - 打包换成 xz 1 级(tar -cJf),去掉 100MB 上限检查和先试压一遍的重复压缩 - 发送方提示打包 / 上传了多少 MB;接管方同一条提示依次显示下载、解包、建 worktree、装会话,完成前不消失 - 会话交接(单会话)仍走 Gitea 附件,Gitea 附件白名单没有 .xz,单独保留 gzip 打包
210 lines
7.2 KiB
TypeScript
210 lines
7.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import {
|
||
canAcceptHandoff,
|
||
canStartHandoff,
|
||
checkWorktreePushed,
|
||
handoffAcceptedStateExtra,
|
||
handoffAcceptedUiPatch,
|
||
handoffLocalTombstones,
|
||
handoffStartedStateExtra,
|
||
handoffStartedUiPatch,
|
||
transferProgressText,
|
||
} from './handoff'
|
||
|
||
const KEY = 'spx-handoff/o/r/issue-9-20261008T010203Z.tar.xz'
|
||
|
||
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', handoffBundleKey: KEY }, '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({ handoffBundleKey: KEY, assignees: ['me', 'x'] }, 'me')).toBe(true)
|
||
})
|
||
it('没有会话包 / 不是我的 / 身份未知 / youtrack → false', () => {
|
||
expect(canAcceptHandoff({ assignees: ['me'] }, 'me')).toBe(false)
|
||
expect(canAcceptHandoff({ handoffBundleKey: KEY, assignees: ['x'] }, 'me')).toBe(false)
|
||
expect(canAcceptHandoff({ handoffBundleKey: KEY, assignees: ['me'] }, undefined)).toBe(false)
|
||
expect(canAcceptHandoff({ source: 'youtrack', handoffBundleKey: KEY, assignees: ['me'] }, 'me')).toBe(false)
|
||
})
|
||
})
|
||
|
||
describe('transferProgressText', () => {
|
||
it('知道总大小时报「已传 / 总共」', () => {
|
||
expect(transferProgressText('上传', 3.2 * 1024 * 1024, 32 * 1024 * 1024)).toBe('上传 3.2MB / 32MB')
|
||
})
|
||
it('总大小未知时只报已传多少', () => {
|
||
expect(transferProgressText('下载', 512 * 1024, 0)).toBe('下载 512KB')
|
||
})
|
||
})
|
||
|
||
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(KEY, 'chw')).toEqual({
|
||
handoffBundleKey: KEY,
|
||
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(KEY, 'chw', 'me', { keepWorktreePath: false })).toEqual({
|
||
handoffBundleKey: KEY,
|
||
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(KEY, 'chw', 'me', { keepWorktreePath: true })
|
||
expect(patch).not.toHaveProperty('worktreePath')
|
||
expect(patch).not.toHaveProperty('worktreeExists')
|
||
expect(patch).toEqual({
|
||
handoffBundleKey: KEY,
|
||
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({
|
||
handoffBundleKey: '',
|
||
handoffFrom: '',
|
||
implementSessionId: 'i',
|
||
reviewSessionId: 'r',
|
||
profilePath: '/p.json',
|
||
worktreePath: '/wt',
|
||
})
|
||
})
|
||
it('接管 UI patch', () => {
|
||
expect(handoffAcceptedUiPatch({
|
||
sessions: { sessionId: 's' },
|
||
profiles: {},
|
||
worktreePath: '/wt',
|
||
worktreeExists: true,
|
||
missingSessionIds: [],
|
||
})).toEqual({
|
||
handoffBundleKey: null,
|
||
handoffFrom: null,
|
||
sessionId: 's',
|
||
worktreePath: '/wt',
|
||
worktreeExists: true,
|
||
missingSessionIds: [],
|
||
})
|
||
})
|
||
it('接管 UI patch:无 worktreePath 时显式置 null,不留旧值', () => {
|
||
expect(handoffAcceptedUiPatch({
|
||
sessions: { sessionId: 's' },
|
||
profiles: {},
|
||
worktreeExists: false,
|
||
missingSessionIds: [],
|
||
})).toEqual({
|
||
handoffBundleKey: null,
|
||
handoffFrom: null,
|
||
sessionId: 's',
|
||
worktreePath: null,
|
||
worktreeExists: false,
|
||
missingSessionIds: [],
|
||
})
|
||
})
|
||
it('接管 UI patch:缺失的会话 id 原样下发,UI 立刻知道哪几行不能 resume', () => {
|
||
expect(handoffAcceptedUiPatch({
|
||
sessions: { sessionId: 's', reviewSessionId: 'r' },
|
||
profiles: {},
|
||
worktreeExists: false,
|
||
missingSessionIds: ['r'],
|
||
})).toEqual({
|
||
handoffBundleKey: null,
|
||
handoffFrom: null,
|
||
sessionId: 's',
|
||
reviewSessionId: 'r',
|
||
worktreePath: null,
|
||
worktreeExists: false,
|
||
missingSessionIds: ['r'],
|
||
})
|
||
})
|
||
})
|