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,320 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { createServer, type Server } from "node:net";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import { PulseClient } from "../lib/notification-pulse-client.ts";
|
|
8
|
+
import { boundedPulseTagReader, boundedPulseTagWriter } from "../lib/notification-pulse-protocol.ts";
|
|
9
|
+
import { parsePulseWav, playPulseWav } from "../lib/notification-pulse-stream.ts";
|
|
10
|
+
|
|
11
|
+
// --- independent golden encoders (deliberately NOT the production codec) ---
|
|
12
|
+
const taggedU32 = (value: number): Buffer => { const b = Buffer.alloc(5); b[0] = 0x4c; b.writeUInt32BE(value, 1); return b; };
|
|
13
|
+
const taggedString = (value: string): Buffer => Buffer.concat([Buffer.from([0x74]), Buffer.from(value, "utf8"), Buffer.from([0x00])]);
|
|
14
|
+
const boolFalse = (): Buffer => Buffer.from([0x30]);
|
|
15
|
+
const usec = (value: number): Buffer => { const b = Buffer.alloc(9); b[0] = 0x55; b.writeBigUInt64BE(BigInt(value), 1); return b; };
|
|
16
|
+
const sampleSpec = (format: number, channels: number, rate: number): Buffer => { const b = Buffer.alloc(7); b[0] = 0x61; b[1] = format; b[2] = channels; b.writeUInt32BE(rate, 3); return b; };
|
|
17
|
+
const channelMap = (channels: number): Buffer => Buffer.from([0x6d, channels, ...(channels === 1 ? [0] : [1, 2])]);
|
|
18
|
+
const reply = (command: number, tag: number, ...args: Buffer[]): Buffer => Buffer.concat([taggedU32(command), taggedU32(tag), ...args]);
|
|
19
|
+
const frame = (channel: number, payload: Buffer, flags = 0): Buffer => {
|
|
20
|
+
const header = Buffer.alloc(20);
|
|
21
|
+
header.writeUInt32BE(payload.length, 0); header.writeUInt32BE(channel, 4); header.writeUInt32BE(flags, 16);
|
|
22
|
+
return Buffer.concat([header, payload]);
|
|
23
|
+
};
|
|
24
|
+
const requestPayload = (index: number, bytes: number): Buffer => reply(61, 0xffffffff, taggedU32(index), taggedU32(bytes));
|
|
25
|
+
const taggedValueAt = (payload: Buffer, at: number): number => payload.readUInt32BE(at + 1);
|
|
26
|
+
const commandOf = (payload: Buffer): number => taggedValueAt(payload, 0);
|
|
27
|
+
const tagOf = (payload: Buffer): number => taggedValueAt(payload, 5);
|
|
28
|
+
|
|
29
|
+
// --- valid WAV builder (content is what the 9a validator already accepts) ---
|
|
30
|
+
function wav(bits: number, channels: number, rate: number, frames: number): Buffer {
|
|
31
|
+
const sampleBytes = bits / 8;
|
|
32
|
+
const dataSize = frames * channels * sampleBytes;
|
|
33
|
+
const b = Buffer.alloc(44 + dataSize, 0x11);
|
|
34
|
+
b.write("RIFF", 0); b.writeUInt32LE(36 + dataSize, 4); b.write("WAVE", 8);
|
|
35
|
+
b.write("fmt ", 12); b.writeUInt32LE(16, 16); b.writeUInt16LE(1, 20);
|
|
36
|
+
b.writeUInt16LE(channels, 22); b.writeUInt32LE(rate, 24);
|
|
37
|
+
b.writeUInt32LE(rate * channels * sampleBytes, 28);
|
|
38
|
+
b.writeUInt16LE(channels * sampleBytes, 32); b.writeUInt16LE(bits, 34);
|
|
39
|
+
b.write("data", 36); b.writeUInt32LE(dataSize, 40);
|
|
40
|
+
return b;
|
|
41
|
+
}
|
|
42
|
+
interface PlayState { seen: number[]; pcm: Buffer[]; createPayload?: Buffer; streamIndex: number; drainSender?: () => void; }
|
|
43
|
+
interface FakeServer { readonly path: string; readonly state: PlayState; close(): Promise<void>; }
|
|
44
|
+
interface PlayScript {
|
|
45
|
+
streamIndex?: number; missing?: number; requests?: number[]; earlyRequestBytes?: number;
|
|
46
|
+
corruptSpec?: boolean; closeAfterFirstChunk?: boolean; errorOnDrain?: boolean; holdDrain?: boolean; unknownRequestIndex?: number;
|
|
47
|
+
}
|
|
48
|
+
async function startPlaybackServer(script: PlayScript): Promise<FakeServer> {
|
|
49
|
+
const dir = await mkdtemp(join(tmpdir(), "pulse-stream-"));
|
|
50
|
+
const path = join(dir, "native");
|
|
51
|
+
const state: PlayState = { seen: [], pcm: [], streamIndex: script.streamIndex ?? 7 };
|
|
52
|
+
const createReply = (tag: number, spec: { format: number; channels: number; rate: number }): Buffer => {
|
|
53
|
+
const echoed = script.corruptSpec ? { format: spec.format === 0 ? 3 : 0, channels: spec.channels, rate: spec.rate } : spec;
|
|
54
|
+
return reply(2, tag, taggedU32(state.streamIndex), taggedU32(state.streamIndex + 1), taggedU32(script.missing ?? 0),
|
|
55
|
+
taggedU32(0xffffffff), taggedU32(0), taggedU32(0), taggedU32(0),
|
|
56
|
+
sampleSpec(echoed.format, echoed.channels, echoed.rate), channelMap(spec.channels),
|
|
57
|
+
taggedU32(0), taggedString("auto_null"), boolFalse(), usec(0));
|
|
58
|
+
};
|
|
59
|
+
const server: Server = createServer(socket => {
|
|
60
|
+
let buffer = Buffer.alloc(0);
|
|
61
|
+
const write = (value: Buffer) => { if (!socket.destroyed) socket.write(value); };
|
|
62
|
+
const send = (payload: Buffer) => write(frame(0xffffffff, payload));
|
|
63
|
+
socket.on("error", () => { /* client closed mid-write */ });
|
|
64
|
+
socket.on("data", chunk => {
|
|
65
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
66
|
+
for (;;) {
|
|
67
|
+
if (buffer.length < 20) break;
|
|
68
|
+
const length = buffer.readUInt32BE(0);
|
|
69
|
+
if (buffer.length < 20 + length) break;
|
|
70
|
+
const payload = Buffer.from(buffer.subarray(20, 20 + length));
|
|
71
|
+
const channel = buffer.readUInt32BE(4);
|
|
72
|
+
buffer = buffer.subarray(20 + length);
|
|
73
|
+
if (channel !== 0xffffffff) { state.pcm.push(payload); if (script.closeAfterFirstChunk) socket.destroy(); continue; }
|
|
74
|
+
const command = commandOf(payload); const tag = tagOf(payload);
|
|
75
|
+
state.seen.push(command);
|
|
76
|
+
if (command === 8) { send(reply(2, tag, taggedU32(35))); continue; }
|
|
77
|
+
if (command === 9) { send(reply(2, tag, taggedU32(0))); continue; }
|
|
78
|
+
if (command === 4) { send(reply(2, tag)); continue; }
|
|
79
|
+
if (command === 12) {
|
|
80
|
+
if (script.errorOnDrain) { send(reply(0, tag, taggedU32(6))); continue; }
|
|
81
|
+
if (script.holdDrain) { state.drainSender = () => send(reply(2, tag)); continue; }
|
|
82
|
+
send(reply(2, tag)); continue;
|
|
83
|
+
}
|
|
84
|
+
if (command === 3) {
|
|
85
|
+
state.createPayload = Buffer.from(payload);
|
|
86
|
+
const spec = { format: payload[11]!, channels: payload[12]!, rate: payload.readUInt32BE(13) };
|
|
87
|
+
let out = frame(0xffffffff, createReply(tag, spec));
|
|
88
|
+
if (script.earlyRequestBytes) out = Buffer.concat([out, frame(0xffffffff, requestPayload(state.streamIndex, script.earlyRequestBytes))]);
|
|
89
|
+
write(out);
|
|
90
|
+
for (const bytes of script.requests ?? []) write(frame(0xffffffff, requestPayload(script.unknownRequestIndex ?? state.streamIndex, bytes)));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
await new Promise<void>((resolve, reject) => { server.once("error", reject); server.listen(path, () => resolve()); });
|
|
97
|
+
return { path, state, close: async () => { await new Promise<void>(resolve => server.close(() => resolve())); await rm(dir, { recursive: true, force: true }); } };
|
|
98
|
+
}
|
|
99
|
+
function fakeClock() {
|
|
100
|
+
let now = 0; let counter = 0;
|
|
101
|
+
const jobs = new Map<number, { at: number; fn: () => void }>();
|
|
102
|
+
return {
|
|
103
|
+
setTimeout: (fn: () => void, ms: number) => { const id = ++counter; jobs.set(id, { at: now + ms, fn }); return id; },
|
|
104
|
+
clearTimeout: (id: number | NodeJS.Timeout) => { jobs.delete(id as number); },
|
|
105
|
+
advance(ms: number) { now += ms; for (const [id, job] of [...jobs]) if (job.at <= now) { jobs.delete(id); job.fn(); } },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const pcmOf = (bytes: Buffer[]): Buffer => Buffer.concat(bytes);
|
|
109
|
+
const dataOf = (bytes: Buffer): Buffer => { let offset = 12; for (;;) { const id = bytes.toString("ascii", offset, offset + 4); const size = bytes.readUInt32LE(offset + 4); if (id === "data") return bytes.subarray(offset + 8, offset + 8 + size); offset += 8 + size + (size % 2); } };
|
|
110
|
+
|
|
111
|
+
test("parsePulseWav extracts PCM and maps 8/16/24/32-bit mono/stereo", () => {
|
|
112
|
+
const cases: Array<[number, number, number]> = [[8, 1, 8000], [16, 2, 44100], [24, 1, 48000], [32, 2, 192000]];
|
|
113
|
+
for (const [bits, channels, rate] of cases) {
|
|
114
|
+
const bytes = wav(bits, channels, rate, 100);
|
|
115
|
+
const pcm = parsePulseWav(bytes);
|
|
116
|
+
assert.equal(pcm.sampleSpec.format, bits === 8 ? 0 : bits === 16 ? 3 : bits === 24 ? 9 : 7, `${bits}bit`);
|
|
117
|
+
assert.equal(pcm.sampleSpec.channels, channels);
|
|
118
|
+
assert.equal(pcm.sampleSpec.rate, rate);
|
|
119
|
+
assert.equal(pcm.channelMap.length, channels);
|
|
120
|
+
assert.equal(pcm.frameSize, channels * bits / 8);
|
|
121
|
+
assert.deepEqual(pcm.data, dataOf(bytes));
|
|
122
|
+
assert.ok(Object.isFrozen(pcm));
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("parsePulseWav rejects invalid and oversized bytes while accepting the 10 s boundary without IO", () => {
|
|
127
|
+
assert.throws(() => parsePulseWav(Buffer.alloc(8)));
|
|
128
|
+
assert.throws(() => parsePulseWav(wav(16, 1, 8000, 100).subarray(0, 40)));
|
|
129
|
+
assert.throws(() => parsePulseWav(wav(16, 3, 8000, 10)));
|
|
130
|
+
assert.throws(() => parsePulseWav(wav(16, 1, 4000, 10)));
|
|
131
|
+
assert.equal(parsePulseWav(wav(16, 1, 8000, 80000)).durationMs, 10000); // exact 10 s boundary
|
|
132
|
+
assert.equal(parsePulseWav(wav(16, 1, 8000, 44640)).durationMs, 5580); // previously rejected 5.58 s
|
|
133
|
+
assert.throws(() => parsePulseWav(wav(16, 1, 8000, 80001))); // just past 10 s
|
|
134
|
+
assert.throws(() => parsePulseWav(wav(32, 2, 192000, 300000))); // > 2 MiB
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("playPulseWav rejects invalid bytes before opening a socket", async () => {
|
|
138
|
+
const server = await startPlaybackServer({});
|
|
139
|
+
try {
|
|
140
|
+
await assert.rejects(playPulseWav(Buffer.alloc(8), undefined, { clientOptions: { socketPath: server.path, cookie: Buffer.alloc(256) } }));
|
|
141
|
+
assert.deepEqual(server.state.seen, []);
|
|
142
|
+
assert.deepEqual(server.state.pcm, []);
|
|
143
|
+
} finally { await server.close(); }
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("golden CREATE v13 frame pins spec, default sink, flags, volume and proplist", async () => {
|
|
147
|
+
const bytes = wav(16, 2, 44100, 100);
|
|
148
|
+
const server = await startPlaybackServer({ missing: dataOf(bytes).length });
|
|
149
|
+
try {
|
|
150
|
+
await playPulseWav(bytes, undefined, { clientOptions: { socketPath: server.path, cookie: Buffer.alloc(256) } });
|
|
151
|
+
const payload = server.state.createPayload!;
|
|
152
|
+
assert.equal(commandOf(payload), 3);
|
|
153
|
+
assert.equal(payload[10], 0x61); // sample_spec
|
|
154
|
+
assert.equal(payload[11], 3); assert.equal(payload[12], 2); assert.equal(payload.readUInt32BE(13), 44100);
|
|
155
|
+
assert.equal(payload[17], 0x6d); // channel_map
|
|
156
|
+
assert.equal(payload[18], 2);
|
|
157
|
+
const sinkIndexAt = 17 + 2 + 2;
|
|
158
|
+
assert.equal(taggedValueAt(payload, sinkIndexAt), 0xffffffff);
|
|
159
|
+
assert.equal(payload[sinkIndexAt + 5], 0x4e); // sink_name null
|
|
160
|
+
const proplistAt = payload.indexOf(0x50, sinkIndexAt);
|
|
161
|
+
assert.ok(payload.indexOf(Buffer.from("media.name\0", "latin1")) > proplistAt);
|
|
162
|
+
assert.ok(payload.indexOf(Buffer.from("media.role\0", "latin1")) > proplistAt);
|
|
163
|
+
assert.ok(payload.indexOf(Buffer.from("event\0", "latin1")) > proplistAt);
|
|
164
|
+
// seven explicit false flags, muted false, adjust_latency false
|
|
165
|
+
assert.ok(payload.subarray(sinkIndexAt).includes(boolFalse()));
|
|
166
|
+
assert.deepEqual(pcmOf(server.state.pcm), dataOf(bytes));
|
|
167
|
+
assert.deepEqual(server.state.seen.slice(-2), [12, 4]);
|
|
168
|
+
} finally { await server.close(); }
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("initial missing plus later REQUEST drives aligned PCM and completes drain/delete", async () => {
|
|
172
|
+
const bytes = wav(16, 1, 8000, 500); const data = dataOf(bytes);
|
|
173
|
+
const server = await startPlaybackServer({ missing: 2, requests: [data.length - 2] });
|
|
174
|
+
try {
|
|
175
|
+
await playPulseWav(bytes, undefined, { clientOptions: { socketPath: server.path, cookie: Buffer.alloc(256) } });
|
|
176
|
+
assert.deepEqual(pcmOf(server.state.pcm), data);
|
|
177
|
+
for (const chunk of server.state.pcm) assert.equal(chunk.length % 2, 0);
|
|
178
|
+
assert.deepEqual(server.state.seen.slice(-2), [12, 4]);
|
|
179
|
+
} finally { await server.close(); }
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("early coalesced REQUEST right after the CREATE reply is queued and folded", async () => {
|
|
183
|
+
const bytes = wav(16, 1, 8000, 200); const data = dataOf(bytes);
|
|
184
|
+
const server = await startPlaybackServer({ missing: 2, earlyRequestBytes: data.length - 2 });
|
|
185
|
+
try {
|
|
186
|
+
await playPulseWav(bytes, undefined, { clientOptions: { socketPath: server.path, cookie: Buffer.alloc(256) } });
|
|
187
|
+
assert.deepEqual(pcmOf(server.state.pcm), data);
|
|
188
|
+
} finally { await server.close(); }
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("tiny unaligned requests accumulate until a whole frame and huge requests stay bounded", async () => {
|
|
192
|
+
const bytes = wav(16, 1, 8000, 64); const data = dataOf(bytes);
|
|
193
|
+
const server = await startPlaybackServer({ missing: 0, requests: [1, 1, 1, 0xffffffff] });
|
|
194
|
+
try {
|
|
195
|
+
await playPulseWav(bytes, undefined, { clientOptions: { socketPath: server.path, cookie: Buffer.alloc(256) } });
|
|
196
|
+
assert.deepEqual(pcmOf(server.state.pcm), data);
|
|
197
|
+
assert.ok(server.state.pcm.every(chunk => chunk.length <= 64 * 1024 && chunk.length % 2 === 0));
|
|
198
|
+
} finally { await server.close(); }
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("mismatched spec and unknown-stream requests fail before/without PCM", async () => {
|
|
202
|
+
const bytes = wav(16, 1, 8000, 100);
|
|
203
|
+
const mismatch = await startPlaybackServer({ missing: dataOf(bytes).length, corruptSpec: true });
|
|
204
|
+
try { await assert.rejects(playPulseWav(bytes, undefined, { clientOptions: { socketPath: mismatch.path, cookie: Buffer.alloc(256) } }), /Pulse stream/); }
|
|
205
|
+
finally { await mismatch.close(); }
|
|
206
|
+
|
|
207
|
+
const unknown = await startPlaybackServer({ missing: 0, requests: [100], unknownRequestIndex: 99 });
|
|
208
|
+
try { await assert.rejects(playPulseWav(bytes, undefined, { clientOptions: { socketPath: unknown.path, cookie: Buffer.alloc(256) } }), /Pulse stream/); }
|
|
209
|
+
finally { await unknown.close(); }
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("ERROR on drain and truncated PCM fail closed with best-effort cleanup", async () => {
|
|
213
|
+
const bytes = wav(16, 1, 8000, 300);
|
|
214
|
+
const errorServer = await startPlaybackServer({ missing: dataOf(bytes).length, errorOnDrain: true });
|
|
215
|
+
try {
|
|
216
|
+
await assert.rejects(playPulseWav(bytes, undefined, { clientOptions: { socketPath: errorServer.path, cookie: Buffer.alloc(256) } }), /Pulse client|Pulse stream/);
|
|
217
|
+
assert.deepEqual(pcmOf(errorServer.state.pcm), dataOf(bytes)); // no extra PCM after the error
|
|
218
|
+
} finally { await errorServer.close(); }
|
|
219
|
+
|
|
220
|
+
const truncating = await startPlaybackServer({ missing: dataOf(bytes).length, closeAfterFirstChunk: true });
|
|
221
|
+
try { await assert.rejects(playPulseWav(bytes, undefined, { clientOptions: { socketPath: truncating.path, cookie: Buffer.alloc(256) } }), /Pulse client|Pulse stream/); }
|
|
222
|
+
finally { await truncating.close(); }
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("a stalled demand fails by the playback budget instead of hanging", async () => {
|
|
226
|
+
const bytes = wav(16, 1, 8000, 100);
|
|
227
|
+
const server = await startPlaybackServer({ missing: 2, requests: [] });
|
|
228
|
+
try { await assert.rejects(playPulseWav(bytes, undefined, { clientOptions: { socketPath: server.path, cookie: Buffer.alloc(256) }, budgetMs: 120 }), /budget|Pulse/); }
|
|
229
|
+
finally { await server.close(); }
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("DRAIN may exceed the 900 ms default via a bounded per-request deadline", async () => {
|
|
233
|
+
const bytes = wav(16, 1, 8000, 100); const data = dataOf(bytes);
|
|
234
|
+
const server = await startPlaybackServer({ missing: data.length, holdDrain: true });
|
|
235
|
+
const clock = fakeClock();
|
|
236
|
+
try {
|
|
237
|
+
const client = new PulseClient({ socketPath: server.path, cookie: Buffer.alloc(256), requestTimeoutMs: 900, setTimeout: clock.setTimeout, clearTimeout: clock.clearTimeout });
|
|
238
|
+
await client.connect();
|
|
239
|
+
let settled = false;
|
|
240
|
+
const playing = playPulseWav(bytes, undefined, { client, requestTimeoutMs: 6000 }).then(() => { settled = true; }, error => { settled = true; throw error; });
|
|
241
|
+
for (let i = 0; i < 20 && !server.state.drainSender; i++) await new Promise(resolve => setTimeout(resolve, 5));
|
|
242
|
+
assert.ok(server.state.drainSender, "drain reached");
|
|
243
|
+
clock.advance(1200); // beyond the 900 ms default, within the 6000 ms override
|
|
244
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
245
|
+
assert.equal(settled, false);
|
|
246
|
+
server.state.drainSender!();
|
|
247
|
+
await playing;
|
|
248
|
+
await client.close();
|
|
249
|
+
} finally { await server.close(); }
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("default engine request deadline allows a full 10 s drain instead of the old 6 s", async () => {
|
|
253
|
+
const bytes = wav(16, 1, 8000, 100); const data = dataOf(bytes);
|
|
254
|
+
const server = await startPlaybackServer({ missing: data.length, holdDrain: true });
|
|
255
|
+
const clock = fakeClock();
|
|
256
|
+
try {
|
|
257
|
+
const client = new PulseClient({ socketPath: server.path, cookie: Buffer.alloc(256), requestTimeoutMs: 900, setTimeout: clock.setTimeout, clearTimeout: clock.clearTimeout });
|
|
258
|
+
await client.connect();
|
|
259
|
+
let settled = false;
|
|
260
|
+
const playing = playPulseWav(bytes, undefined, { client }).then(() => { settled = true; }, () => { settled = true; });
|
|
261
|
+
for (let i = 0; i < 20 && !server.state.drainSender; i++) await new Promise(resolve => setTimeout(resolve, 5));
|
|
262
|
+
assert.ok(server.state.drainSender, "drain reached");
|
|
263
|
+
clock.advance(10000); // past the old 6000 ms deadline, still inside the 10 s duration
|
|
264
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
265
|
+
assert.equal(settled, false);
|
|
266
|
+
clock.advance(1000); // now past the new 11000 ms deadline
|
|
267
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
268
|
+
assert.equal(settled, true);
|
|
269
|
+
await playing;
|
|
270
|
+
await client.close();
|
|
271
|
+
} finally { await server.close(); }
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("abort during drain best-effort deletes and closes without extra PCM", async () => {
|
|
275
|
+
const bytes = wav(16, 1, 8000, 100); const data = dataOf(bytes);
|
|
276
|
+
const server = await startPlaybackServer({ missing: data.length, holdDrain: true });
|
|
277
|
+
try {
|
|
278
|
+
const client = new PulseClient({ socketPath: server.path, cookie: Buffer.alloc(256) });
|
|
279
|
+
await client.connect();
|
|
280
|
+
const controller = new AbortController();
|
|
281
|
+
const playing = playPulseWav(bytes, controller.signal, { client });
|
|
282
|
+
for (let i = 0; i < 20 && !server.state.drainSender; i++) await new Promise(resolve => setTimeout(resolve, 5));
|
|
283
|
+
controller.abort();
|
|
284
|
+
await assert.rejects(playing, /aborted|Pulse/);
|
|
285
|
+
assert.deepEqual(pcmOf(server.state.pcm), data);
|
|
286
|
+
assert.ok(server.state.seen.includes(4)); // best-effort DELETE while the link is still usable
|
|
287
|
+
await client.close();
|
|
288
|
+
} finally { await server.close(); }
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("sendDataFrame returns a promise for backpressure and closes asynchronously", async () => {
|
|
292
|
+
const server = await startPlaybackServer({});
|
|
293
|
+
try {
|
|
294
|
+
const client = new PulseClient({ socketPath: server.path, cookie: Buffer.alloc(256) });
|
|
295
|
+
await client.connect();
|
|
296
|
+
assert.ok(client.sendDataFrame(5, Buffer.from([1, 2])) instanceof Promise);
|
|
297
|
+
await client.sendDataFrame(5, Buffer.from([3, 4]));
|
|
298
|
+
await client.close();
|
|
299
|
+
} finally { await server.close(); }
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("more than 32 coalesced early REQUESTs fail closed before any PCM", async () => {
|
|
303
|
+
type Listener = (command: number, reader: ReturnType<typeof boundedPulseTagReader>) => void;
|
|
304
|
+
const listeners = new Set<Listener>();
|
|
305
|
+
const requestReader = (index: number, bytes: number) => boundedPulseTagReader(boundedPulseTagWriter().u32(index).u32(bytes).finish());
|
|
306
|
+
const createReply = boundedPulseTagWriter().u32(7).u32(8).u32(0).u32(0).u32(0).u32(0).u32(0)
|
|
307
|
+
.sampleSpec({ format: 3, channels: 1, rate: 8000 }).channelMap([0]).u32(0).string(null).boolean(false).usec(0n).finish();
|
|
308
|
+
const sent: Buffer[] = [];
|
|
309
|
+
const client = {
|
|
310
|
+
onEvent: (listener: Listener) => { listeners.add(listener); return () => { listeners.delete(listener); }; },
|
|
311
|
+
request: async (command: number) => {
|
|
312
|
+
if (command === 3) for (let index = 0; index < 33; index++) for (const listener of listeners) listener(61, requestReader(7, 1));
|
|
313
|
+
return boundedPulseTagReader(createReply);
|
|
314
|
+
},
|
|
315
|
+
sendDataFrame: async (_channel: number, payload: Uint8Array) => { sent.push(Buffer.from(payload)); },
|
|
316
|
+
close: async () => {},
|
|
317
|
+
} as unknown as PulseClient;
|
|
318
|
+
await assert.rejects(playPulseWav(wav(16, 1, 8000, 64), undefined, { client }), /early|too many/);
|
|
319
|
+
assert.deepEqual(sent, []);
|
|
320
|
+
});
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { NotificationScheduler, type PlaybackPermit } from "../lib/notification-scheduler.ts";
|
|
4
|
+
import { DEFAULT_NOTIFICATION_SETTINGS, type NotificationSettings, type NotificationSound } from "../lib/notification-policy.ts";
|
|
5
|
+
|
|
6
|
+
class Clock {
|
|
7
|
+
now = 0;
|
|
8
|
+
serial = 0;
|
|
9
|
+
timers = new Map<number, { at: number; fn: () => void }>();
|
|
10
|
+
setTimeout = (fn: () => void, delay: number) => {
|
|
11
|
+
const id = ++this.serial; this.timers.set(id, { at: this.now + delay, fn }); return id;
|
|
12
|
+
};
|
|
13
|
+
clearTimeout = (id: number) => { this.timers.delete(id); };
|
|
14
|
+
advance(ms: number) {
|
|
15
|
+
const end = this.now + ms;
|
|
16
|
+
for (;;) {
|
|
17
|
+
const next = [...this.timers].filter(([, timer]) => timer.at <= end).sort((a, b) => a[1].at - b[1].at)[0];
|
|
18
|
+
if (!next) break;
|
|
19
|
+
this.now = next[1].at; this.timers.delete(next[0]); next[1].fn();
|
|
20
|
+
}
|
|
21
|
+
this.now = end;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const tick = async () => { for (let i = 0; i < 6; i++) await Promise.resolve(); };
|
|
25
|
+
function fixture(slow = false, dedupeCapacity = 256) {
|
|
26
|
+
const clock = new Clock();
|
|
27
|
+
const settings: NotificationSettings = { ...structuredClone(DEFAULT_NOTIFICATION_SETTINGS), enabled: true };
|
|
28
|
+
const calls: Array<{ sound: NotificationSound; signal: AbortSignal; permit: PlaybackPermit; finish: () => void; fail: () => void }> = [];
|
|
29
|
+
const starts: number[] = []; const errors: unknown[] = [];
|
|
30
|
+
const scheduler = new NotificationScheduler({ now: () => clock.now, setTimeout: clock.setTimeout, clearTimeout: clock.clearTimeout,
|
|
31
|
+
dedupeCapacity, onError: error => { errors.push(error); },
|
|
32
|
+
player: (sound, signal, permit) => new Promise<void>((resolve, reject) => {
|
|
33
|
+
calls.push({ sound, signal, permit, finish: resolve, fail: () => reject(new Error("audio")) });
|
|
34
|
+
if (!slow && permit.start()) starts.push(clock.now);
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
scheduler.resetSession("s"); scheduler.configure(settings, false);
|
|
38
|
+
const enqueue = (sequence: number, event = "agent.completed" as const, occurredAt = clock.now) =>
|
|
39
|
+
scheduler.enqueue({ sessionId: "s", runId: "r", sequence, event, occurredAt });
|
|
40
|
+
return { clock, settings, calls, starts, errors, scheduler, enqueue };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test("burst priority belongs to event, ties latest, first coalesce deadline is fixed", () => {
|
|
44
|
+
const f = fixture();
|
|
45
|
+
f.settings.audio.events["agent.failed"] = "builtin:success";
|
|
46
|
+
f.settings.audio.events["agent.completed"] = "builtin:error";
|
|
47
|
+
f.scheduler.configure(f.settings, false);
|
|
48
|
+
f.enqueue(1); f.clock.advance(200);
|
|
49
|
+
f.scheduler.enqueue({ sessionId: "s", runId: "r", sequence: 2, event: "agent.failed", occurredAt: 200 });
|
|
50
|
+
f.enqueue(3); f.clock.advance(99); assert.equal(f.calls.length, 0);
|
|
51
|
+
f.clock.advance(1); assert.equal(f.calls[0].sound, "builtin:success");
|
|
52
|
+
const ties = fixture(); ties.enqueue(1); ties.clock.advance(200);
|
|
53
|
+
ties.settings.audio.events["agent.completed"] = "builtin:attention"; ties.scheduler.configure(ties.settings, false);
|
|
54
|
+
ties.enqueue(2); ties.clock.advance(100); assert.equal(ties.calls[0].sound, "builtin:attention");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("serial active plus one pending, minimum interval measured between starts", async () => {
|
|
58
|
+
const f = fixture(); f.enqueue(1); f.clock.advance(300); f.enqueue(2); f.enqueue(3);
|
|
59
|
+
f.clock.advance(500); assert.equal(f.calls.length, 1);
|
|
60
|
+
f.calls[0].finish(); await tick(); f.clock.advance(499); assert.equal(f.calls.length, 1);
|
|
61
|
+
f.clock.advance(1); assert.deepEqual(f.starts, [300, 1300]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("TTL exact boundary and pending behind unstarted detection never replay", async () => {
|
|
65
|
+
const f = fixture(true); assert.equal(f.enqueue(1, "agent.completed", -2000), false);
|
|
66
|
+
assert.equal(f.enqueue(2, "agent.completed", -1999), true);
|
|
67
|
+
f.clock.advance(300); assert.equal(f.calls.length, 0);
|
|
68
|
+
f.enqueue(3); f.clock.advance(300); f.enqueue(4);
|
|
69
|
+
f.clock.advance(2000); f.calls[0].finish(); await tick(); f.clock.advance(5000);
|
|
70
|
+
assert.equal(f.calls.length, 1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("long playback preserves a pending failure without overlap or priority loss", async () => {
|
|
74
|
+
const f = fixture(); f.enqueue(1); f.clock.advance(300);
|
|
75
|
+
f.clock.advance(10);
|
|
76
|
+
assert.equal(f.scheduler.enqueue({ sessionId: "s", runId: "failed", sequence: 1,
|
|
77
|
+
event: "subagent.failed", occurredAt: f.clock.now }), true);
|
|
78
|
+
f.clock.advance(2419); // The local success WAV lasts 2429 ms.
|
|
79
|
+
assert.equal(f.calls.length, 1);
|
|
80
|
+
assert.equal(f.enqueue(2), false); // A newer success cannot replace the preserved failure.
|
|
81
|
+
assert.equal(f.enqueue(3, "agent.completed", f.clock.now - 2000), false);
|
|
82
|
+
f.calls[0].finish(); await tick();
|
|
83
|
+
assert.equal(f.calls.length, 2);
|
|
84
|
+
assert.equal(f.calls[1].sound, "builtin:error");
|
|
85
|
+
assert.deepEqual(f.starts, [300, 2729]);
|
|
86
|
+
f.calls[1].finish(); await tick(); f.clock.advance(5000);
|
|
87
|
+
assert.equal(f.calls.length, 2);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("pending TTL pauses only after the active playback actually starts", async () => {
|
|
91
|
+
const f = fixture(true); f.enqueue(1); f.clock.advance(300); f.enqueue(2);
|
|
92
|
+
f.clock.advance(200); assert.equal(f.calls[0].permit.start(), true);
|
|
93
|
+
f.clock.advance(3000); assert.equal(f.calls.length, 1);
|
|
94
|
+
f.calls[0].finish(); await tick(); assert.equal(f.calls.length, 2);
|
|
95
|
+
// 200 ms of the pending event's TTL was spent before playback started.
|
|
96
|
+
f.clock.advance(1799); assert.equal(f.calls[1].signal.aborted, false);
|
|
97
|
+
f.clock.advance(1); assert.equal(f.calls[1].signal.aborted, true);
|
|
98
|
+
assert.equal(f.calls[1].permit.start(), false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("equal-priority replacement during playback keeps only the latest candidate", async () => {
|
|
102
|
+
const f = fixture(); f.enqueue(1); f.clock.advance(300); f.enqueue(2);
|
|
103
|
+
f.clock.advance(2500);
|
|
104
|
+
f.settings.audio.events["agent.completed"] = "builtin:attention";
|
|
105
|
+
f.scheduler.configure(f.settings, false);
|
|
106
|
+
assert.equal(f.enqueue(3), true);
|
|
107
|
+
f.clock.advance(2500); f.calls[0].finish(); await tick();
|
|
108
|
+
assert.equal(f.calls.length, 2);
|
|
109
|
+
assert.equal(f.calls[1].sound, "builtin:attention");
|
|
110
|
+
f.calls[1].finish(); await tick(); f.clock.advance(5000);
|
|
111
|
+
assert.equal(f.calls.length, 2);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("started preview also preserves the next automatic candidate", async () => {
|
|
115
|
+
const f = fixture(); assert.equal(f.scheduler.preview("builtin:attention"), true);
|
|
116
|
+
f.enqueue(1); f.clock.advance(2856);
|
|
117
|
+
assert.equal(f.calls.length, 1);
|
|
118
|
+
f.calls[0].finish(); await tick();
|
|
119
|
+
assert.equal(f.calls.length, 2);
|
|
120
|
+
assert.equal(f.calls[1].sound, "builtin:success");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("preserved pending events still expire during minimum interval after playback", async () => {
|
|
124
|
+
const f = fixture(); f.settings.audio.minimumIntervalMs = 6000;
|
|
125
|
+
f.scheduler.configure(f.settings, false); f.enqueue(1); f.clock.advance(300);
|
|
126
|
+
f.enqueue(2); f.clock.advance(2429); f.calls[0].finish(); await tick();
|
|
127
|
+
f.clock.advance(2000); f.clock.advance(6000);
|
|
128
|
+
assert.equal(f.calls.length, 1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("mute, off, session replacement and disposal discard playback-paused pending events", async () => {
|
|
132
|
+
for (const action of ["mute", "off", "session", "dispose"] as const) {
|
|
133
|
+
const f = fixture(); f.enqueue(1); f.clock.advance(300); f.enqueue(2);
|
|
134
|
+
f.clock.advance(3000);
|
|
135
|
+
if (action === "dispose") f.scheduler.dispose();
|
|
136
|
+
else if (action === "session") f.scheduler.resetSession("other");
|
|
137
|
+
else f.scheduler.configure({ ...f.settings, enabled: action !== "off" }, action === "mute");
|
|
138
|
+
assert.equal(f.calls[0].signal.aborted, true);
|
|
139
|
+
f.scheduler.resetSession("s"); f.scheduler.configure(f.settings, false);
|
|
140
|
+
f.calls[0].finish(); await tick(); f.clock.advance(5000);
|
|
141
|
+
assert.equal(f.calls.length, 1);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("slow detection must acquire start permit immediately before spawn; expires and aborts", async () => {
|
|
146
|
+
const f = fixture(true); f.enqueue(1); f.clock.advance(300);
|
|
147
|
+
f.clock.advance(1699); assert.equal(f.calls[0].signal.aborted, false);
|
|
148
|
+
f.clock.advance(1); assert.equal(f.calls[0].signal.aborted, true);
|
|
149
|
+
assert.equal(f.calls[0].permit.start(), false);
|
|
150
|
+
f.enqueue(2); f.clock.advance(300); assert.equal(f.calls.length, 1);
|
|
151
|
+
f.calls[0].finish(); await tick(); assert.equal(f.calls.length, 2);
|
|
152
|
+
assert.equal(f.calls[1].permit.start(), true);
|
|
153
|
+
assert.equal(f.calls[1].permit.start(), false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("settled detection cannot start through its old permit after a new run reserves playback", async () => {
|
|
157
|
+
const f = fixture(true); f.enqueue(1); f.clock.advance(300);
|
|
158
|
+
const old = f.calls[0]; old.finish(); await tick();
|
|
159
|
+
assert.equal(f.scheduler.enqueue({ sessionId: "s", runId: "new", sequence: 1,
|
|
160
|
+
event: "agent.completed", occurredAt: f.clock.now }), true);
|
|
161
|
+
f.clock.advance(300); assert.equal(f.calls.length, 2);
|
|
162
|
+
assert.equal(old.signal.aborted, false); // Still inside TTL, same generation: identity must reject it.
|
|
163
|
+
assert.equal(f.calls[1].permit.start(), true);
|
|
164
|
+
assert.equal(old.permit.start(), false);
|
|
165
|
+
assert.equal(f.calls[1].permit.start(), false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("throttle follows actual start after slow detection, not detection invocation", async () => {
|
|
169
|
+
const f = fixture(true); f.enqueue(1); f.clock.advance(700);
|
|
170
|
+
assert.equal(f.calls[0].permit.start(), true); f.calls[0].finish(); await tick(); f.enqueue(2);
|
|
171
|
+
f.clock.advance(999); assert.equal(f.calls.length, 1);
|
|
172
|
+
f.clock.advance(1); assert.equal(f.calls.length, 2);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("dedupe occurrence not state, bounded cap, session memory reaped, off events consumed", async () => {
|
|
176
|
+
const f = fixture(false, 2); assert.equal(f.enqueue(1), true); assert.equal(f.enqueue(1), false);
|
|
177
|
+
assert.equal(f.enqueue(2), true); assert.equal(f.enqueue(3), true); assert.equal(f.enqueue(1), true);
|
|
178
|
+
f.scheduler.configure({ ...f.settings, enabled: false }, false);
|
|
179
|
+
assert.equal(f.enqueue(9), false); f.scheduler.configure(f.settings, false); assert.equal(f.enqueue(9), false);
|
|
180
|
+
f.scheduler.resetSession("other"); assert.equal(f.enqueue(10), false);
|
|
181
|
+
f.scheduler.resetSession("s"); assert.equal(f.enqueue(9), true);
|
|
182
|
+
f.clock.advance(300); assert.equal(f.calls.length, 1);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("off, mute and session changes abort and invalidate async; no overlap or replay", async () => {
|
|
186
|
+
for (const action of ["off", "mute", "session"] as const) {
|
|
187
|
+
const f = fixture(true); f.enqueue(1); f.clock.advance(300); f.enqueue(2);
|
|
188
|
+
if (action === "session") f.scheduler.resetSession("next");
|
|
189
|
+
else f.scheduler.configure({ ...f.settings, enabled: action !== "off" }, action === "mute");
|
|
190
|
+
assert.equal(f.calls[0].signal.aborted, true); assert.equal(f.calls[0].permit.start(), false);
|
|
191
|
+
f.scheduler.resetSession("s"); f.scheduler.configure(f.settings, false);
|
|
192
|
+
f.clock.advance(5000); assert.equal(f.calls.length, 1);
|
|
193
|
+
assert.equal(f.scheduler.preview("builtin:error"), false);
|
|
194
|
+
f.calls[0].finish(); await tick(); f.clock.advance(5000); assert.equal(f.calls.length, 1);
|
|
195
|
+
assert.equal(f.scheduler.preview("builtin:error"), true);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("preview bypasses off/mute, stays serial, does not flush pending or modify config", async () => {
|
|
200
|
+
const f = fixture(); f.scheduler.configure({ ...f.settings, enabled: false }, true);
|
|
201
|
+
const before = JSON.stringify(f.settings); assert.equal(f.scheduler.preview("builtin:error"), true);
|
|
202
|
+
assert.equal(f.scheduler.preview("builtin:success"), false); assert.equal(f.enqueue(1), false);
|
|
203
|
+
f.calls[0].finish(); await tick(); f.scheduler.configure(f.settings, false); f.enqueue(2);
|
|
204
|
+
assert.equal(f.scheduler.preview("builtin:error"), false);
|
|
205
|
+
f.clock.advance(1000); assert.equal(f.calls.length, 2); assert.equal(JSON.stringify(f.settings), before);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("start at TTL minus one succeeds, exact TTL denied even when timer delivery is late", () => {
|
|
209
|
+
const f = fixture(true); f.enqueue(1); f.clock.advance(1999);
|
|
210
|
+
assert.equal(f.calls[0].permit.start(), true);
|
|
211
|
+
f.clock.advance(1); assert.equal(f.calls[0].signal.aborted, false);
|
|
212
|
+
const late = fixture(true); late.enqueue(1); late.clock.advance(300);
|
|
213
|
+
late.clock.now = 2000; // Simulate blocked event loop: expiry callback has not run.
|
|
214
|
+
assert.equal(late.calls[0].signal.aborted, false);
|
|
215
|
+
assert.equal(late.calls[0].permit.start(), false);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("large bursts still reserve only one pending; silent settings never enable or replay", async () => {
|
|
219
|
+
const f = fixture(); f.enqueue(1); f.clock.advance(300);
|
|
220
|
+
for (let i = 2; i < 1002; i++) f.enqueue(i);
|
|
221
|
+
f.clock.advance(1000); assert.equal(f.calls.length, 1);
|
|
222
|
+
f.calls[0].finish(); await tick(); assert.equal(f.calls.length, 2);
|
|
223
|
+
f.calls[1].finish(); await tick(); f.clock.advance(5000); assert.equal(f.calls.length, 2);
|
|
224
|
+
f.settings.audio.events = {}; f.scheduler.configure(f.settings, false);
|
|
225
|
+
assert.equal(f.enqueue(2000), false);
|
|
226
|
+
f.settings.audio.events["agent.completed"] = "builtin:success"; // Scheduler cloned configuration.
|
|
227
|
+
assert.equal(f.enqueue(2001), false);
|
|
228
|
+
f.scheduler.configure(f.settings, false); assert.equal(f.enqueue(2000), false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("dispose during detection aborts, rejects late errors silently and clears timers", async () => {
|
|
232
|
+
const f = fixture(true); f.enqueue(1); f.clock.advance(300); f.enqueue(2);
|
|
233
|
+
f.scheduler.dispose(); f.scheduler.dispose();
|
|
234
|
+
assert.equal(f.calls[0].signal.aborted, true); assert.equal(f.calls[0].permit.start(), false);
|
|
235
|
+
f.calls[0].fail(); await tick(); f.clock.advance(10000);
|
|
236
|
+
assert.equal(f.errors.length, 0); assert.equal(f.calls.length, 1); assert.equal(f.clock.timers.size, 0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("synchronous player errors release reservation and are contained", () => {
|
|
240
|
+
const clock = new Clock(); let errors = 0;
|
|
241
|
+
const scheduler = new NotificationScheduler({ now: () => clock.now, setTimeout: clock.setTimeout, clearTimeout: clock.clearTimeout,
|
|
242
|
+
player: () => { throw new Error("sync"); }, onError: () => { errors++; } });
|
|
243
|
+
assert.equal(scheduler.preview("builtin:error"), true);
|
|
244
|
+
assert.equal(scheduler.preview("builtin:error"), true); assert.equal(errors, 1); scheduler.dispose();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("errors caught, diagnostics locally limited even if callback throws, disposal idempotent", async () => {
|
|
248
|
+
const f = fixture(); f.scheduler.configure({ ...f.settings, enabled: false }, false);
|
|
249
|
+
for (let i = 0; i < 3; i++) { assert.equal(f.scheduler.preview("builtin:error"), true); f.calls[i].fail(); await tick(); }
|
|
250
|
+
assert.equal(f.errors.length, 1); f.clock.advance(5000);
|
|
251
|
+
f.scheduler.preview("builtin:error"); f.calls[3].fail(); await tick(); assert.equal(f.errors.length, 2);
|
|
252
|
+
f.scheduler.configure(f.settings, false); f.enqueue(1); f.scheduler.dispose(); f.scheduler.dispose();
|
|
253
|
+
f.clock.advance(10000); assert.equal(f.calls.length, 4); assert.equal(f.scheduler.preview("builtin:error"), false);
|
|
254
|
+
const clock = new Clock();
|
|
255
|
+
const scheduler = new NotificationScheduler({ now: () => clock.now, setTimeout: clock.setTimeout, clearTimeout: clock.clearTimeout,
|
|
256
|
+
player: async () => { throw new Error("audio"); }, onError: () => { throw new Error("diagnostic"); } });
|
|
257
|
+
scheduler.preview("builtin:error"); await tick(); scheduler.dispose();
|
|
258
|
+
});
|