@vitest-agent/cli 2.2.12 → 2.2.14

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/commands/agent.js CHANGED
@@ -6,9 +6,9 @@ import { recordCommand } from "./record.js";
6
6
  import { triageCommand } from "./triage.js";
7
7
  import { wrapupCommand } from "./wrapup.js";
8
8
  import * as NodeServices from "@effect/platform-node/NodeServices";
9
- import { classifyTestPath, findOwningWorkspace, resolveProjectKeyFromCwd } from "@vitest-agent/sdk";
9
+ import { classifyTestPath, detectNonDefaultDiscoverStrategy, findOwningWorkspace, resolveProjectKeyFromCwd } from "@vitest-agent/sdk";
10
10
  import { Cause, Effect, Option } from "effect";
11
- import { existsSync } from "node:fs";
11
+ import { existsSync, readFileSync } from "node:fs";
12
12
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
13
13
  import { Argument, Command, Flag } from "effect/unstable/cli";
14
14
  import { findWorkspaceRootSync, getWorkspacePackagesSync } from "@effected/workspaces";
@@ -182,11 +182,56 @@ function crossesNestedPackageBoundary(ownerPath, filePath) {
182
182
  }
183
183
  return false;
184
184
  }
185
+ /**
186
+ * Candidate Vitest/Vite config filenames at a workspace root, in the order
187
+ * Vitest itself would prefer them. `vite.config.*` is a fallback for a
188
+ * workspace that configures the plugin through Vite directly rather than a
189
+ * dedicated `vitest.config.*`.
190
+ * @internal
191
+ */
192
+ const VITEST_CONFIG_CANDIDATES = [
193
+ "vitest.config.ts",
194
+ "vitest.config.mts",
195
+ "vitest.config.js",
196
+ "vitest.config.mjs",
197
+ "vitest.workspace.ts",
198
+ "vitest.workspace.mts",
199
+ "vitest.workspace.js",
200
+ "vitest.workspace.mjs",
201
+ "vite.config.ts",
202
+ "vite.config.mts",
203
+ "vite.config.js",
204
+ "vite.config.mjs"
205
+ ];
206
+ /**
207
+ * Reads the first Vitest/Vite config file found at `root`, returning its
208
+ * source text, or `null` when none of {@link VITEST_CONFIG_CANDIDATES} exists
209
+ * or the file that does exist cannot be read.
210
+ *
211
+ * A `null` result is deliberately indistinguishable between "no config file"
212
+ * and "unreadable config file" — both mean the caller cannot rule out a
213
+ * non-default `DiscoverStrategy` and must fail open (issue #230).
214
+ * @internal
215
+ */
216
+ function readWorkspaceVitestConfigSource(root) {
217
+ for (const candidate of VITEST_CONFIG_CANDIDATES) {
218
+ const candidatePath = join(root, candidate);
219
+ if (!existsSync(candidatePath)) continue;
220
+ try {
221
+ return readFileSync(candidatePath, "utf8");
222
+ } catch {
223
+ return null;
224
+ }
225
+ }
226
+ return null;
227
+ }
185
228
  const checkTestPathSubcommand = Command.make("check-test-path", { path: testPathArg }, (opts) => Effect.sync(() => {
186
229
  const from = process.env.VITEST_AGENT_PROJECT_DIR ?? process.cwd();
187
230
  const absolute = isAbsolute(opts.path) ? opts.path : resolve(from, opts.path);
188
231
  const root = findWorkspaceRootSync(from, nodeSyncOps);
189
232
  if (root === null) process.exit(1);
233
+ const configSource = readWorkspaceVitestConfigSource(root);
234
+ if (configSource === null || detectNonDefaultDiscoverStrategy(configSource)) process.exit(1);
190
235
  const packages = getWorkspacePackagesSync(root, nodeSyncOps);
191
236
  const owner = findOwningWorkspace(packages, absolute);
192
237
  if (owner === null) process.exit(1);
@@ -1,5 +1,5 @@
1
1
  import { recordSessionEnd, recordSessionStart } from "../lib/record-session.js";
2
- import { recordTddArtifactEffect } from "../lib/record-tdd-artifact.js";
2
+ import { dispatchRecordTddArtifactEffect } from "../lib/record-tdd-artifact.js";
3
3
  import { recordTurnEffect } from "../lib/record-turn.js";
4
4
  import { recordRunWorkspaceChangesEffect } from "../lib/record-workspace-changes.js";
5
5
  import { DataReader, DataStore } from "@vitest-agent/sdk";
@@ -85,8 +85,12 @@ const testRunIdOpt = Flag.optional(Flag.integer("test-run-id"));
85
85
  const testFirstFailureRunIdOpt = Flag.optional(Flag.integer("test-first-failure-run-id"));
86
86
  const diffExcerptOpt = Flag.optional(Flag.string("diff-excerpt"));
87
87
  const recordedAtOpt = Flag.string("recorded-at").pipe(Flag.withDefault((/* @__PURE__ */ new Date()).toISOString()));
88
+ const chatIdOptional = Flag.optional(Flag.string("chat-id")).pipe(Flag.withDescription("Host chat id; omit when --tdd-task-id is supplied instead"));
89
+ const tddTaskIdOpt = Flag.optional(Flag.integer("tdd-task-id")).pipe(Flag.withDescription("Explicit TDD task id escape hatch (issue #144): bypasses chat-id -> session -> task resolution entirely"));
90
+ const suiteOpt = Flag.choice("suite", ["vitest", "bats"]).pipe(Flag.withDefault("vitest"), Flag.withDescription("Explicit test-runner marker (issue #363): 'vitest' (default) or 'bats'. Distinguishes a bats run-level artifact (no test_case_id) from a vitest one for the D2 phase-transition validator."));
88
91
  const tddArtifactSubcommand = Command.make("tdd-artifact", {
89
- chatId,
92
+ chatId: chatIdOptional,
93
+ tddTaskId: tddTaskIdOpt,
90
94
  project: projectOptional,
91
95
  cwd: cwdOptional,
92
96
  artifactKind: artifactKindOpt,
@@ -95,12 +99,14 @@ const tddArtifactSubcommand = Command.make("tdd-artifact", {
95
99
  testRunId: testRunIdOpt,
96
100
  testFirstFailureRunId: testFirstFailureRunIdOpt,
97
101
  diffExcerpt: diffExcerptOpt,
98
- recordedAt: recordedAtOpt
102
+ recordedAt: recordedAtOpt,
103
+ suite: suiteOpt
99
104
  }, (opts) => Effect.gen(function* () {
100
105
  let fileId;
101
106
  if (opts.filePath._tag === "Some") fileId = yield* (yield* DataStore).ensureFile(opts.filePath.value);
102
- return yield* recordTddArtifactEffect({
103
- chatId: opts.chatId,
107
+ return yield* dispatchRecordTddArtifactEffect({
108
+ ...opts.chatId._tag === "Some" && { chatId: opts.chatId.value },
109
+ ...opts.tddTaskId._tag === "Some" && { tddTaskId: opts.tddTaskId.value },
104
110
  ...opts.project._tag === "Some" && { project: opts.project.value },
105
111
  ...opts.cwd._tag === "Some" && { cwd: opts.cwd.value },
106
112
  artifactKind: opts.artifactKind,
@@ -109,7 +115,8 @@ const tddArtifactSubcommand = Command.make("tdd-artifact", {
109
115
  ...opts.testRunId._tag === "Some" && { testRunId: opts.testRunId.value },
110
116
  ...opts.testFirstFailureRunId._tag === "Some" && { testFirstFailureRunId: opts.testFirstFailureRunId.value },
111
117
  ...opts.diffExcerpt._tag === "Some" && { diffExcerpt: opts.diffExcerpt.value },
112
- recordedAt: opts.recordedAt
118
+ recordedAt: opts.recordedAt,
119
+ suite: opts.suite
113
120
  });
114
121
  }).pipe(Effect.flatMap((result) => Effect.sync(() => process.stdout.write(`${JSON.stringify(result)}\n`))), Effect.catch((err) => Effect.sync(() => {
115
122
  process.stderr.write(`record tdd-artifact: ${err instanceof Error ? err.message : String(err)}\n`);
package/index.js CHANGED
@@ -11,7 +11,7 @@ import { DATA_DB_FILENAME, REGISTRY_DB_FILENAME, SESSIONS_DB_FILENAME, resolvePr
11
11
  *
12
12
  * @public
13
13
  */
14
- const CURRENT_CLI_VERSION = "2.2.12";
14
+ const CURRENT_CLI_VERSION = "2.2.14";
15
15
 
16
16
  //#endregion
17
17
  export { CURRENT_CLI_VERSION, CliLive, DATA_DB_FILENAME, REGISTRY_DB_FILENAME, SESSIONS_DB_FILENAME, SidecarLive, registerAgentEffect, resolveProjectDataDir, resolveRegistryDir, resolveSessionMapPath };
@@ -39,10 +39,15 @@ const registerAgentEffect = (input) => Effect.gen(function* () {
39
39
  agentKind: input.parentAgentId === void 0 ? "main" : "subagent",
40
40
  agentType: input.agentType,
41
41
  triageWasNonEmpty: false,
42
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
42
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
43
+ conversationId
43
44
  }),
44
45
  onSome: (s) => Effect.succeed(s.id)
45
46
  });
47
+ yield* store.setSessionConversationIdIfNull({
48
+ sessionId: sessionRowId,
49
+ conversationId
50
+ });
46
51
  const agentContext = yield* ctx.captureAgentContext(input.cwd);
47
52
  const clientNonce = input.clientNonce ?? deriveDefaultClientNonce(input);
48
53
  const idempotencyKey = deriveIdempotencyKey({
@@ -3,20 +3,18 @@ import { DataReader, DataStore } from "@vitest-agent/sdk";
3
3
  import { Effect, Option } from "effect";
4
4
 
5
5
  //#region src/lib/record-tdd-artifact.ts
6
- const recordTddArtifactEffect = (input) => Effect.gen(function* () {
6
+ /**
7
+ * Resolve (auto-opening a `spike` phase if needed) the current open
8
+ * phase for `tddTaskId`, then write the artifact under it. Shared by
9
+ * both `recordTddArtifactEffect` (chat-id → session → task resolution)
10
+ * and `recordTddArtifactByTaskIdEffect` (explicit task-id escape hatch).
11
+ */
12
+ const writeArtifactUnderOpenPhase = (tddTaskId, input) => Effect.gen(function* () {
7
13
  const reader = yield* DataReader;
8
14
  const store = yield* DataStore;
9
- const session = yield* resolveSessionForRecording({
10
- chatId: input.chatId,
11
- recordedAt: input.recordedAt,
12
- ...input.project !== void 0 && { project: input.project },
13
- ...input.cwd !== void 0 && { cwd: input.cwd }
14
- });
15
- const openTdd = (yield* reader.listTddTasksForSession(session.id, { walkParents: true })).find((t) => t.endedAt === null);
16
- if (openTdd === void 0) return yield* Effect.fail(/* @__PURE__ */ new Error(`No open TDD task under chat_id ${input.chatId}. Call tdd_task start first.`));
17
- const phaseOpt = yield* reader.getCurrentTddPhase(openTdd.id);
15
+ const phaseOpt = yield* reader.getCurrentTddPhase(tddTaskId);
18
16
  const phaseId = Option.isSome(phaseOpt) ? phaseOpt.value.id : (yield* store.writeTddPhase({
19
- tddTaskId: openTdd.id,
17
+ tddTaskId,
20
18
  phase: "spike",
21
19
  startedAt: input.recordedAt,
22
20
  transitionReason: "auto-opened by record tdd-artifact (no prior phase)"
@@ -30,11 +28,77 @@ const recordTddArtifactEffect = (input) => Effect.gen(function* () {
30
28
  ...input.testRunId !== void 0 && { testRunId: input.testRunId },
31
29
  ...input.testFirstFailureRunId !== void 0 && { testFirstFailureRunId: input.testFirstFailureRunId },
32
30
  ...input.diffExcerpt !== void 0 && { diffExcerpt: input.diffExcerpt },
31
+ ...input.suite !== void 0 && { suite: input.suite },
33
32
  recordedAt: input.recordedAt
34
33
  }),
35
34
  phaseId
36
35
  };
37
36
  });
37
+ const recordTddArtifactEffect = (input) => Effect.gen(function* () {
38
+ const reader = yield* DataReader;
39
+ const session = yield* resolveSessionForRecording({
40
+ chatId: input.chatId,
41
+ recordedAt: input.recordedAt,
42
+ ...input.project !== void 0 && { project: input.project },
43
+ ...input.cwd !== void 0 && { cwd: input.cwd }
44
+ });
45
+ const openTdd = (yield* reader.listTddTasksForSession(session.id, {
46
+ walkParents: true,
47
+ walkConversation: true
48
+ })).find((t) => t.endedAt === null);
49
+ if (openTdd === void 0) return yield* Effect.fail(/* @__PURE__ */ new Error(`No open TDD task under chat_id ${input.chatId}. Call tdd_task start first.`));
50
+ return yield* writeArtifactUnderOpenPhase(openTdd.id, input);
51
+ });
52
+ /**
53
+ * Explicit task-id escape hatch (issue #144). Bypasses `chat_id` →
54
+ * session → task resolution entirely — the caller already knows which
55
+ * TDD task the artifact belongs to (e.g. a hook seeded with
56
+ * `VITEST_AGENT_TDD_TASK_ID` in a detached-session environment where
57
+ * neither the parent walk nor the conversation-id fallback resolves
58
+ * the right task). Fails loudly when the task does not exist or has
59
+ * already ended — writing to a closed task's phase would silently
60
+ * corrupt the evidence trail.
61
+ */
62
+ const recordTddArtifactByTaskIdEffect = (input) => Effect.gen(function* () {
63
+ const taskOpt = yield* (yield* DataReader).getTddTaskById(input.tddTaskId);
64
+ if (Option.isNone(taskOpt)) return yield* Effect.fail(/* @__PURE__ */ new Error(`No TDD task found for id ${input.tddTaskId}.`));
65
+ if (taskOpt.value.endedAt !== null) return yield* Effect.fail(/* @__PURE__ */ new Error(`TDD task ${input.tddTaskId} has already ended (endedAt=${taskOpt.value.endedAt}).`));
66
+ return yield* writeArtifactUnderOpenPhase(input.tddTaskId, input);
67
+ });
68
+ /**
69
+ * CLI-command-facing dispatcher (issue #144). Keeps `commands/record.ts`
70
+ * a thin flag-parsing wrapper per this package's convention. `tddTaskId`
71
+ * takes priority when both are supplied — it is the explicit escape
72
+ * hatch and should never silently fall back to (weaker) session
73
+ * resolution.
74
+ */
75
+ const dispatchRecordTddArtifactEffect = (input) => {
76
+ if (input.tddTaskId !== void 0) return recordTddArtifactByTaskIdEffect({
77
+ tddTaskId: input.tddTaskId,
78
+ artifactKind: input.artifactKind,
79
+ ...input.fileId !== void 0 && { fileId: input.fileId },
80
+ ...input.testCaseId !== void 0 && { testCaseId: input.testCaseId },
81
+ ...input.testRunId !== void 0 && { testRunId: input.testRunId },
82
+ ...input.testFirstFailureRunId !== void 0 && { testFirstFailureRunId: input.testFirstFailureRunId },
83
+ ...input.diffExcerpt !== void 0 && { diffExcerpt: input.diffExcerpt },
84
+ ...input.suite !== void 0 && { suite: input.suite },
85
+ recordedAt: input.recordedAt
86
+ });
87
+ if (input.chatId !== void 0) return recordTddArtifactEffect({
88
+ chatId: input.chatId,
89
+ artifactKind: input.artifactKind,
90
+ ...input.fileId !== void 0 && { fileId: input.fileId },
91
+ ...input.testCaseId !== void 0 && { testCaseId: input.testCaseId },
92
+ ...input.testRunId !== void 0 && { testRunId: input.testRunId },
93
+ ...input.testFirstFailureRunId !== void 0 && { testFirstFailureRunId: input.testFirstFailureRunId },
94
+ ...input.diffExcerpt !== void 0 && { diffExcerpt: input.diffExcerpt },
95
+ ...input.suite !== void 0 && { suite: input.suite },
96
+ recordedAt: input.recordedAt,
97
+ ...input.cwd !== void 0 && { cwd: input.cwd },
98
+ ...input.project !== void 0 && { project: input.project }
99
+ });
100
+ return Effect.fail(/* @__PURE__ */ new Error("record tdd-artifact requires either --chat-id or --tdd-task-id."));
101
+ };
38
102
 
39
103
  //#endregion
40
- export { recordTddArtifactEffect };
104
+ export { dispatchRecordTddArtifactEffect, recordTddArtifactByTaskIdEffect, recordTddArtifactEffect };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/cli",
3
- "version": "2.2.12",
3
+ "version": "2.2.14",
4
4
  "private": false,
5
5
  "description": "On-demand CLI for vitest-agent. Reads cached test data and reports status, overview, coverage, history, trends, and cache health.",
6
6
  "keywords": [
@@ -40,9 +40,9 @@
40
40
  "dependencies": {
41
41
  "@effect/platform-node": "4.0.0-rc.109",
42
42
  "@effect/sql-sqlite-node": "4.0.0-rc.109",
43
- "@effected/workspaces": "^0.18.3",
44
- "@vitest-agent/sdk": "2.4.12",
45
- "@vitest-agent/sidecar": "2.1.12",
43
+ "@effected/workspaces": "^0.19.0",
44
+ "@vitest-agent/sdk": "2.5.0",
45
+ "@vitest-agent/sidecar": "2.1.14",
46
46
  "effect": "4.0.0-rc.109"
47
47
  },
48
48
  "engines": {