@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.
- package/README.md +23 -19
- package/dispatch.d.ts +25 -6
- package/formatters/markdown.js +1 -0
- package/formatters/terminal.js +4 -3
- package/index.d.ts +448 -2341
- package/index.js +10 -68
- package/internal-inject-env.js +5 -7
- package/package.json +3 -19
- package/sidecar-dispatch.js +7 -5
- package/utils/format-console.js +9 -10
- package/utils/format-terminal.js +7 -7
- package/utils/posix-path.js +52 -0
- package/utils/test-location.js +11 -5
- package/version.js +17 -0
- package/layers/ConfigLive.js +0 -46
- package/layers/DataReaderLive.js +0 -1923
- package/layers/DataStoreLive.js +0 -1287
- package/layers/DetailResolverLive.js +0 -9
- package/layers/DiscoveryRegistryLive.js +0 -70
- package/layers/EnvironmentDetectorLive.js +0 -21
- package/layers/EnvironmentDetectorTest.js +0 -13
- package/layers/ExecutorResolverLive.js +0 -9
- package/layers/FormatSelectorLive.js +0 -20
- package/layers/HistoryTrackerLive.js +0 -49
- package/layers/HistoryTrackerTest.js +0 -25
- package/layers/LoggerLive.js +0 -64
- package/layers/OutputPipelineLive.js +0 -13
- package/layers/OutputRendererLive.js +0 -28
- package/layers/PathResolutionLive.js +0 -40
- package/layers/PerClientSessionMapLive.js +0 -145
- package/layers/ProjectDiscoveryLive.js +0 -68
- package/layers/ProjectDiscoveryTest.js +0 -12
- package/layers/ProjectIdentityLive.js +0 -96
- package/layers/RunContextLive.js +0 -81
- package/lib/format-triage.js +0 -103
- package/lib/format-wrapup.js +0 -67
- package/migrations/0001_initial.js +0 -824
- package/migrations/0002_test_artifacts.js +0 -30
- package/migrations/index.js +0 -26
- package/migrations/registry_0001_initial.js +0 -39
- package/migrations/session_map_0001_initial.js +0 -38
- package/services/Config.js +0 -15
- package/services/DataReader.js +0 -8
- package/services/DataStore.js +0 -15
- package/services/DetailResolver.js +0 -8
- package/services/DiscoveryRegistry.js +0 -8
- package/services/EnvironmentDetector.js +0 -8
- package/services/ExecutorResolver.js +0 -8
- package/services/FormatSelector.js +0 -8
- package/services/HistoryTracker.js +0 -20
- package/services/OutputRenderer.js +0 -8
- package/services/PerClientSessionMap.js +0 -22
- package/services/ProjectDiscovery.js +0 -8
- package/services/ProjectIdentity.js +0 -63
- package/services/RunContext.js +0 -53
- package/services/idempotency.js +0 -58
- package/sql/assemblers.js +0 -69
- package/testing/layers.js +0 -21
- package/testing.d.ts +0 -1862
- package/testing.js +0 -254
- package/utils/ensure-migrated.js +0 -52
- package/utils/failure-signature.js +0 -34
- package/utils/resolve-data-path.js +0 -65
- package/utils/resolve-project-key-from-cwd.js +0 -53
- package/utils/resolve-workspace-key.js +0 -41
package/layers/DataReaderLive.js
DELETED
|
@@ -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 };
|