♻️ refactor(vscode): 工单来源 gitea 改名 forge,激活时把本机按旧名存的记录迁过去
This commit is contained in:
1 parent
14bee9f505
commit
451bc91a6b
24 files changed
+234
-90
No files matched your search
@@ -412,9 +412,9 @@ describe('kanban panel cache integration', () => {
|
||||
secondProcess.resolve(42)
|
||||
await loading
|
||||
expect(getLocalIssueState(f.context, 'youtrack', 42).reviewSessionId).toBe('session-42')
|
||||
expect(getLocalIssueState(f.context, 'gitea', 42).reviewSessionId).toBeUndefined()
|
||||
expect(getLocalIssueState(f.context, 'forge', 42).reviewSessionId).toBeUndefined()
|
||||
expect(f.disk.has('superpowers.reviewSessionLaunch.youtrack:42')).toBe(false)
|
||||
expect(f.disk.has('superpowers.reviewSessionLaunch.gitea:42')).toBe(false)
|
||||
expect(f.disk.has('superpowers.reviewSessionLaunch.forge:42')).toBe(false)
|
||||
})
|
||||
|
||||
it('恢复会话失败时工单来源仍与当前快照一致', async () => {
|
||||
|
||||
@@ -66,7 +66,7 @@ export class KanbanWebviewPanel {
|
||||
private readonly panel: WebviewPanel
|
||||
private readonly disposables: { dispose: () => void }[] = []
|
||||
/** 按工单号保存来源,确保状态修改发往正确的工单系统。 */
|
||||
private issueRefs = new Map<number, { source: 'gitea' | 'youtrack', externalId?: string }>()
|
||||
private issueRefs = new Map<number, { source: 'forge' | 'youtrack', externalId?: string }>()
|
||||
/** 工单号 → 归属信息,供写操作门禁判定「这是不是我的工单」。 */
|
||||
private issueOwners = new Map<number, IssueOwnershipFacts>()
|
||||
/** 本机 token 对应的 Gitea login;解析失败为 undefined(此时不锁任何工单)。 */
|
||||
@@ -1034,7 +1034,7 @@ export class KanbanWebviewPanel {
|
||||
return
|
||||
const recoveryIssues = load.updates.reduce(applyIssueUpdate, [...giteaIssues, ...youtrackList])
|
||||
this.issueRefs = new Map(
|
||||
recoveryIssues.map(i => [i.number, { source: i.source ?? 'gitea', externalId: i.externalId }] as const),
|
||||
recoveryIssues.map(i => [i.number, { source: i.source ?? 'forge', externalId: i.externalId }] as const),
|
||||
)
|
||||
await sessions.recoverOpenReviewSessions(this, recoveryIssues.map(i => i.number))
|
||||
if (this.activeLoad !== load || load.invalidated)
|
||||
@@ -1131,12 +1131,12 @@ export class KanbanWebviewPanel {
|
||||
|
||||
/**
|
||||
* Resolve an issue number to its source identity, for routing state
|
||||
* persistence. Unknown numbers default to gitea (back-compat / pre-load).
|
||||
* persistence. Unknown numbers default to the forge (pre-load).
|
||||
*/
|
||||
// internal: handler 模块访问
|
||||
refFor(issueNumber: number): IssueRef {
|
||||
const entry = this.issueRefs.get(issueNumber)
|
||||
return { source: entry?.source ?? 'gitea', number: issueNumber, externalId: entry?.externalId }
|
||||
return { source: entry?.source ?? 'forge', number: issueNumber, externalId: entry?.externalId }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1241,7 +1241,7 @@ export class KanbanWebviewPanel {
|
||||
const snapshot = this.kanbanState.snapshot
|
||||
const issues = snapshot?.issues ?? []
|
||||
this.me = snapshot?.me
|
||||
this.issueRefs = new Map(issues.map(issue => [issue.number, { source: issue.source ?? 'gitea', externalId: issue.externalId }]))
|
||||
this.issueRefs = new Map(issues.map(issue => [issue.number, { source: issue.source ?? 'forge', externalId: issue.externalId }]))
|
||||
this.issueOwners = new Map(issues.map(issue => [issue.number, { poster: issue.poster, assignees: issue.assignees }]))
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -217,7 +217,7 @@ export type WebviewToExtension
|
||||
| { type: 'worktree/open', path: string }
|
||||
| { type: 'worktree/delete', issueNumber: number, path: string }
|
||||
| { type: 'git/merge-preview', issueNumber: number, branch: string }
|
||||
| { type: 'column/change', issueNumber: number, toColumn: IssueColumn, source?: 'gitea' | 'youtrack', externalId?: string }
|
||||
| { type: 'column/change', issueNumber: number, toColumn: IssueColumn, source?: 'forge' | 'youtrack', externalId?: string }
|
||||
| { type: 'handoff/users', issueNumber: number }
|
||||
| { type: 'handoff/start', issueNumber: number, to: string }
|
||||
| { type: 'handoff/accept', issueNumber: number }
|
||||
|
||||
Reference in new issue
Block a user