gentle-pi 2.1.2 → 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 +108 -79
- package/assets/agents/gentle-ai-worker.md +7 -3
- package/assets/agents/jd-fix-agent.md +1 -1
- package/assets/agents/jd-judge-a.md +3 -1
- package/assets/agents/jd-judge-b.md +3 -1
- package/assets/agents/review-readability.md +4 -3
- package/assets/agents/review-reliability.md +4 -3
- package/assets/agents/review-resilience.md +4 -3
- package/assets/agents/review-risk.md +4 -3
- package/assets/agents/sdd-apply.md +11 -4
- package/assets/agents/sdd-archive.md +6 -1
- package/assets/agents/sdd-design.md +6 -1
- package/assets/agents/sdd-explore.md +6 -2
- package/assets/agents/sdd-init.md +10 -2
- package/assets/agents/sdd-onboard.md +6 -1
- package/assets/agents/sdd-proposal.md +8 -1
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +10 -5
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +8 -4
- package/assets/agents/sdd-verify.md +27 -2
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +2 -2
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +2 -2
- package/assets/orchestrator-delegation.md +145 -160
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +25 -48
- package/assets/sdd-orchestrator-workflow.md +163 -25
- package/assets/support/sdd-status-contract.md +24 -6
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
- package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
- package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
- package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
- package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
- package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
- package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
- package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
- package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
- package/docs/native-authority-architecture.md +14 -16
- package/docs/review-integration.md +27 -353
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +2710 -3211
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +21 -12
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +676 -781
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +836 -118
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +578 -0
- package/lib/review-integration-v2.ts +1254 -221
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +230 -67
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -79
- package/runtime/gentle-ai-binary.mjs +174 -3
- package/runtime/native-review-cli.mjs +610 -715
- package/runtime/review-integration-v2.mjs +1223 -190
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +85 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +20 -9
- package/scripts/verify-package-files.mjs +119 -43
- package/skills/_shared/review-ledger-contract.md +11 -19
- 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/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +9 -5
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- 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/background-subagents.test.ts +771 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +16 -0
- package/tests/delegated-key-learnings-contract.test.ts +240 -0
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
- package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
- package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
- package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
- package/tests/fixtures/devbinary/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/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -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-consent-declined.captured.json +19 -0
- package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
- package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
- package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +159 -0
- package/tests/gentle-ai-installer.test.ts +138 -48
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +601 -0
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +75 -2
- package/tests/native-review-cli.test.ts +588 -908
- package/tests/native-review-consent.test.ts +283 -38
- package/tests/native-review-parity-runtime.test.ts +108 -350
- package/tests/native-review-parity.test.ts +684 -732
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +158 -89
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +128 -100
- package/tests/provider-contract-bundle.test.ts +385 -0
- package/tests/provider-contract-mirror.test.ts +206 -0
- package/tests/provider-defect-handoff.test.ts +252 -0
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +898 -12
- package/tests/review-compact-contract.test.ts +29 -122
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +366 -857
- package/tests/review-controller-native-routing.test.ts +1253 -4199
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +236 -70
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +134 -0
- package/tests/review-dispatch-hydration-gap.test.ts +145 -0
- 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 +352 -0
- package/tests/review-host-relay.test.ts +754 -0
- package/tests/review-integration-v2-forward.test.ts +654 -0
- package/tests/review-integration-v2.test.ts +317 -129
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +106 -60
- package/tests/review-recovered-lineage-routing.test.ts +199 -0
- package/tests/review-relay-transport-agent.test.ts +322 -0
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +573 -180
- package/tests/sdd-agent-tools.test.ts +53 -32
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +6 -8
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- 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/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- package/lib/git-commit-transaction.ts +0 -801
- package/lib/native-review-remediation.ts +0 -49
- package/lib/review-compact.ts +0 -947
- package/lib/review-refuter-adapter.ts +0 -129
- package/lib/review-runtime-contract.ts +0 -68
- package/prompts/gcl.md +0 -54
- package/prompts/gis.md +0 -25
- package/prompts/gpr.md +0 -41
- package/prompts/gwr.md +0 -31
- package/runtime/git-commit-transaction.mjs +0 -802
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
- package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
- package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
- package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
- package/tests/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 -302
- package/tests/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
|
@@ -1,26 +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
6
|
import {
|
|
7
|
+
decodeReviewConsentV3,
|
|
8
|
+
} from "../lib/review-integration-v2.ts";
|
|
9
|
+
import {
|
|
10
|
+
NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES,
|
|
7
11
|
NATIVE_REVIEW_ERROR_CODE,
|
|
8
12
|
NativeReviewCliError,
|
|
9
|
-
|
|
13
|
+
NativeReviewCliV216,
|
|
10
14
|
createNodeExecFileAdapter,
|
|
11
15
|
type ExecFileAdapter,
|
|
12
|
-
type NativeStartRequest,
|
|
13
16
|
} from "../lib/native-review-cli.ts";
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class NativeReviewCliV213 extends NativeReviewCliV213Production {
|
|
19
|
-
constructor(...parameters: ConstructorParameters<typeof NativeReviewCliV213Production>) {
|
|
20
|
-
const [adapter, executable, ...rest] = parameters;
|
|
21
|
-
super(adapter, executable ?? "/package/.gentle-ai/gentle-ai", ...rest);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
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>;
|
|
24
21
|
|
|
25
22
|
interface QueuedResult {
|
|
26
23
|
stdout: string;
|
|
@@ -31,14 +28,18 @@ interface QueuedResult {
|
|
|
31
28
|
outputLimitExceeded?: boolean;
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
function queuedAdapter(results: QueuedResult[]): {
|
|
35
|
-
|
|
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 }> = [];
|
|
36
37
|
return {
|
|
37
38
|
calls,
|
|
38
39
|
adapter: async (request) => {
|
|
39
|
-
calls.push(request);
|
|
40
|
-
const result =
|
|
41
|
-
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");
|
|
42
43
|
return {
|
|
43
44
|
stdout: result.stdout,
|
|
44
45
|
stderr: result.stderr ?? "",
|
|
@@ -51,1014 +52,693 @@ function queuedAdapter(results: QueuedResult[]): { adapter: ExecFileAdapter; cal
|
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
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" }) };
|
|
58
|
-
const REVIEW_STATUS = {
|
|
59
|
-
stdout: JSON.stringify({
|
|
60
|
-
schema: "gentle-ai.review-authority-status/v1",
|
|
61
|
-
operation: "review/status",
|
|
62
|
-
repository: "C:\\repo with spaces",
|
|
63
|
-
complete: true,
|
|
64
|
-
authoritative: true,
|
|
65
|
-
status: "clean",
|
|
66
|
-
entries: [],
|
|
67
|
-
locks: [],
|
|
68
|
-
diagnostics: [],
|
|
69
|
-
}),
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
test("native START supports every declared version with the START capability and requires the workspace projection", async () => {
|
|
73
|
-
const start = JSON.parse(START.stdout) as Record<string, unknown>;
|
|
74
|
-
assert.equal((await new NativeReviewCliV213(queuedAdapter([VERSION, { stdout: JSON.stringify({ ...start, projection: "workspace" }) }]).adapter).start({ cwd: "/repo" })).lineageId, "lineage-1");
|
|
75
|
-
assert.equal((await new NativeReviewCliV213(queuedAdapter([STATUS_VERSION, { stdout: JSON.stringify({ ...start, projection: "workspace" }) }]).adapter).start({ cwd: "/repo" })).lineageId, "lineage-1");
|
|
76
|
-
const missing = Object.fromEntries(Object.entries(start).filter(([key]) => key !== "projection"));
|
|
77
|
-
for (const body of [missing, { ...start, projection: "repository" }]) {
|
|
78
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify(body) }]);
|
|
79
|
-
await assert.rejects(() => new NativeReviewCliV213(queue.adapter).start({ cwd: "/repo" }), (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
55
|
+
function client(adapter: ExecFileAdapter): NativeReviewCliV216 {
|
|
56
|
+
return new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024);
|
|
57
|
+
}
|
|
82
58
|
|
|
83
|
-
test("
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
await client.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
["review", "start", "--cwd", "/repo with spaces"],
|
|
93
|
-
["version"],
|
|
94
|
-
["review", "start", "--cwd", "/repo with spaces", "--base-ref", "origin/main", "--committed-only"],
|
|
95
|
-
["version"],
|
|
96
|
-
["review", "start", "--cwd", "/repo with spaces", "--policy", "/repo with spaces/.gentle-ai/policies/team policy.json"],
|
|
97
|
-
["version"],
|
|
98
|
-
["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",
|
|
99
68
|
]);
|
|
100
|
-
assert.equal(queue.calls
|
|
69
|
+
assert.equal(queue.calls[0]?.timeoutMs, 30_000);
|
|
101
70
|
});
|
|
102
71
|
|
|
103
|
-
test("
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
lenses_required: false,
|
|
111
|
-
}),
|
|
112
|
-
}]);
|
|
113
|
-
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
|
+
});
|
|
114
79
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
]) {
|
|
120
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify({ ...start, ...scenario, selected_lenses: null }) }]);
|
|
121
|
-
await assert.rejects(() => new NativeReviewCliV213(queue.adapter).start({ cwd: "/repo" }), NativeReviewCliError);
|
|
122
|
-
}
|
|
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);
|
|
123
84
|
});
|
|
124
85
|
|
|
125
|
-
test("
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
{ action: "blocked-scope-action", lenses_required: false, risk_level: "low", selected_lenses: [] },
|
|
134
|
-
] as const;
|
|
135
|
-
for (const scenario of valid) {
|
|
136
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify({ ...start, ...scenario }) }]);
|
|
137
|
-
const result = await new NativeReviewCliV213(queue.adapter).start({ cwd: "/repo" });
|
|
138
|
-
assert.equal(result.action, scenario.action);
|
|
139
|
-
assert.equal(result.lensesRequired, scenario.lenses_required);
|
|
140
|
-
assert.deepEqual(result.selectedLenses, scenario.selected_lenses);
|
|
141
|
-
}
|
|
142
|
-
for (const scenario of [
|
|
143
|
-
{ action: "created", lenses_required: false, risk_level: "medium", selected_lenses: [] },
|
|
144
|
-
{ action: "created", lenses_required: false, risk_level: "low", selected_lenses: ["review-reliability"] },
|
|
145
|
-
{ action: "reuse-receipt", lenses_required: true, risk_level: "medium", selected_lenses: ["review-reliability"] },
|
|
146
|
-
{ action: "blocked-scope-action", lenses_required: true, risk_level: "medium", selected_lenses: ["review-reliability"] },
|
|
147
|
-
{ action: "resumed", lenses_required: true, risk_level: "medium", selected_lenses: ["review-reliability"], state: "correction_required" },
|
|
148
|
-
] as const) {
|
|
149
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify({ ...start, ...scenario }) }]);
|
|
150
|
-
await assert.rejects(() => new NativeReviewCliV213(queue.adapter).start({ cwd: "/repo" }), NativeReviewCliError);
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("native START binds every risk tier to canonical unique lenses before controller use", async () => {
|
|
155
|
-
const start = JSON.parse(START.stdout) as Record<string, unknown>;
|
|
156
|
-
const high = ["review-risk", "review-resilience", "review-readability", "review-reliability"];
|
|
157
|
-
const valid = queuedAdapter([VERSION, { stdout: JSON.stringify({ ...start, risk_level: "high", selected_lenses: high, lenses_required: false, action: "resumed", state: "validating" }) }]);
|
|
158
|
-
assert.deepEqual((await new NativeReviewCliV213(valid.adapter).start({ cwd: "/repo" })).selectedLenses, high);
|
|
159
|
-
for (const scenario of [
|
|
160
|
-
{ risk_level: "low", selected_lenses: [], lenses_required: true }, { risk_level: "medium", selected_lenses: ["review-risk", "review-reliability"], lenses_required: true },
|
|
161
|
-
{ risk_level: "medium", selected_lenses: ["review-reliability", "review-reliability"], lenses_required: true },
|
|
162
|
-
{ risk_level: "high", selected_lenses: high.slice(0, 3), lenses_required: true }, { risk_level: "high", selected_lenses: [...high, "review-risk"], lenses_required: true },
|
|
163
|
-
{ action: "created", risk_level: "high", selected_lenses: high, lenses_required: false },
|
|
164
|
-
{ action: "resumed", state: "approved", risk_level: "medium", selected_lenses: ["review-reliability"], lenses_required: true },
|
|
165
|
-
] as const) {
|
|
166
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify({ ...start, ...scenario }) }]);
|
|
167
|
-
await assert.rejects(() => new NativeReviewCliV213(queue.adapter).start({ cwd: "/repo" }), NativeReviewCliError);
|
|
168
|
-
}
|
|
169
|
-
});
|
|
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
|
+
});
|
|
170
94
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const client = new NativeReviewCliV213(queue.adapter);
|
|
174
|
-
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"]);
|
|
175
97
|
await assert.rejects(
|
|
176
|
-
() => client.
|
|
177
|
-
|
|
178
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE
|
|
179
|
-
&& error.operation === "version",
|
|
98
|
+
() => client(queuedAdapter([]).adapter).captureCorrectionPlan({ argumentTokens: ["--a={{value}}", "--b={{value}}"], correctionLines: 1, cwd: "/repo" }),
|
|
99
|
+
/CAPTURE_CORRECTION_PLAN requires exactly one/,
|
|
180
100
|
);
|
|
181
|
-
assert.deepEqual(queue.calls.map((call) => call.arguments), [
|
|
182
|
-
["version"],
|
|
183
|
-
["review", "start", "--cwd", "/repo"],
|
|
184
|
-
["version"],
|
|
185
|
-
]);
|
|
186
101
|
});
|
|
187
102
|
|
|
188
|
-
test("
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
{
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
await assert.rejects(() => new NativeReviewCliV213(queue.adapter).start(request as NativeStartRequest), TypeError);
|
|
204
|
-
assert.equal(queue.calls.length, 0);
|
|
205
|
-
}
|
|
206
|
-
});
|
|
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;
|
|
207
118
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
);
|
|
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"]);
|
|
218
128
|
}
|
|
219
|
-
|
|
129
|
+
|
|
220
130
|
await assert.rejects(
|
|
221
|
-
() =>
|
|
222
|
-
|
|
131
|
+
() => client(queuedAdapter([]).adapter).captureProviderRole({ captureOperation: "review.capture-result", argumentTokens: ["--agent=pi"], cwd: "/repo" }),
|
|
132
|
+
/CAPTURE_PROVIDER_ROLE supports only/,
|
|
223
133
|
);
|
|
224
134
|
});
|
|
225
135
|
|
|
226
|
-
test("
|
|
227
|
-
const
|
|
228
|
-
const remediationState = source.remediationState as Record<string, unknown>;
|
|
229
|
-
for (const correctionBudget of [undefined, 0, 17]) {
|
|
230
|
-
const queue = queuedAdapter([VERSION_219, {
|
|
231
|
-
stdout: JSON.stringify({
|
|
232
|
-
...source,
|
|
233
|
-
remediationState: {
|
|
234
|
-
...remediationState,
|
|
235
|
-
...(correctionBudget === undefined ? {} : { correctionBudget }),
|
|
236
|
-
},
|
|
237
|
-
}),
|
|
238
|
-
}]);
|
|
239
|
-
const status = await new NativeReviewCliV213(queue.adapter).sddStatus({ cwd: "/repo", change: "native-review-authority-parity" });
|
|
240
|
-
assert.equal((status.remediationState as Record<string, unknown>).correctionBudget, correctionBudget);
|
|
241
|
-
}
|
|
242
|
-
for (const correctionBudget of [-1, 1.5]) {
|
|
243
|
-
const queue = queuedAdapter([VERSION_219, { stdout: JSON.stringify({ ...source, remediationState: { ...remediationState, correctionBudget } }) }]);
|
|
244
|
-
await assert.rejects(() => new NativeReviewCliV213(queue.adapter).sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }), NativeReviewCliError);
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
test("version process failures retain their typed failure code", async () => {
|
|
249
|
-
for (const result of [
|
|
250
|
-
{ stdout: "", timedOut: true, code: NATIVE_REVIEW_ERROR_CODE.TIMEOUT },
|
|
251
|
-
{ stdout: "", exitCode: 2, code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO },
|
|
252
|
-
{ stdout: "", signal: "SIGTERM" as NodeJS.Signals, code: NATIVE_REVIEW_ERROR_CODE.SIGNAL },
|
|
253
|
-
{ stdout: "", outputLimitExceeded: true, code: NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT },
|
|
254
|
-
]) {
|
|
255
|
-
const queue = queuedAdapter([result]);
|
|
256
|
-
await assert.rejects(
|
|
257
|
-
() => new NativeReviewCliV213(queue.adapter).start({ cwd: "/repo" }),
|
|
258
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === result.code && error.operation === "version",
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test("native mutation uncertainty requires target status before any replay decision", async () => {
|
|
264
|
-
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" }) }]);
|
|
265
138
|
await assert.rejects(
|
|
266
|
-
() =>
|
|
139
|
+
() => client(queue.adapter).captureResult({ argumentTokens: ["--repository-context=rctx1_" + "a".repeat(64)], resultDocument: "{}" }),
|
|
267
140
|
(error: unknown) => error instanceof NativeReviewCliError
|
|
268
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.
|
|
269
|
-
&& error.mutationOutcome === "unknown"
|
|
270
|
-
&& error.nextAction === "review.status",
|
|
141
|
+
&& error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE
|
|
142
|
+
&& error.mutationOutcome === "unknown",
|
|
271
143
|
);
|
|
144
|
+
assert.equal(queue.calls.length, 1);
|
|
272
145
|
});
|
|
273
146
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
START,
|
|
278
|
-
VERSION,
|
|
279
|
-
{ stdout: await fixture("finalize") },
|
|
280
|
-
VERSION,
|
|
281
|
-
{ stdout: await fixture("bind-sdd") },
|
|
282
|
-
]);
|
|
283
|
-
const client = new NativeReviewCliV213(queue.adapter, "/package/.gentle-ai/v2.1.4/gentle-ai", 321, 654);
|
|
284
|
-
await client.start({ cwd: "/repo" });
|
|
285
|
-
await client.finalize({ cwd: "/repo", lineageId: "lineage-1" });
|
|
286
|
-
await client.bindSdd({ cwd: "/repo", change: "native-review-authority-parity", lineage: "issue136-contract-runtime", expectedBindingRevision: "" });
|
|
287
|
-
assert.deepEqual(queue.calls.map((call) => call.timeoutMs), [321, undefined, 321, undefined, 321, undefined]);
|
|
288
|
-
assert.deepEqual(queue.calls.map((call) => call.maxBufferBytes), [654, 654, 654, 654, 654, 654]);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
test("native read-only commands and version checks retain the automatic timeout", async () => {
|
|
292
|
-
const queue = queuedAdapter([VERSION, { stdout: await fixture("validate-allow") }]);
|
|
293
|
-
await new NativeReviewCliV213(queue.adapter, "/package/.gentle-ai/v2.1.4/gentle-ai", 321).validate({ cwd: "/repo", gate: "post-apply", lineageId: "issue136-contract-runtime" });
|
|
294
|
-
assert.deepEqual(queue.calls.map((call) => call.timeoutMs), [321, 321]);
|
|
295
|
-
});
|
|
147
|
+
function currentStatusFixture(): Record<string, unknown> {
|
|
148
|
+
return fixture("status-v5.captured.json");
|
|
149
|
+
}
|
|
296
150
|
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
&& String(diagnostics.stderr).length <= 4_096
|
|
315
|
-
&& (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 },
|
|
316
168
|
},
|
|
317
|
-
|
|
318
|
-
|
|
169
|
+
};
|
|
170
|
+
return status;
|
|
171
|
+
}
|
|
319
172
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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 },
|
|
338
197
|
},
|
|
339
|
-
|
|
340
|
-
|
|
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
|
+
}
|
|
341
203
|
|
|
342
|
-
test("
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
);
|
|
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);
|
|
362
225
|
});
|
|
363
226
|
|
|
364
|
-
test("
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
stderr: "Error: review gate denied: scope-changed\n",
|
|
376
|
-
exitCode: 1,
|
|
377
|
-
}]);
|
|
378
|
-
const result = await new NativeReviewCliV213(queue.adapter).validate({ cwd: "/repo", gate: "post-apply" });
|
|
379
|
-
const returned = result.gateContext.raw.denial as Record<string, string>;
|
|
380
|
-
assert.equal(result.reason.length <= 1_024, true);
|
|
381
|
-
assert.equal(returned.stage.length <= 1_024, true);
|
|
382
|
-
assert.equal(returned.code.length <= 1_024, true);
|
|
383
|
-
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
|
+
]);
|
|
384
238
|
});
|
|
385
239
|
|
|
386
|
-
test("
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
{
|
|
391
|
-
{
|
|
392
|
-
{
|
|
393
|
-
{
|
|
394
|
-
{ ...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" },
|
|
395
248
|
]) {
|
|
396
|
-
const queue = queuedAdapter([
|
|
397
|
-
await assert.rejects(
|
|
398
|
-
|
|
399
|
-
(error: unknown) => error instanceof NativeReviewCliError
|
|
400
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.NON_ZERO
|
|
401
|
-
&& error.diagnostics.denial === undefined,
|
|
402
|
-
);
|
|
249
|
+
const queue = queuedAdapter([]);
|
|
250
|
+
await assert.rejects(() => client(queue.adapter).targetStatus(request), TypeError);
|
|
251
|
+
assert.equal(queue.calls.length, 0);
|
|
403
252
|
}
|
|
404
253
|
});
|
|
405
254
|
|
|
406
|
-
test("
|
|
407
|
-
const queue = queuedAdapter([
|
|
408
|
-
const result = await new NativeReviewCliV213(queue.adapter).validate({ cwd: "/repo", gate: "post-apply", lineageId: "issue136-contract-runtime" });
|
|
409
|
-
assert.equal(result.allowed, true);
|
|
410
|
-
assert.equal(result.action, "continue");
|
|
411
|
-
assert.equal(result.gateContext.lineageId, "issue136-contract-runtime");
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
test("native validate requires the returned gate context to equal the requested gate", async () => {
|
|
415
|
-
const published = JSON.parse(await fixture("validate-allow")) as Record<string, unknown>;
|
|
416
|
-
for (const gate of ["", "pre-push"]) {
|
|
417
|
-
const queue = queuedAdapter([VERSION, {
|
|
418
|
-
stdout: JSON.stringify({
|
|
419
|
-
...published,
|
|
420
|
-
context: { ...(published.context as Record<string, unknown>), gate },
|
|
421
|
-
}),
|
|
422
|
-
}]);
|
|
423
|
-
await assert.rejects(
|
|
424
|
-
() => new NativeReviewCliV213(queue.adapter).validate({ cwd: "/repo", gate: "post-apply", lineageId: "issue136-contract-runtime" }),
|
|
425
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE,
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
const denial = JSON.parse(await fixture("validate-deny")) as Record<string, unknown>;
|
|
429
|
-
const mismatch = queuedAdapter([VERSION, {
|
|
430
|
-
stdout: JSON.stringify({
|
|
431
|
-
...denial,
|
|
432
|
-
context: { ...(denial.context as Record<string, unknown>), gate: "pre-push" },
|
|
433
|
-
}),
|
|
434
|
-
stderr: "Error: review gate denied: scope-changed\n",
|
|
435
|
-
exitCode: 1,
|
|
436
|
-
}]);
|
|
255
|
+
test("negotiated STATUS rejects malformed untracked selection before launching a process", async () => {
|
|
256
|
+
const queue = queuedAdapter([]);
|
|
437
257
|
await assert.rejects(
|
|
438
|
-
() =>
|
|
439
|
-
|
|
258
|
+
() => client(queue.adapter).targetStatus({ cwd: "/repo", untrackedScope: "select", expectedUntrackedInventory: "inventory", intendedUntracked: ["../escape"] }),
|
|
259
|
+
/unique repository-relative paths/,
|
|
440
260
|
);
|
|
261
|
+
assert.equal(queue.calls.length, 0);
|
|
441
262
|
});
|
|
442
263
|
|
|
443
|
-
test("
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
] as const) {
|
|
450
|
-
const queue = queuedAdapter([VERSION, {
|
|
451
|
-
stdout: JSON.stringify({ ...published, result, action }),
|
|
452
|
-
stderr: `Error: review gate denied: ${result}\n`,
|
|
453
|
-
exitCode: 1,
|
|
454
|
-
}]);
|
|
455
|
-
const denial = await new NativeReviewCliV213(queue.adapter).validate({ cwd: "/repo", gate: "post-apply", lineageId: "issue136-contract-runtime" });
|
|
456
|
-
assert.deepEqual({ result: denial.result, allowed: denial.allowed, action: denial.action }, { result, allowed: false, action });
|
|
457
|
-
assert.equal(denial.gateContext.raw.gate, "");
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
test("native validate keeps malformed and unexpected nonzero exits typed", async () => {
|
|
462
|
-
const denial = await fixture("validate-deny");
|
|
463
|
-
for (const scenario of [
|
|
464
|
-
{ result: { stdout: "", exitCode: 1 }, code: NATIVE_REVIEW_ERROR_CODE.EMPTY_OUTPUT },
|
|
465
|
-
{ result: { stdout: "{", exitCode: 1 }, code: NATIVE_REVIEW_ERROR_CODE.MALFORMED_JSON },
|
|
466
|
-
{ result: { stdout: denial, exitCode: 2 }, code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO },
|
|
467
|
-
{ result: { stdout: denial, exitCode: 1, timedOut: true }, code: NATIVE_REVIEW_ERROR_CODE.TIMEOUT },
|
|
468
|
-
{ result: { stdout: denial, exitCode: 1, signal: "SIGTERM" as NodeJS.Signals }, code: NATIVE_REVIEW_ERROR_CODE.SIGNAL },
|
|
469
|
-
]) {
|
|
470
|
-
const queue = queuedAdapter([VERSION, scenario.result]);
|
|
471
|
-
await assert.rejects(
|
|
472
|
-
() => new NativeReviewCliV213(queue.adapter).validate({ cwd: "/repo", gate: "post-apply", lineageId: "issue136-contract-runtime" }),
|
|
473
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === scenario.code && error.operation === "review/validate",
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
const unavailable: ExecFileAdapter = async (request) => {
|
|
477
|
-
if (request.arguments[0] === "version") return { stdout: VERSION.stdout, stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
478
|
-
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 } },
|
|
479
270
|
};
|
|
271
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify(status) }]);
|
|
480
272
|
await assert.rejects(
|
|
481
|
-
() =>
|
|
482
|
-
(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,
|
|
483
275
|
);
|
|
276
|
+
assert.equal(queue.calls.length, 1);
|
|
484
277
|
});
|
|
485
278
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
{ fixtureName: "finalize", optionalKeys: ["receipt_path"], invoke: (client: NativeReviewCliV213) => client.finalize({ cwd: "/repo", lineageId: "lineage-1" }) },
|
|
491
|
-
{ fixtureName: "validate-allow", invoke: (client: NativeReviewCliV213) => client.validate({ cwd: "/repo", gate: "post-apply", lineageId: "issue136-contract-runtime" }) },
|
|
492
|
-
{ fixtureName: "bind-sdd", invoke: (client: NativeReviewCliV213) => client.bindSdd({ cwd: "/repo", change: "native-review-authority-parity", lineage: "issue136-contract-runtime", expectedBindingRevision: "" }) },
|
|
493
|
-
{ fixtureName: "sdd-status", optionalKeys: ["reviewGate", "reviewTransaction", "phaseInstructions"], invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
494
|
-
{ fixtureName: "sdd-status-engram", optionalKeys: ["reviewGate", "reviewTransaction", "phaseInstructions"], invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
495
|
-
];
|
|
496
|
-
for (const operation of operations) {
|
|
497
|
-
const fixtureBody = JSON.parse(await fixture(operation.fixtureName)) as Record<string, unknown>;
|
|
498
|
-
for (const [key, value] of Object.entries(fixtureBody)) {
|
|
499
|
-
const missing = { ...fixtureBody };
|
|
500
|
-
delete missing[key];
|
|
501
|
-
for (const mutated of [...(operation.optionalKeys?.includes(key) ? [] : [missing]), { ...fixtureBody, [key]: typeof value === "string" ? 1 : "wrong-type" }]) {
|
|
502
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify(mutated) }]);
|
|
503
|
-
await assert.rejects(() => operation.invoke(new NativeReviewCliV213(queue.adapter)), NativeReviewCliError, `${operation.fixtureName}.${key}`);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
test("native decoders reject nested mutations and unknown enums", async () => {
|
|
510
|
-
const validate = JSON.parse(await fixture("validate-allow")) as Record<string, unknown>;
|
|
511
|
-
const bind = JSON.parse(await fixture("bind-sdd")) as Record<string, unknown>;
|
|
512
|
-
const status = JSON.parse(await fixture("sdd-status")) as Record<string, unknown>;
|
|
513
|
-
const start = JSON.parse(await fixture("start")) as Record<string, unknown>;
|
|
514
|
-
const finalization = JSON.parse(await fixture("finalize")) as Record<string, unknown>;
|
|
515
|
-
const cases = [
|
|
516
|
-
{ body: { ...start, risk_level: "unknown" }, invoke: (client: NativeReviewCliV213) => client.start({ cwd: "/repo" }) },
|
|
517
|
-
{ body: { ...start, selected_lenses: ["unknown"] }, invoke: (client: NativeReviewCliV213) => client.start({ cwd: "/repo" }) },
|
|
518
|
-
{ body: { ...finalization, state: "unknown" }, invoke: (client: NativeReviewCliV213) => client.finalize({ cwd: "/repo" }) },
|
|
519
|
-
{ body: { ...validate, context: { ...(validate.context as Record<string, unknown>), extra: true } }, invoke: (client: NativeReviewCliV213) => client.validate({ cwd: "/repo", gate: "post-apply" }) },
|
|
520
|
-
{ 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: "" }) },
|
|
521
|
-
{ body: { ...status, nextRecommended: "unknown" }, invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
522
|
-
{ body: { ...status, actionContext: { ...(status.actionContext as Record<string, unknown>), allowedEditRoots: [1] } }, invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
523
|
-
{ body: { ...status, reviewGate: { ...(status.reviewGate as Record<string, unknown>), result: "deny" } }, invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
524
|
-
{ body: { ...status, reviewTransaction: { ...(status.reviewTransaction as Record<string, unknown>), mode: "ordinary" } }, invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
525
|
-
{ 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",
|
|
526
283
|
];
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
{ fixtureName: "sdd-status", nestedKey: "reviewGate", invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
539
|
-
{ fixtureName: "sdd-status", nestedKey: "reviewTransaction", invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
540
|
-
{ fixtureName: "sdd-status-engram", nestedKey: "artifacts", invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }) },
|
|
541
|
-
];
|
|
542
|
-
for (const item of cases) {
|
|
543
|
-
const body = JSON.parse(await fixture(item.fixtureName)) as Record<string, Record<string, unknown>>;
|
|
544
|
-
for (const [field, value] of Object.entries(body[item.nestedKey]!)) {
|
|
545
|
-
const missingNested = { ...body[item.nestedKey] };
|
|
546
|
-
delete missingNested[field];
|
|
547
|
-
const mutations = [
|
|
548
|
-
...(item.optionalNestedKeys?.includes(field) ? [] : [missingNested]),
|
|
549
|
-
{ ...body[item.nestedKey], [field]: typeof value === "string" ? 1 : "wrong-type" },
|
|
550
|
-
{ ...body[item.nestedKey], extra: true },
|
|
551
|
-
];
|
|
552
|
-
for (const nested of mutations) {
|
|
553
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify({ ...body, [item.nestedKey]: nested }) }]);
|
|
554
|
-
await assert.rejects(() => item.invoke(new NativeReviewCliV213(queue.adapter)), NativeReviewCliError, `${item.fixtureName}.${item.nestedKey}.${field}`);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
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
|
+
]);
|
|
558
295
|
});
|
|
559
296
|
|
|
560
|
-
test("native
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
{ result: { stdout: START.stdout, stderr: "unexpected" }, code: NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR },
|
|
567
|
-
{ result: { stdout: "", outputLimitExceeded: true }, code: NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT },
|
|
568
|
-
{ result: { stdout: "" }, code: NATIVE_REVIEW_ERROR_CODE.EMPTY_OUTPUT },
|
|
569
|
-
{ 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",
|
|
570
303
|
];
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
);
|
|
581
|
-
}
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
test("finalize stages every optional document privately and cleans it after failures", async () => {
|
|
585
|
-
const observed: string[] = [];
|
|
586
|
-
let nativeCall = 0;
|
|
587
|
-
const adapter: ExecFileAdapter = async (request) => {
|
|
588
|
-
nativeCall += 1;
|
|
589
|
-
if (nativeCall === 1) return { ...VERSION, stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
590
|
-
for (const argument of request.arguments) if (argument.includes("gentle-ai-finalize-")) observed.push(argument);
|
|
591
|
-
return { stdout: "{", stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
592
|
-
};
|
|
593
|
-
await assert.rejects(
|
|
594
|
-
() => new NativeReviewCliV213(adapter).finalize({
|
|
595
|
-
cwd: "/repo",
|
|
596
|
-
lensResults: [{ lens: "review-risk", document: { id: "risk" } }],
|
|
597
|
-
refuterDocument: { id: "refuter" },
|
|
598
|
-
validationDocument: { id: "validation" },
|
|
599
|
-
evidenceDocument: "evidence",
|
|
600
|
-
}),
|
|
601
|
-
NativeReviewCliError,
|
|
602
|
-
);
|
|
603
|
-
assert.equal(observed.filter((argument) => argument.endsWith(".json")).length, 3);
|
|
604
|
-
await Promise.all(observed.filter((argument) => argument.endsWith(".json")).map(async (path) => assert.rejects(() => import("node:fs/promises").then(({ stat }) => stat(path)))));
|
|
605
|
-
});
|
|
606
|
-
test("native review status uses the anticipated v2.1.5 contract, preserves Windows paths, and never reports mutation", async () => {
|
|
607
|
-
const queue = queuedAdapter([STATUS_VERSION, REVIEW_STATUS]);
|
|
608
|
-
const result = await new NativeReviewCliV213(queue.adapter).reviewStatus({ cwd: "C:\\repo with spaces" });
|
|
609
|
-
assert.equal(result.status, "clean");
|
|
610
|
-
assert.equal(result.complete, true);
|
|
611
|
-
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
|
+
});
|
|
612
313
|
assert.deepEqual(queue.calls.map((call) => call.arguments), [
|
|
613
|
-
["
|
|
614
|
-
["review", "
|
|
314
|
+
["review", "status", "--cwd", "/repo", "--selection-kind=untracked", `--selection-json=${selectionValue}`, "--selection-proof=provider-issued"],
|
|
315
|
+
["review", "start", ...startTokens],
|
|
615
316
|
]);
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const wrongRepository = queuedAdapter([STATUS_VERSION, { stdout: JSON.stringify({ ...JSON.parse(REVIEW_STATUS.stdout), repository: "C:\\other repository" }) }]);
|
|
619
|
-
await assert.rejects(
|
|
620
|
-
() => new NativeReviewCliV213(wrongRepository.adapter).reviewStatus({ cwd: "C:\\repo with spaces" }),
|
|
621
|
-
(error: unknown) => error instanceof NativeReviewCliError
|
|
622
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH
|
|
623
|
-
&& error.operation === "review/status"
|
|
624
|
-
&& error.mutationOutcome === "none",
|
|
625
|
-
);
|
|
626
|
-
|
|
627
|
-
const incomplete = queuedAdapter([STATUS_VERSION, { stdout: JSON.stringify({ ...JSON.parse(REVIEW_STATUS.stdout), complete: false, authoritative: false, status: "invalid" }) }]);
|
|
628
|
-
assert.equal((await new NativeReviewCliV213(incomplete.adapter).reviewStatus({ cwd: "C:\\repo with spaces" })).complete, false);
|
|
629
|
-
|
|
630
|
-
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" }] }) }]);
|
|
631
|
-
const decoded = await new NativeReviewCliV213(populated.adapter).reviewStatus({ cwd: "C:\\repo with spaces" });
|
|
632
|
-
assert.deepEqual(decoded.entries[0]?.recovery?.predecessorLineageId, "old");
|
|
633
|
-
assert.deepEqual(decoded.locks[0]?.owner?.ownerId, "owner");
|
|
634
|
-
assert.deepEqual(decoded.diagnostics, [{ path: "C:\\repo\\.git\\gentle-ai", problem: "stale" }]);
|
|
635
|
-
|
|
636
|
-
const unsupportedLockSchema = queuedAdapter([STATUS_VERSION, {
|
|
637
|
-
stdout: JSON.stringify({
|
|
638
|
-
...JSON.parse(REVIEW_STATUS.stdout),
|
|
639
|
-
locks: [{
|
|
640
|
-
version: "compact-v2",
|
|
641
|
-
path: "C:\\repo\\.git\\gentle-ai\\LOCK",
|
|
642
|
-
status: "owned",
|
|
643
|
-
owner: { schema: "unexpected", owner_id: "owner", pid: 2_147_483_647, host: "host", acquired_at: "2026-07-14T00:00:00Z" },
|
|
644
|
-
}],
|
|
645
|
-
}),
|
|
646
|
-
}]);
|
|
647
|
-
await assert.rejects(
|
|
648
|
-
() => new NativeReviewCliV213(unsupportedLockSchema.adapter).reviewStatus({ cwd: "C:\\repo with spaces" }),
|
|
649
|
-
(error: unknown) => error instanceof NativeReviewCliError
|
|
650
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE
|
|
651
|
-
&& error.operation === "review/status"
|
|
652
|
-
&& error.mutationOutcome === "none",
|
|
653
|
-
);
|
|
654
|
-
|
|
655
|
-
for (const body of [
|
|
656
|
-
{ ...JSON.parse(REVIEW_STATUS.stdout), complete: false, authoritative: true },
|
|
657
|
-
{ ...JSON.parse(REVIEW_STATUS.stdout), entries: [{ version: "compact-v2", lineage_id: "lineage", path: "C:\\repo\\.git\\gentle-ai", status: "active", problems: [], unexpected: true }] },
|
|
658
|
-
{ ...JSON.parse(REVIEW_STATUS.stdout), status: "unknown" },
|
|
659
|
-
]) {
|
|
660
|
-
const malformed = queuedAdapter([STATUS_VERSION, { stdout: JSON.stringify(body) }]);
|
|
661
|
-
await assert.rejects(
|
|
662
|
-
() => new NativeReviewCliV213(malformed.adapter).reviewStatus({ cwd: "C:\\repo with spaces" }),
|
|
663
|
-
(error: unknown) => error instanceof NativeReviewCliError
|
|
664
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE
|
|
665
|
-
&& error.operation === "review/status"
|
|
666
|
-
&& error.mutationOutcome === "none",
|
|
667
|
-
);
|
|
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);
|
|
668
319
|
}
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
test("native review status decodes and retains the v2.2.2 compact snapshot identity", async () => {
|
|
672
|
-
const snapshotIdentity = "sha256:0586c3d40c3acf3db92214ddaf07afe44ad862518b48d608ae4157377ab2f3cc";
|
|
673
|
-
const entry = {
|
|
674
|
-
version: "compact-v2",
|
|
675
|
-
lineage_id: "issue-136-v2-1-2-final",
|
|
676
|
-
path: "C:\\repo with spaces\\.git\\gentle-ai\\review-transactions\\v2\\issue-136-v2-1-2-final",
|
|
677
|
-
status: "active",
|
|
678
|
-
state: "reviewing",
|
|
679
|
-
revision: "sha256:8103efb360d10d1717a4d22e38c8b75074f99680ed6a5eb3ab40e0ed9fb69931",
|
|
680
|
-
snapshot_identity: snapshotIdentity,
|
|
681
|
-
problems: [],
|
|
682
|
-
};
|
|
683
|
-
const status = { ...JSON.parse(REVIEW_STATUS.stdout), status: "active", entries: [entry] };
|
|
684
|
-
const queue = queuedAdapter([{ stdout: "gentle-ai 2.2.2\n" }, { stdout: JSON.stringify(status) }]);
|
|
685
|
-
const decoded = await new NativeReviewCliV213(queue.adapter).reviewStatus({ cwd: "C:\\repo with spaces" });
|
|
686
|
-
assert.equal(decoded.entries[0]?.snapshotIdentity, snapshotIdentity);
|
|
687
|
-
|
|
688
|
-
const malformed = queuedAdapter([{ stdout: "gentle-ai 2.2.2\n" }, { stdout: JSON.stringify({ ...status, entries: [{ ...entry, snapshot_identity: snapshotIdentity.slice("sha256:".length) }] }) }]);
|
|
689
|
-
await assert.rejects(
|
|
690
|
-
() => new NativeReviewCliV213(malformed.adapter).reviewStatus({ cwd: "C:\\repo with spaces" }),
|
|
691
|
-
(error: unknown) => error instanceof NativeReviewCliError
|
|
692
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE
|
|
693
|
-
&& error.operation === "review/status"
|
|
694
|
-
&& error.mutationOutcome === "none",
|
|
695
|
-
);
|
|
696
|
-
});
|
|
697
320
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
// successful operations and reports it as {"status":"released"} without
|
|
701
|
-
// owner metadata (issue #184; reproduced empirically against the system
|
|
702
|
-
// binary). The decoder must accept it or reviewStatus dead-ends with
|
|
703
|
-
// schema-incompatible on every repository that ever completed a review.
|
|
704
|
-
//
|
|
705
|
-
// Lock status stays a CLOSED enum extended by exactly `released` — unlike
|
|
706
|
-
// the cause_category widening in review-integration-v2 (diagnostic
|
|
707
|
-
// metadata nothing routes on), lock status routes blocking behavior in the
|
|
708
|
-
// controller, so an unknown future status must keep failing closed rather
|
|
709
|
-
// than being silently classified as blocking or non-blocking.
|
|
710
|
-
const releasedLock = { version: "compact-v2", path: "C:\\repo\\.git\\gentle-ai\\review-transactions\\v2\\LOCK", status: "released" };
|
|
711
|
-
const released = queuedAdapter([STATUS_VERSION, { stdout: JSON.stringify({ ...JSON.parse(REVIEW_STATUS.stdout), status: "approved", locks: [releasedLock] }) }]);
|
|
712
|
-
const decoded = await new NativeReviewCliV213(released.adapter).reviewStatus({ cwd: "C:\\repo with spaces" });
|
|
713
|
-
assert.equal(decoded.status, "approved");
|
|
714
|
-
assert.deepEqual(decoded.locks, [{ version: "compact-v2", path: "C:\\repo\\.git\\gentle-ai\\review-transactions\\v2\\LOCK", status: "released" }]);
|
|
715
|
-
|
|
716
|
-
// Residual dead-owner metadata may still accompany a released entry.
|
|
717
|
-
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" } }] }) }]);
|
|
718
|
-
assert.equal((await new NativeReviewCliV213(releasedWithOwner.adapter).reviewStatus({ cwd: "C:\\repo with spaces" })).locks[0]?.owner?.ownerId, "dead-owner");
|
|
719
|
-
|
|
720
|
-
for (const status of ["Released", "stale", "unknown-future-status", ""]) {
|
|
721
|
-
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([]);
|
|
722
323
|
await assert.rejects(
|
|
723
|
-
() =>
|
|
724
|
-
|
|
725
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE
|
|
726
|
-
&& error.operation === "review/status"
|
|
727
|
-
&& error.mutationOutcome === "none",
|
|
324
|
+
() => client(rejected.adapter).start({ cwd: "/repo", targetIdentity: TARGET, intendedUntrackedSelection: { argumentTokens, value: selectionValue } }),
|
|
325
|
+
/exactly one provider-issued \{\{value\}\} token/,
|
|
728
326
|
);
|
|
327
|
+
assert.equal(rejected.calls.length, 0);
|
|
729
328
|
}
|
|
730
329
|
});
|
|
731
330
|
|
|
732
|
-
test("native
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const queue = queuedAdapter([
|
|
739
|
-
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
);
|
|
751
|
-
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]);
|
|
752
355
|
});
|
|
753
356
|
|
|
754
|
-
async
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
const queue = queuedAdapter([VERSION, native]);
|
|
762
|
-
const client = new NativeReviewCliV213(
|
|
763
|
-
queue.adapter,
|
|
764
|
-
"/package/.gentle-ai/v2.1.3/gentle-ai",
|
|
765
|
-
30_000,
|
|
766
|
-
1024 * 1024,
|
|
767
|
-
async () => {
|
|
768
|
-
cleanupAttempts += 1;
|
|
769
|
-
throw new Error("cleanup failed");
|
|
770
|
-
},
|
|
771
|
-
);
|
|
772
|
-
const finalize = () => client.finalize({ cwd: "/repo", lensResults: [{ lens: "review-risk", document: { id: "risk" } }] });
|
|
773
|
-
if (native.stdout === "{") {
|
|
774
|
-
await assert.rejects(finalize, (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.MALFORMED_JSON && error.mutationOutcome === "unknown");
|
|
775
|
-
} else {
|
|
776
|
-
assert.match((await finalize()).storeRevision, /^sha256:[0-9a-f]{64}$/);
|
|
777
|
-
}
|
|
778
|
-
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 ");
|
|
779
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));
|
|
780
372
|
});
|
|
781
373
|
|
|
782
|
-
test("
|
|
783
|
-
for (const
|
|
784
|
-
{
|
|
785
|
-
{
|
|
786
|
-
{
|
|
787
|
-
{
|
|
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" },
|
|
788
380
|
]) {
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
call += 1;
|
|
793
|
-
if (call === 1) return { ...VERSION, stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
794
|
-
for (const argument of request.arguments) if (argument.includes("gentle-ai-finalize-")) staged.push(argument);
|
|
795
|
-
return { stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false, ...result };
|
|
796
|
-
};
|
|
797
|
-
const finalize = () => new NativeReviewCliV213(adapter).finalize({ cwd: "/repo", lensResults: [{ lens: "review-risk", document: { id: "risk" } }], refuterDocument: { id: "refuter" }, validationDocument: { id: "validation" }, evidenceDocument: "evidence" });
|
|
798
|
-
if (result.exitCode === 1 || result.timedOut || result.stdout === "{") await assert.rejects(finalize, NativeReviewCliError);
|
|
799
|
-
else await finalize();
|
|
800
|
-
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);
|
|
801
384
|
}
|
|
802
385
|
});
|
|
803
386
|
|
|
804
|
-
test("
|
|
805
|
-
const queue = queuedAdapter([
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
{ stdout: await fixture("sdd-status") },
|
|
816
|
-
]);
|
|
817
|
-
const client = new NativeReviewCliV213(queue.adapter);
|
|
818
|
-
await assert.rejects(() => client.start({ cwd: "/repo", lineageId: "lineage-1" }), NativeReviewCliError);
|
|
819
|
-
const finalized = await client.finalize({ cwd: "/repo", lineageId: "lineage-1" });
|
|
820
|
-
assert.match(finalized.storeRevision, /^sha256:[0-9a-f]{64}$/);
|
|
821
|
-
assert.equal(finalized.action, "validate delivery with gentle-ai review validate --gate <gate>");
|
|
822
|
-
assert.equal((await client.validate({ cwd: "/repo", gate: "post-apply", lineageId: "issue136-contract-runtime" })).allowed, true);
|
|
823
|
-
assert.match((await client.bindSdd({ cwd: "/repo", change: "native-review-authority-parity", lineage: "issue136-contract-runtime", expectedBindingRevision: "" })).revision, /^sha256:[0-9a-f]{64}$/);
|
|
824
|
-
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);
|
|
825
398
|
});
|
|
826
399
|
|
|
827
|
-
test("
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
for (const source of [openspec, engram]) {
|
|
832
|
-
for (const nextRecommended of actions) {
|
|
833
|
-
const body = {
|
|
834
|
-
...source,
|
|
835
|
-
nextRecommended,
|
|
836
|
-
blockedReasons: [],
|
|
837
|
-
reviewGate: { result: "allow", reason: "current bound authority allows delivery" },
|
|
838
|
-
};
|
|
839
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify(body) }]);
|
|
840
|
-
assert.equal(
|
|
841
|
-
(await new NativeReviewCliV213(queue.adapter).sddStatus({ cwd: "/repo", change: "native-review-authority-parity" })).ready,
|
|
842
|
-
nextRecommended === "verify" || nextRecommended === "archive",
|
|
843
|
-
`${source.artifactStore as string}:${nextRecommended}`,
|
|
844
|
-
);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
for (const body of [
|
|
849
|
-
{ ...openspec, nextRecommended: "verify", blockedReasons: [], reviewGate: undefined },
|
|
850
|
-
{ ...openspec, nextRecommended: "archive", blockedReasons: ["stale authority"], reviewGate: { result: "allow", reason: "allow before drift" } },
|
|
851
|
-
{ ...openspec, nextRecommended: "verify", blockedReasons: [], reviewGate: { result: "scope-changed", reason: "candidate changed" } },
|
|
852
|
-
{ ...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" },
|
|
853
404
|
]) {
|
|
854
|
-
const queue = queuedAdapter([
|
|
855
|
-
assert.
|
|
405
|
+
const queue = queuedAdapter([]);
|
|
406
|
+
await assert.rejects(() => client(queue.adapter).captureResult(request), TypeError);
|
|
407
|
+
assert.equal(queue.calls.length, 0);
|
|
856
408
|
}
|
|
857
409
|
});
|
|
858
410
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
for (const nextRecommended of ["verify", "archive"] as const) {
|
|
870
|
-
const body = {
|
|
871
|
-
...openspec,
|
|
872
|
-
nextRecommended,
|
|
873
|
-
blockedReasons: [],
|
|
874
|
-
reviewGate: { result: "invalidated", reason: "review-driven development is disabled and no receipt governs this candidate, so delivery follows ordinary repository policy", delivery: "disabled/unmanaged" },
|
|
875
|
-
};
|
|
876
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify(body) }]);
|
|
877
|
-
const status = await new NativeReviewCliV213(queue.adapter).sddStatus({ cwd: "/repo", change: "native-review-authority-parity" });
|
|
878
|
-
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
|
+
);
|
|
879
421
|
}
|
|
880
|
-
|
|
881
|
-
// Blocked reasons still take priority over a legitimate unmanaged delivery.
|
|
882
|
-
const stillBlocked = {
|
|
883
|
-
...openspec, nextRecommended: "verify", blockedReasons: ["stale authority"],
|
|
884
|
-
reviewGate: { result: "invalidated", reason: "delivery follows ordinary repository policy", delivery: "disabled/unmanaged" },
|
|
885
|
-
};
|
|
886
|
-
const blockedQueue = queuedAdapter([VERSION, { stdout: JSON.stringify(stillBlocked) }]);
|
|
887
|
-
assert.equal((await new NativeReviewCliV213(blockedQueue.adapter).sddStatus({ cwd: "/repo", change: "native-review-authority-parity" })).ready, false);
|
|
888
|
-
|
|
889
|
-
// An unrecognized delivery value must still fail closed (exact enum, not a
|
|
890
|
-
// free string).
|
|
891
|
-
const unknownDelivery = {
|
|
892
|
-
...openspec, nextRecommended: "verify", blockedReasons: [],
|
|
893
|
-
reviewGate: { result: "invalidated", reason: "delivery follows ordinary repository policy", delivery: "unmanaged" },
|
|
894
|
-
};
|
|
895
|
-
const unknownQueue = queuedAdapter([VERSION, { stdout: JSON.stringify(unknownDelivery) }]);
|
|
896
|
-
await assert.rejects(
|
|
897
|
-
() => new NativeReviewCliV213(unknownQueue.adapter).sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }),
|
|
898
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE,
|
|
899
|
-
);
|
|
900
422
|
});
|
|
901
423
|
|
|
902
|
-
test("
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
+
}
|
|
908
437
|
});
|
|
909
438
|
|
|
910
|
-
test("
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
assert.equal(result.allowed, false);
|
|
915
|
-
assert.equal(result.action, "create-new-lineage");
|
|
916
|
-
|
|
917
|
-
for (const stale of [
|
|
918
|
-
{ schema: "gentle-ai.review-gate-result/v1", result: "deny", allowed: false, action: "blocked", reason: "stale", gate_context: {} },
|
|
919
|
-
{ 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 },
|
|
920
443
|
]) {
|
|
921
|
-
const
|
|
922
|
-
const client = new NativeReviewCliV213(staleQueue.adapter);
|
|
444
|
+
const queue = queuedAdapter([result]);
|
|
923
445
|
await assert.rejects(
|
|
924
|
-
() => "
|
|
925
|
-
|
|
926
|
-
: client.bindSdd({ cwd: "/repo", change: "native-review-authority-parity", lineage: "issue136-contract-runtime", expectedBindingRevision: "" }),
|
|
927
|
-
NativeReviewCliError,
|
|
446
|
+
() => client(queue.adapter).targetStatus({ cwd: "/repo" }),
|
|
447
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.mutationOutcome === "none",
|
|
928
448
|
);
|
|
929
449
|
}
|
|
930
450
|
});
|
|
931
451
|
|
|
932
|
-
test("
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
])
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
{ stdout: "", stderr: "missing", exitCode: 1 },
|
|
946
|
-
{ stdout: await fixture("start"), stderr: "warning" },
|
|
947
|
-
{ 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`,
|
|
948
465
|
]) {
|
|
949
|
-
const
|
|
466
|
+
const rejected = queuedAdapter([{ stdout: JSON.stringify(currentStatusFixture()), stderr }]);
|
|
950
467
|
await assert.rejects(
|
|
951
|
-
() =>
|
|
952
|
-
(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,
|
|
953
470
|
);
|
|
954
471
|
}
|
|
955
472
|
});
|
|
956
473
|
|
|
957
|
-
test("native client
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
const status = JSON.parse(await fixture("sdd-status")) as Record<string, unknown>;
|
|
961
|
-
const cases = [
|
|
962
|
-
{ invoke: (client: NativeReviewCliV213) => client.finalize({ cwd: "/repo", lineageId: "lineage-1" }), body: { ...finalize, extra: true } },
|
|
963
|
-
{ invoke: (client: NativeReviewCliV213) => client.bindSdd({ cwd: "/repo", change: "native-review-authority-parity", lineage: "lineage-1", expectedBindingRevision: "" }), body: { ...bind, extra: true } },
|
|
964
|
-
{ invoke: (client: NativeReviewCliV213) => client.sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }), body: { ...status, extra: true } },
|
|
965
|
-
];
|
|
966
|
-
for (const item of cases) {
|
|
967
|
-
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify(item.body) }]);
|
|
968
|
-
await assert.rejects(() => item.invoke(new NativeReviewCliV213(queue.adapter)), NativeReviewCliError);
|
|
969
|
-
}
|
|
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/);
|
|
970
477
|
});
|
|
971
478
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
const
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
const { readFile, stat } = await import("node:fs/promises");
|
|
984
|
-
observed.push({ flag: request.arguments[index]!, file: path, mode: (await stat(path)).mode & 0o777, content: await readFile(path, "utf8") });
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
return { stdout: await fixture("finalize"), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
988
|
-
};
|
|
989
|
-
await new NativeReviewCliV213(adapter).finalize({
|
|
990
|
-
cwd: "/repo",
|
|
991
|
-
lineageId: "lineage-1",
|
|
992
|
-
lensResults: [{ lens: "review-risk", document: { lens: "risk", findings: [], evidence: ["complete candidate reviewed"] } }],
|
|
993
|
-
refuterDocument: { results: [{ finding_id: "RISK-001", outcome: "inconclusive", proof_refs: ["differential-test:candidate still fails"] }] },
|
|
994
|
-
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"] }] },
|
|
995
|
-
evidenceDocument: " focused verification failed\n\n",
|
|
996
|
-
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,
|
|
997
490
|
});
|
|
998
|
-
assert.
|
|
999
|
-
assert.
|
|
1000
|
-
assert.
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
+
}
|
|
1009
514
|
|
|
1010
|
-
|
|
1011
|
-
|
|
515
|
+
const outputLimited = new NativeReviewCliV216(async () => ({
|
|
516
|
+
stdout: "", stderr: "", exitCode: 0, signal: "SIGTERM", timedOut: true, outputLimitExceeded: true,
|
|
517
|
+
}), "/package/.gentle-ai/gentle-ai");
|
|
1012
518
|
await assert.rejects(
|
|
1013
|
-
() =>
|
|
1014
|
-
|
|
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.",
|
|
1015
527
|
);
|
|
1016
|
-
assert.equal(queue.calls.length, 0);
|
|
1017
528
|
});
|
|
1018
529
|
|
|
1019
|
-
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;
|
|
1020
549
|
const adapter: ExecFileAdapter = async (request) => {
|
|
1021
|
-
|
|
550
|
+
receivedSignal = request.signal;
|
|
551
|
+
receivedTimeout = request.timeoutMs;
|
|
1022
552
|
const error = new Error("cancelled");
|
|
1023
553
|
error.name = "AbortError";
|
|
1024
554
|
throw error;
|
|
1025
555
|
};
|
|
1026
556
|
await assert.rejects(
|
|
1027
|
-
() => new
|
|
557
|
+
() => new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai").captureResult({
|
|
558
|
+
argumentTokens: [`--repository-context=${REPOSITORY_CONTEXT}`],
|
|
559
|
+
resultDocument: "{}",
|
|
560
|
+
signal: controller.signal,
|
|
561
|
+
}),
|
|
1028
562
|
(error: unknown) => error instanceof NativeReviewCliError
|
|
1029
563
|
&& error.code === NATIVE_REVIEW_ERROR_CODE.CANCELLED
|
|
1030
564
|
&& error.mutationOutcome === "unknown"
|
|
1031
565
|
&& error.nextAction === "review.status",
|
|
1032
566
|
);
|
|
567
|
+
assert.equal(receivedSignal, controller.signal);
|
|
568
|
+
assert.equal(receivedTimeout, undefined);
|
|
1033
569
|
});
|
|
1034
570
|
|
|
1035
|
-
test("
|
|
1036
|
-
|
|
1037
|
-
const pending = createNodeExecFileAdapter()({ file: process.execPath, arguments: ["-e", "setTimeout(() => {}, 10_000)"], cwd: process.cwd(), timeoutMs: 30_000, maxBufferBytes: 1024, signal: controller.signal });
|
|
1038
|
-
controller.abort();
|
|
1039
|
-
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);
|
|
1040
573
|
});
|
|
1041
574
|
|
|
1042
|
-
test("
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
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 }> = [];
|
|
1046
590
|
const adapter: ExecFileAdapter = async (request) => {
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
const error = new Error("cancelled");
|
|
1051
|
-
error.name = "AbortError";
|
|
1052
|
-
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 };
|
|
1053
594
|
}
|
|
1054
|
-
|
|
595
|
+
return { stdout: JSON.stringify(currentStatusFixture()), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
1055
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" }]);
|
|
1056
603
|
await assert.rejects(
|
|
1057
|
-
() =>
|
|
604
|
+
() => client(diagnostics.adapter).targetStatus({ cwd: "/repo" }),
|
|
1058
605
|
(error: unknown) => error instanceof NativeReviewCliError
|
|
1059
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.
|
|
1060
|
-
&& error.
|
|
1061
|
-
&& 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,
|
|
1062
743
|
);
|
|
1063
|
-
assert.equal(mutationTimeoutMs, undefined);
|
|
1064
744
|
});
|