opencastle 0.21.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/convoy/engine.d.ts +4 -0
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +23 -1
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +148 -0
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/export.d.ts +1 -0
- package/dist/cli/convoy/export.d.ts.map +1 -1
- package/dist/cli/convoy/export.js +39 -0
- package/dist/cli/convoy/export.js.map +1 -1
- package/dist/cli/convoy/export.test.js +31 -0
- package/dist/cli/convoy/export.test.js.map +1 -1
- package/dist/cli/convoy/pipeline.d.ts +35 -0
- package/dist/cli/convoy/pipeline.d.ts.map +1 -0
- package/dist/cli/convoy/pipeline.js +353 -0
- package/dist/cli/convoy/pipeline.js.map +1 -0
- package/dist/cli/convoy/pipeline.test.d.ts +2 -0
- package/dist/cli/convoy/pipeline.test.d.ts.map +1 -0
- package/dist/cli/convoy/pipeline.test.js +778 -0
- package/dist/cli/convoy/pipeline.test.js.map +1 -0
- package/dist/cli/convoy/store.d.ts +18 -4
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +119 -14
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +413 -4
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/types.d.ts +21 -0
- package/dist/cli/convoy/types.d.ts.map +1 -1
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +1 -0
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/run/adapters/claude-code.d.ts.map +1 -1
- package/dist/cli/run/adapters/claude-code.js +13 -0
- package/dist/cli/run/adapters/claude-code.js.map +1 -1
- package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
- package/dist/cli/run/adapters/copilot.js +8 -0
- package/dist/cli/run/adapters/copilot.js.map +1 -1
- package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
- package/dist/cli/run/adapters/cursor.js +13 -0
- package/dist/cli/run/adapters/cursor.js.map +1 -1
- package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
- package/dist/cli/run/adapters/opencode.js +13 -0
- package/dist/cli/run/adapters/opencode.js.map +1 -1
- package/dist/cli/run/schema.d.ts +5 -1
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +41 -8
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +194 -5
- package/dist/cli/run/schema.test.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +167 -2
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +11 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/convoy/engine.test.ts +177 -0
- package/src/cli/convoy/engine.ts +24 -1
- package/src/cli/convoy/export.test.ts +37 -0
- package/src/cli/convoy/export.ts +46 -0
- package/src/cli/convoy/pipeline.test.ts +939 -0
- package/src/cli/convoy/pipeline.ts +430 -0
- package/src/cli/convoy/store.test.ts +452 -4
- package/src/cli/convoy/store.ts +152 -23
- package/src/cli/convoy/types.ts +23 -0
- package/src/cli/dashboard.ts +1 -0
- package/src/cli/run/adapters/claude-code.ts +13 -1
- package/src/cli/run/adapters/copilot.ts +8 -0
- package/src/cli/run/adapters/cursor.ts +13 -1
- package/src/cli/run/adapters/opencode.ts +13 -1
- package/src/cli/run/schema.test.ts +244 -5
- package/src/cli/run/schema.ts +49 -8
- package/src/cli/run.ts +163 -2
- package/src/cli/types.ts +12 -1
- package/src/dashboard/dist/_astro/{index.DyyaCW8L.css → index.Cq68OHaZ.css} +1 -1
- package/src/dashboard/dist/index.html +225 -3
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/src/pages/index.astro +241 -2
- package/src/dashboard/src/styles/dashboard.css +116 -0
- package/src/orchestrator/customizations/KNOWN-ISSUES.md +1 -1
|
@@ -32,6 +32,7 @@ function makeConvoy(overrides: Partial<Parameters<ConvoyStore['insertConvoy']>[0
|
|
|
32
32
|
branch: null,
|
|
33
33
|
created_at: new Date().toISOString(),
|
|
34
34
|
spec_yaml: 'name: test',
|
|
35
|
+
pipeline_id: null,
|
|
35
36
|
...overrides,
|
|
36
37
|
}
|
|
37
38
|
}
|
|
@@ -69,6 +70,19 @@ function makeWorker(overrides: Partial<Parameters<ConvoyStore['insertWorker']>[0
|
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
function makePipeline(overrides: Partial<Parameters<ConvoyStore['insertPipeline']>[0]> = {}) {
|
|
74
|
+
return {
|
|
75
|
+
id: 'pipeline-1',
|
|
76
|
+
name: 'Test Pipeline',
|
|
77
|
+
status: 'pending' as const,
|
|
78
|
+
branch: null,
|
|
79
|
+
spec_yaml: 'name: test-pipeline\nversion: 2',
|
|
80
|
+
convoy_specs: JSON.stringify(['convoys/step1.yml', 'convoys/step2.yml']),
|
|
81
|
+
created_at: new Date().toISOString(),
|
|
82
|
+
...overrides,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
72
86
|
// ── DB creation and WAL mode ──────────────────────────────────────────────────
|
|
73
87
|
|
|
74
88
|
describe('DB creation', () => {
|
|
@@ -84,11 +98,11 @@ describe('DB creation', () => {
|
|
|
84
98
|
expect(row.journal_mode).toBe('wal')
|
|
85
99
|
})
|
|
86
100
|
|
|
87
|
-
it('sets schema version to
|
|
101
|
+
it('sets schema version to 4', () => {
|
|
88
102
|
const db = new DatabaseSync(dbPath)
|
|
89
103
|
const row = db.prepare('PRAGMA user_version').get() as { user_version: number }
|
|
90
104
|
db.close()
|
|
91
|
-
expect(row.user_version).toBe(
|
|
105
|
+
expect(row.user_version).toBe(4)
|
|
92
106
|
})
|
|
93
107
|
|
|
94
108
|
it('creates all required tables', () => {
|
|
@@ -102,6 +116,7 @@ describe('DB creation', () => {
|
|
|
102
116
|
expect(names).toContain('task')
|
|
103
117
|
expect(names).toContain('worker')
|
|
104
118
|
expect(names).toContain('event')
|
|
119
|
+
expect(names).toContain('pipeline')
|
|
105
120
|
})
|
|
106
121
|
|
|
107
122
|
it('reopening an existing DB does not reset schema version', () => {
|
|
@@ -113,7 +128,7 @@ describe('DB creation', () => {
|
|
|
113
128
|
store2.close()
|
|
114
129
|
// Reassign so afterEach does not double-close
|
|
115
130
|
store = createConvoyStore(dbPath)
|
|
116
|
-
expect(row.user_version).toBe(
|
|
131
|
+
expect(row.user_version).toBe(4)
|
|
117
132
|
})
|
|
118
133
|
})
|
|
119
134
|
|
|
@@ -192,7 +207,263 @@ describe('schema migration', () => {
|
|
|
192
207
|
verifyDb.close()
|
|
193
208
|
|
|
194
209
|
expect(cols.map(c => c.name)).toContain('adapter')
|
|
195
|
-
|
|
210
|
+
// v1 chains through v2→v3→v4 in one init, so final version is 4
|
|
211
|
+
expect(version.user_version).toBe(4)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('schema migration v2 to v3 adds cost columns', () => {
|
|
215
|
+
// Create a v2 database manually (has adapter column but no cost columns)
|
|
216
|
+
const v2DbPath = join(tmpDir, 'v2.db')
|
|
217
|
+
const rawDb = new DatabaseSync(v2DbPath)
|
|
218
|
+
rawDb.exec(`
|
|
219
|
+
CREATE TABLE convoy (
|
|
220
|
+
id TEXT PRIMARY KEY,
|
|
221
|
+
name TEXT NOT NULL,
|
|
222
|
+
spec_hash TEXT NOT NULL,
|
|
223
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
224
|
+
branch TEXT,
|
|
225
|
+
created_at TEXT NOT NULL,
|
|
226
|
+
started_at TEXT,
|
|
227
|
+
finished_at TEXT,
|
|
228
|
+
spec_yaml TEXT NOT NULL
|
|
229
|
+
);
|
|
230
|
+
CREATE TABLE task (
|
|
231
|
+
id TEXT PRIMARY KEY,
|
|
232
|
+
convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
233
|
+
phase INTEGER NOT NULL,
|
|
234
|
+
prompt TEXT NOT NULL,
|
|
235
|
+
agent TEXT NOT NULL DEFAULT 'developer',
|
|
236
|
+
adapter TEXT,
|
|
237
|
+
model TEXT,
|
|
238
|
+
timeout_ms INTEGER NOT NULL DEFAULT 1800000,
|
|
239
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
240
|
+
worker_id TEXT,
|
|
241
|
+
worktree TEXT,
|
|
242
|
+
output TEXT,
|
|
243
|
+
exit_code INTEGER,
|
|
244
|
+
started_at TEXT,
|
|
245
|
+
finished_at TEXT,
|
|
246
|
+
retries INTEGER NOT NULL DEFAULT 0,
|
|
247
|
+
max_retries INTEGER NOT NULL DEFAULT 1,
|
|
248
|
+
files TEXT,
|
|
249
|
+
depends_on TEXT
|
|
250
|
+
);
|
|
251
|
+
CREATE TABLE worker (
|
|
252
|
+
id TEXT PRIMARY KEY,
|
|
253
|
+
task_id TEXT REFERENCES task(id),
|
|
254
|
+
adapter TEXT NOT NULL,
|
|
255
|
+
pid INTEGER,
|
|
256
|
+
session_id TEXT,
|
|
257
|
+
status TEXT NOT NULL DEFAULT 'spawned',
|
|
258
|
+
worktree TEXT,
|
|
259
|
+
created_at TEXT NOT NULL,
|
|
260
|
+
finished_at TEXT,
|
|
261
|
+
last_heartbeat TEXT
|
|
262
|
+
);
|
|
263
|
+
CREATE TABLE event (
|
|
264
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
265
|
+
convoy_id TEXT REFERENCES convoy(id),
|
|
266
|
+
task_id TEXT,
|
|
267
|
+
worker_id TEXT,
|
|
268
|
+
type TEXT NOT NULL,
|
|
269
|
+
data TEXT,
|
|
270
|
+
created_at TEXT NOT NULL
|
|
271
|
+
);
|
|
272
|
+
`)
|
|
273
|
+
rawDb.exec('PRAGMA user_version = 2')
|
|
274
|
+
rawDb.close()
|
|
275
|
+
|
|
276
|
+
// Open with createConvoyStore — should apply the v2→v3 migration
|
|
277
|
+
const v2Store = createConvoyStore(v2DbPath)
|
|
278
|
+
v2Store.close()
|
|
279
|
+
|
|
280
|
+
// Verify cost columns were added
|
|
281
|
+
const verifyDb = new DatabaseSync(v2DbPath)
|
|
282
|
+
const taskCols = verifyDb.prepare('PRAGMA table_info(task)').all() as Array<{ name: string }>
|
|
283
|
+
const convoyCols = verifyDb.prepare('PRAGMA table_info(convoy)').all() as Array<{ name: string }>
|
|
284
|
+
const version = verifyDb.prepare('PRAGMA user_version').get() as { user_version: number }
|
|
285
|
+
verifyDb.close()
|
|
286
|
+
|
|
287
|
+
const taskColNames = taskCols.map(c => c.name)
|
|
288
|
+
expect(taskColNames).toContain('prompt_tokens')
|
|
289
|
+
expect(taskColNames).toContain('completion_tokens')
|
|
290
|
+
expect(taskColNames).toContain('total_tokens')
|
|
291
|
+
expect(taskColNames).toContain('cost_usd')
|
|
292
|
+
|
|
293
|
+
const convoyColNames = convoyCols.map(c => c.name)
|
|
294
|
+
expect(convoyColNames).toContain('total_tokens')
|
|
295
|
+
expect(convoyColNames).toContain('total_cost_usd')
|
|
296
|
+
|
|
297
|
+
expect(version.user_version).toBe(4)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('schema migration v1 to v3 chains correctly in a single init', () => {
|
|
301
|
+
// Create a v1 database (task table without adapter or cost columns)
|
|
302
|
+
const v1DbPath = join(tmpDir, 'v1-chain.db')
|
|
303
|
+
const rawDb = new DatabaseSync(v1DbPath)
|
|
304
|
+
rawDb.exec(`
|
|
305
|
+
CREATE TABLE convoy (
|
|
306
|
+
id TEXT PRIMARY KEY,
|
|
307
|
+
name TEXT NOT NULL,
|
|
308
|
+
spec_hash TEXT NOT NULL,
|
|
309
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
310
|
+
branch TEXT,
|
|
311
|
+
created_at TEXT NOT NULL,
|
|
312
|
+
started_at TEXT,
|
|
313
|
+
finished_at TEXT,
|
|
314
|
+
spec_yaml TEXT NOT NULL
|
|
315
|
+
);
|
|
316
|
+
CREATE TABLE task (
|
|
317
|
+
id TEXT PRIMARY KEY,
|
|
318
|
+
convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
319
|
+
phase INTEGER NOT NULL,
|
|
320
|
+
prompt TEXT NOT NULL,
|
|
321
|
+
agent TEXT NOT NULL DEFAULT 'developer',
|
|
322
|
+
model TEXT,
|
|
323
|
+
timeout_ms INTEGER NOT NULL DEFAULT 1800000,
|
|
324
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
325
|
+
worker_id TEXT,
|
|
326
|
+
worktree TEXT,
|
|
327
|
+
output TEXT,
|
|
328
|
+
exit_code INTEGER,
|
|
329
|
+
started_at TEXT,
|
|
330
|
+
finished_at TEXT,
|
|
331
|
+
retries INTEGER NOT NULL DEFAULT 0,
|
|
332
|
+
max_retries INTEGER NOT NULL DEFAULT 1,
|
|
333
|
+
files TEXT,
|
|
334
|
+
depends_on TEXT
|
|
335
|
+
);
|
|
336
|
+
CREATE TABLE worker (
|
|
337
|
+
id TEXT PRIMARY KEY,
|
|
338
|
+
task_id TEXT REFERENCES task(id),
|
|
339
|
+
adapter TEXT NOT NULL,
|
|
340
|
+
pid INTEGER,
|
|
341
|
+
session_id TEXT,
|
|
342
|
+
status TEXT NOT NULL DEFAULT 'spawned',
|
|
343
|
+
worktree TEXT,
|
|
344
|
+
created_at TEXT NOT NULL,
|
|
345
|
+
finished_at TEXT,
|
|
346
|
+
last_heartbeat TEXT
|
|
347
|
+
);
|
|
348
|
+
CREATE TABLE event (
|
|
349
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
350
|
+
convoy_id TEXT REFERENCES convoy(id),
|
|
351
|
+
task_id TEXT,
|
|
352
|
+
worker_id TEXT,
|
|
353
|
+
type TEXT NOT NULL,
|
|
354
|
+
data TEXT,
|
|
355
|
+
created_at TEXT NOT NULL
|
|
356
|
+
);
|
|
357
|
+
`)
|
|
358
|
+
rawDb.exec('PRAGMA user_version = 1')
|
|
359
|
+
rawDb.close()
|
|
360
|
+
|
|
361
|
+
// Open with createConvoyStore — should chain v1→v2→v3 in one init
|
|
362
|
+
const v1Store = createConvoyStore(v1DbPath)
|
|
363
|
+
v1Store.close()
|
|
364
|
+
|
|
365
|
+
// Verify all columns from both migrations are present
|
|
366
|
+
const verifyDb = new DatabaseSync(v1DbPath)
|
|
367
|
+
const taskCols = verifyDb.prepare('PRAGMA table_info(task)').all() as Array<{ name: string }>
|
|
368
|
+
const convoyCols = verifyDb.prepare('PRAGMA table_info(convoy)').all() as Array<{ name: string }>
|
|
369
|
+
const version = verifyDb.prepare('PRAGMA user_version').get() as { user_version: number }
|
|
370
|
+
verifyDb.close()
|
|
371
|
+
|
|
372
|
+
const taskColNames = taskCols.map(c => c.name)
|
|
373
|
+
expect(taskColNames).toContain('adapter')
|
|
374
|
+
expect(taskColNames).toContain('prompt_tokens')
|
|
375
|
+
expect(taskColNames).toContain('completion_tokens')
|
|
376
|
+
expect(taskColNames).toContain('total_tokens')
|
|
377
|
+
expect(taskColNames).toContain('cost_usd')
|
|
378
|
+
|
|
379
|
+
const convoyColNames = convoyCols.map(c => c.name)
|
|
380
|
+
expect(convoyColNames).toContain('total_tokens')
|
|
381
|
+
expect(convoyColNames).toContain('total_cost_usd')
|
|
382
|
+
|
|
383
|
+
expect(version.user_version).toBe(4)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('schema migration v3 to v4 creates pipeline table and adds pipeline_id to convoy', () => {
|
|
387
|
+
const v3DbPath = join(tmpDir, 'v3.db')
|
|
388
|
+
const rawDb = new DatabaseSync(v3DbPath)
|
|
389
|
+
rawDb.exec(`
|
|
390
|
+
CREATE TABLE convoy (
|
|
391
|
+
id TEXT PRIMARY KEY,
|
|
392
|
+
name TEXT NOT NULL,
|
|
393
|
+
spec_hash TEXT NOT NULL,
|
|
394
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
395
|
+
branch TEXT,
|
|
396
|
+
created_at TEXT NOT NULL,
|
|
397
|
+
started_at TEXT,
|
|
398
|
+
finished_at TEXT,
|
|
399
|
+
spec_yaml TEXT NOT NULL,
|
|
400
|
+
total_tokens INTEGER,
|
|
401
|
+
total_cost_usd TEXT
|
|
402
|
+
);
|
|
403
|
+
CREATE TABLE task (
|
|
404
|
+
id TEXT PRIMARY KEY,
|
|
405
|
+
convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
406
|
+
phase INTEGER NOT NULL,
|
|
407
|
+
prompt TEXT NOT NULL,
|
|
408
|
+
agent TEXT NOT NULL DEFAULT 'developer',
|
|
409
|
+
adapter TEXT,
|
|
410
|
+
model TEXT,
|
|
411
|
+
timeout_ms INTEGER NOT NULL DEFAULT 1800000,
|
|
412
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
413
|
+
worker_id TEXT,
|
|
414
|
+
worktree TEXT,
|
|
415
|
+
output TEXT,
|
|
416
|
+
exit_code INTEGER,
|
|
417
|
+
started_at TEXT,
|
|
418
|
+
finished_at TEXT,
|
|
419
|
+
retries INTEGER NOT NULL DEFAULT 0,
|
|
420
|
+
max_retries INTEGER NOT NULL DEFAULT 1,
|
|
421
|
+
files TEXT,
|
|
422
|
+
depends_on TEXT,
|
|
423
|
+
prompt_tokens INTEGER,
|
|
424
|
+
completion_tokens INTEGER,
|
|
425
|
+
total_tokens INTEGER,
|
|
426
|
+
cost_usd TEXT
|
|
427
|
+
);
|
|
428
|
+
CREATE TABLE worker (
|
|
429
|
+
id TEXT PRIMARY KEY,
|
|
430
|
+
task_id TEXT REFERENCES task(id),
|
|
431
|
+
adapter TEXT NOT NULL,
|
|
432
|
+
pid INTEGER,
|
|
433
|
+
session_id TEXT,
|
|
434
|
+
status TEXT NOT NULL DEFAULT 'spawned',
|
|
435
|
+
worktree TEXT,
|
|
436
|
+
created_at TEXT NOT NULL,
|
|
437
|
+
finished_at TEXT,
|
|
438
|
+
last_heartbeat TEXT
|
|
439
|
+
);
|
|
440
|
+
CREATE TABLE event (
|
|
441
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
442
|
+
convoy_id TEXT REFERENCES convoy(id),
|
|
443
|
+
task_id TEXT,
|
|
444
|
+
worker_id TEXT,
|
|
445
|
+
type TEXT NOT NULL,
|
|
446
|
+
data TEXT,
|
|
447
|
+
created_at TEXT NOT NULL
|
|
448
|
+
);
|
|
449
|
+
`)
|
|
450
|
+
rawDb.exec('PRAGMA user_version = 3')
|
|
451
|
+
rawDb.close()
|
|
452
|
+
|
|
453
|
+
const v3Store = createConvoyStore(v3DbPath)
|
|
454
|
+
v3Store.close()
|
|
455
|
+
|
|
456
|
+
const verifyDb = new DatabaseSync(v3DbPath)
|
|
457
|
+
const convoyCols = verifyDb.prepare('PRAGMA table_info(convoy)').all() as Array<{ name: string }>
|
|
458
|
+
const tables = verifyDb
|
|
459
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
|
460
|
+
.all() as Array<{ name: string }>
|
|
461
|
+
const version = verifyDb.prepare('PRAGMA user_version').get() as { user_version: number }
|
|
462
|
+
verifyDb.close()
|
|
463
|
+
|
|
464
|
+
expect(convoyCols.map(c => c.name)).toContain('pipeline_id')
|
|
465
|
+
expect(tables.map(t => t.name)).toContain('pipeline')
|
|
466
|
+
expect(version.user_version).toBe(4)
|
|
196
467
|
})
|
|
197
468
|
})
|
|
198
469
|
|
|
@@ -238,6 +509,25 @@ describe('convoy CRUD', () => {
|
|
|
238
509
|
expect(retrieved.status).toBe('done')
|
|
239
510
|
expect(retrieved.finished_at).toBe(ts)
|
|
240
511
|
})
|
|
512
|
+
|
|
513
|
+
it('cost fields are null by default on a new convoy', () => {
|
|
514
|
+
store.insertConvoy(makeConvoy())
|
|
515
|
+
const retrieved = store.getConvoy('convoy-1')!
|
|
516
|
+
expect(retrieved.total_tokens).toBeNull()
|
|
517
|
+
expect(retrieved.total_cost_usd).toBeNull()
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it('updateConvoyStatus persists total_tokens and total_cost_usd', () => {
|
|
521
|
+
store.insertConvoy(makeConvoy())
|
|
522
|
+
store.updateConvoyStatus('convoy-1', 'done', {
|
|
523
|
+
finished_at: '2026-01-01T01:00:00.000Z',
|
|
524
|
+
total_tokens: 5000,
|
|
525
|
+
total_cost_usd: '0.015000',
|
|
526
|
+
})
|
|
527
|
+
const retrieved = store.getConvoy('convoy-1')!
|
|
528
|
+
expect(retrieved.total_tokens).toBe(5000)
|
|
529
|
+
expect(retrieved.total_cost_usd).toBe('0.015000')
|
|
530
|
+
})
|
|
241
531
|
})
|
|
242
532
|
|
|
243
533
|
// ── task CRUD ─────────────────────────────────────────────────────────────────
|
|
@@ -311,6 +601,30 @@ describe('task CRUD', () => {
|
|
|
311
601
|
expect(task.finished_at).toBe(ts)
|
|
312
602
|
expect(task.retries).toBe(1)
|
|
313
603
|
})
|
|
604
|
+
|
|
605
|
+
it('cost fields are null by default on a new task', () => {
|
|
606
|
+
store.insertTask(makeTask())
|
|
607
|
+
const task = store.getTask('task-1', 'convoy-1')!
|
|
608
|
+
expect(task.prompt_tokens).toBeNull()
|
|
609
|
+
expect(task.completion_tokens).toBeNull()
|
|
610
|
+
expect(task.total_tokens).toBeNull()
|
|
611
|
+
expect(task.cost_usd).toBeNull()
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
it('updateTaskStatus persists cost fields', () => {
|
|
615
|
+
store.insertTask(makeTask())
|
|
616
|
+
store.updateTaskStatus('task-1', 'convoy-1', 'done', {
|
|
617
|
+
prompt_tokens: 1200,
|
|
618
|
+
completion_tokens: 800,
|
|
619
|
+
total_tokens: 2000,
|
|
620
|
+
cost_usd: '0.006000',
|
|
621
|
+
})
|
|
622
|
+
const task = store.getTask('task-1', 'convoy-1')!
|
|
623
|
+
expect(task.prompt_tokens).toBe(1200)
|
|
624
|
+
expect(task.completion_tokens).toBe(800)
|
|
625
|
+
expect(task.total_tokens).toBe(2000)
|
|
626
|
+
expect(task.cost_usd).toBe('0.006000')
|
|
627
|
+
})
|
|
314
628
|
})
|
|
315
629
|
|
|
316
630
|
// ── getReadyTasks ─────────────────────────────────────────────────────────────
|
|
@@ -522,6 +836,140 @@ describe('withTransaction', () => {
|
|
|
522
836
|
})
|
|
523
837
|
})
|
|
524
838
|
|
|
839
|
+
// ── pipeline CRUD ─────────────────────────────────────────────────────────────
|
|
840
|
+
|
|
841
|
+
describe('pipeline CRUD', () => {
|
|
842
|
+
it('inserts and retrieves a pipeline record', () => {
|
|
843
|
+
store.insertPipeline(makePipeline())
|
|
844
|
+
const retrieved = store.getPipeline('pipeline-1')
|
|
845
|
+
expect(retrieved).toBeDefined()
|
|
846
|
+
expect(retrieved!.id).toBe('pipeline-1')
|
|
847
|
+
expect(retrieved!.name).toBe('Test Pipeline')
|
|
848
|
+
expect(retrieved!.status).toBe('pending')
|
|
849
|
+
expect(retrieved!.started_at).toBeNull()
|
|
850
|
+
expect(retrieved!.finished_at).toBeNull()
|
|
851
|
+
expect(retrieved!.total_tokens).toBeNull()
|
|
852
|
+
expect(retrieved!.total_cost_usd).toBeNull()
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
it('returns undefined for missing pipeline', () => {
|
|
856
|
+
expect(store.getPipeline('does-not-exist')).toBeUndefined()
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
it('getLatestPipeline returns most recent pipeline', () => {
|
|
860
|
+
store.insertPipeline(makePipeline({ id: 'pipeline-old', created_at: '2026-01-01T00:00:00.000Z' }))
|
|
861
|
+
store.insertPipeline(makePipeline({ id: 'pipeline-new', created_at: '2026-01-02T00:00:00.000Z' }))
|
|
862
|
+
const latest = store.getLatestPipeline()
|
|
863
|
+
expect(latest?.id).toBe('pipeline-new')
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
it('getLatestPipeline returns undefined when no pipelines exist', () => {
|
|
867
|
+
expect(store.getLatestPipeline()).toBeUndefined()
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
it('updatePipelineStatus updates status', () => {
|
|
871
|
+
store.insertPipeline(makePipeline())
|
|
872
|
+
store.updatePipelineStatus('pipeline-1', 'running')
|
|
873
|
+
expect(store.getPipeline('pipeline-1')!.status).toBe('running')
|
|
874
|
+
})
|
|
875
|
+
|
|
876
|
+
it('updatePipelineStatus sets started_at', () => {
|
|
877
|
+
const ts = '2026-01-01T00:00:00.000Z'
|
|
878
|
+
store.insertPipeline(makePipeline())
|
|
879
|
+
store.updatePipelineStatus('pipeline-1', 'running', { started_at: ts })
|
|
880
|
+
const p = store.getPipeline('pipeline-1')!
|
|
881
|
+
expect(p.status).toBe('running')
|
|
882
|
+
expect(p.started_at).toBe(ts)
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
it('updatePipelineStatus sets finished_at', () => {
|
|
886
|
+
const ts = '2026-01-01T01:00:00.000Z'
|
|
887
|
+
store.insertPipeline(makePipeline())
|
|
888
|
+
store.updatePipelineStatus('pipeline-1', 'done', { finished_at: ts })
|
|
889
|
+
const p = store.getPipeline('pipeline-1')!
|
|
890
|
+
expect(p.status).toBe('done')
|
|
891
|
+
expect(p.finished_at).toBe(ts)
|
|
892
|
+
})
|
|
893
|
+
|
|
894
|
+
it('updatePipelineStatus persists total_tokens and total_cost_usd', () => {
|
|
895
|
+
store.insertPipeline(makePipeline())
|
|
896
|
+
store.updatePipelineStatus('pipeline-1', 'done', {
|
|
897
|
+
finished_at: '2026-01-01T01:00:00.000Z',
|
|
898
|
+
total_tokens: 12000,
|
|
899
|
+
total_cost_usd: '0.036000',
|
|
900
|
+
})
|
|
901
|
+
const p = store.getPipeline('pipeline-1')!
|
|
902
|
+
expect(p.total_tokens).toBe(12000)
|
|
903
|
+
expect(p.total_cost_usd).toBe('0.036000')
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
it('pipeline status can transition through all states', () => {
|
|
907
|
+
store.insertPipeline(makePipeline())
|
|
908
|
+
const states = ['running', 'failed', 'done', 'pending'] as const
|
|
909
|
+
for (const s of states) {
|
|
910
|
+
store.updatePipelineStatus('pipeline-1', s)
|
|
911
|
+
expect(store.getPipeline('pipeline-1')!.status).toBe(s)
|
|
912
|
+
}
|
|
913
|
+
})
|
|
914
|
+
|
|
915
|
+
it('convoy_specs is stored and retrieved as a JSON string', () => {
|
|
916
|
+
const specs = ['convoys/build.yml', 'convoys/test.yml', 'convoys/deploy.yml']
|
|
917
|
+
store.insertPipeline(makePipeline({ convoy_specs: JSON.stringify(specs) }))
|
|
918
|
+
const p = store.getPipeline('pipeline-1')!
|
|
919
|
+
expect(JSON.parse(p.convoy_specs)).toEqual(specs)
|
|
920
|
+
})
|
|
921
|
+
})
|
|
922
|
+
|
|
923
|
+
// ── pipeline-convoy linking ───────────────────────────────────────────────────
|
|
924
|
+
|
|
925
|
+
describe('pipeline-convoy linking', () => {
|
|
926
|
+
it('insertConvoy accepts pipeline_id', () => {
|
|
927
|
+
store.insertPipeline(makePipeline())
|
|
928
|
+
store.insertConvoy(makeConvoy({ pipeline_id: 'pipeline-1' }))
|
|
929
|
+
const c = store.getConvoy('convoy-1')!
|
|
930
|
+
expect(c.pipeline_id).toBe('pipeline-1')
|
|
931
|
+
})
|
|
932
|
+
|
|
933
|
+
it('insertConvoy with null pipeline_id creates a standalone convoy', () => {
|
|
934
|
+
store.insertConvoy(makeConvoy({ pipeline_id: null }))
|
|
935
|
+
const c = store.getConvoy('convoy-1')!
|
|
936
|
+
expect(c.pipeline_id).toBeNull()
|
|
937
|
+
})
|
|
938
|
+
|
|
939
|
+
it('getConvoysByPipeline returns all convoys for a pipeline', () => {
|
|
940
|
+
store.insertPipeline(makePipeline())
|
|
941
|
+
store.insertConvoy(makeConvoy({ id: 'convoy-1', pipeline_id: 'pipeline-1', created_at: '2026-01-01T00:00:00.000Z' }))
|
|
942
|
+
store.insertConvoy(makeConvoy({ id: 'convoy-2', pipeline_id: 'pipeline-1', created_at: '2026-01-01T01:00:00.000Z' }))
|
|
943
|
+
const convoys = store.getConvoysByPipeline('pipeline-1')
|
|
944
|
+
expect(convoys).toHaveLength(2)
|
|
945
|
+
expect(convoys.map(c => c.id)).toEqual(['convoy-1', 'convoy-2'])
|
|
946
|
+
})
|
|
947
|
+
|
|
948
|
+
it('getConvoysByPipeline returns convoys ordered by created_at', () => {
|
|
949
|
+
store.insertPipeline(makePipeline())
|
|
950
|
+
store.insertConvoy(makeConvoy({ id: 'convoy-b', pipeline_id: 'pipeline-1', created_at: '2026-01-01T02:00:00.000Z' }))
|
|
951
|
+
store.insertConvoy(makeConvoy({ id: 'convoy-a', pipeline_id: 'pipeline-1', created_at: '2026-01-01T01:00:00.000Z' }))
|
|
952
|
+
const convoys = store.getConvoysByPipeline('pipeline-1')
|
|
953
|
+
expect(convoys[0].id).toBe('convoy-a')
|
|
954
|
+
expect(convoys[1].id).toBe('convoy-b')
|
|
955
|
+
})
|
|
956
|
+
|
|
957
|
+
it('getConvoysByPipeline returns empty array when no convoys are linked', () => {
|
|
958
|
+
store.insertPipeline(makePipeline())
|
|
959
|
+
expect(store.getConvoysByPipeline('pipeline-1')).toHaveLength(0)
|
|
960
|
+
})
|
|
961
|
+
|
|
962
|
+
it('getConvoysByPipeline does not return convoys from other pipelines', () => {
|
|
963
|
+
store.insertPipeline(makePipeline({ id: 'pipeline-1' }))
|
|
964
|
+
store.insertPipeline(makePipeline({ id: 'pipeline-2' }))
|
|
965
|
+
store.insertConvoy(makeConvoy({ id: 'convoy-1', pipeline_id: 'pipeline-1' }))
|
|
966
|
+
store.insertConvoy(makeConvoy({ id: 'convoy-2', pipeline_id: 'pipeline-2' }))
|
|
967
|
+
const p1Convoys = store.getConvoysByPipeline('pipeline-1')
|
|
968
|
+
expect(p1Convoys).toHaveLength(1)
|
|
969
|
+
expect(p1Convoys[0].id).toBe('convoy-1')
|
|
970
|
+
})
|
|
971
|
+
})
|
|
972
|
+
|
|
525
973
|
// ── close ─────────────────────────────────────────────────────────────────────
|
|
526
974
|
|
|
527
975
|
describe('close', () => {
|