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
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync, chmodSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { gzipSync } from "node:zlib";
|
|
8
|
+
import {
|
|
9
|
+
PI_RUNTIME_IDENTITY,
|
|
10
|
+
PROVIDER_CONTRACT_BUNDLE_INVALID,
|
|
11
|
+
PROVIDER_CONTRACT_ROLE_IDS,
|
|
12
|
+
PROVIDER_TRANSPORT_CAPABILITY,
|
|
13
|
+
generateProviderContractBaselines,
|
|
14
|
+
piRuntimeRegistration,
|
|
15
|
+
verifyProviderContractBundleArchive,
|
|
16
|
+
verifyProviderContractBundleEntries,
|
|
17
|
+
verifyProviderContractBundleTree,
|
|
18
|
+
} from "../lib/provider-contract-bundle.ts";
|
|
19
|
+
|
|
20
|
+
// Fixture provenance: generated from gentle-ai main (contract 1.1.0) with
|
|
21
|
+
// `go run ./internal/providercontractbundlecmd generate --out <dir>`.
|
|
22
|
+
const FIXTURE_ROOT = join(import.meta.dirname, "fixtures", "provider-contract-bundle", "v1.1.0");
|
|
23
|
+
const FIXTURE_PATHS = [
|
|
24
|
+
"README.md",
|
|
25
|
+
"manifest.json",
|
|
26
|
+
"schemas/lens.schema.json",
|
|
27
|
+
"schemas/refuter.schema.json",
|
|
28
|
+
"schemas/targeted-validator.schema.json",
|
|
29
|
+
"vectors/lens.json",
|
|
30
|
+
"vectors/refuter.json",
|
|
31
|
+
"vectors/targeted-validator.json",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
function fixtureEntries(): Map<string, Buffer> {
|
|
35
|
+
return new Map(FIXTURE_PATHS.map((path) => [path, readFileSync(join(FIXTURE_ROOT, path))]));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sha256Hex(bytes: Buffer): string {
|
|
39
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function withManifest(entries: Map<string, Buffer>, mutate: (manifest: Record<string, unknown>) => void): Map<string, Buffer> {
|
|
43
|
+
const manifest = JSON.parse((entries.get("manifest.json") as Buffer).toString("utf8")) as Record<string, unknown>;
|
|
44
|
+
mutate(manifest);
|
|
45
|
+
entries.set("manifest.json", Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"));
|
|
46
|
+
return entries;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A valid 1.0.0-shaped bundle: the manifest predates the runtime registry.
|
|
50
|
+
function contractOneZeroEntries(): Map<string, Buffer> {
|
|
51
|
+
return withManifest(fixtureEntries(), (manifest) => {
|
|
52
|
+
manifest.contract_semver = "1.0.0";
|
|
53
|
+
delete manifest.runtimes;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function assertRejects(entries: Map<string, Buffer>, messagePart: string): void {
|
|
58
|
+
assert.throws(
|
|
59
|
+
() => verifyProviderContractBundleEntries(entries),
|
|
60
|
+
(error: unknown) =>
|
|
61
|
+
error instanceof Error &&
|
|
62
|
+
error.message.startsWith(`${PROVIDER_CONTRACT_BUNDLE_INVALID}:`) &&
|
|
63
|
+
error.message.includes(messagePart),
|
|
64
|
+
`expected rejection mentioning ${JSON.stringify(messagePart)}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- minimal deterministic tar.gz writer for archive-leg tests ---------------
|
|
69
|
+
|
|
70
|
+
interface TarEntry {
|
|
71
|
+
readonly name: string;
|
|
72
|
+
readonly bytes: Buffer;
|
|
73
|
+
readonly mode?: number;
|
|
74
|
+
readonly typeflag?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function tarHeader(entry: TarEntry): Buffer {
|
|
78
|
+
const header = Buffer.alloc(512);
|
|
79
|
+
header.write(entry.name, 0, "ascii");
|
|
80
|
+
header.write(`${(entry.mode ?? 0o644).toString(8).padStart(7, "0")}\0`, 100, "ascii");
|
|
81
|
+
header.write("0000000\0", 108, "ascii");
|
|
82
|
+
header.write("0000000\0", 116, "ascii");
|
|
83
|
+
header.write(`${entry.bytes.length.toString(8).padStart(11, "0")}\0`, 124, "ascii");
|
|
84
|
+
header.write("00000000000\0", 136, "ascii");
|
|
85
|
+
header.write(" ", 148, "ascii");
|
|
86
|
+
header.write(entry.typeflag ?? "0", 156, "ascii");
|
|
87
|
+
header.write("ustar\0", 257, "ascii");
|
|
88
|
+
header.write("00", 263, "ascii");
|
|
89
|
+
let checksum = 0;
|
|
90
|
+
for (const byte of header) checksum += byte;
|
|
91
|
+
header.write(`${checksum.toString(8).padStart(6, "0")}\0 `, 148, "ascii");
|
|
92
|
+
return header;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildTarGz(entries: readonly TarEntry[], trailingBytes = Buffer.alloc(0)): Buffer {
|
|
96
|
+
const blocks: Buffer[] = [];
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
blocks.push(tarHeader(entry));
|
|
99
|
+
const padding = (512 - (entry.bytes.length % 512)) % 512;
|
|
100
|
+
blocks.push(entry.bytes, Buffer.alloc(padding));
|
|
101
|
+
}
|
|
102
|
+
blocks.push(Buffer.alloc(1024), trailingBytes);
|
|
103
|
+
return gzipSync(Buffer.concat(blocks));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function fixtureArchive(): Buffer {
|
|
107
|
+
return buildTarGz([...fixtureEntries()].map(([name, bytes]) => ({ name, bytes })));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function withTemporaryDirectory<T>(run: (directory: string) => T): T {
|
|
111
|
+
const directory = mkdtempSync(join(tmpdir(), "gentle-pi-provider-bundle-"));
|
|
112
|
+
try {
|
|
113
|
+
return run(directory);
|
|
114
|
+
} finally {
|
|
115
|
+
rmSync(directory, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function materialize(entries: Map<string, Buffer>, directory: string): void {
|
|
120
|
+
for (const [name, bytes] of entries) {
|
|
121
|
+
const destination = join(directory, ...name.split("/"));
|
|
122
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
123
|
+
writeFileSync(destination, bytes);
|
|
124
|
+
chmodSync(destination, 0o644);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- positive legs ------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
test("verifies the generated 1.1.0 fixture tree and reports the exact contract surface", () => {
|
|
131
|
+
const bundle = verifyProviderContractBundleTree(FIXTURE_ROOT);
|
|
132
|
+
assert.equal(bundle.contractSemver, "1.1.0");
|
|
133
|
+
assert.deepEqual([bundle.major, bundle.minor, bundle.patch], [1, 1, 0]);
|
|
134
|
+
assert.equal(bundle.transportCapability, PROVIDER_TRANSPORT_CAPABILITY);
|
|
135
|
+
assert.deepEqual(bundle.runtimes, ["claude-code", "codex", "opencode", PI_RUNTIME_IDENTITY]);
|
|
136
|
+
assert.deepEqual(bundle.roles.map((role) => role.id), [...PROVIDER_CONTRACT_ROLE_IDS]);
|
|
137
|
+
assert.equal(bundle.entries.size, 8);
|
|
138
|
+
for (const role of bundle.roles) {
|
|
139
|
+
assert.equal(bundle.entrySha256.get(role.schemaPath), role.schemaSha256);
|
|
140
|
+
assert.equal(bundle.entrySha256.get(role.vectorPath), role.vectorSha256);
|
|
141
|
+
assert.deepEqual(role.requiredCapabilities, [PROVIDER_TRANSPORT_CAPABILITY]);
|
|
142
|
+
}
|
|
143
|
+
assert.match(bundle.treeSha256, /^[0-9a-f]{64}$/);
|
|
144
|
+
assert.deepEqual(piRuntimeRegistration(bundle), { registered: true });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("verifies the same bundle as an in-memory-read archive and reports the archive digest", () => {
|
|
148
|
+
withTemporaryDirectory((directory) => {
|
|
149
|
+
const archiveBytes = fixtureArchive();
|
|
150
|
+
const archivePath = join(directory, "gentle-ai-review-provider-contract-1.1.0.tar.gz");
|
|
151
|
+
writeFileSync(archivePath, archiveBytes);
|
|
152
|
+
const { bundle, archiveSha256 } = verifyProviderContractBundleArchive(archivePath);
|
|
153
|
+
assert.equal(archiveSha256, sha256Hex(archiveBytes));
|
|
154
|
+
const tree = verifyProviderContractBundleTree(FIXTURE_ROOT);
|
|
155
|
+
assert.equal(bundle.treeSha256, tree.treeSha256);
|
|
156
|
+
assert.deepEqual([...bundle.entrySha256.entries()], [...tree.entrySha256.entries()]);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("a 1.0.0 manifest without runtimes is a VALID bundle, but pi is reported unregistered for relay trust", () => {
|
|
161
|
+
const bundle = verifyProviderContractBundleEntries(contractOneZeroEntries());
|
|
162
|
+
assert.equal(bundle.contractSemver, "1.0.0");
|
|
163
|
+
assert.equal(bundle.runtimes, undefined);
|
|
164
|
+
const registration = piRuntimeRegistration(bundle);
|
|
165
|
+
assert.equal(registration.registered, false);
|
|
166
|
+
assert.match(registration.reason ?? "", /predates the runtime identity registry/);
|
|
167
|
+
assert.match(registration.reason ?? "", /must not trust/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("generated baselines project roles, capabilities, and runtime registration deterministically", () => {
|
|
171
|
+
const bundle = verifyProviderContractBundleTree(FIXTURE_ROOT);
|
|
172
|
+
const baselines = generateProviderContractBaselines(bundle);
|
|
173
|
+
assert.deepEqual([...baselines.keys()].sort(), ["provider-capabilities.baseline.json", "provider-roles.baseline.json"]);
|
|
174
|
+
const roles = JSON.parse(baselines.get("provider-roles.baseline.json") as string) as { roles: { id: string; result_schema_id: string; schema_sha256: string }[] };
|
|
175
|
+
assert.deepEqual(roles.roles.map((role) => role.id), [...PROVIDER_CONTRACT_ROLE_IDS]);
|
|
176
|
+
const capabilities = JSON.parse(baselines.get("provider-capabilities.baseline.json") as string) as Record<string, unknown>;
|
|
177
|
+
assert.equal(capabilities.transport_capability, PROVIDER_TRANSPORT_CAPABILITY);
|
|
178
|
+
assert.deepEqual(capabilities.mandatory_capabilities, [PROVIDER_TRANSPORT_CAPABILITY]);
|
|
179
|
+
assert.equal(capabilities.pi_registered, true);
|
|
180
|
+
// Byte determinism: regenerating from the same bundle is identical.
|
|
181
|
+
const again = generateProviderContractBaselines(verifyProviderContractBundleTree(FIXTURE_ROOT));
|
|
182
|
+
assert.deepEqual([...again.entries()], [...baselines.entries()]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// --- inventory and hash rejections ---------------------------------------------
|
|
186
|
+
|
|
187
|
+
test("rejects a ninth file in the inventory", () => {
|
|
188
|
+
const entries = fixtureEntries();
|
|
189
|
+
entries.set("extra.txt", Buffer.from("surprise\n"));
|
|
190
|
+
assertRejects(entries, "9 files");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("rejects a missing bundle entry", () => {
|
|
194
|
+
const entries = fixtureEntries();
|
|
195
|
+
entries.delete("vectors/lens.json");
|
|
196
|
+
assertRejects(entries, "7 files");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("rejects a bundle whose schema bytes do not match the manifest SHA-256", () => {
|
|
200
|
+
const entries = fixtureEntries();
|
|
201
|
+
const tampered = Buffer.from(entries.get("schemas/lens.schema.json") as Buffer);
|
|
202
|
+
tampered[tampered.length - 2] = 0x20;
|
|
203
|
+
entries.set("schemas/lens.schema.json", tampered);
|
|
204
|
+
assertRejects(entries, "does not match its manifest SHA-256");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("rejects a vector that no longer equals the schema's canonical example even when its hash is updated", () => {
|
|
208
|
+
const entries = fixtureEntries();
|
|
209
|
+
const vector = JSON.parse((entries.get("vectors/refuter.json") as Buffer).toString("utf8")) as Record<string, unknown>;
|
|
210
|
+
vector.subject_hash = "0".repeat(64);
|
|
211
|
+
const tampered = Buffer.from(`${JSON.stringify(vector)}\n`, "utf8");
|
|
212
|
+
entries.set("vectors/refuter.json", tampered);
|
|
213
|
+
withManifest(entries, (manifest) => {
|
|
214
|
+
const roles = manifest.roles as { id: string; vector: { sha256: string } }[];
|
|
215
|
+
(roles.find((role) => role.id === "refuter") as { vector: { sha256: string } }).vector.sha256 = sha256Hex(tampered);
|
|
216
|
+
});
|
|
217
|
+
assertRejects(entries, "does not equal the schema's canonical example");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// --- strict manifest decode ------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
test("rejects unknown manifest fields", () => {
|
|
223
|
+
assertRejects(
|
|
224
|
+
withManifest(fixtureEntries(), (manifest) => {
|
|
225
|
+
manifest.surprise = true;
|
|
226
|
+
}),
|
|
227
|
+
'unknown field "surprise"',
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("rejects duplicate manifest JSON keys", () => {
|
|
232
|
+
const entries = fixtureEntries();
|
|
233
|
+
const text = (entries.get("manifest.json") as Buffer).toString("utf8");
|
|
234
|
+
const duplicated = text.replace('"contract_semver"', '"schema": "gentle-ai.review-provider-contract-bundle/v1",\n "contract_semver"');
|
|
235
|
+
entries.set("manifest.json", Buffer.from(duplicated, "utf8"));
|
|
236
|
+
assertRejects(entries, "duplicate key");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("rejects an unpinned transport capability", () => {
|
|
240
|
+
assertRejects(
|
|
241
|
+
withManifest(fixtureEntries(), (manifest) => {
|
|
242
|
+
manifest.transport_capability = "gentle-ai.provider-transport/v2";
|
|
243
|
+
}),
|
|
244
|
+
"transport_capability must be pinned",
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("rejects an unsupported contract major", () => {
|
|
249
|
+
assertRejects(
|
|
250
|
+
withManifest(fixtureEntries(), (manifest) => {
|
|
251
|
+
manifest.contract_semver = "2.0.0";
|
|
252
|
+
}),
|
|
253
|
+
"unsupported provider contract major 2",
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// --- runtimes registry rules -----------------------------------------------------
|
|
258
|
+
|
|
259
|
+
test("rejects a 1.1.0 manifest without the required runtimes registry", () => {
|
|
260
|
+
assertRejects(
|
|
261
|
+
withManifest(fixtureEntries(), (manifest) => {
|
|
262
|
+
delete manifest.runtimes;
|
|
263
|
+
}),
|
|
264
|
+
"manifest.runtimes is required from contract 1.1.0",
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("rejects a 1.1.0 manifest whose runtimes registry does not register pi", () => {
|
|
269
|
+
assertRejects(
|
|
270
|
+
withManifest(fixtureEntries(), (manifest) => {
|
|
271
|
+
manifest.runtimes = ["claude-code", "codex", "opencode"];
|
|
272
|
+
}),
|
|
273
|
+
'does not register the "pi" runtime identity',
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("rejects a 1.0.0 manifest that carries a runtimes registry it cannot have", () => {
|
|
278
|
+
assertRejects(
|
|
279
|
+
withManifest(fixtureEntries(), (manifest) => {
|
|
280
|
+
manifest.contract_semver = "1.0.0";
|
|
281
|
+
}),
|
|
282
|
+
"manifest.runtimes is not part of contract 1.0.0",
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// --- roles and capabilities --------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
test("rejects an unknown role id", () => {
|
|
289
|
+
assertRejects(
|
|
290
|
+
withManifest(fixtureEntries(), (manifest) => {
|
|
291
|
+
(manifest.roles as { id: string }[])[1].id = "judge";
|
|
292
|
+
}),
|
|
293
|
+
'manifest role 1 is "judge"',
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("rejects an unknown mandatory capability", () => {
|
|
298
|
+
assertRejects(
|
|
299
|
+
withManifest(fixtureEntries(), (manifest) => {
|
|
300
|
+
(manifest.roles as { required_capabilities: string[] }[])[0].required_capabilities = [
|
|
301
|
+
PROVIDER_TRANSPORT_CAPABILITY,
|
|
302
|
+
"gentle-ai.telepathy/v1",
|
|
303
|
+
];
|
|
304
|
+
}),
|
|
305
|
+
'unknown mandatory capability "gentle-ai.telepathy/v1"',
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("rejects a schema whose $id does not match the manifest result schema id", () => {
|
|
310
|
+
assertRejects(
|
|
311
|
+
withManifest(fixtureEntries(), (manifest) => {
|
|
312
|
+
(manifest.roles as { result_schema_id: string }[])[0].result_schema_id = "https://gentle-ai.dev/schema/review/other/v1";
|
|
313
|
+
}),
|
|
314
|
+
"$id does not match",
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// --- archive safety ------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
test("rejects a tar entry that escapes the bundle root", () => {
|
|
321
|
+
withTemporaryDirectory((directory) => {
|
|
322
|
+
const entries = [...fixtureEntries()].map(([name, bytes]) => ({ name, bytes }));
|
|
323
|
+
entries.push({ name: "../evil.txt", bytes: Buffer.from("escape\n") });
|
|
324
|
+
const archivePath = join(directory, "escape.tar.gz");
|
|
325
|
+
writeFileSync(archivePath, buildTarGz(entries));
|
|
326
|
+
assert.throws(() => verifyProviderContractBundleArchive(archivePath), /is unsafe/);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("rejects a non-regular tar entry (symlink typeflag)", () => {
|
|
331
|
+
withTemporaryDirectory((directory) => {
|
|
332
|
+
const entries = [...fixtureEntries()].map(([name, bytes]) => ({ name, bytes } as { name: string; bytes: Buffer; typeflag?: string }));
|
|
333
|
+
(entries[0] as { typeflag?: string }).typeflag = "2";
|
|
334
|
+
const archivePath = join(directory, "symlink.tar.gz");
|
|
335
|
+
writeFileSync(archivePath, buildTarGz(entries));
|
|
336
|
+
assert.throws(() => verifyProviderContractBundleArchive(archivePath), /type "2" is forbidden/);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("rejects an executable or setuid tar entry mode", () => {
|
|
341
|
+
withTemporaryDirectory((directory) => {
|
|
342
|
+
const entries = [...fixtureEntries()].map(([name, bytes]) => ({ name, bytes } as { name: string; bytes: Buffer; mode?: number }));
|
|
343
|
+
(entries[2] as { mode?: number }).mode = 0o4755;
|
|
344
|
+
const archivePath = join(directory, "setuid.tar.gz");
|
|
345
|
+
writeFileSync(archivePath, buildTarGz(entries));
|
|
346
|
+
assert.throws(() => verifyProviderContractBundleArchive(archivePath), /unsafe file mode/);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("rejects trailing data after the tar terminator", () => {
|
|
351
|
+
withTemporaryDirectory((directory) => {
|
|
352
|
+
const archivePath = join(directory, "trailing.tar.gz");
|
|
353
|
+
writeFileSync(archivePath, buildTarGz([...fixtureEntries()].map(([name, bytes]) => ({ name, bytes })), Buffer.from("smuggled")));
|
|
354
|
+
assert.throws(() => verifyProviderContractBundleArchive(archivePath), /data after its terminator/);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("rejects a duplicated tar entry path", () => {
|
|
359
|
+
withTemporaryDirectory((directory) => {
|
|
360
|
+
const entries = [...fixtureEntries()].map(([name, bytes]) => ({ name, bytes }));
|
|
361
|
+
entries.push({ name: "README.md", bytes: Buffer.from("second\n") });
|
|
362
|
+
const archivePath = join(directory, "duplicate.tar.gz");
|
|
363
|
+
writeFileSync(archivePath, buildTarGz(entries));
|
|
364
|
+
assert.throws(() => verifyProviderContractBundleArchive(archivePath), /duplicated|more than 8/);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// --- extracted tree safety --------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
test("rejects a bundle tree containing a symlink", { skip: process.platform === "win32" }, () => {
|
|
371
|
+
withTemporaryDirectory((directory) => {
|
|
372
|
+
materialize(fixtureEntries(), directory);
|
|
373
|
+
rmSync(join(directory, "README.md"));
|
|
374
|
+
symlinkSync(join(directory, "manifest.json"), join(directory, "README.md"));
|
|
375
|
+
assert.throws(() => verifyProviderContractBundleTree(directory), /is a symlink/);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("rejects a bundle tree entry with an executable mode", { skip: process.platform === "win32" }, () => {
|
|
380
|
+
withTemporaryDirectory((directory) => {
|
|
381
|
+
materialize(fixtureEntries(), directory);
|
|
382
|
+
chmodSync(join(directory, "schemas/lens.schema.json"), 0o755);
|
|
383
|
+
assert.throws(() => verifyProviderContractBundleTree(directory), /unsafe file mode/);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, chmodSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { gzipSync } from "node:zlib";
|
|
7
|
+
import { checkProviderContractMirror } from "../scripts/check-provider-contract.mjs";
|
|
8
|
+
import {
|
|
9
|
+
PROVIDER_CONTRACT_ACQUISITION,
|
|
10
|
+
PROVIDER_CONTRACT_LOCK_FILE,
|
|
11
|
+
PROVIDER_CONTRACT_LOCK_SCHEMA,
|
|
12
|
+
PROVIDER_CONTRACT_MIRROR_ROOT,
|
|
13
|
+
mirrorProviderContractBundle,
|
|
14
|
+
} from "../scripts/mirror-provider-contract.mjs";
|
|
15
|
+
|
|
16
|
+
// Fixture provenance: generated from gentle-ai main (contract 1.1.0) with
|
|
17
|
+
// `go run ./internal/providercontractbundlecmd generate --out <dir>`.
|
|
18
|
+
const FIXTURE_ROOT = join(import.meta.dirname, "fixtures", "provider-contract-bundle", "v1.1.0");
|
|
19
|
+
const FIXTURE_PATHS = [
|
|
20
|
+
"README.md",
|
|
21
|
+
"manifest.json",
|
|
22
|
+
"schemas/lens.schema.json",
|
|
23
|
+
"schemas/refuter.schema.json",
|
|
24
|
+
"schemas/targeted-validator.schema.json",
|
|
25
|
+
"vectors/lens.json",
|
|
26
|
+
"vectors/refuter.json",
|
|
27
|
+
"vectors/targeted-validator.json",
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
interface LockRecord {
|
|
31
|
+
schema: string;
|
|
32
|
+
acquisition: string;
|
|
33
|
+
contract_semver: string;
|
|
34
|
+
source: { kind: string; archive_sha256?: string };
|
|
35
|
+
tree_sha256: string;
|
|
36
|
+
entries: Record<string, string>;
|
|
37
|
+
generated: Record<string, string>;
|
|
38
|
+
runtimes: string[];
|
|
39
|
+
pi_registered: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function withTemporaryRoot<T>(run: (packageRoot: string) => T): T {
|
|
43
|
+
const packageRoot = mkdtempSync(join(tmpdir(), "gentle-pi-provider-mirror-"));
|
|
44
|
+
try {
|
|
45
|
+
return run(packageRoot);
|
|
46
|
+
} finally {
|
|
47
|
+
rmSync(packageRoot, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mirrorFixtureInto(packageRoot: string): LockRecord {
|
|
52
|
+
mirrorProviderContractBundle(FIXTURE_ROOT, packageRoot);
|
|
53
|
+
const lockPath = join(packageRoot, PROVIDER_CONTRACT_MIRROR_ROOT, PROVIDER_CONTRACT_LOCK_FILE);
|
|
54
|
+
return JSON.parse(readFileSync(lockPath, "utf8")) as LockRecord;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function tarHeader(name: string, bytes: Buffer): Buffer {
|
|
58
|
+
const header = Buffer.alloc(512);
|
|
59
|
+
header.write(name, 0, "ascii");
|
|
60
|
+
header.write("0000644\0", 100, "ascii");
|
|
61
|
+
header.write("0000000\0", 108, "ascii");
|
|
62
|
+
header.write("0000000\0", 116, "ascii");
|
|
63
|
+
header.write(`${bytes.length.toString(8).padStart(11, "0")}\0`, 124, "ascii");
|
|
64
|
+
header.write("00000000000\0", 136, "ascii");
|
|
65
|
+
header.write(" ", 148, "ascii");
|
|
66
|
+
header.write("0", 156, "ascii");
|
|
67
|
+
header.write("ustar\0", 257, "ascii");
|
|
68
|
+
header.write("00", 263, "ascii");
|
|
69
|
+
let checksum = 0;
|
|
70
|
+
for (const byte of header) checksum += byte;
|
|
71
|
+
header.write(`${checksum.toString(8).padStart(6, "0")}\0 `, 148, "ascii");
|
|
72
|
+
return header;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function fixtureArchiveBytes(): Buffer {
|
|
76
|
+
const blocks: Buffer[] = [];
|
|
77
|
+
for (const path of FIXTURE_PATHS) {
|
|
78
|
+
const bytes = readFileSync(join(FIXTURE_ROOT, path));
|
|
79
|
+
blocks.push(tarHeader(path, bytes), bytes, Buffer.alloc((512 - (bytes.length % 512)) % 512));
|
|
80
|
+
}
|
|
81
|
+
blocks.push(Buffer.alloc(1024));
|
|
82
|
+
return gzipSync(Buffer.concat(blocks));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
test("mirrors a verified local bundle tree and the offline drift check passes", () => {
|
|
86
|
+
withTemporaryRoot((packageRoot) => {
|
|
87
|
+
const lock = mirrorFixtureInto(packageRoot);
|
|
88
|
+
assert.equal(lock.schema, PROVIDER_CONTRACT_LOCK_SCHEMA);
|
|
89
|
+
assert.equal(lock.acquisition, PROVIDER_CONTRACT_ACQUISITION);
|
|
90
|
+
assert.equal(lock.contract_semver, "1.1.0");
|
|
91
|
+
assert.deepEqual(lock.source, { kind: "tree" });
|
|
92
|
+
assert.equal(lock.pi_registered, true);
|
|
93
|
+
assert.deepEqual(lock.runtimes, ["claude-code", "codex", "opencode", "pi"]);
|
|
94
|
+
assert.equal(Object.keys(lock.entries).length, 8);
|
|
95
|
+
assert.deepEqual(Object.keys(lock.generated).sort(), [
|
|
96
|
+
"generated/provider-capabilities.baseline.json",
|
|
97
|
+
"generated/provider-roles.baseline.json",
|
|
98
|
+
]);
|
|
99
|
+
for (const path of FIXTURE_PATHS) {
|
|
100
|
+
const mirrored = join(packageRoot, PROVIDER_CONTRACT_MIRROR_ROOT, "v1.1.0", "bundle", path);
|
|
101
|
+
assert.ok(existsSync(mirrored), `${path} must be mirrored`);
|
|
102
|
+
assert.deepEqual(readFileSync(mirrored), readFileSync(join(FIXTURE_ROOT, path)), `${path} must be byte-identical`);
|
|
103
|
+
}
|
|
104
|
+
assert.deepEqual(checkProviderContractMirror(packageRoot), []);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("mirrors a verified local archive and records the archive digest in the lock", () => {
|
|
109
|
+
withTemporaryRoot((packageRoot) => {
|
|
110
|
+
const archivePath = join(packageRoot, "gentle-ai-review-provider-contract-1.1.0.tar.gz");
|
|
111
|
+
writeFileSync(archivePath, fixtureArchiveBytes());
|
|
112
|
+
mirrorProviderContractBundle(archivePath, packageRoot);
|
|
113
|
+
const lock = JSON.parse(
|
|
114
|
+
readFileSync(join(packageRoot, PROVIDER_CONTRACT_MIRROR_ROOT, PROVIDER_CONTRACT_LOCK_FILE), "utf8"),
|
|
115
|
+
) as LockRecord;
|
|
116
|
+
assert.equal(lock.source.kind, "archive");
|
|
117
|
+
assert.match(lock.source.archive_sha256 ?? "", /^[0-9a-f]{64}$/);
|
|
118
|
+
assert.deepEqual(checkProviderContractMirror(packageRoot), []);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("refuses to mirror a 1.0.0 bundle because the pi runtime identity is not registered", () => {
|
|
123
|
+
withTemporaryRoot((packageRoot) => {
|
|
124
|
+
const bundleRoot = join(packageRoot, "bundle-1.0.0");
|
|
125
|
+
for (const path of FIXTURE_PATHS) {
|
|
126
|
+
const destination = join(bundleRoot, ...path.split("/"));
|
|
127
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
128
|
+
writeFileSync(destination, readFileSync(join(FIXTURE_ROOT, path)));
|
|
129
|
+
chmodSync(destination, 0o644);
|
|
130
|
+
}
|
|
131
|
+
const manifest = JSON.parse(readFileSync(join(bundleRoot, "manifest.json"), "utf8")) as Record<string, unknown>;
|
|
132
|
+
manifest.contract_semver = "1.0.0";
|
|
133
|
+
delete manifest.runtimes;
|
|
134
|
+
writeFileSync(join(bundleRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
135
|
+
|
|
136
|
+
assert.throws(
|
|
137
|
+
() => mirrorProviderContractBundle(bundleRoot, packageRoot),
|
|
138
|
+
/refusing to mirror provider contract 1\.0\.0.*not registered/s,
|
|
139
|
+
);
|
|
140
|
+
assert.ok(!existsSync(join(packageRoot, PROVIDER_CONTRACT_MIRROR_ROOT)), "no partial mirror may be written");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("drift check fails when a mirrored schema byte changes", () => {
|
|
145
|
+
withTemporaryRoot((packageRoot) => {
|
|
146
|
+
mirrorFixtureInto(packageRoot);
|
|
147
|
+
const schemaPath = join(packageRoot, PROVIDER_CONTRACT_MIRROR_ROOT, "v1.1.0", "bundle", "schemas", "lens.schema.json");
|
|
148
|
+
writeFileSync(schemaPath, `${readFileSync(schemaPath).toString("utf8")} `);
|
|
149
|
+
const problems = checkProviderContractMirror(packageRoot);
|
|
150
|
+
assert.ok(problems.length > 0);
|
|
151
|
+
assert.match(problems.join("\n"), /failed verification/);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("drift check fails when a generated baseline is edited by hand", () => {
|
|
156
|
+
withTemporaryRoot((packageRoot) => {
|
|
157
|
+
mirrorFixtureInto(packageRoot);
|
|
158
|
+
const baselinePath = join(
|
|
159
|
+
packageRoot,
|
|
160
|
+
PROVIDER_CONTRACT_MIRROR_ROOT,
|
|
161
|
+
"v1.1.0",
|
|
162
|
+
"generated",
|
|
163
|
+
"provider-capabilities.baseline.json",
|
|
164
|
+
);
|
|
165
|
+
const baseline = JSON.parse(readFileSync(baselinePath, "utf8")) as Record<string, unknown>;
|
|
166
|
+
baseline.pi_registered = false;
|
|
167
|
+
writeFileSync(baselinePath, `${JSON.stringify(baseline, null, 2)}\n`);
|
|
168
|
+
const problems = checkProviderContractMirror(packageRoot);
|
|
169
|
+
assert.match(problems.join("\n"), /provider-capabilities\.baseline\.json/);
|
|
170
|
+
assert.match(problems.join("\n"), /not byte-identical|drifted/);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("drift check fails when the lock record disagrees with the mirror", () => {
|
|
175
|
+
withTemporaryRoot((packageRoot) => {
|
|
176
|
+
const lock = mirrorFixtureInto(packageRoot);
|
|
177
|
+
lock.tree_sha256 = "0".repeat(64);
|
|
178
|
+
writeFileSync(
|
|
179
|
+
join(packageRoot, PROVIDER_CONTRACT_MIRROR_ROOT, PROVIDER_CONTRACT_LOCK_FILE),
|
|
180
|
+
`${JSON.stringify(lock, null, 2)}\n`,
|
|
181
|
+
);
|
|
182
|
+
const problems = checkProviderContractMirror(packageRoot);
|
|
183
|
+
assert.match(problems.join("\n"), /tree digest .* does not match lock tree_sha256/);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("drift check fails on an unexpected file inside the mirror directory", () => {
|
|
188
|
+
withTemporaryRoot((packageRoot) => {
|
|
189
|
+
mirrorFixtureInto(packageRoot);
|
|
190
|
+
writeFileSync(join(packageRoot, PROVIDER_CONTRACT_MIRROR_ROOT, "NOTES.md"), "overlay\n");
|
|
191
|
+
const problems = checkProviderContractMirror(packageRoot);
|
|
192
|
+
assert.match(problems.join("\n"), /unexpected entry "NOTES\.md"/);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("drift check fails when the lock is missing entirely", () => {
|
|
197
|
+
withTemporaryRoot((packageRoot) => {
|
|
198
|
+
const problems = checkProviderContractMirror(packageRoot);
|
|
199
|
+
assert.equal(problems.length, 1);
|
|
200
|
+
assert.match(problems[0] as string, /provider-contract\.lock\.json is missing/);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("the committed repository mirror matches its lock and generated baselines", () => {
|
|
205
|
+
assert.deepEqual(checkProviderContractMirror(join(import.meta.dirname, "..")), []);
|
|
206
|
+
});
|