@vitest-agent/sdk 2.4.12 → 2.5.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/index.d.ts +223 -5
- package/index.js +5 -3
- package/layers/DataReaderLive.js +101 -23
- package/layers/DataStoreLive.js +54 -6
- package/migrations/0001_initial.js +6 -2
- package/package.json +2 -2
- package/schemas/ConsoleLeaks.js +20 -3
- package/schemas/Coverage.js +1 -0
- package/schemas/RenderState.js +14 -3
- package/schemas/RunEvent.js +12 -2
- package/testing.d.ts +118 -1
- package/utils/console-leaks.js +42 -15
- package/utils/detect-non-default-discover-strategy.js +38 -0
- package/utils/format-console.js +5 -0
- package/utils/format-scoped-coverage-note.js +25 -0
- package/utils/format-terminal.js +15 -2
- package/utils/resolve-data-path.js +5 -8
- package/utils/validate-phase-transition.js +20 -6
package/layers/DataReaderLive.js
CHANGED
|
@@ -118,7 +118,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
118
118
|
lines: 0
|
|
119
119
|
};
|
|
120
120
|
const baselineRows = yield* sql`SELECT metric, value FROM coverage_baselines
|
|
121
|
-
WHERE project = '__global__' AND pattern = ''`;
|
|
121
|
+
WHERE project = '__global__' AND kind = 'baseline' AND pattern = ''`;
|
|
122
122
|
const thresholds = {};
|
|
123
123
|
for (const b of baselineRows) if (b.metric === "lines") thresholds.lines = b.value;
|
|
124
124
|
else if (b.metric === "functions") thresholds.functions = b.value;
|
|
@@ -253,11 +253,21 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
253
253
|
table: "test_history",
|
|
254
254
|
reason: extractSqlReason(e)
|
|
255
255
|
})));
|
|
256
|
-
|
|
257
|
-
|
|
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
|
+
}));
|
|
258
268
|
const rows = yield* sql`SELECT metric, value, pattern, updated_at
|
|
259
269
|
FROM coverage_baselines
|
|
260
|
-
WHERE project = ${project}
|
|
270
|
+
WHERE project = ${project} AND kind = ${kind}
|
|
261
271
|
ORDER BY pattern, metric`;
|
|
262
272
|
if (rows.length === 0) return Option.none();
|
|
263
273
|
const global = {};
|
|
@@ -284,6 +294,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
284
294
|
table: "coverage_baselines",
|
|
285
295
|
reason: extractSqlReason(e)
|
|
286
296
|
})));
|
|
297
|
+
const getBaselines = (project) => getCoveragePolicy("baseline", project);
|
|
287
298
|
const getTrends = (project, limit) => Effect.gen(function* () {
|
|
288
299
|
yield* Effect.logDebug("getTrends").pipe(Effect.annotateLogs({
|
|
289
300
|
project,
|
|
@@ -454,7 +465,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
454
465
|
ORDER BY timestamp DESC LIMIT 1`;
|
|
455
466
|
if (runs.length === 0) return Option.none();
|
|
456
467
|
const runId = runs[0].id;
|
|
457
|
-
const fileCoverageRows = yield* sql`SELECT f.path as file_path, fc.statements, fc.branches, fc.functions, fc.lines, fc.uncovered_lines
|
|
468
|
+
const fileCoverageRows = yield* sql`SELECT f.path as file_path, fc.statements, fc.branches, fc.functions, fc.lines, fc.uncovered_lines, fc.tier
|
|
458
469
|
FROM file_coverage fc JOIN files f ON f.id = fc.file_id
|
|
459
470
|
WHERE fc.run_id = ${runId}`;
|
|
460
471
|
const trendRows = yield* sql`SELECT statements, branches, functions, lines
|
|
@@ -478,13 +489,20 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
478
489
|
lines: fileCoverageRows.reduce((sum, r) => sum + r.lines, 0) / count
|
|
479
490
|
};
|
|
480
491
|
}
|
|
481
|
-
const
|
|
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
|
+
}));
|
|
482
500
|
const baselines = Option.getOrElse(baselinesOpt, () => ({
|
|
483
501
|
updatedAt: "",
|
|
484
502
|
global: {},
|
|
485
503
|
patterns: []
|
|
486
504
|
}));
|
|
487
|
-
const
|
|
505
|
+
const toFileCoverageReport = (r) => ({
|
|
488
506
|
file: r.file_path,
|
|
489
507
|
summary: {
|
|
490
508
|
statements: r.statements,
|
|
@@ -493,16 +511,30 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
493
511
|
lines: r.lines
|
|
494
512
|
},
|
|
495
513
|
uncoveredLines: r.uncovered_lines ?? ""
|
|
496
|
-
})
|
|
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);
|
|
497
517
|
const report = {
|
|
498
518
|
totals,
|
|
499
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: {
|
|
500
528
|
global: baselines.global,
|
|
501
529
|
patterns: baselines.patterns
|
|
502
530
|
},
|
|
503
531
|
scoped: false,
|
|
504
532
|
lowCoverage,
|
|
505
|
-
lowCoverageFiles: lowCoverage.map((f) => f.file)
|
|
533
|
+
lowCoverageFiles: lowCoverage.map((f) => f.file),
|
|
534
|
+
...belowTarget.length > 0 ? {
|
|
535
|
+
belowTarget,
|
|
536
|
+
belowTargetFiles: belowTarget.map((f) => f.file)
|
|
537
|
+
} : {}
|
|
506
538
|
};
|
|
507
539
|
return Option.some(report);
|
|
508
540
|
}).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
|
|
@@ -895,11 +927,12 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
895
927
|
triageWasNonEmpty: r.triage_was_non_empty === 1,
|
|
896
928
|
startedAt: r.started_at,
|
|
897
929
|
endedAt: r.ended_at,
|
|
898
|
-
endReason: r.end_reason
|
|
930
|
+
endReason: r.end_reason,
|
|
931
|
+
conversationId: r.conversation_id
|
|
899
932
|
});
|
|
900
933
|
const getSessionById = (id) => Effect.gen(function* () {
|
|
901
934
|
yield* Effect.logDebug("getSessionById").pipe(Effect.annotateLogs({ id }));
|
|
902
|
-
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 FROM sessions WHERE id = ${id} LIMIT 1`;
|
|
935
|
+
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`;
|
|
903
936
|
if (rows.length === 0) return Option.none();
|
|
904
937
|
return Option.some(sessionRowToDetail(rows[0]));
|
|
905
938
|
}).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
|
|
@@ -909,7 +942,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
909
942
|
})));
|
|
910
943
|
const getSessionByChatId = (chatId) => Effect.gen(function* () {
|
|
911
944
|
yield* Effect.logDebug("getSessionByChatId").pipe(Effect.annotateLogs({ chatId }));
|
|
912
|
-
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 FROM sessions WHERE chat_id = ${chatId} LIMIT 1`;
|
|
945
|
+
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`;
|
|
913
946
|
if (rows.length === 0) return Option.none();
|
|
914
947
|
return Option.some(sessionRowToDetail(rows[0]));
|
|
915
948
|
}).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
|
|
@@ -920,7 +953,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
920
953
|
const findSessionsByChatPrefix = (prefix) => Effect.gen(function* () {
|
|
921
954
|
yield* Effect.logDebug("findSessionsByChatPrefix").pipe(Effect.annotateLogs({ prefix }));
|
|
922
955
|
const pattern = `${prefix}%`;
|
|
923
|
-
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 FROM sessions WHERE chat_id LIKE ${pattern} ESCAPE '\\' ORDER BY started_at DESC`).map(sessionRowToDetail);
|
|
956
|
+
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);
|
|
924
957
|
}).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
|
|
925
958
|
operation: "read",
|
|
926
959
|
table: "sessions",
|
|
@@ -928,7 +961,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
928
961
|
})));
|
|
929
962
|
const findActiveSubagentSession = (parentSessionId) => Effect.gen(function* () {
|
|
930
963
|
yield* Effect.logDebug("findActiveSubagentSession").pipe(Effect.annotateLogs({ parentSessionId }));
|
|
931
|
-
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 FROM sessions WHERE parent_session_id = ${parentSessionId} AND agent_kind = 'subagent' AND ended_at IS NULL ORDER BY started_at DESC LIMIT 1`;
|
|
964
|
+
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`;
|
|
932
965
|
if (rows.length === 0) return Option.none();
|
|
933
966
|
return Option.some(sessionRowToDetail(rows[0]));
|
|
934
967
|
}).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
|
|
@@ -938,7 +971,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
938
971
|
})));
|
|
939
972
|
const getSessionByTddTaskId = (tddTaskId) => Effect.gen(function* () {
|
|
940
973
|
yield* Effect.logDebug("getSessionByTddTaskId").pipe(Effect.annotateLogs({ tddTaskId }));
|
|
941
|
-
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 FROM sessions WHERE id = (SELECT session_id FROM tdd_tasks WHERE id = ${tddTaskId}) LIMIT 1`;
|
|
974
|
+
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`;
|
|
942
975
|
if (rows.length === 0) return Option.none();
|
|
943
976
|
return Option.some(sessionRowToDetail(rows[0]));
|
|
944
977
|
}).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
|
|
@@ -1078,7 +1111,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
1078
1111
|
const agentKind = options.agentKind ?? null;
|
|
1079
1112
|
return (yield* sql`
|
|
1080
1113
|
SELECT id, chat_id, project, cwd, agent_kind, agent_type,
|
|
1081
|
-
parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason
|
|
1114
|
+
parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason, conversation_id
|
|
1082
1115
|
FROM sessions
|
|
1083
1116
|
WHERE (${project} IS NULL OR project = ${project})
|
|
1084
1117
|
AND (${agentKind} IS NULL OR agent_kind = ${agentKind})
|
|
@@ -1364,7 +1397,8 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
1364
1397
|
END AS test_case_authored_in_session,
|
|
1365
1398
|
a.test_run_id,
|
|
1366
1399
|
a.test_first_failure_run_id,
|
|
1367
|
-
p.behavior_id
|
|
1400
|
+
p.behavior_id,
|
|
1401
|
+
a.suite
|
|
1368
1402
|
FROM tdd_artifacts a
|
|
1369
1403
|
JOIN tdd_phases p ON p.id = a.phase_id
|
|
1370
1404
|
JOIN tdd_tasks ts ON ts.id = p.tdd_task_id
|
|
@@ -1384,7 +1418,8 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
1384
1418
|
test_case_authored_in_session: r.test_case_authored_in_session === 1,
|
|
1385
1419
|
test_run_id: r.test_run_id,
|
|
1386
1420
|
test_first_failure_run_id: r.test_first_failure_run_id,
|
|
1387
|
-
behavior_id: r.behavior_id
|
|
1421
|
+
behavior_id: r.behavior_id,
|
|
1422
|
+
suite: r.suite
|
|
1388
1423
|
});
|
|
1389
1424
|
}).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
|
|
1390
1425
|
operation: "read",
|
|
@@ -1427,7 +1462,8 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
1427
1462
|
const listTddTasksForSession = (sessionId, options) => Effect.gen(function* () {
|
|
1428
1463
|
yield* Effect.logDebug("listTddTasksForSession").pipe(Effect.annotateLogs({
|
|
1429
1464
|
sessionId,
|
|
1430
|
-
walkParents: options?.walkParents ?? false
|
|
1465
|
+
walkParents: options?.walkParents ?? false,
|
|
1466
|
+
walkConversation: options?.walkConversation ?? false
|
|
1431
1467
|
}));
|
|
1432
1468
|
const sessionIds = [sessionId];
|
|
1433
1469
|
if (options?.walkParents === true) {
|
|
@@ -1441,9 +1477,23 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
1441
1477
|
currentId = next;
|
|
1442
1478
|
}
|
|
1443
1479
|
}
|
|
1480
|
+
if (options?.walkConversation === true) {
|
|
1481
|
+
const conversationId = (yield* sql`
|
|
1482
|
+
SELECT conversation_id FROM sessions WHERE id = ${sessionId} LIMIT 1
|
|
1483
|
+
`)[0]?.conversation_id ?? null;
|
|
1484
|
+
if (conversationId !== null) {
|
|
1485
|
+
const conversationSessionRows = yield* sql`
|
|
1486
|
+
SELECT id FROM sessions WHERE conversation_id = ${conversationId}
|
|
1487
|
+
`;
|
|
1488
|
+
for (const row of conversationSessionRows) if (!sessionIds.includes(row.id)) sessionIds.push(row.id);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1444
1491
|
return (yield* sql`
|
|
1445
|
-
SELECT id, session_id, goal, started_at, ended_at, outcome
|
|
1446
|
-
|
|
1492
|
+
SELECT t.id, t.session_id, t.goal, t.started_at, t.ended_at, t.outcome, s.agent_kind
|
|
1493
|
+
FROM tdd_tasks t
|
|
1494
|
+
JOIN sessions s ON s.id = t.session_id
|
|
1495
|
+
WHERE t.session_id IN ${sql.in(sessionIds)}
|
|
1496
|
+
ORDER BY (CASE WHEN s.agent_kind = 'main' THEN 0 ELSE 1 END), t.started_at DESC
|
|
1447
1497
|
`).map((r) => ({
|
|
1448
1498
|
id: r.id,
|
|
1449
1499
|
sessionId: r.session_id,
|
|
@@ -1457,6 +1507,31 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
1457
1507
|
table: "tdd_tasks",
|
|
1458
1508
|
reason: extractSqlReason(e)
|
|
1459
1509
|
})));
|
|
1510
|
+
const countRecentArtifactsInOtherSessionsOfConversation = (input) => Effect.gen(function* () {
|
|
1511
|
+
yield* Effect.logDebug("countRecentArtifactsInOtherSessionsOfConversation").pipe(Effect.annotateLogs(input));
|
|
1512
|
+
const owner = (yield* sql`
|
|
1513
|
+
SELECT t.session_id, s.conversation_id
|
|
1514
|
+
FROM tdd_tasks t
|
|
1515
|
+
JOIN sessions s ON s.id = t.session_id
|
|
1516
|
+
WHERE t.id = ${input.tddTaskId}
|
|
1517
|
+
LIMIT 1
|
|
1518
|
+
`)[0];
|
|
1519
|
+
if (owner === void 0 || owner.conversation_id === null) return 0;
|
|
1520
|
+
return (yield* sql`
|
|
1521
|
+
SELECT COUNT(*) AS n
|
|
1522
|
+
FROM tdd_artifacts a
|
|
1523
|
+
JOIN tdd_phases p ON p.id = a.phase_id
|
|
1524
|
+
JOIN tdd_tasks t ON t.id = p.tdd_task_id
|
|
1525
|
+
JOIN sessions s ON s.id = t.session_id
|
|
1526
|
+
WHERE s.conversation_id = ${owner.conversation_id}
|
|
1527
|
+
AND s.id != ${owner.session_id}
|
|
1528
|
+
AND a.recorded_at >= ${input.sinceIso}
|
|
1529
|
+
`)[0]?.n ?? 0;
|
|
1530
|
+
}).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
|
|
1531
|
+
operation: "read",
|
|
1532
|
+
table: "tdd_artifacts",
|
|
1533
|
+
reason: extractSqlReason(e)
|
|
1534
|
+
})));
|
|
1460
1535
|
const listTddArtifactsForTask = (input) => Effect.gen(function* () {
|
|
1461
1536
|
yield* Effect.logDebug("listTddArtifactsForTask").pipe(Effect.annotateLogs(input));
|
|
1462
1537
|
const limit = input.limit ?? 50;
|
|
@@ -1471,7 +1546,8 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
1471
1546
|
a.test_case_id,
|
|
1472
1547
|
a.test_run_id,
|
|
1473
1548
|
a.test_first_failure_run_id,
|
|
1474
|
-
a.recorded_at
|
|
1549
|
+
a.recorded_at,
|
|
1550
|
+
a.suite
|
|
1475
1551
|
FROM tdd_artifacts a
|
|
1476
1552
|
JOIN tdd_phases p ON p.id = a.phase_id
|
|
1477
1553
|
WHERE p.tdd_task_id = ${input.tddTaskId}
|
|
@@ -1490,7 +1566,8 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
1490
1566
|
testCaseId: r.test_case_id,
|
|
1491
1567
|
testRunId: r.test_run_id,
|
|
1492
1568
|
testFirstFailureRunId: r.test_first_failure_run_id,
|
|
1493
|
-
recordedAt: r.recorded_at
|
|
1569
|
+
recordedAt: r.recorded_at,
|
|
1570
|
+
suite: r.suite
|
|
1494
1571
|
}));
|
|
1495
1572
|
}).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
|
|
1496
1573
|
operation: "read",
|
|
@@ -1715,6 +1792,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
1715
1792
|
getTddArtifactWithContext,
|
|
1716
1793
|
getCommitChanges,
|
|
1717
1794
|
listTddTasksForSession,
|
|
1795
|
+
countRecentArtifactsInOtherSessionsOfConversation,
|
|
1718
1796
|
listTddArtifactsForTask,
|
|
1719
1797
|
listHypotheses,
|
|
1720
1798
|
findIdempotentResponse,
|
package/layers/DataStoreLive.js
CHANGED
|
@@ -201,17 +201,50 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
|
201
201
|
];
|
|
202
202
|
for (const metric of metrics) {
|
|
203
203
|
const value = g[metric];
|
|
204
|
-
if (value !== void 0 && Number.isFinite(value)) yield* sql`INSERT INTO coverage_baselines (project, metric, value, pattern, updated_at) VALUES ('__global__', ${metric}, ${value}, '', ${updatedAt}) ON CONFLICT (project, metric, pattern) DO UPDATE SET value = ${value}, updated_at = ${updatedAt}`;
|
|
204
|
+
if (value !== void 0 && Number.isFinite(value)) yield* sql`INSERT INTO coverage_baselines (project, kind, metric, value, pattern, updated_at) VALUES ('__global__', 'baseline', ${metric}, ${value}, '', ${updatedAt}) ON CONFLICT (project, kind, metric, pattern) DO UPDATE SET value = ${value}, updated_at = ${updatedAt}`;
|
|
205
205
|
}
|
|
206
206
|
for (const [pattern, thresholds] of patterns) for (const metric of metrics) {
|
|
207
207
|
const value = thresholds[metric];
|
|
208
|
-
if (value !== void 0 && Number.isFinite(value)) yield* sql`INSERT INTO coverage_baselines (project, metric, value, pattern, updated_at) VALUES ('__global__', ${metric}, ${value}, ${pattern}, ${updatedAt}) ON CONFLICT (project, metric, pattern) DO UPDATE SET value = ${value}, updated_at = ${updatedAt}`;
|
|
208
|
+
if (value !== void 0 && Number.isFinite(value)) yield* sql`INSERT INTO coverage_baselines (project, kind, metric, value, pattern, updated_at) VALUES ('__global__', 'baseline', ${metric}, ${value}, ${pattern}, ${updatedAt}) ON CONFLICT (project, kind, metric, pattern) DO UPDATE SET value = ${value}, updated_at = ${updatedAt}`;
|
|
209
209
|
}
|
|
210
210
|
}).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
|
|
211
211
|
operation: "write",
|
|
212
212
|
table: "coverage_baselines",
|
|
213
213
|
reason: extractSqlReason(e)
|
|
214
214
|
})));
|
|
215
|
+
/**
|
|
216
|
+
* Shared upsert for the `coverage_baselines` table's non-baseline
|
|
217
|
+
* `kind` rows (`threshold` / `target`). Distinct from `writeBaselines`
|
|
218
|
+
* so the ratcheted high-water mark never collides with the resolved,
|
|
219
|
+
* enforced Vitest thresholds or the aspirational coverageTargets
|
|
220
|
+
* (issue #237).
|
|
221
|
+
*/
|
|
222
|
+
const writeCoveragePolicy = (kind, policy) => sql.withTransaction(Effect.gen(function* () {
|
|
223
|
+
yield* Effect.logDebug("writeCoveragePolicy").pipe(Effect.annotateLogs({ kind }));
|
|
224
|
+
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
225
|
+
const { global: g, patterns = [] } = policy;
|
|
226
|
+
const metrics = [
|
|
227
|
+
"lines",
|
|
228
|
+
"functions",
|
|
229
|
+
"branches",
|
|
230
|
+
"statements"
|
|
231
|
+
];
|
|
232
|
+
yield* sql`DELETE FROM coverage_baselines WHERE project = '__global__' AND kind = ${kind}`;
|
|
233
|
+
for (const metric of metrics) {
|
|
234
|
+
const value = g[metric];
|
|
235
|
+
if (value !== void 0 && Number.isFinite(value)) yield* sql`INSERT INTO coverage_baselines (project, kind, metric, value, pattern, updated_at) VALUES ('__global__', ${kind}, ${metric}, ${value}, '', ${updatedAt})`;
|
|
236
|
+
}
|
|
237
|
+
for (const [pattern, thresholds] of patterns) for (const metric of metrics) {
|
|
238
|
+
const value = thresholds[metric];
|
|
239
|
+
if (value !== void 0 && Number.isFinite(value)) yield* sql`INSERT INTO coverage_baselines (project, kind, metric, value, pattern, updated_at) VALUES ('__global__', ${kind}, ${metric}, ${value}, ${pattern}, ${updatedAt})`;
|
|
240
|
+
}
|
|
241
|
+
})).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
|
|
242
|
+
operation: "write",
|
|
243
|
+
table: "coverage_baselines",
|
|
244
|
+
reason: extractSqlReason(e)
|
|
245
|
+
})));
|
|
246
|
+
const writeThresholds = (thresholds) => writeCoveragePolicy("threshold", thresholds);
|
|
247
|
+
const writeTargets = (targets) => writeCoveragePolicy("target", targets);
|
|
215
248
|
const writeTrends = (project, runId, entry) => Effect.gen(function* () {
|
|
216
249
|
yield* Effect.logDebug("writeTrends").pipe(Effect.annotateLogs({
|
|
217
250
|
project,
|
|
@@ -311,7 +344,7 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
|
311
344
|
})));
|
|
312
345
|
const writeSession = (input) => Effect.gen(function* () {
|
|
313
346
|
yield* Effect.logDebug("writeSession").pipe(Effect.annotateLogs({ chatId: input.chatId }));
|
|
314
|
-
yield* sql`INSERT INTO sessions (chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at) VALUES (${input.chatId}, ${input.project}, ${input.cwd}, ${input.agentKind}, ${input.agentType ?? null}, ${input.parentSessionId ?? null}, ${boolToInt(input.triageWasNonEmpty) ?? 0}, ${input.startedAt})`;
|
|
347
|
+
yield* sql`INSERT INTO sessions (chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, conversation_id) VALUES (${input.chatId}, ${input.project}, ${input.cwd}, ${input.agentKind}, ${input.agentType ?? null}, ${input.parentSessionId ?? null}, ${boolToInt(input.triageWasNonEmpty) ?? 0}, ${input.startedAt}, ${input.conversationId ?? null})`;
|
|
315
348
|
return (yield* sql`SELECT id FROM sessions WHERE chat_id = ${input.chatId}`)[0].id;
|
|
316
349
|
}).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
|
|
317
350
|
operation: "write",
|
|
@@ -320,13 +353,24 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
|
320
353
|
})));
|
|
321
354
|
const upsertSession = (input) => Effect.gen(function* () {
|
|
322
355
|
yield* Effect.logDebug("upsertSession").pipe(Effect.annotateLogs({ chatId: input.chatId }));
|
|
323
|
-
yield* sql`INSERT INTO sessions (chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at) VALUES (${input.chatId}, ${input.project}, ${input.cwd}, ${input.agentKind}, ${input.agentType ?? null}, ${input.parentSessionId ?? null}, ${boolToInt(input.triageWasNonEmpty) ?? 0}, ${input.startedAt}) ON CONFLICT(chat_id) DO NOTHING`;
|
|
356
|
+
yield* sql`INSERT INTO sessions (chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, conversation_id) VALUES (${input.chatId}, ${input.project}, ${input.cwd}, ${input.agentKind}, ${input.agentType ?? null}, ${input.parentSessionId ?? null}, ${boolToInt(input.triageWasNonEmpty) ?? 0}, ${input.startedAt}, ${input.conversationId ?? null}) ON CONFLICT(chat_id) DO NOTHING`;
|
|
324
357
|
return (yield* sql`SELECT id FROM sessions WHERE chat_id = ${input.chatId}`)[0].id;
|
|
325
358
|
}).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
|
|
326
359
|
operation: "write",
|
|
327
360
|
table: "sessions",
|
|
328
361
|
reason: extractSqlReason(e)
|
|
329
362
|
})));
|
|
363
|
+
const setSessionConversationIdIfNull = (input) => Effect.gen(function* () {
|
|
364
|
+
yield* Effect.logDebug("setSessionConversationIdIfNull").pipe(Effect.annotateLogs(input));
|
|
365
|
+
yield* sql`
|
|
366
|
+
UPDATE sessions SET conversation_id = ${input.conversationId}
|
|
367
|
+
WHERE id = ${input.sessionId} AND conversation_id IS NULL
|
|
368
|
+
`;
|
|
369
|
+
}).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
|
|
370
|
+
operation: "write",
|
|
371
|
+
table: "sessions",
|
|
372
|
+
reason: extractSqlReason(e)
|
|
373
|
+
})));
|
|
330
374
|
const writeTurn = (input) => sql.withTransaction(Effect.gen(function* () {
|
|
331
375
|
yield* Effect.logDebug("writeTurn").pipe(Effect.annotateLogs({
|
|
332
376
|
sessionId: input.sessionId,
|
|
@@ -875,7 +919,7 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
|
875
919
|
return (yield* sql`
|
|
876
920
|
INSERT INTO tdd_artifacts
|
|
877
921
|
(phase_id, artifact_kind, file_id, test_case_id, test_run_id,
|
|
878
|
-
test_first_failure_run_id, diff_excerpt, recorded_at)
|
|
922
|
+
test_first_failure_run_id, diff_excerpt, recorded_at, suite)
|
|
879
923
|
VALUES
|
|
880
924
|
(
|
|
881
925
|
${input.phaseId},
|
|
@@ -885,7 +929,8 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
|
885
929
|
${input.testRunId ?? null},
|
|
886
930
|
${input.testFirstFailureRunId ?? null},
|
|
887
931
|
${truncatedDiff},
|
|
888
|
-
${input.recordedAt}
|
|
932
|
+
${input.recordedAt},
|
|
933
|
+
${input.suite ?? "vitest"}
|
|
889
934
|
)
|
|
890
935
|
RETURNING id
|
|
891
936
|
`)[0].id;
|
|
@@ -1151,6 +1196,8 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
|
1151
1196
|
writeCoverage,
|
|
1152
1197
|
writeHistory,
|
|
1153
1198
|
writeBaselines,
|
|
1199
|
+
writeThresholds,
|
|
1200
|
+
writeTargets,
|
|
1154
1201
|
writeTrends,
|
|
1155
1202
|
writeSourceMap,
|
|
1156
1203
|
writeNote,
|
|
@@ -1158,6 +1205,7 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
|
1158
1205
|
deleteNote,
|
|
1159
1206
|
writeSession,
|
|
1160
1207
|
upsertSession,
|
|
1208
|
+
setSessionConversationIdIfNull,
|
|
1161
1209
|
writeTurn,
|
|
1162
1210
|
writeFailureSignature,
|
|
1163
1211
|
endSession,
|
|
@@ -340,13 +340,16 @@ const migration = Effect.gen(function* () {
|
|
|
340
340
|
CREATE TABLE coverage_baselines (
|
|
341
341
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
342
342
|
project TEXT NOT NULL,
|
|
343
|
+
kind TEXT NOT NULL DEFAULT 'baseline' CHECK (kind IN (
|
|
344
|
+
'baseline', 'threshold', 'target'
|
|
345
|
+
)),
|
|
343
346
|
metric TEXT NOT NULL CHECK (metric IN (
|
|
344
347
|
'lines', 'functions', 'branches', 'statements'
|
|
345
348
|
)),
|
|
346
349
|
value REAL NOT NULL,
|
|
347
350
|
pattern TEXT NOT NULL DEFAULT '',
|
|
348
351
|
updated_at TEXT NOT NULL,
|
|
349
|
-
UNIQUE(project, metric, pattern)
|
|
352
|
+
UNIQUE(project, kind, metric, pattern)
|
|
350
353
|
)
|
|
351
354
|
`;
|
|
352
355
|
yield* sql`
|
|
@@ -691,6 +694,7 @@ const migration = Effect.gen(function* () {
|
|
|
691
694
|
test_first_failure_run_id INTEGER REFERENCES test_runs(id) ON DELETE SET NULL,
|
|
692
695
|
diff_excerpt TEXT,
|
|
693
696
|
recorded_at TEXT NOT NULL,
|
|
697
|
+
suite TEXT NOT NULL DEFAULT 'vitest' CHECK (suite IN ('vitest', 'bats')),
|
|
694
698
|
UNIQUE (phase_id, artifact_kind, file_id, test_run_id)
|
|
695
699
|
)
|
|
696
700
|
`;
|
|
@@ -764,7 +768,7 @@ const migration = Effect.gen(function* () {
|
|
|
764
768
|
CREATE TRIGGER trg_sessions_conv_id_immutable
|
|
765
769
|
AFTER UPDATE ON sessions
|
|
766
770
|
FOR EACH ROW
|
|
767
|
-
WHEN OLD.conversation_id IS NOT NEW.conversation_id
|
|
771
|
+
WHEN OLD.conversation_id IS NOT NULL AND OLD.conversation_id IS NOT NEW.conversation_id
|
|
768
772
|
BEGIN
|
|
769
773
|
SELECT RAISE(ABORT, 'sessions.conversation_id is immutable');
|
|
770
774
|
END
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitest-agent/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared Effect Schema, SQLite migrations, data layer, services, formatters, and utilities for vitest-agent packages.",
|
|
6
6
|
"keywords": [
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@effected/jsonc": "^0.8.1",
|
|
52
52
|
"@effected/toml": "^0.5.0",
|
|
53
53
|
"@effected/walker": "^0.5.0",
|
|
54
|
-
"@effected/workspaces": "^0.
|
|
54
|
+
"@effected/workspaces": "^0.19.0",
|
|
55
55
|
"@effected/xdg": "^0.3.0",
|
|
56
56
|
"@effected/yaml": "^0.12.0",
|
|
57
57
|
"acorn": "^8.18.0",
|
package/schemas/ConsoleLeaks.js
CHANGED
|
@@ -14,15 +14,32 @@ const ConsoleLeakFile = Schema.Struct({
|
|
|
14
14
|
sample: Schema.optional(Schema.String)
|
|
15
15
|
}).annotate({ identifier: "ConsoleLeakFile" });
|
|
16
16
|
/**
|
|
17
|
+
* Summary of console output logged inside tests that did NOT pass. Kept
|
|
18
|
+
* separate from `total`/`byFile` so it can never be mistaken for a leak in a
|
|
19
|
+
* passing test — assertion-failure logging that routes through `console.*`
|
|
20
|
+
* (common with logger-backed assertion libraries) would otherwise camouflage
|
|
21
|
+
* genuine leaks on every red run.
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
const ConsoleLeaksFromFailingTests = Schema.Struct({
|
|
25
|
+
total: Schema.Number,
|
|
26
|
+
files: Schema.Number
|
|
27
|
+
}).annotate({ identifier: "ConsoleLeaksFromFailingTests" });
|
|
28
|
+
/**
|
|
17
29
|
* Aggregate stray-console-output signal for a single test run. Omitted from
|
|
18
|
-
* a report entirely when the run produced no user console output
|
|
30
|
+
* a report entirely when the run produced no user console output at all
|
|
31
|
+
* (neither non-failing nor failing-test writes). `total`/`byFile` count only
|
|
32
|
+
* output from tests that did not fail; `fromFailingTests`, when present,
|
|
33
|
+
* summarizes the output Vitest captured from failing tests so it stays
|
|
34
|
+
* visible without polluting the actionable leak signal (issue #263).
|
|
19
35
|
* @public
|
|
20
36
|
*/
|
|
21
37
|
const ConsoleLeaks = Schema.Struct({
|
|
22
38
|
total: Schema.Number,
|
|
23
39
|
byFile: Schema.Array(ConsoleLeakFile),
|
|
24
|
-
truncated: Schema.optional(Schema.Boolean)
|
|
40
|
+
truncated: Schema.optional(Schema.Boolean),
|
|
41
|
+
fromFailingTests: Schema.optional(ConsoleLeaksFromFailingTests)
|
|
25
42
|
}).annotate({ identifier: "ConsoleLeaks" });
|
|
26
43
|
|
|
27
44
|
//#endregion
|
|
28
|
-
export { ConsoleLeakFile, ConsoleLeaks };
|
|
45
|
+
export { ConsoleLeakFile, ConsoleLeaks, ConsoleLeaksFromFailingTests };
|
package/schemas/Coverage.js
CHANGED
|
@@ -41,6 +41,7 @@ const CoverageReport = Schema.Struct({
|
|
|
41
41
|
})),
|
|
42
42
|
scoped: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(Effect.succeed(false))),
|
|
43
43
|
scopedFiles: Schema.optional(Schema.Array(Schema.String)),
|
|
44
|
+
totalFiles: Schema.optional(Schema.Number),
|
|
44
45
|
lowCoverage: Schema.Array(FileCoverageReport),
|
|
45
46
|
lowCoverageFiles: Schema.Array(Schema.String),
|
|
46
47
|
belowTarget: Schema.optional(Schema.Array(FileCoverageReport)),
|
package/schemas/RenderState.js
CHANGED
|
@@ -76,7 +76,10 @@ const CoverageRenderState = Schema.Struct({
|
|
|
76
76
|
metric: CoverageMetric,
|
|
77
77
|
expected: Schema.Number,
|
|
78
78
|
actual: Schema.Number
|
|
79
|
-
}))
|
|
79
|
+
})),
|
|
80
|
+
scoped: Schema.optional(Schema.Boolean),
|
|
81
|
+
scopedFiles: Schema.optional(Schema.Number),
|
|
82
|
+
totalFiles: Schema.optional(Schema.Number)
|
|
80
83
|
}).annotate({ identifier: "CoverageRenderState" });
|
|
81
84
|
/**
|
|
82
85
|
* A `SuggestedAction` event, denormalized into the queue the renderer reads.
|
|
@@ -145,7 +148,14 @@ const RenderState = Schema.Struct({
|
|
|
145
148
|
* that never carried the field (older replay data, hand-built
|
|
146
149
|
* fixtures) falls back to `moduleOrder.length`.
|
|
147
150
|
*/
|
|
148
|
-
collectedModules: Schema.optional(Schema.Number)
|
|
151
|
+
collectedModules: Schema.optional(Schema.Number),
|
|
152
|
+
/**
|
|
153
|
+
* Process-level errors with no owning module, folded from
|
|
154
|
+
* `RunFinished.unhandledErrors`. A run that never carried the field
|
|
155
|
+
* (older replay data, hand-built fixtures) defaults to `[]`. See
|
|
156
|
+
* issue #240.
|
|
157
|
+
*/
|
|
158
|
+
unhandledErrors: Schema.Array(ReportError)
|
|
149
159
|
}).annotate({ identifier: "RenderState" });
|
|
150
160
|
/**
|
|
151
161
|
* Initial state — what the reducer returns when no events have been
|
|
@@ -171,7 +181,8 @@ const initialRenderState = {
|
|
|
171
181
|
coverage: null,
|
|
172
182
|
trend: null,
|
|
173
183
|
failures: [],
|
|
174
|
-
suggestedActions: []
|
|
184
|
+
suggestedActions: [],
|
|
185
|
+
unhandledErrors: []
|
|
175
186
|
};
|
|
176
187
|
|
|
177
188
|
//#endregion
|
package/schemas/RunEvent.js
CHANGED
|
@@ -169,7 +169,10 @@ const RunEvent = Schema.Union([
|
|
|
169
169
|
Schema.TaggedStruct("CoverageReady", {
|
|
170
170
|
metrics: CoverageTotals,
|
|
171
171
|
thresholds: MetricThresholds,
|
|
172
|
-
gaps: Schema.Array(CoverageGap)
|
|
172
|
+
gaps: Schema.Array(CoverageGap),
|
|
173
|
+
scoped: Schema.optional(Schema.Boolean),
|
|
174
|
+
scopedFiles: Schema.optional(Schema.Number),
|
|
175
|
+
totalFiles: Schema.optional(Schema.Number)
|
|
173
176
|
}),
|
|
174
177
|
Schema.TaggedStruct("ThresholdViolation", {
|
|
175
178
|
metric: CoverageMetric,
|
|
@@ -202,7 +205,14 @@ const RunEvent = Schema.Union([
|
|
|
202
205
|
* true count into `RenderState` even when `moduleOrder` only
|
|
203
206
|
* tracks failing modules (report replay). See issue #204.
|
|
204
207
|
*/
|
|
205
|
-
collectedModules: Schema.optional(Schema.Number)
|
|
208
|
+
collectedModules: Schema.optional(Schema.Number),
|
|
209
|
+
/**
|
|
210
|
+
* Process-level errors with no owning module (an unhandled
|
|
211
|
+
* rejection, a worker crash) — Vitest's `unhandledErrors`
|
|
212
|
+
* argument to `onTestRunEnd`. Optional so hand-built fixtures
|
|
213
|
+
* and older replay data keep decoding. See issue #240.
|
|
214
|
+
*/
|
|
215
|
+
unhandledErrors: Schema.optional(Schema.Array(ReportError))
|
|
206
216
|
})
|
|
207
217
|
]).annotate({ identifier: "RunEvent" });
|
|
208
218
|
|