Files
superwork/vscode/src/panel/handlers/handoff.test.ts
T
cruldra 499cfad7d7 ✨ feat(vscode): 工单移交的会话包改走阿里云 OSS、换 xz 压缩,两边进度一步步显示
- 会话包传到 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 打包
2026-10-08 07:17:26 +08:00

210 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'],
})
})
})