198 lines
9.6 KiB
TypeScript
198 lines
9.6 KiB
TypeScript
import type { ExtensionContext } from 'vscode'
|
|
import type { KanbanWebviewPanel } from '../panel/KanbanPanel'
|
|
import type { WebhookEvent } from './eventParser'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { KANBAN_SNAPSHOT_KEY } from '../panel/kanbanSnapshot'
|
|
import { getKanbanState } from '../panel/kanbanState'
|
|
import { webhookCoordinator } from './coordinator'
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
receive: undefined as ((event: WebhookEvent) => void) | undefined,
|
|
token: vi.fn(),
|
|
loadSingle: vi.fn(),
|
|
readState: vi.fn(),
|
|
log: vi.fn(),
|
|
}))
|
|
vi.mock('vscode', () => ({ env: {}, Uri: {}, window: { showInformationMessage: async () => undefined }, workspace: { workspaceFolders: [{ uri: { fsPath: '/repo' } }] } }))
|
|
vi.mock('../logging/logger', () => ({ logger: { add: mocks.log } }))
|
|
vi.mock('../auth/secrets', () => ({ getToken: mocks.token }))
|
|
vi.mock('../auth/identity', () => ({ loginForToken: async () => 'me' }))
|
|
vi.mock('../cc/prompts', () => ({ renderReviewPrompt: () => 'review' }))
|
|
vi.mock('../git/remote', () => ({ detectRepo: async () => ({ host: 'host', owner: 'owner', repo: 'repo' }) }))
|
|
vi.mock('../gitea/api', () => ({ getIssue: async () => ({ assignees: [{ login: 'me' }] }) }))
|
|
vi.mock('../gitea/issueLoader', () => ({ loadSingleIssue: mocks.loadSingle }))
|
|
vi.mock('../gitea/stateJson', () => ({
|
|
readStateJsonComment: mocks.readState,
|
|
mergeStateJsonComment: async () => undefined,
|
|
}))
|
|
vi.mock('../issues/localState', () => ({ overlayLocalIssueState: (_ctx: unknown, issues: unknown[]) => issues, getLocalIssueState: () => ({}) }))
|
|
vi.mock('../panel/handlers/issueChat', () => ({}))
|
|
vi.mock('../panel/handlers/terminals', () => ({ hasLiveIssueSessionTerminal: () => false }))
|
|
vi.mock('../sessions/sessionFiles', () => ({ annotateMissingSessionIds: async (issues: unknown[]) => issues }))
|
|
vi.mock('../settings/store', () => ({ getSettings: () => ({ webhookSecret: '', webhookPort: 12345, devBranch: 'main', releaseBranchPrefix: 'release/', autoReview: true }) }))
|
|
vi.mock('./server', () => ({
|
|
WebhookServer: class {
|
|
setSecret() {}
|
|
start() { return Promise.resolve() }
|
|
stop() { return Promise.resolve() }
|
|
onEvent(receive: (event: WebhookEvent) => void) {
|
|
mocks.receive = receive
|
|
return { dispose() {} }
|
|
}
|
|
},
|
|
}))
|
|
|
|
const cached = { issues: [], scope: 'all', globalAutoReview: false, youtrackConfigured: false }
|
|
const events: WebhookEvent[] = [
|
|
{ kind: 'issue', action: 'closed', issueNumber: 462, title: '', body: '', htmlUrl: '', raw: {} },
|
|
{ kind: 'issue_comment', action: 'created', issueNumber: 462, prNumber: undefined, commentBody: '{"column":"done"}', commentAuthor: 'me', commentHtmlUrl: '', raw: {} },
|
|
{ kind: 'pr', action: 'closed', issueNumber: 462, pr: '470', branch: '', title: '', body: '', htmlUrl: '', raw: {} },
|
|
]
|
|
|
|
function fixture() {
|
|
const disk = new Map<string, unknown>([[KANBAN_SNAPSHOT_KEY, cached]])
|
|
const update = vi.fn(async (key: string, value: unknown) => {
|
|
if (value === undefined)
|
|
disk.delete(key)
|
|
else
|
|
disk.set(key, value)
|
|
})
|
|
const context = { workspaceState: { get: (key: string) => disk.get(key), update } } as unknown as ExtensionContext
|
|
webhookCoordinator.init(context)
|
|
return { disk, update, context }
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mocks.token.mockReset().mockResolvedValue(undefined)
|
|
mocks.loadSingle.mockReset()
|
|
mocks.readState.mockReset().mockResolvedValue({ column: 'review', autoReview: true })
|
|
})
|
|
afterEach(async () => {
|
|
await webhookCoordinator.dispose()
|
|
})
|
|
|
|
describe('coordinator cache invalidation', () => {
|
|
it.each(events)('无面板收到 $kind 后删除持久快照', async (event) => {
|
|
const f = fixture()
|
|
mocks.receive!(event)
|
|
await vi.waitFor(() => expect(f.disk.has(KANBAN_SNAPSHOT_KEY)).toBe(false))
|
|
expect(f.update).toHaveBeenCalledWith(KANBAN_SNAPSHOT_KEY, undefined)
|
|
})
|
|
|
|
it('无面板事件立即清共享内存,删除必须等待正在保存的快照', async () => {
|
|
const f = fixture()
|
|
let release!: () => void
|
|
const gate = new Promise<void>((resolve) => {
|
|
release = resolve
|
|
})
|
|
f.update.mockImplementationOnce(async (key, value) => {
|
|
await gate
|
|
f.disk.set(key, value)
|
|
})
|
|
const state = getKanbanState(f.context.workspaceState)
|
|
const saving = state.apply({ type: 'issues/update', issues: [], scope: 'mine', globalAutoReview: true, youtrackConfigured: false })
|
|
await vi.waitFor(() => expect(f.update).toHaveBeenCalledOnce())
|
|
mocks.receive!(events[0])
|
|
expect(getKanbanState(f.context.workspaceState).snapshot).toBeUndefined()
|
|
expect(f.update).toHaveBeenCalledOnce()
|
|
release()
|
|
await saving
|
|
await vi.waitFor(() => expect(f.disk.has(KANBAN_SNAPSHOT_KEY)).toBe(false))
|
|
expect(f.update).toHaveBeenNthCalledWith(2, KANBAN_SNAPSHOT_KEY, undefined)
|
|
})
|
|
|
|
it.each(['edited', 'deleted'])('无面板收到状态评论 %s 清除缓存', async (action) => {
|
|
const f = fixture()
|
|
const state = getKanbanState(f.context.workspaceState)
|
|
mocks.receive!({ kind: 'issue_comment', action, issueNumber: 462, prNumber: undefined, commentBody: '{"column":"done"}', commentAuthor: 'me', commentHtmlUrl: '', raw: {} })
|
|
expect(state.snapshot).toBeUndefined()
|
|
await vi.waitFor(() => expect(f.disk.has(KANBAN_SNAPSHOT_KEY)).toBe(false))
|
|
})
|
|
|
|
it.each(['created', 'edited', 'deleted'])('无面板收到普通讨论 %s 保留有效缓存', async (action) => {
|
|
const f = fixture()
|
|
const state = getKanbanState(f.context.workspaceState)
|
|
const snapshot = state.snapshot
|
|
mocks.receive!({ kind: 'issue_comment', action, issueNumber: 462, prNumber: undefined, commentBody: '谢谢', commentAuthor: 'other', commentHtmlUrl: '', raw: {} })
|
|
await Promise.resolve()
|
|
expect(state.snapshot).toBe(snapshot)
|
|
expect(f.disk.get(KANBAN_SNAPSHOT_KEY)).toBe(cached)
|
|
expect(f.update).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('读取单卡期间关闭面板,事件结束后不保留过期快照', async () => {
|
|
const f = fixture()
|
|
const state = getKanbanState(f.context.workspaceState)
|
|
let release!: (value: unknown) => void
|
|
mocks.token.mockResolvedValue('token')
|
|
mocks.loadSingle.mockReturnValue(new Promise((resolve) => {
|
|
release = resolve
|
|
}))
|
|
const panel = { noteIssueOwnership: vi.fn(), postMessage: vi.fn() } as unknown as KanbanWebviewPanel
|
|
webhookCoordinator.setActivePanel(panel)
|
|
mocks.receive!(events[0])
|
|
await vi.waitFor(() => expect(mocks.loadSingle).toHaveBeenCalledOnce())
|
|
webhookCoordinator.setActivePanel(undefined)
|
|
release({ id: 'repo#462', number: 462, title: 'merged', column: 'done', htmlUrl: '' })
|
|
await vi.waitFor(() => expect(state.snapshot).toBeUndefined())
|
|
await vi.waitFor(() => expect(f.disk.has(KANBAN_SNAPSHOT_KEY)).toBe(false))
|
|
})
|
|
|
|
it('同一面板持续打开时仍可启动自动审查', async () => {
|
|
fixture()
|
|
mocks.token.mockResolvedValue('token')
|
|
const panel = { postMessage: vi.fn(), triggerAutoReviewTab: vi.fn().mockResolvedValue(undefined) }
|
|
webhookCoordinator.setActivePanel(panel as unknown as KanbanWebviewPanel)
|
|
mocks.receive!({ kind: 'pr', action: 'opened', issueNumber: 462, pr: '470', branch: 'feature', title: '', body: 'Closes #462', htmlUrl: '', raw: {} })
|
|
await vi.waitFor(() => expect(panel.triggerAutoReviewTab).toHaveBeenCalledOnce())
|
|
})
|
|
|
|
it.each([
|
|
{ pendingRead: 1, reopen: false },
|
|
{ pendingRead: 1, reopen: true },
|
|
{ pendingRead: 2, reopen: false },
|
|
{ pendingRead: 2, reopen: true },
|
|
{ pendingRead: 3, reopen: false },
|
|
{ pendingRead: 3, reopen: true },
|
|
])('自动审查第 $pendingRead 次状态读取期间关闭,重开=$reopen,不启动或转移旧任务', async ({ pendingRead, reopen }) => {
|
|
fixture()
|
|
let release!: (value: Record<string, unknown>) => void
|
|
const pending = new Promise<Record<string, unknown>>((resolve) => {
|
|
release = resolve
|
|
})
|
|
let reads = 0
|
|
mocks.token.mockResolvedValue('token')
|
|
mocks.readState.mockImplementation(() => ++reads === pendingRead ? pending : Promise.resolve({ column: 'review', autoReview: true }))
|
|
const first = { postMessage: vi.fn(), triggerAutoReviewTab: vi.fn().mockResolvedValue(undefined) }
|
|
const second = { postMessage: vi.fn(), triggerAutoReviewTab: vi.fn().mockResolvedValue(undefined), reloadAfterInvalidation: vi.fn().mockResolvedValue(undefined) }
|
|
webhookCoordinator.setActivePanel(first as unknown as KanbanWebviewPanel)
|
|
mocks.receive!({ kind: 'pr', action: 'opened', issueNumber: 462, pr: '470', branch: 'feature', title: '', body: 'Closes #462', htmlUrl: '', raw: {} })
|
|
await vi.waitFor(() => expect(mocks.readState).toHaveBeenCalledTimes(pendingRead))
|
|
webhookCoordinator.setActivePanel(undefined)
|
|
if (reopen)
|
|
webhookCoordinator.setActivePanel(second as unknown as KanbanWebviewPanel)
|
|
release({ column: 'review', autoReview: true })
|
|
await vi.waitFor(() => expect(mocks.log.mock.calls.some(([entry]) => entry.message.includes('跳过审查 #462') || entry.message.includes('审查 tab 已就绪 #462'))).toBe(true))
|
|
expect(first.triggerAutoReviewTab).not.toHaveBeenCalled()
|
|
expect(second.triggerAutoReviewTab).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('普通 push 不清看板', async () => {
|
|
const f = fixture()
|
|
mocks.receive!({ kind: 'push', branch: 'main', raw: {} })
|
|
await Promise.resolve()
|
|
expect(f.disk.get(KANBAN_SNAPSHOT_KEY)).toBe(cached)
|
|
expect(f.update).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('有面板时保留快照,由现有事件处理路径更新', async () => {
|
|
const f = fixture()
|
|
webhookCoordinator.setActivePanel({} as KanbanWebviewPanel)
|
|
mocks.receive!(events[2])
|
|
await Promise.resolve()
|
|
expect(f.disk.get(KANBAN_SNAPSHOT_KEY)).toBe(cached)
|
|
expect(f.update).not.toHaveBeenCalled()
|
|
})
|
|
})
|