返回 oh-my-ppt
generate-store-page-edit.test.ts
根目录 / tests / unit / session / generate-store-page-edit.test.ts
1 import { beforeEach, describe, expect, it } from 'vitest'
2 import {
3 hydrateStyleSwitchJob,
4 isStyleSwitchJobActive,
5 isStyleSwitchPageLocked,
6 useGenerateStore
7 } from '../../../src/renderer/src/store/generateStore'
8
9 describe('generateStore page edit job', () => {
10 const sessionId = 'session-1'
11
12 beforeEach(() => {
13 useGenerateStore.getState().reset()
14 })
15
16 it('tracks a page edit without enabling the global generation lock', () => {
17 useGenerateStore.getState().startPageEdit(sessionId, { pageId: 'page-2', pageNumber: 2 })
18 useGenerateStore.getState().updatePageEdit(sessionId, {
19 runId: 'run-edit-1',
20 status: 'running',
21 label: '正在编辑第 2 页',
22 progress: 55
23 })
24
25 expect(useGenerateStore.getState()).toMatchObject({
26 isGenerating: false,
27 pageEditJobs: {
28 [sessionId]: {
29 pageId: 'page-2',
30 pageNumber: 2,
31 runId: 'run-edit-1',
32 status: 'running',
33 progress: 55
34 }
35 }
36 })
37
38 useGenerateStore.getState().finishPageEdit(sessionId)
39 expect(useGenerateStore.getState().pageEditJobs[sessionId]).toBeUndefined()
40 })
41
42 it('keeps a page-edit plan pending without creating a page job', () => {
43 useGenerateStore.getState().startPageEditPlanning(sessionId, 'page-2')
44 useGenerateStore.getState().setPendingPageEditPlan(sessionId, {
45 targetPageId: 'page-2',
46 targetPageNumber: 2,
47 payload: {
48 sessionId: 'session-1',
49 userMessage: '把标题改成更简洁的表达',
50 type: 'page',
51 chatType: 'page',
52 selectedPageId: 'page-2'
53 },
54 plan: {
55 intent: 'content',
56 target: '第 2 页标题',
57 summary: '精简标题并保持现有布局。',
58 changes: ['缩短标题文案'],
59 confirmationQuestion: '确认按此计划修改吗?'
60 }
61 })
62 useGenerateStore.getState().finishPageEditPlanning(sessionId)
63
64 expect(useGenerateStore.getState()).toMatchObject({
65 isGenerating: false,
66 pageEditJobs: {},
67 pageEditPlanning: {
68 [sessionId]: {
69 isAssessing: false,
70 pendingPlan: {
71 targetPageId: 'page-2',
72 plan: { intent: 'content' }
73 }
74 }
75 }
76 })
77
78 useGenerateStore.getState().clearPendingPageEditPlan(sessionId)
79 expect(useGenerateStore.getState().pageEditPlanning[sessionId]?.pendingPlan).toBeNull()
80 })
81
82 it('tracks a deck edit by session without clearing the current pages', () => {
83 useGenerateStore.getState().setPages([
84 {
85 id: 'page-record-1',
86 pageId: 'page-1',
87 pageNumber: 1,
88 title: 'Page 1',
89 html: '<div>Page</div>'
90 }
91 ])
92 useGenerateStore.getState().startDeckEdit(sessionId, { totalPages: 3 })
93 useGenerateStore.getState().updateDeckEdit(sessionId, {
94 runId: 'deck-edit-run-1',
95 status: 'running',
96 label: '正在编辑主会话',
97 progress: 40,
98 totalPages: 3
99 })
100
101 expect(useGenerateStore.getState()).toMatchObject({
102 isGenerating: false,
103 currentPages: [{ pageId: 'page-1' }],
104 deckEditJobs: {
105 [sessionId]: {
106 runId: 'deck-edit-run-1',
107 totalPages: 3,
108 progress: 40
109 }
110 }
111 })
112
113 useGenerateStore.getState().finishDeckEdit(sessionId)
114 expect(useGenerateStore.getState()).toMatchObject({
115 isGenerating: false,
116 deckEditJobs: {},
117 currentPages: [{ pageId: 'page-1' }]
118 })
119 })
120
121 it('keeps jobs and plans isolated between sessions', () => {
122 useGenerateStore.getState().startPageEdit('session-a', { pageId: 'page-a', pageNumber: 1 })
123 useGenerateStore.getState().startDeckEdit('session-b', { totalPages: 2 })
124 useGenerateStore.getState().setPendingPageEditPlan('session-a', {
125 targetPageId: 'page-a',
126 payload: { sessionId: 'session-a', userMessage: '改标题', type: 'page', chatType: 'page' },
127 plan: {
128 intent: 'content',
129 target: '标题',
130 summary: '更新标题',
131 changes: ['替换标题文本'],
132 confirmationQuestion: '确认修改吗?'
133 }
134 })
135
136 expect(useGenerateStore.getState().pageEditJobs['session-b']).toBeUndefined()
137 expect(useGenerateStore.getState().deckEditJobs['session-a']).toBeUndefined()
138 expect(useGenerateStore.getState().pageEditPlanning['session-b']).toBeUndefined()
139 expect(useGenerateStore.getState().pageEditPlanning['session-a']?.pendingPlan).not.toBeNull()
140 })
141
142 it('does not let an older assessment finish a newer assessment', () => {
143 useGenerateStore.getState().startPageEditPlanning(sessionId, 'page-2', 'assessment-old')
144 useGenerateStore.getState().startPageEditPlanning(sessionId, 'page-2', 'assessment-new')
145
146 useGenerateStore.getState().finishPageEditPlanning(sessionId, 'assessment-old')
147 expect(useGenerateStore.getState().pageEditPlanning[sessionId]).toMatchObject({
148 assessmentId: 'assessment-new',
149 isAssessing: true
150 })
151
152 useGenerateStore.getState().finishPageEditPlanning(sessionId, 'assessment-new')
153 expect(useGenerateStore.getState().pageEditPlanning[sessionId]?.isAssessing).toBe(false)
154 })
155
156 it('keeps session errors isolated and clears only the session that starts a new job', () => {
157 useGenerateStore.getState().setSessionError('session-a', 'A failed')
158 useGenerateStore.getState().setSessionError('session-b', 'B failed')
159
160 useGenerateStore.getState().startPageEdit('session-a', { pageId: 'page-a', pageNumber: 1 })
161
162 expect(useGenerateStore.getState().sessionErrors).toEqual({
163 'session-b': 'B failed'
164 })
165
166 useGenerateStore.getState().clearSessionError('session-b')
167 expect(useGenerateStore.getState().sessionErrors).toEqual({})
168 })
169
170 it('keeps a cancelling edit job locked until a terminal event removes it', () => {
171 useGenerateStore.getState().startDeckEdit(sessionId, { totalPages: 2 })
172 useGenerateStore.getState().updateDeckEdit(sessionId, {
173 status: 'cancelling',
174 label: '正在取消并恢复修改前内容'
175 })
176
177 expect(useGenerateStore.getState().deckEditJobs[sessionId]).toMatchObject({
178 status: 'cancelling'
179 })
180
181 useGenerateStore.getState().finishDeckEdit(sessionId)
182 expect(useGenerateStore.getState().deckEditJobs[sessionId]).toBeUndefined()
183 })
184
185 it('unlocks only pages with a committed style-switch result', () => {
186 useGenerateStore.getState().startStyleSwitch(sessionId, {
187 styleId: 'style-new',
188 styleName: 'New style',
189 totalPages: 2,
190 pages: [
191 {
192 pageId: 'page-1',
193 pageNumber: 1,
194 title: 'Page 1',
195 status: 'pending',
196 error: null,
197 retryCount: 0
198 },
199 {
200 pageId: 'page-2',
201 pageNumber: 2,
202 title: 'Page 2',
203 status: 'pending',
204 error: null,
205 retryCount: 0
206 }
207 ]
208 })
209 useGenerateStore.getState().updateStyleSwitchJob(sessionId, {
210 runId: 'style-run-1',
211 status: 'running'
212 })
213 const job = useGenerateStore.getState().styleSwitchJobs[sessionId]
214
215 expect(isStyleSwitchJobActive(job)).toBe(true)
216 expect(isStyleSwitchPageLocked(job, 'page-1')).toBe(true)
217 expect(isStyleSwitchPageLocked(job, 'page-2')).toBe(true)
218
219 useGenerateStore.getState().updateStyleSwitchPage(sessionId, 'page-1', {
220 status: 'completed',
221 error: null
222 })
223 const afterFirstCommit = useGenerateStore.getState().styleSwitchJobs[sessionId]
224 expect(isStyleSwitchPageLocked(afterFirstCommit, 'page-1')).toBe(false)
225 expect(isStyleSwitchPageLocked(afterFirstCommit, 'page-2')).toBe(true)
226
227 useGenerateStore.getState().finishStyleSwitch(sessionId, { status: 'partial', error: null })
228 expect(isStyleSwitchJobActive(useGenerateStore.getState().styleSwitchJobs[sessionId])).toBe(
229 false
230 )
231 expect(
232 isStyleSwitchPageLocked(useGenerateStore.getState().styleSwitchJobs[sessionId], 'page-2')
233 ).toBe(true)
234 })
235
236 it('replaces an optimistic style-switch job with the persisted active run', () => {
237 useGenerateStore.getState().startStyleSwitch(sessionId, {
238 styleId: 'style-new',
239 totalPages: 1,
240 pages: [
241 {
242 pageId: 'page-1',
243 pageNumber: 1,
244 title: 'Optimistic page',
245 status: 'pending',
246 error: null,
247 retryCount: 0
248 }
249 ]
250 })
251
252 hydrateStyleSwitchJob(sessionId, {
253 runId: 'persisted-run',
254 status: 'running',
255 progress: 40,
256 totalPages: 2,
257 targetStyleId: 'style-existing',
258 targetStyleName: 'Existing style',
259 error: null,
260 pages: [
261 {
262 pageId: 'page-1',
263 pageNumber: 1,
264 title: 'Persisted page',
265 status: 'completed',
266 error: null,
267 retryCount: 1
268 },
269 {
270 pageId: 'page-2',
271 pageNumber: 2,
272 title: 'Running page',
273 status: 'running',
274 error: null,
275 retryCount: 0
276 }
277 ]
278 })
279
280 expect(useGenerateStore.getState().styleSwitchJobs[sessionId]).toMatchObject({
281 runId: 'persisted-run',
282 status: 'running',
283 styleId: 'style-existing',
284 totalPages: 2
285 })
286 expect(
287 isStyleSwitchPageLocked(useGenerateStore.getState().styleSwitchJobs[sessionId], 'page-1')
288 ).toBe(false)
289 expect(
290 isStyleSwitchPageLocked(useGenerateStore.getState().styleSwitchJobs[sessionId], 'page-2')
291 ).toBe(true)
292 })
293 })
294
294 lines TYPESCRIPT