@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.
- package/README.md +23 -19
- package/dispatch.d.ts +25 -6
- package/formatters/markdown.js +1 -0
- package/formatters/terminal.js +4 -3
- package/index.d.ts +448 -2341
- package/index.js +10 -68
- package/internal-inject-env.js +5 -7
- package/package.json +3 -19
- package/schemas/run-report-file-1.0.0.json +18 -18
- package/sidecar-dispatch.js +7 -5
- package/utils/format-console.js +9 -10
- package/utils/format-terminal.js +7 -7
- package/utils/posix-path.js +52 -0
- package/utils/test-location.js +11 -5
- package/version.js +17 -0
- package/layers/ConfigLive.js +0 -46
- package/layers/DataReaderLive.js +0 -1923
- package/layers/DataStoreLive.js +0 -1287
- package/layers/DetailResolverLive.js +0 -9
- package/layers/DiscoveryRegistryLive.js +0 -70
- package/layers/EnvironmentDetectorLive.js +0 -21
- package/layers/EnvironmentDetectorTest.js +0 -13
- package/layers/ExecutorResolverLive.js +0 -9
- package/layers/FormatSelectorLive.js +0 -20
- package/layers/HistoryTrackerLive.js +0 -49
- package/layers/HistoryTrackerTest.js +0 -25
- package/layers/LoggerLive.js +0 -64
- package/layers/OutputPipelineLive.js +0 -13
- package/layers/OutputRendererLive.js +0 -28
- package/layers/PathResolutionLive.js +0 -40
- package/layers/PerClientSessionMapLive.js +0 -145
- package/layers/ProjectDiscoveryLive.js +0 -68
- package/layers/ProjectDiscoveryTest.js +0 -12
- package/layers/ProjectIdentityLive.js +0 -96
- package/layers/RunContextLive.js +0 -81
- package/lib/format-triage.js +0 -103
- package/lib/format-wrapup.js +0 -67
- package/migrations/0001_initial.js +0 -824
- package/migrations/0002_test_artifacts.js +0 -30
- package/migrations/index.js +0 -26
- package/migrations/registry_0001_initial.js +0 -39
- package/migrations/session_map_0001_initial.js +0 -38
- package/services/Config.js +0 -15
- package/services/DataReader.js +0 -8
- package/services/DataStore.js +0 -15
- package/services/DetailResolver.js +0 -8
- package/services/DiscoveryRegistry.js +0 -8
- package/services/EnvironmentDetector.js +0 -8
- package/services/ExecutorResolver.js +0 -8
- package/services/FormatSelector.js +0 -8
- package/services/HistoryTracker.js +0 -20
- package/services/OutputRenderer.js +0 -8
- package/services/PerClientSessionMap.js +0 -22
- package/services/ProjectDiscovery.js +0 -8
- package/services/ProjectIdentity.js +0 -63
- package/services/RunContext.js +0 -53
- package/services/idempotency.js +0 -58
- package/sql/assemblers.js +0 -69
- package/testing/layers.js +0 -21
- package/testing.d.ts +0 -1862
- package/testing.js +0 -254
- package/utils/ensure-migrated.js +0 -52
- package/utils/failure-signature.js +0 -34
- package/utils/resolve-data-path.js +0 -65
- package/utils/resolve-project-key-from-cwd.js +0 -53
- 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 };
|
package/layers/LoggerLive.js
DELETED
|
@@ -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 };
|