@vitest-agent/sdk 2.4.13 → 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 +222 -4
- 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/validate-phase-transition.js +20 -6
package/testing.d.ts
CHANGED
|
@@ -132,6 +132,7 @@ declare const AgentReport: Schema.Struct<{
|
|
|
132
132
|
}>>;
|
|
133
133
|
readonly scoped: Schema.withDecodingDefaultKey<Schema.Boolean, never>;
|
|
134
134
|
readonly scopedFiles: Schema.optional<Schema.$Array<Schema.String>>;
|
|
135
|
+
readonly totalFiles: Schema.optional<Schema.Number>;
|
|
135
136
|
readonly lowCoverage: Schema.$Array<Schema.Struct<{
|
|
136
137
|
readonly file: Schema.String;
|
|
137
138
|
readonly summary: Schema.Struct<{
|
|
@@ -170,6 +171,10 @@ declare const AgentReport: Schema.Struct<{
|
|
|
170
171
|
readonly sample: Schema.optional<Schema.String>;
|
|
171
172
|
}>>;
|
|
172
173
|
readonly truncated: Schema.optional<Schema.Boolean>;
|
|
174
|
+
readonly fromFailingTests: Schema.optional<Schema.Struct<{
|
|
175
|
+
readonly total: Schema.Number;
|
|
176
|
+
readonly files: Schema.Number;
|
|
177
|
+
}>>;
|
|
173
178
|
}>>;
|
|
174
179
|
}>;
|
|
175
180
|
/** @public */
|
|
@@ -289,6 +294,7 @@ declare const CoverageReport: Schema.Struct<{
|
|
|
289
294
|
}>>;
|
|
290
295
|
readonly scoped: Schema.withDecodingDefaultKey<Schema.Boolean, never>;
|
|
291
296
|
readonly scopedFiles: Schema.optional<Schema.$Array<Schema.String>>;
|
|
297
|
+
readonly totalFiles: Schema.optional<Schema.Number>;
|
|
292
298
|
readonly lowCoverage: Schema.$Array<Schema.Struct<{
|
|
293
299
|
readonly file: Schema.String;
|
|
294
300
|
readonly summary: Schema.Struct<{
|
|
@@ -469,9 +475,28 @@ type TrendRecord = typeof TrendRecord.Type;
|
|
|
469
475
|
type Phase = "spike" | "red" | "red.triangulate" | "green" | "green.fake-it" | "refactor" | "extended-red" | "green-without-red";
|
|
470
476
|
/** @public */
|
|
471
477
|
type ArtifactKind = "test_written" | "test_failed_run" | "code_written" | "test_passed_run" | "refactor" | "test_weakened";
|
|
478
|
+
/**
|
|
479
|
+
* Explicit test-runner marker on a `tdd_artifacts` row (issue #363). `vitest`
|
|
480
|
+
* artifacts always carry a `test_case_id` when they anchor a specific test;
|
|
481
|
+
* `bats` artifacts never do (there is no `test_cases` row for a bats test),
|
|
482
|
+
* so the D2 binding-rule validator branches on this to accept a bats
|
|
483
|
+
* run-level artifact without weakening the vitest guard.
|
|
484
|
+
* @public
|
|
485
|
+
*/
|
|
486
|
+
type ArtifactSuite = "vitest" | "bats";
|
|
472
487
|
/** @public */
|
|
473
488
|
interface CitedArtifact {
|
|
474
489
|
readonly id: number;
|
|
490
|
+
/**
|
|
491
|
+
* `tdd_phases.id` of the phase this artifact was actually recorded in — the
|
|
492
|
+
* artifact's own phase binding. This is the honest anchor for D2 binding
|
|
493
|
+
* rule 1's window check (issue #245): a test_case's first-ever creation
|
|
494
|
+
* turn can predate the current phase (e.g. authored during spike, then
|
|
495
|
+
* re-run inside red) without the evidence itself being stale, so the
|
|
496
|
+
* window check compares this against `PhaseTransitionContext.current_phase_id`
|
|
497
|
+
* rather than `test_case_created_turn_at`.
|
|
498
|
+
*/
|
|
499
|
+
readonly phase_id: number;
|
|
475
500
|
readonly artifact_kind: ArtifactKind;
|
|
476
501
|
readonly test_case_id: number | null;
|
|
477
502
|
readonly test_case_created_turn_at: string | null;
|
|
@@ -479,6 +504,12 @@ interface CitedArtifact {
|
|
|
479
504
|
readonly test_run_id: number | null;
|
|
480
505
|
readonly test_first_failure_run_id: number | null;
|
|
481
506
|
readonly behavior_id: number | null;
|
|
507
|
+
/**
|
|
508
|
+
* Issue #363: explicit suite marker distinguishing vitest from bats runs.
|
|
509
|
+
* Optional on the pure type so hand-built contexts default to `vitest`;
|
|
510
|
+
* the DB row always carries the persisted value.
|
|
511
|
+
*/
|
|
512
|
+
readonly suite?: ArtifactSuite;
|
|
482
513
|
}
|
|
483
514
|
//#endregion
|
|
484
515
|
//#region src/schemas/Identity.d.ts
|
|
@@ -655,6 +686,29 @@ declare class IdempotencyHit extends IdempotencyHit_base<{
|
|
|
655
686
|
readonly existingAgentId: typeof AgentId.Type;
|
|
656
687
|
}> {}
|
|
657
688
|
//#endregion
|
|
689
|
+
//#region src/schemas/Thresholds.d.ts
|
|
690
|
+
/**
|
|
691
|
+
* Fully resolved thresholds ready for evaluation.
|
|
692
|
+
* @public
|
|
693
|
+
*/
|
|
694
|
+
declare const ResolvedThresholds: Schema.Struct<{
|
|
695
|
+
readonly global: Schema.Struct<{
|
|
696
|
+
readonly lines: Schema.optional<Schema.Number>;
|
|
697
|
+
readonly functions: Schema.optional<Schema.Number>;
|
|
698
|
+
readonly branches: Schema.optional<Schema.Number>;
|
|
699
|
+
readonly statements: Schema.optional<Schema.Number>;
|
|
700
|
+
}>;
|
|
701
|
+
readonly perFile: Schema.withDecodingDefaultKey<Schema.Boolean, never>;
|
|
702
|
+
readonly patterns: Schema.withDecodingDefaultKey<Schema.$Array<Schema.Tuple<readonly [Schema.String, Schema.Struct<{
|
|
703
|
+
readonly lines: Schema.optional<Schema.Number>;
|
|
704
|
+
readonly functions: Schema.optional<Schema.Number>;
|
|
705
|
+
readonly branches: Schema.optional<Schema.Number>;
|
|
706
|
+
readonly statements: Schema.optional<Schema.Number>;
|
|
707
|
+
}>]>>, never>;
|
|
708
|
+
}>;
|
|
709
|
+
/** @public */
|
|
710
|
+
type ResolvedThresholds = typeof ResolvedThresholds.Type;
|
|
711
|
+
//#endregion
|
|
658
712
|
//#region src/services/DataStore.d.ts
|
|
659
713
|
/** @public */
|
|
660
714
|
interface CreateGoalInput {
|
|
@@ -879,6 +933,13 @@ interface SessionInput {
|
|
|
879
933
|
readonly parentSessionId?: number;
|
|
880
934
|
readonly triageWasNonEmpty?: boolean;
|
|
881
935
|
readonly startedAt: string;
|
|
936
|
+
/**
|
|
937
|
+
* Canonical conversation UUID this session belongs to (issue #144).
|
|
938
|
+
* Set once at INSERT time — `sessions.conversation_id` is immutable
|
|
939
|
+
* thereafter (see the `trg_sessions_conv_id_immutable` trigger), so
|
|
940
|
+
* omit it here rather than trying to backfill via an update.
|
|
941
|
+
*/
|
|
942
|
+
readonly conversationId?: string;
|
|
882
943
|
}
|
|
883
944
|
/** @public */
|
|
884
945
|
interface TurnInput {
|
|
@@ -956,6 +1017,8 @@ interface WriteTddArtifactInput {
|
|
|
956
1017
|
readonly testFirstFailureRunId?: number;
|
|
957
1018
|
readonly diffExcerpt?: string;
|
|
958
1019
|
readonly recordedAt: string;
|
|
1020
|
+
/** Issue #363: explicit suite marker. Defaults to `"vitest"` when omitted. */
|
|
1021
|
+
readonly suite?: ArtifactSuite;
|
|
959
1022
|
}
|
|
960
1023
|
/** @public */
|
|
961
1024
|
interface WriteCommitInput {
|
|
@@ -1030,6 +1093,18 @@ declare const DataStore_base: Context.ServiceClass<DataStore, "vitest-agent/Data
|
|
|
1030
1093
|
readonly writeCoverage: (runId: number, coverage: ReadonlyArray<FileCoverageInput>) => Effect.Effect<void, DataStoreError>;
|
|
1031
1094
|
readonly writeHistory: (project: string, fullName: string, modulePath: string, runId: number, timestamp: string, state: string, duration: number | null, flaky: boolean, retryCount: number, errorMessage: string | null) => Effect.Effect<void, DataStoreError>;
|
|
1032
1095
|
readonly writeBaselines: (baselines: CoverageBaselines) => Effect.Effect<void, DataStoreError>;
|
|
1096
|
+
/**
|
|
1097
|
+
* Persist the resolved (enforced) Vitest `coverage.thresholds` for the
|
|
1098
|
+
* current run, distinct from the ratcheted `coverage_baselines` rows.
|
|
1099
|
+
* Stored under `kind='threshold'`. See issue #237.
|
|
1100
|
+
*/
|
|
1101
|
+
readonly writeThresholds: (thresholds: ResolvedThresholds) => Effect.Effect<void, DataStoreError>;
|
|
1102
|
+
/**
|
|
1103
|
+
* Persist the resolved aspirational `coverageTargets` for the current
|
|
1104
|
+
* run, distinct from the ratcheted `coverage_baselines` rows. Stored
|
|
1105
|
+
* under `kind='target'`. See issue #237.
|
|
1106
|
+
*/
|
|
1107
|
+
readonly writeTargets: (targets: ResolvedThresholds) => Effect.Effect<void, DataStoreError>;
|
|
1033
1108
|
readonly writeTrends: (project: string, runId: number, entry: TrendEntry) => Effect.Effect<void, DataStoreError>;
|
|
1034
1109
|
readonly writeSourceMap: (sourceFilePath: string, testModuleId: number, mappingType: string) => Effect.Effect<void, DataStoreError>;
|
|
1035
1110
|
readonly ensureFile: (filePath: string) => Effect.Effect<number, DataStoreError>;
|
|
@@ -1048,6 +1123,20 @@ declare const DataStore_base: Context.ServiceClass<DataStore, "vitest-agent/Data
|
|
|
1048
1123
|
* `INSERT ... ON CONFLICT DO NOTHING`.
|
|
1049
1124
|
*/
|
|
1050
1125
|
readonly upsertSession: (input: SessionInput) => Effect.Effect<number, DataStoreError>;
|
|
1126
|
+
/**
|
|
1127
|
+
* Backfill `sessions.conversation_id` for a row that was inserted
|
|
1128
|
+
* before the canonical conversation id was known (issue #144) —
|
|
1129
|
+
* `record session-start` always precedes `register-agent`, so the
|
|
1130
|
+
* session row exists first. Race-safe and idempotent: the UPDATE's
|
|
1131
|
+
* `WHERE conversation_id IS NULL` guard means a session that
|
|
1132
|
+
* already carries a value is left untouched (zero rows affected),
|
|
1133
|
+
* matching the trigger's null→value-only relaxation rather than
|
|
1134
|
+
* risking an immutability-violation error on a redundant call.
|
|
1135
|
+
*/
|
|
1136
|
+
readonly setSessionConversationIdIfNull: (input: {
|
|
1137
|
+
readonly sessionId: number;
|
|
1138
|
+
readonly conversationId: string;
|
|
1139
|
+
}) => Effect.Effect<void, DataStoreError>;
|
|
1051
1140
|
readonly writeTurn: (input: TurnInput) => Effect.Effect<number, DataStoreError>;
|
|
1052
1141
|
readonly writeFailureSignature: (input: FailureSignatureWriteInput) => Effect.Effect<void, DataStoreError>;
|
|
1053
1142
|
readonly endSession: (chatId: string, endedAt: string, endReason: string | null) => Effect.Effect<void, DataStoreError>;
|
|
@@ -1225,6 +1314,8 @@ interface SessionDetail {
|
|
|
1225
1314
|
readonly startedAt: string;
|
|
1226
1315
|
readonly endedAt: string | null;
|
|
1227
1316
|
readonly endReason: string | null;
|
|
1317
|
+
/** Canonical conversation UUID this session belongs to (issue #144), null when unset. */
|
|
1318
|
+
readonly conversationId: string | null;
|
|
1228
1319
|
}
|
|
1229
1320
|
/** @public */
|
|
1230
1321
|
interface TurnSummary {
|
|
@@ -1406,6 +1497,7 @@ interface TddArtifactRow {
|
|
|
1406
1497
|
readonly testRunId: number | null;
|
|
1407
1498
|
readonly testFirstFailureRunId: number | null;
|
|
1408
1499
|
readonly recordedAt: string;
|
|
1500
|
+
readonly suite: ArtifactSuite;
|
|
1409
1501
|
}
|
|
1410
1502
|
/** @public */
|
|
1411
1503
|
interface TagInventoryRow {
|
|
@@ -1519,7 +1611,32 @@ declare const DataReader_base: Context.ServiceClass<DataReader, "vitest-agent/Da
|
|
|
1519
1611
|
* preserves the prior single-session contract.
|
|
1520
1612
|
*/
|
|
1521
1613
|
readonly walkParents?: boolean;
|
|
1614
|
+
/**
|
|
1615
|
+
* When true AND `sessionId`'s session has a non-null
|
|
1616
|
+
* `conversation_id`, also return tdd_tasks belonging to
|
|
1617
|
+
* any OTHER session sharing that `conversation_id` — the
|
|
1618
|
+
* detached-session fallback (issue #144). A session with
|
|
1619
|
+
* a null `conversation_id` never triggers this fallback.
|
|
1620
|
+
* Rows are ordered so a task owned by an `agent_kind =
|
|
1621
|
+
* 'main'` session sorts first, then by `started_at DESC`.
|
|
1622
|
+
* Default `false` preserves the prior contract.
|
|
1623
|
+
*/
|
|
1624
|
+
readonly walkConversation?: boolean;
|
|
1522
1625
|
}) => Effect.Effect<ReadonlyArray<TddTaskSummary>, DataStoreError>;
|
|
1626
|
+
/**
|
|
1627
|
+
* Diagnostic for the `missing_artifact_evidence` phase-transition
|
|
1628
|
+
* denial (issue #144): counts `tdd_artifacts` rows recorded at or
|
|
1629
|
+
* after `sinceIso` under sessions OTHER than the one that opened
|
|
1630
|
+
* `tddTaskId`, but sharing that session's `conversation_id`. Used
|
|
1631
|
+
* to tell an agent "your hooks may be attributing to a detached
|
|
1632
|
+
* session" rather than a bare "no artifact found". Returns 0 when
|
|
1633
|
+
* `tddTaskId` is unknown or its session's `conversation_id` is
|
|
1634
|
+
* null — the diagnostic never fabricates a signal it cannot back.
|
|
1635
|
+
*/
|
|
1636
|
+
readonly countRecentArtifactsInOtherSessionsOfConversation: (input: {
|
|
1637
|
+
readonly tddTaskId: number;
|
|
1638
|
+
readonly sinceIso: string;
|
|
1639
|
+
}) => Effect.Effect<number, DataStoreError>;
|
|
1523
1640
|
/**
|
|
1524
1641
|
* List artifacts recorded for a TDD task, optionally filtered
|
|
1525
1642
|
* by `artifactKind`, `phaseId`, or `behaviorId`. Returns rows in
|
|
@@ -1578,5 +1695,5 @@ declare function flaky(filename: string): Layer.Layer<DataReader | DataStore | i
|
|
|
1578
1695
|
/** @public */
|
|
1579
1696
|
declare function withTddTask(filename: string): Layer.Layer<DataReader | DataStore | import("effect/unstable/sql/SqlClient").SqlClient | import("@effect/sql-sqlite-node/SqliteClient").SqliteClient | import("@effect/platform-node/NodeServices").NodeServices, BehaviorNotFoundError | DataStoreError | GoalNotFoundError | IllegalStatusTransitionError | import("@effect/sql-sqlite-node/SqliteMigrator").MigrationError | import("effect/unstable/sql/SqlError").SqlError | TddTaskAlreadyEndedError | TddTaskNotFoundError, never>;
|
|
1580
1697
|
//#endregion
|
|
1581
|
-
export { type AcceptanceMetrics, Agent, AgentId, AgentNotFoundError, AgentReport, type ArtifactKind, type AssociateRunSessionInput, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorStatus, CacheManifest, type ChangeKind, ChatId, type CitedArtifact, type CitedArtifactRow, type ClassificationQueryOptions, type CommitChangesEntry, CoverageBaselines, CoverageReport, type CreateBehaviorInput, type CreateGoalInput, type CurrentTddPhase, DataReader, DataStore, DataStoreError, DataStoreTestLayer, type EndTddTaskInput, type FailureSignatureDetail, type FailureSignatureWriteInput, type FileCoverageInput, FileCoverageReport, type FlakyTest, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, type HistoryQueryOptions, HistoryRecord, type HypothesisDetail, type HypothesisInput, IdempotencyHit, type IdempotentResponseInput, type IllegalStatusTransitionEntity, IllegalStatusTransitionError, type ModuleInput, type ModuleListEntry, type NoteInput, type NoteRow, type PersistentFailure, type Phase, type ProjectRunSummary, type RegisterAgentInput, RegistrationConflictError, type RunChangedFile, type RunInvocationMethod, type SessionDetail, type SessionInput, type SettingsInput, type SettingsListEntry, type SettingsRow, type StackFrameInput, type SuiteInput, type SuiteListEntry, type TagInventoryRow, type TddArtifactDetail, type TddArtifactRow, type TddPhaseDetail, TddTaskAlreadyEndedError, type TddTaskDetail, type TddTaskEndOutcome, type TddTaskInput, TddTaskNotFoundError, type TddTaskSummary, type TestCaseInput, type TestError, type TestErrorInput, type TestListEntry, type TestLookupOptions, type TestRunInput, TrendEntry, TrendRecord, type TurnInput, type TurnSearchOptions, type TurnSummary, type UpdateBehaviorInput, type UpdateGoalInput, type ValidateHypothesisInput, type WriteCommitInput, type WriteRunChangedFilesInput, type WriteTddArtifactInput, type WriteTddPhaseInput, type WriteTddPhaseOutput, empty, flaky, makeTestLayer, singlePassingRun, withFailures, withTddTask };
|
|
1698
|
+
export { type AcceptanceMetrics, Agent, AgentId, AgentNotFoundError, AgentReport, type ArtifactKind, type ArtifactSuite, type AssociateRunSessionInput, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorStatus, CacheManifest, type ChangeKind, ChatId, type CitedArtifact, type CitedArtifactRow, type ClassificationQueryOptions, type CommitChangesEntry, CoverageBaselines, CoverageReport, type CreateBehaviorInput, type CreateGoalInput, type CurrentTddPhase, DataReader, DataStore, DataStoreError, DataStoreTestLayer, type EndTddTaskInput, type FailureSignatureDetail, type FailureSignatureWriteInput, type FileCoverageInput, FileCoverageReport, type FlakyTest, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, type HistoryQueryOptions, HistoryRecord, type HypothesisDetail, type HypothesisInput, IdempotencyHit, type IdempotentResponseInput, type IllegalStatusTransitionEntity, IllegalStatusTransitionError, type ModuleInput, type ModuleListEntry, type NoteInput, type NoteRow, type PersistentFailure, type Phase, type ProjectRunSummary, type RegisterAgentInput, RegistrationConflictError, type ResolvedThresholds, type RunChangedFile, type RunInvocationMethod, type SessionDetail, type SessionInput, type SettingsInput, type SettingsListEntry, type SettingsRow, type StackFrameInput, type SuiteInput, type SuiteListEntry, type TagInventoryRow, type TddArtifactDetail, type TddArtifactRow, type TddPhaseDetail, TddTaskAlreadyEndedError, type TddTaskDetail, type TddTaskEndOutcome, type TddTaskInput, TddTaskNotFoundError, type TddTaskSummary, type TestCaseInput, type TestError, type TestErrorInput, type TestListEntry, type TestLookupOptions, type TestRunInput, TrendEntry, TrendRecord, type TurnInput, type TurnSearchOptions, type TurnSummary, type UpdateBehaviorInput, type UpdateGoalInput, type ValidateHypothesisInput, type WriteCommitInput, type WriteRunChangedFilesInput, type WriteTddArtifactInput, type WriteTddPhaseInput, type WriteTddPhaseOutput, empty, flaky, makeTestLayer, singlePassingRun, withFailures, withTddTask };
|
|
1582
1699
|
//# sourceMappingURL=testing.d.ts.map
|
package/utils/console-leaks.js
CHANGED
|
@@ -10,14 +10,28 @@ function truncateSample(content) {
|
|
|
10
10
|
* Aggregate raw console-leak entries into a ConsoleLeaks signal:
|
|
11
11
|
* bucket by file, split stdout/stderr, collect attributable test names,
|
|
12
12
|
* capture one truncated sample per file, sort by total writes descending,
|
|
13
|
-
* and cap the file list.
|
|
14
|
-
*
|
|
13
|
+
* and cap the file list. Entries logged inside a failing test (with
|
|
14
|
+
* `failed` set) are excluded from `total`/`byFile` — the actionable leak signal —
|
|
15
|
+
* and summarized instead in `fromFailingTests` (issue #263: a logger-backed
|
|
16
|
+
* assertion failure otherwise makes every red run look like a leak). Returns
|
|
17
|
+
* `undefined` only when there is no output at all, non-failing or failing.
|
|
15
18
|
* @public
|
|
16
19
|
*/
|
|
17
20
|
function buildConsoleLeaks(entries) {
|
|
18
21
|
if (entries.length === 0) return void 0;
|
|
22
|
+
const nonFailing = entries.filter((e) => e.failed !== true);
|
|
23
|
+
const failing = entries.filter((e) => e.failed === true);
|
|
24
|
+
const fromFailingTests = failing.length > 0 ? {
|
|
25
|
+
total: failing.length,
|
|
26
|
+
files: new Set(failing.map((e) => e.file)).size
|
|
27
|
+
} : void 0;
|
|
28
|
+
if (nonFailing.length === 0) return {
|
|
29
|
+
total: 0,
|
|
30
|
+
byFile: [],
|
|
31
|
+
...fromFailingTests !== void 0 ? { fromFailingTests } : {}
|
|
32
|
+
};
|
|
19
33
|
const byFile = /* @__PURE__ */ new Map();
|
|
20
|
-
for (const e of
|
|
34
|
+
for (const e of nonFailing) {
|
|
21
35
|
let acc = byFile.get(e.file);
|
|
22
36
|
if (acc === void 0) {
|
|
23
37
|
acc = {
|
|
@@ -46,30 +60,43 @@ function buildConsoleLeaks(entries) {
|
|
|
46
60
|
files.sort((a, b) => b.stdout + b.stderr - (a.stdout + a.stderr));
|
|
47
61
|
const truncated = files.length > MAX_FILES;
|
|
48
62
|
return {
|
|
49
|
-
total:
|
|
63
|
+
total: nonFailing.length,
|
|
50
64
|
byFile: truncated ? files.slice(0, MAX_FILES) : files,
|
|
51
|
-
...truncated ? { truncated: true } : {}
|
|
65
|
+
...truncated ? { truncated: true } : {},
|
|
66
|
+
...fromFailingTests !== void 0 ? { fromFailingTests } : {}
|
|
52
67
|
};
|
|
53
68
|
}
|
|
54
69
|
/**
|
|
55
70
|
* Walk a Vitest `File[]` task tree (from `vitest.state.getFiles()`) into flat
|
|
56
71
|
* {@link ConsoleLeakEntry} values. Each task `log` becomes one entry attributed
|
|
57
72
|
* to its enclosing file and, when the log sits on a test task, that test's name.
|
|
73
|
+
* An entry is marked `failed: true` when it was logged inside a test whose own
|
|
74
|
+
* `result.state` is `"fail"`, or — for output with no owning test — when the
|
|
75
|
+
* enclosing file itself failed (e.g. a collection/load error).
|
|
58
76
|
* @public
|
|
59
77
|
*/
|
|
60
78
|
function collectConsoleLeakEntries(files) {
|
|
61
79
|
const entries = [];
|
|
62
|
-
const visit = (task, file, test) => {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
80
|
+
const visit = (task, file, test, testFailed, fileFailed) => {
|
|
81
|
+
const isTest = task.type === "test";
|
|
82
|
+
const currentTest = isTest ? task.fullTestName ?? task.name ?? test : test;
|
|
83
|
+
const currentTestFailed = isTest ? task.result?.state === "fail" : testFailed;
|
|
84
|
+
for (const log of task.logs ?? []) {
|
|
85
|
+
const failed = currentTest !== void 0 ? currentTestFailed : fileFailed;
|
|
86
|
+
entries.push({
|
|
87
|
+
file,
|
|
88
|
+
...currentTest !== void 0 ? { test: currentTest } : {},
|
|
89
|
+
type: log.type,
|
|
90
|
+
content: log.content,
|
|
91
|
+
...failed ? { failed: true } : {}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
for (const child of task.tasks ?? []) visit(child, file, currentTest, currentTestFailed, fileFailed);
|
|
71
95
|
};
|
|
72
|
-
for (const file of files)
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
const fileFailed = file.result?.state === "fail";
|
|
98
|
+
visit(file, file.name ?? "(unknown)", void 0, false, fileFailed);
|
|
99
|
+
}
|
|
73
100
|
return entries;
|
|
74
101
|
}
|
|
75
102
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/utils/detect-non-default-discover-strategy.ts
|
|
2
|
+
/**
|
|
3
|
+
* Cheaply strips `//` line comments and `/* *\/` block comments from `source`.
|
|
4
|
+
*
|
|
5
|
+
* Not a full lexer — a marker string that happens to sit inside a string
|
|
6
|
+
* literal containing comment-like syntax could slip through uncaught. That
|
|
7
|
+
* false positive is acceptable: {@link detectNonDefaultDiscoverStrategy} is
|
|
8
|
+
* only ever used to decide whether to fail open, and failing open is the
|
|
9
|
+
* safe direction.
|
|
10
|
+
*/
|
|
11
|
+
function stripComments(source) {
|
|
12
|
+
return source.replace(/\/\*[^*]*\*+(?:[^/*][^*]*\*+)*\//g, "").replace(/\/\/.*$/gm, "");
|
|
13
|
+
}
|
|
14
|
+
const DISCOVER_STRATEGY_OPTION_RE = /\bdiscoverStrategy\s*:/;
|
|
15
|
+
const ADD_PROJECT_MARKER = ".addProject(";
|
|
16
|
+
const EXTENDS_DEFAULT_STRATEGY_RE = /\bextends\s+DefaultDiscoverStrategy\b/;
|
|
17
|
+
const IMPLEMENTS_STRATEGY_RE = /\bimplements\s+DiscoverStrategy\b/;
|
|
18
|
+
/**
|
|
19
|
+
* Lexically detects whether a Vitest/Vite config source text appears to
|
|
20
|
+
* configure a non-default `DiscoverStrategy` — a custom `discoverStrategy`
|
|
21
|
+
* option (including `discoverStrategy: false`), an `AgentPlugin.discover()`
|
|
22
|
+
* `.addProject(...)` chain, or a class extending `DefaultDiscoverStrategy` /
|
|
23
|
+
* implementing `DiscoverStrategy`.
|
|
24
|
+
*
|
|
25
|
+
* Pure and comment-tolerant (best-effort): a marker mentioned only inside a
|
|
26
|
+
* comment may still register as a false positive if it isn't caught by the
|
|
27
|
+
* comment-stripping pass. That is intentional — this function only ever
|
|
28
|
+
* decides whether to fail open, so a false positive is harmless while a
|
|
29
|
+
* false negative would produce a confidently wrong deny.
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
function detectNonDefaultDiscoverStrategy(source) {
|
|
33
|
+
const stripped = stripComments(source);
|
|
34
|
+
return DISCOVER_STRATEGY_OPTION_RE.test(stripped) || stripped.includes(ADD_PROJECT_MARKER) || EXTENDS_DEFAULT_STRATEGY_RE.test(stripped) || IMPLEMENTS_STRATEGY_RE.test(stripped);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
export { detectNonDefaultDiscoverStrategy };
|
package/utils/format-console.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ansi } from "./ansi.js";
|
|
2
|
+
import { formatScopedCoverageNote } from "./format-scoped-coverage-note.js";
|
|
2
3
|
|
|
3
4
|
//#region src/utils/format-console.ts
|
|
4
5
|
/**
|
|
@@ -161,6 +162,10 @@ function formatConsoleMarkdown(report, options) {
|
|
|
161
162
|
if (report.coverage) {
|
|
162
163
|
const cov = report.coverage;
|
|
163
164
|
const globalThresholds = cov.thresholds.global;
|
|
165
|
+
if (cov.scoped) {
|
|
166
|
+
lines.push(formatScopedCoverageNote(cov.scopedFiles?.length ?? 0, cov.totalFiles));
|
|
167
|
+
lines.push("");
|
|
168
|
+
}
|
|
164
169
|
const filesToShow = cov.lowCoverage.slice(0, coverageConsoleLimit);
|
|
165
170
|
if (filesToShow.length > 0) {
|
|
166
171
|
lines.push(`### Coverage gaps`);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/utils/format-scoped-coverage-note.ts
|
|
2
|
+
/**
|
|
3
|
+
* Format the informational note shown when coverage thresholds were
|
|
4
|
+
* skipped because a run only exercised a subset of the project's test
|
|
5
|
+
* files (issue #160). Vitest's coverage provider enforces
|
|
6
|
+
* `coverage.thresholds` against the whole-project denominator
|
|
7
|
+
* regardless of how many files actually ran, so callers that detect a
|
|
8
|
+
* scoped/partial run suppress the native threshold check and surface
|
|
9
|
+
* this note instead so the agent understands why no pass/fail verdict
|
|
10
|
+
* was rendered for coverage.
|
|
11
|
+
*
|
|
12
|
+
* @param testedFileCount - number of test files that actually ran
|
|
13
|
+
* @param totalFileCount - total number of test files in the project, when
|
|
14
|
+
* known. Some callers (formatters working from a persisted
|
|
15
|
+
* `CoverageReport`, which tracks only the tested-file list) cannot supply
|
|
16
|
+
* this; the note degrades to `"(N test files)"` when omitted.
|
|
17
|
+
* @returns the note text, e.g. `"Coverage thresholds skipped: partial run (2 of 47 test files)"`
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
function formatScopedCoverageNote(testedFileCount, totalFileCount) {
|
|
21
|
+
return `Coverage thresholds skipped: partial run (${totalFileCount !== void 0 ? `${testedFileCount} of ${totalFileCount}` : `${testedFileCount}`} test files)`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
export { formatScopedCoverageNote };
|
package/utils/format-terminal.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ansi } from "./ansi.js";
|
|
2
|
+
import { formatScopedCoverageNote } from "./format-scoped-coverage-note.js";
|
|
2
3
|
import { relativePath } from "./format-console.js";
|
|
3
4
|
import { compressLines } from "./compress-lines.js";
|
|
4
5
|
|
|
@@ -257,6 +258,9 @@ const aggregateCoverage = (reports) => {
|
|
|
257
258
|
let hasCoverage = false;
|
|
258
259
|
let thresholdsGlobal;
|
|
259
260
|
let targetsGlobal;
|
|
261
|
+
let scoped = false;
|
|
262
|
+
let scopedFileCount = 0;
|
|
263
|
+
let totalFiles;
|
|
260
264
|
const belowTargetByFile = /* @__PURE__ */ new Map();
|
|
261
265
|
const belowThresholdByFile = /* @__PURE__ */ new Map();
|
|
262
266
|
for (const r of reports) {
|
|
@@ -265,6 +269,11 @@ const aggregateCoverage = (reports) => {
|
|
|
265
269
|
hasCoverage = true;
|
|
266
270
|
thresholdsGlobal ??= cov.thresholds.global;
|
|
267
271
|
targetsGlobal ??= cov.targets?.global;
|
|
272
|
+
if (cov.scoped) {
|
|
273
|
+
scoped = true;
|
|
274
|
+
scopedFileCount = Math.max(scopedFileCount, cov.scopedFiles?.length ?? 0);
|
|
275
|
+
if (cov.totalFiles !== void 0) totalFiles = cov.totalFiles;
|
|
276
|
+
}
|
|
268
277
|
if (cov.lowCoverage) {
|
|
269
278
|
for (const f of cov.lowCoverage) if (!belowThresholdByFile.has(f.file)) belowThresholdByFile.set(f.file, f);
|
|
270
279
|
}
|
|
@@ -279,8 +288,11 @@ const aggregateCoverage = (reports) => {
|
|
|
279
288
|
thresholdsMet: belowThresholdCount === 0,
|
|
280
289
|
belowThresholdCount,
|
|
281
290
|
belowTargetFiles,
|
|
291
|
+
scoped,
|
|
292
|
+
scopedFileCount,
|
|
282
293
|
...thresholdsGlobal !== void 0 ? { thresholdsGlobal } : {},
|
|
283
|
-
...targetsGlobal !== void 0 ? { targetsGlobal } : {}
|
|
294
|
+
...targetsGlobal !== void 0 ? { targetsGlobal } : {},
|
|
295
|
+
...totalFiles !== void 0 ? { totalFiles } : {}
|
|
284
296
|
};
|
|
285
297
|
};
|
|
286
298
|
/**
|
|
@@ -294,7 +306,8 @@ const renderCoverageSection = (reports, options, ao) => {
|
|
|
294
306
|
const lines = [];
|
|
295
307
|
const thresholdSpec = agg.thresholdsGlobal ? formatTargetSpec(agg.thresholdsGlobal) : "";
|
|
296
308
|
const targetSpec = agg.targetsGlobal ? formatTargetSpec(agg.targetsGlobal) : "";
|
|
297
|
-
if (
|
|
309
|
+
if (agg.scoped) lines.push(formatScopedCoverageNote(agg.scopedFileCount, agg.totalFiles));
|
|
310
|
+
else if (!agg.thresholdsMet) {
|
|
298
311
|
const cross = ansi("✗", "red", ao);
|
|
299
312
|
const fileWord = agg.belowThresholdCount === 1 ? "file" : "files";
|
|
300
313
|
lines.push(`Coverage: ${cross} ${agg.belowThresholdCount} ${fileWord} below minimum thresholds${thresholdSpec ? ` (${thresholdSpec})` : ""}`);
|
|
@@ -55,6 +55,19 @@ const validatePhaseTransition = (ctx) => {
|
|
|
55
55
|
humanHint: `Cannot transition from '${ctx.current_phase}' directly to 'green'. The red phase must be entered explicitly first (${ctx.current_phase}→red), then a failing test written and run, then red→green requested with a test_failed_run artifact.`
|
|
56
56
|
}
|
|
57
57
|
};
|
|
58
|
+
if (ctx.requested_phase === "refactor" && ctx.current_phase !== "green" && ctx.current_phase !== "green.fake-it") {
|
|
59
|
+
const canEnterGreen = ctx.current_phase === "red" || ctx.current_phase === "red.triangulate";
|
|
60
|
+
return {
|
|
61
|
+
accepted: false,
|
|
62
|
+
phase: ctx.current_phase,
|
|
63
|
+
denialReason: "refactor_without_passing_run",
|
|
64
|
+
remediation: {
|
|
65
|
+
suggestedTool: "tdd_phase_transition_request",
|
|
66
|
+
suggestedArgs: { requestedPhase: canEnterGreen ? "green" : "red" },
|
|
67
|
+
humanHint: canEnterGreen ? `Cannot transition from '${ctx.current_phase}' directly to 'refactor'. The green phase must be entered first (${ctx.current_phase}→green with a test_failed_run artifact), then green→refactor requested with a test_passed_run artifact.` : `Cannot transition from '${ctx.current_phase}' directly to 'refactor'. Enter red first (${ctx.current_phase}→red), write and run a failing test, request red→green with a test_failed_run artifact, then green→refactor with a test_passed_run artifact.`
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
58
71
|
const isTriangulateGreen = ctx.current_phase === "red.triangulate" && ctx.requested_phase === "green";
|
|
59
72
|
const expected = requiredArtifactForTransition(ctx.current_phase, ctx.requested_phase);
|
|
60
73
|
if (expected === null) return {
|
|
@@ -71,28 +84,29 @@ const validatePhaseTransition = (ctx) => {
|
|
|
71
84
|
humanHint: expected.humanHint
|
|
72
85
|
}
|
|
73
86
|
};
|
|
74
|
-
|
|
87
|
+
const isBatsRunLevel = ctx.cited_artifact.test_case_id === null && (ctx.cited_artifact.suite ?? "vitest") === "bats";
|
|
88
|
+
if (ctx.cited_artifact.test_case_id === null && !isBatsRunLevel) return {
|
|
75
89
|
accepted: false,
|
|
76
90
|
phase: ctx.current_phase,
|
|
77
91
|
denialReason: "missing_artifact_evidence",
|
|
78
92
|
remediation: {
|
|
79
93
|
suggestedTool: "run_tests",
|
|
80
94
|
suggestedArgs: {},
|
|
81
|
-
humanHint: "The cited artifact
|
|
95
|
+
humanHint: "The cited artifact is run-level (test_case_id is null) and its suite is 'vitest', so it cannot be bound to this phase — only bats-suite run-level artifacts are accepted without a specific test. Run a specific failing test via run_tests so the resulting artifact carries a test_case_id, then cite that artifact."
|
|
82
96
|
}
|
|
83
97
|
};
|
|
84
|
-
if (expected.kind === "test_failed_run") {
|
|
85
|
-
if (!isTriangulateGreen && ctx.
|
|
98
|
+
if (expected.kind === "test_failed_run" || isBatsRunLevel && expected.kind === "test_passed_run") {
|
|
99
|
+
if (!isTriangulateGreen && ctx.current_phase_id !== null && ctx.cited_artifact.phase_id !== ctx.current_phase_id) return {
|
|
86
100
|
accepted: false,
|
|
87
101
|
phase: ctx.current_phase,
|
|
88
102
|
denialReason: "evidence_not_in_phase_window",
|
|
89
103
|
remediation: {
|
|
90
104
|
suggestedTool: "run_tests",
|
|
91
105
|
suggestedArgs: {},
|
|
92
|
-
humanHint: "The cited
|
|
106
|
+
humanHint: "The cited artifact was recorded in a different phase than the one currently open. Run the failing test again inside the current phase so a fresh test_failed_run artifact is bound to it."
|
|
93
107
|
}
|
|
94
108
|
};
|
|
95
|
-
if (!ctx.cited_artifact.test_case_authored_in_session) return {
|
|
109
|
+
if (!isBatsRunLevel && !ctx.cited_artifact.test_case_authored_in_session) return {
|
|
96
110
|
accepted: false,
|
|
97
111
|
phase: ctx.current_phase,
|
|
98
112
|
denialReason: "evidence_not_in_phase_window",
|