gentle-pi 4.0.0 → 4.2.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 +157 -18
- package/assets/agents/gentle-ai-explore.md +11 -4
- package/assets/agents/gentle-ai-verify.md +23 -4
- package/assets/agents/gentle-ai-worker.md +6 -6
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +70 -163
- package/assets/orchestrator-memory.md +11 -4
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +29 -25
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +708 -22
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +399 -0
- package/docs/delegated-verification.md +3 -5
- package/docs/gentle-agents-activity.md +513 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +90 -33
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +1 -1
- package/docs/permission-modes.md +127 -0
- package/docs/prompt-history.md +1 -1
- package/docs/readme-reference.md +310 -47
- package/docs/render-cache-performance.md +28 -0
- package/docs/session-profile-format.md +39 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/yolo-mode.md +11 -10
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/child-context.ts +11 -2
- package/extensions/child-safety.ts +4 -2
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +718 -87
- package/extensions/gentle-ai.ts +991 -263
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +271 -80
- package/extensions/gentle-todo.ts +123 -18
- package/extensions/history/index.ts +11 -6
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +91 -21
- package/extensions/skill-registry.ts +175 -46
- package/extensions/startup-banner.ts +11 -2
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +40 -1
- package/lib/agents-runner.ts +90 -15
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +459 -0
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +484 -0
- package/lib/auto-permission-tool-context.ts +61 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +83 -10
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +14 -4
- package/lib/command-palette-catalog.ts +3 -2
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +104 -28
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-shell-bundled.ts +535 -0
- package/lib/gentle-shell-launcher.ts +75 -24
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +6 -1
- package/lib/nan-provider.ts +33 -11
- package/lib/native-review-cli.ts +194 -73
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase.ts +1 -1
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/review-candidate-view-owner.ts +8 -6
- package/lib/review-candidate-view.ts +3 -2
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-risk-assessment.ts +77 -51
- package/lib/review-sidebar-state.ts +2 -1
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +22 -7
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +261 -0
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-worktree-registry.ts +74 -8
- package/lib/shell-bar.ts +41 -11
- package/lib/shell-changes-view.ts +40 -13
- package/lib/shell-changes.ts +88 -71
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-sidebar-layout.ts +5 -1
- package/lib/shell-todo.ts +81 -26
- package/lib/shell-usage.ts +4 -3
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/vim-editor-adapter.ts +27 -5
- package/lib/visual-customization-policy.ts +20 -9
- package/lib/visual-customize-view.ts +171 -20
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +198 -121
- package/package.json +5 -5
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +536 -0
- package/runtime/gentle-shell-launcher.mjs +74 -23
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +191 -70
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +77 -51
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +7 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +79 -32
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/pi-update.mjs +223 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +22 -8
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +23 -5
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +82 -18
- package/skills/gentle-ai/SKILL.md +12 -11
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +12 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-protocol.test.ts +57 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +89 -4
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +276 -0
- package/tests/auto-mode-sdk.test.ts +84 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +634 -0
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +454 -0
- package/tests/autonomous-guard.test.ts +86 -16
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +52 -1
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +34 -2
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +12 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +107 -2
- package/tests/command-palette.test.ts +5 -5
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -2
- package/tests/devbinary/native-review-parity.devtest.ts +1 -1
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +316 -0
- package/tests/generic-agent-tools.test.ts +16 -1
- package/tests/gentle-agents.test.ts +1895 -76
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +19 -2
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-installer.test.ts +548 -58
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +6 -6
- package/tests/gentle-ai.test.ts +999 -31
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +263 -19
- package/tests/gentle-shell-bundled.test.ts +1036 -0
- package/tests/gentle-shell-launcher.test.ts +251 -25
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +611 -100
- package/tests/gentle-todo.test.ts +317 -14
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/inprocess-reviewer.test.ts +20 -0
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2159 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +395 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +28 -18
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +236 -1
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +1 -1
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +14 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
- package/tests/odd-routing-contract.test.ts +80 -23
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +29 -6
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +6 -5
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/package-manifest.test.ts +105 -21
- package/tests/packed-runner-owned-path.test.ts +35 -1
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +4 -12
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/pi-update-launcher.test.ts +87 -0
- package/tests/pi-update.test.ts +186 -0
- package/tests/provider-defect-handoff.test.ts +4 -3
- package/tests/quiet-tool-rendering.test.ts +160 -3
- package/tests/rdd-aware-verification-contract.test.ts +29 -41
- package/tests/rdd-status-line.test.ts +52 -0
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/review-agent-end-preflight.test.ts +62 -10
- package/tests/review-candidate-view.test.ts +101 -12
- package/tests/review-contract-prompt.test.ts +33 -4
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +255 -1
- package/tests/review-controller-workspace-root.test.ts +43 -2
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +5 -3
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +21 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +5 -4
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-risk-assessment.test.ts +109 -33
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +83 -5
- package/tests/runtime-harness.mjs +42 -15
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +65 -5
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +394 -0
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +57 -2
- package/tests/shell-bar.test.ts +40 -0
- package/tests/shell-changes-view.test.ts +85 -34
- package/tests/shell-changes.test.ts +79 -62
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +27 -2
- package/tests/shell-todo.test.ts +132 -11
- package/tests/shell-usage-view.test.ts +1 -1
- package/tests/shell-usage.test.ts +35 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +311 -5
- package/tests/startup-banner.test.ts +117 -3
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +27 -1
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter.test.ts +47 -21
- package/tests/visual-customization-policy.test.ts +32 -3
- package/tests/visual-customize-view.test.ts +288 -10
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +18 -3
- package/tests/yolo-mode-runtime.test.ts +37 -10
- package/tests/yolo-mode.test.ts +38 -5
- package/tests/yolo-session-policy.test.ts +1 -1
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { artifactFor } from "../scripts/installer-downloads.mjs";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import { PI_INSTALL_VERSION, collectInventory, goAcquisition, planPreflight, pnpmGlobalBin, requirements } from "../scripts/installer-preflight.mjs";
|
|
9
|
+
|
|
10
|
+
const absent = { available: false };
|
|
11
|
+
const tool = (version: string) => ({ available: true, version, usable: true });
|
|
12
|
+
function clean(platform = "linux", arch = "x64") {
|
|
13
|
+
return { platform, arch, node: absent, pnpm: absent, pi: absent, shell: absent,
|
|
14
|
+
gentleAi: absent, go: absent, globalBin: absent, setup: false };
|
|
15
|
+
}
|
|
16
|
+
function installed(platform = "linux") {
|
|
17
|
+
return { ...clean(platform), node: tool("24.1.0"),
|
|
18
|
+
pnpm: { ...tool("12.0.0"), compatible: true }, pi: tool("1.2.0"),
|
|
19
|
+
shell: { ...tool("5.0.0"), global: true },
|
|
20
|
+
gentleAi: { ...tool(requirements.gentleAi), compatible: true },
|
|
21
|
+
globalBin: { available: true, path: "/disposable/bin", writable: true, onPath: true }, setup: true };
|
|
22
|
+
}
|
|
23
|
+
const ids = (inventory: object) => planPreflight(inventory).actions.map((action: { id: string }) => action.id);
|
|
24
|
+
|
|
25
|
+
test("requirements follow repository metadata and native installer pin", () => {
|
|
26
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
27
|
+
assert.equal(requirements.node, pkg.engines.node.slice(2));
|
|
28
|
+
assert.equal(requirements.pi, pkg.peerDependencies["@earendil-works/pi-coding-agent"].slice(2));
|
|
29
|
+
assert.equal(requirements.pnpm, pkg.packageManager.split("@")[1]);
|
|
30
|
+
assert.equal(requirements.shell, pkg.version);
|
|
31
|
+
assert.equal(requirements.go, "1.25.10");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("preflight imports a packed manifest without packageManager using the verified pnpm pin", () => {
|
|
35
|
+
const root = mkdtempSync(fileURLToPath(new URL("./.preflight-packed-", import.meta.url)));
|
|
36
|
+
try {
|
|
37
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
38
|
+
delete pkg.packageManager;
|
|
39
|
+
writeFileSync(join(root, "package.json"), JSON.stringify(pkg));
|
|
40
|
+
cpSync(new URL("../scripts", import.meta.url), join(root, "scripts"), { recursive: true });
|
|
41
|
+
const entry = pathToFileURL(join(root, "scripts/installer-preflight.mjs")).href;
|
|
42
|
+
const result = spawnSync(process.execPath, ["--input-type=module", "-e",
|
|
43
|
+
`const { requirements } = await import(${JSON.stringify(entry)}); console.log(JSON.stringify(requirements));`], { encoding: "utf8" });
|
|
44
|
+
assert.equal(result.status, 0, result.stderr);
|
|
45
|
+
assert.equal(result.stderr, "");
|
|
46
|
+
assert.equal(result.stdout, `${JSON.stringify({ ...requirements, pnpm: artifactFor("pnpm").version })}\n`);
|
|
47
|
+
assert.equal(readFileSync(join(root, "package.json"), "utf8"), JSON.stringify(pkg));
|
|
48
|
+
} finally {
|
|
49
|
+
rmSync(root, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
for (const platform of ["linux", "darwin", "win32"]) {
|
|
54
|
+
for (const arch of ["x64", "arm64"]) {
|
|
55
|
+
test(`clean ${platform}/${arch} has dependency-ordered actions`, () => {
|
|
56
|
+
const plan = planPreflight(clean(platform, arch));
|
|
57
|
+
assert.equal(plan.blockers.length, 0);
|
|
58
|
+
assert.equal(plan.ready, false);
|
|
59
|
+
assert.deepEqual(ids(clean(platform, arch)), ["acquire-node", "verify-node", "acquire-pnpm", "verify-pnpm",
|
|
60
|
+
"setup-global-bin", ...(platform === "win32" ? ["acquire-go", "verify-go"] : []),
|
|
61
|
+
"install-pi", "install-shell", "setup-shell", "verify-readiness"]);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
test(`compatible ${platform} reuses newer global tools without acquiring Go`, () => {
|
|
65
|
+
const inventory = installed(platform);
|
|
66
|
+
const before = structuredClone(inventory);
|
|
67
|
+
const plan = planPreflight(inventory);
|
|
68
|
+
assert.equal(plan.ready, true);
|
|
69
|
+
assert.deepEqual(ids(inventory), ["verify-readiness"]);
|
|
70
|
+
assert.deepEqual(inventory, before);
|
|
71
|
+
assert.equal(plan.tools.node.status, "reusable");
|
|
72
|
+
assert.equal(plan.tools.go.status, "not-required");
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const mainSteps = ["build-gentle-ai-main", "install-shell-main", "record-channel"];
|
|
77
|
+
for (const platform of ["linux", "darwin", "win32"]) {
|
|
78
|
+
test(`the main channel on ${platform} adds the main build steps after a clean installation`, () => {
|
|
79
|
+
const inventory = { ...clean(platform), node: tool("24.1.0"), pnpm: { ...tool("11.1.1"), compatible: true },
|
|
80
|
+
globalBin: { available: true, path: "/disposable/bin", writable: true, onPath: true }, go: tool("1.26.0") };
|
|
81
|
+
const release = ids(inventory);
|
|
82
|
+
const plan = planPreflight(inventory, { channel: "main" });
|
|
83
|
+
assert.deepEqual(plan.blockers, []);
|
|
84
|
+
assert.equal(plan.tools.go.status, "reusable");
|
|
85
|
+
assert.deepEqual(plan.actions.map((action: { id: string }) => action.id), [...release, ...mainSteps]);
|
|
86
|
+
assert.deepEqual(plan.actions.slice(-3), [
|
|
87
|
+
{ id: "build-gentle-ai-main", kind: "build-native-main", target: "gentleAi" },
|
|
88
|
+
{ id: "install-shell-main", kind: "install-global", target: "shell" },
|
|
89
|
+
{ id: "record-channel", kind: "configure", target: "channel" }]);
|
|
90
|
+
assert.equal(plan.ready, false);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const goActions = [{ id: "acquire-go", kind: "acquire", target: "go", version: goAcquisition.version }, { id: "verify-go", kind: "verify", target: "go" }];
|
|
95
|
+
test("the pinned Go is a 1.25 patch release that meets the requirement", () => {
|
|
96
|
+
assert.equal(goAcquisition.version, "1.25.14");
|
|
97
|
+
const [major, minor, patch] = goAcquisition.version.split(".").map(Number);
|
|
98
|
+
const [rMajor, rMinor, rPatch] = requirements.go.split(".").map(Number);
|
|
99
|
+
assert.ok(major === rMajor && minor === rMinor && patch >= rPatch);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("the pinned Go is acquired before the runtime is persisted, so a failed Go download changes nothing", () => {
|
|
103
|
+
const persisting = { ...tool("24.18.0"), persistent: false, npm: true };
|
|
104
|
+
for (const [platform, channel] of [["linux", "main"], ["darwin", "main"], ["win32", "release"]] as const) {
|
|
105
|
+
const inventory = { ...clean(platform), node: persisting, pnpm: { ...tool("11.1.1"), compatible: true, persistent: false },
|
|
106
|
+
globalBin: { available: true, path: "/disposable/bin", writable: true, onPath: true }, go: absent };
|
|
107
|
+
const plan = planPreflight(inventory, { channel });
|
|
108
|
+
assert.deepEqual(plan.blockers, [], platform);
|
|
109
|
+
assert.deepEqual(plan.actions.map((action: { id: string }) => action.id).slice(0, 5),
|
|
110
|
+
["acquire-go", "verify-go", "persist-node", "persist-package-managers", "configure-npm-prefix"], platform);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("the main channel acquires the pinned Go when Go is missing or older, before the build, on every platform", () => {
|
|
115
|
+
for (const platform of ["linux", "darwin", "win32"]) {
|
|
116
|
+
for (const [go, found] of [[absent, null], [tool("1.25.9"), "1.25.9"]] as const) {
|
|
117
|
+
const inventory = { ...clean(platform), node: tool("24.1.0"), pnpm: { ...tool("11.1.1"), compatible: true },
|
|
118
|
+
globalBin: { available: true, path: "/disposable/bin", writable: true, onPath: true }, go };
|
|
119
|
+
const plan = planPreflight(inventory, { channel: "main" });
|
|
120
|
+
assert.deepEqual(plan.blockers, []);
|
|
121
|
+
assert.deepEqual(plan.tools.go, { status: "needs-acquire", required: requirements.go, version: goAcquisition.version, ...(found ? { found } : {}) });
|
|
122
|
+
const order = plan.actions.map((action: { id: string }) => action.id);
|
|
123
|
+
assert.deepEqual(plan.actions.filter((action: { target: string }) => action.target === "go"), goActions);
|
|
124
|
+
assert.deepEqual(order, ["acquire-go", "verify-go", "install-pi", "install-shell", "setup-shell", "verify-readiness", ...mainSteps], platform);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("an unknown Go never blocks a build: the build gets the installer's pinned Go, first on PATH", () => {
|
|
130
|
+
for (const [platform, channel] of [["darwin", "main"], ["linux", "main"], ["win32", "main"], ["win32", "release"]]) {
|
|
131
|
+
const plan = planPreflight({ ...clean(platform), go: { available: null } }, { channel });
|
|
132
|
+
assert.deepEqual(plan.blockers, [], `${platform} ${channel}`);
|
|
133
|
+
assert.deepEqual(plan.tools.go, { status: "needs-acquire", required: requirements.go, version: goAcquisition.version, unchecked: true }, `${platform} ${channel}`);
|
|
134
|
+
assert.deepEqual(plan.actions.filter((action: { target: string }) => action.target === "go"), goActions, `${platform} ${channel}`);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("a release installation on macOS or Linux never acquires Go, whatever Go is there", () => {
|
|
139
|
+
for (const platform of ["linux", "darwin"]) {
|
|
140
|
+
for (const go of [absent, tool("1.24.0"), { available: null }]) {
|
|
141
|
+
const plan = planPreflight({ ...clean(platform), go });
|
|
142
|
+
assert.equal(plan.tools.go.status, "not-required");
|
|
143
|
+
assert.equal(plan.actions.some((action: { target: string }) => action.target === "go"), false);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("the release channel is the default and plans exactly as before", () => {
|
|
149
|
+
for (const inventory of [clean("darwin"), installed("linux"), { ...clean("win32"), go: tool("1.26.0") }]) {
|
|
150
|
+
assert.deepEqual(planPreflight(inventory, { channel: "release" }), planPreflight(inventory));
|
|
151
|
+
}
|
|
152
|
+
assert.equal(planPreflight(clean("darwin")).tools.go.status, "not-required");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("an already set-up stack on the main channel plans no main steps, since nothing is installed", () => {
|
|
156
|
+
const plan = planPreflight({ ...installed("darwin"), go: tool("1.26.0") }, { channel: "main" });
|
|
157
|
+
assert.deepEqual(plan.actions.map((action: { id: string }) => action.id), ["verify-readiness"]);
|
|
158
|
+
assert.equal(plan.ready, true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("an unknown channel is rejected", () => {
|
|
162
|
+
assert.throws(() => planPreflight(clean("linux"), { channel: "nightly" }), /Unsupported installation channel/);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("Windows requires compatible Go only before a missing native binary", () => {
|
|
166
|
+
const inventory = { ...installed("win32"), gentleAi: absent, go: tool("1.26.0") };
|
|
167
|
+
assert.equal(planPreflight(inventory).tools.go.status, "reusable");
|
|
168
|
+
assert.deepEqual(ids(inventory), ["provision-native", "setup-shell", "verify-readiness"]);
|
|
169
|
+
const older = planPreflight({ ...inventory, go: tool("1.25.9") });
|
|
170
|
+
assert.deepEqual(older.blockers, []);
|
|
171
|
+
assert.deepEqual(older.tools.go, { status: "needs-acquire", required: requirements.go, version: goAcquisition.version, found: "1.25.9" });
|
|
172
|
+
assert.deepEqual(older.actions.slice(0, 2), goActions);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
for (const [name, version] of [["node", "22.18.0"], ["shell", "3.9.0"], ["node", "banana"], ["pi", "1.0.0-rc.1"]]) {
|
|
176
|
+
test(`${name} ${version} blocks rather than replacing an existing tool`, () => {
|
|
177
|
+
const inventory = { ...installed(), [name]: tool(version) };
|
|
178
|
+
const plan = planPreflight(inventory);
|
|
179
|
+
assert.ok(plan.blockers.some((b: { tool: string }) => b.tool === name));
|
|
180
|
+
assert.deepEqual(plan.actions, []);
|
|
181
|
+
assert.equal(plan.ready, false);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
test("an older Shell without its global command is reported as incompatible, not unknown", () => {
|
|
186
|
+
const unchecked = { available: null };
|
|
187
|
+
const inventory = { ...installed(), shell: { available: true, version: "3.4.0", usable: false, global: true },
|
|
188
|
+
gentleAi: unchecked, setup: unchecked };
|
|
189
|
+
const plan = planPreflight(inventory);
|
|
190
|
+
assert.deepEqual(plan.blockers, [{ code: "incompatible-tool", tool: "shell" }]);
|
|
191
|
+
assert.equal(plan.tools.shell.status, "incompatible");
|
|
192
|
+
assert.deepEqual(plan.actions, []);
|
|
193
|
+
assert.equal(plan.ready, false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("a Shell installed outside pnpm is the only blocker, without derived Gentle AI and setup blockers", () => {
|
|
197
|
+
const unchecked = { available: null };
|
|
198
|
+
const plan = planPreflight({ ...installed(), shell: { available: null, outsidePnpm: true }, gentleAi: unchecked, setup: unchecked });
|
|
199
|
+
assert.deepEqual(plan.blockers, [{ code: "unknown-tool", tool: "shell" }]);
|
|
200
|
+
assert.deepEqual(plan.actions, []);
|
|
201
|
+
assert.equal(plan.ready, false);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const unchecked = { available: null };
|
|
205
|
+
const owned = (version: string, owner = "pnpm", usable = true) => ({ available: true, version, usable, global: true, owner });
|
|
206
|
+
const updateIds = (plan: { actions: Array<{ id: string }> }) => plan.actions.map((action) => action.id);
|
|
207
|
+
|
|
208
|
+
test("an older Gentle Shell that pnpm or npm owns is updated to the latest release, then set up", () => {
|
|
209
|
+
for (const shell of [owned("3.9.0"), owned("3.9.0", "npm"), owned("3.4.0", "pnpm", false), owned(`${requirements.shell}-main.6e7e3a18f794`, "npm")]) {
|
|
210
|
+
const plan = planPreflight({ ...installed(), shell, gentleAi: unchecked, setup: unchecked });
|
|
211
|
+
assert.deepEqual(plan.blockers, [], JSON.stringify(shell));
|
|
212
|
+
assert.equal(plan.tools.shell.status, "needs-update");
|
|
213
|
+
assert.deepEqual(updateIds(plan), ["update-shell-release", "setup-shell", "verify-readiness"]);
|
|
214
|
+
assert.deepEqual(plan.actions[0], { id: "update-shell-release", kind: "upgrade", target: "shell" });
|
|
215
|
+
assert.equal(plan.ready, false);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("a current Gentle Shell installed by npm needs nothing on the release channel", () => {
|
|
220
|
+
const plan = planPreflight({ ...installed(), shell: owned(requirements.shell, "npm"), gentleAi: unchecked, setup: unchecked });
|
|
221
|
+
assert.deepEqual(plan.blockers, []);
|
|
222
|
+
assert.deepEqual(updateIds(plan), ["verify-readiness"]);
|
|
223
|
+
assert.equal(plan.ready, true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("a current pnpm-owned Gentle Shell plans exactly as before", () => {
|
|
227
|
+
const inventory = installed();
|
|
228
|
+
assert.deepEqual(planPreflight({ ...inventory, shell: { ...inventory.shell, owner: "pnpm" } }), planPreflight(inventory));
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("on the main channel an owned Gentle Shell is updated to the latest main, which needs Go", () => {
|
|
232
|
+
for (const shell of [owned(requirements.shell), owned("3.9.0", "npm"), owned(`${requirements.shell}-main.6e7e3a18f794`)]) {
|
|
233
|
+
const plan = planPreflight({ ...installed(), shell, go: tool("1.26.0"), gentleAi: unchecked, setup: unchecked }, { channel: "main" });
|
|
234
|
+
assert.deepEqual(plan.blockers, []);
|
|
235
|
+
assert.deepEqual(updateIds(plan), ["update-shell-main", "setup-shell", "verify-readiness"]);
|
|
236
|
+
assert.deepEqual(plan.actions[0], { id: "update-shell-main", kind: "upgrade", target: "shell" });
|
|
237
|
+
}
|
|
238
|
+
for (const go of [absent, tool("1.24.0")]) {
|
|
239
|
+
const noGo = planPreflight({ ...installed(), shell: owned("3.9.0"), go, gentleAi: unchecked, setup: unchecked }, { channel: "main" });
|
|
240
|
+
assert.deepEqual(noGo.blockers, []);
|
|
241
|
+
assert.deepEqual(updateIds(noGo), ["acquire-go", "verify-go", "update-shell-main", "setup-shell", "verify-readiness"]);
|
|
242
|
+
}
|
|
243
|
+
// A Windows release update runs gentle-pi's postinstall, which may build Gentle AI.
|
|
244
|
+
const windows = planPreflight({ ...installed("win32"), shell: owned("3.9.0"), go: tool("1.24.0"), gentleAi: unchecked, setup: unchecked });
|
|
245
|
+
assert.deepEqual(updateIds(windows), ["acquire-go", "verify-go", "update-shell-release", "setup-shell", "verify-readiness"]);
|
|
246
|
+
assert.deepEqual(updateIds(planPreflight({ ...installed("darwin"), shell: owned("3.9.0"), go: absent, gentleAi: unchecked, setup: unchecked })),
|
|
247
|
+
["update-shell-release", "setup-shell", "verify-readiness"]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("a missing Pi is installed before an existing Gentle Shell is updated", () => {
|
|
251
|
+
const plan = planPreflight({ ...installed(), pi: absent, shell: owned("3.9.0", "npm"), gentleAi: unchecked, setup: unchecked });
|
|
252
|
+
assert.deepEqual(updateIds(plan), ["install-pi", "update-shell-release", "setup-shell", "verify-readiness"]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("an existing compatible Pi from any installation is reused and only Gentle Shell is installed", () => {
|
|
256
|
+
for (const pi of [tool("1.2.0"), { ...tool("1.2.4"), external: true }]) {
|
|
257
|
+
const plan = planPreflight({ ...installed(), pi, shell: absent, gentleAi: absent, setup: false });
|
|
258
|
+
assert.equal(plan.tools.pi.status, "reusable");
|
|
259
|
+
assert.deepEqual(updateIds(plan), ["install-shell", "setup-shell", "verify-readiness"]);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("a pnpm Pi newer than the installer's pin never blocks: Gentle Shell is added or updated beside it", () => {
|
|
264
|
+
const newer = { ...tool("1.2.1") };
|
|
265
|
+
const install = planPreflight({ ...installed(), pi: newer, shell: absent, gentleAi: absent, setup: false });
|
|
266
|
+
assert.deepEqual([install.blockers, updateIds(install)], [[], ["install-shell", "setup-shell", "verify-readiness"]]);
|
|
267
|
+
const update = planPreflight({ ...installed(), pi: newer, shell: owned("3.9.0"), gentleAi: unchecked, setup: unchecked });
|
|
268
|
+
assert.deepEqual([update.blockers, updateIds(update)], [[], ["update-shell-release", "setup-shell", "verify-readiness"]]);
|
|
269
|
+
// Even a probe that reported an owner is not a blocker: only an older owned Pi is updated.
|
|
270
|
+
assert.deepEqual(planPreflight({ ...installed(), pi: { ...newer, owner: "pnpm" }, shell: absent, gentleAi: absent, setup: false }).blockers, []);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// An older Pi that pnpm or npm owns is updated to the version the installer installs.
|
|
274
|
+
const olderPi = (owner?: string, version = "0.87.1") => ({ ...tool(version), ...(owner ? { owner } : {}) });
|
|
275
|
+
|
|
276
|
+
test("an older Pi that pnpm or npm owns is updated before Gentle Shell is installed, instead of blocking", () => {
|
|
277
|
+
for (const owner of ["pnpm", "npm"]) {
|
|
278
|
+
const plan = planPreflight({ ...installed(), pi: olderPi(owner), shell: absent, gentleAi: absent, setup: false });
|
|
279
|
+
assert.deepEqual(plan.blockers, [], owner);
|
|
280
|
+
assert.deepEqual(plan.tools.pi, { status: "needs-update", required: requirements.pi, version: "0.87.1", owner });
|
|
281
|
+
assert.deepEqual(updateIds(plan), ["update-pi", "install-shell", "setup-shell", "verify-readiness"]);
|
|
282
|
+
assert.deepEqual(plan.actions[0], { id: "update-pi", kind: "upgrade", target: "pi", version: PI_INSTALL_VERSION });
|
|
283
|
+
assert.equal(plan.ready, false);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("an older owned Pi is updated before an existing Gentle Shell is updated", () => {
|
|
288
|
+
const plan = planPreflight({ ...installed(), pi: olderPi("pnpm"), shell: owned("3.9.0", "npm"), gentleAi: unchecked, setup: unchecked });
|
|
289
|
+
assert.deepEqual(plan.blockers, []);
|
|
290
|
+
assert.deepEqual(updateIds(plan), ["update-pi", "update-shell-release", "setup-shell", "verify-readiness"]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("with a current Gentle Shell, an older owned Pi is the only thing updated", () => {
|
|
294
|
+
for (const shell of [owned(requirements.shell, "npm"), { ...tool(requirements.shell), global: true }]) {
|
|
295
|
+
const plan = planPreflight({ ...installed(), pi: olderPi("npm"), shell });
|
|
296
|
+
assert.deepEqual(plan.blockers, [], JSON.stringify(shell));
|
|
297
|
+
assert.deepEqual(updateIds(plan), ["update-pi", "verify-readiness"]);
|
|
298
|
+
assert.equal(plan.ready, false);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("the main channel updates an older owned Pi before the main overlay", () => {
|
|
303
|
+
const inventory = { ...clean("darwin"), node: tool("24.1.0"), pnpm: { ...tool("11.1.1"), compatible: true }, pi: olderPi("pnpm"),
|
|
304
|
+
globalBin: { available: true, path: "/disposable/bin", writable: true, onPath: true }, go: tool("1.26.0") };
|
|
305
|
+
assert.deepEqual(updateIds(planPreflight(inventory, { channel: "main" })),
|
|
306
|
+
["update-pi", "install-shell", "setup-shell", "verify-readiness", ...mainSteps]);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("an older Pi that neither pnpm nor npm owns is left as it is, and the installer's Pi is installed alongside", () => {
|
|
310
|
+
for (const pi of [olderPi(), olderPi("mise"), { ...olderPi(), external: true }, { ...tool("0.99.0"), external: true }]) {
|
|
311
|
+
const plan = planPreflight({ ...installed(), pi, shell: absent, gentleAi: absent, setup: false });
|
|
312
|
+
assert.deepEqual(plan.blockers, [], JSON.stringify(pi));
|
|
313
|
+
assert.deepEqual(plan.tools.pi, { status: "needs-install", required: requirements.pi, version: pi.version });
|
|
314
|
+
// Exactly the absent-Pi installation: Pi and Gentle Shell in one pnpm add.
|
|
315
|
+
assert.deepEqual(updateIds(plan), ["install-pi", "install-shell", "setup-shell", "verify-readiness"]);
|
|
316
|
+
assert.deepEqual(updateIds(plan), updateIds(planPreflight({ ...installed(), pi: absent, shell: absent, gentleAi: absent, setup: false })));
|
|
317
|
+
}
|
|
318
|
+
const update = planPreflight({ ...installed(), pi: olderPi(), shell: owned("3.9.0", "npm"), gentleAi: unchecked, setup: unchecked });
|
|
319
|
+
assert.deepEqual(updateIds(update), ["install-pi", "update-shell-release", "setup-shell", "verify-readiness"]);
|
|
320
|
+
// A current Gentle Shell: only the installer's Pi is added.
|
|
321
|
+
for (const shell of [owned(requirements.shell, "npm"), { ...tool(requirements.shell), global: true }]) {
|
|
322
|
+
assert.deepEqual(updateIds(planPreflight({ ...installed(), pi: olderPi(), shell })), ["install-pi", "verify-readiness"]);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("Pi is never touched when it is current, and an unknown or unusable Pi keeps blocking", () => {
|
|
327
|
+
// A current Pi, even with a known owner, is reused untouched.
|
|
328
|
+
for (const pi of [olderPi("pnpm", requirements.pi), olderPi("npm", "1.2.0"), { ...olderPi(undefined, "1.2.0"), external: true }]) {
|
|
329
|
+
const plan = planPreflight({ ...installed(), pi, shell: absent, gentleAi: absent, setup: false });
|
|
330
|
+
assert.equal(plan.tools.pi.status, "reusable");
|
|
331
|
+
assert.deepEqual(updateIds(plan), ["install-shell", "setup-shell", "verify-readiness"]);
|
|
332
|
+
}
|
|
333
|
+
// A prerelease, an unusable Pi or one whose version cannot be read keeps blocking.
|
|
334
|
+
for (const pi of [olderPi("pnpm", "0.99.1-rc.1"), { ...olderPi("pnpm"), usable: false }, { ...olderPi(), usable: false },
|
|
335
|
+
{ available: null, owner: "pnpm" }, { available: null, outsidePnpm: true }]) {
|
|
336
|
+
const plan = planPreflight({ ...installed(), pi, shell: absent, gentleAi: absent, setup: false });
|
|
337
|
+
assert.ok(plan.blockers.some((b: { tool: string }) => b.tool === "pi"), JSON.stringify(pi));
|
|
338
|
+
assert.deepEqual(plan.actions, []);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("an unknown Gentle AI still blocks when the Shell is reusable", () => {
|
|
343
|
+
const plan = planPreflight({ ...installed(), gentleAi: { available: null } });
|
|
344
|
+
assert.deepEqual(plan.blockers, [{ code: "unknown-tool", tool: "gentleAi" }]);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("a current Shell without its global command still blocks as unknown", () => {
|
|
348
|
+
const plan = planPreflight({ ...installed(), shell: { available: true, version: "5.0.0", usable: false, global: true } });
|
|
349
|
+
assert.deepEqual(plan.blockers, [{ code: "unknown-tool", tool: "shell" }]);
|
|
350
|
+
assert.deepEqual(plan.actions, []);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("missing Windows Go is acquired before reprovisioning an existing Shell", () => {
|
|
354
|
+
const inventory = { ...installed("win32"), gentleAi: absent };
|
|
355
|
+
assert.deepEqual(ids(inventory), ["acquire-go", "verify-go", "provision-native", "setup-shell", "verify-readiness"]);
|
|
356
|
+
// A Go it cannot check is never run either: the pinned Go builds instead.
|
|
357
|
+
for (const go of [tool("devel"), { ...tool("1.26.0"), usable: false }]) {
|
|
358
|
+
assert.equal(planPreflight({ ...inventory, go }).tools.go.status, "needs-acquire");
|
|
359
|
+
assert.deepEqual(ids({ ...inventory, go }), ["acquire-go", "verify-go", "provision-native", "setup-shell", "verify-readiness"]);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// A pinned stack this pnpm installed whose setup did not finish (setup probe: recoverable).
|
|
364
|
+
const recoverable = { available: true, recoverable: true };
|
|
365
|
+
function partial(change: object = {}) {
|
|
366
|
+
return { ...installed(), pi: tool("1.2.0"), shell: { ...tool(requirements.shell), global: true }, setup: recoverable, ...change };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
test("a recoverable stack only reruns setup, with PATH setup when the global bin is off PATH", () => {
|
|
370
|
+
const offPath = { available: true, path: "/disposable/bin", writable: true, onPath: false };
|
|
371
|
+
const plan = planPreflight(partial());
|
|
372
|
+
assert.deepEqual(plan.blockers, []);
|
|
373
|
+
assert.equal(plan.tools.setup.status, "needs-setup");
|
|
374
|
+
assert.equal(plan.ready, false);
|
|
375
|
+
assert.deepEqual(ids(partial()), ["setup-shell", "verify-readiness"]);
|
|
376
|
+
assert.deepEqual(ids(partial({ globalBin: offPath })), ["setup-global-bin", "setup-shell", "verify-readiness"]);
|
|
377
|
+
// Never runtime persistence: the earlier run persisted it before installing the stack.
|
|
378
|
+
const bootstrapPnpm = { ...tool("11.1.1"), compatible: true, persistent: false };
|
|
379
|
+
for (const node of [{ ...tool("24.18.0"), persistent: false, npm: false }, { ...tool("24.18.0"), persistent: true, npm: false }]) {
|
|
380
|
+
assert.deepEqual(ids(partial({ node, pnpm: bootstrapPnpm, globalBin: offPath })), ["setup-global-bin", "setup-shell", "verify-readiness"]);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("only an exact recoverable setup with a verified Gentle AI is a recovery", () => {
|
|
385
|
+
for (const setup of [{ available: null }, { available: true }, { available: true, recoverable: "yes" }, { available: false, recoverable: true }]) {
|
|
386
|
+
const plan = planPreflight(partial({ setup }));
|
|
387
|
+
assert.ok(plan.blockers.some((b: { tool: string; code: string }) => b.tool === "setup" && b.code === "unknown-tool"), JSON.stringify(setup));
|
|
388
|
+
assert.deepEqual(plan.actions, []);
|
|
389
|
+
}
|
|
390
|
+
// A missing native binary keeps its existing provisioning plan; the runner does not run it.
|
|
391
|
+
assert.deepEqual(ids(partial({ gentleAi: absent })), ["provision-native", "setup-shell", "verify-readiness"]);
|
|
392
|
+
for (const change of [{ gentleAi: { available: null } }, { pi: tool("0.99.0") }, { shell: { ...tool(requirements.shell), global: false } }]) {
|
|
393
|
+
assert.deepEqual(ids(partial(change)), []);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("unknown probes and pnpm compatibility fail closed", () => {
|
|
398
|
+
for (const change of [{ node: undefined }, { pnpm: tool("11.1.1") }, { setup: undefined },
|
|
399
|
+
{ gentleAi: { ...tool("5.0.0"), compatible: false } }, { shell: { ...tool("4.0.0"), global: false } }]) {
|
|
400
|
+
assert.ok(planPreflight({ ...installed(), ...change }).blockers.length > 0);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("global-bin must be writable and reachable; known PATH repair is explicit", () => {
|
|
405
|
+
const inventory = installed();
|
|
406
|
+
assert.deepEqual(ids({ ...inventory, globalBin: { ...inventory.globalBin, onPath: false } }),
|
|
407
|
+
["setup-global-bin", "verify-readiness"]);
|
|
408
|
+
for (const globalBin of [{ ...inventory.globalBin, writable: false }, { available: true }]) {
|
|
409
|
+
assert.deepEqual(planPreflight({ ...inventory, globalBin }).actions, []);
|
|
410
|
+
assert.equal(planPreflight({ ...inventory, globalBin }).tools.globalBin.status, "unknown");
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("pnpm global bin is $PNPM_HOME/bin and onPath checks that directory, not $PNPM_HOME", () => {
|
|
415
|
+
const home = "/home/u/.local/share/pnpm";
|
|
416
|
+
assert.deepEqual(pnpmGlobalBin({ platform: "linux", env: { HOME: "/home/u", PATH: `${home}:/usr/bin` } }),
|
|
417
|
+
{ pnpmHome: home, path: `${home}/bin`, onPath: false });
|
|
418
|
+
assert.equal(pnpmGlobalBin({ platform: "linux", env: { HOME: "/home/u", PATH: `/usr/bin:${home}/bin/` } })?.onPath, true);
|
|
419
|
+
assert.equal(pnpmGlobalBin({ platform: "linux", env: { HOME: "/home/u", XDG_DATA_HOME: "/data", PATH: "" } })?.path, "/data/pnpm/bin");
|
|
420
|
+
assert.equal(pnpmGlobalBin({ platform: "darwin", env: { HOME: "/Users/u", PNPM_HOME: "/opt/pnpm", PATH: "/opt/pnpm/bin" } })?.onPath, true);
|
|
421
|
+
assert.equal(pnpmGlobalBin({ platform: "darwin", env: { HOME: "/Users/u" } })?.path, "/Users/u/Library/pnpm/bin");
|
|
422
|
+
assert.deepEqual(pnpmGlobalBin({ platform: "win32", env: { LOCALAPPDATA: "C:\\Users\\u\\AppData\\Local", Path: "c:\\users\\U\\appdata\\local\\PNPM\\Bin\\;C:\\Windows" } }),
|
|
423
|
+
{ pnpmHome: "C:\\Users\\u\\AppData\\Local\\pnpm", path: "C:\\Users\\u\\AppData\\Local\\pnpm\\bin", onPath: true });
|
|
424
|
+
for (const env of [{ PNPM_HOME: "relative/pnpm", HOME: "/home/u" }, { PATH: "/usr/bin" }, { HOME: "relative" }]) {
|
|
425
|
+
assert.equal(pnpmGlobalBin({ platform: "linux", env }), null);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("unsupported targets never offer installation", () => {
|
|
430
|
+
for (const inventory of [clean("freebsd"), clean("linux", "ia32")]) {
|
|
431
|
+
assert.equal(planPreflight(inventory).blockers[0].code, "unsupported-target");
|
|
432
|
+
assert.deepEqual(ids(inventory), []);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("collector invokes only injected named probes, catches failures without logging details", async () => {
|
|
437
|
+
const calls: string[] = [];
|
|
438
|
+
const probes = Object.fromEntries(["node", "pnpm", "pi", "shell", "gentleAi", "go", "globalBin", "setup"].map((name) =>
|
|
439
|
+
[name, async () => { calls.push(name); if (name === "node") throw new Error("private diagnostic"); return absent; }]));
|
|
440
|
+
const inventory = await collectInventory({ platform: "linux", arch: "x64", probes });
|
|
441
|
+
assert.deepEqual(calls, Object.keys(probes));
|
|
442
|
+
assert.deepEqual(inventory.node, { available: null });
|
|
443
|
+
assert.equal(JSON.stringify(inventory).includes("private diagnostic"), false);
|
|
444
|
+
assert.ok(planPreflight(inventory).blockers.length > 0);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("runtime persistence intents persist only what is missing", async () => {
|
|
448
|
+
const { persistencePins } = await import("../scripts/installer-preflight.mjs");
|
|
449
|
+
const full = ["persist-node", "persist-package-managers", "configure-npm-prefix"];
|
|
450
|
+
const persistence = [...full, "persist-npm", "persist-pnpm"];
|
|
451
|
+
const stack = (node: object, pnpmPersistent?: boolean) => ({ ...clean(), node,
|
|
452
|
+
pnpm: { ...tool("11.1.1"), compatible: true, ...(pnpmPersistent === undefined ? {} : { persistent: pnpmPersistent }) },
|
|
453
|
+
globalBin: { available: true, path: "/disposable/bin", writable: true, onPath: true } });
|
|
454
|
+
const rest = ["install-pi", "install-shell", "setup-shell", "verify-readiness"];
|
|
455
|
+
// Bootstrap-only Node: the runtime and npm whatever npm reports, and pnpm only when it is bootstrap-only too.
|
|
456
|
+
for (const npm of [true, false]) {
|
|
457
|
+
for (const pnpm of [true, false, undefined]) {
|
|
458
|
+
const group = pnpm === false ? full : ["persist-node", "persist-npm", "configure-npm-prefix"];
|
|
459
|
+
assert.deepEqual(ids(stack({ ...tool("24.18.0"), persistent: false, npm }, pnpm)), [...group, ...rest]);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
assert.deepEqual(planPreflight(stack({ ...tool("24.18.0"), persistent: false, npm: true })).actions[0],
|
|
463
|
+
{ id: "persist-node", kind: "persist-runtime", target: "node", version: persistencePins.node });
|
|
464
|
+
// Persistent Node: never runtime persistence; one add for the missing npm and/or pnpm.
|
|
465
|
+
const node = (npm: boolean | null) => ({ ...tool("24.18.0"), persistent: true, npm });
|
|
466
|
+
assert.deepEqual(planPreflight(stack(node(false), true)).actions[0],
|
|
467
|
+
{ id: "persist-npm", kind: "install-global", target: "npm", version: persistencePins.npm });
|
|
468
|
+
assert.deepEqual(planPreflight(stack(node(true), false)).actions[0],
|
|
469
|
+
{ id: "persist-pnpm", kind: "install-global", target: "pnpm", version: persistencePins.pnpm });
|
|
470
|
+
assert.deepEqual(ids(stack(node(false), false)), ["persist-package-managers", ...rest]);
|
|
471
|
+
for (const [npm, pnpm] of [[true, true], [true, undefined], [null, true]] as const) {
|
|
472
|
+
assert.deepEqual(ids(stack(node(npm), pnpm)), rest);
|
|
473
|
+
}
|
|
474
|
+
assert.equal(ids(stack(tool("24.18.0"))).some((id) => persistence.includes(id)), false);
|
|
475
|
+
assert.deepEqual(ids(stack({ ...tool("20.0.0"), persistent: false, npm: false })), []);
|
|
476
|
+
const unreachable = { ...stack({ ...tool("24.18.0"), persistent: false, npm: false }),
|
|
477
|
+
globalBin: { available: true, path: "/disposable/bin", writable: true, onPath: false } };
|
|
478
|
+
assert.deepEqual(ids({ ...unreachable, pnpm: { ...unreachable.pnpm, persistent: false } }).slice(0, 4), ["setup-global-bin", ...full]);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("a bootstrap-only Node next to a persistent pnpm persists Node and npm only, never pnpm", async () => {
|
|
482
|
+
const { persistencePins } = await import("../scripts/installer-preflight.mjs");
|
|
483
|
+
const stack = (pnpm: object) => ({ ...clean(), node: { ...tool("24.18.0"), persistent: false, npm: true },
|
|
484
|
+
pnpm: { ...tool("11.1.1"), compatible: true, ...pnpm }, globalBin: { available: true, path: "/disposable/bin", writable: true, onPath: true } });
|
|
485
|
+
const rest = ["install-pi", "install-shell", "setup-shell", "verify-readiness"];
|
|
486
|
+
// A compatible newer pnpm 11 already in $PNPM_HOME/bin (or persistent elsewhere) is kept as it is.
|
|
487
|
+
// So is a Windows user pnpm in $PNPM_HOME\bin that failed the storage walk (S7): never replaced.
|
|
488
|
+
for (const pnpm of [{ version: "11.5.0", persistent: true, inGlobalBin: true }, { persistent: true }, {},
|
|
489
|
+
{ version: "11.9.0", persistent: true, inGlobalBin: true, untrusted: true }]) {
|
|
490
|
+
const plan = planPreflight(stack(pnpm));
|
|
491
|
+
assert.deepEqual(plan.actions.map((action: { id: string }) => action.id), ["persist-node", "persist-npm", "configure-npm-prefix", ...rest], JSON.stringify(pnpm));
|
|
492
|
+
assert.deepEqual(plan.actions[1], { id: "persist-npm", kind: "install-global", target: "npm", version: persistencePins.npm });
|
|
493
|
+
}
|
|
494
|
+
// A bootstrap-only pnpm is still persisted with npm in one add.
|
|
495
|
+
assert.deepEqual(ids(stack({ persistent: false })), ["persist-node", "persist-package-managers", "configure-npm-prefix", ...rest]);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// An older Node or an incompatible pnpm on the user's PATH: the bootstrap's
|
|
499
|
+
// pinned copy runs the installer (the probe's version) and the probe reports
|
|
500
|
+
// the user's version as `found`. That copy is persisted; the user's stays.
|
|
501
|
+
const runtimeStack = (node: object, pnpm: object) => ({ ...clean(), node, pnpm,
|
|
502
|
+
globalBin: { available: true, path: "/disposable/bin", writable: true, onPath: true } });
|
|
503
|
+
const pinnedNode = (found?: string) => ({ ...tool("24.21.0"), persistent: false, npm: false, ...(found ? { found } : {}) });
|
|
504
|
+
const pinnedPnpm = (found?: string) => ({ ...tool("11.1.1"), compatible: true, persistent: false, ...(found ? { found } : {}) });
|
|
505
|
+
const rest = ["install-pi", "install-shell", "setup-shell", "verify-readiness"];
|
|
506
|
+
|
|
507
|
+
test("an older Node is left as it is: the pinned Node is persisted alongside instead of blocking", () => {
|
|
508
|
+
const plan = planPreflight(runtimeStack(pinnedNode("22.18.0"), { ...pinnedPnpm(), persistent: true }));
|
|
509
|
+
assert.deepEqual(plan.blockers, []);
|
|
510
|
+
assert.deepEqual(plan.tools.node, { status: "reusable", required: requirements.node, found: "22.18.0", version: "24.21.0" });
|
|
511
|
+
// The user's persistent pnpm stays; only Node and npm are persisted.
|
|
512
|
+
assert.deepEqual(updateIds(plan), ["persist-node", "persist-npm", "configure-npm-prefix", ...rest]);
|
|
513
|
+
assert.deepEqual(updateIds(planPreflight(runtimeStack(pinnedNode("22.18.0"), pinnedPnpm()))), ["persist-node", "persist-package-managers", "configure-npm-prefix", ...rest]);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("an incompatible pnpm is left as it is: the pinned pnpm is persisted alongside instead of blocking", () => {
|
|
517
|
+
for (const found of ["10.27.0", "12.0.0", "11.0.0"]) {
|
|
518
|
+
const persistentNode = { ...tool("24.18.0"), persistent: true, npm: true };
|
|
519
|
+
const plan = planPreflight(runtimeStack(persistentNode, pinnedPnpm(found)));
|
|
520
|
+
assert.deepEqual(plan.blockers, [], found);
|
|
521
|
+
assert.deepEqual(plan.tools.pnpm, { status: "reusable", required: requirements.pnpm, found, version: requirements.pnpm });
|
|
522
|
+
assert.deepEqual(updateIds(plan), ["persist-pnpm", ...rest]);
|
|
523
|
+
// Without a genuine npm both managers are added in one step, as when pnpm is absent.
|
|
524
|
+
assert.deepEqual(updateIds(planPreflight(runtimeStack({ ...persistentNode, npm: false }, pinnedPnpm(found)))), ["persist-package-managers", ...rest]);
|
|
525
|
+
}
|
|
526
|
+
const both = planPreflight(runtimeStack(pinnedNode("20.0.0"), pinnedPnpm("10.27.0")));
|
|
527
|
+
assert.equal(both.tools.node.found, "20.0.0");
|
|
528
|
+
assert.equal(both.tools.pnpm.found, "10.27.0");
|
|
529
|
+
assert.deepEqual(updateIds(both), ["persist-node", "persist-package-managers", "configure-npm-prefix", ...rest]);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test("a found version is recorded only for a bootstrap copy replacing a stable older or incompatible one", () => {
|
|
533
|
+
// A tool that meets the requirement is untouched, with or without a stray `found`.
|
|
534
|
+
for (const node of [{ ...tool("24.18.0"), persistent: true, npm: true, found: "20.0.0" }, pinnedNode("24.18.0"), pinnedNode("banana")]) {
|
|
535
|
+
const plan = planPreflight(runtimeStack(node, { ...pinnedPnpm(), persistent: true }));
|
|
536
|
+
assert.equal(plan.tools.node.found, undefined, JSON.stringify(node));
|
|
537
|
+
}
|
|
538
|
+
assert.equal(planPreflight(runtimeStack(pinnedNode(), pinnedPnpm("10.27.0-beta.1"))).tools.pnpm.found, undefined);
|
|
539
|
+
// The older tool itself (no bootstrap copy) or one of unknown version keeps blocking.
|
|
540
|
+
for (const node of [tool("22.18.0"), { available: null }]) {
|
|
541
|
+
const plan = planPreflight(runtimeStack(node, pinnedPnpm()));
|
|
542
|
+
assert.ok(plan.blockers.some((b: { tool: string }) => b.tool === "node"), JSON.stringify(node));
|
|
543
|
+
assert.deepEqual(plan.actions, []);
|
|
544
|
+
}
|
|
545
|
+
for (const pnpm of [{ ...tool("10.27.0"), compatible: false }, { available: null }]) {
|
|
546
|
+
assert.ok(planPreflight(runtimeStack(pinnedNode(), pnpm)).blockers.some((b: { tool: string }) => b.tool === "pnpm"));
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test("an update of an existing installation persists nothing, so an older Node or incompatible pnpm still blocks it", () => {
|
|
551
|
+
for (const [name, change] of [["node", { node: pinnedNode("22.18.0") }], ["pnpm", { pnpm: pinnedPnpm("10.27.0") }]] as const) {
|
|
552
|
+
const plan = planPreflight({ ...installed(), shell: owned("3.9.0"), gentleAi: unchecked, setup: unchecked, ...change });
|
|
553
|
+
assert.deepEqual(plan.blockers, [{ code: "incompatible-tool", tool: name }]);
|
|
554
|
+
assert.deepEqual(plan.actions, []);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test("a current Gentle Shell persists nothing either, so an older Node or incompatible pnpm blocks before consent", () => {
|
|
559
|
+
// The runner persists runtimes only while installing Gentle Shell (planGate):
|
|
560
|
+
// these plans would otherwise be rejected as unsupported after consent.
|
|
561
|
+
const current = { ...installed(), shell: owned(requirements.shell) };
|
|
562
|
+
const cases = [
|
|
563
|
+
["node", { node: pinnedNode("22.18.0") }],
|
|
564
|
+
["node", { node: pinnedNode("22.18.0"), pi: { ...tool("0.87.1"), owner: "pnpm" } }],
|
|
565
|
+
["node", { node: pinnedNode("22.18.0"), setup: false }],
|
|
566
|
+
["pnpm", { node: { ...tool("24.18.0"), persistent: true, npm: true }, pnpm: pinnedPnpm("10.27.0") }],
|
|
567
|
+
] as const;
|
|
568
|
+
for (const [name, change] of cases) {
|
|
569
|
+
const plan = planPreflight({ ...current, ...change });
|
|
570
|
+
assert.deepEqual(plan.blockers, [{ code: "incompatible-tool", tool: name }], JSON.stringify(change));
|
|
571
|
+
assert.deepEqual(plan.actions, []);
|
|
572
|
+
}
|
|
573
|
+
// A setup recovery persists nothing and runs with the bootstrap's Node, as before.
|
|
574
|
+
const recovery = planPreflight({ ...current, node: pinnedNode("22.18.0"), setup: recoverable });
|
|
575
|
+
assert.deepEqual(recovery.blockers, []);
|
|
576
|
+
assert.deepEqual(updateIds(recovery), ["setup-shell", "verify-readiness"]);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
test("a user's pnpm in $PNPM_HOME/bin is never persisted over: an older or newer-major one blocks", () => {
|
|
580
|
+
const node = { ...tool("24.18.0"), persistent: true, npm: true };
|
|
581
|
+
for (const version of ["11.0.5", "12.0.0"]) {
|
|
582
|
+
for (const n of [node, pinnedNode("22.18.0")]) {
|
|
583
|
+
const plan = planPreflight(runtimeStack(n, { ...tool(version), compatible: false, persistent: true, inGlobalBin: true }));
|
|
584
|
+
assert.ok(plan.blockers.some((b: { code: string; tool: string }) => b.code === "incompatible-tool" && b.tool === "pnpm"), version);
|
|
585
|
+
assert.deepEqual(plan.actions, []);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
assert.deepEqual(planPreflight(runtimeStack(node, { available: null, inGlobalBin: true })).blockers, [{ code: "unknown-tool", tool: "pnpm" }]);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("a user's pnpm in $PNPM_HOME/bin that could not be checked is left as it is: the bootstrap's pnpm runs, nothing is persisted over it", () => {
|
|
592
|
+
const node = { ...tool("24.18.0"), persistent: true, npm: true };
|
|
593
|
+
const unchecked = { ...tool("11.1.1"), compatible: true, persistent: true, inGlobalBin: true, unchecked: true };
|
|
594
|
+
const rest = ["install-pi", "install-shell", "setup-shell", "verify-readiness"];
|
|
595
|
+
for (const [n, persisted] of [[node, []], [pinnedNode("22.18.0"), ["persist-node", "persist-npm", "configure-npm-prefix"]]] as const) {
|
|
596
|
+
const plan = planPreflight(runtimeStack(n, unchecked));
|
|
597
|
+
assert.deepEqual(plan.blockers, []);
|
|
598
|
+
assert.deepEqual(plan.tools.pnpm, { status: "reusable", required: requirements.pnpm, unchecked: true });
|
|
599
|
+
assert.deepEqual(plan.actions.map((action: { id: string }) => action.id), [...persisted, ...rest]);
|
|
600
|
+
}
|
|
601
|
+
// Only that case is recorded: a checked compatible pnpm there, or an untrusted one (S7), is not.
|
|
602
|
+
for (const pnpm of [{ ...unchecked, unchecked: undefined }, { ...unchecked, unchecked: undefined, untrusted: true }]) {
|
|
603
|
+
assert.deepEqual(planPreflight(runtimeStack(node, pnpm)).tools.pnpm, { status: "reusable", required: requirements.pnpm });
|
|
604
|
+
}
|
|
605
|
+
// One whose version was read and is incompatible still blocks.
|
|
606
|
+
assert.deepEqual(planPreflight(runtimeStack(node, { ...tool("11.0.5"), compatible: false, persistent: true, inGlobalBin: true })).blockers,
|
|
607
|
+
[{ code: "incompatible-tool", tool: "pnpm" }]);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("an older Go is left as it is: the pinned Go is acquired alongside, and a set-up stack downloads nothing", () => {
|
|
611
|
+
for (const [platform, channel, change] of [["win32", "release", { gentleAi: absent }], ["darwin", "main", { shell: absent, setup: false }]] as const) {
|
|
612
|
+
const plan = planPreflight({ ...installed(platform), ...change, go: tool("1.24.0") }, { channel });
|
|
613
|
+
assert.deepEqual(plan.blockers, [], platform);
|
|
614
|
+
assert.equal(plan.tools.go.found, "1.24.0");
|
|
615
|
+
assert.deepEqual(plan.actions.filter((action: { target: string }) => action.target === "go"), goActions);
|
|
616
|
+
}
|
|
617
|
+
// Nothing to build: no download, even on main.
|
|
618
|
+
const ready = planPreflight({ ...installed("darwin"), go: absent }, { channel: "main" });
|
|
619
|
+
assert.deepEqual(ready.actions.map((action: { id: string }) => action.id), ["verify-readiness"]);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// S6: the Windows PNPM_HOME decision (windowsPnpmHome) the wizard records before
|
|
623
|
+
// any probe. A passing default leaves the plan exactly as before.
|
|
624
|
+
const W_DEFAULT = "C:\\Users\\m\\AppData\\Local\\pnpm";
|
|
625
|
+
const W_PRIVATE = "C:\\Users\\m\\.pnpm";
|
|
626
|
+
const weakFinding = { check: "target-acl-mask", at: "C:\\Users\\m\\AppData\\Local", sid: "S-1-5-21-1-2-3-1002", account: "PC\\other", rights: "0x001301BF" };
|
|
627
|
+
test("a Windows PNPM_HOME that passes the walk leaves the plan unchanged; POSIX ignores the record", () => {
|
|
628
|
+
for (const pnpmHome of [{ available: true, path: W_DEFAULT, source: "default" }, { available: true, path: "D:\\pnpm", source: "user" }]) {
|
|
629
|
+
for (const inventory of [clean("win32"), installed("win32")]) assert.deepEqual(planPreflight({ ...inventory, pnpmHome }), planPreflight(inventory));
|
|
630
|
+
}
|
|
631
|
+
const untrusted = { available: true, path: "/home/u/.local/share/pnpm", source: "user", untrusted: weakFinding };
|
|
632
|
+
assert.deepEqual(planPreflight({ ...clean("linux"), pnpmHome: untrusted }), planPreflight(clean("linux")));
|
|
633
|
+
assert.deepEqual(planPreflight({ ...clean("win32"), pnpmHome: { available: null } }), planPreflight(clean("win32")), "no decision recorded");
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
test("a private Windows PNPM_HOME is recorded with the default it replaces and why, and its PATH is set up", () => {
|
|
637
|
+
const pnpmHome = { available: true, path: W_PRIVATE, source: "private", rejected: { path: W_DEFAULT, ...weakFinding } };
|
|
638
|
+
const plan = planPreflight({ ...clean("win32"), pnpmHome });
|
|
639
|
+
assert.deepEqual(plan.blockers, []);
|
|
640
|
+
assert.deepEqual(plan.tools.pnpmHome, { status: "private", path: W_PRIVATE, default: W_DEFAULT, finding: weakFinding });
|
|
641
|
+
assert.deepEqual(plan.actions, planPreflight(clean("win32")).actions);
|
|
642
|
+
assert.ok(plan.actions.some((action: { id: string }) => action.id === "setup-global-bin"));
|
|
643
|
+
// Only known string fields reach the plan.
|
|
644
|
+
const odd = planPreflight({ ...clean("win32"), pnpmHome: { ...pnpmHome, rejected: { path: W_DEFAULT, check: "target-owner", at: W_DEFAULT, sid: 7, extra: "x" } } });
|
|
645
|
+
assert.deepEqual(odd.tools.pnpmHome.finding, { check: "target-owner", at: W_DEFAULT });
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test("an untrusted Windows PNPM_HOME, or one that could not be checked, blocks before any action", () => {
|
|
649
|
+
const cases = [
|
|
650
|
+
[{ available: true, path: W_DEFAULT, source: "default", untrusted: weakFinding, installed: true }, "untrusted-pnpm-home"],
|
|
651
|
+
[{ available: true, path: "D:\\pnpm", source: "user", untrusted: weakFinding }, "untrusted-pnpm-home"],
|
|
652
|
+
[{ available: true, path: W_DEFAULT, source: "default", untrusted: weakFinding, private: { path: W_PRIVATE, foreign: true } }, "untrusted-pnpm-home"],
|
|
653
|
+
[{ available: null, failed: true }, "unknown-tool"],
|
|
654
|
+
] as const;
|
|
655
|
+
for (const [pnpmHome, code] of cases) {
|
|
656
|
+
for (const inventory of [clean("win32"), installed("win32")]) {
|
|
657
|
+
const plan = planPreflight({ ...inventory, pnpmHome });
|
|
658
|
+
assert.deepEqual(plan.blockers.filter((blocker: { tool: string }) => blocker.tool === "pnpmHome"), [{ code, tool: "pnpmHome" }], JSON.stringify(pnpmHome));
|
|
659
|
+
assert.deepEqual(plan.actions, []);
|
|
660
|
+
assert.equal(plan.ready, false);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
assert.equal(planPreflight({ ...clean("win32"), pnpmHome: cases[0][0] }).tools.pnpmHome.status, "untrusted");
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// R1: a blocked Windows PNPM_HOME decision means no probe runs at all; the plan
|
|
667
|
+
// carries only that blocker.
|
|
668
|
+
test("a blocked Windows PNPM_HOME decision runs no probe and plans only that blocker", async () => {
|
|
669
|
+
const cases = [
|
|
670
|
+
[{ available: true, path: W_DEFAULT, source: "default", untrusted: weakFinding, installed: true }, { code: "untrusted-pnpm-home", tool: "pnpmHome" }],
|
|
671
|
+
[{ available: true, path: "D:\\pnpm", source: "user", untrusted: weakFinding }, { code: "untrusted-pnpm-home", tool: "pnpmHome" }],
|
|
672
|
+
[{ available: null, failed: true }, { code: "unknown-tool", tool: "pnpmHome" }],
|
|
673
|
+
] as const;
|
|
674
|
+
for (const [pnpmHome, blocker] of cases) {
|
|
675
|
+
const called: string[] = [];
|
|
676
|
+
const probes = Object.fromEntries(["node", "pnpm", "pi", "shell", "gentleAi", "go", "globalBin", "setup"]
|
|
677
|
+
.map((name) => [name, async () => { called.push(name); return { available: null }; }]));
|
|
678
|
+
const inventory = await collectInventory({ platform: "win32", arch: "x64", probes, pnpmHome });
|
|
679
|
+
assert.deepEqual(called, [], JSON.stringify(pnpmHome));
|
|
680
|
+
assert.deepEqual(inventory.pnpmHome, pnpmHome);
|
|
681
|
+
const plan = planPreflight(inventory);
|
|
682
|
+
assert.deepEqual([plan.blockers, plan.actions, plan.ready], [[blocker], [], false]);
|
|
683
|
+
// Even with every other tool unknown, that blocker stands alone.
|
|
684
|
+
assert.deepEqual(planPreflight({ ...clean("win32"), node: { available: null }, pi: { available: null }, pnpmHome }).blockers, [blocker]);
|
|
685
|
+
}
|
|
686
|
+
// A passing or private decision still runs every probe.
|
|
687
|
+
for (const pnpmHome of [{ available: true, path: W_DEFAULT, source: "default" },
|
|
688
|
+
{ available: true, path: W_PRIVATE, source: "private", rejected: { path: W_DEFAULT, ...weakFinding } }]) {
|
|
689
|
+
const called: string[] = [];
|
|
690
|
+
await collectInventory({ platform: "win32", arch: "x64", probes: { node: async () => { called.push("node"); return absent; } }, pnpmHome });
|
|
691
|
+
assert.deepEqual(called, ["node"]);
|
|
692
|
+
}
|
|
693
|
+
// POSIX ignores a decision record.
|
|
694
|
+
const posixCalled: string[] = [];
|
|
695
|
+
await collectInventory({ platform: "linux", arch: "x64", probes: { node: async () => { posixCalled.push("node"); return absent; } },
|
|
696
|
+
pnpmHome: { available: null, failed: true } });
|
|
697
|
+
assert.deepEqual(posixCalled, ["node"]);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// S6 notice: reused tools whose folders another account can change, from one walk.
|
|
701
|
+
const weakFolder = { check: "parent-acl-mask", at: "C:\\Users\\m\\AppData\\Roaming", sid: "S-1-5-21-1-2-3-1002", account: "PC\\other", rights: "0x001301BF" };
|
|
702
|
+
const allFolders = { node: weakFolder, npm: weakFolder, go: { check: "target-owner", at: "C:\\Go\\bin\\go.exe", sid: "S-1-5-21-9" }, pi: weakFolder, shell: weakFolder };
|
|
703
|
+
test("Windows: collectInventory records the folders walk after the tool probes, and never blocks on it", async () => {
|
|
704
|
+
const order: string[] = [];
|
|
705
|
+
const probes = { ...Object.fromEntries(["node", "pnpm", "pi", "shell", "gentleAi", "go", "globalBin", "setup"].map((name) => [name, async () => { order.push(name); return absent; }])),
|
|
706
|
+
folders: async () => { order.push("folders"); return { node: weakFolder }; } };
|
|
707
|
+
const inventory = await collectInventory({ platform: "win32", arch: "x64", probes });
|
|
708
|
+
assert.deepEqual(order, ["node", "pnpm", "pi", "shell", "gentleAi", "go", "globalBin", "setup", "folders"]);
|
|
709
|
+
assert.deepEqual(inventory.folders, { node: weakFolder });
|
|
710
|
+
for (const folders of [async () => null, async () => ({}), async () => ({ available: null }), async () => { throw new Error("walk failed"); }]) {
|
|
711
|
+
const quiet = await collectInventory({ platform: "win32", arch: "x64", probes: { ...probes, folders } });
|
|
712
|
+
assert.equal("folders" in quiet, false);
|
|
713
|
+
}
|
|
714
|
+
// POSIX and a blocked PNPM_HOME decision never walk.
|
|
715
|
+
order.length = 0;
|
|
716
|
+
assert.equal("folders" in await collectInventory({ platform: "linux", arch: "x64", probes }), false);
|
|
717
|
+
assert.equal(order.includes("folders"), false);
|
|
718
|
+
order.length = 0;
|
|
719
|
+
await collectInventory({ platform: "win32", arch: "x64", probes, pnpmHome: { available: null, failed: true } });
|
|
720
|
+
assert.deepEqual(order, []);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
test("Windows: the plan notes only the reused tools whose folders another account can change, without blocking", () => {
|
|
724
|
+
const reused = { ...clean("win32"), node: { ...tool("24.18.0"), persistent: true, npm: true }, pnpm: { ...tool("11.1.1"), compatible: true, persistent: true },
|
|
725
|
+
pi: { ...tool("1.2.0"), external: true }, go: tool("1.26.0"), globalBin: { available: true, path: "C:\\Users\\m\\AppData\\Local\\pnpm\\bin", writable: true, onPath: true } };
|
|
726
|
+
const plan = planPreflight({ ...reused, folders: allFolders });
|
|
727
|
+
assert.deepEqual(plan.blockers, []);
|
|
728
|
+
assert.deepEqual(plan.actions, planPreflight(reused).actions, "a notice changes no action");
|
|
729
|
+
assert.deepEqual(plan.tools.folders, { status: "notice", reused: [{ tool: "node", ...weakFolder }, { tool: "npm", ...weakFolder }, { tool: "go", ...allFolders.go }, { tool: "pi", ...weakFolder }] },
|
|
730
|
+
"Gentle Shell is installed by pnpm here, so its folder is not reused");
|
|
731
|
+
assert.equal("folders" in planPreflight(reused).tools, false);
|
|
732
|
+
// Not reused: a Node left for the pinned one, a missing npm, Go not needed, a Pi installed alongside.
|
|
733
|
+
const replaced = planPreflight({ ...reused, node: { ...tool("24.21.0"), persistent: false, npm: false, found: "22.0.0" },
|
|
734
|
+
pi: { ...tool("0.80.0"), external: true }, folders: allFolders });
|
|
735
|
+
assert.deepEqual(replaced.tools.folders?.reused.map((entry: { tool: string }) => entry.tool), ["go"], "only Go, which the Windows build still reuses");
|
|
736
|
+
const missingNpm = planPreflight({ ...reused, node: { ...tool("24.18.0"), persistent: true, npm: false }, folders: { npm: weakFolder } });
|
|
737
|
+
assert.equal(missingNpm.tools.folders, undefined);
|
|
738
|
+
// An npm-owned Gentle Shell that is kept or updated with npm is reused.
|
|
739
|
+
const npmShell = planPreflight({ ...installed("win32"), node: { ...tool("24.18.0"), persistent: true, npm: true },
|
|
740
|
+
shell: { ...tool(requirements.shell), global: true, owner: "npm" }, folders: { shell: weakFolder, go: weakFolder } });
|
|
741
|
+
assert.deepEqual(npmShell.tools.folders?.reused.map((entry: { tool: string }) => entry.tool), ["shell"], "no build here, so Go is not reused");
|
|
742
|
+
// A path the walk could not check is noted as such; an unknown code never reaches the plan.
|
|
743
|
+
const unchecked = planPreflight({ ...reused, folders: { node: { check: "unchecked", at: "C:\\nodejs\\node.exe" }, pi: { check: "denied", at: "C:\\x" } } });
|
|
744
|
+
assert.deepEqual(unchecked.tools.folders, { status: "notice", reused: [{ tool: "node", check: "unchecked", at: "C:\\nodejs\\node.exe" }] });
|
|
745
|
+
assert.deepEqual(unchecked.blockers, []);
|
|
746
|
+
// Only known string fields reach the plan; POSIX ignores the record.
|
|
747
|
+
const odd = planPreflight({ ...reused, folders: { node: { ...weakFolder, sid: 7, extra: "x" }, other: weakFolder } });
|
|
748
|
+
assert.deepEqual(odd.tools.folders.reused, [{ tool: "node", check: weakFolder.check, at: weakFolder.at, account: weakFolder.account, rights: weakFolder.rights }]);
|
|
749
|
+
assert.equal(planPreflight({ ...reused, platform: "linux", folders: allFolders }).tools.folders, undefined);
|
|
750
|
+
});
|