gentle-pi 3.2.1 → 3.4.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 +63 -59
- package/assets/orchestrator-delegation.md +1 -1
- package/docs/assets/brand/gentle-shell-banner.gif +0 -0
- package/docs/assets/diagrams/odd-workflow.svg +74 -0
- package/docs/assets/features/agents-view.png +0 -0
- package/docs/assets/features/changes-view.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/assets/features/profiles-routing.png +0 -0
- package/docs/gentle-shell.md +52 -15
- package/docs/readme-reference.md +67 -7
- package/docs/review-integration.md +22 -17
- package/extensions/ask-user-question.ts +210 -0
- package/extensions/gentle-agents.ts +93 -18
- package/extensions/gentle-ai.ts +180 -37
- package/extensions/gentle-shell.ts +476 -39
- package/extensions/gentle-todo.ts +19 -1
- package/extensions/quiet-tools.ts +28 -5
- package/extensions/startup-banner.ts +25 -10
- package/lib/agents-view.ts +41 -14
- package/lib/agents-widget.ts +84 -13
- package/lib/animation-policy.ts +52 -0
- package/lib/background-cache-warming.ts +38 -0
- package/lib/command-palette-catalog.ts +2 -0
- package/lib/double-esc-cancel-policy.ts +138 -0
- package/lib/inprocess-reviewer.ts +297 -0
- package/lib/native-review-cli.ts +50 -10
- package/lib/odd-runtime-delegation-gate.ts +88 -0
- package/lib/questionnaire/questionnaire-view.ts +603 -0
- package/lib/questionnaire/schema.ts +82 -0
- package/lib/questionnaire/validate.ts +141 -0
- package/lib/review-candidate-view-owner.ts +20 -5
- package/lib/review-candidate-view.ts +9 -2
- package/lib/review-host-relay.ts +256 -171
- package/lib/review-integration-v2.ts +114 -27
- package/lib/shell-bar.ts +163 -75
- package/lib/shell-card.ts +19 -9
- package/lib/shell-changes-view.ts +43 -5
- package/lib/shell-changes.ts +92 -5
- package/lib/shell-hover.ts +39 -0
- package/lib/shell-prompt.ts +10 -1
- package/lib/shell-sidebar-layout.ts +118 -16
- package/lib/shell-sidebar.ts +16 -0
- package/lib/shell-todo.ts +7 -1
- package/lib/shell-usage-view.ts +103 -12
- package/lib/shell-usage.ts +120 -6
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +49 -9
- package/runtime/review-integration-v2.mjs +114 -27
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/maintainer/provider-relay-matrix.mjs +118 -47
- package/scripts/verify-package-files.mjs +3 -4
- package/tests/agents-grouping.test.ts +75 -18
- package/tests/agents-view.test.ts +28 -18
- package/tests/agents-widget.test.ts +100 -12
- package/tests/animation-policy.test.ts +42 -0
- package/tests/ask-user-question.test.ts +435 -0
- package/tests/background-cache-warming.test.ts +60 -0
- package/tests/background-subagents.test.ts +68 -0
- package/tests/command-palette.test.ts +10 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +176 -138
- package/tests/double-esc-cancel-policy.test.ts +194 -0
- package/tests/gentle-agents.test.ts +599 -7
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +125 -9
- package/tests/gentle-shell.test.ts +1149 -24
- package/tests/gentle-todo.test.ts +17 -4
- package/tests/inprocess-reviewer.test.ts +460 -0
- package/tests/maintainer/provider-relay.maintest.ts +101 -143
- package/tests/native-review-capability-contract.test.ts +34 -1
- package/tests/native-review-parity.test.ts +19 -0
- package/tests/odd-runtime-delegation-gate.test.ts +212 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/package-manifest.test.ts +6 -17
- package/tests/questionnaire-schema.test.ts +274 -0
- package/tests/questionnaire-view.test.ts +446 -0
- package/tests/rdd-status-line.test.ts +21 -4
- package/tests/review-candidate-owner-retry.test.ts +63 -0
- package/tests/review-candidate-view.test.ts +15 -0
- package/tests/review-controller-native-routing.test.ts +86 -0
- package/tests/review-host-relay-routing.test.ts +77 -0
- package/tests/review-host-relay.test.ts +297 -299
- package/tests/review-integration-v2-forward.test.ts +61 -0
- package/tests/review-integration-v2.test.ts +146 -1
- package/tests/review-ledger-contract.test.ts +1 -2
- package/tests/review-relay-transport-agent.test.ts +129 -26
- package/tests/review-risk-assessment.test.ts +104 -0
- package/tests/runtime-harness.mjs +11 -0
- package/tests/session-changes-shell.test.ts +27 -0
- package/tests/session-worktree-registry.test.ts +41 -0
- package/tests/shell-bar.test.ts +200 -124
- package/tests/shell-card.test.ts +5 -3
- package/tests/shell-changes-view.test.ts +47 -0
- package/tests/shell-changes.test.ts +177 -0
- package/tests/shell-hover.test.ts +19 -0
- package/tests/shell-prompt.test.ts +20 -0
- package/tests/shell-sidebar-fullscreen.test.ts +59 -0
- package/tests/shell-sidebar-layout.test.ts +301 -8
- package/tests/shell-sidebar.test.ts +25 -1
- package/tests/shell-todo.test.ts +36 -0
- package/tests/shell-usage-view.test.ts +120 -1
- package/tests/shell-usage.test.ts +129 -0
- package/tests/skill-collision-prefixes.test.ts +1 -1
- package/tests/startup-banner.test.ts +93 -2
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/lib/opaque-pi-reviewer-adapter.ts +0 -404
- package/skills/release/SKILL.md +0 -137
- package/tests/opaque-pi-reviewer-adapter.test.ts +0 -410
|
@@ -4,9 +4,9 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import { createNodeExecFileAdapter } from "../lib/native-review-cli.ts";
|
|
7
|
+
import { INPROCESS_REVIEWER_FAILURE, type InProcessReviewerOutcome, type InProcessReviewerRequest, type InProcessReviewerDeps, runInProcessReviewer } from "../lib/inprocess-reviewer.ts";
|
|
7
8
|
import {
|
|
8
9
|
REVIEW_HOST_RELAY_FAILURE,
|
|
9
|
-
REVIEW_HOST_RELAY_PI_ARGV,
|
|
10
10
|
REVIEW_HOST_RELAY_PI_TIMEOUT_ENV,
|
|
11
11
|
REVIEW_HOST_RELAY_PI_TIMEOUT_FLOOR_MS,
|
|
12
12
|
REVIEW_HOST_RELAY_PI_TIMEOUT_MAX_MS,
|
|
@@ -27,14 +27,26 @@ import {
|
|
|
27
27
|
type ReviewHostRelayPreparedResult,
|
|
28
28
|
type ReviewHostRelayRequest,
|
|
29
29
|
} from "../lib/review-host-relay.ts";
|
|
30
|
+
import * as reviewHostRelayModule from "../lib/review-host-relay.ts";
|
|
30
31
|
import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "../lib/review-relay-contract.ts";
|
|
31
32
|
import { decodeReviewNextTransitionV3, type ReviewCaptureSubmissionV1, type ReviewCollectInputV3 } from "../lib/review-integration-v2.ts";
|
|
32
33
|
|
|
34
|
+
// gentle-pi#311 P2: the reviewer transport this file exercises no longer
|
|
35
|
+
// spawns a pi child. `runReviewHostRelaySlot`/`prepareReviewHostRelaySlot`
|
|
36
|
+
// take an injectable `runReviewer` (defaulting to the real
|
|
37
|
+
// `runInProcessReviewer`), so every test below fakes that seam instead of a
|
|
38
|
+
// fake `pi` executable on disk. `lib/inprocess-reviewer.ts`'s own tests own
|
|
39
|
+
// the completion's internal behavior (model resolution, auth, thinking
|
|
40
|
+
// mapping, text extraction); this file owns the relay's mapping of that
|
|
41
|
+
// completion's outcome onto a typed `ReviewHostRelayError`, plus everything
|
|
42
|
+
// unrelated to the reviewer transport (materialize, submission, admission,
|
|
43
|
+
// slot detection) which stayed exactly as it was.
|
|
44
|
+
|
|
33
45
|
// ---------------------------------------------------------------------------
|
|
34
|
-
// Fake
|
|
35
|
-
// wrappers in related review tests),
|
|
36
|
-
//
|
|
37
|
-
//
|
|
46
|
+
// Fake gentle-ai binary. Following the repo's fake-executable idiom (shell/git
|
|
47
|
+
// wrappers in related review tests), this is a shebang script written into a
|
|
48
|
+
// scratch directory; a node script is used so binary-unsafe bytes survive
|
|
49
|
+
// verbatim.
|
|
38
50
|
// ---------------------------------------------------------------------------
|
|
39
51
|
|
|
40
52
|
const FAKE_GENTLE_AI = `#!/usr/bin/env node
|
|
@@ -94,52 +106,10 @@ process.stderr.write("unexpected fake gentle-ai invocation\\n");
|
|
|
94
106
|
process.exit(9);
|
|
95
107
|
`;
|
|
96
108
|
|
|
97
|
-
const FAKE_PI = `#!/usr/bin/env node
|
|
98
|
-
const fs = require("node:fs");
|
|
99
|
-
const path = require("node:path");
|
|
100
|
-
const argv = process.argv.slice(2);
|
|
101
|
-
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");
|
|
102
|
-
const chunks = [];
|
|
103
|
-
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
104
|
-
process.stdin.on("end", () => {
|
|
105
|
-
const stdin = Buffer.concat(chunks);
|
|
106
|
-
if (process.env.RELAY_FAKE_PI_STDIN_CAPTURE) fs.writeFileSync(process.env.RELAY_FAKE_PI_STDIN_CAPTURE, stdin);
|
|
107
|
-
const finish = () => {
|
|
108
|
-
const mode = process.env.RELAY_FAKE_PI_MODE || "ok";
|
|
109
|
-
if (process.env.RELAY_FAKE_PI_BREAK_CLEANUP === "true") fs.chmodSync(path.dirname(process.cwd()), 0o500);
|
|
110
|
-
if (mode === "ok") { process.stdout.write(Buffer.from(process.env.RELAY_FAKE_PI_OUTPUT_B64 || "", "base64")); process.exit(0); }
|
|
111
|
-
if (mode === "empty") process.exit(0);
|
|
112
|
-
if (mode === "hang") { setTimeout(() => process.exit(0), 10_000); return; }
|
|
113
|
-
process.stderr.write("pi exploded\\n");
|
|
114
|
-
process.exit(4);
|
|
115
|
-
};
|
|
116
|
-
const barrierDirectory = process.env.RELAY_FAKE_PI_BARRIER_DIR;
|
|
117
|
-
if (barrierDirectory === undefined) { finish(); return; }
|
|
118
|
-
fs.mkdirSync(barrierDirectory, { recursive: true });
|
|
119
|
-
fs.writeFileSync(path.join(barrierDirectory, String(process.pid)), "started");
|
|
120
|
-
const expected = Number(process.env.RELAY_FAKE_PI_BARRIER_COUNT || "4");
|
|
121
|
-
const deadline = Date.now() + 1_000;
|
|
122
|
-
const releaseFile = process.env.RELAY_FAKE_PI_RELEASE_FILE;
|
|
123
|
-
const waitForGroup = () => {
|
|
124
|
-
if (fs.readdirSync(barrierDirectory).length < expected) {
|
|
125
|
-
if (Date.now() >= deadline) { process.stderr.write("reviewer barrier timed out\\n"); process.exit(5); return; }
|
|
126
|
-
setTimeout(waitForGroup, 10);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (releaseFile !== undefined && !fs.existsSync(releaseFile)) { setTimeout(waitForGroup, 10); return; }
|
|
130
|
-
finish();
|
|
131
|
-
};
|
|
132
|
-
waitForGroup();
|
|
133
|
-
});
|
|
134
|
-
`;
|
|
135
|
-
|
|
136
109
|
interface RelayHarness {
|
|
137
110
|
directory: string;
|
|
138
111
|
gentleAi: string;
|
|
139
|
-
pi: string;
|
|
140
112
|
logPath: string;
|
|
141
|
-
piLogPath: string;
|
|
142
|
-
stdinCapturePath: string;
|
|
143
113
|
submitCapturePath: string;
|
|
144
114
|
targetCwd: string;
|
|
145
115
|
environment: NodeJS.ProcessEnv;
|
|
@@ -149,32 +119,25 @@ function harness(t: test.TestContext, overrides: Record<string, string> = {}): R
|
|
|
149
119
|
const directory = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-relay-harness-")));
|
|
150
120
|
t.after(() => rmSync(directory, { recursive: true, force: true }));
|
|
151
121
|
const gentleAi = join(directory, "gentle-ai");
|
|
152
|
-
const pi = join(directory, "pi");
|
|
153
122
|
writeFileSync(gentleAi, FAKE_GENTLE_AI);
|
|
154
123
|
chmodSync(gentleAi, 0o755);
|
|
155
|
-
writeFileSync(pi, FAKE_PI);
|
|
156
|
-
chmodSync(pi, 0o755);
|
|
157
124
|
const logPath = join(directory, "gentle-ai.log");
|
|
158
|
-
const piLogPath = join(directory, "pi.log");
|
|
159
|
-
const stdinCapturePath = join(directory, "pi-stdin.bin");
|
|
160
125
|
const submitCapturePath = join(directory, "submitted.bin");
|
|
161
126
|
const targetCwd = join(directory, "target-worktree");
|
|
162
127
|
mkdirSync(targetCwd);
|
|
163
128
|
const environment: NodeJS.ProcessEnv = {
|
|
164
129
|
...process.env,
|
|
165
130
|
RELAY_FAKE_LOG: logPath,
|
|
166
|
-
RELAY_FAKE_PI_LOG: piLogPath,
|
|
167
|
-
RELAY_FAKE_PI_STDIN_CAPTURE: stdinCapturePath,
|
|
168
131
|
RELAY_FAKE_SUBMIT_CAPTURE: submitCapturePath,
|
|
169
132
|
...overrides,
|
|
170
133
|
};
|
|
171
134
|
// The relay itself must add the handshake; the base environment never
|
|
172
135
|
// carries it, so the fake-binary log proves the injection.
|
|
173
136
|
delete environment[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV];
|
|
174
|
-
return { directory, gentleAi,
|
|
137
|
+
return { directory, gentleAi, logPath, submitCapturePath, targetCwd, environment };
|
|
175
138
|
}
|
|
176
139
|
|
|
177
|
-
function readLog(path: string): Array<{ argv: string[]; contract: string | null; cwd?: string
|
|
140
|
+
function readLog(path: string): Array<{ argv: string[]; contract: string | null; cwd?: string }> {
|
|
178
141
|
if (!existsSync(path)) return [];
|
|
179
142
|
return readFileSync(path, "utf8").split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
180
143
|
}
|
|
@@ -207,44 +170,61 @@ const SUBMISSION: ReviewCaptureSubmissionV1 = Object.freeze({
|
|
|
207
170
|
values: Object.freeze([{ slot: "reviewer_result", domain: "artifact_path_or_stdin", substitutionLocation: BINDING_TOKENS.length }]),
|
|
208
171
|
});
|
|
209
172
|
|
|
210
|
-
// Prompt
|
|
211
|
-
//
|
|
212
|
-
//
|
|
173
|
+
// Prompt bytes deliberately include binary-unsafe content: NUL, control
|
|
174
|
+
// bytes, quotes, backslashes, CRLF, multi-byte UTF-8, and bytes that are not
|
|
175
|
+
// valid UTF-8 at all. The relay must materialize them verbatim into the
|
|
176
|
+
// completion request.
|
|
213
177
|
const PROMPT_BYTES = Buffer.concat([
|
|
214
178
|
Buffer.from('GENTLE_AI_REVIEW_BINDING {"lineage":"review-1d5aadacc600e167"}\n"quotes" \\backslash\r\n\u00e9\u{1F3A9}\n', "utf8"),
|
|
215
179
|
Buffer.from([0x00, 0x01, 0x07, 0xff, 0xfe, 0x00]),
|
|
216
180
|
]);
|
|
217
|
-
// The reviewer
|
|
218
|
-
// assistant text
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const assistantEnd = (text: string, reviewerModel?: string) => {
|
|
223
|
-
const message: Record<string, unknown> = { role: "assistant", content: [{ type: "text", text }] };
|
|
224
|
-
if (reviewerModel !== undefined) message["model"] = reviewerModel;
|
|
225
|
-
return { type: "message_end", message };
|
|
226
|
-
};
|
|
227
|
-
const PI_OUTPUT_BYTES = piEventStream(
|
|
228
|
-
{ type: "message_start", message: { role: "user", content: [] } },
|
|
229
|
-
assistantEnd(PI_REVIEWER_TEXT),
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
function relayRequest(fixture: RelayHarness, overrides: Record<string, unknown> = {}) {
|
|
181
|
+
// The reviewer's completion text — what used to be the pi event stream's
|
|
182
|
+
// extracted assistant text is now simply the in-process outcome's `text`.
|
|
183
|
+
const REVIEWER_TEXT = `{"subject_hash":"sha256:${"a".repeat(64)}","findings":[]}\n🎉\r\n`;
|
|
184
|
+
|
|
185
|
+
function relayRequest(fixture: RelayHarness, overrides: Record<string, unknown> = {}): ReviewHostRelayRequest {
|
|
233
186
|
return {
|
|
234
187
|
captureArgumentTokens: CAPTURE_TOKENS,
|
|
235
188
|
submission: SUBMISSION,
|
|
236
189
|
gentleAiExecutable: fixture.gentleAi,
|
|
237
|
-
piExecutable: fixture.pi,
|
|
238
190
|
targetCwd: fixture.targetCwd,
|
|
191
|
+
reviewerRegistry: {
|
|
192
|
+
find: () => ({ id: "gpt-5", name: "GPT-5", api: "openai-responses", provider: "openai", baseUrl: "https://example.invalid", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 100_000, maxTokens: 8192 }) as never,
|
|
193
|
+
getApiKeyAndHeaders: async () => ({ ok: true, apiKey: "test-key" }),
|
|
194
|
+
},
|
|
195
|
+
selection: "openai/gpt-5",
|
|
196
|
+
routingKey: "review-reliability",
|
|
239
197
|
environment: {
|
|
240
198
|
...fixture.environment,
|
|
241
199
|
RELAY_FAKE_PROMPT_B64: PROMPT_BYTES.toString("base64"),
|
|
242
|
-
RELAY_FAKE_PI_OUTPUT_B64: PI_OUTPUT_BYTES.toString("base64"),
|
|
243
200
|
},
|
|
244
201
|
gentleAiTimeoutMs: 30_000,
|
|
245
202
|
piTimeoutMs: 30_000,
|
|
246
203
|
...overrides,
|
|
247
|
-
};
|
|
204
|
+
} as ReviewHostRelayRequest;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** A `runReviewer` fake resolving with canned completion text, capturing every call. */
|
|
208
|
+
function textReviewer(text: string, reviewerModel = "openai/gpt-5") {
|
|
209
|
+
const calls: InProcessReviewerRequest[] = [];
|
|
210
|
+
const runReviewer = (async (request: InProcessReviewerRequest, _deps: InProcessReviewerDeps): Promise<InProcessReviewerOutcome> => {
|
|
211
|
+
calls.push(request);
|
|
212
|
+
return { kind: "text", text, reviewerModel };
|
|
213
|
+
}) as typeof runInProcessReviewer;
|
|
214
|
+
return { runReviewer, calls };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** A `runReviewer` fake resolving with a canned typed refusal. */
|
|
218
|
+
function refusingReviewer(code: (typeof INPROCESS_REVIEWER_FAILURE)[keyof typeof INPROCESS_REVIEWER_FAILURE], message: string, evidence?: Record<string, unknown>, delayMs = 0): typeof runInProcessReviewer {
|
|
219
|
+
return (async () => {
|
|
220
|
+
if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
221
|
+
return { kind: "refused", code, message, ...(evidence === undefined ? {} : { evidence }) };
|
|
222
|
+
}) as typeof runInProcessReviewer;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Runs one relay slot with a safe-by-default fake reviewer; no test may reach the real network unless it explicitly overrides this. */
|
|
226
|
+
function runRelay(fixture: RelayHarness, overrides: Record<string, unknown> = {}, runReviewer: typeof runInProcessReviewer = textReviewer(REVIEWER_TEXT).runReviewer): Promise<{ promptByteLength: number; resultByteLength: number; submission: string }> {
|
|
227
|
+
return runReviewHostRelaySlot(relayRequest(fixture, overrides), runReviewer);
|
|
248
228
|
}
|
|
249
229
|
|
|
250
230
|
function reviewerGroupRequests(fixture: RelayHarness): ReviewHostRelayRequest[] {
|
|
@@ -252,15 +232,14 @@ function reviewerGroupRequests(fixture: RelayHarness): ReviewHostRelayRequest[]
|
|
|
252
232
|
captureArgumentTokens: CAPTURE_TOKENS.map((token) => token === "--lens=review-reliability"
|
|
253
233
|
? `--lens=${lens}`
|
|
254
234
|
: token === "--order=0" ? `--order=${order}` : token),
|
|
235
|
+
routingKey: lens,
|
|
255
236
|
environment: {
|
|
256
237
|
...fixture.environment,
|
|
257
238
|
RELAY_FAKE_PROMPT_B64: PROMPT_BYTES.toString("base64"),
|
|
258
|
-
RELAY_FAKE_PI_OUTPUT_B64: PI_OUTPUT_BYTES.toString("base64"),
|
|
259
239
|
},
|
|
260
240
|
}));
|
|
261
241
|
}
|
|
262
242
|
|
|
263
|
-
|
|
264
243
|
async function rejectsWithRelayError(promise: Promise<unknown>, kind: string, stage: string): Promise<ReviewHostRelayError> {
|
|
265
244
|
let caught: ReviewHostRelayError | undefined;
|
|
266
245
|
await assert.rejects(promise, (error: unknown) => {
|
|
@@ -299,28 +278,36 @@ test("relay contract constants are the compiled gentle-ai handshake values", ()
|
|
|
299
278
|
assert.equal(GENTLE_PI_REVIEW_RELAY_CONTRACT, "gentle-pi.review-relay/v1");
|
|
300
279
|
});
|
|
301
280
|
|
|
281
|
+
test("no pi child launcher is exported: the lens path completes in-process", () => {
|
|
282
|
+
const relayExports = reviewHostRelayModule as unknown as Record<string, unknown>;
|
|
283
|
+
assert.equal("REVIEW_HOST_RELAY_PI_ARGV" in relayExports, false, "the pinned pi argv export must be gone");
|
|
284
|
+
assert.equal("resolveReviewHostRelayExtensionPaths" in relayExports, false, "the extension allowlist reader must be gone");
|
|
285
|
+
});
|
|
286
|
+
|
|
302
287
|
// ---------------------------------------------------------------------------
|
|
303
288
|
// Happy path
|
|
304
289
|
// ---------------------------------------------------------------------------
|
|
305
290
|
|
|
306
|
-
test("relay happy path moves prompt bytes verbatim
|
|
291
|
+
test("relay happy path moves prompt bytes verbatim into the completion and submits its text through the exact provider submission token", async (t) => {
|
|
307
292
|
const fixture = harness(t);
|
|
308
|
-
const
|
|
293
|
+
const { runReviewer, calls } = textReviewer(REVIEWER_TEXT);
|
|
294
|
+
const result = await runRelay(fixture, {}, runReviewer);
|
|
309
295
|
|
|
310
296
|
assert.equal(result.promptByteLength, PROMPT_BYTES.length);
|
|
311
|
-
assert.equal(result.resultByteLength, Buffer.byteLength(
|
|
297
|
+
assert.equal(result.resultByteLength, Buffer.byteLength(REVIEWER_TEXT));
|
|
312
298
|
assert.equal(JSON.parse(result.submission).admission_decision, "completed");
|
|
313
299
|
|
|
314
|
-
// Prompt bytes reached
|
|
315
|
-
assert.
|
|
316
|
-
|
|
317
|
-
|
|
300
|
+
// Prompt bytes reached the completion request verbatim.
|
|
301
|
+
assert.equal(calls.length, 1);
|
|
302
|
+
assert.ok(calls[0]!.prompt.equals(PROMPT_BYTES));
|
|
303
|
+
// Submission --input file bytes are EXACTLY the reviewer's completion text.
|
|
304
|
+
assert.deepEqual(readFileSync(fixture.submitCapturePath), Buffer.from(REVIEWER_TEXT, "utf8"));
|
|
318
305
|
|
|
319
306
|
const gentleAiCalls = readLog(fixture.logPath);
|
|
320
307
|
assert.equal(gentleAiCalls.length, 2);
|
|
321
308
|
// (a) exact provider tokens, verbatim, in provider order.
|
|
322
309
|
assert.deepEqual(gentleAiCalls[0]!.argv, ["review", "capture-result", ...CAPTURE_TOKENS]);
|
|
323
|
-
// (
|
|
310
|
+
// (b) the provider-owned submission form, verbatim: its exact operation
|
|
324
311
|
// and argument tokens with only the artifact path substituted into the
|
|
325
312
|
// declared {{value}} slot. No agent/materialize, nothing synthesized.
|
|
326
313
|
assert.deepEqual(gentleAiCalls[1]!.argv.slice(0, 2 + BINDING_TOKENS.length), ["review", SUBMISSION.operationToken, ...BINDING_TOKENS]);
|
|
@@ -334,83 +321,74 @@ test("relay happy path moves prompt bytes verbatim and the pi event stream's ass
|
|
|
334
321
|
// environment carried none.
|
|
335
322
|
assert.deepEqual(gentleAiCalls.map((call) => call.contract), [GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT]);
|
|
336
323
|
assert.deepEqual(gentleAiCalls.map((call) => call.cwd), [fixture.targetCwd, fixture.targetCwd]);
|
|
337
|
-
|
|
338
|
-
const piCalls = readLog(fixture.piLogPath);
|
|
339
|
-
assert.equal(piCalls.length, 1);
|
|
340
|
-
// The pi environment stays untouched: no relay handshake is injected.
|
|
341
|
-
assert.equal(piCalls[0]!.contract, null);
|
|
342
|
-
// Fresh EMPTY scratch cwd, removed after the run.
|
|
343
|
-
assert.deepEqual(piCalls[0]!.entries, []);
|
|
344
|
-
assert.notEqual(piCalls[0]!.cwd, process.cwd());
|
|
345
|
-
assert.equal(existsSync(piCalls[0]!.cwd!), false);
|
|
346
324
|
});
|
|
347
325
|
|
|
348
|
-
|
|
326
|
+
// gentle-pi#311 P3: a v9 provider role slot (refuter, targeted validator)
|
|
327
|
+
// reaches this same relay through the exact same request shape a lens
|
|
328
|
+
// materialize slot uses — the only difference is the operation name the
|
|
329
|
+
// provider's own submission descriptor names. The materialize invocation
|
|
330
|
+
// must follow that name, never a hardcoded "capture-result".
|
|
331
|
+
test("the materialize invocation follows the provider's own submission operation, not a hardcoded capture-result", async (t) => {
|
|
349
332
|
const fixture = harness(t);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
"
|
|
354
|
-
"
|
|
355
|
-
"--no-session",
|
|
356
|
-
"--no-tools",
|
|
357
|
-
"--no-extensions",
|
|
358
|
-
"--no-skills",
|
|
359
|
-
"--no-prompt-templates",
|
|
360
|
-
"--no-themes",
|
|
361
|
-
"--no-context-files",
|
|
362
|
-
"--no-approve",
|
|
333
|
+
const bindingTokens = [
|
|
334
|
+
"--lineage=review-1d5aadacc600e167",
|
|
335
|
+
`--expected-revision=sha256:${"c".repeat(64)}`,
|
|
336
|
+
`--target=sha256:${"d".repeat(64)}`,
|
|
337
|
+
`--repository-context=rctx1_${"e".repeat(64)}`,
|
|
363
338
|
];
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
339
|
+
const refuterCaptureTokens = [...bindingTokens, "--agent=pi", "--materialize=true"];
|
|
340
|
+
const refuterSubmission: ReviewCaptureSubmissionV1 = {
|
|
341
|
+
operationToken: "capture-refuter",
|
|
342
|
+
argumentTokens: [...bindingTokens, "--agent=pi", "--input={{value}}"],
|
|
343
|
+
values: [{ slot: "provider_refuter", domain: "artifact_path_or_stdin", substitutionLocation: bindingTokens.length + 1 }],
|
|
344
|
+
};
|
|
345
|
+
const { runReviewer, calls } = textReviewer(REVIEWER_TEXT);
|
|
346
|
+
const result = await runRelay(fixture, { captureArgumentTokens: refuterCaptureTokens, submission: refuterSubmission, routingKey: "review-refuter" }, runReviewer);
|
|
347
|
+
|
|
348
|
+
assert.equal(JSON.parse(result.submission).admission_decision, "completed");
|
|
349
|
+
assert.equal(calls.length, 1);
|
|
350
|
+
const gentleAiCalls = readLog(fixture.logPath);
|
|
351
|
+
assert.equal(gentleAiCalls.length, 2);
|
|
352
|
+
assert.deepEqual(gentleAiCalls[0]!.argv, ["review", "capture-refuter", ...refuterCaptureTokens]);
|
|
353
|
+
assert.deepEqual(gentleAiCalls[1]!.argv.slice(0, 2 + bindingTokens.length), ["review", "capture-refuter", ...bindingTokens]);
|
|
354
|
+
assert.equal(gentleAiCalls[1]!.argv[2 + bindingTokens.length], "--agent=pi");
|
|
355
|
+
const submitted = gentleAiCalls[1]!.argv.at(-1)!;
|
|
356
|
+
assert.match(submitted, /^--input=\S+$/);
|
|
369
357
|
});
|
|
370
358
|
|
|
371
359
|
test("preparation snapshots mutable submission tokens and values before materialization", async (t) => {
|
|
372
360
|
const fixture = harness(t);
|
|
373
|
-
const barrierDirectory = join(fixture.directory, "submission-snapshot-barrier");
|
|
374
|
-
const releaseFile = join(fixture.directory, "submission-snapshot-release");
|
|
375
361
|
const submissionTokens = [...SUBMISSION.argumentTokens];
|
|
376
362
|
const submissionValues = SUBMISSION.values.map((value) => ({ ...value }));
|
|
377
363
|
const submission: ReviewCaptureSubmissionV1 = { ...SUBMISSION, argumentTokens: submissionTokens, values: submissionValues };
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}));
|
|
389
|
-
await waitFor(() => readLog(fixture.piLogPath).length === 1, "reviewer did not reach the snapshot barrier");
|
|
364
|
+
let releaseCompletion: (() => void) | undefined;
|
|
365
|
+
const gate = new Promise<void>((resolve) => { releaseCompletion = resolve; });
|
|
366
|
+
let reached = false;
|
|
367
|
+
const runReviewer = (async () => {
|
|
368
|
+
reached = true;
|
|
369
|
+
await gate;
|
|
370
|
+
return { kind: "text", text: REVIEWER_TEXT, reviewerModel: "openai/gpt-5" } as InProcessReviewerOutcome;
|
|
371
|
+
}) as typeof runInProcessReviewer;
|
|
372
|
+
const prepared = prepareReviewHostRelaySlot(relayRequest(fixture, { submission }), runReviewer);
|
|
373
|
+
await waitFor(() => reached, "the completion was never reached");
|
|
390
374
|
submissionTokens[submissionTokens.length - 1] = "--input=mutated";
|
|
391
375
|
submissionValues[0]!.slot = "mutated";
|
|
392
|
-
|
|
376
|
+
releaseCompletion!();
|
|
393
377
|
|
|
394
378
|
const result = await prepared;
|
|
395
379
|
assert.deepEqual(result.request.submission, SUBMISSION);
|
|
396
380
|
await submitReviewHostRelayPreparedResult(result);
|
|
397
|
-
assert.deepEqual(readFileSync(fixture.submitCapturePath), Buffer.from(
|
|
381
|
+
assert.deepEqual(readFileSync(fixture.submitCapturePath), Buffer.from(REVIEWER_TEXT, "utf8"));
|
|
398
382
|
});
|
|
399
383
|
|
|
400
384
|
test("preparation keeps reviewer bytes private through deferred submission", async (t) => {
|
|
401
385
|
const fixture = harness(t);
|
|
402
|
-
const
|
|
403
|
-
const prepared = await prepareReviewHostRelaySlot(relayRequest(fixture), async () => ({
|
|
404
|
-
stdout: reviewerBytes,
|
|
405
|
-
promptByteLength: PROMPT_BYTES.length,
|
|
406
|
-
stdoutByteLength: reviewerBytes.length,
|
|
407
|
-
}));
|
|
408
|
-
reviewerBytes.fill(0);
|
|
386
|
+
const prepared = await prepareReviewHostRelaySlot(relayRequest(fixture), textReviewer(REVIEWER_TEXT).runReviewer);
|
|
409
387
|
const mutablePrepared = prepared as unknown as { resultBytes?: Buffer };
|
|
410
388
|
mutablePrepared.resultBytes?.fill(0);
|
|
411
389
|
assert.throws(() => { mutablePrepared.resultBytes = Buffer.from("fabricated"); }, TypeError);
|
|
412
390
|
await submitReviewHostRelayPreparedResult(prepared);
|
|
413
|
-
assert.deepEqual(readFileSync(fixture.submitCapturePath),
|
|
391
|
+
assert.deepEqual(readFileSync(fixture.submitCapturePath), Buffer.from(REVIEWER_TEXT, "utf8"));
|
|
414
392
|
|
|
415
393
|
const fabricated = { ...prepared, resultBytes: Buffer.from("fabricated") } as unknown as ReviewHostRelayPreparedResult;
|
|
416
394
|
await assert.rejects(submitReviewHostRelayPreparedResult(fabricated), /recognized prepared result/);
|
|
@@ -420,7 +398,7 @@ test("preparation keeps reviewer bytes private through deferred submission", asy
|
|
|
420
398
|
test("the supplied AbortSignal stays live through deferred submission", async (t) => {
|
|
421
399
|
const fixture = harness(t, { RELAY_FAKE_SUBMIT_DELAY_MS: "1000" });
|
|
422
400
|
const controller = new AbortController();
|
|
423
|
-
const prepared = await prepareReviewHostRelaySlot(relayRequest(fixture, { signal: controller.signal }));
|
|
401
|
+
const prepared = await prepareReviewHostRelaySlot(relayRequest(fixture, { signal: controller.signal }), textReviewer(REVIEWER_TEXT).runReviewer);
|
|
424
402
|
const submission = submitReviewHostRelayPreparedResult(prepared);
|
|
425
403
|
await waitFor(() => readLog(fixture.logPath).length === 2, "submission did not start");
|
|
426
404
|
controller.abort();
|
|
@@ -429,21 +407,22 @@ test("the supplied AbortSignal stays live through deferred submission", async (t
|
|
|
429
407
|
|
|
430
408
|
test("four reviewers cross a shared barrier before any result submission can begin", async (t) => {
|
|
431
409
|
const fixture = harness(t);
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
410
|
+
const requests = reviewerGroupRequests(fixture);
|
|
411
|
+
let started = 0;
|
|
412
|
+
const releases: Array<() => void> = [];
|
|
413
|
+
const runReviewer = (async () => {
|
|
414
|
+
started += 1;
|
|
415
|
+
await new Promise<void>((resolve) => releases.push(resolve));
|
|
416
|
+
return { kind: "text", text: REVIEWER_TEXT, reviewerModel: "openai/gpt-5" } as InProcessReviewerOutcome;
|
|
417
|
+
}) as typeof runInProcessReviewer;
|
|
418
|
+
|
|
419
|
+
const group = runReviewHostRelayReviewerGroup(requests, (request) => prepareReviewHostRelaySlot(request, runReviewer));
|
|
420
|
+
await waitFor(() => started === REVIEWER_GROUP_LENSES.length, "every reviewer must start before any completes");
|
|
421
|
+
for (const release of releases) release();
|
|
422
|
+
const prepared = await group;
|
|
443
423
|
|
|
444
424
|
assert.equal(prepared.length, REVIEWER_GROUP_LENSES.length);
|
|
445
|
-
assert.ok(prepared.every((result) => result.resultByteLength === Buffer.byteLength(
|
|
446
|
-
assert.equal(readLog(fixture.piLogPath).length, REVIEWER_GROUP_LENSES.length);
|
|
425
|
+
assert.ok(prepared.every((result) => result.resultByteLength === Buffer.byteLength(REVIEWER_TEXT)));
|
|
447
426
|
assert.equal(readLog(fixture.logPath).length, REVIEWER_GROUP_LENSES.length, "preparation materializes only; it does not submit");
|
|
448
427
|
});
|
|
449
428
|
|
|
@@ -511,106 +490,133 @@ test("partial reviewer failures wait for a later pending transport and report th
|
|
|
511
490
|
// Fail-closed legs — a typed transport error and NO submission.
|
|
512
491
|
// ---------------------------------------------------------------------------
|
|
513
492
|
|
|
514
|
-
test("materialize nonzero exit fails closed with a typed error and never
|
|
493
|
+
test("materialize nonzero exit fails closed with a typed error and never runs the completion or submits", async (t) => {
|
|
515
494
|
const fixture = harness(t, { RELAY_FAKE_MATERIALIZE_MODE: "fail" });
|
|
516
|
-
const
|
|
495
|
+
const { runReviewer, calls } = textReviewer(REVIEWER_TEXT);
|
|
496
|
+
const error = await rejectsWithRelayError(runRelay(fixture, {}, runReviewer), REVIEW_HOST_RELAY_FAILURE.MATERIALIZE_FAILED, "materialize");
|
|
517
497
|
assert.equal(error.exitCode, 3);
|
|
518
498
|
assert.equal(error.mutationOutcome, "none");
|
|
519
499
|
assert.equal(readLog(fixture.logPath).length, 1);
|
|
520
|
-
assert.equal(
|
|
500
|
+
assert.equal(calls.length, 0, "no completion may run after a materialize failure");
|
|
521
501
|
assert.equal(existsSync(fixture.submitCapturePath), false);
|
|
522
502
|
});
|
|
523
503
|
|
|
524
|
-
test("an empty materialized prompt fails closed before
|
|
504
|
+
test("an empty materialized prompt fails closed before the completion runs", async (t) => {
|
|
525
505
|
const fixture = harness(t, { RELAY_FAKE_MATERIALIZE_MODE: "empty" });
|
|
526
|
-
|
|
527
|
-
|
|
506
|
+
const { runReviewer, calls } = textReviewer(REVIEWER_TEXT);
|
|
507
|
+
await rejectsWithRelayError(runRelay(fixture, {}, runReviewer), REVIEW_HOST_RELAY_FAILURE.EMPTY_PROMPT, "materialize");
|
|
508
|
+
assert.equal(calls.length, 0);
|
|
528
509
|
assert.equal(existsSync(fixture.submitCapturePath), false);
|
|
529
510
|
});
|
|
530
511
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
// Every in-process reviewer refusal code maps to its typed relay code.
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
|
|
516
|
+
test("every in-process reviewer refusal maps to its typed relay failure code, carrying evidence when the outcome had any", async (t) => {
|
|
517
|
+
const cases: Array<{ code: (typeof INPROCESS_REVIEWER_FAILURE)[keyof typeof INPROCESS_REVIEWER_FAILURE]; kind: string; evidence?: Record<string, unknown> }> = [
|
|
518
|
+
{ code: INPROCESS_REVIEWER_FAILURE.SELECTION_INVALID, kind: REVIEW_HOST_RELAY_FAILURE.REVIEWER_CONFIG_INVALID },
|
|
519
|
+
{ code: INPROCESS_REVIEWER_FAILURE.MODEL_NOT_FOUND, kind: REVIEW_HOST_RELAY_FAILURE.REVIEWER_MODEL_NOT_FOUND },
|
|
520
|
+
{ code: INPROCESS_REVIEWER_FAILURE.AUTH_UNAVAILABLE, kind: REVIEW_HOST_RELAY_FAILURE.REVIEWER_AUTH_UNAVAILABLE },
|
|
521
|
+
{ code: INPROCESS_REVIEWER_FAILURE.THINKING_INVALID, kind: REVIEW_HOST_RELAY_FAILURE.REVIEWER_THINKING_INVALID },
|
|
522
|
+
{ code: INPROCESS_REVIEWER_FAILURE.TOOL_CALL_ATTEMPTED, kind: REVIEW_HOST_RELAY_FAILURE.REVIEWER_TOOL_CALL },
|
|
523
|
+
{ code: INPROCESS_REVIEWER_FAILURE.EMPTY_OUTPUT, kind: REVIEW_HOST_RELAY_FAILURE.REVIEWER_EMPTY_OUTPUT, evidence: { stopReason: "stop" } },
|
|
524
|
+
{ code: INPROCESS_REVIEWER_FAILURE.OUTPUT_TOO_LARGE, kind: REVIEW_HOST_RELAY_FAILURE.REVIEWER_OUTPUT_TOO_LARGE },
|
|
525
|
+
{ code: INPROCESS_REVIEWER_FAILURE.PROVIDER_FAILED, kind: REVIEW_HOST_RELAY_FAILURE.PI_FAILED },
|
|
526
|
+
];
|
|
527
|
+
for (const { code, kind, evidence } of cases) {
|
|
528
|
+
const fixture = harness(t);
|
|
529
|
+
const message = `refused: ${code}`;
|
|
530
|
+
const error = await rejectsWithRelayError(runRelay(fixture, {}, refusingReviewer(code, message, evidence)), kind, "pi");
|
|
531
|
+
assert.equal(error.message, message);
|
|
532
|
+
assert.equal(error.mutationOutcome, "none");
|
|
533
|
+
assert.equal(existsSync(fixture.submitCapturePath), false, `${code} must never reach submission`);
|
|
534
|
+
if (evidence !== undefined) assert.deepEqual(error.reviewerEvidence, evidence, `${code} must carry its outcome evidence`);
|
|
535
|
+
}
|
|
538
536
|
});
|
|
539
537
|
|
|
540
|
-
test("
|
|
541
|
-
const fixture = harness(t
|
|
542
|
-
const error = await rejectsWithRelayError(
|
|
543
|
-
|
|
538
|
+
test("a reviewer run that only attempted a tool call fails typed and never submits", async (t) => {
|
|
539
|
+
const fixture = harness(t);
|
|
540
|
+
const error = await rejectsWithRelayError(
|
|
541
|
+
runRelay(fixture, {}, refusingReviewer(INPROCESS_REVIEWER_FAILURE.TOOL_CALL_ATTEMPTED, "Reviewer attempted a tool call for review-reliability; the in-process reviewer completion must answer in text only.")),
|
|
542
|
+
REVIEW_HOST_RELAY_FAILURE.REVIEWER_TOOL_CALL,
|
|
543
|
+
"pi",
|
|
544
|
+
);
|
|
544
545
|
assert.equal(existsSync(fixture.submitCapturePath), false);
|
|
545
|
-
|
|
546
|
-
// bare kind code.
|
|
547
|
-
assert.match(error.message, /no assistant text|no output|not a pi event stream/);
|
|
546
|
+
assert.match(error.message, /tool call/);
|
|
548
547
|
});
|
|
549
548
|
|
|
550
|
-
test("
|
|
549
|
+
test("the relay forwards the caller-owned selection and thinking level to the completion request", async (t) => {
|
|
551
550
|
const fixture = harness(t);
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
assistantEnd("", "nan/deepseek-v4-flash"),
|
|
559
|
-
).toString("base64"),
|
|
560
|
-
},
|
|
561
|
-
})), REVIEW_HOST_RELAY_FAILURE.PI_EMPTY_OUTPUT, "pi");
|
|
562
|
-
assert.equal(existsSync(fixture.submitCapturePath), false);
|
|
563
|
-
assert.match(error.message, /no assistant text/);
|
|
564
|
-
assert.ok((error as unknown as { reviewerEvidence?: { stdoutKind?: string } }).reviewerEvidence !== undefined, "the failure report must carry the reviewer evidence");
|
|
551
|
+
const { runReviewer, calls } = textReviewer(REVIEWER_TEXT);
|
|
552
|
+
await runRelay(fixture, { selection: "minimax/MiniMax-M3", thinking: "high", routingKey: "review-risk" }, runReviewer);
|
|
553
|
+
assert.equal(calls.length, 1);
|
|
554
|
+
assert.equal(calls[0]!.selection, "minimax/MiniMax-M3");
|
|
555
|
+
assert.equal(calls[0]!.thinking, "high");
|
|
556
|
+
assert.equal(calls[0]!.routingKey, "review-risk");
|
|
565
557
|
});
|
|
566
558
|
|
|
567
|
-
|
|
559
|
+
// The in-process reviewer completion is an extension side-call that bypasses
|
|
560
|
+
// pi's main agent loop, so the relay must forward the caller's live session id
|
|
561
|
+
// into the completion request; the completion itself turns it into the
|
|
562
|
+
// OpenCode attribution headers (lib/inprocess-reviewer.ts). A absent session
|
|
563
|
+
// id forwards nothing and is never an error.
|
|
564
|
+
test("the relay forwards the caller's reviewer session id into the completion request", async (t) => {
|
|
568
565
|
const fixture = harness(t);
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
await runReviewHostRelaySlot(relayRequest(fixture, {
|
|
572
|
-
reviewerModel: "minimax/MiniMax-M3",
|
|
573
|
-
reviewerExtensionPaths: [adapterPath],
|
|
574
|
-
}));
|
|
575
|
-
const calls = readLog(fixture.piLogPath);
|
|
566
|
+
const { runReviewer, calls } = textReviewer(REVIEWER_TEXT);
|
|
567
|
+
await runRelay(fixture, { reviewerSessionId: "ses-live-1" }, runReviewer);
|
|
576
568
|
assert.equal(calls.length, 1);
|
|
577
|
-
assert.
|
|
569
|
+
assert.equal(calls[0]!.sessionId, "ses-live-1");
|
|
578
570
|
});
|
|
579
571
|
|
|
580
|
-
test("a
|
|
572
|
+
test("a relay request with no reviewer session id adds no session id to the completion request", async (t) => {
|
|
581
573
|
const fixture = harness(t);
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
{ reviewerExtensionPaths: [join(fixture.directory, "missing-adapter.ts")] },
|
|
587
|
-
]) {
|
|
588
|
-
let caught: unknown;
|
|
589
|
-
try {
|
|
590
|
-
await runReviewHostRelaySlot(relayRequest(fixture, broken));
|
|
591
|
-
} catch (error) {
|
|
592
|
-
caught = error;
|
|
593
|
-
}
|
|
594
|
-
assert.ok(caught instanceof ReviewHostRelayError, `expected a typed relay error for ${JSON.stringify(broken)}`);
|
|
595
|
-
const error = caught as ReviewHostRelayError;
|
|
596
|
-
assert.equal(error.kind, "reviewer-config-invalid");
|
|
597
|
-
assert.equal(error.stage, "pi");
|
|
598
|
-
assert.equal(readLog(fixture.piLogPath).length, 0, "no reviewer may launch on a broken selection");
|
|
599
|
-
assert.equal(readLog(fixture.logPath).length, 0, "no materialization may run on a broken selection");
|
|
600
|
-
}
|
|
574
|
+
const { runReviewer, calls } = textReviewer(REVIEWER_TEXT);
|
|
575
|
+
await runRelay(fixture, {}, runReviewer);
|
|
576
|
+
assert.equal(calls.length, 1);
|
|
577
|
+
assert.equal(calls[0]!.sessionId, undefined);
|
|
601
578
|
});
|
|
602
579
|
|
|
603
|
-
test("
|
|
604
|
-
const fixture = harness(t
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
assert.equal(
|
|
580
|
+
test("a missing model registry is refused typed before materialize ever runs", async (t) => {
|
|
581
|
+
const fixture = harness(t);
|
|
582
|
+
const { runReviewer, calls } = textReviewer(REVIEWER_TEXT);
|
|
583
|
+
const error = await rejectsWithRelayError(runRelay(fixture, { reviewerRegistry: undefined }, runReviewer), REVIEW_HOST_RELAY_FAILURE.REVIEWER_CONFIG_INVALID, "pi");
|
|
584
|
+
assert.equal(calls.length, 0, "no completion may run on a broken configuration");
|
|
585
|
+
assert.equal(readLog(fixture.logPath).length, 0, "no materialization may run without a model registry");
|
|
586
|
+
assert.match(error.message, /model registry/);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test("a routing entry with no configured model is refused typed, naming the routing key, before materialize ever runs", async (t) => {
|
|
590
|
+
const fixture = harness(t);
|
|
591
|
+
const { runReviewer, calls } = textReviewer(REVIEWER_TEXT);
|
|
592
|
+
const error = await rejectsWithRelayError(runRelay(fixture, { selection: undefined, routingKey: "review-risk" }, runReviewer), REVIEW_HOST_RELAY_FAILURE.REVIEWER_CONFIG_INVALID, "pi");
|
|
593
|
+
assert.equal(calls.length, 0);
|
|
594
|
+
assert.equal(readLog(fixture.logPath).length, 0, "no materialization may run without a configured model");
|
|
595
|
+
assert.match(error.message, /review-risk/);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("a tool call's cancelled signal maps to REVIEWER_ABORTED and forwards the caller's own signal to the completion", async (t) => {
|
|
599
|
+
const fixture = harness(t);
|
|
600
|
+
const controller = new AbortController();
|
|
601
|
+
let receivedSignal: AbortSignal | undefined;
|
|
602
|
+
// The signal aborts once the completion actually starts (materialize must
|
|
603
|
+
// already have succeeded with a live signal); this proves the same signal
|
|
604
|
+
// object reaches the completion request, not just that an early abort
|
|
605
|
+
// short-circuits materialize too.
|
|
606
|
+
const runReviewer = (async (request: InProcessReviewerRequest) => {
|
|
607
|
+
receivedSignal = request.signal;
|
|
608
|
+
controller.abort();
|
|
609
|
+
return { kind: "refused", code: INPROCESS_REVIEWER_FAILURE.ABORTED, message: "Reviewer completion for review-reliability was aborted by the caller." } as InProcessReviewerOutcome;
|
|
610
|
+
}) as typeof runInProcessReviewer;
|
|
611
|
+
const error = await rejectsWithRelayError(runRelay(fixture, { signal: controller.signal }, runReviewer), REVIEW_HOST_RELAY_FAILURE.REVIEWER_ABORTED, "pi");
|
|
612
|
+
assert.equal(receivedSignal, controller.signal, "the relay must forward the tool call's own signal into the completion request");
|
|
613
|
+
assert.match(error.message, /aborted/);
|
|
608
614
|
assert.equal(existsSync(fixture.submitCapturePath), false);
|
|
609
615
|
});
|
|
610
616
|
|
|
611
617
|
// ---------------------------------------------------------------------------
|
|
612
618
|
// gentle-pi#367 — the reviewer bound is reachable from production, and a
|
|
613
|
-
//
|
|
619
|
+
// timed-out completion says so with both measurements.
|
|
614
620
|
// ---------------------------------------------------------------------------
|
|
615
621
|
|
|
616
622
|
test("the reviewer bound scales with materialized prompt bytes instead of one fixed number", () => {
|
|
@@ -644,50 +650,26 @@ test("the reviewer bound honours the environment override and ignores malformed
|
|
|
644
650
|
}
|
|
645
651
|
});
|
|
646
652
|
|
|
647
|
-
test("the production relay path resolves the reviewer bound from the environment, with no injected timeout", async (t) => {
|
|
648
|
-
// The regression: piTimeoutMs was reachable only through the
|
|
649
|
-
// production always ran against
|
|
650
|
-
// no timeout at all — the override must reach the
|
|
651
|
-
const fixture = harness(t, {
|
|
653
|
+
test("the production relay path resolves the reviewer bound from the environment and forwards it to the completion, with no injected timeout", async (t) => {
|
|
654
|
+
// The regression this guards: piTimeoutMs was reachable only through the
|
|
655
|
+
// test seam, so production always ran against a fixed bound. This request
|
|
656
|
+
// injects no timeout at all — the override must reach the completion.
|
|
657
|
+
const fixture = harness(t, { [REVIEW_HOST_RELAY_PI_TIMEOUT_ENV]: "300000" });
|
|
658
|
+
const { runReviewer, calls } = textReviewer(REVIEWER_TEXT);
|
|
659
|
+
await runRelay(fixture, { piTimeoutMs: undefined }, runReviewer);
|
|
660
|
+
assert.equal(calls[0]!.timeoutMs, 300_000);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
test("a timed-out reviewer completion reports elapsed, limit, and what to change instead of an opaque transport failure", async (t) => {
|
|
664
|
+
const fixture = harness(t);
|
|
652
665
|
const error = await rejectsWithRelayError(
|
|
653
|
-
|
|
666
|
+
runRelay(fixture, { piTimeoutMs: 300 }, refusingReviewer(INPROCESS_REVIEWER_FAILURE.TIMED_OUT, "Reviewer completion for review-reliability exceeded its 300ms bound.", undefined, 20)),
|
|
654
667
|
REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT,
|
|
655
668
|
"pi",
|
|
656
669
|
);
|
|
657
670
|
assert.equal(error.timeoutMs, 300);
|
|
671
|
+
assert.ok(error.elapsedMs !== null && error.elapsedMs >= 15, `elapsed ${error.elapsedMs} must record the real wall time`);
|
|
658
672
|
assert.equal(error.timedOut, true);
|
|
659
|
-
assert.equal(readLog(fixture.logPath).length, 1, "no submission after a reviewer timeout");
|
|
660
|
-
assert.equal(existsSync(fixture.submitCapturePath), false);
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
test("a relay Pi timeout keeps its typed mapping and timing evidence when opaque scratch cleanup also fails", async (t) => {
|
|
664
|
-
const fixture = harness(t, { RELAY_FAKE_PI_MODE: "hang", RELAY_FAKE_PI_BREAK_CLEANUP: "true" });
|
|
665
|
-
const scratchParent = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-relay-pi-primary-failure-")));
|
|
666
|
-
const originalTmpdir = process.env.TMPDIR;
|
|
667
|
-
process.env.TMPDIR = scratchParent;
|
|
668
|
-
try {
|
|
669
|
-
const error = await rejectsWithRelayError(
|
|
670
|
-
runReviewHostRelaySlot(relayRequest(fixture, { piTimeoutMs: 300 })),
|
|
671
|
-
REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT,
|
|
672
|
-
"pi",
|
|
673
|
-
);
|
|
674
|
-
assert.equal(error.timedOut, true);
|
|
675
|
-
assert.equal(error.timeoutMs, 300);
|
|
676
|
-
assert.ok(error.elapsedMs !== null && error.elapsedMs >= 250);
|
|
677
|
-
} finally {
|
|
678
|
-
if (originalTmpdir === undefined) delete process.env.TMPDIR;
|
|
679
|
-
else process.env.TMPDIR = originalTmpdir;
|
|
680
|
-
chmodSync(scratchParent, 0o700);
|
|
681
|
-
rmSync(scratchParent, { recursive: true, force: true });
|
|
682
|
-
}
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
test("a killed reviewer reports elapsed, limit, and what to change instead of an opaque transport failure", async (t) => {
|
|
686
|
-
const fixture = harness(t, { RELAY_FAKE_PI_MODE: "hang" });
|
|
687
|
-
const error = await rejectsWithRelayError(runReviewHostRelaySlot(relayRequest(fixture, { piTimeoutMs: 300 })), REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi");
|
|
688
|
-
assert.equal(error.timeoutMs, 300);
|
|
689
|
-
assert.ok(error.elapsedMs !== null && error.elapsedMs >= 250, `elapsed ${error.elapsedMs} must record the real wall time`);
|
|
690
|
-
assert.ok(error.elapsedMs! < 10_000, "the reviewer was killed at the bound, not left to finish");
|
|
691
673
|
assert.match(error.message, /exceeded the relay bound/);
|
|
692
674
|
assert.match(error.message, new RegExp(String(error.elapsedMs)));
|
|
693
675
|
assert.match(error.message, /limit for a \d+-byte materialized prompt/);
|
|
@@ -695,18 +677,21 @@ test("a killed reviewer reports elapsed, limit, and what to change instead of an
|
|
|
695
677
|
assert.equal(error.mutationOutcome, "none");
|
|
696
678
|
});
|
|
697
679
|
|
|
698
|
-
test("a
|
|
699
|
-
const fixture = harness(t
|
|
700
|
-
const error = await rejectsWithRelayError(
|
|
680
|
+
test("a provider-failed completion stays distinguishable from a timed-out one and still carries its measurements", async (t) => {
|
|
681
|
+
const fixture = harness(t);
|
|
682
|
+
const error = await rejectsWithRelayError(
|
|
683
|
+
runRelay(fixture, { piTimeoutMs: 30_000 }, refusingReviewer(INPROCESS_REVIEWER_FAILURE.PROVIDER_FAILED, "Reviewer completion failed for review-reliability: upstream 500")),
|
|
684
|
+
REVIEW_HOST_RELAY_FAILURE.PI_FAILED,
|
|
685
|
+
"pi",
|
|
686
|
+
);
|
|
701
687
|
assert.equal(error.timedOut, false);
|
|
702
|
-
assert.equal(error.exitCode, 4);
|
|
703
688
|
assert.equal(error.timeoutMs, 30_000);
|
|
704
689
|
assert.ok(error.elapsedMs !== null && error.elapsedMs >= 0);
|
|
705
690
|
});
|
|
706
691
|
|
|
707
692
|
test("submission refusal is a typed error whose outcome is unknown pending STATUS", async (t) => {
|
|
708
693
|
const fixture = harness(t, { RELAY_FAKE_SUBMIT_MODE: "refuse" });
|
|
709
|
-
const error = await rejectsWithRelayError(
|
|
694
|
+
const error = await rejectsWithRelayError(runRelay(fixture), REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit");
|
|
710
695
|
assert.equal(error.mutationOutcome, "unknown");
|
|
711
696
|
assert.match(error.stderr, /capture binding does not match/);
|
|
712
697
|
assert.equal(readLog(fixture.logPath).length, 2);
|
|
@@ -716,21 +701,14 @@ test("submission refusal is a typed error whose outcome is unknown pending STATU
|
|
|
716
701
|
// typed [invalid_request] refusal and states that the lens slot was not
|
|
717
702
|
// consumed. That is a proven non-mutation, not an unknown outcome, and the
|
|
718
703
|
// refusal text is the only thing that tells the host what to change.
|
|
719
|
-
function admittingRequest(fixture: RelayHarness
|
|
720
|
-
return relayRequest(fixture
|
|
721
|
-
environment: {
|
|
722
|
-
...fixture.environment,
|
|
723
|
-
RELAY_FAKE_PROMPT_B64: PROMPT_BYTES.toString("base64"),
|
|
724
|
-
// The reviewer's payload arrives as the event stream's assistant text.
|
|
725
|
-
RELAY_FAKE_PI_OUTPUT_B64: piEventStream(assistantEnd(reviewerOutput.toString("utf8"))).toString("base64"),
|
|
726
|
-
},
|
|
727
|
-
});
|
|
704
|
+
function admittingRequest(fixture: RelayHarness): ReviewHostRelayRequest {
|
|
705
|
+
return relayRequest(fixture);
|
|
728
706
|
}
|
|
729
707
|
|
|
730
708
|
test("a reviewer printing garbage is refused at admission as a proven non-mutation carrying Go's refusal", async (t) => {
|
|
731
709
|
const fixture = harness(t, { RELAY_FAKE_SUBMIT_MODE: "admit", RELAY_FAKE_EXPECTED_SUBJECT: `sha256:${"a".repeat(64)}` });
|
|
732
710
|
const error = await rejectsWithRelayError(
|
|
733
|
-
runReviewHostRelaySlot(admittingRequest(fixture,
|
|
711
|
+
runReviewHostRelaySlot(admittingRequest(fixture), textReviewer("not json at all").runReviewer),
|
|
734
712
|
REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED,
|
|
735
713
|
"submit",
|
|
736
714
|
);
|
|
@@ -745,9 +723,9 @@ test("a reviewer printing garbage is refused at admission as a proven non-mutati
|
|
|
745
723
|
|
|
746
724
|
test("a reviewer echoing a different subject is refused at admission as a proven non-mutation carrying Go's continuation", async (t) => {
|
|
747
725
|
const fixture = harness(t, { RELAY_FAKE_SUBMIT_MODE: "admit", RELAY_FAKE_EXPECTED_SUBJECT: `sha256:${"a".repeat(64)}` });
|
|
748
|
-
const wrongSubject =
|
|
726
|
+
const wrongSubject = JSON.stringify({ subject_hash: `sha256:${"0".repeat(64)}`, inspection: { status: "completed", paths: [] }, findings: [], evidence: ["x"] });
|
|
749
727
|
const error = await rejectsWithRelayError(
|
|
750
|
-
runReviewHostRelaySlot(admittingRequest(fixture, wrongSubject)),
|
|
728
|
+
runReviewHostRelaySlot(admittingRequest(fixture), textReviewer(wrongSubject).runReviewer),
|
|
751
729
|
REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED,
|
|
752
730
|
"submit",
|
|
753
731
|
);
|
|
@@ -762,7 +740,8 @@ test("a reviewer echoing a different subject is refused at admission as a proven
|
|
|
762
740
|
|
|
763
741
|
test("a submission the fake admits with the expected subject still completes", async (t) => {
|
|
764
742
|
const fixture = harness(t, { RELAY_FAKE_SUBMIT_MODE: "admit", RELAY_FAKE_EXPECTED_SUBJECT: `sha256:${"a".repeat(64)}` });
|
|
765
|
-
const
|
|
743
|
+
const expected = JSON.stringify({ subject_hash: `sha256:${"a".repeat(64)}`, inspection: { status: "completed", paths: [] }, findings: [], evidence: ["x"] });
|
|
744
|
+
const result = await runReviewHostRelaySlot(admittingRequest(fixture), textReviewer(expected).runReviewer);
|
|
766
745
|
assert.equal(JSON.parse(result.submission).admission_decision, "completed");
|
|
767
746
|
});
|
|
768
747
|
|
|
@@ -773,7 +752,7 @@ test("submission refusal preserves its primary evidence when result staging clea
|
|
|
773
752
|
process.env.TMPDIR = scratchParent;
|
|
774
753
|
try {
|
|
775
754
|
const error = await rejectsWithRelayError(
|
|
776
|
-
|
|
755
|
+
runRelay(fixture),
|
|
777
756
|
REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED,
|
|
778
757
|
"submit",
|
|
779
758
|
);
|
|
@@ -791,12 +770,10 @@ test("submission refusal preserves its primary evidence when result staging clea
|
|
|
791
770
|
}
|
|
792
771
|
});
|
|
793
772
|
|
|
794
|
-
test("relay
|
|
795
|
-
const fixture = harness(t, {
|
|
796
|
-
await rejectsWithRelayError(
|
|
797
|
-
|
|
798
|
-
assert.equal(piCalls.length, 1);
|
|
799
|
-
assert.equal(existsSync(piCalls[0]!.cwd!), false);
|
|
773
|
+
test("relay staging directories are removed after a submission failure too", async (t) => {
|
|
774
|
+
const fixture = harness(t, { RELAY_FAKE_SUBMIT_MODE: "refuse" });
|
|
775
|
+
await rejectsWithRelayError(runRelay(fixture), REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit");
|
|
776
|
+
assert.equal(readLog(fixture.logPath).length, 2);
|
|
800
777
|
});
|
|
801
778
|
|
|
802
779
|
test("a result staging cleanup failure remains a typed submit failure", async (t) => {
|
|
@@ -806,7 +783,7 @@ test("a result staging cleanup failure remains a typed submit failure", async (t
|
|
|
806
783
|
process.env.TMPDIR = scratchParent;
|
|
807
784
|
try {
|
|
808
785
|
const error = await rejectsWithRelayError(
|
|
809
|
-
|
|
786
|
+
runRelay(fixture),
|
|
810
787
|
REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED,
|
|
811
788
|
"submit",
|
|
812
789
|
);
|
|
@@ -826,20 +803,22 @@ test("a result staging cleanup failure remains a typed submit failure", async (t
|
|
|
826
803
|
|
|
827
804
|
test("an old binary's unknown-flag refusal classifies as relay-unavailable with the exact report", async (t) => {
|
|
828
805
|
const fixture = harness(t, { RELAY_FAKE_MATERIALIZE_MODE: "unknown-flag" });
|
|
829
|
-
const
|
|
806
|
+
const { runReviewer, calls } = textReviewer(REVIEWER_TEXT);
|
|
807
|
+
const error = await rejectsWithRelayError(runRelay(fixture, {}, runReviewer), REVIEW_HOST_RELAY_FAILURE.RELAY_UNAVAILABLE, "materialize");
|
|
830
808
|
assert.equal(error.message, REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE);
|
|
831
809
|
assert.match(error.stderr, /flag provided but not defined: -materialize/);
|
|
832
|
-
assert.equal(
|
|
810
|
+
assert.equal(calls.length, 0);
|
|
833
811
|
assert.equal(existsSync(fixture.submitCapturePath), false);
|
|
834
812
|
});
|
|
835
813
|
|
|
836
814
|
test("a handshake refusal surfaces the provider refusal verbatim", async (t) => {
|
|
837
815
|
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";
|
|
838
816
|
const fixture = harness(t, { RELAY_FAKE_MATERIALIZE_MODE: "handshake", RELAY_FAKE_HANDSHAKE_STDERR: refusal });
|
|
839
|
-
const
|
|
817
|
+
const { runReviewer, calls } = textReviewer(REVIEWER_TEXT);
|
|
818
|
+
const error = await rejectsWithRelayError(runRelay(fixture, {}, runReviewer), REVIEW_HOST_RELAY_FAILURE.HANDSHAKE_REFUSED, "materialize");
|
|
840
819
|
assert.equal(error.message, refusal);
|
|
841
820
|
assert.equal(error.stderr, refusal);
|
|
842
|
-
assert.equal(
|
|
821
|
+
assert.equal(calls.length, 0);
|
|
843
822
|
});
|
|
844
823
|
|
|
845
824
|
test("refusal classification distinguishes unknown-flag, handshake, and other", () => {
|
|
@@ -854,7 +833,7 @@ test("refusal classification distinguishes unknown-flag, handshake, and other",
|
|
|
854
833
|
// deterministic for this exact slot. Generic admission refusals are repairable
|
|
855
834
|
// by a fresh reviewer and retain the ordinary exact-reoffer behavior.
|
|
856
835
|
test("the unachievable predicate names only deterministic slot failures", () => {
|
|
857
|
-
const killed = new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "
|
|
836
|
+
const killed = new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "the reviewer completion exceeded the relay bound", { timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
|
|
858
837
|
assert.equal(reviewHostRelayUnachievableReason(killed), "relay_transport_bound_exceeded");
|
|
859
838
|
assert.equal(reviewHostRelayUnachievableDetail(killed), "killed after 2256004ms against a 2256000ms relay bound");
|
|
860
839
|
|
|
@@ -868,7 +847,24 @@ test("the unachievable predicate names only deterministic slot failures", () =>
|
|
|
868
847
|
assert.equal(reviewHostRelayUnachievableReason(unknownOutcome), undefined, "an unknown mutation outcome is never deterministic");
|
|
869
848
|
assert.equal(reviewHostRelayUnachievableDetail(unknownOutcome), undefined);
|
|
870
849
|
|
|
871
|
-
for (const kind of [
|
|
850
|
+
for (const kind of [
|
|
851
|
+
REVIEW_HOST_RELAY_FAILURE.PI_FAILED,
|
|
852
|
+
REVIEW_HOST_RELAY_FAILURE.PI_LAUNCH_FAILED,
|
|
853
|
+
REVIEW_HOST_RELAY_FAILURE.PI_EMPTY_OUTPUT,
|
|
854
|
+
REVIEW_HOST_RELAY_FAILURE.REVIEWER_MODEL_NOT_FOUND,
|
|
855
|
+
REVIEW_HOST_RELAY_FAILURE.REVIEWER_AUTH_UNAVAILABLE,
|
|
856
|
+
REVIEW_HOST_RELAY_FAILURE.REVIEWER_THINKING_INVALID,
|
|
857
|
+
REVIEW_HOST_RELAY_FAILURE.REVIEWER_TOOL_CALL,
|
|
858
|
+
REVIEW_HOST_RELAY_FAILURE.REVIEWER_EMPTY_OUTPUT,
|
|
859
|
+
REVIEW_HOST_RELAY_FAILURE.REVIEWER_OUTPUT_TOO_LARGE,
|
|
860
|
+
REVIEW_HOST_RELAY_FAILURE.REVIEWER_ABORTED,
|
|
861
|
+
REVIEW_HOST_RELAY_FAILURE.REVIEWER_CONFIG_INVALID,
|
|
862
|
+
REVIEW_HOST_RELAY_FAILURE.MATERIALIZE_FAILED,
|
|
863
|
+
REVIEW_HOST_RELAY_FAILURE.EMPTY_PROMPT,
|
|
864
|
+
REVIEW_HOST_RELAY_FAILURE.RELAY_UNAVAILABLE,
|
|
865
|
+
REVIEW_HOST_RELAY_FAILURE.HANDSHAKE_REFUSED,
|
|
866
|
+
REVIEW_HOST_RELAY_FAILURE.SUBMISSION_CONTRACT_MISMATCH,
|
|
867
|
+
]) {
|
|
872
868
|
assert.equal(reviewHostRelayUnachievableReason(new ReviewHostRelayError(kind, "pi", "transient")), undefined, `${kind} stays transient`);
|
|
873
869
|
}
|
|
874
870
|
|
|
@@ -942,37 +938,39 @@ test("relay input validation rejects empty or malformed inputs before any proces
|
|
|
942
938
|
|
|
943
939
|
test("a materialize slot without a provider submission fails closed before any process launches", async (t) => {
|
|
944
940
|
const fixture = harness(t);
|
|
941
|
+
const { runReviewer, calls } = textReviewer(REVIEWER_TEXT);
|
|
945
942
|
const error = await rejectsWithRelayError(
|
|
946
|
-
|
|
943
|
+
runRelay(fixture, { submission: undefined }, runReviewer),
|
|
947
944
|
REVIEW_HOST_RELAY_FAILURE.SUBMISSION_CONTRACT_MISMATCH,
|
|
948
945
|
"binding",
|
|
949
946
|
);
|
|
950
947
|
assert.equal(error.message, REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE);
|
|
951
948
|
assert.equal(error.mutationOutcome, "none");
|
|
952
949
|
assert.equal(readLog(fixture.logPath).length, 0, "no gentle-ai invocation was launched");
|
|
953
|
-
assert.equal(
|
|
950
|
+
assert.equal(calls.length, 0, "no completion was run");
|
|
954
951
|
assert.equal(existsSync(fixture.submitCapturePath), false);
|
|
955
952
|
});
|
|
956
953
|
|
|
957
954
|
test("a submission form the relay cannot bind is a typed contract mismatch, never a repaired invocation", async (t) => {
|
|
958
955
|
const fixture = harness(t);
|
|
956
|
+
const { runReviewer, calls } = textReviewer(REVIEWER_TEXT);
|
|
959
957
|
const twoValues: ReviewCaptureSubmissionV1 = {
|
|
960
958
|
...SUBMISSION,
|
|
961
959
|
values: [...SUBMISSION.values, { slot: "extra", domain: "artifact_path_or_stdin", substitutionLocation: 0 }],
|
|
962
960
|
};
|
|
963
|
-
await rejectsWithRelayError(
|
|
961
|
+
await rejectsWithRelayError(runRelay(fixture, { submission: twoValues }, runReviewer), REVIEW_HOST_RELAY_FAILURE.SUBMISSION_CONTRACT_MISMATCH, "binding");
|
|
964
962
|
const noSlot: ReviewCaptureSubmissionV1 = {
|
|
965
963
|
...SUBMISSION,
|
|
966
964
|
argumentTokens: [...BINDING_TOKENS, "--input=/etc/somewhere"],
|
|
967
965
|
};
|
|
968
|
-
await rejectsWithRelayError(
|
|
966
|
+
await rejectsWithRelayError(runRelay(fixture, { submission: noSlot }, runReviewer), REVIEW_HOST_RELAY_FAILURE.SUBMISSION_CONTRACT_MISMATCH, "binding");
|
|
969
967
|
const outOfBounds: ReviewCaptureSubmissionV1 = {
|
|
970
968
|
...SUBMISSION,
|
|
971
969
|
values: [{ slot: "reviewer_result", domain: "artifact_path_or_stdin", substitutionLocation: SUBMISSION.argumentTokens.length }],
|
|
972
970
|
};
|
|
973
|
-
await rejectsWithRelayError(
|
|
971
|
+
await rejectsWithRelayError(runRelay(fixture, { submission: outOfBounds }, runReviewer), REVIEW_HOST_RELAY_FAILURE.SUBMISSION_CONTRACT_MISMATCH, "binding");
|
|
974
972
|
assert.equal(readLog(fixture.logPath).length, 0);
|
|
975
|
-
assert.equal(
|
|
973
|
+
assert.equal(calls.length, 0);
|
|
976
974
|
});
|
|
977
975
|
|
|
978
976
|
test("resolveReviewHostRelaySubmission returns the provider binding untouched", () => {
|