@bjoernrochel/sevro 0.1.0-rc.8 → 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/CONTRIBUTING.md +22 -6
- package/README.md +5 -1
- package/docs/README.md +19 -15
- package/docs/architecture.md +138 -17
- package/docs/assets/README.md +5 -4
- package/docs/builtin-graders-v1.md +9 -2
- package/docs/codex-host.md +155 -57
- package/docs/contracts.md +2 -1
- package/docs/creating-cases.md +9 -2
- package/docs/development-cli.md +187 -19
- package/docs/documentation-quality.md +12 -2
- package/docs/evaluation-workflow.md +110 -0
- package/docs/extension-protocol-v1.md +26 -3
- package/docs/getting-started.md +23 -2
- package/docs/guide-evaluation.md +33 -1
- package/docs/identity-v1.md +25 -2
- package/docs/installing.md +11 -11
- package/docs/native-controls.md +76 -5
- package/docs/native-hosts.md +196 -7
- package/docs/reading-results.md +102 -0
- package/docs/recipes.md +489 -0
- package/docs/releases.md +31 -12
- package/docs/report-v1.md +29 -1
- package/docs/results-v1.md +122 -4
- package/docs/run-settings-v1.md +147 -0
- package/docs/runtime-v1.md +255 -22
- package/docs/troubleshooting.md +12 -12
- package/docs/typescript-quality.md +89 -26
- package/examples/recipes/candidate.ts +26 -0
- package/examples/recipes/cases/coding-change.json +21 -0
- package/examples/recipes/cases/host-tools.json +21 -0
- package/examples/recipes/cases/mise.json +21 -0
- package/examples/recipes/cases/node.json +21 -0
- package/examples/recipes/cases/python-uv.json +21 -0
- package/examples/recipes/cases/semantic.json +17 -0
- package/examples/recipes/coding-change/check.ts +23 -0
- package/examples/recipes/extension-artifacts.ts +65 -0
- package/examples/recipes/extension-methods.ts +174 -0
- package/examples/recipes/extension.ts +37 -0
- package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/README.md +3 -0
- package/examples/recipes/run-inputs.ts +188 -0
- package/examples/recipes/run-options.ts +131 -0
- package/examples/recipes/run.ts +63 -0
- package/examples/recipes/runtime/empty.json +1 -0
- package/examples/recipes/runtime/goals.json +1 -0
- package/examples/recipes/runtime/mise.json +6 -0
- package/examples/recipes/runtime/path.json +1 -0
- package/examples/recipes/runtime/plugins.json +4 -0
- package/examples/recipes/runtime/python-uv.json +10 -0
- package/examples/recipes/semantic-judge.ts +29 -0
- package/examples/templates/coding/adapter.ts +17 -0
- package/examples/templates/coding/case.json +15 -0
- package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
- package/examples/templates/repository-skill/extension-data.ts +64 -0
- package/examples/templates/repository-skill/extension.ts +31 -0
- package/examples/templates/response/adapter.ts +12 -0
- package/examples/templates/response/case.json +13 -0
- package/package.json +11 -5
- package/schemas/report-v1.schema.json +9 -0
- package/schemas/run-evidence-v1.schema.json +2 -1
- package/schemas/runtime-v1.schema.json +43 -7
- package/src/advisory.ts +2 -2
- package/src/candidate-artifacts.ts +42 -11
- package/src/candidate-codex-state.ts +185 -0
- package/src/case-validation.ts +73 -50
- package/src/cli/cancellation.ts +20 -0
- package/src/cli/command.ts +87 -0
- package/src/cli/completion.ts +15 -0
- package/src/cli/demo.ts +60 -0
- package/src/cli/evaluation.ts +55 -0
- package/src/cli/extension.ts +79 -0
- package/src/cli/help.ts +198 -0
- package/src/cli/hosts.ts +55 -0
- package/src/cli/init-settings.ts +30 -0
- package/src/cli/init.ts +111 -0
- package/src/{cli-invocation.ts → cli/invocation.ts} +214 -218
- package/src/cli/loading.ts +151 -0
- package/src/cli/preparation.ts +154 -0
- package/src/cli/presentation.ts +93 -0
- package/src/cli/progress.ts +6 -0
- package/src/cli/run-manifest.ts +128 -0
- package/src/cli/run-options.ts +403 -0
- package/src/cli/run-overrides.ts +69 -0
- package/src/cli/settings.ts +154 -0
- package/src/cli/summary.ts +43 -0
- package/src/cli.ts +13 -591
- package/src/contract-types.ts +94 -0
- package/src/engine.ts +9 -5
- package/src/evaluation/advisory.ts +150 -0
- package/src/{evaluation-check-plan.ts → evaluation/check-plan.ts} +103 -67
- package/src/evaluation/collection.ts +88 -0
- package/src/evaluation/context.ts +10 -0
- package/src/{evaluation-grading-host.ts → evaluation/grading-host.ts} +5 -5
- package/src/evaluation/grading.ts +235 -0
- package/src/{evaluation-invocation-plan.ts → evaluation/invocation-plan.ts} +147 -58
- package/src/{evaluation-plan.ts → evaluation/plan.ts} +208 -141
- package/src/evaluation/progress.ts +19 -0
- package/src/evaluation/resources.ts +241 -0
- package/src/evaluation/run.ts +206 -0
- package/src/{evaluation-semantic.ts → evaluation/semantic.ts} +77 -87
- package/src/evaluation/trial.ts +800 -0
- package/src/{evaluation-types.ts → evaluation/types.ts} +79 -29
- package/src/evidence/host-results.ts +85 -0
- package/src/evidence/retained-artifacts.ts +18 -0
- package/src/evidence/trial-writer.ts +203 -0
- package/src/evidence/usage.ts +17 -0
- package/src/extension-client.ts +34 -96
- package/src/extension-session.ts +24 -48
- package/src/{advisory-fixture.ts → fixtures/advisory.ts} +12 -72
- package/src/{fixture-bin.ts → fixtures/bin.ts} +1 -1
- package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
- package/src/fixtures/create.ts +183 -0
- package/src/{generated-fixture.ts → fixtures/generated.ts} +10 -53
- package/src/{git-hooks.ts → fixtures/git-hooks.ts} +1 -1
- package/src/{preparation.ts → fixtures/preparation.ts} +6 -16
- package/src/{repository-fixture.ts → fixtures/repository.ts} +30 -64
- package/src/{fixture-setup.ts → fixtures/setup.ts} +33 -51
- package/src/{fixture-tools.ts → fixtures/tools.ts} +1 -1
- package/src/{workspace-state.ts → fixtures/workspace-state.ts} +5 -5
- package/src/generated/cli-result.d.cts +2 -1
- package/src/generated/extension.d.cts +2 -1
- package/src/generated/report.cjs +1 -1
- package/src/generated/report.d.cts +2 -1
- package/src/generated/run-evidence.cjs +1 -1
- package/src/generated/run-evidence.d.cts +2 -1
- package/src/generated/runtime.cjs +1 -1
- package/src/generated/runtime.d.cts +2 -1
- package/src/git.ts +93 -0
- package/src/graders/git-head.ts +29 -84
- package/src/graders/shell.ts +50 -68
- package/src/host-diagnostic-redaction.ts +52 -0
- package/src/host-diagnostics.ts +331 -0
- package/src/hosts/claude-continuation.ts +57 -40
- package/src/hosts/claude-credential.ts +160 -63
- package/src/hosts/claude-events.ts +34 -18
- package/src/hosts/claude-keychain.ts +78 -21
- package/src/hosts/claude-login-recovery.ts +85 -0
- package/src/hosts/claude-login-refresh.ts +178 -0
- package/src/hosts/claude-login-store.ts +162 -0
- package/src/hosts/claude-login.ts +107 -0
- package/src/hosts/claude-native-goal.ts +15 -12
- package/src/hosts/claude-process.ts +10 -50
- package/src/hosts/claude-refresh-lock.ts +81 -0
- package/src/hosts/claude-repository-invocation.ts +9 -34
- package/src/hosts/claude-settings.ts +46 -9
- package/src/hosts/claude-skill-activations.ts +169 -0
- package/src/hosts/claude-state.ts +28 -15
- package/src/hosts/claude-tool-calls.ts +13 -10
- package/src/hosts/claude.ts +121 -81
- package/src/hosts/codex-app-server.ts +78 -52
- package/src/hosts/codex-configuration.ts +5 -10
- package/src/hosts/codex-exec-controls.ts +516 -0
- package/src/hosts/codex-marketplace.ts +12 -10
- package/src/hosts/codex-native-calls.ts +15 -122
- package/src/hosts/codex-process.ts +10 -183
- package/src/hosts/codex-profile.ts +36 -21
- package/src/hosts/codex-session-files.ts +1 -9
- package/src/hosts/codex-state.ts +134 -18
- package/src/hosts/codex.ts +210 -183
- package/src/hosts/isolation-common.ts +8 -11
- package/src/hosts/isolation-roots.ts +2 -2
- package/src/hosts/linux-sandbox.ts +15 -2
- package/src/hosts/mac-sandbox.ts +87 -18
- package/src/hosts/native-controls.ts +72 -22
- package/src/hosts/native-request.ts +55 -0
- package/src/hosts/process-lifecycle.ts +216 -48
- package/src/hosts/protected-worktrees.ts +1 -1
- package/src/hosts/skill-hook-catalog.ts +131 -0
- package/src/hosts/skill-hook-handler.ts +374 -0
- package/src/hosts/skill-hooks.ts +577 -0
- package/src/instrumentation.ts +7 -8
- package/src/native-transcript-state.ts +22 -3
- package/src/native-transcript-view.ts +52 -13
- package/src/native-transcripts.ts +26 -11
- package/src/paths.ts +22 -0
- package/src/process-lifecycle.ts +150 -0
- package/src/provenance.ts +27 -7
- package/src/prune.ts +300 -0
- package/src/report.ts +63 -36
- package/src/resolved-case.ts +1 -1
- package/src/results-lock.ts +42 -0
- package/src/results.ts +14 -18
- package/src/{runtime-config.ts → runtime/config.ts} +160 -46
- package/src/{runtime-discovery-apple.ts → runtime/discovery-apple.ts} +3 -3
- package/src/{runtime-discovery-providers.ts → runtime/discovery-providers.ts} +3 -9
- package/src/{runtime-discovery.ts → runtime/discovery.ts} +4 -4
- package/src/{runtime-hook-processes.ts → runtime/hook-processes.ts} +1 -1
- package/src/{runtime-hooks.ts → runtime/hooks.ts} +32 -22
- package/src/{runtime-paths.ts → runtime/paths.ts} +5 -19
- package/src/{runtime-query.ts → runtime/query.ts} +5 -5
- package/src/{runtime-seeds.ts → runtime/seeds.ts} +3 -3
- package/src/runtime/shared-cache.ts +130 -0
- package/src/{runtime-state.ts → runtime/state.ts} +28 -11
- package/src/schema-types.ts +73 -161
- package/src/shell.ts +4 -0
- package/src/transcript-retention.ts +146 -0
- package/src/value-guards.ts +22 -0
- package/docs/assets/sevro-logo.png +0 -0
- package/docs/assets/sevro-symbol.svg +0 -1
- package/docs/assets/sevro-wordmark.svg +0 -1
- package/docs/cache-ownership-validation.md +0 -49
- package/docs/claude-continuation-validation.md +0 -47
- package/docs/concurrency-validation.md +0 -86
- package/docs/configuration-root-validation.md +0 -70
- package/docs/engine-slice.md +0 -114
- package/docs/evidence/runtime-startup-30/README.md +0 -185
- package/docs/evidence/runtime-startup-30/after.json +0 -319
- package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +0 -2
- package/docs/evidence/runtime-startup-30/before.json +0 -403
- package/docs/evidence/runtime-startup-30/consumer-smoke.json +0 -63
- package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +0 -3
- package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +0 -3
- package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +0 -20
- package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +0 -2
- package/docs/evidence.md +0 -17
- package/docs/fixture-cleanup-validation.md +0 -73
- package/docs/guide-verification.json +0 -239
- package/src/evaluation-context.ts +0 -259
- package/src/evaluation-fixture.ts +0 -302
- package/src/evaluation-run.ts +0 -200
- package/src/evaluation-trial.ts +0 -1016
- package/src/hosts/claude-nested-skills.ts +0 -272
- package/src/hosts/codex-command-output.ts +0 -510
- package/src/hosts/codex-executor-command.ts +0 -291
- package/src/hosts/codex-skill-reads.ts +0 -783
- /package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +0 -0
package/src/extension-session.ts
CHANGED
|
@@ -4,9 +4,20 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
4
4
|
import { createReadStream } from "node:fs";
|
|
5
5
|
import { isAbsolute } from "node:path";
|
|
6
6
|
import { canonicalJson, hashJson } from "./identity";
|
|
7
|
-
import type { GeneratedFixture } from "./generated
|
|
8
|
-
import type { RepositoryFixture } from "./repository
|
|
9
|
-
import type { FixtureSetupDeclaration } from "./
|
|
7
|
+
import type { GeneratedFixture } from "./fixtures/generated";
|
|
8
|
+
import type { RepositoryFixture } from "./fixtures/repository";
|
|
9
|
+
import type { FixtureSetupDeclaration } from "./fixtures/setup";
|
|
10
|
+
import type {
|
|
11
|
+
ArtifactReference,
|
|
12
|
+
Assessment,
|
|
13
|
+
CheckDeclaration,
|
|
14
|
+
DomainOutcome,
|
|
15
|
+
ExtensionCheckOutcome,
|
|
16
|
+
MetricEvidence,
|
|
17
|
+
ObservationEvidence,
|
|
18
|
+
TaskVerdict,
|
|
19
|
+
} from "./contract-types";
|
|
20
|
+
import type { InstrumentationRequest } from "./instrumentation";
|
|
10
21
|
import {
|
|
11
22
|
ExtensionProtocolError,
|
|
12
23
|
exchangeExtension,
|
|
@@ -32,11 +43,7 @@ export interface ExtensionCase {
|
|
|
32
43
|
| { kind: "inline"; files: Record<string, string> }
|
|
33
44
|
| RepositoryFixture
|
|
34
45
|
| GeneratedFixture;
|
|
35
|
-
checks:
|
|
36
|
-
id: string;
|
|
37
|
-
grader: string;
|
|
38
|
-
configuration: Record<string, unknown>;
|
|
39
|
-
}[];
|
|
46
|
+
checks: CheckDeclaration[];
|
|
40
47
|
requiredEvidence: string[];
|
|
41
48
|
extensionData: Record<string, unknown>;
|
|
42
49
|
}
|
|
@@ -51,10 +58,7 @@ export interface PreparationResult {
|
|
|
51
58
|
gitExclude?: boolean;
|
|
52
59
|
executable?: boolean;
|
|
53
60
|
}[];
|
|
54
|
-
requestedInstrumentation:
|
|
55
|
-
id: string;
|
|
56
|
-
configuration: Record<string, unknown>;
|
|
57
|
-
}[];
|
|
61
|
+
requestedInstrumentation: InstrumentationRequest[];
|
|
58
62
|
fixtureSetup?: FixtureSetupDeclaration;
|
|
59
63
|
codexMarketplace?: {
|
|
60
64
|
artifactRoot: string;
|
|
@@ -77,48 +81,20 @@ export interface PreparationResult {
|
|
|
77
81
|
|
|
78
82
|
export interface EvaluationRequest {
|
|
79
83
|
caseId: string;
|
|
80
|
-
execution: {
|
|
81
|
-
status: "completed" | "failed" | "cancelled" | "not_run";
|
|
84
|
+
execution: Assessment["execution"] & {
|
|
82
85
|
errorCode?: string;
|
|
83
86
|
};
|
|
84
|
-
observations:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
completeness: "complete" | "partial" | "unavailable";
|
|
88
|
-
data: Record<string, unknown>;
|
|
89
|
-
}[];
|
|
90
|
-
builtinChecks: {
|
|
91
|
-
id: string;
|
|
92
|
-
status: "passed" | "failed" | "unavailable";
|
|
93
|
-
detail?: string;
|
|
94
|
-
evidenceRefs: string[];
|
|
95
|
-
}[];
|
|
96
|
-
artifacts: {
|
|
97
|
-
id: string;
|
|
98
|
-
path: string;
|
|
99
|
-
sha256: string;
|
|
100
|
-
gitExclude?: boolean;
|
|
101
|
-
executable?: boolean;
|
|
102
|
-
}[];
|
|
87
|
+
observations: ObservationEvidence[];
|
|
88
|
+
builtinChecks: ExtensionCheckOutcome[];
|
|
89
|
+
artifacts: ArtifactReference[];
|
|
103
90
|
extensionData: Record<string, unknown>;
|
|
104
91
|
}
|
|
105
92
|
|
|
106
93
|
export interface EvaluationResult {
|
|
107
|
-
checks:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
evidenceRefs: string[];
|
|
112
|
-
}[];
|
|
113
|
-
metrics: { id: string; value: number | null; unit: string }[];
|
|
114
|
-
domainOutcomes?: {
|
|
115
|
-
id: string;
|
|
116
|
-
status: "passed" | "failed" | "unavailable";
|
|
117
|
-
detail?: string;
|
|
118
|
-
evidenceRefs: string[];
|
|
119
|
-
data?: Record<string, unknown>;
|
|
120
|
-
}[];
|
|
121
|
-
taskVerdictRecommendation?: "passed" | "failed" | "not_assessed";
|
|
94
|
+
checks: ExtensionCheckOutcome[];
|
|
95
|
+
metrics: MetricEvidence[];
|
|
96
|
+
domainOutcomes?: DomainOutcome[];
|
|
97
|
+
taskVerdictRecommendation?: TaskVerdict;
|
|
122
98
|
}
|
|
123
99
|
|
|
124
100
|
export interface ExtensionSessionOptions extends ExchangeOptions {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Dirent, Stats } from "node:fs";
|
|
2
|
-
import { isMissingFile } from "./
|
|
2
|
+
import { isMissingFile } from "./tools";
|
|
3
3
|
import {
|
|
4
4
|
lstat,
|
|
5
5
|
mkdir,
|
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
copyFile,
|
|
12
12
|
} from "node:fs/promises";
|
|
13
13
|
import { allocateWorkspace, removeWorkspace } from "./workspace-state";
|
|
14
|
-
import { dirname, isAbsolute, join,
|
|
14
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
15
|
+
import { insideRoot as contained } from "../paths";
|
|
16
|
+
import { runGit } from "../git";
|
|
15
17
|
import { fixtureParts } from "./preparation";
|
|
16
18
|
|
|
17
19
|
const MAX_GIT_OUTPUT = 16 * 1024 * 1024;
|
|
@@ -19,87 +21,25 @@ const MAX_UNTRACKED_BYTES = 32 * 1024 * 1024;
|
|
|
19
21
|
const MAX_UNTRACKED_FILES = 1024;
|
|
20
22
|
const HIDDEN_ROOTS = [".agents", ".claude", ".codex", ".git"];
|
|
21
23
|
|
|
22
|
-
function contained(root: string, path: string): boolean {
|
|
23
|
-
const child = relative(root, path);
|
|
24
|
-
return (
|
|
25
|
-
child === "" ||
|
|
26
|
-
(child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child))
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
24
|
function excluded(path: string, roots: string[]): boolean {
|
|
31
25
|
return roots.some((root) => path === root || path.startsWith(`${root}/`));
|
|
32
26
|
}
|
|
33
27
|
|
|
34
|
-
async function bounded(stream: ReadableStream<Uint8Array>): Promise<Buffer> {
|
|
35
|
-
const chunks: Uint8Array[] = [];
|
|
36
|
-
let size = 0;
|
|
37
|
-
for await (const chunk of stream) {
|
|
38
|
-
size += chunk.byteLength;
|
|
39
|
-
if (size > MAX_GIT_OUTPUT)
|
|
40
|
-
throw new Error("advisory fixture Git output is too large");
|
|
41
|
-
chunks.push(chunk);
|
|
42
|
-
}
|
|
43
|
-
return Buffer.concat(chunks);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function nullGitConfig(): string {
|
|
47
|
-
return process.platform === "win32" ? "NUL" : "/dev/null";
|
|
48
|
-
}
|
|
49
|
-
function gitEnvironment(): Record<string, string> {
|
|
50
|
-
return {
|
|
51
|
-
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
|
52
|
-
...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}),
|
|
53
|
-
GIT_CONFIG_NOSYSTEM: "1",
|
|
54
|
-
GIT_CONFIG_GLOBAL: nullGitConfig(),
|
|
55
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
56
|
-
GIT_AUTHOR_NAME: "Sevro Advisory",
|
|
57
|
-
GIT_AUTHOR_EMAIL: "advisory@sevro.invalid",
|
|
58
|
-
GIT_COMMITTER_NAME: "Sevro Advisory",
|
|
59
|
-
GIT_COMMITTER_EMAIL: "advisory@sevro.invalid",
|
|
60
|
-
GIT_AUTHOR_DATE: "2000-01-01T00:00:00+00:00",
|
|
61
|
-
GIT_COMMITTER_DATE: "2000-01-01T00:00:00+00:00",
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
28
|
async function git(
|
|
66
29
|
cwd: string,
|
|
67
30
|
args: string[],
|
|
68
31
|
input?: Uint8Array,
|
|
69
32
|
): Promise<Buffer> {
|
|
70
|
-
const proc = Bun.spawn(
|
|
71
|
-
["git", "-c", `core.hooksPath=${nullGitConfig()}`, ...args],
|
|
72
|
-
{
|
|
73
|
-
cwd,
|
|
74
|
-
stdin: input ? "pipe" : "ignore",
|
|
75
|
-
stdout: "pipe",
|
|
76
|
-
stderr: "pipe",
|
|
77
|
-
env: gitEnvironment(),
|
|
78
|
-
},
|
|
79
|
-
);
|
|
80
33
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
bounded(proc.stdout),
|
|
90
|
-
bounded(proc.stderr),
|
|
91
|
-
proc.exited,
|
|
92
|
-
write,
|
|
93
|
-
]);
|
|
94
|
-
if (code !== 0) throw new Error("advisory fixture Git operation failed");
|
|
95
|
-
return out;
|
|
34
|
+
return await runGit({
|
|
35
|
+
cwd,
|
|
36
|
+
args,
|
|
37
|
+
input,
|
|
38
|
+
identity: "advisory",
|
|
39
|
+
label: "advisory fixture",
|
|
40
|
+
maxOutputBytes: MAX_GIT_OUTPUT,
|
|
41
|
+
});
|
|
96
42
|
} catch (cause) {
|
|
97
|
-
try {
|
|
98
|
-
proc.kill("SIGKILL");
|
|
99
|
-
} catch {
|
|
100
|
-
// The process may already have exited.
|
|
101
|
-
}
|
|
102
|
-
await proc.exited;
|
|
103
43
|
throw new Error("advisory fixture Git operation failed", { cause });
|
|
104
44
|
}
|
|
105
45
|
}
|
|
@@ -15,6 +15,18 @@ async function restoreDirectoryAccess(path: string): Promise<void> {
|
|
|
15
15
|
await restoreDirectoryAccess(join(path, child.name));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** Remove owned disposable state without following cache or fixture symlinks. */
|
|
19
|
+
export async function removeDisposableDirectory(path: string): Promise<void> {
|
|
20
|
+
try {
|
|
21
|
+
await rm(path, { recursive: true, force: true });
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if ((error as NodeJS.ErrnoException).code === "ENOTDIR") return;
|
|
24
|
+
if (!permissionFailure(error)) throw error;
|
|
25
|
+
await restoreDirectoryAccess(path);
|
|
26
|
+
await rm(path, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
/** Clear a completed fixture while preserving its reserved root for active peers. */
|
|
19
31
|
export async function clearFixtureContents(workspace: string): Promise<void> {
|
|
20
32
|
const root = await lstat(workspace);
|
|
@@ -26,13 +38,7 @@ export async function clearFixtureContents(workspace: string): Promise<void> {
|
|
|
26
38
|
await Promise.all(
|
|
27
39
|
names.map(async (name) => {
|
|
28
40
|
const path = join(workspace, name);
|
|
29
|
-
|
|
30
|
-
await rm(path, { recursive: true, force: true });
|
|
31
|
-
} catch (error) {
|
|
32
|
-
if (!permissionFailure(error)) throw error;
|
|
33
|
-
await restoreDirectoryAccess(path);
|
|
34
|
-
await rm(path, { recursive: true, force: true });
|
|
35
|
-
}
|
|
41
|
+
await removeDisposableDirectory(path);
|
|
36
42
|
}),
|
|
37
43
|
);
|
|
38
44
|
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { appendFile, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { allocateWorkspace, removeWorkspace } from "./workspace-state";
|
|
4
|
+
import { fixtureBinDirectory, installFixtureBin } from "./bin";
|
|
5
|
+
import { installGitHooks } from "./git-hooks";
|
|
6
|
+
import {
|
|
7
|
+
materializeGeneratedFixture,
|
|
8
|
+
type GeneratedFixture,
|
|
9
|
+
} from "./generated";
|
|
10
|
+
import { runFixtureSetup, type FixtureSetup } from "./setup";
|
|
11
|
+
import {
|
|
12
|
+
fixtureParts,
|
|
13
|
+
safePreparationTarget,
|
|
14
|
+
type InlineArtifact,
|
|
15
|
+
type PreparationSources,
|
|
16
|
+
} from "./preparation";
|
|
17
|
+
import {
|
|
18
|
+
applyRepositoryOverlay,
|
|
19
|
+
cloneRepositorySource,
|
|
20
|
+
type RepositoryFixture,
|
|
21
|
+
type RepositorySource,
|
|
22
|
+
} from "./repository";
|
|
23
|
+
import {
|
|
24
|
+
EvaluationConfigurationError,
|
|
25
|
+
type ResolvedCase,
|
|
26
|
+
} from "../evaluation/types";
|
|
27
|
+
|
|
28
|
+
export type FixtureSource<Repository = RepositorySource> =
|
|
29
|
+
| { kind: "inline"; files: ResolvedCase["fixture"]["files"] }
|
|
30
|
+
| { kind: "generated"; fixture: GeneratedFixture }
|
|
31
|
+
| { kind: "repository"; fixture: RepositoryFixture; repository: Repository };
|
|
32
|
+
|
|
33
|
+
interface FixtureOptions {
|
|
34
|
+
artifacts?: InlineArtifact[];
|
|
35
|
+
sources?: PreparationSources;
|
|
36
|
+
setup?: FixtureSetup | null;
|
|
37
|
+
projectRoot: string;
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
reservedWorkspace?: string;
|
|
40
|
+
candidateRuntimeRoot?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function createFixture(
|
|
44
|
+
source: FixtureSource,
|
|
45
|
+
options: FixtureOptions,
|
|
46
|
+
): Promise<string> {
|
|
47
|
+
const paths = inlineFixturePaths(source);
|
|
48
|
+
const artifacts = options.artifacts ?? [];
|
|
49
|
+
const workspace = options.reservedWorkspace ?? (await allocateWorkspace());
|
|
50
|
+
try {
|
|
51
|
+
await initializeFixtureHistory(source, options.sources, workspace);
|
|
52
|
+
await installDeclaredFixtureTools(source, workspace);
|
|
53
|
+
await writeInlineFixtureFiles(paths, workspace);
|
|
54
|
+
await setupFixture(
|
|
55
|
+
options.setup,
|
|
56
|
+
workspace,
|
|
57
|
+
options.projectRoot,
|
|
58
|
+
options.signal,
|
|
59
|
+
options.candidateRuntimeRoot,
|
|
60
|
+
);
|
|
61
|
+
await mountPreparationArtifacts(artifacts, workspace);
|
|
62
|
+
await excludePreparationArtifacts(artifacts, workspace);
|
|
63
|
+
await fixtureBinDirectory(workspace);
|
|
64
|
+
return workspace;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (!options.reservedWorkspace) await removeWorkspace(workspace);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function inlineFixturePaths(source: FixtureSource) {
|
|
72
|
+
return Object.entries(
|
|
73
|
+
source.kind === "inline" ? (source.files ?? {}) : {},
|
|
74
|
+
).map(([path, content]) => ({ path, parts: fixtureParts(path), content }));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function cloneFixtureRepository(
|
|
78
|
+
source: Extract<FixtureSource, { kind: "repository" }>,
|
|
79
|
+
sources: PreparationSources | undefined,
|
|
80
|
+
workspace: string,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
if (!sources)
|
|
83
|
+
throw new EvaluationConfigurationError(
|
|
84
|
+
"repository source root is required and must be absolute",
|
|
85
|
+
);
|
|
86
|
+
await cloneRepositorySource(
|
|
87
|
+
source.fixture.sourceRef,
|
|
88
|
+
sources,
|
|
89
|
+
source.repository,
|
|
90
|
+
workspace,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function initializeFixtureHistory(
|
|
95
|
+
source: FixtureSource,
|
|
96
|
+
sources: PreparationSources | undefined,
|
|
97
|
+
workspace: string,
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
switch (source.kind) {
|
|
100
|
+
case "repository":
|
|
101
|
+
await cloneFixtureRepository(source, sources, workspace);
|
|
102
|
+
await applyRepositoryOverlay(source.fixture, workspace);
|
|
103
|
+
break;
|
|
104
|
+
case "generated":
|
|
105
|
+
await materializeGeneratedFixture(source.fixture, workspace);
|
|
106
|
+
break;
|
|
107
|
+
case "inline":
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function installDeclaredFixtureTools(
|
|
113
|
+
source: FixtureSource,
|
|
114
|
+
workspace: string,
|
|
115
|
+
): Promise<void> {
|
|
116
|
+
const fixture = source.kind === "inline" ? undefined : source.fixture;
|
|
117
|
+
await installFixtureBin(fixture?.bin, workspace);
|
|
118
|
+
await installGitHooks(fixture?.hooks, workspace);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function writeInlineFixtureFiles(
|
|
122
|
+
files: ReturnType<typeof inlineFixturePaths>,
|
|
123
|
+
workspace: string,
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
const target = join(workspace, ...file.parts);
|
|
127
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
128
|
+
await writeFile(target, file.content, { flag: "wx", mode: 0o600 });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function setupFixture(
|
|
133
|
+
setup: FixtureSetup | null | undefined,
|
|
134
|
+
workspace: string,
|
|
135
|
+
projectRoot: string,
|
|
136
|
+
signal?: AbortSignal,
|
|
137
|
+
candidateRuntimeRoot?: string,
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
if (!setup) return;
|
|
140
|
+
await runFixtureSetup(setup, {
|
|
141
|
+
workspace,
|
|
142
|
+
projectRoot,
|
|
143
|
+
fixtureBinDir: (await fixtureBinDirectory(workspace)) ?? undefined,
|
|
144
|
+
signal,
|
|
145
|
+
candidateRuntimeRoot,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function mountPreparationArtifacts(
|
|
150
|
+
artifacts: InlineArtifact[],
|
|
151
|
+
workspace: string,
|
|
152
|
+
): Promise<void> {
|
|
153
|
+
for (const artifact of artifacts) {
|
|
154
|
+
const target = await safePreparationTarget(
|
|
155
|
+
workspace,
|
|
156
|
+
artifact.relativePath,
|
|
157
|
+
);
|
|
158
|
+
await writeFile(target, artifact.bytes, {
|
|
159
|
+
flag: "wx",
|
|
160
|
+
mode: artifact.executable ? 0o700 : 0o600,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function excludePreparationArtifacts(
|
|
166
|
+
artifacts: InlineArtifact[],
|
|
167
|
+
workspace: string,
|
|
168
|
+
): Promise<void> {
|
|
169
|
+
const gitExcluded = artifacts.filter((artifact) => artifact.gitExclude);
|
|
170
|
+
if (!gitExcluded.length) return;
|
|
171
|
+
const special = new Set(["\\", "*", "?", "[", "]", "#", "!", " "]);
|
|
172
|
+
const patterns = gitExcluded.map((artifact) =>
|
|
173
|
+
Array.from(artifact.relativePath)
|
|
174
|
+
.map((character) =>
|
|
175
|
+
special.has(character) ? `\\${character}` : character,
|
|
176
|
+
)
|
|
177
|
+
.join(""),
|
|
178
|
+
);
|
|
179
|
+
await appendFile(
|
|
180
|
+
join(workspace, ".git", "info", "exclude"),
|
|
181
|
+
`\n${patterns.map((path) => `/${path}`).join("\n")}\n`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { canonicalJson } from "
|
|
4
|
-
import { prepareFixtureBin } from "./
|
|
3
|
+
import { canonicalJson } from "../identity";
|
|
4
|
+
import { prepareFixtureBin } from "./bin";
|
|
5
5
|
import { prepareGitHooks } from "./git-hooks";
|
|
6
6
|
import { fixtureParts } from "./preparation";
|
|
7
|
+
import { runGit } from "../git";
|
|
7
8
|
|
|
8
9
|
const MAX_COMMITS = 128;
|
|
9
10
|
const MAX_FILES = 1024;
|
|
@@ -197,60 +198,16 @@ export function prepareGeneratedFixture(value: unknown): GeneratedFixture {
|
|
|
197
198
|
return JSON.parse(canonicalJson(fixture)) as GeneratedFixture;
|
|
198
199
|
}
|
|
199
200
|
|
|
200
|
-
function gitEnvironment() {
|
|
201
|
-
const nullDevice = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
202
|
-
return {
|
|
203
|
-
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
|
204
|
-
...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}),
|
|
205
|
-
GIT_CONFIG_NOSYSTEM: "1",
|
|
206
|
-
GIT_CONFIG_GLOBAL: nullDevice,
|
|
207
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
208
|
-
GIT_AUTHOR_NAME: "Sevro Fixture",
|
|
209
|
-
GIT_AUTHOR_EMAIL: "fixture@sevro.invalid",
|
|
210
|
-
GIT_COMMITTER_NAME: "Sevro Fixture",
|
|
211
|
-
GIT_COMMITTER_EMAIL: "fixture@sevro.invalid",
|
|
212
|
-
GIT_AUTHOR_DATE: "2000-01-01T00:00:00+00:00",
|
|
213
|
-
GIT_COMMITTER_DATE: "2000-01-01T00:00:00+00:00",
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
201
|
async function git(workspace: string, args: string[]): Promise<void> {
|
|
218
|
-
async function bounded(stream: ReadableStream<Uint8Array>): Promise<void> {
|
|
219
|
-
let size = 0;
|
|
220
|
-
for await (const chunk of stream) {
|
|
221
|
-
size += chunk.byteLength;
|
|
222
|
-
if (size > MAX_GIT_OUTPUT)
|
|
223
|
-
throw new Error("generated fixture Git output is too large");
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
const proc = Bun.spawn(
|
|
227
|
-
[
|
|
228
|
-
"git",
|
|
229
|
-
"-c",
|
|
230
|
-
`core.hooksPath=${process.platform === "win32" ? "NUL" : "/dev/null"}`,
|
|
231
|
-
...args,
|
|
232
|
-
],
|
|
233
|
-
{
|
|
234
|
-
cwd: workspace,
|
|
235
|
-
stdout: "pipe",
|
|
236
|
-
stderr: "pipe",
|
|
237
|
-
env: gitEnvironment(),
|
|
238
|
-
},
|
|
239
|
-
);
|
|
240
202
|
try {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
203
|
+
await runGit({
|
|
204
|
+
cwd: workspace,
|
|
205
|
+
args,
|
|
206
|
+
identity: "fixture",
|
|
207
|
+
label: "generated fixture",
|
|
208
|
+
maxOutputBytes: MAX_GIT_OUTPUT,
|
|
209
|
+
});
|
|
247
210
|
} catch (cause) {
|
|
248
|
-
try {
|
|
249
|
-
proc.kill("SIGKILL");
|
|
250
|
-
} catch {
|
|
251
|
-
// The process may have exited before the failure was observed.
|
|
252
|
-
}
|
|
253
|
-
await proc.exited;
|
|
254
211
|
throw new Error("generated fixture Git operation failed", { cause });
|
|
255
212
|
}
|
|
256
213
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { prepareFixtureTools, installFixtureTools } from "./
|
|
2
|
+
import { prepareFixtureTools, installFixtureTools } from "./tools";
|
|
3
3
|
|
|
4
4
|
/** Bound hook declarations and keep every name inside Git's hooks directory. */
|
|
5
5
|
export function prepareGitHooks(value: unknown): Record<string, string> | null {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { isMissingFile } from "./
|
|
2
|
-
import { sha256 } from "
|
|
1
|
+
import { isMissingFile } from "./tools";
|
|
2
|
+
import { sha256 } from "../identity";
|
|
3
3
|
import { lstat, mkdir, readFile, realpath, stat } from "node:fs/promises";
|
|
4
|
-
import { isAbsolute, join
|
|
4
|
+
import { isAbsolute, join } from "node:path";
|
|
5
|
+
import { insideRoot } from "../paths";
|
|
6
|
+
import { containsControl as containsControlCharacter } from "../value-guards";
|
|
5
7
|
import { fileURLToPath } from "node:url";
|
|
6
8
|
|
|
7
9
|
const MAX_ARTIFACT_BYTES = 8 * 1024 * 1024;
|
|
@@ -114,12 +116,6 @@ function requireArtifactFlags(item: PreparationArtifact): void {
|
|
|
114
116
|
if (item.executable !== undefined && typeof item.executable !== "boolean")
|
|
115
117
|
throw new Error("preparation artifact executable must be boolean");
|
|
116
118
|
}
|
|
117
|
-
function containsControlCharacter(path: string): boolean {
|
|
118
|
-
return Array.from(path).some(
|
|
119
|
-
(character) =>
|
|
120
|
-
character.charCodeAt(0) < 0x20 || character.charCodeAt(0) === 0x7f,
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
119
|
function requireInlineSource(
|
|
124
120
|
item: PreparationArtifact,
|
|
125
121
|
): asserts item is PreparationArtifact & { contentBase64: string } {
|
|
@@ -220,13 +216,7 @@ async function preparationSourcePath(url: string): Promise<string> {
|
|
|
220
216
|
}
|
|
221
217
|
}
|
|
222
218
|
function requireContainedSource(root: string, path: string): void {
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
!child ||
|
|
226
|
-
child === ".." ||
|
|
227
|
-
child.startsWith(`..${sep}`) ||
|
|
228
|
-
isAbsolute(child)
|
|
229
|
-
)
|
|
219
|
+
if (root === path || !insideRoot(root, path))
|
|
230
220
|
throw new Error("preparation source escapes its declared root");
|
|
231
221
|
}
|
|
232
222
|
async function preparationSourceSize(path: string): Promise<number> {
|