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,520 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { createNodeExecFileAdapter } from "../lib/native-review-cli.ts";
|
|
7
|
+
import {
|
|
8
|
+
REVIEW_HOST_RELAY_FAILURE,
|
|
9
|
+
REVIEW_HOST_RELAY_PI_ARGV,
|
|
10
|
+
REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE,
|
|
11
|
+
REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE,
|
|
12
|
+
ReviewHostRelayError,
|
|
13
|
+
classifyReviewHostRelayRefusal,
|
|
14
|
+
resolveReviewHostRelaySubmission,
|
|
15
|
+
reviewHostRelaySlots,
|
|
16
|
+
runReviewHostRelaySlot,
|
|
17
|
+
} from "../lib/review-host-relay.ts";
|
|
18
|
+
import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "../lib/review-relay-contract.ts";
|
|
19
|
+
import { decodeReviewNextTransitionV3, type ReviewCaptureSubmissionV1, type ReviewCollectInputV3 } from "../lib/review-integration-v2.ts";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Fake binaries. Following the repo's fake-executable idiom (shell/git
|
|
23
|
+
// wrappers in review-gate/git-commit-transaction tests), these are
|
|
24
|
+
// shebang scripts written into a scratch directory; node scripts are used so
|
|
25
|
+
// binary-unsafe bytes survive verbatim.
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
const FAKE_GENTLE_AI = `#!/usr/bin/env node
|
|
29
|
+
const fs = require("node:fs");
|
|
30
|
+
const argv = process.argv.slice(2);
|
|
31
|
+
if (process.env.RELAY_FAKE_LOG) fs.appendFileSync(process.env.RELAY_FAKE_LOG, JSON.stringify({ argv, contract: process.env.${GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV} ?? null }) + "\\n");
|
|
32
|
+
if (argv.some((token) => token === "--materialize" || token.startsWith("--materialize="))) {
|
|
33
|
+
const mode = process.env.RELAY_FAKE_MATERIALIZE_MODE || "ok";
|
|
34
|
+
if (mode === "ok") { process.stdout.write(Buffer.from(process.env.RELAY_FAKE_PROMPT_B64 || "", "base64")); process.exit(0); }
|
|
35
|
+
if (mode === "empty") process.exit(0);
|
|
36
|
+
if (mode === "unknown-flag") { process.stderr.write("flag provided but not defined: -materialize\\nUsage of gentle-ai review capture-result:\\n"); process.exit(2); }
|
|
37
|
+
if (mode === "handshake") { process.stderr.write(process.env.RELAY_FAKE_HANDSHAKE_STDERR || "the active runtime is not eligible for immutable receipt review"); process.exit(1); }
|
|
38
|
+
process.stderr.write("materialize exploded\\n"); process.exit(3);
|
|
39
|
+
}
|
|
40
|
+
const inputToken = argv.find((token) => token === "--input" || token.startsWith("--input="));
|
|
41
|
+
if (inputToken !== undefined) {
|
|
42
|
+
const mode = process.env.RELAY_FAKE_SUBMIT_MODE || "ok";
|
|
43
|
+
if (mode === "ok") {
|
|
44
|
+
const inputPath = inputToken.startsWith("--input=") ? inputToken.slice("--input=".length) : argv[argv.indexOf("--input") + 1];
|
|
45
|
+
const bytes = fs.readFileSync(inputPath);
|
|
46
|
+
if (process.env.RELAY_FAKE_SUBMIT_CAPTURE) fs.writeFileSync(process.env.RELAY_FAKE_SUBMIT_CAPTURE, bytes);
|
|
47
|
+
process.stdout.write(JSON.stringify({ schema: "gentle-ai.review-result-artifact/v2", admission_decision: "completed" }));
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
process.stderr.write("capture binding does not match the current reviewing authority\\n");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
process.stderr.write("unexpected fake gentle-ai invocation\\n");
|
|
54
|
+
process.exit(9);
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
const FAKE_PI = `#!/usr/bin/env node
|
|
58
|
+
const fs = require("node:fs");
|
|
59
|
+
const argv = process.argv.slice(2);
|
|
60
|
+
if (process.env.RELAY_FAKE_PI_LOG) fs.appendFileSync(process.env.RELAY_FAKE_PI_LOG, JSON.stringify({ argv, cwd: process.cwd(), entries: fs.readdirSync(process.cwd()), contract: process.env.${GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV} ?? null }) + "\\n");
|
|
61
|
+
const chunks = [];
|
|
62
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
63
|
+
process.stdin.on("end", () => {
|
|
64
|
+
const stdin = Buffer.concat(chunks);
|
|
65
|
+
if (process.env.RELAY_FAKE_PI_STDIN_CAPTURE) fs.writeFileSync(process.env.RELAY_FAKE_PI_STDIN_CAPTURE, stdin);
|
|
66
|
+
const mode = process.env.RELAY_FAKE_PI_MODE || "ok";
|
|
67
|
+
if (mode === "ok") { process.stdout.write(Buffer.from(process.env.RELAY_FAKE_PI_OUTPUT_B64 || "", "base64")); process.exit(0); }
|
|
68
|
+
if (mode === "empty") process.exit(0);
|
|
69
|
+
if (mode === "hang") { setTimeout(() => process.exit(0), 10_000); return; }
|
|
70
|
+
process.stderr.write("pi exploded\\n");
|
|
71
|
+
process.exit(4);
|
|
72
|
+
});
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
interface RelayHarness {
|
|
76
|
+
directory: string;
|
|
77
|
+
gentleAi: string;
|
|
78
|
+
pi: string;
|
|
79
|
+
logPath: string;
|
|
80
|
+
piLogPath: string;
|
|
81
|
+
stdinCapturePath: string;
|
|
82
|
+
submitCapturePath: string;
|
|
83
|
+
environment: NodeJS.ProcessEnv;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function harness(t: test.TestContext, overrides: Record<string, string> = {}): RelayHarness {
|
|
87
|
+
const directory = mkdtempSync(join(tmpdir(), "gentle-pi-relay-harness-"));
|
|
88
|
+
t.after(() => rmSync(directory, { recursive: true, force: true }));
|
|
89
|
+
const gentleAi = join(directory, "gentle-ai");
|
|
90
|
+
const pi = join(directory, "pi");
|
|
91
|
+
writeFileSync(gentleAi, FAKE_GENTLE_AI);
|
|
92
|
+
chmodSync(gentleAi, 0o755);
|
|
93
|
+
writeFileSync(pi, FAKE_PI);
|
|
94
|
+
chmodSync(pi, 0o755);
|
|
95
|
+
const logPath = join(directory, "gentle-ai.log");
|
|
96
|
+
const piLogPath = join(directory, "pi.log");
|
|
97
|
+
const stdinCapturePath = join(directory, "pi-stdin.bin");
|
|
98
|
+
const submitCapturePath = join(directory, "submitted.bin");
|
|
99
|
+
const environment: NodeJS.ProcessEnv = {
|
|
100
|
+
...process.env,
|
|
101
|
+
RELAY_FAKE_LOG: logPath,
|
|
102
|
+
RELAY_FAKE_PI_LOG: piLogPath,
|
|
103
|
+
RELAY_FAKE_PI_STDIN_CAPTURE: stdinCapturePath,
|
|
104
|
+
RELAY_FAKE_SUBMIT_CAPTURE: submitCapturePath,
|
|
105
|
+
...overrides,
|
|
106
|
+
};
|
|
107
|
+
// The relay itself must add the handshake; the base environment never
|
|
108
|
+
// carries it, so the fake-binary log proves the injection.
|
|
109
|
+
delete environment[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV];
|
|
110
|
+
return { directory, gentleAi, pi, logPath, piLogPath, stdinCapturePath, submitCapturePath, environment };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readLog(path: string): Array<{ argv: string[]; contract: string | null; cwd?: string; entries?: string[] }> {
|
|
114
|
+
if (!existsSync(path)) return [];
|
|
115
|
+
return readFileSync(path, "utf8").split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const BINDING_TOKENS = Object.freeze([
|
|
119
|
+
"--lineage=review-1d5aadacc600e167",
|
|
120
|
+
`--expected-revision=sha256:${"c".repeat(64)}`,
|
|
121
|
+
`--target=sha256:${"d".repeat(64)}`,
|
|
122
|
+
`--repository-context=rctx1_${"e".repeat(64)}`,
|
|
123
|
+
"--lens=review-reliability",
|
|
124
|
+
"--order=0",
|
|
125
|
+
`--subject-hash=sha256:${"a".repeat(64)}`,
|
|
126
|
+
]);
|
|
127
|
+
const CAPTURE_TOKENS = Object.freeze([...BINDING_TOKENS, "--agent=pi", "--materialize=true"]);
|
|
128
|
+
|
|
129
|
+
// The provider-owned completing form: exact operation and argument tokens
|
|
130
|
+
// with one declared {{value}} substitution slot for the artifact path.
|
|
131
|
+
const SUBMISSION: ReviewCaptureSubmissionV1 = Object.freeze({
|
|
132
|
+
operationToken: "capture-result",
|
|
133
|
+
argumentTokens: Object.freeze([...BINDING_TOKENS, "--input={{value}}"]),
|
|
134
|
+
values: Object.freeze([{ slot: "reviewer_result", domain: "artifact_path_or_stdin", substitutionLocation: BINDING_TOKENS.length }]),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Prompt and result bytes deliberately include binary-unsafe content: NUL,
|
|
138
|
+
// control bytes, quotes, backslashes, CRLF, multi-byte UTF-8, and bytes that
|
|
139
|
+
// are not valid UTF-8 at all. The relay must move them verbatim.
|
|
140
|
+
const PROMPT_BYTES = Buffer.concat([
|
|
141
|
+
Buffer.from('GENTLE_AI_REVIEW_BINDING {"lineage":"review-1d5aadacc600e167"}\n"quotes" \\backslash\r\n\u00e9\u{1F3A9}\n', "utf8"),
|
|
142
|
+
Buffer.from([0x00, 0x01, 0x07, 0xff, 0xfe, 0x00]),
|
|
143
|
+
]);
|
|
144
|
+
const PI_OUTPUT_BYTES = Buffer.concat([
|
|
145
|
+
Buffer.from(`{"subject_hash":"sha256:${"a".repeat(64)}","findings":[]}\n`, "utf8"),
|
|
146
|
+
Buffer.from([0x00, 0xf0, 0x9f, 0x8e, 0xa9, 0xff, 0x0d, 0x0a]),
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
function relayRequest(fixture: RelayHarness, overrides: Record<string, unknown> = {}) {
|
|
150
|
+
return {
|
|
151
|
+
captureArgumentTokens: CAPTURE_TOKENS,
|
|
152
|
+
submission: SUBMISSION,
|
|
153
|
+
gentleAiExecutable: fixture.gentleAi,
|
|
154
|
+
piExecutable: fixture.pi,
|
|
155
|
+
environment: {
|
|
156
|
+
...fixture.environment,
|
|
157
|
+
RELAY_FAKE_PROMPT_B64: PROMPT_BYTES.toString("base64"),
|
|
158
|
+
RELAY_FAKE_PI_OUTPUT_B64: PI_OUTPUT_BYTES.toString("base64"),
|
|
159
|
+
},
|
|
160
|
+
gentleAiTimeoutMs: 30_000,
|
|
161
|
+
piTimeoutMs: 30_000,
|
|
162
|
+
...overrides,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function rejectsWithRelayError(promise: Promise<unknown>, kind: string, stage: string): Promise<ReviewHostRelayError> {
|
|
167
|
+
let caught: ReviewHostRelayError | undefined;
|
|
168
|
+
await assert.rejects(promise, (error: unknown) => {
|
|
169
|
+
assert.ok(error instanceof ReviewHostRelayError, `expected ReviewHostRelayError, received ${String(error)}`);
|
|
170
|
+
caught = error;
|
|
171
|
+
return error.name === "ReviewHostRelayError" && error.kind === kind && error.stage === stage;
|
|
172
|
+
});
|
|
173
|
+
return caught!;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Handshake — every gentle-ai CLI spawn carries the compiled declaration.
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
test("the central native CLI runner declares the relay contract on every gentle-ai spawn", async (t) => {
|
|
181
|
+
const fixture = harness(t);
|
|
182
|
+
const probe = join(fixture.directory, "env-probe");
|
|
183
|
+
writeFileSync(probe, `#!/usr/bin/env node\nprocess.stdout.write(JSON.stringify({ contract: process.env.${GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV} ?? null }));\n`);
|
|
184
|
+
chmodSync(probe, 0o755);
|
|
185
|
+
const hadContract = Object.prototype.hasOwnProperty.call(process.env, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV);
|
|
186
|
+
const previous = process.env[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV];
|
|
187
|
+
delete process.env[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV];
|
|
188
|
+
t.after(() => {
|
|
189
|
+
if (hadContract) process.env[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV] = previous;
|
|
190
|
+
});
|
|
191
|
+
const adapter = createNodeExecFileAdapter();
|
|
192
|
+
for (const argv of [["version"], ["review", "status", "--cwd", fixture.directory]]) {
|
|
193
|
+
const result = await adapter({ file: probe, arguments: argv, cwd: fixture.directory, timeoutMs: 10_000, maxBufferBytes: 1024 * 1024 });
|
|
194
|
+
assert.equal(result.exitCode, 0);
|
|
195
|
+
assert.deepEqual(JSON.parse(result.stdout), { contract: GENTLE_PI_REVIEW_RELAY_CONTRACT });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("relay contract constants are the compiled gentle-ai handshake values", () => {
|
|
200
|
+
assert.equal(GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV, "GENTLE_PI_REVIEW_RELAY_CONTRACT");
|
|
201
|
+
assert.equal(GENTLE_PI_REVIEW_RELAY_CONTRACT, "gentle-pi.review-relay/v1");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Happy path
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
test("relay happy path moves prompt and result bytes verbatim through a fresh empty scratch pi subprocess", async (t) => {
|
|
209
|
+
const fixture = harness(t);
|
|
210
|
+
const result = await runReviewHostRelaySlot(relayRequest(fixture));
|
|
211
|
+
|
|
212
|
+
assert.equal(result.promptByteLength, PROMPT_BYTES.length);
|
|
213
|
+
assert.equal(result.resultByteLength, PI_OUTPUT_BYTES.length);
|
|
214
|
+
assert.equal(JSON.parse(result.submission).admission_decision, "completed");
|
|
215
|
+
|
|
216
|
+
// Prompt bytes reached pi stdin verbatim.
|
|
217
|
+
assert.deepEqual(readFileSync(fixture.stdinCapturePath), PROMPT_BYTES);
|
|
218
|
+
// Submission --input file bytes are EXACTLY the pi stdout bytes.
|
|
219
|
+
assert.deepEqual(readFileSync(fixture.submitCapturePath), PI_OUTPUT_BYTES);
|
|
220
|
+
|
|
221
|
+
const gentleAiCalls = readLog(fixture.logPath);
|
|
222
|
+
assert.equal(gentleAiCalls.length, 2);
|
|
223
|
+
// (a) exact provider tokens, verbatim, in provider order.
|
|
224
|
+
assert.deepEqual(gentleAiCalls[0]!.argv, ["review", "capture-result", ...CAPTURE_TOKENS]);
|
|
225
|
+
// (d) the provider-owned submission form, verbatim: its exact operation
|
|
226
|
+
// and argument tokens with only the artifact path substituted into the
|
|
227
|
+
// declared {{value}} slot. No agent/materialize, nothing synthesized.
|
|
228
|
+
assert.deepEqual(gentleAiCalls[1]!.argv.slice(0, 2 + BINDING_TOKENS.length), ["review", SUBMISSION.operationToken, ...BINDING_TOKENS]);
|
|
229
|
+
const substituted = gentleAiCalls[1]!.argv.at(-1)!;
|
|
230
|
+
assert.match(substituted, /^--input=\S+$/);
|
|
231
|
+
assert.equal(substituted.includes("{{value}}"), false);
|
|
232
|
+
assert.equal(gentleAiCalls[1]!.argv.length, 2 + SUBMISSION.argumentTokens.length);
|
|
233
|
+
assert.equal(gentleAiCalls[1]!.argv.some((token) => token.includes("--agent") || token.includes("--materialize")), false);
|
|
234
|
+
// Handshake declared on both gentle-ai invocations even though the base
|
|
235
|
+
// environment carried none.
|
|
236
|
+
assert.deepEqual(gentleAiCalls.map((call) => call.contract), [GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT]);
|
|
237
|
+
|
|
238
|
+
const piCalls = readLog(fixture.piLogPath);
|
|
239
|
+
assert.equal(piCalls.length, 1);
|
|
240
|
+
// The pi environment stays untouched: no relay handshake is injected.
|
|
241
|
+
assert.equal(piCalls[0]!.contract, null);
|
|
242
|
+
// Fresh EMPTY scratch cwd, removed after the run.
|
|
243
|
+
assert.deepEqual(piCalls[0]!.entries, []);
|
|
244
|
+
assert.notEqual(piCalls[0]!.cwd, process.cwd());
|
|
245
|
+
assert.equal(existsSync(piCalls[0]!.cwd!), false);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("the pi lockdown argv is pinned exactly with no model or provider selection", async (t) => {
|
|
249
|
+
const fixture = harness(t);
|
|
250
|
+
await runReviewHostRelaySlot(relayRequest(fixture));
|
|
251
|
+
const piCalls = readLog(fixture.piLogPath);
|
|
252
|
+
const expected = [
|
|
253
|
+
"--print",
|
|
254
|
+
"--mode", "text",
|
|
255
|
+
"--no-session",
|
|
256
|
+
"--no-tools",
|
|
257
|
+
"--no-extensions",
|
|
258
|
+
"--no-skills",
|
|
259
|
+
"--no-prompt-templates",
|
|
260
|
+
"--no-themes",
|
|
261
|
+
"--no-context-files",
|
|
262
|
+
"--no-approve",
|
|
263
|
+
];
|
|
264
|
+
assert.deepEqual([...REVIEW_HOST_RELAY_PI_ARGV], expected);
|
|
265
|
+
assert.deepEqual(piCalls[0]!.argv, expected);
|
|
266
|
+
assert.equal(piCalls[0]!.argv.some((token) => token.startsWith("--model") || token.startsWith("--provider") || token.startsWith("--profile")), false);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Fail-closed legs — a typed transport error and NO submission.
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
test("materialize nonzero exit fails closed with a typed error and never launches pi or submits", async (t) => {
|
|
274
|
+
const fixture = harness(t, { RELAY_FAKE_MATERIALIZE_MODE: "fail" });
|
|
275
|
+
const error = await rejectsWithRelayError(runReviewHostRelaySlot(relayRequest(fixture)), REVIEW_HOST_RELAY_FAILURE.MATERIALIZE_FAILED, "materialize");
|
|
276
|
+
assert.equal(error.exitCode, 3);
|
|
277
|
+
assert.equal(error.mutationOutcome, "none");
|
|
278
|
+
assert.equal(readLog(fixture.logPath).length, 1);
|
|
279
|
+
assert.equal(readLog(fixture.piLogPath).length, 0);
|
|
280
|
+
assert.equal(existsSync(fixture.submitCapturePath), false);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("an empty materialized prompt fails closed before pi launches", async (t) => {
|
|
284
|
+
const fixture = harness(t, { RELAY_FAKE_MATERIALIZE_MODE: "empty" });
|
|
285
|
+
await rejectsWithRelayError(runReviewHostRelaySlot(relayRequest(fixture)), REVIEW_HOST_RELAY_FAILURE.EMPTY_PROMPT, "materialize");
|
|
286
|
+
assert.equal(readLog(fixture.piLogPath).length, 0);
|
|
287
|
+
assert.equal(existsSync(fixture.submitCapturePath), false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("pi nonzero exit fails closed with a typed error and no submission", async (t) => {
|
|
291
|
+
const fixture = harness(t, { RELAY_FAKE_PI_MODE: "fail" });
|
|
292
|
+
const error = await rejectsWithRelayError(runReviewHostRelaySlot(relayRequest(fixture)), REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi");
|
|
293
|
+
assert.equal(error.exitCode, 4);
|
|
294
|
+
assert.equal(error.mutationOutcome, "none");
|
|
295
|
+
assert.equal(readLog(fixture.logPath).length, 1, "no submission invocation after pi failure");
|
|
296
|
+
assert.equal(existsSync(fixture.submitCapturePath), false);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("empty pi stdout fails closed with a typed error and no submission", async (t) => {
|
|
300
|
+
const fixture = harness(t, { RELAY_FAKE_PI_MODE: "empty" });
|
|
301
|
+
await rejectsWithRelayError(runReviewHostRelaySlot(relayRequest(fixture)), REVIEW_HOST_RELAY_FAILURE.PI_EMPTY_OUTPUT, "pi");
|
|
302
|
+
assert.equal(readLog(fixture.logPath).length, 1);
|
|
303
|
+
assert.equal(existsSync(fixture.submitCapturePath), false);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("pi timeout fails closed with a typed error and no submission", async (t) => {
|
|
307
|
+
const fixture = harness(t, { RELAY_FAKE_PI_MODE: "hang" });
|
|
308
|
+
const error = await rejectsWithRelayError(runReviewHostRelaySlot(relayRequest(fixture, { piTimeoutMs: 300 })), REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi");
|
|
309
|
+
assert.equal(error.timedOut, true);
|
|
310
|
+
assert.equal(readLog(fixture.logPath).length, 1);
|
|
311
|
+
assert.equal(existsSync(fixture.submitCapturePath), false);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("submission refusal is a typed error whose outcome is unknown pending STATUS", async (t) => {
|
|
315
|
+
const fixture = harness(t, { RELAY_FAKE_SUBMIT_MODE: "refuse" });
|
|
316
|
+
const error = await rejectsWithRelayError(runReviewHostRelaySlot(relayRequest(fixture)), REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit");
|
|
317
|
+
assert.equal(error.mutationOutcome, "unknown");
|
|
318
|
+
assert.match(error.stderr, /capture binding does not match/);
|
|
319
|
+
assert.equal(readLog(fixture.logPath).length, 2);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("relay scratch and staging directories are removed after failures too", async (t) => {
|
|
323
|
+
const fixture = harness(t, { RELAY_FAKE_PI_MODE: "fail" });
|
|
324
|
+
await rejectsWithRelayError(runReviewHostRelaySlot(relayRequest(fixture)), REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi");
|
|
325
|
+
const piCalls = readLog(fixture.piLogPath);
|
|
326
|
+
assert.equal(piCalls.length, 1);
|
|
327
|
+
assert.equal(existsSync(piCalls[0]!.cwd!), false);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// Capability detection — typed refusal classes, no version sniffing.
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
test("an old binary's unknown-flag refusal classifies as relay-unavailable with the exact report", async (t) => {
|
|
335
|
+
const fixture = harness(t, { RELAY_FAKE_MATERIALIZE_MODE: "unknown-flag" });
|
|
336
|
+
const error = await rejectsWithRelayError(runReviewHostRelaySlot(relayRequest(fixture)), REVIEW_HOST_RELAY_FAILURE.RELAY_UNAVAILABLE, "materialize");
|
|
337
|
+
assert.equal(error.message, REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE);
|
|
338
|
+
assert.match(error.stderr, /flag provided but not defined: -materialize/);
|
|
339
|
+
assert.equal(readLog(fixture.piLogPath).length, 0);
|
|
340
|
+
assert.equal(existsSync(fixture.submitCapturePath), false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("a handshake refusal surfaces the provider refusal verbatim", async (t) => {
|
|
344
|
+
const refusal = "review capture-result --agent pi: the active runtime is not eligible for immutable receipt review; supported immutable review runtimes: claude-code, codex, opencode";
|
|
345
|
+
const fixture = harness(t, { RELAY_FAKE_MATERIALIZE_MODE: "handshake", RELAY_FAKE_HANDSHAKE_STDERR: refusal });
|
|
346
|
+
const error = await rejectsWithRelayError(runReviewHostRelaySlot(relayRequest(fixture)), REVIEW_HOST_RELAY_FAILURE.HANDSHAKE_REFUSED, "materialize");
|
|
347
|
+
assert.equal(error.message, refusal);
|
|
348
|
+
assert.equal(error.stderr, refusal);
|
|
349
|
+
assert.equal(readLog(fixture.piLogPath).length, 0);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("refusal classification distinguishes unknown-flag, handshake, and other", () => {
|
|
353
|
+
assert.equal(classifyReviewHostRelayRefusal("flag provided but not defined: -materialize\nUsage:"), "unknown-flag");
|
|
354
|
+
assert.equal(classifyReviewHostRelayRefusal("flag provided but not defined: -agent"), "unknown-flag");
|
|
355
|
+
assert.equal(classifyReviewHostRelayRefusal("the active runtime is not eligible for immutable receipt review"), "handshake");
|
|
356
|
+
assert.equal(classifyReviewHostRelayRefusal("declare GENTLE_PI_REVIEW_RELAY_CONTRACT=gentle-pi.review-relay/v1"), "handshake");
|
|
357
|
+
assert.equal(classifyReviewHostRelayRefusal("some unrelated explosion"), "other");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// Slot detection — the provider decides; nothing is inferred.
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
function collectInput(overrides: Partial<{ captureOperation: string; arguments: ReviewCollectInputV3["arguments"]; submission: ReviewCaptureSubmissionV1 | undefined }> = {}): ReviewCollectInputV3 {
|
|
365
|
+
const argumentsList: ReviewCollectInputV3["arguments"] = overrides.arguments ?? [
|
|
366
|
+
{ name: "lineage", value: "review-1d5aadacc600e167", token: "--lineage=review-1d5aadacc600e167" },
|
|
367
|
+
{ name: "expected-revision", value: `sha256:${"c".repeat(64)}`, token: `--expected-revision=sha256:${"c".repeat(64)}` },
|
|
368
|
+
{ name: "target", value: `sha256:${"d".repeat(64)}`, token: `--target=sha256:${"d".repeat(64)}` },
|
|
369
|
+
{ name: "repository-context", value: `rctx1_${"e".repeat(64)}`, token: `--repository-context=rctx1_${"e".repeat(64)}` },
|
|
370
|
+
{ name: "lens", value: "review-reliability", token: "--lens=review-reliability" },
|
|
371
|
+
{ name: "order", value: "0", token: "--order=0" },
|
|
372
|
+
{ name: "subject-hash", value: `sha256:${"a".repeat(64)}`, token: `--subject-hash=sha256:${"a".repeat(64)}` },
|
|
373
|
+
{ name: "agent", value: "pi", token: "--agent=pi" },
|
|
374
|
+
{ name: "materialize", value: "true", token: "--materialize=true" },
|
|
375
|
+
];
|
|
376
|
+
return {
|
|
377
|
+
name: "reviewer_result",
|
|
378
|
+
schema: "https://gentle-ai.dev/schema/review/reviewer/v1",
|
|
379
|
+
captureOperation: overrides.captureOperation ?? "review.capture-result",
|
|
380
|
+
arguments: argumentsList,
|
|
381
|
+
...("submission" in overrides ? (overrides.submission === undefined ? {} : { submission: overrides.submission }) : { submission: SUBMISSION }),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
test("only provider-issued pi --materialize capture-result inputs become relay slots", () => {
|
|
386
|
+
const slots = reviewHostRelaySlots([collectInput()]);
|
|
387
|
+
assert.equal(slots.length, 1);
|
|
388
|
+
assert.deepEqual(slots[0]!.captureArgumentTokens, [...CAPTURE_TOKENS]);
|
|
389
|
+
// The provider-owned completing form passes through verbatim.
|
|
390
|
+
assert.deepEqual(slots[0]!.submission, SUBMISSION);
|
|
391
|
+
assert.equal(slots[0]!.lens, "review-reliability");
|
|
392
|
+
assert.equal(slots[0]!.order, "0");
|
|
393
|
+
|
|
394
|
+
// A materialize slot whose provider omitted the submission still becomes
|
|
395
|
+
// a slot (the provider decided the route); the relay then fails closed.
|
|
396
|
+
const missingSubmission = reviewHostRelaySlots([collectInput({ submission: undefined })]);
|
|
397
|
+
assert.equal(missingSubmission.length, 1);
|
|
398
|
+
assert.equal(missingSubmission[0]!.submission, undefined);
|
|
399
|
+
|
|
400
|
+
const withoutMaterialize = collectInput({ arguments: collectInput().arguments.filter((argument) => argument.name !== "materialize") });
|
|
401
|
+
assert.deepEqual(reviewHostRelaySlots([withoutMaterialize]), []);
|
|
402
|
+
|
|
403
|
+
const withoutAgent = collectInput({ arguments: collectInput().arguments.filter((argument) => argument.name !== "agent") });
|
|
404
|
+
assert.deepEqual(reviewHostRelaySlots([withoutAgent]), []);
|
|
405
|
+
|
|
406
|
+
const foreignAgent = collectInput({ arguments: collectInput().arguments.map((argument) => argument.name === "agent" ? { ...argument, value: "codex", token: "--agent=codex" } : argument) });
|
|
407
|
+
assert.deepEqual(reviewHostRelaySlots([foreignAgent]), []);
|
|
408
|
+
|
|
409
|
+
const evidence = collectInput({ captureOperation: "review.capture-evidence" });
|
|
410
|
+
assert.deepEqual(reviewHostRelaySlots([evidence]), []);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("relay input validation rejects empty or malformed inputs before any process launches", async () => {
|
|
414
|
+
await assert.rejects(runReviewHostRelaySlot({ captureArgumentTokens: [], submission: SUBMISSION }), TypeError);
|
|
415
|
+
await assert.rejects(runReviewHostRelaySlot({ captureArgumentTokens: [""], submission: SUBMISSION }), TypeError);
|
|
416
|
+
await assert.rejects(runReviewHostRelaySlot({ captureArgumentTokens: CAPTURE_TOKENS, submission: SUBMISSION, gentleAiExecutable: "gentle-ai" }), TypeError);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
// Provider-owned submission form — consumed verbatim, never synthesized.
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
test("a materialize slot without a provider submission fails closed before any process launches", async (t) => {
|
|
424
|
+
const fixture = harness(t);
|
|
425
|
+
const error = await rejectsWithRelayError(
|
|
426
|
+
runReviewHostRelaySlot(relayRequest(fixture, { submission: undefined })),
|
|
427
|
+
REVIEW_HOST_RELAY_FAILURE.SUBMISSION_CONTRACT_MISMATCH,
|
|
428
|
+
"binding",
|
|
429
|
+
);
|
|
430
|
+
assert.equal(error.message, REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE);
|
|
431
|
+
assert.equal(error.mutationOutcome, "none");
|
|
432
|
+
assert.equal(readLog(fixture.logPath).length, 0, "no gentle-ai invocation was launched");
|
|
433
|
+
assert.equal(readLog(fixture.piLogPath).length, 0, "no pi subprocess was launched");
|
|
434
|
+
assert.equal(existsSync(fixture.submitCapturePath), false);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("a submission form the relay cannot bind is a typed contract mismatch, never a repaired invocation", async (t) => {
|
|
438
|
+
const fixture = harness(t);
|
|
439
|
+
const twoValues: ReviewCaptureSubmissionV1 = {
|
|
440
|
+
...SUBMISSION,
|
|
441
|
+
values: [...SUBMISSION.values, { slot: "extra", domain: "artifact_path_or_stdin", substitutionLocation: 0 }],
|
|
442
|
+
};
|
|
443
|
+
await rejectsWithRelayError(runReviewHostRelaySlot(relayRequest(fixture, { submission: twoValues })), REVIEW_HOST_RELAY_FAILURE.SUBMISSION_CONTRACT_MISMATCH, "binding");
|
|
444
|
+
const noSlot: ReviewCaptureSubmissionV1 = {
|
|
445
|
+
...SUBMISSION,
|
|
446
|
+
argumentTokens: [...BINDING_TOKENS, "--input=/etc/somewhere"],
|
|
447
|
+
};
|
|
448
|
+
await rejectsWithRelayError(runReviewHostRelaySlot(relayRequest(fixture, { submission: noSlot })), REVIEW_HOST_RELAY_FAILURE.SUBMISSION_CONTRACT_MISMATCH, "binding");
|
|
449
|
+
const outOfBounds: ReviewCaptureSubmissionV1 = {
|
|
450
|
+
...SUBMISSION,
|
|
451
|
+
values: [{ slot: "reviewer_result", domain: "artifact_path_or_stdin", substitutionLocation: SUBMISSION.argumentTokens.length }],
|
|
452
|
+
};
|
|
453
|
+
await rejectsWithRelayError(runReviewHostRelaySlot(relayRequest(fixture, { submission: outOfBounds })), REVIEW_HOST_RELAY_FAILURE.SUBMISSION_CONTRACT_MISMATCH, "binding");
|
|
454
|
+
assert.equal(readLog(fixture.logPath).length, 0);
|
|
455
|
+
assert.equal(readLog(fixture.piLogPath).length, 0);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("resolveReviewHostRelaySubmission returns the provider binding untouched", () => {
|
|
459
|
+
const binding = resolveReviewHostRelaySubmission(SUBMISSION);
|
|
460
|
+
assert.equal(binding.operationToken, "capture-result");
|
|
461
|
+
assert.deepEqual(binding.argumentTokens, SUBMISSION.argumentTokens);
|
|
462
|
+
assert.equal(binding.substitutionLocation, BINDING_TOKENS.length);
|
|
463
|
+
assert.throws(() => resolveReviewHostRelaySubmission(undefined), (error: unknown) =>
|
|
464
|
+
error instanceof ReviewHostRelayError && error.kind === REVIEW_HOST_RELAY_FAILURE.SUBMISSION_CONTRACT_MISMATCH && error.message === REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("the negotiated decoder carries the provider submission through the capture-result collect input", () => {
|
|
468
|
+
const lineageId = "review-1d5aadacc600e167";
|
|
469
|
+
const sha = `sha256:${"c".repeat(64)}`;
|
|
470
|
+
const tree = "3".repeat(40);
|
|
471
|
+
const rawSubmission = {
|
|
472
|
+
operation_token: "capture-result",
|
|
473
|
+
argument_tokens: [...BINDING_TOKENS, "--input={{value}}"],
|
|
474
|
+
values: [{ slot: "reviewer_result", domain: "artifact_path_or_stdin", substitution_location: BINDING_TOKENS.length }],
|
|
475
|
+
};
|
|
476
|
+
const rawInput = {
|
|
477
|
+
name: "reviewer_result",
|
|
478
|
+
schema: "https://gentle-ai.dev/schema/review/reviewer/v1",
|
|
479
|
+
capture_operation: "review.capture-result",
|
|
480
|
+
arguments: [
|
|
481
|
+
{ name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
|
|
482
|
+
{ name: "agent", value: "pi", token: "--agent=pi" },
|
|
483
|
+
{ name: "materialize", value: "true", token: "--materialize=true" },
|
|
484
|
+
],
|
|
485
|
+
artifact_subject: {
|
|
486
|
+
schema: "gentle-ai.review-artifact-subject/v2",
|
|
487
|
+
subject_hash: sha,
|
|
488
|
+
lineage_id: lineageId,
|
|
489
|
+
authority_revision: sha,
|
|
490
|
+
target_identity: sha,
|
|
491
|
+
base_tree: tree,
|
|
492
|
+
candidate_tree: tree,
|
|
493
|
+
changed_path_manifest_sha256: sha,
|
|
494
|
+
lens: "review-reliability",
|
|
495
|
+
selected_order: 0,
|
|
496
|
+
},
|
|
497
|
+
base_tree: tree,
|
|
498
|
+
candidate_tree: tree,
|
|
499
|
+
changed_path_manifest: [{ path: "app.ts", status: "M", old_mode: "100644", new_mode: "100644", deleted: false, type_changed: false, mode_only: false, intended_untracked: false }],
|
|
500
|
+
submission: rawSubmission,
|
|
501
|
+
};
|
|
502
|
+
const decoded = decodeReviewNextTransitionV3({ kind: "collect", reason_code: "reviewer_results_required", collect: { inputs: [rawInput] } });
|
|
503
|
+
assert.equal(decoded.collect!.inputs[0]!.submission!.operationToken, "capture-result");
|
|
504
|
+
assert.deepEqual(decoded.collect!.inputs[0]!.submission!.argumentTokens, rawSubmission.argument_tokens);
|
|
505
|
+
assert.deepEqual(decoded.collect!.inputs[0]!.submission!.values, [{ slot: "reviewer_result", domain: "artifact_path_or_stdin", substitutionLocation: BINDING_TOKENS.length }]);
|
|
506
|
+
|
|
507
|
+
// Strict rejections: submission outside capture-result, and a
|
|
508
|
+
// substitution location outside its own argument tokens.
|
|
509
|
+
assert.throws(() => decodeReviewNextTransitionV3({ kind: "collect", reason_code: "verification_evidence_required", collect: { inputs: [{
|
|
510
|
+
name: "verification_evidence",
|
|
511
|
+
schema: "gentle-ai.review-verification-evidence/v2",
|
|
512
|
+
capture_operation: "review.capture-evidence",
|
|
513
|
+
arguments: [{ name: "lineage", value: lineageId }],
|
|
514
|
+
submission: rawSubmission,
|
|
515
|
+
}] } }), /submission is only valid for review\.capture-result/);
|
|
516
|
+
assert.throws(() => decodeReviewNextTransitionV3({ kind: "collect", reason_code: "reviewer_results_required", collect: { inputs: [{
|
|
517
|
+
...rawInput,
|
|
518
|
+
submission: { ...rawSubmission, values: [{ slot: "reviewer_result", domain: "artifact_path_or_stdin", substitution_location: rawSubmission.argument_tokens.length }] },
|
|
519
|
+
}] } }), /substitution_location/);
|
|
520
|
+
});
|