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,344 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import * as fsPromises from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join, win32 } from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import {
|
|
8
|
+
CHANNEL_SCHEMA,
|
|
9
|
+
GENTLE_AI_MAIN_PACKAGE,
|
|
10
|
+
MainChannelError,
|
|
11
|
+
buildMainGentleAi,
|
|
12
|
+
channelStatePath,
|
|
13
|
+
installOwner,
|
|
14
|
+
mainChannelAdapter,
|
|
15
|
+
mainVersion,
|
|
16
|
+
packMainShell,
|
|
17
|
+
readChannel,
|
|
18
|
+
resolveMainCommit,
|
|
19
|
+
runUpgrade,
|
|
20
|
+
writeChannel,
|
|
21
|
+
} from "../scripts/main-channel.mjs";
|
|
22
|
+
import { artifactFor } from "../scripts/installer-downloads.mjs";
|
|
23
|
+
|
|
24
|
+
const SHELL_SHA = "6e7e3a18f794223396527a54c7c36d19c7d236c6";
|
|
25
|
+
const AI_SHA = "1f9d5e6423e37f7d2316859045f379ba9b5d8c3a";
|
|
26
|
+
const posixOnly = { skip: process.platform === "win32" };
|
|
27
|
+
|
|
28
|
+
function sandbox() {
|
|
29
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "main-channel-")));
|
|
30
|
+
const home = join(root, "home");
|
|
31
|
+
mkdirSync(home);
|
|
32
|
+
return { root, home, ctx: { env: {}, home }, cleanup: () => rmSync(root, { recursive: true, force: true }) };
|
|
33
|
+
}
|
|
34
|
+
type Call = { command: string; argv: string[]; options: { env?: Record<string, string>; cwd?: string } };
|
|
35
|
+
|
|
36
|
+
test("channel state defaults to release and round-trips a main record", async () => {
|
|
37
|
+
const s = sandbox();
|
|
38
|
+
try {
|
|
39
|
+
assert.equal(channelStatePath(s.ctx), join(s.home, ".pi", "gentle-ai", "channel.json"));
|
|
40
|
+
assert.equal(channelStatePath({ env: { GENTLE_PI_CONFIG_HOME: join(s.root, "cfg") }, home: s.home }), join(s.root, "cfg", "channel.json"));
|
|
41
|
+
assert.deepEqual(await readChannel(s.ctx, fsPromises), { channel: "release" });
|
|
42
|
+
await writeChannel(s.ctx, { channel: "main", shellCommit: SHELL_SHA, gentleAiCommit: AI_SHA }, fsPromises);
|
|
43
|
+
assert.deepEqual(JSON.parse(readFileSync(channelStatePath(s.ctx), "utf8")),
|
|
44
|
+
{ schema: CHANNEL_SCHEMA, channel: "main", shellCommit: SHELL_SHA, gentleAiCommit: AI_SHA });
|
|
45
|
+
assert.deepEqual(await readChannel(s.ctx, fsPromises), { channel: "main", shellCommit: SHELL_SHA, gentleAiCommit: AI_SHA });
|
|
46
|
+
} finally { s.cleanup(); }
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("a malformed channel state fails closed instead of falling back to release", async () => {
|
|
50
|
+
const s = sandbox();
|
|
51
|
+
try {
|
|
52
|
+
mkdirSync(join(s.home, ".pi", "gentle-ai"), { recursive: true });
|
|
53
|
+
for (const bad of ["not json", JSON.stringify({ schema: CHANNEL_SCHEMA, channel: "nightly" }),
|
|
54
|
+
JSON.stringify({ schema: CHANNEL_SCHEMA, channel: "main", shellCommit: "abc", gentleAiCommit: AI_SHA })]) {
|
|
55
|
+
writeFileSync(channelStatePath(s.ctx), bad);
|
|
56
|
+
await assert.rejects(readChannel(s.ctx, fsPromises), (error: MainChannelError) => error.code === "channel-state-invalid");
|
|
57
|
+
}
|
|
58
|
+
} finally { s.cleanup(); }
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("the latest main commit comes from GitHub as an exact 40-character SHA", async () => {
|
|
62
|
+
const requests: Array<{ url: string; accept: string | undefined }> = [];
|
|
63
|
+
const fetch = async (url: string, init: { headers: Record<string, string> }) => {
|
|
64
|
+
requests.push({ url, accept: init.headers.Accept });
|
|
65
|
+
return { ok: true, status: 200, text: async () => `${SHELL_SHA}\n` };
|
|
66
|
+
};
|
|
67
|
+
assert.equal(await resolveMainCommit("Gentleman-Programming/gentle-shell", { fetch }), SHELL_SHA);
|
|
68
|
+
assert.deepEqual(requests, [{ url: "https://api.github.com/repos/Gentleman-Programming/gentle-shell/commits/main", accept: "application/vnd.github.sha" }]);
|
|
69
|
+
for (const response of [{ ok: true, status: 200, text: async () => "abc123" }, { ok: false, status: 403, text: async () => SHELL_SHA }]) {
|
|
70
|
+
await assert.rejects(resolveMainCommit("Gentleman-Programming/gentle-shell", { fetch: async () => response }),
|
|
71
|
+
(error: MainChannelError) => error.code === "main-commit-unavailable");
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("a main version marks the release version with the commit", () => {
|
|
76
|
+
assert.equal(mainVersion("4.0.0", SHELL_SHA), "4.0.0-main.6e7e3a18f794");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("the Gentle Shell main package is packed from the exact commit without prepack", async () => {
|
|
80
|
+
const s = sandbox();
|
|
81
|
+
try {
|
|
82
|
+
const calls: Call[] = [];
|
|
83
|
+
let downloaded = "";
|
|
84
|
+
let packed: { version: string; scripts: Record<string, string | undefined> } | null = null;
|
|
85
|
+
const fetch = async (url: string) => {
|
|
86
|
+
downloaded = url;
|
|
87
|
+
return { ok: true, status: 200, arrayBuffer: async () => new TextEncoder().encode("archive").buffer };
|
|
88
|
+
};
|
|
89
|
+
const run = async (command: string, argv: string[], options: Call["options"]) => {
|
|
90
|
+
calls.push({ command, argv, options });
|
|
91
|
+
if (argv[0] === "-xzf") {
|
|
92
|
+
const target = argv[argv.indexOf("-C") + 1];
|
|
93
|
+
writeFileSync(join(target, "package.json"), JSON.stringify({ name: "gentle-pi", version: "4.0.0",
|
|
94
|
+
scripts: { prepack: "pnpm test", prepare: "x", postinstall: "node scripts/install-gentle-ai.mjs" } }));
|
|
95
|
+
} else {
|
|
96
|
+
packed = JSON.parse(readFileSync(join(options.cwd!, "package.json"), "utf8"));
|
|
97
|
+
const destination = argv[argv.indexOf("--pack-destination") + 1];
|
|
98
|
+
writeFileSync(join(destination, "gentle-pi-4.0.0-main.6e7e3a18f794.tgz"), "tgz");
|
|
99
|
+
}
|
|
100
|
+
return { code: 0, stdout: "" };
|
|
101
|
+
};
|
|
102
|
+
const tgz = await packMainShell({ commit: SHELL_SHA, ctx: s.ctx, fetch, run, pnpm: { command: "/bin/pnpm", prefix: [] }, fs: fsPromises });
|
|
103
|
+
assert.equal(downloaded, `https://codeload.github.com/Gentleman-Programming/gentle-shell/tar.gz/${SHELL_SHA}`);
|
|
104
|
+
assert.equal(tgz, join(s.home, ".pi", "gentle-ai", "main", "packages", "gentle-pi-4.0.0-main.6e7e3a18f794.tgz"));
|
|
105
|
+
assert.ok(existsSync(tgz));
|
|
106
|
+
const pack = calls.find((call) => call.command === "/bin/pnpm");
|
|
107
|
+
assert.ok(pack);
|
|
108
|
+
assert.deepEqual(pack.argv.slice(0, 1), ["pack"]);
|
|
109
|
+
assert.ok(packed);
|
|
110
|
+
assert.equal(packed.version, "4.0.0-main.6e7e3a18f794");
|
|
111
|
+
assert.equal(packed.scripts.prepack, undefined);
|
|
112
|
+
assert.equal(packed.scripts.prepare, undefined);
|
|
113
|
+
assert.equal(packed.scripts.postinstall, "node scripts/install-gentle-ai.mjs");
|
|
114
|
+
assert.equal(existsSync(join(s.home, ".pi", "gentle-ai", "main", ".source")), false, "the extracted source is removed");
|
|
115
|
+
} finally { s.cleanup(); }
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Run 38061123855: with Git for Windows' usr\bin first on PATH, MSYS tar reads `D:\...` as a remote host.
|
|
119
|
+
test("on Windows the source archive is extracted by System32's tar.exe by absolute path; POSIX keeps tar from PATH", async () => {
|
|
120
|
+
const windowsTar = win32.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe");
|
|
121
|
+
for (const [platform, expected] of [["win32", windowsTar], ["linux", "tar"], ["darwin", "tar"]]) {
|
|
122
|
+
const s = sandbox();
|
|
123
|
+
try {
|
|
124
|
+
const extracts: string[] = [];
|
|
125
|
+
const fetch = async () => ({ ok: true, status: 200, arrayBuffer: async () => new TextEncoder().encode("archive").buffer });
|
|
126
|
+
const run = async (command: string, argv: string[], options: Call["options"]) => {
|
|
127
|
+
if (argv[0] === "-xzf") {
|
|
128
|
+
extracts.push(command);
|
|
129
|
+
writeFileSync(join(argv[argv.indexOf("-C") + 1], "package.json"), JSON.stringify({ name: "gentle-pi", version: "4.0.0" }));
|
|
130
|
+
} else {
|
|
131
|
+
writeFileSync(join(argv[argv.indexOf("--pack-destination") + 1], "gentle-pi-4.0.0-main.6e7e3a18f794.tgz"), "tgz");
|
|
132
|
+
assert.ok(options.cwd);
|
|
133
|
+
}
|
|
134
|
+
return { code: 0, stdout: "" };
|
|
135
|
+
};
|
|
136
|
+
await packMainShell({ commit: SHELL_SHA, ctx: s.ctx, fetch, run, pnpm: { command: "/bin/pnpm", prefix: [] }, fs: fsPromises, platform });
|
|
137
|
+
assert.deepEqual(extracts, [expected], platform);
|
|
138
|
+
} finally { s.cleanup(); }
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("a failed source extraction or Go build keeps the command's bounded stderr tail as the error's cause", async () => {
|
|
143
|
+
const s = sandbox();
|
|
144
|
+
try {
|
|
145
|
+
const tail = "tar (child): Cannot connect to D: resolve failed\ntar: Error is not recoverable: exiting now\n";
|
|
146
|
+
const requested: Array<number | undefined> = [];
|
|
147
|
+
const fetch = async () => ({ ok: true, status: 200, arrayBuffer: async () => new TextEncoder().encode("archive").buffer });
|
|
148
|
+
const tar = async (_command: string, _argv: string[], options: { stderrTail?: number }) => {
|
|
149
|
+
requested.push(options.stderrTail);
|
|
150
|
+
return { code: 2, stdout: "", stderrTail: tail };
|
|
151
|
+
};
|
|
152
|
+
await assert.rejects(packMainShell({ commit: SHELL_SHA, ctx: s.ctx, fetch, run: tar, pnpm: { command: "/bin/pnpm", prefix: [] }, fs: fsPromises, platform: "linux" }),
|
|
153
|
+
(error: MainChannelError) => error.code === "main-shell-pack-failed" && error.cause instanceof Error && error.cause.message === tail.trim());
|
|
154
|
+
const go = "go: downloading github.com/gentleman-programming/gentle-ai/v4\ngo: verifying module: checksum mismatch\n";
|
|
155
|
+
const build = async (_command: string, _argv: string[], options: { stderrTail?: number }) => {
|
|
156
|
+
requested.push(options.stderrTail);
|
|
157
|
+
return { code: 1, stdout: "", stderrTail: go };
|
|
158
|
+
};
|
|
159
|
+
await assert.rejects(buildMainGentleAi({ commit: AI_SHA, ctx: s.ctx, platform: "linux", goPath: "/go/bin/go", run: build, fs: fsPromises }),
|
|
160
|
+
(error: MainChannelError) => error.code === "main-gentle-ai-build-failed" && error.cause instanceof Error && error.cause.message === go.trim());
|
|
161
|
+
assert.deepEqual(requested, [4096, 4096]);
|
|
162
|
+
// Without any stderr the error has no cause, as before.
|
|
163
|
+
await assert.rejects(buildMainGentleAi({ commit: AI_SHA, ctx: s.ctx, platform: "linux", goPath: "/go/bin/go", run: async () => ({ code: 1, stdout: "" }), fs: fsPromises }),
|
|
164
|
+
(error: MainChannelError) => error.code === "main-gentle-ai-build-failed" && error.cause === undefined);
|
|
165
|
+
} finally { s.cleanup(); }
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("Gentle AI main is built from the exact commit through Go's checksum database and registered as the override", posixOnly, async () => {
|
|
169
|
+
const s = sandbox();
|
|
170
|
+
try {
|
|
171
|
+
const goPath = join(s.root, "go", "bin", "go");
|
|
172
|
+
const calls: Call[] = [];
|
|
173
|
+
const run = async (command: string, argv: string[], options: Call["options"]) => {
|
|
174
|
+
calls.push({ command, argv, options });
|
|
175
|
+
if (argv[0] === "install") {
|
|
176
|
+
mkdirSync(options.env!.GOBIN, { recursive: true });
|
|
177
|
+
const binary = join(options.env!.GOBIN, "gentle-ai");
|
|
178
|
+
writeFileSync(binary, "#!/bin/sh\n");
|
|
179
|
+
chmodSync(binary, 0o755);
|
|
180
|
+
return { code: 0, stdout: "" };
|
|
181
|
+
}
|
|
182
|
+
return { code: 0, stdout: "gentle-ai 4.0.1-0.20261008202137-1f9d5e6423e3\n" };
|
|
183
|
+
};
|
|
184
|
+
const built = await buildMainGentleAi({ commit: AI_SHA, ctx: s.ctx, platform: "darwin", goPath, run, fs: fsPromises });
|
|
185
|
+
const install = calls[0];
|
|
186
|
+
assert.equal(install.command, goPath);
|
|
187
|
+
assert.deepEqual(install.argv, ["install", `${GENTLE_AI_MAIN_PACKAGE}@${AI_SHA}`]);
|
|
188
|
+
for (const [key, value] of Object.entries({ GOPROXY: "https://proxy.golang.org", GOSUMDB: "sum.golang.org", GOTOOLCHAIN: "local",
|
|
189
|
+
GOFLAGS: "-modcacherw", GONOSUMDB: "", GOPRIVATE: "", GOINSECURE: "", GOENV: "off", GOWORK: "off" })) assert.equal(install.options.env![key], value, key);
|
|
190
|
+
assert.equal(built.binaryPath, join(s.home, ".pi", "gentle-ai", "main", "gentle-ai", AI_SHA, "gentle-ai"));
|
|
191
|
+
assert.equal(built.version, "4.0.1-0.20261008202137-1f9d5e6423e3");
|
|
192
|
+
assert.deepEqual(JSON.parse(readFileSync(join(s.home, ".pi", "gentle-ai", "dev-binary.json"), "utf8")),
|
|
193
|
+
{ schema: "gentle-pi.dev-binary/v1", path: built.binaryPath });
|
|
194
|
+
} finally { s.cleanup(); }
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("a Gentle AI build whose version names another commit is rejected and never registered", posixOnly, async () => {
|
|
198
|
+
const s = sandbox();
|
|
199
|
+
try {
|
|
200
|
+
const run = async (_command: string, argv: string[], options: Call["options"]) => {
|
|
201
|
+
if (argv[0] === "install") {
|
|
202
|
+
mkdirSync(options.env!.GOBIN, { recursive: true });
|
|
203
|
+
writeFileSync(join(options.env!.GOBIN, "gentle-ai"), "#!/bin/sh\n", { mode: 0o755 });
|
|
204
|
+
}
|
|
205
|
+
return { code: 0, stdout: "gentle-ai 4.0.1-0.20261008202137-aaaaaaaaaaaa\n" };
|
|
206
|
+
};
|
|
207
|
+
await assert.rejects(buildMainGentleAi({ commit: AI_SHA, ctx: s.ctx, platform: "darwin", goPath: "/go/bin/go", run, fs: fsPromises }),
|
|
208
|
+
(error: MainChannelError) => error.code === "main-gentle-ai-unverified");
|
|
209
|
+
assert.equal(existsSync(join(s.home, ".pi", "gentle-ai", "dev-binary.json")), false);
|
|
210
|
+
} finally { s.cleanup(); }
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("the runner adapter resolves commits and records the channel through the given network and files", async () => {
|
|
214
|
+
const s = sandbox();
|
|
215
|
+
try {
|
|
216
|
+
const urls: string[] = [];
|
|
217
|
+
const adapter = mainChannelAdapter({ fs: fsPromises, fetch: async (url: string) => {
|
|
218
|
+
urls.push(url);
|
|
219
|
+
return { ok: true, status: 200, text: async () => AI_SHA };
|
|
220
|
+
} });
|
|
221
|
+
assert.equal(await adapter.resolveCommit("Gentleman-Programming/gentle-ai"), AI_SHA);
|
|
222
|
+
assert.deepEqual(urls, ["https://api.github.com/repos/Gentleman-Programming/gentle-ai/commits/main"]);
|
|
223
|
+
await adapter.writeChannel(s.ctx, { channel: "main", shellCommit: SHELL_SHA, gentleAiCommit: AI_SHA });
|
|
224
|
+
assert.deepEqual(await readChannel(s.ctx, fsPromises), { channel: "main", shellCommit: SHELL_SHA, gentleAiCommit: AI_SHA });
|
|
225
|
+
} finally { s.cleanup(); }
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("the owner of an installation is pnpm under PNPM_HOME, npm only inside npm's global root, otherwise unknown", () => {
|
|
229
|
+
const roots = { pnpmHome: "/u/Library/pnpm", npmRoot: "/u/.local/lib/node_modules" };
|
|
230
|
+
assert.equal(installOwner({ ...roots, packageRoot: "/u/Library/pnpm/global/v11/x/node_modules/gentle-pi" }), "pnpm");
|
|
231
|
+
assert.equal(installOwner({ ...roots, packageRoot: "/u/.local/lib/node_modules/gentle-pi" }), "npm");
|
|
232
|
+
for (const packageRoot of ["/u/work/gentle-pi", "/u/.local/lib/node_modules", "/u/.local/lib/node_modules/other/gentle-pi"]) {
|
|
233
|
+
assert.equal(installOwner({ ...roots, packageRoot }), null, packageRoot);
|
|
234
|
+
}
|
|
235
|
+
assert.equal(installOwner({ pnpmHome: null, npmRoot: null, packageRoot: "/u/.local/lib/node_modules/gentle-pi" }), null);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("installOwner attributes another named package, such as the scoped Pi, the same way", () => {
|
|
239
|
+
const roots = { pnpmHome: "/u/Library/pnpm", npmRoot: "/u/.local/lib/node_modules", name: "@earendil-works/pi-coding-agent" };
|
|
240
|
+
assert.equal(installOwner({ ...roots, packageRoot: "/u/.local/lib/node_modules/@earendil-works/pi-coding-agent" }), "npm");
|
|
241
|
+
assert.equal(installOwner({ ...roots, packageRoot: "/u/Library/pnpm/global/v11/x/node_modules/@earendil-works/pi-coding-agent" }), "pnpm");
|
|
242
|
+
for (const packageRoot of ["/u/.local/lib/node_modules/gentle-pi", "/u/.local/lib/node_modules/@earendil-works",
|
|
243
|
+
"/u/.local/share/mise/installs/pi/lib/node_modules/@earendil-works/pi-coding-agent"]) {
|
|
244
|
+
assert.equal(installOwner({ ...roots, packageRoot }), null, packageRoot);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("installOwner follows Windows paths on any host: backslashes and case-insensitive roots", () => {
|
|
249
|
+
const roots = { pnpmHome: "C:\\Users\\u\\AppData\\Local\\pnpm", npmRoot: "C:\\Users\\u\\AppData\\Roaming\\npm\\node_modules",
|
|
250
|
+
name: "@earendil-works/pi-coding-agent", platform: "win32" };
|
|
251
|
+
assert.equal(installOwner({ ...roots, packageRoot: "C:\\Users\\u\\AppData\\Roaming\\npm\\node_modules\\@earendil-works\\pi-coding-agent" }), "npm");
|
|
252
|
+
assert.equal(installOwner({ ...roots, packageRoot: "c:\\users\\U\\appdata\\roaming\\NPM\\Node_Modules\\@Earendil-Works\\Pi-Coding-Agent" }), "npm");
|
|
253
|
+
assert.equal(installOwner({ ...roots, packageRoot: "C:\\Users\\u\\AppData\\Local\\PNPM\\global\\v11\\x\\node_modules\\@earendil-works\\pi-coding-agent" }), "pnpm");
|
|
254
|
+
for (const packageRoot of ["C:\\Users\\u\\AppData\\Roaming\\npm\\node_modules\\other\\@earendil-works\\pi-coding-agent",
|
|
255
|
+
"C:\\Users\\u\\AppData\\Roaming\\npm\\node_modules", "D:\\Users\\u\\AppData\\Roaming\\npm\\node_modules\\@earendil-works\\pi-coding-agent",
|
|
256
|
+
"C:\\Users\\u\\AppData\\Local\\pnpm"]) {
|
|
257
|
+
assert.equal(installOwner({ ...roots, packageRoot }), null, packageRoot);
|
|
258
|
+
}
|
|
259
|
+
// POSIX paths keep their exact case.
|
|
260
|
+
assert.equal(installOwner({ pnpmHome: null, npmRoot: "/u/lib/node_modules", packageRoot: "/u/lib/node_modules/Gentle-Pi", platform: "linux" }), null);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// `gentle-shell upgrade --channel main` after the installer provided its own pinned Go.
|
|
264
|
+
const NEW_AI = "2222222222222222222222222222222222222222";
|
|
265
|
+
/** A main install whose Gentle Shell is current, so only Gentle AI is rebuilt with Go. */
|
|
266
|
+
async function upgradeWorld({ userGo = null as string | null, pinned = false, acquireFails = false }) {
|
|
267
|
+
const s = sandbox();
|
|
268
|
+
const goDirectory = join(s.home, ".pi", "gentle-ai", "tools", "go", "1.25.14");
|
|
269
|
+
const pinnedGo = join(goDirectory, "go", "bin", "go");
|
|
270
|
+
if (pinned) {
|
|
271
|
+
mkdirSync(join(goDirectory, "go", "bin"), { recursive: true, mode: 0o700 });
|
|
272
|
+
chmodSync(goDirectory, 0o700);
|
|
273
|
+
writeFileSync(pinnedGo, "#!/bin/sh\n", { mode: 0o755 });
|
|
274
|
+
writeFileSync(join(goDirectory, ".gentle-shell-go"), `${artifactFor("go", "darwin", "arm64").url}\n`);
|
|
275
|
+
}
|
|
276
|
+
await writeChannel(s.ctx, { channel: "main", shellCommit: SHELL_SHA, gentleAiCommit: AI_SHA }, fsPromises);
|
|
277
|
+
const calls: Call[] = [];
|
|
278
|
+
const fetch = async (url: string) => ({ ok: true, status: 200, text: async () => (url.endsWith("/gentle-ai/commits/main") ? NEW_AI : SHELL_SHA) });
|
|
279
|
+
const run = async (command: string, argv: string[], options: Call["options"]) => {
|
|
280
|
+
calls.push({ command, argv, options });
|
|
281
|
+
if (command === "/usr/bin/go" && argv[0] === "version") return { code: 0, stdout: `go version ${userGo === "devel" ? "devel go1.26-abc" : `go${userGo}`} darwin/arm64\n` };
|
|
282
|
+
if (argv[0] === "install") {
|
|
283
|
+
mkdirSync(options.env!.GOBIN, { recursive: true });
|
|
284
|
+
writeFileSync(join(options.env!.GOBIN, "gentle-ai"), "#!/bin/sh\n", { mode: 0o755 });
|
|
285
|
+
return { code: 0, stdout: "" };
|
|
286
|
+
}
|
|
287
|
+
return { code: 0, stdout: `gentle-ai 4.0.1-0.20261008202137-${NEW_AI.slice(0, 12)}\n` };
|
|
288
|
+
};
|
|
289
|
+
const which = async (name: string) => (name === "pnpm" || (name === "go" && userGo !== null) ? `/usr/bin/${name}` : null);
|
|
290
|
+
// The verified download itself is acquireGo's (installer-downloads tests); here it only publishes the pinned path.
|
|
291
|
+
const acquired: string[] = [];
|
|
292
|
+
const acquireGo = async () => {
|
|
293
|
+
acquired.push(pinnedGo);
|
|
294
|
+
// As acquireGo fails: wrapped, with the reason as its cause.
|
|
295
|
+
if (acquireFails) throw new Error("Go verified acquisition failed; nothing was published", { cause: new Error("Conflicting Go destination: /x") });
|
|
296
|
+
return { goPath: pinnedGo, version: "1.25.14", acquired: true };
|
|
297
|
+
};
|
|
298
|
+
const upgrade = () => runUpgrade({ args: ["--channel", "main"], ctx: s.ctx, platform: "darwin", arch: "arm64", packageRoot: join(s.root, "pkg"),
|
|
299
|
+
currentVersion: "4.0.0-main.6e7e3a18f794", adapters: { fetch, run, fs: fsPromises, which, acquireGo }, out: () => {} });
|
|
300
|
+
const builtWith = () => calls.find((call) => call.argv[0] === "install")?.command;
|
|
301
|
+
return { ...s, calls, pinnedGo, upgrade, builtWith, acquired };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
test("upgrading main uses the installer's pinned Go when the user's Go is missing, older or cannot be checked, and leaves the user's Go as it is", posixOnly, async () => {
|
|
305
|
+
for (const userGo of [null, "1.24.0", "devel"]) {
|
|
306
|
+
const w = await upgradeWorld({ userGo, pinned: true });
|
|
307
|
+
try {
|
|
308
|
+
assert.equal(await w.upgrade(), 0);
|
|
309
|
+
assert.equal(w.builtWith(), w.pinnedGo, String(userGo));
|
|
310
|
+
// The user's Go only reported its version.
|
|
311
|
+
assert.deepEqual(w.calls.filter((call) => call.command === "/usr/bin/go").map((call) => call.argv), userGo ? [["version"]] : []);
|
|
312
|
+
assert.deepEqual(w.acquired, [], "an already published pinned Go is reused");
|
|
313
|
+
} finally { w.cleanup(); }
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("upgrading main keeps using a user's Go that meets the requirement, even next to the pinned Go", posixOnly, async () => {
|
|
318
|
+
const w = await upgradeWorld({ userGo: "1.26.0", pinned: true });
|
|
319
|
+
try {
|
|
320
|
+
assert.equal(await w.upgrade(), 0);
|
|
321
|
+
assert.equal(w.builtWith(), "/usr/bin/go");
|
|
322
|
+
} finally { w.cleanup(); }
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("upgrading main without a usable Go downloads the installer's pinned Go once, only to build", posixOnly, async () => {
|
|
326
|
+
for (const userGo of [null, "1.24.0", "devel"]) {
|
|
327
|
+
const w = await upgradeWorld({ userGo });
|
|
328
|
+
try {
|
|
329
|
+
assert.equal(await w.upgrade(), 0);
|
|
330
|
+
assert.deepEqual(w.acquired, [w.pinnedGo], String(userGo));
|
|
331
|
+
assert.equal(w.builtWith(), w.pinnedGo, String(userGo));
|
|
332
|
+
assert.deepEqual(w.calls.filter((call) => call.command === "/usr/bin/go").map((call) => call.argv), userGo ? [["version"]] : []);
|
|
333
|
+
} finally { w.cleanup(); }
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("upgrading main stops before building when the pinned Go cannot be downloaded", posixOnly, async () => {
|
|
338
|
+
const w = await upgradeWorld({ userGo: "devel", acquireFails: true });
|
|
339
|
+
try {
|
|
340
|
+
await assert.rejects(w.upgrade(), (error: MainChannelError) => error.code === "main-requires-tools" &&
|
|
341
|
+
error.message.includes("Go 1.25.14") && error.message.includes("Conflicting Go destination: /x") && !error.message.includes("network"));
|
|
342
|
+
assert.equal(w.builtWith(), undefined);
|
|
343
|
+
} finally { w.cleanup(); }
|
|
344
|
+
});
|
|
@@ -66,8 +66,11 @@ function descriptor(overrides = {}) {
|
|
|
66
66
|
...overrides,
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
|
+
function isRoleError(error: unknown): error is Error & { kind: string; stage: string; mutationOutcome: string } {
|
|
70
|
+
return error instanceof Error && error instanceof ProviderRoleVectorError && "kind" in error && typeof error.kind === "string" && "stage" in error && typeof error.stage === "string" && "mutationOutcome" in error && typeof error.mutationOutcome === "string";
|
|
71
|
+
}
|
|
69
72
|
function rejects(obj, fragment) {
|
|
70
|
-
assert.throws(() => validateDescriptor(obj), (error) => error instanceof DescriptorValidationError && error.message.includes(fragment));
|
|
73
|
+
assert.throws(() => validateDescriptor(obj), (error) => error instanceof Error && error instanceof DescriptorValidationError && error.message.includes(fragment));
|
|
71
74
|
}
|
|
72
75
|
function tempDescriptor(t, obj) {
|
|
73
76
|
const directory = mkdtempSync(join(tmpdir(), "gentle-pi-maintainer-"));
|
|
@@ -102,11 +105,9 @@ test("rejects malformed descriptor fields with exact-shape errors (no defaults,
|
|
|
102
105
|
rejects({ ...descriptor(), cases: [{ ...descriptor().cases[0]!, captureArgumentTokens: [...CAPTURE_TOKENS].filter((t) => t !== "--materialize=true") }] }, "--materialize=true");
|
|
103
106
|
});
|
|
104
107
|
test("submission is validated through the real relay resolver before any process launches (exact shape)", () => {
|
|
105
|
-
const twoValues = structuredClone(SUBMISSION);
|
|
106
|
-
twoValues.values = [...SUBMISSION.values, { slot: "extra", domain: "artifact_path_or_stdin", substitutionLocation: 0 }];
|
|
108
|
+
const twoValues = { ...structuredClone(SUBMISSION), values: [...SUBMISSION.values, { slot: "extra", domain: "artifact_path_or_stdin", substitutionLocation: 0 }] };
|
|
107
109
|
rejects({ ...descriptor(), cases: [{ ...descriptor().cases[0]!, submission: twoValues }] }, "not a bindable provider form");
|
|
108
|
-
const noSlot = structuredClone(SUBMISSION);
|
|
109
|
-
noSlot.argumentTokens = [...BINDING_TOKENS, "--input=/no/value/slot"];
|
|
110
|
+
const noSlot = { ...structuredClone(SUBMISSION), argumentTokens: [...BINDING_TOKENS, "--input=/no/value/slot"] };
|
|
110
111
|
rejects({ ...descriptor(), cases: [{ ...descriptor().cases[0]!, submission: noSlot }] }, "not a bindable provider form");
|
|
111
112
|
rejects({ ...descriptor(), cases: [{ ...descriptor().cases[0]!, submission: { ...structuredClone(SUBMISSION), extra: 1 } }] }, "extra");
|
|
112
113
|
});
|
|
@@ -245,7 +246,7 @@ test("negative control: a role vector failure (not surface-unavailable) is repor
|
|
|
245
246
|
});
|
|
246
247
|
test("runProviderRoleVector treats a prelaunch abort as not-started with no mutation", async () => {
|
|
247
248
|
const controller = new AbortController(); controller.abort();
|
|
248
|
-
await assert.rejects(runProviderRoleVector({ kind: "provider-role-refuter", argumentTokens: REFUTER_TOKENS, gentleAiExecutable: process.execPath, signal: controller.signal }), (error) => error
|
|
249
|
+
await assert.rejects(runProviderRoleVector({ kind: "provider-role-refuter", argumentTokens: REFUTER_TOKENS, gentleAiExecutable: process.execPath, signal: controller.signal }), (error) => isRoleError(error) && error.kind === ROLE_VECTOR_FAILURE.ROLE_ABORTED && error.stage === "launch" && error.mutationOutcome === "none");
|
|
249
250
|
});
|
|
250
251
|
function roleChild(t: test.TestContext, name: string, source: string) {
|
|
251
252
|
const path = sandboxPath(`${name} preload.cjs`), savedNodeOptions = process.env.NODE_OPTIONS; writeFileSync(path, `const roleArgv = ["review", ...process.argv.slice(2)], Module = require("node:module"), resolveFilename = Module._resolveFilename; Module._resolveFilename = function (request, ...args) { return /[\\\\/]review$/.test(request) ? ${JSON.stringify(path)} : resolveFilename.call(this, request, ...args); }; ${source}`);
|
|
@@ -262,19 +263,19 @@ test("runProviderRoleVector executes both real stub-child verbs and decodes snak
|
|
|
262
263
|
test("runProviderRoleVector rejects malformed artifact binding shape before stale binding comparison", async (t) => {
|
|
263
264
|
for (const malformed of [{ lineage_id: "", target_identity: ROLE_TARGET }, { lineage_id: ROLE_LINEAGE, target_identity: "not-a-sha256" }]) {
|
|
264
265
|
const child = roleChild(t, `malformed-${malformed.lineage_id || "lineage"}.cjs`, `process.stdout.write(JSON.stringify({ schema: ${JSON.stringify(PROVIDER_ROLE_CAPTURE_ARTIFACT_SCHEMA)}, role: "refuter", captured: true, ...${JSON.stringify(malformed)} })); process.exit(0);`);
|
|
265
|
-
await assert.rejects(runProviderRoleVector({ kind: "provider-role-refuter", argumentTokens: REFUTER_TOKENS, gentleAiExecutable: child.executable, env: child.env }), (error) => error
|
|
266
|
+
await assert.rejects(runProviderRoleVector({ kind: "provider-role-refuter", argumentTokens: REFUTER_TOKENS, gentleAiExecutable: child.executable, env: child.env }), (error) => isRoleError(error) && error.kind === ROLE_VECTOR_FAILURE.ROLE_FAILED && /typed shape/.test(error.message));
|
|
266
267
|
}
|
|
267
268
|
});
|
|
268
269
|
test("runProviderRoleVector classifies an empty successful artifact as unknown and requires STATUS re-query", async (t) => {
|
|
269
270
|
const child = roleChild(t, "empty-artifact.cjs", "process.exit(0);");
|
|
270
271
|
await assert.rejects(runProviderRoleVector({ kind: "provider-role-refuter", argumentTokens: REFUTER_TOKENS, gentleAiExecutable: child.executable, env: child.env }), (caught) => {
|
|
271
|
-
assert.ok(caught
|
|
272
|
+
assert.ok(isRoleError(caught)); assert.equal(caught.kind, ROLE_VECTOR_FAILURE.EMPTY_ARTIFACT); assert.equal(caught.stage, "execute"); assert.equal(caught.mutationOutcome, "unknown"); assert.match(caught.message, /re-query negotiated STATUS before any retry/); return true;
|
|
272
273
|
});
|
|
273
274
|
});
|
|
274
275
|
test("runProviderRoleVector bounds each output stream before JSON or exit handling", async (t) => {
|
|
275
276
|
for (const stream of ["stdout", "stderr"] as const) {
|
|
276
277
|
const child = roleChild(t, `overflow-${stream}.cjs`, `process.${stream}.write("x".repeat(${ROLE_STREAM_MAX_BYTES + 1})); setInterval(() => {}, 1_000);`);
|
|
277
|
-
await assert.rejects(runProviderRoleVector({ kind: "provider-role-refuter", argumentTokens: REFUTER_TOKENS, gentleAiExecutable: child.executable, env: child.env, timeoutMs: 1_000 }), (error) => error
|
|
278
|
+
await assert.rejects(runProviderRoleVector({ kind: "provider-role-refuter", argumentTokens: REFUTER_TOKENS, gentleAiExecutable: child.executable, env: child.env, timeoutMs: 1_000 }), (error) => isRoleError(error) && error.kind === ROLE_VECTOR_FAILURE.ROLE_OUTPUT_OVERFLOW && error.mutationOutcome === "unknown");
|
|
278
279
|
}
|
|
279
280
|
});
|
|
280
281
|
async function descendantPid(path: string) {
|
|
@@ -293,7 +294,7 @@ test("role watchdog and abort reap descendants on every platform", async (t) =>
|
|
|
293
294
|
t.after(() => { if (pid > 0) try { process.kill(pid, "SIGKILL"); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error; } });
|
|
294
295
|
assert.ok(DEFAULT_ROLE_VECTOR_TIMEOUT_MS > 600_000, "the outer 900s watchdog FIRES after the provider-owned 600s deadline with setup/cancellation margin");
|
|
295
296
|
if (mode === "abort") controller.abort();
|
|
296
|
-
await assert.rejects(run, (error) => error
|
|
297
|
+
await assert.rejects(run, (error) => isRoleError(error) && error.kind === (mode === "abort" ? ROLE_VECTOR_FAILURE.ROLE_ABORTED : ROLE_VECTOR_FAILURE.ROLE_TIMED_OUT) && error.mutationOutcome === "unknown");
|
|
297
298
|
await assertReaped(pid);
|
|
298
299
|
});
|
|
299
300
|
});
|
|
@@ -302,7 +303,7 @@ test("failed tree termination settles despite a surviving descendant retaining i
|
|
|
302
303
|
const child = roleChild(t, "pipe-holder-role-child.cjs", `const fs = require("node:fs"), { spawn } = require("node:child_process"), grandchild = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { stdio: "inherit", env: { ...process.env, NODE_OPTIONS: "" } }); fs.writeFileSync(${JSON.stringify(sandboxPath("ROLE_PID_PLACEHOLDER"))}.replace("ROLE_PID_PLACEHOLDER", ${JSON.stringify("role-pipe-holder.pid")}), String(grandchild.pid)); setInterval(() => {}, 1000);`);
|
|
303
304
|
const run = runProviderRoleVector({ kind: "provider-role-refuter", argumentTokens: REFUTER_TOKENS, gentleAiExecutable: child.executable, env: child.env, signal: controller.signal, terminateProcessTree: (process) => { process.kill("SIGKILL"); return false; } });
|
|
304
305
|
const pid = await descendantPid(grandchild); t.after(() => { try { process.kill(pid, "SIGKILL"); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error; } });
|
|
305
|
-
const started = Date.now(); controller.abort(); await assert.rejects(run, (error) => error
|
|
306
|
+
const started = Date.now(); controller.abort(); await assert.rejects(run, (error) => isRoleError(error) && error.kind === ROLE_VECTOR_FAILURE.ROLE_TERMINATION_FAILED && error.stage === "execute" && error.mutationOutcome === "unknown");
|
|
306
307
|
assert.ok(Date.now() - started < 500, "failed tree termination must not wait for descendant-held pipes to close"); assert.doesNotThrow(() => process.kill(pid, 0), "the fixture descendant must survive while retaining inherited pipes");
|
|
307
308
|
});
|
|
308
309
|
test("stale-target fail-closed: missing or duplicate required binding tokens are rejected before runner invocation", () => {
|
|
@@ -495,7 +496,7 @@ const baselineArmed = typeof baselineBinary === "string" && baselineBinary.lengt
|
|
|
495
496
|
const baselineReason = () => `${BASELINE_ENV} is unset or not an existing absolute path; supply the baseline gentle-ai binary (external evidence establishes whether it is RC8), or set ${REQUIRE_ENV}=1 to fail instead of skip.`;
|
|
496
497
|
if (!baselineArmed && armed) throw new Error(baselineReason());
|
|
497
498
|
if (!baselineArmed) console.log(`tests/maintainer/provider-relay.maintest.ts: ${baselineReason()}`);
|
|
498
|
-
function baselineDescriptor(kind = "relay-unavailable"
|
|
499
|
+
function baselineDescriptor(kind: "relay-unavailable" | "positive-lens" = "relay-unavailable") {
|
|
499
500
|
return validateDescriptor({ ...descriptor(), gentleAiExecutable: baselineBinary, cases: [{ name: "baseline-negative-control", kind, captureArgumentTokens: [...CAPTURE_TOKENS], submission: structuredClone(SUBMISSION) }] });
|
|
500
501
|
}
|
|
501
502
|
test("negative control: runMatrix proves the baseline fails closed as relay-unavailable with zero mutation", { skip: !baselineArmed }, async () => {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { computeModelPriceRatio, modelRoutingLine, renderModelRoutingLine, type ModelCost, type ModelPriceLookup } from "../lib/model-price-ratio.ts";
|
|
4
|
+
|
|
5
|
+
// gentle-shell#1731 S3/AC3: the runtime computes the orchestrator/worker price
|
|
6
|
+
// ratio from catalog prices and renders it as one fact line; the model never
|
|
7
|
+
// estimates it.
|
|
8
|
+
|
|
9
|
+
const catalog: Record<string, ModelCost> = {
|
|
10
|
+
"anthropic/fable": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
|
|
11
|
+
"anthropic/opus": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
12
|
+
"openai/mixed": { input: 3, output: 10, cacheRead: 0.3, cacheWrite: 0 },
|
|
13
|
+
"local/free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
14
|
+
"odd/thirds": { input: 9, output: 9, cacheRead: 0, cacheWrite: 0 },
|
|
15
|
+
"odd/three": { input: 3, output: 3, cacheRead: 0, cacheWrite: 0 },
|
|
16
|
+
"odd/seven": { input: 7, output: 7, cacheRead: 0, cacheWrite: 0 },
|
|
17
|
+
};
|
|
18
|
+
const lookup: ModelPriceLookup = (model) => catalog[`${model.provider}/${model.id}`];
|
|
19
|
+
const fable = { provider: "anthropic", id: "fable" };
|
|
20
|
+
const opus = { provider: "anthropic", id: "opus" };
|
|
21
|
+
|
|
22
|
+
test("renders the input, output, and blended ratio for an expensive orchestrator", () => {
|
|
23
|
+
assert.equal(
|
|
24
|
+
modelRoutingLine(fable, opus, lookup),
|
|
25
|
+
"Model routing: orchestrator anthropic/fable costs 3.0x the worker anthropic/opus (input 3.0x, output 3.0x).",
|
|
26
|
+
);
|
|
27
|
+
const ratio = computeModelPriceRatio(fable, opus, lookup);
|
|
28
|
+
assert.equal(ratio.kind, "known");
|
|
29
|
+
if (ratio.kind === "known") {
|
|
30
|
+
assert.equal(ratio.input, 3);
|
|
31
|
+
assert.equal(ratio.output, 3);
|
|
32
|
+
assert.equal(ratio.blended, 3);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("the blended ratio is the larger of the input and output ratios", () => {
|
|
37
|
+
// fable/mixed: input 15/3 = 5x, output 75/10 = 7.5x.
|
|
38
|
+
assert.equal(
|
|
39
|
+
modelRoutingLine(fable, { provider: "openai", id: "mixed" }, lookup),
|
|
40
|
+
"Model routing: orchestrator anthropic/fable costs 7.5x the worker openai/mixed (input 5.0x, output 7.5x).",
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("the same model renders 1.0x", () => {
|
|
45
|
+
assert.equal(
|
|
46
|
+
modelRoutingLine(opus, opus, lookup),
|
|
47
|
+
"Model routing: orchestrator anthropic/opus costs 1.0x the worker anthropic/opus (input 1.0x, output 1.0x).",
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a cheaper orchestrator renders a ratio below 1 normally", () => {
|
|
52
|
+
assert.equal(
|
|
53
|
+
modelRoutingLine(opus, fable, lookup),
|
|
54
|
+
"Model routing: orchestrator anthropic/opus costs 0.3x the worker anthropic/fable (input 0.3x, output 0.3x).",
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("ratios round to one decimal", () => {
|
|
59
|
+
// 7/3 = 2.333..., 9/7 = 1.2857...
|
|
60
|
+
assert.equal(
|
|
61
|
+
modelRoutingLine({ provider: "odd", id: "seven" }, { provider: "odd", id: "three" }, lookup),
|
|
62
|
+
"Model routing: orchestrator odd/seven costs 2.3x the worker odd/three (input 2.3x, output 2.3x).",
|
|
63
|
+
);
|
|
64
|
+
assert.equal(
|
|
65
|
+
modelRoutingLine({ provider: "odd", id: "thirds" }, { provider: "odd", id: "seven" }, lookup),
|
|
66
|
+
"Model routing: orchestrator odd/thirds costs 1.3x the worker odd/seven (input 1.3x, output 1.3x).",
|
|
67
|
+
);
|
|
68
|
+
assert.equal(renderModelRoutingLine({ kind: "known", orchestrator: "a/x", worker: "b/y", input: 2.95, output: 1.04, blended: 2.95 }),
|
|
69
|
+
"Model routing: orchestrator a/x costs 3.0x the worker b/y (input 3.0x, output 1.0x).");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a provider-less model reference renders its bare id", () => {
|
|
73
|
+
const bare: ModelPriceLookup = (model) => (model.id === "opus" ? catalog["anthropic/opus"] : undefined);
|
|
74
|
+
assert.equal(
|
|
75
|
+
modelRoutingLine({ provider: undefined, id: "opus" }, { provider: undefined, id: "opus" }, bare),
|
|
76
|
+
"Model routing: orchestrator opus costs 1.0x the worker opus (input 1.0x, output 1.0x).",
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("a missing orchestrator or worker model renders the unknown line", () => {
|
|
81
|
+
assert.equal(modelRoutingLine(undefined, opus, lookup), "Model routing: price ratio unknown (orchestrator model unknown).");
|
|
82
|
+
assert.equal(modelRoutingLine(fable, undefined, lookup), "Model routing: price ratio unknown (worker model unknown).");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("a missing catalog price renders the unknown line naming the model", () => {
|
|
86
|
+
assert.equal(
|
|
87
|
+
modelRoutingLine(fable, { provider: "x", id: "unpriced" }, lookup),
|
|
88
|
+
"Model routing: price ratio unknown (no catalog price for x/unpriced).",
|
|
89
|
+
);
|
|
90
|
+
assert.equal(
|
|
91
|
+
modelRoutingLine({ provider: "x", id: "unpriced" }, opus, lookup),
|
|
92
|
+
"Model routing: price ratio unknown (no catalog price for x/unpriced).",
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("a zero or invalid input or output price renders the unknown line", () => {
|
|
97
|
+
assert.equal(
|
|
98
|
+
modelRoutingLine(fable, { provider: "local", id: "free" }, lookup),
|
|
99
|
+
"Model routing: price ratio unknown (zero or invalid price for local/free).",
|
|
100
|
+
);
|
|
101
|
+
assert.equal(
|
|
102
|
+
modelRoutingLine({ provider: "local", id: "free" }, opus, lookup),
|
|
103
|
+
"Model routing: price ratio unknown (zero or invalid price for local/free).",
|
|
104
|
+
);
|
|
105
|
+
const broken: ModelPriceLookup = (model) => (model.id === "opus" ? { input: Number.NaN, output: 25, cacheRead: 0, cacheWrite: 0 } : catalog["anthropic/fable"]);
|
|
106
|
+
assert.equal(modelRoutingLine(fable, opus, broken), "Model routing: price ratio unknown (zero or invalid price for anthropic/opus).");
|
|
107
|
+
const negative: ModelPriceLookup = (model) => (model.id === "opus" ? { input: 5, output: -1, cacheRead: 0, cacheWrite: 0 } : catalog["anthropic/fable"]);
|
|
108
|
+
assert.equal(modelRoutingLine(fable, opus, negative), "Model routing: price ratio unknown (zero or invalid price for anthropic/opus).");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("a throwing price lookup never throws and renders the unknown line", () => {
|
|
112
|
+
const throwing: ModelPriceLookup = () => {
|
|
113
|
+
throw new Error("registry offline");
|
|
114
|
+
};
|
|
115
|
+
assert.equal(modelRoutingLine(fable, opus, throwing), "Model routing: price ratio unknown (price lookup failed).");
|
|
116
|
+
});
|