@vitest-agent/sdk 3.1.1 → 4.0.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.
Files changed (66) hide show
  1. package/README.md +23 -19
  2. package/dispatch.d.ts +25 -6
  3. package/formatters/markdown.js +1 -0
  4. package/formatters/terminal.js +4 -3
  5. package/index.d.ts +448 -2341
  6. package/index.js +10 -68
  7. package/internal-inject-env.js +5 -7
  8. package/package.json +3 -19
  9. package/schemas/run-report-file-1.0.0.json +18 -18
  10. package/sidecar-dispatch.js +7 -5
  11. package/utils/format-console.js +9 -10
  12. package/utils/format-terminal.js +7 -7
  13. package/utils/posix-path.js +52 -0
  14. package/utils/test-location.js +11 -5
  15. package/version.js +17 -0
  16. package/layers/ConfigLive.js +0 -46
  17. package/layers/DataReaderLive.js +0 -1923
  18. package/layers/DataStoreLive.js +0 -1287
  19. package/layers/DetailResolverLive.js +0 -9
  20. package/layers/DiscoveryRegistryLive.js +0 -70
  21. package/layers/EnvironmentDetectorLive.js +0 -21
  22. package/layers/EnvironmentDetectorTest.js +0 -13
  23. package/layers/ExecutorResolverLive.js +0 -9
  24. package/layers/FormatSelectorLive.js +0 -20
  25. package/layers/HistoryTrackerLive.js +0 -49
  26. package/layers/HistoryTrackerTest.js +0 -25
  27. package/layers/LoggerLive.js +0 -64
  28. package/layers/OutputPipelineLive.js +0 -13
  29. package/layers/OutputRendererLive.js +0 -28
  30. package/layers/PathResolutionLive.js +0 -40
  31. package/layers/PerClientSessionMapLive.js +0 -145
  32. package/layers/ProjectDiscoveryLive.js +0 -68
  33. package/layers/ProjectDiscoveryTest.js +0 -12
  34. package/layers/ProjectIdentityLive.js +0 -96
  35. package/layers/RunContextLive.js +0 -81
  36. package/lib/format-triage.js +0 -103
  37. package/lib/format-wrapup.js +0 -67
  38. package/migrations/0001_initial.js +0 -824
  39. package/migrations/0002_test_artifacts.js +0 -30
  40. package/migrations/index.js +0 -26
  41. package/migrations/registry_0001_initial.js +0 -39
  42. package/migrations/session_map_0001_initial.js +0 -38
  43. package/services/Config.js +0 -15
  44. package/services/DataReader.js +0 -8
  45. package/services/DataStore.js +0 -15
  46. package/services/DetailResolver.js +0 -8
  47. package/services/DiscoveryRegistry.js +0 -8
  48. package/services/EnvironmentDetector.js +0 -8
  49. package/services/ExecutorResolver.js +0 -8
  50. package/services/FormatSelector.js +0 -8
  51. package/services/HistoryTracker.js +0 -20
  52. package/services/OutputRenderer.js +0 -8
  53. package/services/PerClientSessionMap.js +0 -22
  54. package/services/ProjectDiscovery.js +0 -8
  55. package/services/ProjectIdentity.js +0 -63
  56. package/services/RunContext.js +0 -53
  57. package/services/idempotency.js +0 -58
  58. package/sql/assemblers.js +0 -69
  59. package/testing/layers.js +0 -21
  60. package/testing.d.ts +0 -1862
  61. package/testing.js +0 -254
  62. package/utils/ensure-migrated.js +0 -52
  63. package/utils/failure-signature.js +0 -34
  64. package/utils/resolve-data-path.js +0 -65
  65. package/utils/resolve-project-key-from-cwd.js +0 -53
  66. package/utils/resolve-workspace-key.js +0 -41
@@ -1,9 +0,0 @@
1
- import { DetailResolver } from "../services/DetailResolver.js";
2
- import { Effect, Layer } from "effect";
3
-
4
- //#region src/layers/DetailResolverLive.ts
5
- /** @public */
6
- const DetailResolverLive = Layer.succeed(DetailResolver, { resolve: (executor, health, explicit) => Effect.succeed(explicit ?? (executor === "agent" || executor === "ci" ? "verbose" : health.hasFailures ? "verbose" : health.belowTargets ? "standard" : health.hasTargets === false ? "neutral" : "minimal")) });
7
-
8
- //#endregion
9
- export { DetailResolverLive };
@@ -1,70 +0,0 @@
1
- import { DataStoreError, extractSqlReason } from "../errors/DataStoreError.js";
2
- import { DiscoveryRegistry } from "../services/DiscoveryRegistry.js";
3
- import { Effect, Layer } from "effect";
4
- import { SqlClient } from "effect/unstable/sql/SqlClient";
5
-
6
- //#region src/layers/DiscoveryRegistryLive.ts
7
- const TABLE = "known_projects";
8
- const rowToDomain = (row) => ({
9
- projectKey: row.project_key,
10
- canonicalForm: row.canonical_form,
11
- dataDbPath: row.data_db_path,
12
- gitRemoteOrigin: row.git_remote_origin,
13
- workspaceRoot: row.workspace_root,
14
- firstSeenAt: row.first_seen_at,
15
- lastSeenAt: row.last_seen_at
16
- });
17
- const dayInMs = 864e5;
18
- /** @public */
19
- const DiscoveryRegistryLive = Layer.effect(DiscoveryRegistry, Effect.gen(function* () {
20
- const sql = yield* SqlClient;
21
- return {
22
- recordProject: (input) => Effect.gen(function* () {
23
- const now = Date.now();
24
- yield* sql`
25
- INSERT INTO ${sql(TABLE)} (
26
- project_key, canonical_form, data_db_path, git_remote_origin, workspace_root,
27
- first_seen_at, last_seen_at
28
- ) VALUES (
29
- ${input.projectKey}, ${input.canonicalForm}, ${input.dataDbPath},
30
- ${input.gitRemoteOrigin}, ${input.workspaceRoot}, ${now}, ${now}
31
- )
32
- ON CONFLICT(project_key) DO UPDATE SET
33
- canonical_form = excluded.canonical_form,
34
- data_db_path = excluded.data_db_path,
35
- git_remote_origin = excluded.git_remote_origin,
36
- workspace_root = excluded.workspace_root,
37
- last_seen_at = excluded.last_seen_at
38
- `;
39
- }).pipe(Effect.mapError((e) => new DataStoreError({
40
- operation: "write",
41
- table: TABLE,
42
- reason: extractSqlReason(e)
43
- }))),
44
- listProjects: () => Effect.gen(function* () {
45
- return (yield* sql`
46
- SELECT project_key, canonical_form, data_db_path, git_remote_origin, workspace_root,
47
- first_seen_at, last_seen_at
48
- FROM ${sql(TABLE)}
49
- ORDER BY last_seen_at DESC, rowid DESC
50
- `).map(rowToDomain);
51
- }).pipe(Effect.mapError((e) => new DataStoreError({
52
- operation: "read",
53
- table: TABLE,
54
- reason: extractSqlReason(e)
55
- }))),
56
- prune: (maxAgeDays) => Effect.gen(function* () {
57
- const cutoff = Date.now() - maxAgeDays * dayInMs;
58
- const removable = (yield* sql`SELECT COUNT(*) AS c FROM ${sql(TABLE)} WHERE last_seen_at < ${cutoff}`)[0]?.c ?? 0;
59
- if (removable > 0) yield* sql`DELETE FROM ${sql(TABLE)} WHERE last_seen_at < ${cutoff}`;
60
- return removable;
61
- }).pipe(Effect.mapError((e) => new DataStoreError({
62
- operation: "write",
63
- table: TABLE,
64
- reason: extractSqlReason(e)
65
- })))
66
- };
67
- }));
68
-
69
- //#endregion
70
- export { DiscoveryRegistryLive };
@@ -1,21 +0,0 @@
1
- import { EnvironmentDetector } from "../services/EnvironmentDetector.js";
2
- import { Effect, Layer } from "effect";
3
- import { agent, isAgent } from "std-env";
4
-
5
- //#region src/layers/EnvironmentDetectorLive.ts
6
- const isGitHub = () => process.env.GITHUB_ACTIONS === "true" || process.env.GITHUB_ACTIONS === "1";
7
- const isCI = () => isGitHub() || process.env.CI === "true";
8
- /** @public */
9
- const EnvironmentDetectorLive = Layer.succeed(EnvironmentDetector, {
10
- detect: () => Effect.sync(() => {
11
- if (isAgent) return "agent-shell";
12
- if (isGitHub()) return "ci-github";
13
- if (isCI()) return "ci-generic";
14
- return "terminal";
15
- }),
16
- isAgent: Effect.sync(() => isAgent),
17
- agentName: Effect.sync(() => agent ?? void 0)
18
- });
19
-
20
- //#endregion
21
- export { EnvironmentDetectorLive };
@@ -1,13 +0,0 @@
1
- import { EnvironmentDetector } from "../services/EnvironmentDetector.js";
2
- import { Effect, Layer } from "effect";
3
-
4
- //#region src/layers/EnvironmentDetectorTest.ts
5
- /** @public */
6
- const EnvironmentDetectorTest = { layer: (env = "terminal") => Layer.succeed(EnvironmentDetector, {
7
- detect: () => Effect.succeed(env),
8
- isAgent: Effect.succeed(env === "agent-shell"),
9
- agentName: Effect.succeed(env === "agent-shell" ? "test-agent" : void 0)
10
- }) };
11
-
12
- //#endregion
13
- export { EnvironmentDetectorTest };
@@ -1,9 +0,0 @@
1
- import { ExecutorResolver } from "../services/ExecutorResolver.js";
2
- import { Effect, Layer } from "effect";
3
-
4
- //#region src/layers/ExecutorResolverLive.ts
5
- /** @public */
6
- const ExecutorResolverLive = Layer.succeed(ExecutorResolver, { resolve: (env) => Effect.succeed(env === "agent-shell" ? "agent" : env === "terminal" ? "human" : "ci") });
7
-
8
- //#endregion
9
- export { ExecutorResolverLive };
@@ -1,20 +0,0 @@
1
- import { FormatSelector } from "../services/FormatSelector.js";
2
- import { Effect, Layer } from "effect";
3
-
4
- //#region src/layers/FormatSelectorLive.ts
5
- /**
6
- * Default format dispatch:
7
- *
8
- * - GitHub Actions CI executor: `ci-annotations` (renders as `::error::`).
9
- * - Human executor: `silent` (Vitest's own reporters handle the user
10
- * experience; we just write to the database).
11
- * - Anyone else (agent-shell, terminal-bound CI): `terminal` — plain
12
- * text + ANSI color, no markdown syntax in the place that doesn't
13
- * render it. Markdown stays available via explicit `format: "markdown"`
14
- * for surfaces that *do* render it (MCP tools, GitHub step summary).
15
- * @public
16
- */
17
- const FormatSelectorLive = Layer.succeed(FormatSelector, { select: (executor, explicitFormat, environment) => Effect.succeed(explicitFormat ?? (environment === "ci-github" && executor === "ci" ? "ci-annotations" : executor === "human" ? "silent" : "terminal")) });
18
-
19
- //#endregion
20
- export { FormatSelectorLive };
@@ -1,49 +0,0 @@
1
- import { DataReader } from "../services/DataReader.js";
2
- import { HistoryTracker, historyKey } from "../services/HistoryTracker.js";
3
- import { classifyTest } from "../utils/classify-test.js";
4
- import { Effect, Layer } from "effect";
5
-
6
- //#region src/layers/HistoryTrackerLive.ts
7
- const WINDOW_SIZE = 10;
8
- /** @public */
9
- const HistoryTrackerLive = Layer.effect(HistoryTracker, Effect.gen(function* () {
10
- const reader = yield* DataReader;
11
- return { classify: (project, testOutcomes, timestamp) => Effect.gen(function* () {
12
- const existing = yield* reader.getHistory(project);
13
- const testMap = /* @__PURE__ */ new Map();
14
- for (const entry of existing.tests) testMap.set(historyKey(entry.modulePath, entry.fullName), {
15
- ...entry,
16
- runs: [...entry.runs]
17
- });
18
- const classifications = /* @__PURE__ */ new Map();
19
- for (const outcome of testOutcomes) {
20
- const key = historyKey(outcome.modulePath, outcome.fullName);
21
- let entry = testMap.get(key);
22
- if (!entry) {
23
- entry = {
24
- modulePath: outcome.modulePath,
25
- fullName: outcome.fullName,
26
- runs: []
27
- };
28
- testMap.set(key, entry);
29
- }
30
- const priorRuns = entry.runs;
31
- entry.runs = [{
32
- timestamp,
33
- state: outcome.state
34
- }, ...priorRuns].slice(0, WINDOW_SIZE);
35
- classifications.set(key, classifyTest(outcome.state, priorRuns));
36
- }
37
- return {
38
- history: {
39
- project,
40
- updatedAt: timestamp,
41
- tests: Array.from(testMap.values())
42
- },
43
- classifications
44
- };
45
- }) };
46
- }));
47
-
48
- //#endregion
49
- export { HistoryTrackerLive };
@@ -1,25 +0,0 @@
1
- import { HistoryTracker } from "../services/HistoryTracker.js";
2
- import { Effect, Layer } from "effect";
3
-
4
- //#region src/layers/HistoryTrackerTest.ts
5
- /** @public */
6
- const HistoryTrackerTest = {
7
- empty: () => ({ classifyCalls: [] }),
8
- layer: (state, cannedResult) => Layer.succeed(HistoryTracker, { classify: (project, _outcomes, timestamp) => Effect.sync(() => {
9
- state.classifyCalls.push({
10
- project,
11
- timestamp
12
- });
13
- return cannedResult ?? {
14
- history: {
15
- project,
16
- updatedAt: timestamp,
17
- tests: []
18
- },
19
- classifications: /* @__PURE__ */ new Map()
20
- };
21
- }) })
22
- };
23
-
24
- //#endregion
25
- export { HistoryTrackerTest };
@@ -1,64 +0,0 @@
1
- import { Layer, LogLevel, Logger, References } from "effect";
2
- import { appendFileSync } from "node:fs";
3
-
4
- //#region src/layers/LoggerLive.ts
5
- /**
6
- * Create a structured JSON (NDJSON) logger layer for stderr.
7
- *
8
- * When level is undefined or `"None"`, installs an empty logger set (silent).
9
- * When logFile is set, composes the stderr logger with a file logger.
10
- * Uses `Logger.formatJson` / `Logger.formatStructured` for machine-readable
11
- * output.
12
- * @public
13
- */
14
- const LoggerLive = (level, logFile) => {
15
- if (!level || level === "None") return Logger.layer([]);
16
- const stderrLogger = Logger.formatJson.pipe(Logger.withConsoleError);
17
- const fileLogger = logFile ? Logger.formatStructured.pipe(Logger.map((entry) => {
18
- const line = JSON.stringify({
19
- timestamp: entry.timestamp,
20
- level: entry.level,
21
- message: entry.message,
22
- ...entry.annotations
23
- });
24
- try {
25
- appendFileSync(logFile, `${line}\n`);
26
- } catch {}
27
- return line;
28
- })) : void 0;
29
- const loggers = fileLogger ? [stderrLogger, fileLogger] : [stderrLogger];
30
- return Layer.merge(Logger.layer(loggers), Layer.succeed(References.MinimumLogLevel, level));
31
- };
32
- /**
33
- * Resolve log level from option or environment variable.
34
- * Priority: explicit option \> VITEST_REPORTER_LOG_LEVEL env var \> undefined
35
- * @public
36
- */
37
- const LEVEL_ALIASES = {
38
- warn: "Warn",
39
- warning: "Warn",
40
- error: "Error",
41
- info: "Info",
42
- debug: "Debug",
43
- trace: "Trace",
44
- fatal: "Fatal",
45
- all: "All",
46
- none: "None"
47
- };
48
- /** @public */
49
- function resolveLogLevel(option) {
50
- const raw = option ?? process.env.VITEST_REPORTER_LOG_LEVEL;
51
- if (!raw) return void 0;
52
- const normalized = LEVEL_ALIASES[raw.toLowerCase()] ?? `${raw.charAt(0).toUpperCase()}${raw.slice(1).toLowerCase()}`;
53
- return LogLevel.values.includes(normalized) ? normalized : void 0;
54
- }
55
- /**
56
- * Resolve log file from option or environment variable.
57
- * @public
58
- */
59
- function resolveLogFile(option) {
60
- return option ?? process.env.VITEST_REPORTER_LOG_FILE ?? void 0;
61
- }
62
-
63
- //#endregion
64
- export { LoggerLive, resolveLogFile, resolveLogLevel };
@@ -1,13 +0,0 @@
1
- import { DetailResolverLive } from "./DetailResolverLive.js";
2
- import { EnvironmentDetectorLive } from "./EnvironmentDetectorLive.js";
3
- import { ExecutorResolverLive } from "./ExecutorResolverLive.js";
4
- import { FormatSelectorLive } from "./FormatSelectorLive.js";
5
- import { OutputRendererLive } from "./OutputRendererLive.js";
6
- import { Layer } from "effect";
7
-
8
- //#region src/layers/OutputPipelineLive.ts
9
- /** @public */
10
- const OutputPipelineLive = Layer.mergeAll(EnvironmentDetectorLive, ExecutorResolverLive, FormatSelectorLive, DetailResolverLive, OutputRendererLive);
11
-
12
- //#endregion
13
- export { OutputPipelineLive };
@@ -1,28 +0,0 @@
1
- import { ciAnnotationsFormatter } from "../formatters/ci-annotations.js";
2
- import { GfmFormatter } from "../formatters/gfm.js";
3
- import { JsonFormatter } from "../formatters/json.js";
4
- import { MarkdownFormatter } from "../formatters/markdown.js";
5
- import { SilentFormatter } from "../formatters/silent.js";
6
- import { TerminalFormatter } from "../formatters/terminal.js";
7
- import { OutputRenderer } from "../services/OutputRenderer.js";
8
- import { Effect, Layer } from "effect";
9
-
10
- //#region src/layers/OutputRendererLive.ts
11
- const formatters = /* @__PURE__ */ new Map([
12
- ["terminal", TerminalFormatter],
13
- ["markdown", MarkdownFormatter],
14
- ["gfm", GfmFormatter],
15
- ["json", JsonFormatter],
16
- ["silent", SilentFormatter],
17
- ["vitest-bypass", SilentFormatter],
18
- ["ci-annotations", ciAnnotationsFormatter]
19
- ]);
20
- /** @public */
21
- const OutputRendererLive = Layer.succeed(OutputRenderer, { render: (reports, format, context) => Effect.sync(() => {
22
- const formatter = formatters.get(format);
23
- if (!formatter) return [];
24
- return formatter.render(reports, context);
25
- }) });
26
-
27
- //#endregion
28
- export { OutputRendererLive };
@@ -1,40 +0,0 @@
1
- import { ConfigLive } from "./ConfigLive.js";
2
- import { Layer } from "effect";
3
- import { WorkspaceDiscovery, WorkspaceRoot } from "@effected/workspaces";
4
- import { AppDirs, Xdg } from "@effected/xdg";
5
-
6
- //#region src/layers/PathResolutionLive.ts
7
- /**
8
- * `AppDirs` over the ambient `Xdg` environment, bound to a `const` so the
9
- * layer memoizes by reference (calling `AppDirs.layer(...)` inline at two
10
- * provide sites would resolve two independent services). Requires
11
- * `FileSystem` + `Path` at the edge.
12
- */
13
- const AppDirsLive = AppDirs.layer({ namespace: "vitest-agent" }).pipe(Layer.provide(Xdg.layer));
14
- /**
15
- * Composite layer providing every service `resolveDataPath` requires:
16
- * `AppDirs` (XDG path resolution), `VitestAgentConfigFile` (TOML
17
- * config loader), and `WorkspaceDiscovery` / `WorkspaceRoot` (workspace name
18
- * lookup).
19
- *
20
- * Callers still need to provide `FileSystem` and `Path` from
21
- * `@effect/platform-node`'s `NodeServices.layer` (or the equivalent on Bun).
22
- *
23
- * `WorkspaceDiscovery` is pinned to `projectDir` as its `cwd`, which sets the
24
- * root for the layer-bound methods (`listPackages`, `info`, `refresh`). The
25
- * per-root methods (`listPackagesIn`, `infoIn`, `refreshIn`) take a directory
26
- * per call and ignore that pin — `resolveWorkspaceKey` and
27
- * `ProjectIdentityLive` use them, so a long-lived host built against one root
28
- * still answers correctly for a git worktree or an unrelated project.
29
- *
30
- * @param projectDir - Absolute path inside the user's workspace, used to
31
- * anchor the config file resolvers and workspace discovery.
32
- * @public
33
- */
34
- const PathResolutionLive = (projectDir) => {
35
- const WorkspaceMinimalLive = WorkspaceDiscovery.layer({ cwd: projectDir }).pipe(Layer.provide(WorkspaceRoot.layer));
36
- return Layer.mergeAll(AppDirsLive, ConfigLive(projectDir), WorkspaceMinimalLive);
37
- };
38
-
39
- //#endregion
40
- export { PathResolutionLive };
@@ -1,145 +0,0 @@
1
- import { DataStoreError, extractSqlReason } from "../errors/DataStoreError.js";
2
- import { PerClientSessionMapReader, PerClientSessionMapWriter } from "../services/PerClientSessionMap.js";
3
- import { Effect, Layer, Option } from "effect";
4
- import { basename } from "node:path";
5
- import { SqlClient } from "effect/unstable/sql/SqlClient";
6
- import { randomUUID } from "node:crypto";
7
-
8
- //#region src/layers/PerClientSessionMapLive.ts
9
- const sessionRowToContext = (row) => ({
10
- hostSessionId: row.host_session_id,
11
- conversationId: row.conversation_id,
12
- projectKey: row.project_key,
13
- projectDir: row.project_dir,
14
- mainAgentId: row.main_agent_id,
15
- startedAt: row.started_at,
16
- endedAt: row.ended_at
17
- });
18
- /**
19
- * Extract the transcript UUID from a transcript file path. Claude
20
- * Code's transcript filenames are the conversation UUID directly
21
- * (with optional `.jsonl` suffix), so the basename is the right
22
- * stable key — path normalization differences (symlinks, user
23
- * renames) don't fragment the table.
24
- */
25
- const extractTranscriptUuid = (transcriptPath) => {
26
- const base = basename(transcriptPath);
27
- const dotIndex = base.lastIndexOf(".");
28
- return dotIndex === -1 ? base : base.slice(0, dotIndex);
29
- };
30
- const lookupConversationImpl = (sql, transcriptPath) => Effect.gen(function* () {
31
- const rows = yield* sql`
32
- SELECT transcript_uuid, transcript_path, conversation_id, first_seen_at, last_seen_at
33
- FROM conversation_map WHERE transcript_uuid = ${extractTranscriptUuid(transcriptPath)}
34
- `;
35
- return rows.length === 0 ? Option.none() : Option.some(rows[0].conversation_id);
36
- }).pipe(Effect.mapError((e) => new DataStoreError({
37
- operation: "read",
38
- table: "conversation_map",
39
- reason: extractSqlReason(e)
40
- })));
41
- const lookupByProjectDirImpl = (sql, projectDir) => Effect.gen(function* () {
42
- const rows = yield* sql`
43
- SELECT host_session_id, conversation_id, project_key, project_dir, main_agent_id, started_at, ended_at
44
- FROM session_map
45
- WHERE project_dir = ${projectDir} AND ended_at IS NULL
46
- ORDER BY started_at DESC
47
- LIMIT 1
48
- `;
49
- return rows.length === 0 ? Option.none() : Option.some(sessionRowToContext(rows[0]));
50
- }).pipe(Effect.mapError((e) => new DataStoreError({
51
- operation: "read",
52
- table: "session_map",
53
- reason: extractSqlReason(e)
54
- })));
55
- /**
56
- * Read-only layer. The entry point provides an `SqlClient` configured
57
- * with the `?mode=ro` URI parameter; this layer wraps the read
58
- * methods and never declares write methods.
59
- * @public
60
- */
61
- const PerClientSessionMapReaderLive = Layer.effect(PerClientSessionMapReader, Effect.gen(function* () {
62
- const sql = yield* SqlClient;
63
- return {
64
- lookupConversation: (path) => lookupConversationImpl(sql, path),
65
- lookupByProjectDir: (dir) => lookupByProjectDirImpl(sql, dir)
66
- };
67
- }));
68
- /**
69
- * Read-write layer. Provides both write methods and the Reader's read
70
- * methods so the same layer instance satisfies callers of either
71
- * tag.
72
- * @public
73
- */
74
- const PerClientSessionMapWriterLive = Layer.effect(PerClientSessionMapWriter, Effect.gen(function* () {
75
- const sql = yield* SqlClient;
76
- const mapConversation = (transcriptPath) => Effect.gen(function* () {
77
- const uuid = extractTranscriptUuid(transcriptPath);
78
- const existing = yield* sql`
79
- SELECT conversation_id FROM conversation_map WHERE transcript_uuid = ${uuid}
80
- `;
81
- const now = Date.now();
82
- if (existing.length > 0) {
83
- yield* sql`UPDATE conversation_map SET last_seen_at = ${now} WHERE transcript_uuid = ${uuid}`;
84
- return existing[0].conversation_id;
85
- }
86
- const conversationId = randomUUID();
87
- yield* sql`
88
- INSERT INTO conversation_map (transcript_uuid, transcript_path, conversation_id, first_seen_at, last_seen_at)
89
- VALUES (${uuid}, ${transcriptPath}, ${conversationId}, ${now}, ${now})
90
- `;
91
- return conversationId;
92
- }).pipe(Effect.mapError((e) => new DataStoreError({
93
- operation: "write",
94
- table: "conversation_map",
95
- reason: extractSqlReason(e)
96
- })));
97
- const mapSession = (input) => Effect.gen(function* () {
98
- const existing = yield* sql`
99
- SELECT main_agent_id, conversation_id FROM session_map WHERE host_session_id = ${input.hostSessionId}
100
- `;
101
- if (existing.length > 0) return {
102
- mainAgentId: existing[0].main_agent_id,
103
- conversationId: existing[0].conversation_id
104
- };
105
- const mainAgentId = randomUUID();
106
- const now = Date.now();
107
- yield* sql`
108
- INSERT INTO session_map (
109
- host_session_id, conversation_id, project_key, project_dir, main_agent_id, started_at, ended_at
110
- ) VALUES (
111
- ${input.hostSessionId}, ${input.conversationId}, ${input.projectKey}, ${input.projectDir},
112
- ${mainAgentId}, ${now}, ${null}
113
- )
114
- `;
115
- return {
116
- mainAgentId,
117
- conversationId: input.conversationId
118
- };
119
- }).pipe(Effect.mapError((e) => new DataStoreError({
120
- operation: "write",
121
- table: "session_map",
122
- reason: extractSqlReason(e)
123
- })));
124
- const endSession = (hostSessionId, endedAt) => sql`UPDATE session_map SET ended_at = ${endedAt} WHERE host_session_id = ${hostSessionId}`.pipe(Effect.asVoid, Effect.mapError((e) => new DataStoreError({
125
- operation: "write",
126
- table: "session_map",
127
- reason: extractSqlReason(e)
128
- })));
129
- return {
130
- mapConversation,
131
- mapSession,
132
- endSession,
133
- lookupConversation: (path) => lookupConversationImpl(sql, path),
134
- lookupByProjectDir: (dir) => lookupByProjectDirImpl(sql, dir)
135
- };
136
- })).pipe(Layer.provideMerge(Layer.effect(PerClientSessionMapReader, Effect.gen(function* () {
137
- const sql = yield* SqlClient;
138
- return {
139
- lookupConversation: (path) => lookupConversationImpl(sql, path),
140
- lookupByProjectDir: (dir) => lookupByProjectDirImpl(sql, dir)
141
- };
142
- }))));
143
-
144
- //#endregion
145
- export { PerClientSessionMapReaderLive, PerClientSessionMapWriterLive };
@@ -1,68 +0,0 @@
1
- import { DiscoveryError } from "../errors/DiscoveryError.js";
2
- import { ProjectDiscovery } from "../services/ProjectDiscovery.js";
3
- import { NON_DISCOVERABLE_DIRS } from "../utils/test-location.js";
4
- import { Effect, FileSystem, Layer } from "effect";
5
-
6
- //#region src/layers/ProjectDiscoveryLive.ts
7
- const TEST_FILE_PATTERN = /\.(test|spec)\.(ts|tsx)$/;
8
- const SKIP_DIRS = /* @__PURE__ */ new Set([
9
- ...NON_DISCOVERABLE_DIRS,
10
- "coverage",
11
- ".turbo",
12
- ".vite"
13
- ]);
14
- function isTestFile(filePath) {
15
- return TEST_FILE_PATTERN.test(filePath);
16
- }
17
- function testFileToSource(testFile) {
18
- return testFile.replace(/\.(test|spec)\.(ts|tsx)$/, ".$2");
19
- }
20
- function walkDir(fs, dir) {
21
- return Effect.gen(function* () {
22
- const entries = yield* fs.readDirectory(dir);
23
- const results = [];
24
- for (const entry of entries) {
25
- if (SKIP_DIRS.has(entry)) continue;
26
- const fullPath = `${dir}/${entry}`;
27
- if ((yield* fs.stat(fullPath)).type === "Directory") {
28
- const subFiles = yield* walkDir(fs, fullPath);
29
- results.push(...subFiles);
30
- } else results.push(fullPath);
31
- }
32
- return results;
33
- });
34
- }
35
- /** @public */
36
- const ProjectDiscoveryLive = Layer.effect(ProjectDiscovery, Effect.gen(function* () {
37
- const fs = yield* FileSystem.FileSystem;
38
- const mapTestToSource = (testFile) => Effect.gen(function* () {
39
- const sourceFile = testFileToSource(testFile);
40
- return (yield* fs.exists(sourceFile)) ? [sourceFile] : [];
41
- }).pipe(Effect.mapError((error) => new DiscoveryError({
42
- operation: "stat",
43
- path: testFile,
44
- reason: String(error)
45
- })));
46
- return {
47
- discoverTestFiles: (rootDir) => Effect.gen(function* () {
48
- const testFiles = (yield* walkDir(fs, rootDir).pipe(Effect.mapError((error) => new DiscoveryError({
49
- operation: "glob",
50
- path: rootDir,
51
- reason: String(error)
52
- })))).filter(isTestFile);
53
- const entries = [];
54
- for (const testFile of testFiles) {
55
- const sourceFiles = yield* mapTestToSource(testFile);
56
- entries.push({
57
- testFile,
58
- sourceFiles
59
- });
60
- }
61
- return entries;
62
- }),
63
- mapTestToSource
64
- };
65
- }));
66
-
67
- //#endregion
68
- export { ProjectDiscoveryLive };
@@ -1,12 +0,0 @@
1
- import { ProjectDiscovery } from "../services/ProjectDiscovery.js";
2
- import { Effect, Layer } from "effect";
3
-
4
- //#region src/layers/ProjectDiscoveryTest.ts
5
- /** @public */
6
- const ProjectDiscoveryTest = { layer: (entries) => Layer.succeed(ProjectDiscovery, {
7
- discoverTestFiles: () => Effect.succeed(entries),
8
- mapTestToSource: (testFile) => Effect.succeed(entries.find((e) => e.testFile === testFile)?.sourceFiles ?? [])
9
- }) };
10
-
11
- //#endregion
12
- export { ProjectDiscoveryTest };