gentle-pi 2.1.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -12
- package/assets/agents/gentle-ai-worker.md +7 -3
- package/assets/agents/jd-fix-agent.md +1 -1
- package/assets/agents/jd-judge-a.md +3 -1
- package/assets/agents/jd-judge-b.md +3 -1
- package/assets/agents/review-readability.md +4 -1
- package/assets/agents/review-reliability.md +4 -1
- package/assets/agents/review-resilience.md +4 -1
- package/assets/agents/review-risk.md +4 -1
- package/assets/agents/sdd-apply.md +6 -1
- package/assets/agents/sdd-archive.md +6 -1
- package/assets/agents/sdd-design.md +6 -1
- package/assets/agents/sdd-explore.md +6 -2
- package/assets/agents/sdd-init.md +10 -2
- package/assets/agents/sdd-onboard.md +6 -1
- package/assets/agents/sdd-proposal.md +6 -1
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +6 -1
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +6 -1
- package/assets/agents/sdd-verify.md +6 -1
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +246 -67
- package/assets/orchestrator.md +7 -14
- package/assets/sdd-orchestrator-workflow.md +154 -9
- package/assets/support/sdd-status-contract.md +19 -1
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
- package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
- package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
- package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
- package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
- package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
- package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
- package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
- package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
- package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
- package/docs/native-authority-architecture.md +5 -5
- package/docs/review-integration.md +22 -2
- package/extensions/gentle-ai.ts +1595 -201
- package/extensions/sdd-init.ts +19 -6
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/git-commit-transaction.ts +77 -17
- package/lib/native-review-cli.ts +528 -65
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +527 -18
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +436 -0
- package/lib/review-integration-v2.ts +537 -36
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +53 -1
- package/package.json +5 -2
- package/runtime/gentle-ai-binary.mjs +173 -2
- package/runtime/git-commit-transaction.mjs +75 -15
- package/runtime/native-review-cli.mjs +524 -61
- package/runtime/review-integration-v2.mjs +536 -35
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/build-git-commit-transaction-runner.mjs +1 -0
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +23 -13
- package/scripts/maintainer/provider-relay-matrix.mjs +219 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/verify-package-files.mjs +110 -33
- package/skills/_shared/review-ledger-contract.md +4 -6
- package/skills/gentle-ai/SKILL.md +4 -4
- package/skills/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +7 -1
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- package/tests/background-subagents.test.ts +771 -0
- package/tests/crosslane/cross-lane.mjs +1169 -0
- package/tests/delegated-key-learnings-contract.test.ts +238 -0
- package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
- package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
- package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
- package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
- package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
- package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
- package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
- package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
- package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
- package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +336 -0
- package/tests/gentle-ai-installer.test.ts +46 -46
- package/tests/git-commit-transaction.test.ts +229 -1
- package/tests/maintainer/provider-relay.maintest.ts +265 -0
- package/tests/native-review-capability-contract.test.ts +48 -2
- package/tests/native-review-cli.test.ts +56 -0
- package/tests/native-review-consent.test.ts +164 -3
- package/tests/native-review-parity-runtime.test.ts +37 -0
- package/tests/native-review-parity.test.ts +218 -15
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/orchestrator-budget.test.ts +30 -5
- package/tests/package-manifest.test.ts +98 -72
- package/tests/provider-contract-bundle.test.ts +385 -0
- package/tests/provider-contract-mirror.test.ts +206 -0
- package/tests/provider-defect-handoff.test.ts +355 -0
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-candidate-view.test.ts +489 -9
- package/tests/review-compact-contract.test.ts +52 -119
- package/tests/review-controller-native-recovery.test.ts +643 -47
- package/tests/review-controller-native-routing.test.ts +1667 -222
- package/tests/review-controller-workspace-root.test.ts +17 -2
- package/tests/review-corrected-finalize-binding.test.ts +175 -0
- package/tests/review-dispatch-hydration-gap.test.ts +197 -0
- package/tests/review-host-relay-routing.test.ts +317 -0
- package/tests/review-host-relay.test.ts +520 -0
- package/tests/review-integration-v2-forward.test.ts +631 -0
- package/tests/review-integration-v2.test.ts +114 -0
- package/tests/review-ledger-contract.test.ts +12 -28
- package/tests/review-recovered-lineage-routing.test.ts +246 -0
- package/tests/review-relay-transport-agent.test.ts +249 -0
- package/tests/runtime-harness.mjs +242 -14
- package/tests/sdd-agent-tools.test.ts +18 -33
- package/tests/skill-collision-prefixes.test.ts +1 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- package/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- package/lib/native-review-remediation.ts +0 -49
- package/lib/review-compact.ts +0 -947
- package/lib/review-refuter-adapter.ts +0 -129
- package/lib/review-runtime-contract.ts +0 -68
- package/prompts/gcl.md +0 -54
- package/prompts/gis.md +0 -25
- package/prompts/gpr.md +0 -41
- package/prompts/gwr.md +0 -31
- package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
- package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
- package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
- package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
- package/tests/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
NativeReviewCliV214 as NativeReviewCliV214Production,
|
|
12
12
|
NativeReviewCliV216,
|
|
13
13
|
clearNativeReviewCapabilitiesCacheForTesting,
|
|
14
|
+
nativeReviewAbandonAuthorization,
|
|
14
15
|
nativeReviewLegacyAliasRepairAuthorization,
|
|
15
16
|
type ExecFileAdapter,
|
|
16
17
|
type NativeReviewCli,
|
|
@@ -69,7 +70,23 @@ const LEGACY_ALIAS_AUTHORIZATION = [
|
|
|
69
70
|
"actor=maintainer",
|
|
70
71
|
"reason=quarantine approved historical alias",
|
|
71
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;
|
|
72
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 = [
|
|
73
90
|
"gentle-ai.review-abandon-authorization/v1",
|
|
74
91
|
"lineage=pristine",
|
|
75
92
|
"revision=revision",
|
|
@@ -77,6 +94,16 @@ const ABANDON_AUTHORIZATION = [
|
|
|
77
94
|
"actor=maintainer",
|
|
78
95
|
"reason=retire pristine lineage",
|
|
79
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;
|
|
80
107
|
const LEGACY_QUARANTINE_AUTHORIZATION = [
|
|
81
108
|
"gentle-ai.review-legacy-quarantine-authorization/v1",
|
|
82
109
|
"repository=/repo",
|
|
@@ -97,6 +124,26 @@ const RECONCILE_AUTHORIZATION = [
|
|
|
97
124
|
"reason=invalid recovery edge",
|
|
98
125
|
].join("\n");
|
|
99
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");
|
|
100
147
|
|
|
101
148
|
function scratchDir(prefix: string): string {
|
|
102
149
|
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
@@ -106,8 +153,9 @@ function scratchDir(prefix: string): string {
|
|
|
106
153
|
|
|
107
154
|
interface RecordedNativeCall { operation: "reclaim" | "recover" | "reconcileAuthority" | "abandon" | "quarantineLegacy"; request: Record<string, unknown>; }
|
|
108
155
|
|
|
109
|
-
function fakeRecoveryNative(record: Record<string, unknown>): { native: NativeReviewCli; calls: RecordedNativeCall[] } {
|
|
156
|
+
function fakeRecoveryNative(record: Record<string, unknown>): { native: NativeReviewCli; calls: RecordedNativeCall[]; statusReads: Array<string | undefined> } {
|
|
110
157
|
const calls: RecordedNativeCall[] = [];
|
|
158
|
+
const statusReads: Array<string | undefined> = [];
|
|
111
159
|
const native = {
|
|
112
160
|
async reclaim(request: Record<string, unknown>) {
|
|
113
161
|
calls.push({ operation: "reclaim", request });
|
|
@@ -130,14 +178,23 @@ function fakeRecoveryNative(record: Record<string, unknown>): { native: NativeRe
|
|
|
130
178
|
return { record };
|
|
131
179
|
},
|
|
132
180
|
async targetStatus(request: { lineageId?: string }) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
actionDisposition: "invalidated",
|
|
136
|
-
authority: { lineageId: request.lineageId ?? "broken", revision: "rev-1" },
|
|
137
|
-
} as unknown;
|
|
181
|
+
statusReads.push(request.lineageId);
|
|
182
|
+
return recoveryTargetStatus(request.lineageId ?? "broken");
|
|
138
183
|
},
|
|
139
184
|
} as unknown as NativeReviewCli;
|
|
140
|
-
return { native, calls };
|
|
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
|
+
};
|
|
141
198
|
}
|
|
142
199
|
|
|
143
200
|
async function runControllerOperation(
|
|
@@ -156,6 +213,39 @@ async function runControllerOperation(
|
|
|
156
213
|
);
|
|
157
214
|
}
|
|
158
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
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** An interactive Pi context that records every prompt it is shown. */
|
|
235
|
+
function interactiveContext(prompts: string[], approve = true): ExtensionContext {
|
|
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;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const HEADLESS_CONTEXT = { hasUI: false, ui: { confirm: async () => true } } as unknown as ExtensionContext;
|
|
248
|
+
|
|
159
249
|
test("native reclaim wrapper issues the exact review reclaim command and returns the audit record", async () => {
|
|
160
250
|
const { adapter, calls } = queuedAdapter([VERSION_219, { stdout: JSON.stringify(RECLAIM_RECORD) }]);
|
|
161
251
|
const cli = new NativeReviewCliV214(adapter);
|
|
@@ -249,7 +339,7 @@ test("native v2.1.9 maintenance wrappers use exact argv and published authorizat
|
|
|
249
339
|
VERSION_219, { stdout: JSON.stringify({ operation: "review/reconcile-authority", record: RECONCILE_RECORD }) },
|
|
250
340
|
]);
|
|
251
341
|
const cli = new NativeReviewCliV214(adapter);
|
|
252
|
-
assert.deepEqual((await cli.abandon!({
|
|
342
|
+
assert.deepEqual((await cli.abandon!({ ...ABANDON_REQUEST })).record, ABANDON_RECORD);
|
|
253
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);
|
|
254
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);
|
|
255
345
|
assert.deepEqual(calls.filter((call) => call.arguments[0] === "review").map((call) => call.arguments), [
|
|
@@ -350,7 +440,7 @@ test("native repair-legacy-alias fails closed for stale bindings, malformed outp
|
|
|
350
440
|
|
|
351
441
|
test("native v2.1.9 maintenance wrappers fail closed before launch for unsupported versions and invalid bindings", async () => {
|
|
352
442
|
const unsupported = queuedAdapter([{ stdout: "gentle-ai 2.1.8\n" }]);
|
|
353
|
-
await assert.rejects(() => new NativeReviewCliV214(unsupported.adapter).abandon!({
|
|
443
|
+
await assert.rejects(() => new NativeReviewCliV214(unsupported.adapter).abandon!({ ...ABANDON_REQUEST }), NativeReviewCliError);
|
|
354
444
|
const invalid = queuedAdapter([]);
|
|
355
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);
|
|
356
446
|
assert.equal(invalid.calls.length, 0);
|
|
@@ -358,14 +448,14 @@ test("native v2.1.9 maintenance wrappers fail closed before launch for unsupport
|
|
|
358
448
|
|
|
359
449
|
test("native v2.1.9 maintenance wrappers preserve only valid prepared audit records on partial failures", async () => {
|
|
360
450
|
for (const [operation, invoke, result] of [
|
|
361
|
-
["review/abandon", (cli: NativeReviewCliV214) => cli.abandon!({
|
|
451
|
+
["review/abandon", (cli: NativeReviewCliV214) => cli.abandon!({ ...ABANDON_REQUEST }), ABANDON_RECORD],
|
|
362
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],
|
|
363
453
|
] as const) {
|
|
364
454
|
const queue = queuedAdapter([VERSION_219, { stdout: JSON.stringify({ operation, record: result }), stderr: "quarantine interrupted", exitCode: 1 }]);
|
|
365
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);
|
|
366
456
|
}
|
|
367
457
|
const malformed = queuedAdapter([VERSION_219, { stdout: JSON.stringify({ operation: "review/abandon" }), exitCode: 1 }]);
|
|
368
|
-
await assert.rejects(() => new NativeReviewCliV214(malformed.adapter).abandon!({
|
|
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);
|
|
369
459
|
});
|
|
370
460
|
|
|
371
461
|
test("native abandon forwards cancellation and preserves the unknown mutation outcome", async () => {
|
|
@@ -379,7 +469,75 @@ test("native abandon forwards cancellation and preserves the unknown mutation ou
|
|
|
379
469
|
error.name = "AbortError";
|
|
380
470
|
throw error;
|
|
381
471
|
};
|
|
382
|
-
await assert.rejects(() => new NativeReviewCliV214(adapter).abandon!({
|
|
472
|
+
await assert.rejects(() => new NativeReviewCliV214(adapter).abandon!({ ...ABANDON_REQUEST, signal: controller.signal }), (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.CANCELLED && error.mutationOutcome === "unknown" && error.nextAction === "review.status");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("native abandon authorization emits the exact nine-line v2 discarded-work binding", () => {
|
|
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);
|
|
383
541
|
});
|
|
384
542
|
|
|
385
543
|
test("native reconcile-authority refuses a mismatched authorization before process launch", async () => {
|
|
@@ -524,24 +682,23 @@ test("RESET without a native client fails closed as unavailable", async () => {
|
|
|
524
682
|
assert.equal(details.mutation_performed, false);
|
|
525
683
|
});
|
|
526
684
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
}, native);
|
|
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");
|
|
545
702
|
assert.equal(details.native_operation, "review recover");
|
|
546
703
|
assert.equal(details.mutation_performed, true);
|
|
547
704
|
assert.deepEqual(details.result, RECOVER_RECORD);
|
|
@@ -551,26 +708,144 @@ test("RECOVER maps to native review recover with the successor authority binding
|
|
|
551
708
|
assert.equal(calls[0]?.request.expectedPredecessorRevision, "rev-1");
|
|
552
709
|
assert.equal(calls[0]?.request.successorLineage, "successor");
|
|
553
710
|
assert.equal(calls[0]?.request.disposition, "invalidated");
|
|
554
|
-
|
|
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"]);
|
|
555
720
|
});
|
|
556
721
|
|
|
557
|
-
test("RECOVER
|
|
722
|
+
test("RECOVER rejects a caller-supplied maintainerAuthorization before reading or prompting", async () => {
|
|
723
|
+
const { native, calls, statusReads } = fakeRecoveryNative(RECOVER_RECORD);
|
|
724
|
+
const run = recoveryController(native, "gentle-pi-recover-caller-authorization-");
|
|
725
|
+
const prompts: string[] = [];
|
|
726
|
+
const details = await run(
|
|
727
|
+
"recover",
|
|
728
|
+
{ operation: "recover", input: JSON.stringify({ ...RECOVER_INPUT, maintainerAuthorization: RECOVER_AUTHORIZATION }) },
|
|
729
|
+
interactiveContext(prompts),
|
|
730
|
+
);
|
|
731
|
+
assert.equal(details.status, "blocked");
|
|
732
|
+
assert.equal(details.outcome, "native-recovery-caller-authorization-rejected");
|
|
733
|
+
assert.equal(details.mutation_performed, false);
|
|
734
|
+
assert.equal(details.mutation_outcome, "none");
|
|
735
|
+
assert.equal(details.next_action, "resubmit-without-maintainer-authorization");
|
|
736
|
+
assert.equal(calls.length, 0);
|
|
737
|
+
assert.deepEqual(statusReads, []);
|
|
738
|
+
assert.deepEqual(prompts, []);
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
test("RECOVER fails closed without an interactive Pi UI", async () => {
|
|
742
|
+
const { native, calls, statusReads } = fakeRecoveryNative(RECOVER_RECORD);
|
|
743
|
+
const run = recoveryController(native, "gentle-pi-recover-headless-");
|
|
744
|
+
await assert.rejects(
|
|
745
|
+
run("recover", { operation: "recover", input: JSON.stringify(RECOVER_INPUT) }, HEADLESS_CONTEXT),
|
|
746
|
+
/Review controller RECOVER requires fresh explicit authorization through the interactive Pi UI; headless execution fails closed/,
|
|
747
|
+
);
|
|
748
|
+
assert.equal(calls.length, 0);
|
|
749
|
+
// Fresh native evidence is read, but nothing mutates without a human.
|
|
750
|
+
assert.deepEqual(statusReads, ["broken"]);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
test("RECOVER refuses to mutate when the human declines the derived binding", async () => {
|
|
558
754
|
const { native, calls } = fakeRecoveryNative(RECOVER_RECORD);
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
755
|
+
const run = recoveryController(native, "gentle-pi-recover-declined-");
|
|
756
|
+
const prompts: string[] = [];
|
|
757
|
+
await assert.rejects(
|
|
758
|
+
run("recover", { operation: "recover", input: JSON.stringify(RECOVER_INPUT) }, interactiveContext(prompts, false)),
|
|
759
|
+
/Review controller RECOVER was not explicitly authorized/,
|
|
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),
|
|
841
|
+
);
|
|
570
842
|
assert.equal(details.outcome, "native-input-required");
|
|
571
843
|
assert.equal(details.native_operation, "review recover");
|
|
844
|
+
// The missing inputs are the native six, never the legacy reset quartet.
|
|
572
845
|
assert.deepEqual(details.missing_input, ["expectedPredecessorRevision", "successorLineage", "disposition", "actor", "reason"]);
|
|
573
846
|
assert.equal(calls.length, 0);
|
|
847
|
+
assert.deepEqual(statusReads, []);
|
|
848
|
+
assert.deepEqual(prompts, []);
|
|
574
849
|
});
|
|
575
850
|
|
|
576
851
|
test("RECONCILE_AUTHORITY routes one exact native mutation and returns its audit record", async () => {
|
|
@@ -720,6 +995,48 @@ test("destructive RESET still fails closed without fresh interactive authorizati
|
|
|
720
995
|
assert.equal(calls.length, 0);
|
|
721
996
|
});
|
|
722
997
|
|
|
998
|
+
// Regression guard for issue #212: separating RECOVER out must leave the legacy
|
|
999
|
+
// repository-wide RESET challenge exactly as it was.
|
|
1000
|
+
test("RESET still demands the complete legacy repository challenge and its own fresh approval", async () => {
|
|
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
|
+
});
|
|
1039
|
+
|
|
723
1040
|
test("RECONCILE_AUTHORITY requires fresh Pi approval for the exact seven-line binding", async () => {
|
|
724
1041
|
const tools = new Map<string, { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> }>();
|
|
725
1042
|
const pi = {
|
|
@@ -767,12 +1084,26 @@ test("published maintenance controller actions require exact inputs and fresh UI
|
|
|
767
1084
|
const controller = tools.get("gentle_review");
|
|
768
1085
|
assert.ok(controller);
|
|
769
1086
|
const cwd = scratchDir("gentle-pi-v219-maintenance-");
|
|
770
|
-
const abandon = { operation: "abandon", input: JSON.stringify({ lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", actor: "maintainer", reason: "retire pristine lineage" }) };
|
|
1087
|
+
const abandon = { operation: "abandon", input: JSON.stringify({ lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ...ABANDON_DISCARDED_WORK, actor: "maintainer", reason: "retire pristine lineage" }) };
|
|
771
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);
|
|
772
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/);
|
|
773
|
-
|
|
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> };
|
|
774
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/);
|
|
775
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"]);
|
|
776
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> };
|
|
777
1108
|
assert.equal(legacy.details.mutation_outcome, "committed");
|
|
778
1109
|
assert.equal(calls[1]?.operation, "quarantineLegacy");
|
|
@@ -845,13 +1176,20 @@ test("REPAIR_LEGACY_ALIAS derives fixed inputs from fresh inventory and requires
|
|
|
845
1176
|
// Pi passes the transition's tokens through verbatim and adds only --input.
|
|
846
1177
|
test("captureResult passes the provider tokens through verbatim and carries no --contract", async (t) => {
|
|
847
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_.
|
|
848
1182
|
const manifest = {
|
|
849
1183
|
schema: "gentle-ai.review-result-artifact/v2",
|
|
850
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,
|
|
851
1191
|
subject_hash: "sha256:" + "a".repeat(64),
|
|
852
1192
|
admission_decision: "completed",
|
|
853
|
-
lens: "review-reliability",
|
|
854
|
-
reference: "rref1_" + "b".repeat(64),
|
|
855
1193
|
};
|
|
856
1194
|
const { adapter, calls } = queuedAdapter([{ stdout: JSON.stringify(manifest) }]);
|
|
857
1195
|
const cli = new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
|
|
@@ -884,6 +1222,45 @@ test("captureResult passes the provider tokens through verbatim and carries no -
|
|
|
884
1222
|
assert.equal(argv.length, 2 + tokens.length + 2);
|
|
885
1223
|
});
|
|
886
1224
|
|
|
1225
|
+
// The admission answer routes through the exact-identity forward decoder:
|
|
1226
|
+
// a manifest missing its binding fields (the shape no real binary ever
|
|
1227
|
+
// emitted) or carrying a foreign locator prefix is refused, never partially
|
|
1228
|
+
// consumed. This is the decoder-freshness discipline reaching the consumer.
|
|
1229
|
+
test("captureResult refuses an under-specified or foreign-locator manifest", async (t) => {
|
|
1230
|
+
t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
|
|
1231
|
+
const full = {
|
|
1232
|
+
schema: "gentle-ai.review-result-artifact/v2",
|
|
1233
|
+
capability: "review.native_result_artifact",
|
|
1234
|
+
reference: "rart1_" + "b".repeat(64),
|
|
1235
|
+
sha256: "sha256:" + "f".repeat(64),
|
|
1236
|
+
lineage_id: "review-1d5aadacc600e167",
|
|
1237
|
+
target_identity: "sha256:" + "d".repeat(64),
|
|
1238
|
+
lens: "review-reliability",
|
|
1239
|
+
selected_order: 0,
|
|
1240
|
+
subject_hash: "sha256:" + "a".repeat(64),
|
|
1241
|
+
admission_decision: "completed",
|
|
1242
|
+
};
|
|
1243
|
+
const legacyPartial = {
|
|
1244
|
+
schema: full.schema,
|
|
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 },
|
|
1255
|
+
]) {
|
|
1256
|
+
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");
|
|
1257
|
+
await assert.rejects(cli.captureResult({
|
|
1258
|
+
argumentTokens: ["--lineage=review-1d5aadacc600e167", "--repository-context=rctx1_" + "e".repeat(64)],
|
|
1259
|
+
resultDocument: JSON.stringify({ subject_hash: full.subject_hash, inspection: { status: "completed", paths: ["a.ts"] }, findings: [], evidence: ["reviewed the complete frozen candidate scope"] }),
|
|
1260
|
+
}));
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
|
|
887
1264
|
test("captureEvidence stages exact bytes, uses the closed outcome argv, and decodes the native record", async (t) => {
|
|
888
1265
|
t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
|
|
889
1266
|
const capabilities = v2Fixture<Record<string, unknown>>("capabilities.fixture.json");
|
|
@@ -959,3 +1336,222 @@ test("negotiated finalize never emits the retired --result flag", async (t) => {
|
|
|
959
1336
|
assert.ok(argv.some((token) => token === "--captured-results" || token.startsWith("--captured-results=")), "finalize must tell the provider to discover the captured results");
|
|
960
1337
|
assert.ok(argv.includes("--contract"), "finalize IS negotiated, unlike capture-result");
|
|
961
1338
|
});
|
|
1339
|
+
|
|
1340
|
+
// gentle-pi#311 P4-roles: the provider-rendered self-contained role vectors
|
|
1341
|
+
// execute EXACTLY as rendered — one CLI invocation, verbatim tokens in
|
|
1342
|
+
// provider order, no --cwd, no --contract, no --input. Go materializes the
|
|
1343
|
+
// role prompt, runs its own locked-down pi process, and admits the verdict.
|
|
1344
|
+
test("captureProviderRole executes the exact rendered vector and decodes only the strict artifact", async () => {
|
|
1345
|
+
const artifact = {
|
|
1346
|
+
schema: "gentle-ai.review-provider-role-capture/v1",
|
|
1347
|
+
lineage_id: "review-1d5aadacc600e167",
|
|
1348
|
+
target_identity: `sha256:${"9".repeat(64)}`,
|
|
1349
|
+
role: "refuter",
|
|
1350
|
+
captured: true,
|
|
1351
|
+
};
|
|
1352
|
+
const calls: Array<{ arguments: readonly string[] }> = [];
|
|
1353
|
+
const cli = new NativeReviewCliV216(async (request) => {
|
|
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
|
+
|
|
1370
|
+
await assert.rejects(
|
|
1371
|
+
cli.captureProviderRole({ captureOperation: "review.capture-result", argumentTokens: tokens, cwd: "/repo" }),
|
|
1372
|
+
/supports only review\.capture-refuter and review\.capture-validation/,
|
|
1373
|
+
);
|
|
1374
|
+
assert.equal(calls.length, 1, "an unknown role operation must fail before any native launch");
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
test("captureProviderRole refuses an uncaptured or malformed role artifact", async () => {
|
|
1378
|
+
const base = {
|
|
1379
|
+
schema: "gentle-ai.review-provider-role-capture/v1",
|
|
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
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
// gentle-pi#311 P5: the provider-driven FINALIZE executes the rendered
|
|
1397
|
+
// transition tokens verbatim and decodes both answer shapes (the negotiated
|
|
1398
|
+
// operation envelope and the plain review/finalize shape a contract-less
|
|
1399
|
+
// transition produces).
|
|
1400
|
+
test("finalizeTransition runs the provider-rendered tokens verbatim and decodes both answer shapes", async (t) => {
|
|
1401
|
+
t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
|
|
1402
|
+
const capabilities = v2Fixture<Record<string, unknown>>("capabilities.fixture.json");
|
|
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);
|
|
1488
|
+
await assert.rejects(
|
|
1489
|
+
cli.captureEvidenceSubmission({ cwd: "/repo", argumentTokens, outcomeSubstitutionLocation: 4, inputSubstitutionLocation: 5, outcome: "passed", evidenceDocument: evidence }),
|
|
1490
|
+
/repository context or --cwd, never both/,
|
|
1491
|
+
);
|
|
1492
|
+
await assert.rejects(
|
|
1493
|
+
cli.captureEvidenceSubmission({ argumentTokens, outcomeSubstitutionLocation: 4, inputSubstitutionLocation: 5, outcome: "failed" as never, evidenceDocument: evidence }),
|
|
1494
|
+
/outcome must be passed, verification_failed, or procedural_tooling_failed/,
|
|
1495
|
+
);
|
|
1496
|
+
await assert.rejects(
|
|
1497
|
+
cli.captureEvidenceSubmission({ argumentTokens: argumentTokens.slice(0, 4), outcomeSubstitutionLocation: 0, inputSubstitutionLocation: 1, outcome: "passed", evidenceDocument: evidence }),
|
|
1498
|
+
/\{\{outcome\}\}/,
|
|
1499
|
+
);
|
|
1500
|
+
assert.equal(calls.length, 2, "every rejected submission must fail before another native launch");
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
test("finalizeSubmission executes the rendered finalize submission tokens verbatim with the {{value}} substitution", async (t) => {
|
|
1504
|
+
t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
|
|
1505
|
+
const capabilities = v2Fixture<Record<string, unknown>>("capabilities.fixture.json");
|
|
1506
|
+
const capabilitiesBody = { ...capabilities, package: { ...(capabilities.package as Record<string, unknown>), version: GENTLE_AI_VERSION } };
|
|
1507
|
+
const finalizeBody = {
|
|
1508
|
+
schema: "gentle-ai.review-integration.operation/v2",
|
|
1509
|
+
contract: "gentle-ai.review-integration/v2",
|
|
1510
|
+
operation: "review.finalize",
|
|
1511
|
+
result: { operation: "review/finalize", lineage_id: "review-1d5aadacc600e167", state: "correction_required", action: "continue the current review state", store_revision: `sha256:${"f".repeat(64)}` },
|
|
1512
|
+
};
|
|
1513
|
+
const planTokens = [
|
|
1514
|
+
"--contract=gentle-ai.review-integration/v2",
|
|
1515
|
+
"--lineage=review-1d5aadacc600e167",
|
|
1516
|
+
`--expected-revision=sha256:${"a".repeat(64)}`,
|
|
1517
|
+
`--target=sha256:${"b".repeat(64)}`,
|
|
1518
|
+
`--request-hash=sha256:${"c".repeat(64)}`,
|
|
1519
|
+
`--repository-context=rctx1_${"e".repeat(64)}`,
|
|
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");
|
|
1557
|
+
});
|