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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
7
7
|
import test from "node:test";
|
|
8
8
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { __testing, createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
@@ -22,7 +22,6 @@ import { canonicalJsonV1, domainHashV1 } from "../lib/review-canonical.ts";
|
|
|
22
22
|
import { CandidateViewRegistry, deriveChangedPathManifest } from "../lib/review-candidate-view.ts";
|
|
23
23
|
import { inspectLegacyReviewAuthorityV1 } from "../lib/review-legacy-detector.ts";
|
|
24
24
|
import { resolveRepositoryAuthorityV1 } from "../lib/review-repository.ts";
|
|
25
|
-
import { NATIVE_REVIEW_REMEDIATION, classifyNativeReviewRemediation } from "../lib/native-review-remediation.ts";
|
|
26
25
|
import type { AuthorityRepairAssessmentV1, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
27
26
|
|
|
28
27
|
interface RegisteredTool {
|
|
@@ -46,6 +45,7 @@ interface RegisteredCommandFixture {
|
|
|
46
45
|
|
|
47
46
|
interface Runtime {
|
|
48
47
|
controller: RegisteredTool;
|
|
48
|
+
scopeReader: RegisteredTool;
|
|
49
49
|
toolCall: ToolCallHandler;
|
|
50
50
|
commands: Map<string, RegisteredCommandFixture>;
|
|
51
51
|
}
|
|
@@ -90,15 +90,23 @@ function runtime(
|
|
|
90
90
|
registerCommand(name: string, definition: RegisteredCommandFixture) { commands.set(name, definition); },
|
|
91
91
|
} as unknown as ExtensionAPI);
|
|
92
92
|
const controller = tools.get("gentle_review");
|
|
93
|
+
const scopeReader = tools.get("gentle_review_scope");
|
|
93
94
|
assert.ok(controller);
|
|
95
|
+
assert.ok(scopeReader);
|
|
94
96
|
assert.ok(toolCall);
|
|
95
|
-
return { controller, toolCall, commands };
|
|
97
|
+
return { controller, scopeReader, toolCall, commands };
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
function context(cwd: string, signal?: AbortSignal): ExtensionContext {
|
|
99
101
|
return { cwd, hasUI: false, signal, ui: { confirm: async () => true } } as unknown as ExtensionContext;
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
function compactCandidateContextManifest(task: string): { encoded: string; sha256: string } {
|
|
105
|
+
const match = /Frozen changed scope manifest \(gzip\+base64url\): `([A-Za-z0-9_-]+)`\.\nFrozen changed scope manifest SHA-256: `([0-9a-f]{64})`\./.exec(task);
|
|
106
|
+
assert.ok(match, "expected a compact candidate context manifest");
|
|
107
|
+
return { encoded: match[1]!, sha256: match[2]! };
|
|
108
|
+
}
|
|
109
|
+
|
|
102
110
|
function interactiveContext(cwd: string, signal?: AbortSignal): ExtensionContext {
|
|
103
111
|
return { cwd, hasUI: true, signal, ui: { confirm: async () => true } } as unknown as ExtensionContext;
|
|
104
112
|
}
|
|
@@ -268,7 +276,7 @@ function fakeNative(overrides: Partial<NativeReviewCli> = {}): NativeReviewCli {
|
|
|
268
276
|
const lineageId = request.lineageId ?? "";
|
|
269
277
|
return lineageId === ""
|
|
270
278
|
? candidateStartTargetStatus(request)
|
|
271
|
-
:
|
|
279
|
+
: candidateFinalizeTargetStatus(request, lineageId);
|
|
272
280
|
},
|
|
273
281
|
...overrides,
|
|
274
282
|
};
|
|
@@ -400,6 +408,24 @@ function candidateStartTargetStatus(request: Parameters<NonNullable<NativeReview
|
|
|
400
408
|
}
|
|
401
409
|
}
|
|
402
410
|
|
|
411
|
+
function candidateFinalizeTargetStatus(request: Parameters<NonNullable<NativeReviewCli["targetStatus"]>>[0], lineageId: string): ReviewStatusV3 {
|
|
412
|
+
let candidate: ReturnType<CandidateViewRegistry["create"]> | undefined;
|
|
413
|
+
try {
|
|
414
|
+
candidate = new CandidateViewRegistry().create({ contributorRoot: request.cwd });
|
|
415
|
+
return targetStatusFixture({
|
|
416
|
+
lineageId,
|
|
417
|
+
baseTree: candidate.baseTree,
|
|
418
|
+
currentCandidateTree: candidate.candidateTree,
|
|
419
|
+
paths: candidate.paths,
|
|
420
|
+
projection: request.projection ?? "workspace",
|
|
421
|
+
});
|
|
422
|
+
} catch {
|
|
423
|
+
return targetStatusFixture({ lineageId });
|
|
424
|
+
} finally {
|
|
425
|
+
candidate?.cleanup();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
403
429
|
function bindReviewerManifest(status: ReviewStatusV3, cwd: string, manifestHash = `sha256:${"7".repeat(64)}`): ReviewStatusV3 {
|
|
404
430
|
const manifest = deriveChangedPathManifest(cwd, status.projection.baseTree, status.projection.currentCandidateTree).map((entry) => ({
|
|
405
431
|
...entry,
|
|
@@ -555,12 +581,60 @@ test("new ordinary START and native-lineage FINALIZE use exactly one native call
|
|
|
555
581
|
}));
|
|
556
582
|
const start = await controller.execute("start", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
|
|
557
583
|
assert.deepEqual(start.details, { operation: "start", result: { lineage_id: "native-lineage", state: "reviewing", risk_tier: "medium", selected_lenses: ["review-reliability"], changed_files: 2, original_changed_lines: 7, correction_budget: 4, action: "created", lenses_required: true }, workspace_root: cwd });
|
|
558
|
-
const finalize = await controller.execute("finalize", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({
|
|
584
|
+
const finalize = await controller.execute("finalize", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({}) }, undefined, undefined, context(cwd));
|
|
559
585
|
assert.deepEqual(finalize.details, { operation: "finalize", result: { lineage_id: "native-lineage", state: "approved", action: "approved", store_revision: "r1", receipt_path: "/opaque/receipt" } });
|
|
560
586
|
assert.equal(starts, 1);
|
|
561
587
|
assert.equal(finalizes, 1);
|
|
562
588
|
});
|
|
563
589
|
|
|
590
|
+
test("native FINALIZE resolves STATUS and mutation against the verified frozen candidate root", async (t) => {
|
|
591
|
+
const cwd = repository(t);
|
|
592
|
+
writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
|
|
593
|
+
const candidateViews = new CandidateViewRegistry();
|
|
594
|
+
const statusRoots: string[] = [];
|
|
595
|
+
let finalizeRoot: string | undefined;
|
|
596
|
+
const native = fakeNative({
|
|
597
|
+
targetStatus: async (request) => {
|
|
598
|
+
if (request.lineageId === undefined) return candidateStartTargetStatus(request);
|
|
599
|
+
statusRoots.push(request.cwd);
|
|
600
|
+
if (request.cwd === cwd) return targetStatusFixture({ applicability: "unrelated", action: "start" });
|
|
601
|
+
const candidate = candidateViews.resolveForFinalize(request.lineageId);
|
|
602
|
+
return targetStatusFixture({
|
|
603
|
+
lineageId: request.lineageId,
|
|
604
|
+
baseTree: candidate.baseTree,
|
|
605
|
+
currentCandidateTree: candidate.candidateTree,
|
|
606
|
+
paths: candidate.paths,
|
|
607
|
+
});
|
|
608
|
+
},
|
|
609
|
+
start: async () => ({
|
|
610
|
+
lineageId: "candidate-root-finalize",
|
|
611
|
+
state: "reviewing",
|
|
612
|
+
riskLevel: "medium",
|
|
613
|
+
selectedLenses: ["review-reliability"],
|
|
614
|
+
changedFiles: 1,
|
|
615
|
+
changedLines: 1,
|
|
616
|
+
correctionBudget: 1,
|
|
617
|
+
action: "created",
|
|
618
|
+
lensesRequired: true,
|
|
619
|
+
}),
|
|
620
|
+
finalize: async (request) => {
|
|
621
|
+
finalizeRoot = request.cwd;
|
|
622
|
+
return { lineageId: "candidate-root-finalize", state: "approved", action: "approved", storeRevision: "r1" };
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
const { controller } = runtime(native, undefined, undefined, undefined, candidateViews);
|
|
626
|
+
await controller.execute("candidate-root-start", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
|
|
627
|
+
const candidateRoot = candidateViews.resolveForFinalize("candidate-root-finalize").root;
|
|
628
|
+
const result = await controller.execute("candidate-root-finalize", {
|
|
629
|
+
operation: "finalize",
|
|
630
|
+
lineageId: "candidate-root-finalize",
|
|
631
|
+
input: JSON.stringify({}),
|
|
632
|
+
}, undefined, undefined, context(cwd));
|
|
633
|
+
assert.deepEqual(statusRoots, [candidateRoot]);
|
|
634
|
+
assert.equal(finalizeRoot, candidateRoot);
|
|
635
|
+
assert.equal((result.details as { result: { state: string } }).result.state, "approved");
|
|
636
|
+
});
|
|
637
|
+
|
|
564
638
|
test("parent subagent_run mutates single and parallel review actors with one verified controller-owned candidate view", async (t) => {
|
|
565
639
|
const cwd = repository(t);
|
|
566
640
|
writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
|
|
@@ -750,91 +824,12 @@ test("fresh negotiated registries reconstruct the frozen candidate before FINALI
|
|
|
750
824
|
const result = await controller.execute("restarted-finalize", {
|
|
751
825
|
operation: "finalize",
|
|
752
826
|
lineageId: "restarted-lineage",
|
|
753
|
-
input: JSON.stringify({
|
|
827
|
+
input: JSON.stringify({}),
|
|
754
828
|
}, undefined, undefined, context(cwd));
|
|
755
829
|
assert.equal(finalizedContent, "export const value = 2;\n");
|
|
756
830
|
assert.equal((result.details as { result: { state: string } }).result.state, "approved");
|
|
757
831
|
});
|
|
758
832
|
|
|
759
|
-
test("fresh FINALIZE restores the provider manifest binding and rejects mode drift on the production path", async (t) => {
|
|
760
|
-
const cwd = repository(t);
|
|
761
|
-
writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
|
|
762
|
-
const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
|
|
763
|
-
const status = targetStatusFixture({ lineageId: "manifest-lineage", baseTree: frozen.baseTree, currentCandidateTree: frozen.candidateTree, paths: frozen.paths });
|
|
764
|
-
bindReviewerManifest(status, cwd);
|
|
765
|
-
const collect = status.nextTransition!.collect!;
|
|
766
|
-
const input = collect.inputs[0]!;
|
|
767
|
-
const changed = input.changedPathManifest![0]!;
|
|
768
|
-
status.nextTransition = { ...status.nextTransition!, collect: { inputs: [{
|
|
769
|
-
...input,
|
|
770
|
-
changedPathManifest: [{
|
|
771
|
-
...changed,
|
|
772
|
-
newMode: "100755",
|
|
773
|
-
modeOnly: true,
|
|
774
|
-
}],
|
|
775
|
-
}] } };
|
|
776
|
-
frozen.cleanup();
|
|
777
|
-
let finalizes = 0;
|
|
778
|
-
const { controller } = runtime(fakeNative({
|
|
779
|
-
targetStatus: async () => status,
|
|
780
|
-
finalize: async () => {
|
|
781
|
-
finalizes += 1;
|
|
782
|
-
return { lineageId: "manifest-lineage", state: "approved", action: "approved", storeRevision: "r1" };
|
|
783
|
-
},
|
|
784
|
-
}), undefined, undefined, undefined, new CandidateViewRegistry());
|
|
785
|
-
const result = await controller.execute("manifest-mode-drift", {
|
|
786
|
-
operation: "finalize",
|
|
787
|
-
lineageId: "manifest-lineage",
|
|
788
|
-
input: JSON.stringify({ review_result: { lens_results: [{ lens: "review-reliability", findings: [], evidence: ["reviewed frozen candidate"] }] } }),
|
|
789
|
-
}, undefined, undefined, context(cwd));
|
|
790
|
-
assert.equal((result.details as { outcome?: string }).outcome, "native-operation-failed");
|
|
791
|
-
assert.equal(((result.details as { diagnostics?: { code?: string } }).diagnostics?.code), "manifest-mode-drift");
|
|
792
|
-
assert.equal(finalizes, 0, "manifest drift must stop before native FINALIZE");
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
test("fresh FINALIZE rejects inconsistent provider manifest hashes before production dispatch", async (t) => {
|
|
796
|
-
const cwd = repository(t);
|
|
797
|
-
writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
|
|
798
|
-
const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
|
|
799
|
-
const status = bindReviewerManifest(targetStatusFixture({ lineageId: "manifest-hash-lineage", baseTree: frozen.baseTree, currentCandidateTree: frozen.candidateTree, paths: frozen.paths }), cwd);
|
|
800
|
-
const first = status.nextTransition!.collect!.inputs[0]!;
|
|
801
|
-
const secondSubjectHash = `sha256:${"a".repeat(64)}`;
|
|
802
|
-
status.nextTransition = { ...status.nextTransition!, collect: { inputs: [first, {
|
|
803
|
-
...first,
|
|
804
|
-
arguments: first.arguments.map((argument) => argument.name === "lens"
|
|
805
|
-
? { ...argument, value: "review-risk", token: "--lens=review-risk" }
|
|
806
|
-
: argument.name === "order"
|
|
807
|
-
? { ...argument, value: "1", token: "--order=1" }
|
|
808
|
-
: argument.name === "subject-hash"
|
|
809
|
-
? { ...argument, value: secondSubjectHash, token: `--subject-hash=${secondSubjectHash}` }
|
|
810
|
-
: argument),
|
|
811
|
-
artifactSubject: {
|
|
812
|
-
...first.artifactSubject!,
|
|
813
|
-
subjectHash: secondSubjectHash,
|
|
814
|
-
changedPathManifestSha256: `sha256:${"b".repeat(64)}`,
|
|
815
|
-
lens: "review-risk",
|
|
816
|
-
selectedOrder: 1,
|
|
817
|
-
},
|
|
818
|
-
}] } };
|
|
819
|
-
frozen.cleanup();
|
|
820
|
-
let finalizes = 0;
|
|
821
|
-
const { controller } = runtime(fakeNative({
|
|
822
|
-
targetStatus: async () => status,
|
|
823
|
-
finalize: async () => {
|
|
824
|
-
finalizes += 1;
|
|
825
|
-
return { lineageId: "manifest-hash-lineage", state: "approved", action: "approved", storeRevision: "r1" };
|
|
826
|
-
},
|
|
827
|
-
}), undefined, undefined, undefined, new CandidateViewRegistry());
|
|
828
|
-
const result = await controller.execute("manifest-hash-drift", {
|
|
829
|
-
operation: "finalize",
|
|
830
|
-
lineageId: "manifest-hash-lineage",
|
|
831
|
-
input: JSON.stringify({ review_result: { lens_results: [{ lens: "review-reliability", findings: [], evidence: ["reviewed frozen candidate"] }] } }),
|
|
832
|
-
}, undefined, undefined, context(cwd));
|
|
833
|
-
assert.equal((result.details as { outcome?: string }).outcome, "native-operation-failed");
|
|
834
|
-
assert.equal(((result.details as { diagnostics?: { code?: string } }).diagnostics?.code), "manifest-input-divergence");
|
|
835
|
-
assert.equal(finalizes, 0);
|
|
836
|
-
});
|
|
837
|
-
|
|
838
833
|
test("forecast-only FINALIZE reconstructs the frozen candidate after a fresh process (#176)", async (t) => {
|
|
839
834
|
const cwd = repository(t);
|
|
840
835
|
writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
|
|
@@ -1313,113 +1308,480 @@ test("production correction routing rejects a second capture that reuses failed
|
|
|
1313
1308
|
execFileSync("chmod", ["-R", "u+w", cwd]);
|
|
1314
1309
|
});
|
|
1315
1310
|
|
|
1316
|
-
|
|
1311
|
+
// Live-emitter correction shapes (probed 2026-08-16 against BOTH the pinned
|
|
1312
|
+
// 2.2.3 binary and dev 2.4.0-main.b1afef46, scripted correction lane):
|
|
1313
|
+
// - The evidence-collect status at `correction_required` carries the
|
|
1314
|
+
// `validation_request` CONTEXT alongside its single `review.capture-evidence`
|
|
1315
|
+
// input (reason correction_repository_verification_required). The request
|
|
1316
|
+
// context is not an offered validation step.
|
|
1317
|
+
// - The post-evidence targeted-validation status keeps state
|
|
1318
|
+
// `correction_required` (never `validating`), and its
|
|
1319
|
+
// `external.run_targeted_validation` input embeds the identical
|
|
1320
|
+
// validation_request.
|
|
1321
|
+
test("production correction routing accepts the live emitters' early validation-request context and correction_required post-evidence state", async (t) => {
|
|
1322
|
+
for (const [outcome, expectedKind] of [
|
|
1323
|
+
["passed", "run-targeted-validation"],
|
|
1324
|
+
["verification_failed", "recapture-required"],
|
|
1325
|
+
] as const) {
|
|
1326
|
+
await t.test(outcome, async (scenario) => {
|
|
1327
|
+
const cwd = repository(scenario);
|
|
1328
|
+
writeFileSync(join(cwd, "app.ts"), `export const live = ${JSON.stringify(outcome)};\n`);
|
|
1329
|
+
const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
|
|
1330
|
+
const beforeCapture = bindCorrectionCollection(targetStatusFixture({
|
|
1331
|
+
lineageId: `live-correction-${outcome.replaceAll("_", "-")}`,
|
|
1332
|
+
authorityState: "correction_required",
|
|
1333
|
+
baseTree: frozen.baseTree,
|
|
1334
|
+
currentCandidateTree: frozen.candidateTree,
|
|
1335
|
+
paths: frozen.paths,
|
|
1336
|
+
}));
|
|
1337
|
+
// The live emitters publish the request context BEFORE evidence.
|
|
1338
|
+
beforeCapture.validationRequest = bindTargetedValidation(targetStatusFixture({
|
|
1339
|
+
lineageId: beforeCapture.authority!.lineageId,
|
|
1340
|
+
authorityState: "correction_required",
|
|
1341
|
+
baseTree: frozen.baseTree,
|
|
1342
|
+
currentCandidateTree: frozen.candidateTree,
|
|
1343
|
+
paths: frozen.paths,
|
|
1344
|
+
})).validationRequest;
|
|
1345
|
+
const afterCapture = targetStatusFixture({
|
|
1346
|
+
lineageId: beforeCapture.authority!.lineageId,
|
|
1347
|
+
// Live post-evidence state on both emitters.
|
|
1348
|
+
authorityState: "correction_required",
|
|
1349
|
+
baseTree: frozen.baseTree,
|
|
1350
|
+
currentCandidateTree: frozen.candidateTree,
|
|
1351
|
+
paths: frozen.paths,
|
|
1352
|
+
});
|
|
1353
|
+
if (outcome === "passed") bindTargetedValidation(afterCapture);
|
|
1354
|
+
else {
|
|
1355
|
+
bindCorrectionCollection(afterCapture);
|
|
1356
|
+
afterCapture.validationRequest = beforeCapture.validationRequest;
|
|
1357
|
+
}
|
|
1358
|
+
frozen.cleanup();
|
|
1359
|
+
const calls: string[] = [];
|
|
1360
|
+
let statuses = 0;
|
|
1361
|
+
let finalizes = 0;
|
|
1362
|
+
const { controller } = runtime(fakeNative({
|
|
1363
|
+
targetStatus: async () => {
|
|
1364
|
+
calls.push("status");
|
|
1365
|
+
statuses += 1;
|
|
1366
|
+
return statuses === 1 ? beforeCapture : afterCapture;
|
|
1367
|
+
},
|
|
1368
|
+
captureEvidence: async (request) => {
|
|
1369
|
+
calls.push("capture-evidence");
|
|
1370
|
+
assert.equal(request.outcome, outcome);
|
|
1371
|
+
return capturedCorrectionEvidence(beforeCapture, outcome, outcome === "passed" ? "7" : "8");
|
|
1372
|
+
},
|
|
1373
|
+
finalize: async () => {
|
|
1374
|
+
calls.push("finalize");
|
|
1375
|
+
finalizes += 1;
|
|
1376
|
+
return { lineageId: beforeCapture.authority!.lineageId, state: "approved", action: "approved", storeRevision: "r-final" };
|
|
1377
|
+
},
|
|
1378
|
+
}), undefined, undefined, undefined, new CandidateViewRegistry());
|
|
1379
|
+
const validation = outcome === "passed" ? {
|
|
1380
|
+
request_hash: "9".repeat(64), correction_ids: [],
|
|
1381
|
+
original_criteria: { passed: true, evidence: ["acceptance passes"] },
|
|
1382
|
+
correction_regression: { passed: true, evidence: ["regression passes"] },
|
|
1383
|
+
fix_caused_findings: [], follow_ups: [],
|
|
1384
|
+
} : undefined;
|
|
1385
|
+
const result = await controller.execute(`live-correction-${outcome}`, {
|
|
1386
|
+
operation: "finalize",
|
|
1387
|
+
lineageId: beforeCapture.authority!.lineageId,
|
|
1388
|
+
input: JSON.stringify({ final_evidence: `evidence: ${outcome}`, final_verification_outcome: outcome, ...(validation === undefined ? {} : { validation }) }),
|
|
1389
|
+
}, undefined, undefined, context(cwd));
|
|
1390
|
+
const details = result.details as Record<string, unknown>;
|
|
1391
|
+
assert.deepEqual(calls.slice(0, 3), ["status", "capture-evidence", "status"], "the early validation-request context must not block the evidence capture the state demands");
|
|
1392
|
+
assert.equal((details.correction_step as { kind?: string } | undefined)?.kind, expectedKind);
|
|
1393
|
+
assert.equal(finalizes, outcome === "passed" ? 1 : 0);
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
// Ordinary final verification (field defect, dev binary 2.4.0-main.b1afef46,
|
|
1399
|
+
// reproduced identically on the pinned 2.2.3 binary): after FINALIZE with
|
|
1400
|
+
// captured results the native state is `validating` and the provider collects
|
|
1401
|
+
// exactly one `review.capture-evidence` record, then offers one execute
|
|
1402
|
+
// `review.finalize --captured-evidence=true` transition. The controller must
|
|
1403
|
+
// follow that transition faithfully — it must never demand its correction-lane
|
|
1404
|
+
// targeted-validation phase, and never substitute the validate gate.
|
|
1405
|
+
function bindFinalVerificationTransition(status: ReviewStatusV3, outcome: "passed" | "verification_failed" | "procedural_tooling_failed"): ReviewStatusV3 {
|
|
1406
|
+
const reasonCode = outcome === "passed"
|
|
1407
|
+
? "captured_verification_evidence_passed"
|
|
1408
|
+
: outcome === "verification_failed" ? "captured_verification_failed" : "captured_verification_tooling_failed";
|
|
1409
|
+
status.nextTransition = {
|
|
1410
|
+
kind: "execute",
|
|
1411
|
+
reasonCode,
|
|
1412
|
+
execute: {
|
|
1413
|
+
operation: "review.finalize",
|
|
1414
|
+
// The second argument deliberately omits `token` to pin the
|
|
1415
|
+
// provider's published hyphenation fallback (captured_evidence ->
|
|
1416
|
+
// --captured-evidence=true); the host never invents another form.
|
|
1417
|
+
arguments: [
|
|
1418
|
+
{ name: "lineage", value: status.authority!.lineageId, token: `--lineage=${status.authority!.lineageId}` },
|
|
1419
|
+
{ name: "captured_evidence", value: "true" },
|
|
1420
|
+
],
|
|
1421
|
+
preconditions: [{ name: "state", value: "validating" }, { name: "verification_outcome", value: outcome }],
|
|
1422
|
+
binding: { targetIdentity: status.targetIdentity, lineageId: status.authority!.lineageId },
|
|
1423
|
+
},
|
|
1424
|
+
};
|
|
1425
|
+
delete status.validationRequest;
|
|
1426
|
+
return status;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
test("ordinary final verification at validating captures evidence then executes the provider finalize transition, never targeted validation", async (t) => {
|
|
1430
|
+
for (const [outcome, terminalState] of [
|
|
1431
|
+
["passed", "approved"],
|
|
1432
|
+
["verification_failed", "escalated"],
|
|
1433
|
+
["procedural_tooling_failed", "escalated"],
|
|
1434
|
+
] as const) {
|
|
1435
|
+
await t.test(outcome, async (scenario) => {
|
|
1436
|
+
const cwd = repository(scenario);
|
|
1437
|
+
writeFileSync(join(cwd, "app.ts"), `export const outcome = ${JSON.stringify(outcome)};\n`);
|
|
1438
|
+
const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
|
|
1439
|
+
const beforeCapture = bindCorrectionCollection(targetStatusFixture({
|
|
1440
|
+
lineageId: "final-verification",
|
|
1441
|
+
authorityState: "validating",
|
|
1442
|
+
baseTree: frozen.baseTree,
|
|
1443
|
+
currentCandidateTree: frozen.candidateTree,
|
|
1444
|
+
paths: frozen.paths,
|
|
1445
|
+
}));
|
|
1446
|
+
const afterCapture = bindFinalVerificationTransition(targetStatusFixture({
|
|
1447
|
+
lineageId: "final-verification",
|
|
1448
|
+
authorityState: "validating",
|
|
1449
|
+
baseTree: frozen.baseTree,
|
|
1450
|
+
currentCandidateTree: frozen.candidateTree,
|
|
1451
|
+
paths: frozen.paths,
|
|
1452
|
+
}), outcome);
|
|
1453
|
+
frozen.cleanup();
|
|
1454
|
+
const calls: string[] = [];
|
|
1455
|
+
const transitions: Array<readonly string[]> = [];
|
|
1456
|
+
let statuses = 0;
|
|
1457
|
+
const { controller } = runtime(fakeNative({
|
|
1458
|
+
targetStatus: async () => {
|
|
1459
|
+
calls.push("status");
|
|
1460
|
+
statuses += 1;
|
|
1461
|
+
return statuses === 1 ? beforeCapture : afterCapture;
|
|
1462
|
+
},
|
|
1463
|
+
captureEvidence: async (request) => {
|
|
1464
|
+
calls.push("capture-evidence");
|
|
1465
|
+
assert.equal(request.outcome, outcome);
|
|
1466
|
+
return capturedCorrectionEvidence(beforeCapture, outcome, "6");
|
|
1467
|
+
},
|
|
1468
|
+
finalize: async () => {
|
|
1469
|
+
calls.push("finalize");
|
|
1470
|
+
return { lineageId: "final-verification", state: terminalState, action: "terminal", storeRevision: "r2" };
|
|
1471
|
+
},
|
|
1472
|
+
finalizeTransition: async (request) => {
|
|
1473
|
+
calls.push("finalize-transition");
|
|
1474
|
+
transitions.push(request.argumentTokens);
|
|
1475
|
+
return { lineageId: "final-verification", state: terminalState, action: "terminal", storeRevision: "r2", receiptPath: "/opaque/receipt" };
|
|
1476
|
+
},
|
|
1477
|
+
}), undefined, undefined, undefined, new CandidateViewRegistry());
|
|
1478
|
+
const result = await controller.execute(`final-verification-${outcome}`, {
|
|
1479
|
+
operation: "finalize",
|
|
1480
|
+
lineageId: "final-verification",
|
|
1481
|
+
input: JSON.stringify({ final_evidence: `verification run: ${outcome}`, final_verification_outcome: outcome }),
|
|
1482
|
+
}, undefined, undefined, context(cwd));
|
|
1483
|
+
const details = result.details as Record<string, unknown>;
|
|
1484
|
+
assert.deepEqual(calls, ["status", "capture-evidence", "status", "finalize-transition"], "the controller must follow the provider transition after evidence capture");
|
|
1485
|
+
assert.deepEqual(transitions, [["--lineage=final-verification", "--captured-evidence=true"]]);
|
|
1486
|
+
assert.equal((details.result as { state?: string } | undefined)?.state, terminalState);
|
|
1487
|
+
assert.equal("correction_step" in details, false, "ordinary final verification is not a correction transaction");
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
test("ordinary final verification rejects a Pi-authored targeted validation document before any capture", async (t) => {
|
|
1317
1493
|
const cwd = repository(t);
|
|
1494
|
+
writeFileSync(join(cwd, "app.ts"), "export const value = 1;\n");
|
|
1495
|
+
const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
|
|
1496
|
+
const status = bindCorrectionCollection(targetStatusFixture({
|
|
1497
|
+
lineageId: "final-verification",
|
|
1498
|
+
authorityState: "validating",
|
|
1499
|
+
baseTree: frozen.baseTree,
|
|
1500
|
+
currentCandidateTree: frozen.candidateTree,
|
|
1501
|
+
paths: frozen.paths,
|
|
1502
|
+
}));
|
|
1503
|
+
frozen.cleanup();
|
|
1504
|
+
let captures = 0;
|
|
1318
1505
|
let finalizes = 0;
|
|
1319
1506
|
const { controller } = runtime(fakeNative({
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1507
|
+
targetStatus: async () => status,
|
|
1508
|
+
captureEvidence: async () => { captures += 1; return capturedCorrectionEvidence(status, "passed", "4"); },
|
|
1509
|
+
finalize: async () => { finalizes += 1; return { lineageId: "final-verification", state: "approved", action: "approved", storeRevision: "r1" }; },
|
|
1510
|
+
}), undefined, undefined, undefined, new CandidateViewRegistry());
|
|
1511
|
+
const result = await controller.execute("final-verification-validation-doc", {
|
|
1512
|
+
operation: "finalize",
|
|
1513
|
+
lineageId: "final-verification",
|
|
1514
|
+
input: JSON.stringify({
|
|
1515
|
+
final_evidence: "verification run: passed",
|
|
1516
|
+
final_verification_outcome: "passed",
|
|
1517
|
+
validation: { request_hash: "9".repeat(64), correction_ids: [], original_criteria: { passed: true, evidence: ["passes"] }, correction_regression: { passed: true, evidence: ["passes"] }, fix_caused_findings: [], follow_ups: [] },
|
|
1518
|
+
}),
|
|
1519
|
+
}, undefined, undefined, context(cwd));
|
|
1520
|
+
assert.equal((result.details as { outcome?: string }).outcome, "native-operation-failed");
|
|
1521
|
+
assert.match(JSON.stringify(result.details), /final-verification-provider-owned/);
|
|
1522
|
+
assert.equal(captures, 0, "an inadmissible payload must fail closed before the single evidence capture is consumed");
|
|
1332
1523
|
assert.equal(finalizes, 0);
|
|
1333
|
-
assert.equal((required.details as { outcome?: string }).outcome, "refuter-required");
|
|
1334
|
-
assert.equal(typeof request?.request_hash, "string");
|
|
1335
|
-
assert.deepEqual(request?.findings.map((row) => (row as { id: string }).id), ["RISK-001"]);
|
|
1336
|
-
const retry = await controller.execute("risk-001-refuter-retry", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({
|
|
1337
|
-
review_result: { ...review_result, refuter_request_hash: request!.request_hash },
|
|
1338
|
-
refuter_batch: { schema: "gentle-ai.refuter-result-batch/v1", request_hash: request!.request_hash, results: [{ finding_id: "RISK-001", outcome: "corroborated", proof_refs: [proof] }] },
|
|
1339
|
-
}) }, undefined, undefined, context(cwd));
|
|
1340
|
-
assert.equal(finalizes, 1);
|
|
1341
|
-
assert.equal((retry.details as { result?: { state?: string } }).result?.state, "validating");
|
|
1342
1524
|
});
|
|
1343
1525
|
|
|
1344
|
-
test("
|
|
1526
|
+
test("ordinary final verification fails closed without substituting a step when the provider offers no finalize transition", async (t) => {
|
|
1345
1527
|
const cwd = repository(t);
|
|
1346
|
-
|
|
1528
|
+
writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
|
|
1529
|
+
const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
|
|
1530
|
+
const beforeCapture = bindCorrectionCollection(targetStatusFixture({
|
|
1531
|
+
lineageId: "final-verification",
|
|
1532
|
+
authorityState: "validating",
|
|
1533
|
+
baseTree: frozen.baseTree,
|
|
1534
|
+
currentCandidateTree: frozen.candidateTree,
|
|
1535
|
+
paths: frozen.paths,
|
|
1536
|
+
}));
|
|
1537
|
+
const afterCapture = targetStatusFixture({
|
|
1538
|
+
lineageId: "final-verification",
|
|
1539
|
+
authorityState: "validating",
|
|
1540
|
+
action: "stop",
|
|
1541
|
+
replayability: "manual_action_required",
|
|
1542
|
+
baseTree: frozen.baseTree,
|
|
1543
|
+
currentCandidateTree: frozen.candidateTree,
|
|
1544
|
+
paths: frozen.paths,
|
|
1545
|
+
});
|
|
1546
|
+
frozen.cleanup();
|
|
1547
|
+
let statuses = 0;
|
|
1548
|
+
let finalizes = 0;
|
|
1549
|
+
let transitions = 0;
|
|
1550
|
+
let validates = 0;
|
|
1347
1551
|
const { controller } = runtime(fakeNative({
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1552
|
+
targetStatus: async () => {
|
|
1553
|
+
statuses += 1;
|
|
1554
|
+
return statuses === 1 ? beforeCapture : afterCapture;
|
|
1555
|
+
},
|
|
1556
|
+
captureEvidence: async () => capturedCorrectionEvidence(beforeCapture, "passed", "5"),
|
|
1557
|
+
finalize: async () => { finalizes += 1; return { lineageId: "final-verification", state: "approved", action: "approved", storeRevision: "r1" }; },
|
|
1558
|
+
finalizeTransition: async () => { transitions += 1; return { lineageId: "final-verification", state: "approved", action: "approved", storeRevision: "r1" }; },
|
|
1559
|
+
validate: async () => { validates += 1; return { allowed: true, result: "allow", action: "continue", reason: "ok", gateContext: nativeGateContext() }; },
|
|
1560
|
+
}), undefined, undefined, undefined, new CandidateViewRegistry());
|
|
1561
|
+
const result = await controller.execute("final-verification-no-transition", {
|
|
1562
|
+
operation: "finalize",
|
|
1563
|
+
lineageId: "final-verification",
|
|
1564
|
+
input: JSON.stringify({ final_evidence: "verification run: passed", final_verification_outcome: "passed" }),
|
|
1565
|
+
}, undefined, undefined, context(cwd));
|
|
1566
|
+
const details = result.details as Record<string, unknown>;
|
|
1567
|
+
assert.equal(details.outcome, "final-verification-transition-unavailable");
|
|
1568
|
+
assert.equal(details.mutation_performed, true, "the committed evidence capture must never be reported as zero mutations");
|
|
1569
|
+
assert.equal(details.mutation_outcome, "committed");
|
|
1570
|
+
assert.equal(finalizes, 0, "no substitute finalize form may be invented");
|
|
1571
|
+
assert.equal(transitions, 0);
|
|
1572
|
+
assert.equal(validates, 0, "the validate gate must never be substituted for a missing transition");
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
// gentle-pi#311 P4-roles fixture: one provider-rendered SELF-CONTAINED role
|
|
1576
|
+
// capture vector (binding tokens + --agent=pi --execute=true, no submission).
|
|
1577
|
+
function bindProviderRoleVector(status: ReviewStatusV3, role: "refuter" | "targeted-validator"): ReviewStatusV3 {
|
|
1578
|
+
const authority = status.authority!;
|
|
1579
|
+
const contextHandle = `rctx1_${"c".repeat(64)}`;
|
|
1580
|
+
const requestHash = `sha256:${"9".repeat(64)}`;
|
|
1581
|
+
const argumentsList = [
|
|
1582
|
+
{ name: "lineage", value: authority.lineageId, token: `--lineage=${authority.lineageId}` },
|
|
1583
|
+
{ name: "expected-revision", value: authority.revision, token: `--expected-revision=${authority.revision}` },
|
|
1584
|
+
{ name: "target", value: status.targetIdentity, token: `--target=${status.targetIdentity}` },
|
|
1585
|
+
{ name: "repository-context", value: contextHandle, token: `--repository-context=${contextHandle}` },
|
|
1586
|
+
...(role === "targeted-validator" ? [{ name: "request-hash", value: requestHash, token: `--request-hash=${requestHash}` }] : []),
|
|
1587
|
+
{ name: "agent", value: "pi", token: "--agent=pi" },
|
|
1588
|
+
{ name: "execute", value: "true", token: "--execute=true" },
|
|
1589
|
+
];
|
|
1590
|
+
status.nextTransition = {
|
|
1591
|
+
kind: "collect",
|
|
1592
|
+
reasonCode: role === "refuter" ? "provider_refuter_required" : "targeted_validation_required",
|
|
1593
|
+
collect: { inputs: [role === "refuter" ? {
|
|
1594
|
+
name: "provider_refuter",
|
|
1595
|
+
schema: "https://gentle-ai.dev/schema/review/refuter/v1",
|
|
1596
|
+
captureOperation: "review.capture-refuter",
|
|
1597
|
+
arguments: argumentsList,
|
|
1598
|
+
} : {
|
|
1599
|
+
name: "provider_targeted_validator",
|
|
1600
|
+
schema: "https://gentle-ai.dev/schema/review/validator/v1",
|
|
1601
|
+
captureOperation: "review.capture-validation",
|
|
1602
|
+
arguments: argumentsList,
|
|
1603
|
+
validationRequest: {
|
|
1604
|
+
schema: "gentle-ai.review-targeted-validation-request/v1" as const,
|
|
1605
|
+
requestHash,
|
|
1606
|
+
lineageId: authority.lineageId,
|
|
1607
|
+
expectedRevision: authority.revision,
|
|
1608
|
+
targetIdentity: status.targetIdentity,
|
|
1609
|
+
fixFindingIds: ["RISK-001"],
|
|
1610
|
+
projection: "workspace" as const,
|
|
1611
|
+
correctionCandidateTree: status.projection.currentCandidateTree,
|
|
1612
|
+
correctionTargetIdentity: status.targetIdentity,
|
|
1613
|
+
correctionPaths: status.projection.paths,
|
|
1614
|
+
correctionPathsDigest: status.projection.pathsDigest,
|
|
1615
|
+
},
|
|
1616
|
+
}] },
|
|
1617
|
+
};
|
|
1618
|
+
return status;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
test("provider refuter vector executes exactly as rendered, then STATUS is re-queried", async (t) => {
|
|
1622
|
+
const cwd = repository(t);
|
|
1623
|
+
const executed: Array<{ captureOperation: string; argumentTokens: readonly string[]; cwd: string }> = [];
|
|
1624
|
+
let finalizes = 0;
|
|
1625
|
+
let statusCalls = 0;
|
|
1626
|
+
const roleStatus = bindProviderRoleVector(targetStatusFixture({ lineageId: "native-lineage" }), "refuter");
|
|
1627
|
+
const settledStatus = targetStatusFixture({ lineageId: "native-lineage" });
|
|
1628
|
+
const { controller } = runtime(fakeNative({
|
|
1629
|
+
targetStatus: async () => {
|
|
1630
|
+
statusCalls += 1;
|
|
1631
|
+
return statusCalls === 1 ? roleStatus : settledStatus;
|
|
1632
|
+
},
|
|
1633
|
+
finalize: async () => {
|
|
1634
|
+
finalizes += 1;
|
|
1351
1635
|
return { lineageId: "native-lineage", state: "approved", action: "approved", storeRevision: "r1" };
|
|
1352
1636
|
},
|
|
1637
|
+
captureProviderRole: async (request) => {
|
|
1638
|
+
executed.push({ captureOperation: request.captureOperation, argumentTokens: request.argumentTokens, cwd: request.cwd });
|
|
1639
|
+
return { schema: "gentle-ai.review-provider-role-capture/v1", lineageId: "native-lineage", targetIdentity: roleStatus.targetIdentity, role: "refuter", captured: true };
|
|
1640
|
+
},
|
|
1353
1641
|
}));
|
|
1354
|
-
await controller.execute("
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
final_verification_passed: false,
|
|
1366
|
-
}) }, undefined, undefined, context(cwd));
|
|
1367
|
-
assert.deepEqual(requests, [{
|
|
1642
|
+
const result = await controller.execute("role-vector", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({}) }, undefined, undefined, context(cwd));
|
|
1643
|
+
assert.deepEqual(executed, [{
|
|
1644
|
+
captureOperation: "review.capture-refuter",
|
|
1645
|
+
argumentTokens: [
|
|
1646
|
+
"--lineage=native-lineage",
|
|
1647
|
+
`--expected-revision=sha256:${"a".repeat(64)}`,
|
|
1648
|
+
`--target=sha256:${"a".repeat(64)}`,
|
|
1649
|
+
`--repository-context=rctx1_${"c".repeat(64)}`,
|
|
1650
|
+
"--agent=pi",
|
|
1651
|
+
"--execute=true",
|
|
1652
|
+
],
|
|
1368
1653
|
cwd,
|
|
1369
|
-
lineageId: "native-lineage",
|
|
1370
|
-
lensResults: [{ lens: "review-risk", document: { lens: "risk", findings: [{ ...finding, lens: "risk" }], evidence: ["complete candidate reviewed"] } }],
|
|
1371
|
-
refuterDocument: { results: refuterBatch.results },
|
|
1372
|
-
validationDocument: { original_criteria: { passed: false, evidence: ["acceptance still fails"] }, correction_regression: { passed: true, evidence: ["regression suite passes"] }, follow_ups: [{ observation: "Track the remaining failure", proof_refs: ["differential-test:candidate still fails"] }] },
|
|
1373
|
-
evidenceDocument: " focused verification failed\n\n",
|
|
1374
|
-
failed: true,
|
|
1375
1654
|
}]);
|
|
1655
|
+
assert.equal(finalizes, 0, "role vectors never route through document finalize");
|
|
1656
|
+
assert.equal(statusCalls, 2, "STATUS must be re-queried after the vector executes");
|
|
1657
|
+
const details = result.details as { provider_roles?: { transport?: string; executed_slots?: Array<Record<string, unknown>> } };
|
|
1658
|
+
assert.equal(details.provider_roles?.transport, "go_owned_pi_process");
|
|
1659
|
+
assert.deepEqual(details.provider_roles?.executed_slots?.map((slot) => slot.role), ["refuter"]);
|
|
1376
1660
|
});
|
|
1377
1661
|
|
|
1378
|
-
test("
|
|
1662
|
+
test("provider targeted-validator vector keeps the frozen request hash token verbatim", async (t) => {
|
|
1379
1663
|
const cwd = repository(t);
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
])
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
assert.equal(finalizes, 0);
|
|
1399
|
-
});
|
|
1664
|
+
const executed: string[][] = [];
|
|
1665
|
+
let statusCalls = 0;
|
|
1666
|
+
const roleStatus = bindProviderRoleVector(targetStatusFixture({ lineageId: "native-lineage", authorityState: "validating" }), "targeted-validator");
|
|
1667
|
+
const { controller } = runtime(fakeNative({
|
|
1668
|
+
targetStatus: async () => {
|
|
1669
|
+
statusCalls += 1;
|
|
1670
|
+
return statusCalls === 1 ? roleStatus : targetStatusFixture({ lineageId: "native-lineage" });
|
|
1671
|
+
},
|
|
1672
|
+
captureProviderRole: async (request) => {
|
|
1673
|
+
executed.push([...request.argumentTokens]);
|
|
1674
|
+
return { schema: "gentle-ai.review-provider-role-capture/v1", lineageId: "native-lineage", targetIdentity: roleStatus.targetIdentity, role: "targeted-validator", captured: true };
|
|
1675
|
+
},
|
|
1676
|
+
}));
|
|
1677
|
+
await controller.execute("validator-vector", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({}) }, undefined, undefined, context(cwd));
|
|
1678
|
+
assert.equal(executed.length, 1);
|
|
1679
|
+
assert.ok(executed[0]!.includes(`--request-hash=sha256:${"9".repeat(64)}`), "the frozen request hash token must pass through verbatim");
|
|
1680
|
+
assert.ok(executed[0]!.includes("--execute=true"));
|
|
1681
|
+
});
|
|
1400
1682
|
|
|
1401
|
-
test("
|
|
1683
|
+
test("a failed provider role vector surfaces the typed error and never auto-relaunches", async (t) => {
|
|
1684
|
+
const cwd = repository(t);
|
|
1685
|
+
let captures = 0;
|
|
1686
|
+
let statusCalls = 0;
|
|
1687
|
+
const roleStatus = bindProviderRoleVector(targetStatusFixture({ lineageId: "native-lineage" }), "refuter");
|
|
1688
|
+
const { controller } = runtime(fakeNative({
|
|
1689
|
+
targetStatus: async () => {
|
|
1690
|
+
statusCalls += 1;
|
|
1691
|
+
return roleStatus;
|
|
1692
|
+
},
|
|
1693
|
+
captureProviderRole: async () => {
|
|
1694
|
+
captures += 1;
|
|
1695
|
+
throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true, "provider refused the role capture");
|
|
1696
|
+
},
|
|
1697
|
+
}));
|
|
1698
|
+
const result = await controller.execute("role-vector-failure", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({}) }, undefined, undefined, context(cwd));
|
|
1699
|
+
const details = result.details as Record<string, unknown>;
|
|
1700
|
+
assert.equal(details.outcome, "provider-role-vector-failed");
|
|
1701
|
+
assert.equal(details.status, "blocked");
|
|
1702
|
+
assert.match(String(details.retry_discipline), /Re-query negotiated STATUS/);
|
|
1703
|
+
assert.equal(captures, 1, "a failed vector must not be relaunched from transcript inference");
|
|
1704
|
+
assert.equal(statusCalls, 1, "the failure envelope is surfaced without a blind relaunch loop");
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
test("negotiated FINALIZE executes the provider-rendered captured-results transition verbatim", async (t) => {
|
|
1708
|
+
const cwd = repository(t);
|
|
1709
|
+
const transitions: Array<{ cwd: string; argumentTokens: readonly string[] }> = [];
|
|
1710
|
+
let finalizes = 0;
|
|
1711
|
+
const status = targetStatusFixture({ lineageId: "native-lineage" });
|
|
1712
|
+
status.nextTransition = {
|
|
1713
|
+
kind: "execute",
|
|
1714
|
+
reasonCode: "captured_results_ready",
|
|
1715
|
+
execute: {
|
|
1716
|
+
operation: "review.finalize",
|
|
1717
|
+
// The second argument deliberately omits `token` to pin the
|
|
1718
|
+
// provider's published hyphenation fallback (captured_results ->
|
|
1719
|
+
// --captured-results=true); the host never invents another form.
|
|
1720
|
+
arguments: [
|
|
1721
|
+
{ name: "lineage", value: "native-lineage", token: "--lineage=native-lineage" },
|
|
1722
|
+
{ name: "captured_results", value: "true" },
|
|
1723
|
+
],
|
|
1724
|
+
preconditions: [{ name: "state", value: "reviewing" }],
|
|
1725
|
+
binding: { targetIdentity: status.targetIdentity, lineageId: "native-lineage" },
|
|
1726
|
+
},
|
|
1727
|
+
};
|
|
1728
|
+
const { controller } = runtime(fakeNative({
|
|
1729
|
+
targetStatus: async () => status,
|
|
1730
|
+
finalize: async () => {
|
|
1731
|
+
finalizes += 1;
|
|
1732
|
+
return { lineageId: "native-lineage", state: "approved", action: "approved", storeRevision: "r1" };
|
|
1733
|
+
},
|
|
1734
|
+
finalizeTransition: async (request) => {
|
|
1735
|
+
transitions.push({ cwd: request.cwd, argumentTokens: request.argumentTokens });
|
|
1736
|
+
return { lineageId: "native-lineage", state: "approved", action: "approved", storeRevision: "r1", receiptPath: "/opaque/receipt" };
|
|
1737
|
+
},
|
|
1738
|
+
}));
|
|
1739
|
+
const result = await controller.execute("captured-results-finalize", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({}) }, undefined, undefined, context(cwd));
|
|
1740
|
+
assert.deepEqual(transitions, [{ cwd, argumentTokens: ["--lineage=native-lineage", "--captured-results=true"] }]);
|
|
1741
|
+
assert.equal(finalizes, 0, "the provider-driven lane never assembles a document finalize");
|
|
1742
|
+
assert.deepEqual(result.details, { operation: "finalize", result: { lineage_id: "native-lineage", state: "approved", action: "approved", store_revision: "r1", receipt_path: "/opaque/receipt" } });
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
test("negotiated FINALIZE refuses a finalize transition bound to a different lineage", async (t) => {
|
|
1746
|
+
const cwd = repository(t);
|
|
1747
|
+
let transitions = 0;
|
|
1748
|
+
const status = targetStatusFixture({ lineageId: "native-lineage" });
|
|
1749
|
+
status.nextTransition = {
|
|
1750
|
+
kind: "execute",
|
|
1751
|
+
reasonCode: "captured_results_ready",
|
|
1752
|
+
execute: {
|
|
1753
|
+
operation: "review.finalize",
|
|
1754
|
+
arguments: [{ name: "lineage", value: "other-lineage", token: "--lineage=other-lineage" }],
|
|
1755
|
+
preconditions: [],
|
|
1756
|
+
binding: { targetIdentity: status.targetIdentity, lineageId: "other-lineage" },
|
|
1757
|
+
},
|
|
1758
|
+
};
|
|
1759
|
+
const { controller } = runtime(fakeNative({
|
|
1760
|
+
targetStatus: async () => status,
|
|
1761
|
+
finalizeTransition: async () => {
|
|
1762
|
+
transitions += 1;
|
|
1763
|
+
return { lineageId: "other-lineage", state: "approved", action: "approved", storeRevision: "r1" };
|
|
1764
|
+
},
|
|
1765
|
+
}));
|
|
1766
|
+
const result = await controller.execute("foreign-transition", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({}) }, undefined, undefined, context(cwd));
|
|
1767
|
+
assert.equal((result.details as { outcome?: string }).outcome, "native-operation-failed");
|
|
1768
|
+
assert.match(JSON.stringify(result.details), /finalize-transition-binding-drift/);
|
|
1769
|
+
assert.equal(transitions, 0);
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
test("native FINALIZE rejects the retired Pi-authored payload fields before native calls", async (t) => {
|
|
1402
1773
|
const cwd = repository(t);
|
|
1403
1774
|
let finalizes = 0;
|
|
1404
1775
|
const { controller } = runtime(fakeNative({ finalize: async () => {
|
|
1405
1776
|
finalizes += 1;
|
|
1406
1777
|
return { lineageId: "native-lineage", state: "validating", action: "continue", storeRevision: "r1" };
|
|
1407
1778
|
} }));
|
|
1408
|
-
const
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
for (const refuter_batch of [
|
|
1413
|
-
{ schema: "gentle-ai.refuter-result-batch/v1", request_hash: "b".repeat(64), results: [row] },
|
|
1414
|
-
{ schema: "gentle-ai.refuter-result-batch/v1", request_hash: expectedHash, results: [] },
|
|
1415
|
-
{ schema: "gentle-ai.refuter-result-batch/v1", request_hash: expectedHash, results: [row, row] },
|
|
1416
|
-
{ schema: "gentle-ai.refuter-result-batch/v1", request_hash: expectedHash, results: [{ ...row, finding_id: "RISK-002" }] },
|
|
1417
|
-
{ schema: "gentle-ai.refuter-result-batch/v1", request_hash: expectedHash, results: [{ ...row, proof_refs: [] }] },
|
|
1779
|
+
for (const retired of [
|
|
1780
|
+
{ review_result: { lens_results: [{ lens: "review-risk", findings: [], evidence: ["reviewed"] }] } },
|
|
1781
|
+
{ refuter_batch: { schema: "gentle-ai.refuter-result-batch/v1", request_hash: "a".repeat(64), results: [] } },
|
|
1782
|
+
{ validation_proof: { original_criteria: { passed: true, evidence: ["ok"] }, correction_regression: { passed: true, evidence: ["ok"] } } },
|
|
1418
1783
|
]) {
|
|
1419
|
-
await assert.rejects(controller.execute("
|
|
1420
|
-
review_result: { lens_results: [{ lens: "review-risk", findings: [finding], evidence: ["reviewed"] }], refuter_request_hash: expectedHash },
|
|
1421
|
-
refuter_batch,
|
|
1422
|
-
}) }, undefined, undefined, context(cwd)));
|
|
1784
|
+
await assert.rejects(controller.execute("retired-field", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify(retired) }, undefined, undefined, context(cwd)));
|
|
1423
1785
|
}
|
|
1424
1786
|
assert.equal(finalizes, 0);
|
|
1425
1787
|
});
|
|
@@ -1452,17 +1814,19 @@ test("controller rejects zero-length final evidence before native calls", async
|
|
|
1452
1814
|
assert.equal(finalizes, 0);
|
|
1453
1815
|
});
|
|
1454
1816
|
|
|
1455
|
-
test("
|
|
1817
|
+
test("native FINALIZE never assembles reviewer documents and forwards only the correction forecast", async (t) => {
|
|
1456
1818
|
const cwd = repository(t);
|
|
1457
1819
|
const requests: Parameters<NativeReviewCli["finalize"]>[0][] = [];
|
|
1458
1820
|
const { controller } = runtime(fakeNative({ finalize: async (request) => {
|
|
1459
1821
|
requests.push(request);
|
|
1460
1822
|
return { lineageId: "native-lineage", state: "correction_required", action: "continue correction", storeRevision: `r${requests.length}` };
|
|
1461
1823
|
} }));
|
|
1462
|
-
await controller.execute("initial", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({
|
|
1824
|
+
await controller.execute("initial", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({}) }, undefined, undefined, context(cwd));
|
|
1463
1825
|
await controller.execute("retry", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({ correction_line_forecast: 1 }) }, undefined, undefined, context(cwd));
|
|
1464
|
-
assert.equal(requests[0]?.lensResults
|
|
1826
|
+
assert.equal(requests[0]?.lensResults, undefined, "lens results are admitted natively, never Pi-assembled");
|
|
1827
|
+
assert.equal(requests[0]?.refuterDocument, undefined);
|
|
1465
1828
|
assert.equal(requests[1]?.lensResults, undefined);
|
|
1829
|
+
assert.equal(requests[1]?.correctionLines, 1);
|
|
1466
1830
|
});
|
|
1467
1831
|
|
|
1468
1832
|
test("native error has no compact fallback and ambiguous mutation demands target status", async (t) => {
|
|
@@ -1510,6 +1874,35 @@ test("native START preserves a candidate-view diagnostic before native invocatio
|
|
|
1510
1874
|
assert.equal(starts, 0);
|
|
1511
1875
|
});
|
|
1512
1876
|
|
|
1877
|
+
test("native START returns a structured pre-native candidate-view output-limit diagnostic without calling native START", async (t) => {
|
|
1878
|
+
const cwd = repository(t);
|
|
1879
|
+
let starts = 0;
|
|
1880
|
+
const candidateViews = new CandidateViewRegistry(() => {
|
|
1881
|
+
throw Object.assign(new Error("sensitive stderr and candidate bytes"), { code: "ENOBUFS", stderr: Buffer.from("sensitive stderr and candidate bytes") });
|
|
1882
|
+
});
|
|
1883
|
+
const { controller } = runtime(fakeNative({
|
|
1884
|
+
start: async () => {
|
|
1885
|
+
starts += 1;
|
|
1886
|
+
return { lineageId: "must-not-start", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
|
|
1887
|
+
},
|
|
1888
|
+
}), undefined, undefined, undefined, candidateViews);
|
|
1889
|
+
const result = await controller.execute("candidate-view-output-limit", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
|
|
1890
|
+
const details = result.details as Record<string, unknown>;
|
|
1891
|
+
assert.equal(details.outcome, "native-operation-failed");
|
|
1892
|
+
assert.equal(details.lineage_created, false);
|
|
1893
|
+
assert.equal(details.mutation_outcome, "none");
|
|
1894
|
+
assert.deepEqual(details.diagnostics, {
|
|
1895
|
+
phase: "candidate-view",
|
|
1896
|
+
category: "output-limit",
|
|
1897
|
+
git_subcommand: "rev-parse",
|
|
1898
|
+
timeout_ms: 10_000,
|
|
1899
|
+
max_buffer_bytes: 64 * 1024 * 1024,
|
|
1900
|
+
message: "candidate-view Git command rev-parse exceeded the 67108864-byte output limit; inspect the candidate state before any new START",
|
|
1901
|
+
});
|
|
1902
|
+
assert.doesNotMatch(JSON.stringify(details), /sensitive stderr|candidate bytes/);
|
|
1903
|
+
assert.equal(starts, 0);
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1513
1906
|
test("ambiguous native START failure preserves rebuilt sanitized diagnostics across duplicated module instances", async (t) => {
|
|
1514
1907
|
const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-native-controller-"));
|
|
1515
1908
|
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
@@ -1607,6 +2000,83 @@ test("native START uses the default policy or a canonical safe policy path, and
|
|
|
1607
2000
|
assert.equal(requests.length, 2);
|
|
1608
2001
|
});
|
|
1609
2002
|
|
|
2003
|
+
test("native ordinary START forwards every allowed focus and leaves the native default omitted", async (t) => {
|
|
2004
|
+
const cwd = repository(t);
|
|
2005
|
+
const requests: Parameters<NativeReviewCli["start"]>[0][] = [];
|
|
2006
|
+
const { controller } = runtime(fakeNative({
|
|
2007
|
+
start: async (request) => {
|
|
2008
|
+
requests.push(request);
|
|
2009
|
+
return { lineageId: `native-${request.focus ?? "default"}`, state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 2, changedLines: 7, correctionBudget: 4, action: "created", lensesRequired: true };
|
|
2010
|
+
},
|
|
2011
|
+
targetStatus: async () => targetStatusFixture({ applicability: "unrelated", action: "start" }),
|
|
2012
|
+
}));
|
|
2013
|
+
for (const focus of ["risk", "resilience", "readability", "reliability"] as const) {
|
|
2014
|
+
await controller.execute(`focused-${focus}`, { operation: "start", input: JSON.stringify({ mode: "ordinary", focus }) }, undefined, undefined, context(cwd));
|
|
2015
|
+
}
|
|
2016
|
+
await controller.execute("default-focus", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
|
|
2017
|
+
assert.deepEqual(requests, [
|
|
2018
|
+
...(["risk", "resilience", "readability", "reliability"] as const).map((focus) => ({ cwd, focus, targetIdentity: `sha256:${"a".repeat(64)}`, projection: "workspace" as const })),
|
|
2019
|
+
{ cwd, targetIdentity: `sha256:${"a".repeat(64)}`, projection: "workspace" },
|
|
2020
|
+
]);
|
|
2021
|
+
});
|
|
2022
|
+
|
|
2023
|
+
test("native ordinary START rejects invalid focus before native calls", async (t) => {
|
|
2024
|
+
const cwd = repository(t);
|
|
2025
|
+
let starts = 0;
|
|
2026
|
+
const { controller } = runtime(fakeNative({
|
|
2027
|
+
start: async () => {
|
|
2028
|
+
starts += 1;
|
|
2029
|
+
return { lineageId: "must-not-start", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
|
|
2030
|
+
},
|
|
2031
|
+
targetStatus: async () => targetStatusFixture({ applicability: "unrelated", action: "start" }),
|
|
2032
|
+
}));
|
|
2033
|
+
for (const focus of ["", "risk ", "RISK", "all", 42, null, [], {}]) {
|
|
2034
|
+
const rejected = await controller.execute("invalid-focus", { operation: "start", input: JSON.stringify({ mode: "ordinary", focus }) }, undefined, undefined, context(cwd));
|
|
2035
|
+
assert.deepEqual(rejected.details, {
|
|
2036
|
+
operation: "start",
|
|
2037
|
+
status: "blocked",
|
|
2038
|
+
outcome: "native-start-input-invalid",
|
|
2039
|
+
reason: "focus-invalid",
|
|
2040
|
+
lineage_created: false,
|
|
2041
|
+
mutation_performed: false,
|
|
2042
|
+
mutation_outcome: "none",
|
|
2043
|
+
reset_eligible: false,
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
assert.equal(starts, 0);
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
test("native ordinary START rejects malformed input before resolving the review-mode gate", async (t) => {
|
|
2050
|
+
const cwd = repository(t);
|
|
2051
|
+
let reviewModeCalls = 0;
|
|
2052
|
+
let starts = 0;
|
|
2053
|
+
const { controller } = runtime(fakeNative({
|
|
2054
|
+
reviewMode: async () => {
|
|
2055
|
+
reviewModeCalls += 1;
|
|
2056
|
+
throw new Error("review mode must not be resolved for malformed ordinary START input");
|
|
2057
|
+
},
|
|
2058
|
+
start: async () => {
|
|
2059
|
+
starts += 1;
|
|
2060
|
+
return { lineageId: "must-not-start", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
|
|
2061
|
+
},
|
|
2062
|
+
}));
|
|
2063
|
+
for (const [input, reason] of [
|
|
2064
|
+
[{ mode: "ordinary", policyHash: "legacy" }, "legacy-policy-hash-unsupported"],
|
|
2065
|
+
[{ mode: "ordinary", focus: "all" }, "focus-invalid"],
|
|
2066
|
+
[{ mode: "ordinary", unexpected: true }, "unknown-field"],
|
|
2067
|
+
[{ mode: "ordinary", policyPath: "outside.json" }, "policy-path-outside-scope"],
|
|
2068
|
+
[{ mode: "ordinary", baseRef: " " }, "base-ref-invalid"],
|
|
2069
|
+
[{ mode: "ordinary", baseRef: "origin/main" }, "committed-only-required"],
|
|
2070
|
+
[{ mode: "ordinary", committedOnly: true }, "committed-only-invalid"],
|
|
2071
|
+
[{ mode: "ordinary", baseRef: "refs/heads/missing", committedOnly: true }, "base-ref-unresolvable"],
|
|
2072
|
+
] as const) {
|
|
2073
|
+
const rejected = await controller.execute("malformed-start", { operation: "start", input: JSON.stringify(input) }, undefined, undefined, context(cwd));
|
|
2074
|
+
assert.equal((rejected.details as { reason?: unknown }).reason, reason);
|
|
2075
|
+
}
|
|
2076
|
+
assert.equal(reviewModeCalls, 0);
|
|
2077
|
+
assert.equal(starts, 0);
|
|
2078
|
+
});
|
|
2079
|
+
|
|
1610
2080
|
test("native START preserves the default dirty-inclusive candidate without base flags", async (t) => {
|
|
1611
2081
|
const cwd = repository(t);
|
|
1612
2082
|
writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
|
|
@@ -1662,6 +2132,47 @@ test("native START binds an acknowledged committed range and native identity to
|
|
|
1662
2132
|
}
|
|
1663
2133
|
});
|
|
1664
2134
|
|
|
2135
|
+
test("native START binds a default dirty-inclusive candidate on an unborn repository against Git's empty tree", async (t) => {
|
|
2136
|
+
// Unborn repository: symbolic HEAD, no commits, staged + untracked content.
|
|
2137
|
+
// The default targetStatus helper builds a REAL candidate view from this
|
|
2138
|
+
// repo, so this exercises the real resolveCandidateBase/materializeCandidate
|
|
2139
|
+
// path on an unborn repository, not a synthetic adapter stub.
|
|
2140
|
+
const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-native-controller-unborn-"));
|
|
2141
|
+
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
2142
|
+
execFileSync("git", ["init", "-b", "main"], { cwd });
|
|
2143
|
+
writeFileSync(join(cwd, "staged.txt"), "first staged\n");
|
|
2144
|
+
execFileSync("git", ["add", "staged.txt"], { cwd });
|
|
2145
|
+
writeFileSync(join(cwd, "untracked.ts"), "export const untracked = true;\n");
|
|
2146
|
+
const emptyTree = execFileSync("git", ["-C", cwd, "mktree"], { encoding: "utf8", input: "" }).trim();
|
|
2147
|
+
const candidateViews = new CandidateViewRegistry();
|
|
2148
|
+
const requests: Parameters<NativeReviewCli["start"]>[0][] = [];
|
|
2149
|
+
const { controller } = runtime(fakeNative({
|
|
2150
|
+
start: async (request) => {
|
|
2151
|
+
requests.push(request);
|
|
2152
|
+
return { lineageId: "unborn-lineage", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 2, changedLines: 2, correctionBudget: 1, action: "created", lensesRequired: true };
|
|
2153
|
+
},
|
|
2154
|
+
}), undefined, undefined, undefined, candidateViews);
|
|
2155
|
+
const started = await controller.execute("unborn-start", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
|
|
2156
|
+
const view = candidateViews.resolveForLens("unborn-lineage", "review-reliability");
|
|
2157
|
+
try {
|
|
2158
|
+
// The unborn candidate base is Git's empty tree, not a phantom commit.
|
|
2159
|
+
assert.equal(view.baseTree, emptyTree);
|
|
2160
|
+
assert.equal(view.baseCommit, "HEAD");
|
|
2161
|
+
assert.equal(view.committedOnly, false);
|
|
2162
|
+
// Staged and untracked workspace content is preserved in the candidate.
|
|
2163
|
+
assert.deepEqual(view.paths, ["staged.txt", "untracked.ts"]);
|
|
2164
|
+
// No baseRef is sent to native START: the unborn default uses the
|
|
2165
|
+
// workspace projection only, exactly as the provider expects.
|
|
2166
|
+
assert.deepEqual(requests, [{ cwd, targetIdentity: `sha256:${"a".repeat(64)}`, projection: "workspace" }]);
|
|
2167
|
+
const actorBinding = (started.details as { actor_binding: { workspace_root: string; candidate_root: string; candidate_tree: string; candidate_paths: readonly string[] } }).actor_binding;
|
|
2168
|
+
assert.equal(actorBinding.workspace_root, cwd);
|
|
2169
|
+
assert.equal(actorBinding.candidate_tree, view.candidateTree);
|
|
2170
|
+
assert.deepEqual(actorBinding.candidate_paths, view.paths);
|
|
2171
|
+
} finally {
|
|
2172
|
+
view.cleanup();
|
|
2173
|
+
}
|
|
2174
|
+
});
|
|
2175
|
+
|
|
1665
2176
|
test("native START fails closed before mutation when the workspace target and immutable candidate view differ", async (t) => {
|
|
1666
2177
|
const cwd = repository(t);
|
|
1667
2178
|
writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
|
|
@@ -1718,6 +2229,52 @@ test("native START re-verifies candidate-view integrity before granting workspac
|
|
|
1718
2229
|
assert.equal(starts, 0);
|
|
1719
2230
|
});
|
|
1720
2231
|
|
|
2232
|
+
test("native START preserves an explicit base-resolution timeout diagnostic without native START", async (t) => {
|
|
2233
|
+
const cwd = repository(t);
|
|
2234
|
+
const bin = join(cwd, "test-bin");
|
|
2235
|
+
mkdirSync(bin);
|
|
2236
|
+
const fakeGit = join(bin, "git");
|
|
2237
|
+
writeFileSync(fakeGit, "#!/bin/sh\nexec sleep 1\n");
|
|
2238
|
+
chmodSync(fakeGit, 0o755);
|
|
2239
|
+
const previousPath = process.env.PATH;
|
|
2240
|
+
const previousTimeout = process.env.GENTLE_PI_CANDIDATE_GIT_TIMEOUT_MS;
|
|
2241
|
+
t.after(() => {
|
|
2242
|
+
if (previousPath === undefined) delete process.env.PATH;
|
|
2243
|
+
else process.env.PATH = previousPath;
|
|
2244
|
+
if (previousTimeout === undefined) delete process.env.GENTLE_PI_CANDIDATE_GIT_TIMEOUT_MS;
|
|
2245
|
+
else process.env.GENTLE_PI_CANDIDATE_GIT_TIMEOUT_MS = previousTimeout;
|
|
2246
|
+
});
|
|
2247
|
+
process.env.PATH = `${bin}${delimiter}${previousPath ?? ""}`;
|
|
2248
|
+
process.env.GENTLE_PI_CANDIDATE_GIT_TIMEOUT_MS = "1";
|
|
2249
|
+
let starts = 0;
|
|
2250
|
+
let statuses = 0;
|
|
2251
|
+
const { controller } = runtime(fakeNative({
|
|
2252
|
+
targetStatus: async () => {
|
|
2253
|
+
statuses += 1;
|
|
2254
|
+
throw new Error("target status must not run after base resolution fails");
|
|
2255
|
+
},
|
|
2256
|
+
start: async () => {
|
|
2257
|
+
starts += 1;
|
|
2258
|
+
return { lineageId: "must-not-start", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
|
|
2259
|
+
},
|
|
2260
|
+
}), undefined, undefined, undefined, new CandidateViewRegistry());
|
|
2261
|
+
const result = await controller.execute("base-resolution-timeout", { operation: "start", input: JSON.stringify({ mode: "ordinary", baseRef: "refs/heads/main", committedOnly: true }) }, undefined, undefined, context(cwd));
|
|
2262
|
+
const details = result.details as Record<string, unknown>;
|
|
2263
|
+
assert.equal(details.outcome, "native-operation-failed");
|
|
2264
|
+
assert.equal(details.lineage_created, false);
|
|
2265
|
+
assert.equal(details.mutation_outcome, "none");
|
|
2266
|
+
assert.deepEqual(details.diagnostics, {
|
|
2267
|
+
phase: "candidate-view",
|
|
2268
|
+
category: "timeout",
|
|
2269
|
+
git_subcommand: "for-each-ref",
|
|
2270
|
+
timeout_ms: 1,
|
|
2271
|
+
max_buffer_bytes: 64 * 1024 * 1024,
|
|
2272
|
+
message: "candidate-view Git command for-each-ref timed out after 1ms; inspect the candidate state before any new START",
|
|
2273
|
+
});
|
|
2274
|
+
assert.equal(statuses, 0);
|
|
2275
|
+
assert.equal(starts, 0);
|
|
2276
|
+
});
|
|
2277
|
+
|
|
1721
2278
|
test("native START rejects an unresolvable explicit base before native mutation", async (t) => {
|
|
1722
2279
|
const cwd = repository(t);
|
|
1723
2280
|
let starts = 0;
|
|
@@ -1852,33 +2409,294 @@ test("native ordinary START blocks unknown input fields before native calls", as
|
|
|
1852
2409
|
const { controller } = runtime(fakeNative({
|
|
1853
2410
|
start: async () => {
|
|
1854
2411
|
starts += 1;
|
|
1855
|
-
return { lineageId: "native-lineage", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 2, changedLines: 7, correctionBudget: 4, action: "created", lensesRequired: true };
|
|
2412
|
+
return { lineageId: "native-lineage", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 2, changedLines: 7, correctionBudget: 4, action: "created", lensesRequired: true };
|
|
2413
|
+
},
|
|
2414
|
+
}));
|
|
2415
|
+
for (const field of ["base_ref", "focus_mode", "unexpected"]) {
|
|
2416
|
+
const rejected = await controller.execute("unknown-start-field", { operation: "start", input: JSON.stringify({ mode: "ordinary", [field]: "origin/main" }) }, undefined, undefined, context(cwd));
|
|
2417
|
+
assert.deepEqual(rejected.details, {
|
|
2418
|
+
operation: "start",
|
|
2419
|
+
status: "blocked",
|
|
2420
|
+
outcome: "native-start-input-invalid",
|
|
2421
|
+
reason: "unknown-field",
|
|
2422
|
+
field,
|
|
2423
|
+
lineage_created: false,
|
|
2424
|
+
mutation_performed: false,
|
|
2425
|
+
mutation_outcome: "none",
|
|
2426
|
+
reset_eligible: false,
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
assert.equal(starts, 0);
|
|
2430
|
+
});
|
|
2431
|
+
|
|
2432
|
+
test("ordinary START rejects a legacy policy hash before native availability", async (t) => {
|
|
2433
|
+
const cwd = repository(t);
|
|
2434
|
+
const { controller } = runtime(null);
|
|
2435
|
+
const result = await controller.execute("legacy-start", { operation: "start", input: JSON.stringify({ mode: "ordinary", policyHash: "a".repeat(64) }) }, undefined, undefined, context(cwd));
|
|
2436
|
+
assert.equal((result.details as { outcome?: string }).outcome, "native-start-legacy-policy-hash-unsupported");
|
|
2437
|
+
});
|
|
2438
|
+
|
|
2439
|
+
test("INSPECT reports a missing package-local native binary with fail-closed reinstall guidance", async (t) => {
|
|
2440
|
+
const cwd = repository(t);
|
|
2441
|
+
const rawPath = "/private/workspace/.gentle-ai/gentle-ai";
|
|
2442
|
+
const rawSecret = "package-binary-secret";
|
|
2443
|
+
const { controller } = runtime(fakeNative({
|
|
2444
|
+
targetStatus: async () => {
|
|
2445
|
+
throw new NativeReviewCliError(
|
|
2446
|
+
NATIVE_REVIEW_ERROR_CODE.PACKAGE_BINARY_MISSING,
|
|
2447
|
+
NATIVE_REVIEW_OPERATION.VERSION,
|
|
2448
|
+
false,
|
|
2449
|
+
false,
|
|
2450
|
+
`missing ${rawPath} token=${rawSecret}`,
|
|
2451
|
+
);
|
|
2452
|
+
},
|
|
2453
|
+
}));
|
|
2454
|
+
const response = await controller.execute("inspect-package-binary-missing", { operation: "inspect" }, undefined, undefined, context(cwd));
|
|
2455
|
+
const details = response.details as Record<string, unknown>;
|
|
2456
|
+
assert.equal(details.status, "blocked");
|
|
2457
|
+
assert.equal(details.outcome, "native-status-package-binary-missing");
|
|
2458
|
+
assert.deepEqual(details.diagnostics, {
|
|
2459
|
+
operation: NATIVE_REVIEW_OPERATION.VERSION,
|
|
2460
|
+
error_code: NATIVE_REVIEW_ERROR_CODE.PACKAGE_BINARY_MISSING,
|
|
2461
|
+
timed_out: false,
|
|
2462
|
+
output_limit_exceeded: false,
|
|
2463
|
+
});
|
|
2464
|
+
assert.equal(details.inventory_complete, false);
|
|
2465
|
+
assert.equal(details.mutation_performed, false);
|
|
2466
|
+
assert.equal(details.mutation_outcome, "none");
|
|
2467
|
+
assert.equal(details.next_action, "reinstall-package-local-gentle-ai");
|
|
2468
|
+
assert.doesNotMatch(JSON.stringify(details), new RegExp(`${rawPath}|${rawSecret}`));
|
|
2469
|
+
});
|
|
2470
|
+
|
|
2471
|
+
test("START reports a missing package-local native binary before any native mutation", async (t) => {
|
|
2472
|
+
const cwd = repository(t);
|
|
2473
|
+
let starts = 0;
|
|
2474
|
+
const { controller } = runtime(fakeNative({
|
|
2475
|
+
targetStatus: async () => {
|
|
2476
|
+
throw new NativeReviewCliError(
|
|
2477
|
+
NATIVE_REVIEW_ERROR_CODE.PACKAGE_BINARY_MISSING,
|
|
2478
|
+
NATIVE_REVIEW_OPERATION.VERSION,
|
|
2479
|
+
false,
|
|
2480
|
+
false,
|
|
2481
|
+
"package-local binary missing",
|
|
2482
|
+
);
|
|
2483
|
+
},
|
|
2484
|
+
start: async () => {
|
|
2485
|
+
starts += 1;
|
|
2486
|
+
return { lineageId: "must-not-start", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
|
|
2487
|
+
},
|
|
2488
|
+
}));
|
|
2489
|
+
const response = await controller.execute("start-package-binary-missing", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
|
|
2490
|
+
const details = response.details as Record<string, unknown>;
|
|
2491
|
+
assert.equal(details.status, "blocked");
|
|
2492
|
+
assert.equal(details.outcome, "native-status-package-binary-missing");
|
|
2493
|
+
assert.equal(details.inventory_complete, false);
|
|
2494
|
+
assert.equal(details.lineage_created, false);
|
|
2495
|
+
assert.equal(details.mutation_performed, false);
|
|
2496
|
+
assert.equal(details.mutation_outcome, "none");
|
|
2497
|
+
assert.equal(details.next_action, "reinstall-package-local-gentle-ai");
|
|
2498
|
+
assert.equal(starts, 0);
|
|
2499
|
+
});
|
|
2500
|
+
|
|
2501
|
+
test("REPAIR reports a missing package-local native binary without touching authority", async (t) => {
|
|
2502
|
+
const cwd = repository(t);
|
|
2503
|
+
const { controller } = runtime(fakeNative({
|
|
2504
|
+
targetStatus: async () => {
|
|
2505
|
+
throw new NativeReviewCliError(
|
|
2506
|
+
NATIVE_REVIEW_ERROR_CODE.PACKAGE_BINARY_MISSING,
|
|
2507
|
+
NATIVE_REVIEW_OPERATION.VERSION,
|
|
2508
|
+
false,
|
|
2509
|
+
false,
|
|
2510
|
+
"package-local binary missing",
|
|
2511
|
+
);
|
|
2512
|
+
},
|
|
2513
|
+
}));
|
|
2514
|
+
const response = await controller.execute("repair-package-binary-missing", { operation: "repair" }, undefined, undefined, context(cwd));
|
|
2515
|
+
const details = response.details as Record<string, unknown>;
|
|
2516
|
+
assert.equal(details.status, "blocked");
|
|
2517
|
+
assert.equal(details.outcome, "native-status-package-binary-missing");
|
|
2518
|
+
assert.equal(details.inventory_complete, false);
|
|
2519
|
+
assert.equal(details.mutation_performed, false);
|
|
2520
|
+
assert.equal(details.mutation_outcome, "none");
|
|
2521
|
+
assert.equal(details.next_action, "reinstall-package-local-gentle-ai");
|
|
2522
|
+
});
|
|
2523
|
+
|
|
2524
|
+
test("START preserves bounded sanitized version-process diagnostics before native mutation", async (t) => {
|
|
2525
|
+
const cwd = repository(t);
|
|
2526
|
+
const rawPath = "/private/workspace/gentle-ai";
|
|
2527
|
+
const rawSecret = "version-process-secret";
|
|
2528
|
+
const stderr = `version process failed token=${rawSecret}\n${"x".repeat(5_000)}`;
|
|
2529
|
+
const foreignError = Object.assign(new Error(`version process failed at ${rawPath} token=${rawSecret}`), {
|
|
2530
|
+
name: "NativeReviewCliError",
|
|
2531
|
+
code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO,
|
|
2532
|
+
diagnostics: {
|
|
2533
|
+
operation: NATIVE_REVIEW_OPERATION.VERSION,
|
|
2534
|
+
error_code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO,
|
|
2535
|
+
exit_code: 17,
|
|
2536
|
+
timed_out: false,
|
|
2537
|
+
output_limit_exceeded: false,
|
|
2538
|
+
stderr,
|
|
2539
|
+
},
|
|
2540
|
+
});
|
|
2541
|
+
let starts = 0;
|
|
2542
|
+
const { controller } = runtime(fakeNative({
|
|
2543
|
+
targetStatus: async () => { throw foreignError; },
|
|
2544
|
+
start: async () => {
|
|
2545
|
+
starts += 1;
|
|
2546
|
+
return { lineageId: "must-not-start", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
|
|
2547
|
+
},
|
|
2548
|
+
}));
|
|
2549
|
+
const response = await controller.execute("start-version-process-failure", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
|
|
2550
|
+
const details = response.details as Record<string, unknown>;
|
|
2551
|
+
const diagnostics = details.diagnostics as { operation?: string; error_code?: string; stderr?: string };
|
|
2552
|
+
assert.equal(details.status, "blocked");
|
|
2553
|
+
assert.equal(details.outcome, "native-operation-failed");
|
|
2554
|
+
assert.equal(diagnostics.operation, NATIVE_REVIEW_OPERATION.VERSION);
|
|
2555
|
+
assert.equal(diagnostics.error_code, NATIVE_REVIEW_ERROR_CODE.NON_ZERO);
|
|
2556
|
+
assert.match(diagnostics.stderr ?? "", /token=\[REDACTED\]/);
|
|
2557
|
+
assert.ok((diagnostics.stderr?.length ?? 0) <= 4_096);
|
|
2558
|
+
assert.equal(details.lineage_created, false);
|
|
2559
|
+
assert.equal(details.mutation_performed, false);
|
|
2560
|
+
assert.equal(details.mutation_outcome, "none");
|
|
2561
|
+
assert.equal(starts, 0);
|
|
2562
|
+
assert.doesNotMatch(JSON.stringify(details), new RegExp(`${rawPath}|${rawSecret}`));
|
|
2563
|
+
|
|
2564
|
+
const inspect = await controller.execute("inspect-version-process-failure", { operation: "inspect" }, undefined, undefined, context(cwd));
|
|
2565
|
+
const inspectDetails = inspect.details as Record<string, unknown>;
|
|
2566
|
+
assert.equal(inspectDetails.status, "blocked");
|
|
2567
|
+
assert.equal(inspectDetails.outcome, "native-status-unavailable");
|
|
2568
|
+
assert.deepEqual(inspectDetails.diagnostics, details.diagnostics);
|
|
2569
|
+
assert.equal(inspectDetails.inventory_complete, false);
|
|
2570
|
+
assert.equal(inspectDetails.mutation_performed, false);
|
|
2571
|
+
assert.equal(inspectDetails.mutation_outcome, "none");
|
|
2572
|
+
assert.doesNotMatch(JSON.stringify(inspectDetails), new RegExp(`${rawPath}|${rawSecret}`));
|
|
2573
|
+
});
|
|
2574
|
+
|
|
2575
|
+
test("INSPECT preserves bounded sanitized review/status process diagnostics and remains fail-closed", async (t) => {
|
|
2576
|
+
const cwd = repository(t);
|
|
2577
|
+
const rawPath = "/private/workspace/review-status";
|
|
2578
|
+
const rawSecret = "review-status-secret";
|
|
2579
|
+
const stderr = `review status failed token=${rawSecret}\n${"x".repeat(5_000)}`;
|
|
2580
|
+
const foreignError = Object.assign(new Error(`review status failed at ${rawPath} token=${rawSecret}`), {
|
|
2581
|
+
name: "NativeReviewCliError",
|
|
2582
|
+
code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO,
|
|
2583
|
+
diagnostics: {
|
|
2584
|
+
operation: NATIVE_REVIEW_OPERATION.STATUS,
|
|
2585
|
+
error_code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO,
|
|
2586
|
+
exit_code: 18,
|
|
2587
|
+
timed_out: false,
|
|
2588
|
+
output_limit_exceeded: false,
|
|
2589
|
+
stderr,
|
|
2590
|
+
},
|
|
2591
|
+
});
|
|
2592
|
+
const { controller } = runtime(fakeNative({
|
|
2593
|
+
targetStatus: async () => { throw foreignError; },
|
|
2594
|
+
}));
|
|
2595
|
+
const response = await controller.execute("inspect-review-status-process-failure", { operation: "inspect" }, undefined, undefined, context(cwd));
|
|
2596
|
+
const details = response.details as Record<string, unknown>;
|
|
2597
|
+
const diagnostics = details.diagnostics as { operation?: string; error_code?: string; stderr?: string };
|
|
2598
|
+
assert.equal(details.status, "blocked");
|
|
2599
|
+
assert.equal(details.outcome, "native-status-unavailable");
|
|
2600
|
+
assert.equal(diagnostics.operation, NATIVE_REVIEW_OPERATION.STATUS);
|
|
2601
|
+
assert.equal(diagnostics.error_code, NATIVE_REVIEW_ERROR_CODE.NON_ZERO);
|
|
2602
|
+
assert.match(diagnostics.stderr ?? "", /token=\[REDACTED\]/);
|
|
2603
|
+
assert.ok((diagnostics.stderr?.length ?? 0) <= 4_096);
|
|
2604
|
+
assert.equal(details.inventory_complete, false);
|
|
2605
|
+
assert.equal(details.mutation_performed, false);
|
|
2606
|
+
assert.equal(details.mutation_outcome, "none");
|
|
2607
|
+
assert.doesNotMatch(JSON.stringify(details), new RegExp(`${rawPath}|${rawSecret}`));
|
|
2608
|
+
});
|
|
2609
|
+
|
|
2610
|
+
test("INSPECT keeps version-incompatible target status failures mapped to native-status-unsupported", async (t) => {
|
|
2611
|
+
const cwd = repository(t);
|
|
2612
|
+
const { controller } = runtime(fakeNative({
|
|
2613
|
+
targetStatus: async () => {
|
|
2614
|
+
throw new NativeReviewCliError(
|
|
2615
|
+
NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE,
|
|
2616
|
+
NATIVE_REVIEW_OPERATION.VERSION,
|
|
2617
|
+
true,
|
|
2618
|
+
false,
|
|
2619
|
+
"native version is incompatible",
|
|
2620
|
+
);
|
|
2621
|
+
},
|
|
2622
|
+
}));
|
|
2623
|
+
const response = await controller.execute("inspect-version-incompatible", { operation: "inspect" }, undefined, undefined, context(cwd));
|
|
2624
|
+
const details = response.details as Record<string, unknown>;
|
|
2625
|
+
assert.equal(details.status, "blocked");
|
|
2626
|
+
assert.equal(details.outcome, "native-status-unsupported");
|
|
2627
|
+
assert.equal(details.inventory_complete, false);
|
|
2628
|
+
assert.equal(details.mutation_performed, false);
|
|
2629
|
+
assert.equal(details.mutation_outcome, undefined);
|
|
2630
|
+
});
|
|
2631
|
+
|
|
2632
|
+
test("INSPECT preserves output-limit diagnostics from native review status and remains fail-closed", async (t) => {
|
|
2633
|
+
const cwd = repository(t);
|
|
2634
|
+
const diagnostics = {
|
|
2635
|
+
operation: NATIVE_REVIEW_OPERATION.STATUS,
|
|
2636
|
+
error_code: NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT,
|
|
2637
|
+
output_limit_exceeded: true,
|
|
2638
|
+
timed_out: false,
|
|
2639
|
+
};
|
|
2640
|
+
const { controller } = runtime(fakeNative({
|
|
2641
|
+
targetStatus: async () => {
|
|
2642
|
+
throw new NativeReviewCliError(
|
|
2643
|
+
NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT,
|
|
2644
|
+
NATIVE_REVIEW_OPERATION.STATUS,
|
|
2645
|
+
true,
|
|
2646
|
+
false,
|
|
2647
|
+
"native process output exceeded limit",
|
|
2648
|
+
diagnostics,
|
|
2649
|
+
);
|
|
2650
|
+
},
|
|
2651
|
+
}));
|
|
2652
|
+
const response = await controller.execute("inspect-output-limit", { operation: "inspect" }, undefined, undefined, context(cwd));
|
|
2653
|
+
const details = response.details as Record<string, unknown>;
|
|
2654
|
+
assert.equal(details.status, "blocked");
|
|
2655
|
+
assert.equal(details.outcome, "native-status-unavailable");
|
|
2656
|
+
assert.deepEqual(details.diagnostics, diagnostics);
|
|
2657
|
+
assert.equal(details.inventory_complete, false);
|
|
2658
|
+
assert.equal(details.next_action, "require-complete-native-authority-inventory");
|
|
2659
|
+
assert.equal(details.mutation_performed, false);
|
|
2660
|
+
assert.equal(details.mutation_outcome, "none");
|
|
2661
|
+
});
|
|
2662
|
+
|
|
2663
|
+
test("START preserves actionable review/status output-limit diagnostics before native mutation", async (t) => {
|
|
2664
|
+
const cwd = repository(t);
|
|
2665
|
+
const diagnostics = {
|
|
2666
|
+
operation: NATIVE_REVIEW_OPERATION.STATUS,
|
|
2667
|
+
error_code: NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT,
|
|
2668
|
+
output_limit_exceeded: true,
|
|
2669
|
+
timed_out: false,
|
|
2670
|
+
max_buffer_bytes: 16 * 1024 * 1024,
|
|
2671
|
+
configuration_hint: "Inspect native review state before any new START; GENTLE_PI_REVIEW_MAX_BUFFER_BYTES accepts a positive decimal up to 67108864.",
|
|
2672
|
+
};
|
|
2673
|
+
let starts = 0;
|
|
2674
|
+
const { controller } = runtime(fakeNative({
|
|
2675
|
+
targetStatus: async () => {
|
|
2676
|
+
throw new NativeReviewCliError(
|
|
2677
|
+
NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT,
|
|
2678
|
+
NATIVE_REVIEW_OPERATION.STATUS,
|
|
2679
|
+
true,
|
|
2680
|
+
false,
|
|
2681
|
+
"native process output exceeded limit",
|
|
2682
|
+
diagnostics,
|
|
2683
|
+
);
|
|
2684
|
+
},
|
|
2685
|
+
start: async () => {
|
|
2686
|
+
starts += 1;
|
|
2687
|
+
return { lineageId: "must-not-start", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
|
|
1856
2688
|
},
|
|
1857
2689
|
}));
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
field,
|
|
1866
|
-
lineage_created: false,
|
|
1867
|
-
mutation_performed: false,
|
|
1868
|
-
mutation_outcome: "none",
|
|
1869
|
-
reset_eligible: false,
|
|
1870
|
-
});
|
|
1871
|
-
}
|
|
2690
|
+
const response = await controller.execute("start-status-output-limit", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
|
|
2691
|
+
const details = response.details as Record<string, unknown>;
|
|
2692
|
+
assert.equal(details.status, "blocked");
|
|
2693
|
+
assert.equal(details.outcome, "native-operation-failed");
|
|
2694
|
+
assert.deepEqual(details.diagnostics, diagnostics);
|
|
2695
|
+
assert.equal(details.lineage_created, false);
|
|
2696
|
+
assert.equal(details.mutation_outcome, "none");
|
|
1872
2697
|
assert.equal(starts, 0);
|
|
1873
2698
|
});
|
|
1874
2699
|
|
|
1875
|
-
test("ordinary START fails closed before legacy policy handling when target status is unavailable", async (t) => {
|
|
1876
|
-
const cwd = repository(t);
|
|
1877
|
-
const { controller } = runtime(null);
|
|
1878
|
-
const result = await controller.execute("legacy-start", { operation: "start", input: JSON.stringify({ mode: "ordinary", policyHash: "a".repeat(64) }) }, undefined, undefined, context(cwd));
|
|
1879
|
-
assert.equal((result.details as { outcome?: string }).outcome, "native-status-unsupported");
|
|
1880
|
-
});
|
|
1881
|
-
|
|
1882
2700
|
test("general STATUS and INSPECT use negotiated target status without mutation or inventory reads", async (t) => {
|
|
1883
2701
|
const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-native-controller-"));
|
|
1884
2702
|
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
@@ -1922,7 +2740,7 @@ test("legacy compact FINALIZE is a typed read-only rejection without native fall
|
|
|
1922
2740
|
}));
|
|
1923
2741
|
const result = await controller.execute(
|
|
1924
2742
|
"legacy-finalize",
|
|
1925
|
-
{ operation: "finalize", lineageId, input: JSON.stringify({
|
|
2743
|
+
{ operation: "finalize", lineageId, input: JSON.stringify({}) },
|
|
1926
2744
|
undefined,
|
|
1927
2745
|
undefined,
|
|
1928
2746
|
context(cwd),
|
|
@@ -1962,7 +2780,7 @@ test("legacy graph-v1 FINALIZE is a typed read-only rejection without native fal
|
|
|
1962
2780
|
}));
|
|
1963
2781
|
const result = await controller.execute(
|
|
1964
2782
|
"legacy-graph-finalize",
|
|
1965
|
-
{ operation: "finalize", lineageId, input: JSON.stringify({
|
|
2783
|
+
{ operation: "finalize", lineageId, input: JSON.stringify({}) },
|
|
1966
2784
|
undefined,
|
|
1967
2785
|
undefined,
|
|
1968
2786
|
context(cwd),
|
|
@@ -2253,7 +3071,7 @@ test("native pre-commit rejects an unproven staged projection before native auth
|
|
|
2253
3071
|
}), undefined, undefined, undefined, candidateViews);
|
|
2254
3072
|
const started = await controller.execute("start", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
|
|
2255
3073
|
const lineageId = (started.details as { result: { lineage_id: string } }).result.lineage_id;
|
|
2256
|
-
await controller.execute("finalize", { operation: "finalize", lineageId, input: JSON.stringify({
|
|
3074
|
+
await controller.execute("finalize", { operation: "finalize", lineageId, input: JSON.stringify({}) }, undefined, undefined, context(cwd));
|
|
2257
3075
|
writeFileSync(join(cwd, "app.ts"), "export const value = 3;\n");
|
|
2258
3076
|
git(cwd, "add", "--", "app.ts", "initially-untracked.ts");
|
|
2259
3077
|
const result = await controller.execute("validate", { operation: "validate", lineageId, idempotencyKey: "projection-drift", command: "git commit -m native", input: "{}" }, undefined, undefined, context(cwd));
|
|
@@ -2276,7 +3094,7 @@ test("native pre-commit binds the exact tracked and initially-untracked projecti
|
|
|
2276
3094
|
const { controller, toolCall } = runtime(native, undefined, undefined, undefined, candidateViews);
|
|
2277
3095
|
const started = await controller.execute("start", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
|
|
2278
3096
|
const lineageId = (started.details as { result: { lineage_id: string } }).result.lineage_id;
|
|
2279
|
-
await controller.execute("finalize", { operation: "finalize", lineageId, input: JSON.stringify({
|
|
3097
|
+
await controller.execute("finalize", { operation: "finalize", lineageId, input: JSON.stringify({}) }, undefined, undefined, context(cwd));
|
|
2280
3098
|
git(cwd, "add", "--", "app.ts", "initially-untracked.ts");
|
|
2281
3099
|
const command = "git commit -m exact-projection";
|
|
2282
3100
|
const allowed = await controller.execute("validate", { operation: "validate", lineageId, idempotencyKey: "exact-projection", command, input: "{}" }, undefined, undefined, context(cwd));
|
|
@@ -3687,7 +4505,7 @@ test("controller preserves every historical response-schema empty-context pre-PR
|
|
|
3687
4505
|
}
|
|
3688
4506
|
});
|
|
3689
4507
|
|
|
3690
|
-
test("parallel 4R dispatch receives
|
|
4508
|
+
test("parallel 4R dispatch receives readable and compact changed scopes before any actor", async (t) => {
|
|
3691
4509
|
const cwd = repository(t);
|
|
3692
4510
|
for (let index = 0; index < 248; index += 1) {
|
|
3693
4511
|
writeFileSync(join(cwd, `unchanged-${String(index).padStart(3, "0")}.txt`), "base\n");
|
|
@@ -3724,9 +4542,24 @@ test("parallel 4R dispatch receives one compact changed scope and blocks oversiz
|
|
|
3724
4542
|
start: async () => ({ lineageId: "c4-oversized", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 80, changedLines: 80, correctionBudget: 40, action: "created", lensesRequired: true }),
|
|
3725
4543
|
}), undefined, undefined, undefined, oversizedViews);
|
|
3726
4544
|
await oversized.controller.execute("c4-oversized-start", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
|
|
3727
|
-
const
|
|
3728
|
-
assert.equal(
|
|
3729
|
-
|
|
4545
|
+
const oversizedDispatch = { agent: "review-reliability", task: "Review oversized scope", mode: "task" };
|
|
4546
|
+
assert.equal(await oversized.toolCall({ toolName: "subagent_run", input: oversizedDispatch }, context(cwd)), undefined, "a compressible oversized scope reaches the actor through its compact manifest");
|
|
4547
|
+
assert.match(oversizedDispatch.task, /Frozen changed scope manifest \(gzip\+base64url\):/);
|
|
4548
|
+
assert.match(oversizedDispatch.task, /Call `gentle_review_scope`/);
|
|
4549
|
+
assert.ok(Buffer.byteLength(oversizedDispatch.task, "utf8") <= 4_096 + "Review oversized scope".length);
|
|
4550
|
+
const compact = compactCandidateContextManifest(oversizedDispatch.task);
|
|
4551
|
+
const actorEntries: Array<{ path: string; mode: string; gitlinkObjectId?: string }> = [];
|
|
4552
|
+
let cursor: number | undefined = 0;
|
|
4553
|
+
while (cursor !== undefined) {
|
|
4554
|
+
const result = await oversized.scopeReader.execute("actor-scope", { manifest: compact.encoded, sha256: compact.sha256, cursor }, undefined, undefined, context(cwd));
|
|
4555
|
+
const page = result.details as { entries: typeof actorEntries; nextCursor?: number; totalPaths: number };
|
|
4556
|
+
actorEntries.push(...page.entries);
|
|
4557
|
+
cursor = page.nextCursor;
|
|
4558
|
+
}
|
|
4559
|
+
const actorCandidate = oversizedViews.resolveForLens("c4-oversized", "review-reliability");
|
|
4560
|
+
assert.deepEqual(actorEntries.map((entry) => entry.path).sort(), [...actorCandidate.paths].sort(), "the actual actor tool recovers every compressed changed path");
|
|
4561
|
+
assert.equal(actorEntries.some((entry) => entry.path.startsWith("unchanged-")), false, "the actor tool never falls back to the ambient or full candidate tree");
|
|
4562
|
+
actorCandidate.cleanup();
|
|
3730
4563
|
});
|
|
3731
4564
|
|
|
3732
4565
|
test("INSPECT relays negotiated target status without inventory reconstruction or mutation", async (t) => {
|
|
@@ -3754,26 +4587,6 @@ test("INSPECT relays negotiated target status without inventory reconstruction o
|
|
|
3754
4587
|
assert.equal(mutations, 0);
|
|
3755
4588
|
});
|
|
3756
4589
|
|
|
3757
|
-
test("native remediation classification accepts only invalid legacy, compact, collision, or reset evidence", () => {
|
|
3758
|
-
const status = (authorityStatus: NativeReviewStatusResult["status"], entries: NativeReviewStatusResult["entries"]): NativeReviewStatusResult => ({
|
|
3759
|
-
repository: "/repo",
|
|
3760
|
-
complete: authorityStatus !== "invalid",
|
|
3761
|
-
authoritative: authorityStatus !== "invalid",
|
|
3762
|
-
status: authorityStatus,
|
|
3763
|
-
entries,
|
|
3764
|
-
locks: [],
|
|
3765
|
-
diagnostics: [],
|
|
3766
|
-
raw: {},
|
|
3767
|
-
});
|
|
3768
|
-
assert.equal(classifyNativeReviewRemediation(status("invalid", [{ version: "legacy-v1", lineageId: "current", path: "/repo/legacy", status: "invalid", problems: [] }]), ["current"]).kind, NATIVE_REVIEW_REMEDIATION.LEGACY);
|
|
3769
|
-
assert.equal(classifyNativeReviewRemediation(status("invalid", [{ version: "compact-v2", lineageId: "current", path: "/repo/compact", status: "invalid", problems: [] }]), ["current"]).kind, NATIVE_REVIEW_REMEDIATION.INVALID_OR_MIXED);
|
|
3770
|
-
assert.equal(classifyNativeReviewRemediation({ ...status("same-lineage-mixed-collision", []), complete: false, authoritative: false }, ["current"]).kind, NATIVE_REVIEW_REMEDIATION.NONE);
|
|
3771
|
-
assert.equal(classifyNativeReviewRemediation(status("reset-in-progress", [])).kind, NATIVE_REVIEW_REMEDIATION.NONE);
|
|
3772
|
-
assert.equal(classifyNativeReviewRemediation(status("invalid", [])).kind, NATIVE_REVIEW_REMEDIATION.NONE);
|
|
3773
|
-
assert.equal(classifyNativeReviewRemediation({ ...status("invalid", [{ version: "legacy-v1", path: "/repo/legacy", status: "invalid", problems: [] }]), complete: true }).kind, NATIVE_REVIEW_REMEDIATION.NONE);
|
|
3774
|
-
assert.equal(classifyNativeReviewRemediation(status("approved", [{ version: "legacy-v1", path: "/repo/legacy", status: "approved", problems: [] }])).kind, NATIVE_REVIEW_REMEDIATION.NONE);
|
|
3775
|
-
});
|
|
3776
|
-
|
|
3777
4590
|
test("native INSPECT never reconstructs reset material from raw Pi corruption", async (t) => {
|
|
3778
4591
|
const nativeStatus = (cwd: string, entries: NativeReviewStatusResult["entries"]): NativeReviewStatusResult => ({
|
|
3779
4592
|
repository: cwd,
|
|
@@ -4276,3 +5089,635 @@ test("successful /gentle:review-mode disable clears pending authorizations even
|
|
|
4276
5089
|
assert.equal(result.block, true);
|
|
4277
5090
|
assert.match(result.reason, /receipt consumption failed closed/, "re-enabled delivery must perform fresh receipt discovery rather than reuse the pre-disable authorization");
|
|
4278
5091
|
});
|
|
5092
|
+
|
|
5093
|
+
function gitStdin(cwd: string, arguments_: readonly string[], input: string): string {
|
|
5094
|
+
return execFileSync("git", [...arguments_], { cwd, encoding: "utf8", input }).trim();
|
|
5095
|
+
}
|
|
5096
|
+
|
|
5097
|
+
test("large repository end-to-end: tiny candidate diff reaches START, reviewer dispatch, FINALIZE, and the validation delivery gate", async (t) => {
|
|
5098
|
+
// Build a genuinely large synthetic repository (5000 unchanged entries) from a single
|
|
5099
|
+
// blob via `git mktree`, avoiding thousands of per-file filesystem writes/subprocesses.
|
|
5100
|
+
// The candidate diff is exactly one modified entry — the smallest review scope — so the
|
|
5101
|
+
// frozen candidate view materializes the full 5000-entry tree through the production
|
|
5102
|
+
// checkout-index path while the reviewer dispatch carries only the one changed path.
|
|
5103
|
+
const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-large-repo-e2e-"));
|
|
5104
|
+
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
5105
|
+
git(cwd, "init", "-b", "main");
|
|
5106
|
+
const baseBlob = gitStdin(cwd, ["hash-object", "-w", "--stdin"], "base\n");
|
|
5107
|
+
const largeCount = 5_000;
|
|
5108
|
+
const baseTreeInput = Array.from({ length: largeCount }, (_, index) =>
|
|
5109
|
+
`100644 blob ${baseBlob}\tunchanged-${String(index).padStart(4, "0")}.txt`,
|
|
5110
|
+
).join("\n");
|
|
5111
|
+
const baseTree = gitStdin(cwd, ["mktree"], baseTreeInput);
|
|
5112
|
+
const candidateBlob = gitStdin(cwd, ["hash-object", "-w", "--stdin"], "candidate\n");
|
|
5113
|
+
const candidateTreeInput = Array.from({ length: largeCount }, (_, index) =>
|
|
5114
|
+
index === 0
|
|
5115
|
+
? `100644 blob ${candidateBlob}\tunchanged-0000.txt`
|
|
5116
|
+
: `100644 blob ${baseBlob}\tunchanged-${String(index).padStart(4, "0")}.txt`,
|
|
5117
|
+
).join("\n");
|
|
5118
|
+
const candidateTree = gitStdin(cwd, ["mktree"], candidateTreeInput);
|
|
5119
|
+
const commitIdentity = ["-c", "user.name=Large Repo E2E", "-c", "user.email=large@example.invalid"];
|
|
5120
|
+
const baseCommit = git(cwd, ...commitIdentity, "commit-tree", baseTree, "-m", "large base");
|
|
5121
|
+
const candidateCommit = git(cwd, ...commitIdentity, "commit-tree", candidateTree, "-p", baseCommit, "-m", "tiny candidate");
|
|
5122
|
+
git(cwd, "update-ref", "refs/heads/main", candidateCommit);
|
|
5123
|
+
|
|
5124
|
+
const candidateViews = new CandidateViewRegistry();
|
|
5125
|
+
const lenses = ["review-risk", "review-resilience", "review-readability", "review-reliability"] as const;
|
|
5126
|
+
const lineageId = "large-repo-e2e";
|
|
5127
|
+
let starts = 0;
|
|
5128
|
+
let finalizes = 0;
|
|
5129
|
+
let validates = 0;
|
|
5130
|
+
let finalizeRoot: string | undefined;
|
|
5131
|
+
const { controller, toolCall } = runtime(fakeNative({
|
|
5132
|
+
start: async () => {
|
|
5133
|
+
starts += 1;
|
|
5134
|
+
return { lineageId, state: "reviewing", riskLevel: "high", selectedLenses: lenses, changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
|
|
5135
|
+
},
|
|
5136
|
+
finalize: async (request) => {
|
|
5137
|
+
finalizes += 1;
|
|
5138
|
+
finalizeRoot = request.cwd;
|
|
5139
|
+
return { lineageId, state: "approved", action: "approved", storeRevision: "r1" };
|
|
5140
|
+
},
|
|
5141
|
+
validate: async () => {
|
|
5142
|
+
validates += 1;
|
|
5143
|
+
return { allowed: true, result: "allow", action: "continue", reason: "native receipt matches the frozen large-repo candidate", gateContext: nativeGateContext(lineageId, "r1", candidateTree) };
|
|
5144
|
+
},
|
|
5145
|
+
targetStatus: async (request) => {
|
|
5146
|
+
if (request.lineageId === undefined) return candidateStartTargetStatus(request);
|
|
5147
|
+
const statusCandidate = new CandidateViewRegistry().create({ contributorRoot: request.cwd, baseRef: baseCommit });
|
|
5148
|
+
try {
|
|
5149
|
+
return targetStatusFixture({ lineageId, baseTree: statusCandidate.baseTree, currentCandidateTree: statusCandidate.candidateTree, paths: statusCandidate.paths });
|
|
5150
|
+
} finally {
|
|
5151
|
+
statusCandidate.cleanup();
|
|
5152
|
+
}
|
|
5153
|
+
},
|
|
5154
|
+
}), undefined, undefined, undefined, candidateViews);
|
|
5155
|
+
|
|
5156
|
+
// START binds the frozen candidate view for the large repository.
|
|
5157
|
+
const start = await controller.execute("large-repo-start", { operation: "start", input: JSON.stringify({ mode: "ordinary", baseRef: baseCommit, committedOnly: true }) }, undefined, undefined, context(cwd));
|
|
5158
|
+
assert.equal(starts, 1);
|
|
5159
|
+
assert.equal((start.details as { result: { lineage_id: string } }).result.lineage_id, lineageId);
|
|
5160
|
+
const frozen = candidateViews.resolveForLens(lineageId, "review-risk");
|
|
5161
|
+
try {
|
|
5162
|
+
// Immutable candidate identity: the frozen view binds the exact synthetic trees.
|
|
5163
|
+
assert.equal(frozen.baseTree, baseTree);
|
|
5164
|
+
assert.equal(frozen.candidateTree, candidateTree);
|
|
5165
|
+
assert.deepEqual(frozen.paths, ["unchanged-0000.txt"]);
|
|
5166
|
+
assert.equal(readdirSync(frozen.root).filter((entry) => entry !== ".git").length, largeCount);
|
|
5167
|
+
assert.equal(readFileSync(join(frozen.root, "unchanged-0000.txt"), "utf8"), "candidate\n");
|
|
5168
|
+
assert.equal(readFileSync(join(frozen.root, "unchanged-4999.txt"), "utf8"), "base\n");
|
|
5169
|
+
|
|
5170
|
+
// Reviewer dispatch injects the controller-owned candidate view carrying only the
|
|
5171
|
+
// tiny changed scope — never the 5000-entry unchanged repository bulk.
|
|
5172
|
+
const dispatch = { agents: [...lenses], task: "Review the tiny candidate diff in a large repository", mode: "task" };
|
|
5173
|
+
assert.equal(await toolCall({ toolName: "subagent_run", input: dispatch }, context(cwd)), undefined);
|
|
5174
|
+
assert.match(dispatch.task, /## Controller-owned candidate view/);
|
|
5175
|
+
assert.match(dispatch.task, new RegExp(`Frozen candidate tree: \`${candidateTree}\``));
|
|
5176
|
+
assert.match(dispatch.task, /unchanged-0000\.txt/);
|
|
5177
|
+
assert.doesNotMatch(dispatch.task, /unchanged-0001\.txt/);
|
|
5178
|
+
assert.doesNotMatch(dispatch.task, /unchanged-0499\.txt/);
|
|
5179
|
+
assert.ok(Buffer.byteLength(dispatch.task, "utf8") <= 4_096 + "Review the tiny candidate diff in a large repository".length);
|
|
5180
|
+
|
|
5181
|
+
// No live-worktree fallback: a rogue untracked file in the contributor working
|
|
5182
|
+
// directory (invisible to the committedOnly candidate view) cannot reach the dispatch.
|
|
5183
|
+
writeFileSync(join(cwd, "rogue-untracked.ts"), "export const rogue = true;\n");
|
|
5184
|
+
const rogueDispatch = { agent: "review-risk", task: "review after rogue", mode: "task" };
|
|
5185
|
+
assert.equal(await toolCall({ toolName: "subagent_run", input: rogueDispatch }, context(cwd)), undefined);
|
|
5186
|
+
assert.doesNotMatch(rogueDispatch.task, /rogue-untracked\.ts/);
|
|
5187
|
+
assert.match(rogueDispatch.task, new RegExp(`Frozen candidate tree: \`${candidateTree}\``));
|
|
5188
|
+
|
|
5189
|
+
// FINALIZE mutates against the frozen candidate root, not the contributor working directory.
|
|
5190
|
+
const finalize = await controller.execute("large-repo-finalize", { operation: "finalize", lineageId, input: JSON.stringify({}) }, undefined, undefined, context(cwd));
|
|
5191
|
+
assert.equal((finalize.details as { result: { state: string } }).result.state, "approved");
|
|
5192
|
+
assert.equal(finalizes, 1);
|
|
5193
|
+
assert.notEqual(finalizeRoot, cwd);
|
|
5194
|
+
assert.equal(finalizeRoot, frozen.root);
|
|
5195
|
+
|
|
5196
|
+
// Validation delivery gate: stage the frozen candidate tree and authorize delivery.
|
|
5197
|
+
git(cwd, "read-tree", candidateTree);
|
|
5198
|
+
const command = "git commit -m large-repo-delivery";
|
|
5199
|
+
const validated = await controller.execute("large-repo-validate", { operation: "validate", lineageId, idempotencyKey: "large-repo-delivery", command, input: "{}" }, undefined, undefined, context(cwd));
|
|
5200
|
+
assert.ok(validates >= 1);
|
|
5201
|
+
assert.notEqual((validated.details as { authorization?: unknown }).authorization, undefined);
|
|
5202
|
+
assert.equal((validated.details as { result?: { allowed?: boolean } }).result?.allowed, true);
|
|
5203
|
+
// The delivery gate allows the authorized command exactly once; replay is blocked.
|
|
5204
|
+
assert.equal(await toolCall({ toolName: "bash", input: { command } }, interactiveContext(cwd)), undefined);
|
|
5205
|
+
const replay = await toolCall({ toolName: "bash", input: { command } }, context(cwd)) as { block: boolean };
|
|
5206
|
+
assert.equal(replay.block, true);
|
|
5207
|
+
} finally {
|
|
5208
|
+
frozen.cleanup();
|
|
5209
|
+
}
|
|
5210
|
+
});
|
|
5211
|
+
|
|
5212
|
+
// Field defect (fambig, 2026-08-16, dev binary 2.4.0-main): at every
|
|
5213
|
+
// evidence-pending sub-state the provider renders the `review.capture-evidence`
|
|
5214
|
+
// collect slot with the identity native demands — for a correction that is the
|
|
5215
|
+
// FIX-DIFF `--target` identity plus an opaque `--repository-context`, never the
|
|
5216
|
+
// top-level live workspace snapshot identity. Rebinding the capture to
|
|
5217
|
+
// `negotiatedStatus.targetIdentity` failed deterministically with
|
|
5218
|
+
// "verification evidence binding does not match the current authority
|
|
5219
|
+
// revision". Collect satisfaction must execute the slot's rendered submission
|
|
5220
|
+
// tokens verbatim, exactly like captureResult does.
|
|
5221
|
+
const EVIDENCE_SLOT_FIX_TARGET = `sha256:${"e".repeat(64)}`;
|
|
5222
|
+
const EVIDENCE_SLOT_REPOSITORY_CONTEXT = `rctx1_${"f".repeat(64)}`;
|
|
5223
|
+
|
|
5224
|
+
function bindEvidenceSubmissionCollection(status: ReviewStatusV3, fixTargetIdentity = EVIDENCE_SLOT_FIX_TARGET, repositoryContext = EVIDENCE_SLOT_REPOSITORY_CONTEXT): ReviewStatusV3 {
|
|
5225
|
+
const lineageId = status.authority!.lineageId;
|
|
5226
|
+
const revision = status.authority!.revision;
|
|
5227
|
+
const argumentTokens = [
|
|
5228
|
+
`--lineage=${lineageId}`,
|
|
5229
|
+
`--expected-revision=${revision}`,
|
|
5230
|
+
`--target=${fixTargetIdentity}`,
|
|
5231
|
+
`--repository-context=${repositoryContext}`,
|
|
5232
|
+
"--outcome={{outcome}}",
|
|
5233
|
+
"--input={{input}}",
|
|
5234
|
+
];
|
|
5235
|
+
status.nextTransition = {
|
|
5236
|
+
kind: "collect",
|
|
5237
|
+
reasonCode: "correction_repository_verification_required",
|
|
5238
|
+
collect: { inputs: [{
|
|
5239
|
+
name: "evidence",
|
|
5240
|
+
schema: "https://gentle-ai.dev/schema/review/verification-evidence/v1",
|
|
5241
|
+
captureOperation: "review.capture-evidence",
|
|
5242
|
+
arguments: [
|
|
5243
|
+
{ name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
|
|
5244
|
+
{ name: "expected-revision", value: revision, token: `--expected-revision=${revision}` },
|
|
5245
|
+
{ name: "target", value: fixTargetIdentity, token: `--target=${fixTargetIdentity}` },
|
|
5246
|
+
{ name: "repository-context", value: repositoryContext, token: `--repository-context=${repositoryContext}` },
|
|
5247
|
+
],
|
|
5248
|
+
submissionDescriptor: {
|
|
5249
|
+
operationToken: "capture-evidence",
|
|
5250
|
+
argumentTokens,
|
|
5251
|
+
values: [
|
|
5252
|
+
{ slot: "outcome", domain: "verification_outcome", allowedValues: ["passed", "verification_failed", "procedural_tooling_failed"], substitutionLocation: 4 },
|
|
5253
|
+
{ slot: "input", domain: "artifact_path_or_stdin", schema: "https://gentle-ai.dev/schema/review/verification-evidence/v1", substitutionLocation: 5 },
|
|
5254
|
+
],
|
|
5255
|
+
},
|
|
5256
|
+
}] },
|
|
5257
|
+
};
|
|
5258
|
+
delete status.validationRequest;
|
|
5259
|
+
return status;
|
|
5260
|
+
}
|
|
5261
|
+
|
|
5262
|
+
// The record the live binary returns for a slot-bound capture: it binds the
|
|
5263
|
+
// slot's fix-diff target identity, and its paths digest covers the record's
|
|
5264
|
+
// own (fix-diff) paths — NOT the frozen projection digest (captured 2026-08-16,
|
|
5265
|
+
// lineage review-2b6206ed68fb9128: record paths ["calc.go"] against projection
|
|
5266
|
+
// paths ["calc.go", "util.go"]).
|
|
5267
|
+
function capturedSlotEvidence(status: ReviewStatusV3, outcome: "passed" | "verification_failed" | "procedural_tooling_failed", identityDigit: string) {
|
|
5268
|
+
return {
|
|
5269
|
+
...capturedCorrectionEvidence(status, outcome, identityDigit),
|
|
5270
|
+
targetIdentity: EVIDENCE_SLOT_FIX_TARGET,
|
|
5271
|
+
pathsDigest: `sha256:${identityDigit.repeat(64)}`,
|
|
5272
|
+
};
|
|
5273
|
+
}
|
|
5274
|
+
|
|
5275
|
+
test("correction evidence capture executes the collect slot's rendered submission tokens verbatim", async (t) => {
|
|
5276
|
+
for (const [outcome, expectedKind] of [
|
|
5277
|
+
["passed", "run-targeted-validation"],
|
|
5278
|
+
["verification_failed", "recapture-required"],
|
|
5279
|
+
] as const) {
|
|
5280
|
+
await t.test(outcome, async (scenario) => {
|
|
5281
|
+
const cwd = repository(scenario);
|
|
5282
|
+
writeFileSync(join(cwd, "app.ts"), `export const outcome = ${JSON.stringify(outcome)};\n`);
|
|
5283
|
+
const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
|
|
5284
|
+
const beforeCapture = bindEvidenceSubmissionCollection(targetStatusFixture({
|
|
5285
|
+
lineageId: `slot-correction-${outcome.replaceAll("_", "-")}`,
|
|
5286
|
+
authorityState: "correction_required",
|
|
5287
|
+
baseTree: frozen.baseTree,
|
|
5288
|
+
currentCandidateTree: frozen.candidateTree,
|
|
5289
|
+
paths: frozen.paths,
|
|
5290
|
+
}));
|
|
5291
|
+
const expectedTokens = beforeCapture.nextTransition!.collect!.inputs[0]!.submissionDescriptor!.argumentTokens;
|
|
5292
|
+
const afterCapture = targetStatusFixture({
|
|
5293
|
+
lineageId: beforeCapture.authority!.lineageId,
|
|
5294
|
+
authorityState: outcome === "passed" ? "validating" : "correction_required",
|
|
5295
|
+
baseTree: frozen.baseTree,
|
|
5296
|
+
currentCandidateTree: frozen.candidateTree,
|
|
5297
|
+
paths: frozen.paths,
|
|
5298
|
+
});
|
|
5299
|
+
if (outcome === "passed") bindTargetedValidation(afterCapture);
|
|
5300
|
+
else bindEvidenceSubmissionCollection(afterCapture);
|
|
5301
|
+
frozen.cleanup();
|
|
5302
|
+
const misboundCaptures: Array<Record<string, unknown>> = [];
|
|
5303
|
+
const submissions: Array<Record<string, unknown>> = [];
|
|
5304
|
+
let statuses = 0;
|
|
5305
|
+
let finalizes = 0;
|
|
5306
|
+
const { controller } = runtime(fakeNative({
|
|
5307
|
+
targetStatus: async () => {
|
|
5308
|
+
statuses += 1;
|
|
5309
|
+
return statuses === 1 ? beforeCapture : afterCapture;
|
|
5310
|
+
},
|
|
5311
|
+
captureEvidence: async (request) => {
|
|
5312
|
+
misboundCaptures.push(request as unknown as Record<string, unknown>);
|
|
5313
|
+
throw new Error("misbound capture: the collect slot's rendered submission tokens were bypassed");
|
|
5314
|
+
},
|
|
5315
|
+
captureEvidenceSubmission: async (request: Record<string, unknown>) => {
|
|
5316
|
+
submissions.push(request);
|
|
5317
|
+
return capturedSlotEvidence(beforeCapture, outcome, outcome === "passed" ? "1" : "2");
|
|
5318
|
+
},
|
|
5319
|
+
finalize: async () => {
|
|
5320
|
+
finalizes += 1;
|
|
5321
|
+
return { lineageId: beforeCapture.authority!.lineageId, state: "approved", action: "approved", storeRevision: "r-final" };
|
|
5322
|
+
},
|
|
5323
|
+
} as unknown as Partial<NativeReviewCli>), undefined, undefined, undefined, new CandidateViewRegistry());
|
|
5324
|
+
const validation = outcome === "passed" ? {
|
|
5325
|
+
request_hash: "9".repeat(64), correction_ids: [],
|
|
5326
|
+
original_criteria: { passed: true, evidence: ["acceptance passes"] },
|
|
5327
|
+
correction_regression: { passed: true, evidence: ["regression passes"] },
|
|
5328
|
+
fix_caused_findings: [], follow_ups: [],
|
|
5329
|
+
} : undefined;
|
|
5330
|
+
const result = await controller.execute(`slot-correction-${outcome}`, {
|
|
5331
|
+
operation: "finalize",
|
|
5332
|
+
lineageId: beforeCapture.authority!.lineageId,
|
|
5333
|
+
input: JSON.stringify({ final_evidence: `evidence: ${outcome}`, final_verification_outcome: outcome, ...(validation === undefined ? {} : { validation }) }),
|
|
5334
|
+
}, undefined, undefined, context(cwd));
|
|
5335
|
+
const details = result.details as Record<string, unknown>;
|
|
5336
|
+
assert.deepEqual(misboundCaptures, [], "capture-evidence must never rebind to the top-level live workspace identity");
|
|
5337
|
+
assert.equal(submissions.length, 1, "the slot submission must be executed exactly once");
|
|
5338
|
+
const submission = submissions[0]!;
|
|
5339
|
+
assert.deepEqual(submission.argumentTokens, expectedTokens, "the provider-rendered submission tokens must be passed verbatim, in provider order");
|
|
5340
|
+
assert.equal(submission.outcomeSubstitutionLocation, 4);
|
|
5341
|
+
assert.equal(submission.inputSubstitutionLocation, 5);
|
|
5342
|
+
assert.equal(submission.cwd, undefined, "the slot's --repository-context is authoritative; never pass --cwd alongside it");
|
|
5343
|
+
assert.equal(submission.outcome, outcome);
|
|
5344
|
+
assert.equal(submission.evidenceDocument, `evidence: ${outcome}`);
|
|
5345
|
+
assert.equal((details.correction_step as { kind?: string } | undefined)?.kind, expectedKind);
|
|
5346
|
+
assert.equal(finalizes, outcome === "passed" ? 1 : 0);
|
|
5347
|
+
});
|
|
5348
|
+
}
|
|
5349
|
+
});
|
|
5350
|
+
|
|
5351
|
+
test("ordinary final verification at validating captures evidence through the slot's rendered submission tokens", async (t) => {
|
|
5352
|
+
const cwd = repository(t);
|
|
5353
|
+
writeFileSync(join(cwd, "app.ts"), "export const finalSlot = true;\n");
|
|
5354
|
+
const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
|
|
5355
|
+
const beforeCapture = bindEvidenceSubmissionCollection(targetStatusFixture({
|
|
5356
|
+
lineageId: "final-verification-slot",
|
|
5357
|
+
authorityState: "validating",
|
|
5358
|
+
baseTree: frozen.baseTree,
|
|
5359
|
+
currentCandidateTree: frozen.candidateTree,
|
|
5360
|
+
paths: frozen.paths,
|
|
5361
|
+
}));
|
|
5362
|
+
const expectedTokens = beforeCapture.nextTransition!.collect!.inputs[0]!.submissionDescriptor!.argumentTokens;
|
|
5363
|
+
const afterCapture = bindFinalVerificationTransition(targetStatusFixture({
|
|
5364
|
+
lineageId: "final-verification-slot",
|
|
5365
|
+
authorityState: "validating",
|
|
5366
|
+
baseTree: frozen.baseTree,
|
|
5367
|
+
currentCandidateTree: frozen.candidateTree,
|
|
5368
|
+
paths: frozen.paths,
|
|
5369
|
+
}), "passed");
|
|
5370
|
+
frozen.cleanup();
|
|
5371
|
+
const misboundCaptures: Array<Record<string, unknown>> = [];
|
|
5372
|
+
const submissions: Array<Record<string, unknown>> = [];
|
|
5373
|
+
const transitions: Array<readonly string[]> = [];
|
|
5374
|
+
let statuses = 0;
|
|
5375
|
+
const { controller } = runtime(fakeNative({
|
|
5376
|
+
targetStatus: async () => {
|
|
5377
|
+
statuses += 1;
|
|
5378
|
+
return statuses === 1 ? beforeCapture : afterCapture;
|
|
5379
|
+
},
|
|
5380
|
+
captureEvidence: async (request) => {
|
|
5381
|
+
misboundCaptures.push(request as unknown as Record<string, unknown>);
|
|
5382
|
+
throw new Error("misbound capture: the collect slot's rendered submission tokens were bypassed");
|
|
5383
|
+
},
|
|
5384
|
+
captureEvidenceSubmission: async (request: Record<string, unknown>) => {
|
|
5385
|
+
submissions.push(request);
|
|
5386
|
+
return capturedSlotEvidence(beforeCapture, "passed", "3");
|
|
5387
|
+
},
|
|
5388
|
+
finalizeTransition: async (request) => {
|
|
5389
|
+
transitions.push(request.argumentTokens);
|
|
5390
|
+
return { lineageId: "final-verification-slot", state: "approved", action: "terminal", storeRevision: "r2", receiptPath: "/opaque/receipt" };
|
|
5391
|
+
},
|
|
5392
|
+
} as unknown as Partial<NativeReviewCli>), undefined, undefined, undefined, new CandidateViewRegistry());
|
|
5393
|
+
const result = await controller.execute("final-verification-slot", {
|
|
5394
|
+
operation: "finalize",
|
|
5395
|
+
lineageId: "final-verification-slot",
|
|
5396
|
+
input: JSON.stringify({ final_evidence: "verification run: passed", final_verification_outcome: "passed" }),
|
|
5397
|
+
}, undefined, undefined, context(cwd));
|
|
5398
|
+
const details = result.details as Record<string, unknown>;
|
|
5399
|
+
assert.deepEqual(misboundCaptures, [], "capture-evidence must never rebind to the top-level live workspace identity");
|
|
5400
|
+
assert.equal(submissions.length, 1);
|
|
5401
|
+
assert.deepEqual(submissions[0]!.argumentTokens, expectedTokens);
|
|
5402
|
+
assert.equal(submissions[0]!.cwd, undefined, "the slot's --repository-context is authoritative; never pass --cwd alongside it");
|
|
5403
|
+
assert.deepEqual(transitions, [["--lineage=final-verification-slot", "--captured-evidence=true"]]);
|
|
5404
|
+
assert.equal((details.result as { state?: string } | undefined)?.state, "approved");
|
|
5405
|
+
});
|
|
5406
|
+
|
|
5407
|
+
// Field defect amplifier (fambig, 2026-08-16): an envelope-less mutating
|
|
5408
|
+
// failure is stamped mutationOutcome "unknown", and the reconciler KEPT
|
|
5409
|
+
// "unknown" even after fresh STATUS proved the authority revision identical to
|
|
5410
|
+
// the pre-operation revision — reporting a replay prohibition for an operation
|
|
5411
|
+
// that provably never mutated. A proven non-mutation must be reported as
|
|
5412
|
+
// mutation_outcome none with the proof named; a genuinely ambiguous result
|
|
5413
|
+
// (revision moved, or STATUS unavailable) stays fail-closed unknown.
|
|
5414
|
+
test("finalize reconciliation downgrades an envelope-less unknown to proven non-mutation when the authority revision is unchanged", async (t) => {
|
|
5415
|
+
const cwd = repository(t);
|
|
5416
|
+
let statuses = 0;
|
|
5417
|
+
const { controller } = runtime(fakeNative({
|
|
5418
|
+
targetStatus: async () => {
|
|
5419
|
+
statuses += 1;
|
|
5420
|
+
return targetStatusFixture({ lineageId: "unchanged-revision" });
|
|
5421
|
+
},
|
|
5422
|
+
finalize: async () => {
|
|
5423
|
+
throw Object.assign(new Error("stderr-only native failure without a typed envelope"), { mutationOutcome: "unknown", nextAction: "review.status" });
|
|
5424
|
+
},
|
|
5425
|
+
}));
|
|
5426
|
+
const details = (await controller.execute("proven-none", { operation: "finalize", lineageId: "unchanged-revision", input: "{}" }, undefined, undefined, context(cwd))).details as Record<string, unknown>;
|
|
5427
|
+
assert.equal(details.outcome, "native-mutation-status-reconciled");
|
|
5428
|
+
assert.equal(details.mutation_performed, false, "a reconciled unchanged revision proves the operation did not mutate");
|
|
5429
|
+
assert.equal(details.mutation_outcome, "none");
|
|
5430
|
+
assert.match(String(details.mutation_outcome_reason), /revision unchanged/i, "the downgrade must name its proof");
|
|
5431
|
+
assert.equal("replayability" in details, false, "a proven non-mutation must not claim replay prohibition");
|
|
5432
|
+
assert.equal(details.next_action, "finalize");
|
|
5433
|
+
assert.equal(statuses, 2);
|
|
5434
|
+
});
|
|
5435
|
+
|
|
5436
|
+
test("finalize reconciliation preserves fail-closed unknown when the reconciled authority revision moved", async (t) => {
|
|
5437
|
+
const cwd = repository(t);
|
|
5438
|
+
const moved = targetStatusFixture({ lineageId: "moved-revision" });
|
|
5439
|
+
moved.authority!.revision = `sha256:${"b".repeat(64)}`;
|
|
5440
|
+
let statuses = 0;
|
|
5441
|
+
const { controller } = runtime(fakeNative({
|
|
5442
|
+
targetStatus: async () => {
|
|
5443
|
+
statuses += 1;
|
|
5444
|
+
return statuses === 1 ? targetStatusFixture({ lineageId: "moved-revision" }) : moved;
|
|
5445
|
+
},
|
|
5446
|
+
finalize: async () => {
|
|
5447
|
+
throw Object.assign(new Error("stderr-only native failure without a typed envelope"), { mutationOutcome: "unknown", nextAction: "review.status" });
|
|
5448
|
+
},
|
|
5449
|
+
}));
|
|
5450
|
+
const details = (await controller.execute("kept-unknown", { operation: "finalize", lineageId: "moved-revision", input: "{}" }, undefined, undefined, context(cwd))).details as Record<string, unknown>;
|
|
5451
|
+
assert.equal(details.outcome, "native-mutation-status-reconciled");
|
|
5452
|
+
assert.equal(details.mutation_outcome, "unknown", "a moved revision is genuinely ambiguous and must stay fail-closed");
|
|
5453
|
+
assert.equal(details.replayability, "not_replayable");
|
|
5454
|
+
});
|
|
5455
|
+
|
|
5456
|
+
// Same misbinding class, remaining finalize-form slots (live smoke,
|
|
5457
|
+
// 2026-08-16, dev binary 2.4.0-main): the correction PLAN and TARGETED
|
|
5458
|
+
// VALIDATION collect slots render `finalize` submission descriptors whose
|
|
5459
|
+
// tokens carry --contract/--lineage/--expected-revision/--target/
|
|
5460
|
+
// --request-hash/--repository-context plus one {{value}} slot. The legacy
|
|
5461
|
+
// reconstructed `finalize --correction-lines/--validation` argv fails on the
|
|
5462
|
+
// live emitter with "reconcile compact predecessor effects: repository
|
|
5463
|
+
// context effect binding or payload does not match committed intent". Collect
|
|
5464
|
+
// satisfaction must execute the rendered tokens verbatim.
|
|
5465
|
+
function bindPlanSubmissionCollection(status: ReviewStatusV3, repositoryContext = EVIDENCE_SLOT_REPOSITORY_CONTEXT): ReviewStatusV3 {
|
|
5466
|
+
const lineageId = status.authority!.lineageId;
|
|
5467
|
+
const revision = status.authority!.revision;
|
|
5468
|
+
status.nextTransition = {
|
|
5469
|
+
kind: "collect",
|
|
5470
|
+
reasonCode: "correction_plan_required",
|
|
5471
|
+
collect: { inputs: [{
|
|
5472
|
+
name: "correction_lines",
|
|
5473
|
+
schema: "gentle-ai.review-correction-plan/v1",
|
|
5474
|
+
captureOperation: "external.plan_correction",
|
|
5475
|
+
arguments: [
|
|
5476
|
+
{ name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
|
|
5477
|
+
{ name: "expected-revision", value: revision, token: `--expected-revision=${revision}` },
|
|
5478
|
+
{ name: "target", value: status.targetIdentity, token: `--target=${status.targetIdentity}` },
|
|
5479
|
+
],
|
|
5480
|
+
submissionDescriptor: {
|
|
5481
|
+
operationToken: "finalize",
|
|
5482
|
+
argumentTokens: [
|
|
5483
|
+
"--contract=gentle-ai.review-integration/v2",
|
|
5484
|
+
`--lineage=${lineageId}`,
|
|
5485
|
+
`--expected-revision=${revision}`,
|
|
5486
|
+
`--target=${status.targetIdentity}`,
|
|
5487
|
+
`--request-hash=sha256:${"c".repeat(64)}`,
|
|
5488
|
+
`--repository-context=${repositoryContext}`,
|
|
5489
|
+
"--correction-lines={{value}}",
|
|
5490
|
+
],
|
|
5491
|
+
value: { slot: "correction_lines", domain: "positive_correction_lines", minimum: 1, maximum: 9, substitutionLocation: 6 },
|
|
5492
|
+
},
|
|
5493
|
+
}] },
|
|
5494
|
+
};
|
|
5495
|
+
delete status.validationRequest;
|
|
5496
|
+
return status;
|
|
5497
|
+
}
|
|
5498
|
+
|
|
5499
|
+
test("correction plan forecast executes the collect slot's rendered finalize submission tokens verbatim", async (t) => {
|
|
5500
|
+
const cwd = repository(t);
|
|
5501
|
+
writeFileSync(join(cwd, "app.ts"), "export const plan = true;\n");
|
|
5502
|
+
const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
|
|
5503
|
+
const status = bindPlanSubmissionCollection(targetStatusFixture({
|
|
5504
|
+
lineageId: "plan-submission",
|
|
5505
|
+
authorityState: "correction_required",
|
|
5506
|
+
baseTree: frozen.baseTree,
|
|
5507
|
+
currentCandidateTree: frozen.candidateTree,
|
|
5508
|
+
paths: frozen.paths,
|
|
5509
|
+
}));
|
|
5510
|
+
const expectedTokens = status.nextTransition!.collect!.inputs[0]!.submissionDescriptor!.argumentTokens;
|
|
5511
|
+
frozen.cleanup();
|
|
5512
|
+
const legacyFinalizes: Array<Record<string, unknown>> = [];
|
|
5513
|
+
const submissions: Array<Record<string, unknown>> = [];
|
|
5514
|
+
const { controller } = runtime(fakeNative({
|
|
5515
|
+
targetStatus: async () => status,
|
|
5516
|
+
finalize: async (request) => {
|
|
5517
|
+
legacyFinalizes.push(request as unknown as Record<string, unknown>);
|
|
5518
|
+
throw new Error("misbound plan: the collect slot's rendered finalize submission tokens were bypassed");
|
|
5519
|
+
},
|
|
5520
|
+
finalizeSubmission: async (request: Record<string, unknown>) => {
|
|
5521
|
+
submissions.push(request);
|
|
5522
|
+
return { lineageId: "plan-submission", state: "correction_required", action: "continue the current review state", storeRevision: "r-plan" };
|
|
5523
|
+
},
|
|
5524
|
+
} as unknown as Partial<NativeReviewCli>), undefined, undefined, undefined, new CandidateViewRegistry());
|
|
5525
|
+
const planned = await controller.execute("plan-submission", {
|
|
5526
|
+
operation: "finalize",
|
|
5527
|
+
lineageId: "plan-submission",
|
|
5528
|
+
input: JSON.stringify({ correction_line_forecast: 2 }),
|
|
5529
|
+
}, undefined, undefined, context(cwd));
|
|
5530
|
+
const details = planned.details as Record<string, unknown>;
|
|
5531
|
+
assert.deepEqual(legacyFinalizes, [], "the legacy reconstructed --correction-lines argv must never run when the slot renders submission tokens");
|
|
5532
|
+
assert.equal(submissions.length, 1);
|
|
5533
|
+
assert.deepEqual(submissions[0]!.argumentTokens, expectedTokens, "the provider-rendered submission tokens must be passed verbatim, in provider order");
|
|
5534
|
+
assert.equal(submissions[0]!.valueSubstitutionLocation, 6);
|
|
5535
|
+
assert.equal(submissions[0]!.valueLiteral, "2");
|
|
5536
|
+
assert.equal((details.result as { state?: string } | undefined)?.state, "correction_required");
|
|
5537
|
+
|
|
5538
|
+
// An out-of-bounds forecast fails closed before any native launch.
|
|
5539
|
+
const outOfBounds = await controller.execute("plan-submission-oob", {
|
|
5540
|
+
operation: "finalize",
|
|
5541
|
+
lineageId: "plan-submission",
|
|
5542
|
+
input: JSON.stringify({ correction_line_forecast: 99 }),
|
|
5543
|
+
}, undefined, undefined, context(cwd));
|
|
5544
|
+
assert.equal((outOfBounds.details as { outcome?: string }).outcome, "native-operation-failed");
|
|
5545
|
+
assert.match(JSON.stringify(outOfBounds.details), /correction-forecast-out-of-bounds/);
|
|
5546
|
+
assert.equal(submissions.length, 1);
|
|
5547
|
+
assert.deepEqual(legacyFinalizes, []);
|
|
5548
|
+
execFileSync("chmod", ["-R", "u+w", cwd]);
|
|
5549
|
+
});
|
|
5550
|
+
|
|
5551
|
+
function bindTargetedValidationSubmission(status: ReviewStatusV3): ReviewStatusV3 {
|
|
5552
|
+
bindTargetedValidation(status);
|
|
5553
|
+
const input = status.nextTransition!.collect!.inputs[0]! as { submissionDescriptor?: unknown };
|
|
5554
|
+
const lineageId = status.authority!.lineageId;
|
|
5555
|
+
const revision = status.authority!.revision;
|
|
5556
|
+
input.submissionDescriptor = {
|
|
5557
|
+
operationToken: "finalize",
|
|
5558
|
+
argumentTokens: [
|
|
5559
|
+
"--contract=gentle-ai.review-integration/v2",
|
|
5560
|
+
`--lineage=${lineageId}`,
|
|
5561
|
+
`--expected-revision=${revision}`,
|
|
5562
|
+
`--target=${status.targetIdentity}`,
|
|
5563
|
+
`--request-hash=${status.validationRequest!.requestHash}`,
|
|
5564
|
+
`--repository-context=${EVIDENCE_SLOT_REPOSITORY_CONTEXT}`,
|
|
5565
|
+
"--validation={{value}}",
|
|
5566
|
+
],
|
|
5567
|
+
value: { slot: "validation", domain: "artifact_path_or_stdin", substitutionLocation: 6 },
|
|
5568
|
+
};
|
|
5569
|
+
return status;
|
|
5570
|
+
}
|
|
5571
|
+
|
|
5572
|
+
test("targeted validation executes the collect slot's rendered finalize submission tokens verbatim", async (t) => {
|
|
5573
|
+
const cwd = repository(t);
|
|
5574
|
+
writeFileSync(join(cwd, "app.ts"), "export const validated = true;\n");
|
|
5575
|
+
const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
|
|
5576
|
+
const beforeCapture = bindEvidenceSubmissionCollection(targetStatusFixture({
|
|
5577
|
+
lineageId: "validation-submission",
|
|
5578
|
+
authorityState: "correction_required",
|
|
5579
|
+
baseTree: frozen.baseTree,
|
|
5580
|
+
currentCandidateTree: frozen.candidateTree,
|
|
5581
|
+
paths: frozen.paths,
|
|
5582
|
+
}));
|
|
5583
|
+
const afterCapture = bindTargetedValidationSubmission(targetStatusFixture({
|
|
5584
|
+
lineageId: "validation-submission",
|
|
5585
|
+
authorityState: "validating",
|
|
5586
|
+
baseTree: frozen.baseTree,
|
|
5587
|
+
currentCandidateTree: frozen.candidateTree,
|
|
5588
|
+
paths: frozen.paths,
|
|
5589
|
+
}));
|
|
5590
|
+
const expectedTokens = (afterCapture.nextTransition!.collect!.inputs[0]! as { submissionDescriptor: { argumentTokens: readonly string[] } }).submissionDescriptor.argumentTokens;
|
|
5591
|
+
frozen.cleanup();
|
|
5592
|
+
const legacyFinalizes: Array<Record<string, unknown>> = [];
|
|
5593
|
+
const submissions: Array<Record<string, unknown>> = [];
|
|
5594
|
+
let statuses = 0;
|
|
5595
|
+
const { controller } = runtime(fakeNative({
|
|
5596
|
+
targetStatus: async () => {
|
|
5597
|
+
statuses += 1;
|
|
5598
|
+
return statuses === 1 ? beforeCapture : afterCapture;
|
|
5599
|
+
},
|
|
5600
|
+
captureEvidenceSubmission: async () => capturedSlotEvidence(beforeCapture, "passed", "7"),
|
|
5601
|
+
finalize: async (request) => {
|
|
5602
|
+
legacyFinalizes.push(request as unknown as Record<string, unknown>);
|
|
5603
|
+
throw new Error("misbound validation: the collect slot's rendered finalize submission tokens were bypassed");
|
|
5604
|
+
},
|
|
5605
|
+
finalizeSubmission: async (request: Record<string, unknown>) => {
|
|
5606
|
+
submissions.push(request);
|
|
5607
|
+
return { lineageId: "validation-submission", state: "approved", action: "approved", storeRevision: "r-approved" };
|
|
5608
|
+
},
|
|
5609
|
+
} as unknown as Partial<NativeReviewCli>), undefined, undefined, undefined, new CandidateViewRegistry());
|
|
5610
|
+
const completed = await controller.execute("validation-submission", {
|
|
5611
|
+
operation: "finalize",
|
|
5612
|
+
lineageId: "validation-submission",
|
|
5613
|
+
input: JSON.stringify({
|
|
5614
|
+
final_evidence: "evidence: passed",
|
|
5615
|
+
final_verification_outcome: "passed",
|
|
5616
|
+
validation: {
|
|
5617
|
+
request_hash: "9".repeat(64), correction_ids: [],
|
|
5618
|
+
original_criteria: { passed: true, evidence: ["acceptance passes"] },
|
|
5619
|
+
correction_regression: { passed: true, evidence: ["regression passes"] },
|
|
5620
|
+
fix_caused_findings: [], follow_ups: [],
|
|
5621
|
+
},
|
|
5622
|
+
}),
|
|
5623
|
+
}, undefined, undefined, context(cwd));
|
|
5624
|
+
const details = completed.details as Record<string, unknown>;
|
|
5625
|
+
assert.deepEqual(legacyFinalizes, [], "the legacy reconstructed --validation argv must never run when the slot renders submission tokens");
|
|
5626
|
+
assert.equal(submissions.length, 1);
|
|
5627
|
+
assert.deepEqual(submissions[0]!.argumentTokens, expectedTokens, "the provider-rendered submission tokens must be passed verbatim, in provider order");
|
|
5628
|
+
assert.equal(submissions[0]!.valueSubstitutionLocation, 6);
|
|
5629
|
+
const staged = JSON.parse(String(submissions[0]!.valueDocument)) as Record<string, unknown>;
|
|
5630
|
+
assert.equal(staged.targeted_validation_request_hash, afterCapture.validationRequest!.requestHash);
|
|
5631
|
+
assert.equal(staged.correction_target_identity, afterCapture.validationRequest!.correctionTargetIdentity);
|
|
5632
|
+
assert.deepEqual(staged.original_criteria, { passed: true, evidence: ["acceptance passes"] });
|
|
5633
|
+
assert.equal((details.result as { state?: string } | undefined)?.state, "approved");
|
|
5634
|
+
});
|
|
5635
|
+
|
|
5636
|
+
// Live smoke root cause (2026-08-16, dev binary 2.4.0-main): status/v5 mints
|
|
5637
|
+
// the opaque --repository-context handle BOUND TO THE STATUS QUERY ROOT, and
|
|
5638
|
+
// every rendered payload embeds it. The finalize lane queries negotiated
|
|
5639
|
+
// STATUS from a frozen candidate-view root, so every rendered submission it
|
|
5640
|
+
// executed carried a context bound to the wrong root and failed the live
|
|
5641
|
+
// emitter's committed-intent reconciliation ("repository context effect
|
|
5642
|
+
// binding or payload does not match committed intent"). On v5 the lane must
|
|
5643
|
+
// rebind its negotiated STATUS to the workspace root before executing any
|
|
5644
|
+
// rendered payload; pinned pre-v5 emitters keep the frozen-view status.
|
|
5645
|
+
test("status/v5 finalize lane rebinds negotiated status to the workspace root before executing rendered payloads", async (t) => {
|
|
5646
|
+
const cwd = repository(t);
|
|
5647
|
+
writeFileSync(join(cwd, "app.ts"), "export const rebind = true;\n");
|
|
5648
|
+
const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
|
|
5649
|
+
const viewContext = `rctx1_${"a".repeat(64)}`;
|
|
5650
|
+
const workspaceContext = `rctx1_${"b".repeat(64)}`;
|
|
5651
|
+
const makeBefore = (repositoryContext: string) => {
|
|
5652
|
+
const status = bindEvidenceSubmissionCollection(targetStatusFixture({
|
|
5653
|
+
lineageId: "v5-rebind",
|
|
5654
|
+
authorityState: "correction_required",
|
|
5655
|
+
baseTree: frozen.baseTree,
|
|
5656
|
+
currentCandidateTree: frozen.candidateTree,
|
|
5657
|
+
paths: frozen.paths,
|
|
5658
|
+
}), EVIDENCE_SLOT_FIX_TARGET, repositoryContext);
|
|
5659
|
+
(status.raw as Record<string, unknown>).schema = "gentle-ai.review-integration.status/v5";
|
|
5660
|
+
return status;
|
|
5661
|
+
};
|
|
5662
|
+
const viewBefore = makeBefore(viewContext);
|
|
5663
|
+
const workspaceBefore = makeBefore(workspaceContext);
|
|
5664
|
+
const workspaceAfter = bindTargetedValidationSubmission(targetStatusFixture({
|
|
5665
|
+
lineageId: "v5-rebind",
|
|
5666
|
+
authorityState: "validating",
|
|
5667
|
+
baseTree: frozen.baseTree,
|
|
5668
|
+
currentCandidateTree: frozen.candidateTree,
|
|
5669
|
+
paths: frozen.paths,
|
|
5670
|
+
}));
|
|
5671
|
+
(workspaceAfter.raw as Record<string, unknown>).schema = "gentle-ai.review-integration.status/v5";
|
|
5672
|
+
// Live post-evidence shape (2026-08-16): the validation request binds the
|
|
5673
|
+
// FROZEN authority target identity while the top-level target identity is
|
|
5674
|
+
// the live workspace snapshot (which now contains the fix).
|
|
5675
|
+
workspaceAfter.authorityTargetIdentity = workspaceAfter.validationRequest!.targetIdentity;
|
|
5676
|
+
workspaceAfter.targetIdentity = `sha256:${"d".repeat(64)}`;
|
|
5677
|
+
const expectedEvidenceTokens = workspaceBefore.nextTransition!.collect!.inputs[0]!.submissionDescriptor!.argumentTokens;
|
|
5678
|
+
const expectedValidationTokens = (workspaceAfter.nextTransition!.collect!.inputs[0]! as { submissionDescriptor: { argumentTokens: readonly string[] } }).submissionDescriptor.argumentTokens;
|
|
5679
|
+
frozen.cleanup();
|
|
5680
|
+
const statusRoots: string[] = [];
|
|
5681
|
+
const evidenceSubmissions: Array<Record<string, unknown>> = [];
|
|
5682
|
+
const finalizeSubmissions: Array<Record<string, unknown>> = [];
|
|
5683
|
+
let workspaceStatuses = 0;
|
|
5684
|
+
const { controller } = runtime(fakeNative({
|
|
5685
|
+
targetStatus: async (request) => {
|
|
5686
|
+
statusRoots.push(request.cwd);
|
|
5687
|
+
if (request.cwd !== cwd) return viewBefore;
|
|
5688
|
+
workspaceStatuses += 1;
|
|
5689
|
+
return workspaceStatuses === 1 ? workspaceBefore : workspaceAfter;
|
|
5690
|
+
},
|
|
5691
|
+
captureEvidenceSubmission: async (request: Record<string, unknown>) => {
|
|
5692
|
+
evidenceSubmissions.push(request);
|
|
5693
|
+
return capturedSlotEvidence(workspaceBefore, "passed", "8");
|
|
5694
|
+
},
|
|
5695
|
+
finalizeSubmission: async (request: Record<string, unknown>) => {
|
|
5696
|
+
finalizeSubmissions.push(request);
|
|
5697
|
+
return { lineageId: "v5-rebind", state: "approved", action: "approved", storeRevision: "r-approved" };
|
|
5698
|
+
},
|
|
5699
|
+
} as unknown as Partial<NativeReviewCli>), undefined, undefined, undefined, new CandidateViewRegistry());
|
|
5700
|
+
const completed = await controller.execute("v5-rebind", {
|
|
5701
|
+
operation: "finalize",
|
|
5702
|
+
lineageId: "v5-rebind",
|
|
5703
|
+
input: JSON.stringify({
|
|
5704
|
+
final_evidence: "evidence: passed",
|
|
5705
|
+
final_verification_outcome: "passed",
|
|
5706
|
+
validation: {
|
|
5707
|
+
request_hash: "9".repeat(64), correction_ids: [],
|
|
5708
|
+
original_criteria: { passed: true, evidence: ["acceptance passes"] },
|
|
5709
|
+
correction_regression: { passed: true, evidence: ["regression passes"] },
|
|
5710
|
+
fix_caused_findings: [], follow_ups: [],
|
|
5711
|
+
},
|
|
5712
|
+
}),
|
|
5713
|
+
}, undefined, undefined, context(cwd));
|
|
5714
|
+
const details = completed.details as Record<string, unknown>;
|
|
5715
|
+
assert.notEqual(statusRoots[0], cwd, "the first status query still freezes through the candidate view root");
|
|
5716
|
+
assert.equal(statusRoots[1], cwd, "a v5 status must be rebound to the workspace root before any rendered payload executes");
|
|
5717
|
+
assert.equal(evidenceSubmissions.length, 1);
|
|
5718
|
+
assert.deepEqual(evidenceSubmissions[0]!.argumentTokens, expectedEvidenceTokens, "the evidence submission must carry the workspace-root-minted tokens");
|
|
5719
|
+
assert.equal(finalizeSubmissions.length, 1);
|
|
5720
|
+
assert.deepEqual(finalizeSubmissions[0]!.argumentTokens, expectedValidationTokens, "the validation submission must carry the workspace-root-minted tokens");
|
|
5721
|
+
assert.equal(statusRoots.filter((root) => root === cwd).length, 2, "post-evidence status must also be workspace-bound");
|
|
5722
|
+
assert.equal((details.result as { state?: string } | undefined)?.state, "approved");
|
|
5723
|
+
});
|