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,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,44 +57,65 @@ interface RegisteredController {
|
|
|
49
57
|
execute: (toolCallId: string, params: unknown, signal: AbortSignal | undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<{ details?: unknown }>;
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
60
|
+
// Every test in this file drives the real pinned binary through a full review
|
|
61
|
+
// lifecycle, and gentle-ai v2.4.0 made receipt-driven development opt-in: an
|
|
62
|
+
// install whose global mode was never set now resolves to off, and START is
|
|
63
|
+
// rejected before it reaches any of the behavior these tests exist to check.
|
|
64
|
+
//
|
|
65
|
+
// Until that release these tests had a silent dependency on whatever the
|
|
66
|
+
// operator happened to have configured. That is not a new hazard introduced by
|
|
67
|
+
// the pin — it was always there — but the flip is what made it visible: the
|
|
68
|
+
// suite was green on a machine with review enabled and red on a fresh CI
|
|
69
|
+
// runner, from identical bytes. A test whose result depends on unversioned
|
|
70
|
+
// machine state is not evidence either way.
|
|
71
|
+
//
|
|
72
|
+
// So each test owns a sandbox HOME and opts in the same way a user does,
|
|
73
|
+
// exactly as gentle-ai did for its own lifecycle fixtures in the commit that
|
|
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.
|
|
79
|
+
async function reviewEnabledHome(t: baseTest.TestContext): Promise<string> {
|
|
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");
|
|
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;
|
|
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
|
+
};
|
|
100
|
+
t.after(async () => {
|
|
101
|
+
if (previousHome === undefined) delete process.env.HOME;
|
|
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;
|
|
109
|
+
await rm(home, { recursive: true, force: true });
|
|
110
|
+
await rm(lifecycleCwd, { recursive: true, force: true });
|
|
111
|
+
});
|
|
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);
|
|
114
|
+
// Assert the opt-in landed rather than assuming it. A silently ineffective
|
|
115
|
+
// enable would put these tests straight back to depending on ambient state,
|
|
116
|
+
// which is the exact failure this helper exists to remove.
|
|
117
|
+
assert.match(enabled.stdout, /"effective": "on"/, "the sandbox HOME must have receipt-driven development explicitly enabled");
|
|
118
|
+
return home;
|
|
90
119
|
}
|
|
91
120
|
|
|
92
121
|
async function run(command: string, arguments_: readonly string[], cwd: string, allowFailure = false, environment?: NodeJS.ProcessEnv): Promise<CommandResult> {
|
|
@@ -100,288 +129,25 @@ async function run(command: string, arguments_: readonly string[], cwd: string,
|
|
|
100
129
|
}
|
|
101
130
|
}
|
|
102
131
|
|
|
103
|
-
async
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const entries = await stagedEntries(repository);
|
|
111
|
-
const stagedPaths = entries.map((entry) => entry.split("\t", 2)[1]);
|
|
112
|
-
const stagedModes = entries.map((entry) => entry.split(" ", 1)[0]);
|
|
113
|
-
assert.equal(stagedTree, candidateTree, `staged tree must match frozen candidate; entries: ${entries.join(", ")}`);
|
|
114
|
-
assert.deepEqual(stagedPaths, [...REVIEWED_PATHS].toSorted());
|
|
115
|
-
assert.deepEqual(stagedModes, REVIEWED_MODES);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function assertScopeChanged(repository: string, lineageId: string): Promise<void> {
|
|
119
|
-
const deniedCommand = await run(binary, ["review", "validate", "--gate", "pre-commit", "--cwd", repository, "--lineage", lineageId], repository, true);
|
|
120
|
-
const denied = JSON.parse(deniedCommand.stdout) as ReviewGateResult;
|
|
121
|
-
assert.equal(deniedCommand.exitCode, 1);
|
|
122
|
-
assert.equal(denied.result, "scope-changed");
|
|
123
|
-
assert.equal(denied.allowed, false);
|
|
124
|
-
assert.deepEqual(denied.context.denial, { stage: "receipt-binding", code: "candidate-or-paths-mismatch" });
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function restoreCandidate(repository: string, candidateTree: string): Promise<void> {
|
|
128
|
-
await writeFile(join(repository, "tracked.txt"), "candidate\n");
|
|
129
|
-
await rm(join(repository, "extra-reviewed-path.txt"), { force: true });
|
|
130
|
-
await run("git", ["reset", "--", "extra-reviewed-path.txt"], repository);
|
|
131
|
-
await run("git", ["add", "--", ...REVIEWED_PATHS], repository);
|
|
132
|
-
await assertPublishedProjection(repository, candidateTree);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async function reviewStatus(repository: string): Promise<Record<string, unknown>> {
|
|
136
|
-
return JSON.parse((await run(binary, ["review", "status", "--cwd", repository], repository)).stdout) as Record<string, unknown>;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function authorityInventory(status: Record<string, unknown>): ReviewAuthorityEntry[] {
|
|
140
|
-
assert.ok(Array.isArray(status.entries), "review status must expose authority entries");
|
|
141
|
-
return status.entries.map((entry) => {
|
|
142
|
-
assert.ok(entry !== null && typeof entry === "object", "authority entry must be an object");
|
|
143
|
-
const candidate = entry as Record<string, unknown>;
|
|
144
|
-
for (const key of ["version", "lineage_id", "status", "state", "revision"] as const) {
|
|
145
|
-
assert.equal(typeof candidate[key], "string", `authority entry ${key} must be stable text`);
|
|
146
|
-
}
|
|
147
|
-
assert.ok(Array.isArray(candidate.problems), "authority entry problems must be an array");
|
|
148
|
-
return {
|
|
149
|
-
version: candidate.version as string,
|
|
150
|
-
lineage_id: candidate.lineage_id as string,
|
|
151
|
-
status: candidate.status as string,
|
|
152
|
-
state: candidate.state as string,
|
|
153
|
-
revision: candidate.revision as string,
|
|
154
|
-
problems: candidate.problems,
|
|
155
|
-
};
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Every reviewer result reaches authority through `review capture-result`.
|
|
160
|
-
// `review finalize --result` was removed because a result handed over that way
|
|
161
|
-
// carries no provider-owned admission, so it cannot prove the lens inspected
|
|
162
|
-
// the frozen candidate. The bindings are never guessed here: each one comes
|
|
163
|
-
// from the collect transition the provider itself publishes, which is also
|
|
164
|
-
// what the removal message tells a caller to do.
|
|
165
|
-
//
|
|
166
|
-
// Artifacts stay outside the repository on purpose. Writing them inside changes
|
|
167
|
-
// the workspace snapshot between deriving a target and using it, and authority
|
|
168
|
-
// correctly rejects the stale target that results.
|
|
169
|
-
async function nextTransition(repository: string): Promise<Record<string, unknown>> {
|
|
170
|
-
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>;
|
|
171
|
-
return status.next_transition as Record<string, unknown>;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async function captureSelectedLenses(repository: string, artifacts: string, evidenceName: string): Promise<void> {
|
|
175
|
-
for (let guard = 0; guard < 8; guard += 1) {
|
|
176
|
-
const transition = await nextTransition(repository);
|
|
177
|
-
const collect = transition.collect as { inputs?: readonly Record<string, unknown>[] } | undefined;
|
|
178
|
-
if (transition.kind !== "collect" || collect?.inputs === undefined) return;
|
|
179
|
-
for (const [index, input] of collect.inputs.entries()) {
|
|
180
|
-
const subject = input.artifact_subject as { subject_hash: string };
|
|
181
|
-
const manifest = input.changed_path_manifest as readonly { path: string }[];
|
|
182
|
-
const result = join(artifacts, `${evidenceName}-capture-${index}.json`);
|
|
183
|
-
await writeFile(result, JSON.stringify({
|
|
184
|
-
subject_hash: subject.subject_hash,
|
|
185
|
-
inspection: { status: "completed", paths: manifest.map((entry) => entry.path) },
|
|
186
|
-
findings: [],
|
|
187
|
-
evidence: ["reviewed the complete frozen candidate scope"],
|
|
188
|
-
}));
|
|
189
|
-
// The published tokens already carry the repository context, and
|
|
190
|
-
// capture-result accepts that or --cwd, never both.
|
|
191
|
-
const tokens = (input.arguments as readonly { token: string }[]).map((argument) => argument.token);
|
|
192
|
-
const captured = await run(binary, ["review", "capture-result", ...tokens, "--input", result], repository);
|
|
193
|
-
assert.equal(captured.exitCode, 0, `capture-result failed: ${captured.stderr}`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
assert.fail("collect transitions did not converge");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async function finalizeEmptyReview(repository: string, artifacts: string, started: ReviewStart, evidenceName: string): Promise<CommandResult> {
|
|
200
|
-
const evidence = join(artifacts, evidenceName);
|
|
201
|
-
await writeFile(evidence, `evidence for ${started.lineage_id}\n`);
|
|
202
|
-
await captureSelectedLenses(repository, artifacts, evidenceName);
|
|
203
|
-
return run(binary, ["review", "finalize", "--cwd", repository, "--lineage", started.lineage_id, "--captured-results=true", "--evidence", evidence], repository);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
test("official pinned package runtime authorizes an unchanged linked-view candidate and denies a changed staging tree", async (t) => {
|
|
207
|
-
assert.equal(createHash("sha256").update(await readFile(binary)).digest("hex"), OFFICIAL_BINARY_SHA256);
|
|
208
|
-
assert.deepEqual(await run(binary, ["version"], packageRoot), { exitCode: 0, stdout: `gentle-ai ${GENTLE_AI_VERSION}\n`, stderr: "" });
|
|
209
|
-
|
|
210
|
-
const workspace = await mkdtemp(join(tmpdir(), "gentle-pi-v216-parity-"));
|
|
211
|
-
const repository = join(workspace, "repository");
|
|
212
|
-
const view = join(workspace, "candidate-view");
|
|
213
|
-
const artifacts = join(workspace, "artifacts");
|
|
214
|
-
const temporaryIndex = join(workspace, "controller.index");
|
|
215
|
-
t.after(async () => rm(workspace, { recursive: true, force: true }));
|
|
216
|
-
|
|
217
|
-
await mkdir(repository);
|
|
218
|
-
await mkdir(artifacts);
|
|
219
|
-
await run("git", ["init", "--initial-branch=main"], repository);
|
|
220
|
-
await run("git", ["config", "user.email", "test@example.invalid"], repository);
|
|
221
|
-
await run("git", ["config", "user.name", "Gentle Pi test"], repository);
|
|
222
|
-
await writeFile(join(repository, "tracked.txt"), "base\n");
|
|
223
|
-
await run("git", ["add", "--", "tracked.txt"], repository);
|
|
224
|
-
await run("git", ["commit", "-m", "base"], repository);
|
|
225
|
-
const native = new RuntimeNativeReviewCliV216(async (request) => {
|
|
226
|
-
const command = await run(binary, request.arguments, request.cwd, true);
|
|
227
|
-
return { ...command, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
228
|
-
});
|
|
229
|
-
assert.equal((await native.reviewStatus({ cwd: repository })).status, "clean");
|
|
230
|
-
await writeFile(join(repository, "tracked.txt"), "candidate\n");
|
|
231
|
-
await writeFile(join(repository, "initially-untracked.txt"), "included\n");
|
|
232
|
-
|
|
233
|
-
const temporaryIndexEnvironment = { ...process.env, GIT_INDEX_FILE: temporaryIndex };
|
|
234
|
-
await run("git", ["read-tree", "HEAD"], repository, false, temporaryIndexEnvironment);
|
|
235
|
-
await run("git", ["add", "--", ...REVIEWED_PATHS], repository, false, temporaryIndexEnvironment);
|
|
236
|
-
const candidateTree = (await run("git", ["write-tree"], repository, false, temporaryIndexEnvironment)).stdout.trim();
|
|
237
|
-
await run("git", ["worktree", "add", "--detach", view, "HEAD"], repository);
|
|
238
|
-
await run("git", ["read-tree", candidateTree], view);
|
|
239
|
-
await run("git", ["checkout-index", "--all", "--force"], view);
|
|
240
|
-
|
|
241
|
-
assert.equal((await readFile(join(view, "tracked.txt"), "utf8")), "candidate\n");
|
|
242
|
-
assert.equal((await readFile(join(view, "initially-untracked.txt"), "utf8")), "included\n");
|
|
243
|
-
const started = JSON.parse((await run(binary, ["review", "start", "--cwd", view], view)).stdout) as ReviewStart;
|
|
244
|
-
|
|
245
|
-
const evidence = join(artifacts, "final-evidence.txt");
|
|
246
|
-
await writeFile(evidence, "linked-view parity probe\n");
|
|
247
|
-
// The linked view is the root authority froze, so its collect bindings are
|
|
248
|
-
// derived from the view, not from the contributor repository.
|
|
249
|
-
await captureSelectedLenses(view, artifacts, "final-evidence.txt");
|
|
250
|
-
await run(binary, ["review", "finalize", "--cwd", view, "--lineage", started.lineage_id, "--captured-results=true", "--evidence", evidence], view);
|
|
251
|
-
|
|
252
|
-
await run("git", ["add", "--", ...REVIEWED_PATHS], repository);
|
|
253
|
-
await assertPublishedProjection(repository, candidateTree);
|
|
254
|
-
const allowed = JSON.parse((await run(binary, ["review", "validate", "--gate", "pre-commit", "--cwd", repository, "--lineage", started.lineage_id], repository)).stdout) as ReviewGateResult;
|
|
255
|
-
assert.equal(allowed.result, "allow");
|
|
256
|
-
assert.equal(allowed.allowed, true);
|
|
257
|
-
assert.equal(allowed.context.candidate_tree, candidateTree);
|
|
258
|
-
assert.equal(allowed.context.paths_digest, CLEAN_DIFFERENTIAL_PUBLISHED_PATHS_DIGEST);
|
|
259
|
-
t.diagnostic(JSON.stringify({ candidateTree, publishedPathsDigest: CLEAN_DIFFERENTIAL_PUBLISHED_PATHS_DIGEST, reviewedPaths: REVIEWED_PATHS, reviewedModes: REVIEWED_MODES, binary, startCwd: view, finalizeCwd: view, artifacts }));
|
|
260
|
-
|
|
261
|
-
await writeFile(join(repository, "tracked.txt"), "changed-after-review\n");
|
|
262
|
-
await run("git", ["add", "--", "tracked.txt"], repository);
|
|
263
|
-
assert.notEqual((await run("git", ["write-tree"], repository)).stdout.trim(), candidateTree);
|
|
264
|
-
await assertScopeChanged(repository, started.lineage_id);
|
|
265
|
-
await restoreCandidate(repository, candidateTree);
|
|
266
|
-
|
|
267
|
-
await writeFile(join(repository, "extra-reviewed-path.txt"), "not in the frozen candidate\n");
|
|
268
|
-
await run("git", ["add", "extra-reviewed-path.txt"], repository);
|
|
269
|
-
assert.notEqual((await run("git", ["write-tree"], repository)).stdout.trim(), candidateTree);
|
|
270
|
-
assert.deepEqual((await stagedEntries(repository)).map((entry) => entry.split("\t", 2)[1]), [...REVIEWED_PATHS, "extra-reviewed-path.txt"].toSorted());
|
|
271
|
-
await assertScopeChanged(repository, started.lineage_id);
|
|
272
|
-
await restoreCandidate(repository, candidateTree);
|
|
273
|
-
|
|
274
|
-
await chmod(join(repository, "tracked.txt"), 0o755);
|
|
275
|
-
await run("git", ["add", "--", "tracked.txt"], repository);
|
|
276
|
-
const modeDrifted = (await stagedEntries(repository)).some((entry) => entry === "100755" || entry.startsWith("100755 "));
|
|
277
|
-
if (modeDrifted) {
|
|
278
|
-
assert.notEqual((await run("git", ["write-tree"], repository)).stdout.trim(), candidateTree);
|
|
279
|
-
await assertScopeChanged(repository, started.lineage_id);
|
|
280
|
-
} else {
|
|
281
|
-
t.diagnostic("skipped native mode-drift denial because this repository/platform does not stage executable-bit changes");
|
|
282
|
-
}
|
|
283
|
-
await chmod(join(repository, "tracked.txt"), 0o644);
|
|
284
|
-
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");
|
|
285
139
|
});
|
|
286
140
|
|
|
287
|
-
test("
|
|
288
|
-
|
|
141
|
+
test("registered gentle_review surfaces the package-pinned Pi transport refusal before native START for a safe internal symlink candidate", async (t) => {
|
|
142
|
+
await reviewEnabledHome(t);
|
|
143
|
+
const workspace = await realpath(await mkdtemp(join(tmpdir(), "gentle-pi-v215-symlink-candidate-")));
|
|
289
144
|
const repository = join(workspace, "repository");
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
await run("git", ["config", "user.email", "test@example.invalid"], repository);
|
|
297
|
-
await run("git", ["config", "user.name", "Gentle Pi test"], repository);
|
|
298
|
-
await writeFile(join(repository, "tracked.txt"), "base\n");
|
|
299
|
-
await run("git", ["add", "--", "tracked.txt"], repository);
|
|
300
|
-
await run("git", ["commit", "-m", "base"], repository);
|
|
301
|
-
|
|
302
|
-
await writeFile(join(repository, "tracked.txt"), "candidate one\n");
|
|
303
|
-
const first = JSON.parse((await run(binary, ["review", "start", "--cwd", repository], repository)).stdout) as ReviewStart;
|
|
304
|
-
const firstInventory = authorityInventory(await reviewStatus(repository));
|
|
305
|
-
const firstReplay = JSON.parse((await run(binary, ["review", "start", "--cwd", repository], repository)).stdout) as ReviewStart;
|
|
306
|
-
const firstReplayInventory = authorityInventory(await reviewStatus(repository));
|
|
307
|
-
assert.equal(firstReplay.lineage_id, first.lineage_id, "replaying an exact START must reuse the frozen lineage");
|
|
308
|
-
assert.deepEqual(firstReplayInventory, firstInventory, "replaying an exact START must not create durable authority");
|
|
309
|
-
|
|
310
|
-
const firstFinalized = JSON.parse((await finalizeEmptyReview(repository, artifacts, first, "first-evidence.txt")).stdout) as ReviewFinalize;
|
|
311
|
-
const firstFinalizedInventory = authorityInventory(await reviewStatus(repository));
|
|
312
|
-
// v2.1.7 terminal replay contract: replay uses the exact explicit lineage with
|
|
313
|
-
// no mutation inputs; re-sending reviewer results after approval is rejected.
|
|
314
|
-
const repeatedResults = await finalizeEmptyReview(repository, artifacts, first, "first-evidence.txt").catch((error: NodeJS.ErrnoException & { stderr?: string }) => error);
|
|
315
|
-
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");
|
|
316
|
-
const firstFinalizeReplay = JSON.parse((await run(binary, ["review", "finalize", "--cwd", repository, "--lineage", first.lineage_id], repository)).stdout) as ReviewFinalize;
|
|
317
|
-
const firstFinalizeReplayInventory = authorityInventory(await reviewStatus(repository));
|
|
318
|
-
assert.equal(firstFinalized.lineage_id, first.lineage_id);
|
|
319
|
-
assert.equal(firstFinalized.state, "approved");
|
|
320
|
-
assert.match(firstFinalized.store_revision, /^sha256:[a-f0-9]{64}$/);
|
|
321
|
-
assert.equal(firstFinalizeReplay.store_revision, firstFinalized.store_revision, "replaying an exact FINALIZE must reuse its receipt revision");
|
|
322
|
-
assert.equal(firstFinalizeReplay.receipt_path, firstFinalized.receipt_path, "replaying an exact FINALIZE must reuse its receipt location");
|
|
323
|
-
assert.deepEqual(firstFinalizeReplayInventory, firstFinalizedInventory, "replaying an exact FINALIZE must not create a durable receipt or authority");
|
|
324
|
-
assert.deepEqual(firstFinalizedInventory, [{ version: "compact-v2", lineage_id: first.lineage_id, status: "approved", state: "approved", revision: firstFinalized.store_revision, problems: [] }]);
|
|
325
|
-
|
|
326
|
-
await run("git", ["add", "--", "tracked.txt"], repository);
|
|
327
|
-
const firstCandidateTree = (await run("git", ["write-tree"], repository)).stdout.trim();
|
|
328
|
-
const firstAllowed = JSON.parse((await run(binary, ["review", "validate", "--gate", "pre-commit", "--cwd", repository, "--lineage", first.lineage_id], repository)).stdout) as ReviewGateResult;
|
|
329
|
-
assert.equal(firstAllowed.result, "allow");
|
|
330
|
-
assert.equal(firstAllowed.context.candidate_tree, firstCandidateTree);
|
|
331
|
-
|
|
332
|
-
await writeFile(join(repository, "tracked.txt"), "candidate two\n");
|
|
333
|
-
await run("git", ["add", "--", "tracked.txt"], repository);
|
|
334
|
-
const secondCandidateTree = (await run("git", ["write-tree"], repository)).stdout.trim();
|
|
335
|
-
assert.notEqual(secondCandidateTree, firstCandidateTree);
|
|
336
|
-
const authorityBeforeCompetingStart = authorityInventory(await reviewStatus(repository));
|
|
337
|
-
const competingStart = await run(binary, ["review", "start", "--cwd", repository, "--lineage", first.lineage_id], repository);
|
|
338
|
-
const competingStartResult = JSON.parse(competingStart.stdout) as ReviewStart;
|
|
339
|
-
const authorityAfterCompetingStart = authorityInventory(await reviewStatus(repository));
|
|
340
|
-
assert.equal(competingStartResult.action, "blocked-scope-action", "a frozen lineage must return a structured scope-action block for a competing candidate");
|
|
341
|
-
assert.equal(competingStartResult.lineage_id, first.lineage_id);
|
|
342
|
-
assert.equal(competingStartResult.state, "approved");
|
|
343
|
-
assert.deepEqual(authorityAfterCompetingStart, authorityBeforeCompetingStart, "a blocked scope action must not mutate approved authority");
|
|
344
|
-
// A plain START over the moved candidate does NOT mint a fresh lineage. An
|
|
345
|
-
// approved lineage whose scope changed is recovered, keeping the operator
|
|
346
|
-
// pointed at the receipt that already exists instead of quietly abandoning
|
|
347
|
-
// it, and the recovery itself needs a maintainer to authorize it. Pi mirrors
|
|
348
|
-
// this because the experience must be the one gentle-ai defines, not a
|
|
349
|
-
// friendlier one Pi invents.
|
|
350
|
-
const second = JSON.parse((await run(binary, ["review", "start", "--cwd", repository], repository)).stdout) as ReviewStart;
|
|
351
|
-
const secondInventory = authorityInventory(await reviewStatus(repository));
|
|
352
|
-
assert.equal(second.action, "recover");
|
|
353
|
-
assert.equal(second.state, "approved");
|
|
354
|
-
assert.equal(second.lineage_id, first.lineage_id, "a moved candidate recovers the approved lineage rather than replacing it");
|
|
355
|
-
const secondReplay = JSON.parse((await run(binary, ["review", "start", "--cwd", repository], repository)).stdout) as ReviewStart;
|
|
356
|
-
const secondReplayInventory = authorityInventory(await reviewStatus(repository));
|
|
357
|
-
assert.equal(secondReplay.action, "recover", "recovery is idempotent");
|
|
358
|
-
assert.equal(secondReplay.lineage_id, second.lineage_id, "replaying the second START must reuse its lineage");
|
|
359
|
-
assert.deepEqual(secondReplayInventory, secondInventory, "replaying the second START must not duplicate durable authority");
|
|
360
|
-
const recoveryTransition = await nextTransition(repository);
|
|
361
|
-
assert.equal(recoveryTransition.kind, "collect");
|
|
362
|
-
assert.equal(recoveryTransition.reason_code, "recovery_authorization_required", "recovery must ask a maintainer, never proceed on its own");
|
|
363
|
-
|
|
364
|
-
// No receipt covers the moved candidate, so delivery is denied rather than
|
|
365
|
-
// waved through, and the denial names `gentle-ai review recover` with the
|
|
366
|
-
// inputs it needs. A blocked gate that names its exit is the whole point.
|
|
367
|
-
await assertScopeChanged(repository, first.lineage_id);
|
|
368
|
-
|
|
369
|
-
await writeFile(join(repository, "tracked.txt"), "candidate one\n");
|
|
370
|
-
await run("git", ["add", "--", "tracked.txt"], repository);
|
|
371
|
-
assert.equal((await run("git", ["write-tree"], repository)).stdout.trim(), firstCandidateTree);
|
|
372
|
-
const firstRestored = JSON.parse((await run(binary, ["review", "validate", "--gate", "pre-commit", "--cwd", repository, "--lineage", first.lineage_id], repository)).stdout) as ReviewGateResult;
|
|
373
|
-
assert.equal(firstRestored.result, "allow", "the old receipt must remain valid for its exact frozen candidate");
|
|
374
|
-
|
|
375
|
-
await writeFile(join(repository, "tracked.txt"), "candidate two\n");
|
|
376
|
-
await run("git", ["add", "--", "tracked.txt"], repository);
|
|
377
|
-
assert.equal((await run("git", ["write-tree"], repository)).stdout.trim(), secondCandidateTree);
|
|
378
|
-
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.");
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
test("registered gentle_review START materializes a safe internal skill symlink and cleans pending consent on session shutdown", async (t) => {
|
|
382
|
-
const workspace = await mkdtemp(join(tmpdir(), "gentle-pi-v215-symlink-candidate-"));
|
|
383
|
-
const repository = join(workspace, "repository");
|
|
384
|
-
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
|
+
});
|
|
385
151
|
|
|
386
152
|
await mkdir(join(repository, ".agents", "skills", "example"), { recursive: true });
|
|
387
153
|
await mkdir(join(repository, ".agent", "skills"), { recursive: true });
|
|
@@ -403,18 +169,17 @@ test("registered gentle_review START materializes a safe internal skill symlink
|
|
|
403
169
|
|
|
404
170
|
const candidateViews = new CandidateViewRegistry();
|
|
405
171
|
let nativeStartReached = false;
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
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.
|
|
409
175
|
const native = new NativeReviewCliV216(async (request) => {
|
|
410
176
|
if (request.arguments[0] === "review" && request.arguments[1] === "start") nativeStartReached = true;
|
|
411
177
|
const command = await run(binary, request.arguments, request.cwd, true);
|
|
412
178
|
return { ...command, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
413
179
|
});
|
|
414
180
|
const tools = new Map<string, RegisteredController>();
|
|
415
|
-
let sessionShutdown: ((event: unknown, context: ExtensionContext) => Promise<void> | void) | undefined;
|
|
416
181
|
createGentleAiExtension({ nativeReviewCli: native, candidateViews } as Parameters<typeof createGentleAiExtension>[0])({
|
|
417
|
-
on(
|
|
182
|
+
on() {},
|
|
418
183
|
registerTool(definition: RegisteredController & { name: string }) { tools.set(definition.name, definition); },
|
|
419
184
|
registerCommand() {},
|
|
420
185
|
} as unknown as ExtensionAPI);
|
|
@@ -424,27 +189,20 @@ test("registered gentle_review START materializes a safe internal skill symlink
|
|
|
424
189
|
let returned: { details?: unknown } | undefined;
|
|
425
190
|
let thrown: unknown;
|
|
426
191
|
try {
|
|
427
|
-
|
|
428
|
-
// Without it the consent path throws on a real ExtensionContext member,
|
|
429
|
-
// and the failure reads as a START problem rather than a stub gap.
|
|
430
|
-
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);
|
|
431
193
|
} catch (caught) {
|
|
432
194
|
thrown = caught;
|
|
433
195
|
}
|
|
434
196
|
const error = thrown instanceof Error ? { name: thrown.name, message: thrown.message } : thrown === undefined ? undefined : String(thrown);
|
|
435
197
|
t.diagnostic(JSON.stringify({ returned: returned?.details, error, nativeStartReached }));
|
|
436
|
-
assert.equal(thrown, undefined, "
|
|
437
|
-
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");
|
|
438
199
|
const result = returned?.details as Record<string, unknown> | undefined;
|
|
200
|
+
const relayTransport = result?.relay_transport as Record<string, unknown> | undefined;
|
|
439
201
|
assert.equal(result?.status, "blocked");
|
|
440
|
-
assert.equal(result?.outcome, "
|
|
441
|
-
assert.equal(
|
|
442
|
-
assert.equal(result?.
|
|
443
|
-
assert.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
await assert.rejects(
|
|
447
|
-
controller.execute("answer-after-shutdown", { operation: "answer-consent", input: JSON.stringify({ consentBinding: result!.consent_binding, answer: "granted" }) }, undefined, undefined, context),
|
|
448
|
-
/unknown, expired, or already consumed/,
|
|
449
|
-
);
|
|
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");
|
|
450
208
|
});
|