gentle-pi 2.1.2 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -79
- package/assets/agents/gentle-ai-worker.md +7 -3
- package/assets/agents/jd-fix-agent.md +1 -1
- package/assets/agents/jd-judge-a.md +3 -1
- package/assets/agents/jd-judge-b.md +3 -1
- package/assets/agents/review-readability.md +4 -3
- package/assets/agents/review-reliability.md +4 -3
- package/assets/agents/review-resilience.md +4 -3
- package/assets/agents/review-risk.md +4 -3
- package/assets/agents/sdd-apply.md +11 -4
- package/assets/agents/sdd-archive.md +6 -1
- package/assets/agents/sdd-design.md +6 -1
- package/assets/agents/sdd-explore.md +6 -2
- package/assets/agents/sdd-init.md +10 -2
- package/assets/agents/sdd-onboard.md +6 -1
- package/assets/agents/sdd-proposal.md +8 -1
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +10 -5
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +8 -4
- package/assets/agents/sdd-verify.md +27 -2
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +2 -2
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +2 -2
- package/assets/orchestrator-delegation.md +145 -160
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +25 -48
- package/assets/sdd-orchestrator-workflow.md +163 -25
- package/assets/support/sdd-status-contract.md +24 -6
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
- package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
- package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
- package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
- package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
- package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
- package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
- package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
- package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
- package/docs/native-authority-architecture.md +14 -16
- package/docs/review-integration.md +27 -353
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +2710 -3211
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +21 -12
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +676 -781
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +836 -118
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +578 -0
- package/lib/review-integration-v2.ts +1254 -221
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +230 -67
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -79
- package/runtime/gentle-ai-binary.mjs +174 -3
- package/runtime/native-review-cli.mjs +610 -715
- package/runtime/review-integration-v2.mjs +1223 -190
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +85 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +20 -9
- package/scripts/verify-package-files.mjs +119 -43
- package/skills/_shared/review-ledger-contract.md +11 -19
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +9 -5
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/background-subagents.test.ts +771 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +16 -0
- package/tests/delegated-key-learnings-contract.test.ts +240 -0
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
- package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
- package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
- package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
- package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
- package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
- package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +159 -0
- package/tests/gentle-ai-installer.test.ts +138 -48
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +601 -0
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +75 -2
- package/tests/native-review-cli.test.ts +588 -908
- package/tests/native-review-consent.test.ts +283 -38
- package/tests/native-review-parity-runtime.test.ts +108 -350
- package/tests/native-review-parity.test.ts +684 -732
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +158 -89
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +128 -100
- package/tests/provider-contract-bundle.test.ts +385 -0
- package/tests/provider-contract-mirror.test.ts +206 -0
- package/tests/provider-defect-handoff.test.ts +252 -0
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +898 -12
- package/tests/review-compact-contract.test.ts +29 -122
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +366 -857
- package/tests/review-controller-native-routing.test.ts +1253 -4199
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +236 -70
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +134 -0
- package/tests/review-dispatch-hydration-gap.test.ts +145 -0
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +352 -0
- package/tests/review-host-relay.test.ts +754 -0
- package/tests/review-integration-v2-forward.test.ts +654 -0
- package/tests/review-integration-v2.test.ts +317 -129
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +106 -60
- package/tests/review-recovered-lineage-routing.test.ts +199 -0
- package/tests/review-relay-transport-agent.test.ts +322 -0
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +573 -180
- package/tests/sdd-agent-tools.test.ts +53 -32
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +6 -8
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- package/tests/writer-edit-surface-scope.test.ts +230 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- package/lib/git-commit-transaction.ts +0 -801
- package/lib/native-review-remediation.ts +0 -49
- package/lib/review-compact.ts +0 -947
- package/lib/review-refuter-adapter.ts +0 -129
- package/lib/review-runtime-contract.ts +0 -68
- package/prompts/gcl.md +0 -54
- package/prompts/gis.md +0 -25
- package/prompts/gpr.md +0 -41
- package/prompts/gwr.md +0 -31
- package/runtime/git-commit-transaction.mjs +0 -802
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
- package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
- package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
- package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -302
- package/tests/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
|
@@ -1,961 +1,470 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import {
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
4
3
|
import { join } from "node:path";
|
|
5
4
|
import test from "node:test";
|
|
6
5
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
6
|
import { __testing, createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
8
7
|
import {
|
|
9
|
-
|
|
8
|
+
NATIVE_REVIEW_LEGACY_ALIAS_REPAIR,
|
|
9
|
+
NATIVE_REVIEW_LEGACY_QUARANTINE,
|
|
10
10
|
NativeReviewCliError,
|
|
11
|
-
NativeReviewCliV214 as NativeReviewCliV214Production,
|
|
12
11
|
NativeReviewCliV216,
|
|
13
|
-
|
|
12
|
+
nativeReviewAbandonAuthorization,
|
|
14
13
|
nativeReviewLegacyAliasRepairAuthorization,
|
|
14
|
+
nativeReviewLegacyQuarantineAuthorization,
|
|
15
|
+
nativeReviewReconcileAuthorization,
|
|
15
16
|
type ExecFileAdapter,
|
|
16
|
-
type NativeReviewCli,
|
|
17
17
|
} from "../lib/native-review-cli.ts";
|
|
18
|
-
import { GENTLE_AI_VERSION } from "../lib/gentle-ai-binary.ts";
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
const v2Fixture = <T = unknown>(name: string): T => JSON.parse(readFileSync(join(v2FixtureRoot, name), "utf8")) as T;
|
|
19
|
+
interface QueuedResult { stdout: string; exitCode?: number; }
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface QueuedResult { stdout: string; stderr?: string; exitCode?: number; }
|
|
33
|
-
|
|
34
|
-
function queuedAdapter(results: QueuedResult[]): { adapter: ExecFileAdapter; calls: Array<{ file: string; arguments: readonly string[]; cwd: string }> } {
|
|
35
|
-
const calls: Array<{ file: string; arguments: readonly string[]; cwd: string }> = [];
|
|
21
|
+
function queuedAdapter(results: readonly QueuedResult[]): {
|
|
22
|
+
adapter: ExecFileAdapter;
|
|
23
|
+
calls: Array<{ arguments: readonly string[]; cwd: string }>;
|
|
24
|
+
} {
|
|
25
|
+
const queue = [...results];
|
|
26
|
+
const calls: Array<{ arguments: readonly string[]; cwd: string }> = [];
|
|
36
27
|
return {
|
|
37
28
|
calls,
|
|
38
29
|
adapter: async (request) => {
|
|
39
|
-
calls.push({
|
|
40
|
-
const result =
|
|
41
|
-
if (
|
|
42
|
-
return { stdout: result.stdout, stderr:
|
|
30
|
+
calls.push({ arguments: request.arguments, cwd: request.cwd });
|
|
31
|
+
const result = queue.shift();
|
|
32
|
+
if (result === undefined) throw new Error("unexpected native invocation");
|
|
33
|
+
return { stdout: result.stdout, stderr: "", exitCode: result.exitCode ?? 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
43
34
|
},
|
|
44
35
|
};
|
|
45
36
|
}
|
|
46
37
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const VERSION_220 = { stdout: "gentle-ai 2.2.0\n" };
|
|
50
|
-
const RECLAIM_RECORD = { schema: "gentle-ai.review-reclaim-audit/v1", lineage: "stuck-lineage", actor: "maintainer", reason: "incomplete entry" };
|
|
51
|
-
const RECOVER_RECORD = { schema: "gentle-ai.review-recovery/v1", predecessor_lineage: "broken", successor_lineage: "successor" };
|
|
52
|
-
const RECONCILE_RECORD = { schema: "gentle-ai.review-reconcile-audit/v1", predecessor_lineage: "predecessor", successor_lineage: "successor", outcome: "quarantined" };
|
|
53
|
-
const RECONCILE_RESULT = { operation: "review/reconcile-authority", record: RECONCILE_RECORD };
|
|
54
|
-
const ABANDON_RECORD = { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "pristine", status: "committed" };
|
|
55
|
-
const LEGACY_QUARANTINE_RECORD = { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "legacy", status: "committed" };
|
|
56
|
-
const LEGACY_FREEZE_DIAGNOSTIC = "historical findings freeze changed unrelated transaction state";
|
|
57
|
-
const LEGACY_FREEZE_DISPOSITION = "quarantine-malformed-freeze-event";
|
|
58
|
-
const COMBINED_RECONCILE_ANOMALIES = "unchanged_target,malformed_recovery_authorization";
|
|
59
|
-
const LEGACY_ALIAS_DIAGNOSTIC = "unsupported historical v1 operation alias";
|
|
60
|
-
const LEGACY_ALIAS_DISPOSITION = "quarantine-approved-historical-alias";
|
|
61
|
-
const LEGACY_ALIAS_RECORD = { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "legacy-alias", status: "committed" };
|
|
62
|
-
const LEGACY_ALIAS_AUTHORIZATION = [
|
|
63
|
-
"gentle-ai.review-legacy-alias-repair-authorization/v1",
|
|
64
|
-
"repository=/repo",
|
|
65
|
-
"lineage=legacy-alias",
|
|
66
|
-
`revision=sha256:${"c".repeat(64)}`,
|
|
67
|
-
`diagnostic=${LEGACY_ALIAS_DIAGNOSTIC}`,
|
|
68
|
-
`disposition=${LEGACY_ALIAS_DISPOSITION}`,
|
|
69
|
-
"actor=maintainer",
|
|
70
|
-
"reason=quarantine approved historical alias",
|
|
71
|
-
].join("\n");
|
|
72
|
-
const ABANDON_AUTHORIZATION = [
|
|
73
|
-
"gentle-ai.review-abandon-authorization/v1",
|
|
74
|
-
"lineage=pristine",
|
|
75
|
-
"revision=revision",
|
|
76
|
-
"snapshot_identity=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
77
|
-
"actor=maintainer",
|
|
78
|
-
"reason=retire pristine lineage",
|
|
79
|
-
].join("\n");
|
|
80
|
-
const LEGACY_QUARANTINE_AUTHORIZATION = [
|
|
81
|
-
"gentle-ai.review-legacy-quarantine-authorization/v1",
|
|
82
|
-
"repository=/repo",
|
|
83
|
-
"lineage=legacy",
|
|
84
|
-
"revision=sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
85
|
-
`diagnostic=${LEGACY_FREEZE_DIAGNOSTIC}`,
|
|
86
|
-
`disposition=${LEGACY_FREEZE_DISPOSITION}`,
|
|
87
|
-
"actor=maintainer",
|
|
88
|
-
"reason=quarantine malformed legacy freeze",
|
|
89
|
-
].join("\n");
|
|
90
|
-
const RECONCILE_AUTHORIZATION = [
|
|
91
|
-
"gentle-ai.review-reconcile-authorization/v1",
|
|
92
|
-
"predecessor_lineage=predecessor",
|
|
93
|
-
"predecessor_revision=predecessor-revision",
|
|
94
|
-
"successor_lineage=successor",
|
|
95
|
-
"successor_revision=successor-revision",
|
|
96
|
-
"actor=maintainer",
|
|
97
|
-
"reason=invalid recovery edge",
|
|
98
|
-
].join("\n");
|
|
99
|
-
const COMBINED_RECONCILE_AUTHORIZATION = `${RECONCILE_AUTHORIZATION}\nanomalies=${COMBINED_RECONCILE_ANOMALIES}`;
|
|
100
|
-
|
|
101
|
-
function scratchDir(prefix: string): string {
|
|
102
|
-
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
103
|
-
test.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
104
|
-
return dir;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
interface RecordedNativeCall { operation: "reclaim" | "recover" | "reconcileAuthority" | "abandon" | "quarantineLegacy"; request: Record<string, unknown>; }
|
|
108
|
-
|
|
109
|
-
function fakeRecoveryNative(record: Record<string, unknown>): { native: NativeReviewCli; calls: RecordedNativeCall[] } {
|
|
110
|
-
const calls: RecordedNativeCall[] = [];
|
|
111
|
-
const native = {
|
|
112
|
-
async reclaim(request: Record<string, unknown>) {
|
|
113
|
-
calls.push({ operation: "reclaim", request });
|
|
114
|
-
return { record };
|
|
115
|
-
},
|
|
116
|
-
async recover(request: Record<string, unknown>) {
|
|
117
|
-
calls.push({ operation: "recover", request });
|
|
118
|
-
return { record };
|
|
119
|
-
},
|
|
120
|
-
async reconcileAuthority(request: Record<string, unknown>) {
|
|
121
|
-
calls.push({ operation: "reconcileAuthority", request });
|
|
122
|
-
return { record };
|
|
123
|
-
},
|
|
124
|
-
async abandon(request: Record<string, unknown>) {
|
|
125
|
-
calls.push({ operation: "abandon", request });
|
|
126
|
-
return { record };
|
|
127
|
-
},
|
|
128
|
-
async quarantineLegacy(request: Record<string, unknown>) {
|
|
129
|
-
calls.push({ operation: "quarantineLegacy", request });
|
|
130
|
-
return { record };
|
|
131
|
-
},
|
|
132
|
-
async targetStatus(request: { lineageId?: string }) {
|
|
133
|
-
return {
|
|
134
|
-
action: "recover",
|
|
135
|
-
actionDisposition: "invalidated",
|
|
136
|
-
authority: { lineageId: request.lineageId ?? "broken", revision: "rev-1" },
|
|
137
|
-
} as unknown;
|
|
138
|
-
},
|
|
139
|
-
} as unknown as NativeReviewCli;
|
|
140
|
-
return { native, calls };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function runControllerOperation(
|
|
144
|
-
parameters: Record<string, unknown>,
|
|
145
|
-
native: NativeReviewCli | null,
|
|
146
|
-
pendingAuthorizations: Map<string, unknown> = new Map(),
|
|
147
|
-
signal?: AbortSignal,
|
|
148
|
-
): Promise<Record<string, unknown>> {
|
|
149
|
-
const cwd = scratchDir("gentle-pi-native-recovery-");
|
|
150
|
-
return await __testing.executeReviewControllerOperation(
|
|
151
|
-
parameters,
|
|
152
|
-
cwd,
|
|
153
|
-
pendingAuthorizations as Map<string, never>,
|
|
154
|
-
native,
|
|
155
|
-
signal,
|
|
156
|
-
);
|
|
38
|
+
function client(adapter: ExecFileAdapter): NativeReviewCliV216 {
|
|
39
|
+
return new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai");
|
|
157
40
|
}
|
|
158
41
|
|
|
159
|
-
|
|
160
|
-
const { adapter, calls } = queuedAdapter([VERSION_219, { stdout: JSON.stringify(RECLAIM_RECORD) }]);
|
|
161
|
-
const cli = new NativeReviewCliV214(adapter);
|
|
162
|
-
const result = await cli.reclaim!({ cwd: "/repo", lineage: "stuck-lineage", actor: "maintainer", reason: "incomplete entry" });
|
|
163
|
-
assert.deepEqual(result.record, RECLAIM_RECORD);
|
|
164
|
-
assert.deepEqual(calls[1]?.arguments, ["review", "reclaim", "--cwd", "/repo", "--lineage", "stuck-lineage", "--actor", "maintainer", "--reason", "incomplete entry"]);
|
|
165
|
-
});
|
|
42
|
+
const SHA = `sha256:${"a".repeat(64)}`;
|
|
166
43
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
44
|
+
// Recovery and maintenance commands retain their published legacy wire forms.
|
|
45
|
+
// These records are intentionally opaque: Pi validates command bindings, then
|
|
46
|
+
// relays the provider's audit record without reinterpreting authority state.
|
|
47
|
+
test("native reclaim and recover preserve their exact authority bindings", async () => {
|
|
48
|
+
const queue = queuedAdapter([
|
|
49
|
+
{ stdout: JSON.stringify({ schema: "gentle-ai.review-reclaim-audit/v1", lineage: "stuck", actor: "maintainer", reason: "incomplete" }) },
|
|
50
|
+
{ stdout: JSON.stringify({ schema: "gentle-ai.review-recovery/v1", predecessor_lineage: "stuck", successor_lineage: "recovered" }) },
|
|
51
|
+
]);
|
|
52
|
+
const review = client(queue.adapter);
|
|
53
|
+
assert.equal((await review.reclaim({ cwd: "/repo", lineage: "stuck", actor: "maintainer", reason: "incomplete" })).record.lineage, "stuck");
|
|
54
|
+
assert.equal((await review.recover({
|
|
171
55
|
cwd: "/repo",
|
|
172
|
-
predecessorLineage: "
|
|
173
|
-
expectedPredecessorRevision: "
|
|
174
|
-
successorLineage: "
|
|
56
|
+
predecessorLineage: "stuck",
|
|
57
|
+
expectedPredecessorRevision: "revision-1",
|
|
58
|
+
successorLineage: "recovered",
|
|
175
59
|
disposition: "invalidated",
|
|
176
60
|
actor: "maintainer",
|
|
177
|
-
reason: "
|
|
178
|
-
maintainerAuthorization: "
|
|
179
|
-
});
|
|
180
|
-
assert.deepEqual(
|
|
181
|
-
|
|
182
|
-
"review", "recover", "--cwd", "/repo",
|
|
183
|
-
"--predecessor-lineage", "broken",
|
|
184
|
-
"--expected-predecessor-revision", "rev-1",
|
|
185
|
-
"--successor-lineage", "successor",
|
|
186
|
-
"--disposition", "invalidated",
|
|
187
|
-
"--actor", "maintainer",
|
|
188
|
-
"--reason", "invalid authority",
|
|
189
|
-
"--maintainer-authorization", "binding",
|
|
61
|
+
reason: "recovery required",
|
|
62
|
+
maintainerAuthorization: "approved\nbinding",
|
|
63
|
+
})).record.successor_lineage, "recovered");
|
|
64
|
+
assert.deepEqual(queue.calls.map((call) => call.arguments), [
|
|
65
|
+
["review", "reclaim", "--cwd", "/repo", "--lineage", "stuck", "--actor", "maintainer", "--reason", "incomplete"],
|
|
66
|
+
["review", "recover", "--cwd", "/repo", "--predecessor-lineage", "stuck", "--expected-predecessor-revision", "revision-1", "--successor-lineage", "recovered", "--disposition", "invalidated", "--actor", "maintainer", "--reason", "recovery required", "--maintainer-authorization", "approved\nbinding"],
|
|
190
67
|
]);
|
|
191
68
|
});
|
|
192
69
|
|
|
193
|
-
test("native
|
|
194
|
-
const { adapter, calls } = queuedAdapter([VERSION_219, { stdout: JSON.stringify(RECONCILE_RESULT) }]);
|
|
195
|
-
const cli = new NativeReviewCliV214(adapter);
|
|
196
|
-
const result = await cli.reconcileAuthority!({
|
|
197
|
-
cwd: "/repo with spaces",
|
|
198
|
-
predecessorLineage: "predecessor",
|
|
199
|
-
expectedPredecessorRevision: "predecessor-revision",
|
|
200
|
-
successorLineage: "successor",
|
|
201
|
-
expectedSuccessorRevision: "successor-revision",
|
|
202
|
-
actor: "maintainer",
|
|
203
|
-
reason: "invalid recovery edge",
|
|
204
|
-
maintainerAuthorization: RECONCILE_AUTHORIZATION,
|
|
205
|
-
});
|
|
206
|
-
assert.deepEqual(result.record, RECONCILE_RECORD);
|
|
207
|
-
assert.deepEqual(calls[1]?.arguments, [
|
|
208
|
-
"review", "reconcile-authority", "--cwd", "/repo with spaces",
|
|
209
|
-
"--predecessor-lineage", "predecessor",
|
|
210
|
-
"--expected-predecessor-revision", "predecessor-revision",
|
|
211
|
-
"--successor-lineage", "successor",
|
|
212
|
-
"--expected-successor-revision", "successor-revision",
|
|
213
|
-
"--actor", "maintainer",
|
|
214
|
-
"--reason", "invalid recovery edge",
|
|
215
|
-
"--maintainer-authorization", RECONCILE_AUTHORIZATION,
|
|
216
|
-
]);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test("native v2.1.8 reconcile-authority accepts its raw audit response but modern envelopes remain strict", async () => {
|
|
70
|
+
test("native abandon carries the exact discarded-work authorization and audit record", async () => {
|
|
220
71
|
const request = {
|
|
221
72
|
cwd: "/repo",
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
73
|
+
lineage: "abandoned",
|
|
74
|
+
expectedRevision: "revision-1",
|
|
75
|
+
snapshotIdentity: SHA,
|
|
76
|
+
capturedLensResults: ["00-risk.json"],
|
|
77
|
+
findingsPresent: true,
|
|
78
|
+
evidenceRecordsPresent: false,
|
|
226
79
|
actor: "maintainer",
|
|
227
|
-
reason: "
|
|
228
|
-
maintainerAuthorization: RECONCILE_AUTHORIZATION,
|
|
80
|
+
reason: "discard candidate",
|
|
229
81
|
};
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
[VERSION_218, { schema: "gentle-ai.review-reconcile-audit/v1", predecessor_lineage: "predecessor" }],
|
|
235
|
-
[VERSION_219, RECONCILE_RECORD],
|
|
236
|
-
] as const) {
|
|
237
|
-
const queue = queuedAdapter([version, { stdout: JSON.stringify(response) }]);
|
|
238
|
-
await assert.rejects(
|
|
239
|
-
() => new NativeReviewCliV214(queue.adapter).reconcileAuthority!(request),
|
|
240
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE,
|
|
241
|
-
);
|
|
242
|
-
}
|
|
82
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify({ operation: "review/abandon", record: { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "abandoned", status: "committed" } }) }]);
|
|
83
|
+
const result = await client(queue.adapter).abandon({ ...request, maintainerAuthorization: nativeReviewAbandonAuthorization(request) });
|
|
84
|
+
assert.equal(result.record.status, "committed");
|
|
85
|
+
assert.deepEqual(queue.calls[0]?.arguments.slice(0, 10), ["review", "abandon", "--cwd", "/repo", "--lineage", "abandoned", "--expected-revision", "revision-1", "--actor", "maintainer"]);
|
|
243
86
|
});
|
|
244
87
|
|
|
245
|
-
test("native
|
|
246
|
-
const { adapter, calls } = queuedAdapter([
|
|
247
|
-
VERSION_219, { stdout: JSON.stringify({ operation: "review/abandon", record: ABANDON_RECORD }) },
|
|
248
|
-
VERSION_219, { stdout: JSON.stringify({ operation: "review/quarantine-legacy", record: LEGACY_QUARANTINE_RECORD }) },
|
|
249
|
-
VERSION_219, { stdout: JSON.stringify({ operation: "review/reconcile-authority", record: RECONCILE_RECORD }) },
|
|
250
|
-
]);
|
|
251
|
-
const cli = new NativeReviewCliV214(adapter);
|
|
252
|
-
assert.deepEqual((await cli.abandon!({ cwd: "/repo", lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", actor: "maintainer", reason: "retire pristine lineage", maintainerAuthorization: ABANDON_AUTHORIZATION })).record, ABANDON_RECORD);
|
|
253
|
-
assert.deepEqual((await cli.quarantineLegacy!({ cwd: "/repo", repository: "/repo", lineage: "legacy", expectedRevision: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", diagnostic: LEGACY_FREEZE_DIAGNOSTIC, disposition: LEGACY_FREEZE_DISPOSITION, actor: "maintainer", reason: "quarantine malformed legacy freeze", maintainerAuthorization: LEGACY_QUARANTINE_AUTHORIZATION })).record, LEGACY_QUARANTINE_RECORD);
|
|
254
|
-
assert.deepEqual((await cli.reconcileAuthority!({ cwd: "/repo", predecessorLineage: "predecessor", expectedPredecessorRevision: "predecessor-revision", successorLineage: "successor", expectedSuccessorRevision: "successor-revision", actor: "maintainer", reason: "invalid recovery edge", anomalies: COMBINED_RECONCILE_ANOMALIES, maintainerAuthorization: COMBINED_RECONCILE_AUTHORIZATION })).record, RECONCILE_RECORD);
|
|
255
|
-
assert.deepEqual(calls.filter((call) => call.arguments[0] === "review").map((call) => call.arguments), [
|
|
256
|
-
["review", "abandon", "--cwd", "/repo", "--lineage", "pristine", "--expected-revision", "revision", "--actor", "maintainer", "--reason", "retire pristine lineage", "--maintainer-authorization", ABANDON_AUTHORIZATION],
|
|
257
|
-
["review", "quarantine-legacy", "--cwd", "/repo", "--lineage", "legacy", "--expected-revision", "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "--diagnostic", LEGACY_FREEZE_DIAGNOSTIC, "--disposition", LEGACY_FREEZE_DISPOSITION, "--actor", "maintainer", "--reason", "quarantine malformed legacy freeze", "--maintainer-authorization", LEGACY_QUARANTINE_AUTHORIZATION],
|
|
258
|
-
["review", "reconcile-authority", "--cwd", "/repo", "--predecessor-lineage", "predecessor", "--expected-predecessor-revision", "predecessor-revision", "--successor-lineage", "successor", "--expected-successor-revision", "successor-revision", "--actor", "maintainer", "--reason", "invalid recovery edge", "--maintainer-authorization", COMBINED_RECONCILE_AUTHORIZATION],
|
|
259
|
-
]);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
test("native v2.2.0 repair-legacy-alias uses the exact fixed binding and preserves idempotent audit records", async () => {
|
|
263
|
-
const { adapter, calls } = queuedAdapter([
|
|
264
|
-
VERSION_220,
|
|
265
|
-
{ stdout: JSON.stringify({ operation: "review/repair-legacy-alias", record: LEGACY_ALIAS_RECORD }) },
|
|
266
|
-
]);
|
|
267
|
-
const cli = new NativeReviewCliV214(adapter);
|
|
88
|
+
test("native quarantine retains published legacy-v1 maintenance compatibility", async () => {
|
|
268
89
|
const request = {
|
|
269
90
|
cwd: "/repo",
|
|
270
91
|
repository: "/repo",
|
|
271
|
-
lineage: "legacy-
|
|
272
|
-
expectedRevision:
|
|
273
|
-
diagnostic:
|
|
274
|
-
disposition:
|
|
92
|
+
lineage: "legacy-freeze",
|
|
93
|
+
expectedRevision: SHA,
|
|
94
|
+
diagnostic: NATIVE_REVIEW_LEGACY_QUARANTINE.DIAGNOSTIC,
|
|
95
|
+
disposition: NATIVE_REVIEW_LEGACY_QUARANTINE.DISPOSITION,
|
|
275
96
|
actor: "maintainer",
|
|
276
|
-
reason: "quarantine
|
|
277
|
-
maintainerAuthorization: LEGACY_ALIAS_AUTHORIZATION,
|
|
97
|
+
reason: "quarantine malformed freeze",
|
|
278
98
|
};
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
assert.
|
|
282
|
-
|
|
283
|
-
"--expected-revision", `sha256:${"c".repeat(64)}`,
|
|
284
|
-
"--diagnostic", LEGACY_ALIAS_DIAGNOSTIC,
|
|
285
|
-
"--disposition", LEGACY_ALIAS_DISPOSITION,
|
|
286
|
-
"--actor", "maintainer", "--reason", "quarantine approved historical alias",
|
|
287
|
-
"--maintainer-authorization", LEGACY_ALIAS_AUTHORIZATION,
|
|
288
|
-
]);
|
|
99
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify({ operation: "review/quarantine-legacy", record: { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "legacy-freeze", status: "committed" } }) }]);
|
|
100
|
+
const result = await client(queue.adapter).quarantineLegacy({ ...request, maintainerAuthorization: nativeReviewLegacyQuarantineAuthorization(request) });
|
|
101
|
+
assert.equal(result.record.lineage_id, "legacy-freeze");
|
|
102
|
+
assert.deepEqual(queue.calls[0]?.arguments.slice(0, 8), ["review", "quarantine-legacy", "--cwd", "/repo", "--lineage", "legacy-freeze", "--expected-revision", SHA]);
|
|
289
103
|
});
|
|
290
104
|
|
|
291
|
-
|
|
292
|
-
// unnegotiated and carries no --contract (asserted at :281-288). The net-new
|
|
293
|
-
// negotiated `repair()` is a DIFFERENT operation and must carry --contract on
|
|
294
|
-
// every invocation, including the capabilities preflight `negotiated()` runs
|
|
295
|
-
// first. A non-eligible preflight assessment lets this test stop after one
|
|
296
|
-
// repair call, without needing a second queued execute-mode response.
|
|
297
|
-
test("negotiated repair carries --contract on every invocation, unlike repair-legacy-alias", async (t) => {
|
|
298
|
-
t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
|
|
299
|
-
const capabilities = v2Fixture<Record<string, unknown>>("capabilities.fixture.json");
|
|
300
|
-
const executableDigest = "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705";
|
|
301
|
-
const capabilitiesBody = { ...capabilities, package: { ...(capabilities.package as Record<string, unknown>), version: GENTLE_AI_VERSION } };
|
|
302
|
-
const preflightResult = {
|
|
303
|
-
schema: "gentle-ai.review-integration.repair/v2",
|
|
304
|
-
contract: "gentle-ai.review-integration/v2",
|
|
305
|
-
operation: "review.repair",
|
|
306
|
-
mode: "preflight",
|
|
307
|
-
assessment: {
|
|
308
|
-
schema: "gentle-ai.review-authority-repair-assessment/v1",
|
|
309
|
-
status: "unsupported",
|
|
310
|
-
counts: { lineages: 0, compact_lineages: 0, legacy_lineages: 0, events: 0, bytes: 0, eligible_candidates: 0, unsupported_lineages: 0, conflicts: 0 },
|
|
311
|
-
supported_operations: ["review/complete-fix", "review/validate-fix"],
|
|
312
|
-
authorization_schema: "gentle-ai.review-repair-authorization/v1",
|
|
313
|
-
},
|
|
314
|
-
required_inputs: [],
|
|
315
|
-
};
|
|
316
|
-
const { adapter, calls } = queuedAdapter([
|
|
317
|
-
{ stdout: JSON.stringify(capabilitiesBody) },
|
|
318
|
-
{ stdout: JSON.stringify(preflightResult) },
|
|
319
|
-
]);
|
|
320
|
-
const cli = new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => executableDigest);
|
|
321
|
-
const result = await cli.repair!({ cwd: "/repo", actor: "maintainer", reason: "quarantine approved historical alias", maintainerAuthorization: "irrelevant-for-non-eligible-preflight" });
|
|
322
|
-
assert.equal(result.mode, "preflight");
|
|
323
|
-
assert.equal(result.assessment.status, "unsupported");
|
|
324
|
-
assert.equal(calls.length, 2, "a non-eligible preflight must never issue an execute-mode call");
|
|
325
|
-
assert.deepEqual(calls[0]?.arguments, ["review", "capabilities", "--contract", "gentle-ai.review-integration/v2"]);
|
|
326
|
-
assert.deepEqual(calls[1]?.arguments, ["review", "repair", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--mode", "preflight"]);
|
|
327
|
-
for (const call of calls) assert.ok(call.arguments.includes("--contract"), "every negotiated repair() invocation must carry --contract");
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
test("native repair-legacy-alias fails closed for stale bindings, malformed output, cancellation, and partial failure", async () => {
|
|
105
|
+
test("native reconciliation accepts only its exact predecessor and successor binding", async () => {
|
|
331
106
|
const request = {
|
|
332
|
-
cwd: "/repo",
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
{ stdout: JSON.stringify({ operation: "review/repair-legacy-alias" }) },
|
|
340
|
-
{ stdout: JSON.stringify({ operation: "review/repair-legacy-alias", record: LEGACY_ALIAS_RECORD }), stderr: "interrupted", exitCode: 1 },
|
|
341
|
-
]) {
|
|
342
|
-
const queue = queuedAdapter([VERSION_220, result]);
|
|
343
|
-
await assert.rejects(
|
|
344
|
-
() => new NativeReviewCliV214(queue.adapter).repairLegacyAlias!(request),
|
|
345
|
-
(error: unknown) => error instanceof NativeReviewCliError
|
|
346
|
-
&& (result.exitCode === 1 ? error.auditRecord?.schema === LEGACY_ALIAS_RECORD.schema : error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE),
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
test("native v2.1.9 maintenance wrappers fail closed before launch for unsupported versions and invalid bindings", async () => {
|
|
352
|
-
const unsupported = queuedAdapter([{ stdout: "gentle-ai 2.1.8\n" }]);
|
|
353
|
-
await assert.rejects(() => new NativeReviewCliV214(unsupported.adapter).abandon!({ cwd: "/repo", lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", actor: "maintainer", reason: "retire pristine lineage", maintainerAuthorization: ABANDON_AUTHORIZATION }), NativeReviewCliError);
|
|
354
|
-
const invalid = queuedAdapter([]);
|
|
355
|
-
await assert.rejects(() => new NativeReviewCliV214(invalid.adapter).reconcileAuthority!({ cwd: "/repo", predecessorLineage: "predecessor", expectedPredecessorRevision: "predecessor-revision", successorLineage: "successor", expectedSuccessorRevision: "successor-revision", actor: "maintainer", reason: "invalid recovery edge", anomalies: "malformed_recovery_authorization,unchanged_target", maintainerAuthorization: COMBINED_RECONCILE_AUTHORIZATION }), TypeError);
|
|
356
|
-
assert.equal(invalid.calls.length, 0);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
test("native v2.1.9 maintenance wrappers preserve only valid prepared audit records on partial failures", async () => {
|
|
360
|
-
for (const [operation, invoke, result] of [
|
|
361
|
-
["review/abandon", (cli: NativeReviewCliV214) => cli.abandon!({ cwd: "/repo", lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", actor: "maintainer", reason: "retire pristine lineage", maintainerAuthorization: ABANDON_AUTHORIZATION }), ABANDON_RECORD],
|
|
362
|
-
["review/quarantine-legacy", (cli: NativeReviewCliV214) => cli.quarantineLegacy!({ cwd: "/repo", repository: "/repo", lineage: "legacy", expectedRevision: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", diagnostic: LEGACY_FREEZE_DIAGNOSTIC, disposition: LEGACY_FREEZE_DISPOSITION, actor: "maintainer", reason: "quarantine malformed legacy freeze", maintainerAuthorization: LEGACY_QUARANTINE_AUTHORIZATION }), LEGACY_QUARANTINE_RECORD],
|
|
363
|
-
] as const) {
|
|
364
|
-
const queue = queuedAdapter([VERSION_219, { stdout: JSON.stringify({ operation, record: result }), stderr: "quarantine interrupted", exitCode: 1 }]);
|
|
365
|
-
await assert.rejects(() => invoke(new NativeReviewCliV214(queue.adapter)), (error: unknown) => error instanceof NativeReviewCliError && error.mutationOutcome === "unknown" && error.nextAction === "review.status" && error.auditRecord?.schema === result.schema);
|
|
366
|
-
}
|
|
367
|
-
const malformed = queuedAdapter([VERSION_219, { stdout: JSON.stringify({ operation: "review/abandon" }), exitCode: 1 }]);
|
|
368
|
-
await assert.rejects(() => new NativeReviewCliV214(malformed.adapter).abandon!({ cwd: "/repo", lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", actor: "maintainer", reason: "retire pristine lineage", maintainerAuthorization: ABANDON_AUTHORIZATION }), (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE && error.auditRecord === undefined);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
test("native abandon forwards cancellation and preserves the unknown mutation outcome", async () => {
|
|
372
|
-
const controller = new AbortController();
|
|
373
|
-
let calls = 0;
|
|
374
|
-
const adapter: ExecFileAdapter = async (request) => {
|
|
375
|
-
calls += 1;
|
|
376
|
-
if (calls === 1) return { ...VERSION_219, stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
377
|
-
assert.equal(request.signal, controller.signal);
|
|
378
|
-
const error = new Error("cancelled");
|
|
379
|
-
error.name = "AbortError";
|
|
380
|
-
throw error;
|
|
107
|
+
cwd: "/repo",
|
|
108
|
+
predecessorLineage: "predecessor",
|
|
109
|
+
expectedPredecessorRevision: "predecessor-revision",
|
|
110
|
+
successorLineage: "successor",
|
|
111
|
+
expectedSuccessorRevision: "successor-revision",
|
|
112
|
+
actor: "maintainer",
|
|
113
|
+
reason: "repair authority edge",
|
|
381
114
|
};
|
|
382
|
-
|
|
115
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify({ operation: "review/reconcile-authority", record: { schema: "gentle-ai.review-reconcile-audit/v1", predecessor_lineage: "predecessor", successor_lineage: "successor", outcome: "quarantined" } }) }]);
|
|
116
|
+
const result = await client(queue.adapter).reconcileAuthority({ ...request, maintainerAuthorization: nativeReviewReconcileAuthorization(request) });
|
|
117
|
+
assert.equal(result.record.successor_lineage, "successor");
|
|
118
|
+
assert.deepEqual(queue.calls[0]?.arguments.slice(0, 12), ["review", "reconcile-authority", "--cwd", "/repo", "--predecessor-lineage", "predecessor", "--expected-predecessor-revision", "predecessor-revision", "--successor-lineage", "successor", "--expected-successor-revision", "successor-revision"]);
|
|
383
119
|
});
|
|
384
120
|
|
|
385
|
-
test("native
|
|
386
|
-
const
|
|
387
|
-
const cli = new NativeReviewCliV214(adapter);
|
|
121
|
+
test("native maintenance rejects tampered authority authorization before launch", async () => {
|
|
122
|
+
const queue = queuedAdapter([]);
|
|
388
123
|
await assert.rejects(
|
|
389
|
-
|
|
124
|
+
() => client(queue.adapter).reconcileAuthority({
|
|
390
125
|
cwd: "/repo",
|
|
391
126
|
predecessorLineage: "predecessor",
|
|
392
127
|
expectedPredecessorRevision: "predecessor-revision",
|
|
393
128
|
successorLineage: "successor",
|
|
394
|
-
expectedSuccessorRevision: "
|
|
129
|
+
expectedSuccessorRevision: "successor-revision",
|
|
395
130
|
actor: "maintainer",
|
|
396
|
-
reason: "
|
|
397
|
-
maintainerAuthorization:
|
|
131
|
+
reason: "repair authority edge",
|
|
132
|
+
maintainerAuthorization: "tampered",
|
|
398
133
|
}),
|
|
399
134
|
/exact target and revision binding/,
|
|
400
135
|
);
|
|
401
|
-
assert.equal(calls.length, 0);
|
|
136
|
+
assert.equal(queue.calls.length, 0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("native authority inventory remains a strict read-only maintenance surface", async () => {
|
|
140
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify({
|
|
141
|
+
schema: "gentle-ai.review-authority-status/v1",
|
|
142
|
+
operation: "review/status",
|
|
143
|
+
repository: process.cwd(),
|
|
144
|
+
complete: true,
|
|
145
|
+
authoritative: true,
|
|
146
|
+
status: "active",
|
|
147
|
+
entries: [{ version: "compact-v2", path: ".git/gentle-ai/reviews/current.json", status: "active", lineage_id: "current", state: "reviewing", revision: "revision-1", problems: [] }],
|
|
148
|
+
locks: [],
|
|
149
|
+
diagnostics: [],
|
|
150
|
+
}) }]);
|
|
151
|
+
const inventory = await client(queue.adapter).reviewStatus({ cwd: process.cwd() });
|
|
152
|
+
assert.equal(inventory.authoritative, true);
|
|
153
|
+
assert.equal(inventory.entries[0]?.lineageId, "current");
|
|
154
|
+
assert.deepEqual(queue.calls[0]?.arguments, ["review", "status", "--cwd", process.cwd()]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("negotiated repair replays only provider-published inputs after a strict preflight", async () => {
|
|
158
|
+
const preflight = JSON.parse(readFileSync(join(process.cwd(), "contracts", "review-integration", "v1", "fixtures", "repair-preflight.fixture.json"), "utf8")) as Record<string, unknown>;
|
|
159
|
+
preflight.schema = "gentle-ai.review-integration.repair/v2";
|
|
160
|
+
preflight.contract = "gentle-ai.review-integration/v2";
|
|
161
|
+
const execute = structuredClone(preflight) as Record<string, unknown>;
|
|
162
|
+
execute.mode = "execute";
|
|
163
|
+
delete execute.provider_inputs;
|
|
164
|
+
execute.required_inputs = [];
|
|
165
|
+
execute.execution = {
|
|
166
|
+
status: "committed",
|
|
167
|
+
class: "legacy_v1_historical_alias",
|
|
168
|
+
lineage_id: "historical-alias",
|
|
169
|
+
revision: `sha256:${"1".repeat(64)}`,
|
|
170
|
+
chain_identity: `sha256:${"2".repeat(64)}`,
|
|
171
|
+
cause: "unsupported_historical_v1_operation_alias",
|
|
172
|
+
disposition: "quarantine-approved-historical-alias",
|
|
173
|
+
assessment_digest: `sha256:${"3".repeat(64)}`,
|
|
174
|
+
request_digest: `sha256:${"4".repeat(64)}`,
|
|
175
|
+
record_identity: `sha256:${"5".repeat(64)}`,
|
|
176
|
+
};
|
|
177
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify(preflight) }, { stdout: JSON.stringify(execute) }]);
|
|
178
|
+
const result = await client(queue.adapter).repair({ cwd: "/repo", actor: "maintainer", reason: "repair alias", maintainerAuthorization: "approved" });
|
|
179
|
+
assert.equal(result.execution?.status, "committed");
|
|
180
|
+
assert.deepEqual(queue.calls.map((call) => call.arguments.slice(0, 8)), [
|
|
181
|
+
["review", "repair", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--mode", "preflight"],
|
|
182
|
+
["review", "repair", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--mode", "execute"],
|
|
183
|
+
]);
|
|
402
184
|
});
|
|
403
185
|
|
|
404
|
-
test("native
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
186
|
+
test("native repair-legacy-alias keeps the historical fixed binding and rejects a tampered one before launch", async () => {
|
|
187
|
+
const request = {
|
|
188
|
+
cwd: "/repo",
|
|
189
|
+
repository: "/repo",
|
|
190
|
+
lineage: "historical-alias",
|
|
191
|
+
expectedRevision: SHA,
|
|
192
|
+
diagnostic: NATIVE_REVIEW_LEGACY_ALIAS_REPAIR.DIAGNOSTIC,
|
|
193
|
+
disposition: NATIVE_REVIEW_LEGACY_ALIAS_REPAIR.DISPOSITION,
|
|
194
|
+
actor: "maintainer",
|
|
195
|
+
reason: "repair historical alias",
|
|
414
196
|
};
|
|
415
|
-
const
|
|
416
|
-
await
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
expectedSuccessorRevision: "successor-revision",
|
|
423
|
-
actor: "maintainer",
|
|
424
|
-
reason: "invalid recovery edge",
|
|
425
|
-
maintainerAuthorization: RECONCILE_AUTHORIZATION,
|
|
426
|
-
signal: controller.signal,
|
|
427
|
-
}),
|
|
428
|
-
(error: unknown) => error instanceof NativeReviewCliError
|
|
429
|
-
&& error.code === NATIVE_REVIEW_ERROR_CODE.CANCELLED
|
|
430
|
-
&& error.operation === "review/reconcile-authority"
|
|
431
|
-
&& error.mutationOutcome === "unknown",
|
|
432
|
-
);
|
|
197
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify({ operation: "review/repair-legacy-alias", record: { schema: "gentle-ai.review-repair-audit/v1", lineage_id: request.lineage, status: "committed" } }) }]);
|
|
198
|
+
const result = await client(queue.adapter).repairLegacyAlias({ ...request, maintainerAuthorization: nativeReviewLegacyAliasRepairAuthorization(request) });
|
|
199
|
+
assert.equal(result.record.lineage_id, request.lineage);
|
|
200
|
+
assert.deepEqual(queue.calls[0]?.arguments.slice(0, 10), ["review", "repair-legacy-alias", "--cwd", "/repo", "--lineage", request.lineage, "--expected-revision", SHA, "--diagnostic", request.diagnostic]);
|
|
201
|
+
const rejected = queuedAdapter([]);
|
|
202
|
+
await assert.rejects(() => client(rejected.adapter).repairLegacyAlias({ ...request, maintainerAuthorization: "tampered" }), /exact repository, lineage, revision/);
|
|
203
|
+
assert.equal(rejected.calls.length, 0);
|
|
433
204
|
});
|
|
434
205
|
|
|
435
|
-
test("native
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
expectedPredecessorRevision: "predecessor-revision",
|
|
443
|
-
successorLineage: "successor",
|
|
444
|
-
expectedSuccessorRevision: "successor-revision",
|
|
445
|
-
actor: "maintainer",
|
|
446
|
-
reason: "invalid recovery edge",
|
|
447
|
-
maintainerAuthorization: RECONCILE_AUTHORIZATION,
|
|
448
|
-
}),
|
|
449
|
-
(error: unknown) => error instanceof NativeReviewCliError
|
|
450
|
-
&& error.mutationOutcome === "unknown"
|
|
451
|
-
&& error.nextAction === "review.status"
|
|
452
|
-
&& error.auditRecord?.schema === RECONCILE_RECORD.schema,
|
|
453
|
-
);
|
|
206
|
+
test("native recovery operations reject malformed canonical inputs before a process can mutate", async () => {
|
|
207
|
+
const queue = queuedAdapter([]);
|
|
208
|
+
await assert.rejects(() => client(queue.adapter).recover({
|
|
209
|
+
cwd: "/repo", predecessorLineage: " predecessor", expectedPredecessorRevision: "revision", successorLineage: "successor", disposition: "invalidated", actor: "maintainer", reason: "reason",
|
|
210
|
+
}), /predecessorLineage/);
|
|
211
|
+
await assert.rejects(() => client(queue.adapter).reclaim({ cwd: "/repo", lineage: "lineage\n", actor: "maintainer", reason: "reason" }), /lineage/);
|
|
212
|
+
assert.equal(queue.calls.length, 0);
|
|
454
213
|
});
|
|
455
214
|
|
|
456
|
-
test("
|
|
457
|
-
const
|
|
458
|
-
|
|
215
|
+
test("partial maintenance failures preserve the provider audit record and unknown mutation outcome", async () => {
|
|
216
|
+
const request = {
|
|
217
|
+
cwd: "/repo", lineage: "abandoned", expectedRevision: "revision-1", snapshotIdentity: SHA,
|
|
218
|
+
capturedLensResults: ["00-risk.json"], findingsPresent: true, evidenceRecordsPresent: false, actor: "maintainer", reason: "discard candidate",
|
|
219
|
+
};
|
|
220
|
+
const queue = queuedAdapter([{ exitCode: 1, stdout: JSON.stringify({ operation: "review/abandon", record: { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: request.lineage, status: "partial" } }) }]);
|
|
459
221
|
await assert.rejects(
|
|
460
|
-
|
|
461
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.
|
|
222
|
+
() => client(queue.adapter).abandon({ ...request, maintainerAuthorization: nativeReviewAbandonAuthorization(request) }),
|
|
223
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.mutationOutcome === "unknown" && error.auditRecord?.status === "partial",
|
|
462
224
|
);
|
|
225
|
+
assert.equal(queue.calls.length, 1);
|
|
463
226
|
});
|
|
464
227
|
|
|
465
|
-
test("RESET
|
|
466
|
-
const {
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
repositoryId: "repo-id",
|
|
472
|
-
commonDirHash: "c".repeat(64),
|
|
473
|
-
inventoryHash: "d".repeat(64),
|
|
474
|
-
confirmation: "DESTROY REVIEW AUTHORITY repo-id",
|
|
475
|
-
lineage: "stuck-lineage",
|
|
476
|
-
actor: "maintainer",
|
|
477
|
-
reason: "incomplete entry",
|
|
478
|
-
}),
|
|
479
|
-
}, native, pending);
|
|
480
|
-
assert.equal(details.operation, "reset");
|
|
481
|
-
assert.equal(details.native_operation, "review reclaim");
|
|
482
|
-
assert.equal(details.mutation_performed, true);
|
|
483
|
-
assert.equal(details.mutation_outcome, "committed");
|
|
484
|
-
assert.deepEqual(details.result, RECLAIM_RECORD);
|
|
485
|
-
assert.equal(details.next_action, "inspect");
|
|
486
|
-
assert.equal(calls.length, 1);
|
|
487
|
-
assert.equal(calls[0]?.operation, "reclaim");
|
|
488
|
-
assert.equal(calls[0]?.request.lineage, "stuck-lineage");
|
|
489
|
-
assert.equal(calls[0]?.request.actor, "maintainer");
|
|
490
|
-
assert.equal(calls[0]?.request.reason, "incomplete entry");
|
|
491
|
-
assert.equal(pending.size, 0);
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
test("RESET without the native reclaim inputs returns a structured request instead of inventing values", async () => {
|
|
495
|
-
const { native, calls } = fakeRecoveryNative(RECLAIM_RECORD);
|
|
496
|
-
const pending = new Map<string, unknown>([["stale", { command: "git push" }]]);
|
|
497
|
-
const details = await runControllerOperation({
|
|
498
|
-
operation: "reset",
|
|
499
|
-
input: JSON.stringify({
|
|
500
|
-
repositoryId: "repo-id",
|
|
501
|
-
commonDirHash: "c".repeat(64),
|
|
502
|
-
inventoryHash: "d".repeat(64),
|
|
503
|
-
confirmation: "DESTROY REVIEW AUTHORITY repo-id",
|
|
504
|
-
}),
|
|
505
|
-
}, native, pending);
|
|
506
|
-
assert.equal(details.status, "blocked");
|
|
507
|
-
assert.equal(details.outcome, "native-input-required");
|
|
508
|
-
assert.equal(details.native_operation, "review reclaim");
|
|
509
|
-
assert.deepEqual(details.missing_input, ["lineage", "actor", "reason"]);
|
|
510
|
-
assert.equal(details.mutation_performed, false);
|
|
511
|
-
assert.equal(details.mutation_outcome, "none");
|
|
512
|
-
assert.equal(calls.length, 0);
|
|
513
|
-
assert.equal(pending.size, 1);
|
|
228
|
+
test("RESET requests native reclaim inputs rather than inventing authority values", async () => {
|
|
229
|
+
const native = { reclaim: async () => { throw new Error("must not reclaim"); } } as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
|
|
230
|
+
const result = await __testing.executeReviewControllerOperation({ operation: "reset", input: JSON.stringify({ lineage: "only-lineage" }) }, process.cwd(), native);
|
|
231
|
+
assert.equal(result.outcome, "native-input-required");
|
|
232
|
+
assert.deepEqual(result.missing_input, ["actor", "reason"]);
|
|
233
|
+
assert.equal(result.mutation_outcome, "none");
|
|
514
234
|
});
|
|
515
235
|
|
|
516
|
-
test("
|
|
517
|
-
|
|
518
|
-
operation: "reset",
|
|
519
|
-
input: JSON.stringify({ lineage: "stuck", actor: "maintainer", reason: "incomplete" }),
|
|
520
|
-
}, null);
|
|
521
|
-
assert.equal(details.status, "blocked");
|
|
522
|
-
assert.equal(details.outcome, "native-recovery-unavailable");
|
|
523
|
-
assert.equal(details.native_operation, "review reclaim");
|
|
524
|
-
assert.equal(details.mutation_performed, false);
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
test("RECOVER maps to native review recover with the successor authority binding", async () => {
|
|
528
|
-
const { native, calls } = fakeRecoveryNative(RECOVER_RECORD);
|
|
529
|
-
const details = await runControllerOperation({
|
|
530
|
-
operation: "recover",
|
|
531
|
-
input: JSON.stringify({
|
|
532
|
-
repositoryId: "repo-id",
|
|
533
|
-
commonDirHash: "c".repeat(64),
|
|
534
|
-
inventoryHash: "d".repeat(64),
|
|
535
|
-
confirmation: "DESTROY REVIEW AUTHORITY repo-id",
|
|
536
|
-
predecessorLineage: "broken",
|
|
537
|
-
expectedPredecessorRevision: "rev-1",
|
|
538
|
-
successorLineage: "successor",
|
|
539
|
-
disposition: "invalidated",
|
|
540
|
-
actor: "maintainer",
|
|
541
|
-
reason: "invalid authority",
|
|
542
|
-
maintainerAuthorization: "binding",
|
|
543
|
-
}),
|
|
544
|
-
}, native);
|
|
545
|
-
assert.equal(details.native_operation, "review recover");
|
|
546
|
-
assert.equal(details.mutation_performed, true);
|
|
547
|
-
assert.deepEqual(details.result, RECOVER_RECORD);
|
|
548
|
-
assert.equal(calls.length, 1);
|
|
549
|
-
assert.equal(calls[0]?.operation, "recover");
|
|
550
|
-
assert.equal(calls[0]?.request.predecessorLineage, "broken");
|
|
551
|
-
assert.equal(calls[0]?.request.expectedPredecessorRevision, "rev-1");
|
|
552
|
-
assert.equal(calls[0]?.request.successorLineage, "successor");
|
|
553
|
-
assert.equal(calls[0]?.request.disposition, "invalidated");
|
|
554
|
-
assert.equal(calls[0]?.request.maintainerAuthorization, "binding");
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
test("RECOVER surfaces every missing successor input including an unsupported disposition", async () => {
|
|
558
|
-
const { native, calls } = fakeRecoveryNative(RECOVER_RECORD);
|
|
559
|
-
const details = await runControllerOperation({
|
|
560
|
-
operation: "recover",
|
|
561
|
-
input: JSON.stringify({
|
|
562
|
-
repositoryId: "repo-id",
|
|
563
|
-
commonDirHash: "c".repeat(64),
|
|
564
|
-
inventoryHash: "d".repeat(64),
|
|
565
|
-
confirmation: "DESTROY REVIEW AUTHORITY repo-id",
|
|
566
|
-
predecessorLineage: "broken",
|
|
567
|
-
disposition: "not-a-disposition",
|
|
568
|
-
}),
|
|
569
|
-
}, native);
|
|
570
|
-
assert.equal(details.outcome, "native-input-required");
|
|
571
|
-
assert.equal(details.native_operation, "review recover");
|
|
572
|
-
assert.deepEqual(details.missing_input, ["expectedPredecessorRevision", "successorLineage", "disposition", "actor", "reason"]);
|
|
573
|
-
assert.equal(calls.length, 0);
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
test("RECONCILE_AUTHORITY routes one exact native mutation and returns its audit record", async () => {
|
|
577
|
-
const { native, calls } = fakeRecoveryNative(RECONCILE_RECORD);
|
|
578
|
-
const pending = new Map<string, unknown>([["stale", { command: "git push" }]]);
|
|
579
|
-
const details = await runControllerOperation({
|
|
580
|
-
operation: "reconcile-authority",
|
|
581
|
-
input: JSON.stringify({
|
|
582
|
-
predecessorLineage: "predecessor",
|
|
583
|
-
expectedPredecessorRevision: "predecessor-revision",
|
|
584
|
-
successorLineage: "successor",
|
|
585
|
-
expectedSuccessorRevision: "successor-revision",
|
|
586
|
-
actor: "maintainer",
|
|
587
|
-
reason: "invalid recovery edge",
|
|
588
|
-
}),
|
|
589
|
-
}, native, pending);
|
|
590
|
-
assert.equal(details.operation, "reconcile-authority");
|
|
591
|
-
assert.equal(details.native_operation, "review reconcile-authority");
|
|
592
|
-
assert.equal(details.mutation_performed, true);
|
|
593
|
-
assert.equal(details.mutation_outcome, "committed");
|
|
594
|
-
assert.deepEqual(details.result, RECONCILE_RECORD);
|
|
595
|
-
assert.equal(details.next_action, "inspect");
|
|
596
|
-
assert.equal(calls.length, 1);
|
|
597
|
-
assert.equal(calls[0]?.operation, "reconcileAuthority");
|
|
598
|
-
assert.equal(calls[0]?.request.expectedPredecessorRevision, "predecessor-revision");
|
|
599
|
-
assert.equal(calls[0]?.request.expectedSuccessorRevision, "successor-revision");
|
|
600
|
-
assert.equal(calls[0]?.request.maintainerAuthorization, RECONCILE_AUTHORIZATION);
|
|
601
|
-
assert.equal(pending.size, 0);
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
test("RECONCILE_AUTHORITY requests every exact native binding before authorization or mutation", async () => {
|
|
605
|
-
const { native, calls } = fakeRecoveryNative(RECONCILE_RECORD);
|
|
606
|
-
const details = await runControllerOperation({ operation: "reconcile-authority", input: "{}" }, native);
|
|
607
|
-
assert.equal(details.status, "blocked");
|
|
608
|
-
assert.equal(details.outcome, "native-input-required");
|
|
609
|
-
assert.deepEqual(details.missing_input, ["predecessorLineage", "expectedPredecessorRevision", "successorLineage", "expectedSuccessorRevision", "actor", "reason"]);
|
|
610
|
-
assert.equal(details.mutation_performed, false);
|
|
611
|
-
assert.equal(details.mutation_outcome, "none");
|
|
612
|
-
assert.equal(calls.length, 0);
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
test("RECONCILE_AUTHORITY returns a typed fail-closed envelope for native cancellation", async () => {
|
|
236
|
+
test("RECOVER rejects caller-authored authorization before status, UI, or native mutation", async () => {
|
|
237
|
+
let statusCalls = 0;
|
|
616
238
|
const native = {
|
|
617
|
-
async
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
successorLineage: "successor",
|
|
627
|
-
expectedSuccessorRevision: "successor-revision",
|
|
628
|
-
actor: "maintainer",
|
|
629
|
-
reason: "invalid recovery edge",
|
|
630
|
-
}),
|
|
631
|
-
}, native);
|
|
632
|
-
assert.equal(details.status, "blocked");
|
|
633
|
-
assert.equal(details.outcome, "native-operation-failed");
|
|
634
|
-
assert.equal(details.mutation_outcome, "unknown");
|
|
635
|
-
assert.equal(details.replayability, "status_required");
|
|
636
|
-
assert.equal(details.next_action, "review.status");
|
|
637
|
-
assert.deepEqual(details.diagnostics, {
|
|
638
|
-
operation: "review/reconcile-authority",
|
|
639
|
-
error_code: "cancelled",
|
|
640
|
-
timed_out: false,
|
|
641
|
-
output_limit_exceeded: false,
|
|
642
|
-
});
|
|
239
|
+
targetStatus: async () => { statusCalls += 1; throw new Error("must not read status"); },
|
|
240
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
|
|
241
|
+
const result = await __testing.executeReviewControllerOperation({
|
|
242
|
+
operation: "recover",
|
|
243
|
+
input: JSON.stringify({ predecessorLineage: "predecessor", expectedPredecessorRevision: "revision", successorLineage: "successor", disposition: "invalidated", actor: "maintainer", reason: "recover", maintainerAuthorization: "caller-authored" }),
|
|
244
|
+
}, process.cwd(), native);
|
|
245
|
+
assert.equal(result.outcome, "native-recovery-caller-authorization-rejected");
|
|
246
|
+
assert.equal(statusCalls, 0);
|
|
247
|
+
assert.equal(result.mutation_outcome, "none");
|
|
643
248
|
});
|
|
644
249
|
|
|
645
|
-
test("RECONCILE_AUTHORITY
|
|
646
|
-
const native = {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const details = await runControllerOperation({
|
|
652
|
-
operation: "reconcile-authority",
|
|
653
|
-
input: JSON.stringify({ predecessorLineage: "predecessor", expectedPredecessorRevision: "predecessor-revision", successorLineage: "successor", expectedSuccessorRevision: "successor-revision", actor: "maintainer", reason: "invalid recovery edge" }),
|
|
654
|
-
}, native);
|
|
655
|
-
assert.equal(details.mutation_outcome, "unknown");
|
|
656
|
-
assert.equal(details.next_action, "review.status");
|
|
657
|
-
assert.deepEqual(details.native_audit_record, RECONCILE_RECORD);
|
|
250
|
+
test("RECONCILE_AUTHORITY returns exact missing-input guidance before native mutation", async () => {
|
|
251
|
+
const native = { reconcileAuthority: async () => { throw new Error("must not reconcile"); } } as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
|
|
252
|
+
const result = await __testing.executeReviewControllerOperation({ operation: "reconcile-authority", input: JSON.stringify({ predecessorLineage: "predecessor" }) }, process.cwd(), native);
|
|
253
|
+
assert.equal(result.outcome, "native-input-required");
|
|
254
|
+
assert.deepEqual(result.missing_input, ["expectedPredecessorRevision", "successorLineage", "expectedSuccessorRevision", "actor", "reason"]);
|
|
255
|
+
assert.equal(result.mutation_outcome, "none");
|
|
658
256
|
});
|
|
659
257
|
|
|
660
|
-
test("
|
|
661
|
-
const
|
|
258
|
+
test("native abandon rejects a legacy authorization before process launch", async () => {
|
|
259
|
+
const request = {
|
|
260
|
+
cwd: "/repo",
|
|
261
|
+
lineage: "abandoned",
|
|
262
|
+
expectedRevision: "revision-1",
|
|
263
|
+
snapshotIdentity: SHA,
|
|
264
|
+
capturedLensResults: ["00-risk.json"],
|
|
265
|
+
findingsPresent: true,
|
|
266
|
+
evidenceRecordsPresent: false,
|
|
267
|
+
actor: "maintainer",
|
|
268
|
+
reason: "discard candidate",
|
|
269
|
+
};
|
|
270
|
+
const queue = queuedAdapter([]);
|
|
662
271
|
await assert.rejects(
|
|
663
|
-
|
|
664
|
-
/
|
|
272
|
+
() => client(queue.adapter).abandon({ ...request, maintainerAuthorization: "gentle-ai.review-abandon-authorization/v1" }),
|
|
273
|
+
/exact lineage, revision, snapshot, reason, discarded-work, and actor binding/,
|
|
665
274
|
);
|
|
666
|
-
assert.equal(calls.length, 0);
|
|
667
|
-
const details = await runControllerOperation({
|
|
668
|
-
operation: "recover-lock",
|
|
669
|
-
input: JSON.stringify({ ownerHash: "a".repeat(64), lineage: "stuck", actor: "maintainer", reason: "stale lock" }),
|
|
670
|
-
}, native);
|
|
671
|
-
assert.equal(details.native_operation, "review reclaim");
|
|
672
|
-
assert.equal(details.mutation_performed, true);
|
|
673
|
-
assert.deepEqual(details.result, RECLAIM_RECORD);
|
|
674
|
-
assert.equal(calls.length, 1);
|
|
675
|
-
assert.equal(calls[0]?.operation, "reclaim");
|
|
676
|
-
assert.equal(calls[0]?.request.lineage, "stuck");
|
|
275
|
+
assert.equal(queue.calls.length, 0);
|
|
677
276
|
});
|
|
678
277
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
278
|
+
function recoveryStatus(options: {
|
|
279
|
+
lineageId?: string;
|
|
280
|
+
revision?: string;
|
|
281
|
+
targetIdentity?: string;
|
|
282
|
+
disposition?: "scope_changed" | "invalidated" | "escalated";
|
|
283
|
+
action?: "recover" | "start";
|
|
284
|
+
} = {}) {
|
|
285
|
+
return {
|
|
286
|
+
action: options.action ?? "recover",
|
|
287
|
+
actionDisposition: options.disposition ?? "invalidated",
|
|
288
|
+
authority: { lineageId: options.lineageId ?? "predecessor", revision: options.revision ?? "revision-1" },
|
|
289
|
+
targetIdentity: options.targetIdentity ?? SHA,
|
|
290
|
+
raw: { schema: "gentle-ai.review-integration.status/v5" },
|
|
291
|
+
} as unknown as import("../lib/review-integration-v2.ts").ReviewStatusV3;
|
|
292
|
+
}
|
|
689
293
|
|
|
690
|
-
|
|
691
|
-
const tools = new Map<string, { execute: (
|
|
692
|
-
|
|
294
|
+
function registeredController(nativeReviewCli: import("../lib/native-review-cli.ts").NativeReviewCli) {
|
|
295
|
+
const tools = new Map<string, { execute: (toolCallId: string, params: unknown, signal: AbortSignal | undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<{ details?: unknown }> }>();
|
|
296
|
+
createGentleAiExtension({ nativeReviewCli })({
|
|
693
297
|
on() {},
|
|
694
|
-
registerTool(definition: { name: string; execute:
|
|
695
|
-
tools.set(definition.name, definition as unknown as { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> });
|
|
696
|
-
},
|
|
298
|
+
registerTool(definition: { name: string; execute: (toolCallId: string, params: unknown, signal: AbortSignal | undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<{ details?: unknown }> }) { tools.set(definition.name, definition); },
|
|
697
299
|
registerCommand() {},
|
|
698
|
-
} as unknown as ExtensionAPI;
|
|
699
|
-
const { native, calls } = fakeRecoveryNative(RECLAIM_RECORD);
|
|
700
|
-
createGentleAiExtension({ nativeReviewCli: native })(pi);
|
|
300
|
+
} as unknown as ExtensionAPI);
|
|
701
301
|
const controller = tools.get("gentle_review");
|
|
702
302
|
assert.ok(controller);
|
|
703
|
-
|
|
704
|
-
|
|
303
|
+
return controller;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function interactiveContext(confirm: boolean): ExtensionContext {
|
|
307
|
+
return { cwd: process.cwd(), hasUI: true, ui: { confirm: async () => confirm } } as unknown as ExtensionContext;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
test("maintenance cancellation preserves the exact signal and unknown mutation outcome", async () => {
|
|
311
|
+
const controller = new AbortController();
|
|
312
|
+
const request = {
|
|
313
|
+
cwd: "/repo", lineage: "abandoned", expectedRevision: "revision-1", snapshotIdentity: SHA,
|
|
314
|
+
capturedLensResults: ["00-risk.json"], findingsPresent: true, evidenceRecordsPresent: false,
|
|
315
|
+
actor: "maintainer", reason: "discard candidate",
|
|
316
|
+
};
|
|
317
|
+
const adapter: ExecFileAdapter = async (call) => {
|
|
318
|
+
assert.equal(call.signal, controller.signal);
|
|
319
|
+
const error = new Error("cancelled");
|
|
320
|
+
error.name = "AbortError";
|
|
321
|
+
throw error;
|
|
322
|
+
};
|
|
705
323
|
await assert.rejects(
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
input: JSON.stringify({
|
|
709
|
-
repositoryId: "repo-id",
|
|
710
|
-
commonDirHash: "c".repeat(64),
|
|
711
|
-
inventoryHash: "d".repeat(64),
|
|
712
|
-
confirmation: "DESTROY REVIEW AUTHORITY repo-id",
|
|
713
|
-
lineage: "stuck",
|
|
714
|
-
actor: "maintainer",
|
|
715
|
-
reason: "incomplete",
|
|
716
|
-
}),
|
|
717
|
-
}, undefined, undefined, ctx),
|
|
718
|
-
/interactive Pi UI.*fails closed/i,
|
|
324
|
+
() => client(adapter).abandon({ ...request, maintainerAuthorization: nativeReviewAbandonAuthorization(request), signal: controller.signal }),
|
|
325
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.code === "cancelled" && error.mutationOutcome === "unknown" && error.nextAction === "review.status",
|
|
719
326
|
);
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
test("RECONCILE_AUTHORITY requires fresh Pi approval for the exact seven-line binding", async () => {
|
|
724
|
-
const tools = new Map<string, { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> }>();
|
|
725
|
-
const pi = {
|
|
726
|
-
on() {},
|
|
727
|
-
registerTool(definition: { name: string; execute: never }) { tools.set(definition.name, definition as never); },
|
|
728
|
-
registerCommand() {},
|
|
729
|
-
} as unknown as ExtensionAPI;
|
|
730
|
-
const { native, calls } = fakeRecoveryNative(RECONCILE_RECORD);
|
|
731
|
-
createGentleAiExtension({ nativeReviewCli: native })(pi);
|
|
732
|
-
const controller = tools.get("gentle_review");
|
|
733
|
-
assert.ok(controller);
|
|
734
|
-
const cwd = scratchDir("gentle-pi-native-reconcile-authorization-");
|
|
735
|
-
const parameters = {
|
|
736
|
-
operation: "reconcile-authority",
|
|
737
|
-
input: JSON.stringify({
|
|
738
|
-
predecessorLineage: "predecessor",
|
|
739
|
-
expectedPredecessorRevision: "predecessor-revision",
|
|
740
|
-
successorLineage: "successor",
|
|
741
|
-
expectedSuccessorRevision: "successor-revision",
|
|
742
|
-
actor: "maintainer",
|
|
743
|
-
reason: "invalid recovery edge",
|
|
744
|
-
}),
|
|
327
|
+
const reconcile = {
|
|
328
|
+
cwd: "/repo", predecessorLineage: "predecessor", expectedPredecessorRevision: "revision-1",
|
|
329
|
+
successorLineage: "successor", expectedSuccessorRevision: "revision-2", actor: "maintainer", reason: "repair edge",
|
|
745
330
|
};
|
|
746
331
|
await assert.rejects(
|
|
747
|
-
|
|
748
|
-
|
|
332
|
+
() => client(adapter).reconcileAuthority({ ...reconcile, maintainerAuthorization: nativeReviewReconcileAuthorization(reconcile), signal: controller.signal }),
|
|
333
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.code === "cancelled" && error.mutationOutcome === "unknown" && error.nextAction === "review.status",
|
|
749
334
|
);
|
|
750
|
-
let prompt = "";
|
|
751
|
-
const approved = await controller.execute("approved-reconcile", parameters, undefined, undefined, {
|
|
752
|
-
cwd,
|
|
753
|
-
hasUI: true,
|
|
754
|
-
ui: { confirm: async (_title: string, message: string) => { prompt = message; return true; } },
|
|
755
|
-
} as unknown as ExtensionContext) as { details: Record<string, unknown> };
|
|
756
|
-
assert.match(prompt, /predecessor_revision=predecessor-revision/);
|
|
757
|
-
assert.match(prompt, /successor_revision=successor-revision/);
|
|
758
|
-
assert.equal(approved.details.mutation_outcome, "committed");
|
|
759
|
-
assert.equal(calls.length, 1);
|
|
760
335
|
});
|
|
761
336
|
|
|
762
|
-
test("
|
|
763
|
-
const
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
assert.equal(
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
337
|
+
test("RECOVER derives a provider-bound authorization, rechecks it, and fails closed on every drift", async () => {
|
|
338
|
+
const requests: Array<Record<string, unknown>> = [];
|
|
339
|
+
const native = {
|
|
340
|
+
targetStatus: async () => recoveryStatus(),
|
|
341
|
+
recover: async (request: Record<string, unknown>) => { requests.push(request); return { record: { schema: "gentle-ai.review-recovery/v1", lineage: "successor" } }; },
|
|
342
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
|
|
343
|
+
const input = { predecessorLineage: "predecessor", expectedPredecessorRevision: "revision-1", successorLineage: "successor", disposition: "invalidated", actor: "maintainer", reason: "recover authority" };
|
|
344
|
+
const result = await __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify(input) }, process.cwd(), native, undefined, undefined, interactiveContext(true));
|
|
345
|
+
assert.equal(result.mutation_outcome, "committed");
|
|
346
|
+
assert.equal(requests.length, 1);
|
|
347
|
+
assert.match(String(requests[0]?.maintainerAuthorization), /^gentle-ai\.review-recovery-authorization\/v1\npredecessor_lineage=predecessor\npredecessor_revision=revision-1\ntarget_identity=/);
|
|
348
|
+
|
|
349
|
+
const wrongDisposition = await __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify({ ...input, disposition: "escalated" }) }, process.cwd(), native, undefined, undefined, interactiveContext(true));
|
|
350
|
+
assert.equal(wrongDisposition.outcome, "native-recovery-disposition-mismatch");
|
|
351
|
+
assert.equal(wrongDisposition.mutation_outcome, "none");
|
|
352
|
+
|
|
353
|
+
for (const status of [
|
|
354
|
+
recoveryStatus({ lineageId: "foreign" }),
|
|
355
|
+
recoveryStatus({ revision: "revision-2" }),
|
|
356
|
+
recoveryStatus({ action: "start" }),
|
|
357
|
+
]) {
|
|
358
|
+
const blocked = await __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify(input) }, process.cwd(), {
|
|
359
|
+
targetStatus: async () => status,
|
|
360
|
+
recover: async () => { throw new Error("must not recover"); },
|
|
361
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli, undefined, undefined, interactiveContext(true));
|
|
362
|
+
assert.equal(blocked.outcome, "native-recovery-status-mismatch");
|
|
363
|
+
assert.equal(blocked.mutation_outcome, "none");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let reads = 0;
|
|
367
|
+
const changed = await __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify(input) }, process.cwd(), {
|
|
368
|
+
targetStatus: async () => (++reads === 1 ? recoveryStatus() : recoveryStatus({ targetIdentity: `sha256:${"b".repeat(64)}` })),
|
|
369
|
+
recover: async () => { throw new Error("must not recover"); },
|
|
370
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli, undefined, undefined, interactiveContext(true));
|
|
371
|
+
assert.equal(changed.outcome, "native-recovery-authority-changed");
|
|
372
|
+
assert.equal(changed.mutation_outcome, "none");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("RECOVER, RESET, and RECONCILE keep provider inputs and failures authority-scoped", async () => {
|
|
376
|
+
const recoverMissing = await __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify({ predecessorLineage: "predecessor", disposition: "not-a-disposition" }) }, process.cwd(), {
|
|
377
|
+
recover: async () => { throw new Error("must not recover"); },
|
|
378
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli);
|
|
379
|
+
assert.deepEqual(recoverMissing.missing_input, ["expectedPredecessorRevision", "successorLineage", "disposition", "actor", "reason"]);
|
|
380
|
+
|
|
381
|
+
const resetUnavailable = await __testing.executeReviewControllerOperation({ operation: "reset", input: JSON.stringify({ lineage: "stuck", actor: "maintainer", reason: "incomplete" }) }, process.cwd(), null);
|
|
382
|
+
assert.equal(resetUnavailable.outcome, "native-recovery-unavailable");
|
|
383
|
+
|
|
384
|
+
const reconciliation = { predecessorLineage: "predecessor", expectedPredecessorRevision: "revision-1", successorLineage: "successor", expectedSuccessorRevision: "revision-2", actor: "maintainer", reason: "repair edge" };
|
|
385
|
+
const completed = await __testing.executeReviewControllerOperation({ operation: "reconcile-authority", input: JSON.stringify(reconciliation) }, process.cwd(), {
|
|
386
|
+
reconcileAuthority: async (request: Record<string, unknown>) => ({ record: { schema: "gentle-ai.review-reconcile-audit/v1", successor: request.successorLineage } }),
|
|
387
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli);
|
|
388
|
+
assert.equal(completed.mutation_outcome, "committed");
|
|
389
|
+
assert.deepEqual(completed.result, { schema: "gentle-ai.review-reconcile-audit/v1", successor: "successor" });
|
|
390
|
+
|
|
391
|
+
for (const error of [
|
|
392
|
+
new NativeReviewCliError("cancelled", "review/reconcile-authority", true, true, "cancelled"),
|
|
393
|
+
new NativeReviewCliError("non_zero", "review/reconcile-authority", true, true, "partial", undefined, { schema: "gentle-ai.review-reconcile-audit/v1", status: "partial" }),
|
|
782
394
|
]) {
|
|
783
|
-
const
|
|
784
|
-
|
|
395
|
+
const failed = await __testing.executeReviewControllerOperation({ operation: "reconcile-authority", input: JSON.stringify(reconciliation) }, process.cwd(), {
|
|
396
|
+
reconcileAuthority: async () => { throw error; },
|
|
397
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli);
|
|
398
|
+
assert.equal(failed.outcome, "native-operation-failed");
|
|
399
|
+
assert.equal(failed.mutation_outcome, "unknown");
|
|
400
|
+
assert.equal(failed.next_action, "review.status");
|
|
785
401
|
}
|
|
786
|
-
assert.equal(calls.length, 2);
|
|
787
402
|
});
|
|
788
403
|
|
|
789
|
-
test("
|
|
790
|
-
const
|
|
791
|
-
const pi = { on() {}, registerTool(definition: { name: string; execute: never }) { tools.set(definition.name, definition as never); }, registerCommand() {} } as unknown as ExtensionAPI;
|
|
792
|
-
const calls: Record<string, unknown>[] = [];
|
|
404
|
+
test("destructive maintenance remains UI-gated and derives authorization at the public controller boundary", async () => {
|
|
405
|
+
const calls: Array<Record<string, unknown>> = [];
|
|
793
406
|
const native = {
|
|
794
|
-
async
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
} as unknown as NativeReviewCli;
|
|
806
|
-
createGentleAiExtension({ nativeReviewCli: native })(pi);
|
|
807
|
-
const controller = tools.get("gentle_review");
|
|
808
|
-
assert.ok(controller);
|
|
809
|
-
const cwd = scratchDir("gentle-pi-v2110-alias-repair-");
|
|
810
|
-
const parameters = { operation: "repair-legacy-alias", input: JSON.stringify({ lineage: "legacy-alias", actor: "maintainer", reason: "quarantine approved historical alias" }) };
|
|
407
|
+
reclaim: async (request: Record<string, unknown>) => { calls.push(request); return { record: { schema: "gentle-ai.review-reclaim-audit/v1" } }; },
|
|
408
|
+
reconcileAuthority: async (request: Record<string, unknown>) => { calls.push(request); return { record: { schema: "gentle-ai.review-reconcile-audit/v1" } }; },
|
|
409
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
|
|
410
|
+
const controller = registeredController(native);
|
|
411
|
+
const reset = { operation: "reset", input: JSON.stringify({ repositoryId: "repo", commonDirHash: "a".repeat(64), inventoryHash: "b".repeat(64), confirmation: "DESTROY REVIEW AUTHORITY repo", lineage: "stuck", actor: "maintainer", reason: "incomplete" }) };
|
|
412
|
+
await assert.rejects(controller.execute("headless", reset, undefined, undefined, { ...interactiveContext(true), hasUI: false } as ExtensionContext), /interactive Pi UI.*fails closed/i);
|
|
413
|
+
await assert.rejects(controller.execute("declined", reset, undefined, undefined, interactiveContext(false)), /not explicitly authorized/);
|
|
414
|
+
const approved = await controller.execute("approved", reset, undefined, undefined, interactiveContext(true));
|
|
415
|
+
assert.equal((approved.details as { mutation_outcome?: string }).mutation_outcome, "committed");
|
|
416
|
+
assert.equal(calls.length, 1);
|
|
417
|
+
assert.equal(calls[0]?.lineage, "stuck");
|
|
811
418
|
await assert.rejects(
|
|
812
|
-
controller.execute("
|
|
813
|
-
/
|
|
419
|
+
controller.execute("missing-challenge", { operation: "reset", input: JSON.stringify({ commonDirHash: "a".repeat(64), inventoryHash: "b".repeat(64), confirmation: "DESTROY REVIEW AUTHORITY repo", lineage: "stuck", actor: "maintainer", reason: "incomplete" }) }, undefined, undefined, interactiveContext(true)),
|
|
420
|
+
/requires an exact string repositoryId/,
|
|
814
421
|
);
|
|
815
|
-
assert.equal(calls.length, 0);
|
|
816
|
-
let prompt = "";
|
|
817
|
-
const approved = await controller.execute("approved-alias-repair", parameters, undefined, undefined, {
|
|
818
|
-
cwd,
|
|
819
|
-
hasUI: true,
|
|
820
|
-
ui: { confirm: async (_title: string, message: string) => { prompt = message; return true; } },
|
|
821
|
-
} as unknown as ExtensionContext) as { details: Record<string, unknown> };
|
|
822
|
-
assert.match(prompt, /repository=\/canonical\/repository/);
|
|
823
|
-
assert.match(prompt, /revision=sha256:c{64}/);
|
|
824
|
-
assert.equal(approved.details.mutation_outcome, "committed");
|
|
825
|
-
assert.equal(calls[0]?.repository, "/canonical/repository");
|
|
826
|
-
assert.equal(calls[0]?.diagnostic, LEGACY_ALIAS_DIAGNOSTIC);
|
|
827
|
-
assert.equal(calls[0]?.disposition, LEGACY_ALIAS_DISPOSITION);
|
|
828
|
-
const injected = await runControllerOperation({ operation: "repair-legacy-alias", input: JSON.stringify({ lineage: "legacy-alias", actor: "maintainer", reason: "no-op", repository: "/attacker" }) }, native);
|
|
829
|
-
assert.equal(injected.outcome, "native-input-invalid");
|
|
830
|
-
await assert.rejects(runControllerOperation({ operation: "dispose-result", input: "{}" }, native), /operation/);
|
|
831
|
-
});
|
|
832
422
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
// provider-owned admission, so it cannot prove the lens inspected the frozen
|
|
837
|
-
// candidate". There was no capture-result surface at all, so Pi could only
|
|
838
|
-
// finalize a zero-lens low-risk candidate. The suite never caught it because
|
|
839
|
-
// it mocks the finalize response.
|
|
840
|
-
//
|
|
841
|
-
// Two inverse contracts meet here. `repair()` above MUST carry --contract.
|
|
842
|
-
// `capture-result` MUST NOT: it is an additive headless command, not a
|
|
843
|
-
// negotiated repository operation, and the provider's own tokens already
|
|
844
|
-
// carry the repository context -- it accepts that or --cwd, never both. So
|
|
845
|
-
// Pi passes the transition's tokens through verbatim and adds only --input.
|
|
846
|
-
test("captureResult passes the provider tokens through verbatim and carries no --contract", async (t) => {
|
|
847
|
-
t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
|
|
848
|
-
const manifest = {
|
|
849
|
-
schema: "gentle-ai.review-result-artifact/v2",
|
|
850
|
-
capability: "review.native_result_artifact",
|
|
851
|
-
subject_hash: "sha256:" + "a".repeat(64),
|
|
852
|
-
admission_decision: "completed",
|
|
853
|
-
lens: "review-reliability",
|
|
854
|
-
reference: "rref1_" + "b".repeat(64),
|
|
855
|
-
};
|
|
856
|
-
const { adapter, calls } = queuedAdapter([{ stdout: JSON.stringify(manifest) }]);
|
|
857
|
-
const cli = new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
|
|
858
|
-
|
|
859
|
-
const tokens = [
|
|
860
|
-
"--lineage=review-1d5aadacc600e167",
|
|
861
|
-
"--expected-revision=sha256:" + "c".repeat(64),
|
|
862
|
-
"--target=sha256:" + "d".repeat(64),
|
|
863
|
-
"--repository-context=rctx1_" + "e".repeat(64),
|
|
864
|
-
"--lens=review-reliability",
|
|
865
|
-
"--order=0",
|
|
866
|
-
"--subject-hash=" + manifest.subject_hash,
|
|
867
|
-
];
|
|
868
|
-
const captured = await cli.captureResult({ argumentTokens: tokens, resultDocument: JSON.stringify({ subject_hash: manifest.subject_hash, inspection: { status: "completed", paths: ["a.ts"] }, findings: [], evidence: ["reviewed the complete frozen candidate scope"] }) });
|
|
869
|
-
|
|
870
|
-
assert.equal(captured.subjectHash, manifest.subject_hash);
|
|
871
|
-
assert.equal(captured.admissionDecision, "completed");
|
|
872
|
-
|
|
873
|
-
// Exactly one invocation: capture-result is headless and never negotiates,
|
|
874
|
-
// so it must not drag a capabilities preflight along with it.
|
|
875
|
-
assert.equal(calls.length, 1);
|
|
876
|
-
const argv = calls[0]!.arguments;
|
|
877
|
-
assert.deepEqual(argv.slice(0, 2), ["review", "capture-result"]);
|
|
878
|
-
assert.equal(argv.includes("--contract"), false, "capture-result accepts no --contract");
|
|
879
|
-
assert.equal(argv.includes("--cwd"), false, "the provider tokens already carry the repository context");
|
|
880
|
-
// Tokens pass through in order, untouched, and --input is the only addition.
|
|
881
|
-
assert.deepEqual(argv.slice(2, 2 + tokens.length), tokens);
|
|
882
|
-
assert.equal(argv.at(-2), "--input");
|
|
883
|
-
assert.match(argv.at(-1) as string, /\S/);
|
|
884
|
-
assert.equal(argv.length, 2 + tokens.length + 2);
|
|
423
|
+
const reconciliation = await controller.execute("reconcile", { operation: "reconcile-authority", input: JSON.stringify({ predecessorLineage: "predecessor", expectedPredecessorRevision: "revision-1", successorLineage: "successor", expectedSuccessorRevision: "revision-2", actor: "maintainer", reason: "repair edge" }) }, undefined, undefined, interactiveContext(true));
|
|
424
|
+
assert.equal((reconciliation.details as { mutation_outcome?: string }).mutation_outcome, "committed");
|
|
425
|
+
assert.match(String(calls[1]?.maintainerAuthorization), /^gentle-ai\.review-reconcile-authorization\/v1\npredecessor_lineage=predecessor\npredecessor_revision=revision-1\nsuccessor_lineage=successor\nsuccessor_revision=revision-2/);
|
|
885
426
|
});
|
|
886
427
|
|
|
887
|
-
test("
|
|
888
|
-
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
schema: "gentle-ai.review-
|
|
893
|
-
|
|
894
|
-
lineage_id: "review-evidence-lineage",
|
|
895
|
-
authority_revision: `sha256:${"a".repeat(64)}`,
|
|
896
|
-
target_identity: `sha256:${"b".repeat(64)}`,
|
|
897
|
-
candidate_tree: "c".repeat(40),
|
|
898
|
-
paths_digest: `sha256:${"d".repeat(64)}`,
|
|
899
|
-
paths: ["app.ts"],
|
|
900
|
-
ledger_ids: [],
|
|
901
|
-
raw_payload_sha256: `sha256:${"e".repeat(64)}`,
|
|
902
|
-
raw_payload_bytes: 24,
|
|
903
|
-
outcome: "verification_failed",
|
|
904
|
-
record_digest: `sha256:${"f".repeat(64)}`,
|
|
905
|
-
};
|
|
906
|
-
let staged = "";
|
|
907
|
-
const calls: Array<{ arguments: readonly string[] }> = [];
|
|
908
|
-
const cli = new NativeReviewCliV216(async (request) => {
|
|
909
|
-
calls.push({ arguments: request.arguments });
|
|
910
|
-
if (request.arguments[1] === "capabilities") return { stdout: JSON.stringify(capabilitiesBody), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
911
|
-
const inputIndex = request.arguments.indexOf("--input");
|
|
912
|
-
assert.ok(inputIndex >= 0);
|
|
913
|
-
staged = readFileSync(request.arguments[inputIndex + 1]!, "utf8");
|
|
914
|
-
return { stdout: JSON.stringify(record), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
915
|
-
}, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
|
|
916
|
-
const evidence = "focused verification failed\n";
|
|
917
|
-
const captured = await cli.captureEvidence({
|
|
918
|
-
cwd: "/repo",
|
|
919
|
-
lineageId: record.lineage_id,
|
|
920
|
-
targetIdentity: record.target_identity,
|
|
921
|
-
expectedRevision: record.authority_revision,
|
|
922
|
-
outcome: "verification_failed",
|
|
923
|
-
evidenceDocument: evidence,
|
|
924
|
-
});
|
|
925
|
-
assert.equal(staged, evidence);
|
|
926
|
-
assert.equal(captured.recordDigest, record.record_digest);
|
|
927
|
-
assert.equal(captured.outcome, "verification_failed");
|
|
928
|
-
const argv = calls[1]!.arguments;
|
|
929
|
-
assert.deepEqual(argv.slice(0, 2), ["review", "capture-evidence"]);
|
|
930
|
-
assert.equal(argv.includes("--contract"), false);
|
|
931
|
-
assert.equal(argv[argv.indexOf("--outcome") + 1], "verification_failed");
|
|
428
|
+
test("RECOVER requires a live UI decision and RECOVER_LOCK requires its owner binding", async () => {
|
|
429
|
+
const input = { predecessorLineage: "predecessor", expectedPredecessorRevision: "revision-1", successorLineage: "successor", disposition: "invalidated", actor: "maintainer", reason: "recover authority" };
|
|
430
|
+
const native = {
|
|
431
|
+
targetStatus: async () => recoveryStatus(),
|
|
432
|
+
recover: async () => ({ record: { schema: "gentle-ai.review-recovery/v1" } }),
|
|
433
|
+
reclaim: async () => ({ record: { schema: "gentle-ai.review-reclaim-audit/v1" } }),
|
|
434
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
|
|
932
435
|
await assert.rejects(
|
|
933
|
-
|
|
934
|
-
/
|
|
436
|
+
__testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify(input) }, process.cwd(), native),
|
|
437
|
+
/fresh explicit authorization through the interactive Pi UI/,
|
|
935
438
|
);
|
|
936
|
-
assert.
|
|
439
|
+
await assert.rejects(
|
|
440
|
+
__testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify(input) }, process.cwd(), native, undefined, undefined, interactiveContext(false)),
|
|
441
|
+
/not explicitly authorized/,
|
|
442
|
+
);
|
|
443
|
+
await assert.rejects(
|
|
444
|
+
__testing.executeReviewControllerOperation({ operation: "recover-lock", input: JSON.stringify({ lineage: "stuck", actor: "maintainer", reason: "stale" }) }, process.cwd(), native),
|
|
445
|
+
/ownerHash/,
|
|
446
|
+
);
|
|
447
|
+
const missingReclaim = await __testing.executeReviewControllerOperation({ operation: "recover-lock", input: JSON.stringify({ ownerHash: "a".repeat(64) }) }, process.cwd(), native);
|
|
448
|
+
assert.deepEqual(missingReclaim.missing_input, ["lineage", "actor", "reason"]);
|
|
449
|
+
const recovered = await __testing.executeReviewControllerOperation({ operation: "recover-lock", input: JSON.stringify({ ownerHash: "a".repeat(64), lineage: "stuck", actor: "maintainer", reason: "stale" }) }, process.cwd(), native);
|
|
450
|
+
assert.equal(recovered.mutation_outcome, "committed");
|
|
937
451
|
});
|
|
938
452
|
|
|
939
|
-
test("
|
|
940
|
-
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
};
|
|
949
|
-
const {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
]);
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
const argv = calls[1]!.arguments;
|
|
958
|
-
assert.equal(argv.includes("--result"), false, "--result is retired; results reach authority through capture-result");
|
|
959
|
-
assert.ok(argv.some((token) => token === "--captured-results" || token.startsWith("--captured-results=")), "finalize must tell the provider to discover the captured results");
|
|
960
|
-
assert.ok(argv.includes("--contract"), "finalize IS negotiated, unlike capture-result");
|
|
453
|
+
test("REPAIR_LEGACY_ALIAS derives its immutable target from fresh native inventory", async () => {
|
|
454
|
+
const calls: Array<Record<string, unknown>> = [];
|
|
455
|
+
const native = {
|
|
456
|
+
reviewStatus: async () => ({
|
|
457
|
+
repository: "/canonical/repository",
|
|
458
|
+
complete: true,
|
|
459
|
+
entries: [{ version: "legacy-v1", status: "invalid", lineageId: "legacy-alias", revision: SHA, problems: [NATIVE_REVIEW_LEGACY_ALIAS_REPAIR.DIAGNOSTIC] }],
|
|
460
|
+
}),
|
|
461
|
+
repairLegacyAlias: async (request: Record<string, unknown>) => { calls.push(request); return { record: { schema: "gentle-ai.review-repair-audit/v1" } }; },
|
|
462
|
+
} as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
|
|
463
|
+
const result = await __testing.executeReviewControllerOperation({ operation: "repair-legacy-alias", input: JSON.stringify({ lineage: "legacy-alias", actor: "maintainer", reason: "repair alias" }) }, process.cwd(), native, undefined, undefined, interactiveContext(true));
|
|
464
|
+
assert.equal(result.mutation_outcome, "committed");
|
|
465
|
+
assert.equal(calls[0]?.repository, "/canonical/repository");
|
|
466
|
+
assert.equal(calls[0]?.expectedRevision, SHA);
|
|
467
|
+
assert.match(String(calls[0]?.maintainerAuthorization), /^gentle-ai\.review-legacy-alias-repair-authorization\/v1\nrepository=\/canonical\/repository/);
|
|
468
|
+
const injected = await __testing.executeReviewControllerOperation({ operation: "repair-legacy-alias", input: JSON.stringify({ lineage: "legacy-alias", actor: "maintainer", reason: "repair alias", repository: "/injected" }) }, process.cwd(), native, undefined, undefined, interactiveContext(true));
|
|
469
|
+
assert.equal(injected.outcome, "native-input-invalid");
|
|
961
470
|
});
|