gentle-pi 1.2.0 → 2.0.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 +40 -9
- package/assets/agents/review-readability.md +0 -1
- package/assets/agents/review-reliability.md +0 -1
- package/assets/agents/review-resilience.md +0 -1
- package/assets/agents/review-risk.md +0 -1
- package/contracts/review-integration/v1/fixtures/binding-revision-conflict.fixture.json +25 -0
- package/contracts/review-integration/v1/fixtures/capabilities-v1.2.fixture.json +263 -0
- package/contracts/review-integration/v1/fixtures/capabilities-v1.3.fixture.json +301 -0
- package/contracts/review-integration/v1/fixtures/capabilities-v1.4.fixture.json +312 -0
- package/contracts/review-integration/v1/fixtures/capabilities-v1.5.fixture.json +321 -0
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +36 -0
- package/contracts/review-integration/v1/fixtures/final-verification-incident.fixture.json +1 -0
- package/contracts/review-integration/v1/fixtures/operation.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/repair-preflight.fixture.json +54 -0
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +97 -0
- package/contracts/review-integration/v1/fixtures/start.fixture.json +25 -1
- package/contracts/review-integration/v1/fixtures/status-ambiguous.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/status-corrupted.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/status-recover.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/status-unrelated.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/status-v2-ambiguous.fixture.json +110 -0
- package/contracts/review-integration/v1/fixtures/status-v2-corrupted.fixture.json +107 -0
- package/contracts/review-integration/v1/fixtures/status-v2-final-verification-retry.fixture.json +192 -0
- package/contracts/review-integration/v1/fixtures/status-v2-recover.fixture.json +130 -0
- package/contracts/review-integration/v1/fixtures/status-v2-repair.fixture.json +161 -0
- package/contracts/review-integration/v1/fixtures/status-v2-unrelated.fixture.json +103 -0
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +193 -0
- package/contracts/review-integration/v1/fixtures/status.fixture.json +19 -5
- package/contracts/review-integration/v1/fixtures/verification-evidence.fixture.json +1 -0
- package/contracts/review-integration/v1/schemas/admitted-result.schema.json +35 -0
- package/contracts/review-integration/v1/schemas/artifact-subject.schema.json +33 -0
- package/contracts/review-integration/v1/schemas/authority-repair-assessment.schema.json +247 -0
- package/contracts/review-integration/v1/schemas/capabilities-v1.2.schema.json +249 -0
- package/contracts/review-integration/v1/schemas/capabilities-v1.3.schema.json +429 -0
- package/contracts/review-integration/v1/schemas/capabilities-v1.4.schema.json +432 -0
- package/contracts/review-integration/v1/schemas/capabilities-v1.5.schema.json +154 -0
- package/contracts/review-integration/v1/schemas/consent.schema.json +86 -0
- package/contracts/review-integration/v1/schemas/failure.schema.json +54 -8
- package/contracts/review-integration/v1/schemas/final-verification-incident.schema.json +51 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +204 -31
- package/contracts/review-integration/v1/schemas/repair.schema.json +251 -0
- package/contracts/review-integration/v1/schemas/result-artifact-v2.schema.json +38 -0
- package/contracts/review-integration/v1/schemas/start-v2.schema.json +216 -0
- package/contracts/review-integration/v1/schemas/start.schema.json +79 -3
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +313 -0
- package/contracts/review-integration/v1/schemas/status.schema.json +37 -10
- package/contracts/review-integration/v1/schemas/targeted-validation-request.schema.json +46 -0
- package/contracts/review-integration/v1/schemas/verification-evidence.schema.json +64 -0
- package/contracts/review-integration/v2/fixtures/capabilities.fixture.json +111 -0
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +36 -0
- package/contracts/review-integration/v2/fixtures/start.fixture.json +93 -0
- package/contracts/review-integration/v2/fixtures/status.fixture.json +113 -0
- package/contracts/review-integration/v2/schemas/admitted-result.schema.json +14 -0
- package/contracts/review-integration/v2/schemas/artifact-subject.schema.json +36 -0
- package/contracts/review-integration/v2/schemas/capabilities.schema.json +111 -0
- package/contracts/review-integration/v2/schemas/consent.schema.json +70 -0
- package/contracts/review-integration/v2/schemas/failure.schema.json +35 -0
- package/contracts/review-integration/v2/schemas/operation.schema.json +46 -0
- package/contracts/review-integration/v2/schemas/repair.schema.json +22 -0
- package/contracts/review-integration/v2/schemas/start.schema.json +106 -0
- package/contracts/review-integration/v2/schemas/status.schema.json +104 -0
- package/docs/native-authority-architecture.md +18 -3
- package/docs/review-integration.md +175 -27
- package/extensions/gentle-ai.ts +414 -19
- package/lib/gentle-ai-binary.ts +1 -1
- package/lib/native-review-cli.ts +679 -57
- package/lib/review-candidate-view.ts +131 -1
- package/lib/review-compact-contract.ts +12 -3
- package/lib/review-consent-latch.ts +51 -0
- package/lib/review-correction-lifecycle.ts +170 -0
- package/lib/review-integration-v2.ts +1563 -0
- package/package.json +15 -11
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +663 -41
- package/runtime/review-integration-v2.mjs +1564 -0
- package/scripts/build-git-commit-transaction-runner.mjs +1 -1
- package/scripts/gentle-ai-installer.mjs +16 -11
- package/scripts/install-gentle-ai.mjs +7 -3
- package/scripts/test-packed-runner.mjs +2 -2
- package/scripts/verify-package-files.mjs +216 -64
- package/skills/release/SKILL.md +38 -11
- package/tests/devbinary/native-review-parity.devtest.ts +348 -0
- package/tests/gentle-ai-binary.test.ts +8 -1
- package/tests/gentle-ai-installer.test.ts +24 -21
- package/tests/native-binary-gate.test.ts +145 -0
- package/tests/native-review-capability-contract.test.ts +87 -0
- package/tests/native-review-cli.test.ts +44 -1
- package/tests/native-review-parity-runtime.test.ts +70 -23
- package/tests/native-review-parity.test.ts +673 -0
- package/tests/native-risk-evidence-parity.test.ts +91 -0
- package/tests/package-manifest.test.ts +72 -7
- package/tests/review-actor-tool-deny.test.ts +20 -5
- package/tests/review-candidate-view.test.ts +331 -1
- package/tests/review-compact-contract.test.ts +5 -0
- package/tests/review-consent-latch.test.ts +67 -0
- package/tests/review-controller-native-recovery.test.ts +180 -5
- package/tests/review-controller-native-routing.test.ts +385 -12
- package/tests/review-controller-workspace-root.test.ts +26 -6
- package/tests/review-correction-lifecycle.test.ts +174 -0
- package/tests/review-integration-v2.test.ts +539 -0
- package/tests/runtime-harness.mjs +187 -0
- package/tests/support/native-binary-gate.ts +86 -0
- package/tests/verify-package-files.test.ts +119 -0
- package/lib/review-integration-v1.ts +0 -795
- package/runtime/review-integration-v1.mjs +0 -796
- package/tests/native-review-integration-v1.test.ts +0 -102
- package/tests/review-integration-v1.test.ts +0 -530
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { createGentleAiExtension } from "../../extensions/gentle-ai.ts";
|
|
9
|
+
import {
|
|
10
|
+
NativeReviewCliV214,
|
|
11
|
+
createNodeExecFileAdapter,
|
|
12
|
+
setNativeCliContractForTesting,
|
|
13
|
+
type ExecFileAdapter,
|
|
14
|
+
type NativeReviewCli,
|
|
15
|
+
} from "../../lib/native-review-cli.ts";
|
|
16
|
+
import { CandidateViewRegistry } from "../../lib/review-candidate-view.ts";
|
|
17
|
+
import { readReviewConsentLatch } from "../../lib/review-consent-latch.ts";
|
|
18
|
+
import type { AuthorityRepairAssessmentV1, ReviewStatusV3 } from "../../lib/review-integration-v2.ts";
|
|
19
|
+
import { requireDevBinary } from "../support/native-binary-gate.ts";
|
|
20
|
+
|
|
21
|
+
// Organic RDD Parity, Phase 6: non-gating dev-binary journey.
|
|
22
|
+
//
|
|
23
|
+
// `pnpm test` never picks this file up — it globs `tests/*.test.ts` only, and
|
|
24
|
+
// this file both lives one directory deeper and ends in `.devtest.ts`. It
|
|
25
|
+
// only runs via `pnpm run test:dev-binary`, and only when GENTLE_AI_DEV_BINARY
|
|
26
|
+
// names an existing absolute path. It never reads or writes the shipped
|
|
27
|
+
// 2.1.11 pins (lib/gentle-ai-binary.ts, scripts/gentle-ai-installer.mjs) —
|
|
28
|
+
// every capability stays negotiated per-process through the executable
|
|
29
|
+
// override seam below, exactly like every other native-review-cli test.
|
|
30
|
+
//
|
|
31
|
+
// Gated through the same loud-skip machinery as the two `tests/*.test.ts`
|
|
32
|
+
// binary-verified suites (Phase 4), extended here to cover this THIRD
|
|
33
|
+
// self-skipping suite (Phase 13.12/13.13 follow-up): under
|
|
34
|
+
// GENTLE_PI_REQUIRE_DEV_BINARY=1 a missing/invalid dev binary throws instead
|
|
35
|
+
// of silently reporting 5 tests / 0 pass / 0 fail, which is exactly how the
|
|
36
|
+
// v2.2.2 "receipt-driven development" terminology rot went unnoticed.
|
|
37
|
+
const DEV_BINARY = process.env.GENTLE_AI_DEV_BINARY;
|
|
38
|
+
const devBinaryGate = requireDevBinary({
|
|
39
|
+
devBinaryPath: DEV_BINARY,
|
|
40
|
+
exists: typeof DEV_BINARY === "string" && DEV_BINARY.length > 0 && DEV_BINARY.startsWith("/") && existsSync(DEV_BINARY),
|
|
41
|
+
env: process.env,
|
|
42
|
+
});
|
|
43
|
+
if (!devBinaryGate.run) console.log(`tests/devbinary/native-review-parity.devtest.ts: ${devBinaryGate.reason}`);
|
|
44
|
+
const RUNNABLE = devBinaryGate.run;
|
|
45
|
+
|
|
46
|
+
// A synthetic capable version, exactly like native-review-parity.test.ts's
|
|
47
|
+
// CAPABLE_VERSION overlay. Real dev binaries never carry a pinned three-part
|
|
48
|
+
// semver (the live binary under test reports "gentle-ai dev-organic-...",
|
|
49
|
+
// confirmed live during Phase 6 ground-truthing), so `verifyVersion`'s frozen
|
|
50
|
+
// `/^gentle-ai ([0-9]+\.[0-9]+\.[0-9]+)\n$/` regex can never match it — by
|
|
51
|
+
// design, a dev build is never a pinned release. The bridge below substitutes
|
|
52
|
+
// exactly one in-memory version response and forwards every other call to the
|
|
53
|
+
// real process untouched, so argv fidelity is guaranteed: NativeReviewCliV214
|
|
54
|
+
// itself builds every argv array here, never this file.
|
|
55
|
+
const BRIDGE_VERSION = "9.8.7";
|
|
56
|
+
|
|
57
|
+
function bridgeAdapter(binary: string): ExecFileAdapter {
|
|
58
|
+
const real = createNodeExecFileAdapter();
|
|
59
|
+
return async (request) => {
|
|
60
|
+
if (request.arguments[0] === "version") {
|
|
61
|
+
return { stdout: `gentle-ai ${BRIDGE_VERSION}\n`, stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
62
|
+
}
|
|
63
|
+
return real({ ...request, file: binary });
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A NativeReviewCli backed by the real dev binary for reviewMode/start/
|
|
68
|
+
// validate (the organic-parity surface under test), with a synthetic
|
|
69
|
+
// targetStatus fixture standing in for the negotiated review-integration/v2
|
|
70
|
+
// contract; the plain-CLI decode path in lib/native-review-cli.ts (reviewMode,
|
|
71
|
+
// start, validate here) stays independent of the negotiated status shape.
|
|
72
|
+
function journeyNative(binary: string): NativeReviewCli {
|
|
73
|
+
const bridge = new NativeReviewCliV214(bridgeAdapter(binary), binary);
|
|
74
|
+
return {
|
|
75
|
+
start: (request) => bridge.start(request),
|
|
76
|
+
validate: (request) => bridge.validate(request),
|
|
77
|
+
reviewMode: (request) => bridge.reviewMode(request),
|
|
78
|
+
async targetStatus(request) {
|
|
79
|
+
return request.lineageId === undefined
|
|
80
|
+
? unrelatedStartTargetStatus()
|
|
81
|
+
: currentTargetStatusFixture(request.lineageId, request.cwd);
|
|
82
|
+
},
|
|
83
|
+
} as unknown as NativeReviewCli;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function git(cwd: string, ...args: string[]): string {
|
|
87
|
+
return execFileSync("git", args, { cwd, encoding: "utf8" }).trim();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function repository(t: test.TestContext): string {
|
|
91
|
+
const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-dev-binary-"));
|
|
92
|
+
t.after(() => {
|
|
93
|
+
try { execFileSync("chmod", ["-R", "u+w", cwd]); } catch { /* best effort */ }
|
|
94
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
95
|
+
});
|
|
96
|
+
git(cwd, "init", "-b", "main");
|
|
97
|
+
git(cwd, "config", "user.email", "devbinary@example.com");
|
|
98
|
+
git(cwd, "config", "user.name", "Dev Binary Journey");
|
|
99
|
+
return cwd;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const UNSUPPORTED_REPAIR_ASSESSMENT: AuthorityRepairAssessmentV1 = {
|
|
103
|
+
schema: "gentle-ai.review-authority-repair-assessment/v1",
|
|
104
|
+
status: "unsupported",
|
|
105
|
+
counts: { lineages: 0, compactLineages: 0, legacyLineages: 0, events: 0, bytes: 0, eligibleCandidates: 0, unsupportedLineages: 0, conflicts: 0 },
|
|
106
|
+
supportedOperations: ["review/complete-fix", "review/validate-fix"],
|
|
107
|
+
authorizationSchema: "gentle-ai.review-repair-authorization/v1",
|
|
108
|
+
};
|
|
109
|
+
const RAW_UNSUPPORTED_REPAIR_ASSESSMENT = {
|
|
110
|
+
schema: UNSUPPORTED_REPAIR_ASSESSMENT.schema,
|
|
111
|
+
status: UNSUPPORTED_REPAIR_ASSESSMENT.status,
|
|
112
|
+
counts: {
|
|
113
|
+
lineages: 0, compact_lineages: 0, legacy_lineages: 0, events: 0, bytes: 0,
|
|
114
|
+
eligible_candidates: 0, unsupported_lineages: 0, conflicts: 0,
|
|
115
|
+
},
|
|
116
|
+
supported_operations: UNSUPPORTED_REPAIR_ASSESSMENT.supportedOperations,
|
|
117
|
+
authorization_schema: UNSUPPORTED_REPAIR_ASSESSMENT.authorizationSchema,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
function unrelatedStartTargetStatus(): ReviewStatusV3 {
|
|
121
|
+
const sha = `sha256:${"a".repeat(64)}`;
|
|
122
|
+
const tree = "b".repeat(40);
|
|
123
|
+
const projection = {
|
|
124
|
+
schema: "gentle-ai.review-integration.projection/v1" as const, kind: "current-changes" as const, projection: "workspace" as const,
|
|
125
|
+
baseTree: tree, initialReviewTree: tree, currentCandidateTree: tree, pathsDigest: sha, paths: [], intendedUntracked: [],
|
|
126
|
+
intendedUntrackedProof: sha, initialSnapshotIdentity: sha, currentSnapshotIdentity: sha,
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
contract: "gentle-ai.review-integration/v2", applicability: "unrelated", receipt: { status: "not_applicable" }, action: "start",
|
|
130
|
+
replayability: "not_replayable", targetIdentity: sha, projection, repair: UNSUPPORTED_REPAIR_ASSESSMENT, candidates: [],
|
|
131
|
+
raw: {
|
|
132
|
+
schema: "gentle-ai.review-integration.status/v3", contract: "gentle-ai.review-integration/v2", operation: "review.status",
|
|
133
|
+
applicability: "unrelated", receipt: { status: "not_applicable" }, action: "start", replayability: "not_replayable", target_identity: sha,
|
|
134
|
+
repair: RAW_UNSUPPORTED_REPAIR_ASSESSMENT,
|
|
135
|
+
projection: { schema: projection.schema, kind: projection.kind, projection: projection.projection, base_tree: tree, initial_review_tree: tree, current_candidate_tree: tree, paths_digest: sha, paths: [], intended_untracked: [], intended_untracked_proof: sha, initial_snapshot_identity: sha, current_snapshot_identity: sha },
|
|
136
|
+
candidates: [],
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function currentTargetStatusFixture(lineageId: string, cwd: string): ReviewStatusV3 {
|
|
142
|
+
const sha = `sha256:${"a".repeat(64)}`;
|
|
143
|
+
const baseTree = git(cwd, "rev-parse", "HEAD^{tree}");
|
|
144
|
+
const candidateTree = git(cwd, "write-tree");
|
|
145
|
+
const projection = {
|
|
146
|
+
schema: "gentle-ai.review-integration.projection/v1" as const, kind: "current-changes" as const, projection: "workspace" as const,
|
|
147
|
+
baseTree, initialReviewTree: candidateTree, currentCandidateTree: candidateTree, pathsDigest: sha, paths: ["app.ts"], intendedUntracked: [],
|
|
148
|
+
intendedUntrackedProof: sha, initialSnapshotIdentity: sha, currentSnapshotIdentity: sha,
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
contract: "gentle-ai.review-integration/v2", applicability: "current_target",
|
|
152
|
+
authority: { version: "compact-v2", lineageId, state: "reviewing", generation: 1, revision: sha },
|
|
153
|
+
receipt: { status: "expected_missing" }, action: "finalize", replayability: "not_replayable",
|
|
154
|
+
frozen: { tier: "medium", originalChangedLines: 1, correctionBudget: 1 },
|
|
155
|
+
targetIdentity: sha, projection, repair: UNSUPPORTED_REPAIR_ASSESSMENT, candidates: [],
|
|
156
|
+
raw: {
|
|
157
|
+
schema: "gentle-ai.review-integration.status/v3", contract: "gentle-ai.review-integration/v2", operation: "review.status",
|
|
158
|
+
applicability: "current_target", receipt: { status: "expected_missing" }, action: "finalize", replayability: "not_replayable", target_identity: sha,
|
|
159
|
+
authority: { version: "compact-v2", lineage_id: lineageId, state: "reviewing", generation: 1, revision: sha },
|
|
160
|
+
frozen: { tier: "medium", original_changed_lines: 1, correction_budget: 1 },
|
|
161
|
+
repair: RAW_UNSUPPORTED_REPAIR_ASSESSMENT,
|
|
162
|
+
projection: { schema: projection.schema, kind: projection.kind, projection: projection.projection, base_tree: baseTree, initial_review_tree: candidateTree, current_candidate_tree: candidateTree, paths_digest: sha, paths: ["app.ts"], intended_untracked: [], intended_untracked_proof: sha, initial_snapshot_identity: sha, current_snapshot_identity: sha },
|
|
163
|
+
candidates: [],
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface RegisteredTool {
|
|
169
|
+
execute: (toolCallId: string, params: unknown, signal: AbortSignal | undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<{ details?: unknown }>;
|
|
170
|
+
}
|
|
171
|
+
interface RegisteredCommandFixture {
|
|
172
|
+
handler: (args: string, ctx: ExtensionContext) => Promise<void>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function runtime(nativeReviewCli: NativeReviewCli): { controller: RegisteredTool; commands: Map<string, RegisteredCommandFixture> } {
|
|
176
|
+
const tools = new Map<string, RegisteredTool>();
|
|
177
|
+
const commands = new Map<string, RegisteredCommandFixture>();
|
|
178
|
+
const dependencies = { nativeReviewCli, candidateViews: new CandidateViewRegistry() } as unknown as Parameters<typeof createGentleAiExtension>[0];
|
|
179
|
+
createGentleAiExtension(dependencies)({
|
|
180
|
+
on() {},
|
|
181
|
+
registerTool(definition: RegisteredTool & { name: string }) { tools.set(definition.name, definition); },
|
|
182
|
+
registerCommand(name: string, definition: RegisteredCommandFixture) { commands.set(name, definition); },
|
|
183
|
+
} as unknown as ExtensionAPI);
|
|
184
|
+
const controller = tools.get("gentle_review");
|
|
185
|
+
assert.ok(controller);
|
|
186
|
+
return { controller: controller!, commands };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function headlessContext(cwd: string, notices: Array<{ message: string; type?: string }> = []): ExtensionContext {
|
|
190
|
+
return { cwd, hasUI: false, ui: { notify: (message: string, type?: string) => { notices.push({ message, type }); } } } as unknown as ExtensionContext;
|
|
191
|
+
}
|
|
192
|
+
function confirmContext(cwd: string, answer: boolean): ExtensionContext {
|
|
193
|
+
return { cwd, hasUI: true, ui: { confirm: async () => answer, notify: () => {} } } as unknown as ExtensionContext;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const START_ORDINARY = { operation: "start", input: JSON.stringify({ mode: "ordinary" }) };
|
|
197
|
+
async function execStart(controller: RegisteredTool, id: string, ctx: ExtensionContext): Promise<Record<string, unknown>> {
|
|
198
|
+
const { details } = await controller.execute(id, START_ORDINARY, undefined, undefined, ctx);
|
|
199
|
+
return details as Record<string, unknown>;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Kill switch round trip (mirrors /gentle:review-mode status|disable|enable
|
|
204
|
+
// from the community guide flows).
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
test("dev-binary: gentle:review-mode round-trips status, disable, and enable against the real binary", { skip: !RUNNABLE }, async (t) => {
|
|
208
|
+
const cwd = repository(t);
|
|
209
|
+
const { commands } = runtime(journeyNative(DEV_BINARY!));
|
|
210
|
+
const command = commands.get("gentle:review-mode")!;
|
|
211
|
+
const notices: Array<{ message: string; type?: string }> = [];
|
|
212
|
+
|
|
213
|
+
await command.handler("status", headlessContext(cwd, notices));
|
|
214
|
+
assert.match(notices.at(-1)!.message, /receipt-driven development: on \(decided by \w+\)/);
|
|
215
|
+
|
|
216
|
+
await command.handler("disable", headlessContext(cwd, notices));
|
|
217
|
+
assert.match(notices.at(-1)!.message, /receipt-driven development: off \(decided by \w+\)/);
|
|
218
|
+
|
|
219
|
+
await command.handler("status", headlessContext(cwd, notices));
|
|
220
|
+
assert.match(notices.at(-1)!.message, /receipt-driven development: off \(decided by \w+\)/);
|
|
221
|
+
|
|
222
|
+
await command.handler("enable", headlessContext(cwd, notices));
|
|
223
|
+
assert.match(notices.at(-1)!.message, /receipt-driven development: on \(decided by \w+\)/);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Tier 0 silence: an empty candidate surfaces its hint verbatim and never
|
|
228
|
+
// triggers a consent prompt or notice.
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
test("dev-binary: an empty candidate stays silent (no consent notice) and surfaces the real hint verbatim", { skip: !RUNNABLE }, async (t) => {
|
|
232
|
+
const cwd = repository(t);
|
|
233
|
+
writeFileSync(join(cwd, "app.ts"), "export const value = 1;\n");
|
|
234
|
+
git(cwd, "add", ".");
|
|
235
|
+
git(cwd, "commit", "-qm", "initial");
|
|
236
|
+
const { controller } = runtime(journeyNative(DEV_BINARY!));
|
|
237
|
+
const notices: Array<{ message: string; type?: string }> = [];
|
|
238
|
+
const result = await execStart(controller, "tier-0", headlessContext(cwd, notices));
|
|
239
|
+
const rendered = result.result as Record<string, unknown>;
|
|
240
|
+
assert.equal(rendered.lenses_required, false);
|
|
241
|
+
assert.equal(typeof rendered.hint, "string");
|
|
242
|
+
assert.match(rendered.hint as string, /the candidate has no pending changes/);
|
|
243
|
+
assert.equal(result.consent_notice, undefined, "tier 0 must never surface a consent notice");
|
|
244
|
+
assert.equal(notices.length, 0, "tier 0 must never notify the user at all");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// Tier 2 evidence + consent envelope via a fake UI seam.
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
test("dev-binary: a high-risk change carries real risk_evidence and drives the consent envelope through a fake UI seam", { skip: !RUNNABLE }, async (t) => {
|
|
252
|
+
const cwd = repository(t);
|
|
253
|
+
const workflowDirectory = join(cwd, ".github", "workflows");
|
|
254
|
+
execFileSync("mkdir", ["-p", workflowDirectory]);
|
|
255
|
+
writeFileSync(join(workflowDirectory, "deploy.yml"), "name: x\n");
|
|
256
|
+
git(cwd, "add", ".");
|
|
257
|
+
git(cwd, "commit", "-qm", "initial");
|
|
258
|
+
writeFileSync(join(workflowDirectory, "deploy.yml"), "name: x\non: push\njobs:\n deploy:\n steps:\n - run: curl -s | bash\n");
|
|
259
|
+
|
|
260
|
+
const acceptedRun = journeyNative(DEV_BINARY!);
|
|
261
|
+
const { controller } = runtime(acceptedRun);
|
|
262
|
+
const accepted = await execStart(controller, "tier-2-accept", confirmContext(cwd, true));
|
|
263
|
+
const rendered = accepted.result as Record<string, unknown>;
|
|
264
|
+
assert.equal(rendered.risk_tier, "high");
|
|
265
|
+
assert.ok(Array.isArray(rendered.risk_evidence) && (rendered.risk_evidence as unknown[]).length > 0, "high risk must carry a non-empty risk_evidence phrase array");
|
|
266
|
+
assert.ok((rendered.risk_evidence as string[])[0]!.includes("deploy.yml"));
|
|
267
|
+
assert.ok(accepted.actor_binding, "accepting consent must proceed to actor_binding");
|
|
268
|
+
assert.equal(readReviewConsentLatch(cwd), true, "accepting consent must record the per-clone latch");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("dev-binary: declining the fake-UI consent prompt withholds actor_binding for this work unit only, against the real binary's own evidence", { skip: !RUNNABLE }, async (t) => {
|
|
272
|
+
const cwd = repository(t);
|
|
273
|
+
const workflowDirectory = join(cwd, ".github", "workflows");
|
|
274
|
+
execFileSync("mkdir", ["-p", workflowDirectory]);
|
|
275
|
+
writeFileSync(join(workflowDirectory, "deploy.yml"), "name: x\n");
|
|
276
|
+
git(cwd, "add", ".");
|
|
277
|
+
git(cwd, "commit", "-qm", "initial");
|
|
278
|
+
writeFileSync(join(workflowDirectory, "deploy.yml"), "name: x\non: push\njobs:\n deploy:\n steps:\n - run: curl -s | bash\n");
|
|
279
|
+
|
|
280
|
+
const { controller } = runtime(journeyNative(DEV_BINARY!));
|
|
281
|
+
const declined = await execStart(controller, "tier-2-decline", confirmContext(cwd, false));
|
|
282
|
+
assert.equal(declined.actor_binding, undefined, "declining must withhold actor dispatch for this work unit");
|
|
283
|
+
assert.equal(readReviewConsentLatch(cwd), false, "declining must never persist the latch");
|
|
284
|
+
assert.ok(declined.result, "the native start result itself is still reported even on decline");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// Disabled/unmanaged delivery renders as a successful skip, never a failure.
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
// IMPORTANT reachability finding (Phase 6 ground-truthing): gentle-ai only
|
|
292
|
+
// emits the disabled/unmanaged delivery envelope through lineage
|
|
293
|
+
// AUTO-DISCOVERY (`review validate --gate <gate>` with no `--lineage`) — its
|
|
294
|
+
// Go source (internal/cli/review_facade.go:2412-2433,1904-1916) routes an
|
|
295
|
+
// explicit `--lineage <id>` through discoverCompactFacadeReview's
|
|
296
|
+
// lineage-specific branch, whose failures are plain `fmt.Errorf` values, never
|
|
297
|
+
// the `*ReviewReceiptDiscoveryError` the disabled/unmanaged check requires —
|
|
298
|
+
// confirmed live against both a nonexistent lineage id ("load compact facade
|
|
299
|
+
// review lineage: ... no such file or directory", exit 1) and a real
|
|
300
|
+
// previously-started lineage under a disabled switch ("facade review receipt
|
|
301
|
+
// is not available", exit 1). Neither is the structured JSON envelope.
|
|
302
|
+
// Pi's `gentle_review` VALIDATE operation always binds an explicit
|
|
303
|
+
// lineageId (extensions/gentle-ai.ts throws "Review controller validate
|
|
304
|
+
// requires a lineageId" otherwise), so this envelope is structurally
|
|
305
|
+
// unreachable through today's controller wiring — this is a pre-existing
|
|
306
|
+
// property of the VALIDATE contract, not something Phase 5.2 introduced.
|
|
307
|
+
// This is flagged as a risk for a future design revision, not silently
|
|
308
|
+
// papered over here. What Phase 6 CAN and does prove against the real binary
|
|
309
|
+
// is that Pi's decoder correctly decodes the envelope gentle-ai actually
|
|
310
|
+
// sends via auto-discovery; the controller-level mapping/early-return
|
|
311
|
+
// (mapNativeValidateResult + the skip response) is proven separately with a
|
|
312
|
+
// synthetic fixture in tests/review-controller-native-routing.test.ts,
|
|
313
|
+
// because gentle-ai's own explicit-lineage code path can never supply it.
|
|
314
|
+
test("dev-binary: VALIDATE via lineage auto-discovery decodes the real disabled/unmanaged delivery envelope", { skip: !RUNNABLE }, async (t) => {
|
|
315
|
+
const cwd = repository(t);
|
|
316
|
+
writeFileSync(join(cwd, "app.ts"), "export const value = 1;\n");
|
|
317
|
+
git(cwd, "add", ".");
|
|
318
|
+
git(cwd, "commit", "-qm", "initial");
|
|
319
|
+
writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
|
|
320
|
+
git(cwd, "add", "--", "app.ts");
|
|
321
|
+
|
|
322
|
+
// Each journey uses its own throwaway temp repository (deleted in
|
|
323
|
+
// repository(t)'s cleanup), so there is no shared clone-local state to
|
|
324
|
+
// restore afterward.
|
|
325
|
+
const native = journeyNative(DEV_BINARY!);
|
|
326
|
+
await native.reviewMode!({ cwd, operation: "disable" });
|
|
327
|
+
|
|
328
|
+
// No lineageId: this is the one shape gentle-ai actually routes through
|
|
329
|
+
// its disabled/unmanaged discovery branch.
|
|
330
|
+
const result = await native.validate({ cwd, gate: "pre-commit" });
|
|
331
|
+
assert.equal(result.delivery, "disabled/unmanaged");
|
|
332
|
+
assert.equal(result.allowed, false);
|
|
333
|
+
assert.equal(result.result, "invalidated");
|
|
334
|
+
assert.equal(result.action, "repository-policy");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test.before(() => {
|
|
338
|
+
if (RUNNABLE) {
|
|
339
|
+
setNativeCliContractForTesting(BRIDGE_VERSION, {
|
|
340
|
+
start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true,
|
|
341
|
+
reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true,
|
|
342
|
+
mode: true, riskEvidence: true, hint: true, delivery: true,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
test.after(() => {
|
|
347
|
+
if (RUNNABLE) setNativeCliContractForTesting(BRIDGE_VERSION, undefined);
|
|
348
|
+
});
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "../lib/gentle-ai-binary.ts";
|
|
14
14
|
import { NativeReviewCliV213, createNativeReviewCli, type ExecFileAdapter } from "../lib/native-review-cli.ts";
|
|
15
15
|
import { resolveGentleAiReleaseAsset } from "../scripts/gentle-ai-installer.mjs";
|
|
16
|
+
import { requireNativeBinary } from "./support/native-binary-gate.ts";
|
|
16
17
|
|
|
17
18
|
const VERSION = { stdout: `gentle-ai ${GENTLE_AI_VERSION}\n`, stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false } as const;
|
|
18
19
|
const RUNTIME_DIRECTORY = `v${GENTLE_AI_VERSION}`;
|
|
@@ -20,7 +21,13 @@ const repoRuntimeBinary = join(import.meta.dirname, "..", ".gentle-ai", RUNTIME_
|
|
|
20
21
|
const releaseDigestsPinned = /^[0-9a-f]{64}$/.test(resolveGentleAiReleaseAsset(process.platform, process.arch).sha256);
|
|
21
22
|
// These integrity tests need the published official binary; they skip while a
|
|
22
23
|
// re-pinned release's archives and digest table are still pending.
|
|
23
|
-
const
|
|
24
|
+
const nativeBinaryGate = requireNativeBinary({
|
|
25
|
+
resolvedBinary: existsSync(repoRuntimeBinary) ? repoRuntimeBinary : undefined,
|
|
26
|
+
digestsPinned: releaseDigestsPinned,
|
|
27
|
+
env: process.env,
|
|
28
|
+
});
|
|
29
|
+
if (!nativeBinaryGate.run) console.log(`gentle-ai-binary: ${nativeBinaryGate.reason}`);
|
|
30
|
+
const verifiedBinaryTest = nativeBinaryGate.run ? test : test.skip;
|
|
24
31
|
|
|
25
32
|
async function writeVerifiedBinary(packageRoot: string, platform = process.platform): Promise<string> {
|
|
26
33
|
const asset = resolveGentleAiReleaseAsset(platform, process.arch);
|
|
@@ -18,16 +18,14 @@ import {
|
|
|
18
18
|
trustedSystemExtractor,
|
|
19
19
|
} from "../scripts/gentle-ai-installer.mjs";
|
|
20
20
|
|
|
21
|
-
// v2.
|
|
21
|
+
// v2.2.2 digests pinned from the published release: archive sha256 values match
|
|
22
22
|
// checksums.txt and freshly computed hashes; binary sha256 values were computed
|
|
23
23
|
// from the extracted executables of each verified archive.
|
|
24
24
|
const EXPECTED_ASSETS = {
|
|
25
|
-
"darwin/amd64": { name: "gentle-ai_2.
|
|
26
|
-
"darwin/arm64": { name: "gentle-ai_2.
|
|
27
|
-
"linux/amd64": { name: "gentle-ai_2.
|
|
28
|
-
"linux/arm64": { name: "gentle-ai_2.
|
|
29
|
-
"windows/amd64": { name: "gentle-ai_2.1.11_windows_amd64.zip", sha256: "43fb4e66cf36a7f01d2d343f59fe8ff37396fc0e98fb24abcd95bc79c2450e9a", binarySha256: "1c92cdcaf826d4bec6a6560770e5a0edefc09997364a664a5033abc6ab932b2b" },
|
|
30
|
-
"windows/arm64": { name: "gentle-ai_2.1.11_windows_arm64.zip", sha256: "9c2884075161715f0efac1ce55cd08f2d9313068f125443ade8de3a83645258a", binarySha256: "a903bface64af274a28cfdd08b5c55c67fc554bac9279d7d466fc3f99020bd89" },
|
|
25
|
+
"darwin/amd64": { name: "gentle-ai_2.2.2_darwin_amd64.tar.gz", sha256: "5ca67829903bf4c6b14665664f80f9d8216c84b10c8e50870d297f452cefb9dc", binarySha256: "9b239423450562d026384f482bbd2f1e3f2820431a84f0921743ac3df9d632de" },
|
|
26
|
+
"darwin/arm64": { name: "gentle-ai_2.2.2_darwin_arm64.tar.gz", sha256: "0193e1a284444dccee2863d31b8dbb76a982e8f9111955908d6a9131c1a5490e", binarySha256: "149b97248552c5e03ebc4d991f86b1360fb847a40fc315555a8aa256f95baca0" },
|
|
27
|
+
"linux/amd64": { name: "gentle-ai_2.2.2_linux_amd64.tar.gz", sha256: "b85bbb20eb2236de97b261df16cfc8d8394dfd07a137e885c4889b62d0c20fa1", binarySha256: "00d5732e8dd3945956800217a4f60213c2d9ca63351092a2cb7f4e5f9ece54f9" },
|
|
28
|
+
"linux/arm64": { name: "gentle-ai_2.2.2_linux_arm64.tar.gz", sha256: "61e7077342448273f0c43af49ce4d182594bab5e4f86f812975af2fbe69e3b0b", binarySha256: "a92685aa7dbea0cc4297d016569b3defe9bb30a7374a620021e245b74f50eb68" },
|
|
31
29
|
} as const;
|
|
32
30
|
|
|
33
31
|
test("default installer package root is the package containing scripts, not its parent", () => {
|
|
@@ -38,15 +36,15 @@ test("default installer package root is the package containing scripts, not its
|
|
|
38
36
|
assert.notEqual(resolveGentleAiInstallerPackageRoot(), dirname(expectedPackageRoot));
|
|
39
37
|
});
|
|
40
38
|
|
|
41
|
-
test("release mapping selects only the supported official v2.
|
|
39
|
+
test("release mapping selects only the supported official v2.2.2 archive and pinned digests", () => {
|
|
42
40
|
assert.deepEqual(
|
|
43
41
|
Object.fromEntries(Object.entries(GENTLE_AI_RELEASE_ASSETS).map(([key, asset]) => [key, { name: asset.name, sha256: asset.sha256, binarySha256: asset.binarySha256 }])),
|
|
44
42
|
EXPECTED_ASSETS,
|
|
45
43
|
);
|
|
46
|
-
assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_2.
|
|
47
|
-
assert.equal(resolveGentleAiReleaseAsset("
|
|
44
|
+
assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_2.2.2_linux_amd64.tar.gz");
|
|
45
|
+
assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_2.2.2_darwin_arm64.tar.gz");
|
|
48
46
|
for (const asset of Object.values(GENTLE_AI_RELEASE_ASSETS)) {
|
|
49
|
-
assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v2\.
|
|
47
|
+
assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v2\.2\.2\//);
|
|
50
48
|
}
|
|
51
49
|
});
|
|
52
50
|
|
|
@@ -67,13 +65,18 @@ test("release digests are all-or-none and install fails closed while any digest
|
|
|
67
65
|
}),
|
|
68
66
|
/checksum mismatch/,
|
|
69
67
|
);
|
|
70
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.
|
|
68
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai")), false);
|
|
71
69
|
}
|
|
72
70
|
});
|
|
73
71
|
|
|
74
|
-
test("win32
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
test("win32 is rejected with the same clear error as any other unsupported platform", () => {
|
|
73
|
+
// v2.2.2 publishes no Windows archive, so there is nothing honest to
|
|
74
|
+
// resolve. Failing here, by name, beats resolving an asset that would 404
|
|
75
|
+
// at download time and blame the network for a packaging decision.
|
|
76
|
+
for (const arch of ["x64", "arm64"]) {
|
|
77
|
+
assert.throws(() => resolveGentleAiReleaseAsset("win32", arch), /unsupported Gentle AI platform\/architecture/);
|
|
78
|
+
assert.throws(() => resolveGentleAiReleaseAsset("windows", arch), /unsupported Gentle AI platform\/architecture/);
|
|
79
|
+
}
|
|
77
80
|
});
|
|
78
81
|
|
|
79
82
|
test("unsupported platform pairs fail clearly before download", () => {
|
|
@@ -141,7 +144,7 @@ test("checksum mismatch cleans temporary state without promoting a binary", asyn
|
|
|
141
144
|
}),
|
|
142
145
|
/checksum mismatch/,
|
|
143
146
|
);
|
|
144
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.
|
|
147
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai")), false);
|
|
145
148
|
assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.startsWith(".gentle-ai-install-")), []);
|
|
146
149
|
});
|
|
147
150
|
|
|
@@ -163,7 +166,7 @@ test("installer promotes only the expected regular executable with executable PO
|
|
|
163
166
|
await chmod(extracted, 0o700);
|
|
164
167
|
},
|
|
165
168
|
});
|
|
166
|
-
const binary = join(packageRoot, ".gentle-ai", "v2.
|
|
169
|
+
const binary = join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai");
|
|
167
170
|
assert.equal(existsSync(binary), true);
|
|
168
171
|
assert.equal(await readFile(binary, "utf8"), "native executable");
|
|
169
172
|
assert.ok(((await stat(binary)).mode & 0o111) !== 0);
|
|
@@ -194,7 +197,7 @@ test("installer rejects an extracted binary that differs from its pinned digest"
|
|
|
194
197
|
}),
|
|
195
198
|
/binary checksum mismatch/,
|
|
196
199
|
);
|
|
197
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.
|
|
200
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai")), false);
|
|
198
201
|
});
|
|
199
202
|
|
|
200
203
|
test("installer repairs a valid non-executable POSIX binary instead of reusing it", async (t) => {
|
|
@@ -217,7 +220,7 @@ test("installer repairs a valid non-executable POSIX binary instead of reusing i
|
|
|
217
220
|
},
|
|
218
221
|
};
|
|
219
222
|
await installGentleAi(options);
|
|
220
|
-
const binary = join(packageRoot, ".gentle-ai", "v2.
|
|
223
|
+
const binary = join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai");
|
|
221
224
|
await chmod(binary, 0o600);
|
|
222
225
|
const repaired = await installGentleAi(options);
|
|
223
226
|
assert.equal(repaired.installed, true);
|
|
@@ -254,7 +257,7 @@ test("installer rejects archives with multiple expected executable entries", asy
|
|
|
254
257
|
}),
|
|
255
258
|
/exactly one regular gentle-ai/,
|
|
256
259
|
);
|
|
257
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.
|
|
260
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai")), false);
|
|
258
261
|
});
|
|
259
262
|
|
|
260
263
|
test("installer rejects an archive without the expected regular executable", async () => {
|
|
@@ -272,5 +275,5 @@ test("installer rejects an archive without the expected regular executable", asy
|
|
|
272
275
|
}),
|
|
273
276
|
/non-regular gentle-ai/,
|
|
274
277
|
);
|
|
275
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.
|
|
278
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai")), false);
|
|
276
279
|
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
NativeBinaryUnavailableError,
|
|
5
|
+
REQUIRE_DEV_BINARY_ENV,
|
|
6
|
+
REQUIRE_NATIVE_BINARY_ENV,
|
|
7
|
+
requireDevBinary,
|
|
8
|
+
requireNativeBinary,
|
|
9
|
+
} from "./support/native-binary-gate.ts";
|
|
10
|
+
|
|
11
|
+
// Two suites used to decide for themselves whether to run:
|
|
12
|
+
//
|
|
13
|
+
// tests/native-review-parity-runtime.test.ts
|
|
14
|
+
// const test = resolvedBinary === undefined ? baseTest.skip : baseTest;
|
|
15
|
+
// tests/gentle-ai-binary.test.ts
|
|
16
|
+
// const verifiedBinaryTest = releaseDigestsPinned && existsSync(...) ? test : test.skip;
|
|
17
|
+
//
|
|
18
|
+
// Both skip silently when the pinned runtime is absent, so a fresh clone or a
|
|
19
|
+
// CI job that never installed the binary reports green with zero real coverage
|
|
20
|
+
// of the thing those suites exist to cover. That is exactly how an earlier
|
|
21
|
+
// protocol-minor gap stayed hidden: the suite reported 0 fail while every test
|
|
22
|
+
// in it had skipped. The gate keeps the local convenience and removes the
|
|
23
|
+
// silence -- under GENTLE_PI_REQUIRE_NATIVE_BINARY=1 an absent binary is a
|
|
24
|
+
// failure, not a skip.
|
|
25
|
+
|
|
26
|
+
const PINNED = { resolvedBinary: "/tmp/gentle-ai", digestsPinned: true } as const;
|
|
27
|
+
|
|
28
|
+
test("the env var name is the one CI sets", () => {
|
|
29
|
+
assert.equal(REQUIRE_NATIVE_BINARY_ENV, "GENTLE_PI_REQUIRE_NATIVE_BINARY");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("a resolved binary with pinned digests runs, and names no skip reason", () => {
|
|
33
|
+
for (const env of [{}, { GENTLE_PI_REQUIRE_NATIVE_BINARY: "1" }]) {
|
|
34
|
+
const gate = requireNativeBinary({ ...PINNED, env });
|
|
35
|
+
assert.equal(gate.run, true);
|
|
36
|
+
assert.equal(Object.hasOwn(gate, "reason"), false);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("without the env var an unresolved binary yields a skip carrying a concrete reason", () => {
|
|
41
|
+
const gate = requireNativeBinary({ resolvedBinary: undefined, digestsPinned: true, env: {} });
|
|
42
|
+
|
|
43
|
+
assert.equal(gate.run, false);
|
|
44
|
+
assert.match(gate.reason as string, /\S/);
|
|
45
|
+
// The reason must name what is missing and how to get it, so a reader of CI
|
|
46
|
+
// output does not have to go read the gate to understand the skip.
|
|
47
|
+
assert.match(gate.reason as string, /binary/i);
|
|
48
|
+
assert.match(gate.reason as string, new RegExp(REQUIRE_NATIVE_BINARY_ENV));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("without the env var unpinned digests yield a skip with a distinct reason", () => {
|
|
52
|
+
const unresolved = requireNativeBinary({ resolvedBinary: undefined, digestsPinned: true, env: {} });
|
|
53
|
+
const unpinned = requireNativeBinary({ ...PINNED, digestsPinned: false, env: {} });
|
|
54
|
+
|
|
55
|
+
assert.equal(unpinned.run, false);
|
|
56
|
+
assert.match(unpinned.reason as string, /digest/i);
|
|
57
|
+
assert.notEqual(unpinned.reason, unresolved.reason, "the two causes must be distinguishable in output");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("under the env var an unresolved binary throws instead of skipping", () => {
|
|
61
|
+
assert.throws(
|
|
62
|
+
() => requireNativeBinary({ resolvedBinary: undefined, digestsPinned: true, env: { GENTLE_PI_REQUIRE_NATIVE_BINARY: "1" } }),
|
|
63
|
+
(error: unknown) => error instanceof NativeBinaryUnavailableError && /binary/i.test((error as Error).message),
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("under the env var unpinned digests throw instead of skipping", () => {
|
|
68
|
+
assert.throws(
|
|
69
|
+
() => requireNativeBinary({ ...PINNED, digestsPinned: false, env: { GENTLE_PI_REQUIRE_NATIVE_BINARY: "1" } }),
|
|
70
|
+
(error: unknown) => error instanceof NativeBinaryUnavailableError && /digest/i.test((error as Error).message),
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("only an exact \"1\" arms the gate, so a stray value cannot silently disarm CI", () => {
|
|
75
|
+
// A truthy-looking value that is not "1" must not be treated as armed, and
|
|
76
|
+
// must not be treated as arming either: the contract is one exact value.
|
|
77
|
+
for (const value of ["0", "", "true", "yes", "false"]) {
|
|
78
|
+
const gate = requireNativeBinary({ resolvedBinary: undefined, digestsPinned: true, env: { GENTLE_PI_REQUIRE_NATIVE_BINARY: value } });
|
|
79
|
+
assert.equal(gate.run, false, `${JSON.stringify(value)} must not arm the gate`);
|
|
80
|
+
}
|
|
81
|
+
assert.throws(() => requireNativeBinary({ resolvedBinary: undefined, digestsPinned: true, env: { GENTLE_PI_REQUIRE_NATIVE_BINARY: "1" } }), NativeBinaryUnavailableError);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Phase 13.12/13.13 follow-up: the same loud-skip gate, extended to the
|
|
85
|
+
// third self-skipping suite (tests/devbinary/native-review-parity.devtest.ts),
|
|
86
|
+
// gated behind its OWN env var so ordinary CI (no dev binary provisioned)
|
|
87
|
+
// keeps skipping silently by default.
|
|
88
|
+
|
|
89
|
+
test("the dev-binary env var name is distinct from the pinned-binary one", () => {
|
|
90
|
+
assert.equal(REQUIRE_DEV_BINARY_ENV, "GENTLE_PI_REQUIRE_DEV_BINARY");
|
|
91
|
+
assert.notEqual(REQUIRE_DEV_BINARY_ENV, REQUIRE_NATIVE_BINARY_ENV);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("an existing dev binary path runs, and names no skip reason", () => {
|
|
95
|
+
for (const env of [{}, { GENTLE_PI_REQUIRE_DEV_BINARY: "1" }]) {
|
|
96
|
+
const gate = requireDevBinary({ devBinaryPath: "/opt/dev/gentle-ai", exists: true, env });
|
|
97
|
+
assert.equal(gate.run, true);
|
|
98
|
+
assert.equal(Object.hasOwn(gate, "reason"), false);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("without the env var a missing GENTLE_AI_DEV_BINARY yields a skip carrying a concrete reason", () => {
|
|
103
|
+
const gate = requireDevBinary({ devBinaryPath: undefined, exists: false, env: {} });
|
|
104
|
+
assert.equal(gate.run, false);
|
|
105
|
+
assert.match(gate.reason as string, /GENTLE_AI_DEV_BINARY/);
|
|
106
|
+
assert.match(gate.reason as string, new RegExp(REQUIRE_DEV_BINARY_ENV));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("without the env var a set but non-existent dev binary path yields a distinct skip reason", () => {
|
|
110
|
+
const missingVar = requireDevBinary({ devBinaryPath: undefined, exists: false, env: {} });
|
|
111
|
+
const badPath = requireDevBinary({ devBinaryPath: "/no/such/gentle-ai", exists: false, env: {} });
|
|
112
|
+
assert.equal(badPath.run, false);
|
|
113
|
+
assert.match(badPath.reason as string, /\/no\/such\/gentle-ai/);
|
|
114
|
+
assert.notEqual(badPath.reason, missingVar.reason, "the two causes must be distinguishable in output");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("under the env var a missing dev binary throws instead of skipping", () => {
|
|
118
|
+
assert.throws(
|
|
119
|
+
() => requireDevBinary({ devBinaryPath: undefined, exists: false, env: { GENTLE_PI_REQUIRE_DEV_BINARY: "1" } }),
|
|
120
|
+
(error: unknown) => error instanceof NativeBinaryUnavailableError && /GENTLE_AI_DEV_BINARY/.test((error as Error).message),
|
|
121
|
+
);
|
|
122
|
+
assert.throws(
|
|
123
|
+
() => requireDevBinary({ devBinaryPath: "/no/such/gentle-ai", exists: false, env: { GENTLE_PI_REQUIRE_DEV_BINARY: "1" } }),
|
|
124
|
+
(error: unknown) => error instanceof NativeBinaryUnavailableError && /\/no\/such\/gentle-ai/.test((error as Error).message),
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("GENTLE_PI_REQUIRE_NATIVE_BINARY never arms the dev-binary gate, and vice versa", () => {
|
|
129
|
+
assert.equal(requireDevBinary({ devBinaryPath: undefined, exists: false, env: { GENTLE_PI_REQUIRE_NATIVE_BINARY: "1" } }).run, false);
|
|
130
|
+
assert.equal(requireNativeBinary({ resolvedBinary: undefined, digestsPinned: true, env: { GENTLE_PI_REQUIRE_DEV_BINARY: "1" } }).run, false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("the gate reads the passed env, never the ambient process env", () => {
|
|
134
|
+
const original = process.env[REQUIRE_NATIVE_BINARY_ENV];
|
|
135
|
+
try {
|
|
136
|
+
process.env[REQUIRE_NATIVE_BINARY_ENV] = "1";
|
|
137
|
+
// An explicit empty env must win: tests have to be able to exercise the
|
|
138
|
+
// unarmed branch even on a CI runner where the variable is set.
|
|
139
|
+
const gate = requireNativeBinary({ resolvedBinary: undefined, digestsPinned: true, env: {} });
|
|
140
|
+
assert.equal(gate.run, false);
|
|
141
|
+
} finally {
|
|
142
|
+
if (original === undefined) delete process.env[REQUIRE_NATIVE_BINARY_ENV];
|
|
143
|
+
else process.env[REQUIRE_NATIVE_BINARY_ENV] = original;
|
|
144
|
+
}
|
|
145
|
+
});
|