opencastle 0.27.1 → 0.27.2
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/dashboard-types.d.ts +146 -0
- package/dist/cli/convoy/dashboard-types.d.ts.map +1 -0
- package/dist/cli/convoy/dashboard-types.js +2 -0
- package/dist/cli/convoy/dashboard-types.js.map +1 -0
- package/dist/cli/convoy/engine.d.ts +0 -1
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +31 -99
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +88 -1
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/event-schemas.d.ts +9 -0
- package/dist/cli/convoy/event-schemas.d.ts.map +1 -0
- package/dist/cli/convoy/event-schemas.js +185 -0
- package/dist/cli/convoy/event-schemas.js.map +1 -0
- package/dist/cli/convoy/events.d.ts +8 -0
- package/dist/cli/convoy/events.d.ts.map +1 -1
- package/dist/cli/convoy/events.js +117 -5
- package/dist/cli/convoy/events.js.map +1 -1
- package/dist/cli/convoy/events.test.js +173 -3
- package/dist/cli/convoy/events.test.js.map +1 -1
- package/dist/cli/convoy/log-merge.test.d.ts +2 -0
- package/dist/cli/convoy/log-merge.test.d.ts.map +1 -0
- package/dist/cli/convoy/log-merge.test.js +147 -0
- package/dist/cli/convoy/log-merge.test.js.map +1 -0
- package/dist/cli/convoy/store.d.ts +52 -2
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +244 -17
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +481 -22
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/types.d.ts +271 -3
- package/dist/cli/convoy/types.d.ts.map +1 -1
- package/dist/cli/convoy/types.js +42 -1
- package/dist/cli/convoy/types.js.map +1 -1
- package/dist/cli/log.d.ts +11 -0
- package/dist/cli/log.d.ts.map +1 -1
- package/dist/cli/log.js +114 -2
- package/dist/cli/log.js.map +1 -1
- package/package.json +5 -1
- package/src/cli/convoy/TELEMETRY.md +203 -0
- package/src/cli/convoy/dashboard-types.ts +141 -0
- package/src/cli/convoy/engine.test.ts +99 -1
- package/src/cli/convoy/engine.ts +27 -96
- package/src/cli/convoy/event-schemas.ts +195 -0
- package/src/cli/convoy/events.test.ts +207 -3
- package/src/cli/convoy/events.ts +119 -5
- package/src/cli/convoy/log-merge.test.ts +179 -0
- package/src/cli/convoy/store.test.ts +545 -22
- package/src/cli/convoy/store.ts +274 -21
- package/src/cli/convoy/types.ts +108 -3
- package/src/cli/log.ts +120 -2
- package/src/dashboard/dist/_astro/{index.DtnyD8a5.css → index.6L3_HsPT.css} +1 -1
- package/src/dashboard/dist/data/.gitkeep +0 -0
- package/src/dashboard/dist/data/convoy-list.json +1 -0
- package/src/dashboard/dist/data/overall-stats.json +24 -0
- package/src/dashboard/dist/index.html +701 -3
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/public/data/.gitkeep +0 -0
- package/src/dashboard/public/data/convoy-list.json +1 -0
- package/src/dashboard/public/data/overall-stats.json +24 -0
- package/src/dashboard/scripts/etl.test.ts +210 -0
- package/src/dashboard/scripts/etl.ts +108 -0
- package/src/dashboard/scripts/integration-test.ts +504 -0
- package/src/dashboard/src/pages/index.astro +854 -15
- package/src/dashboard/src/styles/dashboard.css +557 -1
- package/src/orchestrator/prompts/generate-convoy.prompt.md +212 -13
|
@@ -99,11 +99,11 @@ describe('DB creation', () => {
|
|
|
99
99
|
expect(row.journal_mode).toBe('wal')
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
-
it('sets schema version to
|
|
102
|
+
it('sets schema version to 10', () => {
|
|
103
103
|
const db = new DatabaseSync(dbPath)
|
|
104
104
|
const row = db.prepare('PRAGMA user_version').get() as { user_version: number }
|
|
105
105
|
db.close()
|
|
106
|
-
expect(row.user_version).toBe(
|
|
106
|
+
expect(row.user_version).toBe(10)
|
|
107
107
|
})
|
|
108
108
|
|
|
109
109
|
it('creates all required tables', () => {
|
|
@@ -131,7 +131,7 @@ describe('DB creation', () => {
|
|
|
131
131
|
store2.close()
|
|
132
132
|
// Reassign so afterEach does not double-close
|
|
133
133
|
store = createConvoyStore(dbPath)
|
|
134
|
-
expect(row.user_version).toBe(
|
|
134
|
+
expect(row.user_version).toBe(10)
|
|
135
135
|
})
|
|
136
136
|
})
|
|
137
137
|
|
|
@@ -208,8 +208,8 @@ describe('schema migration', () => {
|
|
|
208
208
|
verifyDb.close()
|
|
209
209
|
|
|
210
210
|
expect(cols.map(c => c.name)).toContain('adapter')
|
|
211
|
-
// v1 chains through v2→v3→v4→...→v7→v8→v9 in one init, so final version is
|
|
212
|
-
expect(version.user_version).toBe(
|
|
211
|
+
// v1 chains through v2→v3→v4→...→v7→v8→v9→v10 in one init, so final version is 10
|
|
212
|
+
expect(version.user_version).toBe(10)
|
|
213
213
|
})
|
|
214
214
|
|
|
215
215
|
it('schema migration v2 to v3 adds cost columns', () => {
|
|
@@ -295,7 +295,7 @@ describe('schema migration', () => {
|
|
|
295
295
|
expect(convoyColNames).toContain('total_tokens')
|
|
296
296
|
expect(convoyColNames).toContain('total_cost_usd')
|
|
297
297
|
|
|
298
|
-
expect(version.user_version).toBe(
|
|
298
|
+
expect(version.user_version).toBe(10)
|
|
299
299
|
})
|
|
300
300
|
|
|
301
301
|
it('schema migration v1 to v3 chains correctly in a single init', () => {
|
|
@@ -381,7 +381,7 @@ describe('schema migration', () => {
|
|
|
381
381
|
expect(convoyColNames).toContain('total_tokens')
|
|
382
382
|
expect(convoyColNames).toContain('total_cost_usd')
|
|
383
383
|
|
|
384
|
-
expect(version.user_version).toBe(
|
|
384
|
+
expect(version.user_version).toBe(10)
|
|
385
385
|
})
|
|
386
386
|
|
|
387
387
|
it('schema migration v3 to v4 creates pipeline table and adds pipeline_id to convoy', () => {
|
|
@@ -464,7 +464,7 @@ describe('schema migration', () => {
|
|
|
464
464
|
|
|
465
465
|
expect(convoyCols.map(c => c.name)).toContain('pipeline_id')
|
|
466
466
|
expect(tables.map(t => t.name)).toContain('pipeline')
|
|
467
|
-
expect(version.user_version).toBe(
|
|
467
|
+
expect(version.user_version).toBe(10)
|
|
468
468
|
})
|
|
469
469
|
})
|
|
470
470
|
|
|
@@ -523,11 +523,11 @@ describe('convoy CRUD', () => {
|
|
|
523
523
|
store.updateConvoyStatus('convoy-1', 'done', {
|
|
524
524
|
finished_at: '2026-01-01T01:00:00.000Z',
|
|
525
525
|
total_tokens: 5000,
|
|
526
|
-
total_cost_usd:
|
|
526
|
+
total_cost_usd: 0.015,
|
|
527
527
|
})
|
|
528
528
|
const retrieved = store.getConvoy('convoy-1')!
|
|
529
529
|
expect(retrieved.total_tokens).toBe(5000)
|
|
530
|
-
expect(retrieved.total_cost_usd).toBe(
|
|
530
|
+
expect(retrieved.total_cost_usd).toBe(0.015)
|
|
531
531
|
})
|
|
532
532
|
})
|
|
533
533
|
|
|
@@ -618,13 +618,13 @@ describe('task CRUD', () => {
|
|
|
618
618
|
prompt_tokens: 1200,
|
|
619
619
|
completion_tokens: 800,
|
|
620
620
|
total_tokens: 2000,
|
|
621
|
-
cost_usd:
|
|
621
|
+
cost_usd: 0.006,
|
|
622
622
|
})
|
|
623
623
|
const task = store.getTask('task-1', 'convoy-1')!
|
|
624
624
|
expect(task.prompt_tokens).toBe(1200)
|
|
625
625
|
expect(task.completion_tokens).toBe(800)
|
|
626
626
|
expect(task.total_tokens).toBe(2000)
|
|
627
|
-
expect(task.cost_usd).toBe(
|
|
627
|
+
expect(task.cost_usd).toBe(0.006)
|
|
628
628
|
})
|
|
629
629
|
})
|
|
630
630
|
|
|
@@ -897,11 +897,11 @@ describe('pipeline CRUD', () => {
|
|
|
897
897
|
store.updatePipelineStatus('pipeline-1', 'done', {
|
|
898
898
|
finished_at: '2026-01-01T01:00:00.000Z',
|
|
899
899
|
total_tokens: 12000,
|
|
900
|
-
total_cost_usd:
|
|
900
|
+
total_cost_usd: 0.036,
|
|
901
901
|
})
|
|
902
902
|
const p = store.getPipeline('pipeline-1')!
|
|
903
903
|
expect(p.total_tokens).toBe(12000)
|
|
904
|
-
expect(p.total_cost_usd).toBe(
|
|
904
|
+
expect(p.total_cost_usd).toBe(0.036)
|
|
905
905
|
})
|
|
906
906
|
|
|
907
907
|
it('pipeline status can transition through all states', () => {
|
|
@@ -1444,7 +1444,7 @@ describe('schema migration v5 → v6', () => {
|
|
|
1444
1444
|
v5Verify.close()
|
|
1445
1445
|
migratedStore.close()
|
|
1446
1446
|
|
|
1447
|
-
expect(row.user_version).toBe(
|
|
1447
|
+
expect(row.user_version).toBe(10)
|
|
1448
1448
|
expect(taskStepTable?.name).toBe('task_step')
|
|
1449
1449
|
expect(convoy?.id).toBe('convoy-auto')
|
|
1450
1450
|
expect(task?.id).toBe('task-auto')
|
|
@@ -1614,7 +1614,7 @@ describe('schema migration v6→v7 (drift detection columns)', () => {
|
|
|
1614
1614
|
|
|
1615
1615
|
expect(cols.map(c => c.name)).toContain('drift_score')
|
|
1616
1616
|
expect(cols.map(c => c.name)).toContain('drift_retried')
|
|
1617
|
-
expect(version.user_version).toBe(
|
|
1617
|
+
expect(version.user_version).toBe(10)
|
|
1618
1618
|
})
|
|
1619
1619
|
|
|
1620
1620
|
it('new databases include drift_score and drift_retried in CREATE TABLE', () => {
|
|
@@ -1816,10 +1816,10 @@ describe('artifact CRUD', () => {
|
|
|
1816
1816
|
})
|
|
1817
1817
|
})
|
|
1818
1818
|
|
|
1819
|
-
// ── migration full chain v4→
|
|
1819
|
+
// ── migration full chain v4→v10 ────────────────────────────────────────────────
|
|
1820
1820
|
|
|
1821
|
-
describe('migration full chain v4→
|
|
1822
|
-
it('migrates a seeded v4 database to
|
|
1821
|
+
describe('migration full chain v4→v10', () => {
|
|
1822
|
+
it('migrates a seeded v4 database to v10, preserving data and adding all tables/columns', () => {
|
|
1823
1823
|
const chainDbPath = join(tmpDir, 'v4-chain.db')
|
|
1824
1824
|
const v4Db = createV4Db(chainDbPath)
|
|
1825
1825
|
// Seed realistic v4 data
|
|
@@ -1841,15 +1841,15 @@ describe('migration full chain v4→v9', () => {
|
|
|
1841
1841
|
).run()
|
|
1842
1842
|
v4Db.close()
|
|
1843
1843
|
|
|
1844
|
-
// Trigger the full v4→
|
|
1844
|
+
// Trigger the full v4→v10 migration chain
|
|
1845
1845
|
const migratedStore = createConvoyStore(chainDbPath)
|
|
1846
1846
|
migratedStore.close()
|
|
1847
1847
|
|
|
1848
1848
|
const verifyDb = new DatabaseSync(chainDbPath)
|
|
1849
1849
|
|
|
1850
|
-
// Verify user_version =
|
|
1850
|
+
// Verify user_version = 10
|
|
1851
1851
|
const version = (verifyDb.prepare('PRAGMA user_version').get() as { user_version: number }).user_version
|
|
1852
|
-
expect(version).toBe(
|
|
1852
|
+
expect(version).toBe(10)
|
|
1853
1853
|
|
|
1854
1854
|
// Verify all new tables exist
|
|
1855
1855
|
const tables = (verifyDb.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as Array<{ name: string }>).map(t => t.name)
|
|
@@ -2406,6 +2406,147 @@ describe('v8→v9 migration', () => {
|
|
|
2406
2406
|
})
|
|
2407
2407
|
})
|
|
2408
2408
|
|
|
2409
|
+
describe('v9→v10 migration', () => {
|
|
2410
|
+
it('adds numeric cost columns and backfills data from TEXT columns', () => {
|
|
2411
|
+
const migDir = realpathSync(mkdtempSync(join(tmpdir(), 'mig-v10-test-')))
|
|
2412
|
+
const migDb = join(migDir, 'mig.db')
|
|
2413
|
+
|
|
2414
|
+
const db = new DatabaseSync(migDb)
|
|
2415
|
+
db.exec('PRAGMA journal_mode = WAL')
|
|
2416
|
+
db.exec(`
|
|
2417
|
+
CREATE TABLE convoy (
|
|
2418
|
+
id TEXT PRIMARY KEY, name TEXT NOT NULL, spec_hash TEXT NOT NULL,
|
|
2419
|
+
status TEXT NOT NULL DEFAULT 'pending', branch TEXT,
|
|
2420
|
+
created_at TEXT NOT NULL, started_at TEXT, finished_at TEXT,
|
|
2421
|
+
spec_yaml TEXT NOT NULL, total_tokens INTEGER, total_cost_usd TEXT,
|
|
2422
|
+
pipeline_id TEXT, circuit_state TEXT, review_tokens_total INTEGER, review_budget INTEGER
|
|
2423
|
+
);
|
|
2424
|
+
CREATE TABLE pipeline (
|
|
2425
|
+
id TEXT PRIMARY KEY, name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending',
|
|
2426
|
+
branch TEXT, spec_yaml TEXT NOT NULL, convoy_specs TEXT NOT NULL,
|
|
2427
|
+
created_at TEXT NOT NULL, started_at TEXT, finished_at TEXT,
|
|
2428
|
+
total_tokens INTEGER, total_cost_usd TEXT
|
|
2429
|
+
);
|
|
2430
|
+
CREATE TABLE task (
|
|
2431
|
+
id TEXT PRIMARY KEY, convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
2432
|
+
phase INTEGER NOT NULL, prompt TEXT NOT NULL, agent TEXT NOT NULL DEFAULT 'developer',
|
|
2433
|
+
adapter TEXT, model TEXT, timeout_ms INTEGER NOT NULL DEFAULT 1800000,
|
|
2434
|
+
status TEXT NOT NULL DEFAULT 'pending', worker_id TEXT, worktree TEXT, output TEXT,
|
|
2435
|
+
exit_code INTEGER, started_at TEXT, finished_at TEXT,
|
|
2436
|
+
retries INTEGER NOT NULL DEFAULT 0, max_retries INTEGER NOT NULL DEFAULT 1,
|
|
2437
|
+
files TEXT, depends_on TEXT, prompt_tokens INTEGER, completion_tokens INTEGER,
|
|
2438
|
+
total_tokens INTEGER, cost_usd TEXT, gates TEXT,
|
|
2439
|
+
on_exhausted TEXT NOT NULL DEFAULT 'dlq', injected INTEGER NOT NULL DEFAULT 0,
|
|
2440
|
+
provenance TEXT, idempotency_key TEXT, current_step INTEGER, total_steps INTEGER,
|
|
2441
|
+
review_level TEXT, review_verdict TEXT, review_tokens INTEGER, review_model TEXT,
|
|
2442
|
+
panel_attempts INTEGER NOT NULL DEFAULT 0, dispute_id TEXT,
|
|
2443
|
+
drift_score REAL, drift_retried INTEGER NOT NULL DEFAULT 0,
|
|
2444
|
+
outputs TEXT, inputs TEXT, discovered_issues TEXT
|
|
2445
|
+
);
|
|
2446
|
+
CREATE TABLE worker (
|
|
2447
|
+
id TEXT PRIMARY KEY, task_id TEXT REFERENCES task(id), adapter TEXT NOT NULL,
|
|
2448
|
+
pid INTEGER, session_id TEXT, status TEXT NOT NULL DEFAULT 'spawned',
|
|
2449
|
+
worktree TEXT, created_at TEXT NOT NULL, finished_at TEXT, last_heartbeat TEXT
|
|
2450
|
+
);
|
|
2451
|
+
CREATE TABLE event (
|
|
2452
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, convoy_id TEXT REFERENCES convoy(id),
|
|
2453
|
+
task_id TEXT, worker_id TEXT, type TEXT NOT NULL, data TEXT, created_at TEXT NOT NULL
|
|
2454
|
+
);
|
|
2455
|
+
CREATE TABLE dlq (
|
|
2456
|
+
id TEXT PRIMARY KEY, convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
2457
|
+
task_id TEXT NOT NULL REFERENCES task(id), agent TEXT NOT NULL,
|
|
2458
|
+
failure_type TEXT NOT NULL, error_output TEXT, attempts INTEGER NOT NULL,
|
|
2459
|
+
tokens_spent INTEGER, escalation_task_id TEXT, resolved INTEGER NOT NULL DEFAULT 0,
|
|
2460
|
+
resolution TEXT, created_at TEXT NOT NULL, resolved_at TEXT
|
|
2461
|
+
);
|
|
2462
|
+
CREATE TABLE task_step (
|
|
2463
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL REFERENCES task(id),
|
|
2464
|
+
step_index INTEGER NOT NULL, prompt TEXT NOT NULL, gates TEXT,
|
|
2465
|
+
status TEXT NOT NULL DEFAULT 'pending', exit_code INTEGER, output TEXT,
|
|
2466
|
+
started_at TEXT, finished_at TEXT
|
|
2467
|
+
);
|
|
2468
|
+
CREATE TABLE artifact (
|
|
2469
|
+
id TEXT PRIMARY KEY, convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
2470
|
+
task_id TEXT NOT NULL REFERENCES task(id), name TEXT NOT NULL, type TEXT NOT NULL,
|
|
2471
|
+
content TEXT NOT NULL, created_at TEXT NOT NULL, UNIQUE(convoy_id, name)
|
|
2472
|
+
);
|
|
2473
|
+
CREATE TABLE agent_identity (
|
|
2474
|
+
id TEXT PRIMARY KEY, agent TEXT NOT NULL, convoy_id TEXT NOT NULL,
|
|
2475
|
+
task_id TEXT NOT NULL, summary TEXT NOT NULL, created_at TEXT NOT NULL,
|
|
2476
|
+
retention_days INTEGER NOT NULL DEFAULT 90
|
|
2477
|
+
);
|
|
2478
|
+
CREATE TABLE scratchpad (
|
|
2479
|
+
key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
2480
|
+
);
|
|
2481
|
+
PRAGMA user_version = 9;
|
|
2482
|
+
`)
|
|
2483
|
+
|
|
2484
|
+
// Seed data with TEXT cost values (pre-migration state)
|
|
2485
|
+
db.prepare(
|
|
2486
|
+
`INSERT INTO convoy (id, name, spec_hash, status, created_at, spec_yaml, total_cost_usd)
|
|
2487
|
+
VALUES ('c-1', 'Test', 'hash1', 'done', '2026-01-01T00:00:00.000Z', 'name: test', '1.23')`,
|
|
2488
|
+
).run()
|
|
2489
|
+
db.prepare(
|
|
2490
|
+
`INSERT INTO convoy (id, name, spec_hash, status, created_at, spec_yaml, total_cost_usd)
|
|
2491
|
+
VALUES ('c-null', 'NullCost', 'hash2', 'pending', '2026-01-01T00:00:00.000Z', 'name: test', NULL)`,
|
|
2492
|
+
).run()
|
|
2493
|
+
db.prepare(
|
|
2494
|
+
`INSERT INTO task (id, convoy_id, phase, prompt, agent, timeout_ms, status, retries, max_retries, cost_usd)
|
|
2495
|
+
VALUES ('t-1', 'c-1', 0, 'Do it', 'developer', 1800000, 'done', 0, 1, '0.45')`,
|
|
2496
|
+
).run()
|
|
2497
|
+
db.prepare(
|
|
2498
|
+
`INSERT INTO task (id, convoy_id, phase, prompt, agent, timeout_ms, status, retries, max_retries, cost_usd)
|
|
2499
|
+
VALUES ('t-null', 'c-null', 0, 'Do it', 'developer', 1800000, 'pending', 0, 1, NULL)`,
|
|
2500
|
+
).run()
|
|
2501
|
+
db.close()
|
|
2502
|
+
|
|
2503
|
+
// Open with createConvoyStore — triggers v9→v10 migration
|
|
2504
|
+
const migratedStore = createConvoyStore(migDb)
|
|
2505
|
+
|
|
2506
|
+
// Verify convoy cost is numeric
|
|
2507
|
+
const convoy = migratedStore.getConvoy('c-1')!
|
|
2508
|
+
expect(convoy.total_cost_usd).toBe(1.23)
|
|
2509
|
+
expect((convoy.total_cost_usd as number).toFixed(2)).toBe('1.23')
|
|
2510
|
+
expect(convoy.total_cost_usd! > 0).toBe(true)
|
|
2511
|
+
|
|
2512
|
+
// Verify null preservation
|
|
2513
|
+
const convoyNull = migratedStore.getConvoy('c-null')!
|
|
2514
|
+
expect(convoyNull.total_cost_usd).toBeNull()
|
|
2515
|
+
|
|
2516
|
+
// Verify task cost is numeric
|
|
2517
|
+
const task = migratedStore.getTask('t-1', 'c-1')!
|
|
2518
|
+
expect(task.cost_usd).toBe(0.45)
|
|
2519
|
+
expect((task.cost_usd as number).toFixed(2)).toBe('0.45')
|
|
2520
|
+
expect(task.cost_usd! > 0).toBe(true)
|
|
2521
|
+
|
|
2522
|
+
// Verify task null preservation
|
|
2523
|
+
const taskNull = migratedStore.getTask('t-null', 'c-null')!
|
|
2524
|
+
expect(taskNull.cost_usd).toBeNull()
|
|
2525
|
+
|
|
2526
|
+
migratedStore.close()
|
|
2527
|
+
|
|
2528
|
+
// Verify version = 10
|
|
2529
|
+
const verifyDb = new DatabaseSync(migDb)
|
|
2530
|
+
const version = (verifyDb.prepare('PRAGMA user_version').get() as { user_version: number }).user_version
|
|
2531
|
+
expect(version).toBe(10)
|
|
2532
|
+
|
|
2533
|
+
// Verify new REAL columns exist
|
|
2534
|
+
const convoyCols = (verifyDb.prepare('PRAGMA table_info(convoy)').all() as Array<{ name: string }>).map(c => c.name)
|
|
2535
|
+
expect(convoyCols).toContain('total_cost_usd_num')
|
|
2536
|
+
const taskCols = (verifyDb.prepare('PRAGMA table_info(task)').all() as Array<{ name: string }>).map(c => c.name)
|
|
2537
|
+
expect(taskCols).toContain('cost_usd_num')
|
|
2538
|
+
const pipelineCols = (verifyDb.prepare('PRAGMA table_info(pipeline)').all() as Array<{ name: string }>).map(c => c.name)
|
|
2539
|
+
expect(pipelineCols).toContain('total_cost_usd_num')
|
|
2540
|
+
|
|
2541
|
+
verifyDb.close()
|
|
2542
|
+
|
|
2543
|
+
// Verify backup was created
|
|
2544
|
+
expect(existsSync(`${migDb}.v9.bak`)).toBe(true)
|
|
2545
|
+
|
|
2546
|
+
rmSync(migDir, { recursive: true, force: true })
|
|
2547
|
+
})
|
|
2548
|
+
})
|
|
2549
|
+
|
|
2409
2550
|
describe('size limit enforcement', () => {
|
|
2410
2551
|
it('insertConvoy rejects spec_yaml exceeding 256KB', () => {
|
|
2411
2552
|
const bigSpecYaml = 'x'.repeat(256 * 1024 + 1)
|
|
@@ -2476,3 +2617,385 @@ describe('size limit enforcement', () => {
|
|
|
2476
2617
|
).toThrow(FieldSizeLimitError)
|
|
2477
2618
|
})
|
|
2478
2619
|
})
|
|
2620
|
+
|
|
2621
|
+
// ── Dashboard aggregate methods ───────────────────────────────────────────────
|
|
2622
|
+
|
|
2623
|
+
describe('Dashboard aggregate methods', () => {
|
|
2624
|
+
// Seed helper: inserts 5 convoys, 20 tasks, 3 DLQ entries
|
|
2625
|
+
function seedDashboardData() {
|
|
2626
|
+
// Convoy 1: done, 30s duration
|
|
2627
|
+
store.insertConvoy(makeConvoy({ id: 'dash-c1', name: 'Dash Convoy 1', status: 'pending' as const, created_at: '2026-01-01T10:00:00.000Z' }))
|
|
2628
|
+
store.updateConvoyStatus('dash-c1', 'done', { started_at: '2026-01-01T10:00:00.000Z', finished_at: '2026-01-01T10:00:30.000Z', total_tokens: 1000, total_cost_usd: 0.01 })
|
|
2629
|
+
|
|
2630
|
+
// Convoy 2: done, 60s duration
|
|
2631
|
+
store.insertConvoy(makeConvoy({ id: 'dash-c2', name: 'Dash Convoy 2', status: 'pending' as const, created_at: '2026-01-02T10:00:00.000Z' }))
|
|
2632
|
+
store.updateConvoyStatus('dash-c2', 'done', { started_at: '2026-01-02T10:00:00.000Z', finished_at: '2026-01-02T10:01:00.000Z', total_tokens: 2000, total_cost_usd: 0.02 })
|
|
2633
|
+
|
|
2634
|
+
// Convoy 3: running
|
|
2635
|
+
store.insertConvoy(makeConvoy({ id: 'dash-c3', name: 'Dash Convoy 3', status: 'pending' as const, created_at: '2026-01-03T10:00:00.000Z' }))
|
|
2636
|
+
store.updateConvoyStatus('dash-c3', 'running', { started_at: '2026-01-03T10:00:00.000Z' })
|
|
2637
|
+
|
|
2638
|
+
// Convoy 4: failed, 20s duration
|
|
2639
|
+
store.insertConvoy(makeConvoy({ id: 'dash-c4', name: 'Dash Convoy 4', status: 'pending' as const, created_at: '2026-01-04T10:00:00.000Z' }))
|
|
2640
|
+
store.updateConvoyStatus('dash-c4', 'failed', { started_at: '2026-01-04T10:00:00.000Z', finished_at: '2026-01-04T10:00:20.000Z', total_tokens: 500, total_cost_usd: 0.005 })
|
|
2641
|
+
|
|
2642
|
+
// Convoy 5: pending (no timestamps)
|
|
2643
|
+
store.insertConvoy(makeConvoy({ id: 'dash-c5', name: 'Dash Convoy 5', status: 'pending' as const, created_at: '2026-01-05T10:00:00.000Z' }))
|
|
2644
|
+
|
|
2645
|
+
// Tasks across convoys (20 total)
|
|
2646
|
+
const taskDefs = [
|
|
2647
|
+
{ id: 'dt-1', convoy_id: 'dash-c1', agent: 'developer', model: 'gpt-4o', status: 'done' as const, retries: 0, total_tokens: 100 },
|
|
2648
|
+
{ id: 'dt-2', convoy_id: 'dash-c1', agent: 'developer', model: 'gpt-4o', status: 'done' as const, retries: 0, total_tokens: 150 },
|
|
2649
|
+
{ id: 'dt-3', convoy_id: 'dash-c1', agent: 'reviewer', model: 'gpt-4o-mini', status: 'done' as const, retries: 0, total_tokens: 50 },
|
|
2650
|
+
{ id: 'dt-4', convoy_id: 'dash-c1', agent: 'reviewer', model: 'gpt-4o-mini', status: 'done' as const, retries: 1, total_tokens: 60 },
|
|
2651
|
+
{ id: 'dt-5', convoy_id: 'dash-c2', agent: 'developer', model: 'gpt-4o', status: 'done' as const, retries: 0, total_tokens: 200 },
|
|
2652
|
+
{ id: 'dt-6', convoy_id: 'dash-c2', agent: 'developer', model: 'gpt-4o', status: 'done' as const, retries: 0, total_tokens: 250 },
|
|
2653
|
+
{ id: 'dt-7', convoy_id: 'dash-c2', agent: 'developer', model: 'gpt-4o', status: 'done' as const, retries: 2, total_tokens: 300 },
|
|
2654
|
+
{ id: 'dt-8', convoy_id: 'dash-c2', agent: 'qa', model: null, status: 'done' as const, retries: 0, total_tokens: 80 },
|
|
2655
|
+
{ id: 'dt-9', convoy_id: 'dash-c3', agent: 'developer', model: 'gpt-4o', status: 'running' as const, retries: 0, total_tokens: null },
|
|
2656
|
+
{ id: 'dt-10', convoy_id: 'dash-c3', agent: 'developer', model: 'gpt-4o', status: 'assigned' as const, retries: 0, total_tokens: null },
|
|
2657
|
+
{ id: 'dt-11', convoy_id: 'dash-c3', agent: 'reviewer', model: 'gpt-4o-mini', status: 'pending' as const, retries: 0, total_tokens: null },
|
|
2658
|
+
{ id: 'dt-12', convoy_id: 'dash-c3', agent: 'reviewer', model: 'gpt-4o-mini', status: 'pending' as const, retries: 0, total_tokens: null },
|
|
2659
|
+
{ id: 'dt-13', convoy_id: 'dash-c4', agent: 'developer', model: 'gpt-4o', status: 'failed' as const, retries: 3, total_tokens: 120 },
|
|
2660
|
+
{ id: 'dt-14', convoy_id: 'dash-c4', agent: 'developer', model: 'gpt-4o', status: 'gate-failed' as const, retries: 0, total_tokens: 90 },
|
|
2661
|
+
{ id: 'dt-15', convoy_id: 'dash-c4', agent: 'qa', model: null, status: 'review-blocked' as const, retries: 0, total_tokens: 40 },
|
|
2662
|
+
{ id: 'dt-16', convoy_id: 'dash-c4', agent: 'qa', model: null, status: 'disputed' as const, retries: 1, total_tokens: 30 },
|
|
2663
|
+
{ id: 'dt-17', convoy_id: 'dash-c5', agent: 'developer', model: 'gpt-4o', status: 'pending' as const, retries: 0, total_tokens: null },
|
|
2664
|
+
{ id: 'dt-18', convoy_id: 'dash-c5', agent: 'developer', model: 'gpt-4o', status: 'pending' as const, retries: 0, total_tokens: null },
|
|
2665
|
+
{ id: 'dt-19', convoy_id: 'dash-c5', agent: 'reviewer', model: 'gpt-4o-mini', status: 'pending' as const, retries: 0, total_tokens: null },
|
|
2666
|
+
{ id: 'dt-20', convoy_id: 'dash-c5', agent: 'qa', model: null, status: 'pending' as const, retries: 0, total_tokens: null },
|
|
2667
|
+
]
|
|
2668
|
+
|
|
2669
|
+
for (const t of taskDefs) {
|
|
2670
|
+
try {
|
|
2671
|
+
store.insertTask(makeTask({ id: t.id, convoy_id: t.convoy_id, agent: t.agent, model: t.model, retries: t.retries }))
|
|
2672
|
+
} catch {
|
|
2673
|
+
// already exists
|
|
2674
|
+
}
|
|
2675
|
+
if (t.status !== 'pending') {
|
|
2676
|
+
store.updateTaskStatus(t.id, t.convoy_id, t.status, t.total_tokens !== null ? { total_tokens: t.total_tokens } : undefined)
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
// 3 DLQ entries with different failure_types
|
|
2681
|
+
const dlqEntries = [
|
|
2682
|
+
{ id: 'dlq-1', convoy_id: 'dash-c4', task_id: 'dt-13', agent: 'developer', failure_type: 'timeout' },
|
|
2683
|
+
{ id: 'dlq-2', convoy_id: 'dash-c4', task_id: 'dt-14', agent: 'developer', failure_type: 'gate_failure' },
|
|
2684
|
+
{ id: 'dlq-3', convoy_id: 'dash-c4', task_id: 'dt-15', agent: 'qa', failure_type: 'timeout' },
|
|
2685
|
+
]
|
|
2686
|
+
for (const d of dlqEntries) {
|
|
2687
|
+
store.insertDlqEntry({
|
|
2688
|
+
id: d.id,
|
|
2689
|
+
convoy_id: d.convoy_id,
|
|
2690
|
+
task_id: d.task_id,
|
|
2691
|
+
agent: d.agent,
|
|
2692
|
+
failure_type: d.failure_type,
|
|
2693
|
+
error_output: null,
|
|
2694
|
+
attempts: 1,
|
|
2695
|
+
tokens_spent: null,
|
|
2696
|
+
escalation_task_id: null,
|
|
2697
|
+
resolved: 0,
|
|
2698
|
+
resolution: null,
|
|
2699
|
+
created_at: new Date().toISOString(),
|
|
2700
|
+
resolved_at: null,
|
|
2701
|
+
})
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
describe('getConvoyCounts', () => {
|
|
2706
|
+
it('returns all zeros on empty database', () => {
|
|
2707
|
+
const result = store.getConvoyCounts()
|
|
2708
|
+
expect(result).toEqual({ total: 0, running: 0, done: 0, failed: 0, gate_failed: 0 })
|
|
2709
|
+
})
|
|
2710
|
+
|
|
2711
|
+
it('returns correct counts with seeded data', () => {
|
|
2712
|
+
seedDashboardData()
|
|
2713
|
+
const result = store.getConvoyCounts()
|
|
2714
|
+
expect(result.total).toBe(5)
|
|
2715
|
+
expect(result.done).toBe(2)
|
|
2716
|
+
expect(result.running).toBe(1)
|
|
2717
|
+
expect(result.failed).toBe(1)
|
|
2718
|
+
})
|
|
2719
|
+
})
|
|
2720
|
+
|
|
2721
|
+
describe('getConvoyDurationStats', () => {
|
|
2722
|
+
it('returns all null on empty database', () => {
|
|
2723
|
+
const result = store.getConvoyDurationStats()
|
|
2724
|
+
expect(result).toEqual({ avg_sec: null, p95_sec: null, max_sec: null })
|
|
2725
|
+
})
|
|
2726
|
+
|
|
2727
|
+
it('returns reasonable values with seeded data', () => {
|
|
2728
|
+
seedDashboardData()
|
|
2729
|
+
const result = store.getConvoyDurationStats()
|
|
2730
|
+
// 3 convoys have both timestamps: 30s, 60s, 20s — avg = 36.67
|
|
2731
|
+
expect(result.avg_sec).not.toBeNull()
|
|
2732
|
+
expect(result.avg_sec!).toBeGreaterThan(0)
|
|
2733
|
+
expect(result.max_sec).toBeGreaterThanOrEqual(result.avg_sec!)
|
|
2734
|
+
expect(result.p95_sec).not.toBeNull()
|
|
2735
|
+
})
|
|
2736
|
+
|
|
2737
|
+
it('returns correct avg for single completed convoy', () => {
|
|
2738
|
+
store.insertConvoy(makeConvoy({ id: 'single-c', name: 'Single', status: 'pending' as const }))
|
|
2739
|
+
store.updateConvoyStatus('single-c', 'done', {
|
|
2740
|
+
started_at: '2026-01-01T10:00:00.000Z',
|
|
2741
|
+
finished_at: '2026-01-01T10:01:00.000Z',
|
|
2742
|
+
})
|
|
2743
|
+
const result = store.getConvoyDurationStats()
|
|
2744
|
+
expect(result.avg_sec).toBeCloseTo(60, 0)
|
|
2745
|
+
expect(result.p95_sec).toBeCloseTo(60, 0)
|
|
2746
|
+
expect(result.max_sec).toBeCloseTo(60, 0)
|
|
2747
|
+
})
|
|
2748
|
+
})
|
|
2749
|
+
|
|
2750
|
+
describe('getTokenAndCostTotals', () => {
|
|
2751
|
+
it('returns zeros on empty database', () => {
|
|
2752
|
+
const result = store.getTokenAndCostTotals()
|
|
2753
|
+
expect(result).toEqual({ total_tokens: 0, total_cost_usd: 0 })
|
|
2754
|
+
})
|
|
2755
|
+
|
|
2756
|
+
it('returns correct sums with seeded data', () => {
|
|
2757
|
+
seedDashboardData()
|
|
2758
|
+
const result = store.getTokenAndCostTotals()
|
|
2759
|
+
// convoy totals: 1000+2000+500 = 3500 tokens (c3 and c5 have none)
|
|
2760
|
+
expect(result.total_tokens).toBe(3500)
|
|
2761
|
+
// cost: 0.01+0.02+0.005 = 0.035
|
|
2762
|
+
expect(result.total_cost_usd).toBeCloseTo(0.035, 5)
|
|
2763
|
+
})
|
|
2764
|
+
})
|
|
2765
|
+
|
|
2766
|
+
describe('getTopAgents', () => {
|
|
2767
|
+
it('returns empty array on empty database', () => {
|
|
2768
|
+
const result = store.getTopAgents(5)
|
|
2769
|
+
expect(result).toEqual([])
|
|
2770
|
+
})
|
|
2771
|
+
|
|
2772
|
+
it('returns agents ordered by task_count DESC', () => {
|
|
2773
|
+
seedDashboardData()
|
|
2774
|
+
const result = store.getTopAgents(10)
|
|
2775
|
+
expect(result.length).toBeGreaterThan(0)
|
|
2776
|
+
// developer should be top agent (appears most)
|
|
2777
|
+
expect(result[0].agent).toBe('developer')
|
|
2778
|
+
// verify descending order
|
|
2779
|
+
for (let i = 1; i < result.length; i++) {
|
|
2780
|
+
expect(result[i - 1].task_count).toBeGreaterThanOrEqual(result[i].task_count)
|
|
2781
|
+
}
|
|
2782
|
+
})
|
|
2783
|
+
|
|
2784
|
+
it('respects the limit parameter', () => {
|
|
2785
|
+
seedDashboardData()
|
|
2786
|
+
const result = store.getTopAgents(1)
|
|
2787
|
+
expect(result).toHaveLength(1)
|
|
2788
|
+
})
|
|
2789
|
+
|
|
2790
|
+
it('each entry has agent, task_count, total_tokens', () => {
|
|
2791
|
+
seedDashboardData()
|
|
2792
|
+
const result = store.getTopAgents(5)
|
|
2793
|
+
for (const row of result) {
|
|
2794
|
+
expect(typeof row.agent).toBe('string')
|
|
2795
|
+
expect(typeof row.task_count).toBe('number')
|
|
2796
|
+
expect(typeof row.total_tokens).toBe('number')
|
|
2797
|
+
}
|
|
2798
|
+
})
|
|
2799
|
+
})
|
|
2800
|
+
|
|
2801
|
+
describe('getTopModels', () => {
|
|
2802
|
+
it('returns empty array on empty database', () => {
|
|
2803
|
+
const result = store.getTopModels(5)
|
|
2804
|
+
expect(result).toEqual([])
|
|
2805
|
+
})
|
|
2806
|
+
|
|
2807
|
+
it('returns models ordered by task_count DESC', () => {
|
|
2808
|
+
seedDashboardData()
|
|
2809
|
+
const result = store.getTopModels(10)
|
|
2810
|
+
expect(result.length).toBeGreaterThan(0)
|
|
2811
|
+
for (let i = 1; i < result.length; i++) {
|
|
2812
|
+
expect(result[i - 1].task_count).toBeGreaterThanOrEqual(result[i].task_count)
|
|
2813
|
+
}
|
|
2814
|
+
})
|
|
2815
|
+
|
|
2816
|
+
it('excludes tasks with null model', () => {
|
|
2817
|
+
seedDashboardData()
|
|
2818
|
+
const result = store.getTopModels(10)
|
|
2819
|
+
for (const row of result) {
|
|
2820
|
+
expect(row.model).not.toBeNull()
|
|
2821
|
+
}
|
|
2822
|
+
})
|
|
2823
|
+
})
|
|
2824
|
+
|
|
2825
|
+
describe('getDlqSummary', () => {
|
|
2826
|
+
it('returns count=0 and empty array on empty database', () => {
|
|
2827
|
+
const result = store.getDlqSummary()
|
|
2828
|
+
expect(result).toEqual({ count: 0, top_failure_types: [] })
|
|
2829
|
+
})
|
|
2830
|
+
|
|
2831
|
+
it('returns count=3 with seeded data', () => {
|
|
2832
|
+
seedDashboardData()
|
|
2833
|
+
const result = store.getDlqSummary()
|
|
2834
|
+
expect(result.count).toBe(3)
|
|
2835
|
+
})
|
|
2836
|
+
|
|
2837
|
+
it('groups failure types correctly', () => {
|
|
2838
|
+
seedDashboardData()
|
|
2839
|
+
const result = store.getDlqSummary()
|
|
2840
|
+
const timeoutEntry = result.top_failure_types.find(t => t.type === 'timeout')
|
|
2841
|
+
const gateEntry = result.top_failure_types.find(t => t.type === 'gate_failure')
|
|
2842
|
+
expect(timeoutEntry?.count).toBe(2)
|
|
2843
|
+
expect(gateEntry?.count).toBe(1)
|
|
2844
|
+
})
|
|
2845
|
+
|
|
2846
|
+
it('orders failure types by count DESC', () => {
|
|
2847
|
+
seedDashboardData()
|
|
2848
|
+
const result = store.getDlqSummary()
|
|
2849
|
+
for (let i = 1; i < result.top_failure_types.length; i++) {
|
|
2850
|
+
expect(result.top_failure_types[i - 1].count).toBeGreaterThanOrEqual(result.top_failure_types[i].count)
|
|
2851
|
+
}
|
|
2852
|
+
})
|
|
2853
|
+
})
|
|
2854
|
+
|
|
2855
|
+
describe('getConvoyTaskSummary', () => {
|
|
2856
|
+
it('returns all zeros for non-existent convoy', () => {
|
|
2857
|
+
const result = store.getConvoyTaskSummary('nonexistent')
|
|
2858
|
+
expect(result).toEqual({
|
|
2859
|
+
total: 0, done: 0, running: 0, failed: 0,
|
|
2860
|
+
review_blocked: 0, disputed: 0, reviewed: 0, panel_reviewed: 0,
|
|
2861
|
+
tasks_with_drift: 0, max_drift_score: null, drift_retried: 0,
|
|
2862
|
+
})
|
|
2863
|
+
})
|
|
2864
|
+
|
|
2865
|
+
it('returns correct per-status counts (done convoys)', () => {
|
|
2866
|
+
seedDashboardData()
|
|
2867
|
+
const result = store.getConvoyTaskSummary('dash-c1')
|
|
2868
|
+
// c1 has tasks dt-1..dt-4: all done
|
|
2869
|
+
expect(result.total).toBe(4)
|
|
2870
|
+
expect(result.done).toBe(4)
|
|
2871
|
+
expect(result.running).toBe(0)
|
|
2872
|
+
expect(result.failed).toBe(0)
|
|
2873
|
+
})
|
|
2874
|
+
|
|
2875
|
+
it('returns correct counts for running convoy with mixed statuses', () => {
|
|
2876
|
+
seedDashboardData()
|
|
2877
|
+
const result = store.getConvoyTaskSummary('dash-c3')
|
|
2878
|
+
// c3: running(1), assigned(1), pending(2)
|
|
2879
|
+
expect(result.total).toBe(4)
|
|
2880
|
+
expect(result.running).toBe(2) // running + assigned
|
|
2881
|
+
})
|
|
2882
|
+
|
|
2883
|
+
it('returns failed and review_blocked and disputed counts', () => {
|
|
2884
|
+
seedDashboardData()
|
|
2885
|
+
const result = store.getConvoyTaskSummary('dash-c4')
|
|
2886
|
+
// c4: failed(1), gate-failed(1), review-blocked(1), disputed(1)
|
|
2887
|
+
expect(result.failed).toBe(2) // failed + gate-failed
|
|
2888
|
+
expect(result.review_blocked).toBe(1)
|
|
2889
|
+
expect(result.disputed).toBe(1)
|
|
2890
|
+
})
|
|
2891
|
+
})
|
|
2892
|
+
|
|
2893
|
+
describe('getConvoyList', () => {
|
|
2894
|
+
it('returns empty array on empty database', () => {
|
|
2895
|
+
const result = store.getConvoyList(10, 0)
|
|
2896
|
+
expect(result).toEqual([])
|
|
2897
|
+
})
|
|
2898
|
+
|
|
2899
|
+
it('returns convoys ordered by created_at DESC', () => {
|
|
2900
|
+
seedDashboardData()
|
|
2901
|
+
const result = store.getConvoyList(10, 0)
|
|
2902
|
+
expect(result.length).toBeGreaterThan(0)
|
|
2903
|
+
for (let i = 1; i < result.length; i++) {
|
|
2904
|
+
expect(result[i - 1].created_at >= result[i].created_at).toBe(true)
|
|
2905
|
+
}
|
|
2906
|
+
})
|
|
2907
|
+
|
|
2908
|
+
it('respects limit', () => {
|
|
2909
|
+
seedDashboardData()
|
|
2910
|
+
const result = store.getConvoyList(2, 0)
|
|
2911
|
+
expect(result).toHaveLength(2)
|
|
2912
|
+
})
|
|
2913
|
+
|
|
2914
|
+
it('respects offset for pagination', () => {
|
|
2915
|
+
seedDashboardData()
|
|
2916
|
+
const first = store.getConvoyList(5, 0)
|
|
2917
|
+
const second = store.getConvoyList(5, 2)
|
|
2918
|
+
// Items 2+ should appear in second page
|
|
2919
|
+
expect(second[0].id).toBe(first[2].id)
|
|
2920
|
+
})
|
|
2921
|
+
|
|
2922
|
+
it('returns ConvoyRecord with total_cost_usd_num alias', () => {
|
|
2923
|
+
seedDashboardData()
|
|
2924
|
+
const result = store.getConvoyList(5, 0)
|
|
2925
|
+
// Should not throw and should return records with expected shape
|
|
2926
|
+
for (const r of result) {
|
|
2927
|
+
expect(typeof r.id).toBe('string')
|
|
2928
|
+
expect(typeof r.status).toBe('string')
|
|
2929
|
+
}
|
|
2930
|
+
})
|
|
2931
|
+
})
|
|
2932
|
+
|
|
2933
|
+
describe('getConvoyDetails', () => {
|
|
2934
|
+
it('returns null for non-existent convoy', () => {
|
|
2935
|
+
const result = store.getConvoyDetails('nonexistent')
|
|
2936
|
+
expect(result).toBeNull()
|
|
2937
|
+
})
|
|
2938
|
+
|
|
2939
|
+
it('returns full detail object for existing convoy', () => {
|
|
2940
|
+
seedDashboardData()
|
|
2941
|
+
const result = store.getConvoyDetails('dash-c1')
|
|
2942
|
+
expect(result).not.toBeNull()
|
|
2943
|
+
expect(result).toHaveProperty('convoy')
|
|
2944
|
+
expect(result).toHaveProperty('taskSummary')
|
|
2945
|
+
expect(result).toHaveProperty('quality')
|
|
2946
|
+
expect(result).toHaveProperty('drift')
|
|
2947
|
+
expect(result).toHaveProperty('dlq_count')
|
|
2948
|
+
expect(result).toHaveProperty('dlq_entries')
|
|
2949
|
+
expect(result).toHaveProperty('artifact_count')
|
|
2950
|
+
expect(result).toHaveProperty('artifacts')
|
|
2951
|
+
expect(result).toHaveProperty('has_more_events')
|
|
2952
|
+
expect(result).toHaveProperty('events')
|
|
2953
|
+
expect(result).toHaveProperty('tasks')
|
|
2954
|
+
})
|
|
2955
|
+
|
|
2956
|
+
it('convoy sub-object has correct fields', () => {
|
|
2957
|
+
seedDashboardData()
|
|
2958
|
+
const result = store.getConvoyDetails('dash-c1')!
|
|
2959
|
+
expect(result.convoy.id).toBe('dash-c1')
|
|
2960
|
+
expect(result.convoy.name).toBe('Dash Convoy 1')
|
|
2961
|
+
expect(result.convoy.status).toBe('done')
|
|
2962
|
+
expect(result.convoy.total_tokens).toBe(1000)
|
|
2963
|
+
expect(typeof result.convoy.total_cost_usd).toBe('number')
|
|
2964
|
+
})
|
|
2965
|
+
|
|
2966
|
+
it('tasks list matches getTasksByConvoy', () => {
|
|
2967
|
+
seedDashboardData()
|
|
2968
|
+
const detail = store.getConvoyDetails('dash-c1')!
|
|
2969
|
+
const direct = store.getTasksByConvoy('dash-c1')
|
|
2970
|
+
expect(detail.tasks).toHaveLength(direct.length)
|
|
2971
|
+
const detailIds = detail.tasks.map(t => t.id).sort()
|
|
2972
|
+
const directIds = direct.map(t => t.id).sort()
|
|
2973
|
+
expect(detailIds).toEqual(directIds)
|
|
2974
|
+
})
|
|
2975
|
+
|
|
2976
|
+
it('taskSummary matches getConvoyTaskSummary', () => {
|
|
2977
|
+
seedDashboardData()
|
|
2978
|
+
const detail = store.getConvoyDetails('dash-c1')!
|
|
2979
|
+
const direct = store.getConvoyTaskSummary('dash-c1')
|
|
2980
|
+
expect(detail.taskSummary.total).toBe(direct.total)
|
|
2981
|
+
expect(detail.taskSummary.done).toBe(direct.done)
|
|
2982
|
+
})
|
|
2983
|
+
|
|
2984
|
+
it('dlq_count and dlq_entries match listDlqEntries for convoy with DLQ', () => {
|
|
2985
|
+
seedDashboardData()
|
|
2986
|
+
const detail = store.getConvoyDetails('dash-c4')!
|
|
2987
|
+
const direct = store.listDlqEntries('dash-c4')
|
|
2988
|
+
expect(detail.dlq_count).toBe(direct.length)
|
|
2989
|
+
expect(detail.dlq_entries).toHaveLength(direct.length)
|
|
2990
|
+
const detailIds = detail.dlq_entries.map(d => d.id).sort()
|
|
2991
|
+
const directIds = direct.map(d => d.id).sort()
|
|
2992
|
+
expect(detailIds).toEqual(directIds)
|
|
2993
|
+
})
|
|
2994
|
+
|
|
2995
|
+
it('has_more_events is false when no events', () => {
|
|
2996
|
+
seedDashboardData()
|
|
2997
|
+
const result = store.getConvoyDetails('dash-c1')!
|
|
2998
|
+
expect(result.has_more_events).toBe(false)
|
|
2999
|
+
})
|
|
3000
|
+
})
|
|
3001
|
+
})
|