gentle-pi 2.1.2 → 2.2.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 +70 -12
- package/assets/agents/gentle-ai-worker.md +7 -3
- package/assets/agents/jd-fix-agent.md +1 -1
- package/assets/agents/jd-judge-a.md +3 -1
- package/assets/agents/jd-judge-b.md +3 -1
- package/assets/agents/review-readability.md +4 -1
- package/assets/agents/review-reliability.md +4 -1
- package/assets/agents/review-resilience.md +4 -1
- package/assets/agents/review-risk.md +4 -1
- package/assets/agents/sdd-apply.md +6 -1
- package/assets/agents/sdd-archive.md +6 -1
- package/assets/agents/sdd-design.md +6 -1
- package/assets/agents/sdd-explore.md +6 -2
- package/assets/agents/sdd-init.md +10 -2
- package/assets/agents/sdd-onboard.md +6 -1
- package/assets/agents/sdd-proposal.md +6 -1
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +6 -1
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +6 -1
- package/assets/agents/sdd-verify.md +6 -1
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +246 -67
- package/assets/orchestrator.md +7 -14
- package/assets/sdd-orchestrator-workflow.md +154 -9
- package/assets/support/sdd-status-contract.md +19 -1
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
- package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
- package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
- package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
- package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
- package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
- package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
- package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
- package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
- package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
- package/docs/native-authority-architecture.md +5 -5
- package/docs/review-integration.md +22 -2
- package/extensions/gentle-ai.ts +1595 -201
- package/extensions/sdd-init.ts +19 -6
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/git-commit-transaction.ts +77 -17
- package/lib/native-review-cli.ts +528 -65
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +527 -18
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +436 -0
- package/lib/review-integration-v2.ts +537 -36
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +53 -1
- package/package.json +5 -2
- package/runtime/gentle-ai-binary.mjs +173 -2
- package/runtime/git-commit-transaction.mjs +75 -15
- package/runtime/native-review-cli.mjs +524 -61
- package/runtime/review-integration-v2.mjs +536 -35
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/build-git-commit-transaction-runner.mjs +1 -0
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +23 -13
- package/scripts/maintainer/provider-relay-matrix.mjs +219 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/verify-package-files.mjs +110 -33
- package/skills/_shared/review-ledger-contract.md +4 -6
- package/skills/gentle-ai/SKILL.md +4 -4
- package/skills/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +7 -1
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- package/tests/background-subagents.test.ts +771 -0
- package/tests/crosslane/cross-lane.mjs +1169 -0
- package/tests/delegated-key-learnings-contract.test.ts +238 -0
- package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
- package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
- package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
- package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
- package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
- package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
- package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
- package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
- package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
- package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +336 -0
- package/tests/gentle-ai-installer.test.ts +46 -46
- package/tests/git-commit-transaction.test.ts +229 -1
- package/tests/maintainer/provider-relay.maintest.ts +265 -0
- package/tests/native-review-capability-contract.test.ts +48 -2
- package/tests/native-review-cli.test.ts +56 -0
- package/tests/native-review-consent.test.ts +164 -3
- package/tests/native-review-parity-runtime.test.ts +37 -0
- package/tests/native-review-parity.test.ts +218 -15
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/orchestrator-budget.test.ts +30 -5
- package/tests/package-manifest.test.ts +98 -72
- package/tests/provider-contract-bundle.test.ts +385 -0
- package/tests/provider-contract-mirror.test.ts +206 -0
- package/tests/provider-defect-handoff.test.ts +355 -0
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-candidate-view.test.ts +489 -9
- package/tests/review-compact-contract.test.ts +52 -119
- package/tests/review-controller-native-recovery.test.ts +643 -47
- package/tests/review-controller-native-routing.test.ts +1667 -222
- package/tests/review-controller-workspace-root.test.ts +17 -2
- package/tests/review-corrected-finalize-binding.test.ts +175 -0
- package/tests/review-dispatch-hydration-gap.test.ts +197 -0
- package/tests/review-host-relay-routing.test.ts +317 -0
- package/tests/review-host-relay.test.ts +520 -0
- package/tests/review-integration-v2-forward.test.ts +631 -0
- package/tests/review-integration-v2.test.ts +114 -0
- package/tests/review-ledger-contract.test.ts +12 -28
- package/tests/review-recovered-lineage-routing.test.ts +246 -0
- package/tests/review-relay-transport-agent.test.ts +249 -0
- package/tests/runtime-harness.mjs +242 -14
- package/tests/sdd-agent-tools.test.ts +18 -33
- package/tests/skill-collision-prefixes.test.ts +1 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- package/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- package/lib/native-review-remediation.ts +0 -49
- package/lib/review-compact.ts +0 -947
- package/lib/review-refuter-adapter.ts +0 -129
- package/lib/review-runtime-contract.ts +0 -68
- package/prompts/gcl.md +0 -54
- package/prompts/gis.md +0 -25
- package/prompts/gpr.md +0 -41
- package/prompts/gwr.md +0 -31
- package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
- package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
- package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
- package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
- package/tests/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
|
@@ -19,14 +19,14 @@ import {
|
|
|
19
19
|
trustedSystemExtractor,
|
|
20
20
|
} from "../scripts/gentle-ai-installer.mjs";
|
|
21
21
|
|
|
22
|
-
// v2.
|
|
22
|
+
// v2.4.0 digests pinned from the published release: archive sha256 values match
|
|
23
23
|
// checksums.txt and freshly computed hashes; binary sha256 values were computed
|
|
24
24
|
// from the extracted executables of each verified archive.
|
|
25
25
|
const EXPECTED_ASSETS = {
|
|
26
|
-
"darwin/amd64": { name: "gentle-ai_2.
|
|
27
|
-
"darwin/arm64": { name: "gentle-ai_2.
|
|
28
|
-
"linux/amd64": { name: "gentle-ai_2.
|
|
29
|
-
"linux/arm64": { name: "gentle-ai_2.
|
|
26
|
+
"darwin/amd64": { name: "gentle-ai_2.4.0_darwin_amd64.tar.gz", sha256: "c3f5a4251e927c41864e7a9a5670ac3fce97c6282755ec8e9b2e7e064067f750", binarySha256: "c4d25b98bae0d06b33baa23ba997cb64ab63c2e93e97983bcb9c239139bceaba" },
|
|
27
|
+
"darwin/arm64": { name: "gentle-ai_2.4.0_darwin_arm64.tar.gz", sha256: "2240ae797119dd1dc5a0dd2fd2711077ce6712e66ab0b67461344c983478f7f4", binarySha256: "54a8a5957ce4f10f36b46ef818c888f484a3e213a71faca6b7783af6c029eb77" },
|
|
28
|
+
"linux/amd64": { name: "gentle-ai_2.4.0_linux_amd64.tar.gz", sha256: "517eb8420cf966b74be1413a7ea1036119a5ccbb376e0778aa9f4d4d283e2e9a", binarySha256: "0be446737f8f1a9458fd2891d488beb2d72786578afead9bbf2d6ddddc9ec113" },
|
|
29
|
+
"linux/arm64": { name: "gentle-ai_2.4.0_linux_arm64.tar.gz", sha256: "1badec9cad1f2bbeb22b5c6804c3a88c601d844abae231264e416e8095f2a15f", binarySha256: "cdd8fa43c3417e844cc11d2e436d4667edb0a54f086e0eb1d6cd3f7493bfee5a" },
|
|
30
30
|
} as const;
|
|
31
31
|
|
|
32
32
|
test("default installer package root is the package containing scripts, not its parent", () => {
|
|
@@ -37,15 +37,15 @@ test("default installer package root is the package containing scripts, not its
|
|
|
37
37
|
assert.notEqual(resolveGentleAiInstallerPackageRoot(), dirname(expectedPackageRoot));
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
test("release mapping selects only the supported official v2.
|
|
40
|
+
test("release mapping selects only the supported official v2.4.0 archive and pinned digests", () => {
|
|
41
41
|
assert.deepEqual(
|
|
42
42
|
Object.fromEntries(Object.entries(GENTLE_AI_RELEASE_ASSETS).map(([key, asset]) => [key, { name: asset.name, sha256: asset.sha256, binarySha256: asset.binarySha256 }])),
|
|
43
43
|
EXPECTED_ASSETS,
|
|
44
44
|
);
|
|
45
|
-
assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_2.
|
|
46
|
-
assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_2.
|
|
45
|
+
assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_2.4.0_linux_amd64.tar.gz");
|
|
46
|
+
assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_2.4.0_darwin_arm64.tar.gz");
|
|
47
47
|
for (const asset of Object.values(GENTLE_AI_RELEASE_ASSETS)) {
|
|
48
|
-
assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v2\.
|
|
48
|
+
assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v2\.4\.0\//);
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
|
|
@@ -66,7 +66,7 @@ test("release digests are all-or-none and install fails closed while any digest
|
|
|
66
66
|
}),
|
|
67
67
|
/checksum mismatch/,
|
|
68
68
|
);
|
|
69
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.
|
|
69
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai")), false);
|
|
70
70
|
}
|
|
71
71
|
});
|
|
72
72
|
|
|
@@ -95,7 +95,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
|
|
|
95
95
|
const metadata = [
|
|
96
96
|
"gentle-ai.exe: go1.25.10",
|
|
97
97
|
"\tpath\tgithub.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai",
|
|
98
|
-
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.
|
|
98
|
+
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.4.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
|
|
99
99
|
"\tbuild\t-buildmode=exe", "\tbuild\t-compiler=gc", "\tbuild\tCGO_ENABLED=0", `\tbuild\tGOARCH=${fixtureOptions.goArchitecture ?? "amd64"}`, "\tbuild\tGOOS=windows",
|
|
100
100
|
].join("\n");
|
|
101
101
|
const run = async (file: string, arguments_: string[], options: WindowsGoCall["options"]) => {
|
|
@@ -113,7 +113,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
|
|
|
113
113
|
return { stdout: "", stderr: "" };
|
|
114
114
|
}
|
|
115
115
|
if (file === goExecutable && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
|
|
116
|
-
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 2.
|
|
116
|
+
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 2.4.0\n", stderr: "" };
|
|
117
117
|
throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
|
|
118
118
|
};
|
|
119
119
|
return { calls, run, setGoExecutable: (path: string) => { goExecutable = path; } };
|
|
@@ -129,7 +129,7 @@ test("win32 x64 and arm64 install the exact Go SumDB source tag without archive
|
|
|
129
129
|
const result = await installGentleAi({ packageRoot, platform: "win32", arch, execFile: fixture.run, resolveGoExecutable: async () => goPath });
|
|
130
130
|
assert.equal(result.installed, true);
|
|
131
131
|
assert.deepEqual(fixture.calls.filter((call) => call.file === goPath).map((call) => call.arguments_.slice(0, 2)), [
|
|
132
|
-
["version"], ["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.
|
|
132
|
+
["version"], ["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.4.0"], ["version", "-m"],
|
|
133
133
|
]);
|
|
134
134
|
}
|
|
135
135
|
});
|
|
@@ -148,7 +148,7 @@ test("Windows source install reports missing or too-old Go without publishing a
|
|
|
148
148
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
|
|
149
149
|
(error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
|
|
150
150
|
);
|
|
151
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.
|
|
151
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai.exe")), false);
|
|
152
152
|
assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.includes("install-")), []);
|
|
153
153
|
}
|
|
154
154
|
});
|
|
@@ -167,7 +167,7 @@ test("Windows source install cleans staging after Go failure or wrong built vers
|
|
|
167
167
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
|
|
168
168
|
(error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
|
|
169
169
|
);
|
|
170
|
-
const runtimeDirectory = join(packageRoot, ".gentle-ai", "v2.
|
|
170
|
+
const runtimeDirectory = join(packageRoot, ".gentle-ai", "v2.4.0");
|
|
171
171
|
assert.ok(fixture.calls.some((call) => call.arguments_[0] === "install"));
|
|
172
172
|
assert.equal(existsSync(join(runtimeDirectory, "gentle-ai.exe")), false);
|
|
173
173
|
assert.equal(existsSync(runtimeDirectory) && (await readdir(runtimeDirectory)).some((entry) => entry.startsWith(".go-install-") || entry.endsWith(".tmp")), false);
|
|
@@ -188,7 +188,7 @@ test("Windows source installs reuse only a fully verified package-local binary",
|
|
|
188
188
|
assert.equal(reused.installed, false);
|
|
189
189
|
assert.ok(reuse.calls.every((call) => call.file !== "gentle-ai"), "the installer must never fall back to ambient gentle-ai on PATH");
|
|
190
190
|
|
|
191
|
-
await writeFile(join(packageRoot, ".gentle-ai", "v2.
|
|
191
|
+
await writeFile(join(packageRoot, ".gentle-ai", "v2.4.0", "integrity.json"), "{}\n");
|
|
192
192
|
const repaired = windowsGoFixture();
|
|
193
193
|
repaired.setGoExecutable(goPath);
|
|
194
194
|
assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: repaired.run, resolveGoExecutable: async () => goPath })).installed, true);
|
|
@@ -230,7 +230,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
|
|
|
230
230
|
const metadata = fixtureOptions.metadataOverride ?? [
|
|
231
231
|
"gentle-ai.exe: go1.25.10",
|
|
232
232
|
"\tpath\tgithub.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai",
|
|
233
|
-
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.
|
|
233
|
+
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.4.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
|
|
234
234
|
"\tbuild\t-buildmode=exe",
|
|
235
235
|
"\tbuild\t-compiler=gc",
|
|
236
236
|
"\tbuild\tCGO_ENABLED=0",
|
|
@@ -250,7 +250,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
|
|
|
250
250
|
return { stdout: "", stderr: "" };
|
|
251
251
|
}
|
|
252
252
|
if (file === goPath && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
|
|
253
|
-
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 2.
|
|
253
|
+
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 2.4.0\n", stderr: "" };
|
|
254
254
|
throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
|
|
255
255
|
};
|
|
256
256
|
return {
|
|
@@ -292,7 +292,7 @@ test("Windows source installation resolves one validated Go executable and seals
|
|
|
292
292
|
const goCalls = fixture.calls.filter((call) => call.file === fixture.goPath);
|
|
293
293
|
assert.deepEqual(goCalls.map((call) => call.arguments_.slice(0, 2)), [
|
|
294
294
|
["version"],
|
|
295
|
-
["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.
|
|
295
|
+
["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.4.0"],
|
|
296
296
|
["version", "-m"],
|
|
297
297
|
]);
|
|
298
298
|
for (const call of goCalls) {
|
|
@@ -318,14 +318,14 @@ test("Windows source manifest binds verified Go metadata and architecture", asyn
|
|
|
318
318
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-provenance-"));
|
|
319
319
|
const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: "arm64" });
|
|
320
320
|
const result = await installGentleAi({ packageRoot, platform: "win32", arch: "arm64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
321
|
-
const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v2.
|
|
321
|
+
const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v2.4.0", "integrity.json"), "utf8")) as Record<string, string>;
|
|
322
322
|
assert.equal(result.installed, true);
|
|
323
323
|
assert.deepEqual(manifest, {
|
|
324
|
-
version: "2.
|
|
324
|
+
version: "2.4.0",
|
|
325
325
|
method: "go-sumdb-source-build",
|
|
326
326
|
package: "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai",
|
|
327
327
|
module: "github.com/gentleman-programming/gentle-ai/v2",
|
|
328
|
-
tag: "v2.
|
|
328
|
+
tag: "v2.4.0",
|
|
329
329
|
architecture: "arm64",
|
|
330
330
|
binarySha256: createHash("sha256").update("trusted Windows source build").digest("hex"),
|
|
331
331
|
moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|
|
@@ -345,12 +345,12 @@ test("Windows source installation rejects Go metadata for a different architectu
|
|
|
345
345
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
|
|
346
346
|
(error: unknown) => error instanceof Error && "code" in error && error.code === "GENTLE_AI_GO_INSTALL_FAILED",
|
|
347
347
|
);
|
|
348
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.
|
|
348
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai.exe")), false);
|
|
349
349
|
});
|
|
350
350
|
|
|
351
351
|
test("Windows source installation treats a fresh ownerless lock as active", async () => {
|
|
352
352
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-lock-"));
|
|
353
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".v2.
|
|
353
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v2.4.0.install.lock");
|
|
354
354
|
await mkdir(lockPath, { recursive: true });
|
|
355
355
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
356
356
|
await assertManualLockRecoveryRequired(packageRoot, fixture);
|
|
@@ -358,7 +358,7 @@ test("Windows source installation treats a fresh ownerless lock as active", asyn
|
|
|
358
358
|
|
|
359
359
|
test("Windows source installation preserves a lock whose owner nonce changed before release", async () => {
|
|
360
360
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owner-lock-"));
|
|
361
|
-
const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v2.
|
|
361
|
+
const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v2.4.0.install.lock", "owner.json");
|
|
362
362
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
363
363
|
let replacedOwner = false;
|
|
364
364
|
const run = async (...arguments_: Parameters<typeof fixture.run>) => {
|
|
@@ -375,7 +375,7 @@ test("Windows source installation preserves a lock whose owner nonce changed bef
|
|
|
375
375
|
});
|
|
376
376
|
|
|
377
377
|
async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Awaited<ReturnType<typeof hardenedWindowsGoFixture>>, options: Record<string, unknown> = {}) {
|
|
378
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".v2.
|
|
378
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v2.4.0.install.lock");
|
|
379
379
|
await assert.rejects(
|
|
380
380
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, ...options }),
|
|
381
381
|
(error: unknown) => error instanceof Error && error.message.includes(lockPath) && /confirm no installer is active.*remove.*manually/i.test(error.message),
|
|
@@ -385,18 +385,18 @@ async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Aw
|
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
function tombstonePath(packageRoot: string, nonce: string): string {
|
|
388
|
-
return join(packageRoot, ".gentle-ai", `.v2.
|
|
388
|
+
return join(packageRoot, ".gentle-ai", `.v2.4.0.install.tombstone-${nonce}`);
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
async function tombstones(packageRoot: string): Promise<string[]> {
|
|
392
392
|
const runtimeRoot = join(packageRoot, ".gentle-ai");
|
|
393
393
|
return existsSync(runtimeRoot)
|
|
394
|
-
? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v2.
|
|
394
|
+
? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v2.4.0.install.tombstone-"))
|
|
395
395
|
: [];
|
|
396
396
|
}
|
|
397
397
|
|
|
398
398
|
function backupBundlePath(packageRoot: string, nonce: string): string {
|
|
399
|
-
return join(packageRoot, ".gentle-ai", `.v2.
|
|
399
|
+
return join(packageRoot, ".gentle-ai", `.v2.4.0.backup-${nonce}`);
|
|
400
400
|
}
|
|
401
401
|
|
|
402
402
|
async function copyWindowsBundle(source: string, destination: string): Promise<void> {
|
|
@@ -407,7 +407,7 @@ async function copyWindowsBundle(source: string, destination: string): Promise<v
|
|
|
407
407
|
|
|
408
408
|
test("Windows source installation fails closed for an ownerless lock even after the stale threshold", async () => {
|
|
409
409
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-stale-lock-"));
|
|
410
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".v2.
|
|
410
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v2.4.0.install.lock");
|
|
411
411
|
await mkdir(lockPath, { recursive: true });
|
|
412
412
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
413
413
|
await assertManualLockRecoveryRequired(packageRoot, fixture, { now: () => Date.now() + 10 * 60_000 });
|
|
@@ -415,7 +415,7 @@ test("Windows source installation fails closed for an ownerless lock even after
|
|
|
415
415
|
|
|
416
416
|
test("Windows source installation fails closed for a stale owner lock", async () => {
|
|
417
417
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-stale-lock-"));
|
|
418
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".v2.
|
|
418
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v2.4.0.install.lock");
|
|
419
419
|
await mkdir(lockPath, { recursive: true });
|
|
420
420
|
await writeFile(join(lockPath, "owner.json"), `${JSON.stringify({ createdAt: 0, nonce: "0".repeat(64) })}\n`);
|
|
421
421
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -467,7 +467,7 @@ test("Windows source acquisition fails closed when a tombstone appears after its
|
|
|
467
467
|
(error: unknown) => error instanceof Error && error.message.includes(foreignTombstone),
|
|
468
468
|
);
|
|
469
469
|
assert.equal(existsSync(foreignTombstone), true);
|
|
470
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.
|
|
470
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.4.0.install.lock")), false);
|
|
471
471
|
assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false);
|
|
472
472
|
});
|
|
473
473
|
|
|
@@ -475,7 +475,7 @@ test("Windows source release deletes its matching tombstone and allows reuse", a
|
|
|
475
475
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owned-lock-release-"));
|
|
476
476
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
477
477
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
478
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.
|
|
478
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.4.0.install.lock")), false);
|
|
479
479
|
assert.deepEqual(await tombstones(packageRoot), []);
|
|
480
480
|
const reuse = await hardenedWindowsGoFixture(packageRoot);
|
|
481
481
|
assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false);
|
|
@@ -485,7 +485,7 @@ test("Windows source recovers a valid backup after a crash between publication r
|
|
|
485
485
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-crash-"));
|
|
486
486
|
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
487
487
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
488
|
-
const live = join(packageRoot, ".gentle-ai", "v2.
|
|
488
|
+
const live = join(packageRoot, ".gentle-ai", "v2.4.0");
|
|
489
489
|
const backup = backupBundlePath(packageRoot, "crash");
|
|
490
490
|
await rename(live, backup);
|
|
491
491
|
const recovery = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -521,7 +521,7 @@ test("Windows source cleans one validated backup only when the live bundle is va
|
|
|
521
521
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-valid-live-"));
|
|
522
522
|
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
523
523
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
524
|
-
const live = join(packageRoot, ".gentle-ai", "v2.
|
|
524
|
+
const live = join(packageRoot, ".gentle-ai", "v2.4.0");
|
|
525
525
|
const backup = backupBundlePath(packageRoot, "valid");
|
|
526
526
|
await copyWindowsBundle(live, backup);
|
|
527
527
|
const reuse = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -534,7 +534,7 @@ test("Windows source preserves a valid backup when the live bundle is invalid",
|
|
|
534
534
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-invalid-live-"));
|
|
535
535
|
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
536
536
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
537
|
-
const live = join(packageRoot, ".gentle-ai", "v2.
|
|
537
|
+
const live = join(packageRoot, ".gentle-ai", "v2.4.0");
|
|
538
538
|
const backup = backupBundlePath(packageRoot, "valid");
|
|
539
539
|
await copyWindowsBundle(live, backup);
|
|
540
540
|
await writeFile(join(live, "integrity.json"), "{}\n");
|
|
@@ -552,7 +552,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
|
|
|
552
552
|
const fixture = await hardenedWindowsGoFixture(packageRoot, { blockInstall: true });
|
|
553
553
|
const first = installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
554
554
|
await fixture.waitForInstall();
|
|
555
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".v2.
|
|
555
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v2.4.0.install.lock");
|
|
556
556
|
await assert.rejects(
|
|
557
557
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
|
|
558
558
|
(error: unknown) => error instanceof Error && error.message.includes(lockPath),
|
|
@@ -566,7 +566,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
|
|
|
566
566
|
|
|
567
567
|
test("Windows source publication rolls back a prior bundle when final directory swap fails", async () => {
|
|
568
568
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-rollback-"));
|
|
569
|
-
const versionDirectory = join(packageRoot, ".gentle-ai", "v2.
|
|
569
|
+
const versionDirectory = join(packageRoot, ".gentle-ai", "v2.4.0");
|
|
570
570
|
await mkdir(versionDirectory, { recursive: true });
|
|
571
571
|
await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
|
|
572
572
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -590,7 +590,7 @@ test("Windows source publication rolls back a prior bundle when final directory
|
|
|
590
590
|
test("Darwin/Linux signed bundles retain their four-field manifest and reusable compatibility", async () => {
|
|
591
591
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-signed-compatibility-"));
|
|
592
592
|
const payload = Buffer.from("signed archive fixture");
|
|
593
|
-
const asset = { name: "gentle-ai_2.
|
|
593
|
+
const asset = { name: "gentle-ai_2.4.0_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), url: "https://example.invalid/gentle-ai.tar.gz", executable: "gentle-ai" };
|
|
594
594
|
const options = {
|
|
595
595
|
packageRoot,
|
|
596
596
|
platform: "linux",
|
|
@@ -603,7 +603,7 @@ test("Darwin/Linux signed bundles retain their four-field manifest and reusable
|
|
|
603
603
|
},
|
|
604
604
|
};
|
|
605
605
|
await installGentleAi(options);
|
|
606
|
-
const manifestPath = join(packageRoot, ".gentle-ai", "v2.
|
|
606
|
+
const manifestPath = join(packageRoot, ".gentle-ai", "v2.4.0", "integrity.json");
|
|
607
607
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, string>;
|
|
608
608
|
assert.deepEqual(Object.keys(manifest), ["version", "asset", "assetSha256", "binarySha256"]);
|
|
609
609
|
assert.equal((await installGentleAi({ ...options, download: async () => { throw new Error("signed bundle must be reused"); } })).installed, false);
|
|
@@ -680,7 +680,7 @@ test("checksum mismatch cleans temporary state without promoting a binary", asyn
|
|
|
680
680
|
}),
|
|
681
681
|
/checksum mismatch/,
|
|
682
682
|
);
|
|
683
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.
|
|
683
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai")), false);
|
|
684
684
|
assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.startsWith(".gentle-ai-install-")), []);
|
|
685
685
|
});
|
|
686
686
|
|
|
@@ -702,7 +702,7 @@ test("installer promotes only the expected regular executable with executable PO
|
|
|
702
702
|
await chmod(extracted, 0o700);
|
|
703
703
|
},
|
|
704
704
|
});
|
|
705
|
-
const binary = join(packageRoot, ".gentle-ai", "v2.
|
|
705
|
+
const binary = join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai");
|
|
706
706
|
assert.equal(existsSync(binary), true);
|
|
707
707
|
assert.equal(await readFile(binary, "utf8"), "native executable");
|
|
708
708
|
assert.ok(((await stat(binary)).mode & 0o111) !== 0);
|
|
@@ -733,7 +733,7 @@ test("installer rejects an extracted binary that differs from its pinned digest"
|
|
|
733
733
|
}),
|
|
734
734
|
/binary checksum mismatch/,
|
|
735
735
|
);
|
|
736
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.
|
|
736
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai")), false);
|
|
737
737
|
});
|
|
738
738
|
|
|
739
739
|
test("installer repairs a valid non-executable POSIX binary instead of reusing it", async (t) => {
|
|
@@ -756,7 +756,7 @@ test("installer repairs a valid non-executable POSIX binary instead of reusing i
|
|
|
756
756
|
},
|
|
757
757
|
};
|
|
758
758
|
await installGentleAi(options);
|
|
759
|
-
const binary = join(packageRoot, ".gentle-ai", "v2.
|
|
759
|
+
const binary = join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai");
|
|
760
760
|
await chmod(binary, 0o600);
|
|
761
761
|
const repaired = await installGentleAi(options);
|
|
762
762
|
assert.equal(repaired.installed, true);
|
|
@@ -793,7 +793,7 @@ test("installer rejects archives with multiple expected executable entries", asy
|
|
|
793
793
|
}),
|
|
794
794
|
/exactly one regular gentle-ai/,
|
|
795
795
|
);
|
|
796
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.
|
|
796
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai")), false);
|
|
797
797
|
});
|
|
798
798
|
|
|
799
799
|
test("installer rejects an archive without the expected regular executable", async () => {
|
|
@@ -811,5 +811,5 @@ test("installer rejects an archive without the expected regular executable", asy
|
|
|
811
811
|
}),
|
|
812
812
|
/non-regular gentle-ai/,
|
|
813
813
|
);
|
|
814
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.
|
|
814
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.4.0", "gentle-ai")), false);
|
|
815
815
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import {
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
5
6
|
import { join } from "node:path";
|
|
6
7
|
import test from "node:test";
|
|
@@ -8,12 +9,14 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
8
9
|
import {
|
|
9
10
|
COMMIT_TRANSACTION_STATE,
|
|
10
11
|
assertNoUnresolvedCommitTransaction,
|
|
12
|
+
abandonCommitTransaction,
|
|
11
13
|
inspectCommitTransaction,
|
|
12
14
|
prepareCommitTransactionInvocation,
|
|
13
15
|
reconcileCommitTransaction,
|
|
14
16
|
runGitCommitTransaction,
|
|
15
17
|
verifyCommitTransactionResult,
|
|
16
18
|
} from "../lib/git-commit-transaction.ts";
|
|
19
|
+
import type { CommitTransactionInvocation } from "../lib/git-commit-transaction.ts";
|
|
17
20
|
import type { NativeReviewCli, NativeValidateResult } from "../lib/native-review-cli.ts";
|
|
18
21
|
|
|
19
22
|
function git(cwd: string, ...arguments_: string[]): string {
|
|
@@ -300,3 +303,228 @@ test("cancellation cannot strand a commit after HEAD advances", async (t) => {
|
|
|
300
303
|
assert.equal(result.status, "committed");
|
|
301
304
|
assert.deepEqual(inspectCommitTransaction(cwd), { status: "clean" });
|
|
302
305
|
});
|
|
306
|
+
|
|
307
|
+
function unbornRepository(t: test.TestContext): string {
|
|
308
|
+
const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-commit-transaction-unborn-"));
|
|
309
|
+
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
310
|
+
git(cwd, "init", "-b", "main");
|
|
311
|
+
git(cwd, "config", "user.name", "Commit Transaction Test");
|
|
312
|
+
git(cwd, "config", "user.email", "commit-transaction@example.invalid");
|
|
313
|
+
writeFileSync(join(cwd, "initial.txt"), "first commit\n");
|
|
314
|
+
git(cwd, "add", "initial.txt");
|
|
315
|
+
return cwd;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
test("an unborn repository creates its first reviewed commit without a prior HEAD", async (t) => {
|
|
319
|
+
const cwd = unbornRepository(t);
|
|
320
|
+
const intendedTree = git(cwd, "write-tree");
|
|
321
|
+
const command = `git commit ${JSON.stringify("-m")} ${JSON.stringify("initial")}`;
|
|
322
|
+
const invocation = prepareCommitTransactionInvocation({
|
|
323
|
+
command,
|
|
324
|
+
cwd,
|
|
325
|
+
arguments: ["-m", "initial"],
|
|
326
|
+
authorization: {
|
|
327
|
+
lineageId: "unborn-first",
|
|
328
|
+
storeRevision: "sha256:" + "a".repeat(64),
|
|
329
|
+
fingerprint: "sha256:" + "b".repeat(64),
|
|
330
|
+
intendedTree,
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
const result = await runGitCommitTransaction(invocation, { nativeReviewCli: native(cwd, "unborn-first") });
|
|
334
|
+
assert.equal(result.status, "committed");
|
|
335
|
+
assert.equal(result.tree, intendedTree);
|
|
336
|
+
assert.equal(result.head, git(cwd, "rev-parse", "HEAD"));
|
|
337
|
+
assert.equal(git(cwd, "rev-parse", "HEAD^{tree}"), intendedTree);
|
|
338
|
+
assert.deepEqual(inspectCommitTransaction(cwd), { status: "clean" });
|
|
339
|
+
const verified = verifyCommitTransactionResult(cwd, result.transactionId);
|
|
340
|
+
assert.equal(verified.tree, intendedTree);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("an unborn repository that fails to commit leaves no HEAD and allows exact retry", async (t) => {
|
|
344
|
+
const cwd = unbornRepository(t);
|
|
345
|
+
const intendedTree = git(cwd, "write-tree");
|
|
346
|
+
const command = `git commit ${JSON.stringify("-m")} ${JSON.stringify("initial")}`;
|
|
347
|
+
const invocation = prepareCommitTransactionInvocation({
|
|
348
|
+
command,
|
|
349
|
+
cwd,
|
|
350
|
+
arguments: ["-m", "initial"],
|
|
351
|
+
authorization: {
|
|
352
|
+
lineageId: "unborn-retry",
|
|
353
|
+
storeRevision: "sha256:" + "a".repeat(64),
|
|
354
|
+
fingerprint: "sha256:" + "b".repeat(64),
|
|
355
|
+
intendedTree,
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
await assert.rejects(
|
|
359
|
+
runGitCommitTransaction(invocation, { nativeReviewCli: native(cwd, "unborn-retry", "invalidated") }),
|
|
360
|
+
/native pre-commit validation denied/,
|
|
361
|
+
);
|
|
362
|
+
assert.throws(() => git(cwd, "rev-parse", "--verify", "HEAD"), /fatal|Needed/i);
|
|
363
|
+
assert.equal(inspectCommitTransaction(cwd).record?.state, COMMIT_TRANSACTION_STATE.VALIDATION_FAILED);
|
|
364
|
+
// A denied attempt leaves the index intact and HEAD unborn, so the exact
|
|
365
|
+
// commit command can be retried. The failed transaction requires explicit
|
|
366
|
+
// recovery: abandon archives it (HEAD never moved, so abandon is allowed),
|
|
367
|
+
// then a fresh invocation authorizes the same tree and commits successfully.
|
|
368
|
+
abandonCommitTransaction(cwd);
|
|
369
|
+
assert.equal(inspectCommitTransaction(cwd).status, "clean");
|
|
370
|
+
const retry = prepareCommitTransactionInvocation({
|
|
371
|
+
command,
|
|
372
|
+
cwd,
|
|
373
|
+
arguments: ["-m", "initial"],
|
|
374
|
+
authorization: {
|
|
375
|
+
lineageId: "unborn-retry",
|
|
376
|
+
storeRevision: "sha256:" + "a".repeat(64),
|
|
377
|
+
fingerprint: "sha256:" + "b".repeat(64),
|
|
378
|
+
intendedTree,
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
const result = await runGitCommitTransaction(retry, { nativeReviewCli: native(cwd, "unborn-retry") });
|
|
382
|
+
assert.equal(result.status, "committed");
|
|
383
|
+
assert.equal(result.tree, intendedTree);
|
|
384
|
+
assert.equal(result.head, git(cwd, "rev-parse", "HEAD"));
|
|
385
|
+
assert.equal(git(cwd, "rev-parse", "HEAD^{tree}"), intendedTree);
|
|
386
|
+
assert.deepEqual(inspectCommitTransaction(cwd), { status: "clean" });
|
|
387
|
+
const verified = verifyCommitTransactionResult(cwd, result.transactionId);
|
|
388
|
+
assert.equal(verified.tree, intendedTree);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
function unbornInvocation(cwd: string, lineageId: string): CommitTransactionInvocation {
|
|
392
|
+
return prepareCommitTransactionInvocation({
|
|
393
|
+
command: "git commit -m initial",
|
|
394
|
+
cwd,
|
|
395
|
+
arguments: ["-m", "initial"],
|
|
396
|
+
authorization: {
|
|
397
|
+
lineageId,
|
|
398
|
+
storeRevision: "sha256:" + "a".repeat(64),
|
|
399
|
+
fingerprint: "sha256:" + "b".repeat(64),
|
|
400
|
+
intendedTree: git(cwd, "write-tree"),
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Mirrors lib/git-commit-transaction.ts canonicalJson + sha256 so tests can
|
|
406
|
+
// prove the durable repository identity is byte-for-byte compatible with the
|
|
407
|
+
// previous formula `sha256(canonicalJson({ common_directory: commonDir, roots }))`.
|
|
408
|
+
function canonicalJson(value: unknown): string {
|
|
409
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
410
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
411
|
+
const object = value as Record<string, unknown>;
|
|
412
|
+
return `{${Object.keys(object).filter((key) => object[key] !== undefined).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
|
|
413
|
+
}
|
|
414
|
+
function identitySha256(value: string): string {
|
|
415
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
test("a born repository keeps the durable transaction identity formula with sorted root commits across the unborn-handling upgrade", async (t) => {
|
|
419
|
+
const cwd = repository(t);
|
|
420
|
+
stage(cwd);
|
|
421
|
+
installHook(cwd, "pre-commit", "exit 23");
|
|
422
|
+
const commonDir = realpathSync(git(cwd, "rev-parse", "--path-format=absolute", "--git-common-dir"));
|
|
423
|
+
const roots = git(cwd, "rev-list", "--max-parents=0", "HEAD").split(/\r?\n/).filter(Boolean).sort();
|
|
424
|
+
const expected = identitySha256(canonicalJson({ common_directory: commonDir, roots }));
|
|
425
|
+
await assert.rejects(
|
|
426
|
+
runGitCommitTransaction(invocation(cwd, "born-identity"), { nativeReviewCli: native(cwd, "born-identity") }),
|
|
427
|
+
/pre-commit hook failed/,
|
|
428
|
+
);
|
|
429
|
+
const record = inspectCommitTransaction(cwd).record;
|
|
430
|
+
assert.ok(record, "a failing hook leaves an active transaction record carrying the repository identity");
|
|
431
|
+
assert.equal(record!.repository_id, expected);
|
|
432
|
+
assert.equal(record!.common_directory, commonDir);
|
|
433
|
+
abandonCommitTransaction(cwd);
|
|
434
|
+
assert.equal(inspectCommitTransaction(cwd).status, "clean");
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("an unborn repository derives a deterministic repository identity without rev-list HEAD and without masking errors", async (t) => {
|
|
438
|
+
const cwd = unbornRepository(t);
|
|
439
|
+
const commonDir = realpathSync(git(cwd, "rev-parse", "--path-format=absolute", "--git-common-dir"));
|
|
440
|
+
// Unborn has no root commits reachable from HEAD; the identity uses the
|
|
441
|
+
// empty roots set, which is deterministic and avoids `rev-list HEAD`.
|
|
442
|
+
const expected = identitySha256(canonicalJson({ common_directory: commonDir, roots: [] }));
|
|
443
|
+
const intendedTree = git(cwd, "write-tree");
|
|
444
|
+
const command = `git commit ${JSON.stringify("-m")} ${JSON.stringify("initial")}`;
|
|
445
|
+
const invocation = prepareCommitTransactionInvocation({
|
|
446
|
+
command,
|
|
447
|
+
cwd,
|
|
448
|
+
arguments: ["-m", "initial"],
|
|
449
|
+
authorization: {
|
|
450
|
+
lineageId: "unborn-identity",
|
|
451
|
+
storeRevision: "sha256:" + "a".repeat(64),
|
|
452
|
+
fingerprint: "sha256:" + "b".repeat(64),
|
|
453
|
+
intendedTree,
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
await assert.rejects(
|
|
457
|
+
runGitCommitTransaction(invocation, { nativeReviewCli: native(cwd, "unborn-identity", "invalidated") }),
|
|
458
|
+
/native pre-commit validation denied/,
|
|
459
|
+
);
|
|
460
|
+
const record = inspectCommitTransaction(cwd).record;
|
|
461
|
+
assert.ok(record, "a denied unborn attempt leaves an active transaction record carrying the repository identity");
|
|
462
|
+
assert.equal(record!.repository_id, expected);
|
|
463
|
+
assert.equal(record!.common_directory, commonDir);
|
|
464
|
+
abandonCommitTransaction(cwd);
|
|
465
|
+
assert.equal(inspectCommitTransaction(cwd).status, "clean");
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Resolves the canonical absolute git common directory for asserting the
|
|
469
|
+
// absence of the transaction state root without assuming a literal `.git`.
|
|
470
|
+
function transactionStateRoot(cwd: string): string {
|
|
471
|
+
const commonDir = realpathSync(git(cwd, "rev-parse", "--path-format=absolute", "--git-common-dir"));
|
|
472
|
+
return join(commonDir, "gentle-pi", "commit-transactions");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
test("preparation fails closed for a corrupt HEAD ref without writing transaction state and reports corruption", (t) => {
|
|
476
|
+
const cwd = unbornRepository(t);
|
|
477
|
+
// A garbage ref (not a valid SHA) makes HEAD unresolvable. Resolving root
|
|
478
|
+
// commits during repository binding rethrows, so preparation must throw,
|
|
479
|
+
// no transaction state may be written, and inspection reports corruption
|
|
480
|
+
// (repository identity itself cannot be resolved while HEAD is corrupt).
|
|
481
|
+
mkdirSync(join(cwd, ".git", "refs", "heads"), { recursive: true });
|
|
482
|
+
writeFileSync(join(cwd, ".git", "refs", "heads", "main"), "z".repeat(40));
|
|
483
|
+
assert.throws(() => unbornInvocation(cwd, "corrupt-head"));
|
|
484
|
+
assert.equal(existsSync(transactionStateRoot(cwd)), false, "no transaction state root is written for a corrupt HEAD");
|
|
485
|
+
const inspection = inspectCommitTransaction(cwd);
|
|
486
|
+
assert.equal(inspection.status, "corrupted", "repository corruption makes inspection corrupted even with no transaction state written");
|
|
487
|
+
assert.ok(typeof inspection.reason === "string" && inspection.reason.length > 0, "inspection reason is present and actionable");
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("preparation fails closed for a symbolic HEAD pointing at a missing object instead of classifying it as unborn", (t) => {
|
|
491
|
+
const cwd = unbornRepository(t);
|
|
492
|
+
// A valid-format SHA with no object: rev-parse --verify HEAD succeeds, so
|
|
493
|
+
// resolveHead returns a commit id rather than classifying HEAD as unborn.
|
|
494
|
+
// Resolving root commits then fails on the missing object, so preparation
|
|
495
|
+
// must throw, no transaction state may be written, and inspection reports
|
|
496
|
+
// corruption. This must never be classified as an unborn empty-roots repo.
|
|
497
|
+
mkdirSync(join(cwd, ".git", "refs", "heads"), { recursive: true });
|
|
498
|
+
writeFileSync(join(cwd, ".git", "refs", "heads", "main"), "0123456789abcdef0123456789abcdef01234567\n");
|
|
499
|
+
assert.throws(() => unbornInvocation(cwd, "missing-object-head"));
|
|
500
|
+
assert.equal(existsSync(transactionStateRoot(cwd)), false, "no transaction state root is written for a missing-object HEAD");
|
|
501
|
+
const inspection = inspectCommitTransaction(cwd);
|
|
502
|
+
assert.equal(inspection.status, "corrupted", "a missing-object HEAD is repository corruption, not a clean or unborn repo");
|
|
503
|
+
assert.ok(typeof inspection.reason === "string" && inspection.reason.length > 0, "inspection reason is present and actionable");
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("resolveHead propagates a probe timeout instead of masking it as an unborn HEAD", (t) => {
|
|
507
|
+
const cwd = unbornRepository(t);
|
|
508
|
+
const wrapperDir = mkdtempSync(join(tmpdir(), "gentle-pi-commit-transaction-timeout-"));
|
|
509
|
+
t.after(() => rmSync(wrapperDir, { recursive: true, force: true }));
|
|
510
|
+
const realGit = execFileSync("which", ["git"], { encoding: "utf8" }).trim();
|
|
511
|
+
writeFileSync(join(wrapperDir, "git"), `#!/bin/sh\nif [ "$1" = "rev-parse" ] && [ "$2" = "--verify" ] && [ "$3" = "HEAD" ] && [ $# -eq 3 ]; then\nsleep 30\nelse\nexec ${realGit} "$@"\nfi\n`);
|
|
512
|
+
chmodSync(join(wrapperDir, "git"), 0o755);
|
|
513
|
+
const originalPath = process.env.PATH;
|
|
514
|
+
process.env.PATH = `${wrapperDir}:${originalPath}`;
|
|
515
|
+
try {
|
|
516
|
+
// The probe wrapper sleeps past GIT_TIMEOUT_MS (10s), so execFileSync
|
|
517
|
+
// kills the process and resolveHead rethrows the raw timeout error
|
|
518
|
+
// rather than classifying it as an unborn HEAD. Accept only a failure
|
|
519
|
+
// that genuinely represents a timeout, not any arbitrary error.
|
|
520
|
+
assert.throws(
|
|
521
|
+
() => unbornInvocation(cwd, "timeout-head"),
|
|
522
|
+
(error: unknown) => error !== null && typeof error === "object"
|
|
523
|
+
&& ((error as { code?: unknown }).code === "ETIMEDOUT" || (error as { killed?: unknown }).killed === true),
|
|
524
|
+
"resolveHead must propagate a probe timeout instead of masking it as an unborn HEAD",
|
|
525
|
+
);
|
|
526
|
+
} finally {
|
|
527
|
+
process.env.PATH = originalPath;
|
|
528
|
+
}
|
|
529
|
+
assert.equal(inspectCommitTransaction(cwd).status, "clean");
|
|
530
|
+
});
|