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.
Files changed (425) hide show
  1. package/README.md +157 -18
  2. package/assets/agents/gentle-ai-explore.md +11 -4
  3. package/assets/agents/gentle-ai-verify.md +23 -4
  4. package/assets/agents/gentle-ai-worker.md +6 -6
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +70 -163
  9. package/assets/orchestrator-memory.md +11 -4
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-tracking.md +53 -0
  12. package/assets/orchestrator-verification.md +28 -0
  13. package/assets/orchestrator-writer.md +38 -0
  14. package/assets/orchestrator.md +29 -25
  15. package/assets/sounds/LICENSE.md +52 -0
  16. package/assets/sounds/attention.wav +0 -0
  17. package/assets/sounds/error.wav +0 -0
  18. package/assets/sounds/success.wav +0 -0
  19. package/bin/engram-guard +9 -0
  20. package/bin/gentle-shell-install.mjs +302 -0
  21. package/bin/gentle-shell.mjs +708 -22
  22. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  23. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  24. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  25. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +399 -0
  30. package/docs/delegated-verification.md +3 -5
  31. package/docs/gentle-agents-activity.md +513 -0
  32. package/docs/gentle-shell-uninstall.md +136 -0
  33. package/docs/gentle-shell.md +90 -33
  34. package/docs/install-wizard.md +2040 -0
  35. package/docs/native-authority-architecture.md +1 -1
  36. package/docs/permission-modes.md +127 -0
  37. package/docs/prompt-history.md +1 -1
  38. package/docs/readme-reference.md +310 -47
  39. package/docs/render-cache-performance.md +28 -0
  40. package/docs/session-profile-format.md +39 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/yolo-mode.md +11 -10
  44. package/extensions/ask-user-choice.ts +4 -0
  45. package/extensions/ask-user-question.ts +6 -1
  46. package/extensions/child-context.ts +11 -2
  47. package/extensions/child-safety.ts +4 -2
  48. package/extensions/codegraph-tools.ts +18 -5
  49. package/extensions/gentle-agents.ts +718 -87
  50. package/extensions/gentle-ai.ts +991 -263
  51. package/extensions/gentle-herdr-activity.ts +81 -0
  52. package/extensions/gentle-notifications.ts +104 -0
  53. package/extensions/gentle-shell.ts +271 -80
  54. package/extensions/gentle-todo.ts +123 -18
  55. package/extensions/history/index.ts +11 -6
  56. package/extensions/pi-pretty.ts +39 -3
  57. package/extensions/quiet-tools.ts +91 -21
  58. package/extensions/skill-registry.ts +175 -46
  59. package/extensions/startup-banner.ts +11 -2
  60. package/lib/agent-home.ts +10 -1
  61. package/lib/agent-model-resolution.ts +80 -0
  62. package/lib/agent-profiles.ts +13 -3
  63. package/lib/agents-completion-delivery.ts +11 -3
  64. package/lib/agents-messaging.ts +1 -1
  65. package/lib/agents-protocol.ts +40 -1
  66. package/lib/agents-runner.ts +90 -15
  67. package/lib/auto-permission-answers.ts +133 -0
  68. package/lib/auto-permission-classifier.ts +459 -0
  69. package/lib/auto-permission-evidence.ts +88 -0
  70. package/lib/auto-permission-memory.ts +69 -0
  71. package/lib/auto-permission-runtime.ts +484 -0
  72. package/lib/auto-permission-tool-context.ts +61 -0
  73. package/lib/background-jobs-tools.ts +202 -0
  74. package/lib/background-jobs.ts +253 -0
  75. package/lib/background-monitor.ts +154 -0
  76. package/lib/bounded-writer-admission.ts +83 -10
  77. package/lib/builtin-codemode-optout.ts +166 -0
  78. package/lib/card-content-policy.ts +85 -0
  79. package/lib/child-package-injection.ts +62 -0
  80. package/lib/codemode-renderer.ts +14 -4
  81. package/lib/command-palette-catalog.ts +3 -2
  82. package/lib/context-threshold.ts +26 -0
  83. package/lib/destructive-command-guard.ts +104 -28
  84. package/lib/gentle-ai-binary.ts +109 -4
  85. package/lib/gentle-shell-bundled.ts +535 -0
  86. package/lib/gentle-shell-launcher.ts +75 -24
  87. package/lib/gentle-shell-startup-flow.ts +54 -0
  88. package/lib/gentle-shell-startup-update.ts +92 -0
  89. package/lib/gentle-shell-uninstall.ts +840 -0
  90. package/lib/gentle-shell-update-panel.ts +231 -0
  91. package/lib/herdr-activity.ts +183 -0
  92. package/lib/inprocess-reviewer.ts +3 -0
  93. package/lib/jobs-sidebar-state.ts +20 -0
  94. package/lib/jobs-view.ts +161 -0
  95. package/lib/model-price-ratio.ts +83 -0
  96. package/lib/model-routing-authority.ts +6 -1
  97. package/lib/nan-provider.ts +33 -11
  98. package/lib/native-review-cli.ts +194 -73
  99. package/lib/notification-audio-native.ts +162 -0
  100. package/lib/notification-audio-windows.ts +399 -0
  101. package/lib/notification-audio.ts +332 -0
  102. package/lib/notification-customize.ts +241 -0
  103. package/lib/notification-events.ts +113 -0
  104. package/lib/notification-policy.ts +148 -0
  105. package/lib/notification-pulse-client.ts +367 -0
  106. package/lib/notification-pulse-protocol.ts +305 -0
  107. package/lib/notification-pulse-stream.ts +213 -0
  108. package/lib/notification-pulse-worker.ts +66 -0
  109. package/lib/notification-scheduler.ts +173 -0
  110. package/lib/notification-service.ts +202 -0
  111. package/lib/notification-ui.ts +87 -0
  112. package/lib/odd-phase.ts +1 -1
  113. package/lib/orchestrator-alias.ts +20 -0
  114. package/lib/orchestrator-catalog.ts +79 -0
  115. package/lib/orchestrator-consultation.ts +61 -0
  116. package/lib/orchestrator-discovery.ts +49 -0
  117. package/lib/orchestrator-helper-consent.ts +122 -0
  118. package/lib/orchestrator-helper.ts +167 -0
  119. package/lib/orchestrator-message-card.ts +105 -0
  120. package/lib/orchestrator-presence.ts +115 -14
  121. package/lib/orchestrator-scope.ts +80 -0
  122. package/lib/orchestrator-state.ts +107 -0
  123. package/lib/orchestrator-tool-card.ts +280 -0
  124. package/lib/orchestrator-work-search.ts +218 -0
  125. package/lib/orchestrator-work.ts +79 -0
  126. package/lib/permission-mode-policy.ts +59 -0
  127. package/lib/review-candidate-view-owner.ts +8 -6
  128. package/lib/review-candidate-view.ts +3 -2
  129. package/lib/review-compact-contract.ts +2 -2
  130. package/lib/review-host-relay.ts +7 -2
  131. package/lib/review-integration-v2.ts +100 -22
  132. package/lib/review-publication-gate.ts +1 -1
  133. package/lib/review-risk-assessment.ts +77 -51
  134. package/lib/review-sidebar-state.ts +2 -1
  135. package/lib/review-transaction.ts +72 -3
  136. package/lib/runtime-metrics-children.ts +1 -1
  137. package/lib/runtime-metrics-native.ts +3 -2
  138. package/lib/selection-engine.ts +22 -7
  139. package/lib/session-change-capture.ts +5 -4
  140. package/lib/session-changes.ts +27 -22
  141. package/lib/session-profile-binding.ts +83 -0
  142. package/lib/session-profile-freeze.ts +261 -0
  143. package/lib/session-profile-persistence.ts +187 -0
  144. package/lib/session-worktree-registry.ts +74 -8
  145. package/lib/shell-bar.ts +41 -11
  146. package/lib/shell-changes-view.ts +40 -13
  147. package/lib/shell-changes.ts +88 -71
  148. package/lib/shell-prompt.ts +6 -4
  149. package/lib/shell-sidebar-layout.ts +5 -1
  150. package/lib/shell-todo.ts +81 -26
  151. package/lib/shell-usage.ts +4 -3
  152. package/lib/status-timing-diagnostics.ts +201 -0
  153. package/lib/telemetry-trigger.ts +3 -1
  154. package/lib/vim-editor-adapter.ts +27 -5
  155. package/lib/visual-customization-policy.ts +20 -9
  156. package/lib/visual-customize-view.ts +171 -20
  157. package/lib/writer-surfaces.ts +162 -0
  158. package/lib/yolo-session-policy.ts +198 -121
  159. package/package.json +5 -5
  160. package/runtime/child-package-injection.mjs +63 -0
  161. package/runtime/gentle-ai-binary.mjs +109 -4
  162. package/runtime/gentle-shell-bundled.mjs +536 -0
  163. package/runtime/gentle-shell-launcher.mjs +74 -23
  164. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  165. package/runtime/gentle-shell-startup-update.mjs +93 -0
  166. package/runtime/gentle-shell-uninstall.mjs +841 -0
  167. package/runtime/gentle-shell-update-panel.mjs +232 -0
  168. package/runtime/native-review-cli.mjs +191 -70
  169. package/runtime/review-integration-v2.mjs +100 -22
  170. package/runtime/review-risk-assessment.mjs +77 -51
  171. package/runtime/status-timing-diagnostics.mjs +202 -0
  172. package/runtime/telemetry-trigger.mjs +3 -1
  173. package/scripts/bootstrap.cmd +255 -0
  174. package/scripts/bootstrap.sh +168 -0
  175. package/scripts/build-distribution.mjs +157 -0
  176. package/scripts/build-installer-bundles.mjs +136 -0
  177. package/scripts/build-runtime-modules.mjs +7 -0
  178. package/scripts/bundled-install.mjs +1308 -0
  179. package/scripts/bundled-wizard.mjs +399 -0
  180. package/scripts/check-types.mjs +4 -6
  181. package/scripts/gentle-ai-installer.mjs +79 -32
  182. package/scripts/install-gentle-ai.mjs +48 -1
  183. package/scripts/install-tui-mode-setting.mjs +10 -1
  184. package/scripts/install-wizard-preview.mjs +154 -0
  185. package/scripts/installer-downloads.mjs +648 -0
  186. package/scripts/installer-preflight.mjs +387 -0
  187. package/scripts/installer-probes.mjs +564 -0
  188. package/scripts/installer-runner.mjs +1102 -0
  189. package/scripts/installer-server.mjs +881 -0
  190. package/scripts/installer-windows-artifacts.json +15 -0
  191. package/scripts/installer-windows.mjs +752 -0
  192. package/scripts/main-channel.mjs +452 -0
  193. package/scripts/npm-pack-result.mjs +31 -0
  194. package/scripts/pi-update.mjs +223 -0
  195. package/scripts/render-cache-performance-worker.mjs +205 -0
  196. package/scripts/render-cache-performance.mjs +115 -0
  197. package/scripts/run-test-suite.mjs +22 -8
  198. package/scripts/test-installer-acceptance.sh +161 -0
  199. package/scripts/test-packed-runner.mjs +23 -5
  200. package/scripts/types-baseline.json +2 -92
  201. package/scripts/verify-package-files.mjs +82 -18
  202. package/skills/gentle-ai/SKILL.md +12 -11
  203. package/tests/agent-home.test.ts +56 -3
  204. package/tests/agent-model-resolution.test.ts +113 -0
  205. package/tests/agent-profiles.test.ts +12 -0
  206. package/tests/agent-risk-escalation.test.ts +123 -0
  207. package/tests/agents-completion-delivery.test.ts +17 -0
  208. package/tests/agents-protocol.test.ts +57 -0
  209. package/tests/agents-queries.test.ts +9 -9
  210. package/tests/agents-runner-process.test.ts +31 -0
  211. package/tests/agents-runner.test.ts +89 -4
  212. package/tests/append-system-prompt-route.test.ts +18 -1
  213. package/tests/auto-mode-human-answers.test.ts +68 -0
  214. package/tests/auto-mode-invariants.test.ts +276 -0
  215. package/tests/auto-mode-sdk.test.ts +84 -0
  216. package/tests/auto-mode.test.ts +49 -0
  217. package/tests/auto-permission-answers.test.ts +170 -0
  218. package/tests/auto-permission-classifier.test.ts +634 -0
  219. package/tests/auto-permission-compound.test.ts +24 -0
  220. package/tests/auto-permission-evidence.test.ts +88 -0
  221. package/tests/auto-permission-memory.test.ts +46 -0
  222. package/tests/auto-permission-public-regressions.test.ts +111 -0
  223. package/tests/auto-permission-runtime.test.ts +454 -0
  224. package/tests/autonomous-guard.test.ts +86 -16
  225. package/tests/background-cache-warming.test.ts +2 -1
  226. package/tests/background-jobs.test.ts +227 -0
  227. package/tests/background-monitor.test.ts +195 -0
  228. package/tests/background-subagents-default-mode.test.ts +28 -5
  229. package/tests/bounded-stop-contract.test.ts +85 -0
  230. package/tests/bounded-writer-admission.test.ts +52 -1
  231. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  232. package/tests/bridge-wake-rendering.test.ts +56 -0
  233. package/tests/build-distribution.test.ts +269 -0
  234. package/tests/builtin-codemode-optout.test.ts +242 -0
  235. package/tests/bundled-install.test.ts +1319 -0
  236. package/tests/bundled-wizard.test.ts +505 -0
  237. package/tests/card-content-policy.test.ts +68 -0
  238. package/tests/card-style-policy.test.ts +34 -2
  239. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  240. package/tests/child-package-entrypoints.test.ts +65 -0
  241. package/tests/child-package-injection.test.ts +74 -0
  242. package/tests/child-safety.test.ts +12 -0
  243. package/tests/child-web-package.test.ts +224 -0
  244. package/tests/codegraph-tools.test.ts +35 -0
  245. package/tests/codemode-rendering.test.ts +107 -2
  246. package/tests/command-palette.test.ts +5 -5
  247. package/tests/context-threshold.test.ts +31 -0
  248. package/tests/delegate-for-reason-contract.test.ts +327 -0
  249. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  250. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  251. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  252. package/tests/engram-safety.test.ts +316 -0
  253. package/tests/generic-agent-tools.test.ts +16 -1
  254. package/tests/gentle-agents.test.ts +1895 -76
  255. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  256. package/tests/gentle-ai-binary.test.ts +19 -2
  257. package/tests/gentle-ai-child-guards.test.ts +202 -0
  258. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  259. package/tests/gentle-ai-installer.test.ts +548 -58
  260. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  261. package/tests/gentle-ai-renderer.test.ts +6 -6
  262. package/tests/gentle-ai.test.ts +999 -31
  263. package/tests/gentle-herdr-activity.test.ts +100 -0
  264. package/tests/gentle-notifications.test.ts +427 -0
  265. package/tests/gentle-shell-bin.test.ts +263 -19
  266. package/tests/gentle-shell-bundled.test.ts +1036 -0
  267. package/tests/gentle-shell-launcher.test.ts +251 -25
  268. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  269. package/tests/gentle-shell-startup-update.test.ts +85 -0
  270. package/tests/gentle-shell-uninstall.test.ts +978 -0
  271. package/tests/gentle-shell-update-panel.test.ts +158 -0
  272. package/tests/gentle-shell.test.ts +611 -100
  273. package/tests/gentle-todo.test.ts +317 -14
  274. package/tests/herdr-activity.test.ts +243 -0
  275. package/tests/history-agent-home.test.ts +81 -0
  276. package/tests/history-child-guard.test.ts +70 -0
  277. package/tests/inprocess-reviewer.test.ts +20 -0
  278. package/tests/install-gentle-ai-failure.test.ts +69 -0
  279. package/tests/install-tui-mode-setting.test.ts +15 -0
  280. package/tests/install-wizard.test.ts +1223 -0
  281. package/tests/installer-bundles.test.ts +122 -0
  282. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  283. package/tests/installer-preflight.test.ts +750 -0
  284. package/tests/installer-probes.test.ts +1106 -0
  285. package/tests/installer-runner.test.ts +2159 -0
  286. package/tests/installer-server.test.ts +1567 -0
  287. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  288. package/tests/jobs-view.test.ts +160 -0
  289. package/tests/main-channel-upgrade.test.ts +395 -0
  290. package/tests/main-channel.test.ts +344 -0
  291. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  292. package/tests/model-price-ratio.test.ts +116 -0
  293. package/tests/model-profiles-preservation.test.ts +153 -0
  294. package/tests/model-routing-package.test.ts +10 -0
  295. package/tests/nan-provider-runtime.test.ts +156 -0
  296. package/tests/nan-provider.test.ts +28 -18
  297. package/tests/native-binary-gate.test.ts +2 -0
  298. package/tests/native-review-capability-contract.test.ts +7 -1
  299. package/tests/native-review-cli.test.ts +236 -1
  300. package/tests/native-review-consent.test.ts +2 -2
  301. package/tests/native-review-parity-runtime.test.ts +1 -1
  302. package/tests/native-review-parity.test.ts +12 -48
  303. package/tests/native-risk-evidence-parity.test.ts +21 -0
  304. package/tests/no-self-pause-contract.test.ts +87 -0
  305. package/tests/notification-audio-native.test.ts +281 -0
  306. package/tests/notification-audio-windows.test.ts +581 -0
  307. package/tests/notification-audio.test.ts +548 -0
  308. package/tests/notification-customize.test.ts +655 -0
  309. package/tests/notification-events.test.ts +115 -0
  310. package/tests/notification-policy.test.ts +201 -0
  311. package/tests/notification-pulse-client.test.ts +356 -0
  312. package/tests/notification-pulse-protocol.test.ts +213 -0
  313. package/tests/notification-pulse-stream.test.ts +320 -0
  314. package/tests/notification-scheduler.test.ts +258 -0
  315. package/tests/notification-ui.test.ts +157 -0
  316. package/tests/npm-pack-result.test.ts +41 -0
  317. package/tests/odd-integration.test.ts +14 -5
  318. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  319. package/tests/odd-routing-contract.test.ts +80 -23
  320. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  321. package/tests/orchestrator-budget.test.ts +29 -6
  322. package/tests/orchestrator-catalog.test.ts +146 -0
  323. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  324. package/tests/orchestrator-consultation.test.ts +111 -0
  325. package/tests/orchestrator-discovery.test.ts +241 -0
  326. package/tests/orchestrator-helper-consent.test.ts +171 -0
  327. package/tests/orchestrator-helper.test.ts +297 -0
  328. package/tests/orchestrator-message-card.test.ts +190 -0
  329. package/tests/orchestrator-presence.test.ts +23 -0
  330. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  331. package/tests/orchestrator-scope.test.ts +107 -0
  332. package/tests/orchestrator-state.test.ts +114 -0
  333. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  334. package/tests/orchestrator-tool-card.test.ts +398 -0
  335. package/tests/orchestrator-work-search.test.ts +318 -0
  336. package/tests/orchestrator-work.test.ts +147 -0
  337. package/tests/package-manifest.test.ts +105 -21
  338. package/tests/packed-runner-owned-path.test.ts +35 -1
  339. package/tests/parallel-reason-contract.test.ts +21 -0
  340. package/tests/parallel-writer-admission.test.ts +210 -0
  341. package/tests/permission-mode-policy.test.ts +194 -0
  342. package/tests/persona-single-channel.test.ts +4 -12
  343. package/tests/pi-pretty.test.ts +56 -0
  344. package/tests/pi-update-launcher.test.ts +87 -0
  345. package/tests/pi-update.test.ts +186 -0
  346. package/tests/provider-defect-handoff.test.ts +4 -3
  347. package/tests/quiet-tool-rendering.test.ts +160 -3
  348. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  349. package/tests/rdd-status-line.test.ts +52 -0
  350. package/tests/render-cache-performance.test.ts +48 -0
  351. package/tests/review-agent-end-preflight.test.ts +62 -10
  352. package/tests/review-candidate-view.test.ts +101 -12
  353. package/tests/review-contract-prompt.test.ts +33 -4
  354. package/tests/review-controller-native-recovery.test.ts +327 -17
  355. package/tests/review-controller-native-routing.test.ts +255 -1
  356. package/tests/review-controller-workspace-root.test.ts +43 -2
  357. package/tests/review-controller.test.ts +2 -2
  358. package/tests/review-correction-lifecycle.test.ts +2 -1
  359. package/tests/review-gate.test.ts +7 -4
  360. package/tests/review-graph-schema.test.ts +2 -2
  361. package/tests/review-host-relay-routing.test.ts +5 -3
  362. package/tests/review-host-relay.test.ts +49 -2
  363. package/tests/review-integration-v2-forward.test.ts +21 -1
  364. package/tests/review-json-arguments.test.ts +167 -0
  365. package/tests/review-last-event-closure.test.ts +157 -8
  366. package/tests/review-ledger-contract.test.ts +5 -4
  367. package/tests/review-policy-judgment-day.test.ts +1 -1
  368. package/tests/review-policy-ordinary.test.ts +2 -2
  369. package/tests/review-relay-transport-agent.test.ts +66 -1
  370. package/tests/review-risk-assessment.test.ts +109 -33
  371. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  372. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  373. package/tests/review-test-fixtures.ts +7 -1
  374. package/tests/review-transaction.test.ts +18 -1
  375. package/tests/run-test-suite.test.ts +83 -5
  376. package/tests/runtime-harness.mjs +42 -15
  377. package/tests/runtime-metrics-extension.test.ts +1 -1
  378. package/tests/runtime-metrics-native.test.ts +31 -1
  379. package/tests/selection-engine.test.ts +65 -5
  380. package/tests/session-change-capture.test.ts +10 -10
  381. package/tests/session-changes-shell.test.ts +242 -48
  382. package/tests/session-changes.test.ts +17 -24
  383. package/tests/session-profile-binding.test.ts +163 -0
  384. package/tests/session-profile-freeze.test.ts +394 -0
  385. package/tests/session-profile-persistence.test.ts +334 -0
  386. package/tests/session-subject-routing-contract.test.ts +34 -0
  387. package/tests/session-worktree-registry.test.ts +57 -2
  388. package/tests/shell-bar.test.ts +40 -0
  389. package/tests/shell-changes-view.test.ts +85 -34
  390. package/tests/shell-changes.test.ts +79 -62
  391. package/tests/shell-render-cache-runtime.test.ts +69 -0
  392. package/tests/shell-render-cache.test.ts +129 -0
  393. package/tests/shell-sidebar-layout.test.ts +27 -2
  394. package/tests/shell-todo.test.ts +132 -11
  395. package/tests/shell-usage-view.test.ts +1 -1
  396. package/tests/shell-usage.test.ts +35 -0
  397. package/tests/short-check-routing-contract.test.ts +49 -0
  398. package/tests/skill-registry-watcher.test.ts +119 -0
  399. package/tests/skill-registry.test.ts +311 -5
  400. package/tests/startup-banner.test.ts +117 -3
  401. package/tests/startup-home-repo.test.ts +170 -0
  402. package/tests/status-timing-diagnostics.test.ts +283 -0
  403. package/tests/support/orchestrator-modules.ts +19 -0
  404. package/tests/task-size-routing-contract.test.ts +241 -0
  405. package/tests/telemetry-trigger.test.ts +27 -1
  406. package/tests/test-strength-contract.test.ts +124 -0
  407. package/tests/verify-agent-contract.test.ts +90 -0
  408. package/tests/verify-first-launch-contract.test.ts +139 -0
  409. package/tests/verify-package-files.test.ts +56 -0
  410. package/tests/vim-editor-adapter.test.ts +47 -21
  411. package/tests/visual-customization-policy.test.ts +32 -3
  412. package/tests/visual-customize-view.test.ts +288 -10
  413. package/tests/writer-edit-surface-scope.test.ts +59 -2
  414. package/tests/writer-surfaces.test.ts +199 -0
  415. package/tests/yolo-customize.test.ts +18 -3
  416. package/tests/yolo-mode-runtime.test.ts +37 -10
  417. package/tests/yolo-mode.test.ts +38 -5
  418. package/tests/yolo-session-policy.test.ts +1 -1
  419. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  420. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  421. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  422. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  423. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  424. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  425. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -0,0 +1,356 @@
1
+ import assert from "node:assert/strict";
2
+ import { lstat, mkdtemp, rm, stat, symlink, writeFile } from "node:fs/promises";
3
+ import { createServer, type Server, type Socket } from "node:net";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { test } from "node:test";
7
+ import {
8
+ DEFAULT_REQUEST_TIMEOUT_MS, MAX_PENDING_REQUESTS, PULSE_COOKIE_BYTES,
9
+ PulseClient, PulseCookieError, loadPulseCookie, resolvePulseCookiePath, resolvePulseSocketPath,
10
+ } from "../lib/notification-pulse-client.ts";
11
+
12
+ // --- independent golden encoders (deliberately NOT the production codec) ---
13
+ const taggedU32 = (value: number): Buffer => { const b = Buffer.alloc(5); b[0] = 0x4c; b.writeUInt32BE(value, 1); return b; };
14
+ const taggedString = (value: string): Buffer => Buffer.concat([Buffer.from([0x74]), Buffer.from(value, "utf8"), Buffer.from([0x00])]);
15
+ const nullString = (): Buffer => Buffer.from([0x4e]);
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 reply = (command: number, tag: number, ...args: Buffer[]): Buffer => Buffer.concat([taggedU32(command), taggedU32(tag), ...args]);
18
+ const frame = (channel: number, flags: number, payload: Buffer, offset = 0n): Buffer => {
19
+ const header = Buffer.alloc(20);
20
+ header.writeUInt32BE(payload.length, 0); header.writeUInt32BE(channel, 4);
21
+ header.writeUInt32BE(Number(offset >> 32n), 8); header.writeUInt32BE(Number(offset & 0xffffffffn), 12); header.writeUInt32BE(flags, 16);
22
+ return Buffer.concat([header, payload]);
23
+ };
24
+ const taggedValueAt = (payload: Buffer, at: number): number => payload.readUInt32BE(at + 1);
25
+ const commandOf = (payload: Buffer): number => taggedValueAt(payload, 0);
26
+ const tagOf = (payload: Buffer): number => taggedValueAt(payload, 5);
27
+
28
+ interface FakeFrame { readonly channel: number; readonly payload: Buffer; }
29
+ interface FakeServer {
30
+ readonly path: string;
31
+ readonly frames: FakeFrame[];
32
+ close(): Promise<void>;
33
+ }
34
+ async function startFakeServer(handler: (frame: FakeFrame, send: (payload: Buffer) => void, socket: Socket) => void): Promise<FakeServer> {
35
+ const dir = await mkdtemp(join(tmpdir(), "pulse-client-"));
36
+ const path = join(dir, "native");
37
+ const frames: FakeFrame[] = [];
38
+ const server: Server = createServer(connection => {
39
+ let buffer = Buffer.alloc(0);
40
+ connection.on("data", chunk => {
41
+ buffer = Buffer.concat([buffer, chunk]);
42
+ for (;;) {
43
+ if (buffer.length < 20) break;
44
+ const length = buffer.readUInt32BE(0);
45
+ if (buffer.length < 20 + length) break;
46
+ const payload = Buffer.from(buffer.subarray(20, 20 + length));
47
+ const channel = buffer.readUInt32BE(4);
48
+ buffer = buffer.subarray(20 + length);
49
+ frames.push({ channel, payload });
50
+ handler({ channel, payload }, replyPayload => connection.write(frame(0xffffffff, 0, replyPayload)), connection);
51
+ }
52
+ });
53
+ });
54
+ await new Promise<void>((resolve, reject) => { server.once("error", reject); server.listen(path, () => resolve()); });
55
+ return { path, frames, close: async () => { await new Promise<void>(resolve => server.close(() => resolve())); await rm(dir, { recursive: true, force: true }); } };
56
+ }
57
+ /** Answers AUTH (8), SET_CLIENT_NAME (9) and GET_SERVER_INFO (20) with golden bytes. */
58
+ function probeHandler(defaultSink: string | null, seen: number[]) {
59
+ return (request: FakeFrame, send: (payload: Buffer) => void) => {
60
+ const command = commandOf(request.payload); const tag = tagOf(request.payload);
61
+ seen.push(command);
62
+ if (command === 8) send(reply(2, tag, taggedU32(0x80000023))); // server 35 with a high flag bit
63
+ else if (command === 9) send(reply(2, tag, taggedU32(0)));
64
+ else if (command === 20) send(reply(2, tag, taggedString("pulseaudio"), taggedString("17.0"), taggedString("user"), taggedString("host"),
65
+ sampleSpec(3, 2, 44100), defaultSink === null ? nullString() : taggedString(defaultSink), nullString(), taggedU32(0x12345678)));
66
+ };
67
+ }
68
+
69
+ test("socket resolution accepts unix/absolute forms and denies network, URL, list and control paths", () => {
70
+ assert.equal(resolvePulseSocketPath({ PULSE_SERVER: "unix:/tmp/pulse.sock" }), "/tmp/pulse.sock");
71
+ assert.equal(resolvePulseSocketPath({ PULSE_SERVER: "/run/user/9/pulse/native" }), "/run/user/9/pulse/native");
72
+ assert.equal(resolvePulseSocketPath({ XDG_RUNTIME_DIR: "/run/user/1000" }), "/run/user/1000/pulse/native");
73
+ assert.equal(resolvePulseSocketPath({}, 1000), "/run/user/1000/pulse/native");
74
+ for (const bad of ["tcp:host:4713", "tcp6:[::1]:4713", "http://x/native", "{unix:/a,unix:/b}", "unix:relative/path", "~/.pulse/native", "unix:/a,b", "unix:/a\u0000b", "unix:/a\nb", "unix:/a/../b"])
75
+ assert.throws(() => resolvePulseSocketPath({ PULSE_SERVER: bad }), /Pulse client/, bad);
76
+ });
77
+
78
+ test("cookie path resolution is absolute-literal only and never scans directories", () => {
79
+ assert.equal(resolvePulseCookiePath({ PULSE_COOKIE: "/tmp/cookie" }), "/tmp/cookie");
80
+ assert.equal(resolvePulseCookiePath({ XDG_CONFIG_HOME: "/cfg" }), "/cfg/pulse/cookie");
81
+ assert.equal(resolvePulseCookiePath({ HOME: "/home/u" }), "/home/u/.config/pulse/cookie");
82
+ assert.equal(resolvePulseCookiePath({}), undefined);
83
+ for (const bad of ["relative", "http://x/cookie", "/a\u0000b", "/a/../b"])
84
+ assert.throws(() => resolvePulseCookiePath({ PULSE_COOKIE: bad }), /Pulse client/, bad);
85
+ });
86
+
87
+ test("cookie loading returns 256 zero bytes for a missing file and creates nothing", async () => {
88
+ const dir = await mkdtemp(join(tmpdir(), "pulse-cookie-"));
89
+ const path = join(dir, "cookie");
90
+ const bytes = await loadPulseCookie(path);
91
+ assert.equal(bytes.length, PULSE_COOKIE_BYTES);
92
+ assert.ok(bytes.every(byte => byte === 0));
93
+ await assert.rejects(stat(path), (error: NodeJS.ErrnoException) => error.code === "ENOENT");
94
+ assert.equal((await loadPulseCookie(undefined)).length, PULSE_COOKIE_BYTES);
95
+ await rm(dir, { recursive: true, force: true });
96
+ });
97
+
98
+ test("cookie loading reads an existing 256-byte private file and fails closed on symlinks/size/type", async () => {
99
+ const dir = await mkdtemp(join(tmpdir(), "pulse-cookie-"));
100
+ const path = join(dir, "cookie");
101
+ await writeFile(path, Buffer.alloc(PULSE_COOKIE_BYTES, 0xab), { mode: 0o400 });
102
+ assert.ok((await loadPulseCookie(path)).every(byte => byte === 0xab));
103
+ const link = join(dir, "link");
104
+ await symlink(path, link);
105
+ await assert.rejects(loadPulseCookie(link), PulseCookieError);
106
+ const short = join(dir, "short");
107
+ await writeFile(short, Buffer.alloc(PULSE_COOKIE_BYTES - 1));
108
+ await assert.rejects(loadPulseCookie(short), PulseCookieError);
109
+ await assert.rejects(loadPulseCookie(dir), PulseCookieError);
110
+ await rm(dir, { recursive: true, force: true });
111
+ });
112
+
113
+ test("requests are lazy and stay offline until connect", () => {
114
+ const client = new PulseClient({ socketPath: "/nonexistent/pulse", cookie: Buffer.alloc(PULSE_COOKIE_BYTES) });
115
+ assert.ok(client instanceof PulseClient);
116
+ void assert.rejects(client.request(20, Buffer.alloc(0)));
117
+ });
118
+
119
+ test("probe authenticates, reads server info, reports wav only with a sink and closes", async () => {
120
+ const seen: number[] = [];
121
+ const server = await startFakeServer(probeHandler("auto_null", seen));
122
+ try {
123
+ const client = new PulseClient({ socketPath: server.path, cookie: Buffer.alloc(PULSE_COOKIE_BYTES) });
124
+ const result = await client.probe();
125
+ assert.deepEqual(result, { available: true, formats: ["wav"] });
126
+ assert.deepEqual(seen, [8, 9, 20]);
127
+ assert.ok(server.frames.every(entry => entry.channel === 0xffffffff));
128
+ assert.ok(server.frames.every(entry => commandOf(entry.payload) !== 61));
129
+
130
+ const auth = server.frames.find(entry => commandOf(entry.payload) === 8)!;
131
+ assert.equal(auth.payload.length, 276);
132
+ assert.equal(taggedValueAt(auth.payload, 10), 13); // protocol version
133
+ assert.equal(auth.payload[15], 0x78); // arbitrary tag
134
+ assert.equal(auth.payload.readUInt32BE(16), PULSE_COOKIE_BYTES); // raw length, not 'L'
135
+ assert.ok(auth.payload.subarray(20).every(byte => byte === 0));
136
+
137
+ const name = server.frames.find(entry => commandOf(entry.payload) === 9)!;
138
+ for (const needle of ["tapplication.name\0", "tapplication.type\0", "gentle-shell\0"])
139
+ assert.ok(name.payload.includes(Buffer.from(needle, "latin1")), needle);
140
+ } finally { await server.close(); }
141
+ });
142
+
143
+ test("probe reports unavailable when the server has no default sink", async () => {
144
+ const seen: number[] = [];
145
+ const server = await startFakeServer(probeHandler(null, seen));
146
+ try {
147
+ const result = await new PulseClient({ socketPath: server.path, cookie: Buffer.alloc(PULSE_COOKIE_BYTES) }).probe();
148
+ assert.deepEqual(result, { available: false, formats: [] });
149
+ } finally { await server.close(); }
150
+ });
151
+
152
+ test("auth strips the high version bits and rejects servers below v13", async () => {
153
+ const server = await startFakeServer((request, send) => {
154
+ const command = commandOf(request.payload); const tag = tagOf(request.payload);
155
+ if (command === 8) send(reply(2, tag, taggedU32(0x80000023)));
156
+ else if (command === 9) send(reply(2, tag, taggedU32(0)));
157
+ });
158
+ try {
159
+ const client = new PulseClient({ socketPath: server.path, cookie: Buffer.alloc(PULSE_COOKIE_BYTES) });
160
+ await client.connect();
161
+ assert.equal(await client.authenticate(Buffer.alloc(PULSE_COOKIE_BYTES)), 35);
162
+ await client.close();
163
+ } finally { await server.close(); }
164
+
165
+ const old = await startFakeServer((request, send) => { if (commandOf(request.payload) === 8) send(reply(2, tagOf(request.payload), taggedU32(12))); });
166
+ try {
167
+ const client = new PulseClient({ socketPath: old.path, cookie: Buffer.alloc(PULSE_COOKIE_BYTES) });
168
+ await client.connect();
169
+ await assert.rejects(client.authenticate(Buffer.alloc(PULSE_COOKIE_BYTES)), /version/);
170
+ await client.close();
171
+ } finally { await old.close(); }
172
+ });
173
+
174
+ test("delayed replies reject and close the connection", async () => {
175
+ const server = await startFakeServer(request => {
176
+ if (commandOf(request.payload) === 8) { /* never answer */ }
177
+ });
178
+ try {
179
+ const client = new PulseClient({ socketPath: server.path, cookie: Buffer.alloc(PULSE_COOKIE_BYTES), requestTimeoutMs: 120 });
180
+ await client.connect();
181
+ const pending = client.request(8, taggedU32(13));
182
+ await assert.rejects(pending, /timed out/);
183
+ await client.close();
184
+ } finally { await server.close(); }
185
+ });
186
+
187
+ test("ERROR command rejects the matching tag and fails the connection closed", async () => {
188
+ const server = await startFakeServer((request, send) => {
189
+ if (commandOf(request.payload) === 20) send(reply(0, tagOf(request.payload), taggedU32(6))); // PA_ERR_ACCESS
190
+ });
191
+ try {
192
+ const client = new PulseClient({ socketPath: server.path, cookie: Buffer.alloc(PULSE_COOKIE_BYTES) });
193
+ await client.connect();
194
+ await assert.rejects(client.request(20, Buffer.alloc(0)), /error/);
195
+ await assert.rejects(client.request(20, Buffer.alloc(0)), /closed|error/);
196
+ await client.close();
197
+ } finally { await server.close(); }
198
+ });
199
+
200
+ test("unknown server commands and unmatched reply tags fail closed", async () => {
201
+ const unknown = await startFakeServer((_request, send) => send(reply(200, 1)));
202
+ try {
203
+ const client = new PulseClient({ socketPath: unknown.path, cookie: Buffer.alloc(PULSE_COOKIE_BYTES) });
204
+ await client.connect();
205
+ await assert.rejects(client.request(20, Buffer.alloc(0)), /Pulse client/);
206
+ await client.close();
207
+ } finally { await unknown.close(); }
208
+
209
+ const unmatched = await startFakeServer((_request, send) => send(reply(2, 4242, taggedU32(0))));
210
+ try {
211
+ const client = new PulseClient({ socketPath: unmatched.path, cookie: Buffer.alloc(PULSE_COOKIE_BYTES) });
212
+ await client.connect();
213
+ await assert.rejects(client.request(20, Buffer.alloc(0)), /Pulse client/);
214
+ await client.close();
215
+ } finally { await unmatched.close(); }
216
+ });
217
+
218
+ test("unsolicited REQUEST is delivered while benign events are ignored and replies still resolve", async () => {
219
+ const server = await startFakeServer((request, send) => {
220
+ const command = commandOf(request.payload); const tag = tagOf(request.payload);
221
+ if (command === 20) {
222
+ send(reply(61, 0xffffffff, taggedU32(0), taggedU32(4096))); // REQUEST
223
+ send(reply(63, 0xffffffff, taggedU32(0))); // UNDERFLOW, benign
224
+ send(reply(2, tag, taggedU32(0)));
225
+ }
226
+ });
227
+ try {
228
+ const client = new PulseClient({ socketPath: server.path, cookie: Buffer.alloc(PULSE_COOKIE_BYTES) });
229
+ const events: number[] = [];
230
+ const off = client.onEvent((command: number) => events.push(command));
231
+ await client.connect();
232
+ await client.request(20, Buffer.alloc(0));
233
+ off();
234
+ assert.deepEqual(events, [61]);
235
+ await client.close();
236
+ } finally { await server.close(); }
237
+ });
238
+
239
+ test("close and abort reject every pending request without unhandled rejections", async () => {
240
+ const server = await startFakeServer(() => { /* never answer */ });
241
+ try {
242
+ const client = new PulseClient({ socketPath: server.path, cookie: Buffer.alloc(PULSE_COOKIE_BYTES) });
243
+ await client.connect();
244
+ const first = client.request(20, Buffer.alloc(0)).then(() => "resolved", () => "rejected");
245
+ const controller = new AbortController();
246
+ const second = client.request(20, Buffer.alloc(0), controller.signal).then(() => "resolved", (error: Error) => error.message);
247
+ controller.abort();
248
+ assert.match(await second, /aborted/);
249
+ const third = client.request(20, Buffer.alloc(0)).then(() => "resolved", () => "rejected");
250
+ await client.close();
251
+ assert.deepEqual(await Promise.all([first, third]), ["rejected", "rejected"]);
252
+ await client.close();
253
+ } finally { await server.close(); }
254
+ });
255
+
256
+ test("pending tag budget is bounded", async () => {
257
+ const server = await startFakeServer(() => { /* never answer */ });
258
+ try {
259
+ const client = new PulseClient({ socketPath: server.path, cookie: Buffer.alloc(PULSE_COOKIE_BYTES), requestTimeoutMs: 5000 });
260
+ await client.connect();
261
+ const pending = Array.from({ length: MAX_PENDING_REQUESTS }, () => client.request(20, Buffer.alloc(0)).then(() => "resolved", () => "rejected"));
262
+ await assert.rejects(client.request(20, Buffer.alloc(0)), /pending/);
263
+ await client.close();
264
+ assert.ok((await Promise.all(pending)).every(entry => entry === "rejected"));
265
+ } finally { await server.close(); }
266
+ });
267
+
268
+ test("delayed replies meet the bounded deadline and fragmented/coalesced frames survive", async () => {
269
+ const server = await startFakeServer((request, send, socket) => {
270
+ if (commandOf(request.payload) !== 20) return;
271
+ const payload = reply(2, tagOf(request.payload), taggedU32(0));
272
+ socket.write(frame(0xffffffff, 0, payload).subarray(0, 12));
273
+ setTimeout(() => socket.write(frame(0xffffffff, 0, payload).subarray(12)), 10);
274
+ });
275
+ try {
276
+ const client = new PulseClient({ socketPath: server.path, cookie: Buffer.alloc(PULSE_COOKIE_BYTES), requestTimeoutMs: DEFAULT_REQUEST_TIMEOUT_MS });
277
+ await client.connect();
278
+ assert.ok(await client.request(20, Buffer.alloc(0)));
279
+ await client.close();
280
+ } finally { await server.close(); }
281
+
282
+ const late = await startFakeServer(() => { /* never answer */ });
283
+ try {
284
+ const client = new PulseClient({ socketPath: late.path, cookie: Buffer.alloc(PULSE_COOKIE_BYTES), requestTimeoutMs: 60 });
285
+ await client.connect();
286
+ await assert.rejects(client.request(20, Buffer.alloc(0)), /timed out/);
287
+ await client.close();
288
+ } finally { await late.close(); }
289
+ });
290
+
291
+ test("SHM-flagged frames and truncated transports fail closed", async () => {
292
+ const shm = await startFakeServer((request, send, socket) => {
293
+ if (commandOf(request.payload) === 20) socket.write(frame(3, 0x80000000, Buffer.from([1])));
294
+ });
295
+ try {
296
+ const client = new PulseClient({ socketPath: shm.path, cookie: Buffer.alloc(PULSE_COOKIE_BYTES) });
297
+ await client.connect();
298
+ await assert.rejects(client.request(20, Buffer.alloc(0)), /Pulse client/);
299
+ await client.close();
300
+ } finally { await shm.close(); }
301
+
302
+ const truncated = await startFakeServer((_request, _send, socket) => socket.destroy());
303
+ try {
304
+ const client = new PulseClient({ socketPath: truncated.path, cookie: Buffer.alloc(PULSE_COOKIE_BYTES) });
305
+ await client.connect();
306
+ await assert.rejects(client.request(20, Buffer.alloc(0)), /Pulse client/);
307
+ await client.close();
308
+ } finally { await truncated.close(); }
309
+ });
310
+
311
+ test("probe surfaces a generic failure (no secrets) and still closes when auth fails", async () => {
312
+ const server = await startFakeServer((request, send) => { if (commandOf(request.payload) === 8) send(reply(0, tagOf(request.payload), taggedU32(6))); });
313
+ try {
314
+ const client = new PulseClient({ socketPath: server.path, cookie: Buffer.alloc(PULSE_COOKIE_BYTES) });
315
+ await assert.rejects(client.probe(), (error: Error) => /Pulse client/.test(error.message) && !/\u0000/.test(error.message));
316
+ } finally { await server.close(); }
317
+ });
318
+
319
+ test("sendDataFrame writes a raw memblock frame reserved for N2", async () => {
320
+ const server = await startFakeServer(() => { /* no reply needed */ });
321
+ try {
322
+ const client = new PulseClient({ socketPath: server.path, cookie: Buffer.alloc(PULSE_COOKIE_BYTES) });
323
+ await client.connect();
324
+ client.sendDataFrame(4, Buffer.from([1, 2, 3]));
325
+ await new Promise(resolve => setTimeout(resolve, 10));
326
+ assert.ok(server.frames.some(entry => entry.channel === 4 && entry.payload.equals(Buffer.from([1, 2, 3]))));
327
+ await client.close();
328
+ } finally { await server.close(); }
329
+ });
330
+
331
+ test("connect follows a symlinked socket path (WSLg style) without rewriting it", async () => {
332
+ const seen: number[] = [];
333
+ const server = await startFakeServer(probeHandler("auto_null", seen));
334
+ const dir = await mkdtemp(join(tmpdir(), "pulse-link-"));
335
+ const link = join(dir, "native");
336
+ await symlink(server.path, link);
337
+ try {
338
+ const result = await new PulseClient({ socketPath: link, cookie: Buffer.alloc(PULSE_COOKIE_BYTES) }).probe();
339
+ assert.equal(result.available, true);
340
+ assert.ok(await lstat(link).then(value => value.isSymbolicLink()));
341
+ } finally { await rm(dir, { recursive: true, force: true }); await server.close(); }
342
+ });
343
+
344
+ test("probe uses a zero cookie from a missing cookie path and never creates it", async () => {
345
+ const seen: number[] = [];
346
+ const server = await startFakeServer(probeHandler("auto_null", seen));
347
+ const dir = await mkdtemp(join(tmpdir(), "pulse-cookie-missing-"));
348
+ const cookiePath = join(dir, "cookie");
349
+ try {
350
+ const result = await new PulseClient({ socketPath: server.path, cookiePath }).probe();
351
+ assert.deepEqual(result, { available: true, formats: ["wav"] });
352
+ const auth = server.frames.find(entry => commandOf(entry.payload) === 8)!;
353
+ assert.ok(auth.payload.subarray(20).every(byte => byte === 0));
354
+ await assert.rejects(stat(cookiePath), (error: NodeJS.ErrnoException) => error.code === "ENOENT");
355
+ } finally { await rm(dir, { recursive: true, force: true }); await server.close(); }
356
+ });
@@ -0,0 +1,213 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import {
4
+ CONTROL_CHANNEL, MAX_CONTROL_PAYLOAD, MAX_FRAMES_PER_PUSH, NATIVE_PROTOCOL_VERSION, PulseCommand,
5
+ boundedPulseFrameDecoder, boundedPulseTagReader, boundedPulseTagWriter, encodePulseFrame,
6
+ } from "../lib/notification-pulse-protocol.ts";
7
+
8
+ /** Hex literal with optional spaces, so the golden vectors read like the wire. */
9
+ const hex = (value: string): Buffer => Buffer.from(value.replace(/\s+/g, ""), "hex");
10
+
11
+ test("protocol constants pin the v13 wire subset without extra commands", () => {
12
+ assert.equal(NATIVE_PROTOCOL_VERSION, 13);
13
+ assert.equal(CONTROL_CHANNEL, 0xffffffff);
14
+ assert.deepEqual(PulseCommand, {
15
+ ERROR: 0, TIMEOUT: 1, REPLY: 2, CREATE_PLAYBACK_STREAM: 3, DELETE_PLAYBACK_STREAM: 4,
16
+ AUTH: 8, SET_CLIENT_NAME: 9, DRAIN_PLAYBACK_STREAM: 12, GET_SERVER_INFO: 20, REQUEST: 61,
17
+ });
18
+ assert.equal(MAX_CONTROL_PAYLOAD, 64 * 1024);
19
+ assert.equal(MAX_FRAMES_PER_PUSH, 32);
20
+ });
21
+
22
+ test("golden AUTH setup: tagged u32 version then raw-length arbitrary cookie", () => {
23
+ const out = boundedPulseTagWriter().u32(NATIVE_PROTOCOL_VERSION).arbitrary(Buffer.alloc(256)).finish();
24
+ const expected = Buffer.concat([hex("4c 00 00 00 0d"), Buffer.from([0x78]), hex("00 00 01 00"), Buffer.alloc(256)]);
25
+ assert.deepEqual(out, expected);
26
+ assert.equal(out.length, 266);
27
+ // The cookie carries its own raw big-endian u32 length ('x'), never a tagged 'L' length.
28
+ assert.equal(out[5], 0x78);
29
+ assert.notEqual(out[5], 0x4c);
30
+ });
31
+
32
+ test("golden proplist carries two equal lengths: tagged 'L' then raw 'x'", () => {
33
+ const out = boundedPulseTagWriter().proplist([["application.name", "gentle-shell"]]).finish();
34
+ const expected = Buffer.concat([
35
+ hex("50"), // 'P'
36
+ Buffer.from("tapplication.name\0", "latin1"),
37
+ hex("4c 00 00 00 0d"), // tagged u32 value length
38
+ hex("78 00 00 00 0d"), // raw arbitrary value length, same value
39
+ Buffer.from("gentle-shell\0", "latin1"),
40
+ hex("4e"), // 'N'
41
+ ]);
42
+ assert.deepEqual(out, expected);
43
+ });
44
+
45
+ test("nullable strings encode 'N' for null and 't'+NUL for empty", () => {
46
+ assert.deepEqual(boundedPulseTagWriter().string(null).finish(), Buffer.from([0x4e]));
47
+ assert.deepEqual(boundedPulseTagWriter().string("").finish(), Buffer.from([0x74, 0x00]));
48
+ });
49
+
50
+ test("golden sample_spec encodings match the verified sample.h values", () => {
51
+ const cases: Array<[number, number, number, string]> = [
52
+ [0, 1, 8000, "61 00 01 00 00 1f 40"], // U8
53
+ [3, 2, 44100, "61 03 02 00 00 ac 44"], // S16LE
54
+ [9, 1, 48000, "61 09 01 00 00 bb 80"], // S24LE packed, not S24_32LE
55
+ [7, 2, 192000, "61 07 02 00 02 ee 00"], // S32LE
56
+ ];
57
+ for (const [format, channels, rate, expected] of cases)
58
+ assert.deepEqual(boundedPulseTagWriter().sampleSpec({ format, channels, rate }).finish(), hex(expected), `${format}/${channels}/${rate}`);
59
+ });
60
+
61
+ test("golden channel_map, cvolume, usec, boolean and u8 primitives", () => {
62
+ assert.deepEqual(boundedPulseTagWriter().channelMap([0, 1]).finish(), hex("6d 02 00 01"));
63
+ assert.deepEqual(boundedPulseTagWriter().cvolume([0x00010000]).finish(), hex("76 01 00 01 00 00"));
64
+ assert.deepEqual(boundedPulseTagWriter().usec(0x0102030405060708n).finish(), hex("55 01 02 03 04 05 06 07 08"));
65
+ assert.deepEqual(boundedPulseTagWriter().boolean(true).finish(), Buffer.from([0x31]));
66
+ assert.deepEqual(boundedPulseTagWriter().boolean(false).finish(), Buffer.from([0x30]));
67
+ assert.deepEqual(boundedPulseTagWriter().u8(0x5a).finish(), hex("42 5a"));
68
+ });
69
+
70
+ test("reader decodes every primitive back and assertEOF rejects trailing bytes", () => {
71
+ const buffer = boundedPulseTagWriter().u32(13).u8(7).string("x").string(null).arbitrary(Buffer.from([1, 2]))
72
+ .boolean(true).usec(5n).sampleSpec({ format: 3, channels: 1, rate: 8000 }).channelMap([0])
73
+ .cvolume([0x10000]).proplist([["k", Buffer.from([9])]]).finish();
74
+ const reader = boundedPulseTagReader(buffer);
75
+ assert.equal(reader.u32(), 13);
76
+ assert.equal(reader.u8(), 7);
77
+ assert.equal(reader.string(), "x");
78
+ assert.equal(reader.string(), null);
79
+ assert.deepEqual(reader.arbitrary(), Buffer.from([1, 2]));
80
+ assert.equal(reader.boolean(), true);
81
+ assert.equal(reader.usec(), 5n);
82
+ assert.deepEqual(reader.sampleSpec(), { format: 3, channels: 1, rate: 8000 });
83
+ assert.deepEqual(reader.channelMap(), [0]);
84
+ assert.deepEqual(reader.cvolume(), [0x10000]);
85
+ assert.deepEqual(reader.proplist(), new Map([["k", Buffer.from([9])]]));
86
+ reader.assertEOF();
87
+ const trailing = boundedPulseTagReader(hex("31 00"));
88
+ assert.equal(trailing.boolean(), true);
89
+ assert.throws(() => trailing.assertEOF());
90
+ });
91
+
92
+ test("reader fails closed on truncated tags, values and bad UTF-8", () => {
93
+ assert.throws(() => boundedPulseTagReader(hex("4c 00 00 00")).u32());
94
+ assert.throws(() => boundedPulseTagReader(hex("78 00 00")).arbitrary());
95
+ assert.throws(() => boundedPulseTagReader(Buffer.from([0x62])).boolean()); // 'b' is not a valid boolean tag
96
+ assert.throws(() => boundedPulseTagReader(hex("74 ff 00")).string());
97
+ assert.throws(() => boundedPulseTagReader(Buffer.alloc(0)).u8());
98
+ });
99
+
100
+ test("reader rejects a proplist whose declared length differs from the arbitrary length", () => {
101
+ const bytes = Buffer.concat([
102
+ hex("50"),
103
+ Buffer.from("tkey\0", "latin1"),
104
+ hex("4c 00 00 00 05"),
105
+ hex("78 00 00 00 03"),
106
+ Buffer.from("abc", "latin1"),
107
+ hex("4e"),
108
+ ]);
109
+ assert.throws(() => boundedPulseTagReader(bytes).proplist());
110
+ });
111
+
112
+ test("writer rejects overflow, embedded NUL, control keys, duplicates and oversize", () => {
113
+ const writer = () => boundedPulseTagWriter();
114
+ assert.throws(() => writer().u32(-1));
115
+ assert.throws(() => writer().u32(0x1_0000_0000));
116
+ assert.throws(() => writer().u32(1.5));
117
+ assert.throws(() => writer().u8(256));
118
+ assert.throws(() => writer().usec(-1n));
119
+ assert.throws(() => writer().usec(1n << 64n));
120
+ assert.throws(() => writer().boolean("yes" as unknown as boolean));
121
+ assert.throws(() => writer().string("a\0b"));
122
+ assert.throws(() => writer().proplist([["", "x"]]));
123
+ assert.throws(() => writer().proplist([["a\0b", "x"]]));
124
+ assert.throws(() => writer().proplist([["a\nb", "x"]]));
125
+ assert.throws(() => writer().proplist([["k", "a"], ["k", "b"]]));
126
+ assert.throws(() => writer().arbitrary(Buffer.alloc(MAX_CONTROL_PAYLOAD)));
127
+ });
128
+
129
+ test("golden control frame pins the 20-byte descriptor", () => {
130
+ const payload = hex("4c 00 00 00 0d");
131
+ const frame = encodePulseFrame({ channel: CONTROL_CHANNEL, offset: 0n, flags: 0 }, payload);
132
+ assert.deepEqual(frame, Buffer.concat([hex("00 00 00 05 ff ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"), payload]));
133
+ });
134
+
135
+ test("decoder reassembles fragmented headers and payloads across pushes", () => {
136
+ const payload = Buffer.from("hello");
137
+ const wire = encodePulseFrame({ channel: 3, offset: 0n, flags: 0 }, payload);
138
+ const decoder = boundedPulseFrameDecoder();
139
+ const frames: ReturnType<typeof decoder.push> = [];
140
+ for (const byte of wire) frames.push(...decoder.push(Buffer.from([byte])));
141
+ assert.equal(frames.length, 1);
142
+ assert.equal(frames[0]!.channel, 3);
143
+ assert.equal(frames[0]!.offset, 0n);
144
+ assert.equal(frames[0]!.flags, 0);
145
+ assert.deepEqual(frames[0]!.payload, payload);
146
+ });
147
+
148
+ test("decoder splits coalesced frames and preserves order", () => {
149
+ const first = encodePulseFrame({ channel: CONTROL_CHANNEL, offset: 0n, flags: 0 }, Buffer.from([1]));
150
+ const second = encodePulseFrame({ channel: 5, offset: 0n, flags: 0 }, Buffer.from([2, 3]));
151
+ const frames = boundedPulseFrameDecoder().push(Buffer.concat([first, second]));
152
+ assert.equal(frames.length, 2);
153
+ assert.equal(frames[0]!.channel, CONTROL_CHANNEL);
154
+ assert.equal(frames[1]!.channel, 5);
155
+ assert.deepEqual(frames[1]!.payload, Buffer.from([2, 3]));
156
+ });
157
+
158
+ test("decoder fails closed on invalid lengths, SHM flags and control offsets", () => {
159
+ const decoder = () => boundedPulseFrameDecoder();
160
+ assert.throws(() => decoder().push(hex("00 01 00 01 ff ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"))); // > 64 KiB
161
+ assert.throws(() => decoder().push(hex("00 00 00 00 ff ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"))); // zero length
162
+ assert.throws(() => decoder().push(hex("00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 00"))); // SHM flag
163
+ assert.throws(() => decoder().push(hex("00 00 00 01 ff ff ff ff 00 00 00 00 00 00 00 01 00 00 00 00"))); // control offset != 0
164
+ assert.throws(() => encodePulseFrame({ channel: CONTROL_CHANNEL, offset: 1n, flags: 0 }, Buffer.from([1])));
165
+ });
166
+
167
+ test("decoder bounds frames per call and does not buffer beyond one frame budget", () => {
168
+ const many = Buffer.concat(Array.from({ length: MAX_FRAMES_PER_PUSH + 1 }, () =>
169
+ encodePulseFrame({ channel: CONTROL_CHANNEL, offset: 0n, flags: 0 }, Buffer.from([1]))));
170
+ assert.throws(() => boundedPulseFrameDecoder().push(many));
171
+ });
172
+
173
+ test("decoder accepts arbitrary fragmentation boundaries", () => {
174
+ const wire = Buffer.concat([
175
+ encodePulseFrame({ channel: CONTROL_CHANNEL, offset: 0n, flags: 0 }, hex("4c 00 00 00 0d")),
176
+ encodePulseFrame({ channel: 7, offset: 0n, flags: 0 }, Buffer.alloc(5, 0xaa)),
177
+ ]);
178
+ for (let size = 1; size <= wire.length; size += 3) {
179
+ const decoder = boundedPulseFrameDecoder();
180
+ const frames: ReturnType<typeof decoder.push> = [];
181
+ for (let offset = 0; offset < wire.length; offset += size) frames.push(...decoder.push(wire.subarray(offset, offset + size)));
182
+ assert.equal(frames.length, 2);
183
+ assert.deepEqual(frames[0]!.payload, hex("4c 00 00 00 0d"));
184
+ assert.deepEqual(frames[1]!.payload, Buffer.alloc(5, 0xaa));
185
+ }
186
+ });
187
+
188
+ test("memblock frames keep a nonzero offset with an accepted seek mode", () => {
189
+ const wire = encodePulseFrame({ channel: 9, offset: 0x1_0000_0002n, flags: 1 }, Buffer.from([1, 2, 3]));
190
+ const frames = boundedPulseFrameDecoder().push(wire);
191
+ assert.equal(frames.length, 1);
192
+ assert.equal(frames[0]!.channel, 9);
193
+ assert.equal(frames[0]!.offset, 0x1_0000_0002n);
194
+ assert.equal(frames[0]!.flags, 1);
195
+ assert.deepEqual(frames[0]!.payload, Buffer.from([1, 2, 3]));
196
+ const badSeek = Buffer.from(wire);
197
+ badSeek.writeUInt32BE(3, 16);
198
+ assert.throws(() => boundedPulseFrameDecoder().push(badSeek));
199
+ });
200
+
201
+ test("proplist binary values are preserved byte-for-byte without an added NUL", () => {
202
+ const value = Buffer.from([0x00, 0x7f, 0xff]);
203
+ const entries = boundedPulseTagReader(boundedPulseTagWriter().proplist([["k", value]]).finish()).proplist();
204
+ assert.deepEqual(entries.get("k"), value);
205
+ });
206
+
207
+ test("frame encoder rejects oversized payloads and invalid channels", () => {
208
+ assert.throws(() => encodePulseFrame({ channel: 0, offset: 0n, flags: 0 }, Buffer.alloc(MAX_CONTROL_PAYLOAD + 1)));
209
+ assert.throws(() => encodePulseFrame({ channel: 0, offset: 0n, flags: 0 }, Buffer.alloc(0)));
210
+ assert.throws(() => encodePulseFrame({ channel: -1, offset: 0n, flags: 0 }, Buffer.from([1])));
211
+ const empty = boundedPulseFrameDecoder().push(Buffer.alloc(0));
212
+ assert.deepEqual(empty, []);
213
+ });