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,194 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { mkdtempSync, readFileSync, statSync, writeFileSync, rmSync, readdirSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import fsPromises from "node:fs/promises";
|
|
6
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { registerYoloSessionPolicy, discoverYoloUiAdapter } from "../lib/yolo-session-policy.ts";
|
|
10
|
+
import { parsePermissionModePolicyFile, resolvePermissionModePolicy, writePermissionModePolicy, permissionModePolicyFile, PERMISSION_MODE_SCHEMA } from "../lib/permission-mode-policy.ts";
|
|
11
|
+
|
|
12
|
+
function fixture(t: test.TestContext) {
|
|
13
|
+
const home = mkdtempSync(join(tmpdir(), "gentle-mode-"));
|
|
14
|
+
t.after(() => rmSync(home, { recursive: true, force: true }));
|
|
15
|
+
return { gentlePiConfigHome: home };
|
|
16
|
+
}
|
|
17
|
+
function host(home: string, env: NodeJS.ProcessEnv = {}, onChange?: () => void) {
|
|
18
|
+
const commands = new Map<string, { handler(args: string, context: ExtensionContext): Promise<void> }>();
|
|
19
|
+
const listeners = new Map<string, (value: unknown) => void>();
|
|
20
|
+
const notices: string[] = [];
|
|
21
|
+
const statuses = new Map<string, string | undefined>();
|
|
22
|
+
let sessionId = "first";
|
|
23
|
+
const context = { cwd: home, mode: "tui", hasUI: true,
|
|
24
|
+
sessionManager: { getSessionId: () => sessionId },
|
|
25
|
+
ui: { notify: (s: string) => notices.push(s), setStatus: (k: string, s?: string) => statuses.set(k, s), setWidget() {} },
|
|
26
|
+
} as unknown as ExtensionContext;
|
|
27
|
+
const pi = { registerCommand: (n: string, c: { handler(args: string, context: ExtensionContext): Promise<void> }) => commands.set(n, c),
|
|
28
|
+
events: { on: (n: string, fn: (value: unknown) => void) => listeners.set(n, fn), emit: (n: string, v: unknown) => listeners.get(n)?.(v) },
|
|
29
|
+
} as unknown as ExtensionAPI;
|
|
30
|
+
const controller = registerYoloSessionPolicy(pi, { ...env, GENTLE_PI_CONFIG_HOME: home }, { onChange });
|
|
31
|
+
return { controller, context, pi, commands, notices, statuses, next: () => { sessionId = "next"; } };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test("superseded YOLO enables stay silent while genuine activation failure warns", async t => {
|
|
35
|
+
const options = fixture(t), h = host(options.gentlePiConfigHome);
|
|
36
|
+
const original = fsPromises.realpath;
|
|
37
|
+
const captures: Array<{ release(): void }> = [];
|
|
38
|
+
let captured!: () => void;
|
|
39
|
+
const firstCaptured = new Promise<void>(resolve => { captured = resolve; });
|
|
40
|
+
t.mock.method(fsPromises, "realpath", async (path: string) => {
|
|
41
|
+
await new Promise<void>(resolve => { captures.push({ release: resolve }); captured(); });
|
|
42
|
+
return original(path);
|
|
43
|
+
});
|
|
44
|
+
syncBuiltinESMExports();
|
|
45
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
46
|
+
const older = h.controller.humanAction("on", h.context);
|
|
47
|
+
await firstCaptured;
|
|
48
|
+
const newer = h.controller.humanAction("on", h.context);
|
|
49
|
+
assert.equal(captures.length, 2);
|
|
50
|
+
captures[1].release(); await newer;
|
|
51
|
+
captures[0].release(); await older;
|
|
52
|
+
assert.deepEqual(h.notices, ["🚀 YOLO ON 🔥 — destructive confirmations remain"]);
|
|
53
|
+
t.mock.restoreAll(); syncBuiltinESMExports();
|
|
54
|
+
assert.equal(await h.controller.mode(h.context), "yolo");
|
|
55
|
+
Object.assign(h.context, { hasUI: false, mode: "json" });
|
|
56
|
+
await h.controller.humanAction("on", h.context);
|
|
57
|
+
assert.deepEqual(h.notices, ["🚀 YOLO ON 🔥 — destructive confirmations remain",
|
|
58
|
+
"YOLO OFF — activation requires an interactive primary TUI session and an identifiable workspace."]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("strict global policy defaults Normal; round trips all modes atomically and privately", t => {
|
|
62
|
+
const options = fixture(t);
|
|
63
|
+
assert.equal(resolvePermissionModePolicy(options).mode, "normal");
|
|
64
|
+
for (const mode of ["normal", "auto", "yolo"] as const) {
|
|
65
|
+
const path = writePermissionModePolicy(mode, options);
|
|
66
|
+
assert.equal(path, permissionModePolicyFile(options));
|
|
67
|
+
assert.equal(resolvePermissionModePolicy(options).mode, mode);
|
|
68
|
+
assert.equal(statSync(path).mode & 0o777, 0o600);
|
|
69
|
+
assert.deepEqual(readdirSync(options.gentlePiConfigHome), ["permission-mode.json"]);
|
|
70
|
+
assert.equal(parsePermissionModePolicyFile(readFileSync(path, "utf8")), mode);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("malformed, extra-field, invalid and unreadable preferences fail Normal and refuse overwrite", t => {
|
|
75
|
+
const options = fixture(t);
|
|
76
|
+
for (const raw of ["{", "null", "[]", JSON.stringify({ schema: PERMISSION_MODE_SCHEMA, mode: "AUTO" }), JSON.stringify({ schema: PERMISSION_MODE_SCHEMA, mode: "yolo", extra: true })]) {
|
|
77
|
+
writeFileSync(permissionModePolicyFile(options), raw);
|
|
78
|
+
assert.equal(resolvePermissionModePolicy(options).mode, "normal");
|
|
79
|
+
assert.equal(resolvePermissionModePolicy(options).malformed, true);
|
|
80
|
+
assert.throws(() => writePermissionModePolicy("auto", options));
|
|
81
|
+
assert.equal(readFileSync(permissionModePolicyFile(options), "utf8"), raw);
|
|
82
|
+
}
|
|
83
|
+
assert.throws(() => writePermissionModePolicy("invalid" as "normal", options));
|
|
84
|
+
assert.equal(resolvePermissionModePolicy({ gentlePiConfigHome: permissionModePolicyFile(options) }).mode, "normal");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("non-Git primary uses exclusive live modes; saved defaults affect fresh sessions only", async t => {
|
|
88
|
+
const options = fixture(t), h = host(options.gentlePiConfigHome);
|
|
89
|
+
assert.equal(await h.controller.mode(h.context), "normal");
|
|
90
|
+
assert.equal(await h.controller.saveDefault("auto", h.context), true);
|
|
91
|
+
assert.match(h.notices.at(-1)!, /additional model calls.*separate usage/i);
|
|
92
|
+
assert.equal(await h.controller.mode(h.context), "normal");
|
|
93
|
+
assert.equal(await h.controller.setMode("yolo", h.context), true);
|
|
94
|
+
assert.equal(await h.controller.active(h.context), true);
|
|
95
|
+
assert.equal(await h.controller.setMode("auto", h.context), true);
|
|
96
|
+
assert.equal(await h.controller.active(h.context), false);
|
|
97
|
+
assert.match(h.statuses.get("gentle:mode")!, /AUTO/);
|
|
98
|
+
assert.equal(await h.controller.setMode("normal", h.context), true);
|
|
99
|
+
assert.equal(await h.controller.mode(h.context), "normal");
|
|
100
|
+
h.next(); h.controller.reset(h.context);
|
|
101
|
+
assert.equal(await h.controller.mode(h.context), "auto");
|
|
102
|
+
h.controller.reset(h.context);
|
|
103
|
+
assert.equal(await h.controller.mode(h.context), "normal", "same-session revocation does not replay defaults");
|
|
104
|
+
const fresh = host(options.gentlePiConfigHome);
|
|
105
|
+
assert.equal(await fresh.controller.mode(fresh.context), "auto");
|
|
106
|
+
assert.equal(fresh.notices.length, 0, "saved preference does not repeatedly prompt");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("trusted defaults support headless primary but never children or headless mutation", async t => {
|
|
110
|
+
const options = fixture(t);
|
|
111
|
+
writePermissionModePolicy("yolo", options);
|
|
112
|
+
const h = host(options.gentlePiConfigHome);
|
|
113
|
+
Object.assign(h.context, { hasUI: false, mode: "json" });
|
|
114
|
+
assert.equal(await h.controller.mode(h.context), "yolo");
|
|
115
|
+
assert.equal(await h.controller.setMode("auto", h.context), false);
|
|
116
|
+
assert.equal(await h.controller.saveDefault("normal", h.context), false);
|
|
117
|
+
assert.equal(h.controller.defaultMode(), "yolo");
|
|
118
|
+
const child = host(options.gentlePiConfigHome, { GENTLE_PI_AGENTS_CHILD: "1" });
|
|
119
|
+
assert.equal(await child.controller.mode(child.context), "normal");
|
|
120
|
+
assert.equal(await child.controller.saveDefault("auto", child.context), false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("slash and optional adapter operations share mode state; invalid arguments do not mutate", async t => {
|
|
124
|
+
const options = fixture(t), h = host(options.gentlePiConfigHome);
|
|
125
|
+
const command = h.commands.get("gentle:mode"); assert.ok(command);
|
|
126
|
+
await command.handler("auto", h.context);
|
|
127
|
+
assert.equal(await h.controller.mode(h.context), "auto");
|
|
128
|
+
await command.handler("default yolo", h.context);
|
|
129
|
+
assert.equal(h.controller.defaultMode(), "yolo");
|
|
130
|
+
assert.equal(await h.controller.mode(h.context), "auto");
|
|
131
|
+
await command.handler("invalid", h.context);
|
|
132
|
+
assert.equal(await h.controller.mode(h.context), "auto");
|
|
133
|
+
const adapter = await discoverYoloUiAdapter(h.pi, h.context); assert.ok(adapter);
|
|
134
|
+
assert.equal(await adapter.readMode!(), "auto");
|
|
135
|
+
assert.equal(await adapter.setMode!("normal"), true);
|
|
136
|
+
assert.equal(await adapter.read(), "OFF");
|
|
137
|
+
await adapter.toggle(); assert.equal(await adapter.read(), "ON");
|
|
138
|
+
const epoch = h.controller.epoch();
|
|
139
|
+
h.next();
|
|
140
|
+
assert.equal(await adapter.setMode!("auto"), false);
|
|
141
|
+
assert.equal(await adapter.saveDefault!("normal"), false);
|
|
142
|
+
assert.equal(await adapter.read(), "UNAVAILABLE");
|
|
143
|
+
h.controller.reset(h.context); assert.ok(h.controller.epoch() > epoch);
|
|
144
|
+
adapter.dispose();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("disposing the UI adapter during pending activation discards its mode choice", async t => {
|
|
148
|
+
const options = fixture(t), h = host(options.gentlePiConfigHome);
|
|
149
|
+
const adapter = await discoverYoloUiAdapter(h.pi, h.context); assert.ok(adapter);
|
|
150
|
+
const pending = adapter.setMode!("auto");
|
|
151
|
+
adapter.dispose();
|
|
152
|
+
assert.equal(await pending, false);
|
|
153
|
+
assert.equal(await h.controller.mode(h.context), "normal");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("reset during initial default capture revokes without replaying that session default", async t => {
|
|
157
|
+
const options = fixture(t);
|
|
158
|
+
writePermissionModePolicy("auto", options);
|
|
159
|
+
const h = host(options.gentlePiConfigHome);
|
|
160
|
+
const pending = h.controller.mode(h.context);
|
|
161
|
+
h.controller.reset(h.context);
|
|
162
|
+
assert.equal(await pending, "normal");
|
|
163
|
+
assert.equal(await h.controller.mode(h.context), "normal");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("a concurrent mode switch during default-save initialization discards the stale save", async t => {
|
|
167
|
+
const options = fixture(t);
|
|
168
|
+
let once = true;
|
|
169
|
+
let superseding: Promise<boolean> | undefined;
|
|
170
|
+
const h = host(options.gentlePiConfigHome, {}, () => {
|
|
171
|
+
if (!once) return;
|
|
172
|
+
once = false;
|
|
173
|
+
superseding = h.controller.setMode("normal", h.context);
|
|
174
|
+
});
|
|
175
|
+
assert.equal(await h.controller.saveDefault("yolo", h.context), false);
|
|
176
|
+
await superseding;
|
|
177
|
+
assert.equal(h.controller.defaultMode(), "normal");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("stale pending activation, scope loss and malformed preference remain closed", async t => {
|
|
181
|
+
const options = fixture(t), h = host(options.gentlePiConfigHome);
|
|
182
|
+
const pending = h.controller.setMode("auto", h.context);
|
|
183
|
+
h.controller.reset(h.context);
|
|
184
|
+
assert.equal(await pending, false);
|
|
185
|
+
assert.equal(await h.controller.mode(h.context), "normal");
|
|
186
|
+
assert.equal(await h.controller.setMode("auto", h.context), true);
|
|
187
|
+
Object.assign(h.context, { cwd: "/" });
|
|
188
|
+
assert.equal(await h.controller.mode(h.context), "normal");
|
|
189
|
+
Object.assign(h.context, { cwd: options.gentlePiConfigHome });
|
|
190
|
+
assert.equal(await h.controller.mode(h.context), "normal", "revoked modes do not resurrect");
|
|
191
|
+
writeFileSync(permissionModePolicyFile(options), "{");
|
|
192
|
+
assert.equal(await h.controller.saveDefault("yolo", h.context), false);
|
|
193
|
+
assert.equal(readFileSync(permissionModePolicyFile(options), "utf8"), "{");
|
|
194
|
+
});
|
|
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import test from "node:test";
|
|
5
5
|
import { __testing } from "../extensions/gentle-ai.ts";
|
|
6
|
+
import { readDelegationDetail } from "./support/orchestrator-modules.ts";
|
|
6
7
|
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// persona-single-channel migration test
|
|
@@ -328,10 +329,7 @@ test("Table B rule: LB2 subagent-English delegation kept verbatim in orchestrato
|
|
|
328
329
|
// assets/orchestrator-delegation.md (delegation-scoped rule); the always-on
|
|
329
330
|
// combined injection now only carries a pointer to it. Union read so this
|
|
330
331
|
// assertion is repointed, not weakened.
|
|
331
|
-
const delegationDetail =
|
|
332
|
-
fileURLToPath(new URL("../assets/orchestrator-delegation.md", import.meta.url)),
|
|
333
|
-
"utf8",
|
|
334
|
-
);
|
|
332
|
+
const delegationDetail = readDelegationDetail();
|
|
335
333
|
for (const persona of ["gentleman", "neutral"] as const) {
|
|
336
334
|
const prompt = __testing.buildGentlePrompt(persona) + delegationDetail;
|
|
337
335
|
assert.match(
|
|
@@ -369,10 +367,7 @@ test("Table B rule: LB5 exceptions kept verbatim in orchestrator (unique)", () =
|
|
|
369
367
|
// assets/orchestrator-delegation.md (delegation-scoped exceptions); the
|
|
370
368
|
// always-on combined injection now only carries a pointer to it. Union
|
|
371
369
|
// read so this assertion is repointed, not weakened.
|
|
372
|
-
const delegationDetail =
|
|
373
|
-
fileURLToPath(new URL("../assets/orchestrator-delegation.md", import.meta.url)),
|
|
374
|
-
"utf8",
|
|
375
|
-
);
|
|
370
|
+
const delegationDetail = readDelegationDetail();
|
|
376
371
|
for (const persona of ["gentleman", "neutral"] as const) {
|
|
377
372
|
const prompt = __testing.buildGentlePrompt(persona) + delegationDetail;
|
|
378
373
|
assert.match(
|
|
@@ -456,10 +451,7 @@ test("dup guard (exact-string): LB2/LB3/LB4 each occur exactly once", () => {
|
|
|
456
451
|
// the always-on combined injection by design — see "No Double-Delivery").
|
|
457
452
|
// LB3/LB4 stay verbatim in the always-on core. Union read for LB2 so the
|
|
458
453
|
// "exactly once" guard is repointed to its new home, not weakened.
|
|
459
|
-
const delegationDetail =
|
|
460
|
-
fileURLToPath(new URL("../assets/orchestrator-delegation.md", import.meta.url)),
|
|
461
|
-
"utf8",
|
|
462
|
-
);
|
|
454
|
+
const delegationDetail = readDelegationDetail();
|
|
463
455
|
const lb2 =
|
|
464
456
|
"Subagent-facing prompts should be written in English by default, even when the user speaks Spanish.";
|
|
465
457
|
const lb3 =
|
package/tests/pi-pretty.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import pretty from "../extensions/pi-pretty.ts";
|
|
4
|
+
import { setBundledInstallForTesting } from "../lib/gentle-shell-bundled.ts";
|
|
4
5
|
|
|
5
6
|
test("bundled pretty cannot replace or restore an editor owned by the host", async () => {
|
|
6
7
|
const handlers = new Map<string, Function[]>();
|
|
@@ -42,9 +43,64 @@ test("bundled pretty cannot replace or restore an editor owned by the host", asy
|
|
|
42
43
|
assert.equal(message, undefined);
|
|
43
44
|
});
|
|
44
45
|
|
|
46
|
+
test("bundled pretty keeps Pi's native Working row inside Herdr", async () => {
|
|
47
|
+
const handlers = new Map<string, Function[]>();
|
|
48
|
+
const pi = { on(name: string, handler: Function) { handlers.set(name, [...(handlers.get(name) ?? []), handler]); } };
|
|
49
|
+
let visible: boolean | undefined;
|
|
50
|
+
const ctx = { mode: "tui", ui: { setWorkingVisible(value: boolean) { visible = value; } } };
|
|
51
|
+
await pretty(pi, undefined, async () => {}, { HERDR_ENV: "1" });
|
|
52
|
+
for (const handler of handlers.get("session_start") ?? []) await handler({}, ctx);
|
|
53
|
+
assert.notEqual(visible, false, "Herdr's native Pi detection reads the standard Working row");
|
|
54
|
+
});
|
|
55
|
+
|
|
45
56
|
test("disabled shell leaves bundled editor behavior untouched", async () => {
|
|
46
57
|
let received: unknown;
|
|
47
58
|
const pi = {};
|
|
48
59
|
await pretty(pi, undefined, async (api: unknown) => { received = api; }, { GENTLE_PI_SHELL: "0" });
|
|
49
60
|
assert.equal(received, pi);
|
|
50
61
|
});
|
|
62
|
+
|
|
63
|
+
// gentle-shell#1690: a delegated child has no shell (shellEnabled is false),
|
|
64
|
+
// so the upstream fallback would start FFF indexing in every child.
|
|
65
|
+
test("delegated children never load upstream pi-pretty", async () => {
|
|
66
|
+
let loaded = false;
|
|
67
|
+
const pi = {};
|
|
68
|
+
const result = await pretty(pi, undefined, async () => { loaded = true; }, { GENTLE_PI_AGENTS_CHILD: "1" });
|
|
69
|
+
assert.equal(loaded, false);
|
|
70
|
+
assert.equal(result, undefined);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// S9: outside a bundled install Pi's deps reach pi-pretty unchanged, so its
|
|
74
|
+
// FFF store stays where it always was ($HOME/.pi/agent, or none without HOME).
|
|
75
|
+
test("outside a bundled install pi-pretty gets Pi's deps unchanged", async () => {
|
|
76
|
+
const given = { TextComponent: "text" };
|
|
77
|
+
for (const env of [{}, { PI_CODING_AGENT_DIR: "/agent-home" }, { PI_CODING_AGENT_DIR: "/agent-home", GENTLE_PI_SHELL: "0" }, { GENTLE_SHELL_PREFIX: "/not-ours" }]) {
|
|
78
|
+
for (const deps of [undefined, given]) {
|
|
79
|
+
let received: unknown = "unset";
|
|
80
|
+
await pretty({ on() {} }, deps, async (_api: unknown, forwarded: unknown) => { received = forwarded; }, env);
|
|
81
|
+
assert.equal(received, deps, JSON.stringify(env));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// In a bundled install the FFF store follows the agent home. The testing
|
|
87
|
+
// overlay stands in for this package running from a prefix; the current
|
|
88
|
+
// folder serves as one because the prefix must exist.
|
|
89
|
+
test("in a bundled install pi-pretty gets the agent home and Pi's tool factories through deps.sdk", async (t) => {
|
|
90
|
+
setBundledInstallForTesting({ root: process.cwd() });
|
|
91
|
+
t.after(() => setBundledInstallForTesting(undefined));
|
|
92
|
+
const factories = ["createReadToolDefinition", "createBashToolDefinition", "createLsToolDefinition", "createFindToolDefinition", "createGrepToolDefinition"];
|
|
93
|
+
const bundledEnv = { GENTLE_SHELL_PREFIX: process.cwd(), PI_CODING_AGENT_DIR: "/agent-home" };
|
|
94
|
+
for (const env of [bundledEnv, { ...bundledEnv, GENTLE_PI_SHELL: "0" }]) {
|
|
95
|
+
let received: any;
|
|
96
|
+
await pretty({ on() {} }, { TextComponent: "text" }, async (_api: unknown, deps: unknown) => { received = deps; }, env);
|
|
97
|
+
assert.equal(received.TextComponent, "text", "the host's other deps are kept");
|
|
98
|
+
assert.equal(received.sdk.getAgentDir(), "/agent-home");
|
|
99
|
+
for (const name of factories) assert.equal(typeof received.sdk[name], "function", name);
|
|
100
|
+
}
|
|
101
|
+
// A host that already passes its own sdk keeps it.
|
|
102
|
+
const sdk = { getAgentDir: () => "/host" };
|
|
103
|
+
let kept: any;
|
|
104
|
+
await pretty({ on() {} }, { sdk }, async (_api: unknown, deps: unknown) => { kept = deps; }, bundledEnv);
|
|
105
|
+
assert.equal(kept.sdk, sdk);
|
|
106
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
|
|
5
|
+
const PI = '@earendil-works/pi-coding-agent';
|
|
6
|
+
// A virtual symlink filesystem: listed global links point into a pnpm store.
|
|
7
|
+
// The store's adjacent peer is a sibling, not Shell/node_modules/Pi.
|
|
8
|
+
function launch(args: string[], scenario = 'owned') {
|
|
9
|
+
const adjacent = scenario.startsWith('adjacent');
|
|
10
|
+
const npm = scenario.startsWith('npm-');
|
|
11
|
+
const nodeModules = '/global/v11/selected/node_modules';
|
|
12
|
+
const shellRoot = '/store/shell-peer/node_modules/gentle-pi';
|
|
13
|
+
const piRoot = adjacent ? '/store/shell-peer/node_modules/' + PI : '/store/pi/node_modules/' + PI;
|
|
14
|
+
const target = adjacent ? 'gentle-pi' : PI;
|
|
15
|
+
const dependencies: Record<string, object> = {[target]:{path: `${nodeModules}/${target}`,
|
|
16
|
+
...(scenario === 'npm-link' ? {link:true} : {}),
|
|
17
|
+
...(scenario === 'npm-file' ? {resolved:'file:/source/pi'} : {})}};
|
|
18
|
+
if (scenario.endsWith('shared')) dependencies['unrelated'] = {path:`${nodeModules}/unrelated`};
|
|
19
|
+
if (scenario === 'separate') dependencies['unrelated'] = {path:'/global/v11/other/node_modules/unrelated'};
|
|
20
|
+
const groups = [{path:'/global/v11',dependencies}, ...(scenario === 'ambiguous' ? [{path:'/global/other',dependencies}] : [])];
|
|
21
|
+
const listing = scenario === 'malformed' ? '{' : JSON.stringify(npm ? groups[0] : groups);
|
|
22
|
+
// pnpm links only the top-level package into the global group; the peer
|
|
23
|
+
// is reachable solely as the Shell package's store sibling.
|
|
24
|
+
const links = {[`${nodeModules}/${target}`]:adjacent ? shellRoot : piRoot};
|
|
25
|
+
const roots = [nodeModules, '/global/v11/other/node_modules', shellRoot, piRoot, `${piRoot}/dist/bundle/cli.js`];
|
|
26
|
+
const bootstrap = `import { registerHooks } from 'node:module';
|
|
27
|
+
const original = ${JSON.stringify(new URL('../bin/gentle-shell.mjs', import.meta.url).href)};
|
|
28
|
+
registerHooks({load(url, context, next) {
|
|
29
|
+
if (url === original) {
|
|
30
|
+
let source = next(url, context).source.toString();
|
|
31
|
+
source = source.replace('const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));', ${JSON.stringify(`const packageRoot = '${shellRoot}';`)});
|
|
32
|
+
source = source.replace('const bundled = resolveBundledInstall({ packageRoot, platform: process.platform, fs: bundledFs, verifying });', 'const bundled = undefined;');
|
|
33
|
+
source = source.replace('runtime = resolvePiRuntime({', ${JSON.stringify(`runtime = ({ kind: '${adjacent ? 'bundled' : 'env'}', command: '${adjacent ? 'node' : piRoot + '/dist/bundle/cli.js'}', args: ${JSON.stringify(adjacent ? [piRoot + '/dist/bundle/cli.js'] : [])} }); /*`)}).replace('nodeExecPath: process.execPath,\\n\\t\\t});', 'nodeExecPath: process.execPath, */');
|
|
34
|
+
source = source.replace('const versionProbe = spawnSync(versionProbePlan.command, versionProbePlan.args, {', 'const versionProbe = { stdout: "1.2.0" }; /*').replace('env: bundledEnv ?? process.env,\\n\\t});', 'env: bundledEnv ?? process.env, */');
|
|
35
|
+
source = source.replace('async function packageManagerAdapters() {', 'async function packageManagerAdapters() { return globalThis.fakeAdapters; } async function unusedAdapters() {');
|
|
36
|
+
source = source.replace('let homeHadContentBeforeBootstrap = false;', 'if (passthrough.length === 1) { console.error("error: pi cannot self-update this installation. This installation is not managed by a global pnpm install."); process.exitCode = 1; return; } console.log("FORWARD " + JSON.stringify(passthrough)); return; let homeHadContentBeforeBootstrap = false;');
|
|
37
|
+
return {format:'module', source, shortCircuit:true};
|
|
38
|
+
}
|
|
39
|
+
return next(url,context);
|
|
40
|
+
}});
|
|
41
|
+
const links = ${JSON.stringify(links)}, roots = ${JSON.stringify(roots)};
|
|
42
|
+
globalThis.fakeAdapters = {
|
|
43
|
+
fs: { realpath: async p => { if (links[p]) return links[p]; if (roots.includes(p)) return p; throw new Error('ENOENT ' + p); },
|
|
44
|
+
readFile: async p => { if (p === '${shellRoot}/package.json') return JSON.stringify({name:'gentle-pi',version:'4.1.0'}); if(p === '${piRoot}/package.json') return JSON.stringify({name:'${PI}',version:'1.2.0',bin:{pi:'dist/bundle/cli.js'}}); throw new Error('ENOENT ' + p); } },
|
|
45
|
+
which: async n => n === '${npm ? 'npm' : 'pnpm'}' ? '/fake/' + n : null,
|
|
46
|
+
run: async (cmd, args) => {
|
|
47
|
+
if(args.includes('list') || args.includes('ls')) return {code:0,stdout:${JSON.stringify(listing)}};
|
|
48
|
+
console.log('MUTATION ' + JSON.stringify(args)); return {code:0};
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
await import(original);`;
|
|
52
|
+
return spawnSync(process.execPath, ['--import', `data:text/javascript,${encodeURIComponent(bootstrap)}`, 'bin/gentle-shell.mjs', ...args], {encoding:'utf8'});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const scenario of ['owned','adjacent','separate']) test(`public linked-store update succeeds for ${scenario}`, () => {
|
|
56
|
+
const result = launch(['update'], scenario);
|
|
57
|
+
assert.equal(result.status, 0, result.stderr);
|
|
58
|
+
assert.match(result.stdout, scenario === 'adjacent' ? /MUTATION \["add","-g","gentle-pi@4\.1\.0","--allow-build=gentle-pi"\]/ : /MUTATION \["add","-g","@earendil-works\/pi-coding-agent@latest"\]/);
|
|
59
|
+
assert.doesNotMatch(result.stdout, /FORWARD/);
|
|
60
|
+
// The fake manager changes nothing, so the readback reports the same version and pnpm's release-age rule.
|
|
61
|
+
assert.match(result.stdout, /the selected Pi is still 1\.2\.0\./);
|
|
62
|
+
assert.match(result.stdout, /minimumReleaseAge/);
|
|
63
|
+
assert.equal(result.stderr, '');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('public all updates only its owner and then forwards extension updates', () => {
|
|
67
|
+
const result = launch(['update', '--all']);
|
|
68
|
+
assert.equal(result.status, 0, result.stderr);
|
|
69
|
+
assert.match(result.stdout, /MUTATION .*add.*@earendil-works\/pi-coding-agent@latest/);
|
|
70
|
+
assert.match(result.stdout, /FORWARD \["update","--extensions"\]/);
|
|
71
|
+
assert.equal(result.stderr, '');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
for (const scenario of ['ambiguous', 'malformed', 'npm-link', 'npm-file', 'shared', 'adjacent-shared']) test(`public update rejects ${scenario} ownership without mutation`, () => {
|
|
75
|
+
const result = launch(['update'], scenario);
|
|
76
|
+
assert.equal(result.status, 1, result.stdout);
|
|
77
|
+
assert.equal(result.stdout, '');
|
|
78
|
+
assert.match(result.stderr, /ownership/);
|
|
79
|
+
assert.doesNotMatch(result.stdout + result.stderr, /MUTATION/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('public extensions-only update preserves the forwarded command', () => {
|
|
83
|
+
const result = launch(['update', '--extensions-only']);
|
|
84
|
+
assert.equal(result.status, 0, result.stderr);
|
|
85
|
+
assert.equal(result.stdout, 'FORWARD ["update","--extensions-only"]\n');
|
|
86
|
+
assert.equal(result.stderr, '');
|
|
87
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { managedUpdatePlan, updateSelectedPi } from '../scripts/pi-update.mjs';
|
|
4
|
+
import { bundledUpdatePlan, bundledUpdateNext } from '../runtime/gentle-shell-bundled.mjs';
|
|
5
|
+
|
|
6
|
+
const PI = '@earendil-works/pi-coding-agent';
|
|
7
|
+
const CLI = 'dist/bundle/cli.js';
|
|
8
|
+
|
|
9
|
+
// The layouts pnpm v11 and npm really produce. pnpm links each top-level
|
|
10
|
+
// package from its own global group into the linked store, where Shell's
|
|
11
|
+
// peer is a sibling of the Shell package; a separate global Pi links to the
|
|
12
|
+
// same store entry. npm nests the peer under the Shell package.
|
|
13
|
+
function fixture({windows = false, npm = false, runtime = 'adjacent', groups = 'both', peerAfter = '1.2.0', shellVersion = '4.1.0',
|
|
14
|
+
badManifest = false, failed = false, mismatch = false, linked = false, readback = true, unrelatedPeer = false, shim = ''} = {}) {
|
|
15
|
+
const base = windows ? 'C:/pnpm' : '/pnpm';
|
|
16
|
+
const storePi = (version: string) => `${base}/store/links/@earendil-works/pi-coding-agent/${version}/h/node_modules/${PI}`;
|
|
17
|
+
const shellModules = npm ? `${base}/npm/lib/node_modules` : `${base}/store/links/@/gentle-pi/4.1.0/h1/node_modules`;
|
|
18
|
+
const shell = `${shellModules}/gentle-pi`;
|
|
19
|
+
const nestedPeer = `${shell}/node_modules/${PI}`;
|
|
20
|
+
const g1 = npm ? shellModules : `${base}/global/v11/g1/node_modules`;
|
|
21
|
+
const g2 = npm ? shellModules : `${base}/global/v11/g2/node_modules`;
|
|
22
|
+
const g3 = `${base}/global/v11/g3/node_modules`;
|
|
23
|
+
const state = {peer: '1.1.0', group: '1.1.0'};
|
|
24
|
+
const peerRoot = () => npm ? nestedPeer : storePi(state.peer);
|
|
25
|
+
const selectedCli = `${npm ? (runtime === 'adjacent' ? nestedPeer : `${shellModules}/${PI}`) : storePi('1.1.0')}/${CLI}`;
|
|
26
|
+
// A package-manager shim on PATH: its text names the global Pi group's cli.js.
|
|
27
|
+
const shimFile = `${base}/bin/pi${windows ? '.CMD' : ''}`;
|
|
28
|
+
const shimText = shim === 'posix' ? `#!/bin/sh\nbasedir=$(dirname "$0")\nexec node "$basedir/../global/v11/g2/node_modules/${PI}/${CLI}" "$@"\n`
|
|
29
|
+
: shim === 'windows' ? `@"%~dp0\\..\\global\\v11\\g2\\node_modules\\@earendil-works\\pi-coding-agent\\dist\\bundle\\cli.js" %*\r\n`
|
|
30
|
+
: shim === 'two' ? `"$basedir/../a/dist/bundle/cli.js" "$basedir/../b/dist/bundle/cli.js"` : shim === 'none' ? '#!/bin/sh\nexec something\n' : '';
|
|
31
|
+
const mutations: string[][] = [];
|
|
32
|
+
const out: string[] = [];
|
|
33
|
+
const cwds: unknown[] = [];
|
|
34
|
+
const entry = (pkg: string, modules: string, extra = {}) => ({[pkg]: {path: `${modules}/${pkg}`, ...extra}});
|
|
35
|
+
const shellEntry = entry('gentle-pi', g1, linked ? {link: true} : {});
|
|
36
|
+
const piEntry = entry(PI, g2, linked ? {resolved: 'file:/src/pi'} : {});
|
|
37
|
+
const listing = () => {
|
|
38
|
+
const deps = {...(groups !== 'pi' ? shellEntry : {}), ...(groups !== 'shell' ? piEntry : {}),
|
|
39
|
+
...(groups === 'shared' ? entry('unrelated-tool', g1) : {}), ...(groups === 'separate' ? entry('unrelated-tool', g3) : {})};
|
|
40
|
+
const group = {path: npm ? shellModules : `${base}/global/v11`, dependencies: deps};
|
|
41
|
+
return npm ? group : [group];
|
|
42
|
+
};
|
|
43
|
+
const exists = (n: string) => [g1, g2, g3, shellModules, shell, storePi(state.peer), storePi(state.group), storePi('1.1.0'), nestedPeer]
|
|
44
|
+
.some(root => n === root || n.startsWith(root + '/'));
|
|
45
|
+
const realpath = async (value: string) => {
|
|
46
|
+
const n = value.replaceAll('\\', '/');
|
|
47
|
+
if (n === shimFile) return n;
|
|
48
|
+
// Symlinked components inside a longer path resolve like the real filesystem does.
|
|
49
|
+
if (n.startsWith(`${g2}/${PI}/`)) return (npm ? `${g2}/${PI}` : storePi(state.group)) + n.slice(`${g2}/${PI}`.length);
|
|
50
|
+
if (n === `${g1}/gentle-pi` || n === `${g1}/unrelated-tool` || n === `${g3}/unrelated-tool`) return n === `${g1}/gentle-pi` ? shell : n;
|
|
51
|
+
if (n === `${g2}/${PI}`) return npm ? n : storePi(state.group);
|
|
52
|
+
if (!npm && n === `${shellModules}/${PI}`) return storePi(state.peer);
|
|
53
|
+
if (!npm && n === nestedPeer) throw new Error('ENOENT nested peer');
|
|
54
|
+
// Only joined (backslash) paths mismatch: the selected CLI itself stays canonical.
|
|
55
|
+
if (mismatch && n.endsWith(CLI) && value.includes('\\')) return `${base}/other/cli.js`;
|
|
56
|
+
if (exists(n)) return n;
|
|
57
|
+
throw new Error(`ENOENT ${n}`);
|
|
58
|
+
};
|
|
59
|
+
const readFile = async (value: string) => {
|
|
60
|
+
const n = value.replaceAll('\\', '/');
|
|
61
|
+
if (n === shimFile) return shimText;
|
|
62
|
+
if (n === `${shell}/package.json`) return JSON.stringify({name: 'gentle-pi', ...(shellVersion ? {version: shellVersion} : {})});
|
|
63
|
+
if (n === `${g1}/unrelated-tool/package.json`) return JSON.stringify({name: 'unrelated-tool'});
|
|
64
|
+
const version = n.startsWith(nestedPeer) ? state.peer : /pi-coding-agent\/([\d.]+)\/h\//.exec(n)?.[1];
|
|
65
|
+
if (badManifest || unrelatedPeer) return JSON.stringify({name: unrelatedPeer ? 'other' : PI, version});
|
|
66
|
+
return JSON.stringify({name: PI, version, bin: {pi: CLI}});
|
|
67
|
+
};
|
|
68
|
+
const run = async (_command: string, args: string[], options?: {cwd?: unknown}) => {
|
|
69
|
+
cwds.push(options?.cwd);
|
|
70
|
+
if (args.includes('list') || args.includes('ls')) return {code: failed ? 1 : 0, stdout: readback || mutations.length === 0 ? JSON.stringify(listing()) : '{'};
|
|
71
|
+
mutations.push(args);
|
|
72
|
+
if (args.some(arg => arg.startsWith('gentle-pi@'))) state.peer = peerAfter;
|
|
73
|
+
if (args.some(arg => arg.startsWith(PI + '@'))) state.group = peerAfter;
|
|
74
|
+
return {code: 0};
|
|
75
|
+
};
|
|
76
|
+
return {mutations, out, cwds, state, shell, selectedCli, input: {platform: windows ? 'win32' : 'linux', packageRoot: `${g1}/gentle-pi`, currentVersion: '1.1.0', cwd: '/neutral',
|
|
77
|
+
runtime: runtime === 'adjacent' ? {kind: 'bundled', command: 'node', args: [selectedCli]} : {kind: runtime, command: shim ? shimFile : selectedCli, args: []},
|
|
78
|
+
out: (line: string) => out.push(line),
|
|
79
|
+
adapters: {which: async (name: string) => name === (npm ? 'npm' : 'pnpm') ? `/fake/${name}` : null, fs: {realpath, readFile}, run}}};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
test('prefix distribution still redirects self update and forwards only extensions for all', () => {
|
|
83
|
+
assert.deepEqual(bundledUpdatePlan(['update']), {self: true});
|
|
84
|
+
assert.deepEqual(bundledUpdateNext(bundledUpdatePlan(['update']), 0), {exitCode: 0});
|
|
85
|
+
assert.deepEqual(bundledUpdatePlan(['update', '--all']), {self: true, piArgs: ['update', '--extensions']});
|
|
86
|
+
assert.deepEqual(bundledUpdatePlan(['update', '--extensions-only']), {self: false, piArgs: ['update', '--extensions-only']});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
for (const args of [['update', '--extensions-only'], ['update', '--extensions'], ['update', 'npm:custom'], ['update', '--models'], ['update', '--help'], ['update', '--unknown']]) test(`no ownership interception for ${args.join(' ')}`, () => {
|
|
90
|
+
assert.deepEqual(managedUpdatePlan(args), {self: false, piArgs: args});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('all strips only self update before forwarding extension flags', () => {
|
|
94
|
+
assert.deepEqual(managedUpdatePlan(['update', '--all', '--approve']), {self: true, piArgs: ['update', '--extensions', '--approve']});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
for (const windows of [false, true]) test(`an adjacent peer is refreshed by reinstalling its Shell package at the same version, never the separate global Pi (windows: ${windows})`, async () => {
|
|
98
|
+
const f = fixture({windows});
|
|
99
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
100
|
+
assert.deepEqual(f.mutations, [['add', '-g', 'gentle-pi@4.1.0', '--allow-build=gentle-pi']]);
|
|
101
|
+
assert.deepEqual(f.out, ['pnpm installed gentle-pi@4.1.0; the selected Pi is now 1.2.0 (was 1.1.0).']);
|
|
102
|
+
assert.equal(f.state.group, '1.1.0');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
for (const runtime of ['path', 'env']) test(`a ${runtime} runtime sharing the store entry is updated through its own global Pi package`, async () => {
|
|
106
|
+
const f = fixture({runtime});
|
|
107
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
108
|
+
assert.deepEqual(f.mutations, [['add', '-g', `${PI}@latest`]]);
|
|
109
|
+
assert.deepEqual(f.out, [`pnpm installed ${PI}@latest; the selected Pi is now 1.2.0 (was 1.1.0).`]);
|
|
110
|
+
assert.equal(f.state.peer, '1.1.0');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('npm nests the peer under Shell and reinstalls Shell for it', async () => {
|
|
114
|
+
const f = fixture({npm: true});
|
|
115
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
116
|
+
assert.deepEqual(f.mutations, [['install', '-g', 'gentle-pi@4.1.0']]);
|
|
117
|
+
assert.deepEqual(f.out, ['npm installed gentle-pi@4.1.0; the selected Pi is now 1.2.0 (was 1.1.0).']);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('an npm PATH runtime updates the global Pi without its scripts', async () => {
|
|
121
|
+
const f = fixture({npm: true, runtime: 'path'});
|
|
122
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
123
|
+
assert.deepEqual(f.mutations, [['install', '-g', `${PI}@latest`, '--ignore-scripts']]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('an unchanged pnpm result names the minimum-release-age rule instead of claiming latest', async () => {
|
|
127
|
+
const f = fixture({peerAfter: '1.1.0'});
|
|
128
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
129
|
+
assert.equal(f.out[0], 'pnpm installed gentle-pi@4.1.0, and the selected Pi is still 1.1.0.');
|
|
130
|
+
assert.match(f.out[1], /minimumReleaseAge/);
|
|
131
|
+
assert.equal(f.out.length, 2);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('every package manager call runs from the neutral directory, never the project cwd', async () => {
|
|
135
|
+
const f = fixture();
|
|
136
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
137
|
+
assert.deepEqual(f.cwds, ['/neutral', '/neutral', '/neutral']);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('an unreadable readback is reported, not invented', async () => {
|
|
141
|
+
const f = fixture({readback: false});
|
|
142
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
143
|
+
assert.deepEqual(f.out, ['pnpm installed gentle-pi@4.1.0. The Pi version it now supplies could not be read back; run \'gentle-shell --version\' to check it.']);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('separate aggregated groups do not block the Shell reinstall', async () => {
|
|
147
|
+
const f = fixture({groups: 'separate'});
|
|
148
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
149
|
+
assert.deepEqual(f.mutations, [['add', '-g', 'gentle-pi@4.1.0', '--allow-build=gentle-pi']]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
for (const [name, options] of Object.entries({
|
|
153
|
+
'listing failed': {failed: true},
|
|
154
|
+
'peer identity differs': {badManifest: true},
|
|
155
|
+
'peer package is not Pi': {unrelatedPeer: true},
|
|
156
|
+
'selected CLI mismatches on Windows': {windows: true, mismatch: true},
|
|
157
|
+
'linked or file suppliers': {linked: true},
|
|
158
|
+
'Shell shares its global group': {groups: 'shared'},
|
|
159
|
+
'Shell version is missing': {shellVersion: ''},
|
|
160
|
+
'only a separate global Pi is listed for an adjacent runtime': {groups: 'pi'},
|
|
161
|
+
'only Shell is listed for a PATH runtime': {runtime: 'path', groups: 'shell'},
|
|
162
|
+
})) test(`ownership rejects without mutation when ${name}`, async () => {
|
|
163
|
+
const f = fixture(options);
|
|
164
|
+
await assert.rejects(updateSelectedPi(f.input), /ownership/);
|
|
165
|
+
assert.deepEqual(f.mutations, []);
|
|
166
|
+
assert.deepEqual(f.out, []);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
for (const [name, options] of Object.entries({'pnpm POSIX shim': {shim: 'posix', runtime: 'path'}, 'Windows .CMD shim': {shim: 'windows', runtime: 'path', windows: true}, 'GENTLE_SHELL_PI shim override': {shim: 'posix', runtime: 'env'}})) test(`a ${name} resolves to the global Pi it runs`, async () => {
|
|
170
|
+
const f = fixture(options);
|
|
171
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
172
|
+
assert.deepEqual(f.mutations, [['add', '-g', `${PI}@latest`]]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
for (const [name, shim] of Object.entries({'names several targets': 'two', 'is not a Pi entry': 'none'})) test(`a shim that ${name} is refused without mutation`, async () => {
|
|
176
|
+
const f = fixture({shim, runtime: 'path'});
|
|
177
|
+
await assert.rejects(updateSelectedPi(f.input), /ownership/);
|
|
178
|
+
assert.deepEqual(f.mutations, []);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('an unrelated selected override cannot authorize the adjacent Shell group', async () => {
|
|
182
|
+
const f = fixture();
|
|
183
|
+
f.input.runtime = {kind: 'env', command: '/unrelated/pi', args: []};
|
|
184
|
+
await assert.rejects(updateSelectedPi(f.input), /ownership/);
|
|
185
|
+
assert.deepEqual(f.mutations, []);
|
|
186
|
+
});
|
|
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import test from "node:test";
|
|
5
|
+
import { readDelegationDetail } from "./support/orchestrator-modules.ts";
|
|
5
6
|
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
7
8
|
// Provider Defect Handoff — structural readback tests (issue #256, track 5)
|
|
@@ -16,8 +17,8 @@ import test from "node:test";
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
|
|
18
19
|
const REPO_ROOT = join(import.meta.dirname, "..");
|
|
19
|
-
|
|
20
|
-
const DELEGATION =
|
|
20
|
+
// gentle-shell#1494: the handoff now lives in assets/orchestrator-prompts.md; the union keeps it pinned.
|
|
21
|
+
const DELEGATION = readDelegationDetail();
|
|
21
22
|
|
|
22
23
|
const CHOICE_TOKENS = ["report_and_continue", "continue_without_reporting", "stop_here"] as const;
|
|
23
24
|
|
|
@@ -92,7 +93,7 @@ test("orchestrator-delegation.md states the admissibility-before-relay rule", ()
|
|
|
92
93
|
});
|
|
93
94
|
|
|
94
95
|
test("orchestrator-delegation.md excludes local consent lifecycle outcomes from provider-defect reporting", () => {
|
|
95
|
-
assert.match(DELEGATION,
|
|
96
|
+
assert.match(DELEGATION, /Pending consent has no response deadline\. `consent-binding-already-consumed` is a local lifecycle outcome, not a Gentle AI provider defect/i);
|
|
96
97
|
assert.match(DELEGATION, /An unknown consent binding is reportable only when independent evidence proves a fresh, same-session, unconsumed binding was lost/i);
|
|
97
98
|
assert.match(DELEGATION, /Never infer that evidence from the old combined stale-binding message/i);
|
|
98
99
|
});
|