@vitest-agent/engine 2.1.0 → 3.0.1

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/stores.js CHANGED
@@ -1,5 +1,4 @@
1
- import { Context, Effect } from "effect";
2
- import { SqlClient } from "effect/sql/SqlClient";
1
+ import { Context } from "effect";
3
2
 
4
3
  // src/stores.ts
5
4
  /**
@@ -10,24 +9,34 @@ import { SqlClient } from "effect/sql/SqlClient";
10
9
  */
11
10
  const LEDGER_OPTIONS = { adoptMigratorLedger: true };
12
11
  /**
13
- * Convert a {@link MigrationRecord} to the `StoreMigration` list `Store` runs,
14
- * parsing each key exactly as effect/sql's `Migrator.fromRecord` does
15
- * (`/^(\d+)_(.+)$/`: `"0001_initial"` → id `1`, name `"initial"`), so the ids
16
- * and names line up with an adopted `effect_sql_migrations` ledger. A key that
17
- * does not match is skipped, as effect/sql skips it.
12
+ * How long a connection waits on a locked database before `SQLITE_BUSY`:
13
+ * five seconds, the value the 2.x migrations set with `PRAGMA busy_timeout`.
14
+ * Passed as the driver's `busyTimeout`, which applies it on every open.
18
15
  *
19
16
  * @internal
20
17
  */
21
- const toStoreMigrations = (migrations) => Object.entries(migrations).flatMap(([key, migration]) => {
22
- const match = key.match(/^(\d+)_(.+)$/);
23
- if (match === null) return [];
24
- const [, id, name] = match;
25
- return [{
26
- id: Number(id),
27
- name,
28
- up: (sql) => Effect.provideService(migration, SqlClient, sql)
29
- }];
30
- }).sort((a, b) => a.id - b.id);
18
+ const BUSY_TIMEOUT = "5 seconds";
19
+ /**
20
+ * The per-connection settings every database applies as its connection
21
+ * opens, before any migration: foreign-key enforcement on. The 2.x
22
+ * migrations set `foreign_keys` / `busy_timeout` themselves, which a
23
+ * migration can only do for the one connection that runs it; the
24
+ * `node:sqlite` driver happens to default both today, and this pins them
25
+ * so a driver default change cannot silently drop them.
26
+ *
27
+ * @internal
28
+ */
29
+ const applyConnectionPragmas = (sql) => sql`PRAGMA foreign_keys = ON`;
30
+ /**
31
+ * The connection options every keyed store opens with: the driver's
32
+ * `busyTimeout` and {@link applyConnectionPragmas} as `onConnect`.
33
+ *
34
+ * @internal
35
+ */
36
+ const CONNECTION_OPTIONS = {
37
+ client: { busyTimeout: BUSY_TIMEOUT },
38
+ onConnect: applyConnectionPragmas
39
+ };
31
40
  /**
32
41
  * The global discovery registry (`<XDG data>/vitest-agent/registry.db`).
33
42
  * @internal
@@ -39,4 +48,4 @@ var RegistryStore = class extends Context.Service()("vitest-agent/RegistryStore"
39
48
  */
40
49
  var SessionMapStore = class extends Context.Service()("vitest-agent/SessionMapStore") {};
41
50
 
42
- export { LEDGER_OPTIONS, RegistryStore, SessionMapStore, toStoreMigrations };
51
+ export { BUSY_TIMEOUT, CONNECTION_OPTIONS, LEDGER_OPTIONS, RegistryStore, SessionMapStore, applyConnectionPragmas };
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.59.4"
8
+ "packageVersion": "7.60.0"
9
9
  }
10
10
  ]
11
11
  }
@@ -1,24 +1,32 @@
1
1
  import { LoggerLive } from "../layers/LoggerLive.js";
2
- import { NodePlatformLayer, makeSqliteStack } from "../platform.js";
3
- import { Effect, Layer } from "effect";
4
- import { SqlClient } from "effect/sql/SqlClient";
2
+ import { PROJECT_MIGRATIONS } from "../migrations/index.js";
3
+ import { BUSY_TIMEOUT, LEDGER_OPTIONS, applyConnectionPragmas } from "../stores.js";
4
+ import { NodePlatformLayer } from "../platform.js";
5
+ import { Effect } from "effect";
6
+ import { Store } from "@effected/store";
5
7
 
6
8
  // src/utils/ensure-migrated.ts
7
- const GLOBAL_KEY = Symbol.for("vitest-agent/migration-promises");
8
- const getCache = () => {
9
- const g = globalThis;
10
- let cache = g[GLOBAL_KEY];
11
- if (!cache) {
12
- cache = /* @__PURE__ */ new Map();
13
- g[GLOBAL_KEY] = cache;
14
- }
15
- return cache;
16
- };
17
9
  /**
18
- * Ensure the SQLite database at `dbPath` is migrated. Runs migrations at
19
- * most once per dbPath in the current process; concurrent calls share the
20
- * same in-flight promise. Subsequent calls (after the first resolves) are
21
- * no-ops.
10
+ * The project migrations as `Store` runs them, built once so every call
11
+ * hands `Store.migrateOnce` the same list.
12
+ */
13
+ const PROJECT_STORE_MIGRATIONS = Store.fromMigratorRecord(PROJECT_MIGRATIONS);
14
+ /**
15
+ * Ensure the SQLite database at `dbPath` is migrated to the head of the
16
+ * project migration set.
17
+ *
18
+ * Runs through `@effected/store`'s `Store.migrateOnce`: at most one
19
+ * successful migration run per database per process, shared by every
20
+ * concurrent caller (and by every copy of the kit loaded in the process).
21
+ * The run is keyed on the real path of the file's directory, so a relative,
22
+ * an absolute and a symlinked spelling of one database share it. A failed
23
+ * run (for example `SQLITE_BUSY`) is not remembered: the next call tries
24
+ * again. The connection opens with the same per-connection settings as
25
+ * `makeSqliteStack` (`busy_timeout`, `synchronous = NORMAL`,
26
+ * `foreign_keys = ON`).
27
+ *
28
+ * The parent directory must exist; `dbPath` must name a file (`":memory:"`
29
+ * is a wiring defect).
22
30
  *
23
31
  * @param dbPath - the SQLite file to migrate
24
32
  * @param logLevel - the diagnostics level for the migrator's records; see `LoggerLive`
@@ -28,25 +36,13 @@ const getCache = () => {
28
36
  * @public
29
37
  */
30
38
  function ensureMigrated(dbPath, logLevel, logFile, env) {
31
- const cache = getCache();
32
- const cached = cache.get(dbPath);
33
- if (cached) return cached;
34
- const { SqliteLayer, MigratorLayer } = makeSqliteStack(dbPath);
35
- const program = Effect.gen(function* () {
36
- yield* SqlClient;
37
- }).pipe(Effect.provide(MigratorLayer), Effect.provide(Layer.merge(SqliteLayer, NodePlatformLayer)), Effect.provide(LoggerLive(logLevel, logFile, env)));
38
- const promise = Effect.runPromise(program);
39
- cache.set(dbPath, promise);
40
- promise.catch(() => {});
41
- return promise;
42
- }
43
- /**
44
- * Reset the migration cache. Test-only.
45
- *
46
- * @internal
47
- */
48
- function _resetMigrationCacheForTesting() {
49
- getCache().clear();
39
+ return Effect.runPromise(Store.migrateOnce({
40
+ filename: dbPath,
41
+ migrations: PROJECT_STORE_MIGRATIONS,
42
+ ...LEDGER_OPTIONS,
43
+ client: { busyTimeout: BUSY_TIMEOUT },
44
+ onConnect: (sql) => Effect.andThen(sql`PRAGMA synchronous = NORMAL`, applyConnectionPragmas(sql))
45
+ }).pipe(Effect.provide(NodePlatformLayer), Effect.provide(LoggerLive(logLevel, logFile, env))));
50
46
  }
51
47
 
52
- export { _resetMigrationCacheForTesting, ensureMigrated };
48
+ export { ensureMigrated };
@@ -0,0 +1,55 @@
1
+ import { Cause } from "effect";
2
+ import { CliFailure, Render } from "@effected/cli";
3
+
4
+ // src/utils/format-fatal-error.ts
5
+ const ISSUE_URL = "https://github.com/spencerbeggs/vitest-agent/issues";
6
+ /**
7
+ * Plain text for a person reading stderr: no escapes of any kind, Unicode
8
+ * glyphs, unbounded width (a terminal wraps a long line itself).
9
+ */
10
+ const CONTEXT = Render.contextOf({ audience: "human" });
11
+ /**
12
+ * Format an unexpected failure for stderr, with the issue URL appended.
13
+ *
14
+ * @remarks
15
+ * Under Effect v4, `Effect.runPromise` / `Effect.runSync` reject with
16
+ * `Cause.squash` of the failure: the error value itself, with no
17
+ * FiberFailure wrapper and no `Cause` attached. So a `Cause` is used as it
18
+ * is (one from `Effect.runPromiseExit` keeps every reason and the span
19
+ * trail), and anything else is rendered as a defect, `Cause.die(err)`: an
20
+ * unexpected throw is a bug to report, and a defect is the shape whose
21
+ * document carries the stack. `@effected/cli`'s `CliFailure.toDoc` keeps
22
+ * the program's own frames, hides `node_modules` and runtime frames,
23
+ * follows an `Error.cause` chain, and renders a non-`Error` value as its
24
+ * text.
25
+ *
26
+ * Total: `CliFailure.toDoc` guards the `stack`, `message`, `cause` and
27
+ * `issue` reads itself, but wrapping a `Proxy` whose traps throw can still
28
+ * throw, so any throw while formatting falls back to a fixed marker naming
29
+ * the value's constructor.
30
+ *
31
+ * @param err - the thrown value, rejection reason, or `Cause`
32
+ * @returns the formatted failure followed by the issue-report line
33
+ * @public
34
+ */
35
+ const formatFatalError = (err) => {
36
+ let detail;
37
+ try {
38
+ const cause = Cause.isCause(err) ? err : Cause.die(err);
39
+ detail = Render.plain(CliFailure.toDoc(cause), CONTEXT);
40
+ } catch {
41
+ detail = `<unserializable ${constructorName(err)}>`;
42
+ }
43
+ return `${detail}\nPlease report at ${ISSUE_URL}`;
44
+ };
45
+ /** The value's constructor name, guarded: an own `constructor` getter or a `Proxy` trap can throw too. */
46
+ const constructorName = (err) => {
47
+ try {
48
+ const name = err?.constructor?.name;
49
+ return typeof name === "string" && name !== "" ? name : "error";
50
+ } catch {
51
+ return "error";
52
+ }
53
+ };
54
+
55
+ export { formatFatalError };
@@ -0,0 +1,101 @@
1
+ import { Option } from "effect";
2
+ import { RuntimeEnv } from "@effected/env";
3
+
4
+ // src/utils/host-metadata.ts
5
+ /**
6
+ * The `ci_provider` string persisted for each CI whose job coordinate is
7
+ * recorded. These are this project's own spellings (`github`, `circle`),
8
+ * stored since 2.0; they are mapped from `CiName` rather than replaced by it
9
+ * so existing rows keep matching.
10
+ */
11
+ const CI_ROWS = {
12
+ "github-actions": {
13
+ provider: "github",
14
+ variable: "GITHUB_RUN_ID",
15
+ coordinate: "runId"
16
+ },
17
+ buildkite: {
18
+ provider: "buildkite",
19
+ variable: "BUILDKITE_JOB_ID",
20
+ coordinate: "jobId"
21
+ },
22
+ circleci: {
23
+ provider: "circle",
24
+ variable: "CIRCLE_BUILD_NUM",
25
+ coordinate: "jobId"
26
+ },
27
+ gitlab: {
28
+ provider: "gitlab",
29
+ variable: "CI_JOB_ID",
30
+ coordinate: "jobId"
31
+ }
32
+ };
33
+ const NONE = {
34
+ source: null,
35
+ value: null,
36
+ metadata: null
37
+ };
38
+ /**
39
+ * The host identity of an environment record, from `@effected/env`'s
40
+ * `RuntimeEnv` snapshot (`session`, `ci` / `ciRun`, `termProgram`).
41
+ *
42
+ * Priority: the terminal session (`TMUX_PANE`, `WT_SESSION`, `WEZTERM_PANE`,
43
+ * `KITTY_WINDOW_ID`, `TERM_SESSION_ID`), then the CI job (GitHub Actions run,
44
+ * Buildkite, CircleCI, GitLab job). The first match is the source; when
45
+ * nothing matches, all three fields are `null`. Every metadata object
46
+ * carries `term_program` / `term_program_version` when they are set, then
47
+ * the source's own keys (`tmux_socket` for a tmux pane, `ci`, `ci_provider`
48
+ * and `github_run_attempt` for CI), in that key order.
49
+ *
50
+ * GNU screen (`STY`) is not a source: the snapshot is taken with `STY`
51
+ * removed, so a screen session falls through to the next source exactly as
52
+ * it did before the kit, and no `STY` row is ever written. The
53
+ * `VSCODE_INJECTION` + parent-pid sources need `process.ppid` and are the
54
+ * caller's.
55
+ *
56
+ * @param env - the environment record to read (the front end passes `process.env`)
57
+ * @public
58
+ */
59
+ const probeHostMetadataFromEnv = (env) => {
60
+ const snapshot = RuntimeEnv.fromRecord({
61
+ ...env,
62
+ STY: void 0
63
+ });
64
+ const decoration = (extra) => ({
65
+ ...Option.match(snapshot.termProgram.name, {
66
+ onNone: () => ({}),
67
+ onSome: (name) => ({ term_program: name })
68
+ }),
69
+ ...Option.match(snapshot.termProgram.version, {
70
+ onNone: () => ({}),
71
+ onSome: (version) => ({ term_program_version: version })
72
+ }),
73
+ ...extra
74
+ });
75
+ if (Option.isSome(snapshot.session)) {
76
+ const session = snapshot.session.value;
77
+ const socket = session.source === "tmux" ? Option.filter(session.multiplexer, (multiplexer) => multiplexer.name === "tmux") : Option.none();
78
+ return {
79
+ source: session.variable,
80
+ value: session.id,
81
+ metadata: decoration(Option.isSome(socket) ? { tmux_socket: socket.value.value } : {})
82
+ };
83
+ }
84
+ if (Option.isNone(snapshot.ci) || Option.isNone(snapshot.ciRun)) return NONE;
85
+ const row = CI_ROWS[snapshot.ci.value];
86
+ if (row === void 0) return NONE;
87
+ const run = snapshot.ciRun.value;
88
+ const value = run[row.coordinate];
89
+ if (Option.isNone(value)) return NONE;
90
+ return {
91
+ source: row.variable,
92
+ value: value.value,
93
+ metadata: decoration({
94
+ ci: true,
95
+ ci_provider: row.provider,
96
+ ...row.provider === "github" && Option.isSome(run.attempt) ? { github_run_attempt: run.attempt.value } : {}
97
+ })
98
+ };
99
+ };
100
+
101
+ export { probeHostMetadataFromEnv };
@@ -0,0 +1,21 @@
1
+ import { Option, Schema } from "effect";
2
+ import { Repository } from "@effected/package-json";
3
+
4
+ // src/utils/package-manifest.ts
5
+ const decodeRepository = Schema.decodeUnknownOption(Repository.FromValue);
6
+ /**
7
+ * The trimmed `repository` URL a manifest names, from the string form or the
8
+ * object form's `url`, or `null` when there is none (absent, blank, an object
9
+ * with no string `url`, or a value of neither form).
10
+ *
11
+ * @internal
12
+ */
13
+ const repositoryUrlOf = (repository) => repository === void 0 ? null : Option.match(decodeRepository(repository), {
14
+ onNone: () => null,
15
+ onSome: (repo) => {
16
+ const url = repo.url.trim();
17
+ return url.length > 0 ? url : null;
18
+ }
19
+ });
20
+
21
+ export { repositoryUrlOf };
@@ -1,7 +1,7 @@
1
1
  import { VitestAgentConfigFile } from "../services/Config.js";
2
2
  import { resolveProjectKeyFromCwdEffect } from "./resolve-project-key-from-cwd.js";
3
- import { VitestAgentConfig, normalizeWorkspaceKey } from "@vitest-agent/sdk";
4
- import { Effect, FileSystem } from "effect";
3
+ import { ProjectIdentitySourceError, VitestAgentConfig } from "@vitest-agent/sdk";
4
+ import { Effect, FileSystem, Result } from "effect";
5
5
  import { AppDirs } from "@effected/xdg";
6
6
  import { join } from "node:path";
7
7
 
@@ -12,6 +12,20 @@ import { join } from "node:path";
12
12
  */
13
13
  const DATABASE_FILENAME = "data.db";
14
14
  /**
15
+ * The data directory name for a `projectKey` from `vitest-agent.config.toml`,
16
+ * or a `ProjectIdentitySourceError` (source `toml`) when it maps to `.` or
17
+ * `..` — the same refusal `ProjectIdentityLive` gives the same key.
18
+ */
19
+ const configuredKey = (projectKey, projectDir) => Result.match(AppDirs.segment(projectKey), {
20
+ onSuccess: Effect.succeed,
21
+ onFailure: (cause) => Effect.fail(new ProjectIdentitySourceError({
22
+ source: "toml",
23
+ path: projectDir,
24
+ detail: `projectKey ${JSON.stringify(projectKey)} is not a usable directory name (${cause.message})`,
25
+ cause
26
+ }))
27
+ });
28
+ /**
15
29
  * Resolve the absolute path to the SQLite database for `projectDir`.
16
30
  *
17
31
  * The directory containing the database is ensured to exist before the
@@ -23,7 +37,10 @@ const DATABASE_FILENAME = "data.db";
23
37
  *
24
38
  * 1. `options.cacheDir` — programmatic override.
25
39
  * 2. `cacheDir` from `vitest-agent.config.toml`.
26
- * 3. `<XDG data>/<normalized projectKey from config>/data.db`.
40
+ * 3. `<XDG data>/<normalized projectKey from config>/data.db`. A
41
+ * `projectKey` that normalizes to `.` or `..` fails with
42
+ * `ProjectIdentitySourceError` (source `toml`) rather than writing
43
+ * outside the data root.
27
44
  * 4. `<XDG data>/<projectKey resolved from cwd's package.json>/data.db`.
28
45
  * Source 4 prefers `repository.url` (canonicalized to `host__path`),
29
46
  * falling back to the normalized `name`. This matches the sidecar
@@ -54,7 +71,7 @@ const resolveDataPath = (projectDir, options = {}) => Effect.gen(function* () {
54
71
  return join(loaded.cacheDir, DATABASE_FILENAME);
55
72
  }
56
73
  const dataRoot = yield* (yield* AppDirs).ensureData;
57
- const key = loaded.projectKey ? normalizeWorkspaceKey(loaded.projectKey) : yield* resolveProjectKeyFromCwdEffect(projectDir);
74
+ const key = loaded.projectKey ? yield* configuredKey(loaded.projectKey, projectDir) : yield* resolveProjectKeyFromCwdEffect(projectDir);
58
75
  const dir = join(dataRoot, key);
59
76
  yield* fs.makeDirectory(dir, { recursive: true });
60
77
  return join(dir, DATABASE_FILENAME);
@@ -1,49 +1,40 @@
1
- import { gitUrlToProjectKey, normalizeWorkspaceKey } from "@vitest-agent/sdk";
2
- import { Effect, FileSystem } from "effect";
3
- import { dirname, join } from "node:path";
4
- import { existsSync, readFileSync } from "node:fs";
1
+ import { repositoryUrlOf } from "./package-manifest.js";
2
+ import { gitUrlToProjectKey } from "@vitest-agent/sdk";
3
+ import { Effect, FileSystem, Option, Path, Result } from "effect";
4
+ import { AppDirs } from "@effected/xdg";
5
+ import { join } from "node:path";
6
+ import { Walker } from "@effected/walker";
7
+ import { LenientManifest } from "@effected/package-json";
8
+ import { readFileSync } from "node:fs";
9
+ import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
5
10
 
6
11
  // src/utils/resolve-project-key-from-cwd.ts
7
12
  const PACKAGE_JSON = "package.json";
8
13
  const findNearestPackageJson = (startDir) => {
9
- let dir = startDir;
10
- while (true) {
11
- const candidate = join(dir, PACKAGE_JSON);
12
- if (existsSync(candidate)) return candidate;
13
- const parent = dirname(dir);
14
- if (parent === dir) return null;
15
- dir = parent;
16
- }
17
- };
18
- const readRepositoryUrl = (parsed) => {
19
- const repo = parsed.repository;
20
- if (typeof repo === "string" && repo.trim().length > 0) return repo.trim();
21
- if (repo !== null && typeof repo === "object" && "url" in repo && typeof repo.url === "string") {
22
- const url = repo.url.trim();
23
- return url.length > 0 ? url : null;
24
- }
25
- return null;
14
+ const dirs = Walker.ascendSync(startDir, { path: nodeWalkerSyncOps.path });
15
+ return Option.getOrNull(Walker.findUpwardSync(dirs, (dir) => [join(dir, PACKAGE_JSON)], nodeWalkerSyncOps));
26
16
  };
27
17
  /**
28
18
  * The key a `package.json` body names: the canonical `host__path` form of
29
19
  * `repository.url` when present and canonicalizable, else the normalized
30
- * `name`, else `null` (no usable field, or malformed JSON).
20
+ * `name`, else `null` (no usable field, malformed JSON, or JSON that is not
21
+ * an object).
31
22
  */
32
23
  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 {}
24
+ const parsed = LenientManifest.parseResult(content);
25
+ if (Result.isFailure(parsed)) return null;
26
+ const manifest = parsed.success;
27
+ const repoUrl = repositoryUrlOf(manifest.repository);
28
+ if (repoUrl !== null) {
29
+ const key = gitUrlToProjectKey(repoUrl);
30
+ if (key !== null) return key;
31
+ }
32
+ if (manifest.name !== void 0) return Result.getOrNull(AppDirs.segment(manifest.name));
42
33
  return null;
43
34
  };
44
35
  const basenameKey = (cwd) => {
45
36
  const basename = cwd.split("/").filter((segment) => segment.length > 0).pop() ?? "anonymous-project";
46
- return normalizeWorkspaceKey(basename);
37
+ return Result.getOrElse(AppDirs.segment(basename), () => "anonymous-project");
47
38
  };
48
39
  /**
49
40
  * Compute the project key for a workspace by reading `package.json`
@@ -74,7 +65,8 @@ const resolveProjectKeyFromCwd = (cwd) => {
74
65
  /**
75
66
  * {@link resolveProjectKeyFromCwd} read through the ambient
76
67
  * `FileSystem` service instead of `node:fs` — same upward walk to the
77
- * nearest `package.json`, same precedence, same basename fallback.
68
+ * nearest `package.json` (`@effected/walker`'s `Walker.ascend` +
69
+ * `Walker.findUpward`), same precedence, same basename fallback.
78
70
  *
79
71
  * Never fails: a filesystem error while probing or reading is treated like
80
72
  * a missing or malformed `package.json`, exactly as the synchronous form
@@ -85,17 +77,11 @@ const resolveProjectKeyFromCwd = (cwd) => {
85
77
  */
86
78
  const resolveProjectKeyFromCwdEffect = (cwd) => Effect.gen(function* () {
87
79
  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
- });
80
+ const dirs = yield* Walker.ascend(cwd);
81
+ const found = yield* Walker.findUpward(dirs, (dir) => [join(dir, PACKAGE_JSON)]);
82
+ if (Option.isNone(found)) return basenameKey(cwd);
83
+ const content = yield* fs.readFileString(found.value).pipe(Effect.orElseSucceed(() => null));
84
+ return (content === null ? null : keyFromPackageJson(content)) ?? basenameKey(cwd);
85
+ }).pipe(Effect.provide(Path.layer));
100
86
 
101
87
  export { resolveProjectKeyFromCwd, resolveProjectKeyFromCwdEffect };
@@ -1,6 +1,6 @@
1
- import { normalizeWorkspaceKey } from "@vitest-agent/sdk";
2
- import { Effect } from "effect";
1
+ import { Effect, Result } from "effect";
3
2
  import { WorkspaceDiscovery, WorkspaceRootNotFoundError } from "@effected/workspaces";
3
+ import { AppDirs } from "@effected/xdg";
4
4
 
5
5
  // src/utils/resolve-workspace-key.ts
6
6
  /**
@@ -8,7 +8,8 @@ import { WorkspaceDiscovery, WorkspaceRootNotFoundError } from "@effected/worksp
8
8
  * `projectDir`.
9
9
  *
10
10
  * The key is the root `package.json`'s `name`, normalized via
11
- * `normalizeWorkspaceKey`. This becomes the directory segment under
11
+ * `@effected/xdg`'s `AppDirs.segment` (a name that maps to `.` or `..` fails
12
+ * with `AppDirsSegmentError`). This becomes the directory segment under
12
13
  * `$XDG_DATA_HOME/vitest-agent/` where the SQLite database lives.
13
14
  *
14
15
  * Fails with `WorkspaceRootNotFoundError` when no root workspace is
@@ -34,7 +35,10 @@ const resolveWorkspaceKey = (projectDir) => Effect.gen(function* () {
34
35
  searchPath: projectDir,
35
36
  markers: []
36
37
  }));
37
- return normalizeWorkspaceKey(root.name);
38
+ return yield* Result.match(AppDirs.segment(root.name), {
39
+ onSuccess: Effect.succeed,
40
+ onFailure: Effect.fail
41
+ });
38
42
  });
39
43
 
40
44
  export { resolveWorkspaceKey };
package/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
2
  /** Build-time injected package version (see architecture.md D36). @public */
3
- const CURRENT_ENGINE_VERSION = "2.1.0";
3
+ const CURRENT_ENGINE_VERSION = "3.0.1";
4
4
 
5
5
  export { CURRENT_ENGINE_VERSION };