gentle-pi 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -80
- package/assets/agents/review-readability.md +0 -2
- package/assets/agents/review-reliability.md +0 -2
- package/assets/agents/review-resilience.md +0 -2
- package/assets/agents/review-risk.md +0 -2
- package/assets/agents/sdd-apply.md +5 -3
- package/assets/agents/sdd-proposal.md +2 -0
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-status.md +4 -4
- package/assets/agents/sdd-tasks.md +2 -3
- package/assets/agents/sdd-verify.md +21 -1
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +54 -248
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +22 -38
- package/assets/sdd-orchestrator-workflow.md +30 -37
- package/assets/support/sdd-status-contract.md +7 -7
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/docs/native-authority-architecture.md +9 -11
- package/docs/review-integration.md +27 -373
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +1778 -3673
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +4 -8
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +372 -940
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/review-candidate-view.ts +341 -132
- package/lib/review-host-relay.ts +210 -68
- package/lib/review-integration-v2.ts +839 -307
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/sdd-preflight.ts +177 -66
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -82
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +322 -890
- package/runtime/review-integration-v2.mjs +789 -257
- package/runtime/review-relay-contract.mjs +1 -1
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
- package/scripts/gentle-ai-installer.mjs +75 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
- package/scripts/test-packed-runner.mjs +4 -7
- package/scripts/verify-package-files.mjs +12 -13
- package/skills/_shared/review-ledger-contract.md +8 -14
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/judgment-day/SKILL.md +5 -7
- package/skills/rdd-defect-workflow/SKILL.md +3 -3
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +15 -1168
- package/tests/delegated-key-learnings-contract.test.ts +8 -6
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary.test.ts +51 -228
- package/tests/gentle-ai-installer.test.ts +137 -47
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +340 -4
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +28 -1
- package/tests/native-review-cli.test.ts +586 -962
- package/tests/native-review-consent.test.ts +150 -66
- package/tests/native-review-parity-runtime.test.ts +80 -359
- package/tests/native-review-parity.test.ts +674 -925
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +132 -88
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +37 -35
- package/tests/provider-defect-handoff.test.ts +15 -118
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +409 -3
- package/tests/review-compact-contract.test.ts +4 -30
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +363 -1450
- package/tests/review-controller-native-routing.test.ts +1251 -5642
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +222 -71
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +112 -153
- package/tests/review-dispatch-hydration-gap.test.ts +1 -53
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +117 -82
- package/tests/review-host-relay.test.ts +242 -8
- package/tests/review-integration-v2-forward.test.ts +576 -553
- package/tests/review-integration-v2.test.ts +239 -165
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +97 -35
- package/tests/review-recovered-lineage-routing.test.ts +0 -47
- package/tests/review-relay-transport-agent.test.ts +119 -46
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +361 -196
- package/tests/sdd-agent-tools.test.ts +36 -0
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +5 -8
- package/tests/writer-edit-surface-scope.test.ts +230 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/lib/git-commit-transaction.ts +0 -861
- package/runtime/git-commit-transaction.mjs +0 -862
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -530
|
@@ -1,1557 +1,470 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import {
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
4
3
|
import { join } from "node:path";
|
|
5
4
|
import test from "node:test";
|
|
6
5
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
6
|
import { __testing, createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
8
7
|
import {
|
|
9
|
-
|
|
8
|
+
NATIVE_REVIEW_LEGACY_ALIAS_REPAIR,
|
|
9
|
+
NATIVE_REVIEW_LEGACY_QUARANTINE,
|
|
10
10
|
NativeReviewCliError,
|
|
11
|
-
NativeReviewCliV214 as NativeReviewCliV214Production,
|
|
12
11
|
NativeReviewCliV216,
|
|
13
|
-
clearNativeReviewCapabilitiesCacheForTesting,
|
|
14
12
|
nativeReviewAbandonAuthorization,
|
|
15
13
|
nativeReviewLegacyAliasRepairAuthorization,
|
|
14
|
+
nativeReviewLegacyQuarantineAuthorization,
|
|
15
|
+
nativeReviewReconcileAuthorization,
|
|
16
16
|
type ExecFileAdapter,
|
|
17
|
-
type NativeReviewCli,
|
|
18
17
|
} from "../lib/native-review-cli.ts";
|
|
19
|
-
import { GENTLE_AI_VERSION } from "../lib/gentle-ai-binary.ts";
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
const v2Fixture = <T = unknown>(name: string): T => JSON.parse(readFileSync(join(v2FixtureRoot, name), "utf8")) as T;
|
|
19
|
+
interface QueuedResult { stdout: string; exitCode?: number; }
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface QueuedResult { stdout: string; stderr?: string; exitCode?: number; }
|
|
34
|
-
|
|
35
|
-
function queuedAdapter(results: QueuedResult[]): { adapter: ExecFileAdapter; calls: Array<{ file: string; arguments: readonly string[]; cwd: string }> } {
|
|
36
|
-
const calls: Array<{ file: string; arguments: readonly string[]; cwd: string }> = [];
|
|
21
|
+
function queuedAdapter(results: readonly QueuedResult[]): {
|
|
22
|
+
adapter: ExecFileAdapter;
|
|
23
|
+
calls: Array<{ arguments: readonly string[]; cwd: string }>;
|
|
24
|
+
} {
|
|
25
|
+
const queue = [...results];
|
|
26
|
+
const calls: Array<{ arguments: readonly string[]; cwd: string }> = [];
|
|
37
27
|
return {
|
|
38
28
|
calls,
|
|
39
29
|
adapter: async (request) => {
|
|
40
|
-
calls.push({
|
|
41
|
-
const result =
|
|
42
|
-
if (
|
|
43
|
-
return { stdout: result.stdout, stderr:
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const VERSION_219 = { stdout: "gentle-ai 2.1.9\n" };
|
|
49
|
-
const VERSION_218 = { stdout: "gentle-ai 2.1.8\n" };
|
|
50
|
-
const VERSION_220 = { stdout: "gentle-ai 2.2.0\n" };
|
|
51
|
-
const RECLAIM_RECORD = { schema: "gentle-ai.review-reclaim-audit/v1", lineage: "stuck-lineage", actor: "maintainer", reason: "incomplete entry" };
|
|
52
|
-
const RECOVER_RECORD = { schema: "gentle-ai.review-recovery/v1", predecessor_lineage: "broken", successor_lineage: "successor" };
|
|
53
|
-
const RECONCILE_RECORD = { schema: "gentle-ai.review-reconcile-audit/v1", predecessor_lineage: "predecessor", successor_lineage: "successor", outcome: "quarantined" };
|
|
54
|
-
const RECONCILE_RESULT = { operation: "review/reconcile-authority", record: RECONCILE_RECORD };
|
|
55
|
-
const ABANDON_RECORD = { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "pristine", status: "committed" };
|
|
56
|
-
const LEGACY_QUARANTINE_RECORD = { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "legacy", status: "committed" };
|
|
57
|
-
const LEGACY_FREEZE_DIAGNOSTIC = "historical findings freeze changed unrelated transaction state";
|
|
58
|
-
const LEGACY_FREEZE_DISPOSITION = "quarantine-malformed-freeze-event";
|
|
59
|
-
const COMBINED_RECONCILE_ANOMALIES = "unchanged_target,malformed_recovery_authorization";
|
|
60
|
-
const LEGACY_ALIAS_DIAGNOSTIC = "unsupported historical v1 operation alias";
|
|
61
|
-
const LEGACY_ALIAS_DISPOSITION = "quarantine-approved-historical-alias";
|
|
62
|
-
const LEGACY_ALIAS_RECORD = { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "legacy-alias", status: "committed" };
|
|
63
|
-
const LEGACY_ALIAS_AUTHORIZATION = [
|
|
64
|
-
"gentle-ai.review-legacy-alias-repair-authorization/v1",
|
|
65
|
-
"repository=/repo",
|
|
66
|
-
"lineage=legacy-alias",
|
|
67
|
-
`revision=sha256:${"c".repeat(64)}`,
|
|
68
|
-
`diagnostic=${LEGACY_ALIAS_DIAGNOSTIC}`,
|
|
69
|
-
`disposition=${LEGACY_ALIAS_DISPOSITION}`,
|
|
70
|
-
"actor=maintainer",
|
|
71
|
-
"reason=quarantine approved historical alias",
|
|
72
|
-
].join("\n");
|
|
73
|
-
const ABANDON_DISCARDED_WORK = {
|
|
74
|
-
capturedLensResults: ["00-risk.json", "01-readability.json"],
|
|
75
|
-
findingsPresent: true,
|
|
76
|
-
evidenceRecordsPresent: false,
|
|
77
|
-
} as const;
|
|
78
|
-
const ABANDON_AUTHORIZATION = [
|
|
79
|
-
"gentle-ai.review-abandon-authorization/v2",
|
|
80
|
-
"lineage=pristine",
|
|
81
|
-
"revision=revision",
|
|
82
|
-
"snapshot_identity=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
83
|
-
"reason=retire pristine lineage",
|
|
84
|
-
"captured_lens_results=00-risk.json,01-readability.json",
|
|
85
|
-
"findings_present=true",
|
|
86
|
-
"evidence_records_present=false",
|
|
87
|
-
"actor=maintainer",
|
|
88
|
-
].join("\n");
|
|
89
|
-
const LEGACY_V1_ABANDON_AUTHORIZATION = [
|
|
90
|
-
"gentle-ai.review-abandon-authorization/v1",
|
|
91
|
-
"lineage=pristine",
|
|
92
|
-
"revision=revision",
|
|
93
|
-
"snapshot_identity=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
94
|
-
"actor=maintainer",
|
|
95
|
-
"reason=retire pristine lineage",
|
|
96
|
-
].join("\n");
|
|
97
|
-
const ABANDON_REQUEST = {
|
|
98
|
-
cwd: "/repo",
|
|
99
|
-
lineage: "pristine",
|
|
100
|
-
expectedRevision: "revision",
|
|
101
|
-
snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
102
|
-
...ABANDON_DISCARDED_WORK,
|
|
103
|
-
actor: "maintainer",
|
|
104
|
-
reason: "retire pristine lineage",
|
|
105
|
-
maintainerAuthorization: ABANDON_AUTHORIZATION,
|
|
106
|
-
} as const;
|
|
107
|
-
const LEGACY_QUARANTINE_AUTHORIZATION = [
|
|
108
|
-
"gentle-ai.review-legacy-quarantine-authorization/v1",
|
|
109
|
-
"repository=/repo",
|
|
110
|
-
"lineage=legacy",
|
|
111
|
-
"revision=sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
112
|
-
`diagnostic=${LEGACY_FREEZE_DIAGNOSTIC}`,
|
|
113
|
-
`disposition=${LEGACY_FREEZE_DISPOSITION}`,
|
|
114
|
-
"actor=maintainer",
|
|
115
|
-
"reason=quarantine malformed legacy freeze",
|
|
116
|
-
].join("\n");
|
|
117
|
-
const RECONCILE_AUTHORIZATION = [
|
|
118
|
-
"gentle-ai.review-reconcile-authorization/v1",
|
|
119
|
-
"predecessor_lineage=predecessor",
|
|
120
|
-
"predecessor_revision=predecessor-revision",
|
|
121
|
-
"successor_lineage=successor",
|
|
122
|
-
"successor_revision=successor-revision",
|
|
123
|
-
"actor=maintainer",
|
|
124
|
-
"reason=invalid recovery edge",
|
|
125
|
-
].join("\n");
|
|
126
|
-
const COMBINED_RECONCILE_AUTHORIZATION = `${RECONCILE_AUTHORIZATION}\nanomalies=${COMBINED_RECONCILE_ANOMALIES}`;
|
|
127
|
-
const RECOVER_TARGET_IDENTITY = `sha256:${"e".repeat(64)}`;
|
|
128
|
-
// The six canonical native recovery inputs, and nothing else. INSPECT never
|
|
129
|
-
// publishes the legacy RESET quartet for a compact-v2 recovery, so this is the
|
|
130
|
-
// complete request a maintainer can actually assemble (issue #212).
|
|
131
|
-
const RECOVER_INPUT = {
|
|
132
|
-
predecessorLineage: "broken",
|
|
133
|
-
expectedPredecessorRevision: "rev-1",
|
|
134
|
-
successorLineage: "successor",
|
|
135
|
-
disposition: "invalidated",
|
|
136
|
-
actor: "maintainer",
|
|
137
|
-
reason: "invalid authority",
|
|
138
|
-
} as const;
|
|
139
|
-
const RECOVER_AUTHORIZATION = [
|
|
140
|
-
"gentle-ai.review-recovery-authorization/v1",
|
|
141
|
-
"predecessor_lineage=broken",
|
|
142
|
-
"predecessor_revision=rev-1",
|
|
143
|
-
`target_identity=${RECOVER_TARGET_IDENTITY}`,
|
|
144
|
-
"actor=maintainer",
|
|
145
|
-
"reason=invalid authority",
|
|
146
|
-
].join("\n");
|
|
147
|
-
|
|
148
|
-
function scratchDir(prefix: string): string {
|
|
149
|
-
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
150
|
-
test.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
151
|
-
return dir;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
interface RecordedNativeCall { operation: "reclaim" | "recover" | "reconcileAuthority" | "abandon" | "quarantineLegacy"; request: Record<string, unknown>; }
|
|
155
|
-
|
|
156
|
-
function fakeRecoveryNative(record: Record<string, unknown>): { native: NativeReviewCli; calls: RecordedNativeCall[]; statusReads: Array<string | undefined> } {
|
|
157
|
-
const calls: RecordedNativeCall[] = [];
|
|
158
|
-
const statusReads: Array<string | undefined> = [];
|
|
159
|
-
const native = {
|
|
160
|
-
async reclaim(request: Record<string, unknown>) {
|
|
161
|
-
calls.push({ operation: "reclaim", request });
|
|
162
|
-
return { record };
|
|
163
|
-
},
|
|
164
|
-
async recover(request: Record<string, unknown>) {
|
|
165
|
-
calls.push({ operation: "recover", request });
|
|
166
|
-
return { record };
|
|
167
|
-
},
|
|
168
|
-
async reconcileAuthority(request: Record<string, unknown>) {
|
|
169
|
-
calls.push({ operation: "reconcileAuthority", request });
|
|
170
|
-
return { record };
|
|
171
|
-
},
|
|
172
|
-
async abandon(request: Record<string, unknown>) {
|
|
173
|
-
calls.push({ operation: "abandon", request });
|
|
174
|
-
return { record };
|
|
175
|
-
},
|
|
176
|
-
async quarantineLegacy(request: Record<string, unknown>) {
|
|
177
|
-
calls.push({ operation: "quarantineLegacy", request });
|
|
178
|
-
return { record };
|
|
179
|
-
},
|
|
180
|
-
async targetStatus(request: { lineageId?: string }) {
|
|
181
|
-
statusReads.push(request.lineageId);
|
|
182
|
-
return recoveryTargetStatus(request.lineageId ?? "broken");
|
|
30
|
+
calls.push({ arguments: request.arguments, cwd: request.cwd });
|
|
31
|
+
const result = queue.shift();
|
|
32
|
+
if (result === undefined) throw new Error("unexpected native invocation");
|
|
33
|
+
return { stdout: result.stdout, stderr: "", exitCode: result.exitCode ?? 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
183
34
|
},
|
|
184
|
-
} as unknown as NativeReviewCli;
|
|
185
|
-
return { native, calls, statusReads };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** One recovery-eligible native target status, shaped as the provider publishes it. */
|
|
189
|
-
function recoveryTargetStatus(lineageId: string, overrides: Record<string, unknown> = {}): unknown {
|
|
190
|
-
return {
|
|
191
|
-
action: "recover",
|
|
192
|
-
actionDisposition: "invalidated",
|
|
193
|
-
authority: { lineageId, revision: "rev-1" },
|
|
194
|
-
targetIdentity: RECOVER_TARGET_IDENTITY,
|
|
195
|
-
raw: { action: "recover", target_identity: RECOVER_TARGET_IDENTITY },
|
|
196
|
-
...overrides,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async function runControllerOperation(
|
|
201
|
-
parameters: Record<string, unknown>,
|
|
202
|
-
native: NativeReviewCli | null,
|
|
203
|
-
pendingAuthorizations: Map<string, unknown> = new Map(),
|
|
204
|
-
signal?: AbortSignal,
|
|
205
|
-
): Promise<Record<string, unknown>> {
|
|
206
|
-
const cwd = scratchDir("gentle-pi-native-recovery-");
|
|
207
|
-
return await __testing.executeReviewControllerOperation(
|
|
208
|
-
parameters,
|
|
209
|
-
cwd,
|
|
210
|
-
pendingAuthorizations as Map<string, never>,
|
|
211
|
-
native,
|
|
212
|
-
signal,
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* The registered `gentle_review` tool, driven exactly as Pi drives it. RECOVER
|
|
218
|
-
* regressions hide from `executeReviewControllerOperation` alone, because the
|
|
219
|
-
* defect in issue #212 lived in the authorization preflight the tool runs first.
|
|
220
|
-
*/
|
|
221
|
-
function recoveryController(native: NativeReviewCli, prefix: string): (id: string, parameters: Record<string, unknown>, context: ExtensionContext) => Promise<Record<string, unknown>> {
|
|
222
|
-
const tools = new Map<string, { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> }>();
|
|
223
|
-
const pi = { on() {}, registerTool(definition: { name: string; execute: never }) { tools.set(definition.name, definition as never); }, registerCommand() {} } as unknown as ExtensionAPI;
|
|
224
|
-
createGentleAiExtension({ nativeReviewCli: native })(pi);
|
|
225
|
-
const controller = tools.get("gentle_review");
|
|
226
|
-
assert.ok(controller);
|
|
227
|
-
const cwd = scratchDir(prefix);
|
|
228
|
-
return async (id, parameters, context) => {
|
|
229
|
-
const result = await controller.execute(id, parameters, undefined, undefined, { ...context, cwd } as unknown as ExtensionContext) as { details: Record<string, unknown> };
|
|
230
|
-
return result.details;
|
|
231
35
|
};
|
|
232
36
|
}
|
|
233
37
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
hasUI: true,
|
|
238
|
-
ui: {
|
|
239
|
-
async confirm(_title: string, message: string) {
|
|
240
|
-
prompts.push(message);
|
|
241
|
-
return approve;
|
|
242
|
-
},
|
|
243
|
-
},
|
|
244
|
-
} as unknown as ExtensionContext;
|
|
38
|
+
function client(adapter: ExecFileAdapter): NativeReviewCliV216 {
|
|
39
|
+
return new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai");
|
|
245
40
|
}
|
|
246
41
|
|
|
247
|
-
const
|
|
42
|
+
const SHA = `sha256:${"a".repeat(64)}`;
|
|
248
43
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const result = await cli.recover!({
|
|
44
|
+
// Recovery and maintenance commands retain their published legacy wire forms.
|
|
45
|
+
// These records are intentionally opaque: Pi validates command bindings, then
|
|
46
|
+
// relays the provider's audit record without reinterpreting authority state.
|
|
47
|
+
test("native reclaim and recover preserve their exact authority bindings", async () => {
|
|
48
|
+
const queue = queuedAdapter([
|
|
49
|
+
{ stdout: JSON.stringify({ schema: "gentle-ai.review-reclaim-audit/v1", lineage: "stuck", actor: "maintainer", reason: "incomplete" }) },
|
|
50
|
+
{ stdout: JSON.stringify({ schema: "gentle-ai.review-recovery/v1", predecessor_lineage: "stuck", successor_lineage: "recovered" }) },
|
|
51
|
+
]);
|
|
52
|
+
const review = client(queue.adapter);
|
|
53
|
+
assert.equal((await review.reclaim({ cwd: "/repo", lineage: "stuck", actor: "maintainer", reason: "incomplete" })).record.lineage, "stuck");
|
|
54
|
+
assert.equal((await review.recover({
|
|
261
55
|
cwd: "/repo",
|
|
262
|
-
predecessorLineage: "
|
|
263
|
-
expectedPredecessorRevision: "
|
|
264
|
-
successorLineage: "
|
|
56
|
+
predecessorLineage: "stuck",
|
|
57
|
+
expectedPredecessorRevision: "revision-1",
|
|
58
|
+
successorLineage: "recovered",
|
|
265
59
|
disposition: "invalidated",
|
|
266
60
|
actor: "maintainer",
|
|
267
|
-
reason: "
|
|
268
|
-
maintainerAuthorization: "
|
|
269
|
-
});
|
|
270
|
-
assert.deepEqual(
|
|
271
|
-
|
|
272
|
-
"review", "recover", "--cwd", "/repo",
|
|
273
|
-
"--predecessor-lineage", "broken",
|
|
274
|
-
"--expected-predecessor-revision", "rev-1",
|
|
275
|
-
"--successor-lineage", "successor",
|
|
276
|
-
"--disposition", "invalidated",
|
|
277
|
-
"--actor", "maintainer",
|
|
278
|
-
"--reason", "invalid authority",
|
|
279
|
-
"--maintainer-authorization", "binding",
|
|
280
|
-
]);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
test("native reconcile-authority wrapper binds the exact target revisions and authorization without a shell", async () => {
|
|
284
|
-
const { adapter, calls } = queuedAdapter([VERSION_219, { stdout: JSON.stringify(RECONCILE_RESULT) }]);
|
|
285
|
-
const cli = new NativeReviewCliV214(adapter);
|
|
286
|
-
const result = await cli.reconcileAuthority!({
|
|
287
|
-
cwd: "/repo with spaces",
|
|
288
|
-
predecessorLineage: "predecessor",
|
|
289
|
-
expectedPredecessorRevision: "predecessor-revision",
|
|
290
|
-
successorLineage: "successor",
|
|
291
|
-
expectedSuccessorRevision: "successor-revision",
|
|
292
|
-
actor: "maintainer",
|
|
293
|
-
reason: "invalid recovery edge",
|
|
294
|
-
maintainerAuthorization: RECONCILE_AUTHORIZATION,
|
|
295
|
-
});
|
|
296
|
-
assert.deepEqual(result.record, RECONCILE_RECORD);
|
|
297
|
-
assert.deepEqual(calls[1]?.arguments, [
|
|
298
|
-
"review", "reconcile-authority", "--cwd", "/repo with spaces",
|
|
299
|
-
"--predecessor-lineage", "predecessor",
|
|
300
|
-
"--expected-predecessor-revision", "predecessor-revision",
|
|
301
|
-
"--successor-lineage", "successor",
|
|
302
|
-
"--expected-successor-revision", "successor-revision",
|
|
303
|
-
"--actor", "maintainer",
|
|
304
|
-
"--reason", "invalid recovery edge",
|
|
305
|
-
"--maintainer-authorization", RECONCILE_AUTHORIZATION,
|
|
61
|
+
reason: "recovery required",
|
|
62
|
+
maintainerAuthorization: "approved\nbinding",
|
|
63
|
+
})).record.successor_lineage, "recovered");
|
|
64
|
+
assert.deepEqual(queue.calls.map((call) => call.arguments), [
|
|
65
|
+
["review", "reclaim", "--cwd", "/repo", "--lineage", "stuck", "--actor", "maintainer", "--reason", "incomplete"],
|
|
66
|
+
["review", "recover", "--cwd", "/repo", "--predecessor-lineage", "stuck", "--expected-predecessor-revision", "revision-1", "--successor-lineage", "recovered", "--disposition", "invalidated", "--actor", "maintainer", "--reason", "recovery required", "--maintainer-authorization", "approved\nbinding"],
|
|
306
67
|
]);
|
|
307
68
|
});
|
|
308
69
|
|
|
309
|
-
test("native
|
|
70
|
+
test("native abandon carries the exact discarded-work authorization and audit record", async () => {
|
|
310
71
|
const request = {
|
|
311
72
|
cwd: "/repo",
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
73
|
+
lineage: "abandoned",
|
|
74
|
+
expectedRevision: "revision-1",
|
|
75
|
+
snapshotIdentity: SHA,
|
|
76
|
+
capturedLensResults: ["00-risk.json"],
|
|
77
|
+
findingsPresent: true,
|
|
78
|
+
evidenceRecordsPresent: false,
|
|
316
79
|
actor: "maintainer",
|
|
317
|
-
reason: "
|
|
318
|
-
maintainerAuthorization: RECONCILE_AUTHORIZATION,
|
|
80
|
+
reason: "discard candidate",
|
|
319
81
|
};
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
[VERSION_218, { schema: "gentle-ai.review-reconcile-audit/v1", predecessor_lineage: "predecessor" }],
|
|
325
|
-
[VERSION_219, RECONCILE_RECORD],
|
|
326
|
-
] as const) {
|
|
327
|
-
const queue = queuedAdapter([version, { stdout: JSON.stringify(response) }]);
|
|
328
|
-
await assert.rejects(
|
|
329
|
-
() => new NativeReviewCliV214(queue.adapter).reconcileAuthority!(request),
|
|
330
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE,
|
|
331
|
-
);
|
|
332
|
-
}
|
|
82
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify({ operation: "review/abandon", record: { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "abandoned", status: "committed" } }) }]);
|
|
83
|
+
const result = await client(queue.adapter).abandon({ ...request, maintainerAuthorization: nativeReviewAbandonAuthorization(request) });
|
|
84
|
+
assert.equal(result.record.status, "committed");
|
|
85
|
+
assert.deepEqual(queue.calls[0]?.arguments.slice(0, 10), ["review", "abandon", "--cwd", "/repo", "--lineage", "abandoned", "--expected-revision", "revision-1", "--actor", "maintainer"]);
|
|
333
86
|
});
|
|
334
87
|
|
|
335
|
-
test("native
|
|
336
|
-
const { adapter, calls } = queuedAdapter([
|
|
337
|
-
VERSION_219, { stdout: JSON.stringify({ operation: "review/abandon", record: ABANDON_RECORD }) },
|
|
338
|
-
VERSION_219, { stdout: JSON.stringify({ operation: "review/quarantine-legacy", record: LEGACY_QUARANTINE_RECORD }) },
|
|
339
|
-
VERSION_219, { stdout: JSON.stringify({ operation: "review/reconcile-authority", record: RECONCILE_RECORD }) },
|
|
340
|
-
]);
|
|
341
|
-
const cli = new NativeReviewCliV214(adapter);
|
|
342
|
-
assert.deepEqual((await cli.abandon!({ ...ABANDON_REQUEST })).record, ABANDON_RECORD);
|
|
343
|
-
assert.deepEqual((await cli.quarantineLegacy!({ cwd: "/repo", repository: "/repo", lineage: "legacy", expectedRevision: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", diagnostic: LEGACY_FREEZE_DIAGNOSTIC, disposition: LEGACY_FREEZE_DISPOSITION, actor: "maintainer", reason: "quarantine malformed legacy freeze", maintainerAuthorization: LEGACY_QUARANTINE_AUTHORIZATION })).record, LEGACY_QUARANTINE_RECORD);
|
|
344
|
-
assert.deepEqual((await cli.reconcileAuthority!({ cwd: "/repo", predecessorLineage: "predecessor", expectedPredecessorRevision: "predecessor-revision", successorLineage: "successor", expectedSuccessorRevision: "successor-revision", actor: "maintainer", reason: "invalid recovery edge", anomalies: COMBINED_RECONCILE_ANOMALIES, maintainerAuthorization: COMBINED_RECONCILE_AUTHORIZATION })).record, RECONCILE_RECORD);
|
|
345
|
-
assert.deepEqual(calls.filter((call) => call.arguments[0] === "review").map((call) => call.arguments), [
|
|
346
|
-
["review", "abandon", "--cwd", "/repo", "--lineage", "pristine", "--expected-revision", "revision", "--actor", "maintainer", "--reason", "retire pristine lineage", "--maintainer-authorization", ABANDON_AUTHORIZATION],
|
|
347
|
-
["review", "quarantine-legacy", "--cwd", "/repo", "--lineage", "legacy", "--expected-revision", "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "--diagnostic", LEGACY_FREEZE_DIAGNOSTIC, "--disposition", LEGACY_FREEZE_DISPOSITION, "--actor", "maintainer", "--reason", "quarantine malformed legacy freeze", "--maintainer-authorization", LEGACY_QUARANTINE_AUTHORIZATION],
|
|
348
|
-
["review", "reconcile-authority", "--cwd", "/repo", "--predecessor-lineage", "predecessor", "--expected-predecessor-revision", "predecessor-revision", "--successor-lineage", "successor", "--expected-successor-revision", "successor-revision", "--actor", "maintainer", "--reason", "invalid recovery edge", "--maintainer-authorization", COMBINED_RECONCILE_AUTHORIZATION],
|
|
349
|
-
]);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
test("native v2.2.0 repair-legacy-alias uses the exact fixed binding and preserves idempotent audit records", async () => {
|
|
353
|
-
const { adapter, calls } = queuedAdapter([
|
|
354
|
-
VERSION_220,
|
|
355
|
-
{ stdout: JSON.stringify({ operation: "review/repair-legacy-alias", record: LEGACY_ALIAS_RECORD }) },
|
|
356
|
-
]);
|
|
357
|
-
const cli = new NativeReviewCliV214(adapter);
|
|
88
|
+
test("native quarantine retains published legacy-v1 maintenance compatibility", async () => {
|
|
358
89
|
const request = {
|
|
359
90
|
cwd: "/repo",
|
|
360
91
|
repository: "/repo",
|
|
361
|
-
lineage: "legacy-
|
|
362
|
-
expectedRevision:
|
|
363
|
-
diagnostic:
|
|
364
|
-
disposition:
|
|
92
|
+
lineage: "legacy-freeze",
|
|
93
|
+
expectedRevision: SHA,
|
|
94
|
+
diagnostic: NATIVE_REVIEW_LEGACY_QUARANTINE.DIAGNOSTIC,
|
|
95
|
+
disposition: NATIVE_REVIEW_LEGACY_QUARANTINE.DISPOSITION,
|
|
365
96
|
actor: "maintainer",
|
|
366
|
-
reason: "quarantine
|
|
367
|
-
maintainerAuthorization: LEGACY_ALIAS_AUTHORIZATION,
|
|
97
|
+
reason: "quarantine malformed freeze",
|
|
368
98
|
};
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
assert.
|
|
372
|
-
|
|
373
|
-
"--expected-revision", `sha256:${"c".repeat(64)}`,
|
|
374
|
-
"--diagnostic", LEGACY_ALIAS_DIAGNOSTIC,
|
|
375
|
-
"--disposition", LEGACY_ALIAS_DISPOSITION,
|
|
376
|
-
"--actor", "maintainer", "--reason", "quarantine approved historical alias",
|
|
377
|
-
"--maintainer-authorization", LEGACY_ALIAS_AUTHORIZATION,
|
|
378
|
-
]);
|
|
99
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify({ operation: "review/quarantine-legacy", record: { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "legacy-freeze", status: "committed" } }) }]);
|
|
100
|
+
const result = await client(queue.adapter).quarantineLegacy({ ...request, maintainerAuthorization: nativeReviewLegacyQuarantineAuthorization(request) });
|
|
101
|
+
assert.equal(result.record.lineage_id, "legacy-freeze");
|
|
102
|
+
assert.deepEqual(queue.calls[0]?.arguments.slice(0, 8), ["review", "quarantine-legacy", "--cwd", "/repo", "--lineage", "legacy-freeze", "--expected-revision", SHA]);
|
|
379
103
|
});
|
|
380
104
|
|
|
381
|
-
|
|
382
|
-
// unnegotiated and carries no --contract (asserted at :281-288). The net-new
|
|
383
|
-
// negotiated `repair()` is a DIFFERENT operation and must carry --contract on
|
|
384
|
-
// every invocation, including the capabilities preflight `negotiated()` runs
|
|
385
|
-
// first. A non-eligible preflight assessment lets this test stop after one
|
|
386
|
-
// repair call, without needing a second queued execute-mode response.
|
|
387
|
-
test("negotiated repair carries --contract on every invocation, unlike repair-legacy-alias", async (t) => {
|
|
388
|
-
t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
|
|
389
|
-
const capabilities = v2Fixture<Record<string, unknown>>("capabilities.fixture.json");
|
|
390
|
-
const executableDigest = "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705";
|
|
391
|
-
const capabilitiesBody = { ...capabilities, package: { ...(capabilities.package as Record<string, unknown>), version: GENTLE_AI_VERSION } };
|
|
392
|
-
const preflightResult = {
|
|
393
|
-
schema: "gentle-ai.review-integration.repair/v2",
|
|
394
|
-
contract: "gentle-ai.review-integration/v2",
|
|
395
|
-
operation: "review.repair",
|
|
396
|
-
mode: "preflight",
|
|
397
|
-
assessment: {
|
|
398
|
-
schema: "gentle-ai.review-authority-repair-assessment/v1",
|
|
399
|
-
status: "unsupported",
|
|
400
|
-
counts: { lineages: 0, compact_lineages: 0, legacy_lineages: 0, events: 0, bytes: 0, eligible_candidates: 0, unsupported_lineages: 0, conflicts: 0 },
|
|
401
|
-
supported_operations: ["review/complete-fix", "review/validate-fix"],
|
|
402
|
-
authorization_schema: "gentle-ai.review-repair-authorization/v1",
|
|
403
|
-
},
|
|
404
|
-
required_inputs: [],
|
|
405
|
-
};
|
|
406
|
-
const { adapter, calls } = queuedAdapter([
|
|
407
|
-
{ stdout: JSON.stringify(capabilitiesBody) },
|
|
408
|
-
{ stdout: JSON.stringify(preflightResult) },
|
|
409
|
-
]);
|
|
410
|
-
const cli = new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => executableDigest);
|
|
411
|
-
const result = await cli.repair!({ cwd: "/repo", actor: "maintainer", reason: "quarantine approved historical alias", maintainerAuthorization: "irrelevant-for-non-eligible-preflight" });
|
|
412
|
-
assert.equal(result.mode, "preflight");
|
|
413
|
-
assert.equal(result.assessment.status, "unsupported");
|
|
414
|
-
assert.equal(calls.length, 2, "a non-eligible preflight must never issue an execute-mode call");
|
|
415
|
-
assert.deepEqual(calls[0]?.arguments, ["review", "capabilities", "--contract", "gentle-ai.review-integration/v2"]);
|
|
416
|
-
assert.deepEqual(calls[1]?.arguments, ["review", "repair", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--mode", "preflight"]);
|
|
417
|
-
for (const call of calls) assert.ok(call.arguments.includes("--contract"), "every negotiated repair() invocation must carry --contract");
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
test("native repair-legacy-alias fails closed for stale bindings, malformed output, cancellation, and partial failure", async () => {
|
|
105
|
+
test("native reconciliation accepts only its exact predecessor and successor binding", async () => {
|
|
421
106
|
const request = {
|
|
422
|
-
cwd: "/repo",
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
{ stdout: JSON.stringify({ operation: "review/repair-legacy-alias" }) },
|
|
430
|
-
{ stdout: JSON.stringify({ operation: "review/repair-legacy-alias", record: LEGACY_ALIAS_RECORD }), stderr: "interrupted", exitCode: 1 },
|
|
431
|
-
]) {
|
|
432
|
-
const queue = queuedAdapter([VERSION_220, result]);
|
|
433
|
-
await assert.rejects(
|
|
434
|
-
() => new NativeReviewCliV214(queue.adapter).repairLegacyAlias!(request),
|
|
435
|
-
(error: unknown) => error instanceof NativeReviewCliError
|
|
436
|
-
&& (result.exitCode === 1 ? error.auditRecord?.schema === LEGACY_ALIAS_RECORD.schema : error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE),
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
test("native v2.1.9 maintenance wrappers fail closed before launch for unsupported versions and invalid bindings", async () => {
|
|
442
|
-
const unsupported = queuedAdapter([{ stdout: "gentle-ai 2.1.8\n" }]);
|
|
443
|
-
await assert.rejects(() => new NativeReviewCliV214(unsupported.adapter).abandon!({ ...ABANDON_REQUEST }), NativeReviewCliError);
|
|
444
|
-
const invalid = queuedAdapter([]);
|
|
445
|
-
await assert.rejects(() => new NativeReviewCliV214(invalid.adapter).reconcileAuthority!({ cwd: "/repo", predecessorLineage: "predecessor", expectedPredecessorRevision: "predecessor-revision", successorLineage: "successor", expectedSuccessorRevision: "successor-revision", actor: "maintainer", reason: "invalid recovery edge", anomalies: "malformed_recovery_authorization,unchanged_target", maintainerAuthorization: COMBINED_RECONCILE_AUTHORIZATION }), TypeError);
|
|
446
|
-
assert.equal(invalid.calls.length, 0);
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
test("native v2.1.9 maintenance wrappers preserve only valid prepared audit records on partial failures", async () => {
|
|
450
|
-
for (const [operation, invoke, result] of [
|
|
451
|
-
["review/abandon", (cli: NativeReviewCliV214) => cli.abandon!({ ...ABANDON_REQUEST }), ABANDON_RECORD],
|
|
452
|
-
["review/quarantine-legacy", (cli: NativeReviewCliV214) => cli.quarantineLegacy!({ cwd: "/repo", repository: "/repo", lineage: "legacy", expectedRevision: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", diagnostic: LEGACY_FREEZE_DIAGNOSTIC, disposition: LEGACY_FREEZE_DISPOSITION, actor: "maintainer", reason: "quarantine malformed legacy freeze", maintainerAuthorization: LEGACY_QUARANTINE_AUTHORIZATION }), LEGACY_QUARANTINE_RECORD],
|
|
453
|
-
] as const) {
|
|
454
|
-
const queue = queuedAdapter([VERSION_219, { stdout: JSON.stringify({ operation, record: result }), stderr: "quarantine interrupted", exitCode: 1 }]);
|
|
455
|
-
await assert.rejects(() => invoke(new NativeReviewCliV214(queue.adapter)), (error: unknown) => error instanceof NativeReviewCliError && error.mutationOutcome === "unknown" && error.nextAction === "review.status" && error.auditRecord?.schema === result.schema);
|
|
456
|
-
}
|
|
457
|
-
const malformed = queuedAdapter([VERSION_219, { stdout: JSON.stringify({ operation: "review/abandon" }), exitCode: 1 }]);
|
|
458
|
-
await assert.rejects(() => new NativeReviewCliV214(malformed.adapter).abandon!({ ...ABANDON_REQUEST }), (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE && error.auditRecord === undefined);
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
test("native abandon forwards cancellation and preserves the unknown mutation outcome", async () => {
|
|
462
|
-
const controller = new AbortController();
|
|
463
|
-
let calls = 0;
|
|
464
|
-
const adapter: ExecFileAdapter = async (request) => {
|
|
465
|
-
calls += 1;
|
|
466
|
-
if (calls === 1) return { ...VERSION_219, stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
467
|
-
assert.equal(request.signal, controller.signal);
|
|
468
|
-
const error = new Error("cancelled");
|
|
469
|
-
error.name = "AbortError";
|
|
470
|
-
throw error;
|
|
107
|
+
cwd: "/repo",
|
|
108
|
+
predecessorLineage: "predecessor",
|
|
109
|
+
expectedPredecessorRevision: "predecessor-revision",
|
|
110
|
+
successorLineage: "successor",
|
|
111
|
+
expectedSuccessorRevision: "successor-revision",
|
|
112
|
+
actor: "maintainer",
|
|
113
|
+
reason: "repair authority edge",
|
|
471
114
|
};
|
|
472
|
-
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
assert.equal(
|
|
477
|
-
nativeReviewAbandonAuthorization({
|
|
478
|
-
lineage: "pristine",
|
|
479
|
-
expectedRevision: "revision",
|
|
480
|
-
snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
481
|
-
actor: " maintainer ",
|
|
482
|
-
reason: "operator_disposition",
|
|
483
|
-
capturedLensResults: ["00-risk.json", "01-readability.json"],
|
|
484
|
-
findingsPresent: true,
|
|
485
|
-
evidenceRecordsPresent: false,
|
|
486
|
-
}),
|
|
487
|
-
[
|
|
488
|
-
"gentle-ai.review-abandon-authorization/v2",
|
|
489
|
-
"lineage=pristine",
|
|
490
|
-
"revision=revision",
|
|
491
|
-
"snapshot_identity=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
492
|
-
"reason=operator_disposition",
|
|
493
|
-
"captured_lens_results=00-risk.json,01-readability.json",
|
|
494
|
-
"findings_present=true",
|
|
495
|
-
"evidence_records_present=false",
|
|
496
|
-
"actor=maintainer",
|
|
497
|
-
].join("\n"),
|
|
498
|
-
);
|
|
499
|
-
assert.equal(
|
|
500
|
-
nativeReviewAbandonAuthorization({
|
|
501
|
-
lineage: "pristine",
|
|
502
|
-
expectedRevision: "revision",
|
|
503
|
-
snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
504
|
-
actor: "maintainer",
|
|
505
|
-
reason: "retired_schema",
|
|
506
|
-
capturedLensResults: [],
|
|
507
|
-
findingsPresent: false,
|
|
508
|
-
evidenceRecordsPresent: true,
|
|
509
|
-
}),
|
|
510
|
-
[
|
|
511
|
-
"gentle-ai.review-abandon-authorization/v2",
|
|
512
|
-
"lineage=pristine",
|
|
513
|
-
"revision=revision",
|
|
514
|
-
"snapshot_identity=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
515
|
-
"reason=retired_schema",
|
|
516
|
-
"captured_lens_results=",
|
|
517
|
-
"findings_present=false",
|
|
518
|
-
"evidence_records_present=true",
|
|
519
|
-
"actor=maintainer",
|
|
520
|
-
].join("\n"),
|
|
521
|
-
);
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
test("native abandon refuses the legacy v1 authorization binding before process launch", async () => {
|
|
525
|
-
const { adapter, calls } = queuedAdapter([]);
|
|
526
|
-
await assert.rejects(
|
|
527
|
-
() => new NativeReviewCliV214(adapter).abandon!({ ...ABANDON_REQUEST, maintainerAuthorization: LEGACY_V1_ABANDON_AUTHORIZATION }),
|
|
528
|
-
TypeError,
|
|
529
|
-
);
|
|
530
|
-
assert.equal(calls.length, 0);
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
test("native abandon validates discarded-work inputs before process launch", async () => {
|
|
534
|
-
const { adapter, calls } = queuedAdapter([]);
|
|
535
|
-
const cli = new NativeReviewCliV214(adapter);
|
|
536
|
-
await assert.rejects(() => cli.abandon!({ ...ABANDON_REQUEST, capturedLensResults: "00-risk.json" as unknown as string[] }), /capturedLensResults/);
|
|
537
|
-
await assert.rejects(() => cli.abandon!({ ...ABANDON_REQUEST, capturedLensResults: ["00-risk.json", " padded "] }), /capturedLensResults/);
|
|
538
|
-
await assert.rejects(() => cli.abandon!({ ...ABANDON_REQUEST, findingsPresent: "true" as unknown as boolean }), /findingsPresent/);
|
|
539
|
-
await assert.rejects(() => cli.abandon!({ ...ABANDON_REQUEST, evidenceRecordsPresent: undefined as unknown as boolean }), /evidenceRecordsPresent/);
|
|
540
|
-
assert.equal(calls.length, 0);
|
|
115
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify({ operation: "review/reconcile-authority", record: { schema: "gentle-ai.review-reconcile-audit/v1", predecessor_lineage: "predecessor", successor_lineage: "successor", outcome: "quarantined" } }) }]);
|
|
116
|
+
const result = await client(queue.adapter).reconcileAuthority({ ...request, maintainerAuthorization: nativeReviewReconcileAuthorization(request) });
|
|
117
|
+
assert.equal(result.record.successor_lineage, "successor");
|
|
118
|
+
assert.deepEqual(queue.calls[0]?.arguments.slice(0, 12), ["review", "reconcile-authority", "--cwd", "/repo", "--predecessor-lineage", "predecessor", "--expected-predecessor-revision", "predecessor-revision", "--successor-lineage", "successor", "--expected-successor-revision", "successor-revision"]);
|
|
541
119
|
});
|
|
542
120
|
|
|
543
|
-
test("native
|
|
544
|
-
const
|
|
545
|
-
const cli = new NativeReviewCliV214(adapter);
|
|
121
|
+
test("native maintenance rejects tampered authority authorization before launch", async () => {
|
|
122
|
+
const queue = queuedAdapter([]);
|
|
546
123
|
await assert.rejects(
|
|
547
|
-
|
|
124
|
+
() => client(queue.adapter).reconcileAuthority({
|
|
548
125
|
cwd: "/repo",
|
|
549
126
|
predecessorLineage: "predecessor",
|
|
550
127
|
expectedPredecessorRevision: "predecessor-revision",
|
|
551
128
|
successorLineage: "successor",
|
|
552
|
-
expectedSuccessorRevision: "
|
|
129
|
+
expectedSuccessorRevision: "successor-revision",
|
|
553
130
|
actor: "maintainer",
|
|
554
|
-
reason: "
|
|
555
|
-
maintainerAuthorization:
|
|
131
|
+
reason: "repair authority edge",
|
|
132
|
+
maintainerAuthorization: "tampered",
|
|
556
133
|
}),
|
|
557
134
|
/exact target and revision binding/,
|
|
558
135
|
);
|
|
559
|
-
assert.equal(calls.length, 0);
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
test("native
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
136
|
+
assert.equal(queue.calls.length, 0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("native authority inventory remains a strict read-only maintenance surface", async () => {
|
|
140
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify({
|
|
141
|
+
schema: "gentle-ai.review-authority-status/v1",
|
|
142
|
+
operation: "review/status",
|
|
143
|
+
repository: process.cwd(),
|
|
144
|
+
complete: true,
|
|
145
|
+
authoritative: true,
|
|
146
|
+
status: "active",
|
|
147
|
+
entries: [{ version: "compact-v2", path: ".git/gentle-ai/reviews/current.json", status: "active", lineage_id: "current", state: "reviewing", revision: "revision-1", problems: [] }],
|
|
148
|
+
locks: [],
|
|
149
|
+
diagnostics: [],
|
|
150
|
+
}) }]);
|
|
151
|
+
const inventory = await client(queue.adapter).reviewStatus({ cwd: process.cwd() });
|
|
152
|
+
assert.equal(inventory.authoritative, true);
|
|
153
|
+
assert.equal(inventory.entries[0]?.lineageId, "current");
|
|
154
|
+
assert.deepEqual(queue.calls[0]?.arguments, ["review", "status", "--cwd", process.cwd()]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("negotiated repair replays only provider-published inputs after a strict preflight", async () => {
|
|
158
|
+
const preflight = JSON.parse(readFileSync(join(process.cwd(), "contracts", "review-integration", "v1", "fixtures", "repair-preflight.fixture.json"), "utf8")) as Record<string, unknown>;
|
|
159
|
+
preflight.schema = "gentle-ai.review-integration.repair/v2";
|
|
160
|
+
preflight.contract = "gentle-ai.review-integration/v2";
|
|
161
|
+
const execute = structuredClone(preflight) as Record<string, unknown>;
|
|
162
|
+
execute.mode = "execute";
|
|
163
|
+
delete execute.provider_inputs;
|
|
164
|
+
execute.required_inputs = [];
|
|
165
|
+
execute.execution = {
|
|
166
|
+
status: "committed",
|
|
167
|
+
class: "legacy_v1_historical_alias",
|
|
168
|
+
lineage_id: "historical-alias",
|
|
169
|
+
revision: `sha256:${"1".repeat(64)}`,
|
|
170
|
+
chain_identity: `sha256:${"2".repeat(64)}`,
|
|
171
|
+
cause: "unsupported_historical_v1_operation_alias",
|
|
172
|
+
disposition: "quarantine-approved-historical-alias",
|
|
173
|
+
assessment_digest: `sha256:${"3".repeat(64)}`,
|
|
174
|
+
request_digest: `sha256:${"4".repeat(64)}`,
|
|
175
|
+
record_identity: `sha256:${"5".repeat(64)}`,
|
|
572
176
|
};
|
|
573
|
-
const
|
|
574
|
-
await
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
expectedSuccessorRevision: "successor-revision",
|
|
581
|
-
actor: "maintainer",
|
|
582
|
-
reason: "invalid recovery edge",
|
|
583
|
-
maintainerAuthorization: RECONCILE_AUTHORIZATION,
|
|
584
|
-
signal: controller.signal,
|
|
585
|
-
}),
|
|
586
|
-
(error: unknown) => error instanceof NativeReviewCliError
|
|
587
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.CANCELLED
|
|
588
|
-
&& error.operation === "review/reconcile-authority"
|
|
589
|
-
&& error.mutationOutcome === "unknown",
|
|
590
|
-
);
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
test("native reconcile-authority preserves the prepared audit record on partial failure", async () => {
|
|
594
|
-
const { adapter } = queuedAdapter([VERSION_219, { stdout: JSON.stringify(RECONCILE_RESULT), stderr: "quarantine interrupted", exitCode: 1 }]);
|
|
595
|
-
const cli = new NativeReviewCliV214(adapter);
|
|
596
|
-
await assert.rejects(
|
|
597
|
-
cli.reconcileAuthority!({
|
|
598
|
-
cwd: "/repo",
|
|
599
|
-
predecessorLineage: "predecessor",
|
|
600
|
-
expectedPredecessorRevision: "predecessor-revision",
|
|
601
|
-
successorLineage: "successor",
|
|
602
|
-
expectedSuccessorRevision: "successor-revision",
|
|
603
|
-
actor: "maintainer",
|
|
604
|
-
reason: "invalid recovery edge",
|
|
605
|
-
maintainerAuthorization: RECONCILE_AUTHORIZATION,
|
|
606
|
-
}),
|
|
607
|
-
(error: unknown) => error instanceof NativeReviewCliError
|
|
608
|
-
&& error.mutationOutcome === "unknown"
|
|
609
|
-
&& error.nextAction === "review.status"
|
|
610
|
-
&& error.auditRecord?.schema === RECONCILE_RECORD.schema,
|
|
611
|
-
);
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
test("native recovery wrappers accept the published 2.1.9 contract and refuse older recovery binaries", async () => {
|
|
615
|
-
const { adapter } = queuedAdapter([{ stdout: "gentle-ai 2.1.7\n" }]);
|
|
616
|
-
const cli = new NativeReviewCliV214(adapter);
|
|
617
|
-
await assert.rejects(
|
|
618
|
-
cli.reclaim!({ cwd: "/repo", lineage: "stuck", actor: "maintainer", reason: "incomplete" }),
|
|
619
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE,
|
|
620
|
-
);
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
test("RESET maps to native review reclaim with the exact audited inputs and clears pending authorizations", async () => {
|
|
624
|
-
const { native, calls } = fakeRecoveryNative(RECLAIM_RECORD);
|
|
625
|
-
const pending = new Map<string, unknown>([["stale", { command: "git push" }]]);
|
|
626
|
-
const details = await runControllerOperation({
|
|
627
|
-
operation: "reset",
|
|
628
|
-
input: JSON.stringify({
|
|
629
|
-
repositoryId: "repo-id",
|
|
630
|
-
commonDirHash: "c".repeat(64),
|
|
631
|
-
inventoryHash: "d".repeat(64),
|
|
632
|
-
confirmation: "DESTROY REVIEW AUTHORITY repo-id",
|
|
633
|
-
lineage: "stuck-lineage",
|
|
634
|
-
actor: "maintainer",
|
|
635
|
-
reason: "incomplete entry",
|
|
636
|
-
}),
|
|
637
|
-
}, native, pending);
|
|
638
|
-
assert.equal(details.operation, "reset");
|
|
639
|
-
assert.equal(details.native_operation, "review reclaim");
|
|
640
|
-
assert.equal(details.mutation_performed, true);
|
|
641
|
-
assert.equal(details.mutation_outcome, "committed");
|
|
642
|
-
assert.deepEqual(details.result, RECLAIM_RECORD);
|
|
643
|
-
assert.equal(details.next_action, "inspect");
|
|
644
|
-
assert.equal(calls.length, 1);
|
|
645
|
-
assert.equal(calls[0]?.operation, "reclaim");
|
|
646
|
-
assert.equal(calls[0]?.request.lineage, "stuck-lineage");
|
|
647
|
-
assert.equal(calls[0]?.request.actor, "maintainer");
|
|
648
|
-
assert.equal(calls[0]?.request.reason, "incomplete entry");
|
|
649
|
-
assert.equal(pending.size, 0);
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
test("RESET without the native reclaim inputs returns a structured request instead of inventing values", async () => {
|
|
653
|
-
const { native, calls } = fakeRecoveryNative(RECLAIM_RECORD);
|
|
654
|
-
const pending = new Map<string, unknown>([["stale", { command: "git push" }]]);
|
|
655
|
-
const details = await runControllerOperation({
|
|
656
|
-
operation: "reset",
|
|
657
|
-
input: JSON.stringify({
|
|
658
|
-
repositoryId: "repo-id",
|
|
659
|
-
commonDirHash: "c".repeat(64),
|
|
660
|
-
inventoryHash: "d".repeat(64),
|
|
661
|
-
confirmation: "DESTROY REVIEW AUTHORITY repo-id",
|
|
662
|
-
}),
|
|
663
|
-
}, native, pending);
|
|
664
|
-
assert.equal(details.status, "blocked");
|
|
665
|
-
assert.equal(details.outcome, "native-input-required");
|
|
666
|
-
assert.equal(details.native_operation, "review reclaim");
|
|
667
|
-
assert.deepEqual(details.missing_input, ["lineage", "actor", "reason"]);
|
|
668
|
-
assert.equal(details.mutation_performed, false);
|
|
669
|
-
assert.equal(details.mutation_outcome, "none");
|
|
670
|
-
assert.equal(calls.length, 0);
|
|
671
|
-
assert.equal(pending.size, 1);
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
test("RESET without a native client fails closed as unavailable", async () => {
|
|
675
|
-
const details = await runControllerOperation({
|
|
676
|
-
operation: "reset",
|
|
677
|
-
input: JSON.stringify({ lineage: "stuck", actor: "maintainer", reason: "incomplete" }),
|
|
678
|
-
}, null);
|
|
679
|
-
assert.equal(details.status, "blocked");
|
|
680
|
-
assert.equal(details.outcome, "native-recovery-unavailable");
|
|
681
|
-
assert.equal(details.native_operation, "review reclaim");
|
|
682
|
-
assert.equal(details.mutation_performed, false);
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
// Issue #212: `authorizeDestructiveReviewOperation` classified native RECOVER as
|
|
686
|
-
// a legacy repository-wide RESET, so a correct six-field native recovery was
|
|
687
|
-
// refused with "Review controller recover requires an exact string repositoryId"
|
|
688
|
-
// before it ever reached the native router. INSPECT does not publish that
|
|
689
|
-
// quartet for compact-v2 recovery, which left the only supported flow
|
|
690
|
-
// unreachable. Reported by @PwnLabsmx, independently reproduced on macOS by
|
|
691
|
-
// @e-Evolution and again by the maintainer.
|
|
692
|
-
//
|
|
693
|
-
// Every RECOVER test below drives the registered tool rather than
|
|
694
|
-
// `executeReviewControllerOperation`, because the defect lived in the
|
|
695
|
-
// authorization preflight the tool runs before that function.
|
|
696
|
-
test("RECOVER accepts the six canonical native inputs without the legacy RESET challenge", async () => {
|
|
697
|
-
const { native, calls, statusReads } = fakeRecoveryNative(RECOVER_RECORD);
|
|
698
|
-
const run = recoveryController(native, "gentle-pi-recover-six-field-");
|
|
699
|
-
const prompts: string[] = [];
|
|
700
|
-
const details = await run("recover", { operation: "recover", input: JSON.stringify(RECOVER_INPUT) }, interactiveContext(prompts));
|
|
701
|
-
assert.equal(details.status, undefined, "the native six-field recovery contract is complete on its own");
|
|
702
|
-
assert.equal(details.native_operation, "review recover");
|
|
703
|
-
assert.equal(details.mutation_performed, true);
|
|
704
|
-
assert.deepEqual(details.result, RECOVER_RECORD);
|
|
705
|
-
assert.equal(calls.length, 1);
|
|
706
|
-
assert.equal(calls[0]?.operation, "recover");
|
|
707
|
-
assert.equal(calls[0]?.request.predecessorLineage, "broken");
|
|
708
|
-
assert.equal(calls[0]?.request.expectedPredecessorRevision, "rev-1");
|
|
709
|
-
assert.equal(calls[0]?.request.successorLineage, "successor");
|
|
710
|
-
assert.equal(calls[0]?.request.disposition, "invalidated");
|
|
711
|
-
// Pi derives the binding; it is never the caller's to supply.
|
|
712
|
-
assert.equal(calls[0]?.request.maintainerAuthorization, RECOVER_AUTHORIZATION);
|
|
713
|
-
assert.equal(prompts.length, 1);
|
|
714
|
-
assert.match(prompts[0]!, /gentle-ai\.review-recovery-authorization\/v1/);
|
|
715
|
-
assert.match(prompts[0]!, new RegExp(`target_identity=${RECOVER_TARGET_IDENTITY}`));
|
|
716
|
-
assert.match(prompts[0]!, /Provider-selected disposition: invalidated/);
|
|
717
|
-
assert.equal(prompts[0]!.includes("repositoryId"), false, "the legacy reset challenge has no place in native recovery");
|
|
718
|
-
// Status is read before approval and read again before mutation.
|
|
719
|
-
assert.deepEqual(statusReads, ["broken", "broken"]);
|
|
177
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify(preflight) }, { stdout: JSON.stringify(execute) }]);
|
|
178
|
+
const result = await client(queue.adapter).repair({ cwd: "/repo", actor: "maintainer", reason: "repair alias", maintainerAuthorization: "approved" });
|
|
179
|
+
assert.equal(result.execution?.status, "committed");
|
|
180
|
+
assert.deepEqual(queue.calls.map((call) => call.arguments.slice(0, 8)), [
|
|
181
|
+
["review", "repair", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--mode", "preflight"],
|
|
182
|
+
["review", "repair", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--mode", "execute"],
|
|
183
|
+
]);
|
|
720
184
|
});
|
|
721
185
|
|
|
722
|
-
test("
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
assert.equal(
|
|
736
|
-
assert.
|
|
737
|
-
|
|
738
|
-
assert.
|
|
186
|
+
test("native repair-legacy-alias keeps the historical fixed binding and rejects a tampered one before launch", async () => {
|
|
187
|
+
const request = {
|
|
188
|
+
cwd: "/repo",
|
|
189
|
+
repository: "/repo",
|
|
190
|
+
lineage: "historical-alias",
|
|
191
|
+
expectedRevision: SHA,
|
|
192
|
+
diagnostic: NATIVE_REVIEW_LEGACY_ALIAS_REPAIR.DIAGNOSTIC,
|
|
193
|
+
disposition: NATIVE_REVIEW_LEGACY_ALIAS_REPAIR.DISPOSITION,
|
|
194
|
+
actor: "maintainer",
|
|
195
|
+
reason: "repair historical alias",
|
|
196
|
+
};
|
|
197
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify({ operation: "review/repair-legacy-alias", record: { schema: "gentle-ai.review-repair-audit/v1", lineage_id: request.lineage, status: "committed" } }) }]);
|
|
198
|
+
const result = await client(queue.adapter).repairLegacyAlias({ ...request, maintainerAuthorization: nativeReviewLegacyAliasRepairAuthorization(request) });
|
|
199
|
+
assert.equal(result.record.lineage_id, request.lineage);
|
|
200
|
+
assert.deepEqual(queue.calls[0]?.arguments.slice(0, 10), ["review", "repair-legacy-alias", "--cwd", "/repo", "--lineage", request.lineage, "--expected-revision", SHA, "--diagnostic", request.diagnostic]);
|
|
201
|
+
const rejected = queuedAdapter([]);
|
|
202
|
+
await assert.rejects(() => client(rejected.adapter).repairLegacyAlias({ ...request, maintainerAuthorization: "tampered" }), /exact repository, lineage, revision/);
|
|
203
|
+
assert.equal(rejected.calls.length, 0);
|
|
739
204
|
});
|
|
740
205
|
|
|
741
|
-
test("
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
);
|
|
748
|
-
assert.equal(calls.length, 0);
|
|
749
|
-
// Fresh native evidence is read, but nothing mutates without a human.
|
|
750
|
-
assert.deepEqual(statusReads, ["broken"]);
|
|
206
|
+
test("native recovery operations reject malformed canonical inputs before a process can mutate", async () => {
|
|
207
|
+
const queue = queuedAdapter([]);
|
|
208
|
+
await assert.rejects(() => client(queue.adapter).recover({
|
|
209
|
+
cwd: "/repo", predecessorLineage: " predecessor", expectedPredecessorRevision: "revision", successorLineage: "successor", disposition: "invalidated", actor: "maintainer", reason: "reason",
|
|
210
|
+
}), /predecessorLineage/);
|
|
211
|
+
await assert.rejects(() => client(queue.adapter).reclaim({ cwd: "/repo", lineage: "lineage\n", actor: "maintainer", reason: "reason" }), /lineage/);
|
|
212
|
+
assert.equal(queue.calls.length, 0);
|
|
751
213
|
});
|
|
752
214
|
|
|
753
|
-
test("
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
215
|
+
test("partial maintenance failures preserve the provider audit record and unknown mutation outcome", async () => {
|
|
216
|
+
const request = {
|
|
217
|
+
cwd: "/repo", lineage: "abandoned", expectedRevision: "revision-1", snapshotIdentity: SHA,
|
|
218
|
+
capturedLensResults: ["00-risk.json"], findingsPresent: true, evidenceRecordsPresent: false, actor: "maintainer", reason: "discard candidate",
|
|
219
|
+
};
|
|
220
|
+
const queue = queuedAdapter([{ exitCode: 1, stdout: JSON.stringify({ operation: "review/abandon", record: { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: request.lineage, status: "partial" } }) }]);
|
|
757
221
|
await assert.rejects(
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
);
|
|
761
|
-
assert.equal(prompts.length, 1);
|
|
762
|
-
assert.equal(calls.length, 0);
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
test("RECOVER refuses a foreign, stale, or no-longer-recoverable authority before prompting", async () => {
|
|
766
|
-
for (const [label, status] of [
|
|
767
|
-
["foreign", recoveryTargetStatus("someone-elses-lineage")],
|
|
768
|
-
["stale", recoveryTargetStatus("broken", { authority: { lineageId: "broken", revision: "rev-2" } })],
|
|
769
|
-
["not recovery-eligible", recoveryTargetStatus("broken", { action: "start", actionDisposition: undefined })],
|
|
770
|
-
] as const) {
|
|
771
|
-
const calls: Record<string, unknown>[] = [];
|
|
772
|
-
const native = {
|
|
773
|
-
async recover(request: Record<string, unknown>) { calls.push(request); return { record: RECOVER_RECORD }; },
|
|
774
|
-
async targetStatus() { return status; },
|
|
775
|
-
} as unknown as NativeReviewCli;
|
|
776
|
-
const run = recoveryController(native, `gentle-pi-recover-${label.replace(/[^a-z]+/g, "-")}-`);
|
|
777
|
-
const prompts: string[] = [];
|
|
778
|
-
const details = await run("recover", { operation: "recover", input: JSON.stringify(RECOVER_INPUT) }, interactiveContext(prompts));
|
|
779
|
-
assert.equal(details.status, "blocked", label);
|
|
780
|
-
assert.equal(details.outcome, "native-recovery-status-mismatch", label);
|
|
781
|
-
assert.equal(details.mutation_performed, false, label);
|
|
782
|
-
assert.equal(calls.length, 0, label);
|
|
783
|
-
assert.deepEqual(prompts, [], label);
|
|
784
|
-
}
|
|
785
|
-
});
|
|
786
|
-
|
|
787
|
-
test("RECOVER refuses a disposition the provider did not select", async () => {
|
|
788
|
-
const { native, calls } = fakeRecoveryNative(RECOVER_RECORD);
|
|
789
|
-
const run = recoveryController(native, "gentle-pi-recover-disposition-");
|
|
790
|
-
const prompts: string[] = [];
|
|
791
|
-
const details = await run("recover", { operation: "recover", input: JSON.stringify({ ...RECOVER_INPUT, disposition: "escalated" }) }, interactiveContext(prompts));
|
|
792
|
-
assert.equal(details.status, "blocked");
|
|
793
|
-
assert.equal(details.outcome, "native-recovery-disposition-mismatch");
|
|
794
|
-
assert.equal(details.provider_disposition, "invalidated");
|
|
795
|
-
assert.equal(details.mutation_performed, false);
|
|
796
|
-
assert.equal(calls.length, 0);
|
|
797
|
-
assert.deepEqual(prompts, []);
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
// The human deliberates for an unbounded interval. An authority that advanced
|
|
801
|
-
// while they were deciding was never the one they approved, so the approval and
|
|
802
|
-
// its derived binding are pinned and re-checked against a fresh read.
|
|
803
|
-
test("RECOVER refuses to mutate when the authority changes between approval and mutation", async () => {
|
|
804
|
-
for (const [label, drifted] of [
|
|
805
|
-
["revision advanced", recoveryTargetStatus("broken", { authority: { lineageId: "broken", revision: "rev-2" } })],
|
|
806
|
-
["target identity moved", recoveryTargetStatus("broken", { targetIdentity: `sha256:${"f".repeat(64)}` })],
|
|
807
|
-
["disposition changed", recoveryTargetStatus("broken", { actionDisposition: "escalated" })],
|
|
808
|
-
["no longer recoverable", recoveryTargetStatus("broken", { action: "start", actionDisposition: undefined })],
|
|
809
|
-
] as const) {
|
|
810
|
-
const calls: Record<string, unknown>[] = [];
|
|
811
|
-
let reads = 0;
|
|
812
|
-
const native = {
|
|
813
|
-
async recover(request: Record<string, unknown>) { calls.push(request); return { record: RECOVER_RECORD }; },
|
|
814
|
-
async targetStatus() {
|
|
815
|
-
reads += 1;
|
|
816
|
-
return reads === 1 ? recoveryTargetStatus("broken") : drifted;
|
|
817
|
-
},
|
|
818
|
-
} as unknown as NativeReviewCli;
|
|
819
|
-
const run = recoveryController(native, `gentle-pi-recover-toctou-${label.replace(/[^a-z]+/g, "-")}-`);
|
|
820
|
-
const prompts: string[] = [];
|
|
821
|
-
const details = await run("recover", { operation: "recover", input: JSON.stringify(RECOVER_INPUT) }, interactiveContext(prompts));
|
|
822
|
-
assert.equal(prompts.length, 1, label);
|
|
823
|
-
assert.equal(reads, 2, label);
|
|
824
|
-
assert.equal(details.status, "blocked", label);
|
|
825
|
-
assert.equal(details.outcome, "native-recovery-authority-changed", label);
|
|
826
|
-
assert.equal(details.mutation_performed, false, label);
|
|
827
|
-
assert.equal(details.mutation_outcome, "none", label);
|
|
828
|
-
assert.equal(details.next_action, "reinspect-and-reauthorize-recovery", label);
|
|
829
|
-
assert.equal(calls.length, 0, label);
|
|
830
|
-
}
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
test("RECOVER surfaces every missing successor input including an unsupported disposition", async () => {
|
|
834
|
-
const { native, calls, statusReads } = fakeRecoveryNative(RECOVER_RECORD);
|
|
835
|
-
const run = recoveryController(native, "gentle-pi-recover-missing-input-");
|
|
836
|
-
const prompts: string[] = [];
|
|
837
|
-
const details = await run(
|
|
838
|
-
"recover",
|
|
839
|
-
{ operation: "recover", input: JSON.stringify({ predecessorLineage: "broken", disposition: "not-a-disposition" }) },
|
|
840
|
-
interactiveContext(prompts),
|
|
222
|
+
() => client(queue.adapter).abandon({ ...request, maintainerAuthorization: nativeReviewAbandonAuthorization(request) }),
|
|
223
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.mutationOutcome === "unknown" && error.auditRecord?.status === "partial",
|
|
841
224
|
);
|
|
842
|
-
assert.equal(
|
|
843
|
-
assert.equal(details.native_operation, "review recover");
|
|
844
|
-
// The missing inputs are the native six, never the legacy reset quartet.
|
|
845
|
-
assert.deepEqual(details.missing_input, ["expectedPredecessorRevision", "successorLineage", "disposition", "actor", "reason"]);
|
|
846
|
-
assert.equal(calls.length, 0);
|
|
847
|
-
assert.deepEqual(statusReads, []);
|
|
848
|
-
assert.deepEqual(prompts, []);
|
|
849
|
-
});
|
|
850
|
-
|
|
851
|
-
test("RECONCILE_AUTHORITY routes one exact native mutation and returns its audit record", async () => {
|
|
852
|
-
const { native, calls } = fakeRecoveryNative(RECONCILE_RECORD);
|
|
853
|
-
const pending = new Map<string, unknown>([["stale", { command: "git push" }]]);
|
|
854
|
-
const details = await runControllerOperation({
|
|
855
|
-
operation: "reconcile-authority",
|
|
856
|
-
input: JSON.stringify({
|
|
857
|
-
predecessorLineage: "predecessor",
|
|
858
|
-
expectedPredecessorRevision: "predecessor-revision",
|
|
859
|
-
successorLineage: "successor",
|
|
860
|
-
expectedSuccessorRevision: "successor-revision",
|
|
861
|
-
actor: "maintainer",
|
|
862
|
-
reason: "invalid recovery edge",
|
|
863
|
-
}),
|
|
864
|
-
}, native, pending);
|
|
865
|
-
assert.equal(details.operation, "reconcile-authority");
|
|
866
|
-
assert.equal(details.native_operation, "review reconcile-authority");
|
|
867
|
-
assert.equal(details.mutation_performed, true);
|
|
868
|
-
assert.equal(details.mutation_outcome, "committed");
|
|
869
|
-
assert.deepEqual(details.result, RECONCILE_RECORD);
|
|
870
|
-
assert.equal(details.next_action, "inspect");
|
|
871
|
-
assert.equal(calls.length, 1);
|
|
872
|
-
assert.equal(calls[0]?.operation, "reconcileAuthority");
|
|
873
|
-
assert.equal(calls[0]?.request.expectedPredecessorRevision, "predecessor-revision");
|
|
874
|
-
assert.equal(calls[0]?.request.expectedSuccessorRevision, "successor-revision");
|
|
875
|
-
assert.equal(calls[0]?.request.maintainerAuthorization, RECONCILE_AUTHORIZATION);
|
|
876
|
-
assert.equal(pending.size, 0);
|
|
877
|
-
});
|
|
878
|
-
|
|
879
|
-
test("RECONCILE_AUTHORITY requests every exact native binding before authorization or mutation", async () => {
|
|
880
|
-
const { native, calls } = fakeRecoveryNative(RECONCILE_RECORD);
|
|
881
|
-
const details = await runControllerOperation({ operation: "reconcile-authority", input: "{}" }, native);
|
|
882
|
-
assert.equal(details.status, "blocked");
|
|
883
|
-
assert.equal(details.outcome, "native-input-required");
|
|
884
|
-
assert.deepEqual(details.missing_input, ["predecessorLineage", "expectedPredecessorRevision", "successorLineage", "expectedSuccessorRevision", "actor", "reason"]);
|
|
885
|
-
assert.equal(details.mutation_performed, false);
|
|
886
|
-
assert.equal(details.mutation_outcome, "none");
|
|
887
|
-
assert.equal(calls.length, 0);
|
|
225
|
+
assert.equal(queue.calls.length, 1);
|
|
888
226
|
});
|
|
889
227
|
|
|
890
|
-
test("
|
|
891
|
-
const native = {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
const details = await runControllerOperation({
|
|
897
|
-
operation: "reconcile-authority",
|
|
898
|
-
input: JSON.stringify({
|
|
899
|
-
predecessorLineage: "predecessor",
|
|
900
|
-
expectedPredecessorRevision: "predecessor-revision",
|
|
901
|
-
successorLineage: "successor",
|
|
902
|
-
expectedSuccessorRevision: "successor-revision",
|
|
903
|
-
actor: "maintainer",
|
|
904
|
-
reason: "invalid recovery edge",
|
|
905
|
-
}),
|
|
906
|
-
}, native);
|
|
907
|
-
assert.equal(details.status, "blocked");
|
|
908
|
-
assert.equal(details.outcome, "native-operation-failed");
|
|
909
|
-
assert.equal(details.mutation_outcome, "unknown");
|
|
910
|
-
assert.equal(details.replayability, "status_required");
|
|
911
|
-
assert.equal(details.next_action, "review.status");
|
|
912
|
-
assert.deepEqual(details.diagnostics, {
|
|
913
|
-
operation: "review/reconcile-authority",
|
|
914
|
-
error_code: "cancelled",
|
|
915
|
-
timed_out: false,
|
|
916
|
-
output_limit_exceeded: false,
|
|
917
|
-
});
|
|
228
|
+
test("RESET requests native reclaim inputs rather than inventing authority values", async () => {
|
|
229
|
+
const native = { reclaim: async () => { throw new Error("must not reclaim"); } } as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
|
|
230
|
+
const result = await __testing.executeReviewControllerOperation({ operation: "reset", input: JSON.stringify({ lineage: "only-lineage" }) }, process.cwd(), native);
|
|
231
|
+
assert.equal(result.outcome, "native-input-required");
|
|
232
|
+
assert.deepEqual(result.missing_input, ["actor", "reason"]);
|
|
233
|
+
assert.equal(result.mutation_outcome, "none");
|
|
918
234
|
});
|
|
919
235
|
|
|
920
|
-
test("
|
|
236
|
+
test("RECOVER rejects caller-authored authorization before status, UI, or native mutation", async () => {
|
|
237
|
+
let statusCalls = 0;
|
|
921
238
|
const native = {
|
|
922
|
-
async
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
assert.equal(
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
239
|
+
targetStatus: async () => { statusCalls += 1; throw new Error("must not read status"); },
|
|
240
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
|
|
241
|
+
const result = await __testing.executeReviewControllerOperation({
|
|
242
|
+
operation: "recover",
|
|
243
|
+
input: JSON.stringify({ predecessorLineage: "predecessor", expectedPredecessorRevision: "revision", successorLineage: "successor", disposition: "invalidated", actor: "maintainer", reason: "recover", maintainerAuthorization: "caller-authored" }),
|
|
244
|
+
}, process.cwd(), native);
|
|
245
|
+
assert.equal(result.outcome, "native-recovery-caller-authorization-rejected");
|
|
246
|
+
assert.equal(statusCalls, 0);
|
|
247
|
+
assert.equal(result.mutation_outcome, "none");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("RECONCILE_AUTHORITY returns exact missing-input guidance before native mutation", async () => {
|
|
251
|
+
const native = { reconcileAuthority: async () => { throw new Error("must not reconcile"); } } as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
|
|
252
|
+
const result = await __testing.executeReviewControllerOperation({ operation: "reconcile-authority", input: JSON.stringify({ predecessorLineage: "predecessor" }) }, process.cwd(), native);
|
|
253
|
+
assert.equal(result.outcome, "native-input-required");
|
|
254
|
+
assert.deepEqual(result.missing_input, ["expectedPredecessorRevision", "successorLineage", "expectedSuccessorRevision", "actor", "reason"]);
|
|
255
|
+
assert.equal(result.mutation_outcome, "none");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("native abandon rejects a legacy authorization before process launch", async () => {
|
|
259
|
+
const request = {
|
|
260
|
+
cwd: "/repo",
|
|
261
|
+
lineage: "abandoned",
|
|
262
|
+
expectedRevision: "revision-1",
|
|
263
|
+
snapshotIdentity: SHA,
|
|
264
|
+
capturedLensResults: ["00-risk.json"],
|
|
265
|
+
findingsPresent: true,
|
|
266
|
+
evidenceRecordsPresent: false,
|
|
267
|
+
actor: "maintainer",
|
|
268
|
+
reason: "discard candidate",
|
|
269
|
+
};
|
|
270
|
+
const queue = queuedAdapter([]);
|
|
937
271
|
await assert.rejects(
|
|
938
|
-
|
|
939
|
-
/
|
|
272
|
+
() => client(queue.adapter).abandon({ ...request, maintainerAuthorization: "gentle-ai.review-abandon-authorization/v1" }),
|
|
273
|
+
/exact lineage, revision, snapshot, reason, discarded-work, and actor binding/,
|
|
940
274
|
);
|
|
941
|
-
assert.equal(calls.length, 0);
|
|
942
|
-
const details = await runControllerOperation({
|
|
943
|
-
operation: "recover-lock",
|
|
944
|
-
input: JSON.stringify({ ownerHash: "a".repeat(64), lineage: "stuck", actor: "maintainer", reason: "stale lock" }),
|
|
945
|
-
}, native);
|
|
946
|
-
assert.equal(details.native_operation, "review reclaim");
|
|
947
|
-
assert.equal(details.mutation_performed, true);
|
|
948
|
-
assert.deepEqual(details.result, RECLAIM_RECORD);
|
|
949
|
-
assert.equal(calls.length, 1);
|
|
950
|
-
assert.equal(calls[0]?.operation, "reclaim");
|
|
951
|
-
assert.equal(calls[0]?.request.lineage, "stuck");
|
|
275
|
+
assert.equal(queue.calls.length, 0);
|
|
952
276
|
});
|
|
953
277
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
278
|
+
function recoveryStatus(options: {
|
|
279
|
+
lineageId?: string;
|
|
280
|
+
revision?: string;
|
|
281
|
+
targetIdentity?: string;
|
|
282
|
+
disposition?: "scope_changed" | "invalidated" | "escalated";
|
|
283
|
+
action?: "recover" | "start";
|
|
284
|
+
} = {}) {
|
|
285
|
+
return {
|
|
286
|
+
action: options.action ?? "recover",
|
|
287
|
+
actionDisposition: options.disposition ?? "invalidated",
|
|
288
|
+
authority: { lineageId: options.lineageId ?? "predecessor", revision: options.revision ?? "revision-1" },
|
|
289
|
+
targetIdentity: options.targetIdentity ?? SHA,
|
|
290
|
+
raw: { schema: "gentle-ai.review-integration.status/v5" },
|
|
291
|
+
} as unknown as import("../lib/review-integration-v2.ts").ReviewStatusV3;
|
|
292
|
+
}
|
|
964
293
|
|
|
965
|
-
|
|
966
|
-
const tools = new Map<string, { execute: (
|
|
967
|
-
|
|
294
|
+
function registeredController(nativeReviewCli: import("../lib/native-review-cli.ts").NativeReviewCli) {
|
|
295
|
+
const tools = new Map<string, { execute: (toolCallId: string, params: unknown, signal: AbortSignal | undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<{ details?: unknown }> }>();
|
|
296
|
+
createGentleAiExtension({ nativeReviewCli })({
|
|
968
297
|
on() {},
|
|
969
|
-
registerTool(definition: { name: string; execute:
|
|
970
|
-
tools.set(definition.name, definition as unknown as { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> });
|
|
971
|
-
},
|
|
298
|
+
registerTool(definition: { name: string; execute: (toolCallId: string, params: unknown, signal: AbortSignal | undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<{ details?: unknown }> }) { tools.set(definition.name, definition); },
|
|
972
299
|
registerCommand() {},
|
|
973
|
-
} as unknown as ExtensionAPI;
|
|
974
|
-
const { native, calls } = fakeRecoveryNative(RECLAIM_RECORD);
|
|
975
|
-
createGentleAiExtension({ nativeReviewCli: native })(pi);
|
|
300
|
+
} as unknown as ExtensionAPI);
|
|
976
301
|
const controller = tools.get("gentle_review");
|
|
977
302
|
assert.ok(controller);
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
await assert.rejects(
|
|
981
|
-
controller.execute("headless-reset", {
|
|
982
|
-
operation: "reset",
|
|
983
|
-
input: JSON.stringify({
|
|
984
|
-
repositoryId: "repo-id",
|
|
985
|
-
commonDirHash: "c".repeat(64),
|
|
986
|
-
inventoryHash: "d".repeat(64),
|
|
987
|
-
confirmation: "DESTROY REVIEW AUTHORITY repo-id",
|
|
988
|
-
lineage: "stuck",
|
|
989
|
-
actor: "maintainer",
|
|
990
|
-
reason: "incomplete",
|
|
991
|
-
}),
|
|
992
|
-
}, undefined, undefined, ctx),
|
|
993
|
-
/interactive Pi UI.*fails closed/i,
|
|
994
|
-
);
|
|
995
|
-
assert.equal(calls.length, 0);
|
|
996
|
-
});
|
|
303
|
+
return controller;
|
|
304
|
+
}
|
|
997
305
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
const tools = new Map<string, { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> }>();
|
|
1002
|
-
const pi = { on() {}, registerTool(definition: { name: string; execute: never }) { tools.set(definition.name, definition as never); }, registerCommand() {} } as unknown as ExtensionAPI;
|
|
1003
|
-
const { native, calls } = fakeRecoveryNative(RECLAIM_RECORD);
|
|
1004
|
-
createGentleAiExtension({ nativeReviewCli: native })(pi);
|
|
1005
|
-
const controller = tools.get("gentle_review");
|
|
1006
|
-
assert.ok(controller);
|
|
1007
|
-
const cwd = scratchDir("gentle-pi-native-reset-challenge-");
|
|
1008
|
-
const challenge = {
|
|
1009
|
-
repositoryId: "repo-id",
|
|
1010
|
-
commonDirHash: "c".repeat(64),
|
|
1011
|
-
inventoryHash: "d".repeat(64),
|
|
1012
|
-
confirmation: "DESTROY REVIEW AUTHORITY repo-id",
|
|
1013
|
-
};
|
|
1014
|
-
const reclaim = { lineage: "stuck", actor: "maintainer", reason: "incomplete" };
|
|
1015
|
-
for (const key of ["repositoryId", "commonDirHash", "inventoryHash", "confirmation"] as const) {
|
|
1016
|
-
const incomplete: Record<string, unknown> = { ...challenge, ...reclaim };
|
|
1017
|
-
delete incomplete[key];
|
|
1018
|
-
await assert.rejects(
|
|
1019
|
-
controller.execute(`reset-missing-${key}`, { operation: "reset", input: JSON.stringify(incomplete) }, undefined, undefined, {
|
|
1020
|
-
cwd, hasUI: true, ui: { confirm: async () => true },
|
|
1021
|
-
} as unknown as ExtensionContext),
|
|
1022
|
-
new RegExp(`Review controller reset requires an exact string ${key}`),
|
|
1023
|
-
);
|
|
1024
|
-
}
|
|
1025
|
-
assert.equal(calls.length, 0);
|
|
1026
|
-
let prompt = "";
|
|
1027
|
-
const approved = await controller.execute("approved-reset", { operation: "reset", input: JSON.stringify({ ...challenge, ...reclaim }) }, undefined, undefined, {
|
|
1028
|
-
cwd,
|
|
1029
|
-
hasUI: true,
|
|
1030
|
-
ui: { confirm: async (_title: string, message: string) => { prompt = message; return true; } },
|
|
1031
|
-
} as unknown as ExtensionContext) as { details: Record<string, unknown> };
|
|
1032
|
-
assert.match(prompt, /Repository: repo-id/);
|
|
1033
|
-
assert.match(prompt, /Exact challenge: DESTROY REVIEW AUTHORITY repo-id/);
|
|
1034
|
-
assert.match(prompt, /This invalidates all prior review authority for this repository\./);
|
|
1035
|
-
assert.equal(approved.details.mutation_performed, true);
|
|
1036
|
-
assert.equal(calls.length, 1);
|
|
1037
|
-
assert.equal(calls[0]?.operation, "reclaim");
|
|
1038
|
-
});
|
|
306
|
+
function interactiveContext(confirm: boolean): ExtensionContext {
|
|
307
|
+
return { cwd: process.cwd(), hasUI: true, ui: { confirm: async () => confirm } } as unknown as ExtensionContext;
|
|
308
|
+
}
|
|
1039
309
|
|
|
1040
|
-
test("
|
|
1041
|
-
const
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
const parameters = {
|
|
1053
|
-
operation: "reconcile-authority",
|
|
1054
|
-
input: JSON.stringify({
|
|
1055
|
-
predecessorLineage: "predecessor",
|
|
1056
|
-
expectedPredecessorRevision: "predecessor-revision",
|
|
1057
|
-
successorLineage: "successor",
|
|
1058
|
-
expectedSuccessorRevision: "successor-revision",
|
|
1059
|
-
actor: "maintainer",
|
|
1060
|
-
reason: "invalid recovery edge",
|
|
1061
|
-
}),
|
|
310
|
+
test("maintenance cancellation preserves the exact signal and unknown mutation outcome", async () => {
|
|
311
|
+
const controller = new AbortController();
|
|
312
|
+
const request = {
|
|
313
|
+
cwd: "/repo", lineage: "abandoned", expectedRevision: "revision-1", snapshotIdentity: SHA,
|
|
314
|
+
capturedLensResults: ["00-risk.json"], findingsPresent: true, evidenceRecordsPresent: false,
|
|
315
|
+
actor: "maintainer", reason: "discard candidate",
|
|
316
|
+
};
|
|
317
|
+
const adapter: ExecFileAdapter = async (call) => {
|
|
318
|
+
assert.equal(call.signal, controller.signal);
|
|
319
|
+
const error = new Error("cancelled");
|
|
320
|
+
error.name = "AbortError";
|
|
321
|
+
throw error;
|
|
1062
322
|
};
|
|
1063
323
|
await assert.rejects(
|
|
1064
|
-
|
|
1065
|
-
|
|
324
|
+
() => client(adapter).abandon({ ...request, maintainerAuthorization: nativeReviewAbandonAuthorization(request), signal: controller.signal }),
|
|
325
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.code === "cancelled" && error.mutationOutcome === "unknown" && error.nextAction === "review.status",
|
|
1066
326
|
);
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
ui: { confirm: async (_title: string, message: string) => { prompt = message; return true; } },
|
|
1072
|
-
} as unknown as ExtensionContext) as { details: Record<string, unknown> };
|
|
1073
|
-
assert.match(prompt, /predecessor_revision=predecessor-revision/);
|
|
1074
|
-
assert.match(prompt, /successor_revision=successor-revision/);
|
|
1075
|
-
assert.equal(approved.details.mutation_outcome, "committed");
|
|
1076
|
-
assert.equal(calls.length, 1);
|
|
1077
|
-
});
|
|
1078
|
-
|
|
1079
|
-
test("published maintenance controller actions require exact inputs and fresh UI approval", async () => {
|
|
1080
|
-
const tools = new Map<string, { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> }>();
|
|
1081
|
-
const pi = { on() {}, registerTool(definition: { name: string; execute: never }) { tools.set(definition.name, definition as never); }, registerCommand() {} } as unknown as ExtensionAPI;
|
|
1082
|
-
const { native, calls } = fakeRecoveryNative(ABANDON_RECORD);
|
|
1083
|
-
createGentleAiExtension({ nativeReviewCli: native })(pi);
|
|
1084
|
-
const controller = tools.get("gentle_review");
|
|
1085
|
-
assert.ok(controller);
|
|
1086
|
-
const cwd = scratchDir("gentle-pi-v219-maintenance-");
|
|
1087
|
-
const abandon = { operation: "abandon", input: JSON.stringify({ lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ...ABANDON_DISCARDED_WORK, actor: "maintainer", reason: "retire pristine lineage" }) };
|
|
1088
|
-
await assert.rejects(controller.execute("headless-abandon", abandon, undefined, undefined, { cwd, hasUI: false, ui: { confirm: async () => true } } as unknown as ExtensionContext), /interactive Pi UI.*fails closed/i);
|
|
1089
|
-
await assert.rejects(controller.execute("denied-abandon", abandon, undefined, undefined, { cwd, hasUI: true, ui: { confirm: async () => false } } as unknown as ExtensionContext), /not explicitly authorized/);
|
|
1090
|
-
let abandonPrompt = "";
|
|
1091
|
-
const approved = await controller.execute("approved-abandon", abandon, undefined, undefined, { cwd, hasUI: true, ui: { confirm: async (_title: string, message: string) => { abandonPrompt = message; return true; } } } as unknown as ExtensionContext) as { details: Record<string, unknown> };
|
|
1092
|
-
assert.equal(approved.details.mutation_outcome, "committed");
|
|
1093
|
-
assert.match(abandonPrompt, /gentle-ai\.review-abandon-authorization\/v2/);
|
|
1094
|
-
assert.match(abandonPrompt, /captured_lens_results=00-risk\.json,01-readability\.json/);
|
|
1095
|
-
assert.match(abandonPrompt, /findings_present=true\nevidence_records_present=false\nactor=maintainer/);
|
|
1096
|
-
assert.equal(calls[0]?.operation, "abandon");
|
|
1097
|
-
assert.equal((calls[0]?.request as { maintainerAuthorization?: string }).maintainerAuthorization, ABANDON_AUTHORIZATION);
|
|
1098
|
-
assert.deepEqual(calls[0]?.request.capturedLensResults, ["00-risk.json", "01-readability.json"]);
|
|
1099
|
-
assert.equal(calls[0]?.request.findingsPresent, true);
|
|
1100
|
-
assert.equal(calls[0]?.request.evidenceRecordsPresent, false);
|
|
1101
|
-
const missingDiscarded = await runControllerOperation({ operation: "abandon", input: JSON.stringify({ lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", actor: "maintainer", reason: "retire pristine lineage" }) }, native);
|
|
1102
|
-
assert.equal(missingDiscarded.outcome, "native-input-required");
|
|
1103
|
-
assert.deepEqual(missingDiscarded.missing_input, ["capturedLensResults", "findingsPresent", "evidenceRecordsPresent"]);
|
|
1104
|
-
const invalidDiscarded = await runControllerOperation({ operation: "abandon", input: JSON.stringify({ lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", capturedLensResults: ["00-risk.json", " padded "], findingsPresent: true, evidenceRecordsPresent: false, actor: "maintainer", reason: "retire pristine lineage" }) }, native);
|
|
1105
|
-
assert.equal(invalidDiscarded.outcome, "native-input-required");
|
|
1106
|
-
assert.deepEqual(invalidDiscarded.missing_input, ["capturedLensResults"]);
|
|
1107
|
-
const legacy = await controller.execute("approved-legacy-quarantine", { operation: "quarantine-legacy", input: JSON.stringify({ repository: "/repo", lineage: "legacy", expectedRevision: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", diagnostic: LEGACY_FREEZE_DIAGNOSTIC, disposition: LEGACY_FREEZE_DISPOSITION, actor: "maintainer", reason: "quarantine malformed legacy freeze" }) }, undefined, undefined, { cwd, hasUI: true, ui: { confirm: async () => true } } as unknown as ExtensionContext) as { details: Record<string, unknown> };
|
|
1108
|
-
assert.equal(legacy.details.mutation_outcome, "committed");
|
|
1109
|
-
assert.equal(calls[1]?.operation, "quarantineLegacy");
|
|
1110
|
-
for (const input of [
|
|
1111
|
-
{ operation: "quarantine-legacy", input: JSON.stringify({ repository: "/repo", lineage: "legacy", expectedRevision: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", diagnostic: "unsupported historical v1 operation alias", disposition: LEGACY_FREEZE_DISPOSITION, actor: "maintainer", reason: "no-op" }) },
|
|
1112
|
-
{ operation: "reconcile-authority", input: JSON.stringify({ predecessorLineage: "predecessor", expectedPredecessorRevision: "predecessor-revision", successorLineage: "successor", expectedSuccessorRevision: "successor-revision", actor: "maintainer", reason: "no-op", anomalies: "malformed_recovery_authorization,unchanged_target" }) },
|
|
1113
|
-
]) {
|
|
1114
|
-
const details = await runControllerOperation(input, native);
|
|
1115
|
-
assert.equal(details.outcome, "native-input-invalid");
|
|
1116
|
-
}
|
|
1117
|
-
assert.equal(calls.length, 2);
|
|
1118
|
-
});
|
|
1119
|
-
|
|
1120
|
-
test("REPAIR_LEGACY_ALIAS derives fixed inputs from fresh inventory and requires fresh UI approval", async () => {
|
|
1121
|
-
const tools = new Map<string, { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> }>();
|
|
1122
|
-
const pi = { on() {}, registerTool(definition: { name: string; execute: never }) { tools.set(definition.name, definition as never); }, registerCommand() {} } as unknown as ExtensionAPI;
|
|
1123
|
-
const calls: Record<string, unknown>[] = [];
|
|
1124
|
-
const native = {
|
|
1125
|
-
async reviewStatus() {
|
|
1126
|
-
return {
|
|
1127
|
-
repository: "/canonical/repository",
|
|
1128
|
-
complete: true,
|
|
1129
|
-
entries: [{ version: "legacy-v1", status: "invalid", lineageId: "legacy-alias", revision: `sha256:${"c".repeat(64)}`, problems: [LEGACY_ALIAS_DIAGNOSTIC] }],
|
|
1130
|
-
};
|
|
1131
|
-
},
|
|
1132
|
-
async repairLegacyAlias(request: Record<string, unknown>) {
|
|
1133
|
-
calls.push(request);
|
|
1134
|
-
return { record: LEGACY_ALIAS_RECORD };
|
|
1135
|
-
},
|
|
1136
|
-
} as unknown as NativeReviewCli;
|
|
1137
|
-
createGentleAiExtension({ nativeReviewCli: native })(pi);
|
|
1138
|
-
const controller = tools.get("gentle_review");
|
|
1139
|
-
assert.ok(controller);
|
|
1140
|
-
const cwd = scratchDir("gentle-pi-v2110-alias-repair-");
|
|
1141
|
-
const parameters = { operation: "repair-legacy-alias", input: JSON.stringify({ lineage: "legacy-alias", actor: "maintainer", reason: "quarantine approved historical alias" }) };
|
|
327
|
+
const reconcile = {
|
|
328
|
+
cwd: "/repo", predecessorLineage: "predecessor", expectedPredecessorRevision: "revision-1",
|
|
329
|
+
successorLineage: "successor", expectedSuccessorRevision: "revision-2", actor: "maintainer", reason: "repair edge",
|
|
330
|
+
};
|
|
1142
331
|
await assert.rejects(
|
|
1143
|
-
|
|
1144
|
-
|
|
332
|
+
() => client(adapter).reconcileAuthority({ ...reconcile, maintainerAuthorization: nativeReviewReconcileAuthorization(reconcile), signal: controller.signal }),
|
|
333
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.code === "cancelled" && error.mutationOutcome === "unknown" && error.nextAction === "review.status",
|
|
1145
334
|
);
|
|
1146
|
-
assert.equal(calls.length, 0);
|
|
1147
|
-
let prompt = "";
|
|
1148
|
-
const approved = await controller.execute("approved-alias-repair", parameters, undefined, undefined, {
|
|
1149
|
-
cwd,
|
|
1150
|
-
hasUI: true,
|
|
1151
|
-
ui: { confirm: async (_title: string, message: string) => { prompt = message; return true; } },
|
|
1152
|
-
} as unknown as ExtensionContext) as { details: Record<string, unknown> };
|
|
1153
|
-
assert.match(prompt, /repository=\/canonical\/repository/);
|
|
1154
|
-
assert.match(prompt, /revision=sha256:c{64}/);
|
|
1155
|
-
assert.equal(approved.details.mutation_outcome, "committed");
|
|
1156
|
-
assert.equal(calls[0]?.repository, "/canonical/repository");
|
|
1157
|
-
assert.equal(calls[0]?.diagnostic, LEGACY_ALIAS_DIAGNOSTIC);
|
|
1158
|
-
assert.equal(calls[0]?.disposition, LEGACY_ALIAS_DISPOSITION);
|
|
1159
|
-
const injected = await runControllerOperation({ operation: "repair-legacy-alias", input: JSON.stringify({ lineage: "legacy-alias", actor: "maintainer", reason: "no-op", repository: "/attacker" }) }, native);
|
|
1160
|
-
assert.equal(injected.outcome, "native-input-invalid");
|
|
1161
|
-
await assert.rejects(runControllerOperation({ operation: "dispose-result", input: "{}" }, native), /operation/);
|
|
1162
|
-
});
|
|
1163
|
-
|
|
1164
|
-
// The capture-result gap, found by benchmarking Pi's client against the real
|
|
1165
|
-
// binary: `finalize()` emitted `--result <file>` per lens, a flag gentle-ai
|
|
1166
|
-
// retired because "a reviewer result supplied this way carries no
|
|
1167
|
-
// provider-owned admission, so it cannot prove the lens inspected the frozen
|
|
1168
|
-
// candidate". There was no capture-result surface at all, so Pi could only
|
|
1169
|
-
// finalize a zero-lens low-risk candidate. The suite never caught it because
|
|
1170
|
-
// it mocks the finalize response.
|
|
1171
|
-
//
|
|
1172
|
-
// Two inverse contracts meet here. `repair()` above MUST carry --contract.
|
|
1173
|
-
// `capture-result` MUST NOT: it is an additive headless command, not a
|
|
1174
|
-
// negotiated repository operation, and the provider's own tokens already
|
|
1175
|
-
// carry the repository context -- it accepts that or --cwd, never both. So
|
|
1176
|
-
// Pi passes the transition's tokens through verbatim and adds only --input.
|
|
1177
|
-
test("captureResult passes the provider tokens through verbatim and carries no --contract", async (t) => {
|
|
1178
|
-
t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
|
|
1179
|
-
// The full live envelope shape, confirmed against real capture-result runs
|
|
1180
|
-
// on both the pinned line and gentle-ai main (tests/fixtures/devbinary/
|
|
1181
|
-
// result-artifact-v2.captured.json): the opaque locator prefix is rart1_.
|
|
1182
|
-
const manifest = {
|
|
1183
|
-
schema: "gentle-ai.review-result-artifact/v2",
|
|
1184
|
-
capability: "review.native_result_artifact",
|
|
1185
|
-
reference: "rart1_" + "b".repeat(64),
|
|
1186
|
-
sha256: "sha256:" + "f".repeat(64),
|
|
1187
|
-
lineage_id: "review-1d5aadacc600e167",
|
|
1188
|
-
target_identity: "sha256:" + "d".repeat(64),
|
|
1189
|
-
lens: "review-reliability",
|
|
1190
|
-
selected_order: 0,
|
|
1191
|
-
subject_hash: "sha256:" + "a".repeat(64),
|
|
1192
|
-
admission_decision: "completed",
|
|
1193
|
-
};
|
|
1194
|
-
const { adapter, calls } = queuedAdapter([{ stdout: JSON.stringify(manifest) }]);
|
|
1195
|
-
const cli = new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
|
|
1196
|
-
|
|
1197
|
-
const tokens = [
|
|
1198
|
-
"--lineage=review-1d5aadacc600e167",
|
|
1199
|
-
"--expected-revision=sha256:" + "c".repeat(64),
|
|
1200
|
-
"--target=sha256:" + "d".repeat(64),
|
|
1201
|
-
"--repository-context=rctx1_" + "e".repeat(64),
|
|
1202
|
-
"--lens=review-reliability",
|
|
1203
|
-
"--order=0",
|
|
1204
|
-
"--subject-hash=" + manifest.subject_hash,
|
|
1205
|
-
];
|
|
1206
|
-
const captured = await cli.captureResult({ argumentTokens: tokens, resultDocument: JSON.stringify({ subject_hash: manifest.subject_hash, inspection: { status: "completed", paths: ["a.ts"] }, findings: [], evidence: ["reviewed the complete frozen candidate scope"] }) });
|
|
1207
|
-
|
|
1208
|
-
assert.equal(captured.subjectHash, manifest.subject_hash);
|
|
1209
|
-
assert.equal(captured.admissionDecision, "completed");
|
|
1210
|
-
|
|
1211
|
-
// Exactly one invocation: capture-result is headless and never negotiates,
|
|
1212
|
-
// so it must not drag a capabilities preflight along with it.
|
|
1213
|
-
assert.equal(calls.length, 1);
|
|
1214
|
-
const argv = calls[0]!.arguments;
|
|
1215
|
-
assert.deepEqual(argv.slice(0, 2), ["review", "capture-result"]);
|
|
1216
|
-
assert.equal(argv.includes("--contract"), false, "capture-result accepts no --contract");
|
|
1217
|
-
assert.equal(argv.includes("--cwd"), false, "the provider tokens already carry the repository context");
|
|
1218
|
-
// Tokens pass through in order, untouched, and --input is the only addition.
|
|
1219
|
-
assert.deepEqual(argv.slice(2, 2 + tokens.length), tokens);
|
|
1220
|
-
assert.equal(argv.at(-2), "--input");
|
|
1221
|
-
assert.match(argv.at(-1) as string, /\S/);
|
|
1222
|
-
assert.equal(argv.length, 2 + tokens.length + 2);
|
|
1223
335
|
});
|
|
1224
336
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
capability: full.capability,
|
|
1246
|
-
subject_hash: full.subject_hash,
|
|
1247
|
-
admission_decision: full.admission_decision,
|
|
1248
|
-
lens: full.lens,
|
|
1249
|
-
reference: full.reference,
|
|
1250
|
-
};
|
|
1251
|
-
for (const body of [
|
|
1252
|
-
legacyPartial,
|
|
1253
|
-
{ ...full, reference: "rref1_" + "b".repeat(64) },
|
|
1254
|
-
{ ...full, unadvertised: true },
|
|
337
|
+
test("RECOVER derives a provider-bound authorization, rechecks it, and fails closed on every drift", async () => {
|
|
338
|
+
const requests: Array<Record<string, unknown>> = [];
|
|
339
|
+
const native = {
|
|
340
|
+
targetStatus: async () => recoveryStatus(),
|
|
341
|
+
recover: async (request: Record<string, unknown>) => { requests.push(request); return { record: { schema: "gentle-ai.review-recovery/v1", lineage: "successor" } }; },
|
|
342
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
|
|
343
|
+
const input = { predecessorLineage: "predecessor", expectedPredecessorRevision: "revision-1", successorLineage: "successor", disposition: "invalidated", actor: "maintainer", reason: "recover authority" };
|
|
344
|
+
const result = await __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify(input) }, process.cwd(), native, undefined, undefined, interactiveContext(true));
|
|
345
|
+
assert.equal(result.mutation_outcome, "committed");
|
|
346
|
+
assert.equal(requests.length, 1);
|
|
347
|
+
assert.match(String(requests[0]?.maintainerAuthorization), /^gentle-ai\.review-recovery-authorization\/v1\npredecessor_lineage=predecessor\npredecessor_revision=revision-1\ntarget_identity=/);
|
|
348
|
+
|
|
349
|
+
const wrongDisposition = await __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify({ ...input, disposition: "escalated" }) }, process.cwd(), native, undefined, undefined, interactiveContext(true));
|
|
350
|
+
assert.equal(wrongDisposition.outcome, "native-recovery-disposition-mismatch");
|
|
351
|
+
assert.equal(wrongDisposition.mutation_outcome, "none");
|
|
352
|
+
|
|
353
|
+
for (const status of [
|
|
354
|
+
recoveryStatus({ lineageId: "foreign" }),
|
|
355
|
+
recoveryStatus({ revision: "revision-2" }),
|
|
356
|
+
recoveryStatus({ action: "start" }),
|
|
1255
357
|
]) {
|
|
1256
|
-
const
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
358
|
+
const blocked = await __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify(input) }, process.cwd(), {
|
|
359
|
+
targetStatus: async () => status,
|
|
360
|
+
recover: async () => { throw new Error("must not recover"); },
|
|
361
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli, undefined, undefined, interactiveContext(true));
|
|
362
|
+
assert.equal(blocked.outcome, "native-recovery-status-mismatch");
|
|
363
|
+
assert.equal(blocked.mutation_outcome, "none");
|
|
1261
364
|
}
|
|
1262
|
-
});
|
|
1263
365
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
};
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
evidenceDocument: evidence,
|
|
1301
|
-
});
|
|
1302
|
-
assert.equal(staged, evidence);
|
|
1303
|
-
assert.equal(captured.recordDigest, record.record_digest);
|
|
1304
|
-
assert.equal(captured.outcome, "verification_failed");
|
|
1305
|
-
const argv = calls[1]!.arguments;
|
|
1306
|
-
assert.deepEqual(argv.slice(0, 2), ["review", "capture-evidence"]);
|
|
1307
|
-
assert.equal(argv.includes("--contract"), false);
|
|
1308
|
-
assert.equal(argv[argv.indexOf("--outcome") + 1], "verification_failed");
|
|
1309
|
-
await assert.rejects(
|
|
1310
|
-
cli.captureEvidence({ cwd: "/repo", lineageId: record.lineage_id, targetIdentity: record.target_identity, expectedRevision: record.authority_revision, outcome: "failed" as never, evidenceDocument: evidence }),
|
|
1311
|
-
/outcome must be passed, verification_failed, or procedural_tooling_failed/,
|
|
1312
|
-
);
|
|
1313
|
-
assert.equal(calls.length, 2, "outside-domain outcomes must fail before another native launch");
|
|
1314
|
-
});
|
|
1315
|
-
|
|
1316
|
-
test("negotiated finalize never emits the retired --result flag", async (t) => {
|
|
1317
|
-
t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
|
|
1318
|
-
const capabilities = v2Fixture<Record<string, unknown>>("capabilities.fixture.json");
|
|
1319
|
-
const capabilitiesBody = { ...capabilities, package: { ...(capabilities.package as Record<string, unknown>), version: GENTLE_AI_VERSION } };
|
|
1320
|
-
const finalizeBody = {
|
|
1321
|
-
schema: "gentle-ai.review-integration.operation/v2",
|
|
1322
|
-
contract: "gentle-ai.review-integration/v2",
|
|
1323
|
-
operation: "review.finalize",
|
|
1324
|
-
result: { operation: "review/finalize", lineage_id: "review-1d5aadacc600e167", state: "approved", action: "validate delivery", store_revision: "sha256:" + "f".repeat(64) },
|
|
1325
|
-
};
|
|
1326
|
-
const { adapter, calls } = queuedAdapter([
|
|
1327
|
-
{ stdout: JSON.stringify(capabilitiesBody) },
|
|
1328
|
-
{ stdout: JSON.stringify(finalizeBody) },
|
|
1329
|
-
]);
|
|
1330
|
-
const cli = new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
|
|
1331
|
-
|
|
1332
|
-
await cli.finalize({ cwd: "/repo", lineageId: "review-1d5aadacc600e167", capturedResults: true });
|
|
1333
|
-
|
|
1334
|
-
const argv = calls[1]!.arguments;
|
|
1335
|
-
assert.equal(argv.includes("--result"), false, "--result is retired; results reach authority through capture-result");
|
|
1336
|
-
assert.ok(argv.some((token) => token === "--captured-results" || token.startsWith("--captured-results=")), "finalize must tell the provider to discover the captured results");
|
|
1337
|
-
assert.ok(argv.includes("--contract"), "finalize IS negotiated, unlike capture-result");
|
|
366
|
+
let reads = 0;
|
|
367
|
+
const changed = await __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify(input) }, process.cwd(), {
|
|
368
|
+
targetStatus: async () => (++reads === 1 ? recoveryStatus() : recoveryStatus({ targetIdentity: `sha256:${"b".repeat(64)}` })),
|
|
369
|
+
recover: async () => { throw new Error("must not recover"); },
|
|
370
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli, undefined, undefined, interactiveContext(true));
|
|
371
|
+
assert.equal(changed.outcome, "native-recovery-authority-changed");
|
|
372
|
+
assert.equal(changed.mutation_outcome, "none");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("RECOVER, RESET, and RECONCILE keep provider inputs and failures authority-scoped", async () => {
|
|
376
|
+
const recoverMissing = await __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify({ predecessorLineage: "predecessor", disposition: "not-a-disposition" }) }, process.cwd(), {
|
|
377
|
+
recover: async () => { throw new Error("must not recover"); },
|
|
378
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli);
|
|
379
|
+
assert.deepEqual(recoverMissing.missing_input, ["expectedPredecessorRevision", "successorLineage", "disposition", "actor", "reason"]);
|
|
380
|
+
|
|
381
|
+
const resetUnavailable = await __testing.executeReviewControllerOperation({ operation: "reset", input: JSON.stringify({ lineage: "stuck", actor: "maintainer", reason: "incomplete" }) }, process.cwd(), null);
|
|
382
|
+
assert.equal(resetUnavailable.outcome, "native-recovery-unavailable");
|
|
383
|
+
|
|
384
|
+
const reconciliation = { predecessorLineage: "predecessor", expectedPredecessorRevision: "revision-1", successorLineage: "successor", expectedSuccessorRevision: "revision-2", actor: "maintainer", reason: "repair edge" };
|
|
385
|
+
const completed = await __testing.executeReviewControllerOperation({ operation: "reconcile-authority", input: JSON.stringify(reconciliation) }, process.cwd(), {
|
|
386
|
+
reconcileAuthority: async (request: Record<string, unknown>) => ({ record: { schema: "gentle-ai.review-reconcile-audit/v1", successor: request.successorLineage } }),
|
|
387
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli);
|
|
388
|
+
assert.equal(completed.mutation_outcome, "committed");
|
|
389
|
+
assert.deepEqual(completed.result, { schema: "gentle-ai.review-reconcile-audit/v1", successor: "successor" });
|
|
390
|
+
|
|
391
|
+
for (const error of [
|
|
392
|
+
new NativeReviewCliError("cancelled", "review/reconcile-authority", true, true, "cancelled"),
|
|
393
|
+
new NativeReviewCliError("non_zero", "review/reconcile-authority", true, true, "partial", undefined, { schema: "gentle-ai.review-reconcile-audit/v1", status: "partial" }),
|
|
394
|
+
]) {
|
|
395
|
+
const failed = await __testing.executeReviewControllerOperation({ operation: "reconcile-authority", input: JSON.stringify(reconciliation) }, process.cwd(), {
|
|
396
|
+
reconcileAuthority: async () => { throw error; },
|
|
397
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli);
|
|
398
|
+
assert.equal(failed.outcome, "native-operation-failed");
|
|
399
|
+
assert.equal(failed.mutation_outcome, "unknown");
|
|
400
|
+
assert.equal(failed.next_action, "review.status");
|
|
401
|
+
}
|
|
1338
402
|
});
|
|
1339
403
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
};
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
calls.push({ arguments: request.arguments });
|
|
1355
|
-
return { stdout: JSON.stringify(artifact), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
1356
|
-
}, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
|
|
1357
|
-
const tokens = [
|
|
1358
|
-
"--lineage=review-1d5aadacc600e167",
|
|
1359
|
-
`--expected-revision=sha256:${"a".repeat(64)}`,
|
|
1360
|
-
`--target=sha256:${"9".repeat(64)}`,
|
|
1361
|
-
`--repository-context=rctx1_${"c".repeat(64)}`,
|
|
1362
|
-
"--agent=pi",
|
|
1363
|
-
"--execute=true",
|
|
1364
|
-
];
|
|
1365
|
-
const captured = await cli.captureProviderRole({ captureOperation: "review.capture-refuter", argumentTokens: tokens, cwd: "/repo" });
|
|
1366
|
-
assert.equal(captured.role, "refuter");
|
|
1367
|
-
assert.equal(captured.lineageId, "review-1d5aadacc600e167");
|
|
1368
|
-
assert.deepEqual(calls[0]!.arguments, ["review", "capture-refuter", ...tokens], "the vector runs verbatim: nothing added, removed, or reordered");
|
|
1369
|
-
|
|
404
|
+
test("destructive maintenance remains UI-gated and derives authorization at the public controller boundary", async () => {
|
|
405
|
+
const calls: Array<Record<string, unknown>> = [];
|
|
406
|
+
const native = {
|
|
407
|
+
reclaim: async (request: Record<string, unknown>) => { calls.push(request); return { record: { schema: "gentle-ai.review-reclaim-audit/v1" } }; },
|
|
408
|
+
reconcileAuthority: async (request: Record<string, unknown>) => { calls.push(request); return { record: { schema: "gentle-ai.review-reconcile-audit/v1" } }; },
|
|
409
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
|
|
410
|
+
const controller = registeredController(native);
|
|
411
|
+
const reset = { operation: "reset", input: JSON.stringify({ repositoryId: "repo", commonDirHash: "a".repeat(64), inventoryHash: "b".repeat(64), confirmation: "DESTROY REVIEW AUTHORITY repo", lineage: "stuck", actor: "maintainer", reason: "incomplete" }) };
|
|
412
|
+
await assert.rejects(controller.execute("headless", reset, undefined, undefined, { ...interactiveContext(true), hasUI: false } as ExtensionContext), /interactive Pi UI.*fails closed/i);
|
|
413
|
+
await assert.rejects(controller.execute("declined", reset, undefined, undefined, interactiveContext(false)), /not explicitly authorized/);
|
|
414
|
+
const approved = await controller.execute("approved", reset, undefined, undefined, interactiveContext(true));
|
|
415
|
+
assert.equal((approved.details as { mutation_outcome?: string }).mutation_outcome, "committed");
|
|
416
|
+
assert.equal(calls.length, 1);
|
|
417
|
+
assert.equal(calls[0]?.lineage, "stuck");
|
|
1370
418
|
await assert.rejects(
|
|
1371
|
-
|
|
1372
|
-
/
|
|
419
|
+
controller.execute("missing-challenge", { operation: "reset", input: JSON.stringify({ commonDirHash: "a".repeat(64), inventoryHash: "b".repeat(64), confirmation: "DESTROY REVIEW AUTHORITY repo", lineage: "stuck", actor: "maintainer", reason: "incomplete" }) }, undefined, undefined, interactiveContext(true)),
|
|
420
|
+
/requires an exact string repositoryId/,
|
|
1373
421
|
);
|
|
1374
|
-
assert.equal(calls.length, 1, "an unknown role operation must fail before any native launch");
|
|
1375
|
-
});
|
|
1376
422
|
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
lineage_id: "review-1d5aadacc600e167",
|
|
1381
|
-
target_identity: `sha256:${"9".repeat(64)}`,
|
|
1382
|
-
role: "targeted-validator",
|
|
1383
|
-
captured: true,
|
|
1384
|
-
};
|
|
1385
|
-
for (const body of [
|
|
1386
|
-
{ ...base, captured: false },
|
|
1387
|
-
{ ...base, role: "reviewer" },
|
|
1388
|
-
{ ...base, schema: "gentle-ai.review-result-artifact/v2" },
|
|
1389
|
-
{ ...base, extra: true },
|
|
1390
|
-
]) {
|
|
1391
|
-
const cli = new NativeReviewCliV216(async () => ({ stdout: JSON.stringify(body), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false }), "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
|
|
1392
|
-
await assert.rejects(cli.captureProviderRole({ captureOperation: "review.capture-validation", argumentTokens: ["--agent=pi", "--execute=true"], cwd: "/repo" }));
|
|
1393
|
-
}
|
|
423
|
+
const reconciliation = await controller.execute("reconcile", { operation: "reconcile-authority", input: JSON.stringify({ predecessorLineage: "predecessor", expectedPredecessorRevision: "revision-1", successorLineage: "successor", expectedSuccessorRevision: "revision-2", actor: "maintainer", reason: "repair edge" }) }, undefined, undefined, interactiveContext(true));
|
|
424
|
+
assert.equal((reconciliation.details as { mutation_outcome?: string }).mutation_outcome, "committed");
|
|
425
|
+
assert.match(String(calls[1]?.maintainerAuthorization), /^gentle-ai\.review-reconcile-authorization\/v1\npredecessor_lineage=predecessor\npredecessor_revision=revision-1\nsuccessor_lineage=successor\nsuccessor_revision=revision-2/);
|
|
1394
426
|
});
|
|
1395
427
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
const capabilitiesBody = { ...capabilities, package: { ...(capabilities.package as Record<string, unknown>), version: GENTLE_AI_VERSION } };
|
|
1404
|
-
const plainBody = { operation: "review/finalize", lineage_id: "review-1d5aadacc600e167", state: "approved", action: "validate delivery", store_revision: `sha256:${"f".repeat(64)}` };
|
|
1405
|
-
const envelopeBody = {
|
|
1406
|
-
schema: "gentle-ai.review-integration.operation/v2",
|
|
1407
|
-
contract: "gentle-ai.review-integration/v2",
|
|
1408
|
-
operation: "review.finalize",
|
|
1409
|
-
result: { operation: "review/finalize", lineage_id: "review-1d5aadacc600e167", state: "approved", action: "validate delivery", store_revision: `sha256:${"f".repeat(64)}` },
|
|
1410
|
-
};
|
|
1411
|
-
const { adapter, calls } = queuedAdapter([
|
|
1412
|
-
{ stdout: JSON.stringify(capabilitiesBody) },
|
|
1413
|
-
{ stdout: JSON.stringify(plainBody) },
|
|
1414
|
-
{ stdout: JSON.stringify(envelopeBody) },
|
|
1415
|
-
]);
|
|
1416
|
-
const cli = new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
|
|
1417
|
-
const tokens = ["--lineage=review-1d5aadacc600e167", "--captured-results=true"];
|
|
1418
|
-
const plain = await cli.finalizeTransition({ cwd: "/repo", argumentTokens: tokens });
|
|
1419
|
-
assert.equal(plain.state, "approved");
|
|
1420
|
-
assert.deepEqual(calls[1]!.arguments, ["review", "finalize", ...tokens], "no --contract, --cwd, or document flag is invented for a rendered transition");
|
|
1421
|
-
const negotiated = await cli.finalizeTransition({ cwd: "/repo", argumentTokens: tokens });
|
|
1422
|
-
assert.equal(negotiated.storeRevision, `sha256:${"f".repeat(64)}`);
|
|
1423
|
-
await assert.rejects(cli.finalizeTransition({ cwd: "/repo", argumentTokens: [] }), /requires the provider-rendered argument tokens/);
|
|
1424
|
-
});
|
|
1425
|
-
|
|
1426
|
-
// Field defect (fambig, 2026-08-16): the evidence collect slot renders the
|
|
1427
|
-
// exact `review capture-evidence` submission tokens — fix-diff `--target`,
|
|
1428
|
-
// `--expected-revision`, and an opaque cwd-independent `--repository-context` —
|
|
1429
|
-
// with `{{outcome}}`/`{{input}}` slots. Satisfying the slot means executing
|
|
1430
|
-
// those tokens verbatim with only the two slot substitutions, exactly like
|
|
1431
|
-
// captureResult; identities are never reconstructed on the client.
|
|
1432
|
-
test("captureEvidenceSubmission executes the provider-rendered submission tokens verbatim with slot substitution", async (t) => {
|
|
1433
|
-
t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
|
|
1434
|
-
const capabilities = v2Fixture<Record<string, unknown>>("capabilities.fixture.json");
|
|
1435
|
-
const capabilitiesBody = { ...capabilities, package: { ...(capabilities.package as Record<string, unknown>), version: GENTLE_AI_VERSION } };
|
|
1436
|
-
const record = {
|
|
1437
|
-
schema: "gentle-ai.review-verification-evidence/v2",
|
|
1438
|
-
version: 2,
|
|
1439
|
-
lineage_id: "review-evidence-lineage",
|
|
1440
|
-
authority_revision: `sha256:${"a".repeat(64)}`,
|
|
1441
|
-
target_identity: `sha256:${"b".repeat(64)}`,
|
|
1442
|
-
candidate_tree: "c".repeat(40),
|
|
1443
|
-
paths_digest: `sha256:${"d".repeat(64)}`,
|
|
1444
|
-
paths: ["calc.go"],
|
|
1445
|
-
ledger_ids: ["R3-1"],
|
|
1446
|
-
raw_payload_sha256: `sha256:${"e".repeat(64)}`,
|
|
1447
|
-
raw_payload_bytes: 24,
|
|
1448
|
-
outcome: "passed",
|
|
1449
|
-
record_digest: `sha256:${"f".repeat(64)}`,
|
|
1450
|
-
};
|
|
1451
|
-
const argumentTokens = [
|
|
1452
|
-
`--lineage=${record.lineage_id}`,
|
|
1453
|
-
`--expected-revision=${record.authority_revision}`,
|
|
1454
|
-
`--target=${record.target_identity}`,
|
|
1455
|
-
`--repository-context=rctx1_${"e".repeat(64)}`,
|
|
1456
|
-
"--outcome={{outcome}}",
|
|
1457
|
-
"--input={{input}}",
|
|
1458
|
-
];
|
|
1459
|
-
let staged = "";
|
|
1460
|
-
const calls: Array<{ arguments: readonly string[] }> = [];
|
|
1461
|
-
const cli = new NativeReviewCliV216(async (request) => {
|
|
1462
|
-
calls.push({ arguments: request.arguments });
|
|
1463
|
-
if (request.arguments[1] === "capabilities") return { stdout: JSON.stringify(capabilitiesBody), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
1464
|
-
const inputToken = request.arguments.find((token) => token.startsWith("--input="));
|
|
1465
|
-
assert.ok(inputToken !== undefined);
|
|
1466
|
-
staged = readFileSync(inputToken.slice("--input=".length), "utf8");
|
|
1467
|
-
return { stdout: JSON.stringify(record), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
1468
|
-
}, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
|
|
1469
|
-
const evidence = "verification: go test ./... passed\n";
|
|
1470
|
-
const captured = await cli.captureEvidenceSubmission({
|
|
1471
|
-
argumentTokens,
|
|
1472
|
-
outcomeSubstitutionLocation: 4,
|
|
1473
|
-
inputSubstitutionLocation: 5,
|
|
1474
|
-
outcome: "passed",
|
|
1475
|
-
evidenceDocument: evidence,
|
|
1476
|
-
});
|
|
1477
|
-
assert.equal(staged, evidence, "the evidence bytes must be staged exactly");
|
|
1478
|
-
assert.equal(captured.recordDigest, record.record_digest);
|
|
1479
|
-
assert.equal(captured.targetIdentity, record.target_identity);
|
|
1480
|
-
const argv = calls[1]!.arguments;
|
|
1481
|
-
assert.deepEqual(argv.slice(0, 2), ["review", "capture-evidence"]);
|
|
1482
|
-
assert.deepEqual(argv.slice(2, 6), argumentTokens.slice(0, 4), "the identity-bearing tokens must pass through verbatim, in provider order");
|
|
1483
|
-
assert.equal(argv[6], "--outcome=passed");
|
|
1484
|
-
assert.match(String(argv[7]), /^--input=./);
|
|
1485
|
-
assert.equal(argv.length, 8);
|
|
1486
|
-
assert.equal(argv.includes("--cwd"), false, "the repository context is authoritative and cwd-independent");
|
|
1487
|
-
assert.equal(argv.includes("--contract"), false);
|
|
428
|
+
test("RECOVER requires a live UI decision and RECOVER_LOCK requires its owner binding", async () => {
|
|
429
|
+
const input = { predecessorLineage: "predecessor", expectedPredecessorRevision: "revision-1", successorLineage: "successor", disposition: "invalidated", actor: "maintainer", reason: "recover authority" };
|
|
430
|
+
const native = {
|
|
431
|
+
targetStatus: async () => recoveryStatus(),
|
|
432
|
+
recover: async () => ({ record: { schema: "gentle-ai.review-recovery/v1" } }),
|
|
433
|
+
reclaim: async () => ({ record: { schema: "gentle-ai.review-reclaim-audit/v1" } }),
|
|
434
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
|
|
1488
435
|
await assert.rejects(
|
|
1489
|
-
|
|
1490
|
-
/
|
|
436
|
+
__testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify(input) }, process.cwd(), native),
|
|
437
|
+
/fresh explicit authorization through the interactive Pi UI/,
|
|
1491
438
|
);
|
|
1492
439
|
await assert.rejects(
|
|
1493
|
-
|
|
1494
|
-
/
|
|
440
|
+
__testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify(input) }, process.cwd(), native, undefined, undefined, interactiveContext(false)),
|
|
441
|
+
/not explicitly authorized/,
|
|
1495
442
|
);
|
|
1496
443
|
await assert.rejects(
|
|
1497
|
-
|
|
1498
|
-
|
|
444
|
+
__testing.executeReviewControllerOperation({ operation: "recover-lock", input: JSON.stringify({ lineage: "stuck", actor: "maintainer", reason: "stale" }) }, process.cwd(), native),
|
|
445
|
+
/ownerHash/,
|
|
1499
446
|
);
|
|
1500
|
-
|
|
447
|
+
const missingReclaim = await __testing.executeReviewControllerOperation({ operation: "recover-lock", input: JSON.stringify({ ownerHash: "a".repeat(64) }) }, process.cwd(), native);
|
|
448
|
+
assert.deepEqual(missingReclaim.missing_input, ["lineage", "actor", "reason"]);
|
|
449
|
+
const recovered = await __testing.executeReviewControllerOperation({ operation: "recover-lock", input: JSON.stringify({ ownerHash: "a".repeat(64), lineage: "stuck", actor: "maintainer", reason: "stale" }) }, process.cwd(), native);
|
|
450
|
+
assert.equal(recovered.mutation_outcome, "committed");
|
|
1501
451
|
});
|
|
1502
452
|
|
|
1503
|
-
test("
|
|
1504
|
-
|
|
1505
|
-
const
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
};
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
"--correction-lines={{value}}",
|
|
1521
|
-
];
|
|
1522
|
-
const calls: Array<{ arguments: readonly string[] }> = [];
|
|
1523
|
-
let staged: string | undefined;
|
|
1524
|
-
const cli = new NativeReviewCliV216(async (request) => {
|
|
1525
|
-
calls.push({ arguments: request.arguments });
|
|
1526
|
-
if (request.arguments[1] === "capabilities") return { stdout: JSON.stringify(capabilitiesBody), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
1527
|
-
const validationToken = request.arguments.find((token) => token.startsWith("--validation="));
|
|
1528
|
-
if (validationToken !== undefined) staged = readFileSync(validationToken.slice("--validation=".length), "utf8");
|
|
1529
|
-
return { stdout: JSON.stringify(finalizeBody), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
1530
|
-
}, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
|
|
1531
|
-
|
|
1532
|
-
// Plan form: literal substitution, every other token verbatim.
|
|
1533
|
-
const planned = await cli.finalizeSubmission({ cwd: "/repo", argumentTokens: planTokens, valueSubstitutionLocation: 6, valueLiteral: "2" });
|
|
1534
|
-
assert.equal(planned.state, "correction_required");
|
|
1535
|
-
const planArgv = calls[1]!.arguments;
|
|
1536
|
-
assert.deepEqual(planArgv.slice(0, 2), ["review", "finalize"]);
|
|
1537
|
-
assert.deepEqual(planArgv.slice(2, 8), planTokens.slice(0, 6), "identity-bearing tokens must pass through verbatim, in provider order");
|
|
1538
|
-
assert.equal(planArgv[8], "--correction-lines=2");
|
|
1539
|
-
assert.equal(planArgv.length, 9);
|
|
1540
|
-
|
|
1541
|
-
// Validation form: the document is staged to a 0o600 artifact whose path substitutes {{value}}.
|
|
1542
|
-
const validationTokens = [...planTokens.slice(0, 6), "--validation={{value}}"];
|
|
1543
|
-
const document = JSON.stringify({ targeted_validation_request_hash: `sha256:${"9".repeat(64)}`, original_criteria: { passed: true, evidence: ["ok"] } });
|
|
1544
|
-
await cli.finalizeSubmission({ cwd: "/repo", argumentTokens: validationTokens, valueSubstitutionLocation: 6, valueDocument: document });
|
|
1545
|
-
assert.equal(staged, document, "the validation document must be staged byte-exact");
|
|
1546
|
-
|
|
1547
|
-
// Guards: exactly one substitution form, and a real {{value}} slot.
|
|
1548
|
-
await assert.rejects(
|
|
1549
|
-
cli.finalizeSubmission({ cwd: "/repo", argumentTokens: planTokens, valueSubstitutionLocation: 6, valueLiteral: "2", valueDocument: document }),
|
|
1550
|
-
/exactly one of valueLiteral or valueDocument/,
|
|
1551
|
-
);
|
|
1552
|
-
await assert.rejects(
|
|
1553
|
-
cli.finalizeSubmission({ cwd: "/repo", argumentTokens: planTokens.slice(0, 6), valueSubstitutionLocation: 0, valueLiteral: "2" }),
|
|
1554
|
-
/\{\{value\}\}/,
|
|
1555
|
-
);
|
|
1556
|
-
assert.equal(calls.length, 3, "rejected submissions must fail before another native launch");
|
|
453
|
+
test("REPAIR_LEGACY_ALIAS derives its immutable target from fresh native inventory", async () => {
|
|
454
|
+
const calls: Array<Record<string, unknown>> = [];
|
|
455
|
+
const native = {
|
|
456
|
+
reviewStatus: async () => ({
|
|
457
|
+
repository: "/canonical/repository",
|
|
458
|
+
complete: true,
|
|
459
|
+
entries: [{ version: "legacy-v1", status: "invalid", lineageId: "legacy-alias", revision: SHA, problems: [NATIVE_REVIEW_LEGACY_ALIAS_REPAIR.DIAGNOSTIC] }],
|
|
460
|
+
}),
|
|
461
|
+
repairLegacyAlias: async (request: Record<string, unknown>) => { calls.push(request); return { record: { schema: "gentle-ai.review-repair-audit/v1" } }; },
|
|
462
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
|
|
463
|
+
const result = await __testing.executeReviewControllerOperation({ operation: "repair-legacy-alias", input: JSON.stringify({ lineage: "legacy-alias", actor: "maintainer", reason: "repair alias" }) }, process.cwd(), native, undefined, undefined, interactiveContext(true));
|
|
464
|
+
assert.equal(result.mutation_outcome, "committed");
|
|
465
|
+
assert.equal(calls[0]?.repository, "/canonical/repository");
|
|
466
|
+
assert.equal(calls[0]?.expectedRevision, SHA);
|
|
467
|
+
assert.match(String(calls[0]?.maintainerAuthorization), /^gentle-ai\.review-legacy-alias-repair-authorization\/v1\nrepository=\/canonical\/repository/);
|
|
468
|
+
const injected = await __testing.executeReviewControllerOperation({ operation: "repair-legacy-alias", input: JSON.stringify({ lineage: "legacy-alias", actor: "maintainer", reason: "repair alias", repository: "/injected" }) }, process.cwd(), native, undefined, undefined, interactiveContext(true));
|
|
469
|
+
assert.equal(injected.outcome, "native-input-invalid");
|
|
1557
470
|
});
|