返回 oh-my-ppt
deck-progress-stage-bounds.test.ts
根目录 / tests / unit / ipc / deck-progress-stage-bounds.test.ts
1 import { describe, expect, it, vi } from 'vitest'
2
3 vi.mock('electron', () => ({ BrowserWindow: {}, safeStorage: {} }))
4 vi.mock('@electron-toolkit/utils', () => ({ is: {} }))
5 vi.mock('electron-log/main.js', () => ({ default: { info: vi.fn(), error: vi.fn(), warn: vi.fn() } }))
6
7 import {
8 createIpcContext,
9 getDeckProgressStageBounds,
10 shouldEmitLlmStatusUpdate
11 } from '../../../src/main/ipc/context'
12 import { TypedEventBus } from '../../../src/main/agent-runtime/events/bus'
13
14 describe('getDeckProgressStageBounds', () => {
15 it('caps preflight/planning at 10 so later stages own the upper range', () => {
16 expect(getDeckProgressStageBounds('preflight')).toEqual({ min: 0, max: 10 })
17 expect(getDeckProgressStageBounds('planning')).toEqual({ min: 0, max: 10 })
18 })
19
20 it('keeps rendering inside [10, 90] so finalization can claim [90, 100]', () => {
21 expect(getDeckProgressStageBounds('rendering')).toEqual({ min: 10, max: 90 })
22 })
23
24 it('clamps unknown stages (incl. editing) to [0, 90]', () => {
25 expect(getDeckProgressStageBounds('editing')).toEqual({ min: 0, max: 90 })
26 expect(getDeckProgressStageBounds('whatever')).toEqual({ min: 0, max: 90 })
27 })
28
29 it('lets finalizing reach 100 so page-beautify milestones are not stalled at 90', () => {
30 // Regression: post-agent emits (83/87/91/95/100) used to be clamped to ≤90 by the
31 // default branch, making the bar freeze one tick short of done. The finalizing stage
32 // exists specifically to let single-page workflows complete visibly.
33 const bounds = getDeckProgressStageBounds('finalizing')
34 expect(bounds.min).toBeLessThanOrEqual(83)
35 expect(bounds.max).toBeGreaterThanOrEqual(100)
36 })
37
38 it('coalesces repetitive llm status updates while preserving meaningful progress changes', () => {
39 const previous = {
40 stage: 'editing',
41 label: '生成页面',
42 detail: '',
43 progress: 45,
44 emittedAt: 1_000
45 }
46
47 expect(
48 shouldEmitLlmStatusUpdate(previous, { ...previous, progress: 45 }, 1_200)
49 ).toBe(false)
50 expect(
51 shouldEmitLlmStatusUpdate(previous, { ...previous, progress: 46 }, 1_200)
52 ).toBe(false)
53 expect(
54 shouldEmitLlmStatusUpdate(previous, { ...previous, progress: 47 }, 1_200)
55 ).toBe(false)
56 expect(
57 shouldEmitLlmStatusUpdate(previous, { ...previous, progress: 50 }, 1_200)
58 ).toBe(true)
59 expect(
60 shouldEmitLlmStatusUpdate(previous, { ...previous, label: '校验页面' }, 1_200)
61 ).toBe(true)
62 expect(
63 shouldEmitLlmStatusUpdate(previous, { ...previous, progress: 45 }, 1_800)
64 ).toBe(false)
65 })
66
67 it('separates streamed chunks from persisted lifecycle events on the typed runtime bus', () => {
68 const eventBus = new TypedEventBus()
69 const events: Array<{ type: string; domain: string; jobId: string }> = []
70 eventBus.subscribe({}, (event) => events.push(event))
71 const context = createIpcContext(
72 {
73 isDestroyed: () => false,
74 isMinimized: () => false,
75 isVisible: () => true,
76 restore: vi.fn(),
77 show: vi.fn(),
78 focus: vi.fn()
79 } as never,
80 {} as never,
81 {} as never,
82 eventBus
83 )
84
85 context.beginSessionRunState({
86 sessionId: 'session-1',
87 runId: 'run-1',
88 mode: 'generate',
89 totalPages: 1,
90 status: 'running'
91 })
92 context.emitGenerateChunk('session-1', {
93 type: 'run_error',
94 payload: { runId: 'run-1', message: '生成已取消', cancelled: true }
95 })
96
97 expect(events).toEqual([
98 expect.objectContaining({ type: 'job.started', domain: 'generation', jobId: 'run-1' }),
99 expect.objectContaining({ type: 'generation.chunk', domain: 'generation', jobId: 'run-1' })
100 ])
101
102 context.emitRuntimeJobTerminal({
103 sessionId: 'session-1',
104 jobId: 'run-1',
105 status: 'cancelled'
106 })
107
108 expect(events).toEqual([
109 expect.objectContaining({ type: 'job.started', domain: 'generation', jobId: 'run-1' }),
110 expect.objectContaining({ type: 'generation.chunk', domain: 'generation', jobId: 'run-1' }),
111 expect.objectContaining({ type: 'job.cancelled', domain: 'generation', jobId: 'run-1' })
112 ])
113 })
114 })
115
115 lines TYPESCRIPT