gentle-pi 4.2.0 → 4.4.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 +340 -138
- package/assets/agents/gentle-ai-explore.md +1 -0
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -1
- package/assets/design-canvas/canvas.css +648 -0
- package/assets/design-canvas/canvas.js +1497 -0
- package/assets/design-canvas/index.html +88 -0
- package/assets/design-canvas/inspector.js +290 -0
- package/assets/orchestrator-delegation-reference.md +163 -0
- package/assets/orchestrator-delegation.md +25 -176
- package/assets/orchestrator-tracking.md +1 -1
- package/assets/orchestrator-verification.md +14 -8
- package/assets/orchestrator-writer.md +5 -1
- package/assets/orchestrator.md +11 -11
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +237 -139
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
- package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/assets/diagrams/agent-orchestration.svg +54 -15
- package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
- package/docs/assets/diagrams/measured-routing.svg +49 -0
- package/docs/assets/diagrams/native-review.svg +52 -12
- package/docs/assets/diagrams/odd-workflow.svg +128 -66
- package/docs/assets/diagrams/permission-modes.svg +51 -0
- package/docs/assets/features/audio-notifications.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/bundled-install.md +65 -30
- package/docs/delegated-verification.md +16 -8
- package/docs/gentle-agents-activity.md +16 -6
- package/docs/gentle-music.md +61 -0
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +80 -9
- package/docs/install-wizard.md +37 -6
- package/docs/permission-modes.md +8 -3
- package/docs/readme-reference.md +120 -28
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +38 -10
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +270 -48
- package/extensions/gentle-ai.ts +490 -113
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-music.ts +289 -0
- package/extensions/gentle-shell.ts +359 -85
- package/extensions/gentle-todo.ts +43 -9
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +384 -0
- package/extensions/skill-registry.ts +63 -8
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +60 -1
- package/lib/agents-runner.ts +107 -9
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-view.ts +3 -1
- package/lib/agents-widget.ts +14 -1
- package/lib/auto-permission-memory.ts +48 -18
- package/lib/auto-permission-runtime.ts +21 -6
- package/lib/bounded-writer-admission.ts +51 -0
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +2 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +120 -0
- package/lib/delivered-test-lint.ts +80 -0
- package/lib/design-canvas-server.ts +947 -0
- package/lib/design-comments.ts +437 -0
- package/lib/design-handoffs.ts +313 -0
- package/lib/design-open-browser.ts +58 -0
- package/lib/design-sources.ts +120 -0
- package/lib/destructive-command-guard.ts +120 -9
- package/lib/external-editor.ts +31 -0
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +146 -36
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +154 -15
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +51 -2
- package/lib/herdr-activity.ts +24 -0
- package/lib/image-attachments.ts +150 -0
- package/lib/inprocess-reviewer.ts +33 -2
- package/lib/nan-provider.ts +3 -0
- package/lib/native-review-cli.ts +20 -5
- package/lib/notification-audio-native.ts +149 -13
- package/lib/notification-audio.ts +4 -0
- package/lib/notification-pulse-worker.ts +82 -18
- package/lib/notification-service.ts +5 -1
- package/lib/orchestrator-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +87 -8
- package/lib/orchestrator-scope.ts +31 -3
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- package/lib/provider-contract-bundle.ts +99 -53
- package/lib/review-candidate-view-owner.ts +29 -19
- package/lib/review-candidate-view.ts +36 -2
- package/lib/review-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/session-profile-append-controller.ts +713 -0
- package/lib/session-profile-disk-reader.ts +182 -0
- package/lib/session-profile-freeze.ts +30 -1
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +523 -0
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +29 -28
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +26 -6
- package/lib/shell-todo.ts +24 -5
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +30 -0
- package/lib/vim-editor-adapter.ts +11 -8
- package/lib/voice-artifacts.ts +188 -0
- package/lib/voice-command.ts +576 -0
- package/lib/voice-install.ts +455 -0
- package/lib/voice-platform.ts +99 -0
- package/lib/voice-push-to-talk.ts +262 -0
- package/lib/voice-session.ts +460 -0
- package/lib/voice-settings.ts +50 -0
- package/lib/voice-ui.ts +158 -0
- package/lib/yolo-session-policy.ts +58 -22
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +1 -2
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +145 -35
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +152 -13
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +51 -2
- package/runtime/native-review-cli.mjs +19 -4
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +119 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +5 -3
- package/scripts/installer-server.mjs +14 -4
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +94 -19
- package/scripts/pi-update.mjs +34 -6
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +43 -13
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agent-risk-escalation.test.ts +37 -10
- package/tests/agents-config.test.ts +61 -0
- package/tests/agents-fake-child.ts +8 -1
- package/tests/agents-protocol.test.ts +32 -0
- package/tests/agents-runner.test.ts +485 -7
- package/tests/agents-session-transport.test.ts +53 -3
- package/tests/ask-user-choice.test.ts +30 -0
- package/tests/auto-mode.test.ts +17 -0
- package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
- package/tests/auto-permission-runtime.test.ts +85 -1
- package/tests/autonomous-guard.test.ts +142 -0
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/bundled-install.test.ts +3 -3
- package/tests/card-content-policy.test.ts +19 -9
- package/tests/child-safety.test.ts +39 -0
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +15 -5
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +225 -0
- package/tests/delegate-for-reason-contract.test.ts +92 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- package/tests/design-canvas-assets.test.ts +710 -0
- package/tests/design-canvas-server.test.ts +1096 -0
- package/tests/design-comments.test.ts +201 -0
- package/tests/design-handoffs.test.ts +282 -0
- package/tests/design-open-browser.test.ts +70 -0
- package/tests/design-sources.test.ts +132 -0
- package/tests/destructive-command-guard.test.ts +85 -1
- package/tests/engram-safety.test.ts +132 -57
- package/tests/external-editor.test.ts +84 -0
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
- package/tests/gauge-theme.test.ts +120 -0
- package/tests/gentle-agents.test.ts +645 -21
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-child-guards.test.ts +61 -0
- package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-ai.test.ts +292 -13
- package/tests/gentle-design.test.ts +1313 -0
- package/tests/gentle-music.test.ts +335 -0
- package/tests/gentle-shell-bin.test.ts +163 -0
- package/tests/gentle-shell-bundled.test.ts +392 -176
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +107 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +89 -0
- package/tests/gentle-shell.test.ts +225 -17
- package/tests/gentle-todo.test.ts +190 -0
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/image-attachments.test.ts +119 -0
- package/tests/image-paste-chips.test.ts +236 -0
- package/tests/inprocess-reviewer.test.ts +55 -2
- package/tests/installer-bundles.test.ts +12 -6
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +16 -0
- package/tests/installer-server.test.ts +10 -2
- package/tests/installer-windows-bootstrap.test.ts +94 -7
- package/tests/installer-windows-diagnostics.test.ts +111 -0
- package/tests/main-channel-upgrade.test.ts +232 -2
- package/tests/main-channel.test.ts +140 -21
- package/tests/nan-provider.test.ts +36 -0
- package/tests/native-review-capability-contract.test.ts +17 -4
- package/tests/native-review-parity.test.ts +23 -2
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/orchestrator-budget.test.ts +22 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +80 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +24 -20
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update-launcher.test.ts +21 -2
- package/tests/pi-update.test.ts +99 -4
- package/tests/primary-session-prompt.test.ts +101 -0
- package/tests/provider-contract-bundle.test.ts +118 -0
- package/tests/provider-contract-mirror.test.ts +24 -0
- package/tests/quiet-tool-rendering.test.ts +23 -0
- package/tests/rdd-aware-verification-contract.test.ts +29 -11
- package/tests/rdd-status-line.test.ts +79 -0
- package/tests/review-agent-end-preflight.test.ts +6 -0
- package/tests/review-candidate-view.test.ts +49 -0
- package/tests/review-contract-prompt.test.ts +5 -2
- package/tests/review-controller-native-routing.test.ts +14 -1
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +147 -3
- package/tests/review-host-relay.test.ts +9 -2
- package/tests/review-json-arguments.test.ts +20 -3
- package/tests/review-loadout-rdd-off-sdk.test.ts +258 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- package/tests/review-risk-assessment.test.ts +163 -76
- package/tests/review-session-standing-permission-controller.test.ts +14 -1
- package/tests/review-session-standing-permission.test.ts +27 -1
- package/tests/run-test-suite.test.ts +15 -0
- package/tests/runtime-harness.mjs +16 -6
- package/tests/session-changes-shell.test.ts +1 -1
- package/tests/session-changes.test.ts +2 -1
- package/tests/session-profile-append-controller-fork.test.ts +458 -0
- package/tests/session-profile-append-controller-pi.test.ts +290 -0
- package/tests/session-profile-append-controller.test.ts +731 -0
- package/tests/session-profile-disk-reader-pi.test.ts +183 -0
- package/tests/session-profile-disk-reader.test.ts +311 -0
- package/tests/session-profile-freeze.test.ts +23 -0
- package/tests/session-scratch.test.ts +1011 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +21 -2
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-prompt.test.ts +3 -3
- package/tests/shell-render-cache.test.ts +134 -1
- package/tests/shell-sidebar-layout.test.ts +50 -0
- package/tests/short-check-routing-contract.test.ts +1 -1
- package/tests/skill-collision-prefixes.test.ts +3 -1
- package/tests/skill-registry-watcher.test.ts +17 -5
- package/tests/skill-registry.test.ts +47 -3
- package/tests/small-task-inline-contract.test.ts +37 -0
- package/tests/standalone-config-home.test.ts +324 -0
- package/tests/startup-banner.test.ts +5 -4
- package/tests/startup-home-repo.test.ts +34 -0
- package/tests/support/orchestrator-modules.ts +1 -0
- package/tests/task-size-routing-contract.test.ts +70 -10
- package/tests/test-scratch-contract.test.ts +102 -0
- package/tests/test-strength-contract.test.ts +36 -1
- package/tests/tool-batching-contract.test.ts +64 -0
- package/tests/verification-proportionality.test.ts +46 -0
- package/tests/verifier-admission.test.ts +154 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- package/tests/voice-command.test.ts +773 -0
- package/tests/voice-install.test.ts +323 -0
- package/tests/voice-platform.test.ts +176 -0
- package/tests/voice-push-to-talk.test.ts +444 -0
- package/tests/voice-session.test.ts +434 -0
- package/tests/voice-settings.test.ts +49 -0
- package/tests/voice-ui.test.ts +97 -0
- package/tests/voice-worker.test.ts +445 -0
- package/tests/weak-model-economy-prompt.test.ts +32 -0
- package/tests/windows-hidden-processes.test.ts +30 -2
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +19 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
- package/tests/yolo-temp-scratch.test.ts +71 -0
- package/scripts/mirror-odd-routing.mjs +0 -242
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import * as fsPromises from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join, win32 } from "node:path";
|
|
@@ -7,6 +7,7 @@ import test from "node:test";
|
|
|
7
7
|
import {
|
|
8
8
|
CHANNEL_SCHEMA,
|
|
9
9
|
GENTLE_AI_MAIN_PACKAGE,
|
|
10
|
+
GENTLE_AI_MODULE,
|
|
10
11
|
MainChannelError,
|
|
11
12
|
buildMainGentleAi,
|
|
12
13
|
channelStatePath,
|
|
@@ -23,6 +24,8 @@ import { artifactFor } from "../scripts/installer-downloads.mjs";
|
|
|
23
24
|
|
|
24
25
|
const SHELL_SHA = "6e7e3a18f794223396527a54c7c36d19c7d236c6";
|
|
25
26
|
const AI_SHA = "1f9d5e6423e37f7d2316859045f379ba9b5d8c3a";
|
|
27
|
+
const PSEUDO = "v4.0.1-0.20261008202137-1f9d5e6423e3";
|
|
28
|
+
const LIST_FAILURE = "go: github.com/gentleman-programming/gentle-ai/v4@v4.0.1-0.20261008202137-1f9d5e6423e3: module lookup failed";
|
|
26
29
|
const posixOnly = { skip: process.platform === "win32" };
|
|
27
30
|
|
|
28
31
|
function sandbox() {
|
|
@@ -33,6 +36,38 @@ function sandbox() {
|
|
|
33
36
|
}
|
|
34
37
|
type Call = { command: string; argv: string[]; options: { env?: Record<string, string>; cwd?: string } };
|
|
35
38
|
|
|
39
|
+
// Emulates the Go and binary invocations buildMainGentleAi makes, each driven by
|
|
40
|
+
// what `go list -m -json` resolves, what the binary embeds, and what it prints.
|
|
41
|
+
function gentleAiBuildRun(world: {
|
|
42
|
+
commit: string;
|
|
43
|
+
listVersion?: string;
|
|
44
|
+
originHash?: string;
|
|
45
|
+
embeddedVersion?: string;
|
|
46
|
+
probeVersion?: string;
|
|
47
|
+
listFails?: boolean;
|
|
48
|
+
}) {
|
|
49
|
+
const { commit, listVersion = "v4.2.0", originHash = commit, embeddedVersion = listVersion, probeVersion = listVersion.replace(/^v/, ""), listFails = false } = world;
|
|
50
|
+
const calls: Call[] = [];
|
|
51
|
+
const run = async (command: string, argv: string[], options: Call["options"]) => {
|
|
52
|
+
calls.push({ command, argv, options });
|
|
53
|
+
if (argv[0] === "install") {
|
|
54
|
+
mkdirSync(options.env!.GOBIN, { recursive: true });
|
|
55
|
+
writeFileSync(join(options.env!.GOBIN, "gentle-ai"), "#!/bin/sh\n", { mode: 0o755 });
|
|
56
|
+
return { code: 0, stdout: "" };
|
|
57
|
+
}
|
|
58
|
+
if (argv[0] === "list") {
|
|
59
|
+
if (listFails) return { code: 1, stdout: "", stderrTail: `${LIST_FAILURE}\n` };
|
|
60
|
+
return { code: 0, stdout: JSON.stringify({ Path: GENTLE_AI_MODULE, Version: listVersion,
|
|
61
|
+
Origin: { VCS: "git", URL: "https://github.com/gentleman-programming/gentle-ai", Hash: originHash, Ref: "refs/tags/v4.2.0" } }) };
|
|
62
|
+
}
|
|
63
|
+
if (argv[0] === "version" && argv[1] === "-m") {
|
|
64
|
+
return { code: 0, stdout: `${command} (binary)\n\tmod\t${GENTLE_AI_MODULE}\t${embeddedVersion}\th1:J8EluS7QrfM+m3vmQlF26B/shZRjD6rBfsKtrABL5mc=\n` };
|
|
65
|
+
}
|
|
66
|
+
return { code: 0, stdout: `gentle-ai ${probeVersion}\n` };
|
|
67
|
+
};
|
|
68
|
+
return { calls, run };
|
|
69
|
+
}
|
|
70
|
+
|
|
36
71
|
test("channel state defaults to release and round-trips a main record", async () => {
|
|
37
72
|
const s = sandbox();
|
|
38
73
|
try {
|
|
@@ -169,18 +204,7 @@ test("Gentle AI main is built from the exact commit through Go's checksum databa
|
|
|
169
204
|
const s = sandbox();
|
|
170
205
|
try {
|
|
171
206
|
const goPath = join(s.root, "go", "bin", "go");
|
|
172
|
-
const calls
|
|
173
|
-
const run = async (command: string, argv: string[], options: Call["options"]) => {
|
|
174
|
-
calls.push({ command, argv, options });
|
|
175
|
-
if (argv[0] === "install") {
|
|
176
|
-
mkdirSync(options.env!.GOBIN, { recursive: true });
|
|
177
|
-
const binary = join(options.env!.GOBIN, "gentle-ai");
|
|
178
|
-
writeFileSync(binary, "#!/bin/sh\n");
|
|
179
|
-
chmodSync(binary, 0o755);
|
|
180
|
-
return { code: 0, stdout: "" };
|
|
181
|
-
}
|
|
182
|
-
return { code: 0, stdout: "gentle-ai 4.0.1-0.20261008202137-1f9d5e6423e3\n" };
|
|
183
|
-
};
|
|
207
|
+
const { calls, run } = gentleAiBuildRun({ commit: AI_SHA, listVersion: PSEUDO });
|
|
184
208
|
const built = await buildMainGentleAi({ commit: AI_SHA, ctx: s.ctx, platform: "darwin", goPath, run, fs: fsPromises });
|
|
185
209
|
const install = calls[0];
|
|
186
210
|
assert.equal(install.command, goPath);
|
|
@@ -194,19 +218,107 @@ test("Gentle AI main is built from the exact commit through Go's checksum databa
|
|
|
194
218
|
} finally { s.cleanup(); }
|
|
195
219
|
});
|
|
196
220
|
|
|
197
|
-
|
|
221
|
+
// #2301: when main's HEAD is exactly a tag commit, Go stamps the tag (v4.2.0), not a
|
|
222
|
+
// pseudo-version, so the suffix check alone rejects every tagged release.
|
|
223
|
+
test("a main commit that is exactly a release tag is accepted when Go resolves that tag to the commit", posixOnly, async () => {
|
|
198
224
|
const s = sandbox();
|
|
199
225
|
try {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
226
|
+
const TAGGED = "4acaf4241beae1e0c977c92dfe406a270f8d40de";
|
|
227
|
+
const { run } = gentleAiBuildRun({ commit: TAGGED, listVersion: "v4.2.0", originHash: TAGGED });
|
|
228
|
+
const built = await buildMainGentleAi({ commit: TAGGED, ctx: s.ctx, platform: "darwin", goPath: "/go/bin/go", run, fs: fsPromises });
|
|
229
|
+
assert.equal(built.binaryPath, join(s.home, ".pi", "gentle-ai", "main", "gentle-ai", TAGGED, "gentle-ai"));
|
|
230
|
+
assert.equal(built.version, "4.2.0");
|
|
231
|
+
assert.deepEqual(JSON.parse(readFileSync(join(s.home, ".pi", "gentle-ai", "dev-binary.json"), "utf8")),
|
|
232
|
+
{ schema: "gentle-pi.dev-binary/v1", path: built.binaryPath });
|
|
233
|
+
} finally { s.cleanup(); }
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("a go list whose Origin.Hash names another commit is rejected, and the build is removed", posixOnly, async () => {
|
|
237
|
+
const s = sandbox();
|
|
238
|
+
try {
|
|
239
|
+
const OTHER = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
240
|
+
const { run } = gentleAiBuildRun({ commit: AI_SHA, listVersion: PSEUDO, originHash: OTHER });
|
|
241
|
+
await assert.rejects(buildMainGentleAi({ commit: AI_SHA, ctx: s.ctx, platform: "darwin", goPath: "/go/bin/go", run, fs: fsPromises }),
|
|
242
|
+
(error: MainChannelError) => error.code === "main-gentle-ai-unverified" && error.message.includes(OTHER) && error.message.includes(AI_SHA.slice(0, 12)));
|
|
243
|
+
assert.equal(existsSync(join(s.home, ".pi", "gentle-ai", "dev-binary.json")), false);
|
|
244
|
+
assert.equal(existsSync(join(s.home, ".pi", "gentle-ai", "main", "gentle-ai", AI_SHA)), false);
|
|
245
|
+
assert.equal(existsSync(join(s.home, ".pi", "gentle-ai", "main", ".build")), false);
|
|
246
|
+
} finally { s.cleanup(); }
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("a binary whose embedded module version differs from what Go resolved is rejected and never registered", posixOnly, async () => {
|
|
250
|
+
const s = sandbox();
|
|
251
|
+
try {
|
|
252
|
+
const { run } = gentleAiBuildRun({ commit: AI_SHA, listVersion: PSEUDO, embeddedVersion: "v4.1.2" });
|
|
253
|
+
await assert.rejects(buildMainGentleAi({ commit: AI_SHA, ctx: s.ctx, platform: "darwin", goPath: "/go/bin/go", run, fs: fsPromises }),
|
|
254
|
+
(error: MainChannelError) => error.code === "main-gentle-ai-unverified");
|
|
255
|
+
assert.equal(existsSync(join(s.home, ".pi", "gentle-ai", "dev-binary.json")), false);
|
|
256
|
+
assert.equal(existsSync(join(s.home, ".pi", "gentle-ai", "main", "gentle-ai", AI_SHA)), false);
|
|
257
|
+
} finally { s.cleanup(); }
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("a failed go list is rejected as unverified, with its stderr tail as the cause, and nothing is registered", posixOnly, async () => {
|
|
261
|
+
const s = sandbox();
|
|
262
|
+
try {
|
|
263
|
+
const { run } = gentleAiBuildRun({ commit: AI_SHA, listVersion: PSEUDO, listFails: true });
|
|
264
|
+
await assert.rejects(buildMainGentleAi({ commit: AI_SHA, ctx: s.ctx, platform: "darwin", goPath: "/go/bin/go", run, fs: fsPromises }),
|
|
265
|
+
(error: MainChannelError) => error.code === "main-gentle-ai-unverified" && error.cause instanceof Error && error.cause.message === LIST_FAILURE);
|
|
266
|
+
assert.equal(existsSync(join(s.home, ".pi", "gentle-ai", "dev-binary.json")), false);
|
|
267
|
+
assert.equal(existsSync(join(s.home, ".pi", "gentle-ai", "main", "gentle-ai", AI_SHA)), false);
|
|
268
|
+
} finally { s.cleanup(); }
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Rebuilding a commit that is already registered (a rerun wizard step, a lost
|
|
272
|
+
// channel record) must not delete the working override when verification fails.
|
|
273
|
+
for (const [name, failure] of Object.entries({
|
|
274
|
+
"go list fails": { listFails: true },
|
|
275
|
+
"the embedded version differs": { embeddedVersion: "v4.1.2" },
|
|
276
|
+
"the reported version differs": { probeVersion: "4.0.1-0.20261008202137-aaaaaaaaaaaa" },
|
|
277
|
+
})) test(`a registered build of the same commit survives a rebuild whose verification fails because ${name}`, posixOnly, async () => {
|
|
278
|
+
const s = sandbox();
|
|
279
|
+
try {
|
|
280
|
+
const directory = join(s.home, ".pi", "gentle-ai", "main", "gentle-ai", AI_SHA);
|
|
281
|
+
const binary = join(directory, "gentle-ai");
|
|
282
|
+
const registration = join(s.home, ".pi", "gentle-ai", "dev-binary.json");
|
|
283
|
+
mkdirSync(directory, { recursive: true });
|
|
284
|
+
writeFileSync(binary, "#!/bin/sh\n# registered\n", { mode: 0o755 });
|
|
285
|
+
const record = JSON.stringify({ schema: "gentle-pi.dev-binary/v1", path: binary });
|
|
286
|
+
writeFileSync(registration, record);
|
|
287
|
+
const { run } = gentleAiBuildRun({ commit: AI_SHA, listVersion: PSEUDO, ...failure });
|
|
288
|
+
await assert.rejects(buildMainGentleAi({ commit: AI_SHA, ctx: s.ctx, platform: "darwin", goPath: "/go/bin/go", run, fs: fsPromises }),
|
|
289
|
+
(error: MainChannelError) => error.code === "main-gentle-ai-unverified");
|
|
290
|
+
assert.equal(readFileSync(binary, "utf8"), "#!/bin/sh\n# registered\n");
|
|
291
|
+
assert.equal(readFileSync(registration, "utf8"), record);
|
|
292
|
+
assert.deepEqual(readdirSync(join(s.home, ".pi", "gentle-ai", "main", "gentle-ai")), [AI_SHA]);
|
|
293
|
+
assert.equal(existsSync(join(s.home, ".pi", "gentle-ai", "main", ".build")), false);
|
|
294
|
+
} finally { s.cleanup(); }
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("go list and go version -m run with the same sealed environment and cwd as go install", posixOnly, async () => {
|
|
298
|
+
const s = sandbox();
|
|
299
|
+
try {
|
|
300
|
+
const goPath = join(s.root, "go", "bin", "go");
|
|
301
|
+
const { calls, run } = gentleAiBuildRun({ commit: AI_SHA, listVersion: PSEUDO });
|
|
302
|
+
await buildMainGentleAi({ commit: AI_SHA, ctx: s.ctx, platform: "darwin", goPath, run, fs: fsPromises });
|
|
303
|
+
const sealed = { GOPROXY: "https://proxy.golang.org", GOSUMDB: "sum.golang.org", GOTOOLCHAIN: "local",
|
|
304
|
+
GOFLAGS: "-modcacherw", GONOSUMDB: "", GOPRIVATE: "", GOINSECURE: "", GOENV: "off", GOWORK: "off" };
|
|
305
|
+
const goCalls = calls.filter((call) => call.command === goPath);
|
|
306
|
+
assert.deepEqual(goCalls.map((call) => call.argv[0]), ["install", "list", "version"]);
|
|
307
|
+
for (const call of goCalls) {
|
|
308
|
+
assert.equal(call.options.cwd, join(s.home, ".pi", "gentle-ai", "main", ".build"), call.argv.join(" "));
|
|
309
|
+
for (const [key, value] of Object.entries(sealed)) assert.equal(call.options.env![key], value, `${key} for ${call.argv.join(" ")}`);
|
|
310
|
+
}
|
|
311
|
+
} finally { s.cleanup(); }
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("a Gentle AI build whose reported version differs from the resolved module version is rejected and never registered", posixOnly, async () => {
|
|
315
|
+
const s = sandbox();
|
|
316
|
+
try {
|
|
317
|
+
const { run } = gentleAiBuildRun({ commit: AI_SHA, listVersion: PSEUDO, probeVersion: "4.0.1-0.20261008202137-aaaaaaaaaaaa" });
|
|
207
318
|
await assert.rejects(buildMainGentleAi({ commit: AI_SHA, ctx: s.ctx, platform: "darwin", goPath: "/go/bin/go", run, fs: fsPromises }),
|
|
208
319
|
(error: MainChannelError) => error.code === "main-gentle-ai-unverified");
|
|
209
320
|
assert.equal(existsSync(join(s.home, ".pi", "gentle-ai", "dev-binary.json")), false);
|
|
321
|
+
assert.equal(existsSync(join(s.home, ".pi", "gentle-ai", "main", "gentle-ai", AI_SHA)), false);
|
|
210
322
|
} finally { s.cleanup(); }
|
|
211
323
|
});
|
|
212
324
|
|
|
@@ -278,6 +390,13 @@ async function upgradeWorld({ userGo = null as string | null, pinned = false, ac
|
|
|
278
390
|
const fetch = async (url: string) => ({ ok: true, status: 200, text: async () => (url.endsWith("/gentle-ai/commits/main") ? NEW_AI : SHELL_SHA) });
|
|
279
391
|
const run = async (command: string, argv: string[], options: Call["options"]) => {
|
|
280
392
|
calls.push({ command, argv, options });
|
|
393
|
+
const isGo = command === "/usr/bin/go" || command === pinnedGo;
|
|
394
|
+
const pseudo = (commit: string) => `v4.0.1-0.20261008202137-${commit.slice(0, 12)}`;
|
|
395
|
+
if (isGo && argv[0] === "version" && argv[1] === "-m") return { code: 0, stdout: `${command} (binary)\n\tmod\t${GENTLE_AI_MODULE}\t${pseudo(NEW_AI)}\th1:J8EluS7QrfM+m3vmQlF26B/shZRjD6rBfsKtrABL5mc=\n` };
|
|
396
|
+
if (isGo && argv[0] === "list") {
|
|
397
|
+
const requested = argv[3].slice(`${GENTLE_AI_MODULE}@`.length);
|
|
398
|
+
return { code: 0, stdout: JSON.stringify({ Path: GENTLE_AI_MODULE, Version: pseudo(requested), Origin: { VCS: "git", URL: "https://github.com/gentleman-programming/gentle-ai", Hash: requested } }) };
|
|
399
|
+
}
|
|
281
400
|
if (command === "/usr/bin/go" && argv[0] === "version") return { code: 0, stdout: `go version ${userGo === "devel" ? "devel go1.26-abc" : `go${userGo}`} darwin/arm64\n` };
|
|
282
401
|
if (argv[0] === "install") {
|
|
283
402
|
mkdirSync(options.env!.GOBIN, { recursive: true });
|
|
@@ -3,6 +3,7 @@ import test from "node:test";
|
|
|
3
3
|
import type { RefreshModelsContext } from "@earendil-works/pi-ai";
|
|
4
4
|
import { clampThinkingLevel, createModels, getSupportedThinkingLevels, InMemoryCredentialStore } from "@earendil-works/pi-ai";
|
|
5
5
|
import type { Provider } from "@earendil-works/pi-ai";
|
|
6
|
+
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
6
7
|
import nanProviderExtension from "../extensions/nan-provider.ts";
|
|
7
8
|
import { createNanProviderConfig as createNativeProvider, isNanAdaptiveReasoningModel, NAN_PROVIDER_BASE_URL, NAN_PROVIDER_ID } from "../lib/nan-provider.ts";
|
|
8
9
|
|
|
@@ -195,6 +196,41 @@ test("initial catalog contains all seven documented chat models with configured
|
|
|
195
196
|
assert.deepEqual(model?.cost, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
196
197
|
});
|
|
197
198
|
|
|
199
|
+
test("only NaN DeepSeek serializes its unchanged output cap as max_tokens", async () => {
|
|
200
|
+
const models = createNativeProvider().getModels();
|
|
201
|
+
const model = models.find((model) => model.id === "deepseek-v4-flash")!;
|
|
202
|
+
let body: Record<string, unknown> | undefined;
|
|
203
|
+
const result = await completeSimple(model, { messages: [{ role: "user", content: "offline probe", timestamp: 0 }] }, {
|
|
204
|
+
apiKey: "synthetic-key", reasoning: "minimal",
|
|
205
|
+
fetch: async (input, init) => {
|
|
206
|
+
assert.equal(String(input), `${NAN_PROVIDER_BASE_URL}/chat/completions`);
|
|
207
|
+
body = JSON.parse(String(init?.body));
|
|
208
|
+
return new Response('data: {"id":"offline","choices":[{"index":0,"delta":{"role":"assistant","content":"ok"},"finish_reason":"stop"}]}\n\ndata: [DONE]\n\n', {
|
|
209
|
+
headers: { "content-type": "text/event-stream" },
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
assert.equal(result.stopReason, "stop");
|
|
214
|
+
assert.equal(body?.max_tokens, 16_384);
|
|
215
|
+
assert.ok(!Object.hasOwn(body!, "max_completion_tokens"));
|
|
216
|
+
assert.equal(body?.reasoning_effort, "minimal");
|
|
217
|
+
assert.equal(model.maxTokens, 16_384);
|
|
218
|
+
assert.equal(model.compat?.maxTokensField, "max_tokens");
|
|
219
|
+
assert.ok(models.filter((candidate) => candidate.id !== model.id).every((candidate) => candidate.compat === undefined));
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("DeepSeek compat is isolated across snapshots, providers, and live refreshes", async () => {
|
|
223
|
+
const provider = createNativeProvider({ fetchImpl: async () => jsonResponse({ data: [{ id: "deepseek-v4-flash" }] }) });
|
|
224
|
+
for (const live of [false, true]) {
|
|
225
|
+
if (live) await provider.refreshModels!(refreshContext({ type: "api_key", key: "synthetic-key" }));
|
|
226
|
+
const model = provider.getModels().find((model) => model.id === "deepseek-v4-flash")!;
|
|
227
|
+
assert.equal(model.compat?.maxTokensField, "max_tokens");
|
|
228
|
+
model.compat!.maxTokensField = "max_completion_tokens";
|
|
229
|
+
assert.equal(provider.getModels().find((candidate) => candidate.id === model.id)!.compat?.maxTokensField, "max_tokens");
|
|
230
|
+
assert.equal(createNativeProvider().getModels().find((model) => model.id === "deepseek-v4-flash")!.compat?.maxTokensField, "max_tokens");
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
198
234
|
test("GLM models expose exactly NaN's four efforts and no off", () => {
|
|
199
235
|
const models = createNativeProvider().getModels();
|
|
200
236
|
for (const id of ["glm5.3", "glm5.3-flash"]) {
|
|
@@ -103,9 +103,10 @@ test("2.4.0 repeats 2.2.3 because the START envelope Pi consumes still omits ris
|
|
|
103
103
|
// the envelope carries, not a capability it lost.
|
|
104
104
|
// delivery `review validate --gate pre-commit` still answers
|
|
105
105
|
// `gentle-ai.review-gate-result/v1` with
|
|
106
|
-
// `delivery: "disabled/unmanaged"` at exit 0.
|
|
107
|
-
//
|
|
108
|
-
//
|
|
106
|
+
// `delivery: "disabled/unmanaged"` at exit 0. RDD is on by
|
|
107
|
+
// default again since gentle-ai v4.0.0, so this is the answer
|
|
108
|
+
// for an install that turned review off, and the column still
|
|
109
|
+
// matters.
|
|
109
110
|
//
|
|
110
111
|
// riskEvidence and hint stay dark for exactly the reason the 2.2.0 comment
|
|
111
112
|
// gives, re-proven rather than inherited: the negotiated
|
|
@@ -379,11 +380,23 @@ test("4.1.1 explicitly repeats 4.0.0 because capabilities/v2.6 is unchanged desp
|
|
|
379
380
|
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["4.0.0"] as Record<string, boolean>);
|
|
380
381
|
});
|
|
381
382
|
|
|
383
|
+
test("4.2.0 explicitly repeats 4.1.1 because capabilities/v2.6 is unchanged despite provider contract 1.4.0", () => {
|
|
384
|
+
const contract = NATIVE_CLI_CONTRACTS["4.2.0"] as Record<string, boolean>;
|
|
385
|
+
assert.ok(contract);
|
|
386
|
+
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["4.1.1"] as Record<string, boolean>);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("4.3.1 explicitly repeats 4.2.0 because capabilities/v2.6 and provider contract 1.4.0 are unchanged", () => {
|
|
390
|
+
const contract = NATIVE_CLI_CONTRACTS["4.3.1"] as Record<string, boolean>;
|
|
391
|
+
assert.ok(contract);
|
|
392
|
+
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["4.2.0"] as Record<string, boolean>);
|
|
393
|
+
});
|
|
394
|
+
|
|
382
395
|
test("no shipped version key was added beyond the pin bump", () => {
|
|
383
396
|
// Rows are promises to consumers, so a new key only ever appears in a
|
|
384
397
|
// dedicated commit alongside a pin bump, never as a side effect. v2.2.4 and
|
|
385
398
|
// v2.3.0 shipped upstream while Pi stayed on 2.2.3 and were never pinned,
|
|
386
399
|
// so they get no row: a row asserts ground truth measured against a binary
|
|
387
400
|
// Pi actually ran, and the table only has to be ascending, not gapless.
|
|
388
|
-
assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0", "2.5.0-rc.3", "2.5.0", "2.6.0", "2.7.0", "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.1", "3.0.0", "3.0.1", "3.1.0", "3.2.1", "3.4.0", "3.5.0", "3.6.0", "3.6.1", "3.7.0", "4.0.0", "4.1.1"]);
|
|
401
|
+
assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0", "2.5.0-rc.3", "2.5.0", "2.6.0", "2.7.0", "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.1", "3.0.0", "3.0.1", "3.1.0", "3.2.1", "3.4.0", "3.5.0", "3.6.0", "3.6.1", "3.7.0", "4.0.0", "4.1.1", "4.2.0", "4.3.1"]);
|
|
389
402
|
});
|
|
@@ -3,7 +3,8 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import test from "node:test";
|
|
6
|
+
import test, { after, before } from "node:test";
|
|
7
|
+
import { setWindowsAclAuthorityForTesting } from "../lib/review-candidate-view-owner.ts";
|
|
7
8
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
9
|
import { __testing, createGentleAiExtension, PendingReviewConsentRegistry } from "../extensions/gentle-ai.ts";
|
|
9
10
|
import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
|
|
@@ -28,6 +29,24 @@ import {
|
|
|
28
29
|
type ReviewStatusV3,
|
|
29
30
|
} from "../lib/review-integration-v2.ts";
|
|
30
31
|
|
|
32
|
+
// Coordination tests use fixture authority; real Windows ACL coverage lives
|
|
33
|
+
// in review-candidate-view.test.ts, in a separate test process.
|
|
34
|
+
let fixtureAclChecks = 0;
|
|
35
|
+
before(() => {
|
|
36
|
+
if (process.platform === "win32") {
|
|
37
|
+
setWindowsAclAuthorityForTesting(() => { fixtureAclChecks++; });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
after(() => setWindowsAclAuthorityForTesting());
|
|
41
|
+
|
|
42
|
+
test("coordination suites isolate Windows ACL authority with teardown", () => {
|
|
43
|
+
for (const file of ["native-review-parity", "review-controller-native-routing", "review-session-standing-permission-controller"]) {
|
|
44
|
+
const source = readFileSync(new URL(`./${file}.test.ts`, import.meta.url), "utf8");
|
|
45
|
+
assert.match(source, /import \{ setWindowsAclAuthorityForTesting \} from/, file);
|
|
46
|
+
assert.match(source, /after\(\(\) => setWindowsAclAuthorityForTesting\(\)\)/, file);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
31
50
|
const CAPTURED_FIXTURES = join(process.cwd(), "tests", "fixtures", "devbinary");
|
|
32
51
|
|
|
33
52
|
function captured(name: string): unknown {
|
|
@@ -218,7 +237,9 @@ function repository(t: test.TestContext): string {
|
|
|
218
237
|
|
|
219
238
|
function startStatus(cwd: string): ReviewStatusV3 {
|
|
220
239
|
const candidates = new CandidateViewRegistry();
|
|
240
|
+
const checksBefore = fixtureAclChecks;
|
|
221
241
|
const candidate = candidates.create({ contributorRoot: cwd });
|
|
242
|
+
if (process.platform === "win32") assert.ok(fixtureAclChecks > checksBefore, "coordination candidates must use the fixture ACL authority");
|
|
222
243
|
try {
|
|
223
244
|
return {
|
|
224
245
|
contract: "gentle-ai.review-integration/v2",
|
|
@@ -363,7 +384,7 @@ test("review-mode gate retains every off-source continuation and fails closed on
|
|
|
363
384
|
for (const [source, expected] of [
|
|
364
385
|
["clone_local", /clear this clone-local override/],
|
|
365
386
|
["global", /cannot override a global off/],
|
|
366
|
-
["default", /
|
|
387
|
+
["default", /RDD is on by default, but this install could not resolve the review mode and failed closed to off/],
|
|
367
388
|
] as const) {
|
|
368
389
|
const native = {
|
|
369
390
|
reviewMode: async () => ({ operation: "status", scope: "clone", status: { global: source === "global" ? "off" : "", cloneLocal: source === "clone_local" ? "off" : "", effective: "off" as const, source } }),
|
|
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import test from "node:test";
|
|
5
|
+
import { __testing } from "../extensions/gentle-ai.ts";
|
|
5
6
|
|
|
6
7
|
// gentle-shell#1731 T13 (evidence L24): in bench task x4 the user wrote "I may
|
|
7
8
|
// stop you between the two and pick the second part up later"; every Gentle
|
|
@@ -62,7 +63,8 @@ test("T13: the Close gate requires continuing or quoting the user's explicit sto
|
|
|
62
63
|
|
|
63
64
|
test("T13: budgets hold and the pinned human-control line is untouched", () => {
|
|
64
65
|
const bytes = Buffer.byteLength(tracking, "utf8");
|
|
65
|
-
|
|
66
|
+
// S14: close the open slice before a high-risk commit; outcomes per commit (12,600 -> 12,850 B), same as MODULE_BUDGETS.
|
|
67
|
+
assert.ok(bytes <= 12_850, `orchestrator-tracking.md is ${bytes} B, over its 12,850 B budget`);
|
|
66
68
|
assert.ok(core.includes("- Preserve human control: user decisions beat agent momentum."), "core human-control line must stay verbatim");
|
|
67
69
|
assert.ok(core.includes("4. **Track**") && core.includes("`orchestrator-tracking.md`"), "Track must still load the tracking module");
|
|
68
70
|
});
|
|
@@ -70,7 +72,8 @@ test("T13: budgets hold and the pinned human-control line is untouched", () => {
|
|
|
70
72
|
test("T13: the always-on harness narrows conditional intent and gates Close on pending tasks", () => {
|
|
71
73
|
// The harness ODD steps are injected on every request, including the small
|
|
72
74
|
// path that never loads the tracking module, so the narrowing must live there too.
|
|
73
|
-
|
|
75
|
+
// They are rendered from the mirrored routing/pi.md, so check the rendered harness.
|
|
76
|
+
const harness = __testing.buildGentlePrompt("gentleman");
|
|
74
77
|
assert.ok(
|
|
75
78
|
harness.includes("Ambiguous or conditional change intent (unclear whether a change is authorized at all) gets one clarification; stop and wait."),
|
|
76
79
|
"harness Authorize step must narrow conditional intent",
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { EventEmitter } from "node:events";
|
|
3
4
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
5
|
import { createServer, type Server } from "node:net";
|
|
5
6
|
import { tmpdir } from "node:os";
|
|
6
7
|
import { join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
7
9
|
import { test } from "node:test";
|
|
8
10
|
import { NativePulsePlayer, NATIVE_PULSE_SCHEMA, type NativePulseOptions, type NativePulseSpawnOptions } from "../lib/notification-audio-native.ts";
|
|
9
11
|
|
|
@@ -75,9 +77,12 @@ async function startFakeServer(): Promise<FakeServer> {
|
|
|
75
77
|
class FakeChild extends EventEmitter {
|
|
76
78
|
readonly stdout = new EventEmitter();
|
|
77
79
|
readonly stderr = new EventEmitter();
|
|
80
|
+
readonly stdin = { writes: [] as string[], write(chunk: string): boolean { this.writes.push(chunk); return true; } };
|
|
78
81
|
readonly kills: string[] = [];
|
|
79
82
|
kill(signal: string): boolean { this.kills.push(signal); return true; }
|
|
80
83
|
}
|
|
84
|
+
const okPlay = (): string => `${JSON.stringify({ schema: NATIVE_PULSE_SCHEMA, ok: true, played: true })}\n`;
|
|
85
|
+
const failPlay = (): string => `${JSON.stringify({ schema: NATIVE_PULSE_SCHEMA, ok: false })}\n`;
|
|
81
86
|
function recorder(platform = "linux", overrides: Partial<NativePulseOptions> = {}) {
|
|
82
87
|
const spawned: Array<{ executable: string; args: string[]; options: NativePulseSpawnOptions }> = [];
|
|
83
88
|
const children: FakeChild[] = [];
|
|
@@ -135,11 +140,77 @@ test("play gate false prevents spawning and no async runs after it", async () =>
|
|
|
135
140
|
const { player: second, spawned: secondSpawned, children } = recorder();
|
|
136
141
|
const playing = second.play("/tmp/s.wav", undefined, { permit: () => true });
|
|
137
142
|
assert.equal(secondSpawned.length, 1);
|
|
138
|
-
children[0]!.stdout.emit("data",
|
|
139
|
-
children[0]!.emit("close");
|
|
143
|
+
children[0]!.stdout.emit("data", okPlay());
|
|
140
144
|
await playing;
|
|
141
145
|
});
|
|
142
146
|
|
|
147
|
+
/** N plays must reuse ONE persistent `serve` worker: spawn once, queue, answer one envelope per play. */
|
|
148
|
+
test("persistent serve worker: N plays spawn exactly one worker and each play succeeds", async () => {
|
|
149
|
+
const { player, spawned, children } = recorder();
|
|
150
|
+
const paths = ["/tmp/a.wav", "/tmp/b.wav", "/tmp/c.wav"];
|
|
151
|
+
const plays = paths.map(path => player.play(path));
|
|
152
|
+
assert.equal(spawned.length, 1, "N plays must spawn exactly one serve worker");
|
|
153
|
+
assert.equal(spawned[0]!.args[3], "serve");
|
|
154
|
+
assert.deepEqual(spawned[0]!.options.stdio, ["pipe", "pipe", "pipe"]);
|
|
155
|
+
assert.equal(children[0]!.stdin.writes.length, 1, "queued plays must not write until the active play settles");
|
|
156
|
+
for (const write of children[0]!.stdin.writes) {
|
|
157
|
+
const request = JSON.parse(write) as { schema: string; op: string; path: string };
|
|
158
|
+
assert.equal(request.schema, NATIVE_PULSE_SCHEMA); assert.equal(request.op, "play"); assert.equal(request.path, paths[0]);
|
|
159
|
+
}
|
|
160
|
+
for (let i = 0; i < plays.length; i++) children[0]!.stdout.emit("data", okPlay());
|
|
161
|
+
await Promise.all(plays);
|
|
162
|
+
assert.equal(spawned.length, 1);
|
|
163
|
+
assert.equal(children[0]!.stdin.writes.length, 3);
|
|
164
|
+
assert.deepEqual(children[0]!.stdin.writes.map(write => (JSON.parse(write) as { path: string }).path), paths);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("a crashed serve worker is replaced by a fresh spawn on the next play", async () => {
|
|
168
|
+
const { player, spawned, children } = recorder();
|
|
169
|
+
const first = player.play("/tmp/a.wav");
|
|
170
|
+
children[0]!.stdout.emit("data", okPlay());
|
|
171
|
+
await first;
|
|
172
|
+
children[0]!.emit("close"); // crash while idle
|
|
173
|
+
const second = player.play("/tmp/b.wav");
|
|
174
|
+
assert.equal(spawned.length, 2, "the next play must spawn a fresh worker after a crash");
|
|
175
|
+
assert.equal(spawned[1]!.args[3], "serve");
|
|
176
|
+
children[1]!.stdout.emit("data", okPlay());
|
|
177
|
+
await second;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("a crash with a play in flight fails that play privately and the next play spawns fresh", async () => {
|
|
181
|
+
const { player, spawned, children } = recorder();
|
|
182
|
+
const failing = player.play("/tmp/a.wav");
|
|
183
|
+
children[0]!.emit("close");
|
|
184
|
+
await assert.rejects(failing, /Native pulse/);
|
|
185
|
+
const next = player.play("/tmp/b.wav");
|
|
186
|
+
assert.equal(spawned.length, 2);
|
|
187
|
+
children[1]!.stdout.emit("data", okPlay());
|
|
188
|
+
await next;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("dispose kills the persistent worker, fails pending plays and the next play spawns fresh", async () => {
|
|
192
|
+
const { player, spawned, children } = recorder();
|
|
193
|
+
const playing = player.play("/tmp/a.wav");
|
|
194
|
+
player.dispose();
|
|
195
|
+
assert.deepEqual(children[0]!.kills, ["SIGKILL"]);
|
|
196
|
+
await assert.rejects(playing, /Native pulse/);
|
|
197
|
+
const next = player.play("/tmp/b.wav");
|
|
198
|
+
assert.equal(spawned.length, 2);
|
|
199
|
+
children[1]!.stdout.emit("data", okPlay());
|
|
200
|
+
await next;
|
|
201
|
+
player.dispose(); // a second dispose is a no-op
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("pending plays are bounded fail-closed and never grow without bound", async () => {
|
|
205
|
+
const { player, spawned, children } = recorder();
|
|
206
|
+
const plays = Array.from({ length: 10 }, (_, i) => player.play(`/tmp/${i}.wav`).catch(error => error));
|
|
207
|
+
assert.equal(spawned.length, 1);
|
|
208
|
+
assert.match(String(await plays[9]), /too many pending/);
|
|
209
|
+
for (let i = 0; i < 9; i++) children[0]!.stdout.emit("data", okPlay()); // settle active + queued
|
|
210
|
+
const settled = await Promise.all(plays);
|
|
211
|
+
assert.equal(settled.filter(value => value instanceof Error).length, 1);
|
|
212
|
+
});
|
|
213
|
+
|
|
143
214
|
test("malformed, oversize and error output fail privately and kill at most once", async () => {
|
|
144
215
|
const malformed = recorder();
|
|
145
216
|
const malformedProbe = malformed.player.probe();
|
|
@@ -158,9 +229,11 @@ test("malformed, oversize and error output fail privately and kill at most once"
|
|
|
158
229
|
|
|
159
230
|
const failure = recorder();
|
|
160
231
|
const playing = failure.player.play("/private/snapshot.wav");
|
|
161
|
-
failure.children[0]!.stdout.emit("data",
|
|
162
|
-
|
|
232
|
+
failure.children[0]!.stdout.emit("data", failPlay());
|
|
233
|
+
// A private ok:false leaves the healthy worker serviceable; no kill, no restart needed.
|
|
163
234
|
await assert.rejects(playing, (error: Error) => /Native pulse/.test(error.message) && !/snapshot|cookie|\/private/.test(error.message));
|
|
235
|
+
assert.equal(failure.children[0]!.kills.length, 0);
|
|
236
|
+
assert.equal(failure.spawned.length, 1);
|
|
164
237
|
});
|
|
165
238
|
|
|
166
239
|
test("timeout and abort SIGKILL once and settle only on close", async () => {
|
|
@@ -181,6 +254,11 @@ test("timeout and abort SIGKILL once and settle only on close", async () => {
|
|
|
181
254
|
const playing = aborted.player.play("/tmp/s.wav", controller.signal);
|
|
182
255
|
controller.abort();
|
|
183
256
|
assert.deepEqual(aborted.children[0]!.kills, ["SIGKILL"]);
|
|
257
|
+
let abortSettled = false; void playing.then(() => { abortSettled = true; }, () => { abortSettled = true; });
|
|
258
|
+
aborted.children[0]!.emit("exit", null);
|
|
259
|
+
aborted.children[0]!.emit("error", new Error("kill race")); // Node emits close after error
|
|
260
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
261
|
+
assert.equal(abortSettled, false);
|
|
184
262
|
aborted.children[0]!.emit("close");
|
|
185
263
|
await assert.rejects(playing, /aborted|Native pulse/);
|
|
186
264
|
});
|
|
@@ -210,7 +288,8 @@ test("probe never throws for offline/malformed IPC while play rejects and cleans
|
|
|
210
288
|
assert.deepEqual(await probing, { available: false, formats: [] });
|
|
211
289
|
const { player: playPlayer, children: playChildren } = recorder();
|
|
212
290
|
const playing = playPlayer.play("/tmp/s.wav");
|
|
213
|
-
playChildren[0]!.stdout.emit("data", JSON.stringify({ schema: "wrong", ok: true, played: true }));
|
|
291
|
+
playChildren[0]!.stdout.emit("data", `${JSON.stringify({ schema: "wrong", ok: true, played: true })}\n`);
|
|
292
|
+
assert.deepEqual(playChildren[0]!.kills, ["SIGKILL"], "a malformed serve line is a protocol violation: kill once");
|
|
214
293
|
playChildren[0]!.emit("close");
|
|
215
294
|
await assert.rejects(playing, /Native pulse/);
|
|
216
295
|
});
|
|
@@ -259,13 +338,13 @@ test("real worker plays a fake PCM snapshot to a fake socket with no OS audio",
|
|
|
259
338
|
const snapshot = join(dir, "sound.wav");
|
|
260
339
|
writeFileSync(snapshot, bytes, { mode: 0o400 });
|
|
261
340
|
const server = await startFakeServer();
|
|
341
|
+
const player = new NativePulsePlayer({ platform: "linux", env: { PULSE_SERVER: `unix:${server.path}` }, playTimeoutMs: 8000 });
|
|
262
342
|
try {
|
|
263
|
-
const player = new NativePulsePlayer({ platform: "linux", env: { PULSE_SERVER: `unix:${server.path}` }, playTimeoutMs: 8000 });
|
|
264
343
|
await player.play(snapshot);
|
|
265
344
|
assert.deepEqual(Buffer.concat(server.pcm), dataOf(bytes));
|
|
266
345
|
assert.ok(server.seen.includes(3) && server.seen.includes(12) && server.seen.includes(4));
|
|
267
346
|
assert.ok(existsSync(snapshot), "the bridge/worker must not delete the snapshot; the owner does");
|
|
268
|
-
} finally { await server.close(); rmSync(dir, { recursive: true, force: true }); }
|
|
347
|
+
} finally { player.dispose(); await server.close(); rmSync(dir, { recursive: true, force: true }); }
|
|
269
348
|
});
|
|
270
349
|
|
|
271
350
|
test("real worker rejects invalid WAV before opening the socket", async () => {
|
|
@@ -273,9 +352,49 @@ test("real worker rejects invalid WAV before opening the socket", async () => {
|
|
|
273
352
|
const snapshot = join(dir, "sound.wav");
|
|
274
353
|
writeFileSync(snapshot, Buffer.alloc(64, 0x00), { mode: 0o400 });
|
|
275
354
|
const server = await startFakeServer();
|
|
355
|
+
const player = new NativePulsePlayer({ platform: "linux", env: { PULSE_SERVER: `unix:${server.path}` }, playTimeoutMs: 8000 });
|
|
276
356
|
try {
|
|
277
|
-
const player = new NativePulsePlayer({ platform: "linux", env: { PULSE_SERVER: `unix:${server.path}` }, playTimeoutMs: 8000 });
|
|
278
357
|
await assert.rejects(player.play(snapshot), /Native pulse/);
|
|
279
358
|
assert.deepEqual(server.seen, []);
|
|
280
|
-
} finally { await server.close(); rmSync(dir, { recursive: true, force: true }); }
|
|
359
|
+
} finally { player.dispose(); await server.close(); rmSync(dir, { recursive: true, force: true }); }
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
/** Direct serve-protocol check: bounded fail-closed requests, private per-play failures, stdin EOF teardown. */
|
|
363
|
+
test("real serve worker fails closed on protocol violations and exits on stdin EOF", async () => {
|
|
364
|
+
const workerPath = fileURLToPath(new URL("../lib/notification-pulse-worker.ts", import.meta.url));
|
|
365
|
+
const serveEnv = { ...process.env, NODE_NO_WARNINGS: "1", GENTLE_PI_AGENTS_CHILD: "1" };
|
|
366
|
+
const spawnServe = () => spawn(process.execPath, ["--experimental-strip-types", workerPath, "serve"],
|
|
367
|
+
{ stdio: ["pipe", "pipe", "pipe"], env: serveEnv });
|
|
368
|
+
const collect = (child: ReturnType<typeof spawn>): string[] => {
|
|
369
|
+
const lines: string[] = []; let text = "";
|
|
370
|
+
child.stdout!.setEncoding("utf8");
|
|
371
|
+
child.stdout!.on("data", (chunk: string) => {
|
|
372
|
+
text += chunk;
|
|
373
|
+
for (let index = text.indexOf("\n"); index !== -1; index = text.indexOf("\n")) { lines.push(text.slice(0, index)); text = text.slice(index + 1); }
|
|
374
|
+
});
|
|
375
|
+
return lines;
|
|
376
|
+
};
|
|
377
|
+
const exited = (child: ReturnType<typeof spawn>): Promise<number | null> => new Promise(resolve => {
|
|
378
|
+
if (child.exitCode !== null || child.signalCode !== null) { resolve(child.exitCode); return; }
|
|
379
|
+
child.once("exit", code => resolve(code));
|
|
380
|
+
});
|
|
381
|
+
// (a) A malformed line is a protocol violation: one generic envelope, then a crash the owner restarts.
|
|
382
|
+
const crashing = spawnServe();
|
|
383
|
+
const crashingLines = collect(crashing);
|
|
384
|
+
crashing.stdin!.write("not json\n");
|
|
385
|
+
assert.equal(await exited(crashing), 1);
|
|
386
|
+
assert.deepEqual(await crashingLines, [JSON.stringify({ schema: "gentle.audio.pulse/v1", ok: false })]);
|
|
387
|
+
// (b) A schema-valid request for a missing snapshot fails privately and the worker keeps serving.
|
|
388
|
+
const serving = spawnServe();
|
|
389
|
+
const servingLines = collect(serving);
|
|
390
|
+
for (const path of ["/nonexistent/gentle-a.wav", "/nonexistent/gentle-b.wav"])
|
|
391
|
+
serving.stdin!.write(`${JSON.stringify({ schema: "gentle.audio.pulse/v1", op: "play", path })}\n`);
|
|
392
|
+
await new Promise(resolve => setTimeout(resolve, 1200));
|
|
393
|
+
assert.deepEqual(servingLines, [
|
|
394
|
+
JSON.stringify({ schema: "gentle.audio.pulse/v1", ok: false }),
|
|
395
|
+
JSON.stringify({ schema: "gentle.audio.pulse/v1", ok: false }),
|
|
396
|
+
]);
|
|
397
|
+
// (c) stdin EOF (owner death) is a clean exit.
|
|
398
|
+
serving.stdin!.end();
|
|
399
|
+
assert.equal(await exited(serving), 0);
|
|
281
400
|
});
|