返回 oh-my-ppt
session-jobs.test.ts
根目录 / tests / unit / db / session-jobs.test.ts
1 import { mkdtemp, rm } from 'node:fs/promises'
2 import os from 'node:os'
3 import path from 'node:path'
4 import { createClient } from '@libsql/client'
5 import { afterEach, describe, expect, it, vi } from 'vitest'
6
7 vi.mock('electron', () => ({
8 app: {
9 getPath: vi.fn(() => path.join(os.tmpdir(), 'ohmyppt-test-user-data'))
10 }
11 }))
12
13 vi.mock('@electron-toolkit/utils', () => ({
14 is: { dev: true }
15 }))
16
17 import { PPTDatabase } from '../../../src/main/db/database'
18
19 describe('session jobs', () => {
20 const roots: string[] = []
21
22 afterEach(async () => {
23 for (const root of roots.splice(0)) {
24 await rm(root, { recursive: true, force: true })
25 }
26 })
27
28 it('creates the run and unified page-edit job atomically', async () => {
29 const root = await mkdtemp(path.join(os.tmpdir(), 'ohmyppt-session-jobs-'))
30 roots.push(root)
31 const db = new PPTDatabase(path.join(root, 'test.db'))
32 await db.init()
33
34 try {
35 const sessionId = await db.createSession({
36 id: 'session-job-1',
37 title: 'Job test',
38 slideSizeId: 'wide-16-9',
39 slideWidth: 1600,
40 slideHeight: 900,
41 provider: 'test',
42 model: 'test-model'
43 })
44 await db.createGenerationRunWithSessionJob({
45 run: {
46 id: 'run-page-edit-1',
47 sessionId,
48 mode: 'edit',
49 totalPages: 1,
50 modelConfigId: 'model-1',
51 metadata: { jobType: 'page-edit' }
52 },
53 job: {
54 id: 'run-page-edit-1',
55 sessionId,
56 kind: 'page-edit',
57 status: 'active',
58 previousSessionStatus: 'completed',
59 targetPageId: 'page-1',
60 targetPageNumber: 1,
61 selector: '#title',
62 totalPages: 1
63 }
64 })
65
66 await expect(db.getGenerationRun('run-page-edit-1')).resolves.toMatchObject({
67 session_id: sessionId,
68 mode: 'edit',
69 status: 'running'
70 })
71 await expect(db.getLatestSessionJob(sessionId, ['page-edit'])).resolves.toMatchObject({
72 id: 'run-page-edit-1',
73 kind: 'page-edit',
74 previous_session_status: 'completed',
75 target_page_id: 'page-1',
76 target_page_number: 1,
77 selector: '#title',
78 status: 'active'
79 })
80 await expect(db.listActiveSessionJobs(['page-edit'])).resolves.toHaveLength(1)
81
82 await db.updateSessionJobStatus('run-page-edit-1', 'finished')
83 await expect(db.listActiveSessionJobs(['page-edit'])).resolves.toEqual([])
84 } finally {
85 await db.close()
86 }
87 })
88
89 it('migrates a legacy generation job into session_jobs during startup', async () => {
90 const root = await mkdtemp(path.join(os.tmpdir(), 'ohmyppt-session-jobs-migration-'))
91 roots.push(root)
92 const dbPath = path.join(root, 'test.db')
93 const client = createClient({ url: `file:${dbPath}` })
94 await client.executeMultiple(`
95 CREATE TABLE sessions (
96 id TEXT PRIMARY KEY,
97 title TEXT NOT NULL,
98 topic TEXT,
99 style_id TEXT,
100 page_count INTEGER,
101 slide_size_id TEXT NOT NULL DEFAULT 'wide-16-9',
102 slide_width INTEGER NOT NULL DEFAULT 1600,
103 slide_height INTEGER NOT NULL DEFAULT 900,
104 reference_document_path TEXT,
105 status TEXT NOT NULL DEFAULT 'active',
106 provider TEXT NOT NULL,
107 model TEXT NOT NULL,
108 created_at INTEGER NOT NULL,
109 updated_at INTEGER NOT NULL,
110 metadata TEXT,
111 design_contract TEXT,
112 current_operation_id TEXT,
113 current_commit TEXT
114 );
115 CREATE TABLE generation_runs (
116 id TEXT PRIMARY KEY,
117 session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
118 mode TEXT NOT NULL DEFAULT 'generate',
119 status TEXT NOT NULL DEFAULT 'running',
120 total_pages INTEGER NOT NULL DEFAULT 0,
121 error TEXT,
122 metadata TEXT,
123 created_at INTEGER NOT NULL,
124 updated_at INTEGER NOT NULL
125 );
126 CREATE TABLE generation_jobs (
127 id TEXT PRIMARY KEY REFERENCES generation_runs(id) ON DELETE CASCADE,
128 session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
129 kind TEXT NOT NULL,
130 status TEXT NOT NULL,
131 created_at INTEGER NOT NULL,
132 updated_at INTEGER NOT NULL
133 );
134 CREATE TABLE page_edit_jobs (
135 id TEXT PRIMARY KEY REFERENCES generation_runs(id) ON DELETE CASCADE,
136 session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
137 target_page_id TEXT NOT NULL,
138 target_page_number INTEGER,
139 selector TEXT,
140 previous_session_status TEXT NOT NULL,
141 status TEXT NOT NULL,
142 abort_reason TEXT,
143 created_at INTEGER NOT NULL,
144 activated_at INTEGER,
145 updated_at INTEGER NOT NULL,
146 finished_at INTEGER
147 );
148 CREATE TABLE deck_edit_jobs (
149 id TEXT PRIMARY KEY REFERENCES generation_runs(id) ON DELETE CASCADE,
150 session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
151 previous_session_status TEXT NOT NULL,
152 total_pages INTEGER NOT NULL,
153 status TEXT NOT NULL,
154 abort_reason TEXT,
155 created_at INTEGER NOT NULL,
156 activated_at INTEGER,
157 updated_at INTEGER NOT NULL,
158 finished_at INTEGER
159 );
160 INSERT INTO sessions (
161 id, title, status, provider, model, created_at, updated_at
162 ) VALUES ('legacy-session', 'Legacy', 'active', 'test', 'test-model', 1, 1);
163 INSERT INTO generation_runs (
164 id, session_id, mode, status, total_pages, metadata, created_at, updated_at
165 ) VALUES (
166 'legacy-run', 'legacy-session', 'generate', 'running', 3,
167 '{"previousSessionStatus":"completed"}', 1, 1
168 );
169 INSERT INTO generation_jobs (id, session_id, kind, status, created_at, updated_at)
170 VALUES ('legacy-run', 'legacy-session', 'standard', 'active', 1, 1);
171 INSERT INTO generation_runs (
172 id, session_id, mode, status, total_pages, created_at, updated_at
173 ) VALUES ('legacy-page-run', 'legacy-session', 'edit', 'running', 1, 1, 1);
174 INSERT INTO page_edit_jobs (
175 id, session_id, target_page_id, target_page_number, selector, previous_session_status,
176 status, created_at, updated_at
177 ) VALUES (
178 'legacy-page-run', 'legacy-session', 'page-2', 2, '#title', 'failed', 'active', 2, 2
179 );
180 INSERT INTO generation_jobs (id, session_id, kind, status, created_at, updated_at)
181 VALUES ('legacy-page-run', 'legacy-session', 'edit', 'active', 2, 2);
182 INSERT INTO generation_runs (
183 id, session_id, mode, status, total_pages, created_at, updated_at
184 ) VALUES ('legacy-deck-run', 'legacy-session', 'edit', 'running', 4, 1, 1);
185 INSERT INTO deck_edit_jobs (
186 id, session_id, previous_session_status, total_pages, status, created_at, updated_at
187 ) VALUES ('legacy-deck-run', 'legacy-session', 'completed', 4, 'active', 3, 3);
188 INSERT INTO generation_jobs (id, session_id, kind, status, created_at, updated_at)
189 VALUES ('legacy-deck-run', 'legacy-session', 'standard', 'active', 3, 3);
190 INSERT INTO page_edit_jobs (
191 id, session_id, target_page_id, target_page_number, selector, previous_session_status,
192 status, created_at, updated_at
193 ) VALUES (
194 'legacy-deck-run', 'legacy-session', 'page-4', 4, '#deck-title', 'failed', 'active', 3, 3
195 );
196 `)
197 client.close()
198
199 const db = new PPTDatabase(dbPath)
200 await db.init()
201 try {
202 await expect(db.getLatestSessionJob('legacy-session')).resolves.toMatchObject({
203 id: 'legacy-deck-run',
204 kind: 'deck-edit',
205 previous_session_status: 'completed',
206 status: 'active',
207 total_pages: 4,
208 target_page_id: null
209 })
210 await expect(db.getSessionJob('legacy-run')).resolves.toMatchObject({
211 kind: 'standard',
212 previous_session_status: 'completed',
213 total_pages: 3
214 })
215 await expect(db.getSessionJob('legacy-page-run')).resolves.toMatchObject({
216 kind: 'page-edit',
217 previous_session_status: 'failed',
218 target_page_id: 'page-2',
219 target_page_number: 2,
220 selector: '#title'
221 })
222 } finally {
223 await db.close()
224 }
225 })
226
227 it('keeps legacy job tables intact when a transactional migration fails', async () => {
228 const root = await mkdtemp(path.join(os.tmpdir(), 'ohmyppt-session-jobs-rollback-'))
229 roots.push(root)
230 const dbPath = path.join(root, 'test.db')
231 const client = createClient({ url: `file:${dbPath}` })
232 await client.executeMultiple(`
233 CREATE TABLE sessions (
234 id TEXT PRIMARY KEY,
235 title TEXT NOT NULL,
236 topic TEXT,
237 style_id TEXT,
238 page_count INTEGER,
239 slide_size_id TEXT NOT NULL DEFAULT 'wide-16-9',
240 slide_width INTEGER NOT NULL DEFAULT 1600,
241 slide_height INTEGER NOT NULL DEFAULT 900,
242 reference_document_path TEXT,
243 status TEXT NOT NULL DEFAULT 'active',
244 provider TEXT NOT NULL,
245 model TEXT NOT NULL,
246 created_at INTEGER NOT NULL,
247 updated_at INTEGER NOT NULL,
248 metadata TEXT,
249 design_contract TEXT,
250 current_operation_id TEXT,
251 current_commit TEXT
252 );
253 CREATE TABLE generation_runs (
254 id TEXT PRIMARY KEY,
255 session_id TEXT NOT NULL,
256 mode TEXT NOT NULL DEFAULT 'generate',
257 status TEXT NOT NULL DEFAULT 'running',
258 total_pages INTEGER NOT NULL DEFAULT 0,
259 error TEXT,
260 metadata TEXT,
261 created_at INTEGER NOT NULL,
262 updated_at INTEGER NOT NULL
263 );
264 CREATE TABLE generation_jobs (
265 id TEXT PRIMARY KEY,
266 session_id TEXT NOT NULL,
267 kind TEXT NOT NULL,
268 status TEXT NOT NULL,
269 created_at INTEGER NOT NULL,
270 updated_at INTEGER NOT NULL
271 );
272 CREATE TABLE session_jobs (
273 id TEXT PRIMARY KEY,
274 session_id TEXT NOT NULL,
275 kind TEXT NOT NULL CHECK(kind = 'blocked'),
276 previous_session_status TEXT NOT NULL,
277 target_page_id TEXT,
278 target_page_number INTEGER,
279 selector TEXT,
280 total_pages INTEGER,
281 status TEXT NOT NULL,
282 abort_reason TEXT,
283 created_at INTEGER NOT NULL,
284 activated_at INTEGER,
285 updated_at INTEGER NOT NULL,
286 finished_at INTEGER
287 );
288 INSERT INTO sessions (id, title, provider, model, created_at, updated_at)
289 VALUES ('rollback-session', 'Rollback', 'test', 'test-model', 1, 1);
290 INSERT INTO generation_runs (id, session_id, mode, status, total_pages, created_at, updated_at)
291 VALUES ('rollback-run', 'rollback-session', 'generate', 'running', 1, 1, 1);
292 INSERT INTO generation_jobs (id, session_id, kind, status, created_at, updated_at)
293 VALUES ('rollback-run', 'rollback-session', 'standard', 'active', 1, 1);
294 `)
295 client.close()
296
297 const db = new PPTDatabase(dbPath)
298 try {
299 await expect(db.init()).rejects.toThrow()
300 } finally {
301 await db.close()
302 }
303
304 const verificationClient = createClient({ url: `file:${dbPath}` })
305 try {
306 const legacyJobs = await verificationClient.execute('SELECT id FROM generation_jobs')
307 const migratedJobs = await verificationClient.execute('SELECT id FROM session_jobs')
308 expect(legacyJobs.rows).toHaveLength(1)
309 expect(migratedJobs.rows).toHaveLength(0)
310 } finally {
311 verificationClient.close()
312 }
313 })
314 })
315
315 lines TYPESCRIPT