gentle-pi 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -80
- package/assets/agents/review-readability.md +0 -2
- package/assets/agents/review-reliability.md +0 -2
- package/assets/agents/review-resilience.md +0 -2
- package/assets/agents/review-risk.md +0 -2
- package/assets/agents/sdd-apply.md +5 -3
- package/assets/agents/sdd-proposal.md +2 -0
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-status.md +4 -4
- package/assets/agents/sdd-tasks.md +2 -3
- package/assets/agents/sdd-verify.md +21 -1
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +54 -248
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +22 -38
- package/assets/sdd-orchestrator-workflow.md +30 -37
- package/assets/support/sdd-status-contract.md +7 -7
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/docs/native-authority-architecture.md +9 -11
- package/docs/review-integration.md +27 -373
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +1778 -3673
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +4 -8
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +372 -940
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/review-candidate-view.ts +341 -132
- package/lib/review-host-relay.ts +210 -68
- package/lib/review-integration-v2.ts +839 -307
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/sdd-preflight.ts +177 -66
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -82
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +322 -890
- package/runtime/review-integration-v2.mjs +789 -257
- package/runtime/review-relay-contract.mjs +1 -1
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
- package/scripts/gentle-ai-installer.mjs +75 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
- package/scripts/test-packed-runner.mjs +4 -7
- package/scripts/verify-package-files.mjs +12 -13
- package/skills/_shared/review-ledger-contract.md +8 -14
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/judgment-day/SKILL.md +5 -7
- package/skills/rdd-defect-workflow/SKILL.md +3 -3
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +15 -1168
- package/tests/delegated-key-learnings-contract.test.ts +8 -6
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary.test.ts +51 -228
- package/tests/gentle-ai-installer.test.ts +137 -47
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +340 -4
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +28 -1
- package/tests/native-review-cli.test.ts +586 -962
- package/tests/native-review-consent.test.ts +150 -66
- package/tests/native-review-parity-runtime.test.ts +80 -359
- package/tests/native-review-parity.test.ts +674 -925
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +132 -88
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +37 -35
- package/tests/provider-defect-handoff.test.ts +15 -118
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +409 -3
- package/tests/review-compact-contract.test.ts +4 -30
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +363 -1450
- package/tests/review-controller-native-routing.test.ts +1251 -5642
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +222 -71
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +112 -153
- package/tests/review-dispatch-hydration-gap.test.ts +1 -53
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +117 -82
- package/tests/review-host-relay.test.ts +242 -8
- package/tests/review-integration-v2-forward.test.ts +576 -553
- package/tests/review-integration-v2.test.ts +239 -165
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +97 -35
- package/tests/review-recovered-lineage-routing.test.ts +0 -47
- package/tests/review-relay-transport-agent.test.ts +119 -46
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +361 -196
- package/tests/sdd-agent-tools.test.ts +36 -0
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +5 -8
- package/tests/writer-edit-surface-scope.test.ts +230 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/lib/git-commit-transaction.ts +0 -861
- package/runtime/git-commit-transaction.mjs +0 -862
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -530
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
2
|
import { execFile } from "node:child_process";
|
|
4
|
-
import {
|
|
3
|
+
import { mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
4
|
+
import { mkdir, mkdtemp, readFile, realpath, rm, symlink, writeFile } from "node:fs/promises";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
7
7
|
import baseTest from "node:test";
|
|
@@ -9,21 +9,35 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
import { promisify } from "node:util";
|
|
10
10
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
GENTLE_AI_DEV_BINARY_ENV,
|
|
14
|
+
resolveGentleAiBinary,
|
|
15
|
+
type GentleAiDevBinaryEnvironment,
|
|
16
|
+
} from "../lib/gentle-ai-binary.ts";
|
|
13
17
|
import { NativeReviewCliV216 } from "../lib/native-review-cli.ts";
|
|
14
|
-
import { NativeReviewCliV216 as RuntimeNativeReviewCliV216 } from "../runtime/native-review-cli.mjs";
|
|
15
18
|
import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
|
|
16
|
-
import {
|
|
19
|
+
import { decodeReviewLastEventClosureV1 } from "../lib/review-integration-v2.ts";
|
|
20
|
+
import { decodeReviewLastEventClosureV1 as decodeRuntimeReviewLastEventClosureV1 } from "../runtime/review-integration-v2.mjs";
|
|
17
21
|
import { requireNativeBinary } from "./support/native-binary-gate.ts";
|
|
18
22
|
|
|
19
23
|
const execFileAsync = promisify(execFile);
|
|
20
24
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
25
|
+
const pinnedBinaryHome = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-pinned-runtime-home-")));
|
|
26
|
+
const pinnedBinaryEnvironment: GentleAiDevBinaryEnvironment = {
|
|
27
|
+
env: { ...process.env },
|
|
28
|
+
home: pinnedBinaryHome,
|
|
29
|
+
};
|
|
30
|
+
delete pinnedBinaryEnvironment.env[GENTLE_AI_DEV_BINARY_ENV];
|
|
31
|
+
delete pinnedBinaryEnvironment.env.GENTLE_PI_CONFIG_HOME;
|
|
32
|
+
baseTest.after(() => rmSync(pinnedBinaryHome, { recursive: true, force: true }));
|
|
21
33
|
// The parity suite exercises the published official binary; it skips while a
|
|
22
34
|
// re-pinned release's archives and digest table are still pending, because the
|
|
23
35
|
// pinned package-local binary cannot be installed or integrity-verified yet.
|
|
36
|
+
// Dev-binary override state is intentionally excluded: these assertions verify
|
|
37
|
+
// the package pin, while explicit dev-binary behavior belongs to its own suite.
|
|
24
38
|
const resolvedBinary = (() => {
|
|
25
39
|
try {
|
|
26
|
-
return resolveGentleAiBinary(packageRoot, process.platform);
|
|
40
|
+
return resolveGentleAiBinary(packageRoot, process.platform, undefined, pinnedBinaryEnvironment);
|
|
27
41
|
} catch {
|
|
28
42
|
return undefined;
|
|
29
43
|
}
|
|
@@ -32,12 +46,6 @@ const nativeBinaryGate = requireNativeBinary({ resolvedBinary, digestsPinned: tr
|
|
|
32
46
|
if (!nativeBinaryGate.run) console.log(`native-review-parity-runtime: ${nativeBinaryGate.reason}`);
|
|
33
47
|
const test = nativeBinaryGate.run ? baseTest : baseTest.skip;
|
|
34
48
|
const binary = resolvedBinary ?? "";
|
|
35
|
-
const OFFICIAL_BINARY_SHA256 = resolveGentleAiReleaseAsset(process.platform, process.arch).binarySha256;
|
|
36
|
-
const REVIEWED_PATHS = ["tracked.txt", "initially-untracked.txt"] as const;
|
|
37
|
-
// Golden captured by the clean external-artifact differential fixture for v2.1.3.
|
|
38
|
-
// This is released runtime output, not a locally reconstructed authority digest.
|
|
39
|
-
const CLEAN_DIFFERENTIAL_PUBLISHED_PATHS_DIGEST = "sha256:5d91d7650fcbd1165e9cd88c144bf28d82913e3537abd7b4fdc8ad0adb9eab9c";
|
|
40
|
-
const REVIEWED_MODES = ["100644", "100644"];
|
|
41
49
|
|
|
42
50
|
interface CommandResult {
|
|
43
51
|
exitCode: number;
|
|
@@ -49,46 +57,6 @@ interface RegisteredController {
|
|
|
49
57
|
execute: (toolCallId: string, params: unknown, signal: AbortSignal | undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<{ details?: unknown }>;
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
interface ReviewStart {
|
|
53
|
-
lineage_id: string;
|
|
54
|
-
selected_lenses: string[];
|
|
55
|
-
action?: string;
|
|
56
|
-
state?: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
interface ReviewFinalize {
|
|
60
|
-
lineage_id: string;
|
|
61
|
-
state: string;
|
|
62
|
-
store_revision: string;
|
|
63
|
-
receipt_path: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface ReviewAuthorityEntry {
|
|
67
|
-
version: string;
|
|
68
|
-
lineage_id: string;
|
|
69
|
-
status: string;
|
|
70
|
-
state: string;
|
|
71
|
-
revision: string;
|
|
72
|
-
problems: unknown[];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
interface ReviewGateContext {
|
|
76
|
-
candidate_tree: string;
|
|
77
|
-
paths_digest: string;
|
|
78
|
-
denial?: ReviewDenial;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
interface ReviewDenial {
|
|
82
|
-
stage: string;
|
|
83
|
-
code: string;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
interface ReviewGateResult {
|
|
87
|
-
result: string;
|
|
88
|
-
allowed: boolean;
|
|
89
|
-
context: ReviewGateContext;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
60
|
// Every test in this file drives the real pinned binary through a full review
|
|
93
61
|
// lifecycle, and gentle-ai v2.4.0 made receipt-driven development opt-in: an
|
|
94
62
|
// install whose global mode was never set now resolves to off, and START is
|
|
@@ -103,19 +71,46 @@ interface ReviewGateResult {
|
|
|
103
71
|
//
|
|
104
72
|
// So each test owns a sandbox HOME and opts in the same way a user does,
|
|
105
73
|
// exactly as gentle-ai did for its own lifecycle fixtures in the commit that
|
|
106
|
-
// flipped the default. The
|
|
107
|
-
//
|
|
108
|
-
//
|
|
74
|
+
// flipped the default. The extension-registered controller path inherits this
|
|
75
|
+
// process environment, so HOME and XDG state are restored afterwards. The
|
|
76
|
+
// global enable runs from a disposable repository: this package worktree may
|
|
77
|
+
// intentionally have clone-local mode off, which must never participate in the
|
|
78
|
+
// fixture's lifecycle.
|
|
109
79
|
async function reviewEnabledHome(t: baseTest.TestContext): Promise<string> {
|
|
110
|
-
const home = await mkdtemp(join(tmpdir(), "gentle-pi-review-home-"));
|
|
80
|
+
const home = await realpath(await mkdtemp(join(tmpdir(), "gentle-pi-review-home-")));
|
|
81
|
+
const lifecycleCwd = await realpath(await mkdtemp(join(tmpdir(), "gentle-pi-review-lifecycle-")));
|
|
82
|
+
const xdgConfigHome = join(home, ".config");
|
|
83
|
+
const xdgDataHome = join(home, ".local", "share");
|
|
84
|
+
const xdgCacheHome = join(home, ".cache");
|
|
111
85
|
const previousHome = process.env.HOME;
|
|
86
|
+
const previousXdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
87
|
+
const previousXdgDataHome = process.env.XDG_DATA_HOME;
|
|
88
|
+
const previousXdgCacheHome = process.env.XDG_CACHE_HOME;
|
|
112
89
|
process.env.HOME = home;
|
|
90
|
+
process.env.XDG_CONFIG_HOME = xdgConfigHome;
|
|
91
|
+
process.env.XDG_DATA_HOME = xdgDataHome;
|
|
92
|
+
process.env.XDG_CACHE_HOME = xdgCacheHome;
|
|
93
|
+
const environment = {
|
|
94
|
+
...process.env,
|
|
95
|
+
HOME: home,
|
|
96
|
+
XDG_CONFIG_HOME: xdgConfigHome,
|
|
97
|
+
XDG_DATA_HOME: xdgDataHome,
|
|
98
|
+
XDG_CACHE_HOME: xdgCacheHome,
|
|
99
|
+
};
|
|
113
100
|
t.after(async () => {
|
|
114
101
|
if (previousHome === undefined) delete process.env.HOME;
|
|
115
102
|
else process.env.HOME = previousHome;
|
|
103
|
+
if (previousXdgConfigHome === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
104
|
+
else process.env.XDG_CONFIG_HOME = previousXdgConfigHome;
|
|
105
|
+
if (previousXdgDataHome === undefined) delete process.env.XDG_DATA_HOME;
|
|
106
|
+
else process.env.XDG_DATA_HOME = previousXdgDataHome;
|
|
107
|
+
if (previousXdgCacheHome === undefined) delete process.env.XDG_CACHE_HOME;
|
|
108
|
+
else process.env.XDG_CACHE_HOME = previousXdgCacheHome;
|
|
116
109
|
await rm(home, { recursive: true, force: true });
|
|
110
|
+
await rm(lifecycleCwd, { recursive: true, force: true });
|
|
117
111
|
});
|
|
118
|
-
|
|
112
|
+
await run("git", ["init", "--quiet"], lifecycleCwd, false, environment);
|
|
113
|
+
const enabled = await run(binary, ["review", "mode", "enable", "--scope", "global", "--cwd", lifecycleCwd, "--json"], lifecycleCwd, false, environment);
|
|
119
114
|
// Assert the opt-in landed rather than assuming it. A silently ineffective
|
|
120
115
|
// enable would put these tests straight back to depending on ambient state,
|
|
121
116
|
// which is the exact failure this helper exists to remove.
|
|
@@ -134,291 +129,25 @@ async function run(command: string, arguments_: readonly string[], cwd: string,
|
|
|
134
129
|
}
|
|
135
130
|
}
|
|
136
131
|
|
|
137
|
-
async
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const entries = await stagedEntries(repository);
|
|
145
|
-
const stagedPaths = entries.map((entry) => entry.split("\t", 2)[1]);
|
|
146
|
-
const stagedModes = entries.map((entry) => entry.split(" ", 1)[0]);
|
|
147
|
-
assert.equal(stagedTree, candidateTree, `staged tree must match frozen candidate; entries: ${entries.join(", ")}`);
|
|
148
|
-
assert.deepEqual(stagedPaths, [...REVIEWED_PATHS].toSorted());
|
|
149
|
-
assert.deepEqual(stagedModes, REVIEWED_MODES);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function assertScopeChanged(repository: string, lineageId: string): Promise<void> {
|
|
153
|
-
const deniedCommand = await run(binary, ["review", "validate", "--gate", "pre-commit", "--cwd", repository, "--lineage", lineageId], repository, true);
|
|
154
|
-
const denied = JSON.parse(deniedCommand.stdout) as ReviewGateResult;
|
|
155
|
-
assert.equal(deniedCommand.exitCode, 1);
|
|
156
|
-
assert.equal(denied.result, "scope-changed");
|
|
157
|
-
assert.equal(denied.allowed, false);
|
|
158
|
-
assert.deepEqual(denied.context.denial, { stage: "receipt-binding", code: "candidate-or-paths-mismatch" });
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async function restoreCandidate(repository: string, candidateTree: string): Promise<void> {
|
|
162
|
-
await writeFile(join(repository, "tracked.txt"), "candidate\n");
|
|
163
|
-
await rm(join(repository, "extra-reviewed-path.txt"), { force: true });
|
|
164
|
-
await run("git", ["reset", "--", "extra-reviewed-path.txt"], repository);
|
|
165
|
-
await run("git", ["add", "--", ...REVIEWED_PATHS], repository);
|
|
166
|
-
await assertPublishedProjection(repository, candidateTree);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async function reviewStatus(repository: string): Promise<Record<string, unknown>> {
|
|
170
|
-
return JSON.parse((await run(binary, ["review", "status", "--cwd", repository], repository)).stdout) as Record<string, unknown>;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function authorityInventory(status: Record<string, unknown>): ReviewAuthorityEntry[] {
|
|
174
|
-
assert.ok(Array.isArray(status.entries), "review status must expose authority entries");
|
|
175
|
-
return status.entries.map((entry) => {
|
|
176
|
-
assert.ok(entry !== null && typeof entry === "object", "authority entry must be an object");
|
|
177
|
-
const candidate = entry as Record<string, unknown>;
|
|
178
|
-
for (const key of ["version", "lineage_id", "status", "state", "revision"] as const) {
|
|
179
|
-
assert.equal(typeof candidate[key], "string", `authority entry ${key} must be stable text`);
|
|
180
|
-
}
|
|
181
|
-
assert.ok(Array.isArray(candidate.problems), "authority entry problems must be an array");
|
|
182
|
-
return {
|
|
183
|
-
version: candidate.version as string,
|
|
184
|
-
lineage_id: candidate.lineage_id as string,
|
|
185
|
-
status: candidate.status as string,
|
|
186
|
-
state: candidate.state as string,
|
|
187
|
-
revision: candidate.revision as string,
|
|
188
|
-
problems: candidate.problems,
|
|
189
|
-
};
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Every reviewer result reaches authority through `review capture-result`.
|
|
194
|
-
// `review finalize --result` was removed because a result handed over that way
|
|
195
|
-
// carries no provider-owned admission, so it cannot prove the lens inspected
|
|
196
|
-
// the frozen candidate. The bindings are never guessed here: each one comes
|
|
197
|
-
// from the collect transition the provider itself publishes, which is also
|
|
198
|
-
// what the removal message tells a caller to do.
|
|
199
|
-
//
|
|
200
|
-
// Artifacts stay outside the repository on purpose. Writing them inside changes
|
|
201
|
-
// the workspace snapshot between deriving a target and using it, and authority
|
|
202
|
-
// correctly rejects the stale target that results.
|
|
203
|
-
async function nextTransition(repository: string): Promise<Record<string, unknown>> {
|
|
204
|
-
const status = JSON.parse((await run(binary, ["review", "status", "--contract", "gentle-ai.review-integration/v2", "--cwd", repository, "--projection", "workspace", "--next-transition"], repository)).stdout) as Record<string, unknown>;
|
|
205
|
-
return status.next_transition as Record<string, unknown>;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async function captureSelectedLenses(repository: string, artifacts: string, evidenceName: string): Promise<void> {
|
|
209
|
-
for (let guard = 0; guard < 8; guard += 1) {
|
|
210
|
-
const transition = await nextTransition(repository);
|
|
211
|
-
const collect = transition.collect as { inputs?: readonly Record<string, unknown>[] } | undefined;
|
|
212
|
-
if (transition.kind !== "collect" || collect?.inputs === undefined) return;
|
|
213
|
-
for (const [index, input] of collect.inputs.entries()) {
|
|
214
|
-
const subject = input.artifact_subject as { subject_hash: string };
|
|
215
|
-
const manifest = input.changed_path_manifest as readonly { path: string }[];
|
|
216
|
-
const result = join(artifacts, `${evidenceName}-capture-${index}.json`);
|
|
217
|
-
await writeFile(result, JSON.stringify({
|
|
218
|
-
subject_hash: subject.subject_hash,
|
|
219
|
-
inspection: { status: "completed", paths: manifest.map((entry) => entry.path) },
|
|
220
|
-
findings: [],
|
|
221
|
-
evidence: ["reviewed the complete frozen candidate scope"],
|
|
222
|
-
}));
|
|
223
|
-
// The published tokens already carry the repository context, and
|
|
224
|
-
// capture-result accepts that or --cwd, never both.
|
|
225
|
-
const tokens = (input.arguments as readonly { token: string }[]).map((argument) => argument.token);
|
|
226
|
-
const captured = await run(binary, ["review", "capture-result", ...tokens, "--input", result], repository);
|
|
227
|
-
assert.equal(captured.exitCode, 0, `capture-result failed: ${captured.stderr}`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
assert.fail("collect transitions did not converge");
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async function finalizeEmptyReview(repository: string, artifacts: string, started: ReviewStart, evidenceName: string): Promise<CommandResult> {
|
|
234
|
-
const evidence = join(artifacts, evidenceName);
|
|
235
|
-
await writeFile(evidence, `evidence for ${started.lineage_id}\n`);
|
|
236
|
-
await captureSelectedLenses(repository, artifacts, evidenceName);
|
|
237
|
-
return run(binary, ["review", "finalize", "--cwd", repository, "--lineage", started.lineage_id, "--captured-results=true", "--evidence", evidence], repository);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
test("official pinned package runtime authorizes an unchanged linked-view candidate and denies a changed staging tree", async (t) => {
|
|
241
|
-
await reviewEnabledHome(t);
|
|
242
|
-
assert.equal(createHash("sha256").update(await readFile(binary)).digest("hex"), OFFICIAL_BINARY_SHA256);
|
|
243
|
-
assert.deepEqual(await run(binary, ["version"], packageRoot), { exitCode: 0, stdout: `gentle-ai ${GENTLE_AI_VERSION}\n`, stderr: "" });
|
|
244
|
-
|
|
245
|
-
const workspace = await mkdtemp(join(tmpdir(), "gentle-pi-v216-parity-"));
|
|
246
|
-
const repository = join(workspace, "repository");
|
|
247
|
-
const view = join(workspace, "candidate-view");
|
|
248
|
-
const artifacts = join(workspace, "artifacts");
|
|
249
|
-
const temporaryIndex = join(workspace, "controller.index");
|
|
250
|
-
t.after(async () => rm(workspace, { recursive: true, force: true }));
|
|
251
|
-
|
|
252
|
-
await mkdir(repository);
|
|
253
|
-
await mkdir(artifacts);
|
|
254
|
-
await run("git", ["init", "--initial-branch=main"], repository);
|
|
255
|
-
await run("git", ["config", "user.email", "test@example.invalid"], repository);
|
|
256
|
-
await run("git", ["config", "user.name", "Gentle Pi test"], repository);
|
|
257
|
-
await writeFile(join(repository, "tracked.txt"), "base\n");
|
|
258
|
-
await run("git", ["add", "--", "tracked.txt"], repository);
|
|
259
|
-
await run("git", ["commit", "-m", "base"], repository);
|
|
260
|
-
const native = new RuntimeNativeReviewCliV216(async (request) => {
|
|
261
|
-
const command = await run(binary, request.arguments, request.cwd, true);
|
|
262
|
-
return { ...command, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
263
|
-
});
|
|
264
|
-
assert.equal((await native.reviewStatus({ cwd: repository })).status, "clean");
|
|
265
|
-
await writeFile(join(repository, "tracked.txt"), "candidate\n");
|
|
266
|
-
await writeFile(join(repository, "initially-untracked.txt"), "included\n");
|
|
267
|
-
|
|
268
|
-
const temporaryIndexEnvironment = { ...process.env, GIT_INDEX_FILE: temporaryIndex };
|
|
269
|
-
await run("git", ["read-tree", "HEAD"], repository, false, temporaryIndexEnvironment);
|
|
270
|
-
await run("git", ["add", "--", ...REVIEWED_PATHS], repository, false, temporaryIndexEnvironment);
|
|
271
|
-
const candidateTree = (await run("git", ["write-tree"], repository, false, temporaryIndexEnvironment)).stdout.trim();
|
|
272
|
-
await run("git", ["worktree", "add", "--detach", view, "HEAD"], repository);
|
|
273
|
-
await run("git", ["read-tree", candidateTree], view);
|
|
274
|
-
await run("git", ["checkout-index", "--all", "--force"], view);
|
|
275
|
-
|
|
276
|
-
assert.equal((await readFile(join(view, "tracked.txt"), "utf8")), "candidate\n");
|
|
277
|
-
assert.equal((await readFile(join(view, "initially-untracked.txt"), "utf8")), "included\n");
|
|
278
|
-
const started = JSON.parse((await run(binary, ["review", "start", "--cwd", view], view)).stdout) as ReviewStart;
|
|
279
|
-
|
|
280
|
-
const evidence = join(artifacts, "final-evidence.txt");
|
|
281
|
-
await writeFile(evidence, "linked-view parity probe\n");
|
|
282
|
-
// The linked view is the root authority froze, so its collect bindings are
|
|
283
|
-
// derived from the view, not from the contributor repository.
|
|
284
|
-
await captureSelectedLenses(view, artifacts, "final-evidence.txt");
|
|
285
|
-
await run(binary, ["review", "finalize", "--cwd", view, "--lineage", started.lineage_id, "--captured-results=true", "--evidence", evidence], view);
|
|
286
|
-
|
|
287
|
-
await run("git", ["add", "--", ...REVIEWED_PATHS], repository);
|
|
288
|
-
await assertPublishedProjection(repository, candidateTree);
|
|
289
|
-
const allowed = JSON.parse((await run(binary, ["review", "validate", "--gate", "pre-commit", "--cwd", repository, "--lineage", started.lineage_id], repository)).stdout) as ReviewGateResult;
|
|
290
|
-
assert.equal(allowed.result, "allow");
|
|
291
|
-
assert.equal(allowed.allowed, true);
|
|
292
|
-
assert.equal(allowed.context.candidate_tree, candidateTree);
|
|
293
|
-
assert.equal(allowed.context.paths_digest, CLEAN_DIFFERENTIAL_PUBLISHED_PATHS_DIGEST);
|
|
294
|
-
t.diagnostic(JSON.stringify({ candidateTree, publishedPathsDigest: CLEAN_DIFFERENTIAL_PUBLISHED_PATHS_DIGEST, reviewedPaths: REVIEWED_PATHS, reviewedModes: REVIEWED_MODES, binary, startCwd: view, finalizeCwd: view, artifacts }));
|
|
295
|
-
|
|
296
|
-
await writeFile(join(repository, "tracked.txt"), "changed-after-review\n");
|
|
297
|
-
await run("git", ["add", "--", "tracked.txt"], repository);
|
|
298
|
-
assert.notEqual((await run("git", ["write-tree"], repository)).stdout.trim(), candidateTree);
|
|
299
|
-
await assertScopeChanged(repository, started.lineage_id);
|
|
300
|
-
await restoreCandidate(repository, candidateTree);
|
|
301
|
-
|
|
302
|
-
await writeFile(join(repository, "extra-reviewed-path.txt"), "not in the frozen candidate\n");
|
|
303
|
-
await run("git", ["add", "extra-reviewed-path.txt"], repository);
|
|
304
|
-
assert.notEqual((await run("git", ["write-tree"], repository)).stdout.trim(), candidateTree);
|
|
305
|
-
assert.deepEqual((await stagedEntries(repository)).map((entry) => entry.split("\t", 2)[1]), [...REVIEWED_PATHS, "extra-reviewed-path.txt"].toSorted());
|
|
306
|
-
await assertScopeChanged(repository, started.lineage_id);
|
|
307
|
-
await restoreCandidate(repository, candidateTree);
|
|
308
|
-
|
|
309
|
-
await chmod(join(repository, "tracked.txt"), 0o755);
|
|
310
|
-
await run("git", ["add", "--", "tracked.txt"], repository);
|
|
311
|
-
const modeDrifted = (await stagedEntries(repository)).some((entry) => entry === "100755" || entry.startsWith("100755 "));
|
|
312
|
-
if (modeDrifted) {
|
|
313
|
-
assert.notEqual((await run("git", ["write-tree"], repository)).stdout.trim(), candidateTree);
|
|
314
|
-
await assertScopeChanged(repository, started.lineage_id);
|
|
315
|
-
} else {
|
|
316
|
-
t.diagnostic("skipped native mode-drift denial because this repository/platform does not stage executable-bit changes");
|
|
317
|
-
}
|
|
318
|
-
await chmod(join(repository, "tracked.txt"), 0o644);
|
|
319
|
-
await restoreCandidate(repository, candidateTree);
|
|
132
|
+
test("generated runtime decoder consumes the captured terminal closure directly", async () => {
|
|
133
|
+
const fixture = JSON.parse(await readFile(join(packageRoot, "tests", "fixtures", "devbinary", "last-event-capture-result-approved.captured.json"), "utf8"));
|
|
134
|
+
const source = decodeReviewLastEventClosureV1(fixture);
|
|
135
|
+
const runtime = decodeRuntimeReviewLastEventClosureV1(fixture);
|
|
136
|
+
assert.deepEqual(runtime, source);
|
|
137
|
+
assert.equal(runtime.operation, "review/capture-result");
|
|
138
|
+
assert.equal(runtime.state, "approved");
|
|
320
139
|
});
|
|
321
140
|
|
|
322
|
-
test("
|
|
141
|
+
test("registered gentle_review surfaces the package-pinned Pi transport refusal before native START for a safe internal symlink candidate", async (t) => {
|
|
323
142
|
await reviewEnabledHome(t);
|
|
324
|
-
const workspace = await mkdtemp(join(tmpdir(), "gentle-pi-v215-
|
|
143
|
+
const workspace = await realpath(await mkdtemp(join(tmpdir(), "gentle-pi-v215-symlink-candidate-")));
|
|
325
144
|
const repository = join(workspace, "repository");
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
await run("git", ["config", "user.email", "test@example.invalid"], repository);
|
|
333
|
-
await run("git", ["config", "user.name", "Gentle Pi test"], repository);
|
|
334
|
-
await writeFile(join(repository, "tracked.txt"), "base\n");
|
|
335
|
-
await run("git", ["add", "--", "tracked.txt"], repository);
|
|
336
|
-
await run("git", ["commit", "-m", "base"], repository);
|
|
337
|
-
|
|
338
|
-
await writeFile(join(repository, "tracked.txt"), "candidate one\n");
|
|
339
|
-
const first = JSON.parse((await run(binary, ["review", "start", "--cwd", repository], repository)).stdout) as ReviewStart;
|
|
340
|
-
const firstInventory = authorityInventory(await reviewStatus(repository));
|
|
341
|
-
const firstReplay = JSON.parse((await run(binary, ["review", "start", "--cwd", repository], repository)).stdout) as ReviewStart;
|
|
342
|
-
const firstReplayInventory = authorityInventory(await reviewStatus(repository));
|
|
343
|
-
assert.equal(firstReplay.lineage_id, first.lineage_id, "replaying an exact START must reuse the frozen lineage");
|
|
344
|
-
assert.deepEqual(firstReplayInventory, firstInventory, "replaying an exact START must not create durable authority");
|
|
345
|
-
|
|
346
|
-
const firstFinalized = JSON.parse((await finalizeEmptyReview(repository, artifacts, first, "first-evidence.txt")).stdout) as ReviewFinalize;
|
|
347
|
-
const firstFinalizedInventory = authorityInventory(await reviewStatus(repository));
|
|
348
|
-
// v2.1.7 terminal replay contract: replay uses the exact explicit lineage with
|
|
349
|
-
// no mutation inputs; re-sending reviewer results after approval is rejected.
|
|
350
|
-
const repeatedResults = await finalizeEmptyReview(repository, artifacts, first, "first-evidence.txt").catch((error: NodeJS.ErrnoException & { stderr?: string }) => error);
|
|
351
|
-
assert.match((repeatedResults as { stderr?: string }).stderr ?? "", /terminal review finalize accepts no review inputs; exact replay requires only --lineage/, "terminal authority must reject replayed reviewer results");
|
|
352
|
-
const firstFinalizeReplay = JSON.parse((await run(binary, ["review", "finalize", "--cwd", repository, "--lineage", first.lineage_id], repository)).stdout) as ReviewFinalize;
|
|
353
|
-
const firstFinalizeReplayInventory = authorityInventory(await reviewStatus(repository));
|
|
354
|
-
assert.equal(firstFinalized.lineage_id, first.lineage_id);
|
|
355
|
-
assert.equal(firstFinalized.state, "approved");
|
|
356
|
-
assert.match(firstFinalized.store_revision, /^sha256:[a-f0-9]{64}$/);
|
|
357
|
-
assert.equal(firstFinalizeReplay.store_revision, firstFinalized.store_revision, "replaying an exact FINALIZE must reuse its receipt revision");
|
|
358
|
-
assert.equal(firstFinalizeReplay.receipt_path, firstFinalized.receipt_path, "replaying an exact FINALIZE must reuse its receipt location");
|
|
359
|
-
assert.deepEqual(firstFinalizeReplayInventory, firstFinalizedInventory, "replaying an exact FINALIZE must not create a durable receipt or authority");
|
|
360
|
-
assert.deepEqual(firstFinalizedInventory, [{ version: "compact-v2", lineage_id: first.lineage_id, status: "approved", state: "approved", revision: firstFinalized.store_revision, problems: [] }]);
|
|
361
|
-
|
|
362
|
-
await run("git", ["add", "--", "tracked.txt"], repository);
|
|
363
|
-
const firstCandidateTree = (await run("git", ["write-tree"], repository)).stdout.trim();
|
|
364
|
-
const firstAllowed = JSON.parse((await run(binary, ["review", "validate", "--gate", "pre-commit", "--cwd", repository, "--lineage", first.lineage_id], repository)).stdout) as ReviewGateResult;
|
|
365
|
-
assert.equal(firstAllowed.result, "allow");
|
|
366
|
-
assert.equal(firstAllowed.context.candidate_tree, firstCandidateTree);
|
|
367
|
-
|
|
368
|
-
await writeFile(join(repository, "tracked.txt"), "candidate two\n");
|
|
369
|
-
await run("git", ["add", "--", "tracked.txt"], repository);
|
|
370
|
-
const secondCandidateTree = (await run("git", ["write-tree"], repository)).stdout.trim();
|
|
371
|
-
assert.notEqual(secondCandidateTree, firstCandidateTree);
|
|
372
|
-
const authorityBeforeCompetingStart = authorityInventory(await reviewStatus(repository));
|
|
373
|
-
const competingStart = await run(binary, ["review", "start", "--cwd", repository, "--lineage", first.lineage_id], repository);
|
|
374
|
-
const competingStartResult = JSON.parse(competingStart.stdout) as ReviewStart;
|
|
375
|
-
const authorityAfterCompetingStart = authorityInventory(await reviewStatus(repository));
|
|
376
|
-
assert.equal(competingStartResult.action, "blocked-scope-action", "a frozen lineage must return a structured scope-action block for a competing candidate");
|
|
377
|
-
assert.equal(competingStartResult.lineage_id, first.lineage_id);
|
|
378
|
-
assert.equal(competingStartResult.state, "approved");
|
|
379
|
-
assert.deepEqual(authorityAfterCompetingStart, authorityBeforeCompetingStart, "a blocked scope action must not mutate approved authority");
|
|
380
|
-
// A plain START over the moved candidate does NOT mint a fresh lineage. An
|
|
381
|
-
// approved lineage whose scope changed is recovered, keeping the operator
|
|
382
|
-
// pointed at the receipt that already exists instead of quietly abandoning
|
|
383
|
-
// it, and the recovery itself needs a maintainer to authorize it. Pi mirrors
|
|
384
|
-
// this because the experience must be the one gentle-ai defines, not a
|
|
385
|
-
// friendlier one Pi invents.
|
|
386
|
-
const second = JSON.parse((await run(binary, ["review", "start", "--cwd", repository], repository)).stdout) as ReviewStart;
|
|
387
|
-
const secondInventory = authorityInventory(await reviewStatus(repository));
|
|
388
|
-
assert.equal(second.action, "recover");
|
|
389
|
-
assert.equal(second.state, "approved");
|
|
390
|
-
assert.equal(second.lineage_id, first.lineage_id, "a moved candidate recovers the approved lineage rather than replacing it");
|
|
391
|
-
const secondReplay = JSON.parse((await run(binary, ["review", "start", "--cwd", repository], repository)).stdout) as ReviewStart;
|
|
392
|
-
const secondReplayInventory = authorityInventory(await reviewStatus(repository));
|
|
393
|
-
assert.equal(secondReplay.action, "recover", "recovery is idempotent");
|
|
394
|
-
assert.equal(secondReplay.lineage_id, second.lineage_id, "replaying the second START must reuse its lineage");
|
|
395
|
-
assert.deepEqual(secondReplayInventory, secondInventory, "replaying the second START must not duplicate durable authority");
|
|
396
|
-
const recoveryTransition = await nextTransition(repository);
|
|
397
|
-
assert.equal(recoveryTransition.kind, "collect");
|
|
398
|
-
assert.equal(recoveryTransition.reason_code, "recovery_authorization_required", "recovery must ask a maintainer, never proceed on its own");
|
|
399
|
-
|
|
400
|
-
// No receipt covers the moved candidate, so delivery is denied rather than
|
|
401
|
-
// waved through, and the denial names `gentle-ai review recover` with the
|
|
402
|
-
// inputs it needs. A blocked gate that names its exit is the whole point.
|
|
403
|
-
await assertScopeChanged(repository, first.lineage_id);
|
|
404
|
-
|
|
405
|
-
await writeFile(join(repository, "tracked.txt"), "candidate one\n");
|
|
406
|
-
await run("git", ["add", "--", "tracked.txt"], repository);
|
|
407
|
-
assert.equal((await run("git", ["write-tree"], repository)).stdout.trim(), firstCandidateTree);
|
|
408
|
-
const firstRestored = JSON.parse((await run(binary, ["review", "validate", "--gate", "pre-commit", "--cwd", repository, "--lineage", first.lineage_id], repository)).stdout) as ReviewGateResult;
|
|
409
|
-
assert.equal(firstRestored.result, "allow", "the old receipt must remain valid for its exact frozen candidate");
|
|
410
|
-
|
|
411
|
-
await writeFile(join(repository, "tracked.txt"), "candidate two\n");
|
|
412
|
-
await run("git", ["add", "--", "tracked.txt"], repository);
|
|
413
|
-
assert.equal((await run("git", ["write-tree"], repository)).stdout.trim(), secondCandidateTree);
|
|
414
|
-
t.diagnostic("pre-push, pre-pr, and release require remote/publication evidence; their network-aware gate contracts remain covered by dedicated gate integration tests rather than this hermetic binary E2E.");
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
test("registered gentle_review START materializes a safe internal skill symlink and cleans pending consent on session shutdown", async (t) => {
|
|
418
|
-
await reviewEnabledHome(t);
|
|
419
|
-
const workspace = await mkdtemp(join(tmpdir(), "gentle-pi-v215-symlink-candidate-"));
|
|
420
|
-
const repository = join(workspace, "repository");
|
|
421
|
-
t.after(async () => rm(workspace, { recursive: true, force: true }));
|
|
145
|
+
t.after(async () => {
|
|
146
|
+
// Candidate views are intentionally read-only. Restore test-workspace write
|
|
147
|
+
// permissions before cleanup when transport refusal stops before START.
|
|
148
|
+
await run("chmod", ["-R", "u+w", workspace], workspace, true);
|
|
149
|
+
await rm(workspace, { recursive: true, force: true });
|
|
150
|
+
});
|
|
422
151
|
|
|
423
152
|
await mkdir(join(repository, ".agents", "skills", "example"), { recursive: true });
|
|
424
153
|
await mkdir(join(repository, ".agent", "skills"), { recursive: true });
|
|
@@ -440,18 +169,17 @@ test("registered gentle_review START materializes a safe internal skill symlink
|
|
|
440
169
|
|
|
441
170
|
const candidateViews = new CandidateViewRegistry();
|
|
442
171
|
let nativeStartReached = false;
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
//
|
|
172
|
+
// Materialization and lexical symlink escape rejection have dedicated
|
|
173
|
+
// candidate-view coverage. This safe shape proves negotiated Pi transport
|
|
174
|
+
// refusal happens before native START, regardless of candidate materialization.
|
|
446
175
|
const native = new NativeReviewCliV216(async (request) => {
|
|
447
176
|
if (request.arguments[0] === "review" && request.arguments[1] === "start") nativeStartReached = true;
|
|
448
177
|
const command = await run(binary, request.arguments, request.cwd, true);
|
|
449
178
|
return { ...command, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
450
179
|
});
|
|
451
180
|
const tools = new Map<string, RegisteredController>();
|
|
452
|
-
let sessionShutdown: ((event: unknown, context: ExtensionContext) => Promise<void> | void) | undefined;
|
|
453
181
|
createGentleAiExtension({ nativeReviewCli: native, candidateViews } as Parameters<typeof createGentleAiExtension>[0])({
|
|
454
|
-
on(
|
|
182
|
+
on() {},
|
|
455
183
|
registerTool(definition: RegisteredController & { name: string }) { tools.set(definition.name, definition); },
|
|
456
184
|
registerCommand() {},
|
|
457
185
|
} as unknown as ExtensionAPI);
|
|
@@ -461,27 +189,20 @@ test("registered gentle_review START materializes a safe internal skill symlink
|
|
|
461
189
|
let returned: { details?: unknown } | undefined;
|
|
462
190
|
let thrown: unknown;
|
|
463
191
|
try {
|
|
464
|
-
|
|
465
|
-
// Without it the consent path throws on a real ExtensionContext member,
|
|
466
|
-
// and the failure reads as a START problem rather than a stub gap.
|
|
467
|
-
returned = await controller.execute("issue-146-start", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, { cwd: repository, hasUI: false, ui: { confirm: async () => true, notify: () => {} } } as unknown as ExtensionContext);
|
|
192
|
+
returned = await controller.execute("issue-146-start", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, { cwd: repository, hasUI: false, ui: { notify: () => {} } } as unknown as ExtensionContext);
|
|
468
193
|
} catch (caught) {
|
|
469
194
|
thrown = caught;
|
|
470
195
|
}
|
|
471
196
|
const error = thrown instanceof Error ? { name: thrown.name, message: thrown.message } : thrown === undefined ? undefined : String(thrown);
|
|
472
197
|
t.diagnostic(JSON.stringify({ returned: returned?.details, error, nativeStartReached }));
|
|
473
|
-
assert.equal(thrown, undefined, "
|
|
474
|
-
assert.equal(nativeStartReached, true, "safe internal symlink materialization must reach native START");
|
|
198
|
+
assert.equal(thrown, undefined, "the Pi transport refusal must be returned, not thrown");
|
|
475
199
|
const result = returned?.details as Record<string, unknown> | undefined;
|
|
200
|
+
const relayTransport = result?.relay_transport as Record<string, unknown> | undefined;
|
|
476
201
|
assert.equal(result?.status, "blocked");
|
|
477
|
-
assert.equal(result?.outcome, "
|
|
478
|
-
assert.equal(
|
|
479
|
-
assert.equal(result?.
|
|
480
|
-
assert.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
await assert.rejects(
|
|
484
|
-
controller.execute("answer-after-shutdown", { operation: "answer-consent", input: JSON.stringify({ consentBinding: result!.consent_binding, answer: "granted" }) }, undefined, undefined, context),
|
|
485
|
-
/unknown, expired, or already consumed/,
|
|
486
|
-
);
|
|
202
|
+
assert.equal(result?.outcome, "pi-host-relay-transport-unavailable");
|
|
203
|
+
assert.equal(relayTransport?.code, "immutable_review_transport_unsupported");
|
|
204
|
+
assert.equal(result?.mutation_performed, false);
|
|
205
|
+
assert.equal(result?.mutation_outcome, "none");
|
|
206
|
+
if (result !== undefined && "lineage_created" in result) assert.equal(result.lineage_created, false);
|
|
207
|
+
assert.equal(nativeStartReached, false, "negotiated Pi transport refusal must preclude native START and any agent-less fallback");
|
|
487
208
|
});
|