@vitest-agent/engine 0.2.11 → 0.3.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 CHANGED
@@ -2427,6 +2427,21 @@ export interface SessionContext {
2427
2427
  * @public
2428
2428
  */
2429
2429
  export declare const parseSessionEnvExports: (content: string) => Record<string, string>;
2430
+ /**
2431
+ * The synchronous filesystem reads {@link recoverSessionContextFromSessionEnv}
2432
+ * performs. Each member throws on a missing or unreadable path, exactly as
2433
+ * the `node:fs` call it stands in for; recovery treats a throw as "skip".
2434
+ *
2435
+ * @public
2436
+ */
2437
+ export interface SessionEnvFileSystem {
2438
+ /** Entry names directly under `path` (`readdirSync`). */
2439
+ readonly readDirectory: (path: string) => ReadonlyArray<string>;
2440
+ /** The UTF-8 text of the file at `path` (`readFileSync(path, "utf8")`). */
2441
+ readonly readFile: (path: string) => string;
2442
+ /** Modification time of the file at `path`, in epoch milliseconds (`statSync(path).mtimeMs`). */
2443
+ readonly mtimeMs: (path: string) => number;
2444
+ }
2430
2445
  /**
2431
2446
  * Recover a {@link SessionContext} from the newest session-env hook file
2432
2447
  * whose `VITEST_AGENT_PROJECT_DIR` matches `projectDir`.
@@ -2440,12 +2455,15 @@ export declare const parseSessionEnvExports: (content: string) => Record<string,
2440
2455
  *
2441
2456
  * @param opts - `projectDir` to match against; `homeDir` is the caller's
2442
2457
  * home directory (ambient input — the MCP bin passes `os.homedir()`),
2443
- * under which `.claude/session-env` is read
2458
+ * under which `.claude/session-env` is read; `fileSystem` replaces the
2459
+ * `node:fs` reads (tests pass an in-memory volume) and defaults to the
2460
+ * real disk
2444
2461
  * @public
2445
2462
  */
2446
2463
  export declare const recoverSessionContextFromSessionEnv: (opts: {
2447
2464
  readonly projectDir: string;
2448
2465
  readonly homeDir: string;
2466
+ readonly fileSystem?: SessionEnvFileSystem;
2449
2467
  }) => SessionContext | null;
2450
2468
  //#endregion
2451
2469
  //#region src/project-dir.d.ts
@@ -2650,9 +2668,26 @@ export declare const resolveDataPath: (projectDir: string, options?: ResolveData
2650
2668
  * Always returns a non-empty string — falls back to the cwd basename
2651
2669
  * (or `"anonymous-project"`) so callers don't have to handle the
2652
2670
  * empty case.
2671
+ *
2672
+ * Synchronous and reads the real disk through `node:fs`; Effect callers
2673
+ * that already hold a `FileSystem` use
2674
+ * {@link resolveProjectKeyFromCwdEffect}, which applies the same rules.
2653
2675
  * @public
2654
2676
  */
2655
2677
  export declare const resolveProjectKeyFromCwd: (cwd: string) => string;
2678
+ /**
2679
+ * {@link resolveProjectKeyFromCwd} read through the ambient
2680
+ * `FileSystem` service instead of `node:fs` — same upward walk to the
2681
+ * nearest `package.json`, same precedence, same basename fallback.
2682
+ *
2683
+ * Never fails: a filesystem error while probing or reading is treated like
2684
+ * a missing or malformed `package.json`, exactly as the synchronous form
2685
+ * does.
2686
+ *
2687
+ * @param cwd - Absolute directory to start the upward walk from.
2688
+ * @public
2689
+ */
2690
+ export declare const resolveProjectKeyFromCwdEffect: (cwd: string) => Effect.Effect<string, never, FileSystem.FileSystem>;
2656
2691
  //#endregion
2657
2692
  //#region src/utils/resolve-workspace-key.d.ts
2658
2693
  /**
package/index.js CHANGED
@@ -55,9 +55,9 @@ import { resolveProjectDir } from "./project-dir.js";
55
55
  import { assembleHistoryRecord, assembleManifest } from "./sql/assemblers.js";
56
56
  import { _resetMigrationCacheForTesting, ensureMigrated } from "./utils/ensure-migrated.js";
57
57
  import { computeFailureSignature, normalizeAssertionShape } from "./utils/failure-signature.js";
58
- import { resolveProjectKeyFromCwd } from "./utils/resolve-project-key-from-cwd.js";
58
+ import { resolveProjectKeyFromCwd, resolveProjectKeyFromCwdEffect } from "./utils/resolve-project-key-from-cwd.js";
59
59
  import { DATABASE_FILENAME, resolveDataPath } from "./utils/resolve-data-path.js";
60
60
  import { resolveWorkspaceKey } from "./utils/resolve-workspace-key.js";
61
61
  import { CURRENT_ENGINE_VERSION } from "./version.js";
62
62
 
63
- export { APP_NAMESPACE, AgentContext, CURRENT_ENGINE_VERSION, ConfigLive, DATABASE_FILENAME, DATA_DB_FILENAME, DATA_FALLBACK_DIR, DataReader, DataReaderLive, DataStore, DataStoreLive, DetailResolver, DetailResolverLive, DiscoveryRegistry, DiscoveryRegistryLive, EnvironmentDetector, EnvironmentDetectorLive, EnvironmentDetectorTest, ExecutorResolver, ExecutorResolverLive, FormatSelector, FormatSelectorLive, HistoryTracker, HistoryTrackerLive, HistoryTrackerTest, IDEMPOTENCY_ROOT_SENTINEL, INLINE_ATTACHMENT_BODY_CAP_BYTES, LoggerLive, NodePlatformLayer, OutputPipelineLive, OutputRenderer, OutputRendererLive, PROJECT_MIGRATIONS, PathResolutionLive, PerClientSessionMapReader, PerClientSessionMapReaderLive, PerClientSessionMapWriter, PerClientSessionMapWriterLive, PlatformLive, ProjectDiscovery, ProjectDiscoveryLive, ProjectDiscoveryTest, ProjectIdentity, ProjectIdentityLive, ProjectIdentityTest, REGISTRY_DB_FILENAME, RunContext, RunContext$, RunContextLive, RunContextService, RunContextTest, SESSIONS_DB_FILENAME, SidecarPlatformLive, VitestAgentConfigFile, _resetMigrationCacheForTesting, assembleHistoryRecord, assembleManifest, classifyEnvironment, computeFailureSignature, deriveIdempotencyKey, dispatchRecordTddArtifactEffect, endAgentEffect, ensureMigrated, formatTriageEffect, formatWrapupEffect, historyKey, makeSqliteStack, migration as migration0001, migration$1 as migration0002, normalizeAssertionShape, parseAndValidateTurnPayload, parseSessionEnvExports, recordRunWorkspaceChangesEffect, recordSessionEnd, recordSessionStart, recordTddArtifactByTaskIdEffect, recordTddArtifactEffect, recordTurnEffect, recoverSessionContextFromSessionEnv, registerAgentEffect, migration$2 as registryMigration0001, resolveDataPath, resolveHookPaths, resolveLogFile, resolveLogLevel, resolveProjectDir, resolveProjectIdentityFromCandidates, resolveProjectKeyFromCwd, resolveSessionForRecording, resolveSessionMapPath, resolveWorkspaceKey, migration$3 as sessionMapMigration0001 };
63
+ export { APP_NAMESPACE, AgentContext, CURRENT_ENGINE_VERSION, ConfigLive, DATABASE_FILENAME, DATA_DB_FILENAME, DATA_FALLBACK_DIR, DataReader, DataReaderLive, DataStore, DataStoreLive, DetailResolver, DetailResolverLive, DiscoveryRegistry, DiscoveryRegistryLive, EnvironmentDetector, EnvironmentDetectorLive, EnvironmentDetectorTest, ExecutorResolver, ExecutorResolverLive, FormatSelector, FormatSelectorLive, HistoryTracker, HistoryTrackerLive, HistoryTrackerTest, IDEMPOTENCY_ROOT_SENTINEL, INLINE_ATTACHMENT_BODY_CAP_BYTES, LoggerLive, NodePlatformLayer, OutputPipelineLive, OutputRenderer, OutputRendererLive, PROJECT_MIGRATIONS, PathResolutionLive, PerClientSessionMapReader, PerClientSessionMapReaderLive, PerClientSessionMapWriter, PerClientSessionMapWriterLive, PlatformLive, ProjectDiscovery, ProjectDiscoveryLive, ProjectDiscoveryTest, ProjectIdentity, ProjectIdentityLive, ProjectIdentityTest, REGISTRY_DB_FILENAME, RunContext, RunContext$, RunContextLive, RunContextService, RunContextTest, SESSIONS_DB_FILENAME, SidecarPlatformLive, VitestAgentConfigFile, _resetMigrationCacheForTesting, assembleHistoryRecord, assembleManifest, classifyEnvironment, computeFailureSignature, deriveIdempotencyKey, dispatchRecordTddArtifactEffect, endAgentEffect, ensureMigrated, formatTriageEffect, formatWrapupEffect, historyKey, makeSqliteStack, migration as migration0001, migration$1 as migration0002, normalizeAssertionShape, parseAndValidateTurnPayload, parseSessionEnvExports, recordRunWorkspaceChangesEffect, recordSessionEnd, recordSessionStart, recordTddArtifactByTaskIdEffect, recordTddArtifactEffect, recordTurnEffect, recoverSessionContextFromSessionEnv, registerAgentEffect, migration$2 as registryMigration0001, resolveDataPath, resolveHookPaths, resolveLogFile, resolveLogLevel, resolveProjectDir, resolveProjectIdentityFromCandidates, resolveProjectKeyFromCwd, resolveProjectKeyFromCwdEffect, resolveSessionForRecording, resolveSessionMapPath, resolveWorkspaceKey, migration$3 as sessionMapMigration0001 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/engine",
3
- "version": "0.2.11",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "Shared services, layers, SQLite data layer, migrations and platform resolution for the vitest-agent CLI and MCP server.",
6
6
  "keywords": [
@@ -32,6 +32,12 @@ const parseSessionEnvExports = (content) => {
32
32
  }
33
33
  return out;
34
34
  };
35
+ /** The default {@link SessionEnvFileSystem}: the real disk through `node:fs`. */
36
+ const nodeSessionEnvFileSystem = {
37
+ readDirectory: (path) => readdirSync(path),
38
+ readFile: (path) => readFileSync(path, "utf8"),
39
+ mtimeMs: (path) => statSync(path).mtimeMs
40
+ };
35
41
  /**
36
42
  * Recover a {@link SessionContext} from the newest session-env hook file
37
43
  * whose `VITEST_AGENT_PROJECT_DIR` matches `projectDir`.
@@ -45,15 +51,18 @@ const parseSessionEnvExports = (content) => {
45
51
  *
46
52
  * @param opts - `projectDir` to match against; `homeDir` is the caller's
47
53
  * home directory (ambient input — the MCP bin passes `os.homedir()`),
48
- * under which `.claude/session-env` is read
54
+ * under which `.claude/session-env` is read; `fileSystem` replaces the
55
+ * `node:fs` reads (tests pass an in-memory volume) and defaults to the
56
+ * real disk
49
57
  * @public
50
58
  */
51
59
  const recoverSessionContextFromSessionEnv = (opts) => {
60
+ const fs = opts.fileSystem ?? nodeSessionEnvFileSystem;
52
61
  const root = join(opts.homeDir, ".claude", "session-env");
53
62
  const wantDir = resolve(opts.projectDir);
54
63
  let entries;
55
64
  try {
56
- entries = readdirSync(root);
65
+ entries = fs.readDirectory(root);
57
66
  } catch {
58
67
  return null;
59
68
  }
@@ -61,15 +70,15 @@ const recoverSessionContextFromSessionEnv = (opts) => {
61
70
  for (const entry of entries) {
62
71
  const file = join(root, entry, "vitest-agent-hook.sh");
63
72
  try {
64
- const st = statSync(file);
65
- const env = parseSessionEnvExports(readFileSync(file, "utf8"));
73
+ const mtimeMs = fs.mtimeMs(file);
74
+ const env = parseSessionEnvExports(fs.readFile(file));
66
75
  const chatId = env.VITEST_AGENT_CHAT_ID;
67
76
  const conversationId = env.VITEST_AGENT_CONVERSATION_ID;
68
77
  const mainAgentId = env.VITEST_AGENT_MAIN_AGENT_ID ?? env.VITEST_AGENT_AGENT_ID;
69
78
  const fileProjectDir = env.VITEST_AGENT_PROJECT_DIR;
70
79
  if (chatId === void 0 || chatId.length === 0 || conversationId === void 0 || conversationId.length === 0 || mainAgentId === void 0 || mainAgentId.length === 0 || fileProjectDir === void 0 || resolve(fileProjectDir) !== wantDir) continue;
71
- if (best === null || st.mtimeMs > best.mtimeMs) best = {
72
- mtimeMs: st.mtimeMs,
80
+ if (best === null || mtimeMs > best.mtimeMs) best = {
81
+ mtimeMs,
73
82
  ctx: {
74
83
  chatId,
75
84
  conversationId,
@@ -1,5 +1,5 @@
1
1
  import { VitestAgentConfigFile } from "../services/Config.js";
2
- import { resolveProjectKeyFromCwd } from "./resolve-project-key-from-cwd.js";
2
+ import { resolveProjectKeyFromCwdEffect } from "./resolve-project-key-from-cwd.js";
3
3
  import { VitestAgentConfig, normalizeWorkspaceKey } from "@vitest-agent/sdk";
4
4
  import { Effect, FileSystem } from "effect";
5
5
  import { AppDirs } from "@effected/xdg";
@@ -54,7 +54,7 @@ const resolveDataPath = (projectDir, options = {}) => Effect.gen(function* () {
54
54
  return join(loaded.cacheDir, DATABASE_FILENAME);
55
55
  }
56
56
  const dataRoot = yield* (yield* AppDirs).ensureData;
57
- const key = loaded.projectKey ? normalizeWorkspaceKey(loaded.projectKey) : resolveProjectKeyFromCwd(projectDir);
57
+ const key = loaded.projectKey ? normalizeWorkspaceKey(loaded.projectKey) : yield* resolveProjectKeyFromCwdEffect(projectDir);
58
58
  const dir = join(dataRoot, key);
59
59
  yield* fs.makeDirectory(dir, { recursive: true });
60
60
  return join(dir, DATABASE_FILENAME);
@@ -1,4 +1,5 @@
1
1
  import { gitUrlToProjectKey, normalizeWorkspaceKey } from "@vitest-agent/sdk";
2
+ import { Effect, FileSystem } from "effect";
2
3
  import { existsSync, readFileSync } from "node:fs";
3
4
  import { dirname, join } from "node:path";
4
5
 
@@ -24,6 +25,27 @@ const readRepositoryUrl = (parsed) => {
24
25
  return null;
25
26
  };
26
27
  /**
28
+ * The key a `package.json` body names: the canonical `host__path` form of
29
+ * `repository.url` when present and canonicalizable, else the normalized
30
+ * `name`, else `null` (no usable field, or malformed JSON).
31
+ */
32
+ const keyFromPackageJson = (content) => {
33
+ try {
34
+ const parsed = JSON.parse(content);
35
+ const repoUrl = readRepositoryUrl(parsed);
36
+ if (repoUrl !== null) {
37
+ const key = gitUrlToProjectKey(repoUrl);
38
+ if (key !== null) return key;
39
+ }
40
+ if (typeof parsed.name === "string" && parsed.name.length > 0) return normalizeWorkspaceKey(parsed.name);
41
+ } catch {}
42
+ return null;
43
+ };
44
+ const basenameKey = (cwd) => {
45
+ const basename = cwd.split("/").filter((segment) => segment.length > 0).pop() ?? "anonymous-project";
46
+ return normalizeWorkspaceKey(basename);
47
+ };
48
+ /**
27
49
  * Compute the project key for a workspace by reading `package.json`
28
50
  * fields directly. Returns the canonical `host__path` form when a git
29
51
  * remote URL is present, otherwise the normalized package name.
@@ -31,22 +53,50 @@ const readRepositoryUrl = (parsed) => {
31
53
  * Always returns a non-empty string — falls back to the cwd basename
32
54
  * (or `"anonymous-project"`) so callers don't have to handle the
33
55
  * empty case.
56
+ *
57
+ * Synchronous and reads the real disk through `node:fs`; Effect callers
58
+ * that already hold a `FileSystem` use
59
+ * {@link resolveProjectKeyFromCwdEffect}, which applies the same rules.
34
60
  * @public
35
61
  */
36
62
  const resolveProjectKeyFromCwd = (cwd) => {
37
63
  const pkgPath = findNearestPackageJson(cwd);
38
- if (pkgPath !== null) try {
39
- const parsed = JSON.parse(readFileSync(pkgPath, "utf-8"));
40
- const repoUrl = readRepositoryUrl(parsed);
41
- if (repoUrl !== null) {
42
- const key = gitUrlToProjectKey(repoUrl);
43
- if (key !== null) return key;
44
- }
45
- if (typeof parsed.name === "string" && parsed.name.length > 0) return normalizeWorkspaceKey(parsed.name);
46
- } catch {}
47
- const basename = cwd.split("/").filter((segment) => segment.length > 0).pop() ?? "anonymous-project";
48
- return normalizeWorkspaceKey(basename);
64
+ if (pkgPath !== null) {
65
+ let content = null;
66
+ try {
67
+ content = readFileSync(pkgPath, "utf-8");
68
+ } catch {}
69
+ const key = content === null ? null : keyFromPackageJson(content);
70
+ if (key !== null) return key;
71
+ }
72
+ return basenameKey(cwd);
49
73
  };
74
+ /**
75
+ * {@link resolveProjectKeyFromCwd} read through the ambient
76
+ * `FileSystem` service instead of `node:fs` — same upward walk to the
77
+ * nearest `package.json`, same precedence, same basename fallback.
78
+ *
79
+ * Never fails: a filesystem error while probing or reading is treated like
80
+ * a missing or malformed `package.json`, exactly as the synchronous form
81
+ * does.
82
+ *
83
+ * @param cwd - Absolute directory to start the upward walk from.
84
+ * @public
85
+ */
86
+ const resolveProjectKeyFromCwdEffect = (cwd) => Effect.gen(function* () {
87
+ const fs = yield* FileSystem.FileSystem;
88
+ let dir = cwd;
89
+ while (true) {
90
+ const candidate = join(dir, PACKAGE_JSON);
91
+ if (yield* fs.exists(candidate).pipe(Effect.orElseSucceed(() => false))) {
92
+ const content = yield* fs.readFileString(candidate).pipe(Effect.orElseSucceed(() => null));
93
+ return (content === null ? null : keyFromPackageJson(content)) ?? basenameKey(cwd);
94
+ }
95
+ const parent = dirname(dir);
96
+ if (parent === dir) return basenameKey(cwd);
97
+ dir = parent;
98
+ }
99
+ });
50
100
 
51
101
  //#endregion
52
- export { resolveProjectKeyFromCwd };
102
+ export { resolveProjectKeyFromCwd, resolveProjectKeyFromCwdEffect };
package/version.js CHANGED
@@ -1,6 +1,6 @@
1
1
  //#region src/version.ts
2
2
  /** Build-time injected package version (see architecture.md D36). @public */
3
- const CURRENT_ENGINE_VERSION = "0.2.11";
3
+ const CURRENT_ENGINE_VERSION = "0.3.0";
4
4
 
5
5
  //#endregion
6
6
  export { CURRENT_ENGINE_VERSION };