@vitest-agent/sdk 1.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/DataReader-2kJ9I9Hh.d.ts +1809 -0
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/dispatch.d.ts +48 -0
- package/dispatch.js +5 -0
- package/errors/AgentErrors.js +65 -0
- package/errors/DataStoreError.js +44 -0
- package/errors/DiscoveryError.js +21 -0
- package/errors/PathResolutionError.js +27 -0
- package/errors/ProjectIdentityError.js +29 -0
- package/errors/RunContextError.js +35 -0
- package/errors/TddErrors.js +66 -0
- package/exit-code-for-tag.js +18 -0
- package/formatters/ci-annotations.js +61 -0
- package/formatters/gfm.js +20 -0
- package/formatters/json.js +18 -0
- package/formatters/markdown.js +39 -0
- package/formatters/silent.js +9 -0
- package/formatters/terminal.js +56 -0
- package/index.d.ts +3334 -0
- package/index.js +120 -0
- package/internal-inject-env.js +42 -0
- package/layers/ConfigLive.js +47 -0
- package/layers/DataReaderLive.js +1666 -0
- package/layers/DataStoreLive.js +1185 -0
- package/layers/DetailResolverLive.js +9 -0
- package/layers/DiscoveryRegistryLive.js +70 -0
- package/layers/EnvironmentDetectorLive.js +21 -0
- package/layers/EnvironmentDetectorTest.js +13 -0
- package/layers/ExecutorResolverLive.js +9 -0
- package/layers/FormatSelectorLive.js +20 -0
- package/layers/HistoryTrackerLive.js +47 -0
- package/layers/HistoryTrackerTest.js +25 -0
- package/layers/LoggerLive.js +64 -0
- package/layers/OutputPipelineLive.js +13 -0
- package/layers/OutputRendererLive.js +28 -0
- package/layers/PathResolutionLive.js +36 -0
- package/layers/PerClientSessionMapLive.js +145 -0
- package/layers/ProjectDiscoveryLive.js +70 -0
- package/layers/ProjectDiscoveryTest.js +12 -0
- package/layers/ProjectIdentityLive.js +92 -0
- package/layers/RunContextLive.js +81 -0
- package/lib/format-triage.js +103 -0
- package/lib/format-wrapup.js +67 -0
- package/migrations/0001_initial.js +849 -0
- package/migrations/registry_0001_initial.js +39 -0
- package/migrations/session_map_0001_initial.js +61 -0
- package/package.json +64 -0
- package/schemas/Agent.js +42 -0
- package/schemas/AgentReport.js +71 -0
- package/schemas/Baselines.js +16 -0
- package/schemas/CacheManifest.js +27 -0
- package/schemas/ChannelEvent.js +138 -0
- package/schemas/Common.js +105 -0
- package/schemas/Config.js +28 -0
- package/schemas/Coverage.js +51 -0
- package/schemas/CoverageLevel.js +117 -0
- package/schemas/CoverageTargets.js +39 -0
- package/schemas/History.js +31 -0
- package/schemas/Identity.js +64 -0
- package/schemas/Options.js +57 -0
- package/schemas/RenderState.js +161 -0
- package/schemas/RunEvent.js +163 -0
- package/schemas/Tdd.js +44 -0
- package/schemas/Thresholds.js +30 -0
- package/schemas/Transport.js +16 -0
- package/schemas/Trends.js +23 -0
- package/schemas/turns/FileEditPayload.js +15 -0
- package/schemas/turns/HookFirePayload.js +13 -0
- package/schemas/turns/HypothesisPayload.js +13 -0
- package/schemas/turns/NotePayload.js +13 -0
- package/schemas/turns/ToolCallPayload.js +13 -0
- package/schemas/turns/ToolResultPayload.js +15 -0
- package/schemas/turns/UserPromptPayload.js +12 -0
- package/schemas/turns/index.js +15 -0
- package/services/Config.js +15 -0
- package/services/DataReader.js +8 -0
- package/services/DataStore.js +8 -0
- package/services/DetailResolver.js +8 -0
- package/services/DiscoveryRegistry.js +8 -0
- package/services/EnvironmentDetector.js +8 -0
- package/services/ExecutorResolver.js +8 -0
- package/services/FormatSelector.js +8 -0
- package/services/HistoryTracker.js +8 -0
- package/services/OutputRenderer.js +8 -0
- package/services/PerClientSessionMap.js +22 -0
- package/services/ProjectDiscovery.js +8 -0
- package/services/ProjectIdentity.js +63 -0
- package/services/RunContext.js +53 -0
- package/services/idempotency.js +57 -0
- package/sidecar-dispatch.js +84 -0
- package/sql/assemblers.js +59 -0
- package/testing/layers.js +21 -0
- package/testing.d.ts +25 -0
- package/testing.js +243 -0
- package/tsdoc-metadata.json +11 -0
- package/utils/ansi.js +49 -0
- package/utils/build-report.js +130 -0
- package/utils/canonicalize-git-url.js +66 -0
- package/utils/classify-test.js +22 -0
- package/utils/compress-lines.js +45 -0
- package/utils/compute-trend.js +88 -0
- package/utils/detect-pm.js +58 -0
- package/utils/detect-timeout.js +17 -0
- package/utils/ensure-migrated.js +52 -0
- package/utils/failure-signature.js +33 -0
- package/utils/format-console.js +219 -0
- package/utils/format-fatal-error.js +35 -0
- package/utils/format-gfm.js +161 -0
- package/utils/format-terminal.js +480 -0
- package/utils/function-boundary.js +75 -0
- package/utils/hyperlink.js +11 -0
- package/utils/match-vitest-command.js +88 -0
- package/utils/normalize-workspace-key.js +34 -0
- package/utils/probe-host-metadata.js +99 -0
- package/utils/resolve-data-path.js +66 -0
- package/utils/resolve-project-key-from-cwd.js +52 -0
- package/utils/resolve-workspace-key.js +33 -0
- package/utils/safe-filename.js +34 -0
- package/utils/validate-coverage-targets-shape.js +84 -0
- package/utils/validate-phase-transition.js +113 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/schemas/Thresholds.ts
|
|
4
|
+
/**
|
|
5
|
+
* Per-metric threshold values. All optional -- only set metrics are enforced.
|
|
6
|
+
* @public
|
|
7
|
+
*/
|
|
8
|
+
const MetricThresholds = Schema.Struct({
|
|
9
|
+
lines: Schema.optional(Schema.Number),
|
|
10
|
+
functions: Schema.optional(Schema.Number),
|
|
11
|
+
branches: Schema.optional(Schema.Number),
|
|
12
|
+
statements: Schema.optional(Schema.Number)
|
|
13
|
+
}).annotations({ identifier: "MetricThresholds" });
|
|
14
|
+
/**
|
|
15
|
+
* A glob pattern paired with its metric thresholds.
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
const PatternThresholds = Schema.Tuple(Schema.String, MetricThresholds).annotations({ identifier: "PatternThresholds" });
|
|
19
|
+
/**
|
|
20
|
+
* Fully resolved thresholds ready for evaluation.
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
const ResolvedThresholds = Schema.Struct({
|
|
24
|
+
global: MetricThresholds,
|
|
25
|
+
perFile: Schema.optionalWith(Schema.Boolean, { default: () => false }),
|
|
26
|
+
patterns: Schema.optionalWith(Schema.Array(PatternThresholds), { default: () => [] })
|
|
27
|
+
}).annotations({ identifier: "ResolvedThresholds" });
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { MetricThresholds, PatternThresholds, ResolvedThresholds };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/schemas/Transport.ts
|
|
4
|
+
/**
|
|
5
|
+
* Transport binding for the persistence layer.
|
|
6
|
+
*
|
|
7
|
+
* 2.x ships only `{ kind: "local" }`. Modeled as a single-member
|
|
8
|
+
* discriminated union so the 3.0 cloud-backend swap (D1, Turso, etc.)
|
|
9
|
+
* lands as a pure addition of new union members — no schema-shape
|
|
10
|
+
* diff and no breaking API change at the call site.
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
const Transport = Schema.Union(Schema.Struct({ kind: Schema.Literal("local") })).annotations({ identifier: "Transport" });
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { Transport };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CoverageTotals } from "./Coverage.js";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/schemas/Trends.ts
|
|
5
|
+
/**
|
|
6
|
+
* A single coverage trend data point recorded on a full test run.
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
const TrendEntry = Schema.Struct({
|
|
10
|
+
timestamp: Schema.String,
|
|
11
|
+
coverage: CoverageTotals,
|
|
12
|
+
delta: CoverageTotals,
|
|
13
|
+
direction: Schema.Literal("improving", "regressing", "stable"),
|
|
14
|
+
targetsHash: Schema.optional(Schema.String)
|
|
15
|
+
}).annotations({ identifier: "TrendEntry" });
|
|
16
|
+
/**
|
|
17
|
+
* Per-project trend record with sliding window of entries.
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
const TrendRecord = Schema.Struct({ entries: Schema.Array(TrendEntry) }).annotations({ identifier: "TrendRecord" });
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
export { TrendEntry, TrendRecord };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/schemas/turns/FileEditPayload.ts
|
|
4
|
+
/** @public */
|
|
5
|
+
const FileEditPayload = Schema.Struct({
|
|
6
|
+
type: Schema.Literal("file_edit"),
|
|
7
|
+
file_path: Schema.String,
|
|
8
|
+
edit_kind: Schema.Literal("write", "edit", "multi_edit"),
|
|
9
|
+
lines_added: Schema.optional(Schema.Number),
|
|
10
|
+
lines_removed: Schema.optional(Schema.Number),
|
|
11
|
+
diff: Schema.optional(Schema.String)
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { FileEditPayload };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/schemas/turns/HookFirePayload.ts
|
|
4
|
+
/** @public */
|
|
5
|
+
const HookFirePayload = Schema.Struct({
|
|
6
|
+
type: Schema.Literal("hook_fire"),
|
|
7
|
+
hook_kind: Schema.Literal("SessionStart", "SessionEnd", "Stop", "StopFailure", "SubagentStart", "SubagentStop", "PreCompact", "PostCompact", "PreToolUse", "PostToolUse", "PostToolUseFailure", "UserPromptSubmit", "FileChanged"),
|
|
8
|
+
chat_id: Schema.optional(Schema.String),
|
|
9
|
+
previous_record_failures: Schema.optional(Schema.Array(Schema.String))
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { HookFirePayload };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/schemas/turns/HypothesisPayload.ts
|
|
4
|
+
/** @public */
|
|
5
|
+
const HypothesisPayload = Schema.Struct({
|
|
6
|
+
type: Schema.Literal("hypothesis"),
|
|
7
|
+
content: Schema.String,
|
|
8
|
+
cited_test_error_id: Schema.optional(Schema.Number),
|
|
9
|
+
cited_stack_frame_id: Schema.optional(Schema.Number)
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { HypothesisPayload };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/schemas/turns/NotePayload.ts
|
|
4
|
+
/** @public */
|
|
5
|
+
const NotePayload = Schema.Struct({
|
|
6
|
+
type: Schema.Literal("note"),
|
|
7
|
+
scope: Schema.String,
|
|
8
|
+
title: Schema.optional(Schema.String),
|
|
9
|
+
content: Schema.String
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { NotePayload };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/schemas/turns/ToolCallPayload.ts
|
|
4
|
+
/** @public */
|
|
5
|
+
const ToolCallPayload = Schema.Struct({
|
|
6
|
+
type: Schema.Literal("tool_call"),
|
|
7
|
+
tool_name: Schema.String,
|
|
8
|
+
tool_input: Schema.Unknown,
|
|
9
|
+
tool_use_id: Schema.optional(Schema.String)
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { ToolCallPayload };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/schemas/turns/ToolResultPayload.ts
|
|
4
|
+
/** @public */
|
|
5
|
+
const ToolResultPayload = Schema.Struct({
|
|
6
|
+
type: Schema.Literal("tool_result"),
|
|
7
|
+
tool_name: Schema.String,
|
|
8
|
+
tool_use_id: Schema.optional(Schema.String),
|
|
9
|
+
result_summary: Schema.optional(Schema.String),
|
|
10
|
+
success: Schema.Boolean,
|
|
11
|
+
duration_ms: Schema.optional(Schema.Number)
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { ToolResultPayload };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/schemas/turns/UserPromptPayload.ts
|
|
4
|
+
/** @public */
|
|
5
|
+
const UserPromptPayload = Schema.Struct({
|
|
6
|
+
type: Schema.Literal("user_prompt"),
|
|
7
|
+
prompt: Schema.String,
|
|
8
|
+
cc_message_id: Schema.optional(Schema.String)
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
export { UserPromptPayload };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FileEditPayload } from "./FileEditPayload.js";
|
|
2
|
+
import { HookFirePayload } from "./HookFirePayload.js";
|
|
3
|
+
import { HypothesisPayload } from "./HypothesisPayload.js";
|
|
4
|
+
import { NotePayload } from "./NotePayload.js";
|
|
5
|
+
import { ToolCallPayload } from "./ToolCallPayload.js";
|
|
6
|
+
import { ToolResultPayload } from "./ToolResultPayload.js";
|
|
7
|
+
import { UserPromptPayload } from "./UserPromptPayload.js";
|
|
8
|
+
import { Schema } from "effect";
|
|
9
|
+
|
|
10
|
+
//#region src/schemas/turns/index.ts
|
|
11
|
+
/** @public */
|
|
12
|
+
const TurnPayload = Schema.Union(UserPromptPayload, ToolCallPayload, ToolResultPayload, FileEditPayload, HookFirePayload, NotePayload, HypothesisPayload);
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { TurnPayload };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ConfigFile } from "config-file-effect";
|
|
2
|
+
|
|
3
|
+
//#region src/services/Config.ts
|
|
4
|
+
/**
|
|
5
|
+
* Typed `Context.Tag` for the vitest-agent config file service.
|
|
6
|
+
*
|
|
7
|
+
* Both runtime packages (reporter, MCP) yield this tag to access the loaded
|
|
8
|
+
* `VitestAgentConfig`. The live layer is built per `projectDir` via
|
|
9
|
+
* `ConfigLive(projectDir)`.
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
const VitestAgentConfigFile = ConfigFile.Tag("vitest-agent/Config");
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { VitestAgentConfigFile };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Context } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/services/PerClientSessionMap.ts
|
|
4
|
+
/**
|
|
5
|
+
* Read-only side of the session map.
|
|
6
|
+
*
|
|
7
|
+
* Provided by `PerClientSessionMapReaderLive` (opens SQLite with
|
|
8
|
+
* `?mode=ro`). MCP server depends on this tag — calls to write
|
|
9
|
+
* methods would be a type error.
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
var PerClientSessionMapReader = class extends Context.Tag("vitest-agent/PerClientSessionMapReader")() {};
|
|
13
|
+
/**
|
|
14
|
+
* Read-write side of the session map. Sidecar provides this; the
|
|
15
|
+
* Writer layer also satisfies the Reader tag so consumers of either
|
|
16
|
+
* one resolve.
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
var PerClientSessionMapWriter = class extends Context.Tag("vitest-agent/PerClientSessionMapWriter")() {};
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
export { PerClientSessionMapReader, PerClientSessionMapWriter };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { canonicalizeGitUrl, gitUrlToProjectKey } from "../utils/canonicalize-git-url.js";
|
|
2
|
+
import { normalizeWorkspaceKey } from "../utils/normalize-workspace-key.js";
|
|
3
|
+
import { Context } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/services/ProjectIdentity.ts
|
|
6
|
+
const isUseful = (value) => value !== void 0 && value.trim().length > 0;
|
|
7
|
+
/**
|
|
8
|
+
* Pure priority resolver. Returns `null` when no candidate produces a
|
|
9
|
+
* usable value — the caller wraps `null` into a tagged error.
|
|
10
|
+
*
|
|
11
|
+
* For non-git sources the `canonicalForm` equals the original
|
|
12
|
+
* (un-normalized) input so the discovery registry can display
|
|
13
|
+
* `@spencerbeggs/vitest-agent` rather than the underscored form. For
|
|
14
|
+
* git sources, `canonicalForm` is the slash-preserving `host/path`
|
|
15
|
+
* shape from `canonicalizeGitUrl`.
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
const resolveProjectIdentityFromCandidates = (candidates) => {
|
|
19
|
+
if (isUseful(candidates.explicit)) return {
|
|
20
|
+
projectKey: normalizeWorkspaceKey(candidates.explicit),
|
|
21
|
+
canonicalForm: candidates.explicit,
|
|
22
|
+
source: "explicit"
|
|
23
|
+
};
|
|
24
|
+
if (isUseful(candidates.toml)) return {
|
|
25
|
+
projectKey: normalizeWorkspaceKey(candidates.toml),
|
|
26
|
+
canonicalForm: candidates.toml,
|
|
27
|
+
source: "toml"
|
|
28
|
+
};
|
|
29
|
+
if (isUseful(candidates.gitRemote)) {
|
|
30
|
+
const canonical = canonicalizeGitUrl(candidates.gitRemote);
|
|
31
|
+
const key = gitUrlToProjectKey(candidates.gitRemote);
|
|
32
|
+
if (canonical !== null && key !== null) return {
|
|
33
|
+
projectKey: key,
|
|
34
|
+
canonicalForm: canonical,
|
|
35
|
+
source: "git-remote"
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (isUseful(candidates.packageJsonRepoUrl)) {
|
|
39
|
+
const canonical = canonicalizeGitUrl(candidates.packageJsonRepoUrl);
|
|
40
|
+
const key = gitUrlToProjectKey(candidates.packageJsonRepoUrl);
|
|
41
|
+
if (canonical !== null && key !== null) return {
|
|
42
|
+
projectKey: key,
|
|
43
|
+
canonicalForm: canonical,
|
|
44
|
+
source: "package-repository"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (isUseful(candidates.packageJsonName)) return {
|
|
48
|
+
projectKey: normalizeWorkspaceKey(candidates.packageJsonName),
|
|
49
|
+
canonicalForm: candidates.packageJsonName,
|
|
50
|
+
source: "package-name"
|
|
51
|
+
};
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Service tag. The Live layer (in `layers/ProjectIdentityLive.ts`)
|
|
56
|
+
* wires up the I/O sources; tests provide a stub that returns a
|
|
57
|
+
* pre-built `ResolvedIdentity`.
|
|
58
|
+
* @public
|
|
59
|
+
*/
|
|
60
|
+
var ProjectIdentity = class extends Context.Tag("vitest-agent/ProjectIdentity")() {};
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
export { ProjectIdentity, resolveProjectIdentityFromCandidates };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Context, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/services/RunContext.ts
|
|
4
|
+
/**
|
|
5
|
+
* Per-run snapshot of the workspace's git state and host context.
|
|
6
|
+
* Every field is nullable: a non-git workspace has all-NULL git
|
|
7
|
+
* fields, a CI run with no terminal probe match has all-NULL host
|
|
8
|
+
* fields, and a worker that can't read upstream info has a NULL
|
|
9
|
+
* `gitUpstream` while everything else is set.
|
|
10
|
+
*
|
|
11
|
+
* Declared as a `Schema.Class` so callers can `new RunContext({...})`
|
|
12
|
+
* and `Schema.encode(...)` for the SQLite row mapping.
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
var RunContext = class extends Schema.Class("vitest-agent/RunContext")({
|
|
16
|
+
gitBranch: Schema.NullOr(Schema.String),
|
|
17
|
+
gitCommitSha: Schema.NullOr(Schema.String),
|
|
18
|
+
gitDirty: Schema.NullOr(Schema.Boolean),
|
|
19
|
+
gitUpstream: Schema.NullOr(Schema.String),
|
|
20
|
+
gitWorktreeDir: Schema.NullOr(Schema.String),
|
|
21
|
+
hostSource: Schema.NullOr(Schema.String),
|
|
22
|
+
hostValue: Schema.NullOr(Schema.String),
|
|
23
|
+
hostMetadata: Schema.NullOr(Schema.Unknown)
|
|
24
|
+
}) {};
|
|
25
|
+
/**
|
|
26
|
+
* Subset of {@link RunContext} captured at agent registration. Only
|
|
27
|
+
* the inheritable git context — branch, sha, worktree path. Mid-run
|
|
28
|
+
* dirty-tree state and upstream tracking are per-run concerns.
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
var AgentContext = class extends Schema.Class("vitest-agent/AgentContext")({
|
|
32
|
+
startGitBranch: Schema.NullOr(Schema.String),
|
|
33
|
+
startGitCommitSha: Schema.NullOr(Schema.String),
|
|
34
|
+
startWorktreeDir: Schema.NullOr(Schema.String)
|
|
35
|
+
}) {};
|
|
36
|
+
/**
|
|
37
|
+
* Service tag. The Live layer captures real I/O; the Test layer
|
|
38
|
+
* returns whatever the test fixture passes.
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
41
|
+
var RunContext$ = class extends Context.Tag("vitest-agent/RunContextService")() {};
|
|
42
|
+
/**
|
|
43
|
+
* Re-export the service tag under a less-collision-prone name. The
|
|
44
|
+
* `RunContext` Schema.Class above shares its symbol-name with the
|
|
45
|
+
* service tag concept; the `$` suffix on the Context.Tag class name
|
|
46
|
+
* keeps both addressable from the same module.
|
|
47
|
+
*
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
const RunContextService = RunContext$;
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
export { AgentContext, RunContext, RunContext$, RunContextService };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
//#region src/services/idempotency.ts
|
|
4
|
+
/**
|
|
5
|
+
* Sentinel substituted for `parentAgentId` when the value is `null`.
|
|
6
|
+
* Avoids ambiguity around hashing the literal string "null" or empty
|
|
7
|
+
* string, both of which could collide with caller-supplied values.
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
const IDEMPOTENCY_ROOT_SENTINEL = "__ROOT__";
|
|
11
|
+
/**
|
|
12
|
+
* Derive the canonical idempotency key for an `agents` row.
|
|
13
|
+
*
|
|
14
|
+
* Algorithm:
|
|
15
|
+
* sha256(agentType + "|" + (parentAgentId ?? "__ROOT__") + "|" + clientNonce)
|
|
16
|
+
* then base32 (RFC 4648 alphabet, lowercased) truncated to 26 chars.
|
|
17
|
+
*
|
|
18
|
+
* 26 base32 chars = 130 bits, far beyond the birthday-bound for the
|
|
19
|
+
* scoping (UNIQUE per session). The base32 alphabet is a-z2-7 — all
|
|
20
|
+
* lowercase to keep the column lexicographically friendly and
|
|
21
|
+
* case-insensitive on filesystems that we never hit but might in
|
|
22
|
+
* future debugging.
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
const deriveIdempotencyKey = (input) => {
|
|
26
|
+
const parent = input.parentAgentId ?? "__ROOT__";
|
|
27
|
+
const message = `${input.agentType}|${parent}|${input.clientNonce}`;
|
|
28
|
+
return base32Encode(createHash("sha256").update(message).digest()).slice(0, 26);
|
|
29
|
+
};
|
|
30
|
+
const BASE32_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";
|
|
31
|
+
/**
|
|
32
|
+
* RFC 4648 base32 encoder, lowercase, no padding. Produces ceil(8n/5)
|
|
33
|
+
* characters from an `n`-byte input. SHA-256 output (32 bytes) yields
|
|
34
|
+
* 52 base32 chars; the caller truncates to 26.
|
|
35
|
+
*/
|
|
36
|
+
const base32Encode = (bytes) => {
|
|
37
|
+
let output = "";
|
|
38
|
+
let buffer = 0;
|
|
39
|
+
let bitsInBuffer = 0;
|
|
40
|
+
for (const byte of bytes) {
|
|
41
|
+
buffer = buffer << 8 | byte;
|
|
42
|
+
bitsInBuffer += 8;
|
|
43
|
+
while (bitsInBuffer >= 5) {
|
|
44
|
+
bitsInBuffer -= 5;
|
|
45
|
+
const index = buffer >> bitsInBuffer & 31;
|
|
46
|
+
output += BASE32_ALPHABET[index];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (bitsInBuffer > 0) {
|
|
50
|
+
const index = buffer << 5 - bitsInBuffer & 31;
|
|
51
|
+
output += BASE32_ALPHABET[index];
|
|
52
|
+
}
|
|
53
|
+
return output;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
export { IDEMPOTENCY_ROOT_SENTINEL, deriveIdempotencyKey };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { exitCodeForTag } from "./exit-code-for-tag.js";
|
|
2
|
+
import { injectEnv } from "./internal-inject-env.js";
|
|
3
|
+
|
|
4
|
+
//#region src/sidecar-dispatch.ts
|
|
5
|
+
/**
|
|
6
|
+
* Parse `--flag value` pairs from an argv slice into a plain record.
|
|
7
|
+
* Bare tokens and unknown shapes are ignored — the subcommand handlers
|
|
8
|
+
* read only the flags they expect.
|
|
9
|
+
*/
|
|
10
|
+
const parseFlags = (argv) => {
|
|
11
|
+
const flags = {};
|
|
12
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
13
|
+
const token = argv[i];
|
|
14
|
+
if (token?.startsWith("--")) {
|
|
15
|
+
const key = token.slice(2);
|
|
16
|
+
const next = argv[i + 1];
|
|
17
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
18
|
+
flags[key] = next;
|
|
19
|
+
i += 1;
|
|
20
|
+
} else flags[key] = "";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return flags;
|
|
24
|
+
};
|
|
25
|
+
const tagFromError = (err) => {
|
|
26
|
+
if (err !== null && typeof err === "object" && "_tag" in err && typeof err._tag === "string") return err._tag;
|
|
27
|
+
return "Defect";
|
|
28
|
+
};
|
|
29
|
+
const messageFromError = (err) => {
|
|
30
|
+
if (err instanceof Error) return err.message;
|
|
31
|
+
if (err !== null && typeof err === "object") {
|
|
32
|
+
const e = err;
|
|
33
|
+
if (typeof e.reason === "string") return e.reason;
|
|
34
|
+
if (typeof e.message === "string") return e.message;
|
|
35
|
+
}
|
|
36
|
+
return String(err ?? "unknown error");
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Dispatch one argv invocation. `argv` is the post-`node post-bin`
|
|
40
|
+
* slice — i.e. `process.argv.slice(2)`. Never throws: every failure is
|
|
41
|
+
* folded into the returned {@link DispatchResult}.
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
const dispatch = async (argv) => {
|
|
45
|
+
const subcommand = argv[0];
|
|
46
|
+
const rest = argv.slice(1);
|
|
47
|
+
if (subcommand === "inject-env") {
|
|
48
|
+
const flags = parseFlags(rest);
|
|
49
|
+
const command = flags.command;
|
|
50
|
+
const cwd = flags.cwd ?? process.cwd();
|
|
51
|
+
if (command === void 0) return {
|
|
52
|
+
stdout: "",
|
|
53
|
+
stderr: `${exitCodeForTag("Defect")} Defect: inject-env requires --command\n`,
|
|
54
|
+
code: exitCodeForTag("Defect")
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
return {
|
|
58
|
+
stdout: `${injectEnv({
|
|
59
|
+
command,
|
|
60
|
+
cwd,
|
|
61
|
+
env: process.env
|
|
62
|
+
})}\n`,
|
|
63
|
+
stderr: "",
|
|
64
|
+
code: 0
|
|
65
|
+
};
|
|
66
|
+
} catch (err) {
|
|
67
|
+
const tag = tagFromError(err);
|
|
68
|
+
return {
|
|
69
|
+
stdout: "",
|
|
70
|
+
stderr: `${exitCodeForTag(tag)} ${tag}: ${messageFromError(err)}\n`,
|
|
71
|
+
code: exitCodeForTag(tag)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const label = subcommand ?? "(none)";
|
|
76
|
+
return {
|
|
77
|
+
stdout: "",
|
|
78
|
+
stderr: `${exitCodeForTag("Defect")} Defect: unknown subcommand: ${label}\n`,
|
|
79
|
+
code: exitCodeForTag("Defect")
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
export { dispatch };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//#region src/sql/assemblers.ts
|
|
2
|
+
/**
|
|
3
|
+
* Builds a `CacheManifest` from an array of test_runs aggregation rows.
|
|
4
|
+
*
|
|
5
|
+
* Each row represents the latest run for a project/sub_project pair.
|
|
6
|
+
* The `dbPath` is stored as `cacheDir` so CLI tools can locate the source.
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
function assembleManifest(rows, dbPath) {
|
|
10
|
+
return {
|
|
11
|
+
updatedAt: rows.reduce((latest, row) => {
|
|
12
|
+
return row.timestamp > latest ? row.timestamp : latest;
|
|
13
|
+
}, (/* @__PURE__ */ new Date(0)).toISOString()),
|
|
14
|
+
cacheDir: dbPath,
|
|
15
|
+
projects: rows.map((row) => {
|
|
16
|
+
const projectName = row.sub_project ? `${row.project}:${row.sub_project}` : row.project;
|
|
17
|
+
const safeProjectName = projectName.replace(/[^a-z0-9@._-]/gi, "_");
|
|
18
|
+
const reportFile = `sql:reports/${safeProjectName}.json`;
|
|
19
|
+
const historyFile = `sql:history/${safeProjectName}.history.json`;
|
|
20
|
+
const lastResult = (() => {
|
|
21
|
+
if (row.reason === "passed" || row.reason === "failed" || row.reason === "interrupted") return row.reason;
|
|
22
|
+
return null;
|
|
23
|
+
})();
|
|
24
|
+
return {
|
|
25
|
+
project: projectName,
|
|
26
|
+
reportFile,
|
|
27
|
+
historyFile,
|
|
28
|
+
lastRun: row.timestamp,
|
|
29
|
+
lastResult
|
|
30
|
+
};
|
|
31
|
+
})
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Groups test_history rows by `full_name` into a keyed record.
|
|
36
|
+
*
|
|
37
|
+
* Returns `Record<fullName, { runs: TestRun[] }>` where `runs` are the
|
|
38
|
+
* accumulated test runs for that test, in the order they appear in `rows`.
|
|
39
|
+
*
|
|
40
|
+
* Only rows with state `"passed"` or `"failed"` are included in runs
|
|
41
|
+
* (matching the `TestRun` schema constraint).
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
function assembleHistoryRecord(rows) {
|
|
45
|
+
const record = {};
|
|
46
|
+
for (const row of rows) {
|
|
47
|
+
const state = row.state === "passed" || row.state === "failed" ? row.state : null;
|
|
48
|
+
if (state === null) continue;
|
|
49
|
+
if (!record[row.full_name]) record[row.full_name] = { runs: [] };
|
|
50
|
+
record[row.full_name].runs.push({
|
|
51
|
+
timestamp: row.timestamp,
|
|
52
|
+
state
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return record;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
export { assembleHistoryRecord, assembleManifest };
|