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
|
@@ -16,12 +16,16 @@ import {
|
|
|
16
16
|
bundledUpdatePlan,
|
|
17
17
|
parseBundledUpgradeArgs,
|
|
18
18
|
withBundledNpmCommand,
|
|
19
|
+
withoutBundledNpmCommand,
|
|
20
|
+
withoutMissingBundledNpmCommand,
|
|
19
21
|
resolveBundledInstall,
|
|
20
22
|
setBundledInstallForTesting,
|
|
21
23
|
type BundledFs,
|
|
24
|
+
isBundledNpmCommand,
|
|
22
25
|
type BundledPi,
|
|
23
26
|
} from "../lib/gentle-shell-bundled.ts";
|
|
24
27
|
import { artifactFor, engramPinVersion } from "../scripts/installer-downloads.mjs";
|
|
28
|
+
import { bundledNpmCommand, prefixLayout } from "../scripts/bundled-install.mjs";
|
|
25
29
|
import { gentlePiConfigHome } from "../lib/agent-home.ts";
|
|
26
30
|
import { resolveBackgroundSubagentsPolicy } from "../lib/background-subagents-policy.ts";
|
|
27
31
|
|
|
@@ -128,7 +132,7 @@ test("bundledPi fails with the version, the pinned Pi and the way to repair it",
|
|
|
128
132
|
assert.equal(bundledPi(install, { platform: "linux", fs: outside }).ok, false);
|
|
129
133
|
});
|
|
130
134
|
|
|
131
|
-
const ISOLATION = { prefix: "/g", engram: "/g/runtime/engram-3.
|
|
135
|
+
const ISOLATION = { prefix: "/g", engram: "/g/runtime/engram-3.5.2/engram" };
|
|
132
136
|
|
|
133
137
|
test("bundledChildEnv keeps the user's environment and adds only the bundled Pi, the prefix and Engram, no Pi override and no Pi update check", () => {
|
|
134
138
|
const user = { PATH: "/home/ana/.nvm/bin:/usr/bin", NODE_OPTIONS: "--max-old-space-size=4096", npm_config_registry: "https://npm.example", npm_config_prefix: "/home/ana/.npm-global",
|
|
@@ -140,7 +144,7 @@ test("bundledChildEnv keeps the user's environment and adds only the bundled Pi,
|
|
|
140
144
|
// folder from the prefix, and Gentle AI 4.0.0 reads that variable as a parent.
|
|
141
145
|
assert.deepEqual(env, { PATH: user.PATH, NODE_OPTIONS: user.NODE_OPTIONS, npm_config_registry: user.npm_config_registry, npm_config_prefix: user.npm_config_prefix,
|
|
142
146
|
GENTLE_PI_CONFIG_HOME: user.GENTLE_PI_CONFIG_HOME, GENTLE_PI_AGENTS_PI: "/g/node /g/cli.js", PI_SKIP_VERSION_CHECK: "1", GENTLE_SHELL_PREFIX: "/g",
|
|
143
|
-
ENGRAM_BIN: "/g/runtime/engram-3.
|
|
147
|
+
ENGRAM_BIN: "/g/runtime/engram-3.5.2/engram" });
|
|
144
148
|
// The subagent runner splits that variable on whitespace: a spaced path falls
|
|
145
149
|
// back to its own default, the running node and cli.js.
|
|
146
150
|
const spaced = bundledChildEnv({ GENTLE_PI_AGENTS_PI: "pi" }, { node: "C:\\Users\\Ana Li\\node.exe", cli: "C:\\cli.js", ...ISOLATION });
|
|
@@ -174,6 +178,83 @@ test("withBundledNpmCommand sets only npmCommand and leaves an already bundled s
|
|
|
174
178
|
for (const malformed of ["{", "[]", "null"]) assert.deepEqual(withBundledNpmCommand(malformed, command), { status: "malformed" });
|
|
175
179
|
});
|
|
176
180
|
|
|
181
|
+
// #2469: the npmCommand a bundled self-uninstall left behind names a node.exe
|
|
182
|
+
// the uninstall removed, so Pi package installs fail with ENOENT until it is
|
|
183
|
+
// gone; a command naming nothing inside the removed prefix stays.
|
|
184
|
+
test("withoutBundledNpmCommand removes the key when an entry names the removed prefix and keeps the rest", () => {
|
|
185
|
+
const text = '{\n "theme": "gentle",\n "npmCommand": ["/p/runtime/node.exe", "--update-notifier=false"]\n}\n';
|
|
186
|
+
const removed = withoutBundledNpmCommand(text, (path) => path === "/p/runtime/node.exe");
|
|
187
|
+
assert.ok(removed.status === "removed", removed.status);
|
|
188
|
+
assert.deepEqual(JSON.parse(removed.text), { theme: "gentle" });
|
|
189
|
+
assert.match(removed.text, /^\{\n {4}"theme"/);
|
|
190
|
+
assert.ok(removed.text.endsWith("}\n"));
|
|
191
|
+
assert.deepEqual(withoutBundledNpmCommand(undefined, () => true), { status: "unchanged" });
|
|
192
|
+
assert.deepEqual(withoutBundledNpmCommand('{"npmCommand":"npm"}', () => true), { status: "unchanged" });
|
|
193
|
+
assert.deepEqual(withoutBundledNpmCommand('{"npmCommand":["node"]}', () => true), { status: "unchanged" });
|
|
194
|
+
assert.deepEqual(withoutBundledNpmCommand('{"npmCommand":["/other/node"]}', () => false), { status: "unchanged" });
|
|
195
|
+
assert.deepEqual(withoutBundledNpmCommand('{"theme":"gentle"}', () => true), { status: "unchanged" });
|
|
196
|
+
for (const malformed of ["{", "[]", "null"]) assert.deepEqual(withoutBundledNpmCommand(malformed, () => true), { status: "malformed" });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("withoutMissingBundledNpmCommand removes only a bundled npmCommand whose executable is an absolute path that is gone", () => {
|
|
200
|
+
const text = '{\n "theme": "gentle",\n "npmCommand": ["/p/runtime/node-24.21.0/bin/node", "--userconfig=/p/pnpm/config/npmrc", "--globalconfig=/p/npm/npmrc", "--cache=/p/npm/cache", "--update-notifier=false"]\n}\n';
|
|
201
|
+
const removed = withoutMissingBundledNpmCommand(text, (path) => path !== "/p/runtime/node-24.21.0/bin/node");
|
|
202
|
+
assert.ok(removed.status === "removed", removed.status);
|
|
203
|
+
assert.deepEqual(JSON.parse(removed.text), { theme: "gentle" });
|
|
204
|
+
// An executable that exists, a PATH command, a non-array value, no key, no file.
|
|
205
|
+
assert.deepEqual(withoutMissingBundledNpmCommand(text, () => true), { status: "unchanged" });
|
|
206
|
+
assert.deepEqual(withoutMissingBundledNpmCommand('{"npmCommand":["npm"]}', () => false), { status: "unchanged" });
|
|
207
|
+
assert.deepEqual(withoutMissingBundledNpmCommand('{"npmCommand":"npm"}', () => false), { status: "unchanged" });
|
|
208
|
+
assert.deepEqual(withoutMissingBundledNpmCommand('{"theme":"gentle"}', () => false), { status: "unchanged" });
|
|
209
|
+
assert.deepEqual(withoutMissingBundledNpmCommand(undefined, () => false), { status: "unchanged" });
|
|
210
|
+
for (const malformed of ["{", "[]", "null"]) assert.deepEqual(withoutMissingBundledNpmCommand(malformed, () => false), { status: "malformed" });
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// A missing program alone is not proof the command is ours: a user's
|
|
214
|
+
// npmCommand on an offline drive must survive the launcher repair, so only the
|
|
215
|
+
// shape a bundled install writes is ever dropped (PR #2472 review).
|
|
216
|
+
test("withoutMissingBundledNpmCommand never drops a user's npmCommand, even with a missing program", () => {
|
|
217
|
+
// The posix form the launcher sees on this platform is dropped. The Windows
|
|
218
|
+
// form is covered by isBundledNpmCommand: the launcher's isAbsolute is
|
|
219
|
+
// platform-specific, so a C:\ path is only absolute on Windows.
|
|
220
|
+
const posix = "/home/ana/.gentle-shell";
|
|
221
|
+
const flags = (root: string, sep: string) => [`--userconfig=${root}${sep}pnpm${sep}config${sep}npmrc`, `--globalconfig=${root}${sep}npm${sep}npmrc`, `--cache=${root}${sep}npm${sep}cache`];
|
|
222
|
+
const bundledForm = [`${posix}/runtime/node-24.21.0/bin/node`, `${posix}/runtime/node-24.21.0/lib/node_modules/npm/bin/npm-cli.js`, ...flags(posix, "/"), `--prefix=${posix}/npm/prefix`, "--update-notifier=false"];
|
|
223
|
+
{
|
|
224
|
+
const command = bundledForm;
|
|
225
|
+
const text = JSON.stringify({ theme: "gentle", npmCommand: command });
|
|
226
|
+
const removed = withoutMissingBundledNpmCommand(text, () => false);
|
|
227
|
+
assert.ok(removed.status === "removed", `${command[0]}: ${removed.status}`);
|
|
228
|
+
}
|
|
229
|
+
// What a user writes is never ours, even with a missing program.
|
|
230
|
+
const userCommands: unknown[][] = [
|
|
231
|
+
["npm"],
|
|
232
|
+
["C:\\tools\\node.exe", "C:\\tools\\npm-cli.js"],
|
|
233
|
+
["C:\\Users\\ana\\AppData\\Local\\gentle-shell\\runtime\\node-24.21.0\\node.exe", "npm-cli.js"],
|
|
234
|
+
["/opt/runtime/node-20.0.0/bin/node", "/opt/npm-cli.js", "--update-notifier=false"],
|
|
235
|
+
];
|
|
236
|
+
for (const command of userCommands) {
|
|
237
|
+
const text = JSON.stringify({ theme: "gentle", npmCommand: command });
|
|
238
|
+
assert.deepEqual(withoutMissingBundledNpmCommand(text, () => false), { status: "unchanged" }, JSON.stringify(command));
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("isBundledNpmCommand recognizes only the npmCommand a bundled install writes, in every released form", () => {
|
|
243
|
+
const win = "C:\\Users\\ana\\AppData\\Local\\gentle-shell";
|
|
244
|
+
const posix = "/home/ana/.gentle-shell";
|
|
245
|
+
const flags = (root: string, sep: string) => [`--userconfig=${root}${sep}pnpm${sep}config${sep}npmrc`, `--globalconfig=${root}${sep}npm${sep}npmrc`, `--cache=${root}${sep}npm${sep}cache`];
|
|
246
|
+
// What the installer writes today on each platform, and its two released forms.
|
|
247
|
+
assert.equal(isBundledNpmCommand(bundledNpmCommand(prefixLayout({ platform: "win32", home: "C:\\Users\\ana", root: win }))), true);
|
|
248
|
+
assert.equal(isBundledNpmCommand(bundledNpmCommand(prefixLayout({ platform: "darwin", home: "/home/ana" }))), true);
|
|
249
|
+
assert.equal(isBundledNpmCommand([`${win}\\runtime\\node-24.21.0\\node.exe`, `${win}\\runtime\\node-24.21.0\\gentle-shell\\npm`, ...flags(win, "\\"), "--update-notifier=false"]), true);
|
|
250
|
+
assert.equal(isBundledNpmCommand([`${posix}/runtime/node-24.21.0/bin/node`, `${posix}/runtime/node-24.21.0/lib/node_modules/npm/bin/npm-cli.js`, ...flags(posix, "/"), `--prefix=${posix}/npm/prefix`, "--update-notifier=false"]), true);
|
|
251
|
+
// What a user writes is never ours, even with a missing program.
|
|
252
|
+
for (const command of [["npm"], ["C:\\tools\\node.exe", "C:\\tools\\npm-cli.js"], [`${win}\\runtime\\node-24.21.0\\node.exe`, "npm-cli.js"],
|
|
253
|
+
["/opt/runtime/node-20.0.0/bin/node", "/opt/npm-cli.js", "--update-notifier=false"]]) {
|
|
254
|
+
assert.equal(isBundledNpmCommand(command), false, JSON.stringify(command));
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
177
258
|
test("bundledUpdatePlan redirects Pi's own update and keeps the extension updates", () => {
|
|
178
259
|
assert.deepEqual(bundledUpdatePlan(["update"]), { self: true });
|
|
179
260
|
assert.deepEqual(bundledUpdatePlan(["update", "--self"]), { self: true });
|
|
@@ -235,7 +316,7 @@ const posixHost = process.platform === "win32" ? "the bundled prefix fixture use
|
|
|
235
316
|
const SHELL = JSON.parse(readFileSync(join(repository, "package.json"), "utf8")).version;
|
|
236
317
|
const FIXTURE_ID = `${SHELL}-1.2.0`;
|
|
237
318
|
const FAKE_PI = [
|
|
238
|
-
"import {
|
|
319
|
+
"import {mkdirSync, readFileSync, writeFileSync} from 'node:fs'; import {join, dirname} from 'node:path';",
|
|
239
320
|
"const args = process.argv.slice(2);",
|
|
240
321
|
"if (args[0] === 'install' && args[1] === 'npm:gentle-engram@0.4.2' && process.env.FIXTURE_INSTALL_CAPABILITY !== '0') {",
|
|
241
322
|
" const home = process.env.PI_CODING_AGENT_DIR; const metadata = join(home, 'npm/node_modules/gentle-engram/package.json'); mkdirSync(dirname(metadata), {recursive:true});",
|
|
@@ -245,37 +326,46 @@ const FAKE_PI = [
|
|
|
245
326
|
" if (index < 0) packages.push(args[1]); else packages[index] = typeof packages[index] === 'string' ? args[1] : {...packages[index], source:args[1]}; settings.packages = packages; writeFileSync(path, JSON.stringify(settings)); }",
|
|
246
327
|
"}",
|
|
247
328
|
"if (args.includes('--version')) { console.log('1.2.0'); process.exit(0); }",
|
|
248
|
-
// Stands in for gentle-engram creating the store through the bundled core during the session.
|
|
249
|
-
"if (process.env.FIXTURE_CREATE_STORE && args[0] !== 'install' && args[0] !== 'list') {",
|
|
250
|
-
" const data = join(process.env.HOME, '.engram');",
|
|
251
|
-
" if (process.env.ENGRAM_BIN?.includes('engram-guard') !== true) { mkdirSync(data, {recursive:true});",
|
|
252
|
-
" if (!existsSync(join(data, '.instance-id'))) writeFileSync(join(data, '.instance-id'), process.env.FIXTURE_CREATE_STORE + '\\n');",
|
|
253
|
-
" const header = Buffer.alloc(4096); header.write('SQLite format 3\\0', 0, 'latin1'); header.writeUInt16BE(4096, 16); header[18] = 2; header[19] = 2;",
|
|
254
|
-
" writeFileSync(join(data, 'engram.db'), header); writeFileSync(join(data, 'engram.db-wal'), '');",
|
|
255
|
-
" }",
|
|
256
|
-
"}",
|
|
257
329
|
"const pick = ['PATH', 'GENTLE_SHELL_PI', 'GENTLE_PI_AGENTS_PI', 'PI_SKIP_VERSION_CHECK', 'npm_config_prefix', 'npm_config_registry', 'NODE_OPTIONS', 'PI_CODING_AGENT_DIR',",
|
|
258
|
-
"\t'GENTLE_SHELL_PREFIX', 'GENTLE_PI_CONFIG_HOME', 'ENGRAM_BIN', 'ENGRAM_URL', 'ENGRAM_STARTUP_POLICY', 'ENGRAM_STARTUP_NOTICE', 'GENTLE_AI_HOME', 'GENTLE_AI_PI_CONFIG_DIR', 'GENTLE_AI_ENGRAM_BIN', 'GENTLE_AI_NO_SELF_UPDATE', 'GH_TOKEN', 'ENGRAM_NO_UPDATE_CHECK'];",
|
|
259
|
-
"
|
|
260
|
-
"const provenance = (() => { try { return JSON.parse(readFileSync(record, 'utf8')); } catch { return null; } })();",
|
|
261
|
-
"console.log(JSON.stringify({ cli: process.argv[1], args, provenance, env: Object.fromEntries(pick.map((k) => [k, process.env[k]])) }));",
|
|
262
|
-
// Stands in for a crash, kill or power loss of the launcher before its exit handler runs.
|
|
263
|
-
"if (process.env.FIXTURE_KILL_LAUNCHER && args[0] !== 'install') process.kill(process.ppid, 'SIGKILL');",
|
|
330
|
+
"\t'GENTLE_SHELL_PREFIX', 'GENTLE_PI_CONFIG_HOME', 'ENGRAM_BIN', 'ENGRAM_URL', 'ENGRAM_SERVER_MODE', 'ENGRAM_SOCKET', 'ENGRAM_STARTUP_POLICY', 'ENGRAM_STARTUP_NOTICE', 'GENTLE_AI_HOME', 'GENTLE_AI_PI_CONFIG_DIR', 'GENTLE_AI_ENGRAM_BIN', 'GENTLE_AI_NO_SELF_UPDATE', 'GH_TOKEN', 'ENGRAM_NO_UPDATE_CHECK'];",
|
|
331
|
+
"console.log(JSON.stringify({ cli: process.argv[1], args, env: Object.fromEntries(pick.map((k) => [k, process.env[k]])) }));",
|
|
264
332
|
].join("\n");
|
|
333
|
+
const READY_COMPATIBILITY = { revision: 1, source: "active", status: "ready", offline_automatic_opening_authorized: false, active_reuse_authorized: true,
|
|
334
|
+
instance_id: "0123456789abcdef0123456789abcdef", runtime_id: "a".repeat(32), capability: "startup-verified-store-v1" };
|
|
335
|
+
// The bundled core: `serve` logs its invocation (FIXTURE_SERVE_LOG) and, like
|
|
336
|
+
// Engram 3.5.2, answers the ready /compatibility contract on a port it publishes
|
|
337
|
+
// in FIXTURE_ENGRAM_PORT_FILE, where the redirected default endpoint finds it.
|
|
338
|
+
// FIXTURE_SERVE=exit fails like a lost bind; race fails after starting the
|
|
339
|
+
// server a concurrent launch would. Every other command only prints a line.
|
|
265
340
|
const FIXTURE_CORE = [
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
'
|
|
341
|
+
`#!${process.execPath}`,
|
|
342
|
+
"const { appendFileSync, renameSync, writeFileSync } = require('node:fs'); const { spawn } = require('node:child_process');",
|
|
343
|
+
"const args = process.argv.slice(2);",
|
|
344
|
+
"if (args[0] !== 'serve') { console.log('engram'); process.exit(0); }",
|
|
345
|
+
"const mode = process.env.FIXTURE_SERVE ?? 'ready';",
|
|
346
|
+
"const pick = ['HOME', 'GH_TOKEN', 'GITHUB_TOKEN', 'ENGRAM_NO_UPDATE_CHECK', 'ENGRAM_URL', 'ENGRAM_BIN', 'ENGRAM_HTTP_TOKEN', 'ENGRAM_STARTUP_POLICY'];",
|
|
347
|
+
"appendFileSync(process.env.FIXTURE_SERVE_LOG, JSON.stringify({ args, mode, cwd: process.cwd(), pid: process.pid, env: Object.fromEntries(pick.map((k) => [k, process.env[k]])) }) + '\\n');",
|
|
348
|
+
"if (mode === 'exit') process.exit(1);",
|
|
349
|
+
"if (mode === 'race') { spawn(process.execPath, [process.argv[1], 'serve'], { env: { ...process.env, FIXTURE_SERVE: 'late' }, detached: true, stdio: 'ignore' }).unref(); process.exit(1); }",
|
|
350
|
+
`const ready = ${JSON.stringify(JSON.stringify(READY_COMPATIBILITY))};`,
|
|
351
|
+
"setTimeout(() => require('node:http').createServer((request, response) => {",
|
|
352
|
+
" response.writeHead(request.url === '/compatibility' ? 200 : 404, { 'content-type': 'application/json' }); response.end(request.url === '/compatibility' ? ready : '{}');",
|
|
353
|
+
"}).listen(0, '127.0.0.1', function () { const file = process.env.FIXTURE_ENGRAM_PORT_FILE; writeFileSync(file + '.tmp', String(this.address().port)); renameSync(file + '.tmp', file); }),",
|
|
354
|
+
" mode === 'late' ? 300 : 0);",
|
|
355
|
+
"setTimeout(() => process.exit(0), 60000);",
|
|
273
356
|
"",
|
|
274
357
|
].join("\n");
|
|
275
358
|
|
|
276
359
|
function bundledPrefix(t: TestContext, { pi = "1.2.0", core = true } = {}) {
|
|
277
360
|
const base = realpathSync(mkdtempSync(join(tmpdir(), "gentle-shell-bundled-")));
|
|
278
|
-
|
|
361
|
+
const serveLog = join(base, "engram-serve.log");
|
|
362
|
+
const serves = (): Array<{ args: string[]; mode: string; cwd: string; pid: number; env: Record<string, string | undefined> }> =>
|
|
363
|
+
existsSync(serveLog) ? readFileSync(serveLog, "utf8").trim().split("\n").map((line) => JSON.parse(line)) : [];
|
|
364
|
+
// The detached servers outlive their launcher by design: stop them with the fixture.
|
|
365
|
+
t.after(() => {
|
|
366
|
+
for (const { pid } of serves()) try { process.kill(pid); } catch { /* Already exited. */ }
|
|
367
|
+
rmSync(base, { recursive: true, force: true });
|
|
368
|
+
});
|
|
279
369
|
const home = join(base, "home");
|
|
280
370
|
const root = join(home, ".gentle-shell");
|
|
281
371
|
const version = join(root, "versions", FIXTURE_ID);
|
|
@@ -338,33 +428,38 @@ function bundledPrefix(t: TestContext, { pi = "1.2.0", core = true } = {}) {
|
|
|
338
428
|
mkdirSync(userBin);
|
|
339
429
|
writeFileSync(join(userBin, "pi"), "#!/bin/sh\necho 9.9.9\n", { mode: 0o755 });
|
|
340
430
|
// No test contacts the actual default Engram endpoint: connections to 7437
|
|
341
|
-
// are redirected to a
|
|
342
|
-
// server
|
|
431
|
+
// are redirected to a local fixture server (FIXTURE_ENGRAM_PORT), to the
|
|
432
|
+
// server the bundled core's `serve` published (FIXTURE_ENGRAM_PORT_FILE), or
|
|
433
|
+
// else to a closed ephemeral port (refused). FIXTURE_NO_PROBE makes any probe fail the test.
|
|
343
434
|
const network = join(base, "network.mjs");
|
|
344
|
-
|
|
435
|
+
const portFile = join(base, "engram-port");
|
|
436
|
+
writeFileSync(network, `import net from 'node:net'; import { readFileSync } from 'node:fs'; import { syncBuiltinESMExports } from 'node:module';
|
|
345
437
|
const original = net.createConnection;
|
|
346
438
|
const probe = net.createServer(); await new Promise((resolve) => probe.listen(0, '127.0.0.1', resolve));
|
|
347
439
|
const closed = probe.address().port; await new Promise((resolve) => probe.close(resolve));
|
|
348
440
|
net.createConnection = (options, ...rest) => {
|
|
349
441
|
if (Number(options?.port ?? options) !== 7437) return original(options, ...rest);
|
|
350
442
|
if (process.env.FIXTURE_NO_PROBE === '1') { process.stderr.write('unexpected Engram probe\\n'); process.exit(97); }
|
|
351
|
-
|
|
443
|
+
const served = () => { try { return readFileSync(process.env.FIXTURE_ENGRAM_PORT_FILE, 'utf8'); } catch { return ''; } };
|
|
444
|
+
return original({ ...options, port: Number(process.env.FIXTURE_ENGRAM_PORT || served() || closed) }, ...rest);
|
|
352
445
|
};
|
|
353
446
|
syncBuiltinESMExports();`);
|
|
354
447
|
const metadata = join(home, ".gentle-shell", "agent", "npm", "node_modules", "gentle-engram", "package.json");
|
|
355
448
|
mkdirSync(dirname(metadata), { recursive: true });
|
|
356
449
|
writeFileSync(metadata, JSON.stringify({ name: "gentle-engram", version: "0.4.2", engram: { startupPolicy: 1 } }));
|
|
357
450
|
const env = { HOME: home, PATH: `${userBin}:/usr/bin:/bin`, GENTLE_SHELL_PI: join(userBin, "pi"), GENTLE_SHELL_NO_AUTO_SETUP: "1",
|
|
358
|
-
NODE_OPTIONS: `--no-warnings --import=${network} --import=${registerHooks}`, npm_config_registry: "https://npm.example"
|
|
451
|
+
NODE_OPTIONS: `--no-warnings --import=${network} --import=${registerHooks}`, npm_config_registry: "https://npm.example",
|
|
452
|
+
FIXTURE_SERVE_LOG: serveLog, FIXTURE_ENGRAM_PORT_FILE: portFile };
|
|
359
453
|
const entry = join(root, "current", "node_modules", "gentle-pi", "bin", "gentle-shell.mjs");
|
|
360
454
|
const run = (args: string[], extra: Record<string, string> = {}) => spawnSync(process.execPath, [entry, ...args], { env: { ...env, ...extra }, encoding: "utf8", input: "" });
|
|
361
|
-
return { root, home, userBin, version, piDir, npmCli, engram, metadata, network, guard, run, env, entry: join(shell, "bin", "gentle-shell.mjs") };
|
|
455
|
+
return { root, home, userBin, version, piDir, npmCli, engram, metadata, network, guard, run, env, serves, portFile, entry: join(shell, "bin", "gentle-shell.mjs") };
|
|
362
456
|
}
|
|
363
457
|
|
|
364
458
|
const STORE_ID = "0123456789abcdef0123456789abcdef";
|
|
365
459
|
const OTHER_STORE_ID = "fedcba9876543210fedcba9876543210";
|
|
460
|
+
const DEFAULT_URL = "http://127.0.0.1:7437";
|
|
366
461
|
const lastLaunch = (result: { stdout: string }) => JSON.parse(result.stdout.trim().split("\n").at(-1)!);
|
|
367
|
-
const memoryOf = (result: { stdout: string }) => [lastLaunch(result).env.ENGRAM_STARTUP_POLICY, lastLaunch(result).env.ENGRAM_BIN];
|
|
462
|
+
const memoryOf = (result: { stdout: string }) => [lastLaunch(result).env.ENGRAM_STARTUP_POLICY, lastLaunch(result).env.ENGRAM_BIN, lastLaunch(result).env.ENGRAM_URL];
|
|
368
463
|
const provenancePath = (prefix: { root: string }) => join(prefix.root, "config", "engram-provenance.json");
|
|
369
464
|
const sha256File = (path: string) => createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
370
465
|
function writeHeader(path: string, userVersion: number, magic = "SQLite format 3\0") {
|
|
@@ -389,78 +484,213 @@ async function engramServer(t: TestContext, routes: Record<string, [number, unkn
|
|
|
389
484
|
return new Promise((resolve) => child.stdout.once("data", (chunk) => resolve(String(chunk).trim())));
|
|
390
485
|
}
|
|
391
486
|
|
|
392
|
-
|
|
487
|
+
// Whether the server a launch started still answers after the launcher exited.
|
|
488
|
+
async function stillServing(prefix: ReturnType<typeof bundledPrefix>): Promise<boolean> {
|
|
489
|
+
const response = await fetch(`http://127.0.0.1:${readFileSync(prefix.portFile, "utf8")}/compatibility`);
|
|
490
|
+
return response.ok && (await response.json()).status === "ready";
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
test("with nothing listening Shell starts the bundled core's shared server detached, with a clean environment, and Pi reuses it", { skip: posixHost }, async (t) => {
|
|
393
494
|
const prefix = bundledPrefix(t);
|
|
394
|
-
const
|
|
395
|
-
const result = prefix.run([], { FIXTURE_PROVISION_ENV: provisioning, GH_TOKEN: "fixture-token", GITHUB_TOKEN: "fixture-token" });
|
|
495
|
+
const result = prefix.run([], { GH_TOKEN: "fixture-token", GITHUB_TOKEN: "fixture-token", ENGRAM_HTTP_TOKEN: "fixture-http" });
|
|
396
496
|
assert.equal(result.status, 0, result.stderr);
|
|
397
|
-
|
|
398
|
-
assert.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
assert.deepEqual(
|
|
403
|
-
//
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
assert.deepEqual([
|
|
497
|
+
assert.deepEqual(memoryOf(result), ["allow", prefix.guard, DEFAULT_URL]);
|
|
498
|
+
assert.doesNotMatch(result.stderr, /memory disabled/);
|
|
499
|
+
const [serve, ...others] = prefix.serves();
|
|
500
|
+
assert.deepEqual(others, []);
|
|
501
|
+
// Engram's own defaults: no port, data directory or URL chosen for it.
|
|
502
|
+
assert.deepEqual([serve.args, serve.cwd, serve.env.HOME], [["serve"], prefix.home, prefix.home]);
|
|
503
|
+
// S8: no update check, no GitHub tokens and no inherited Engram settings; Pi keeps the user's environment.
|
|
504
|
+
assert.deepEqual([serve.env.ENGRAM_NO_UPDATE_CHECK, serve.env.GH_TOKEN, serve.env.GITHUB_TOKEN, serve.env.ENGRAM_HTTP_TOKEN, serve.env.ENGRAM_URL, serve.env.ENGRAM_BIN],
|
|
505
|
+
["1", undefined, undefined, undefined, undefined, undefined]);
|
|
506
|
+
assert.deepEqual([lastLaunch(result).env.GH_TOKEN, lastLaunch(result).env.ENGRAM_NO_UPDATE_CHECK], ["fixture-token", undefined]);
|
|
507
|
+
// Detached: it outlives the launcher and Pi, and the next launch reuses it without starting another.
|
|
508
|
+
assert.equal(await stillServing(prefix), true);
|
|
509
|
+
const second = prefix.run([]);
|
|
510
|
+
assert.equal(second.status, 0, second.stderr);
|
|
511
|
+
assert.deepEqual(memoryOf(second), ["allow", prefix.guard, DEFAULT_URL]);
|
|
512
|
+
assert.equal(prefix.serves().length, 1);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("a server that fails to start or never reports ready disables memory for that launch and Shell still starts Pi", { skip: posixHost }, (t) => {
|
|
516
|
+
const prefix = bundledPrefix(t);
|
|
517
|
+
const result = prefix.run([], { FIXTURE_SERVE: "exit" });
|
|
518
|
+
assert.equal(result.status, 0, result.stderr);
|
|
519
|
+
assert.deepEqual(memoryOf(result), ["disabled", prefix.guard, undefined]);
|
|
520
|
+
assert.match(result.stderr, /memory disabled.*could not start the shared Engram server/s);
|
|
521
|
+
assert.equal(lastLaunch(result).cli.endsWith(join("dist", "bundle", "cli.js")), true);
|
|
522
|
+
assert.equal(prefix.serves().length, 1);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("concurrent starts converge: a failed bind followed by a ready /compatibility counts as started", { skip: posixHost }, (t) => {
|
|
526
|
+
const prefix = bundledPrefix(t);
|
|
527
|
+
const result = prefix.run([], { FIXTURE_SERVE: "race" });
|
|
528
|
+
assert.equal(result.status, 0, result.stderr);
|
|
529
|
+
assert.deepEqual(memoryOf(result), ["allow", prefix.guard, DEFAULT_URL]);
|
|
530
|
+
assert.doesNotMatch(result.stderr, /memory disabled/);
|
|
531
|
+
assert.deepEqual(prefix.serves().map(({ mode }) => mode), ["race", "late"]);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("bundled memory never requires or selects a session-owned server, with the 0.4.2 companion and an explicit mode kept", { skip: posixHost }, (t) => {
|
|
535
|
+
const prefix = bundledPrefix(t);
|
|
536
|
+
const result = prefix.run([]);
|
|
537
|
+
assert.equal(result.status, 0, result.stderr);
|
|
538
|
+
assert.deepEqual(memoryOf(result), ["allow", prefix.guard, DEFAULT_URL]);
|
|
539
|
+
assert.equal(lastLaunch(result).env.ENGRAM_SERVER_MODE, undefined);
|
|
540
|
+
assert.doesNotMatch(result.stderr, /session-owned/);
|
|
541
|
+
assert.deepEqual(JSON.parse(readFileSync(prefix.metadata, "utf8")).engram, { startupPolicy: 1 });
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("an explicit Engram server mode is a selection: kept, never probed or started, memory disabled", { skip: posixHost }, (t) => {
|
|
545
|
+
for (const mode of ["session-owned", "bogus"]) {
|
|
546
|
+
const prefix = bundledPrefix(t);
|
|
547
|
+
const result = prefix.run([], { ENGRAM_SERVER_MODE: mode, FIXTURE_NO_PROBE: "1" });
|
|
548
|
+
assert.equal(result.status, 0, result.stderr);
|
|
549
|
+
assert.deepEqual(memoryOf(result), ["disabled", prefix.guard, undefined], mode);
|
|
550
|
+
assert.equal(lastLaunch(result).env.ENGRAM_SERVER_MODE, mode);
|
|
551
|
+
assert.deepEqual(prefix.serves(), [], mode);
|
|
552
|
+
}
|
|
407
553
|
});
|
|
408
554
|
|
|
409
|
-
test("a
|
|
555
|
+
test("a mixed local and npm companion selection is preserved and memory still uses the shared server", { skip: posixHost }, (t) => {
|
|
556
|
+
const prefix = bundledPrefix(t);
|
|
557
|
+
const local = join(prefix.home, "legacy-companion");
|
|
558
|
+
mkdirSync(local);
|
|
559
|
+
writeFileSync(join(local, "package.json"), JSON.stringify({ name: "gentle-engram", engram: { startupPolicy: 1 } }));
|
|
560
|
+
const settings = join(prefix.home, ".gentle-shell", "agent", "settings.json");
|
|
561
|
+
const selected = JSON.stringify({ packages: [local, "npm:gentle-engram@0.4.2"] });
|
|
562
|
+
writeFileSync(settings, selected);
|
|
563
|
+
const result = prefix.run([]);
|
|
564
|
+
assert.equal(result.status, 0, result.stderr);
|
|
565
|
+
assert.deepEqual(memoryOf(result), ["allow", prefix.guard, DEFAULT_URL]);
|
|
566
|
+
assert.deepEqual(JSON.parse(readFileSync(settings, "utf8")).packages, JSON.parse(selected).packages);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test("an existing store, including one the global CLI created, starts the shared server and Shell never touches it", { skip: posixHost }, (t) => {
|
|
410
570
|
const prefix = bundledPrefix(t);
|
|
411
|
-
assert.equal(prefix.run([]).status, 0);
|
|
412
571
|
const data = join(prefix.home, ".engram");
|
|
413
|
-
assert.deepEqual(readdirSync(data).sort(), [".instance-id"]);
|
|
414
|
-
assert.deepEqual(memoryOf(prefix.run([])), ["allow", prefix.engram]);
|
|
415
|
-
// Another Engram client replaces the shared directory with its own store.
|
|
416
|
-
rmSync(data, { recursive: true });
|
|
417
572
|
mkdirSync(data);
|
|
418
573
|
writeFileSync(join(data, ".instance-id"), `${OTHER_STORE_ID}\n`);
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
574
|
+
for (const userVersion of [0, 1]) {
|
|
575
|
+
writeHeader(join(data, "engram.db"), userVersion);
|
|
576
|
+
const before = readFileSync(join(data, "engram.db"));
|
|
577
|
+
const result = prefix.run([]);
|
|
578
|
+
assert.equal(result.status, 0, result.stderr);
|
|
579
|
+
assert.deepEqual(memoryOf(result), ["allow", prefix.guard, DEFAULT_URL], String(userVersion));
|
|
580
|
+
assert.deepEqual(readFileSync(join(data, "engram.db")), before);
|
|
581
|
+
}
|
|
582
|
+
assert.deepEqual(readdirSync(data).sort(), [".instance-id", "engram.db"]);
|
|
583
|
+
assert.equal(existsSync(provenancePath(prefix)), false);
|
|
423
584
|
});
|
|
424
|
-
|
|
585
|
+
|
|
586
|
+
test("a store a newer Engram migrated keeps the server stopped and memory disabled", { skip: posixHost }, (t) => {
|
|
425
587
|
const prefix = bundledPrefix(t);
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
588
|
+
const data = join(prefix.home, ".engram");
|
|
589
|
+
mkdirSync(data);
|
|
590
|
+
writeHeader(join(data, "engram.db"), 2);
|
|
591
|
+
const result = prefix.run([]);
|
|
592
|
+
assert.equal(result.status, 0, result.stderr);
|
|
593
|
+
assert.deepEqual(memoryOf(result), ["disabled", prefix.guard, undefined]);
|
|
594
|
+
assert.match(result.stderr, /memory disabled.*newer Engram/s);
|
|
595
|
+
assert.deepEqual(prefix.serves(), []);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
function migrationFixture(t: TestContext, outcome = "success") {
|
|
599
|
+
const prefix = bundledPrefix(t);
|
|
600
|
+
// A store an earlier Shell provisioned and recorded with the bundled core.
|
|
601
|
+
const source = join(prefix.home, ".engram");
|
|
602
|
+
mkdirSync(source);
|
|
603
|
+
writeFileSync(join(source, ".instance-id"), `${STORE_ID}\n`);
|
|
604
|
+
writeHeader(join(source, "engram.db"), 1);
|
|
605
|
+
writeFileSync(join(source, "config.json"), '{"preserved":true}\n');
|
|
606
|
+
mkdirSync(dirname(provenancePath(prefix)), { recursive: true });
|
|
607
|
+
writeFileSync(provenancePath(prefix), `${JSON.stringify({ schema: 1, dataDir: source, instanceId: STORE_ID, coreSha256: sha256File(prefix.engram) }, null, 2)}\n`);
|
|
608
|
+
const originalRecord = readFileSync(provenancePath(prefix), "utf8");
|
|
609
|
+
const sourceBytes = readFileSync(join(source, "engram.db"));
|
|
610
|
+
const invocation = join(prefix.home, "migration-invoked");
|
|
611
|
+
writeFileSync(prefix.engram, `#!${process.execPath}\nimport {cpSync,writeFileSync} from 'node:fs';\nconst args=process.argv.slice(2);\nif(args[0]==='--help'){console.log(${JSON.stringify(outcome === "unsupported" ? "Commands: compatibility" : " migrate-copy --data-dir <absolute-source> --output-dir <new-absolute-destination> --confirm-quiescent --json")});process.exit(0);}\nwriteFileSync(${JSON.stringify(invocation)},JSON.stringify({args,token:process.env.GH_TOKEN,cloud:process.env.ENGRAM_CLOUD_TOKEN}));\nif(args[0]!=='migrate-copy') process.exit(2);\nconst source=args[args.indexOf('--data-dir')+1],output=args[args.indexOf('--output-dir')+1];\nif(${JSON.stringify(outcome)}==='failure'){process.stderr.write('migration rejected\\n');process.exit(1);}\ncpSync(source,output,{recursive:true});\nconsole.log(JSON.stringify({source_dir:source,output_dir:output,instance_id:${JSON.stringify(outcome === "wrong-identity" ? OTHER_STORE_ID : STORE_ID)},migration_completed:true}));\n`, { mode: 0o700 });
|
|
612
|
+
return { ...prefix, source, originalRecord, sourceBytes, invocation };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
test("an explicit Engram socket remains untouched and is never probed", { skip: posixHost }, (t) => {
|
|
616
|
+
const prefix = bundledPrefix(t);
|
|
617
|
+
const result = prefix.run([], { ENGRAM_SOCKET: "/fixture/selected.sock", FIXTURE_NO_PROBE: "1" });
|
|
618
|
+
assert.equal(result.status, 0, result.stderr);
|
|
619
|
+
assert.deepEqual(memoryOf(result), ["disabled", prefix.guard, undefined]);
|
|
620
|
+
assert.equal(lastLaunch(result).env.ENGRAM_SOCKET, "/fixture/selected.sock");
|
|
621
|
+
assert.deepEqual(prefix.serves(), []);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test("memory migrate explicitly promotes a verified copy and retains the original store", { skip: posixHost }, (t) => {
|
|
625
|
+
const prefix = migrationFixture(t);
|
|
626
|
+
const result = prefix.run(["memory", "migrate", "--confirm-quiescent"], { FIXTURE_NO_PROBE: "1", GH_TOKEN: "fixture-secret", ENGRAM_CLOUD_TOKEN: "fixture-cloud" });
|
|
627
|
+
assert.equal(result.status, 0, result.stderr);
|
|
628
|
+
assert.equal(result.stderr, "");
|
|
629
|
+
const receipt = JSON.parse(result.stdout);
|
|
630
|
+
assert.deepEqual(Object.keys(receipt).sort(), ["backup_dir", "instance_id", "status"]);
|
|
631
|
+
assert.equal(receipt.status, "migrated");
|
|
632
|
+
assert.equal(receipt.instance_id, STORE_ID);
|
|
633
|
+
assert.deepEqual(readFileSync(join(receipt.backup_dir, "engram.db")), prefix.sourceBytes);
|
|
634
|
+
assert.equal(readFileSync(join(receipt.backup_dir, "config.json"), "utf8"), '{"preserved":true}\n');
|
|
635
|
+
assert.equal(readFileSync(join(prefix.source, "config.json"), "utf8"), '{"preserved":true}\n');
|
|
636
|
+
assert.deepEqual(JSON.parse(readFileSync(provenancePath(prefix), "utf8")), { schema: 1, dataDir: prefix.source, instanceId: STORE_ID, coreSha256: sha256File(prefix.engram) });
|
|
637
|
+
const invoked = JSON.parse(readFileSync(prefix.invocation, "utf8"));
|
|
638
|
+
assert.equal(invoked.args[0], "migrate-copy");
|
|
639
|
+
assert.equal(invoked.token, undefined);
|
|
640
|
+
assert.equal(invoked.cloud, undefined);
|
|
641
|
+
assert.equal(existsSync(join(prefix.root, "config", "engram-migration.json")), false);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test("memory migrate rejects missing confirmation and malformed flags without invoking Engram", { skip: posixHost }, (t) => {
|
|
645
|
+
const prefix = migrationFixture(t);
|
|
646
|
+
for (const args of [["memory", "migrate"], ["memory", "migrate", "--confirm-quiescent", "--unknown"], ["memory", "migrate", "--confirm-quiescent", "--confirm-quiescent"]]) {
|
|
647
|
+
const result = prefix.run(args, { FIXTURE_NO_PROBE: "1" });
|
|
648
|
+
assert.equal(result.status, 2, result.stderr);
|
|
649
|
+
assert.equal(result.stdout, "");
|
|
650
|
+
assert.equal(result.stderr, "Usage: gentle-shell memory migrate --confirm-quiescent\nStop every Engram client before running this command.\n");
|
|
651
|
+
assert.equal(existsSync(prefix.invocation), false);
|
|
652
|
+
assert.equal(readFileSync(provenancePath(prefix), "utf8"), prefix.originalRecord);
|
|
653
|
+
assert.deepEqual(readFileSync(join(prefix.source, "engram.db")), prefix.sourceBytes);
|
|
434
654
|
}
|
|
435
|
-
assert.equal(readFileSync(provenancePath(prefix), "utf8"), recorded);
|
|
436
655
|
});
|
|
437
656
|
|
|
438
|
-
test("
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
assert.equal(
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
assert.
|
|
458
|
-
|
|
459
|
-
assert.deepEqual([seen.env.ENGRAM_STARTUP_POLICY, seen.env.ENGRAM_BIN], ["disabled", prefix.guard], name);
|
|
460
|
-
assert.match(result.stderr, /memory disabled.*could not verify.*compatibility.*Back up/s, name);
|
|
657
|
+
test("memory migrate never sends an unknown command to an older bundled core", { skip: posixHost }, (t) => {
|
|
658
|
+
const prefix = migrationFixture(t, "unsupported");
|
|
659
|
+
const result = prefix.run(["memory", "migrate", "--confirm-quiescent"], { FIXTURE_NO_PROBE: "1" });
|
|
660
|
+
assert.equal(result.status, 1, result.stderr);
|
|
661
|
+
assert.equal(result.stdout, "");
|
|
662
|
+
assert.match(result.stderr, /does not support migrate-copy/);
|
|
663
|
+
assert.equal(existsSync(prefix.invocation), false);
|
|
664
|
+
assert.equal(readFileSync(provenancePath(prefix), "utf8"), prefix.originalRecord);
|
|
665
|
+
assert.deepEqual(readFileSync(join(prefix.source, "engram.db")), prefix.sourceBytes);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
test("memory migrate refuses provider failure and mismatched evidence without changing provenance or source", { skip: posixHost }, (t) => {
|
|
669
|
+
for (const outcome of ["failure", "wrong-identity"]) {
|
|
670
|
+
const prefix = migrationFixture(t, outcome);
|
|
671
|
+
const result = prefix.run(["memory", "migrate", "--confirm-quiescent"], { FIXTURE_NO_PROBE: "1" });
|
|
672
|
+
assert.equal(result.status, 1, result.stderr);
|
|
673
|
+
assert.equal(result.stdout, "");
|
|
674
|
+
assert.match(result.stderr, /^gentle-shell: memory migration failed:/);
|
|
675
|
+
assert.equal(readFileSync(provenancePath(prefix), "utf8"), prefix.originalRecord);
|
|
676
|
+
assert.deepEqual(readFileSync(join(prefix.source, "engram.db")), prefix.sourceBytes);
|
|
677
|
+
assert.equal(existsSync(join(prefix.root, "config", "engram-migration.json")), false);
|
|
461
678
|
}
|
|
462
679
|
});
|
|
463
680
|
|
|
681
|
+
test("an interrupted memory promotion keeps the server stopped until recovery", { skip: posixHost }, (t) => {
|
|
682
|
+
const prefix = bundledPrefix(t);
|
|
683
|
+
mkdirSync(join(prefix.root, "config"), { recursive: true });
|
|
684
|
+
const marker = join(prefix.root, "config", "engram-migration.json");
|
|
685
|
+
writeFileSync(marker, '{"interrupted":true}\n');
|
|
686
|
+
const result = prefix.run([], { FIXTURE_NO_PROBE: "1" });
|
|
687
|
+
assert.equal(result.status, 0, result.stderr);
|
|
688
|
+
assert.deepEqual(memoryOf(result), ["disabled", prefix.guard, undefined]);
|
|
689
|
+
assert.match(result.stderr, /incomplete memory migration/);
|
|
690
|
+
assert.deepEqual(prefix.serves(), []);
|
|
691
|
+
assert.equal(readFileSync(marker, "utf8"), '{"interrupted":true}\n');
|
|
692
|
+
});
|
|
693
|
+
|
|
464
694
|
test("public help explains compatibility evidence without promising update recovery", { skip: posixHost }, (t) => {
|
|
465
695
|
const prefix = bundledPrefix(t);
|
|
466
696
|
const result = prefix.run(["--help"]);
|
|
@@ -468,40 +698,21 @@ test("public help explains compatibility evidence without promising update recov
|
|
|
468
698
|
assert.equal(result.stderr, "");
|
|
469
699
|
assert.match(result.stdout, /Matching versions or updating alone do not prove compatibility/);
|
|
470
700
|
assert.match(result.stdout, /\/compatibility/);
|
|
471
|
-
assert.
|
|
701
|
+
assert.match(result.stdout, /shares the global Engram server.*nothing listens.*engram serve/s);
|
|
702
|
+
assert.doesNotMatch(result.stdout, /update Engram, restart|Shell-provisioned|session-owned/);
|
|
472
703
|
});
|
|
473
704
|
|
|
474
|
-
test("a
|
|
705
|
+
test("a listener without the ready contract is never replaced, even reporting the bundled core version", { skip: posixHost }, async (t) => {
|
|
475
706
|
const prefix = bundledPrefix(t);
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
writeFileSync(join(data, ".instance-id"), `${STORE_ID}\n`);
|
|
479
|
-
writeHeader(join(data, "engram.db"), 1);
|
|
480
|
-
const result = prefix.run([], { FIXTURE_CREATE_STORE: STORE_ID });
|
|
707
|
+
const port = await engramServer(t, { "/health": [200, { status: "ok", instance_id: STORE_ID, version: engramPinVersion }] });
|
|
708
|
+
const result = prefix.run([], { FIXTURE_ENGRAM_PORT: port });
|
|
481
709
|
assert.equal(result.status, 0, result.stderr);
|
|
482
|
-
assert.deepEqual(
|
|
483
|
-
assert.
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
test("a listening server is reused for Shell's store only with the recorded /health instance and the exact core version", { skip: posixHost }, async (t) => {
|
|
487
|
-
const cases: Array<[string, unknown, string]> = [
|
|
488
|
-
["match", { status: "ok", instance_id: STORE_ID, version: engramPinVersion }, "allow"],
|
|
489
|
-
["other instance", { status: "ok", instance_id: OTHER_STORE_ID, version: engramPinVersion }, "disabled"],
|
|
490
|
-
["other version", { status: "ok", instance_id: STORE_ID, version: "9.9.9" }, "disabled"],
|
|
491
|
-
];
|
|
492
|
-
for (const [name, health, policy] of cases) {
|
|
493
|
-
const prefix = bundledPrefix(t);
|
|
494
|
-
assert.equal(prefix.run([], { FIXTURE_CREATE_STORE: STORE_ID }).status, 0, name);
|
|
495
|
-
const port = await engramServer(t, { "/health": [200, health] });
|
|
496
|
-
const result = prefix.run([], { FIXTURE_ENGRAM_PORT: port });
|
|
497
|
-
assert.equal(result.status, 0, `${name}: ${result.stderr}`);
|
|
498
|
-
const seen = lastLaunch(result);
|
|
499
|
-
assert.deepEqual([seen.env.ENGRAM_STARTUP_POLICY, seen.env.ENGRAM_BIN, seen.env.ENGRAM_URL],
|
|
500
|
-
[policy, policy === "allow" ? prefix.engram : prefix.guard, undefined], name);
|
|
501
|
-
}
|
|
710
|
+
assert.deepEqual(memoryOf(result), ["disabled", prefix.guard, undefined]);
|
|
711
|
+
assert.match(result.stderr, /memory disabled.*could not verify/s);
|
|
712
|
+
assert.deepEqual(prefix.serves(), []);
|
|
502
713
|
});
|
|
503
714
|
|
|
504
|
-
test("a verified /compatibility server is reused through an explicit URL and the guard
|
|
715
|
+
test("a verified /compatibility server is reused unchanged through an explicit URL and the guard", { skip: posixHost }, async (t) => {
|
|
505
716
|
const ready = { revision: 1, source: "active", status: "ready", offline_automatic_opening_authorized: false, active_reuse_authorized: true,
|
|
506
717
|
instance_id: STORE_ID, runtime_id: "a".repeat(32), capability: "startup-verified-store-v1" };
|
|
507
718
|
const prefix = bundledPrefix(t);
|
|
@@ -513,11 +724,12 @@ test("a verified /compatibility server is reused through an explicit URL and the
|
|
|
513
724
|
assert.equal(result.status, 0, result.stderr);
|
|
514
725
|
const seen = lastLaunch(result);
|
|
515
726
|
assert.deepEqual([seen.env.ENGRAM_STARTUP_POLICY, seen.env.ENGRAM_URL, seen.env.ENGRAM_BIN], ["allow", "http://127.0.0.1:7437", prefix.guard]);
|
|
516
|
-
assert.
|
|
727
|
+
assert.deepEqual(prefix.serves(), []);
|
|
517
728
|
const unready = bundledPrefix(t);
|
|
518
729
|
mkdirSync(join(unready.home, ".engram"));
|
|
519
730
|
const notReady = await engramServer(t, { "/compatibility": [200, { ...ready, status: "unknown", active_reuse_authorized: false }] });
|
|
520
731
|
assert.equal(lastLaunch(unready.run([], { FIXTURE_ENGRAM_PORT: notReady })).env.ENGRAM_STARTUP_POLICY, "disabled");
|
|
732
|
+
assert.deepEqual(unready.serves(), []);
|
|
521
733
|
});
|
|
522
734
|
|
|
523
735
|
test("a foreign store behind a 3.3.2 server stays guarded and the launch names the server version to update", { skip: posixHost }, async (t) => {
|
|
@@ -534,34 +746,10 @@ test("a foreign store behind a 3.3.2 server stays guarded and the launch names t
|
|
|
534
746
|
assert.deepEqual([seen.env.ENGRAM_STARTUP_POLICY, seen.env.ENGRAM_BIN, seen.env.ENGRAM_URL], ["disabled", prefix.guard, undefined]);
|
|
535
747
|
assert.match(result.stderr, /memory disabled.*reports version 3\.3\.2.*update that server to 3\.4\.0 or later/s);
|
|
536
748
|
assert.deepEqual(readFileSync(join(data, "engram.db")), before);
|
|
537
|
-
assert.
|
|
749
|
+
assert.deepEqual(prefix.serves(), []);
|
|
538
750
|
});
|
|
539
751
|
|
|
540
|
-
test("
|
|
541
|
-
const prefix = bundledPrefix(t);
|
|
542
|
-
const first = prefix.run([], { FIXTURE_CREATE_STORE: STORE_ID, FIXTURE_KILL_LAUNCHER: "1" });
|
|
543
|
-
assert.equal(first.signal, "SIGKILL");
|
|
544
|
-
assert.equal(JSON.parse(readFileSync(provenancePath(prefix), "utf8")).instanceId, STORE_ID);
|
|
545
|
-
assert.deepEqual(memoryOf(prefix.run([])), ["allow", prefix.engram]);
|
|
546
|
-
});
|
|
547
|
-
test("a failed identity provisioning disables memory for that launch and records nothing", { skip: posixHost }, (t) => {
|
|
548
|
-
const cases: Array<[string, Record<string, string>, (prefix: ReturnType<typeof bundledPrefix>) => void]> = [
|
|
549
|
-
["instance-id failure", { FIXTURE_INSTANCE_ID_FAIL: "1" }, () => {}],
|
|
550
|
-
["invalid identity file", { FIXTURE_INSTANCE_ID: "not-an-id" }, () => {}],
|
|
551
|
-
["provenance write failure", {}, (prefix) => mkdirSync(provenancePath(prefix), { recursive: true })],
|
|
552
|
-
];
|
|
553
|
-
for (const [name, extra, prepare] of cases) {
|
|
554
|
-
const prefix = bundledPrefix(t);
|
|
555
|
-
prepare(prefix);
|
|
556
|
-
const result = prefix.run([], extra);
|
|
557
|
-
assert.equal(result.status, 0, `${name}: ${result.stderr}`);
|
|
558
|
-
assert.deepEqual(memoryOf(result), ["disabled", prefix.guard], name);
|
|
559
|
-
assert.match(result.stderr, /could not provision/, name);
|
|
560
|
-
assert.equal(lastLaunch(result).provenance, null, name);
|
|
561
|
-
}
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
test("--version and forwarded Pi version or help flags never probe Engram, install the companion or print memory notices", { skip: posixHost }, (t) => {
|
|
752
|
+
test("--version and forwarded Pi version or help flags never probe or start Engram, install the companion or print memory notices", { skip: posixHost }, (t) => {
|
|
565
753
|
const prefix = bundledPrefix(t);
|
|
566
754
|
writeFileSync(prefix.metadata, '{"name":"gentle-engram","version":"0.4.1"}');
|
|
567
755
|
mkdirSync(join(prefix.home, ".engram"));
|
|
@@ -575,7 +763,7 @@ test("--version and forwarded Pi version or help flags never probe Engram, insta
|
|
|
575
763
|
assert.equal(readFileSync(settings, "utf8"), before, args.join(" "));
|
|
576
764
|
assert.equal(JSON.parse(readFileSync(prefix.metadata, "utf8")).version, "0.4.1", args.join(" "));
|
|
577
765
|
}
|
|
578
|
-
assert.equal(
|
|
766
|
+
assert.equal(prefix.serves().length, 1);
|
|
579
767
|
});
|
|
580
768
|
|
|
581
769
|
test("-v or --help as a prompt or an option value never switches memory off", { skip: posixHost }, (t) => {
|
|
@@ -583,8 +771,8 @@ test("-v or --help as a prompt or an option value never switches memory off", {
|
|
|
583
771
|
const prefix = bundledPrefix(t);
|
|
584
772
|
const result = prefix.run(args);
|
|
585
773
|
assert.equal(result.status, 0, `${args.join(" ")}: ${result.stderr}`);
|
|
586
|
-
assert.deepEqual(memoryOf(result), ["allow", prefix.
|
|
587
|
-
assert.equal(
|
|
774
|
+
assert.deepEqual(memoryOf(result), ["allow", prefix.guard, DEFAULT_URL], args.join(" "));
|
|
775
|
+
assert.equal(prefix.serves().length, 1, args.join(" "));
|
|
588
776
|
}
|
|
589
777
|
});
|
|
590
778
|
test("a missing bundled core uses the guard, never an engram on PATH", { skip: posixHost }, (t) => {
|
|
@@ -609,20 +797,23 @@ function countingGentleAi(prefix: ReturnType<typeof bundledPrefix>) {
|
|
|
609
797
|
return { bin, runs: () => (existsSync(log) ? readFileSync(log, "utf8").split("\n").filter(Boolean).length : 0) };
|
|
610
798
|
}
|
|
611
799
|
|
|
612
|
-
test("setup on a fresh machine publishes the bundled core; the next launch
|
|
800
|
+
test("setup on a fresh machine publishes the bundled core; the next launch starts the shared server and later launches reuse it", { skip: posixHost }, (t) => {
|
|
613
801
|
const prefix = bundledPrefix(t, { core: false });
|
|
614
802
|
const installer = countingGentleAi(prefix);
|
|
615
803
|
const setup = prefix.run(["setup"], { GENTLE_SHELL_GENTLE_AI_BIN: installer.bin });
|
|
616
804
|
assert.equal(setup.status, 0, setup.stderr);
|
|
617
805
|
assert.equal(installer.runs(), 1);
|
|
618
806
|
assert.equal(existsSync(prefix.engram), true);
|
|
619
|
-
|
|
807
|
+
// Setup itself never starts a server.
|
|
808
|
+
assert.deepEqual(prefix.serves(), []);
|
|
809
|
+
const first = prefix.run([]);
|
|
620
810
|
assert.equal(first.status, 0, first.stderr);
|
|
621
|
-
assert.deepEqual(
|
|
622
|
-
assert.equal(JSON.parse(readFileSync(provenancePath(prefix), "utf8")).instanceId, STORE_ID);
|
|
811
|
+
assert.deepEqual(memoryOf(first), ["allow", prefix.guard, DEFAULT_URL]);
|
|
623
812
|
const second = prefix.run([]);
|
|
624
813
|
assert.equal(second.status, 0, second.stderr);
|
|
625
|
-
assert.deepEqual(
|
|
814
|
+
assert.deepEqual(memoryOf(second), ["allow", prefix.guard, DEFAULT_URL]);
|
|
815
|
+
assert.equal(prefix.serves().length, 1);
|
|
816
|
+
assert.equal(existsSync(provenancePath(prefix)), false);
|
|
626
817
|
});
|
|
627
818
|
|
|
628
819
|
test("an explicit ENGRAM_URL alone keeps the verified core, or the guard without it, as ENGRAM_BIN", { skip: posixHost }, (t) => {
|
|
@@ -646,12 +837,13 @@ test("a failed companion check after setup is a warning, not a setup failure", {
|
|
|
646
837
|
assert.equal(setup.status, 0, setup.stderr);
|
|
647
838
|
assert.equal(installer.runs(), 1);
|
|
648
839
|
assert.match(setup.stderr, /startupPolicy/);
|
|
649
|
-
|
|
840
|
+
// The guard, not the companion check, protects memory: the shared server is still reused.
|
|
841
|
+
assert.deepEqual(memoryOf(prefix.run([], { FIXTURE_INSTALL_CAPABILITY: "0" })), ["allow", prefix.guard, DEFAULT_URL]);
|
|
650
842
|
assert.deepEqual(readdirSync(data), ["engram.db"]);
|
|
651
843
|
assert.equal(readFileSync(join(data, "engram.db"), "utf8"), "untouched database sentinel");
|
|
652
844
|
});
|
|
653
845
|
|
|
654
|
-
test("setup with
|
|
846
|
+
test("setup with an existing store runs the installer like the baseline and the launch reuses the shared server behind the guard", { skip: posixHost }, (t) => {
|
|
655
847
|
const prefix = bundledPrefix(t, { core: false });
|
|
656
848
|
const data = join(prefix.home, ".engram");
|
|
657
849
|
mkdirSync(data);
|
|
@@ -661,13 +853,11 @@ test("setup with a foreign existing store runs the installer like the baseline,
|
|
|
661
853
|
assert.equal(setup.status, 0, setup.stderr);
|
|
662
854
|
assert.equal(installer.runs(), 1);
|
|
663
855
|
assert.equal(existsSync(prefix.engram), true);
|
|
664
|
-
assert.
|
|
665
|
-
assert.doesNotMatch(setup.stderr, /update Engram and restart/);
|
|
666
|
-
assert.doesNotMatch(setup.stderr, /deferred/);
|
|
856
|
+
assert.doesNotMatch(setup.stderr, /memory disabled|deferred/);
|
|
667
857
|
const guarded = prefix.run([]);
|
|
668
858
|
assert.equal(guarded.status, 0, guarded.stderr);
|
|
669
859
|
const env = lastLaunch(guarded).env;
|
|
670
|
-
assert.deepEqual([env.ENGRAM_STARTUP_POLICY, env.ENGRAM_BIN, env.ENGRAM_URL], ["
|
|
860
|
+
assert.deepEqual([env.ENGRAM_STARTUP_POLICY, env.ENGRAM_BIN, env.ENGRAM_URL], ["allow", prefix.guard, DEFAULT_URL]);
|
|
671
861
|
// Subagents receive the same environment (GENTLE_PI_AGENTS_PI runs the same Pi with it).
|
|
672
862
|
assert.equal(env.GENTLE_PI_AGENTS_PI.endsWith(join("dist", "bundle", "cli.js")), true);
|
|
673
863
|
const provider = spawnSync(env.ENGRAM_BIN, ["instance-id"], { env: { ...prefix.env, ...env }, encoding: "utf8" });
|
|
@@ -682,15 +872,14 @@ test("setup with a foreign existing store runs the installer like the baseline,
|
|
|
682
872
|
assert.equal(existsSync(provenancePath(prefix)), false);
|
|
683
873
|
});
|
|
684
874
|
|
|
685
|
-
test("a gentle-pi version change
|
|
875
|
+
test("a gentle-pi version change reruns setup once and keeps memory on the shared server", { skip: posixHost }, (t) => {
|
|
686
876
|
const prefix = bundledPrefix(t, { core: false });
|
|
687
877
|
const installer = countingGentleAi(prefix);
|
|
688
878
|
const auto = { GENTLE_SHELL_NO_AUTO_SETUP: "", GENTLE_SHELL_GENTLE_AI_BIN: installer.bin };
|
|
689
|
-
const first = prefix.run([],
|
|
879
|
+
const first = prefix.run([], auto);
|
|
690
880
|
assert.equal(first.status, 0, first.stderr);
|
|
691
881
|
assert.equal(installer.runs(), 1);
|
|
692
|
-
assert.deepEqual(
|
|
693
|
-
assert.equal(existsSync(provenancePath(prefix)), true);
|
|
882
|
+
assert.deepEqual(memoryOf(first), ["allow", prefix.guard, DEFAULT_URL]);
|
|
694
883
|
assert.equal(prefix.run([], auto).status, 0);
|
|
695
884
|
assert.equal(installer.runs(), 1);
|
|
696
885
|
const manifest = join(dirname(dirname(prefix.entry)), "package.json");
|
|
@@ -699,7 +888,8 @@ test("a gentle-pi version change re-provisions Shell's own store once and keeps
|
|
|
699
888
|
assert.equal(changed.status, 0, changed.stderr);
|
|
700
889
|
assert.equal(installer.runs(), 2);
|
|
701
890
|
assert.doesNotMatch(changed.stderr, /automatic setup failed|deferred/);
|
|
702
|
-
assert.deepEqual(
|
|
891
|
+
assert.deepEqual(memoryOf(changed), ["allow", prefix.guard, DEFAULT_URL]);
|
|
892
|
+
assert.equal(prefix.serves().length, 1);
|
|
703
893
|
assert.equal(prefix.run([], auto).status, 0);
|
|
704
894
|
assert.equal(installer.runs(), 2);
|
|
705
895
|
});
|
|
@@ -720,8 +910,7 @@ test("capability readback also requires Pi to declare the selected pinned compan
|
|
|
720
910
|
const result = prefix.run([], { FIXTURE_INSTALL_CAPABILITY: "metadata-only" });
|
|
721
911
|
assert.equal(result.status, 0, result.stderr);
|
|
722
912
|
assert.match(result.stderr, /startupPolicy/);
|
|
723
|
-
|
|
724
|
-
assert.deepEqual(readdirSync(join(prefix.home, ".engram")), [".instance-id"]);
|
|
913
|
+
assert.equal(lastLaunch(result).env.ENGRAM_BIN, prefix.guard);
|
|
725
914
|
});
|
|
726
915
|
|
|
727
916
|
test("pinning preserves resource filters and unrelated settings and does not reinstall next launch", { skip: posixHost }, (t) => {
|
|
@@ -737,16 +926,19 @@ test("pinning preserves resource filters and unrelated settings and does not rei
|
|
|
737
926
|
assert.doesNotMatch(prefix.run([]).stderr, /installing policy-capable companion/);
|
|
738
927
|
});
|
|
739
928
|
|
|
740
|
-
test("final startup rechecks
|
|
929
|
+
test("final startup rechecks a store a newer Engram migrated during automatic setup and never starts a server on it", { skip: posixHost }, (t) => {
|
|
741
930
|
const prefix = bundledPrefix(t);
|
|
742
931
|
const fake = join(prefix.userBin, "gentle-ai");
|
|
743
|
-
|
|
932
|
+
const db = join(prefix.home, ".engram", "engram.db");
|
|
933
|
+
writeFileSync(fake, `#!${process.execPath}\nconst { mkdirSync, writeFileSync } = require('node:fs');\nmkdirSync(${JSON.stringify(dirname(db))}, { recursive: true });\n` +
|
|
934
|
+
`const header = Buffer.alloc(4096); header.write('SQLite format 3\\0', 0, 'latin1'); header.writeUInt16BE(4096, 16); header[18] = 2; header[19] = 2; header.writeUInt32BE(2, 60);\n` +
|
|
935
|
+
`writeFileSync(${JSON.stringify(db)}, header);\n`, { mode: 0o755 });
|
|
744
936
|
const result = prefix.run([], { GENTLE_SHELL_NO_AUTO_SETUP: "", GENTLE_SHELL_GENTLE_AI_BIN: fake });
|
|
745
937
|
assert.equal(result.status, 0, result.stderr);
|
|
746
|
-
|
|
747
|
-
assert.deepEqual(
|
|
748
|
-
assert.
|
|
749
|
-
assert.
|
|
938
|
+
assert.equal(existsSync(db), true);
|
|
939
|
+
assert.deepEqual(memoryOf(result), ["disabled", prefix.guard, undefined]);
|
|
940
|
+
assert.match(result.stderr, /memory disabled.*newer Engram/s);
|
|
941
|
+
assert.deepEqual(prefix.serves(), []);
|
|
750
942
|
});
|
|
751
943
|
|
|
752
944
|
test("explicit endpoints and invalid policies never trigger availability probes or override executables", { skip: posixHost }, (t) => {
|
|
@@ -760,6 +952,7 @@ test("explicit endpoints and invalid policies never trigger availability probes
|
|
|
760
952
|
const policyOnly = prefix.run([], { FIXTURE_NO_PROBE: "1", ENGRAM_STARTUP_POLICY: "invalid" });
|
|
761
953
|
assert.equal(policyOnly.status, 0, policyOnly.stderr);
|
|
762
954
|
assert.equal(lastLaunch(policyOnly).env.ENGRAM_BIN, prefix.guard);
|
|
955
|
+
assert.deepEqual(prefix.serves(), []);
|
|
763
956
|
});
|
|
764
957
|
|
|
765
958
|
test("startup offer precedes Pi, not-now preserves config and omit suppresses just that release", { skip: posixHost }, (t) => {
|
|
@@ -776,15 +969,36 @@ test("startup offer precedes Pi, not-now preserves config and omit suppresses ju
|
|
|
776
969
|
env: { ...prefix.env, NODE_OPTIONS: `${prefix.env.NODE_OPTIONS} --import=data:text/javascript,${encodeURIComponent(preload(latest))}` },
|
|
777
970
|
encoding: "utf8", input, timeout: 10000,
|
|
778
971
|
});
|
|
972
|
+
// A recorded lookup answers the next launch without the network, so these
|
|
973
|
+
// runs simulate later days by aging the cache back past the TTL; the live
|
|
974
|
+
// lookup then records its own answer again.
|
|
975
|
+
const ageCache = () => {
|
|
976
|
+
const parsed = JSON.parse(readFileSync(config, "utf8"));
|
|
977
|
+
const latest = typeof parsed.startupReleaseCheck?.latest === "string" ? parsed.startupReleaseCheck.latest : "999.0.0";
|
|
978
|
+
parsed.startupReleaseCheck = { checkedAt: new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString(), latest };
|
|
979
|
+
writeFileSync(config, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
980
|
+
};
|
|
779
981
|
const skipped = run("999.0.0", "2\r");
|
|
780
982
|
assert.equal(skipped.status, 0, skipped.stderr);
|
|
781
983
|
assert.match(skipped.stdout, /Gentle Shell update available/);
|
|
782
984
|
assert.ok(skipped.stdout.indexOf("Gentle Shell update available") < skipped.stdout.indexOf('"cli":'));
|
|
783
|
-
|
|
985
|
+
// A not-now choice still records no decision, but the live lookup it paid
|
|
986
|
+
// for lands in the config as the cache tomorrow's launch reuses.
|
|
987
|
+
const afterSkipped = JSON.parse(readFileSync(config, "utf8"));
|
|
988
|
+
assert.equal(afterSkipped.other, "preserved");
|
|
989
|
+
assert.equal(afterSkipped.skippedStartupRelease, undefined);
|
|
990
|
+
assert.equal(afterSkipped.startupReleaseCheck.latest, "999.0.0");
|
|
991
|
+
assert.ok(!Number.isNaN(Date.parse(afterSkipped.startupReleaseCheck.checkedAt)));
|
|
992
|
+
ageCache();
|
|
784
993
|
const omitted = run("999.0.0", "3\r");
|
|
785
994
|
assert.equal(omitted.status, 0, omitted.stderr);
|
|
786
|
-
|
|
995
|
+
const afterOmit = JSON.parse(readFileSync(config, "utf8"));
|
|
996
|
+
assert.equal(afterOmit.other, "preserved");
|
|
997
|
+
assert.equal(afterOmit.skippedStartupRelease, "999.0.0");
|
|
998
|
+
assert.equal(afterOmit.startupReleaseCheck.latest, "999.0.0");
|
|
999
|
+
ageCache();
|
|
787
1000
|
assert.doesNotMatch(run("999.0.0", "").stdout, /Gentle Shell update available/);
|
|
1001
|
+
ageCache();
|
|
788
1002
|
assert.match(run("999.0.1", "2\r").stdout, /Gentle Shell update available/);
|
|
789
1003
|
const command = run("999.0.1", "", ["list"]);
|
|
790
1004
|
assert.equal(command.status, 0, command.stderr);
|
|
@@ -810,11 +1024,13 @@ test("the bundled launcher runs only its version's Pi, with the user's environme
|
|
|
810
1024
|
assert.equal(seen.env.NODE_OPTIONS, prefix.env.NODE_OPTIONS);
|
|
811
1025
|
assert.equal(seen.env.npm_config_registry, "https://npm.example");
|
|
812
1026
|
assert.equal(seen.env.npm_config_prefix, undefined);
|
|
813
|
-
// S9: gentle-pi's config folder inside the prefix, the
|
|
814
|
-
// gentle-engram, and none of Gentle AI's own variables.
|
|
1027
|
+
// S9: gentle-pi's config folder inside the prefix, the shared server's URL
|
|
1028
|
+
// behind the guard for gentle-engram, and none of Gentle AI's own variables.
|
|
1029
|
+
// A Pi subcommand never starts the server.
|
|
815
1030
|
assert.equal(seen.env.GENTLE_SHELL_PREFIX, prefix.root);
|
|
816
1031
|
assert.equal(seen.env.GENTLE_PI_CONFIG_HOME, undefined);
|
|
817
|
-
assert.
|
|
1032
|
+
assert.deepEqual([seen.env.ENGRAM_BIN, seen.env.ENGRAM_URL], [prefix.guard, DEFAULT_URL]);
|
|
1033
|
+
assert.deepEqual(prefix.serves(), []);
|
|
818
1034
|
for (const name of ["GENTLE_AI_HOME", "GENTLE_AI_PI_CONFIG_DIR", "GENTLE_AI_ENGRAM_BIN", "GENTLE_AI_NO_SELF_UPDATE"]) assert.equal(seen.env[name], undefined, name);
|
|
819
1035
|
// Pi's own npm is ours, configured only from the prefix, through the isolated home's settings.
|
|
820
1036
|
const settings = JSON.parse(readFileSync(join(prefix.home, ".gentle-shell", "agent", "settings.json"), "utf8"));
|