gentle-pi 2.2.0 → 2.3.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 +51 -80
- package/assets/agents/review-readability.md +0 -2
- package/assets/agents/review-reliability.md +0 -2
- package/assets/agents/review-resilience.md +0 -2
- package/assets/agents/review-risk.md +0 -2
- package/assets/agents/sdd-apply.md +5 -3
- package/assets/agents/sdd-proposal.md +2 -0
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-status.md +4 -4
- package/assets/agents/sdd-tasks.md +2 -3
- package/assets/agents/sdd-verify.md +21 -1
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +54 -248
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +22 -38
- package/assets/sdd-orchestrator-workflow.md +30 -37
- package/assets/support/sdd-status-contract.md +7 -7
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/docs/native-authority-architecture.md +9 -11
- package/docs/review-integration.md +27 -373
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +1778 -3673
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +4 -8
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +372 -940
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/review-candidate-view.ts +341 -132
- package/lib/review-host-relay.ts +210 -68
- package/lib/review-integration-v2.ts +839 -307
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/sdd-preflight.ts +177 -66
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -82
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +322 -890
- package/runtime/review-integration-v2.mjs +789 -257
- package/runtime/review-relay-contract.mjs +1 -1
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
- package/scripts/gentle-ai-installer.mjs +75 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
- package/scripts/test-packed-runner.mjs +4 -7
- package/scripts/verify-package-files.mjs +12 -13
- package/skills/_shared/review-ledger-contract.md +8 -14
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/judgment-day/SKILL.md +5 -7
- package/skills/rdd-defect-workflow/SKILL.md +3 -3
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +15 -1168
- package/tests/delegated-key-learnings-contract.test.ts +8 -6
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary.test.ts +51 -228
- package/tests/gentle-ai-installer.test.ts +137 -47
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +340 -4
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +28 -1
- package/tests/native-review-cli.test.ts +586 -962
- package/tests/native-review-consent.test.ts +150 -66
- package/tests/native-review-parity-runtime.test.ts +80 -359
- package/tests/native-review-parity.test.ts +674 -925
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +132 -88
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +37 -35
- package/tests/provider-defect-handoff.test.ts +15 -118
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +409 -3
- package/tests/review-compact-contract.test.ts +4 -30
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +363 -1450
- package/tests/review-controller-native-routing.test.ts +1251 -5642
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +222 -71
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +112 -153
- package/tests/review-dispatch-hydration-gap.test.ts +1 -53
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +117 -82
- package/tests/review-host-relay.test.ts +242 -8
- package/tests/review-integration-v2-forward.test.ts +576 -553
- package/tests/review-integration-v2.test.ts +239 -165
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +97 -35
- package/tests/review-recovered-lineage-routing.test.ts +0 -47
- package/tests/review-relay-transport-agent.test.ts +119 -46
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +361 -196
- package/tests/sdd-agent-tools.test.ts +36 -0
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +5 -8
- package/tests/writer-edit-surface-scope.test.ts +230 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/lib/git-commit-transaction.ts +0 -861
- package/runtime/git-commit-transaction.mjs +0 -862
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -530
|
@@ -1,32 +1,23 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import {
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, symlinkSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
|
+
import {
|
|
7
|
+
decodeReviewConsentV3,
|
|
8
|
+
} from "../lib/review-integration-v2.ts";
|
|
6
9
|
import {
|
|
7
10
|
NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES,
|
|
8
11
|
NATIVE_REVIEW_ERROR_CODE,
|
|
9
12
|
NativeReviewCliError,
|
|
10
|
-
NativeReviewCliV213 as NativeReviewCliV213Production,
|
|
11
13
|
NativeReviewCliV216,
|
|
12
14
|
createNodeExecFileAdapter,
|
|
13
15
|
type ExecFileAdapter,
|
|
14
|
-
type NativeStartRequest,
|
|
15
16
|
} from "../lib/native-review-cli.ts";
|
|
16
|
-
import {
|
|
17
|
-
NativeReviewCliV214 as RuntimeNativeReviewCliV214,
|
|
18
|
-
NativeReviewCliV216 as RuntimeNativeReviewCliV216,
|
|
19
|
-
} from "../runtime/native-review-cli.mjs";
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class NativeReviewCliV213 extends NativeReviewCliV213Production {
|
|
25
|
-
constructor(...parameters: ConstructorParameters<typeof NativeReviewCliV213Production>) {
|
|
26
|
-
const [adapter, executable, ...rest] = parameters;
|
|
27
|
-
super(adapter, executable ?? "/package/.gentle-ai/gentle-ai", ...rest);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
18
|
+
const fixture = (name: string): Record<string, unknown> => JSON.parse(
|
|
19
|
+
readFileSync(join(process.cwd(), "tests", "fixtures", "devbinary", name), "utf8"),
|
|
20
|
+
) as Record<string, unknown>;
|
|
30
21
|
|
|
31
22
|
interface QueuedResult {
|
|
32
23
|
stdout: string;
|
|
@@ -37,14 +28,18 @@ interface QueuedResult {
|
|
|
37
28
|
outputLimitExceeded?: boolean;
|
|
38
29
|
}
|
|
39
30
|
|
|
40
|
-
function queuedAdapter(results: QueuedResult[]): {
|
|
41
|
-
|
|
31
|
+
function queuedAdapter(results: readonly QueuedResult[]): {
|
|
32
|
+
adapter: ExecFileAdapter;
|
|
33
|
+
calls: Array<{ file: string; arguments: readonly string[]; cwd: string; timeoutMs: number | undefined }>;
|
|
34
|
+
} {
|
|
35
|
+
const queue = [...results];
|
|
36
|
+
const calls: Array<{ file: string; arguments: readonly string[]; cwd: string; timeoutMs: number | undefined }> = [];
|
|
42
37
|
return {
|
|
43
38
|
calls,
|
|
44
39
|
adapter: async (request) => {
|
|
45
|
-
calls.push(request);
|
|
46
|
-
const result =
|
|
47
|
-
if (
|
|
40
|
+
calls.push({ file: request.file, arguments: request.arguments, cwd: request.cwd, timeoutMs: request.timeoutMs });
|
|
41
|
+
const result = queue.shift();
|
|
42
|
+
if (result === undefined) throw new Error("unexpected native invocation");
|
|
48
43
|
return {
|
|
49
44
|
stdout: result.stdout,
|
|
50
45
|
stderr: result.stderr ?? "",
|
|
@@ -57,1064 +52,693 @@ function queuedAdapter(results: QueuedResult[]): { adapter: ExecFileAdapter; cal
|
|
|
57
52
|
};
|
|
58
53
|
}
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const START = { stdout: JSON.stringify({ operation: "review/start", lineage_id: "lineage-1", state: "reviewing", risk_level: "medium", selected_lenses: ["review-reliability"], changed_files: 1, changed_lines: 2, correction_budget: 1, action: "created", lenses_required: true, projection: "workspace" }) };
|
|
64
|
-
const REVIEW_STATUS = {
|
|
65
|
-
stdout: JSON.stringify({
|
|
66
|
-
schema: "gentle-ai.review-authority-status/v1",
|
|
67
|
-
operation: "review/status",
|
|
68
|
-
repository: "C:\\repo with spaces",
|
|
69
|
-
complete: true,
|
|
70
|
-
authoritative: true,
|
|
71
|
-
status: "clean",
|
|
72
|
-
entries: [],
|
|
73
|
-
locks: [],
|
|
74
|
-
diagnostics: [],
|
|
75
|
-
}),
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
test("native START supports every declared version with the START capability and requires the workspace projection", async () => {
|
|
79
|
-
const start = JSON.parse(START.stdout) as Record<string, unknown>;
|
|
80
|
-
assert.equal((await new NativeReviewCliV213(queuedAdapter([VERSION, { stdout: JSON.stringify({ ...start, projection: "workspace" }) }]).adapter).start({ cwd: "/repo" })).lineageId, "lineage-1");
|
|
81
|
-
assert.equal((await new NativeReviewCliV213(queuedAdapter([STATUS_VERSION, { stdout: JSON.stringify({ ...start, projection: "workspace" }) }]).adapter).start({ cwd: "/repo" })).lineageId, "lineage-1");
|
|
82
|
-
const missing = Object.fromEntries(Object.entries(start).filter(([key]) => key !== "projection"));
|
|
83
|
-
for (const body of [missing, { ...start, projection: "repository" }]) {
|
|
84
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify(body) }]);
|
|
85
|
-
await assert.rejects(() => new NativeReviewCliV213(queue.adapter).start({ cwd: "/repo" }), (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
55
|
+
function client(adapter: ExecFileAdapter): NativeReviewCliV216 {
|
|
56
|
+
return new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024);
|
|
57
|
+
}
|
|
88
58
|
|
|
89
|
-
test("
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
await client.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
["review", "start", "--cwd", "/repo with spaces"],
|
|
99
|
-
["version"],
|
|
100
|
-
["review", "start", "--cwd", "/repo with spaces", "--base-ref", "origin/main", "--committed-only"],
|
|
101
|
-
["version"],
|
|
102
|
-
["review", "start", "--cwd", "/repo with spaces", "--policy", "/repo with spaces/.gentle-ai/policies/team policy.json"],
|
|
103
|
-
["version"],
|
|
104
|
-
["review", "start", "--cwd", "/repo with spaces"],
|
|
59
|
+
test("negotiated STATUS accepts the pinned v5 receipt before routing its transition", async () => {
|
|
60
|
+
const status = fixture("status-v5.captured.json");
|
|
61
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify(status) }]);
|
|
62
|
+
const result = await client(queue.adapter).targetStatus({ cwd: "/repo", lineageId: "review-status-fixture", agent: "pi" });
|
|
63
|
+
assert.deepEqual(result.receipt, { status: "not_applicable" });
|
|
64
|
+
assert.equal(result.nextTransition?.kind, "collect");
|
|
65
|
+
assert.deepEqual(queue.calls[0]?.arguments, [
|
|
66
|
+
"review", "status", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo",
|
|
67
|
+
"--projection", "workspace", "--lineage", "review-status-fixture", "--agent", "pi", "--next-transition",
|
|
105
68
|
]);
|
|
106
|
-
assert.equal(queue.calls
|
|
69
|
+
assert.equal(queue.calls[0]?.timeoutMs, 30_000);
|
|
107
70
|
});
|
|
108
71
|
|
|
109
|
-
test("
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
lenses_required: false,
|
|
117
|
-
}),
|
|
118
|
-
}]);
|
|
119
|
-
assert.deepEqual((await new NativeReviewCliV213(valid.adapter).start({ cwd: "/repo" })).selectedLenses, []);
|
|
72
|
+
test("a final reviewer capture returns its native last-event closure without a follow-up command", async () => {
|
|
73
|
+
const closure = fixture("last-event-capture-result-approved.captured.json");
|
|
74
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify(closure) }]);
|
|
75
|
+
const result = await client(queue.adapter).captureResult({
|
|
76
|
+
argumentTokens: ["--repository-context=rctx1_" + "a".repeat(64), "--lineage=review-c7c923a031112dd7"],
|
|
77
|
+
resultDocument: "{\"subject_hash\":\"sha256:captured\"}",
|
|
78
|
+
});
|
|
120
79
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
]) {
|
|
126
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify({ ...start, ...scenario, selected_lenses: null }) }]);
|
|
127
|
-
await assert.rejects(() => new NativeReviewCliV213(queue.adapter).start({ cwd: "/repo" }), NativeReviewCliError);
|
|
128
|
-
}
|
|
80
|
+
assert.equal("operation" in result && result.operation, "review/capture-result");
|
|
81
|
+
assert.deepEqual(queue.calls[0]?.arguments.slice(0, 4), ["review", "capture-result", "--repository-context=rctx1_" + "a".repeat(64), "--lineage=review-c7c923a031112dd7"]);
|
|
82
|
+
assert.equal(queue.calls.length, 1);
|
|
83
|
+
assert.equal(queue.calls[0]?.arguments.includes("--contract"), false);
|
|
129
84
|
});
|
|
130
85
|
|
|
131
|
-
test("
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
{ action: "blocked-scope-action", lenses_required: false, risk_level: "low", selected_lenses: [] },
|
|
140
|
-
] as const;
|
|
141
|
-
for (const scenario of valid) {
|
|
142
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify({ ...start, ...scenario }) }]);
|
|
143
|
-
const result = await new NativeReviewCliV213(queue.adapter).start({ cwd: "/repo" });
|
|
144
|
-
assert.equal(result.action, scenario.action);
|
|
145
|
-
assert.equal(result.lensesRequired, scenario.lenses_required);
|
|
146
|
-
assert.deepEqual(result.selectedLenses, scenario.selected_lenses);
|
|
147
|
-
}
|
|
148
|
-
for (const scenario of [
|
|
149
|
-
{ action: "created", lenses_required: false, risk_level: "medium", selected_lenses: [] },
|
|
150
|
-
{ action: "created", lenses_required: false, risk_level: "low", selected_lenses: ["review-reliability"] },
|
|
151
|
-
{ action: "reuse-receipt", lenses_required: true, risk_level: "medium", selected_lenses: ["review-reliability"] },
|
|
152
|
-
{ action: "blocked-scope-action", lenses_required: true, risk_level: "medium", selected_lenses: ["review-reliability"] },
|
|
153
|
-
{ action: "resumed", lenses_required: true, risk_level: "medium", selected_lenses: ["review-reliability"], state: "correction_required" },
|
|
154
|
-
] as const) {
|
|
155
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify({ ...start, ...scenario }) }]);
|
|
156
|
-
await assert.rejects(() => new NativeReviewCliV213(queue.adapter).start({ cwd: "/repo" }), NativeReviewCliError);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("native START binds every risk tier to canonical unique lenses before controller use", async () => {
|
|
161
|
-
const start = JSON.parse(START.stdout) as Record<string, unknown>;
|
|
162
|
-
const high = ["review-risk", "review-resilience", "review-readability", "review-reliability"];
|
|
163
|
-
const valid = queuedAdapter([VERSION, { stdout: JSON.stringify({ ...start, risk_level: "high", selected_lenses: high, lenses_required: false, action: "resumed", state: "validating" }) }]);
|
|
164
|
-
assert.deepEqual((await new NativeReviewCliV213(valid.adapter).start({ cwd: "/repo" })).selectedLenses, high);
|
|
165
|
-
for (const scenario of [
|
|
166
|
-
{ risk_level: "low", selected_lenses: [], lenses_required: true }, { risk_level: "medium", selected_lenses: ["review-risk", "review-reliability"], lenses_required: true },
|
|
167
|
-
{ risk_level: "medium", selected_lenses: ["review-reliability", "review-reliability"], lenses_required: true },
|
|
168
|
-
{ risk_level: "high", selected_lenses: high.slice(0, 3), lenses_required: true }, { risk_level: "high", selected_lenses: [...high, "review-risk"], lenses_required: true },
|
|
169
|
-
{ action: "created", risk_level: "high", selected_lenses: high, lenses_required: false },
|
|
170
|
-
{ action: "resumed", state: "approved", risk_level: "medium", selected_lenses: ["review-reliability"], lenses_required: true },
|
|
171
|
-
] as const) {
|
|
172
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify({ ...start, ...scenario }) }]);
|
|
173
|
-
await assert.rejects(() => new NativeReviewCliV213(queue.adapter).start({ cwd: "/repo" }), NativeReviewCliError);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
86
|
+
test("correction-plan capture substitutes only the provider slot and closes natively", async () => {
|
|
87
|
+
const closure = fixture("last-event-capture-correction-plan.captured.json");
|
|
88
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify(closure) }]);
|
|
89
|
+
const result = await client(queue.adapter).captureCorrectionPlan({
|
|
90
|
+
argumentTokens: ["--lineage=review-c7c923a031112dd7", "--correction-lines={{value}}"],
|
|
91
|
+
correctionLines: 7,
|
|
92
|
+
cwd: "/repo",
|
|
93
|
+
});
|
|
176
94
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const client = new NativeReviewCliV213(queue.adapter);
|
|
180
|
-
await client.start({ cwd: "/repo" });
|
|
95
|
+
assert.equal(result.operation, "review.capture-correction-plan");
|
|
96
|
+
assert.deepEqual(queue.calls[0]?.arguments, ["review", "capture-correction-plan", "--lineage=review-c7c923a031112dd7", "--correction-lines=7"]);
|
|
181
97
|
await assert.rejects(
|
|
182
|
-
() => client.
|
|
183
|
-
|
|
184
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE
|
|
185
|
-
&& error.operation === "version",
|
|
98
|
+
() => client(queuedAdapter([]).adapter).captureCorrectionPlan({ argumentTokens: ["--a={{value}}", "--b={{value}}"], correctionLines: 1, cwd: "/repo" }),
|
|
99
|
+
/CAPTURE_CORRECTION_PLAN requires exactly one/,
|
|
186
100
|
);
|
|
187
|
-
assert.deepEqual(queue.calls.map((call) => call.arguments), [
|
|
188
|
-
["version"],
|
|
189
|
-
["review", "start", "--cwd", "/repo"],
|
|
190
|
-
["version"],
|
|
191
|
-
]);
|
|
192
101
|
});
|
|
193
102
|
|
|
194
|
-
test("
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
{
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
await assert.rejects(() => new NativeReviewCliV213(queue.adapter).start(request as NativeStartRequest), TypeError);
|
|
210
|
-
assert.equal(queue.calls.length, 0);
|
|
211
|
-
}
|
|
212
|
-
});
|
|
103
|
+
test("provider-owned refuter and targeted-validator vectors accept only their mapped terminal closures", async () => {
|
|
104
|
+
const captures = [
|
|
105
|
+
{
|
|
106
|
+
captureOperation: "review.capture-refuter",
|
|
107
|
+
closure: fixture("last-event-capture-refuter-approved.captured.json"),
|
|
108
|
+
closureOperation: "review.capture-refuter",
|
|
109
|
+
commandOperation: "capture-refuter",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
captureOperation: "review.capture-validation",
|
|
113
|
+
closure: fixture("last-event-capture-validation-approved.captured.json"),
|
|
114
|
+
closureOperation: "review/capture-validation",
|
|
115
|
+
commandOperation: "capture-validation",
|
|
116
|
+
},
|
|
117
|
+
] as const;
|
|
213
118
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
);
|
|
119
|
+
for (const capture of captures) {
|
|
120
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify(capture.closure) }]);
|
|
121
|
+
const result = await client(queue.adapter).captureProviderRole({
|
|
122
|
+
captureOperation: capture.captureOperation,
|
|
123
|
+
argumentTokens: ["--repository-context=rctx1_" + "a".repeat(64), "--agent=pi", "--execute=true"],
|
|
124
|
+
cwd: "/repo",
|
|
125
|
+
});
|
|
126
|
+
assert.equal("operation" in result && result.operation, capture.closureOperation);
|
|
127
|
+
assert.deepEqual(queue.calls[0]?.arguments, ["review", capture.commandOperation, "--repository-context=rctx1_" + "a".repeat(64), "--agent=pi", "--execute=true"]);
|
|
224
128
|
}
|
|
225
|
-
|
|
129
|
+
|
|
226
130
|
await assert.rejects(
|
|
227
|
-
() =>
|
|
228
|
-
|
|
131
|
+
() => client(queuedAdapter([]).adapter).captureProviderRole({ captureOperation: "review.capture-result", argumentTokens: ["--agent=pi"], cwd: "/repo" }),
|
|
132
|
+
/CAPTURE_PROVIDER_ROLE supports only/,
|
|
229
133
|
);
|
|
230
134
|
});
|
|
231
135
|
|
|
232
|
-
test("
|
|
233
|
-
const
|
|
234
|
-
const remediationState = source.remediationState as Record<string, unknown>;
|
|
235
|
-
for (const correctionBudget of [undefined, 0, 17]) {
|
|
236
|
-
const queue = queuedAdapter([VERSION_219, {
|
|
237
|
-
stdout: JSON.stringify({
|
|
238
|
-
...source,
|
|
239
|
-
remediationState: {
|
|
240
|
-
...remediationState,
|
|
241
|
-
...(correctionBudget === undefined ? {} : { correctionBudget }),
|
|
242
|
-
},
|
|
243
|
-
}),
|
|
244
|
-
}]);
|
|
245
|
-
const status = await new NativeReviewCliV213(queue.adapter).sddStatus({ cwd: "/repo", change: "native-review-authority-parity" });
|
|
246
|
-
assert.equal((status.remediationState as Record<string, unknown>).correctionBudget, correctionBudget);
|
|
247
|
-
}
|
|
248
|
-
for (const correctionBudget of [-1, 1.5]) {
|
|
249
|
-
const queue = queuedAdapter([VERSION_219, { stdout: JSON.stringify({ ...source, remediationState: { ...remediationState, correctionBudget } }) }]);
|
|
250
|
-
await assert.rejects(() => new NativeReviewCliV213(queue.adapter).sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }), NativeReviewCliError);
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test("version process failures retain their typed failure code", async () => {
|
|
255
|
-
for (const result of [
|
|
256
|
-
{ stdout: "", timedOut: true, code: NATIVE_REVIEW_ERROR_CODE.TIMEOUT },
|
|
257
|
-
{ stdout: "", exitCode: 2, code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO },
|
|
258
|
-
{ stdout: "", signal: "SIGTERM" as NodeJS.Signals, code: NATIVE_REVIEW_ERROR_CODE.SIGNAL },
|
|
259
|
-
{ stdout: "", outputLimitExceeded: true, code: NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT },
|
|
260
|
-
]) {
|
|
261
|
-
const queue = queuedAdapter([result]);
|
|
262
|
-
await assert.rejects(
|
|
263
|
-
() => new NativeReviewCliV213(queue.adapter).start({ cwd: "/repo" }),
|
|
264
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === result.code && error.operation === "version",
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
test("native output limits dominate killed timeout signals and expose the bounded configuration guidance", async () => {
|
|
270
|
-
const assertOutputLimit = (error: unknown): boolean => {
|
|
271
|
-
const value = error as { name?: unknown; code?: unknown; diagnostics?: { timed_out?: unknown; output_limit_exceeded?: unknown; max_buffer_bytes?: unknown; configuration_hint?: unknown } };
|
|
272
|
-
const diagnostics = value.diagnostics;
|
|
273
|
-
return value.name === "NativeReviewCliError"
|
|
274
|
-
&& value.code === NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT
|
|
275
|
-
&& diagnostics?.timed_out === false
|
|
276
|
-
&& diagnostics?.output_limit_exceeded === true
|
|
277
|
-
&& diagnostics?.max_buffer_bytes === NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES
|
|
278
|
-
&& diagnostics?.configuration_hint === "Inspect native review state before any new START; GENTLE_PI_REVIEW_MAX_BUFFER_BYTES accepts a positive decimal up to 67108864.";
|
|
279
|
-
};
|
|
280
|
-
const queue = queuedAdapter([VERSION, { stdout: "", timedOut: true, outputLimitExceeded: true }]);
|
|
281
|
-
await assert.rejects(() => new NativeReviewCliV213(queue.adapter).start({ cwd: "/repo" }), assertOutputLimit);
|
|
282
|
-
const runtimeQueue = queuedAdapter([VERSION, { stdout: "", timedOut: true, outputLimitExceeded: true }]);
|
|
283
|
-
await assert.rejects(() => new RuntimeNativeReviewCliV214(runtimeQueue.adapter).start({ cwd: "/repo" }), assertOutputLimit);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
test("native mutation uncertainty requires target status before any replay decision", async () => {
|
|
287
|
-
const queue = queuedAdapter([VERSION, { stdout: "", timedOut: true }]);
|
|
136
|
+
test("malformed closure output remains a typed schema failure and never authorizes a retry", async () => {
|
|
137
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify({ schema: "gentle-ai.review-last-event-closure/v1", operation: "review/capture-result" }) }]);
|
|
288
138
|
await assert.rejects(
|
|
289
|
-
() =>
|
|
139
|
+
() => client(queue.adapter).captureResult({ argumentTokens: ["--repository-context=rctx1_" + "a".repeat(64)], resultDocument: "{}" }),
|
|
290
140
|
(error: unknown) => error instanceof NativeReviewCliError
|
|
291
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.
|
|
292
|
-
&& error.mutationOutcome === "unknown"
|
|
293
|
-
&& error.nextAction === "review.status",
|
|
141
|
+
&& error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE
|
|
142
|
+
&& error.mutationOutcome === "unknown",
|
|
294
143
|
);
|
|
144
|
+
assert.equal(queue.calls.length, 1);
|
|
295
145
|
});
|
|
296
146
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const invalidOverrides = ["", "0", "-1", "1.5", "not-a-number", "67108865"];
|
|
301
|
-
const withOverride = <T>(value: string | undefined, callback: () => T): T => {
|
|
302
|
-
const previous = process.env.GENTLE_PI_REVIEW_MAX_BUFFER_BYTES;
|
|
303
|
-
try {
|
|
304
|
-
if (value === undefined) delete process.env.GENTLE_PI_REVIEW_MAX_BUFFER_BYTES;
|
|
305
|
-
else process.env.GENTLE_PI_REVIEW_MAX_BUFFER_BYTES = value;
|
|
306
|
-
return callback();
|
|
307
|
-
} finally {
|
|
308
|
-
if (previous === undefined) delete process.env.GENTLE_PI_REVIEW_MAX_BUFFER_BYTES;
|
|
309
|
-
else process.env.GENTLE_PI_REVIEW_MAX_BUFFER_BYTES = previous;
|
|
310
|
-
}
|
|
311
|
-
};
|
|
312
|
-
for (const [name, overrideValue, expected] of [
|
|
313
|
-
["default", undefined, expectedDefault],
|
|
314
|
-
["valid override", override, Number(override)],
|
|
315
|
-
...invalidOverrides.map((value) => [`invalid override ${JSON.stringify(value)}`, value, expectedDefault] as const),
|
|
316
|
-
] as const) {
|
|
317
|
-
const queue = queuedAdapter([VERSION, START]);
|
|
318
|
-
const client = withOverride(overrideValue, () => new NativeReviewCliV213(queue.adapter));
|
|
319
|
-
const v216 = withOverride(overrideValue, () => new NativeReviewCliV216(queue.adapter, "/package/.gentle-ai/gentle-ai"));
|
|
320
|
-
const runtimeV214 = withOverride(overrideValue, () => new RuntimeNativeReviewCliV214(queue.adapter, "/package/.gentle-ai/gentle-ai"));
|
|
321
|
-
const runtimeV216 = withOverride(overrideValue, () => new RuntimeNativeReviewCliV216(queue.adapter, "/package/.gentle-ai/gentle-ai"));
|
|
322
|
-
assert.equal(Reflect.get(v216, "maxBufferBytes"), expected, name);
|
|
323
|
-
assert.equal(Reflect.get(runtimeV214, "maxBufferBytes"), expected, `${name} runtime v214`);
|
|
324
|
-
assert.equal(Reflect.get(runtimeV216, "maxBufferBytes"), expected, `${name} runtime v216`);
|
|
325
|
-
await client.start({ cwd: "/repo" });
|
|
326
|
-
assert.deepEqual(queue.calls.map((call) => call.maxBufferBytes), [expected, expected], name);
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
test("native mutating commands omit the automatic timeout while preserving output caps", async () => {
|
|
331
|
-
const queue = queuedAdapter([
|
|
332
|
-
VERSION,
|
|
333
|
-
START,
|
|
334
|
-
VERSION,
|
|
335
|
-
{ stdout: await fixture("finalize") },
|
|
336
|
-
VERSION,
|
|
337
|
-
{ stdout: await fixture("bind-sdd") },
|
|
338
|
-
]);
|
|
339
|
-
const client = new NativeReviewCliV213(queue.adapter, "/package/.gentle-ai/v2.1.4/gentle-ai", 321, 654);
|
|
340
|
-
await client.start({ cwd: "/repo" });
|
|
341
|
-
await client.finalize({ cwd: "/repo", lineageId: "lineage-1" });
|
|
342
|
-
await client.bindSdd({ cwd: "/repo", change: "native-review-authority-parity", lineage: "issue136-contract-runtime", expectedBindingRevision: "" });
|
|
343
|
-
assert.deepEqual(queue.calls.map((call) => call.timeoutMs), [321, undefined, 321, undefined, 321, undefined]);
|
|
344
|
-
assert.deepEqual(queue.calls.map((call) => call.maxBufferBytes), [654, 654, 654, 654, 654, 654]);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
test("native read-only commands and version checks retain the automatic timeout", async () => {
|
|
348
|
-
const queue = queuedAdapter([VERSION, { stdout: await fixture("validate-allow") }]);
|
|
349
|
-
await new NativeReviewCliV213(queue.adapter, "/package/.gentle-ai/v2.1.4/gentle-ai", 321).validate({ cwd: "/repo", gate: "post-apply", lineageId: "issue136-contract-runtime" });
|
|
350
|
-
assert.deepEqual(queue.calls.map((call) => call.timeoutMs), [321, 321]);
|
|
351
|
-
});
|
|
147
|
+
function currentStatusFixture(): Record<string, unknown> {
|
|
148
|
+
return fixture("status-v5.captured.json");
|
|
149
|
+
}
|
|
352
150
|
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
&& String(diagnostics.stderr).length <= 4_096
|
|
371
|
-
&& (diagnostics.denial as Record<string, unknown> | undefined)?.result === "scope-changed";
|
|
151
|
+
function negotiatedStartStatus(targetIdentity: string, tokens: readonly string[]): Record<string, unknown> {
|
|
152
|
+
const status = currentStatusFixture();
|
|
153
|
+
status.target_identity = targetIdentity;
|
|
154
|
+
const projection = status.projection as Record<string, unknown>;
|
|
155
|
+
projection.initial_snapshot_identity = targetIdentity;
|
|
156
|
+
projection.current_snapshot_identity = targetIdentity;
|
|
157
|
+
status.next_transition = {
|
|
158
|
+
kind: "execute",
|
|
159
|
+
reason_code: "review_start_required",
|
|
160
|
+
execute: {
|
|
161
|
+
operation: "review.start",
|
|
162
|
+
arguments: tokens.map((token) => {
|
|
163
|
+
const separator = token.indexOf("=");
|
|
164
|
+
return { name: token.slice(2, separator), value: token.slice(separator + 1), token };
|
|
165
|
+
}),
|
|
166
|
+
preconditions: [],
|
|
167
|
+
binding: { target_identity: targetIdentity },
|
|
372
168
|
},
|
|
373
|
-
|
|
374
|
-
|
|
169
|
+
};
|
|
170
|
+
return status;
|
|
171
|
+
}
|
|
375
172
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
173
|
+
const TARGET = `sha256:${"b".repeat(64)}`;
|
|
174
|
+
const REPOSITORY_CONTEXT = `rctx1_${"c".repeat(64)}`;
|
|
175
|
+
|
|
176
|
+
function reviewingStartV4(targetIdentity: string): Record<string, unknown> {
|
|
177
|
+
const start = fixture("start-v3-consent-granted.captured.json");
|
|
178
|
+
start.schema = "gentle-ai.review-integration.start/v4";
|
|
179
|
+
(start.repository_context as Record<string, unknown>).target_identity = targetIdentity;
|
|
180
|
+
for (const subject of start.artifact_subjects as Record<string, unknown>[]) subject.target_identity = targetIdentity;
|
|
181
|
+
const baseTree = start.base_tree as string;
|
|
182
|
+
start.next_transition = {
|
|
183
|
+
kind: "execute",
|
|
184
|
+
reason_code: "review_status_required",
|
|
185
|
+
execute: {
|
|
186
|
+
operation: "review.status",
|
|
187
|
+
arguments: [
|
|
188
|
+
{ name: "contract", value: "gentle-ai.review-integration/v2", token: "--contract=gentle-ai.review-integration/v2" },
|
|
189
|
+
{ name: "next-transition", value: "true", token: "--next-transition=true" },
|
|
190
|
+
{ name: "lineage", value: start.lineage_id, token: `--lineage=${start.lineage_id}` },
|
|
191
|
+
{ name: "agent", value: "pi", token: "--agent=pi" },
|
|
192
|
+
{ name: "base-ref", value: baseTree, token: `--base-ref=${baseTree}` },
|
|
193
|
+
{ name: "committed-only", value: "true", token: "--committed-only=true" },
|
|
194
|
+
],
|
|
195
|
+
preconditions: [],
|
|
196
|
+
binding: { target_identity: targetIdentity },
|
|
394
197
|
},
|
|
395
|
-
|
|
396
|
-
|
|
198
|
+
};
|
|
199
|
+
const execute = (start.next_transition as Record<string, unknown>).execute as Record<string, unknown>;
|
|
200
|
+
execute.selector_arguments = (execute.arguments as Record<string, unknown>[]).slice(-2).map((argument) => ({ ...argument }));
|
|
201
|
+
return start;
|
|
202
|
+
}
|
|
397
203
|
|
|
398
|
-
test("
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
);
|
|
204
|
+
test("negotiated STATUS forwards its exact ordered workspace, base, lineage, and untracked selection argv", async () => {
|
|
205
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify(currentStatusFixture()) }]);
|
|
206
|
+
const result = await client(queue.adapter).targetStatus({
|
|
207
|
+
cwd: "/repo with spaces",
|
|
208
|
+
projection: "staged",
|
|
209
|
+
baseRef: "origin/main",
|
|
210
|
+
committedOnly: true,
|
|
211
|
+
lineageId: "review-current",
|
|
212
|
+
agent: "pi",
|
|
213
|
+
untrackedScope: "select",
|
|
214
|
+
expectedUntrackedInventory: "inventory-sha256",
|
|
215
|
+
intendedUntracked: ["new.txt", "nested/other.txt"],
|
|
216
|
+
});
|
|
217
|
+
assert.equal(result.action, "start");
|
|
218
|
+
assert.deepEqual(queue.calls[0]?.arguments, [
|
|
219
|
+
"review", "status", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo with spaces",
|
|
220
|
+
"--projection", "staged", "--untracked-scope=select", "--expected-untracked-inventory=inventory-sha256",
|
|
221
|
+
"--intended-untracked=new.txt", "--intended-untracked=nested/other.txt", "--base-ref", "origin/main", "--committed-only",
|
|
222
|
+
"--lineage", "review-current", "--agent", "pi", "--next-transition",
|
|
223
|
+
]);
|
|
224
|
+
assert.equal(queue.calls[0]?.timeoutMs, 30_000);
|
|
418
225
|
});
|
|
419
226
|
|
|
420
|
-
test("
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
stderr: "Error: review gate denied: scope-changed\n",
|
|
432
|
-
exitCode: 1,
|
|
433
|
-
}]);
|
|
434
|
-
const result = await new NativeReviewCliV213(queue.adapter).validate({ cwd: "/repo", gate: "post-apply" });
|
|
435
|
-
const returned = result.gateContext.raw.denial as Record<string, string>;
|
|
436
|
-
assert.equal(result.reason.length <= 1_024, true);
|
|
437
|
-
assert.equal(returned.stage.length <= 1_024, true);
|
|
438
|
-
assert.equal(returned.code.length <= 1_024, true);
|
|
439
|
-
assert.equal(JSON.stringify({ reason: result.reason, returned }).includes("secret"), false);
|
|
227
|
+
test("negotiated STATUS emits the explicit committed selector argv", async () => {
|
|
228
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify(currentStatusFixture()) }]);
|
|
229
|
+
await client(queue.adapter).targetStatus({
|
|
230
|
+
cwd: "/repo",
|
|
231
|
+
baseRef: "refs/heads/main",
|
|
232
|
+
committedOnly: true,
|
|
233
|
+
});
|
|
234
|
+
assert.deepEqual(queue.calls[0]?.arguments, [
|
|
235
|
+
"review", "status", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo",
|
|
236
|
+
"--projection", "workspace", "--base-ref", "refs/heads/main", "--committed-only", "--next-transition",
|
|
237
|
+
]);
|
|
440
238
|
});
|
|
441
239
|
|
|
442
|
-
test("
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
{
|
|
447
|
-
{
|
|
448
|
-
{
|
|
449
|
-
{
|
|
450
|
-
{ ...published, reason: "reason " },
|
|
240
|
+
test("negotiated STATUS rejects malformed committed selectors before launching a process", async () => {
|
|
241
|
+
for (const request of [
|
|
242
|
+
{ cwd: "/repo", baseRef: "", committedOnly: true },
|
|
243
|
+
{ cwd: "/repo", baseRef: 42, committedOnly: true },
|
|
244
|
+
{ cwd: "/repo", committedOnly: true },
|
|
245
|
+
{ cwd: "/repo", baseRef: "refs/heads/main" },
|
|
246
|
+
{ cwd: "/repo", baseRef: "refs/heads/main", committedOnly: false },
|
|
247
|
+
{ cwd: "/repo", baseRef: "refs/heads/main", committedOnly: "true" },
|
|
451
248
|
]) {
|
|
452
|
-
const queue = queuedAdapter([
|
|
453
|
-
await assert.rejects(
|
|
454
|
-
|
|
455
|
-
(error: unknown) => error instanceof NativeReviewCliError
|
|
456
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.NON_ZERO
|
|
457
|
-
&& error.diagnostics.denial === undefined,
|
|
458
|
-
);
|
|
249
|
+
const queue = queuedAdapter([]);
|
|
250
|
+
await assert.rejects(() => client(queue.adapter).targetStatus(request), TypeError);
|
|
251
|
+
assert.equal(queue.calls.length, 0);
|
|
459
252
|
}
|
|
460
253
|
});
|
|
461
254
|
|
|
462
|
-
test("
|
|
463
|
-
const queue = queuedAdapter([
|
|
464
|
-
const result = await new NativeReviewCliV213(queue.adapter).validate({ cwd: "/repo", gate: "post-apply", lineageId: "issue136-contract-runtime" });
|
|
465
|
-
assert.equal(result.allowed, true);
|
|
466
|
-
assert.equal(result.action, "continue");
|
|
467
|
-
assert.equal(result.gateContext.lineageId, "issue136-contract-runtime");
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
test("native validate requires the returned gate context to equal the requested gate", async () => {
|
|
471
|
-
const published = JSON.parse(await fixture("validate-allow")) as Record<string, unknown>;
|
|
472
|
-
for (const gate of ["", "pre-push"]) {
|
|
473
|
-
const queue = queuedAdapter([VERSION, {
|
|
474
|
-
stdout: JSON.stringify({
|
|
475
|
-
...published,
|
|
476
|
-
context: { ...(published.context as Record<string, unknown>), gate },
|
|
477
|
-
}),
|
|
478
|
-
}]);
|
|
479
|
-
await assert.rejects(
|
|
480
|
-
() => new NativeReviewCliV213(queue.adapter).validate({ cwd: "/repo", gate: "post-apply", lineageId: "issue136-contract-runtime" }),
|
|
481
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE,
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
const denial = JSON.parse(await fixture("validate-deny")) as Record<string, unknown>;
|
|
485
|
-
const mismatch = queuedAdapter([VERSION, {
|
|
486
|
-
stdout: JSON.stringify({
|
|
487
|
-
...denial,
|
|
488
|
-
context: { ...(denial.context as Record<string, unknown>), gate: "pre-push" },
|
|
489
|
-
}),
|
|
490
|
-
stderr: "Error: review gate denied: scope-changed\n",
|
|
491
|
-
exitCode: 1,
|
|
492
|
-
}]);
|
|
255
|
+
test("negotiated STATUS rejects malformed untracked selection before launching a process", async () => {
|
|
256
|
+
const queue = queuedAdapter([]);
|
|
493
257
|
await assert.rejects(
|
|
494
|
-
() =>
|
|
495
|
-
|
|
258
|
+
() => client(queue.adapter).targetStatus({ cwd: "/repo", untrackedScope: "select", expectedUntrackedInventory: "inventory", intendedUntracked: ["../escape"] }),
|
|
259
|
+
/unique repository-relative paths/,
|
|
496
260
|
);
|
|
261
|
+
assert.equal(queue.calls.length, 0);
|
|
497
262
|
});
|
|
498
263
|
|
|
499
|
-
test("
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
] as const) {
|
|
506
|
-
const queue = queuedAdapter([VERSION, {
|
|
507
|
-
stdout: JSON.stringify({ ...published, result, action }),
|
|
508
|
-
stderr: `Error: review gate denied: ${result}\n`,
|
|
509
|
-
exitCode: 1,
|
|
510
|
-
}]);
|
|
511
|
-
const denial = await new NativeReviewCliV213(queue.adapter).validate({ cwd: "/repo", gate: "post-apply", lineageId: "issue136-contract-runtime" });
|
|
512
|
-
assert.deepEqual({ result: denial.result, allowed: denial.allowed, action: denial.action }, { result, allowed: false, action });
|
|
513
|
-
assert.equal(denial.gateContext.raw.gate, "");
|
|
514
|
-
}
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
test("native validate keeps malformed and unexpected nonzero exits typed", async () => {
|
|
518
|
-
const denial = await fixture("validate-deny");
|
|
519
|
-
for (const scenario of [
|
|
520
|
-
{ result: { stdout: "", exitCode: 1 }, code: NATIVE_REVIEW_ERROR_CODE.EMPTY_OUTPUT },
|
|
521
|
-
{ result: { stdout: "{", exitCode: 1 }, code: NATIVE_REVIEW_ERROR_CODE.MALFORMED_JSON },
|
|
522
|
-
{ result: { stdout: denial, exitCode: 2 }, code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO },
|
|
523
|
-
{ result: { stdout: denial, exitCode: 1, timedOut: true }, code: NATIVE_REVIEW_ERROR_CODE.TIMEOUT },
|
|
524
|
-
{ result: { stdout: denial, exitCode: 1, signal: "SIGTERM" as NodeJS.Signals }, code: NATIVE_REVIEW_ERROR_CODE.SIGNAL },
|
|
525
|
-
]) {
|
|
526
|
-
const queue = queuedAdapter([VERSION, scenario.result]);
|
|
527
|
-
await assert.rejects(
|
|
528
|
-
() => new NativeReviewCliV213(queue.adapter).validate({ cwd: "/repo", gate: "post-apply", lineageId: "issue136-contract-runtime" }),
|
|
529
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === scenario.code && error.operation === "review/validate",
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
const unavailable: ExecFileAdapter = async (request) => {
|
|
533
|
-
if (request.arguments[0] === "version") return { stdout: VERSION.stdout, stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
534
|
-
throw Object.assign(new Error("spawn"), { code: "ENOENT" });
|
|
264
|
+
test("negotiated STATUS rejects an unsupported provider execute operation before argv synthesis", async () => {
|
|
265
|
+
const status = currentStatusFixture();
|
|
266
|
+
status.next_transition = {
|
|
267
|
+
kind: "execute",
|
|
268
|
+
reason_code: "future_operation",
|
|
269
|
+
execute: { operation: "review.dispose-result", arguments: [], preconditions: [], binding: { target_identity: TARGET } },
|
|
535
270
|
};
|
|
271
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify(status) }]);
|
|
536
272
|
await assert.rejects(
|
|
537
|
-
() =>
|
|
538
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.
|
|
273
|
+
() => client(queue.adapter).targetStatus({ cwd: "/repo" }),
|
|
274
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.UNSUPPORTED_TRANSITION_OPERATION,
|
|
539
275
|
);
|
|
276
|
+
assert.equal(queue.calls.length, 1);
|
|
540
277
|
});
|
|
541
278
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
{ fixtureName: "finalize", optionalKeys: ["receipt_path"], invoke: (client: NativeReviewCliV213) => client.finalize({ cwd: "/repo", lineageId: "lineage-1" }) },
|
|
547
|
-
{ fixtureName: "validate-allow", invoke: (client: NativeReviewCliV213) => client.validate({ cwd: "/repo", gate: "post-apply", lineageId: "issue136-contract-runtime" }) },
|
|
548
|
-
{ fixtureName: "bind-sdd", invoke: (client: NativeReviewCliV213) => client.bindSdd({ cwd: "/repo", change: "native-review-authority-parity", lineage: "issue136-contract-runtime", expectedBindingRevision: "" }) },
|
|
549
|
-
{ fixtureName: "sdd-status", optionalKeys: ["reviewGate", "reviewTransaction", "phaseInstructions"], invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
550
|
-
{ fixtureName: "sdd-status-engram", optionalKeys: ["reviewGate", "reviewTransaction", "phaseInstructions"], invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
551
|
-
];
|
|
552
|
-
for (const operation of operations) {
|
|
553
|
-
const fixtureBody = JSON.parse(await fixture(operation.fixtureName)) as Record<string, unknown>;
|
|
554
|
-
for (const [key, value] of Object.entries(fixtureBody)) {
|
|
555
|
-
const missing = { ...fixtureBody };
|
|
556
|
-
delete missing[key];
|
|
557
|
-
for (const mutated of [...(operation.optionalKeys?.includes(key) ? [] : [missing]), { ...fixtureBody, [key]: typeof value === "string" ? 1 : "wrong-type" }]) {
|
|
558
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify(mutated) }]);
|
|
559
|
-
await assert.rejects(() => operation.invoke(new NativeReviewCliV213(queue.adapter)), NativeReviewCliError, `${operation.fixtureName}.${key}`);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
test("native decoders reject nested mutations and unknown enums", async () => {
|
|
566
|
-
const validate = JSON.parse(await fixture("validate-allow")) as Record<string, unknown>;
|
|
567
|
-
const bind = JSON.parse(await fixture("bind-sdd")) as Record<string, unknown>;
|
|
568
|
-
const status = JSON.parse(await fixture("sdd-status")) as Record<string, unknown>;
|
|
569
|
-
const start = JSON.parse(await fixture("start")) as Record<string, unknown>;
|
|
570
|
-
const finalization = JSON.parse(await fixture("finalize")) as Record<string, unknown>;
|
|
571
|
-
const cases = [
|
|
572
|
-
{ body: { ...start, risk_level: "unknown" }, invoke: (client: NativeReviewCliV213) => client.start({ cwd: "/repo" }) },
|
|
573
|
-
{ body: { ...start, selected_lenses: ["unknown"] }, invoke: (client: NativeReviewCliV213) => client.start({ cwd: "/repo" }) },
|
|
574
|
-
{ body: { ...finalization, state: "unknown" }, invoke: (client: NativeReviewCliV213) => client.finalize({ cwd: "/repo" }) },
|
|
575
|
-
{ body: { ...validate, context: { ...(validate.context as Record<string, unknown>), extra: true } }, invoke: (client: NativeReviewCliV213) => client.validate({ cwd: "/repo", gate: "post-apply" }) },
|
|
576
|
-
{ body: { ...bind, gate_context: { ...(bind.gate_context as Record<string, unknown>), candidate_tree: 1 } }, invoke: (client: NativeReviewCliV213) => client.bindSdd({ cwd: "/repo", change: "native-review-authority-parity", lineage: "issue136-contract-runtime", expectedBindingRevision: "" }) },
|
|
577
|
-
{ body: { ...status, nextRecommended: "unknown" }, invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
578
|
-
{ body: { ...status, actionContext: { ...(status.actionContext as Record<string, unknown>), allowedEditRoots: [1] } }, invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
579
|
-
{ body: { ...status, reviewGate: { ...(status.reviewGate as Record<string, unknown>), result: "deny" } }, invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
580
|
-
{ body: { ...status, reviewTransaction: { ...(status.reviewTransaction as Record<string, unknown>), mode: "ordinary" } }, invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
581
|
-
{ body: { ...status, reviewTransaction: { ...(status.reviewTransaction as Record<string, unknown>), snapshot: { ...((status.reviewTransaction as Record<string, unknown>).snapshot as Record<string, unknown>), extra: true } } }, invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
279
|
+
test("native START queries STATUS then executes only the provider-rendered ordered transition", async () => {
|
|
280
|
+
const tokens = [
|
|
281
|
+
"--contract=gentle-ai.review-integration/v2", "--cwd=/repo", `--target=${TARGET}`,
|
|
282
|
+
"--projection=workspace", "--agent=pi", "--consent=relay",
|
|
582
283
|
];
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
{ fixtureName: "sdd-status", nestedKey: "reviewGate", invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
595
|
-
{ fixtureName: "sdd-status", nestedKey: "reviewTransaction", invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
596
|
-
{ fixtureName: "sdd-status-engram", nestedKey: "artifacts", invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
597
|
-
];
|
|
598
|
-
for (const item of cases) {
|
|
599
|
-
const body = JSON.parse(await fixture(item.fixtureName)) as Record<string, Record<string, unknown>>;
|
|
600
|
-
for (const [field, value] of Object.entries(body[item.nestedKey]!)) {
|
|
601
|
-
const missingNested = { ...body[item.nestedKey] };
|
|
602
|
-
delete missingNested[field];
|
|
603
|
-
const mutations = [
|
|
604
|
-
...(item.optionalNestedKeys?.includes(field) ? [] : [missingNested]),
|
|
605
|
-
{ ...body[item.nestedKey], [field]: typeof value === "string" ? 1 : "wrong-type" },
|
|
606
|
-
{ ...body[item.nestedKey], extra: true },
|
|
607
|
-
];
|
|
608
|
-
for (const nested of mutations) {
|
|
609
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify({ ...body, [item.nestedKey]: nested }) }]);
|
|
610
|
-
await assert.rejects(() => item.invoke(new NativeReviewCliV213(queue.adapter)), NativeReviewCliError, `${item.fixtureName}.${item.nestedKey}.${field}`);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
284
|
+
const queue = queuedAdapter([
|
|
285
|
+
{ stdout: JSON.stringify(negotiatedStartStatus(TARGET, tokens)) },
|
|
286
|
+
{ stdout: JSON.stringify(fixture("start-v3-zero-lens-closed.captured.json")) },
|
|
287
|
+
]);
|
|
288
|
+
const result = await client(queue.adapter).start({ cwd: "/repo", targetIdentity: TARGET });
|
|
289
|
+
assert.equal(result.action, "closed");
|
|
290
|
+
assert.equal(result.hint, undefined);
|
|
291
|
+
assert.deepEqual(queue.calls.map((call) => call.arguments), [
|
|
292
|
+
["review", "status", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--projection", "workspace", "--agent", "pi", "--next-transition"],
|
|
293
|
+
["review", "start", ...tokens],
|
|
294
|
+
]);
|
|
614
295
|
});
|
|
615
296
|
|
|
616
|
-
test("native
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
{ result: { stdout: START.stdout, stderr: "unexpected" }, code: NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR },
|
|
623
|
-
{ result: { stdout: "", outputLimitExceeded: true }, code: NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT },
|
|
624
|
-
{ result: { stdout: "" }, code: NATIVE_REVIEW_ERROR_CODE.EMPTY_OUTPUT },
|
|
625
|
-
{ result: { stdout: "{" }, code: NATIVE_REVIEW_ERROR_CODE.MALFORMED_JSON },
|
|
297
|
+
test("native START relays provider-owned intended-untracked selection and transition argv verbatim", async () => {
|
|
298
|
+
const selectionValue = JSON.stringify(["new.ts", "nested/other.ts"]);
|
|
299
|
+
const selectionTokens = ["--selection-kind=untracked", "--selection-json={{value}}", "--selection-proof=provider-issued"];
|
|
300
|
+
const startTokens = [
|
|
301
|
+
"--contract=gentle-ai.review-integration/v2", "--cwd=/repo", `--target=${TARGET}`,
|
|
302
|
+
"--projection=workspace", "--agent=pi", "--consent=relay",
|
|
626
303
|
];
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
test("finalize stages every optional document privately and cleans it after failures", async () => {
|
|
641
|
-
const observed: string[] = [];
|
|
642
|
-
let nativeCall = 0;
|
|
643
|
-
const adapter: ExecFileAdapter = async (request) => {
|
|
644
|
-
nativeCall += 1;
|
|
645
|
-
if (nativeCall === 1) return { ...VERSION, stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
646
|
-
for (const argument of request.arguments) if (argument.includes("gentle-ai-finalize-")) observed.push(argument);
|
|
647
|
-
return { stdout: "{", stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
648
|
-
};
|
|
649
|
-
await assert.rejects(
|
|
650
|
-
() => new NativeReviewCliV213(adapter).finalize({
|
|
651
|
-
cwd: "/repo",
|
|
652
|
-
lensResults: [{ lens: "review-risk", document: { id: "risk" } }],
|
|
653
|
-
refuterDocument: { id: "refuter" },
|
|
654
|
-
validationDocument: { id: "validation" },
|
|
655
|
-
evidenceDocument: "evidence",
|
|
656
|
-
}),
|
|
657
|
-
NativeReviewCliError,
|
|
658
|
-
);
|
|
659
|
-
assert.equal(observed.filter((argument) => argument.endsWith(".json")).length, 3);
|
|
660
|
-
await Promise.all(observed.filter((argument) => argument.endsWith(".json")).map(async (path) => assert.rejects(() => import("node:fs/promises").then(({ stat }) => stat(path)))));
|
|
661
|
-
});
|
|
662
|
-
test("native review status uses the anticipated v2.1.5 contract, preserves Windows paths, and never reports mutation", async () => {
|
|
663
|
-
const queue = queuedAdapter([STATUS_VERSION, REVIEW_STATUS]);
|
|
664
|
-
const result = await new NativeReviewCliV213(queue.adapter).reviewStatus({ cwd: "C:\\repo with spaces" });
|
|
665
|
-
assert.equal(result.status, "clean");
|
|
666
|
-
assert.equal(result.complete, true);
|
|
667
|
-
assert.equal(result.authoritative, true);
|
|
304
|
+
const queue = queuedAdapter([
|
|
305
|
+
{ stdout: JSON.stringify(negotiatedStartStatus(TARGET, startTokens)) },
|
|
306
|
+
{ stdout: JSON.stringify(fixture("start-v3-zero-lens-closed.captured.json")) },
|
|
307
|
+
]);
|
|
308
|
+
await client(queue.adapter).start({
|
|
309
|
+
cwd: "/repo",
|
|
310
|
+
targetIdentity: TARGET,
|
|
311
|
+
intendedUntrackedSelection: { argumentTokens: selectionTokens, value: selectionValue },
|
|
312
|
+
});
|
|
668
313
|
assert.deepEqual(queue.calls.map((call) => call.arguments), [
|
|
669
|
-
["
|
|
670
|
-
["review", "
|
|
314
|
+
["review", "status", "--cwd", "/repo", "--selection-kind=untracked", `--selection-json=${selectionValue}`, "--selection-proof=provider-issued"],
|
|
315
|
+
["review", "start", ...startTokens],
|
|
671
316
|
]);
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
const wrongRepository = queuedAdapter([STATUS_VERSION, { stdout: JSON.stringify({ ...JSON.parse(REVIEW_STATUS.stdout), repository: "C:\\other repository" }) }]);
|
|
675
|
-
await assert.rejects(
|
|
676
|
-
() => new NativeReviewCliV213(wrongRepository.adapter).reviewStatus({ cwd: "C:\\repo with spaces" }),
|
|
677
|
-
(error: unknown) => error instanceof NativeReviewCliError
|
|
678
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH
|
|
679
|
-
&& error.operation === "review/status"
|
|
680
|
-
&& error.mutationOutcome === "none",
|
|
681
|
-
);
|
|
682
|
-
|
|
683
|
-
const incomplete = queuedAdapter([STATUS_VERSION, { stdout: JSON.stringify({ ...JSON.parse(REVIEW_STATUS.stdout), complete: false, authoritative: false, status: "invalid" }) }]);
|
|
684
|
-
assert.equal((await new NativeReviewCliV213(incomplete.adapter).reviewStatus({ cwd: "C:\\repo with spaces" })).complete, false);
|
|
685
|
-
|
|
686
|
-
const populated = queuedAdapter([STATUS_VERSION, { stdout: JSON.stringify({ ...JSON.parse(REVIEW_STATUS.stdout), status: "active", entries: [{ version: "compact-v2", lineage_id: "lineage", path: "C:\\repo\\.git\\gentle-ai", status: "active", revision: "r1", recovery: { predecessor_lineage_id: "old", predecessor_revision: "r0", disposition: "scope_changed", reason: "scope", actor: "maintainer", recovered_at: "2026-07-14T00:00:00Z" }, problems: ["stale"] }], locks: [{ version: "compact-v2", path: "C:\\repo\\.git\\gentle-ai\\LOCK", status: "owned", owner: { schema: "gentle-ai.review-store-lock/v1", owner_id: "owner", pid: 1, host: "host", acquired_at: "2026-07-14T00:00:00Z" } }], diagnostics: [{ path: "C:\\repo\\.git\\gentle-ai", problem: "stale" }] }) }]);
|
|
687
|
-
const decoded = await new NativeReviewCliV213(populated.adapter).reviewStatus({ cwd: "C:\\repo with spaces" });
|
|
688
|
-
assert.deepEqual(decoded.entries[0]?.recovery?.predecessorLineageId, "old");
|
|
689
|
-
assert.deepEqual(decoded.locks[0]?.owner?.ownerId, "owner");
|
|
690
|
-
assert.deepEqual(decoded.diagnostics, [{ path: "C:\\repo\\.git\\gentle-ai", problem: "stale" }]);
|
|
691
|
-
|
|
692
|
-
const unsupportedLockSchema = queuedAdapter([STATUS_VERSION, {
|
|
693
|
-
stdout: JSON.stringify({
|
|
694
|
-
...JSON.parse(REVIEW_STATUS.stdout),
|
|
695
|
-
locks: [{
|
|
696
|
-
version: "compact-v2",
|
|
697
|
-
path: "C:\\repo\\.git\\gentle-ai\\LOCK",
|
|
698
|
-
status: "owned",
|
|
699
|
-
owner: { schema: "unexpected", owner_id: "owner", pid: 2_147_483_647, host: "host", acquired_at: "2026-07-14T00:00:00Z" },
|
|
700
|
-
}],
|
|
701
|
-
}),
|
|
702
|
-
}]);
|
|
703
|
-
await assert.rejects(
|
|
704
|
-
() => new NativeReviewCliV213(unsupportedLockSchema.adapter).reviewStatus({ cwd: "C:\\repo with spaces" }),
|
|
705
|
-
(error: unknown) => error instanceof NativeReviewCliError
|
|
706
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE
|
|
707
|
-
&& error.operation === "review/status"
|
|
708
|
-
&& error.mutationOutcome === "none",
|
|
709
|
-
);
|
|
710
|
-
|
|
711
|
-
for (const body of [
|
|
712
|
-
{ ...JSON.parse(REVIEW_STATUS.stdout), complete: false, authoritative: true },
|
|
713
|
-
{ ...JSON.parse(REVIEW_STATUS.stdout), entries: [{ version: "compact-v2", lineage_id: "lineage", path: "C:\\repo\\.git\\gentle-ai", status: "active", problems: [], unexpected: true }] },
|
|
714
|
-
{ ...JSON.parse(REVIEW_STATUS.stdout), status: "unknown" },
|
|
715
|
-
]) {
|
|
716
|
-
const malformed = queuedAdapter([STATUS_VERSION, { stdout: JSON.stringify(body) }]);
|
|
717
|
-
await assert.rejects(
|
|
718
|
-
() => new NativeReviewCliV213(malformed.adapter).reviewStatus({ cwd: "C:\\repo with spaces" }),
|
|
719
|
-
(error: unknown) => error instanceof NativeReviewCliError
|
|
720
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE
|
|
721
|
-
&& error.operation === "review/status"
|
|
722
|
-
&& error.mutationOutcome === "none",
|
|
723
|
-
);
|
|
317
|
+
for (const flag of ["--untracked-scope", "--expected-untracked-inventory", "--intended-untracked", "--contract", "--agent", "--projection"]) {
|
|
318
|
+
assert.equal(queue.calls[0]?.arguments.includes(flag), false, flag);
|
|
724
319
|
}
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
test("native review status decodes and retains the v2.2.2 compact snapshot identity", async () => {
|
|
728
|
-
const snapshotIdentity = "sha256:0586c3d40c3acf3db92214ddaf07afe44ad862518b48d608ae4157377ab2f3cc";
|
|
729
|
-
const entry = {
|
|
730
|
-
version: "compact-v2",
|
|
731
|
-
lineage_id: "issue-136-v2-1-2-final",
|
|
732
|
-
path: "C:\\repo with spaces\\.git\\gentle-ai\\review-transactions\\v2\\issue-136-v2-1-2-final",
|
|
733
|
-
status: "active",
|
|
734
|
-
state: "reviewing",
|
|
735
|
-
revision: "sha256:8103efb360d10d1717a4d22e38c8b75074f99680ed6a5eb3ab40e0ed9fb69931",
|
|
736
|
-
snapshot_identity: snapshotIdentity,
|
|
737
|
-
problems: [],
|
|
738
|
-
};
|
|
739
|
-
const status = { ...JSON.parse(REVIEW_STATUS.stdout), status: "active", entries: [entry] };
|
|
740
|
-
const queue = queuedAdapter([{ stdout: "gentle-ai 2.2.2\n" }, { stdout: JSON.stringify(status) }]);
|
|
741
|
-
const decoded = await new NativeReviewCliV213(queue.adapter).reviewStatus({ cwd: "C:\\repo with spaces" });
|
|
742
|
-
assert.equal(decoded.entries[0]?.snapshotIdentity, snapshotIdentity);
|
|
743
320
|
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
() => new NativeReviewCliV213(malformed.adapter).reviewStatus({ cwd: "C:\\repo with spaces" }),
|
|
747
|
-
(error: unknown) => error instanceof NativeReviewCliError
|
|
748
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE
|
|
749
|
-
&& error.operation === "review/status"
|
|
750
|
-
&& error.mutationOutcome === "none",
|
|
751
|
-
);
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
test("native review status decodes 2.1.8 released lock residue and keeps unknown lock statuses fail-closed", async () => {
|
|
755
|
-
// gentle-ai 2.1.8 leaves review-transactions/v2/LOCK behind after ORDINARY
|
|
756
|
-
// successful operations and reports it as {"status":"released"} without
|
|
757
|
-
// owner metadata (issue #184; reproduced empirically against the system
|
|
758
|
-
// binary). The decoder must accept it or reviewStatus dead-ends with
|
|
759
|
-
// schema-incompatible on every repository that ever completed a review.
|
|
760
|
-
//
|
|
761
|
-
// Lock status stays a CLOSED enum extended by exactly `released` — unlike
|
|
762
|
-
// the cause_category widening in review-integration-v2 (diagnostic
|
|
763
|
-
// metadata nothing routes on), lock status routes blocking behavior in the
|
|
764
|
-
// controller, so an unknown future status must keep failing closed rather
|
|
765
|
-
// than being silently classified as blocking or non-blocking.
|
|
766
|
-
const releasedLock = { version: "compact-v2", path: "C:\\repo\\.git\\gentle-ai\\review-transactions\\v2\\LOCK", status: "released" };
|
|
767
|
-
const released = queuedAdapter([STATUS_VERSION, { stdout: JSON.stringify({ ...JSON.parse(REVIEW_STATUS.stdout), status: "approved", locks: [releasedLock] }) }]);
|
|
768
|
-
const decoded = await new NativeReviewCliV213(released.adapter).reviewStatus({ cwd: "C:\\repo with spaces" });
|
|
769
|
-
assert.equal(decoded.status, "approved");
|
|
770
|
-
assert.deepEqual(decoded.locks, [{ version: "compact-v2", path: "C:\\repo\\.git\\gentle-ai\\review-transactions\\v2\\LOCK", status: "released" }]);
|
|
771
|
-
|
|
772
|
-
// Residual dead-owner metadata may still accompany a released entry.
|
|
773
|
-
const releasedWithOwner = queuedAdapter([STATUS_VERSION, { stdout: JSON.stringify({ ...JSON.parse(REVIEW_STATUS.stdout), locks: [{ ...releasedLock, owner: { schema: "gentle-ai.review-store-lock/v1", owner_id: "dead-owner", pid: 1, host: "host", acquired_at: "2026-07-14T00:00:00Z" } }] }) }]);
|
|
774
|
-
assert.equal((await new NativeReviewCliV213(releasedWithOwner.adapter).reviewStatus({ cwd: "C:\\repo with spaces" })).locks[0]?.owner?.ownerId, "dead-owner");
|
|
775
|
-
|
|
776
|
-
for (const status of ["Released", "stale", "unknown-future-status", ""]) {
|
|
777
|
-
const malformed = queuedAdapter([STATUS_VERSION, { stdout: JSON.stringify({ ...JSON.parse(REVIEW_STATUS.stdout), locks: [{ ...releasedLock, status }] }) }]);
|
|
321
|
+
for (const argumentTokens of [["--selection-json=value"], ["--selection-json={{value}}", "--duplicate={{value}}"], ["--selection-json={{value}}{{value}}"]]) {
|
|
322
|
+
const rejected = queuedAdapter([]);
|
|
778
323
|
await assert.rejects(
|
|
779
|
-
() =>
|
|
780
|
-
|
|
781
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE
|
|
782
|
-
&& error.operation === "review/status"
|
|
783
|
-
&& error.mutationOutcome === "none",
|
|
324
|
+
() => client(rejected.adapter).start({ cwd: "/repo", targetIdentity: TARGET, intendedUntrackedSelection: { argumentTokens, value: selectionValue } }),
|
|
325
|
+
/exactly one provider-issued \{\{value\}\} token/,
|
|
784
326
|
);
|
|
327
|
+
assert.equal(rejected.calls.length, 0);
|
|
785
328
|
}
|
|
786
329
|
});
|
|
787
330
|
|
|
788
|
-
test("native
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
const queue = queuedAdapter([
|
|
795
|
-
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
);
|
|
807
|
-
assert.deepEqual(
|
|
331
|
+
test("native START retains the provider-owned START/v4 status transition in exact argument order", async () => {
|
|
332
|
+
const tokens = [
|
|
333
|
+
"--contract=gentle-ai.review-integration/v2", "--cwd=/repo", `--target=${TARGET}`,
|
|
334
|
+
"--projection=workspace", "--agent=pi", "--consent=relay",
|
|
335
|
+
];
|
|
336
|
+
const start = reviewingStartV4(TARGET);
|
|
337
|
+
const queue = queuedAdapter([
|
|
338
|
+
{ stdout: JSON.stringify(negotiatedStartStatus(TARGET, tokens)) },
|
|
339
|
+
{ stdout: JSON.stringify(start) },
|
|
340
|
+
]);
|
|
341
|
+
const result = await client(queue.adapter).start({ cwd: "/repo", targetIdentity: TARGET });
|
|
342
|
+
assert.deepEqual(result.nextTransition?.execute?.arguments.map((argument) => argument.token), [
|
|
343
|
+
"--contract=gentle-ai.review-integration/v2",
|
|
344
|
+
"--next-transition=true",
|
|
345
|
+
`--lineage=${result.lineageId}`,
|
|
346
|
+
"--agent=pi",
|
|
347
|
+
`--base-ref=${start.base_tree}`,
|
|
348
|
+
"--committed-only=true",
|
|
349
|
+
]);
|
|
350
|
+
assert.deepEqual(result.nextTransition?.execute?.selectorArguments?.map((argument) => argument.token), [
|
|
351
|
+
`--base-ref=${start.base_tree}`,
|
|
352
|
+
"--committed-only=true",
|
|
353
|
+
]);
|
|
354
|
+
assert.deepEqual(queue.calls[1]?.arguments, ["review", "start", ...tokens]);
|
|
808
355
|
});
|
|
809
356
|
|
|
810
|
-
async
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const queue = queuedAdapter([VERSION, native]);
|
|
818
|
-
const client = new NativeReviewCliV213(
|
|
819
|
-
queue.adapter,
|
|
820
|
-
"/package/.gentle-ai/v2.1.3/gentle-ai",
|
|
821
|
-
30_000,
|
|
822
|
-
1024 * 1024,
|
|
823
|
-
async () => {
|
|
824
|
-
cleanupAttempts += 1;
|
|
825
|
-
throw new Error("cleanup failed");
|
|
826
|
-
},
|
|
827
|
-
);
|
|
828
|
-
const finalize = () => client.finalize({ cwd: "/repo", lensResults: [{ lens: "review-risk", document: { id: "risk" } }] });
|
|
829
|
-
if (native.stdout === "{") {
|
|
830
|
-
await assert.rejects(finalize, (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.MALFORMED_JSON && error.mutationOutcome === "unknown");
|
|
831
|
-
} else {
|
|
832
|
-
assert.match((await finalize()).storeRevision, /^sha256:[0-9a-f]{64}$/);
|
|
833
|
-
}
|
|
834
|
-
assert.equal(cleanupAttempts, 1);
|
|
357
|
+
test("native consent completion decodes START/v4 and replays its provider argv unchanged", async () => {
|
|
358
|
+
const rawConsent = fixture("consent-v3.captured.json");
|
|
359
|
+
rawConsent.agent = "pi";
|
|
360
|
+
for (const choice of rawConsent.choices as Record<string, unknown>[]) {
|
|
361
|
+
choice.invocation = String(choice.invocation)
|
|
362
|
+
.replace(/--cwd \S+ --target/, "--cwd /repo --target")
|
|
363
|
+
.replace(" --consent ", " --agent pi --consent ");
|
|
835
364
|
}
|
|
365
|
+
const consent = decodeReviewConsentV3(rawConsent, "pi");
|
|
366
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify(reviewingStartV4(consent.targetIdentity)) }]);
|
|
367
|
+
const answered = await client(queue.adapter).answerConsent({ cwd: "/repo", consent, answer: "granted" });
|
|
368
|
+
assert.equal(answered.kind, "started");
|
|
369
|
+
if (answered.kind !== "started") throw new Error("expected started consent result");
|
|
370
|
+
assert.equal(answered.start.nextTransition?.execute?.operation, "review.status");
|
|
371
|
+
assert.deepEqual(queue.calls[0]?.arguments, consent.choices[0].invocation.split(" ").slice(1));
|
|
836
372
|
});
|
|
837
373
|
|
|
838
|
-
test("
|
|
839
|
-
for (const
|
|
840
|
-
{
|
|
841
|
-
{
|
|
842
|
-
{
|
|
843
|
-
{
|
|
374
|
+
test("native START refuses invalid committed-range and target bindings before STATUS", async () => {
|
|
375
|
+
for (const request of [
|
|
376
|
+
{ cwd: "/repo", baseRef: "origin/main" },
|
|
377
|
+
{ cwd: "/repo", committedOnly: true },
|
|
378
|
+
{ cwd: "/repo", baseRef: " origin/main", committedOnly: true },
|
|
379
|
+
{ cwd: "/repo", targetIdentity: "not-a-sha" },
|
|
844
380
|
]) {
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
call += 1;
|
|
849
|
-
if (call === 1) return { ...VERSION, stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
850
|
-
for (const argument of request.arguments) if (argument.includes("gentle-ai-finalize-")) staged.push(argument);
|
|
851
|
-
return { stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false, ...result };
|
|
852
|
-
};
|
|
853
|
-
const finalize = () => new NativeReviewCliV213(adapter).finalize({ cwd: "/repo", lensResults: [{ lens: "review-risk", document: { id: "risk" } }], refuterDocument: { id: "refuter" }, validationDocument: { id: "validation" }, evidenceDocument: "evidence" });
|
|
854
|
-
if (result.exitCode === 1 || result.timedOut || result.stdout === "{") await assert.rejects(finalize, NativeReviewCliError);
|
|
855
|
-
else await finalize();
|
|
856
|
-
await Promise.all(staged.filter((path) => path.endsWith(".json")).map(async (path) => assert.rejects(() => import("node:fs/promises").then(({ stat }) => stat(path)))));
|
|
381
|
+
const queue = queuedAdapter([]);
|
|
382
|
+
await assert.rejects(() => client(queue.adapter).start(request), TypeError);
|
|
383
|
+
assert.equal(queue.calls.length, 0);
|
|
857
384
|
}
|
|
858
385
|
});
|
|
859
386
|
|
|
860
|
-
test("
|
|
861
|
-
const queue = queuedAdapter([
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
{ stdout: await fixture("sdd-status") },
|
|
872
|
-
]);
|
|
873
|
-
const client = new NativeReviewCliV213(queue.adapter);
|
|
874
|
-
await assert.rejects(() => client.start({ cwd: "/repo", lineageId: "lineage-1" }), NativeReviewCliError);
|
|
875
|
-
const finalized = await client.finalize({ cwd: "/repo", lineageId: "lineage-1" });
|
|
876
|
-
assert.match(finalized.storeRevision, /^sha256:[0-9a-f]{64}$/);
|
|
877
|
-
assert.equal(finalized.action, "validate delivery with gentle-ai review validate --gate <gate>");
|
|
878
|
-
assert.equal((await client.validate({ cwd: "/repo", gate: "post-apply", lineageId: "issue136-contract-runtime" })).allowed, true);
|
|
879
|
-
assert.match((await client.bindSdd({ cwd: "/repo", change: "native-review-authority-parity", lineage: "issue136-contract-runtime", expectedBindingRevision: "" })).revision, /^sha256:[0-9a-f]{64}$/);
|
|
880
|
-
assert.equal((await client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" })).ready, true);
|
|
387
|
+
test("capture-result stages a private input and preserves provider tokens without adding a contract", async () => {
|
|
388
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify(fixture("last-event-capture-result-approved.captured.json")) }]);
|
|
389
|
+
const result = await client(queue.adapter).captureResult({
|
|
390
|
+
argumentTokens: [`--repository-context=${REPOSITORY_CONTEXT}`, "--lineage=review-c7c923a031112dd7", "--target=sha256:captured"],
|
|
391
|
+
resultDocument: "{\"subject_hash\":\"sha256:captured\"}",
|
|
392
|
+
});
|
|
393
|
+
assert.equal("operation" in result && result.operation, "review/capture-result");
|
|
394
|
+
assert.deepEqual(queue.calls[0]?.arguments.slice(0, 5), ["review", "capture-result", `--repository-context=${REPOSITORY_CONTEXT}`, "--lineage=review-c7c923a031112dd7", "--target=sha256:captured"]);
|
|
395
|
+
assert.equal(queue.calls[0]?.arguments.includes("--contract"), false);
|
|
396
|
+
assert.equal(queue.calls[0]?.arguments.at(-2), "--input");
|
|
397
|
+
assert.equal(queue.calls[0]?.timeoutMs, undefined);
|
|
881
398
|
});
|
|
882
399
|
|
|
883
|
-
test("
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
for (const source of [openspec, engram]) {
|
|
888
|
-
for (const nextRecommended of actions) {
|
|
889
|
-
const body = {
|
|
890
|
-
...source,
|
|
891
|
-
nextRecommended,
|
|
892
|
-
blockedReasons: [],
|
|
893
|
-
reviewGate: { result: "allow", reason: "current bound authority allows delivery" },
|
|
894
|
-
};
|
|
895
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify(body) }]);
|
|
896
|
-
assert.equal(
|
|
897
|
-
(await new NativeReviewCliV213(queue.adapter).sddStatus({ cwd: "/repo", change: "native-review-authority-parity" })).ready,
|
|
898
|
-
nextRecommended === "verify" || nextRecommended === "archive",
|
|
899
|
-
`${source.artifactStore as string}:${nextRecommended}`,
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
for (const body of [
|
|
905
|
-
{ ...openspec, nextRecommended: "verify", blockedReasons: [], reviewGate: undefined },
|
|
906
|
-
{ ...openspec, nextRecommended: "archive", blockedReasons: ["stale authority"], reviewGate: { result: "allow", reason: "allow before drift" } },
|
|
907
|
-
{ ...openspec, nextRecommended: "verify", blockedReasons: [], reviewGate: { result: "scope-changed", reason: "candidate changed" } },
|
|
908
|
-
{ ...openspec, nextRecommended: "archive", blockedReasons: [], reviewGate: { result: "invalidated", reason: "authority is stale" } },
|
|
400
|
+
test("capture-result rejects an empty document and a conflicting repository context before launch", async () => {
|
|
401
|
+
for (const request of [
|
|
402
|
+
{ argumentTokens: [`--repository-context=${REPOSITORY_CONTEXT}`], resultDocument: "" },
|
|
403
|
+
{ argumentTokens: [`--repository-context=${REPOSITORY_CONTEXT}`], resultDocument: "{}", cwd: "/repo" },
|
|
909
404
|
]) {
|
|
910
|
-
const queue = queuedAdapter([
|
|
911
|
-
assert.
|
|
405
|
+
const queue = queuedAdapter([]);
|
|
406
|
+
await assert.rejects(() => client(queue.adapter).captureResult(request), TypeError);
|
|
407
|
+
assert.equal(queue.calls.length, 0);
|
|
912
408
|
}
|
|
913
409
|
});
|
|
914
410
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
for (const nextRecommended of ["verify", "archive"] as const) {
|
|
926
|
-
const body = {
|
|
927
|
-
...openspec,
|
|
928
|
-
nextRecommended,
|
|
929
|
-
blockedReasons: [],
|
|
930
|
-
reviewGate: { result: "invalidated", reason: "review-driven development is disabled and no receipt governs this candidate, so delivery follows ordinary repository policy", delivery: "disabled/unmanaged" },
|
|
931
|
-
};
|
|
932
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify(body) }]);
|
|
933
|
-
const status = await new NativeReviewCliV213(queue.adapter).sddStatus({ cwd: "/repo", change: "native-review-authority-parity" });
|
|
934
|
-
assert.equal(status.ready, true, `${nextRecommended} must be ready under disabled/unmanaged delivery even though result is not "allow"`);
|
|
411
|
+
test("terminal correction and provider-role captures preserve unknown mutation failure semantics", async () => {
|
|
412
|
+
for (const run of [
|
|
413
|
+
(review: NativeReviewCliV216) => review.captureCorrectionPlan({ argumentTokens: ["--lineage=review-c7c923a031112dd7", "--correction-lines={{value}}"], correctionLines: 1, cwd: "/repo" }),
|
|
414
|
+
(review: NativeReviewCliV216) => review.captureProviderRole({ captureOperation: "review.capture-refuter", argumentTokens: ["--lineage=review-c7c923a031112dd7"], cwd: "/repo" }),
|
|
415
|
+
]) {
|
|
416
|
+
const queue = queuedAdapter([{ stdout: "", timedOut: true }]);
|
|
417
|
+
await assert.rejects(
|
|
418
|
+
() => run(client(queue.adapter)),
|
|
419
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.TIMEOUT && error.mutationOutcome === "unknown" && error.nextAction === "review.status",
|
|
420
|
+
);
|
|
935
421
|
}
|
|
936
|
-
|
|
937
|
-
// Blocked reasons still take priority over a legitimate unmanaged delivery.
|
|
938
|
-
const stillBlocked = {
|
|
939
|
-
...openspec, nextRecommended: "verify", blockedReasons: ["stale authority"],
|
|
940
|
-
reviewGate: { result: "invalidated", reason: "delivery follows ordinary repository policy", delivery: "disabled/unmanaged" },
|
|
941
|
-
};
|
|
942
|
-
const blockedQueue = queuedAdapter([VERSION, { stdout: JSON.stringify(stillBlocked) }]);
|
|
943
|
-
assert.equal((await new NativeReviewCliV213(blockedQueue.adapter).sddStatus({ cwd: "/repo", change: "native-review-authority-parity" })).ready, false);
|
|
944
|
-
|
|
945
|
-
// An unrecognized delivery value must still fail closed (exact enum, not a
|
|
946
|
-
// free string).
|
|
947
|
-
const unknownDelivery = {
|
|
948
|
-
...openspec, nextRecommended: "verify", blockedReasons: [],
|
|
949
|
-
reviewGate: { result: "invalidated", reason: "delivery follows ordinary repository policy", delivery: "unmanaged" },
|
|
950
|
-
};
|
|
951
|
-
const unknownQueue = queuedAdapter([VERSION, { stdout: JSON.stringify(unknownDelivery) }]);
|
|
952
|
-
await assert.rejects(
|
|
953
|
-
() => new NativeReviewCliV213(unknownQueue.adapter).sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }),
|
|
954
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE,
|
|
955
|
-
);
|
|
956
422
|
});
|
|
957
423
|
|
|
958
|
-
test("
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
424
|
+
test("read-only STATUS process failures remain typed and never claim mutation", async () => {
|
|
425
|
+
for (const [result, code] of [
|
|
426
|
+
[{ stdout: "", outputLimitExceeded: true }, NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT],
|
|
427
|
+
[{ stdout: "", timedOut: true }, NATIVE_REVIEW_ERROR_CODE.TIMEOUT],
|
|
428
|
+
[{ stdout: "", signal: "SIGTERM" as NodeJS.Signals }, NATIVE_REVIEW_ERROR_CODE.SIGNAL],
|
|
429
|
+
[{ stdout: "not-json" }, NATIVE_REVIEW_ERROR_CODE.MALFORMED_JSON],
|
|
430
|
+
] as const) {
|
|
431
|
+
const queue = queuedAdapter([result]);
|
|
432
|
+
await assert.rejects(
|
|
433
|
+
() => client(queue.adapter).targetStatus({ cwd: "/repo" }),
|
|
434
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.code === code && error.mutationOutcome === "none",
|
|
435
|
+
);
|
|
436
|
+
}
|
|
964
437
|
});
|
|
965
438
|
|
|
966
|
-
test("
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
assert.equal(result.allowed, false);
|
|
971
|
-
assert.equal(result.action, "create-new-lineage");
|
|
972
|
-
|
|
973
|
-
for (const stale of [
|
|
974
|
-
{ schema: "gentle-ai.review-gate-result/v1", result: "deny", allowed: false, action: "blocked", reason: "stale", gate_context: {} },
|
|
975
|
-
{ schema: "gentle-ai.review-sdd-binding/v1", repository: "repo", change: "native-review-authority-parity", path: "openspec/changes/native-review-authority-parity", lineage_id: "issue136-contract-runtime", authority_revision: "revision", receipt_hash: "receipt", binding_revision: "binding", gate_context: {} },
|
|
439
|
+
test("read-only STATUS rejects unexpected stderr and nonzero failures without decoding authority", async () => {
|
|
440
|
+
for (const result of [
|
|
441
|
+
{ stdout: JSON.stringify(currentStatusFixture()), stderr: "unexpected provider noise" },
|
|
442
|
+
{ stdout: JSON.stringify({ schema: "gentle-ai.review-integration.failure/v2" }), exitCode: 1 },
|
|
976
443
|
]) {
|
|
977
|
-
const
|
|
978
|
-
const client = new NativeReviewCliV213(staleQueue.adapter);
|
|
444
|
+
const queue = queuedAdapter([result]);
|
|
979
445
|
await assert.rejects(
|
|
980
|
-
() => "
|
|
981
|
-
|
|
982
|
-
: client.bindSdd({ cwd: "/repo", change: "native-review-authority-parity", lineage: "issue136-contract-runtime", expectedBindingRevision: "" }),
|
|
983
|
-
NativeReviewCliError,
|
|
446
|
+
() => client(queue.adapter).targetStatus({ cwd: "/repo" }),
|
|
447
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.mutationOutcome === "none",
|
|
984
448
|
);
|
|
985
449
|
}
|
|
986
450
|
});
|
|
987
451
|
|
|
988
|
-
test("
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
])
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
{ stdout: "", stderr: "missing", exitCode: 1 },
|
|
1002
|
-
{ stdout: await fixture("start"), stderr: "warning" },
|
|
1003
|
-
{ stdout: "", timedOut: true },
|
|
452
|
+
test("negotiated STATUS accepts only exact provider forecast narration on stderr", async () => {
|
|
453
|
+
const narration = [
|
|
454
|
+
"Forecast horizon: partial",
|
|
455
|
+
"step 1: collect; reason_code=provider_refuter_required; description=run the provider-owned refuter",
|
|
456
|
+
"Re-query STATUS after completing this partial head.",
|
|
457
|
+
].join("\n");
|
|
458
|
+
const accepted = queuedAdapter([{ stdout: JSON.stringify(currentStatusFixture()), stderr: narration }]);
|
|
459
|
+
assert.equal((await client(accepted.adapter).targetStatus({ cwd: "/repo" })).action, "start");
|
|
460
|
+
|
|
461
|
+
for (const stderr of [
|
|
462
|
+
"Forecast horizon: unknown",
|
|
463
|
+
`prefix ${narration}`,
|
|
464
|
+
`${narration}\nunexpected provider noise`,
|
|
1004
465
|
]) {
|
|
1005
|
-
const
|
|
466
|
+
const rejected = queuedAdapter([{ stdout: JSON.stringify(currentStatusFixture()), stderr }]);
|
|
1006
467
|
await assert.rejects(
|
|
1007
|
-
() =>
|
|
1008
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.
|
|
468
|
+
() => client(rejected.adapter).targetStatus({ cwd: "/repo" }),
|
|
469
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR,
|
|
1009
470
|
);
|
|
1010
471
|
}
|
|
1011
472
|
});
|
|
1012
473
|
|
|
1013
|
-
test("native client
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
const status = JSON.parse(await fixture("sdd-status")) as Record<string, unknown>;
|
|
1017
|
-
const cases = [
|
|
1018
|
-
{ invoke: (client: NativeReviewCliV213) => client.finalize({ cwd: "/repo", lineageId: "lineage-1" }), body: { ...finalize, extra: true } },
|
|
1019
|
-
{ invoke: (client: NativeReviewCliV213) => client.bindSdd({ cwd: "/repo", change: "native-review-authority-parity", lineage: "lineage-1", expectedBindingRevision: "" }), body: { ...bind, extra: true } },
|
|
1020
|
-
{ invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }), body: { ...status, extra: true } },
|
|
1021
|
-
];
|
|
1022
|
-
for (const item of cases) {
|
|
1023
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify(item.body) }]);
|
|
1024
|
-
await assert.rejects(() => item.invoke(new NativeReviewCliV213(queue.adapter)), NativeReviewCliError);
|
|
1025
|
-
}
|
|
474
|
+
test("native client requires an absolute package-local executable before any invocation", () => {
|
|
475
|
+
assert.throws(() => new NativeReviewCliV216(queuedAdapter([]).adapter, "gentle-ai"), /absolute package-local executable/);
|
|
476
|
+
assert.throws(() => new NativeReviewCliV216(queuedAdapter([]).adapter, "relative/gentle-ai"), /absolute package-local executable/);
|
|
1026
477
|
});
|
|
1027
478
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
const { readFile, stat } = await import("node:fs/promises");
|
|
1040
|
-
observed.push({ flag: request.arguments[index]!, file: path, mode: (await stat(path)).mode & 0o777, content: await readFile(path, "utf8") });
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
return { stdout: await fixture("finalize"), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
1044
|
-
};
|
|
1045
|
-
await new NativeReviewCliV213(adapter).finalize({
|
|
1046
|
-
cwd: "/repo",
|
|
1047
|
-
lineageId: "lineage-1",
|
|
1048
|
-
lensResults: [{ lens: "review-risk", document: { lens: "risk", findings: [], evidence: ["complete candidate reviewed"] } }],
|
|
1049
|
-
refuterDocument: { results: [{ finding_id: "RISK-001", outcome: "inconclusive", proof_refs: ["differential-test:candidate still fails"] }] },
|
|
1050
|
-
validationDocument: { original_criteria: { passed: false, evidence: ["acceptance still fails"] }, correction_regression: { passed: true, evidence: ["regression suite passes"] }, follow_ups: [{ observation: "Track the remaining failure", proof_refs: ["differential-test:candidate still fails"] }] },
|
|
1051
|
-
evidenceDocument: " focused verification failed\n\n",
|
|
1052
|
-
failed: true,
|
|
479
|
+
// O012 migrated from the retired per-operation version probe. V216 executes
|
|
480
|
+
// negotiated argv directly; this test proves the argv boundary without
|
|
481
|
+
// resurrecting NativeReviewCliV213 or capability/version negotiation.
|
|
482
|
+
test("native client executes current negotiated argv without a shell", async () => {
|
|
483
|
+
const literal = "$GENTLE_PI_MUST_NOT_EXPAND";
|
|
484
|
+
const result = await createNodeExecFileAdapter()({
|
|
485
|
+
file: process.execPath,
|
|
486
|
+
arguments: ["-p", "process.argv[1]", literal],
|
|
487
|
+
cwd: process.cwd(),
|
|
488
|
+
timeoutMs: 30_000,
|
|
489
|
+
maxBufferBytes: 1024,
|
|
1053
490
|
});
|
|
1054
|
-
assert.
|
|
1055
|
-
assert.
|
|
1056
|
-
assert.
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
491
|
+
assert.equal(result.exitCode, 0);
|
|
492
|
+
assert.equal(result.stderr, "");
|
|
493
|
+
assert.equal(result.stdout.trim(), literal);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("current STATUS preserves bounded output configuration and output-limit precedence", async () => {
|
|
497
|
+
const previous = process.env.GENTLE_PI_REVIEW_MAX_BUFFER_BYTES;
|
|
498
|
+
try {
|
|
499
|
+
for (const [environmentValue, expectedBuffer] of [[undefined, NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES], ["2097152", 2_097_152], ["invalid", NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES]] as const) {
|
|
500
|
+
if (environmentValue === undefined) delete process.env.GENTLE_PI_REVIEW_MAX_BUFFER_BYTES;
|
|
501
|
+
else process.env.GENTLE_PI_REVIEW_MAX_BUFFER_BYTES = environmentValue;
|
|
502
|
+
const calls: Array<{ timeoutMs: number | undefined; maxBufferBytes: number }> = [];
|
|
503
|
+
const adapter: ExecFileAdapter = async (request) => {
|
|
504
|
+
calls.push({ timeoutMs: request.timeoutMs, maxBufferBytes: request.maxBufferBytes });
|
|
505
|
+
return { stdout: JSON.stringify(currentStatusFixture()), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
506
|
+
};
|
|
507
|
+
await new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai").targetStatus({ cwd: "/repo" });
|
|
508
|
+
assert.deepEqual(calls, [{ timeoutMs: 30_000, maxBufferBytes: expectedBuffer }]);
|
|
509
|
+
}
|
|
510
|
+
} finally {
|
|
511
|
+
if (previous === undefined) delete process.env.GENTLE_PI_REVIEW_MAX_BUFFER_BYTES;
|
|
512
|
+
else process.env.GENTLE_PI_REVIEW_MAX_BUFFER_BYTES = previous;
|
|
513
|
+
}
|
|
1065
514
|
|
|
1066
|
-
|
|
1067
|
-
|
|
515
|
+
const outputLimited = new NativeReviewCliV216(async () => ({
|
|
516
|
+
stdout: "", stderr: "", exitCode: 0, signal: "SIGTERM", timedOut: true, outputLimitExceeded: true,
|
|
517
|
+
}), "/package/.gentle-ai/gentle-ai");
|
|
1068
518
|
await assert.rejects(
|
|
1069
|
-
() =>
|
|
1070
|
-
|
|
519
|
+
() => outputLimited.targetStatus({ cwd: "/repo" }),
|
|
520
|
+
(error: unknown) => error instanceof NativeReviewCliError
|
|
521
|
+
&& error.code === NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT
|
|
522
|
+
&& error.mutationOutcome === "none"
|
|
523
|
+
&& error.diagnostics.timed_out === false
|
|
524
|
+
&& error.diagnostics.output_limit_exceeded === true
|
|
525
|
+
&& error.diagnostics.max_buffer_bytes === NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES
|
|
526
|
+
&& error.diagnostics.configuration_hint === "Inspect native review state before any new START; GENTLE_PI_REVIEW_MAX_BUFFER_BYTES accepts a positive decimal up to 67108864.",
|
|
1071
527
|
);
|
|
1072
|
-
assert.equal(queue.calls.length, 0);
|
|
1073
528
|
});
|
|
1074
529
|
|
|
1075
|
-
test("
|
|
530
|
+
test("node execFile adapter passes AbortSignal to child_process", async () => {
|
|
531
|
+
const controller = new AbortController();
|
|
532
|
+
const pending = createNodeExecFileAdapter()({
|
|
533
|
+
file: process.execPath,
|
|
534
|
+
arguments: ["-e", "setTimeout(() => {}, 10_000)"],
|
|
535
|
+
cwd: process.cwd(),
|
|
536
|
+
timeoutMs: 30_000,
|
|
537
|
+
maxBufferBytes: 1024,
|
|
538
|
+
signal: controller.signal,
|
|
539
|
+
});
|
|
540
|
+
controller.abort();
|
|
541
|
+
await assert.rejects(pending, (error: unknown) => error instanceof Error && error.name === "AbortError");
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("capture-result receives the controller AbortSignal without an automatic mutation timeout", async () => {
|
|
545
|
+
const controller = new AbortController();
|
|
546
|
+
controller.abort();
|
|
547
|
+
let receivedSignal: AbortSignal | undefined;
|
|
548
|
+
let receivedTimeout: number | undefined;
|
|
1076
549
|
const adapter: ExecFileAdapter = async (request) => {
|
|
1077
|
-
|
|
550
|
+
receivedSignal = request.signal;
|
|
551
|
+
receivedTimeout = request.timeoutMs;
|
|
1078
552
|
const error = new Error("cancelled");
|
|
1079
553
|
error.name = "AbortError";
|
|
1080
554
|
throw error;
|
|
1081
555
|
};
|
|
1082
556
|
await assert.rejects(
|
|
1083
|
-
() => new
|
|
557
|
+
() => new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai").captureResult({
|
|
558
|
+
argumentTokens: [`--repository-context=${REPOSITORY_CONTEXT}`],
|
|
559
|
+
resultDocument: "{}",
|
|
560
|
+
signal: controller.signal,
|
|
561
|
+
}),
|
|
1084
562
|
(error: unknown) => error instanceof NativeReviewCliError
|
|
1085
563
|
&& error.code === NATIVE_REVIEW_ERROR_CODE.CANCELLED
|
|
1086
564
|
&& error.mutationOutcome === "unknown"
|
|
1087
565
|
&& error.nextAction === "review.status",
|
|
1088
566
|
);
|
|
567
|
+
assert.equal(receivedSignal, controller.signal);
|
|
568
|
+
assert.equal(receivedTimeout, undefined);
|
|
1089
569
|
});
|
|
1090
570
|
|
|
1091
|
-
test("
|
|
1092
|
-
|
|
1093
|
-
const pending = createNodeExecFileAdapter()({ file: process.execPath, arguments: ["-e", "setTimeout(() => {}, 10_000)"], cwd: process.cwd(), timeoutMs: 30_000, maxBufferBytes: 1024, signal: controller.signal });
|
|
1094
|
-
controller.abort();
|
|
1095
|
-
await assert.rejects(pending, (error: unknown) => error instanceof Error && error.name === "AbortError");
|
|
571
|
+
test("native review client leaves SDD status resolution to the local SDD engine", () => {
|
|
572
|
+
assert.equal("sddStatus" in client(queuedAdapter([]).adapter), false);
|
|
1096
573
|
});
|
|
1097
574
|
|
|
1098
|
-
test("
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
575
|
+
test("read-only authority inventory rejects a repository identity mismatch after decoding", async () => {
|
|
576
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify({
|
|
577
|
+
schema: "gentle-ai.review-authority-status/v1", operation: "review/status", repository: "/foreign-repository",
|
|
578
|
+
complete: true, authoritative: true, status: "clean", entries: [], locks: [], diagnostics: [],
|
|
579
|
+
}) }]);
|
|
580
|
+
await assert.rejects(
|
|
581
|
+
() => client(queue.adapter).reviewStatus({ cwd: "/repo" }),
|
|
582
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH && error.mutationOutcome === "none",
|
|
583
|
+
);
|
|
584
|
+
assert.deepEqual(queue.calls[0]?.arguments, ["review", "status", "--cwd", "/repo"]);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
test("current native process boundary preserves caps, timeout classes, and sanitized diagnostics", async () => {
|
|
589
|
+
const calls: Array<{ timeoutMs: number | undefined; maxBufferBytes: number }> = [];
|
|
1102
590
|
const adapter: ExecFileAdapter = async (request) => {
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
const error = new Error("cancelled");
|
|
1107
|
-
error.name = "AbortError";
|
|
1108
|
-
throw error;
|
|
591
|
+
calls.push({ timeoutMs: request.timeoutMs, maxBufferBytes: request.maxBufferBytes });
|
|
592
|
+
if (request.arguments[1] === "capture-result") {
|
|
593
|
+
return { stdout: JSON.stringify(fixture("last-event-capture-result-approved.captured.json")), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
1109
594
|
}
|
|
1110
|
-
|
|
595
|
+
return { stdout: JSON.stringify(currentStatusFixture()), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
1111
596
|
};
|
|
597
|
+
const review = new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 321, 654);
|
|
598
|
+
await review.targetStatus({ cwd: "/repo" });
|
|
599
|
+
await review.captureResult({ argumentTokens: [`--repository-context=${REPOSITORY_CONTEXT}`], resultDocument: "{}" });
|
|
600
|
+
assert.deepEqual(calls, [{ timeoutMs: 321, maxBufferBytes: 654 }, { timeoutMs: undefined, maxBufferBytes: 654 }]);
|
|
601
|
+
|
|
602
|
+
const diagnostics = queuedAdapter([{ stdout: JSON.stringify(currentStatusFixture()), stderr: "\u001b]8;;https://example.invalid/token\u0007token=super-secret\n" }]);
|
|
1112
603
|
await assert.rejects(
|
|
1113
|
-
() =>
|
|
604
|
+
() => client(diagnostics.adapter).targetStatus({ cwd: "/repo" }),
|
|
1114
605
|
(error: unknown) => error instanceof NativeReviewCliError
|
|
1115
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.
|
|
1116
|
-
&& error.
|
|
1117
|
-
&& error.
|
|
606
|
+
&& error.code === NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR
|
|
607
|
+
&& !String(error.diagnostics.stderr).includes("super-secret")
|
|
608
|
+
&& !/[\u0000-\u001f\u007f]/.test(String(error.diagnostics.stderr)),
|
|
609
|
+
);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test("current review STATUS retains compact snapshot and released-lock wire fields", async (t) => {
|
|
613
|
+
const repository = process.cwd();
|
|
614
|
+
const snapshotIdentity = `sha256:${"d".repeat(64)}`;
|
|
615
|
+
const body = {
|
|
616
|
+
schema: "gentle-ai.review-authority-status/v1",
|
|
617
|
+
operation: "review/status",
|
|
618
|
+
repository,
|
|
619
|
+
complete: true,
|
|
620
|
+
authoritative: true,
|
|
621
|
+
status: "active",
|
|
622
|
+
entries: [{ version: "compact-v2", lineage_id: "current-lineage", path: `${repository}/.git/gentle-ai`, status: "active", revision: `sha256:${"e".repeat(64)}`, snapshot_identity: snapshotIdentity, problems: [] }],
|
|
623
|
+
locks: [{ version: "compact-v2", path: `${repository}/.git/gentle-ai/LOCK`, status: "released" }],
|
|
624
|
+
diagnostics: [],
|
|
625
|
+
};
|
|
626
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify(body) }]);
|
|
627
|
+
const status = await client(queue.adapter).reviewStatus({ cwd: repository });
|
|
628
|
+
assert.equal(status.entries[0]?.snapshotIdentity, snapshotIdentity);
|
|
629
|
+
assert.deepEqual(status.locks, [{ version: "compact-v2", path: `${repository}/.git/gentle-ai/LOCK`, status: "released" }]);
|
|
630
|
+
for (const lockStatus of ["Released", "stale", "future"]) {
|
|
631
|
+
const malformed = queuedAdapter([{ stdout: JSON.stringify({ ...body, locks: [{ ...body.locks[0], status: lockStatus }] }) }]);
|
|
632
|
+
await assert.rejects(
|
|
633
|
+
() => client(malformed.adapter).reviewStatus({ cwd: repository }),
|
|
634
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE,
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
if (process.platform !== "win32") {
|
|
638
|
+
const root = mkdtempSync(join(tmpdir(), "gentle-pi-native-status-"));
|
|
639
|
+
const alias = `${root}-alias`;
|
|
640
|
+
symlinkSync(root, alias, "dir");
|
|
641
|
+
t.after(() => {
|
|
642
|
+
rmSync(alias, { force: true });
|
|
643
|
+
rmSync(root, { recursive: true, force: true });
|
|
644
|
+
});
|
|
645
|
+
const canonical = queuedAdapter([{ stdout: JSON.stringify({ ...body, repository: root }) }]);
|
|
646
|
+
assert.equal((await client(canonical.adapter).reviewStatus({ cwd: alias })).repository, root);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// ---------------------------------------------------------------------------
|
|
651
|
+
// acknowledge-approved — gentle-ai #3947 made the burn print one typed
|
|
652
|
+
// `gentle-ai.review-acknowledged/v1` envelope; every published release up to
|
|
653
|
+
// v2.5.0-rc.3 burns in silence. Both are a successful burn; anything else on
|
|
654
|
+
// stdout is not.
|
|
655
|
+
// ---------------------------------------------------------------------------
|
|
656
|
+
|
|
657
|
+
const ACKNOWLEDGED_LINEAGE = "review-3ec95251db75f626";
|
|
658
|
+
const ACKNOWLEDGED_TARGET = "sha256:b505dcd8d82395c053c9786935e11e0e235cbdecca4f1f46f98c768ea6248d3d";
|
|
659
|
+
const ACKNOWLEDGED_REVISION = "sha256:9732b1c3526bfecd3851093239241145c970cc126acea59bfaf14133214b60ee";
|
|
660
|
+
const ACKNOWLEDGEMENT_TOKENS = [`--cwd=/repo`, `--lineage=${ACKNOWLEDGED_LINEAGE}`, `--target=${ACKNOWLEDGED_TARGET}`, `--expected-revision=${ACKNOWLEDGED_REVISION}`, "--token=provider-issued-once"] as const;
|
|
661
|
+
const ACKNOWLEDGED_BINDING = { lineageId: ACKNOWLEDGED_LINEAGE, targetIdentity: ACKNOWLEDGED_TARGET, revision: ACKNOWLEDGED_REVISION } as const;
|
|
662
|
+
|
|
663
|
+
test("acknowledge-approved keeps accepting the pinned silent burn byte-for-byte", async () => {
|
|
664
|
+
const queue = queuedAdapter([{ stdout: "" }]);
|
|
665
|
+
const result = await client(queue.adapter).acknowledgeApproved({ argumentTokens: ACKNOWLEDGEMENT_TOKENS, cwd: "/repo", binding: ACKNOWLEDGED_BINDING });
|
|
666
|
+
assert.equal(result, undefined);
|
|
667
|
+
assert.deepEqual(queue.calls[0]?.arguments, ["review", "acknowledge-approved", ...ACKNOWLEDGEMENT_TOKENS]);
|
|
668
|
+
assert.equal(queue.calls[0]?.cwd, "/repo");
|
|
669
|
+
assert.equal(queue.calls[0]?.timeoutMs, undefined);
|
|
670
|
+
assert.equal(queue.calls.length, 1);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
test("acknowledge-approved decodes the captured review-acknowledged/v1 burn envelope", async () => {
|
|
674
|
+
const raw = readFileSync(join(process.cwd(), "tests", "fixtures", "devbinary", "review-acknowledged-v1.captured.json"), "utf8");
|
|
675
|
+
const queue = queuedAdapter([{ stdout: raw }]);
|
|
676
|
+
const result = await client(queue.adapter).acknowledgeApproved({ argumentTokens: ACKNOWLEDGEMENT_TOKENS, cwd: "/repo", binding: ACKNOWLEDGED_BINDING });
|
|
677
|
+
assert.equal(result?.schema, "gentle-ai.review-acknowledged/v1");
|
|
678
|
+
assert.deepEqual(
|
|
679
|
+
{ lineageId: result?.lineageId, targetIdentity: result?.targetIdentity, consumedRevision: result?.consumedRevision, authority: result?.authority },
|
|
680
|
+
{ lineageId: ACKNOWLEDGED_LINEAGE, targetIdentity: ACKNOWLEDGED_TARGET, consumedRevision: ACKNOWLEDGED_REVISION, authority: "burned" },
|
|
681
|
+
);
|
|
682
|
+
assert.deepEqual(queue.calls[0]?.arguments, ["review", "acknowledge-approved", ...ACKNOWLEDGEMENT_TOKENS]);
|
|
683
|
+
assert.equal(queue.calls.length, 1);
|
|
684
|
+
|
|
685
|
+
const unbound = await client(queuedAdapter([{ stdout: raw }]).adapter).acknowledgeApproved({ argumentTokens: ACKNOWLEDGEMENT_TOKENS, cwd: "/repo" });
|
|
686
|
+
assert.equal(unbound?.consumedRevision, ACKNOWLEDGED_REVISION);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test("acknowledge-approved fails closed on a foreign, drifted, or mismatched success body", async () => {
|
|
690
|
+
const raw = JSON.parse(readFileSync(join(process.cwd(), "tests", "fixtures", "devbinary", "review-acknowledged-v1.captured.json"), "utf8")) as Record<string, unknown>;
|
|
691
|
+
const bodies: Array<[string, string, { lineageId: string; targetIdentity: string; revision: string }]> = [
|
|
692
|
+
["a STATUS envelope", JSON.stringify(fixture("status-v5.captured.json")), ACKNOWLEDGED_BINDING],
|
|
693
|
+
["a last-event closure", JSON.stringify(fixture("last-event-capture-result-approved.captured.json")), ACKNOWLEDGED_BINDING],
|
|
694
|
+
["an envelope with an unknown field", JSON.stringify({ ...raw, receipt: {} }), ACKNOWLEDGED_BINDING],
|
|
695
|
+
["an envelope naming another lineage", JSON.stringify(raw), { ...ACKNOWLEDGED_BINDING, lineageId: "review-other" }],
|
|
696
|
+
["an envelope naming another revision", JSON.stringify(raw), { ...ACKNOWLEDGED_BINDING, revision: `sha256:${"c".repeat(64)}` }],
|
|
697
|
+
["non-JSON output", "acknowledged\n", ACKNOWLEDGED_BINDING],
|
|
698
|
+
];
|
|
699
|
+
for (const [name, stdout, binding] of bodies) {
|
|
700
|
+
const queue = queuedAdapter([{ stdout }]);
|
|
701
|
+
await assert.rejects(
|
|
702
|
+
() => client(queue.adapter).acknowledgeApproved({ argumentTokens: ACKNOWLEDGEMENT_TOKENS, cwd: "/repo", binding }),
|
|
703
|
+
(error: unknown) => error instanceof NativeReviewCliError
|
|
704
|
+
&& (error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE || error.code === NATIVE_REVIEW_ERROR_CODE.MALFORMED_JSON)
|
|
705
|
+
&& error.mutationOutcome === "unknown",
|
|
706
|
+
name,
|
|
707
|
+
);
|
|
708
|
+
assert.equal(queue.calls.length, 1, name);
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test("acknowledge-approved keeps its fail-closed stderr and typed refusal discipline around the envelope", async () => {
|
|
713
|
+
const raw = readFileSync(join(process.cwd(), "tests", "fixtures", "devbinary", "review-acknowledged-v1.captured.json"), "utf8");
|
|
714
|
+
await assert.rejects(
|
|
715
|
+
() => client(queuedAdapter([{ stdout: raw, stderr: "note: burned\n" }]).adapter).acknowledgeApproved({ argumentTokens: ACKNOWLEDGEMENT_TOKENS, cwd: "/repo", binding: ACKNOWLEDGED_BINDING }),
|
|
716
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR,
|
|
717
|
+
);
|
|
718
|
+
await assert.rejects(
|
|
719
|
+
() => client(queuedAdapter([{ stdout: "", stderr: "note: burned\n" }]).adapter).acknowledgeApproved({ argumentTokens: ACKNOWLEDGEMENT_TOKENS, cwd: "/repo", binding: ACKNOWLEDGED_BINDING }),
|
|
720
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR,
|
|
721
|
+
);
|
|
722
|
+
const failure = {
|
|
723
|
+
schema: "gentle-ai.review-integration.failure/v2",
|
|
724
|
+
contract: "gentle-ai.review-integration/v2",
|
|
725
|
+
operation: "review.capture-result",
|
|
726
|
+
phase: "pre_native",
|
|
727
|
+
code: "gate_scope_changed",
|
|
728
|
+
message: "the target scope changed since START",
|
|
729
|
+
mutation_outcome: "not_started",
|
|
730
|
+
authority_applicability: "current_target",
|
|
731
|
+
retry_safe: true,
|
|
732
|
+
replayability: "manual_action_required",
|
|
733
|
+
required_inputs: [],
|
|
734
|
+
next_action: "explicit-maintainer-action",
|
|
735
|
+
};
|
|
736
|
+
await assert.rejects(
|
|
737
|
+
() => client(queuedAdapter([{ stdout: JSON.stringify(failure), exitCode: 1 }]).adapter).acknowledgeApproved({ argumentTokens: ACKNOWLEDGEMENT_TOKENS, cwd: "/repo", binding: ACKNOWLEDGED_BINDING }),
|
|
738
|
+
(error: unknown) => error instanceof Error && error.name === "NativeReviewIntegrationError",
|
|
739
|
+
);
|
|
740
|
+
await assert.rejects(
|
|
741
|
+
() => client(queuedAdapter([{ stdout: "", stderr: "Error: approved acknowledgement names no live compact authority\n", exitCode: 1 }]).adapter).acknowledgeApproved({ argumentTokens: ACKNOWLEDGEMENT_TOKENS, cwd: "/repo", binding: ACKNOWLEDGED_BINDING }),
|
|
742
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.EMPTY_OUTPUT,
|
|
1118
743
|
);
|
|
1119
|
-
assert.equal(mutationTimeoutMs, undefined);
|
|
1120
744
|
});
|