♻️ refactor(vscode): 工单来源 gitea 改名 forge,激活时把本机按旧名存的记录迁过去

This commit is contained in:
cruldra committed 2026-10-19 08:00:29 +11:00
1 parent 14bee9f505
commit 451bc91a6b
24 files changed
+234 -90

No files matched your search

+1 -1
View File
@@ -17,7 +17,7 @@ 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', source: 'forge' }, 'me')).toBe(true)
expect(canStartHandoff({ column: 'todo', assignees: [] }, 'me')).toBe(true)
expect(canStartHandoff({ column: 'todo', assignees: ['me', 'x'] }, 'me')).toBe(true)
})
+2 -2
View File
@@ -4,7 +4,7 @@ import { formatByteSize } from '../../cc/handoffArchive'
import { LOCAL_PROFILE_FIELDS, LOCAL_STATE_FIELDS } from '../../issues/localState'
export function canStartHandoff(issue: {
source?: 'gitea' | 'youtrack'
source?: 'forge' | 'youtrack'
column: IssueColumn
handoffBundleKey?: string
assignees?: string[]
@@ -15,7 +15,7 @@ export function canStartHandoff(issue: {
}
export function canAcceptHandoff(issue: {
source?: 'gitea' | 'youtrack'
source?: 'forge' | 'youtrack'
handoffBundleKey?: string
assignees?: string[]
}, me: string | undefined): boolean {
+3 -3
View File
@@ -185,7 +185,7 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
const knownColumns: IssueColumn[] = ['todo', 'in-progress', 'review', 'done']
const column = knownColumns.includes(state.column as IssueColumn) ? (state.column as IssueColumn) : 'todo'
const assignees = issue.assignees.map(a => a.login)
if (!canStartHandoff({ source: 'gitea', column, handoffBundleKey: str(state.handoffBundleKey), assignees }, me)) {
if (!canStartHandoff({ source: 'forge', column, handoffBundleKey: str(state.handoffBundleKey), assignees }, me)) {
const message = `#${issueNumber} 不是你负责的工单,或已在移交中 / 已完成,不能移交`
toast(panel, 'error', message)
return { ok: false, message }
@@ -377,7 +377,7 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
}
}
async function requestFacts(panel: KanbanWebviewPanel, issueNumber: number): Promise<{ me: string, facts: { source: 'gitea', column: IssueColumn, handoffBundleKey?: string, handoffRequestFrom?: string, assignees: string[] } } | undefined> {
async function requestFacts(panel: KanbanWebviewPanel, issueNumber: number): Promise<{ me: string, facts: { source: 'forge', column: IssueColumn, handoffBundleKey?: string, handoffRequestFrom?: string, assignees: string[] } } | undefined> {
const ctx = await resolveRepoCtx(panel)
if (!ctx)
return
@@ -399,7 +399,7 @@ async function requestFacts(panel: KanbanWebviewPanel, issueNumber: number): Pro
return {
me: ctx.me,
facts: {
source: 'gitea',
source: 'forge',
column,
...(str(state.handoffBundleKey) ? { handoffBundleKey: str(state.handoffBundleKey) } : {}),
...(str(state.handoffRequestFrom) ? { handoffRequestFrom: str(state.handoffRequestFrom) } : {}),
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
import { canAnswerHandoffRequest, canAskHandoff, canCancelHandoffRequest, handoffRequestNotices, handoffRequestSystemNotice } from './handoffRequest'
const review = {
source: 'gitea' as const,
source: 'forge' as const,
column: 'review' as const,
assignees: ['chw'],
}
+1 -1
View File
@@ -1,5 +1,5 @@
export interface HandoffRequestFacts {
source?: 'gitea' | 'youtrack'
source?: 'forge' | 'youtrack'
column: 'todo' | 'in-progress' | 'review' | 'done'
handoffBundleKey?: string
handoffRequestFrom?: string
@@ -45,7 +45,7 @@ const LAUNCH = { cwd: '/worktree', startedAt: STARTED_AT, prompt: PROMPT }
const panels: KanbanWebviewPanel[] = []
let resolveSession!: (id: string | null) => void
function fixture(source: 'gitea' | 'youtrack' = 'gitea') {
function fixture(source: 'forge' | 'youtrack' = 'forge') {
const store = new Map<string, unknown>()
const context = {
workspaceState: {
@@ -126,7 +126,7 @@ describe('reviewSessionCapture', () => {
mocks.watch.mockResolvedValue({ sessionId: Promise.resolve(SID), initialSessionId: SID })
await recoverOpenReviewSessions(f.panel, [466])
expect(mocks.watch).toHaveBeenCalledWith(expect.objectContaining({ ...LAUNCH, startedAt: STARTED_AT }))
expect(getLocalIssueState(f.context, 'gitea', 466).reviewSessionId).toBe(SID)
expect(getLocalIssueState(f.context, 'forge', 466).reviewSessionId).toBe(SID)
expect(f.terminal.sendText).not.toHaveBeenCalled()
expect(f.terminal.show).not.toHaveBeenCalled()
})
@@ -134,28 +134,28 @@ describe('reviewSessionCapture', () => {
it('尚未落盘的恢复立即返回,之后后台补回ID', async () => {
const f = fixture()
await expect(recoverReviewSessionCapture(f.panel, 466, f.terminal)).resolves.toBeNull()
expect(getLocalIssueState(f.context, 'gitea', 466).reviewSessionId).toBeUndefined()
expect(getLocalIssueState(f.context, 'forge', 466).reviewSessionId).toBeUndefined()
resolveSession(SID)
await vi.waitFor(() => expect(getLocalIssueState(f.context, 'gitea', 466).reviewSessionId).toBe(SID))
await vi.waitFor(() => expect(getLocalIssueState(f.context, 'forge', 466).reviewSessionId).toBe(SID))
})
it('用户清除留下的墓碑不得自动补回', async () => {
const f = fixture()
f.store.set('superpowers.localIssueState', { 'gitea:466': { reviewSessionId: '' } })
f.store.set('superpowers.localIssueState', { 'forge:466': { reviewSessionId: '' } })
await expect(recoverReviewSessionCapture(f.panel, 466, f.terminal)).resolves.toBeNull()
expect(mocks.exec).not.toHaveBeenCalled()
expect(mocks.watch).not.toHaveBeenCalled()
expect(getLocalIssueState(f.context, 'gitea', 466).reviewSessionId).toBe('')
expect(getLocalIssueState(f.context, 'forge', 466).reviewSessionId).toBe('')
})
it('用户明确新建的审查可以替换之前清除留下的墓碑', async () => {
const f = fixture()
f.store.set('superpowers.localIssueState', { 'gitea:466': { reviewSessionId: '' } })
f.store.set('superpowers.localIssueState', { 'forge:466': { reviewSessionId: '' } })
const prepared = await prepareReviewSessionCapture(f.panel, 466, LAUNCH)
const captured = prepared.attach(f.terminal)
resolveSession(SID)
await expect(captured).resolves.toBe(SID)
expect(getLocalIssueState(f.context, 'gitea', 466).reviewSessionId).toBe(SID)
expect(getLocalIssueState(f.context, 'forge', 466).reviewSessionId).toBe(SID)
})
it('清除时中止待捕获并删除启动记录,之后不写回ID', async () => {
@@ -166,7 +166,7 @@ describe('reviewSessionCapture', () => {
resolveSession(SID)
await expect(captured).resolves.toBeNull()
expect(f.store.has(f.launchKey)).toBe(false)
expect(getLocalIssueState(f.context, 'gitea', 466).reviewSessionId).toBeUndefined()
expect(getLocalIssueState(f.context, 'forge', 466).reviewSessionId).toBeUndefined()
})
it('准备期间被取消,不允许稍后继续创建终端', async () => {
@@ -7,7 +7,7 @@ import * as path from 'node:path'
import process from 'node:process'
import { window } from 'vscode'
import { watchForCodexSession } from '../../cc/codexSessionWatcher'
import { getLocalIssueState, mergeLocalIssueState } from '../../issues/localState'
import { getLocalIssueState, localIssueKey, mergeLocalIssueState } from '../../issues/localState'
import { logger } from '../../logging/logger'
import { defaultCodexSessionsDir } from '../../sessions/sessionFiles'
@@ -32,7 +32,7 @@ function captureMap(panel: KanbanWebviewPanel): Map<number, ReviewCapture> {
}
function launchKey(source: ReviewCapture['source'], issueNumber: number): string {
return `superpowers.reviewSessionLaunch.${source}:${issueNumber}`
return `superpowers.reviewSessionLaunch.${localIssueKey(source, issueNumber)}`
}
/** 用户清除关联时先中止捕获,防止稍后落盘的文件把关联补回来。 */
@@ -40,7 +40,7 @@ export async function cancelReviewSessionCapture(panel: KanbanWebviewPanel, issu
const map = captureMap(panel)
const capture = map.get(issueNumber)
const recovery = recoveries.get(panel)?.get(issueNumber)
const source = capture?.source ?? recovery?.source ?? panel.refFor(issueNumber).source ?? 'gitea'
const source = capture?.source ?? recovery?.source ?? panel.refFor(issueNumber).source ?? 'forge'
recovery?.abort.abort()
capture?.abort.abort()
map.delete(issueNumber)
@@ -85,7 +85,7 @@ export async function prepareReviewSessionCapture(
panel: KanbanWebviewPanel,
issueNumber: number,
launch: CodexSessionLaunch,
source: ReviewCapture['source'] = panel.refFor(issueNumber).source ?? 'gitea',
source: ReviewCapture['source'] = panel.refFor(issueNumber).source ?? 'forge',
): Promise<{
attach: (terminal: Terminal) => Promise<string | null>
initialSessionId: string | null
@@ -269,7 +269,7 @@ export function recoverReviewSessionCapture(panel: KanbanWebviewPanel, issueNumb
if (existing)
return existing.result
const abort = new AbortController()
const source = panel.refFor(issueNumber).source ?? 'gitea'
const source = panel.refFor(issueNumber).source ?? 'forge'
const result = recoverReviewSessionCaptureInner(panel, issueNumber, terminal, abort.signal, source)
.finally(() => {
if (map.get(issueNumber)?.result === result)