🐛 fix(vscode): 修复看板缓存并增加问答默认模型
This commit is contained in:
1 parent
c38f1dc468
commit
012daf95b5
25 files changed
+1753
-164
No files matched your search
@@ -0,0 +1,198 @@
|
||||
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', async importOriginal => ({
|
||||
hasStateColumn: (await importOriginal<typeof import('../gitea/stateJson')>()).hasStateColumn,
|
||||
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()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user