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
|
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { EventEmitter } from "node:events";
|
|
5
5
|
import { PassThrough } from "node:stream";
|
|
6
|
-
import { chmod, copyFile, mkdtemp, mkdir, readFile, readdir, rename, stat, symlink, writeFile } from "node:fs/promises";
|
|
6
|
+
import { chmod, copyFile, mkdtemp, mkdir, readFile, readdir, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
9
|
import test from "node:test";
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
INSTALLER_VERSION,
|
|
14
14
|
GENTLE_AI_RELEASE_ASSETS,
|
|
15
15
|
GENTLE_AI_WINDOWS_SOURCE_MODULE,
|
|
16
|
+
GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH,
|
|
17
|
+
GENTLE_AI_WINDOWS_SOURCE_TAG,
|
|
16
18
|
GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|
|
17
19
|
downloadGentleAiAsset,
|
|
18
20
|
gentleAiAssetForm,
|
|
@@ -22,20 +24,33 @@ import {
|
|
|
22
24
|
trustedSystemExtractor,
|
|
23
25
|
} from "../scripts/gentle-ai-installer.mjs";
|
|
24
26
|
|
|
25
|
-
// v4.
|
|
27
|
+
// v4.1.1 archive digests independently match the minisign-signed release
|
|
26
28
|
// checksums; binary digests were computed from the extracted executables.
|
|
29
|
+
function releaseAssets(): Record<string, { name: string; sha256: string; binarySha256: string; url: string }> {
|
|
30
|
+
const assets: Record<string, { name: string; sha256: string; binarySha256: string; url: string }> = {};
|
|
31
|
+
for (const [key, value] of Object.entries(GENTLE_AI_RELEASE_ASSETS)) {
|
|
32
|
+
assert.ok(typeof value === "object" && value !== null);
|
|
33
|
+
assert.ok("name" in value && typeof value.name === "string");
|
|
34
|
+
assert.ok("sha256" in value && typeof value.sha256 === "string");
|
|
35
|
+
assert.ok("binarySha256" in value && typeof value.binarySha256 === "string");
|
|
36
|
+
assert.ok("url" in value && typeof value.url === "string");
|
|
37
|
+
assets[key] = { name: value.name, sha256: value.sha256, binarySha256: value.binarySha256, url: value.url };
|
|
38
|
+
}
|
|
39
|
+
return assets;
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
const EXPECTED_ASSETS = {
|
|
28
|
-
"darwin/amd64": { name: "gentle-ai_4.
|
|
29
|
-
"darwin/arm64": { name: "gentle-ai_4.
|
|
30
|
-
"linux/amd64": { name: "gentle-ai_4.
|
|
31
|
-
"linux/arm64": { name: "gentle-ai_4.
|
|
43
|
+
"darwin/amd64": { name: "gentle-ai_4.1.1_darwin_amd64.tar.gz", sha256: "f2e9aebfba3056e88a8d7c7193e4e8c04282dc126d5eaa2216bc807deadecffd", binarySha256: "82351dd86bf7323c6a5b9e9e68b3cc2893a65a4df04fbdf1ef1801cabf5d2338" },
|
|
44
|
+
"darwin/arm64": { name: "gentle-ai_4.1.1_darwin_arm64.tar.gz", sha256: "8093dced37130cb26534d81663013f81d9039342e33f00b8c85aa5319aa7eea9", binarySha256: "6362e3641b87ae2b41a8dd7695dad4cf47457ee86c9f63792e474476bfdb6b6a" },
|
|
45
|
+
"linux/amd64": { name: "gentle-ai_4.1.1_linux_amd64.tar.gz", sha256: "57869ebcdba5ba9942493ab413bbc15d146e2451c4dca3eaafcf163fcbd646b4", binarySha256: "7c638c8467efc7036e0ec6b8a1d9080a5e9a64f7ae6cbc6074fce577f789b7fe" },
|
|
46
|
+
"linux/arm64": { name: "gentle-ai_4.1.1_linux_arm64.tar.gz", sha256: "5f8fa38573eea0f2110902f0996aa0b008aac58dc0f9a6abbdf301dcbc93f4eb", binarySha256: "76a36a73d6905a16221496ffe9a193bd79e17951463d711cd03abf370349ef55" },
|
|
32
47
|
} as const;
|
|
33
48
|
|
|
34
|
-
test("published Gentle AI v4.
|
|
35
|
-
assert.equal(INSTALLER_VERSION, "4.
|
|
49
|
+
test("published Gentle AI v4.1.1 is the installer pin", () => {
|
|
50
|
+
assert.equal(INSTALLER_VERSION, "4.1.1");
|
|
36
51
|
// v4.0.0 moved the Go module path to the /v4 major-version suffix.
|
|
37
52
|
assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE, "github.com/gentleman-programming/gentle-ai/v4");
|
|
38
|
-
assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM, "h1
|
|
53
|
+
assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM, "h1:+WmZ2IvIOiJ8qtHa7wsjnYKNasem+PByeOZ6HIjoAL8=");
|
|
39
54
|
});
|
|
40
55
|
|
|
41
56
|
test("default installer package root is the package containing scripts, not its parent", () => {
|
|
@@ -46,15 +61,15 @@ test("default installer package root is the package containing scripts, not its
|
|
|
46
61
|
assert.notEqual(resolveGentleAiInstallerPackageRoot(), dirname(expectedPackageRoot));
|
|
47
62
|
});
|
|
48
63
|
|
|
49
|
-
test("release mapping selects only the supported official v4.
|
|
64
|
+
test("release mapping selects only the supported official v4.1.1 assets and pinned digests", () => {
|
|
50
65
|
assert.deepEqual(
|
|
51
|
-
Object.fromEntries(Object.entries(
|
|
66
|
+
Object.fromEntries(Object.entries(releaseAssets()).map(([key, asset]) => [key, { name: asset.name, sha256: asset.sha256, binarySha256: asset.binarySha256 }])),
|
|
52
67
|
EXPECTED_ASSETS,
|
|
53
68
|
);
|
|
54
|
-
assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_4.
|
|
55
|
-
assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_4.
|
|
56
|
-
for (const asset of Object.values(
|
|
57
|
-
assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v4\.
|
|
69
|
+
assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_4.1.1_linux_amd64.tar.gz");
|
|
70
|
+
assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_4.1.1_darwin_arm64.tar.gz");
|
|
71
|
+
for (const asset of Object.values(releaseAssets())) {
|
|
72
|
+
assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v4\.1\.1\//);
|
|
58
73
|
}
|
|
59
74
|
});
|
|
60
75
|
|
|
@@ -64,18 +79,18 @@ test("raw release assets are admitted only under a prerelease pin", () => {
|
|
|
64
79
|
assert.equal(gentleAiAssetForm("gentle-ai_2.5.0-rc.3_windows_amd64.exe", "2.5.0-rc.3"), "raw-binary");
|
|
65
80
|
// A raw binary under a stable pin means the pin itself is wrong: stable
|
|
66
81
|
// releases publish signed archives only, so this fails closed pre-download.
|
|
67
|
-
assert.throws(() => gentleAiAssetForm("gentle-ai_4.
|
|
82
|
+
assert.throws(() => gentleAiAssetForm("gentle-ai_4.1.1_linux_amd64", "4.1.1"), /only admitted for a prerelease pin/);
|
|
68
83
|
assert.throws(() => gentleAiAssetForm("gentle-ai.dmg", "2.5.0-rc.3"), /unsupported Gentle AI release asset form/);
|
|
69
84
|
// The current stable pin admits every pinned asset row through the same
|
|
70
85
|
// gate the installer uses at download time (default installerVersion
|
|
71
86
|
// argument): signed archives, never raw binaries.
|
|
72
|
-
for (const asset of Object.values(
|
|
87
|
+
for (const asset of Object.values(releaseAssets())) {
|
|
73
88
|
assert.equal(gentleAiAssetForm(asset.name), "archive");
|
|
74
89
|
}
|
|
75
90
|
});
|
|
76
91
|
|
|
77
92
|
test("release digests are all-or-none and install fails closed while any digest is pending", async () => {
|
|
78
|
-
const digests = Object.values(
|
|
93
|
+
const digests = Object.values(releaseAssets()).flatMap((asset) => [asset.sha256, asset.binarySha256]);
|
|
79
94
|
const pinned = digests.filter((digest) => /^[0-9a-f]{64}$/.test(digest));
|
|
80
95
|
const pending = digests.filter((digest) => digest === GENTLE_AI_PENDING_DIGEST);
|
|
81
96
|
assert.equal(pinned.length + pending.length, digests.length, "every digest must be pinned hex or the explicit pending sentinel");
|
|
@@ -91,7 +106,7 @@ test("release digests are all-or-none and install fails closed while any digest
|
|
|
91
106
|
}),
|
|
92
107
|
/checksum mismatch/,
|
|
93
108
|
);
|
|
94
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.
|
|
109
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai")), false);
|
|
95
110
|
}
|
|
96
111
|
});
|
|
97
112
|
|
|
@@ -99,6 +114,7 @@ interface WindowsGoCall {
|
|
|
99
114
|
file: string;
|
|
100
115
|
arguments_: string[];
|
|
101
116
|
options: {
|
|
117
|
+
cwd?: string;
|
|
102
118
|
env?: NodeJS.ProcessEnv;
|
|
103
119
|
shell?: boolean;
|
|
104
120
|
timeout?: number;
|
|
@@ -120,7 +136,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
|
|
|
120
136
|
const metadata = [
|
|
121
137
|
"gentle-ai.exe: go1.25.10",
|
|
122
138
|
"\tpath\tgithub.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
|
|
123
|
-
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.
|
|
139
|
+
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.1.1\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
|
|
124
140
|
"\tbuild\t-buildmode=exe", "\tbuild\t-compiler=gc", "\tbuild\tCGO_ENABLED=0", `\tbuild\tGOARCH=${fixtureOptions.goArchitecture ?? "amd64"}`, "\tbuild\tGOOS=windows",
|
|
125
141
|
].join("\n");
|
|
126
142
|
const run = async (file: string, arguments_: string[], options: WindowsGoCall["options"]) => {
|
|
@@ -138,7 +154,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
|
|
|
138
154
|
return { stdout: "", stderr: "" };
|
|
139
155
|
}
|
|
140
156
|
if (file === goExecutable && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
|
|
141
|
-
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 4.
|
|
157
|
+
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 4.1.1\n", stderr: "" };
|
|
142
158
|
throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
|
|
143
159
|
};
|
|
144
160
|
return { calls, run, setGoExecutable: (path: string) => { goExecutable = path; } };
|
|
@@ -154,7 +170,7 @@ test("win32 x64 and arm64 install the exact Go SumDB source tag without archive
|
|
|
154
170
|
const result = await installGentleAi({ packageRoot, platform: "win32", arch, execFile: fixture.run, resolveGoExecutable: async () => goPath });
|
|
155
171
|
assert.equal(result.installed, true);
|
|
156
172
|
assert.deepEqual(fixture.calls.filter((call) => call.file === goPath).map((call) => call.arguments_.slice(0, 2)), [
|
|
157
|
-
["version"], ["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.
|
|
173
|
+
["version"], ["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.1.1"], ["version", "-m"],
|
|
158
174
|
]);
|
|
159
175
|
}
|
|
160
176
|
});
|
|
@@ -173,11 +189,60 @@ test("Windows source install reports missing or too-old Go without publishing a
|
|
|
173
189
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
|
|
174
190
|
(error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
|
|
175
191
|
);
|
|
176
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.
|
|
192
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai.exe")), false);
|
|
177
193
|
assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.includes("install-")), []);
|
|
178
194
|
}
|
|
179
195
|
});
|
|
180
196
|
|
|
197
|
+
test("a failed Windows go install keeps the go command's error, with its stdout and stderr, as the cause", async () => {
|
|
198
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-cause-"));
|
|
199
|
+
try {
|
|
200
|
+
const installError = Object.assign(new Error("Command failed: go.exe install\ngo: verifying module: checksum mismatch"),
|
|
201
|
+
{ code: 1, stdout: "", stderr: "go: verifying module: checksum mismatch\n" });
|
|
202
|
+
const fixture = windowsGoFixture({ installError });
|
|
203
|
+
const goPath = join(packageRoot, "go.exe");
|
|
204
|
+
await writeFile(goPath, "trusted local Go executable");
|
|
205
|
+
fixture.setGoExecutable(goPath);
|
|
206
|
+
await assert.rejects(
|
|
207
|
+
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
|
|
208
|
+
(error: unknown) => error instanceof Error && error.cause === installError && (error.cause as { stderr: string }).stderr.includes("checksum mismatch"),
|
|
209
|
+
);
|
|
210
|
+
} finally { await rm(packageRoot, { recursive: true, force: true }); }
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Run 38063142924: from a 179-character pnpm store package root, Go's asm.exe failed with
|
|
214
|
+
// "The directory name is invalid." because its working directory, inside GOMODCACHE, exceeded MAX_PATH.
|
|
215
|
+
test("Windows source install builds in a short private directory under the temp root, never inside the package, and removes it", async () => {
|
|
216
|
+
for (const installError of [undefined, Object.assign(new Error("go install failed"), { code: 1, stderr: "go: failed\n" })]) {
|
|
217
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-short-build-"));
|
|
218
|
+
const temporaryDirectory = await mkdtemp(join(tmpdir(), "gentle-pi-installer-temp-"));
|
|
219
|
+
try {
|
|
220
|
+
const fixture = windowsGoFixture({ installError });
|
|
221
|
+
const goPath = join(packageRoot, "go.exe");
|
|
222
|
+
await writeFile(goPath, "trusted local Go executable");
|
|
223
|
+
fixture.setGoExecutable(goPath);
|
|
224
|
+
const install = installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath, temporaryDirectory });
|
|
225
|
+
if (installError) await assert.rejects(install, (error: unknown) => error instanceof Error && "code" in error && error.code === "GENTLE_AI_GO_INSTALL_FAILED");
|
|
226
|
+
else assert.equal((await install).installed, true);
|
|
227
|
+
const goInstall = fixture.calls.find((call) => call.arguments_[0] === "install");
|
|
228
|
+
assert.ok(goInstall);
|
|
229
|
+
const buildDirectory = goInstall.options.cwd ?? "";
|
|
230
|
+
assert.match(buildDirectory.slice(temporaryDirectory.length), /^[\\/]gai-[^\\/]+$/, "a fresh gai- directory directly under the temp root");
|
|
231
|
+
const env = goInstall.options.env ?? {};
|
|
232
|
+
for (const key of ["GOBIN", "GOPATH", "GOMODCACHE", "GOCACHE", "TEMP", "TMP"]) {
|
|
233
|
+
assert.ok(env[key]?.startsWith(buildDirectory), `${key} is under the build directory`);
|
|
234
|
+
assert.equal(env[key]?.startsWith(packageRoot), false, `${key} is outside the package`);
|
|
235
|
+
}
|
|
236
|
+
for (const call of fixture.calls.filter((entry) => entry.file === goPath)) assert.ok(call.options.cwd?.startsWith(buildDirectory), call.arguments_.join(" "));
|
|
237
|
+
assert.deepEqual(await readdir(temporaryDirectory), [], "the build directory is removed");
|
|
238
|
+
if (!installError) assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai.exe")), true, "the verified binary is published into the package");
|
|
239
|
+
} finally {
|
|
240
|
+
await rm(packageRoot, { recursive: true, force: true });
|
|
241
|
+
await rm(temporaryDirectory, { recursive: true, force: true });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
181
246
|
test("Windows source install cleans staging after Go failure or wrong built version", async () => {
|
|
182
247
|
for (const fixtureOptions of [
|
|
183
248
|
{ installError: Object.assign(new Error("go install timed out"), { code: "ETIMEDOUT" }), expectedCode: "GENTLE_AI_GO_INSTALL_FAILED" },
|
|
@@ -192,7 +257,7 @@ test("Windows source install cleans staging after Go failure or wrong built vers
|
|
|
192
257
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
|
|
193
258
|
(error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
|
|
194
259
|
);
|
|
195
|
-
const runtimeDirectory = join(packageRoot, ".gentle-ai", "v4.
|
|
260
|
+
const runtimeDirectory = join(packageRoot, ".gentle-ai", "v4.1.1");
|
|
196
261
|
assert.ok(fixture.calls.some((call) => call.arguments_[0] === "install"));
|
|
197
262
|
assert.equal(existsSync(join(runtimeDirectory, "gentle-ai.exe")), false);
|
|
198
263
|
assert.equal(existsSync(runtimeDirectory) && (await readdir(runtimeDirectory)).some((entry) => entry.startsWith(".go-install-") || entry.endsWith(".tmp")), false);
|
|
@@ -213,7 +278,7 @@ test("Windows source installs reuse only a fully verified package-local binary",
|
|
|
213
278
|
assert.equal(reused.installed, false);
|
|
214
279
|
assert.ok(reuse.calls.every((call) => call.file !== "gentle-ai"), "the installer must never fall back to ambient gentle-ai on PATH");
|
|
215
280
|
|
|
216
|
-
await writeFile(join(packageRoot, ".gentle-ai", "v4.
|
|
281
|
+
await writeFile(join(packageRoot, ".gentle-ai", "v4.1.1", "integrity.json"), "{}\n");
|
|
217
282
|
const repaired = windowsGoFixture();
|
|
218
283
|
repaired.setGoExecutable(goPath);
|
|
219
284
|
assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: repaired.run, resolveGoExecutable: async () => goPath })).installed, true);
|
|
@@ -255,7 +320,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
|
|
|
255
320
|
const metadata = fixtureOptions.metadataOverride ?? [
|
|
256
321
|
"gentle-ai.exe: go1.25.10",
|
|
257
322
|
"\tpath\tgithub.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
|
|
258
|
-
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.
|
|
323
|
+
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.1.1\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
|
|
259
324
|
"\tbuild\t-buildmode=exe",
|
|
260
325
|
"\tbuild\t-compiler=gc",
|
|
261
326
|
"\tbuild\tCGO_ENABLED=0",
|
|
@@ -275,7 +340,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
|
|
|
275
340
|
return { stdout: "", stderr: "" };
|
|
276
341
|
}
|
|
277
342
|
if (file === goPath && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
|
|
278
|
-
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 4.
|
|
343
|
+
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 4.1.1\n", stderr: "" };
|
|
279
344
|
throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
|
|
280
345
|
};
|
|
281
346
|
return {
|
|
@@ -317,7 +382,7 @@ test("Windows source installation resolves one validated Go executable and seals
|
|
|
317
382
|
const goCalls = fixture.calls.filter((call) => call.file === fixture.goPath);
|
|
318
383
|
assert.deepEqual(goCalls.map((call) => call.arguments_.slice(0, 2)), [
|
|
319
384
|
["version"],
|
|
320
|
-
["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.
|
|
385
|
+
["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.1.1"],
|
|
321
386
|
["version", "-m"],
|
|
322
387
|
]);
|
|
323
388
|
for (const call of goCalls) {
|
|
@@ -343,14 +408,14 @@ test("Windows source manifest binds verified Go metadata and architecture", asyn
|
|
|
343
408
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-provenance-"));
|
|
344
409
|
const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: "arm64" });
|
|
345
410
|
const result = await installGentleAi({ packageRoot, platform: "win32", arch: "arm64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
346
|
-
const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v4.
|
|
411
|
+
const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v4.1.1", "integrity.json"), "utf8")) as Record<string, string>;
|
|
347
412
|
assert.equal(result.installed, true);
|
|
348
413
|
assert.deepEqual(manifest, {
|
|
349
|
-
version: "4.
|
|
414
|
+
version: "4.1.1",
|
|
350
415
|
method: "go-sumdb-source-build",
|
|
351
416
|
package: "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
|
|
352
417
|
module: "github.com/gentleman-programming/gentle-ai/v4",
|
|
353
|
-
tag: "v4.
|
|
418
|
+
tag: "v4.1.1",
|
|
354
419
|
architecture: "arm64",
|
|
355
420
|
binarySha256: createHash("sha256").update("trusted Windows source build").digest("hex"),
|
|
356
421
|
moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|
|
@@ -370,12 +435,12 @@ test("Windows source installation rejects Go metadata for a different architectu
|
|
|
370
435
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
|
|
371
436
|
(error: unknown) => error instanceof Error && "code" in error && error.code === "GENTLE_AI_GO_INSTALL_FAILED",
|
|
372
437
|
);
|
|
373
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.
|
|
438
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai.exe")), false);
|
|
374
439
|
});
|
|
375
440
|
|
|
376
441
|
test("Windows source installation treats a fresh ownerless lock as active", async () => {
|
|
377
442
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-lock-"));
|
|
378
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".v4.
|
|
443
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock");
|
|
379
444
|
await mkdir(lockPath, { recursive: true });
|
|
380
445
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
381
446
|
await assertManualLockRecoveryRequired(packageRoot, fixture);
|
|
@@ -383,7 +448,7 @@ test("Windows source installation treats a fresh ownerless lock as active", asyn
|
|
|
383
448
|
|
|
384
449
|
test("Windows source installation preserves a lock whose owner nonce changed before release", async () => {
|
|
385
450
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owner-lock-"));
|
|
386
|
-
const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v4.
|
|
451
|
+
const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock", "owner.json");
|
|
387
452
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
388
453
|
let replacedOwner = false;
|
|
389
454
|
const run = async (...arguments_: Parameters<typeof fixture.run>) => {
|
|
@@ -400,7 +465,7 @@ test("Windows source installation preserves a lock whose owner nonce changed bef
|
|
|
400
465
|
});
|
|
401
466
|
|
|
402
467
|
async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Awaited<ReturnType<typeof hardenedWindowsGoFixture>>, options: Record<string, unknown> = {}) {
|
|
403
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".v4.
|
|
468
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock");
|
|
404
469
|
await assert.rejects(
|
|
405
470
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, ...options }),
|
|
406
471
|
(error: unknown) => error instanceof Error && error.message.includes(lockPath) && /confirm no installer is active.*remove.*manually/i.test(error.message),
|
|
@@ -410,18 +475,18 @@ async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Aw
|
|
|
410
475
|
}
|
|
411
476
|
|
|
412
477
|
function tombstonePath(packageRoot: string, nonce: string): string {
|
|
413
|
-
return join(packageRoot, ".gentle-ai", `.v4.
|
|
478
|
+
return join(packageRoot, ".gentle-ai", `.v4.1.1.install.tombstone-${nonce}`);
|
|
414
479
|
}
|
|
415
480
|
|
|
416
481
|
async function tombstones(packageRoot: string): Promise<string[]> {
|
|
417
482
|
const runtimeRoot = join(packageRoot, ".gentle-ai");
|
|
418
483
|
return existsSync(runtimeRoot)
|
|
419
|
-
? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v4.
|
|
484
|
+
? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v4.1.1.install.tombstone-"))
|
|
420
485
|
: [];
|
|
421
486
|
}
|
|
422
487
|
|
|
423
488
|
function backupBundlePath(packageRoot: string, nonce: string): string {
|
|
424
|
-
return join(packageRoot, ".gentle-ai", `.v4.
|
|
489
|
+
return join(packageRoot, ".gentle-ai", `.v4.1.1.backup-${nonce}`);
|
|
425
490
|
}
|
|
426
491
|
|
|
427
492
|
async function copyWindowsBundle(source: string, destination: string): Promise<void> {
|
|
@@ -432,7 +497,7 @@ async function copyWindowsBundle(source: string, destination: string): Promise<v
|
|
|
432
497
|
|
|
433
498
|
test("Windows source installation fails closed for an ownerless lock even after the stale threshold", async () => {
|
|
434
499
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-stale-lock-"));
|
|
435
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".v4.
|
|
500
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock");
|
|
436
501
|
await mkdir(lockPath, { recursive: true });
|
|
437
502
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
438
503
|
await assertManualLockRecoveryRequired(packageRoot, fixture, { now: () => Date.now() + 10 * 60_000 });
|
|
@@ -440,7 +505,7 @@ test("Windows source installation fails closed for an ownerless lock even after
|
|
|
440
505
|
|
|
441
506
|
test("Windows source installation fails closed for a stale owner lock", async () => {
|
|
442
507
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-stale-lock-"));
|
|
443
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".v4.
|
|
508
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock");
|
|
444
509
|
await mkdir(lockPath, { recursive: true });
|
|
445
510
|
await writeFile(join(lockPath, "owner.json"), `${JSON.stringify({ createdAt: 0, nonce: "0".repeat(64) })}\n`);
|
|
446
511
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -492,7 +557,7 @@ test("Windows source acquisition fails closed when a tombstone appears after its
|
|
|
492
557
|
(error: unknown) => error instanceof Error && error.message.includes(foreignTombstone),
|
|
493
558
|
);
|
|
494
559
|
assert.equal(existsSync(foreignTombstone), true);
|
|
495
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.
|
|
560
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock")), false);
|
|
496
561
|
assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false);
|
|
497
562
|
});
|
|
498
563
|
|
|
@@ -500,7 +565,7 @@ test("Windows source release deletes its matching tombstone and allows reuse", a
|
|
|
500
565
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owned-lock-release-"));
|
|
501
566
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
502
567
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
503
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.
|
|
568
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock")), false);
|
|
504
569
|
assert.deepEqual(await tombstones(packageRoot), []);
|
|
505
570
|
const reuse = await hardenedWindowsGoFixture(packageRoot);
|
|
506
571
|
assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false);
|
|
@@ -510,7 +575,7 @@ test("Windows source recovers a valid backup after a crash between publication r
|
|
|
510
575
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-crash-"));
|
|
511
576
|
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
512
577
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
513
|
-
const live = join(packageRoot, ".gentle-ai", "v4.
|
|
578
|
+
const live = join(packageRoot, ".gentle-ai", "v4.1.1");
|
|
514
579
|
const backup = backupBundlePath(packageRoot, "crash");
|
|
515
580
|
await rename(live, backup);
|
|
516
581
|
const recovery = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -546,7 +611,7 @@ test("Windows source cleans one validated backup only when the live bundle is va
|
|
|
546
611
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-valid-live-"));
|
|
547
612
|
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
548
613
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
549
|
-
const live = join(packageRoot, ".gentle-ai", "v4.
|
|
614
|
+
const live = join(packageRoot, ".gentle-ai", "v4.1.1");
|
|
550
615
|
const backup = backupBundlePath(packageRoot, "valid");
|
|
551
616
|
await copyWindowsBundle(live, backup);
|
|
552
617
|
const reuse = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -559,7 +624,7 @@ test("Windows source preserves a valid backup when the live bundle is invalid",
|
|
|
559
624
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-invalid-live-"));
|
|
560
625
|
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
561
626
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
562
|
-
const live = join(packageRoot, ".gentle-ai", "v4.
|
|
627
|
+
const live = join(packageRoot, ".gentle-ai", "v4.1.1");
|
|
563
628
|
const backup = backupBundlePath(packageRoot, "valid");
|
|
564
629
|
await copyWindowsBundle(live, backup);
|
|
565
630
|
await writeFile(join(live, "integrity.json"), "{}\n");
|
|
@@ -577,7 +642,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
|
|
|
577
642
|
const fixture = await hardenedWindowsGoFixture(packageRoot, { blockInstall: true });
|
|
578
643
|
const first = installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
579
644
|
await fixture.waitForInstall();
|
|
580
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".v4.
|
|
645
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock");
|
|
581
646
|
await assert.rejects(
|
|
582
647
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
|
|
583
648
|
(error: unknown) => error instanceof Error && error.message.includes(lockPath),
|
|
@@ -591,7 +656,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
|
|
|
591
656
|
|
|
592
657
|
test("Windows source publication rolls back a prior bundle when final directory swap fails", async () => {
|
|
593
658
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-rollback-"));
|
|
594
|
-
const versionDirectory = join(packageRoot, ".gentle-ai", "v4.
|
|
659
|
+
const versionDirectory = join(packageRoot, ".gentle-ai", "v4.1.1");
|
|
595
660
|
await mkdir(versionDirectory, { recursive: true });
|
|
596
661
|
await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
|
|
597
662
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -612,6 +677,405 @@ test("Windows source publication rolls back a prior bundle when final directory
|
|
|
612
677
|
assert.equal(await readFile(join(versionDirectory, "old.txt"), "utf8"), "previous bundle");
|
|
613
678
|
});
|
|
614
679
|
|
|
680
|
+
function codedError(code: string, message: string) {
|
|
681
|
+
return Object.assign(new Error(message), { code });
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function expectedWindowsSourceManifest(architecture: "x64" | "arm64") {
|
|
685
|
+
return {
|
|
686
|
+
version: INSTALLER_VERSION,
|
|
687
|
+
method: "go-sumdb-source-build",
|
|
688
|
+
package: GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH,
|
|
689
|
+
module: GENTLE_AI_WINDOWS_SOURCE_MODULE,
|
|
690
|
+
tag: GENTLE_AI_WINDOWS_SOURCE_TAG,
|
|
691
|
+
architecture,
|
|
692
|
+
binarySha256: createHash("sha256").update("trusted Windows source build").digest("hex"),
|
|
693
|
+
moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|
|
694
|
+
goVersion: "go1.25.10",
|
|
695
|
+
goos: "windows",
|
|
696
|
+
goarch: architecture === "x64" ? "amd64" : "arm64",
|
|
697
|
+
buildMode: "exe",
|
|
698
|
+
compiler: "gc",
|
|
699
|
+
cgoEnabled: "0",
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function publicationLeftovers(packageRoot: string) {
|
|
704
|
+
const runtimeRoot = join(packageRoot, ".gentle-ai");
|
|
705
|
+
return existsSync(runtimeRoot)
|
|
706
|
+
? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(`.v${INSTALLER_VERSION}.staging-`) || entry.startsWith(`.v${INSTALLER_VERSION}.backup-`) || entry.startsWith(`.v${INSTALLER_VERSION}.install.`))
|
|
707
|
+
: [];
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function waitForCount(getCount: () => number, expected: number, label: string) {
|
|
711
|
+
const deadline = Date.now() + 5_000;
|
|
712
|
+
while (Date.now() < deadline) {
|
|
713
|
+
if (getCount() >= expected) return;
|
|
714
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
715
|
+
}
|
|
716
|
+
throw new Error(`timed out waiting for ${label} (${getCount()} < ${expected})`);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async function settleWithMockedTimers<T>(t: { mock: { timers: { tick(milliseconds: number): void } } }, operation: Promise<T>): Promise<T> {
|
|
720
|
+
let done = false;
|
|
721
|
+
let value: T | undefined;
|
|
722
|
+
let failure: { error: unknown } | undefined;
|
|
723
|
+
operation.then(
|
|
724
|
+
(resolved) => { value = resolved; done = true; },
|
|
725
|
+
(error) => { failure = { error }; done = true; },
|
|
726
|
+
);
|
|
727
|
+
const deadline = Date.now() + 5_000;
|
|
728
|
+
while (!done && Date.now() < deadline) {
|
|
729
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
730
|
+
if (!done) t.mock.timers.tick(1_600);
|
|
731
|
+
}
|
|
732
|
+
if (!done) throw new Error("install did not settle under mocked publication timers");
|
|
733
|
+
if (failure) throw failure.error;
|
|
734
|
+
return value as T;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
test("Windows source publication retries a transient staging lock then reuses the published bundle", async () => {
|
|
738
|
+
for (const [arch, goArchitecture] of [["x64", "amd64"], ["arm64", "arm64"]] as const) {
|
|
739
|
+
const packageRoot = await mkdtemp(join(tmpdir(), `gentle-pi-installer-windows-retry-${arch}-`));
|
|
740
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: goArchitecture });
|
|
741
|
+
const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
|
|
742
|
+
let stagingAttempts = 0;
|
|
743
|
+
const result = await installGentleAi({
|
|
744
|
+
packageRoot,
|
|
745
|
+
platform: "win32",
|
|
746
|
+
arch,
|
|
747
|
+
execFile: fixture.run,
|
|
748
|
+
resolveGoExecutable: fixture.resolveGoExecutable,
|
|
749
|
+
rename: async (from: string, to: string) => {
|
|
750
|
+
if (from.includes(".staging-") && to === versionDirectory) {
|
|
751
|
+
stagingAttempts += 1;
|
|
752
|
+
if (stagingAttempts === 1) throw codedError("EPERM", "simulated staging lock");
|
|
753
|
+
}
|
|
754
|
+
await rename(from, to);
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
assert.equal(result.installed, true);
|
|
758
|
+
assert.equal(stagingAttempts, 2);
|
|
759
|
+
assert.equal(fixture.calls.filter((call) => call.file === fixture.goPath && call.arguments_[0] === "install").length, 1);
|
|
760
|
+
assert.equal(await readFile(join(versionDirectory, "gentle-ai.exe"), "utf8"), "trusted Windows source build");
|
|
761
|
+
assert.equal(await readFile(join(versionDirectory, "integrity.json"), "utf8"), `${JSON.stringify(expectedWindowsSourceManifest(arch))}\n`);
|
|
762
|
+
assert.deepEqual(await publicationLeftovers(packageRoot), []);
|
|
763
|
+
const reuse = await hardenedWindowsGoFixture(packageRoot, { architecture: goArchitecture });
|
|
764
|
+
assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch, execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false);
|
|
765
|
+
assert.equal(reuse.calls.some((call) => call.arguments_[0] === "install"), false);
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
test("Windows publication retries EPERM, EBUSY, and EACCES through five attempts with increasing delays", async (t) => {
|
|
770
|
+
t.mock.timers.enable({ apis: ["setTimeout"] });
|
|
771
|
+
const delays = [200, 400, 800, 1600];
|
|
772
|
+
for (const code of ["EPERM", "EBUSY", "EACCES"] as const) {
|
|
773
|
+
const packageRoot = await mkdtemp(join(tmpdir(), `gentle-pi-installer-windows-retry-budget-${code}-`));
|
|
774
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
775
|
+
const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
|
|
776
|
+
let stagingAttempts = 0;
|
|
777
|
+
const install = installGentleAi({
|
|
778
|
+
packageRoot,
|
|
779
|
+
platform: "win32",
|
|
780
|
+
arch: "x64",
|
|
781
|
+
execFile: fixture.run,
|
|
782
|
+
resolveGoExecutable: fixture.resolveGoExecutable,
|
|
783
|
+
rename: async (from: string, to: string) => {
|
|
784
|
+
if (from.includes(".staging-") && to === versionDirectory) {
|
|
785
|
+
stagingAttempts += 1;
|
|
786
|
+
if (stagingAttempts < 5) throw codedError(code, `simulated ${code} lock`);
|
|
787
|
+
}
|
|
788
|
+
await rename(from, to);
|
|
789
|
+
},
|
|
790
|
+
});
|
|
791
|
+
for (let attempt = 1; attempt <= 4; attempt += 1) {
|
|
792
|
+
await waitForCount(() => stagingAttempts, attempt, `${code} attempt ${attempt}`);
|
|
793
|
+
t.mock.timers.tick(delays[attempt - 1] - 1);
|
|
794
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
795
|
+
assert.equal(stagingAttempts, attempt, `${code} must not retry before ${delays[attempt - 1]}ms`);
|
|
796
|
+
t.mock.timers.tick(1);
|
|
797
|
+
}
|
|
798
|
+
await waitForCount(() => stagingAttempts, 5, `${code} fifth attempt`);
|
|
799
|
+
assert.equal((await install).installed, true, `${code} must succeed on the fifth attempt without a further delay`);
|
|
800
|
+
assert.equal(stagingAttempts, 5);
|
|
801
|
+
assert.equal(fixture.calls.filter((call) => call.file === fixture.goPath && call.arguments_[0] === "install").length, 1);
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
test("Windows source replacement retries transient backup and staging locks then removes the backup", async () => {
|
|
806
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-replace-retry-"));
|
|
807
|
+
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
808
|
+
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
809
|
+
const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
|
|
810
|
+
await writeFile(join(versionDirectory, "integrity.json"), "{}\n");
|
|
811
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
812
|
+
let backupAttempts = 0;
|
|
813
|
+
let stagingAttempts = 0;
|
|
814
|
+
const result = await installGentleAi({
|
|
815
|
+
packageRoot,
|
|
816
|
+
platform: "win32",
|
|
817
|
+
arch: "x64",
|
|
818
|
+
execFile: fixture.run,
|
|
819
|
+
resolveGoExecutable: fixture.resolveGoExecutable,
|
|
820
|
+
rename: async (from: string, to: string) => {
|
|
821
|
+
if (from === versionDirectory && to.includes(".backup-")) {
|
|
822
|
+
backupAttempts += 1;
|
|
823
|
+
if (backupAttempts === 1) throw codedError("EPERM", "simulated backup lock");
|
|
824
|
+
}
|
|
825
|
+
if (from.includes(".staging-") && to === versionDirectory) {
|
|
826
|
+
stagingAttempts += 1;
|
|
827
|
+
if (stagingAttempts === 1) throw codedError("EBUSY", "simulated staging lock");
|
|
828
|
+
}
|
|
829
|
+
await rename(from, to);
|
|
830
|
+
},
|
|
831
|
+
});
|
|
832
|
+
assert.equal(result.installed, true);
|
|
833
|
+
assert.equal(backupAttempts, 2);
|
|
834
|
+
assert.equal(stagingAttempts, 2);
|
|
835
|
+
assert.equal(await readFile(join(versionDirectory, "gentle-ai.exe"), "utf8"), "trusted Windows source build");
|
|
836
|
+
assert.equal(await readFile(join(versionDirectory, "integrity.json"), "utf8"), `${JSON.stringify(expectedWindowsSourceManifest("x64"))}\n`);
|
|
837
|
+
assert.equal((await readdir(join(packageRoot, ".gentle-ai"))).some((entry) => entry.startsWith(`.v${INSTALLER_VERSION}.backup-`)), false);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
test("Windows source publication restores the prior bundle after exhausting a staging lock, retrying a transient rollback", async (t) => {
|
|
841
|
+
t.mock.timers.enable({ apis: ["setTimeout"] });
|
|
842
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-permanent-lock-"));
|
|
843
|
+
const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
|
|
844
|
+
await mkdir(versionDirectory, { recursive: true });
|
|
845
|
+
await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
|
|
846
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
847
|
+
const stagingErrors: Error[] = [];
|
|
848
|
+
let rollbackAttempts = 0;
|
|
849
|
+
await assert.rejects(
|
|
850
|
+
settleWithMockedTimers(t, installGentleAi({
|
|
851
|
+
packageRoot,
|
|
852
|
+
platform: "win32",
|
|
853
|
+
arch: "x64",
|
|
854
|
+
execFile: fixture.run,
|
|
855
|
+
resolveGoExecutable: fixture.resolveGoExecutable,
|
|
856
|
+
rename: async (from: string, to: string) => {
|
|
857
|
+
if (from.includes(".staging-") && to === versionDirectory) {
|
|
858
|
+
const error = codedError("EPERM", `simulated staging lock ${stagingErrors.length + 1}`);
|
|
859
|
+
stagingErrors.push(error);
|
|
860
|
+
throw error;
|
|
861
|
+
}
|
|
862
|
+
if (from.includes(".backup-") && to === versionDirectory) {
|
|
863
|
+
rollbackAttempts += 1;
|
|
864
|
+
if (rollbackAttempts === 1) throw codedError("EBUSY", "simulated rollback lock");
|
|
865
|
+
}
|
|
866
|
+
await rename(from, to);
|
|
867
|
+
},
|
|
868
|
+
})),
|
|
869
|
+
(error: unknown) => error === stagingErrors[4],
|
|
870
|
+
);
|
|
871
|
+
assert.equal(stagingErrors.length, 5);
|
|
872
|
+
assert.equal(rollbackAttempts, 2);
|
|
873
|
+
assert.equal(await readFile(join(versionDirectory, "old.txt"), "utf8"), "previous bundle");
|
|
874
|
+
assert.equal((await readdir(join(packageRoot, ".gentle-ai"))).some((entry) => entry.startsWith(`.v${INSTALLER_VERSION}.backup-`)), false);
|
|
875
|
+
assert.equal(existsSync(join(versionDirectory, "gentle-ai.exe")), false);
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test("Windows source publication leaves the live bundle untouched when the backup move is exhausted", async (t) => {
|
|
879
|
+
t.mock.timers.enable({ apis: ["setTimeout"] });
|
|
880
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-backup-exhausted-"));
|
|
881
|
+
const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
|
|
882
|
+
await mkdir(versionDirectory, { recursive: true });
|
|
883
|
+
await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
|
|
884
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
885
|
+
const backupErrors: Error[] = [];
|
|
886
|
+
await assert.rejects(
|
|
887
|
+
settleWithMockedTimers(t, installGentleAi({
|
|
888
|
+
packageRoot,
|
|
889
|
+
platform: "win32",
|
|
890
|
+
arch: "x64",
|
|
891
|
+
execFile: fixture.run,
|
|
892
|
+
resolveGoExecutable: fixture.resolveGoExecutable,
|
|
893
|
+
rename: async (from: string, to: string) => {
|
|
894
|
+
if (from === versionDirectory && to.includes(".backup-")) {
|
|
895
|
+
const error = codedError("EPERM", `simulated backup lock ${backupErrors.length + 1}`);
|
|
896
|
+
backupErrors.push(error);
|
|
897
|
+
throw error;
|
|
898
|
+
}
|
|
899
|
+
await rename(from, to);
|
|
900
|
+
},
|
|
901
|
+
})),
|
|
902
|
+
(error: unknown) => error === backupErrors[4],
|
|
903
|
+
);
|
|
904
|
+
assert.equal(backupErrors.length, 5);
|
|
905
|
+
assert.equal(await readFile(join(versionDirectory, "old.txt"), "utf8"), "previous bundle");
|
|
906
|
+
assert.equal((await readdir(join(packageRoot, ".gentle-ai"))).some((entry) => entry.startsWith(`.v${INSTALLER_VERSION}.backup-`)), false);
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
test("Windows source publication retains the backup when rollback retries are exhausted", async (t) => {
|
|
910
|
+
t.mock.timers.enable({ apis: ["setTimeout"] });
|
|
911
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-rollback-exhausted-"));
|
|
912
|
+
const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
|
|
913
|
+
await mkdir(versionDirectory, { recursive: true });
|
|
914
|
+
await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
|
|
915
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
916
|
+
const rollbackErrors: Error[] = [];
|
|
917
|
+
await assert.rejects(
|
|
918
|
+
settleWithMockedTimers(t, installGentleAi({
|
|
919
|
+
packageRoot,
|
|
920
|
+
platform: "win32",
|
|
921
|
+
arch: "x64",
|
|
922
|
+
execFile: fixture.run,
|
|
923
|
+
resolveGoExecutable: fixture.resolveGoExecutable,
|
|
924
|
+
rename: async (from: string, to: string) => {
|
|
925
|
+
if (from.includes(".staging-") && to === versionDirectory) throw codedError("EPERM", "simulated staging lock");
|
|
926
|
+
if (from.includes(".backup-") && to === versionDirectory) {
|
|
927
|
+
const error = codedError("EACCES", `simulated rollback lock ${rollbackErrors.length + 1}`);
|
|
928
|
+
rollbackErrors.push(error);
|
|
929
|
+
throw error;
|
|
930
|
+
}
|
|
931
|
+
await rename(from, to);
|
|
932
|
+
},
|
|
933
|
+
})),
|
|
934
|
+
(error: unknown) => error instanceof Error
|
|
935
|
+
&& error.message === "Gentle AI bundle publication failed and rollback could not restore the prior bundle"
|
|
936
|
+
&& error.cause === rollbackErrors[4],
|
|
937
|
+
);
|
|
938
|
+
assert.equal(rollbackErrors.length, 5);
|
|
939
|
+
assert.equal(existsSync(versionDirectory), false);
|
|
940
|
+
const backups = (await readdir(join(packageRoot, ".gentle-ai"))).filter((entry) => entry.startsWith(`.v${INSTALLER_VERSION}.backup-`));
|
|
941
|
+
assert.equal(backups.length, 1);
|
|
942
|
+
assert.equal(await readFile(join(packageRoot, ".gentle-ai", backups[0], "old.txt"), "utf8"), "previous bundle");
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
test("Windows source restores a valid backup through a transient lock on the next install", async () => {
|
|
946
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-restore-retry-"));
|
|
947
|
+
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
948
|
+
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
949
|
+
const live = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
|
|
950
|
+
const backup = backupBundlePath(packageRoot, "restore");
|
|
951
|
+
await rename(live, backup);
|
|
952
|
+
const recovery = await hardenedWindowsGoFixture(packageRoot);
|
|
953
|
+
let restoreAttempts = 0;
|
|
954
|
+
let buildAttempted = false;
|
|
955
|
+
const run = async (...arguments_: Parameters<typeof recovery.run>) => {
|
|
956
|
+
if (arguments_[1][0] === "install") { buildAttempted = true; throw new Error("recovery must precede a new build"); }
|
|
957
|
+
return recovery.run(...arguments_);
|
|
958
|
+
};
|
|
959
|
+
const restored = await installGentleAi({
|
|
960
|
+
packageRoot,
|
|
961
|
+
platform: "win32",
|
|
962
|
+
arch: "x64",
|
|
963
|
+
execFile: run,
|
|
964
|
+
resolveGoExecutable: recovery.resolveGoExecutable,
|
|
965
|
+
rename: async (from: string, to: string) => {
|
|
966
|
+
if (from.includes(".backup-") && to === live) {
|
|
967
|
+
restoreAttempts += 1;
|
|
968
|
+
if (restoreAttempts === 1) throw codedError("EPERM", "simulated restore lock");
|
|
969
|
+
}
|
|
970
|
+
await rename(from, to);
|
|
971
|
+
},
|
|
972
|
+
});
|
|
973
|
+
assert.equal(restored.installed, false);
|
|
974
|
+
assert.equal(restoreAttempts, 2);
|
|
975
|
+
assert.equal(existsSync(live), true);
|
|
976
|
+
assert.equal(existsSync(backup), false);
|
|
977
|
+
assert.equal(buildAttempted, false);
|
|
978
|
+
assert.equal(await readFile(join(live, "gentle-ai.exe"), "utf8"), "trusted Windows source build");
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
test("Windows publication does not retry ENOSPC or uncoded rename failures", async () => {
|
|
982
|
+
for (const failure of [codedError("ENOSPC", "simulated disk full"), new Error("simulated uncoded failure")]) {
|
|
983
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-nonretryable-"));
|
|
984
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
985
|
+
const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
|
|
986
|
+
let stagingAttempts = 0;
|
|
987
|
+
await assert.rejects(
|
|
988
|
+
() => installGentleAi({
|
|
989
|
+
packageRoot,
|
|
990
|
+
platform: "win32",
|
|
991
|
+
arch: "x64",
|
|
992
|
+
execFile: fixture.run,
|
|
993
|
+
resolveGoExecutable: fixture.resolveGoExecutable,
|
|
994
|
+
rename: async (from: string, to: string) => {
|
|
995
|
+
if (from.includes(".staging-") && to === versionDirectory) {
|
|
996
|
+
stagingAttempts += 1;
|
|
997
|
+
throw failure;
|
|
998
|
+
}
|
|
999
|
+
await rename(from, to);
|
|
1000
|
+
},
|
|
1001
|
+
}),
|
|
1002
|
+
(error: unknown) => error === failure,
|
|
1003
|
+
);
|
|
1004
|
+
assert.equal(stagingAttempts, 1);
|
|
1005
|
+
assert.equal(existsSync(join(versionDirectory, "gentle-ai.exe")), false);
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
test("Darwin/Linux signed publication fails on EPERM after one attempt", async () => {
|
|
1010
|
+
const payload = Buffer.from("signed archive fixture");
|
|
1011
|
+
const binary = "signed binary";
|
|
1012
|
+
for (const [platform, key] of [["darwin", "darwin/amd64"], ["linux", "linux/amd64"]] as const) {
|
|
1013
|
+
const packageRoot = await mkdtemp(join(tmpdir(), `gentle-pi-installer-signed-noretry-${platform}-`));
|
|
1014
|
+
const asset = {
|
|
1015
|
+
name: `gentle-ai_${INSTALLER_VERSION}_${platform}_amd64.tar.gz`,
|
|
1016
|
+
sha256: createHash("sha256").update(payload).digest("hex"),
|
|
1017
|
+
binarySha256: createHash("sha256").update(binary).digest("hex"),
|
|
1018
|
+
url: "https://example.invalid/gentle-ai.tar.gz",
|
|
1019
|
+
executable: "gentle-ai",
|
|
1020
|
+
};
|
|
1021
|
+
const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
|
|
1022
|
+
let stagingAttempts = 0;
|
|
1023
|
+
await assert.rejects(
|
|
1024
|
+
() => installGentleAi({
|
|
1025
|
+
packageRoot,
|
|
1026
|
+
platform,
|
|
1027
|
+
arch: "x64",
|
|
1028
|
+
releaseAssets: { [key]: asset },
|
|
1029
|
+
download: async (_url: string, destination: string) => writeFile(destination, payload),
|
|
1030
|
+
extractArchive: async (_archive: string, destination: string) => {
|
|
1031
|
+
await mkdir(destination, { recursive: true });
|
|
1032
|
+
await writeFile(join(destination, "gentle-ai"), binary);
|
|
1033
|
+
},
|
|
1034
|
+
rename: async (from: string, to: string) => {
|
|
1035
|
+
if (from.includes(".staging-") && to === versionDirectory) {
|
|
1036
|
+
stagingAttempts += 1;
|
|
1037
|
+
throw codedError("EPERM", "simulated staging lock");
|
|
1038
|
+
}
|
|
1039
|
+
await rename(from, to);
|
|
1040
|
+
},
|
|
1041
|
+
}),
|
|
1042
|
+
(error: unknown) => error instanceof Error && (error as NodeJS.ErrnoException).code === "EPERM",
|
|
1043
|
+
);
|
|
1044
|
+
assert.equal(stagingAttempts, 1, platform);
|
|
1045
|
+
assert.equal(existsSync(join(versionDirectory, "gentle-ai")), false);
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
test("Windows source install with a persistent publication lock cleans staging without pretending a runtime exists", async (t) => {
|
|
1050
|
+
t.mock.timers.enable({ apis: ["setTimeout"] });
|
|
1051
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-persistent-lock-"));
|
|
1052
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
1053
|
+
const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
|
|
1054
|
+
const stagingErrors: Error[] = [];
|
|
1055
|
+
await assert.rejects(
|
|
1056
|
+
settleWithMockedTimers(t, installGentleAi({
|
|
1057
|
+
packageRoot,
|
|
1058
|
+
platform: "win32",
|
|
1059
|
+
arch: "x64",
|
|
1060
|
+
execFile: fixture.run,
|
|
1061
|
+
resolveGoExecutable: fixture.resolveGoExecutable,
|
|
1062
|
+
rename: async (from: string, to: string) => {
|
|
1063
|
+
if (from.includes(".staging-") && to === versionDirectory) {
|
|
1064
|
+
const error = codedError("EPERM", `simulated staging lock ${stagingErrors.length + 1}`);
|
|
1065
|
+
stagingErrors.push(error);
|
|
1066
|
+
throw error;
|
|
1067
|
+
}
|
|
1068
|
+
await rename(from, to);
|
|
1069
|
+
},
|
|
1070
|
+
})),
|
|
1071
|
+
(error: unknown) => error === stagingErrors[4],
|
|
1072
|
+
);
|
|
1073
|
+
assert.equal(stagingErrors.length, 5);
|
|
1074
|
+
assert.equal(existsSync(join(versionDirectory, "gentle-ai.exe")), false);
|
|
1075
|
+
assert.equal(existsSync(versionDirectory), false);
|
|
1076
|
+
assert.deepEqual(await publicationLeftovers(packageRoot), []);
|
|
1077
|
+
});
|
|
1078
|
+
|
|
615
1079
|
test("Darwin/Linux signed bundles retain their four-field manifest and reusable compatibility", async () => {
|
|
616
1080
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-signed-compatibility-"));
|
|
617
1081
|
const payload = Buffer.from("signed archive fixture");
|
|
@@ -628,7 +1092,7 @@ test("Darwin/Linux signed bundles retain their four-field manifest and reusable
|
|
|
628
1092
|
},
|
|
629
1093
|
};
|
|
630
1094
|
await installGentleAi(options);
|
|
631
|
-
const manifestPath = join(packageRoot, ".gentle-ai", "v4.
|
|
1095
|
+
const manifestPath = join(packageRoot, ".gentle-ai", "v4.1.1", "integrity.json");
|
|
632
1096
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, string>;
|
|
633
1097
|
assert.deepEqual(Object.keys(manifest), ["version", "asset", "assetSha256", "binarySha256"]);
|
|
634
1098
|
assert.equal((await installGentleAi({ ...options, download: async () => { throw new Error("signed bundle must be reused"); } })).installed, false);
|
|
@@ -641,7 +1105,7 @@ test("a pinned asset falls back to its gentle-pi mirror only on download failure
|
|
|
641
1105
|
// under the current stable pin, whose form gate refuses raw binaries.
|
|
642
1106
|
const payload = Buffer.from("signed archive fixture");
|
|
643
1107
|
const binary = "mirrored binary";
|
|
644
|
-
const baseAsset = { name: "gentle-ai_4.
|
|
1108
|
+
const baseAsset = { name: "gentle-ai_4.1.1_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), binarySha256: createHash("sha256").update(binary).digest("hex"), url: "https://example.invalid/upstream", mirrorUrl: "https://example.invalid/mirror", executable: "gentle-ai" };
|
|
645
1109
|
const extractArchive = async (_archive: string, destination: string) => {
|
|
646
1110
|
await mkdir(destination, { recursive: true });
|
|
647
1111
|
await writeFile(join(destination, "gentle-ai"), binary);
|
|
@@ -724,21 +1188,47 @@ function pendingRequest() {
|
|
|
724
1188
|
pending.setTimeout = () => undefined;
|
|
725
1189
|
return pending;
|
|
726
1190
|
}
|
|
727
|
-
test("download bounds stalled headers and bodies with transient retry exhaustion", async () => {
|
|
1191
|
+
test("download bounds stalled headers and bodies with transient retry exhaustion", async (t) => {
|
|
728
1192
|
for (const [stage, request] of [
|
|
729
|
-
["headers", () => pendingRequest()],
|
|
1193
|
+
["headers", (_url: URL, _options: unknown, _callback: (response: PassThrough & { statusCode?: number; headers: Record<string, string> }) => void) => pendingRequest()],
|
|
730
1194
|
["body", (_url: URL, _options: unknown, callback: (response: PassThrough & { statusCode?: number; headers: Record<string, string> }) => void) => {
|
|
731
1195
|
const response = Object.assign(new PassThrough(), { statusCode: 200, headers: {} });
|
|
732
1196
|
queueMicrotask(() => callback(response));
|
|
733
1197
|
return pendingRequest();
|
|
734
1198
|
}],
|
|
735
1199
|
] as const) {
|
|
1200
|
+
// A private directory per run: the PID-derived path this test used before lived
|
|
1201
|
+
// in the shared tmpdir and collided with leftovers from earlier runs, which is
|
|
1202
|
+
// how an EEXIST from the exclusive open reached the assertion instead of the
|
|
1203
|
+
// timeout under test.
|
|
1204
|
+
const directory = await mkdtemp(join(tmpdir(), "gentle-pi-stalled-"));
|
|
1205
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
1206
|
+
const destination = join(directory, stage);
|
|
736
1207
|
let attempts = 0;
|
|
737
|
-
await assert.rejects(() => downloadGentleAiAsset("https://example.invalid/archive",
|
|
1208
|
+
await assert.rejects(() => downloadGentleAiAsset("https://example.invalid/archive", destination, 1024, 0, { request: (url: URL, options: unknown, callback: (response: PassThrough & { statusCode?: number; headers: Record<string, string> }) => void) => { attempts += 1; return request(url, options, callback); }, headerTimeoutMs: 1, bodyTimeoutMs: 1, maxAttempts: 2, retryDelayMs: 0 }), new RegExp(`download ${stage} timed out`));
|
|
738
1209
|
assert.equal(attempts, 2);
|
|
1210
|
+
// A failed attempt settles only once its destination stream closed, so the retry
|
|
1211
|
+
// never races a late recreation of the path, and nothing partial survives.
|
|
1212
|
+
assert.equal(existsSync(destination), false, `a failed ${stage} download leaves no partial file behind`);
|
|
739
1213
|
}
|
|
740
1214
|
});
|
|
741
1215
|
|
|
1216
|
+
test("download never removes a destination it did not create", async (t) => {
|
|
1217
|
+
const directory = await mkdtemp(join(tmpdir(), "gentle-pi-existing-"));
|
|
1218
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
1219
|
+
const destination = join(directory, "archive");
|
|
1220
|
+
await writeFile(destination, "a file this download never owned");
|
|
1221
|
+
// A single attempt on purpose: the retry path removes the destination on its own,
|
|
1222
|
+
// so the ownership rule is only observable without a retry.
|
|
1223
|
+
const request = (_url: URL, _options: unknown, callback: (response: PassThrough & { statusCode?: number; headers: Record<string, string> }) => void) => {
|
|
1224
|
+
const response = Object.assign(new PassThrough(), { statusCode: 200, headers: {} });
|
|
1225
|
+
queueMicrotask(() => callback(response));
|
|
1226
|
+
return pendingRequest();
|
|
1227
|
+
};
|
|
1228
|
+
await assert.rejects(() => downloadGentleAiAsset("https://example.invalid/archive", destination, 1024, 0, { request, headerTimeoutMs: 1, bodyTimeoutMs: 1, maxAttempts: 1, retryDelayMs: 0 }));
|
|
1229
|
+
assert.equal(await readFile(destination, "utf8"), "a file this download never owned");
|
|
1230
|
+
});
|
|
1231
|
+
|
|
742
1232
|
test("download retries only transient HTTP statuses and exhausts within the attempt bound", async () => {
|
|
743
1233
|
for (const [status, expectedAttempts] of [[429, 2], [500, 2], [502, 2], [503, 2], [504, 2], [400, 1], [404, 1]] as const) {
|
|
744
1234
|
let attempts = 0;
|
|
@@ -759,7 +1249,7 @@ test("checksum mismatch cleans temporary state without promoting a binary", asyn
|
|
|
759
1249
|
}),
|
|
760
1250
|
/checksum mismatch/,
|
|
761
1251
|
);
|
|
762
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.
|
|
1252
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai")), false);
|
|
763
1253
|
assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.startsWith(".gentle-ai-install-")), []);
|
|
764
1254
|
});
|
|
765
1255
|
|
|
@@ -781,7 +1271,7 @@ test("installer promotes only the expected regular executable with executable PO
|
|
|
781
1271
|
await chmod(extracted, 0o700);
|
|
782
1272
|
},
|
|
783
1273
|
});
|
|
784
|
-
const binary = join(packageRoot, ".gentle-ai", "v4.
|
|
1274
|
+
const binary = join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai");
|
|
785
1275
|
assert.equal(existsSync(binary), true);
|
|
786
1276
|
assert.equal(await readFile(binary, "utf8"), "native executable");
|
|
787
1277
|
assert.ok(((await stat(binary)).mode & 0o111) !== 0);
|
|
@@ -812,7 +1302,7 @@ test("installer rejects an extracted binary that differs from its pinned digest"
|
|
|
812
1302
|
}),
|
|
813
1303
|
/binary checksum mismatch/,
|
|
814
1304
|
);
|
|
815
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.
|
|
1305
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai")), false);
|
|
816
1306
|
});
|
|
817
1307
|
|
|
818
1308
|
test("installer repairs a valid non-executable POSIX binary instead of reusing it", async (t) => {
|
|
@@ -835,7 +1325,7 @@ test("installer repairs a valid non-executable POSIX binary instead of reusing i
|
|
|
835
1325
|
},
|
|
836
1326
|
};
|
|
837
1327
|
await installGentleAi(options);
|
|
838
|
-
const binary = join(packageRoot, ".gentle-ai", "v4.
|
|
1328
|
+
const binary = join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai");
|
|
839
1329
|
await chmod(binary, 0o600);
|
|
840
1330
|
const repaired = await installGentleAi(options);
|
|
841
1331
|
assert.equal(repaired.installed, true);
|
|
@@ -872,7 +1362,7 @@ test("installer rejects archives with multiple expected executable entries", asy
|
|
|
872
1362
|
}),
|
|
873
1363
|
/exactly one regular gentle-ai/,
|
|
874
1364
|
);
|
|
875
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.
|
|
1365
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai")), false);
|
|
876
1366
|
});
|
|
877
1367
|
|
|
878
1368
|
test("installer rejects an archive without the expected regular executable", async () => {
|
|
@@ -890,7 +1380,7 @@ test("installer rejects an archive without the expected regular executable", asy
|
|
|
890
1380
|
}),
|
|
891
1381
|
/non-regular gentle-ai/,
|
|
892
1382
|
);
|
|
893
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.
|
|
1383
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai")), false);
|
|
894
1384
|
});
|
|
895
1385
|
|
|
896
1386
|
test("safeRemoveDirectory passes Windows-safe retry options to recursive rm cleanup", async () => {
|