@vitest-agent/sdk 3.1.2 → 4.0.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.
Files changed (65) hide show
  1. package/README.md +23 -19
  2. package/dispatch.d.ts +25 -6
  3. package/formatters/markdown.js +1 -0
  4. package/formatters/terminal.js +4 -3
  5. package/index.d.ts +448 -2341
  6. package/index.js +10 -68
  7. package/internal-inject-env.js +5 -7
  8. package/package.json +3 -19
  9. package/sidecar-dispatch.js +7 -5
  10. package/utils/format-console.js +9 -10
  11. package/utils/format-terminal.js +7 -7
  12. package/utils/posix-path.js +52 -0
  13. package/utils/test-location.js +11 -5
  14. package/version.js +17 -0
  15. package/layers/ConfigLive.js +0 -46
  16. package/layers/DataReaderLive.js +0 -1923
  17. package/layers/DataStoreLive.js +0 -1287
  18. package/layers/DetailResolverLive.js +0 -9
  19. package/layers/DiscoveryRegistryLive.js +0 -70
  20. package/layers/EnvironmentDetectorLive.js +0 -21
  21. package/layers/EnvironmentDetectorTest.js +0 -13
  22. package/layers/ExecutorResolverLive.js +0 -9
  23. package/layers/FormatSelectorLive.js +0 -20
  24. package/layers/HistoryTrackerLive.js +0 -49
  25. package/layers/HistoryTrackerTest.js +0 -25
  26. package/layers/LoggerLive.js +0 -64
  27. package/layers/OutputPipelineLive.js +0 -13
  28. package/layers/OutputRendererLive.js +0 -28
  29. package/layers/PathResolutionLive.js +0 -40
  30. package/layers/PerClientSessionMapLive.js +0 -145
  31. package/layers/ProjectDiscoveryLive.js +0 -68
  32. package/layers/ProjectDiscoveryTest.js +0 -12
  33. package/layers/ProjectIdentityLive.js +0 -96
  34. package/layers/RunContextLive.js +0 -81
  35. package/lib/format-triage.js +0 -103
  36. package/lib/format-wrapup.js +0 -67
  37. package/migrations/0001_initial.js +0 -824
  38. package/migrations/0002_test_artifacts.js +0 -30
  39. package/migrations/index.js +0 -26
  40. package/migrations/registry_0001_initial.js +0 -39
  41. package/migrations/session_map_0001_initial.js +0 -38
  42. package/services/Config.js +0 -15
  43. package/services/DataReader.js +0 -8
  44. package/services/DataStore.js +0 -15
  45. package/services/DetailResolver.js +0 -8
  46. package/services/DiscoveryRegistry.js +0 -8
  47. package/services/EnvironmentDetector.js +0 -8
  48. package/services/ExecutorResolver.js +0 -8
  49. package/services/FormatSelector.js +0 -8
  50. package/services/HistoryTracker.js +0 -20
  51. package/services/OutputRenderer.js +0 -8
  52. package/services/PerClientSessionMap.js +0 -22
  53. package/services/ProjectDiscovery.js +0 -8
  54. package/services/ProjectIdentity.js +0 -63
  55. package/services/RunContext.js +0 -53
  56. package/services/idempotency.js +0 -58
  57. package/sql/assemblers.js +0 -69
  58. package/testing/layers.js +0 -21
  59. package/testing.d.ts +0 -1862
  60. package/testing.js +0 -254
  61. package/utils/ensure-migrated.js +0 -52
  62. package/utils/failure-signature.js +0 -34
  63. package/utils/resolve-data-path.js +0 -65
  64. package/utils/resolve-project-key-from-cwd.js +0 -53
  65. package/utils/resolve-workspace-key.js +0 -41
@@ -1,1923 +0,0 @@
1
- import { DataStoreError, extractSqlReason } from "../errors/DataStoreError.js";
2
- import { DataReader } from "../services/DataReader.js";
3
- import { historyKey } from "../services/HistoryTracker.js";
4
- import { Effect, Layer, Option } from "effect";
5
- import { SqlClient } from "effect/unstable/sql/SqlClient";
6
-
7
- //#region src/layers/DataReaderLive.ts
8
- /** @public */
9
- const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
10
- const sql = yield* SqlClient;
11
- const getLatestRun = (project) => Effect.gen(function* () {
12
- yield* Effect.logDebug("getLatestRun").pipe(Effect.annotateLogs({ project }));
13
- const runs = yield* sql`SELECT id, timestamp, reason, duration, total, passed, failed, skipped, project
14
- FROM test_runs
15
- WHERE project = ${project}
16
- ORDER BY timestamp DESC LIMIT 1`;
17
- if (runs.length === 0) return Option.none();
18
- const run = runs[0];
19
- const failedModules = yield* sql`SELECT f.path as file_path, tm.state as module_state, tm.duration as module_duration, tm.id as module_id
20
- FROM test_modules tm
21
- JOIN files f ON f.id = tm.file_id
22
- WHERE tm.run_id = ${run.id} AND tm.state = 'failed'`;
23
- const moduleIds = failedModules.map((m) => m.module_id);
24
- const allTestCases = moduleIds.length > 0 ? yield* sql`SELECT tc.module_id, tc.id as test_case_id, tc.name, tc.full_name, tc.state, tc.duration, tc.flaky, tc.slow, tc.classification
25
- FROM test_cases tc WHERE tc.module_id IN ${sql.in(moduleIds)}` : [];
26
- const allErrors = moduleIds.length > 0 ? yield* sql`SELECT te.test_case_id, te.module_id, te.scope, te.message, te.stack, te.diff
27
- FROM test_errors te
28
- WHERE te.run_id = ${run.id}
29
- AND te.module_id IN ${sql.in(moduleIds)}` : [];
30
- const testsByModule = /* @__PURE__ */ new Map();
31
- for (const tc of allTestCases) {
32
- const arr = testsByModule.get(tc.module_id);
33
- if (arr) arr.push(tc);
34
- else testsByModule.set(tc.module_id, [tc]);
35
- }
36
- const errorsByTestCase = /* @__PURE__ */ new Map();
37
- const errorsByModule = /* @__PURE__ */ new Map();
38
- for (const e of allErrors) if (e.scope === "test" && e.test_case_id != null) {
39
- const arr = errorsByTestCase.get(e.test_case_id);
40
- if (arr) arr.push(e);
41
- else errorsByTestCase.set(e.test_case_id, [e]);
42
- } else if (e.scope === "module" && e.module_id != null) {
43
- const arr = errorsByModule.get(e.module_id);
44
- if (arr) arr.push(e);
45
- else errorsByModule.set(e.module_id, [e]);
46
- }
47
- const failedModuleReports = failedModules.map((mod) => {
48
- const tests = testsByModule.get(mod.module_id) ?? [];
49
- const modErrors = errorsByModule.get(mod.module_id) ?? [];
50
- const testReports = tests.map((tc) => {
51
- const tcErrors = errorsByTestCase.get(tc.test_case_id) ?? [];
52
- return {
53
- name: tc.name,
54
- fullName: tc.full_name,
55
- state: tc.state,
56
- ...tc.duration != null ? { duration: tc.duration } : {},
57
- ...tc.flaky === 1 ? { flaky: true } : {},
58
- ...tc.slow === 1 ? { slow: true } : {},
59
- ...tcErrors.length > 0 ? { errors: tcErrors.map((e) => ({
60
- message: e.message,
61
- ...e.stack != null ? { stack: e.stack } : {},
62
- ...e.diff != null ? { diff: e.diff } : {}
63
- })) } : {},
64
- ...tc.classification != null ? { classification: tc.classification } : {}
65
- };
66
- });
67
- return {
68
- file: mod.file_path,
69
- state: mod.module_state,
70
- ...mod.module_duration != null ? { duration: mod.module_duration } : {},
71
- ...modErrors.length > 0 ? { errors: modErrors.map((e) => ({
72
- message: e.message,
73
- ...e.stack != null ? { stack: e.stack } : {},
74
- ...e.diff != null ? { diff: e.diff } : {}
75
- })) } : {},
76
- tests: testReports
77
- };
78
- });
79
- const unhandledErrors = yield* sql`SELECT message, stack, diff FROM test_errors
80
- WHERE run_id = ${run.id} AND scope = 'unhandled'`;
81
- const failedFiles = failedModules.map((m) => m.file_path);
82
- const projectName = run.project;
83
- const fileCovRows = yield* sql`SELECT f.path AS file_path, fc.statements, fc.branches, fc.functions, fc.lines,
84
- fc.uncovered_lines, fc.tier
85
- FROM file_coverage fc
86
- JOIN files f ON f.id = fc.file_id
87
- WHERE fc.run_id = ${run.id}`;
88
- const lowCoverage = fileCovRows.filter((r) => r.tier === "below_threshold").map((r) => ({
89
- file: r.file_path,
90
- summary: {
91
- statements: r.statements,
92
- branches: r.branches,
93
- functions: r.functions,
94
- lines: r.lines
95
- },
96
- uncoveredLines: r.uncovered_lines ?? ""
97
- }));
98
- const belowTarget = fileCovRows.filter((r) => r.tier === "below_target").map((r) => ({
99
- file: r.file_path,
100
- summary: {
101
- statements: r.statements,
102
- branches: r.branches,
103
- functions: r.functions,
104
- lines: r.lines
105
- },
106
- uncoveredLines: r.uncovered_lines ?? ""
107
- }));
108
- const trendRows = yield* sql`SELECT statements, branches, functions, lines FROM coverage_trends WHERE run_id = ${run.id} LIMIT 1`;
109
- const totals = trendRows[0] ? {
110
- statements: trendRows[0].statements,
111
- branches: trendRows[0].branches,
112
- functions: trendRows[0].functions,
113
- lines: trendRows[0].lines
114
- } : {
115
- statements: 0,
116
- branches: 0,
117
- functions: 0,
118
- lines: 0
119
- };
120
- const baselineRows = yield* sql`SELECT metric, value FROM coverage_baselines
121
- WHERE project = '__global__' AND kind = 'baseline' AND pattern = ''`;
122
- const thresholds = {};
123
- for (const b of baselineRows) if (b.metric === "lines") thresholds.lines = b.value;
124
- else if (b.metric === "functions") thresholds.functions = b.value;
125
- else if (b.metric === "branches") thresholds.branches = b.value;
126
- else if (b.metric === "statements") thresholds.statements = b.value;
127
- const coverage = fileCovRows.length > 0 || trendRows.length > 0 ? {
128
- totals,
129
- thresholds: {
130
- global: thresholds,
131
- patterns: []
132
- },
133
- scoped: false,
134
- lowCoverage,
135
- lowCoverageFiles: lowCoverage.map((f) => f.file),
136
- ...belowTarget.length > 0 ? {
137
- belowTarget,
138
- belowTargetFiles: belowTarget.map((f) => f.file)
139
- } : {}
140
- } : void 0;
141
- const report = {
142
- timestamp: run.timestamp,
143
- project: projectName,
144
- reason: run.reason,
145
- summary: {
146
- total: run.total,
147
- passed: run.passed,
148
- failed: run.failed,
149
- skipped: run.skipped,
150
- duration: run.duration
151
- },
152
- failed: failedModuleReports,
153
- unhandledErrors: unhandledErrors.map((e) => ({
154
- message: e.message,
155
- ...e.stack != null ? { stack: e.stack } : {},
156
- ...e.diff != null ? { diff: e.diff } : {}
157
- })),
158
- failedFiles,
159
- ...coverage ? { coverage } : {}
160
- };
161
- return Option.some(report);
162
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
163
- operation: "read",
164
- table: "test_runs",
165
- reason: extractSqlReason(e)
166
- })));
167
- const getRunsByProject = () => Effect.gen(function* () {
168
- yield* Effect.logDebug("getRunsByProject");
169
- return (yield* sql`SELECT
170
- t1.project,
171
- t1.timestamp as last_run,
172
- t1.reason as last_result,
173
- t1.total,
174
- t1.passed,
175
- t1.failed,
176
- t1.skipped
177
- FROM test_runs t1
178
- INNER JOIN (
179
- SELECT project, MAX(timestamp) as max_ts
180
- FROM test_runs
181
- GROUP BY project
182
- ) t2 ON t1.project = t2.project
183
- AND t1.timestamp = t2.max_ts`).map((r) => ({
184
- project: r.project,
185
- lastRun: r.last_run,
186
- lastResult: r.last_result,
187
- total: r.total,
188
- passed: r.passed,
189
- failed: r.failed,
190
- skipped: r.skipped
191
- }));
192
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
193
- operation: "read",
194
- table: "test_runs",
195
- reason: extractSqlReason(e)
196
- })));
197
- const getHistory = (project, options) => Effect.gen(function* () {
198
- yield* Effect.logDebug("getHistory").pipe(Effect.annotateLogs({
199
- project,
200
- ...options
201
- }));
202
- const testName = options?.testName ?? null;
203
- const modulePath = options?.modulePath ?? null;
204
- const limit = options?.limit ?? 20;
205
- const rows = yield* sql`WITH ranked AS (
206
- SELECT module_path, full_name, timestamp, state,
207
- ROW_NUMBER() OVER (
208
- PARTITION BY module_path, full_name
209
- ORDER BY timestamp DESC
210
- ) AS rn
211
- FROM test_history
212
- WHERE project = ${project}
213
- AND (${testName} IS NULL OR full_name = ${testName})
214
- AND (${modulePath} IS NULL OR module_path = ${modulePath})
215
- )
216
- SELECT module_path, full_name, timestamp, state
217
- FROM ranked
218
- WHERE rn <= ${limit}
219
- ORDER BY module_path, full_name, timestamp DESC`;
220
- const testsMap = /* @__PURE__ */ new Map();
221
- for (const row of rows) {
222
- if (row.state !== "passed" && row.state !== "failed") continue;
223
- const key = historyKey(row.module_path, row.full_name);
224
- const existing = testsMap.get(key);
225
- if (existing) existing.runs.push({
226
- timestamp: row.timestamp,
227
- state: row.state
228
- });
229
- else testsMap.set(key, {
230
- modulePath: row.module_path,
231
- fullName: row.full_name,
232
- runs: [{
233
- timestamp: row.timestamp,
234
- state: row.state
235
- }]
236
- });
237
- }
238
- const tests = Array.from(testsMap.values()).map(({ modulePath, fullName, runs }) => ({
239
- modulePath,
240
- fullName,
241
- runs
242
- }));
243
- return {
244
- project,
245
- updatedAt: tests.reduce((latest, t) => {
246
- const ts = t.runs[0]?.timestamp ?? "";
247
- return ts > latest ? ts : latest;
248
- }, "") || (/* @__PURE__ */ new Date()).toISOString(),
249
- tests
250
- };
251
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
252
- operation: "read",
253
- table: "test_history",
254
- reason: extractSqlReason(e)
255
- })));
256
- /**
257
- * Shared read for the `coverage_baselines` table's per-`kind` rows
258
- * (`baseline` / `threshold` / `target`). Returns `Option.none()` when
259
- * no rows of that kind were ever persisted, so a caller can tell
260
- * "never configured" apart from "configured with zero metrics"
261
- * (issue #237).
262
- */
263
- const getCoveragePolicy = (kind, project) => Effect.gen(function* () {
264
- yield* Effect.logDebug("getCoveragePolicy").pipe(Effect.annotateLogs({
265
- kind,
266
- project
267
- }));
268
- const rows = yield* sql`SELECT metric, value, pattern, updated_at
269
- FROM coverage_baselines
270
- WHERE project = ${project} AND kind = ${kind}
271
- ORDER BY pattern, metric`;
272
- if (rows.length === 0) return Option.none();
273
- const global = {};
274
- const patternsMap = /* @__PURE__ */ new Map();
275
- let updatedAt = rows[0].updated_at;
276
- for (const row of rows) {
277
- if (row.updated_at > updatedAt) updatedAt = row.updated_at;
278
- if (row.pattern === "") global[row.metric] = row.value;
279
- else {
280
- const existing = patternsMap.get(row.pattern);
281
- if (existing) existing[row.metric] = row.value;
282
- else patternsMap.set(row.pattern, { [row.metric]: row.value });
283
- }
284
- }
285
- const patterns = Array.from(patternsMap.entries());
286
- const baselines = {
287
- updatedAt,
288
- global,
289
- patterns
290
- };
291
- return Option.some(baselines);
292
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
293
- operation: "read",
294
- table: "coverage_baselines",
295
- reason: extractSqlReason(e)
296
- })));
297
- const getBaselines = (project) => getCoveragePolicy("baseline", project);
298
- const getTrends = (project, limit) => Effect.gen(function* () {
299
- yield* Effect.logDebug("getTrends").pipe(Effect.annotateLogs({
300
- project,
301
- limit: limit ?? 50
302
- }));
303
- const rows = yield* sql`SELECT timestamp, lines, functions, branches, statements, direction, targets_hash
304
- FROM coverage_trends
305
- WHERE project = ${project}
306
- ORDER BY timestamp DESC
307
- LIMIT ${limit ?? 50}`;
308
- if (rows.length === 0) return Option.none();
309
- const chronological = [...rows].reverse();
310
- const record = { entries: chronological.map((row, i) => {
311
- const prev = i > 0 ? chronological[i - 1] : null;
312
- return {
313
- timestamp: row.timestamp,
314
- coverage: {
315
- lines: row.lines,
316
- functions: row.functions,
317
- branches: row.branches,
318
- statements: row.statements
319
- },
320
- delta: prev ? {
321
- lines: row.lines - prev.lines,
322
- functions: row.functions - prev.functions,
323
- branches: row.branches - prev.branches,
324
- statements: row.statements - prev.statements
325
- } : {
326
- lines: 0,
327
- functions: 0,
328
- branches: 0,
329
- statements: 0
330
- },
331
- direction: row.direction,
332
- ...row.targets_hash != null ? { targetsHash: row.targets_hash } : {}
333
- };
334
- }) };
335
- return Option.some(record);
336
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
337
- operation: "read",
338
- table: "coverage_trends",
339
- reason: extractSqlReason(e)
340
- })));
341
- const getFlaky = (project, options) => Effect.gen(function* () {
342
- yield* Effect.logDebug("getFlaky").pipe(Effect.annotateLogs({
343
- project,
344
- ...options
345
- }));
346
- const testName = options?.testName ?? null;
347
- const modulePath = options?.modulePath ?? null;
348
- return (yield* sql`SELECT
349
- th1.full_name,
350
- th1.module_path,
351
- th1.project,
352
- SUM(CASE WHEN th1.state = 'passed' THEN 1 ELSE 0 END) as pass_count,
353
- SUM(CASE WHEN th1.state = 'failed' THEN 1 ELSE 0 END) as fail_count,
354
- (SELECT state FROM test_history th2
355
- WHERE th2.full_name = th1.full_name
356
- AND th2.module_path = th1.module_path
357
- AND th2.project = th1.project
358
- ORDER BY timestamp DESC LIMIT 1) as last_state,
359
- MAX(th1.timestamp) as last_timestamp
360
- FROM test_history th1
361
- WHERE th1.project = ${project}
362
- AND (${testName} IS NULL OR th1.full_name = ${testName})
363
- AND (${modulePath} IS NULL OR th1.module_path = ${modulePath})
364
- GROUP BY th1.full_name, th1.module_path, th1.project
365
- -- Flaky means oscillation, not a clean recovery. A test with both
366
- -- passes and fails is only flaky when at least one failure occurs at
367
- -- or after the earliest pass (a fail-after-pass regression). A
368
- -- monotonic red->green cycle — every failure precedes every pass — is
369
- -- a recovery, not flakiness, so it is excluded here. ISO-8601
370
- -- timestamps compare lexicographically. Grouping keys on module_path
371
- -- so same-named tests in different files stay distinct series.
372
- HAVING pass_count > 0 AND fail_count > 0
373
- AND MAX(CASE WHEN th1.state = 'failed' THEN th1.timestamp END)
374
- >= MIN(CASE WHEN th1.state = 'passed' THEN th1.timestamp END)`).map((r) => ({
375
- fullName: r.full_name,
376
- modulePath: r.module_path,
377
- project: r.project,
378
- passCount: r.pass_count,
379
- failCount: r.fail_count,
380
- lastState: r.last_state,
381
- lastTimestamp: r.last_timestamp
382
- }));
383
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
384
- operation: "read",
385
- table: "test_history",
386
- reason: extractSqlReason(e)
387
- })));
388
- const getPersistentFailures = (project, options) => Effect.gen(function* () {
389
- yield* Effect.logDebug("getPersistentFailures").pipe(Effect.annotateLogs({
390
- project,
391
- ...options
392
- }));
393
- const testName = options?.testName ?? null;
394
- const modulePath = options?.modulePath ?? null;
395
- return (yield* sql`WITH ranked AS (
396
- SELECT full_name, module_path, project, state, timestamp, error_message,
397
- ROW_NUMBER() OVER (
398
- PARTITION BY project, module_path, full_name
399
- ORDER BY timestamp DESC
400
- ) as rn
401
- FROM test_history
402
- WHERE project = ${project}
403
- AND (${testName} IS NULL OR full_name = ${testName})
404
- AND (${modulePath} IS NULL OR module_path = ${modulePath})
405
- ),
406
- streak AS (
407
- SELECT full_name, module_path, project, state, timestamp, error_message, rn,
408
- MIN(CASE WHEN state != 'failed' THEN rn END) OVER (
409
- PARTITION BY project, module_path, full_name
410
- ) as first_pass_rn
411
- FROM ranked
412
- )
413
- SELECT
414
- full_name, module_path, project,
415
- COUNT(*) as consecutive_failures,
416
- MIN(timestamp) as first_failed_at,
417
- MAX(timestamp) as last_failed_at,
418
- (SELECT error_message FROM streak s2
419
- WHERE s2.full_name = streak.full_name
420
- AND s2.module_path = streak.module_path
421
- AND s2.project = streak.project
422
- AND s2.rn = 1) as last_error_message
423
- FROM streak
424
- WHERE state = 'failed'
425
- AND (first_pass_rn IS NULL OR rn < first_pass_rn)
426
- GROUP BY full_name, module_path, project
427
- HAVING consecutive_failures >= 2`).map((row) => ({
428
- fullName: row.full_name,
429
- modulePath: row.module_path,
430
- project: row.project,
431
- consecutiveFailures: row.consecutive_failures,
432
- firstFailedAt: row.first_failed_at,
433
- lastFailedAt: row.last_failed_at,
434
- lastErrorMessage: row.last_error_message
435
- }));
436
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
437
- operation: "read",
438
- table: "test_history",
439
- reason: extractSqlReason(e)
440
- })));
441
- const getFileCoverage = (runId) => Effect.gen(function* () {
442
- yield* Effect.logDebug("getFileCoverage").pipe(Effect.annotateLogs({ runId }));
443
- return (yield* sql`SELECT f.path as file_path, fc.statements, fc.branches, fc.functions, fc.lines, fc.uncovered_lines
444
- FROM file_coverage fc
445
- JOIN files f ON f.id = fc.file_id
446
- WHERE fc.run_id = ${runId}`).map((r) => ({
447
- file: r.file_path,
448
- summary: {
449
- statements: r.statements,
450
- branches: r.branches,
451
- functions: r.functions,
452
- lines: r.lines
453
- },
454
- uncoveredLines: r.uncovered_lines ?? ""
455
- }));
456
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
457
- operation: "read",
458
- table: "file_coverage",
459
- reason: extractSqlReason(e)
460
- })));
461
- const getCoverage = (project) => Effect.gen(function* () {
462
- yield* Effect.logDebug("getCoverage").pipe(Effect.annotateLogs({ project }));
463
- const runs = yield* sql`SELECT id, timestamp FROM test_runs
464
- WHERE project = ${project}
465
- ORDER BY timestamp DESC LIMIT 1`;
466
- if (runs.length === 0) return Option.none();
467
- const runId = runs[0].id;
468
- const fileCoverageRows = yield* sql`SELECT f.path as file_path, fc.statements, fc.branches, fc.functions, fc.lines, fc.uncovered_lines, fc.tier
469
- FROM file_coverage fc JOIN files f ON f.id = fc.file_id
470
- WHERE fc.run_id = ${runId}`;
471
- const trendRows = yield* sql`SELECT statements, branches, functions, lines
472
- FROM coverage_trends
473
- WHERE project = ${project}
474
- ORDER BY timestamp DESC LIMIT 1`;
475
- if (fileCoverageRows.length === 0 && trendRows.length === 0) return Option.none();
476
- let totals;
477
- if (trendRows.length > 0) totals = {
478
- statements: trendRows[0].statements,
479
- branches: trendRows[0].branches,
480
- functions: trendRows[0].functions,
481
- lines: trendRows[0].lines
482
- };
483
- else {
484
- const count = fileCoverageRows.length;
485
- totals = {
486
- statements: fileCoverageRows.reduce((sum, r) => sum + r.statements, 0) / count,
487
- branches: fileCoverageRows.reduce((sum, r) => sum + r.branches, 0) / count,
488
- functions: fileCoverageRows.reduce((sum, r) => sum + r.functions, 0) / count,
489
- lines: fileCoverageRows.reduce((sum, r) => sum + r.lines, 0) / count
490
- };
491
- }
492
- const thresholdsOpt = yield* getCoveragePolicy("threshold", "__global__");
493
- const targetsOpt = yield* getCoveragePolicy("target", "__global__");
494
- const baselinesOpt = yield* getCoveragePolicy("baseline", "__global__");
495
- const thresholds = Option.getOrElse(thresholdsOpt, () => ({
496
- updatedAt: "",
497
- global: {},
498
- patterns: []
499
- }));
500
- const baselines = Option.getOrElse(baselinesOpt, () => ({
501
- updatedAt: "",
502
- global: {},
503
- patterns: []
504
- }));
505
- const toFileCoverageReport = (r) => ({
506
- file: r.file_path,
507
- summary: {
508
- statements: r.statements,
509
- branches: r.branches,
510
- functions: r.functions,
511
- lines: r.lines
512
- },
513
- uncoveredLines: r.uncovered_lines ?? ""
514
- });
515
- const lowCoverage = fileCoverageRows.filter((r) => r.tier === "below_threshold").map(toFileCoverageReport);
516
- const belowTarget = fileCoverageRows.filter((r) => r.tier === "below_target").map(toFileCoverageReport);
517
- const report = {
518
- totals,
519
- thresholds: {
520
- global: thresholds.global,
521
- patterns: thresholds.patterns
522
- },
523
- ...Option.isSome(targetsOpt) ? { targets: {
524
- global: targetsOpt.value.global,
525
- patterns: targetsOpt.value.patterns
526
- } } : {},
527
- baselines: {
528
- global: baselines.global,
529
- patterns: baselines.patterns
530
- },
531
- scoped: false,
532
- lowCoverage,
533
- lowCoverageFiles: lowCoverage.map((f) => f.file),
534
- ...belowTarget.length > 0 ? {
535
- belowTarget,
536
- belowTargetFiles: belowTarget.map((f) => f.file)
537
- } : {}
538
- };
539
- return Option.some(report);
540
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
541
- operation: "read",
542
- table: "file_coverage",
543
- reason: extractSqlReason(e)
544
- })));
545
- const getTestsForFile = (filePath) => Effect.gen(function* () {
546
- yield* Effect.logDebug("getTestsForFile").pipe(Effect.annotateLogs({ filePath }));
547
- return (yield* sql`SELECT DISTINCT f.path
548
- FROM source_test_map stm
549
- JOIN files sf ON sf.id = stm.source_file_id
550
- JOIN test_modules tm ON tm.id = stm.test_module_id
551
- JOIN files f ON f.id = tm.file_id
552
- WHERE sf.path = ${filePath}
553
- ORDER BY f.path`).map((r) => r.path);
554
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
555
- operation: "read",
556
- table: "source_test_map",
557
- reason: extractSqlReason(e)
558
- })));
559
- const getErrors = (project, errorName) => Effect.gen(function* () {
560
- yield* Effect.logDebug("getErrors").pipe(Effect.annotateLogs({
561
- project,
562
- errorName: errorName ?? null
563
- }));
564
- const runs = yield* sql`SELECT id FROM test_runs
565
- WHERE project = ${project}
566
- ORDER BY timestamp DESC LIMIT 1`;
567
- if (runs.length === 0) return [];
568
- const runId = runs[0].id;
569
- return (yield* errorName ? sql`SELECT te.id, sf.id AS top_stack_frame_id,
570
- te.name, te.message, te.diff, te.actual, te.expected, te.stack, te.scope,
571
- tc.full_name as test_full_name,
572
- f.path as module_file
573
- FROM test_errors te
574
- LEFT JOIN test_cases tc ON tc.id = te.test_case_id
575
- LEFT JOIN test_modules tm ON tm.id = COALESCE(te.module_id, tc.module_id)
576
- LEFT JOIN files f ON f.id = tm.file_id
577
- LEFT JOIN stack_frames sf ON sf.error_id = te.id AND sf.ordinal = 0
578
- WHERE te.run_id = ${runId} AND te.name = ${errorName}` : sql`SELECT te.id, sf.id AS top_stack_frame_id,
579
- te.name, te.message, te.diff, te.actual, te.expected, te.stack, te.scope,
580
- tc.full_name as test_full_name,
581
- f.path as module_file
582
- FROM test_errors te
583
- LEFT JOIN test_cases tc ON tc.id = te.test_case_id
584
- LEFT JOIN test_modules tm ON tm.id = COALESCE(te.module_id, tc.module_id)
585
- LEFT JOIN files f ON f.id = tm.file_id
586
- LEFT JOIN stack_frames sf ON sf.error_id = te.id AND sf.ordinal = 0
587
- WHERE te.run_id = ${runId}`).map((r) => ({
588
- id: r.id,
589
- topStackFrameId: r.top_stack_frame_id,
590
- name: r.name,
591
- message: r.message,
592
- diff: r.diff,
593
- actual: r.actual,
594
- expected: r.expected,
595
- stack: r.stack,
596
- scope: r.scope,
597
- testFullName: r.test_full_name,
598
- moduleFile: r.module_file
599
- }));
600
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
601
- operation: "read",
602
- table: "test_errors",
603
- reason: extractSqlReason(e)
604
- })));
605
- const latestRunId = (project) => Effect.gen(function* () {
606
- const runs = yield* sql`SELECT id FROM test_runs WHERE project = ${project} ORDER BY timestamp DESC LIMIT 1`;
607
- return runs.length === 0 ? null : runs[0].id;
608
- });
609
- const attachmentsFor = (column, ownerId) => Effect.gen(function* () {
610
- return (column === "annotation_id" ? yield* sql`SELECT content_type, path, body, body_encoding, byte_size FROM attachments WHERE annotation_id = ${ownerId} ORDER BY id` : yield* sql`SELECT content_type, path, body, body_encoding, byte_size FROM attachments WHERE artifact_id = ${ownerId} ORDER BY id`).map((r) => ({
611
- ...r.content_type !== null && { contentType: r.content_type },
612
- ...r.path !== null && { path: r.path },
613
- ...r.body !== null && { body: r.body },
614
- ...r.body_encoding !== null && { bodyEncoding: r.body_encoding },
615
- byteSize: r.byte_size
616
- }));
617
- });
618
- const getAnnotationsForTest = (project, fullName, options) => Effect.gen(function* () {
619
- yield* Effect.logDebug("getAnnotationsForTest").pipe(Effect.annotateLogs({
620
- project,
621
- fullName
622
- }));
623
- const runId = yield* latestRunId(project);
624
- if (runId === null) return [];
625
- const modulePath = options?.modulePath ?? null;
626
- const rows = yield* sql`SELECT ta.id, ta.type, ta.message, ta.location_line, ta.location_column, f.path AS location_path
627
- FROM test_annotations ta
628
- JOIN test_cases tc ON tc.id = ta.test_case_id
629
- JOIN test_modules tm ON tm.id = tc.module_id
630
- LEFT JOIN files f ON f.id = ta.location_file_id
631
- WHERE tm.run_id = ${runId} AND tc.full_name = ${fullName}
632
- AND (${modulePath} IS NULL OR tm.relative_module_id = ${modulePath})
633
- ORDER BY ta.id`;
634
- const out = [];
635
- for (const r of rows) {
636
- const attachments = yield* attachmentsFor("annotation_id", r.id);
637
- out.push({
638
- id: r.id,
639
- type: r.type,
640
- message: r.message,
641
- ...r.location_path !== null && r.location_line !== null && r.location_column !== null && { location: {
642
- file: r.location_path,
643
- line: r.location_line,
644
- column: r.location_column
645
- } },
646
- attachments
647
- });
648
- }
649
- return out;
650
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
651
- operation: "read",
652
- table: "test_annotations",
653
- reason: extractSqlReason(e)
654
- })));
655
- const getArtifactsForTest = (project, fullName, options) => Effect.gen(function* () {
656
- yield* Effect.logDebug("getArtifactsForTest").pipe(Effect.annotateLogs({
657
- project,
658
- fullName
659
- }));
660
- const runId = yield* latestRunId(project);
661
- if (runId === null) return [];
662
- const modulePath = options?.modulePath ?? null;
663
- const rows = yield* sql`SELECT ta.id, ta.type, ta.message, ta.data, ta.location_line, ta.location_column, f.path AS location_path
664
- FROM test_artifacts ta
665
- JOIN test_cases tc ON tc.id = ta.test_case_id
666
- JOIN test_modules tm ON tm.id = tc.module_id
667
- LEFT JOIN files f ON f.id = ta.location_file_id
668
- WHERE tm.run_id = ${runId} AND tc.full_name = ${fullName}
669
- AND (${modulePath} IS NULL OR tm.relative_module_id = ${modulePath})
670
- ORDER BY ta.id`;
671
- const out = [];
672
- for (const r of rows) {
673
- const attachments = yield* attachmentsFor("artifact_id", r.id);
674
- out.push({
675
- id: r.id,
676
- type: r.type,
677
- message: r.message,
678
- data: r.data,
679
- ...r.location_path !== null && r.location_line !== null && r.location_column !== null && { location: {
680
- file: r.location_path,
681
- line: r.location_line,
682
- column: r.location_column
683
- } },
684
- attachments
685
- });
686
- }
687
- return out;
688
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
689
- operation: "read",
690
- table: "test_artifacts",
691
- reason: extractSqlReason(e)
692
- })));
693
- const getNotes = (scope, project, testFullName) => Effect.gen(function* () {
694
- yield* Effect.logDebug("getNotes").pipe(Effect.annotateLogs({
695
- scope: scope ?? null,
696
- project: project ?? null
697
- }));
698
- if (scope && project && testFullName) return (yield* sql`SELECT * FROM notes
699
- WHERE scope = ${scope} AND project = ${project} AND test_full_name = ${testFullName}
700
- ORDER BY created_at DESC`).map(mapNoteRow);
701
- if (scope && project) return (yield* sql`SELECT * FROM notes
702
- WHERE scope = ${scope} AND project = ${project}
703
- ORDER BY created_at DESC`).map(mapNoteRow);
704
- if (scope) return (yield* sql`SELECT * FROM notes
705
- WHERE scope = ${scope}
706
- ORDER BY created_at DESC`).map(mapNoteRow);
707
- if (project) return (yield* sql`SELECT * FROM notes
708
- WHERE project = ${project}
709
- ORDER BY created_at DESC`).map(mapNoteRow);
710
- return (yield* sql`SELECT * FROM notes ORDER BY created_at DESC`).map(mapNoteRow);
711
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
712
- operation: "read",
713
- table: "notes",
714
- reason: extractSqlReason(e)
715
- })));
716
- const getNoteById = (id) => Effect.gen(function* () {
717
- yield* Effect.logDebug("getNoteById").pipe(Effect.annotateLogs({ id }));
718
- const rows = yield* sql`SELECT * FROM notes WHERE id = ${id}`;
719
- if (rows.length === 0) return Option.none();
720
- return Option.some(mapNoteRow(rows[0]));
721
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
722
- operation: "read",
723
- table: "notes",
724
- reason: extractSqlReason(e)
725
- })));
726
- const searchNotes = (query) => Effect.gen(function* () {
727
- yield* Effect.logDebug("searchNotes").pipe(Effect.annotateLogs({ query }));
728
- return (yield* sql`SELECT n.*
729
- FROM notes n
730
- JOIN notes_fts ON n.id = notes_fts.rowid
731
- WHERE notes_fts MATCH ${query}
732
- ORDER BY rank`).map(mapNoteRow);
733
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
734
- operation: "read",
735
- table: "notes",
736
- reason: extractSqlReason(e)
737
- })));
738
- const getManifest = () => Effect.gen(function* () {
739
- yield* Effect.logDebug("getManifest");
740
- const rows = yield* sql`SELECT
741
- t1.project,
742
- t1.timestamp as last_run,
743
- t1.reason as last_result
744
- FROM test_runs t1
745
- INNER JOIN (
746
- SELECT project, MAX(timestamp) as max_ts
747
- FROM test_runs
748
- GROUP BY project
749
- ) t2 ON t1.project = t2.project
750
- AND t1.timestamp = t2.max_ts`;
751
- if (rows.length === 0) return Option.none();
752
- const dbPath = (yield* sql`PRAGMA database_list`.pipe(Effect.catch(() => Effect.succeed([])))).find((d) => d.name === "main")?.file ?? "";
753
- const projects = rows.map((r) => ({
754
- project: r.project,
755
- reportFile: dbPath,
756
- historyFile: dbPath,
757
- lastRun: r.last_run,
758
- lastResult: r.last_result
759
- }));
760
- const manifest = {
761
- updatedAt: rows.reduce((latest, r) => {
762
- if (!r.last_run) return latest;
763
- if (!latest) return r.last_run;
764
- return r.last_run > latest ? r.last_run : latest;
765
- }, null) ?? (/* @__PURE__ */ new Date()).toISOString(),
766
- cacheDir: dbPath,
767
- projects
768
- };
769
- return Option.some(manifest);
770
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
771
- operation: "read",
772
- table: "test_runs",
773
- reason: extractSqlReason(e)
774
- })));
775
- const getSettings = (hash) => Effect.gen(function* () {
776
- yield* Effect.logDebug("getSettings").pipe(Effect.annotateLogs({ hash }));
777
- const rows = yield* sql`SELECT hash, vitest_version, pool, environment, coverage_provider, created_at
778
- FROM settings WHERE hash = ${hash}`;
779
- if (rows.length === 0) return Option.none();
780
- const row = rows[0];
781
- const envRows = yield* sql`SELECT key, value FROM settings_env_vars WHERE settings_hash = ${hash}`;
782
- const envVars = {};
783
- for (const ev of envRows) envVars[ev.key] = ev.value;
784
- const settings = {
785
- hash: row.hash,
786
- reporters: null,
787
- coverageEnabled: row.coverage_provider != null,
788
- coverageProvider: row.coverage_provider,
789
- coverageThresholds: null,
790
- coverageTargets: null,
791
- pool: row.pool,
792
- shard: null,
793
- project: null,
794
- environment: row.environment,
795
- envVars,
796
- capturedAt: row.created_at
797
- };
798
- return Option.some(settings);
799
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
800
- operation: "read",
801
- table: "settings",
802
- reason: extractSqlReason(e)
803
- })));
804
- const getLatestSettings = () => Effect.gen(function* () {
805
- yield* Effect.logDebug("getLatestSettings");
806
- const rows = yield* sql`SELECT hash, pool, environment, coverage_provider, created_at
807
- FROM settings ORDER BY rowid DESC LIMIT 1`;
808
- if (rows.length === 0) return Option.none();
809
- const row = rows[0];
810
- const envRows = yield* sql`SELECT key, value FROM settings_env_vars WHERE settings_hash = ${row.hash}`;
811
- const envVars = {};
812
- for (const ev of envRows) envVars[ev.key] = ev.value;
813
- const settings = {
814
- hash: row.hash,
815
- reporters: null,
816
- coverageEnabled: row.coverage_provider != null,
817
- coverageProvider: row.coverage_provider,
818
- coverageThresholds: null,
819
- coverageTargets: null,
820
- pool: row.pool,
821
- shard: null,
822
- project: null,
823
- environment: row.environment,
824
- envVars,
825
- capturedAt: row.created_at
826
- };
827
- return Option.some(settings);
828
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
829
- operation: "read",
830
- table: "settings",
831
- reason: extractSqlReason(e)
832
- })));
833
- const getTestByFullName = (project, fullName, options) => Effect.gen(function* () {
834
- yield* Effect.logDebug("getTestByFullName").pipe(Effect.annotateLogs({
835
- project,
836
- fullName,
837
- ...options
838
- }));
839
- const runs = yield* sql`SELECT id FROM test_runs
840
- WHERE project = ${project}
841
- ORDER BY timestamp DESC LIMIT 1`;
842
- if (runs.length === 0) return Option.none();
843
- const runId = runs[0].id;
844
- const modulePath = options?.modulePath ?? null;
845
- const rows = yield* sql`SELECT tc.id, tc.full_name, tc.state, tc.duration, f.path as relative_module_id, tc.classification
846
- FROM test_cases tc
847
- JOIN test_modules tm ON tm.id = tc.module_id
848
- JOIN files f ON f.id = tm.file_id
849
- WHERE tm.run_id = ${runId} AND tc.full_name = ${fullName}
850
- AND (${modulePath} IS NULL OR f.path = ${modulePath})
851
- ORDER BY f.path ASC, tc.id ASC
852
- LIMIT 1`;
853
- if (rows.length === 0) return Option.none();
854
- const r = rows[0];
855
- return Option.some({
856
- id: r.id,
857
- fullName: r.full_name,
858
- state: r.state,
859
- duration: r.duration,
860
- module: r.relative_module_id,
861
- classification: r.classification
862
- });
863
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
864
- operation: "read",
865
- table: "test_cases",
866
- reason: extractSqlReason(e)
867
- })));
868
- const getTestModulesByFullName = (project, fullName) => Effect.gen(function* () {
869
- yield* Effect.logDebug("getTestModulesByFullName").pipe(Effect.annotateLogs({
870
- project,
871
- fullName
872
- }));
873
- const runs = yield* sql`SELECT id FROM test_runs
874
- WHERE project = ${project}
875
- ORDER BY timestamp DESC LIMIT 1`;
876
- if (runs.length === 0) return [];
877
- const runId = runs[0].id;
878
- return (yield* sql`SELECT DISTINCT f.path as relative_module_id
879
- FROM test_cases tc
880
- JOIN test_modules tm ON tm.id = tc.module_id
881
- JOIN files f ON f.id = tm.file_id
882
- WHERE tm.run_id = ${runId} AND tc.full_name = ${fullName}
883
- ORDER BY f.path ASC`).map((r) => r.relative_module_id);
884
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
885
- operation: "read",
886
- table: "test_cases",
887
- reason: extractSqlReason(e)
888
- })));
889
- const listTests = (project, options) => Effect.gen(function* () {
890
- yield* Effect.logDebug("listTests").pipe(Effect.annotateLogs({ project }));
891
- const runs = yield* sql`SELECT id FROM test_runs
892
- WHERE project = ${project}
893
- ORDER BY timestamp DESC LIMIT 1`;
894
- if (runs.length === 0) return [];
895
- const runId = runs[0].id;
896
- const effectiveLimit = Math.min(options?.limit ?? 100, 500);
897
- const state = options?.state;
898
- const mod = options?.module;
899
- if (state && mod) return (yield* sql`SELECT tc.id, tc.full_name as "full_name", tc.state, tc.duration, f.path as file_path, tc.classification
900
- FROM test_cases tc
901
- JOIN test_modules tm ON tm.id = tc.module_id
902
- JOIN files f ON f.id = tm.file_id
903
- WHERE tm.run_id = ${runId} AND tc.state = ${state} AND f.path = ${mod}
904
- ORDER BY tc.full_name
905
- LIMIT ${effectiveLimit}`).map(mapTestListRow);
906
- if (state) return (yield* sql`SELECT tc.id, tc.full_name as "full_name", tc.state, tc.duration, f.path as file_path, tc.classification
907
- FROM test_cases tc
908
- JOIN test_modules tm ON tm.id = tc.module_id
909
- JOIN files f ON f.id = tm.file_id
910
- WHERE tm.run_id = ${runId} AND tc.state = ${state}
911
- ORDER BY tc.full_name
912
- LIMIT ${effectiveLimit}`).map(mapTestListRow);
913
- if (mod) return (yield* sql`SELECT tc.id, tc.full_name as "full_name", tc.state, tc.duration, f.path as file_path, tc.classification
914
- FROM test_cases tc
915
- JOIN test_modules tm ON tm.id = tc.module_id
916
- JOIN files f ON f.id = tm.file_id
917
- WHERE tm.run_id = ${runId} AND f.path = ${mod}
918
- ORDER BY tc.full_name
919
- LIMIT ${effectiveLimit}`).map(mapTestListRow);
920
- return (yield* sql`SELECT tc.id, tc.full_name as "full_name", tc.state, tc.duration, f.path as file_path, tc.classification
921
- FROM test_cases tc
922
- JOIN test_modules tm ON tm.id = tc.module_id
923
- JOIN files f ON f.id = tm.file_id
924
- WHERE tm.run_id = ${runId}
925
- ORDER BY tc.full_name
926
- LIMIT ${effectiveLimit}`).map(mapTestListRow);
927
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
928
- operation: "read",
929
- table: "test_cases",
930
- reason: extractSqlReason(e)
931
- })));
932
- const listModules = (project) => Effect.gen(function* () {
933
- yield* Effect.logDebug("listModules").pipe(Effect.annotateLogs({ project }));
934
- const runs = yield* sql`SELECT id FROM test_runs
935
- WHERE project = ${project}
936
- ORDER BY timestamp DESC LIMIT 1`;
937
- if (runs.length === 0) return [];
938
- const runId = runs[0].id;
939
- return (yield* sql`SELECT tm.id, f.path as file_path, tm.state,
940
- (SELECT COUNT(*) FROM test_cases tc WHERE tc.module_id = tm.id) as test_count,
941
- tm.duration
942
- FROM test_modules tm
943
- JOIN files f ON f.id = tm.file_id
944
- WHERE tm.run_id = ${runId}
945
- ORDER BY f.path`).map((r) => ({
946
- id: r.id,
947
- file: r.file_path,
948
- state: r.state,
949
- testCount: r.test_count,
950
- duration: r.duration
951
- }));
952
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
953
- operation: "read",
954
- table: "test_modules",
955
- reason: extractSqlReason(e)
956
- })));
957
- const listSuites = (project, options) => Effect.gen(function* () {
958
- yield* Effect.logDebug("listSuites").pipe(Effect.annotateLogs({ project }));
959
- const runs = yield* sql`SELECT id FROM test_runs
960
- WHERE project = ${project}
961
- ORDER BY timestamp DESC LIMIT 1`;
962
- if (runs.length === 0) return [];
963
- const runId = runs[0].id;
964
- const mod = options?.module;
965
- if (mod) return (yield* sql`SELECT ts.id, ts.name, f.path as file_path, ts.state,
966
- (SELECT COUNT(*) FROM test_cases tc WHERE tc.suite_id = ts.id) as test_count
967
- FROM test_suites ts
968
- JOIN test_modules tm ON tm.id = ts.module_id
969
- JOIN files f ON f.id = tm.file_id
970
- WHERE tm.run_id = ${runId} AND f.path = ${mod}
971
- ORDER BY ts.name`).map((r) => ({
972
- id: r.id,
973
- name: r.name,
974
- module: r.file_path,
975
- state: r.state,
976
- testCount: r.test_count
977
- }));
978
- return (yield* sql`SELECT ts.id, ts.name, f.path as file_path, ts.state,
979
- (SELECT COUNT(*) FROM test_cases tc WHERE tc.suite_id = ts.id) as test_count
980
- FROM test_suites ts
981
- JOIN test_modules tm ON tm.id = ts.module_id
982
- JOIN files f ON f.id = tm.file_id
983
- WHERE tm.run_id = ${runId}
984
- ORDER BY ts.name`).map((r) => ({
985
- id: r.id,
986
- name: r.name,
987
- module: r.file_path,
988
- state: r.state,
989
- testCount: r.test_count
990
- }));
991
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
992
- operation: "read",
993
- table: "test_suites",
994
- reason: extractSqlReason(e)
995
- })));
996
- const listSettings = () => Effect.gen(function* () {
997
- yield* Effect.logDebug("listSettings");
998
- return (yield* sql`SELECT hash, created_at FROM settings ORDER BY rowid DESC`).map((r) => ({
999
- hash: r.hash,
1000
- capturedAt: r.created_at
1001
- }));
1002
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1003
- operation: "read",
1004
- table: "settings",
1005
- reason: extractSqlReason(e)
1006
- })));
1007
- const sessionRowToDetail = (r) => ({
1008
- id: r.id,
1009
- chatId: r.chat_id,
1010
- project: r.project,
1011
- cwd: r.cwd,
1012
- agentKind: r.agent_kind,
1013
- agentType: r.agent_type,
1014
- parentSessionId: r.parent_session_id,
1015
- triageWasNonEmpty: r.triage_was_non_empty === 1,
1016
- startedAt: r.started_at,
1017
- endedAt: r.ended_at,
1018
- endReason: r.end_reason,
1019
- conversationId: r.conversation_id
1020
- });
1021
- const getSessionById = (id) => Effect.gen(function* () {
1022
- yield* Effect.logDebug("getSessionById").pipe(Effect.annotateLogs({ id }));
1023
- const rows = yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason, conversation_id FROM sessions WHERE id = ${id} LIMIT 1`;
1024
- if (rows.length === 0) return Option.none();
1025
- return Option.some(sessionRowToDetail(rows[0]));
1026
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1027
- operation: "read",
1028
- table: "sessions",
1029
- reason: extractSqlReason(e)
1030
- })));
1031
- const getSessionByChatId = (chatId) => Effect.gen(function* () {
1032
- yield* Effect.logDebug("getSessionByChatId").pipe(Effect.annotateLogs({ chatId }));
1033
- const rows = yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason, conversation_id FROM sessions WHERE chat_id = ${chatId} LIMIT 1`;
1034
- if (rows.length === 0) return Option.none();
1035
- return Option.some(sessionRowToDetail(rows[0]));
1036
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1037
- operation: "read",
1038
- table: "sessions",
1039
- reason: extractSqlReason(e)
1040
- })));
1041
- const findSessionsByChatPrefix = (prefix) => Effect.gen(function* () {
1042
- yield* Effect.logDebug("findSessionsByChatPrefix").pipe(Effect.annotateLogs({ prefix }));
1043
- const pattern = `${prefix}%`;
1044
- return (yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason, conversation_id FROM sessions WHERE chat_id LIKE ${pattern} ESCAPE '\\' ORDER BY started_at DESC`).map(sessionRowToDetail);
1045
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1046
- operation: "read",
1047
- table: "sessions",
1048
- reason: extractSqlReason(e)
1049
- })));
1050
- const findActiveSubagentSession = (parentSessionId) => Effect.gen(function* () {
1051
- yield* Effect.logDebug("findActiveSubagentSession").pipe(Effect.annotateLogs({ parentSessionId }));
1052
- const rows = yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason, conversation_id FROM sessions WHERE parent_session_id = ${parentSessionId} AND agent_kind = 'subagent' AND ended_at IS NULL ORDER BY started_at DESC LIMIT 1`;
1053
- if (rows.length === 0) return Option.none();
1054
- return Option.some(sessionRowToDetail(rows[0]));
1055
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1056
- operation: "read",
1057
- table: "sessions",
1058
- reason: extractSqlReason(e)
1059
- })));
1060
- const getSessionByTddTaskId = (tddTaskId) => Effect.gen(function* () {
1061
- yield* Effect.logDebug("getSessionByTddTaskId").pipe(Effect.annotateLogs({ tddTaskId }));
1062
- const rows = yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason, conversation_id FROM sessions WHERE id = (SELECT session_id FROM tdd_tasks WHERE id = ${tddTaskId}) LIMIT 1`;
1063
- if (rows.length === 0) return Option.none();
1064
- return Option.some(sessionRowToDetail(rows[0]));
1065
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1066
- operation: "read",
1067
- table: "sessions",
1068
- reason: extractSqlReason(e)
1069
- })));
1070
- const searchTurns = (options) => Effect.gen(function* () {
1071
- yield* Effect.logDebug("searchTurns").pipe(Effect.annotateLogs({ ...options }));
1072
- const limit = options.limit ?? 100;
1073
- const sessionFilter = options.sessionId !== void 0 ? sql` AND session_id = ${options.sessionId}` : sql``;
1074
- const typeFilter = options.type !== void 0 ? sql` AND type = ${options.type}` : sql``;
1075
- const sinceFilter = options.since !== void 0 ? sql` AND occurred_at >= ${options.since}` : sql``;
1076
- return (yield* sql`SELECT id, session_id, turn_no, type, payload, occurred_at FROM turns WHERE 1=1${sessionFilter}${typeFilter}${sinceFilter} ORDER BY occurred_at DESC, turn_no DESC LIMIT ${limit}`).map((r) => ({
1077
- id: r.id,
1078
- sessionId: r.session_id,
1079
- turnNo: r.turn_no,
1080
- type: r.type,
1081
- payload: r.payload,
1082
- occurredAt: r.occurred_at
1083
- }));
1084
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1085
- operation: "read",
1086
- table: "turns",
1087
- reason: extractSqlReason(e)
1088
- })));
1089
- const computeAcceptanceMetrics = () => Effect.gen(function* () {
1090
- yield* Effect.logDebug("computeAcceptanceMetrics");
1091
- const m1 = yield* sql`
1092
- WITH task_orderings AS (
1093
- SELECT
1094
- p.tdd_task_id,
1095
- MIN(CASE WHEN a.artifact_kind = 'test_failed_run' THEN a.id END) AS first_failed_run,
1096
- MIN(CASE WHEN a.artifact_kind = 'code_written' THEN a.id END) AS first_code_written
1097
- FROM tdd_artifacts a
1098
- JOIN tdd_phases p ON a.phase_id = p.id
1099
- WHERE a.artifact_kind IN ('test_failed_run', 'code_written')
1100
- GROUP BY p.tdd_task_id
1101
- )
1102
- SELECT
1103
- COUNT(*) AS total,
1104
- COALESCE(SUM(CASE WHEN first_failed_run < first_code_written THEN 1 ELSE 0 END), 0) AS compliant
1105
- FROM task_orderings
1106
- WHERE first_code_written IS NOT NULL
1107
- `;
1108
- const m2 = yield* sql`
1109
- WITH wrap_up_fires AS (
1110
- SELECT t.session_id
1111
- FROM turns t
1112
- WHERE t.type = 'hook_fire'
1113
- AND json_extract(t.payload, '$.hook_kind') IN ('SessionEnd', 'PreCompact', 'Stop')
1114
- ),
1115
- followups AS (
1116
- SELECT DISTINCT t.session_id
1117
- FROM turns t
1118
- JOIN tool_invocations ti ON ti.turn_id = t.id
1119
- WHERE ti.tool_name IN ('note', 'hypothesis', 'tdd_task', 'note_create', 'hypothesis_validate', 'tdd_session_end')
1120
- )
1121
- SELECT
1122
- (SELECT COUNT(DISTINCT session_id) FROM wrap_up_fires) AS total,
1123
- (SELECT COUNT(DISTINCT wf.session_id)
1124
- FROM wrap_up_fires wf JOIN followups f ON f.session_id = wf.session_id) AS with_followup
1125
- `;
1126
- const m3 = yield* sql`
1127
- WITH triaged_sessions AS (
1128
- SELECT id AS session_id FROM sessions WHERE triage_was_non_empty = 1
1129
- ),
1130
- ranked_tool_results AS (
1131
- SELECT session_id, id AS turn_id,
1132
- ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY turn_no) AS result_no
1133
- FROM turns
1134
- WHERE type = 'tool_result'
1135
- ),
1136
- first_three_tool_calls AS (
1137
- SELECT r.session_id, ti.tool_name
1138
- FROM ranked_tool_results r
1139
- JOIN tool_invocations ti ON ti.turn_id = r.turn_id
1140
- WHERE r.result_no <= 3
1141
- ),
1142
- referenced AS (
1143
- SELECT DISTINCT ts.session_id
1144
- FROM triaged_sessions ts
1145
- JOIN first_three_tool_calls ftc ON ftc.session_id = ts.session_id
1146
- WHERE ftc.tool_name IN ('tdd_session_resume', 'run_tests', 'test_history',
1147
- 'failure_signature_get', 'tdd_session_start')
1148
- )
1149
- SELECT
1150
- (SELECT COUNT(*) FROM triaged_sessions) AS total,
1151
- (SELECT COUNT(*) FROM referenced) AS referenced_count
1152
- `;
1153
- const m4 = yield* sql`
1154
- SELECT
1155
- COUNT(*) AS total,
1156
- COALESCE(SUM(CASE WHEN weakened_count = 0 THEN 1 ELSE 0 END), 0) AS clean_sessions
1157
- FROM (
1158
- SELECT ts.id AS tdd_task_id,
1159
- COUNT(a.id) AS weakened_count
1160
- FROM tdd_tasks ts
1161
- LEFT JOIN tdd_phases p ON p.tdd_task_id = ts.id
1162
- LEFT JOIN tdd_artifacts a ON a.phase_id = p.id AND a.artifact_kind = 'test_weakened'
1163
- WHERE ts.ended_at IS NOT NULL
1164
- GROUP BY ts.id
1165
- ) tasks_with_counts
1166
- `;
1167
- const ratio = (n, d) => d === 0 ? 0 : n / d;
1168
- return {
1169
- phaseEvidenceIntegrity: {
1170
- total: m1[0].total,
1171
- compliant: m1[0].compliant,
1172
- ratio: ratio(m1[0].compliant, m1[0].total)
1173
- },
1174
- complianceHookResponsiveness: {
1175
- total: m2[0].total,
1176
- withFollowup: m2[0].with_followup,
1177
- ratio: ratio(m2[0].with_followup, m2[0].total)
1178
- },
1179
- orientationUsefulness: {
1180
- total: m3[0].total,
1181
- referencedCount: m3[0].referenced_count,
1182
- ratio: ratio(m3[0].referenced_count, m3[0].total)
1183
- },
1184
- antiPatternDetectionRate: {
1185
- total: m4[0].total,
1186
- cleanSessions: m4[0].clean_sessions,
1187
- ratio: ratio(m4[0].clean_sessions, m4[0].total)
1188
- }
1189
- };
1190
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1191
- operation: "read",
1192
- table: "tdd_tasks",
1193
- reason: extractSqlReason(e)
1194
- })));
1195
- const listSessions = (options) => Effect.gen(function* () {
1196
- yield* Effect.logDebug("listSessions").pipe(Effect.annotateLogs({ ...options }));
1197
- const limit = options.limit ?? 50;
1198
- const project = options.project ?? null;
1199
- const agentKind = options.agentKind ?? null;
1200
- return (yield* sql`
1201
- SELECT id, chat_id, project, cwd, agent_kind, agent_type,
1202
- parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason, conversation_id
1203
- FROM sessions
1204
- WHERE (${project} IS NULL OR project = ${project})
1205
- AND (${agentKind} IS NULL OR agent_kind = ${agentKind})
1206
- ORDER BY started_at DESC
1207
- LIMIT ${limit}
1208
- `).map(sessionRowToDetail);
1209
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1210
- operation: "read",
1211
- table: "sessions",
1212
- reason: extractSqlReason(e)
1213
- })));
1214
- const getFailureSignatureByHash = (hash) => Effect.gen(function* () {
1215
- yield* Effect.logDebug("getFailureSignatureByHash").pipe(Effect.annotateLogs({ hash }));
1216
- const sigRows = yield* sql`
1217
- SELECT signature_hash, first_seen_run_id, first_seen_at, last_seen_at, occurrence_count
1218
- FROM failure_signatures WHERE signature_hash = ${hash} LIMIT 1
1219
- `;
1220
- if (sigRows.length === 0) return Option.none();
1221
- const errRows = yield* sql`
1222
- SELECT run_id, message, name FROM test_errors
1223
- WHERE signature_hash = ${hash}
1224
- ORDER BY run_id DESC
1225
- LIMIT 10
1226
- `;
1227
- const sig = sigRows[0];
1228
- return Option.some({
1229
- signatureHash: sig.signature_hash,
1230
- firstSeenRunId: sig.first_seen_run_id,
1231
- firstSeenAt: sig.first_seen_at,
1232
- lastSeenAt: sig.last_seen_at,
1233
- occurrenceCount: sig.occurrence_count,
1234
- recentErrors: errRows.map((e) => ({
1235
- runId: e.run_id,
1236
- message: e.message,
1237
- errorName: e.name
1238
- }))
1239
- });
1240
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1241
- operation: "read",
1242
- table: "failure_signatures",
1243
- reason: extractSqlReason(e)
1244
- })));
1245
- const goalRowFromDb = (row) => ({
1246
- id: row.id,
1247
- sessionId: row.session_id,
1248
- ordinal: row.ordinal,
1249
- goal: row.goal,
1250
- status: row.status,
1251
- createdAt: row.created_at
1252
- });
1253
- const behaviorRowFromDb = (row) => ({
1254
- id: row.id,
1255
- goalId: row.goal_id,
1256
- ordinal: row.ordinal,
1257
- behavior: row.behavior,
1258
- suggestedTestName: row.suggested_test_name,
1259
- status: row.status,
1260
- createdAt: row.created_at
1261
- });
1262
- const fetchGoalsWithBehaviors = (sessionId) => Effect.gen(function* () {
1263
- const goals = yield* sql`
1264
- SELECT id, session_id, ordinal, goal, status, created_at
1265
- FROM tdd_session_goals
1266
- WHERE session_id = ${sessionId}
1267
- ORDER BY ordinal
1268
- `;
1269
- if (goals.length === 0) return [];
1270
- const goalIds = goals.map((g) => g.id);
1271
- const behaviors = yield* sql`
1272
- SELECT id, goal_id, ordinal, behavior, suggested_test_name, status, created_at
1273
- FROM tdd_session_behaviors
1274
- WHERE goal_id IN ${sql.in(goalIds)}
1275
- ORDER BY goal_id, ordinal
1276
- `;
1277
- const byGoal = /* @__PURE__ */ new Map();
1278
- for (const id of goalIds) byGoal.set(id, []);
1279
- for (const b of behaviors) {
1280
- const list = byGoal.get(b.goal_id);
1281
- if (list) list.push(behaviorRowFromDb(b));
1282
- }
1283
- return goals.map((g) => ({
1284
- ...goalRowFromDb(g),
1285
- behaviors: byGoal.get(g.id) ?? []
1286
- }));
1287
- });
1288
- const getGoalById = (id) => Effect.gen(function* () {
1289
- const goalRows = yield* sql`
1290
- SELECT id, session_id, ordinal, goal, status, created_at
1291
- FROM tdd_session_goals
1292
- WHERE id = ${id}
1293
- `;
1294
- if (goalRows.length === 0) return Option.none();
1295
- const beh = yield* sql`
1296
- SELECT id, goal_id, ordinal, behavior, suggested_test_name, status, created_at
1297
- FROM tdd_session_behaviors
1298
- WHERE goal_id = ${id}
1299
- ORDER BY ordinal
1300
- `;
1301
- return Option.some({
1302
- ...goalRowFromDb(goalRows[0]),
1303
- behaviors: beh.map(behaviorRowFromDb)
1304
- });
1305
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1306
- operation: "read",
1307
- table: "tdd_session_goals",
1308
- reason: extractSqlReason(e)
1309
- })));
1310
- const getGoalsByTddTask = (tddTaskId) => fetchGoalsWithBehaviors(tddTaskId).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1311
- operation: "read",
1312
- table: "tdd_session_goals",
1313
- reason: extractSqlReason(e)
1314
- })));
1315
- const getBehaviorById = (id) => Effect.gen(function* () {
1316
- const rows = yield* sql`
1317
- SELECT b.id AS b_id, b.goal_id AS b_goal_id, b.ordinal AS b_ordinal, b.behavior AS b_behavior,
1318
- b.suggested_test_name AS b_suggested_test_name, b.status AS b_status,
1319
- b.created_at AS b_created_at,
1320
- g.id AS g_id, g.goal AS g_goal, g.status AS g_status
1321
- FROM tdd_session_behaviors b
1322
- JOIN tdd_session_goals g ON g.id = b.goal_id
1323
- WHERE b.id = ${id}
1324
- `;
1325
- if (rows.length === 0) return Option.none();
1326
- const r = rows[0];
1327
- const deps = yield* sql`
1328
- SELECT b.id, b.goal_id, b.ordinal, b.behavior, b.suggested_test_name, b.status, b.created_at
1329
- FROM tdd_behavior_dependencies d
1330
- JOIN tdd_session_behaviors b ON b.id = d.depends_on_id
1331
- WHERE d.behavior_id = ${id}
1332
- ORDER BY b.ordinal
1333
- `;
1334
- return Option.some({
1335
- id: r.b_id,
1336
- goalId: r.b_goal_id,
1337
- ordinal: r.b_ordinal,
1338
- behavior: r.b_behavior,
1339
- suggestedTestName: r.b_suggested_test_name,
1340
- status: r.b_status,
1341
- createdAt: r.b_created_at,
1342
- parentGoal: {
1343
- id: r.g_id,
1344
- goal: r.g_goal,
1345
- status: r.g_status
1346
- },
1347
- dependencies: deps.map(behaviorRowFromDb)
1348
- });
1349
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1350
- operation: "read",
1351
- table: "tdd_session_behaviors",
1352
- reason: extractSqlReason(e)
1353
- })));
1354
- const getBehaviorsByGoal = (goalId) => Effect.gen(function* () {
1355
- return (yield* sql`
1356
- SELECT id, goal_id, ordinal, behavior, suggested_test_name, status, created_at
1357
- FROM tdd_session_behaviors
1358
- WHERE goal_id = ${goalId}
1359
- ORDER BY ordinal
1360
- `).map(behaviorRowFromDb);
1361
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1362
- operation: "read",
1363
- table: "tdd_session_behaviors",
1364
- reason: extractSqlReason(e)
1365
- })));
1366
- const getBehaviorsByTddTask = (tddTaskId) => Effect.gen(function* () {
1367
- return (yield* sql`
1368
- SELECT b.id, b.goal_id, b.ordinal, b.behavior, b.suggested_test_name, b.status, b.created_at
1369
- FROM tdd_session_behaviors b
1370
- JOIN tdd_session_goals g ON g.id = b.goal_id
1371
- WHERE g.session_id = ${tddTaskId}
1372
- ORDER BY g.ordinal, b.ordinal
1373
- `).map(behaviorRowFromDb);
1374
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1375
- operation: "read",
1376
- table: "tdd_session_behaviors",
1377
- reason: extractSqlReason(e)
1378
- })));
1379
- const getBehaviorDependencies = (behaviorId) => Effect.gen(function* () {
1380
- return (yield* sql`
1381
- SELECT b.id, b.goal_id, b.ordinal, b.behavior, b.suggested_test_name, b.status, b.created_at
1382
- FROM tdd_behavior_dependencies d
1383
- JOIN tdd_session_behaviors b ON b.id = d.depends_on_id
1384
- WHERE d.behavior_id = ${behaviorId}
1385
- ORDER BY b.ordinal
1386
- `).map(behaviorRowFromDb);
1387
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1388
- operation: "read",
1389
- table: "tdd_behavior_dependencies",
1390
- reason: extractSqlReason(e)
1391
- })));
1392
- const resolveGoalIdForBehavior = (behaviorId) => Effect.gen(function* () {
1393
- const rows = yield* sql`
1394
- SELECT goal_id FROM tdd_session_behaviors WHERE id = ${behaviorId}
1395
- `;
1396
- return rows.length === 0 ? Option.none() : Option.some(rows[0].goal_id);
1397
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1398
- operation: "read",
1399
- table: "tdd_session_behaviors",
1400
- reason: extractSqlReason(e)
1401
- })));
1402
- const getTddTaskById = (id) => Effect.gen(function* () {
1403
- yield* Effect.logDebug("getTddTaskById").pipe(Effect.annotateLogs({ id }));
1404
- const taskRows = yield* sql`
1405
- SELECT id, session_id, goal, started_at, ended_at, outcome, run_id
1406
- FROM tdd_tasks WHERE id = ${id} LIMIT 1
1407
- `;
1408
- if (taskRows.length === 0) return Option.none();
1409
- const tddTask = taskRows[0];
1410
- const goals = yield* fetchGoalsWithBehaviors(id);
1411
- const phaseRows = yield* sql`
1412
- SELECT id, behavior_id, phase, started_at, ended_at, transition_reason
1413
- FROM tdd_phases WHERE tdd_task_id = ${id}
1414
- ORDER BY started_at ASC
1415
- `;
1416
- const artifactRows = yield* sql`
1417
- SELECT a.id, a.phase_id, a.artifact_kind, a.test_case_id, a.test_run_id, a.recorded_at
1418
- FROM tdd_artifacts a
1419
- JOIN tdd_phases p ON a.phase_id = p.id
1420
- WHERE p.tdd_task_id = ${id}
1421
- ORDER BY a.recorded_at ASC
1422
- `;
1423
- return Option.some({
1424
- id: tddTask.id,
1425
- sessionId: tddTask.session_id,
1426
- goal: tddTask.goal,
1427
- startedAt: tddTask.started_at,
1428
- endedAt: tddTask.ended_at,
1429
- outcome: tddTask.outcome,
1430
- runId: tddTask.run_id,
1431
- goals,
1432
- phases: phaseRows.map((p) => ({
1433
- id: p.id,
1434
- behaviorId: p.behavior_id,
1435
- phase: p.phase,
1436
- startedAt: p.started_at,
1437
- endedAt: p.ended_at,
1438
- transitionReason: p.transition_reason
1439
- })),
1440
- artifacts: artifactRows.map((a) => ({
1441
- id: a.id,
1442
- phaseId: a.phase_id,
1443
- artifactKind: a.artifact_kind,
1444
- testCaseId: a.test_case_id,
1445
- testRunId: a.test_run_id,
1446
- recordedAt: a.recorded_at
1447
- }))
1448
- });
1449
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1450
- operation: "read",
1451
- table: "tdd_tasks",
1452
- reason: extractSqlReason(e)
1453
- })));
1454
- const getCurrentTddPhase = (tddTaskId) => Effect.gen(function* () {
1455
- yield* Effect.logDebug("getCurrentTddPhase").pipe(Effect.annotateLogs({ tddTaskId }));
1456
- const rows = yield* sql`
1457
- SELECT id, phase, started_at, behavior_id FROM tdd_phases
1458
- WHERE tdd_task_id = ${tddTaskId} AND ended_at IS NULL
1459
- ORDER BY started_at DESC LIMIT 1
1460
- `;
1461
- if (rows.length === 0) return Option.none();
1462
- return Option.some({
1463
- id: rows[0].id,
1464
- phase: rows[0].phase,
1465
- startedAt: rows[0].started_at,
1466
- behaviorId: rows[0].behavior_id
1467
- });
1468
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1469
- operation: "read",
1470
- table: "tdd_phases",
1471
- reason: extractSqlReason(e)
1472
- })));
1473
- const getTddArtifactWithContext = (artifactId) => Effect.gen(function* () {
1474
- yield* Effect.logDebug("getTddArtifactWithContext").pipe(Effect.annotateLogs({ artifactId }));
1475
- const rows = yield* sql`
1476
- SELECT
1477
- a.id,
1478
- a.phase_id,
1479
- a.artifact_kind,
1480
- a.test_case_id,
1481
- ttc.occurred_at AS test_case_created_turn_at,
1482
- CASE
1483
- WHEN ttc.session_id = sess.id THEN 1
1484
- ELSE 0
1485
- END AS test_case_authored_in_session,
1486
- a.test_run_id,
1487
- a.test_first_failure_run_id,
1488
- p.behavior_id,
1489
- a.suite
1490
- FROM tdd_artifacts a
1491
- JOIN tdd_phases p ON p.id = a.phase_id
1492
- JOIN tdd_tasks ts ON ts.id = p.tdd_task_id
1493
- JOIN sessions sess ON sess.id = ts.session_id
1494
- LEFT JOIN test_cases tc ON tc.id = a.test_case_id
1495
- LEFT JOIN turns ttc ON ttc.id = tc.created_turn_id
1496
- WHERE a.id = ${artifactId}
1497
- `;
1498
- if (rows.length === 0) return Option.none();
1499
- const r = rows[0];
1500
- return Option.some({
1501
- id: r.id,
1502
- phase_id: r.phase_id,
1503
- artifact_kind: r.artifact_kind,
1504
- test_case_id: r.test_case_id,
1505
- test_case_created_turn_at: r.test_case_created_turn_at,
1506
- test_case_authored_in_session: r.test_case_authored_in_session === 1,
1507
- test_run_id: r.test_run_id,
1508
- test_first_failure_run_id: r.test_first_failure_run_id,
1509
- behavior_id: r.behavior_id,
1510
- suite: r.suite
1511
- });
1512
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1513
- operation: "read",
1514
- table: "tdd_artifacts",
1515
- reason: extractSqlReason(e)
1516
- })));
1517
- const getCommitChanges = (sha) => Effect.gen(function* () {
1518
- yield* Effect.logDebug("getCommitChanges").pipe(Effect.annotateLogs({ sha: sha ?? "ALL" }));
1519
- const commitRows = sha !== void 0 ? yield* sql`SELECT sha, parent_sha, message, author, committed_at, branch FROM commits WHERE sha = ${sha}` : yield* sql`
1520
- SELECT sha, parent_sha, message, author, committed_at, branch FROM commits
1521
- ORDER BY committed_at DESC NULLS LAST LIMIT 20
1522
- `;
1523
- const out = [];
1524
- for (const c of commitRows) {
1525
- const fileRows = yield* sql`
1526
- SELECT f.path, rcf.change_kind
1527
- FROM run_changed_files rcf
1528
- JOIN files f ON f.id = rcf.file_id
1529
- WHERE rcf.commit_sha = ${c.sha}
1530
- `;
1531
- out.push({
1532
- sha: c.sha,
1533
- parentSha: c.parent_sha,
1534
- message: c.message,
1535
- author: c.author,
1536
- committedAt: c.committed_at,
1537
- branch: c.branch,
1538
- files: fileRows.map((r) => ({
1539
- filePath: r.path,
1540
- changeKind: r.change_kind
1541
- }))
1542
- });
1543
- }
1544
- return out;
1545
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1546
- operation: "read",
1547
- table: "commits",
1548
- reason: extractSqlReason(e)
1549
- })));
1550
- const listTddTasksForSession = (sessionId, options) => Effect.gen(function* () {
1551
- yield* Effect.logDebug("listTddTasksForSession").pipe(Effect.annotateLogs({
1552
- sessionId,
1553
- walkParents: options?.walkParents ?? false,
1554
- walkConversation: options?.walkConversation ?? false
1555
- }));
1556
- const sessionIds = [sessionId];
1557
- if (options?.walkParents === true) {
1558
- let currentId = sessionId;
1559
- for (let i = 0; i < 64 && currentId !== null; i++) {
1560
- const next = (yield* sql`
1561
- SELECT parent_session_id FROM sessions WHERE id = ${currentId} LIMIT 1
1562
- `)[0]?.parent_session_id ?? null;
1563
- if (next === null || sessionIds.includes(next)) break;
1564
- sessionIds.push(next);
1565
- currentId = next;
1566
- }
1567
- }
1568
- if (options?.walkConversation === true) {
1569
- const conversationId = (yield* sql`
1570
- SELECT conversation_id FROM sessions WHERE id = ${sessionId} LIMIT 1
1571
- `)[0]?.conversation_id ?? null;
1572
- if (conversationId !== null) {
1573
- const conversationSessionRows = yield* sql`
1574
- SELECT id FROM sessions WHERE conversation_id = ${conversationId}
1575
- `;
1576
- for (const row of conversationSessionRows) if (!sessionIds.includes(row.id)) sessionIds.push(row.id);
1577
- }
1578
- }
1579
- return (yield* sql`
1580
- SELECT t.id, t.session_id, t.goal, t.started_at, t.ended_at, t.outcome, s.agent_kind
1581
- FROM tdd_tasks t
1582
- JOIN sessions s ON s.id = t.session_id
1583
- WHERE t.session_id IN ${sql.in(sessionIds)}
1584
- ORDER BY (CASE WHEN s.agent_kind = 'main' THEN 0 ELSE 1 END), t.started_at DESC
1585
- `).map((r) => ({
1586
- id: r.id,
1587
- sessionId: r.session_id,
1588
- goal: r.goal,
1589
- startedAt: r.started_at,
1590
- endedAt: r.ended_at,
1591
- outcome: r.outcome
1592
- }));
1593
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1594
- operation: "read",
1595
- table: "tdd_tasks",
1596
- reason: extractSqlReason(e)
1597
- })));
1598
- const countRecentArtifactsInOtherSessionsOfConversation = (input) => Effect.gen(function* () {
1599
- yield* Effect.logDebug("countRecentArtifactsInOtherSessionsOfConversation").pipe(Effect.annotateLogs(input));
1600
- const owner = (yield* sql`
1601
- SELECT t.session_id, s.conversation_id
1602
- FROM tdd_tasks t
1603
- JOIN sessions s ON s.id = t.session_id
1604
- WHERE t.id = ${input.tddTaskId}
1605
- LIMIT 1
1606
- `)[0];
1607
- if (owner === void 0 || owner.conversation_id === null) return 0;
1608
- return (yield* sql`
1609
- SELECT COUNT(*) AS n
1610
- FROM tdd_artifacts a
1611
- JOIN tdd_phases p ON p.id = a.phase_id
1612
- JOIN tdd_tasks t ON t.id = p.tdd_task_id
1613
- JOIN sessions s ON s.id = t.session_id
1614
- WHERE s.conversation_id = ${owner.conversation_id}
1615
- AND s.id != ${owner.session_id}
1616
- AND a.recorded_at >= ${input.sinceIso}
1617
- `)[0]?.n ?? 0;
1618
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1619
- operation: "read",
1620
- table: "tdd_artifacts",
1621
- reason: extractSqlReason(e)
1622
- })));
1623
- const listTddArtifactsForTask = (input) => Effect.gen(function* () {
1624
- yield* Effect.logDebug("listTddArtifactsForTask").pipe(Effect.annotateLogs(input));
1625
- const limit = input.limit ?? 50;
1626
- return (yield* sql`
1627
- SELECT
1628
- a.id,
1629
- p.tdd_task_id AS tdd_task_id,
1630
- a.phase_id,
1631
- p.phase AS phase_name,
1632
- a.artifact_kind,
1633
- p.behavior_id,
1634
- a.test_case_id,
1635
- a.test_run_id,
1636
- a.test_first_failure_run_id,
1637
- a.recorded_at,
1638
- a.suite
1639
- FROM tdd_artifacts a
1640
- JOIN tdd_phases p ON p.id = a.phase_id
1641
- WHERE p.tdd_task_id = ${input.tddTaskId}
1642
- AND (${input.artifactKind ?? null} IS NULL OR a.artifact_kind = ${input.artifactKind ?? null})
1643
- AND (${input.phaseId ?? null} IS NULL OR a.phase_id = ${input.phaseId ?? null})
1644
- AND (${input.behaviorId ?? null} IS NULL OR p.behavior_id = ${input.behaviorId ?? null})
1645
- ORDER BY a.recorded_at DESC, a.id DESC
1646
- LIMIT ${limit}
1647
- `).map((r) => ({
1648
- id: r.id,
1649
- tddTaskId: r.tdd_task_id,
1650
- phaseId: r.phase_id,
1651
- phaseName: r.phase_name,
1652
- artifactKind: r.artifact_kind,
1653
- behaviorId: r.behavior_id,
1654
- testCaseId: r.test_case_id,
1655
- testRunId: r.test_run_id,
1656
- testFirstFailureRunId: r.test_first_failure_run_id,
1657
- recordedAt: r.recorded_at,
1658
- suite: r.suite
1659
- }));
1660
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1661
- operation: "read",
1662
- table: "tdd_artifacts",
1663
- reason: extractSqlReason(e)
1664
- })));
1665
- const listHypotheses = (options) => Effect.gen(function* () {
1666
- yield* Effect.logDebug("listHypotheses").pipe(Effect.annotateLogs({ ...options }));
1667
- const limit = options.limit ?? 50;
1668
- const sessionId = options.sessionId ?? null;
1669
- const outcome = options.outcome ?? null;
1670
- return (yield* sql`
1671
- SELECT id, session_id, content, cited_test_error_id, cited_stack_frame_id,
1672
- validation_outcome, validated_at
1673
- FROM hypotheses
1674
- WHERE (${sessionId} IS NULL OR session_id = ${sessionId})
1675
- AND (
1676
- ${outcome} IS NULL
1677
- OR (${outcome} = 'open' AND validation_outcome IS NULL)
1678
- OR (${outcome} != 'open' AND validation_outcome = ${outcome})
1679
- )
1680
- ORDER BY id DESC
1681
- LIMIT ${limit}
1682
- `).map((r) => ({
1683
- id: r.id,
1684
- sessionId: r.session_id,
1685
- content: r.content,
1686
- citedTestErrorId: r.cited_test_error_id,
1687
- citedStackFrameId: r.cited_stack_frame_id,
1688
- validationOutcome: r.validation_outcome,
1689
- validatedAt: r.validated_at
1690
- }));
1691
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1692
- operation: "read",
1693
- table: "hypotheses",
1694
- reason: extractSqlReason(e)
1695
- })));
1696
- const findIdempotentResponse = (procedurePath, key) => Effect.gen(function* () {
1697
- yield* Effect.logDebug("findIdempotentResponse").pipe(Effect.annotateLogs({
1698
- procedurePath,
1699
- key
1700
- }));
1701
- const rows = yield* sql`
1702
- SELECT result_json FROM mcp_idempotent_responses
1703
- WHERE procedure_path = ${procedurePath} AND key = ${key}
1704
- LIMIT 1
1705
- `;
1706
- return rows.length === 0 ? Option.none() : Option.some(rows[0].result_json);
1707
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1708
- operation: "read",
1709
- table: "mcp_idempotent_responses",
1710
- reason: extractSqlReason(e)
1711
- })));
1712
- const getLatestTestCaseForSession = (chatId) => Effect.gen(function* () {
1713
- yield* Effect.logDebug("getLatestTestCaseForSession").pipe(Effect.annotateLogs({ chatId }));
1714
- const rows = yield* sql`
1715
- SELECT tc.id
1716
- FROM test_cases tc
1717
- JOIN test_modules tm ON tc.module_id = tm.id
1718
- JOIN files f_mod ON f_mod.id = tm.file_id
1719
- WHERE tm.run_id = (SELECT id FROM test_runs ORDER BY id DESC LIMIT 1)
1720
- AND EXISTS (
1721
- SELECT 1
1722
- FROM file_edits fe
1723
- JOIN turns t ON fe.turn_id = t.id
1724
- JOIN sessions s ON t.session_id = s.id
1725
- JOIN files f_edit ON fe.file_id = f_edit.id
1726
- WHERE s.chat_id = ${chatId}
1727
- AND (
1728
- f_edit.path = f_mod.path
1729
- OR f_edit.path LIKE '%/' || f_mod.path
1730
- OR f_mod.path LIKE '%/' || f_edit.path
1731
- )
1732
- )
1733
- ORDER BY tc.id DESC
1734
- LIMIT 1
1735
- `;
1736
- return rows.length === 0 ? Option.none() : Option.some(rows[0].id);
1737
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1738
- operation: "read",
1739
- table: "test_cases",
1740
- reason: extractSqlReason(e)
1741
- })));
1742
- const listTagInventory = (options) => Effect.gen(function* () {
1743
- yield* Effect.logDebug("listTagInventory").pipe(Effect.annotateLogs({ project: options?.project }));
1744
- if (options?.project) {
1745
- const runs = yield* sql`SELECT id FROM test_runs
1746
- WHERE project = ${options.project}
1747
- ORDER BY timestamp DESC LIMIT 1`;
1748
- if (runs.length === 0) return [];
1749
- const runId = runs[0].id;
1750
- return (yield* sql`
1751
- SELECT t.name AS tag, ${options.project} AS project,
1752
- COUNT(DISTINCT tm.id) AS module_count,
1753
- COUNT(tc.id) AS test_count
1754
- FROM test_cases tc
1755
- JOIN test_modules tm ON tm.id = tc.module_id
1756
- JOIN test_case_tags tct ON tct.test_case_id = tc.id
1757
- JOIN tags t ON t.id = tct.tag_id
1758
- WHERE tm.run_id = ${runId}
1759
- GROUP BY t.name
1760
- ORDER BY t.name
1761
- `).map((r) => ({
1762
- tag: r.tag,
1763
- project: r.project,
1764
- moduleCount: r.module_count,
1765
- testCount: r.test_count
1766
- }));
1767
- }
1768
- return (yield* sql`
1769
- SELECT t.name AS tag, tr.project AS project,
1770
- COUNT(DISTINCT tm.id) AS module_count,
1771
- COUNT(tc.id) AS test_count
1772
- FROM test_runs tr
1773
- JOIN (
1774
- SELECT project, MAX(timestamp) AS max_ts
1775
- FROM test_runs
1776
- GROUP BY project
1777
- ) latest ON tr.project = latest.project AND tr.timestamp = latest.max_ts
1778
- JOIN test_modules tm ON tm.run_id = tr.id
1779
- JOIN test_cases tc ON tc.module_id = tm.id
1780
- JOIN test_case_tags tct ON tct.test_case_id = tc.id
1781
- JOIN tags t ON t.id = tct.tag_id
1782
- GROUP BY tr.project, t.name
1783
- ORDER BY tr.project, t.name
1784
- `).map((r) => ({
1785
- tag: r.tag,
1786
- project: r.project,
1787
- moduleCount: r.module_count,
1788
- testCount: r.test_count
1789
- }));
1790
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1791
- operation: "read",
1792
- table: "tags",
1793
- reason: extractSqlReason(e)
1794
- })));
1795
- const listTestsForTag = (tag, options) => Effect.gen(function* () {
1796
- yield* Effect.logDebug("listTestsForTag").pipe(Effect.annotateLogs({
1797
- tag,
1798
- project: options?.project
1799
- }));
1800
- if (options?.project) {
1801
- const runs = yield* sql`SELECT id FROM test_runs
1802
- WHERE project = ${options.project}
1803
- ORDER BY timestamp DESC LIMIT 1`;
1804
- if (runs.length === 0) return [];
1805
- const runId = runs[0].id;
1806
- return (yield* sql`
1807
- SELECT tc.id, tc.full_name, tc.state, tc.duration, f.path AS file_path, tc.classification
1808
- FROM test_cases tc
1809
- JOIN test_modules tm ON tm.id = tc.module_id
1810
- JOIN files f ON f.id = tm.file_id
1811
- JOIN test_case_tags tct ON tct.test_case_id = tc.id
1812
- JOIN tags t ON t.id = tct.tag_id
1813
- WHERE tm.run_id = ${runId} AND t.name = ${tag}
1814
- ORDER BY tc.full_name
1815
- `).map(mapTestListRow);
1816
- }
1817
- return (yield* sql`
1818
- SELECT tc.id, tc.full_name, tc.state, tc.duration, f.path AS file_path, tc.classification
1819
- FROM test_runs tr
1820
- JOIN (
1821
- SELECT project, MAX(timestamp) AS max_ts
1822
- FROM test_runs
1823
- GROUP BY project
1824
- ) latest ON tr.project = latest.project AND tr.timestamp = latest.max_ts
1825
- JOIN test_modules tm ON tm.run_id = tr.id
1826
- JOIN files f ON f.id = tm.file_id
1827
- JOIN test_cases tc ON tc.module_id = tm.id
1828
- JOIN test_case_tags tct ON tct.test_case_id = tc.id
1829
- JOIN tags t ON t.id = tct.tag_id
1830
- WHERE t.name = ${tag}
1831
- ORDER BY tr.project, tc.full_name
1832
- `).map(mapTestListRow);
1833
- }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1834
- operation: "read",
1835
- table: "tags",
1836
- reason: extractSqlReason(e)
1837
- })));
1838
- return {
1839
- getLatestRun,
1840
- getRunsByProject,
1841
- getHistory,
1842
- getBaselines,
1843
- getTrends,
1844
- getFlaky,
1845
- getPersistentFailures,
1846
- getFileCoverage,
1847
- getCoverage,
1848
- getTestsForFile,
1849
- getErrors,
1850
- getAnnotationsForTest,
1851
- getArtifactsForTest,
1852
- getNotes,
1853
- getNoteById,
1854
- searchNotes,
1855
- getManifest,
1856
- getSettings,
1857
- getLatestSettings,
1858
- getTestByFullName,
1859
- getTestModulesByFullName,
1860
- listTests,
1861
- listModules,
1862
- listSuites,
1863
- listSettings,
1864
- getSessionById,
1865
- getSessionByChatId,
1866
- findSessionsByChatPrefix,
1867
- findActiveSubagentSession,
1868
- getSessionByTddTaskId,
1869
- listSessions,
1870
- searchTurns,
1871
- computeAcceptanceMetrics,
1872
- getFailureSignatureByHash,
1873
- getTddTaskById,
1874
- getGoalById,
1875
- getGoalsByTddTask,
1876
- getBehaviorById,
1877
- getBehaviorsByGoal,
1878
- getBehaviorsByTddTask,
1879
- getBehaviorDependencies,
1880
- resolveGoalIdForBehavior,
1881
- getCurrentTddPhase,
1882
- getTddArtifactWithContext,
1883
- getCommitChanges,
1884
- listTddTasksForSession,
1885
- countRecentArtifactsInOtherSessionsOfConversation,
1886
- listTddArtifactsForTask,
1887
- listHypotheses,
1888
- findIdempotentResponse,
1889
- getLatestTestCaseForSession,
1890
- listTagInventory,
1891
- listTestsForTag
1892
- };
1893
- }));
1894
- function mapTestListRow(row) {
1895
- return {
1896
- id: row.id,
1897
- fullName: row.full_name,
1898
- state: row.state,
1899
- duration: row.duration,
1900
- module: row.file_path,
1901
- classification: row.classification
1902
- };
1903
- }
1904
- function mapNoteRow(row) {
1905
- return {
1906
- id: row.id,
1907
- title: row.title,
1908
- content: row.content,
1909
- scope: row.scope,
1910
- project: row.project,
1911
- testFullName: row.test_full_name,
1912
- modulePath: row.module_path,
1913
- parentNoteId: row.parent_note_id,
1914
- createdBy: row.created_by,
1915
- expiresAt: row.expires_at,
1916
- pinned: row.pinned === 1,
1917
- createdAt: row.created_at,
1918
- updatedAt: row.updated_at
1919
- };
1920
- }
1921
-
1922
- //#endregion
1923
- export { DataReaderLive };