gentle-pi 3.7.0 → 4.1.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 +155 -10
- package/assets/agents/gentle-ai-explore.md +13 -6
- package/assets/agents/gentle-ai-verify.md +27 -6
- package/assets/agents/gentle-ai-worker.md +13 -15
- 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 +75 -169
- package/assets/orchestrator-memory.md +12 -26
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-skills.md +1 -1
- 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 +30 -38
- 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/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +956 -50
- 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/bundled-install.md +397 -0
- package/docs/delegated-verification.md +27 -4
- package/docs/gentle-agents-activity.md +537 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +165 -51
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +3 -3
- package/docs/permission-modes.md +82 -0
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +364 -231
- package/docs/render-cache-performance.md +28 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +35 -0
- package/extensions/child-safety.ts +25 -0
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +1093 -326
- package/extensions/gentle-ai.ts +1636 -890
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +1619 -136
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +126 -20
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1424 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +254 -92
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +191 -58
- package/extensions/startup-banner.ts +71 -33
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +99 -0
- package/lib/agents-runner.ts +180 -107
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/auto-permission-classifier.ts +280 -0
- package/lib/auto-permission-runtime.ts +268 -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 +220 -0
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +195 -0
- package/lib/command-palette-catalog.ts +5 -10
- package/lib/command-palette.ts +25 -14
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-bundled.ts +517 -0
- package/lib/gentle-shell-launcher.ts +89 -27
- package/lib/gentle-shell-resume-hint.ts +176 -0
- 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/history-capture-policy.ts +95 -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 +5 -1
- package/lib/nan-provider.ts +249 -0
- package/lib/native-review-cli.ts +242 -180
- 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-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- 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/overlay-repaint.ts +26 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +73 -21
- package/lib/review-candidate-view.ts +115 -27
- 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-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +227 -56
- package/lib/review-sidebar-state.ts +224 -0
- 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 +527 -0
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +266 -0
- package/lib/session-worktree-registry.ts +87 -9
- package/lib/shell-bar.ts +215 -30
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +42 -14
- package/lib/shell-changes.ts +86 -71
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +83 -15
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +84 -31
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +70 -13
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +383 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +119 -0
- package/lib/visual-customize-view.ts +481 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +317 -0
- package/package.json +20 -8
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +518 -0
- package/runtime/gentle-shell-launcher.mjs +87 -25
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- 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 +239 -177
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +225 -54
- 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 +8 -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 +83 -35
- 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/mirror-odd-routing.mjs +2 -2
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +90 -0
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +54 -19
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +91 -38
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +16 -21
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +30 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +97 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +467 -93
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/auto-mode-invariants.test.ts +258 -0
- package/tests/auto-mode-sdk.test.ts +57 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-classifier.test.ts +283 -0
- package/tests/auto-permission-runtime.test.ts +196 -0
- package/tests/autonomous-guard.test.ts +69 -1
- 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 +146 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- 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 +87 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-context-files.test.ts +255 -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 +94 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +596 -0
- package/tests/command-palette.test.ts +43 -7
- 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 -78
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +171 -3
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +288 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +69 -0
- package/tests/gentle-agents.test.ts +2804 -346
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +21 -4
- 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-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +566 -62
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +493 -14
- package/tests/gentle-ai.test.ts +1427 -236
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +896 -115
- package/tests/gentle-shell-bundled.test.ts +1002 -0
- package/tests/gentle-shell-launcher.test.ts +333 -65
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- 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 +4216 -119
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +333 -10
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +49 -19
- 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 +2158 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +389 -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-authority.test.ts +16 -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 +481 -0
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +13 -1
- package/tests/native-review-cli.test.ts +239 -118
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +101 -4
- 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 +76 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
- package/tests/odd-routing-contract.test.ts +158 -50
- package/tests/orchestrator-budget.test.ts +43 -45
- 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 +8 -7
- 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/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +344 -123
- package/tests/packed-runner-owned-path.test.ts +80 -0
- 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 +10 -18
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/provider-defect-handoff.test.ts +7 -14
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +569 -187
- package/tests/rdd-aware-verification-contract.test.ts +103 -40
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +237 -21
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +399 -12
- package/tests/review-contract-prompt.test.ts +138 -44
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +866 -6
- package/tests/review-controller-workspace-root.test.ts +206 -6
- 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 +342 -4
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +221 -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 +14 -37
- 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-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +594 -26
- 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-sidebar-state.test.ts +402 -0
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +202 -0
- package/tests/runtime-harness.mjs +186 -800
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +451 -0
- 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-messaging-grants.test.ts +255 -0
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +381 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +134 -2
- package/tests/shell-bar.test.ts +422 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +135 -32
- package/tests/shell-changes.test.ts +61 -62
- package/tests/shell-prompt.test.ts +94 -2
- 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 +351 -22
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +207 -3
- package/tests/shell-usage-view.test.ts +28 -1
- package/tests/shell-usage.test.ts +108 -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 +357 -2
- package/tests/startup-banner.test.ts +246 -4
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -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 +107 -20
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/theme-customization.test.ts +72 -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-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +139 -0
- package/tests/visual-customize-view.test.ts +696 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +271 -0
- package/tests/yolo-mode-runtime.test.ts +188 -0
- package/tests/yolo-mode.test.ts +289 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
- /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
|
@@ -181,8 +181,6 @@ interface ParityRuntime {
|
|
|
181
181
|
interface ParityRuntimeOptions {
|
|
182
182
|
candidateViews?: CandidateViewRegistry;
|
|
183
183
|
pendingReviewConsentRegistry?: PendingReviewConsentRegistry;
|
|
184
|
-
now?: () => number;
|
|
185
|
-
scheduleTimer?: (callback: () => void, delayMs: number) => { unref: () => void };
|
|
186
184
|
}
|
|
187
185
|
|
|
188
186
|
function parityRuntime(nativeReviewCli: NativeReviewCli | null, options: ParityRuntimeOptions = {}): ParityRuntime {
|
|
@@ -193,8 +191,6 @@ function parityRuntime(nativeReviewCli: NativeReviewCli | null, options: ParityR
|
|
|
193
191
|
nativeReviewCli,
|
|
194
192
|
candidateViews: options.candidateViews ?? new CandidateViewRegistry(),
|
|
195
193
|
pendingReviewConsentRegistry: options.pendingReviewConsentRegistry ?? new PendingReviewConsentRegistry(),
|
|
196
|
-
now: options.now,
|
|
197
|
-
scheduleTimer: options.scheduleTimer,
|
|
198
194
|
} as unknown as Parameters<typeof createGentleAiExtension>[0];
|
|
199
195
|
__testing.createGentleAiExtension(dependencies)({
|
|
200
196
|
on(name: string, handler: RegisteredEvent) { events.set(name, handler); },
|
|
@@ -330,7 +326,7 @@ async function answerConsent(runtime: ParityRuntime, cwd: string, binding: unkno
|
|
|
330
326
|
// that names itself and its exit. It still carries the current negotiated
|
|
331
327
|
// STATUS as reconciliation context, but it never reads as a healthy
|
|
332
328
|
// pre-start "ready".
|
|
333
|
-
function assertStaleConsentBinding(outcome: Record<string, unknown>, binding: unknown, code: "consent-binding-unknown" | "consent-binding-
|
|
329
|
+
function assertStaleConsentBinding(outcome: Record<string, unknown>, binding: unknown, code: "consent-binding-unknown" | "consent-binding-already-consumed"): void {
|
|
334
330
|
assert.equal(outcome.status, "blocked");
|
|
335
331
|
assert.equal(outcome.outcome, "consent-binding-stale");
|
|
336
332
|
assert.equal(outcome.consent_binding, binding);
|
|
@@ -677,7 +673,7 @@ test("unavailable and ambiguous consent follow-ups never replay a consumed bindi
|
|
|
677
673
|
assert.equal(statusCalls, callsBeforeStaleReconciliation + 1, "already-consumed binding reconciliation performs exactly one additional STATUS call");
|
|
678
674
|
});
|
|
679
675
|
|
|
680
|
-
test("concurrent answers atomically claim consent before review-mode gating
|
|
676
|
+
test("concurrent answers atomically claim consent before review-mode gating", async (t) => {
|
|
681
677
|
const cwd = repository(t);
|
|
682
678
|
const fixture = consentNative(cwd);
|
|
683
679
|
const candidateViews = new CandidateViewRegistry();
|
|
@@ -687,7 +683,6 @@ test("concurrent answers atomically claim consent before review-mode gating and
|
|
|
687
683
|
cleanupCalls += 1;
|
|
688
684
|
cleanup(token);
|
|
689
685
|
};
|
|
690
|
-
const scheduled: Array<() => void> = [];
|
|
691
686
|
let releaseModeGate: (() => void) | undefined;
|
|
692
687
|
const modeGate = new Promise<void>((resolve) => { releaseModeGate = resolve; });
|
|
693
688
|
let modeCalls = 0;
|
|
@@ -702,13 +697,12 @@ test("concurrent answers atomically claim consent before review-mode gating and
|
|
|
702
697
|
let nativeAnswerCalls = 0;
|
|
703
698
|
fixture.native.answerConsent = async (request) => {
|
|
704
699
|
nativeAnswerCalls += 1;
|
|
705
|
-
assert.equal(cleanupCalls, 0, "
|
|
700
|
+
assert.equal(cleanupCalls, 0, "candidate authority remains live before the provider answer");
|
|
706
701
|
await providerGate;
|
|
707
702
|
return await nativeAnswer(request);
|
|
708
703
|
};
|
|
709
704
|
const runtime = parityRuntime(fixture.native, {
|
|
710
705
|
candidateViews,
|
|
711
|
-
scheduleTimer: (callback) => { scheduled.push(callback); return { unref() {} }; },
|
|
712
706
|
});
|
|
713
707
|
const blocked = await beginConsent(runtime, cwd);
|
|
714
708
|
fixture.native.reviewMode = async () => {
|
|
@@ -726,9 +720,7 @@ test("concurrent answers atomically claim consent before review-mode gating and
|
|
|
726
720
|
assert.equal(modeCalls, 1, "the first answer pauses at the asynchronous review-mode gate");
|
|
727
721
|
const second = answerConsent(runtime, cwd, blocked.consent_binding, "declined");
|
|
728
722
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
729
|
-
assert.equal(
|
|
730
|
-
scheduled[0]!();
|
|
731
|
-
assert.equal(cleanupCalls, 0, "a queued expiry callback is a harmless no-op after the answer claim");
|
|
723
|
+
assert.equal(cleanupCalls, 0, "claimed consent retains candidate authority until the answer settles");
|
|
732
724
|
|
|
733
725
|
releaseModeGate!();
|
|
734
726
|
releaseProvider!();
|
|
@@ -741,47 +733,19 @@ test("concurrent answers atomically claim consent before review-mode gating and
|
|
|
741
733
|
assert.equal(cleanupCalls, 1, "only the answered binding cleans candidate authority");
|
|
742
734
|
});
|
|
743
735
|
|
|
744
|
-
test("
|
|
736
|
+
test("unanswered consent remains reusable after the former deadline and answers only once", async (t) => {
|
|
745
737
|
const cwd = repository(t);
|
|
746
738
|
const fixture = consentNative(cwd);
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const scheduled: Array<() => void> = [];
|
|
751
|
-
const runtime = parityRuntime(fixture.native, {
|
|
752
|
-
pendingReviewConsentRegistry: registry,
|
|
753
|
-
now: () => now,
|
|
754
|
-
scheduleTimer: (callback) => { scheduled.push(callback); return { unref() {} }; },
|
|
755
|
-
});
|
|
739
|
+
const runtime = parityRuntime(fixture.native);
|
|
740
|
+
let now = 1_000;
|
|
741
|
+
t.mock.method(Date, "now", () => now);
|
|
756
742
|
const first = await beginConsent(runtime, cwd);
|
|
743
|
+
now += 24 * 60 * 60 * 1000;
|
|
757
744
|
const reused = await beginConsent(runtime, cwd);
|
|
758
745
|
assert.equal(reused.consent_binding, first.consent_binding);
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
// session-local disposition remains only to classify this stale answer.
|
|
763
|
-
scheduled[0]!();
|
|
764
|
-
const timerExpired = await answerConsent(runtime, cwd, first.consent_binding, "declined");
|
|
765
|
-
assert.equal(timerExpired.operation, "answer-consent");
|
|
766
|
-
assertStaleConsentBinding(timerExpired, first.consent_binding, "consent-binding-expired");
|
|
767
|
-
assert.match(String((timerExpired.diagnostics as { message?: unknown }).message), /expired after 10 minutes/);
|
|
768
|
-
const second = await beginConsent(runtime, cwd);
|
|
769
|
-
assert.notEqual(second.consent_binding, first.consent_binding);
|
|
770
|
-
assert.equal(scheduled.length, 2);
|
|
771
|
-
assertStaleConsentBinding(await answerConsent(runtime, cwd, first.consent_binding, "declined"), first.consent_binding, "consent-binding-expired");
|
|
772
|
-
|
|
773
|
-
// No second callback runs: START synchronously prunes this unused binding
|
|
774
|
-
// before it can reuse the frozen candidate, and its answer stays typed expiry.
|
|
775
|
-
now += 10 * 60 * 1000;
|
|
776
|
-
const third = await beginConsent(runtime, cwd);
|
|
777
|
-
assert.notEqual(third.consent_binding, second.consent_binding);
|
|
778
|
-
assertStaleConsentBinding(await answerConsent(runtime, cwd, second.consent_binding, "declined"), second.consent_binding, "consent-binding-expired");
|
|
779
|
-
|
|
780
|
-
const reloaded = parityRuntime(fixture.native, { pendingReviewConsentRegistry: new PendingReviewConsentRegistry() });
|
|
781
|
-
assertStaleConsentBinding(await answerConsent(reloaded, cwd, first.consent_binding, "declined"), first.consent_binding, "consent-binding-unknown");
|
|
782
|
-
const afterReload = await beginConsent(reloaded, cwd);
|
|
783
|
-
assert.notEqual(afterReload.consent_binding, third.consent_binding);
|
|
784
|
-
assert.equal(fixture.starts.count, 5);
|
|
746
|
+
const answered = await answerConsent(runtime, cwd, first.consent_binding, "declined");
|
|
747
|
+
assert.equal(answered.outcome, "consent-declined-this-candidate");
|
|
748
|
+
assertStaleConsentBinding(await answerConsent(runtime, cwd, first.consent_binding, "declined"), first.consent_binding, "consent-binding-already-consumed");
|
|
785
749
|
});
|
|
786
750
|
|
|
787
751
|
test("native START maps only provider facts and omits absent evidence", async (t) => {
|
|
@@ -89,3 +89,24 @@ test("an empty file is named before it is described", () => {
|
|
|
89
89
|
test("a reason with no path contributes no phrase", () => {
|
|
90
90
|
assert.deepEqual(nativeRiskEvidencePhrases("high", [{ code: "empty_content" }]), []);
|
|
91
91
|
});
|
|
92
|
+
|
|
93
|
+
// Field truth from gentle-ai main (4.1.0-dev): one committed high-risk
|
|
94
|
+
// candidate's consent/v3 `risk_evidence` beside its START/v4 `risk_reasons`.
|
|
95
|
+
test("dangerous sink and agent escalation reasons render the gentle-ai phrases", () => {
|
|
96
|
+
assert.deepEqual(
|
|
97
|
+
nativeRiskEvidencePhrases("high", [
|
|
98
|
+
{ code: "dangerous_sink", signal: "dangerous_sink", path: "auth/session.py" },
|
|
99
|
+
{ code: "hot_path", signal: "auth", path: "auth/session.py" },
|
|
100
|
+
{ code: "process_boundary", signal: "shell_process", path: "auth/session.py" },
|
|
101
|
+
]),
|
|
102
|
+
[
|
|
103
|
+
"a dangerous code pattern in auth/session.py",
|
|
104
|
+
"authentication in auth/session.py",
|
|
105
|
+
"code that starts other processes in auth/session.py",
|
|
106
|
+
],
|
|
107
|
+
);
|
|
108
|
+
assert.deepEqual(
|
|
109
|
+
nativeRiskEvidencePhrases("high", [{ code: "agent_escalation", signal: "agent_escalation" }]),
|
|
110
|
+
["the agent that made this change flagged it as high risk"],
|
|
111
|
+
);
|
|
112
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
|
|
6
|
+
// gentle-shell#1731 T13 (evidence L24): in bench task x4 the user wrote "I may
|
|
7
|
+
// stop you between the two and pick the second part up later"; every Gentle
|
|
8
|
+
// cell delivered only the first part and paused by itself. Hedged delivery
|
|
9
|
+
// wording asks for resumable notes and separate commits, never a stop, and
|
|
10
|
+
// the close step must not leave tracked tasks pending without a user stop.
|
|
11
|
+
|
|
12
|
+
const REPO_ROOT = join(import.meta.dirname, "..");
|
|
13
|
+
const read = (relative: string): string => readFileSync(join(REPO_ROOT, relative), "utf8");
|
|
14
|
+
|
|
15
|
+
const tracking = read("assets/orchestrator-tracking.md");
|
|
16
|
+
const core = read("assets/orchestrator.md");
|
|
17
|
+
|
|
18
|
+
function authorizationSection(): string {
|
|
19
|
+
const start = tracking.indexOf("#### Authorization and progress");
|
|
20
|
+
assert.ok(start !== -1, "tracking module must keep its Authorization and progress section");
|
|
21
|
+
const next = tracking.indexOf("\n#### ", start + 1);
|
|
22
|
+
return tracking.slice(start, next === -1 ? undefined : next);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("T13: conditional change intent is narrowed to whether a change is authorized at all", () => {
|
|
26
|
+
const section = authorizationSection();
|
|
27
|
+
assert.ok(
|
|
28
|
+
section.includes(
|
|
29
|
+
"Ambiguous or conditional change intent (unclear whether a change is authorized at all) requires one clarification; stop and wait.",
|
|
30
|
+
),
|
|
31
|
+
"the clarification clause must be scoped to authorization itself",
|
|
32
|
+
);
|
|
33
|
+
assert.ok(
|
|
34
|
+
!section.includes("Ambiguous or conditional change intent requires one clarification"),
|
|
35
|
+
"the unscoped clarification clause must be gone",
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("T13: hedged delivery wording asks for resumable notes and separate commits, not a stop", () => {
|
|
40
|
+
const section = authorizationSection();
|
|
41
|
+
for (const clause of [
|
|
42
|
+
"Hedged delivery wording (the user may stop you, review commits one at a time, or pick the work up later)",
|
|
43
|
+
"asks for resumable notes and separate commits, not a stop",
|
|
44
|
+
"complete every authorized task in the current turn unless the user explicitly says to wait",
|
|
45
|
+
]) {
|
|
46
|
+
assert.ok(section.includes(clause), `tracking module is missing: ${clause}`);
|
|
47
|
+
}
|
|
48
|
+
const hedge = section.slice(section.indexOf("Hedged delivery wording"));
|
|
49
|
+
const sentence = hedge.slice(0, hedge.indexOf(". ") + 1);
|
|
50
|
+
assert.doesNotMatch(sentence, /\bask (?:one|a|the user)\b|question/i, "the hedge rule must not add a product or design question");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("T13: the Close gate requires continuing or quoting the user's explicit stop", () => {
|
|
54
|
+
const section = authorizationSection();
|
|
55
|
+
assert.ok(
|
|
56
|
+
section.includes(
|
|
57
|
+
"Close gate: before the final answer, if any tracked task in the feature document or `todo` is still pending, continue it or quote the exact user sentence that orders the stop.",
|
|
58
|
+
),
|
|
59
|
+
"tracking module must carry the Close gate",
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("T13: budgets hold and the pinned human-control line is untouched", () => {
|
|
64
|
+
const bytes = Buffer.byteLength(tracking, "utf8");
|
|
65
|
+
assert.ok(bytes <= 12_600, `orchestrator-tracking.md is ${bytes} B, over its 12,600 B budget`);
|
|
66
|
+
assert.ok(core.includes("- Preserve human control: user decisions beat agent momentum."), "core human-control line must stay verbatim");
|
|
67
|
+
assert.ok(core.includes("4. **Track**") && core.includes("`orchestrator-tracking.md`"), "Track must still load the tracking module");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("T13: the always-on harness narrows conditional intent and gates Close on pending tasks", () => {
|
|
71
|
+
// The harness ODD steps are injected on every request, including the small
|
|
72
|
+
// path that never loads the tracking module, so the narrowing must live there too.
|
|
73
|
+
const harness = read("extensions/gentle-ai.ts");
|
|
74
|
+
assert.ok(
|
|
75
|
+
harness.includes("Ambiguous or conditional change intent (unclear whether a change is authorized at all) gets one clarification; stop and wait."),
|
|
76
|
+
"harness Authorize step must narrow conditional intent",
|
|
77
|
+
);
|
|
78
|
+
assert.ok(!harness.includes("Ambiguous or conditional change intent gets one clarification"), "the broad harness wording must be gone");
|
|
79
|
+
assert.ok(
|
|
80
|
+
harness.includes("A user saying they may stop you or resume later asks for notes and separate commits, not a stop."),
|
|
81
|
+
"harness must say hedged delivery wording is not a stop",
|
|
82
|
+
);
|
|
83
|
+
assert.ok(
|
|
84
|
+
harness.includes("Never end with a tracked task pending unless you quote the user's explicit stop."),
|
|
85
|
+
"harness Close step must gate pending tracked tasks",
|
|
86
|
+
);
|
|
87
|
+
});
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { createServer, type Server } from "node:net";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
import { NativePulsePlayer, NATIVE_PULSE_SCHEMA, type NativePulseOptions, type NativePulseSpawnOptions } from "../lib/notification-audio-native.ts";
|
|
9
|
+
|
|
10
|
+
// --- independent golden encoders (deliberately NOT the production codec) ---
|
|
11
|
+
const taggedU32 = (value: number): Buffer => { const b = Buffer.alloc(5); b[0] = 0x4c; b.writeUInt32BE(value, 1); return b; };
|
|
12
|
+
const taggedString = (value: string): Buffer => Buffer.concat([Buffer.from([0x74]), Buffer.from(value, "utf8"), Buffer.from([0x00])]);
|
|
13
|
+
const nullString = (): Buffer => Buffer.from([0x4e]);
|
|
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): Buffer => { const header = Buffer.alloc(20); header.writeUInt32BE(payload.length, 0); header.writeUInt32BE(channel, 4); return Buffer.concat([header, payload]); };
|
|
20
|
+
const taggedValueAt = (payload: Buffer, at: number): number => payload.readUInt32BE(at + 1);
|
|
21
|
+
|
|
22
|
+
function wav(bits: number, channels: number, rate: number, frames: number): Buffer {
|
|
23
|
+
const sampleBytes = bits / 8; const dataSize = frames * channels * sampleBytes;
|
|
24
|
+
const b = Buffer.alloc(44 + dataSize, 0x11);
|
|
25
|
+
b.write("RIFF", 0); b.writeUInt32LE(36 + dataSize, 4); b.write("WAVE", 8);
|
|
26
|
+
b.write("fmt ", 12); b.writeUInt32LE(16, 16); b.writeUInt16LE(1, 20);
|
|
27
|
+
b.writeUInt16LE(channels, 22); b.writeUInt32LE(rate, 24); b.writeUInt32LE(rate * channels * sampleBytes, 28);
|
|
28
|
+
b.writeUInt16LE(channels * sampleBytes, 32); b.writeUInt16LE(bits, 34);
|
|
29
|
+
b.write("data", 36); b.writeUInt32LE(dataSize, 40);
|
|
30
|
+
return b;
|
|
31
|
+
}
|
|
32
|
+
function 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); } }
|
|
33
|
+
|
|
34
|
+
interface FakeServer { readonly path: string; readonly seen: number[]; readonly pcm: Buffer[]; close(): Promise<void>; }
|
|
35
|
+
async function startFakeServer(): Promise<FakeServer> {
|
|
36
|
+
const dir = mkdtempSync(join(tmpdir(), "native-pulse-"));
|
|
37
|
+
const path = join(dir, "native");
|
|
38
|
+
const seen: number[] = []; const pcm: Buffer[] = [];
|
|
39
|
+
const server: Server = createServer(socket => {
|
|
40
|
+
let buffer = Buffer.alloc(0);
|
|
41
|
+
const write = (value: Buffer) => { if (!socket.destroyed) socket.write(value); };
|
|
42
|
+
const send = (payload: Buffer) => write(frame(0xffffffff, payload));
|
|
43
|
+
socket.on("error", () => { /* child closed */ });
|
|
44
|
+
socket.on("data", chunk => {
|
|
45
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
46
|
+
for (;;) {
|
|
47
|
+
if (buffer.length < 20) break;
|
|
48
|
+
const length = buffer.readUInt32BE(0);
|
|
49
|
+
if (buffer.length < 20 + length) break;
|
|
50
|
+
const payload = Buffer.from(buffer.subarray(20, 20 + length));
|
|
51
|
+
const channel = buffer.readUInt32BE(4);
|
|
52
|
+
buffer = buffer.subarray(20 + length);
|
|
53
|
+
if (channel !== 0xffffffff) { pcm.push(payload); continue; }
|
|
54
|
+
const command = taggedValueAt(payload, 0); const tag = taggedValueAt(payload, 5);
|
|
55
|
+
seen.push(command);
|
|
56
|
+
if (command === 8) { send(reply(2, tag, taggedU32(35))); continue; }
|
|
57
|
+
if (command === 9) { send(reply(2, tag, taggedU32(0))); continue; }
|
|
58
|
+
if (command === 20) { send(reply(2, tag, taggedString("pulseaudio"), taggedString("17.0"), taggedString("user"), taggedString("host"), sampleSpec(3, 1, 8000), taggedString("auto_null"), nullString(), taggedU32(1))); continue; }
|
|
59
|
+
if (command === 3) {
|
|
60
|
+
const spec = { format: payload[11]!, channels: payload[12]!, rate: payload.readUInt32BE(13) };
|
|
61
|
+
const dataLength = 0; // worker sends initial missing = 0 then requests; keep it simple
|
|
62
|
+
write(frame(0xffffffff, reply(2, tag, taggedU32(7), taggedU32(8), taggedU32(dataLength), taggedU32(0xffffffff), taggedU32(0), taggedU32(0), taggedU32(0), sampleSpec(spec.format, spec.channels, spec.rate), channelMap(spec.channels), taggedU32(0), taggedString("auto_null"), boolFalse(), usec(0))));
|
|
63
|
+
write(frame(0xffffffff, reply(61, 0xffffffff, taggedU32(7), taggedU32(64 * 1024))));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (command === 12) { send(reply(2, tag)); continue; }
|
|
67
|
+
if (command === 4) { send(reply(2, tag)); continue; }
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
await new Promise<void>((resolve, reject) => { server.once("error", reject); server.listen(path, () => resolve()); });
|
|
72
|
+
return { path, seen, pcm, close: async () => { await new Promise<void>(resolve => server.close(() => resolve())); rmSync(dir, { recursive: true, force: true }); } };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class FakeChild extends EventEmitter {
|
|
76
|
+
readonly stdout = new EventEmitter();
|
|
77
|
+
readonly stderr = new EventEmitter();
|
|
78
|
+
readonly kills: string[] = [];
|
|
79
|
+
kill(signal: string): boolean { this.kills.push(signal); return true; }
|
|
80
|
+
}
|
|
81
|
+
function recorder(platform = "linux", overrides: Partial<NativePulseOptions> = {}) {
|
|
82
|
+
const spawned: Array<{ executable: string; args: string[]; options: NativePulseSpawnOptions }> = [];
|
|
83
|
+
const children: FakeChild[] = [];
|
|
84
|
+
const player = new NativePulsePlayer({
|
|
85
|
+
platform,
|
|
86
|
+
spawn: (executable, args, options) => { spawned.push({ executable, args, options }); const child = new FakeChild(); children.push(child); return child; },
|
|
87
|
+
env: { PULSE_SERVER: "unix:/tmp/x", NODE_OPTIONS: "--inspect", DEBUG: "*", HOME: "/home/u" },
|
|
88
|
+
...overrides,
|
|
89
|
+
});
|
|
90
|
+
return { player, spawned, children };
|
|
91
|
+
}
|
|
92
|
+
const okProbe = () => JSON.stringify({ schema: NATIVE_PULSE_SCHEMA, ok: true, available: true, formats: ["wav"] });
|
|
93
|
+
|
|
94
|
+
test("bridge is lazy and spawns owned Node with fixed flags, scrubbed env and Linux only", async () => {
|
|
95
|
+
const { player, spawned, children } = recorder();
|
|
96
|
+
assert.deepEqual(spawned, []);
|
|
97
|
+
const probing = player.probe();
|
|
98
|
+
assert.equal(spawned.length, 1);
|
|
99
|
+
const [call] = spawned;
|
|
100
|
+
assert.equal(call.executable, process.execPath);
|
|
101
|
+
assert.equal(call.args[0], "--experimental-strip-types");
|
|
102
|
+
assert.equal(call.args[1], "--max-old-space-size=32");
|
|
103
|
+
assert.match(call.args[2]!, /notification-pulse-worker\.ts$/);
|
|
104
|
+
assert.equal(call.args[3], "probe");
|
|
105
|
+
assert.deepEqual(call.options.stdio, ["ignore", "pipe", "pipe"]);
|
|
106
|
+
assert.equal(call.options.shell, false); assert.equal(call.options.windowsHide, true); assert.equal(call.options.detached, false);
|
|
107
|
+
const env = call.options.env as NodeJS.ProcessEnv;
|
|
108
|
+
assert.equal(env.NODE_NO_WARNINGS, "1"); assert.equal(env.GENTLE_PI_AGENTS_CHILD, "1");
|
|
109
|
+
assert.equal(env.NODE_OPTIONS, undefined); assert.equal(env.DEBUG, undefined); assert.equal(env.PULSE_SERVER, "unix:/tmp/x");
|
|
110
|
+
assert.deepEqual(player.getNativeFormats(), []); // default unavailable until a real probe
|
|
111
|
+
children[0]!.stdout.emit("data", okProbe());
|
|
112
|
+
children[0]!.emit("close");
|
|
113
|
+
assert.deepEqual(await probing, { available: true, formats: ["wav"] });
|
|
114
|
+
assert.deepEqual(player.getNativeFormats(), ["wav"]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("abort before spawn performs no work and unsupported platforms never spawn", async () => {
|
|
118
|
+
const { player, spawned } = recorder();
|
|
119
|
+
const controller = new AbortController(); controller.abort();
|
|
120
|
+
assert.deepEqual(await player.probe(controller.signal), { available: false, formats: [] });
|
|
121
|
+
assert.equal(spawned.length, 0);
|
|
122
|
+
for (const platform of ["darwin", "win32"]) {
|
|
123
|
+
const other = recorder(platform);
|
|
124
|
+
assert.equal(other.player.supportsTarget(), false);
|
|
125
|
+
assert.deepEqual(await other.player.probe(), { available: false, formats: [] });
|
|
126
|
+
await assert.rejects(other.player.play("/tmp/s.wav"), /Native pulse|unsupported/);
|
|
127
|
+
assert.equal(other.spawned.length, 0);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("play gate false prevents spawning and no async runs after it", async () => {
|
|
132
|
+
const { player, spawned } = recorder();
|
|
133
|
+
await assert.rejects(player.play("/tmp/s.wav", undefined, () => false), /permit/);
|
|
134
|
+
assert.equal(spawned.length, 0);
|
|
135
|
+
const { player: second, spawned: secondSpawned, children } = recorder();
|
|
136
|
+
const playing = second.play("/tmp/s.wav", undefined, { permit: () => true });
|
|
137
|
+
assert.equal(secondSpawned.length, 1);
|
|
138
|
+
children[0]!.stdout.emit("data", JSON.stringify({ schema: NATIVE_PULSE_SCHEMA, ok: true, played: true }));
|
|
139
|
+
children[0]!.emit("close");
|
|
140
|
+
await playing;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("malformed, oversize and error output fail privately and kill at most once", async () => {
|
|
144
|
+
const malformed = recorder();
|
|
145
|
+
const malformedProbe = malformed.player.probe();
|
|
146
|
+
malformed.children[0]!.stdout.emit("data", "not json");
|
|
147
|
+
malformed.children[0]!.emit("close");
|
|
148
|
+
assert.deepEqual(await malformedProbe, { available: false, formats: [] });
|
|
149
|
+
assert.equal(malformed.player.getNativeFormats().length, 0);
|
|
150
|
+
|
|
151
|
+
const oversize = recorder();
|
|
152
|
+
const oversizeProbe = oversize.player.probe();
|
|
153
|
+
oversize.children[0]!.stdout.emit("data", "x".repeat(2048));
|
|
154
|
+
assert.equal(oversize.children[0]!.kills.length, 1);
|
|
155
|
+
assert.equal(oversize.children[0]!.kills[0], "SIGKILL");
|
|
156
|
+
oversize.children[0]!.emit("close");
|
|
157
|
+
assert.deepEqual(await oversizeProbe, { available: false, formats: [] });
|
|
158
|
+
|
|
159
|
+
const failure = recorder();
|
|
160
|
+
const playing = failure.player.play("/private/snapshot.wav");
|
|
161
|
+
failure.children[0]!.stdout.emit("data", JSON.stringify({ schema: NATIVE_PULSE_SCHEMA, ok: false }));
|
|
162
|
+
failure.children[0]!.emit("close");
|
|
163
|
+
await assert.rejects(playing, (error: Error) => /Native pulse/.test(error.message) && !/snapshot|cookie|\/private/.test(error.message));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("timeout and abort SIGKILL once and settle only on close", async () => {
|
|
167
|
+
const timed = recorder("linux", { probeTimeoutMs: 15 });
|
|
168
|
+
const probing = timed.player.probe();
|
|
169
|
+
await new Promise(resolve => setTimeout(resolve, 45));
|
|
170
|
+
assert.deepEqual(timed.children[0]!.kills, ["SIGKILL"]);
|
|
171
|
+
let settled = false; void probing.then(() => { settled = true; });
|
|
172
|
+
timed.children[0]!.emit("exit", 0);
|
|
173
|
+
timed.children[0]!.emit("error", new Error("spawn failed")); // Node emits close after error
|
|
174
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
175
|
+
assert.equal(settled, false);
|
|
176
|
+
timed.children[0]!.emit("close");
|
|
177
|
+
assert.deepEqual(await probing, { available: false, formats: [] });
|
|
178
|
+
|
|
179
|
+
const aborted = recorder();
|
|
180
|
+
const controller = new AbortController();
|
|
181
|
+
const playing = aborted.player.play("/tmp/s.wav", controller.signal);
|
|
182
|
+
controller.abort();
|
|
183
|
+
assert.deepEqual(aborted.children[0]!.kills, ["SIGKILL"]);
|
|
184
|
+
aborted.children[0]!.emit("close");
|
|
185
|
+
await assert.rejects(playing, /aborted|Native pulse/);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("default play deadline is 11000 ms and outlives the accepted 10 s maximum", async () => {
|
|
189
|
+
let scheduledMs = 0; let fire: (() => void) | undefined;
|
|
190
|
+
const { player, children } = recorder("linux", {
|
|
191
|
+
setTimeout: (callback, ms) => { scheduledMs = ms; fire = callback; return 1; },
|
|
192
|
+
clearTimeout: () => {},
|
|
193
|
+
});
|
|
194
|
+
const playing = player.play("/tmp/s.wav", undefined, () => true);
|
|
195
|
+
assert.equal(children.length, 1);
|
|
196
|
+
assert.equal(scheduledMs, 11000);
|
|
197
|
+
assert.ok(scheduledMs > 10000, "the native bridge must outlive the accepted 10 s file");
|
|
198
|
+
assert.deepEqual(children[0]!.kills, []);
|
|
199
|
+
fire!();
|
|
200
|
+
assert.deepEqual(children[0]!.kills, ["SIGKILL"]);
|
|
201
|
+
children[0]!.emit("close");
|
|
202
|
+
await assert.rejects(playing, /Native pulse/);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("probe never throws for offline/malformed IPC while play rejects and cleans", async () => {
|
|
206
|
+
const { player, children } = recorder();
|
|
207
|
+
const probing = player.probe();
|
|
208
|
+
children[0]!.stdout.emit("data", JSON.stringify({ schema: "wrong", ok: true, available: true, formats: ["wav"] }));
|
|
209
|
+
children[0]!.emit("close");
|
|
210
|
+
assert.deepEqual(await probing, { available: false, formats: [] });
|
|
211
|
+
const { player: playPlayer, children: playChildren } = recorder();
|
|
212
|
+
const playing = playPlayer.play("/tmp/s.wav");
|
|
213
|
+
playChildren[0]!.stdout.emit("data", JSON.stringify({ schema: "wrong", ok: true, played: true }));
|
|
214
|
+
playChildren[0]!.emit("close");
|
|
215
|
+
await assert.rejects(playing, /Native pulse/);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("bridge and worker import no SDK, UI, or addon surface", () => {
|
|
219
|
+
for (const path of ["lib/notification-audio-native.ts", "lib/notification-pulse-worker.ts"]) {
|
|
220
|
+
const source = readFileSync(join(process.cwd(), path), "utf8");
|
|
221
|
+
assert.doesNotMatch(source, /@earendil-works/, path);
|
|
222
|
+
assert.doesNotMatch(source, /from\s+["'][^"']*extensions\//, path);
|
|
223
|
+
assert.doesNotMatch(source, /native\/|\.node["']/, path);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("child environment is an allowlist: Pulse vars kept, credentials and debug dropped", async () => {
|
|
228
|
+
const captured: NodeJS.ProcessEnv[] = [];
|
|
229
|
+
const player = new NativePulsePlayer({
|
|
230
|
+
platform: "linux",
|
|
231
|
+
spawn: (_executable, _args, options) => { captured.push(options.env); return new FakeChild(); },
|
|
232
|
+
env: { PULSE_SERVER: "unix:/s", PULSE_COOKIE: "/c", XDG_RUNTIME_DIR: "/r", XDG_CONFIG_HOME: "/cfg", HOME: "/h", LANG: "C",
|
|
233
|
+
OPENAI_API_KEY: "sk", ANTHROPIC_API_KEY: "x", AWS_SECRET_ACCESS_KEY: "y", NODE_OPTIONS: "--inspect", NODE_PATH: "/n", DEBUG: "*", PATH: "/bin" },
|
|
234
|
+
});
|
|
235
|
+
void player.probe();
|
|
236
|
+
const env = captured[0]!;
|
|
237
|
+
assert.equal(env.PULSE_SERVER, "unix:/s"); assert.equal(env.PULSE_COOKIE, "/c");
|
|
238
|
+
assert.equal(env.XDG_RUNTIME_DIR, "/r"); assert.equal(env.XDG_CONFIG_HOME, "/cfg"); assert.equal(env.HOME, "/h"); assert.equal(env.LANG, "C");
|
|
239
|
+
for (const drop of ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "AWS_SECRET_ACCESS_KEY", "NODE_OPTIONS", "NODE_PATH", "DEBUG", "PATH"])
|
|
240
|
+
assert.equal(env[drop], undefined, drop);
|
|
241
|
+
assert.equal(env.NODE_NO_WARNINGS, "1"); assert.equal(env.GENTLE_PI_AGENTS_CHILD, "1");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("real worker probe against a fake Unix socket authenticates without CREATE", async () => {
|
|
245
|
+
const server = await startFakeServer();
|
|
246
|
+
try {
|
|
247
|
+
// Exercise the Linux Pulse transport against a fake socket on any Unix host.
|
|
248
|
+
const player = new NativePulsePlayer({ platform: "linux", env: { PULSE_SERVER: `unix:${server.path}` }, probeTimeoutMs: 8000 });
|
|
249
|
+
const result = await player.probe();
|
|
250
|
+
assert.deepEqual(result, { available: true, formats: ["wav"] });
|
|
251
|
+
assert.deepEqual(server.seen, [8, 9, 20]);
|
|
252
|
+
assert.ok(!server.seen.includes(3), "probe must not create a stream");
|
|
253
|
+
} finally { await server.close(); }
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("real worker plays a fake PCM snapshot to a fake socket with no OS audio", async () => {
|
|
257
|
+
const bytes = wav(16, 1, 8000, 800);
|
|
258
|
+
const dir = mkdtempSync(join(tmpdir(), "native-pulse-snap-"));
|
|
259
|
+
const snapshot = join(dir, "sound.wav");
|
|
260
|
+
writeFileSync(snapshot, bytes, { mode: 0o400 });
|
|
261
|
+
const server = await startFakeServer();
|
|
262
|
+
try {
|
|
263
|
+
const player = new NativePulsePlayer({ platform: "linux", env: { PULSE_SERVER: `unix:${server.path}` }, playTimeoutMs: 8000 });
|
|
264
|
+
await player.play(snapshot);
|
|
265
|
+
assert.deepEqual(Buffer.concat(server.pcm), dataOf(bytes));
|
|
266
|
+
assert.ok(server.seen.includes(3) && server.seen.includes(12) && server.seen.includes(4));
|
|
267
|
+
assert.ok(existsSync(snapshot), "the bridge/worker must not delete the snapshot; the owner does");
|
|
268
|
+
} finally { await server.close(); rmSync(dir, { recursive: true, force: true }); }
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("real worker rejects invalid WAV before opening the socket", async () => {
|
|
272
|
+
const dir = mkdtempSync(join(tmpdir(), "native-pulse-bad-"));
|
|
273
|
+
const snapshot = join(dir, "sound.wav");
|
|
274
|
+
writeFileSync(snapshot, Buffer.alloc(64, 0x00), { mode: 0o400 });
|
|
275
|
+
const server = await startFakeServer();
|
|
276
|
+
try {
|
|
277
|
+
const player = new NativePulsePlayer({ platform: "linux", env: { PULSE_SERVER: `unix:${server.path}` }, playTimeoutMs: 8000 });
|
|
278
|
+
await assert.rejects(player.play(snapshot), /Native pulse/);
|
|
279
|
+
assert.deepEqual(server.seen, []);
|
|
280
|
+
} finally { await server.close(); rmSync(dir, { recursive: true, force: true }); }
|
|
281
|
+
});
|