@polderlabs/bizar 6.3.0 → 7.0.1

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 (264) hide show
  1. package/.claude/hooks/sessionend-recall.mjs +3 -3
  2. package/.claude/hooks/worker-suggest.mjs +110 -0
  3. package/.claude/settings.json +9 -0
  4. package/README.md +125 -47
  5. package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js → EnvVarsSection-weVnLz3Z.js} +3 -3
  6. package/bizar-dash/dist/assets/EnvVarsSection-weVnLz3Z.js.map +1 -0
  7. package/bizar-dash/dist/assets/MobileChat-BRFjer8p.js +1 -0
  8. package/bizar-dash/dist/assets/{MobileChat-BJrqwVDd.js.map → MobileChat-BRFjer8p.js.map} +1 -1
  9. package/bizar-dash/dist/assets/MobileSettings-Dw5jyJWi.js +1 -0
  10. package/bizar-dash/dist/assets/MobileSettings-Dw5jyJWi.js.map +1 -0
  11. package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js → Toast-WTxc3m2g.js} +1 -1
  12. package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js.map → Toast-WTxc3m2g.js.map} +1 -1
  13. package/bizar-dash/dist/assets/flow-BpU0-gK2.js +1 -0
  14. package/bizar-dash/dist/assets/flow-BpU0-gK2.js.map +1 -0
  15. package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js +1 -0
  16. package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js.map +1 -0
  17. package/bizar-dash/dist/assets/{icons-Do5N2jmX.js → icons-B0AKnYI3.js} +98 -103
  18. package/bizar-dash/dist/assets/icons-B0AKnYI3.js.map +1 -0
  19. package/bizar-dash/dist/assets/main-BE4Uh9im.css +1 -0
  20. package/bizar-dash/dist/assets/main-DqC58FEZ.js +18 -0
  21. package/bizar-dash/dist/assets/main-DqC58FEZ.js.map +1 -0
  22. package/bizar-dash/dist/assets/markdown-w2bDDkAH.js +1 -0
  23. package/bizar-dash/dist/assets/markdown-w2bDDkAH.js.map +1 -0
  24. package/bizar-dash/dist/assets/mobile-DRZsK3ic.js +1 -0
  25. package/bizar-dash/dist/assets/{mobile-DYCHcUpq.js.map → mobile-DRZsK3ic.js.map} +1 -1
  26. package/bizar-dash/dist/assets/mobile-layout-BWLtwtfQ.css +1 -0
  27. package/bizar-dash/dist/assets/mobile-layout-Ckru3HKJ.js +2 -0
  28. package/bizar-dash/dist/assets/mobile-layout-Ckru3HKJ.js.map +1 -0
  29. package/bizar-dash/dist/assets/react-vendor-BnuhLJ6X.css +1 -0
  30. package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js +46 -0
  31. package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js.map +1 -0
  32. package/bizar-dash/dist/assets/useSlashCommands-BAxVOU_a.js +2 -0
  33. package/bizar-dash/dist/assets/useSlashCommands-BAxVOU_a.js.map +1 -0
  34. package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js +45 -0
  35. package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js.map +1 -0
  36. package/bizar-dash/dist/index.html +13 -11
  37. package/bizar-dash/dist/mobile.html +8 -5
  38. package/bizar-dash/src/server/api.mjs +11 -0
  39. package/bizar-dash/src/server/memory-consolidator.mjs +225 -0
  40. package/bizar-dash/src/server/memory-distillation.mjs +296 -0
  41. package/bizar-dash/src/server/routes/distill.mjs +129 -0
  42. package/bizar-dash/src/server/routes/goal-planner.mjs +236 -0
  43. package/bizar-dash/src/web/App.tsx +4 -0
  44. package/bizar-dash/src/web/components/Tag.tsx +18 -5
  45. package/bizar-dash/src/web/components/Topbar.tsx +3 -0
  46. package/bizar-dash/src/web/components/agents/CommunicationLog.tsx +126 -0
  47. package/bizar-dash/src/web/components/agents/DependencyGraph.tsx +163 -0
  48. package/bizar-dash/src/web/components/agents/QualityGates.tsx +123 -0
  49. package/bizar-dash/src/web/components/agents/RealTimeEventLog.tsx +127 -0
  50. package/bizar-dash/src/web/components/agents/RoutingDecisions.test.tsx +148 -0
  51. package/bizar-dash/src/web/components/agents/RoutingDecisions.tsx +160 -0
  52. package/bizar-dash/src/web/components/goals/GoalInput.tsx +86 -0
  53. package/bizar-dash/src/web/components/goals/PlanVisualization.tsx +111 -0
  54. package/bizar-dash/src/web/lib/goapPlanner.ts +614 -0
  55. package/bizar-dash/src/web/lib/types.ts +25 -0
  56. package/bizar-dash/src/web/mobile/MobileApp.tsx +33 -2
  57. package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +2 -2
  58. package/bizar-dash/src/web/mobile/MobileEval.tsx +4 -4
  59. package/bizar-dash/src/web/mobile/MobileMemory.tsx +2 -2
  60. package/bizar-dash/src/web/mobile/MobileSettings.tsx +3 -3
  61. package/bizar-dash/src/web/mobile/MobileTasks.tsx +2 -2
  62. package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +3 -3
  63. package/bizar-dash/src/web/styles/agents-redesign.css +311 -0
  64. package/bizar-dash/src/web/styles/main.css +251 -0
  65. package/bizar-dash/src/web/styles/mobile.css +41 -0
  66. package/bizar-dash/src/web/styles/tasks-redesign.css +253 -0
  67. package/bizar-dash/src/web/ui/controls/Button.tsx +77 -0
  68. package/bizar-dash/src/web/ui/controls/Checkbox.tsx +95 -0
  69. package/bizar-dash/src/web/ui/controls/IconButton.tsx +66 -0
  70. package/bizar-dash/src/web/ui/controls/Kbd.tsx +24 -0
  71. package/bizar-dash/src/web/ui/controls/NumberInput.tsx +77 -0
  72. package/bizar-dash/src/web/ui/controls/RadioGroup.tsx +109 -0
  73. package/bizar-dash/src/web/ui/controls/SearchInput.tsx +91 -0
  74. package/bizar-dash/src/web/ui/controls/Select.tsx +92 -0
  75. package/bizar-dash/src/web/ui/controls/Slider.tsx +70 -0
  76. package/bizar-dash/src/web/ui/controls/TextInput.tsx +67 -0
  77. package/bizar-dash/src/web/ui/controls/Textarea.tsx +56 -0
  78. package/bizar-dash/src/web/ui/controls/Toggle.tsx +79 -0
  79. package/bizar-dash/src/web/ui/controls/controls.css +476 -0
  80. package/bizar-dash/src/web/ui/controls/index.ts +35 -0
  81. package/bizar-dash/src/web/ui/data/BarChart.tsx +60 -0
  82. package/bizar-dash/src/web/ui/data/DataTable.tsx +216 -0
  83. package/bizar-dash/src/web/ui/data/EmptyState.tsx +60 -0
  84. package/bizar-dash/src/web/ui/data/ErrorState.tsx +56 -0
  85. package/bizar-dash/src/web/ui/data/KeyValueList.tsx +106 -0
  86. package/bizar-dash/src/web/ui/data/LoadingState.tsx +65 -0
  87. package/bizar-dash/src/web/ui/data/Sparkline.tsx +87 -0
  88. package/bizar-dash/src/web/ui/data/StatTile.tsx +87 -0
  89. package/bizar-dash/src/web/ui/data/data.css +535 -0
  90. package/bizar-dash/src/web/ui/data/index.ts +31 -0
  91. package/bizar-dash/src/web/ui/feedback/Badge.tsx +49 -0
  92. package/bizar-dash/src/web/ui/feedback/Dialog.tsx +209 -0
  93. package/bizar-dash/src/web/ui/feedback/ProgressBar.tsx +69 -0
  94. package/bizar-dash/src/web/ui/feedback/StatusDot.tsx +50 -0
  95. package/bizar-dash/src/web/ui/feedback/Toast.tsx +200 -0
  96. package/bizar-dash/src/web/ui/feedback/Tooltip.tsx +126 -0
  97. package/bizar-dash/src/web/ui/feedback/feedback.css +314 -0
  98. package/bizar-dash/src/web/ui/feedback/index.ts +43 -0
  99. package/bizar-dash/src/web/ui/index.ts +56 -0
  100. package/bizar-dash/src/web/ui/layout/AppShell.tsx +51 -0
  101. package/bizar-dash/src/web/ui/layout/Breadcrumbs.tsx +97 -0
  102. package/bizar-dash/src/web/ui/layout/Panel.tsx +73 -0
  103. package/bizar-dash/src/web/ui/layout/PanelHeader.tsx +39 -0
  104. package/bizar-dash/src/web/ui/layout/Sidebar.tsx +131 -0
  105. package/bizar-dash/src/web/ui/layout/Tabs.tsx +133 -0
  106. package/bizar-dash/src/web/ui/layout/Topbar.tsx +42 -0
  107. package/bizar-dash/src/web/ui/layout/ViewHeader.tsx +48 -0
  108. package/bizar-dash/src/web/ui/layout/index.ts +34 -0
  109. package/bizar-dash/src/web/ui/layout/layout.css +464 -0
  110. package/bizar-dash/src/web/ui/navigation/CommandPalette.tsx +199 -0
  111. package/bizar-dash/src/web/ui/navigation/NavGroup.tsx +95 -0
  112. package/bizar-dash/src/web/ui/navigation/NavLink.tsx +93 -0
  113. package/bizar-dash/src/web/ui/navigation/index.ts +17 -0
  114. package/bizar-dash/src/web/ui/navigation/navigation.css +343 -0
  115. package/bizar-dash/src/web/ui/primitives/Box.tsx +91 -0
  116. package/bizar-dash/src/web/ui/primitives/Grid.tsx +49 -0
  117. package/bizar-dash/src/web/ui/primitives/Inline.tsx +55 -0
  118. package/bizar-dash/src/web/ui/primitives/Separator.tsx +46 -0
  119. package/bizar-dash/src/web/ui/primitives/Stack.tsx +54 -0
  120. package/bizar-dash/src/web/ui/primitives/VisuallyHidden.tsx +26 -0
  121. package/bizar-dash/src/web/ui/primitives/index.ts +23 -0
  122. package/bizar-dash/src/web/ui/primitives/primitives.css +183 -0
  123. package/bizar-dash/src/web/ui/styles/globals.css +86 -0
  124. package/bizar-dash/src/web/ui/styles/reset.css +125 -0
  125. package/bizar-dash/src/web/ui/styles/tokens.css +208 -0
  126. package/bizar-dash/src/web/ui/theme/ThemeProvider.tsx +142 -0
  127. package/bizar-dash/src/web/ui/theme/tokens.ts +131 -0
  128. package/bizar-dash/src/web/ui/theme/useTheme.ts +22 -0
  129. package/bizar-dash/src/web/ui/utils/cx.ts +42 -0
  130. package/bizar-dash/src/web/views/Agents.tsx +516 -268
  131. package/bizar-dash/src/web/views/GoalPlanner.tsx +158 -0
  132. package/bizar-dash/src/web/views/Overview.tsx +985 -448
  133. package/bizar-dash/src/web/views/Tasks.tsx +271 -273
  134. package/bizar-dash/tests/communication-log.test.tsx +67 -0
  135. package/bizar-dash/tests/dep-graph.test.tsx +40 -0
  136. package/bizar-dash/tests/event-log.test.tsx +61 -0
  137. package/bizar-dash/tests/goal-input.test.tsx +72 -0
  138. package/bizar-dash/tests/goap-planner.test.ts +68 -0
  139. package/bizar-dash/tests/quality-gates.test.tsx +59 -0
  140. package/bizar-dash/tests/setup.ts +31 -1
  141. package/bizar-dash/tests/ui/controls/Button.test.tsx +50 -0
  142. package/bizar-dash/tests/ui/controls/Checkbox.test.tsx +44 -0
  143. package/bizar-dash/tests/ui/controls/IconButton.test.tsx +37 -0
  144. package/bizar-dash/tests/ui/controls/Kbd.test.tsx +39 -0
  145. package/bizar-dash/tests/ui/controls/NumberInput.test.tsx +56 -0
  146. package/bizar-dash/tests/ui/controls/RadioGroup.test.tsx +58 -0
  147. package/bizar-dash/tests/ui/controls/SearchInput.test.tsx +52 -0
  148. package/bizar-dash/tests/ui/controls/Select.test.tsx +66 -0
  149. package/bizar-dash/tests/ui/controls/Slider.test.tsx +41 -0
  150. package/bizar-dash/tests/ui/controls/TextInput.test.tsx +44 -0
  151. package/bizar-dash/tests/ui/controls/Textarea.test.tsx +56 -0
  152. package/bizar-dash/tests/ui/controls/Toggle.test.tsx +39 -0
  153. package/bizar-dash/tests/ui/data/BarChart.test.tsx +55 -0
  154. package/bizar-dash/tests/ui/data/DataTable.test.tsx +112 -0
  155. package/bizar-dash/tests/ui/data/EmptyState.test.tsx +57 -0
  156. package/bizar-dash/tests/ui/data/ErrorState.test.tsx +47 -0
  157. package/bizar-dash/tests/ui/data/KeyValueList.test.tsx +59 -0
  158. package/bizar-dash/tests/ui/data/LoadingState.test.tsx +37 -0
  159. package/bizar-dash/tests/ui/data/Sparkline.test.tsx +56 -0
  160. package/bizar-dash/tests/ui/data/StatTile.test.tsx +69 -0
  161. package/bizar-dash/tests/ui/feedback/Badge.test.tsx +41 -0
  162. package/bizar-dash/tests/ui/feedback/Dialog.test.tsx +99 -0
  163. package/bizar-dash/tests/ui/feedback/ProgressBar.test.tsx +66 -0
  164. package/bizar-dash/tests/ui/feedback/StatusDot.test.tsx +41 -0
  165. package/bizar-dash/tests/ui/feedback/Toast.test.tsx +124 -0
  166. package/bizar-dash/tests/ui/feedback/Tooltip.test.tsx +102 -0
  167. package/bizar-dash/tests/ui/layout/AppShell.test.tsx +50 -0
  168. package/bizar-dash/tests/ui/layout/Breadcrumbs.test.tsx +85 -0
  169. package/bizar-dash/tests/ui/layout/Panel.test.tsx +52 -0
  170. package/bizar-dash/tests/ui/layout/PanelHeader.test.tsx +32 -0
  171. package/bizar-dash/tests/ui/layout/Sidebar.test.tsx +95 -0
  172. package/bizar-dash/tests/ui/layout/Tabs.test.tsx +87 -0
  173. package/bizar-dash/tests/ui/layout/Topbar.test.tsx +41 -0
  174. package/bizar-dash/tests/ui/layout/ViewHeader.test.tsx +47 -0
  175. package/bizar-dash/tests/ui/navigation/CommandPalette.test.tsx +105 -0
  176. package/bizar-dash/tests/ui/navigation/NavGroup.test.tsx +55 -0
  177. package/bizar-dash/tests/ui/navigation/NavLink.test.tsx +61 -0
  178. package/bizar-dash/tests/ui/primitives/Box.test.tsx +49 -0
  179. package/bizar-dash/tests/ui/primitives/Grid.test.tsx +37 -0
  180. package/bizar-dash/tests/ui/primitives/Inline.test.tsx +33 -0
  181. package/bizar-dash/tests/ui/primitives/Separator.test.tsx +34 -0
  182. package/bizar-dash/tests/ui/primitives/Stack.test.tsx +45 -0
  183. package/bizar-dash/tests/ui/primitives/VisuallyHidden.test.tsx +27 -0
  184. package/bizar-dash/tests/views/Agents.test.tsx +359 -0
  185. package/bizar-dash/tests/views/Overview.test.tsx +413 -0
  186. package/bizar-dash/tests/views/Tasks.test.tsx +250 -0
  187. package/bizar-dash/vitest.config.ts +3 -1
  188. package/cli/__tests__/cost-gate.test.mjs +301 -0
  189. package/cli/__tests__/feature-list-bridge.test.mjs +371 -0
  190. package/cli/bin.mjs +38 -0
  191. package/cli/commands/claim.mjs +282 -0
  192. package/cli/commands/cost.mjs +279 -0
  193. package/cli/cost-gate.mjs +501 -0
  194. package/cli/feature-list-bridge.mjs +400 -0
  195. package/cli/memory-constants.mjs +17 -0
  196. package/cli/provision-claude.mjs +94 -0
  197. package/cli/worker-dispatcher.mjs +259 -0
  198. package/cli/worker-dispatcher.test.mjs +204 -0
  199. package/config/trigger-patterns.json +186 -0
  200. package/package.json +2 -2
  201. package/packages/sdk/package.json +1 -1
  202. package/packages/sdk/src/agent-registry.ts +304 -0
  203. package/packages/sdk/src/consensus/byzantine.ts +587 -0
  204. package/packages/sdk/src/consensus/index.ts +147 -0
  205. package/packages/sdk/src/consensus/queen.ts +173 -0
  206. package/packages/sdk/src/consensus/types.ts +199 -0
  207. package/packages/sdk/src/federation/audit.ts +156 -0
  208. package/packages/sdk/src/federation/budget.ts +269 -0
  209. package/packages/sdk/src/federation/envelope.ts +144 -0
  210. package/packages/sdk/src/federation/hmac.ts +160 -0
  211. package/packages/sdk/src/federation/index.ts +443 -0
  212. package/packages/sdk/src/federation/pii.ts +241 -0
  213. package/packages/sdk/src/federation/policy.ts +112 -0
  214. package/packages/sdk/src/federation/trust.ts +146 -0
  215. package/packages/sdk/src/index.ts +67 -0
  216. package/packages/sdk/src/mcp/server.ts +351 -1
  217. package/packages/sdk/src/router/codemod-intent.ts +125 -0
  218. package/packages/sdk/src/router/index.ts +247 -0
  219. package/packages/sdk/src/router/memory-distillation-shim.mjs +18 -0
  220. package/packages/sdk/src/router/memory-distillation.ts +249 -0
  221. package/packages/sdk/src/router/model-router.ts +235 -0
  222. package/packages/sdk/src/router/q-learning-router.ts +304 -0
  223. package/packages/sdk/src/swarm-topology.ts +279 -0
  224. package/packages/sdk/tests/agent-registry.test.ts +285 -0
  225. package/packages/sdk/tests/consensus.test.ts +376 -0
  226. package/packages/sdk/tests/federation/audit.test.ts +164 -0
  227. package/packages/sdk/tests/federation/budget.test.ts +189 -0
  228. package/packages/sdk/tests/federation/envelope.test.ts +120 -0
  229. package/packages/sdk/tests/federation/hmac.test.ts +194 -0
  230. package/packages/sdk/tests/federation/orchestrator.test.ts +176 -0
  231. package/packages/sdk/tests/federation/pii.test.ts +106 -0
  232. package/packages/sdk/tests/federation/policy.test.ts +140 -0
  233. package/packages/sdk/tests/federation/trust.test.ts +124 -0
  234. package/packages/sdk/tests/mcp-tools.test.ts +204 -0
  235. package/packages/sdk/tests/memory-distillation.test.mjs +186 -0
  236. package/packages/sdk/tests/model-router.test.mjs +108 -0
  237. package/packages/sdk/tests/q-learning-router.test.mjs +100 -0
  238. package/packages/sdk/tests/router-orchestrator.test.mjs +172 -0
  239. package/packages/sdk/tests/router.test.mjs +91 -0
  240. package/packages/sdk/tests/sdk.test.mjs +26 -2
  241. package/packages/sdk/tests/swarm-topology.test.ts +258 -0
  242. package/scripts/bh-full-e2e.mjs +11 -15
  243. package/bizar-dash/dist/assets/EnvVarsSection-B58aiJiE.js.map +0 -1
  244. package/bizar-dash/dist/assets/MobileChat-BJrqwVDd.js +0 -1
  245. package/bizar-dash/dist/assets/MobileSettings-CEQNJNLJ.js +0 -1
  246. package/bizar-dash/dist/assets/MobileSettings-CEQNJNLJ.js.map +0 -1
  247. package/bizar-dash/dist/assets/icons-Do5N2jmX.js.map +0 -1
  248. package/bizar-dash/dist/assets/main-IvfQAOfy.js +0 -18
  249. package/bizar-dash/dist/assets/main-IvfQAOfy.js.map +0 -1
  250. package/bizar-dash/dist/assets/main-ietCEg_R.css +0 -1
  251. package/bizar-dash/dist/assets/markdown-tOLaD6nm.js +0 -1
  252. package/bizar-dash/dist/assets/markdown-tOLaD6nm.js.map +0 -1
  253. package/bizar-dash/dist/assets/mobile-DYCHcUpq.js +0 -1
  254. package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js +0 -2
  255. package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js.map +0 -1
  256. package/bizar-dash/dist/assets/mobile-layout-CJnZNLy3.css +0 -1
  257. package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js +0 -40
  258. package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js.map +0 -1
  259. package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js +0 -2
  260. package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js.map +0 -1
  261. package/bizar-dash/dist/assets/vendor-C843201K.js +0 -29
  262. package/bizar-dash/dist/assets/vendor-C843201K.js.map +0 -1
  263. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  264. package/packages/sdk/package-lock.json +0 -32
@@ -0,0 +1,106 @@
1
+ /**
2
+ * federation/pii.test.ts — F-038 PII pipeline tests.
3
+ *
4
+ * Covers:
5
+ * - soc2 mode redacts emails + SSNs + IPs, blocks credit cards.
6
+ * - gdpr mode also redacts names + addresses.
7
+ * - hipaa mode is the strictest (redacts everything sensitive).
8
+ * - permissive mode only blocks the highest-severity categories.
9
+ * - PiiPipeline class wrapper setMode / process flow.
10
+ */
11
+
12
+ import { describe, test, expect } from "vitest";
13
+ import {
14
+ PII_MODES,
15
+ PiiPipeline,
16
+ apply,
17
+ isPiiMode,
18
+ type PiiMode,
19
+ } from "../../src/federation/pii.js";
20
+
21
+ describe("federation/pii — apply() per mode", () => {
22
+ test("soc2 redacts emails + IPs + SSNs, blocks credit cards", () => {
23
+ const r = apply("contact alice@example.com or 192.168.1.1, SSN 123-45-6789, CC 4111-1111-1111-1111", "soc2");
24
+ expect(r.blocked).toBe(true);
25
+ // Even though we blocked CC, the transformed text is empty.
26
+ expect(r.transformed).toBe("");
27
+ expect(r.detections.length).toBeGreaterThanOrEqual(3);
28
+ });
29
+
30
+ test("soc2 non-CC text returns redacted output", () => {
31
+ const r = apply("contact alice@example.com from 10.0.0.1", "soc2");
32
+ expect(r.blocked).toBe(false);
33
+ expect(r.transformed).toContain("[REDACTED:email]");
34
+ expect(r.transformed).toContain("[REDACTED:ip_address]");
35
+ expect(r.transformed).not.toContain("alice@example.com");
36
+ expect(r.transformed).not.toContain("10.0.0.1");
37
+ });
38
+
39
+ test("gdpr redacts emails, IPs, names, addresses", () => {
40
+ const r = apply("Dr. Alice Smith at alice@example.com from 10 Downing St", "gdpr");
41
+ expect(r.transformed).toContain("[REDACTED:email]");
42
+ expect(r.transformed).not.toContain("alice@example.com");
43
+ });
44
+
45
+ test("hipaa is the strictest — emails + SSNs + IPs + phones redacted", () => {
46
+ const r = apply("call 555-123-4567 or email alice@example.com, SSN 123-45-6789", "hipaa");
47
+ expect(r.transformed).not.toContain("alice@example.com");
48
+ expect(r.transformed).not.toContain("123-45-6789");
49
+ expect(r.transformed).not.toContain("555-123-4567");
50
+ });
51
+
52
+ test("permissive passes emails + IPs but blocks AWS keys + private keys + github tokens", () => {
53
+ const clean = apply("contact alice@example.com from 10.0.0.1", "permissive");
54
+ expect(clean.blocked).toBe(false);
55
+ expect(clean.transformed).toContain("alice@example.com");
56
+ expect(clean.transformed).toContain("10.0.0.1");
57
+
58
+ const awsKey = apply("AWS key: AKIAIOSFODNN7EXAMPLE", "permissive");
59
+ expect(awsKey.blocked).toBe(true);
60
+
61
+ const priv = apply("-----BEGIN RSA PRIVATE KEY-----", "permissive");
62
+ expect(priv.blocked).toBe(true);
63
+
64
+ const gh = apply("github token: ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "permissive");
65
+ expect(gh.blocked).toBe(true);
66
+ });
67
+
68
+ test("unknown mode throws", () => {
69
+ expect(() => apply("text", "unknown" as unknown as PiiMode)).toThrow(/unknown mode/);
70
+ });
71
+
72
+ test("each declared mode is accepted by isPiiMode", () => {
73
+ for (const m of PII_MODES) {
74
+ expect(isPiiMode(m)).toBe(true);
75
+ }
76
+ expect(isPiiMode(null)).toBe(false);
77
+ expect(isPiiMode("xx")).toBe(false);
78
+ });
79
+ });
80
+
81
+ describe("federation/pii — PiiPipeline class", () => {
82
+ test("defaults to soc2 mode", () => {
83
+ const p = new PiiPipeline();
84
+ expect(p.getMode()).toBe("soc2");
85
+ });
86
+
87
+ test("setMode accepts the 4 documented modes", () => {
88
+ const p = new PiiPipeline();
89
+ for (const m of PII_MODES) {
90
+ p.setMode(m);
91
+ expect(p.getMode()).toBe(m);
92
+ }
93
+ });
94
+
95
+ test("setMode throws on unknown mode", () => {
96
+ const p = new PiiPipeline();
97
+ expect(() => p.setMode("nope" as unknown as PiiMode)).toThrow(/unknown mode/);
98
+ });
99
+
100
+ test("process() returns the same shape as apply()", () => {
101
+ const p = new PiiPipeline({ mode: "gdpr" });
102
+ const r = p.process("alice@example.com");
103
+ expect(r.transformed).toContain("[REDACTED:email]");
104
+ expect(r.mode).toBe("gdpr");
105
+ });
106
+ });
@@ -0,0 +1,140 @@
1
+ /**
2
+ * federation/policy.test.ts — F-038 PolicyEngine tests.
3
+ *
4
+ * Covers:
5
+ * - maxHops enforcement: hopCount >= maxHops denies.
6
+ * - Action allowlist: unknown + disallowed message kinds denied.
7
+ * - Peer blocklist: blocked source node denied.
8
+ * - Budget validation: missing + invalid budget denied.
9
+ * - Runtime mutators: allowMessageType / denyMessageType / setBlocked.
10
+ */
11
+
12
+ import { describe, test, expect, beforeEach } from "vitest";
13
+ import { PolicyEngine, DEFAULT_MAX_HOPS } from "../../src/federation/policy.js";
14
+ import {
15
+ emptyScanResult,
16
+ type EnvelopeBudget,
17
+ type FederationEnvelope,
18
+ } from "../../src/federation/envelope.js";
19
+
20
+ function makeEnvelope(overrides: Partial<FederationEnvelope> = {}): FederationEnvelope {
21
+ return {
22
+ envelopeId: "env-1",
23
+ sourceNodeId: "node-A",
24
+ targetNodeId: "node-B",
25
+ sessionId: "sess-1",
26
+ messageType: "heartbeat",
27
+ payload: null,
28
+ timestamp: new Date().toISOString(),
29
+ nonce: "nonce-1",
30
+ hmacSignature: "sig-1",
31
+ piiScanResult: emptyScanResult(),
32
+ hopCount: 0,
33
+ ...overrides,
34
+ };
35
+ }
36
+
37
+ const defaultBudget: EnvelopeBudget = { maxTokens: 1000, maxUsd: 1.0, maxHops: 3 };
38
+
39
+ describe("federation/policy — enforce()", () => {
40
+ let engine: PolicyEngine;
41
+
42
+ beforeEach(() => {
43
+ engine = new PolicyEngine();
44
+ });
45
+
46
+ test("admits a fresh envelope with default budget", () => {
47
+ const e = makeEnvelope();
48
+ const r = engine.enforce(e, defaultBudget);
49
+ expect(r.allowed).toBe(true);
50
+ });
51
+
52
+ test("denies when hopCount >= maxHops", () => {
53
+ const e = makeEnvelope({ hopCount: 3 });
54
+ const r = engine.enforce(e, defaultBudget);
55
+ expect(r.allowed).toBe(false);
56
+ if (!r.allowed) expect(r.denialReason).toBe("max_hops_exceeded");
57
+ });
58
+
59
+ test("denies when hopCount exceeds the engine ceiling (default 3)", () => {
60
+ const e = makeEnvelope({ hopCount: 4, budget: { maxTokens: 10, maxUsd: 1, maxHops: 100 } });
61
+ const r = engine.enforce(e, { maxTokens: 10, maxUsd: 1, maxHops: 100 });
62
+ expect(r.allowed).toBe(false);
63
+ if (!r.allowed) expect(r.denialReason).toBe("max_hops_exceeded");
64
+ });
65
+
66
+ test("admits hopCount=2 with maxHops=3 budget", () => {
67
+ const e = makeEnvelope({ hopCount: 2 });
68
+ const r = engine.enforce(e, defaultBudget);
69
+ expect(r.allowed).toBe(true);
70
+ });
71
+
72
+ test("denies unknown message kind", () => {
73
+ const e = makeEnvelope({ messageType: "not-a-kind" as never });
74
+ const r = engine.enforce(e, defaultBudget);
75
+ expect(r.allowed).toBe(false);
76
+ if (!r.allowed) expect(r.denialReason).toBe("message_type_not_allowed");
77
+ });
78
+
79
+ test("denies message kind outside the configured allowlist", () => {
80
+ const strict = new PolicyEngine({ allowedMessageTypes: ["heartbeat"] });
81
+ const e = makeEnvelope({ messageType: "agent-spawn" });
82
+ const r = strict.enforce(e, defaultBudget);
83
+ expect(r.allowed).toBe(false);
84
+ if (!r.allowed) expect(r.denialReason).toBe("message_type_not_allowed");
85
+ });
86
+
87
+ test("denies peer in the blocklist", () => {
88
+ const strict = new PolicyEngine({ peerBlocklist: ["node-A"] });
89
+ const e = makeEnvelope();
90
+ const r = strict.enforce(e, defaultBudget);
91
+ expect(r.allowed).toBe(false);
92
+ if (!r.allowed) expect(r.denialReason).toBe("peer_blocked");
93
+ });
94
+
95
+ test("denies when budget is missing", () => {
96
+ const r = engine.enforce(makeEnvelope(), undefined);
97
+ expect(r.allowed).toBe(false);
98
+ if (!r.allowed) expect(r.denialReason).toBe("budget_missing");
99
+ });
100
+
101
+ test("denies when budget has invalid maxHops", () => {
102
+ const r = engine.enforce(makeEnvelope(), { maxTokens: 1, maxUsd: 1, maxHops: Number.NaN });
103
+ expect(r.allowed).toBe(false);
104
+ if (!r.allowed) expect(r.denialReason).toBe("budget_invalid");
105
+ });
106
+ });
107
+
108
+ describe("federation/policy — runtime mutators", () => {
109
+ test("allowMessageType / denyMessageType mutate the allowlist", () => {
110
+ const engine = new PolicyEngine({ allowedMessageTypes: ["heartbeat"] });
111
+ const e = makeEnvelope({ messageType: "agent-spawn" });
112
+ expect(engine.enforce(e, defaultBudget).allowed).toBe(false);
113
+ engine.allowMessageType("agent-spawn");
114
+ expect(engine.enforce(e, defaultBudget).allowed).toBe(true);
115
+ engine.denyMessageType("heartbeat");
116
+ expect(engine.enforce(makeEnvelope({ messageType: "heartbeat" }), defaultBudget).allowed).toBe(false);
117
+ });
118
+
119
+ test("setBlocked mutates the blocklist", () => {
120
+ const engine = new PolicyEngine();
121
+ const e = makeEnvelope();
122
+ expect(engine.enforce(e, defaultBudget).allowed).toBe(true);
123
+ engine.setBlocked("node-A", true);
124
+ expect(engine.enforce(e, defaultBudget).allowed).toBe(false);
125
+ engine.setBlocked("node-A", false);
126
+ expect(engine.enforce(e, defaultBudget).allowed).toBe(true);
127
+ });
128
+
129
+ test("DEFAULT_MAX_HOPS is 3 per spec", () => {
130
+ expect(DEFAULT_MAX_HOPS).toBe(3);
131
+ });
132
+
133
+ test("engine ceiling override is respected", () => {
134
+ const strict = new PolicyEngine({ maxHops: 1 });
135
+ const e = makeEnvelope({ hopCount: 1 });
136
+ const r = strict.enforce(e, { maxTokens: 10, maxUsd: 1, maxHops: 99 });
137
+ expect(r.allowed).toBe(false);
138
+ if (!r.allowed) expect(r.denialReason).toBe("max_hops_exceeded");
139
+ });
140
+ });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * federation/trust.test.ts — F-038 TrustEvaluator tests.
3
+ *
4
+ * Covers:
5
+ * - evaluate() allows allowlisted peer with fresh envelope.
6
+ * - evaluate() denies non-allowlisted peer.
7
+ * - evaluate() denies too-old envelope.
8
+ * - recordOutcome() updates the recent fail rate.
9
+ * - Low score (high fail rate) triggers denial.
10
+ * - setAllowlisted mutates the runtime allowlist.
11
+ */
12
+
13
+ import { describe, test, expect, beforeEach } from "vitest";
14
+ import {
15
+ MAX_TRUST_AGE_MS,
16
+ MIN_TRUST_SCORE,
17
+ TrustEvaluator,
18
+ type PeerState,
19
+ } from "../../src/federation/trust.js";
20
+ import {
21
+ emptyScanResult,
22
+ type FederationEnvelope,
23
+ } from "../../src/federation/envelope.js";
24
+
25
+ function makeEnvelope(overrides: Partial<FederationEnvelope> = {}): FederationEnvelope {
26
+ return {
27
+ envelopeId: "env-1",
28
+ sourceNodeId: "node-A",
29
+ targetNodeId: "node-B",
30
+ sessionId: "sess-1",
31
+ messageType: "heartbeat",
32
+ payload: null,
33
+ timestamp: new Date().toISOString(),
34
+ nonce: "nonce-1",
35
+ hmacSignature: "sig-1",
36
+ piiScanResult: emptyScanResult(),
37
+ ...overrides,
38
+ };
39
+ }
40
+
41
+ function allowlistedPeer(nodeId = "node-A"): PeerState {
42
+ return { nodeId, allowlisted: true, recentFailRate: 0, lastSeenMs: Date.now() };
43
+ }
44
+
45
+ describe("federation/trust — evaluate()", () => {
46
+ let te: TrustEvaluator;
47
+
48
+ beforeEach(() => {
49
+ te = new TrustEvaluator({ allowlist: ["node-A"] });
50
+ });
51
+
52
+ test("allows an allowlisted peer with a fresh envelope", () => {
53
+ const r = te.evaluate(makeEnvelope(), allowlistedPeer());
54
+ expect(r.allowed).toBe(true);
55
+ expect(r.score).toBeGreaterThanOrEqual(MIN_TRUST_SCORE);
56
+ });
57
+
58
+ test("denies a non-allowlisted peer", () => {
59
+ const peer: PeerState = { nodeId: "node-X", allowlisted: false, recentFailRate: 0 };
60
+ const r = te.evaluate(makeEnvelope(), peer);
61
+ expect(r.allowed).toBe(false);
62
+ if (!r.allowed) expect(r.reason).toMatch(/allowlist/);
63
+ });
64
+
65
+ test("denies an envelope older than MAX_TRUST_AGE_MS", () => {
66
+ const old = makeEnvelope({
67
+ timestamp: new Date(Date.now() - MAX_TRUST_AGE_MS - 1000).toISOString(),
68
+ });
69
+ const r = te.evaluate(old, allowlistedPeer());
70
+ expect(r.allowed).toBe(false);
71
+ if (!r.allowed) expect(r.reason).toMatch(/too_old/);
72
+ });
73
+
74
+ test("denies when score falls below MIN_TRUST_SCORE (high fail rate)", () => {
75
+ te.recordOutcome("node-A", false);
76
+ te.recordOutcome("node-A", false);
77
+ te.recordOutcome("node-A", false);
78
+ te.recordOutcome("node-A", false);
79
+ const peer = te.getPeerState("node-A");
80
+ const r = te.evaluate(makeEnvelope(), peer);
81
+ expect(r.allowed).toBe(false);
82
+ if (!r.allowed) expect(r.reason).toMatch(/score_below_floor/);
83
+ });
84
+
85
+ test("allows when only successes are recorded", () => {
86
+ for (let i = 0; i < 5; i++) te.recordOutcome("node-A", true);
87
+ const peer = te.getPeerState("node-A");
88
+ expect(peer.recentFailRate).toBe(0);
89
+ const r = te.evaluate(makeEnvelope(), peer);
90
+ expect(r.allowed).toBe(true);
91
+ });
92
+ });
93
+
94
+ describe("federation/trust — runtime mutations", () => {
95
+ test("setAllowlisted adds a new peer at runtime", () => {
96
+ const te = new TrustEvaluator({ allowlist: [] });
97
+ expect(te.isAllowlisted("node-A")).toBe(false);
98
+ te.setAllowlisted("node-A", true);
99
+ expect(te.isAllowlisted("node-A")).toBe(true);
100
+ });
101
+
102
+ test("setAllowlisted(false) removes a peer at runtime", () => {
103
+ const te = new TrustEvaluator({ allowlist: ["node-A"] });
104
+ expect(te.isAllowlisted("node-A")).toBe(true);
105
+ te.setAllowlisted("node-A", false);
106
+ expect(te.isAllowlisted("node-A")).toBe(false);
107
+ });
108
+
109
+ test("getPeerState returns default zeros for an unknown peer", () => {
110
+ const te = new TrustEvaluator();
111
+ const peer = te.getPeerState("never-seen");
112
+ expect(peer.allowlisted).toBe(false);
113
+ expect(peer.recentFailRate).toBe(0);
114
+ expect(peer.lastSeenMs).toBeUndefined();
115
+ });
116
+
117
+ test("computeScore is in [0, 1] for the corner cases", () => {
118
+ const te = new TrustEvaluator({ allowlist: ["node-A"] });
119
+ const env = makeEnvelope();
120
+ expect(te.computeScore(env, { nodeId: "node-A", allowlisted: true, recentFailRate: 0 })).toBeLessThanOrEqual(1);
121
+ expect(te.computeScore(env, { nodeId: "node-A", allowlisted: true, recentFailRate: 1 })).toBeGreaterThanOrEqual(0);
122
+ expect(te.computeScore(env, { nodeId: "node-A", allowlisted: false, recentFailRate: 0 })).toBe(0);
123
+ });
124
+ });
@@ -0,0 +1,204 @@
1
+ /**
2
+ * mcp-tools.test.ts — exercises the 4 new MCP tools (F-032) by calling
3
+ * each tool's `handler` directly and asserting the return shape.
4
+ *
5
+ * The tools live in `packages/sdk/src/mcp/server.ts`; we import the
6
+ * compiled `BIZAR_TOOLS` array and walk it to find each tool by name.
7
+ */
8
+
9
+ import { describe, test, expect, beforeEach } from "vitest";
10
+ import { BIZAR_TOOLS, type SdkMcpToolDef } from "../src/mcp/server.js";
11
+ import { bizarAgentRegistry } from "../src/agent-registry.js";
12
+ import { swarmTopologyRegistry } from "../src/swarm-topology.js";
13
+
14
+ function findTool(name: string): SdkMcpToolDef {
15
+ const t = BIZAR_TOOLS.find((x) => x.name === name);
16
+ if (!t) throw new Error(`tool not found: ${name}`);
17
+ return t;
18
+ }
19
+
20
+ async function call(toolName: string, args: unknown): Promise<unknown> {
21
+ const tool = findTool(toolName);
22
+ const res = await tool.handler(args, {});
23
+ // Standard MCP shape: { content: [{ type: 'text', text: '...' }] }
24
+ if (!Array.isArray(res.content) || res.content.length !== 1) {
25
+ throw new Error(`unexpected response shape from ${toolName}`);
26
+ }
27
+ const block = res.content[0];
28
+ if (block.type !== "text") throw new Error(`unexpected block type: ${block.type}`);
29
+ try { return JSON.parse(block.text); }
30
+ catch { return block.text; }
31
+ }
32
+
33
+ describe("MCP tools — F-032 swarm/agent lifecycle", () => {
34
+ beforeEach(() => {
35
+ // Each test starts from a clean slate so list assertions are deterministic.
36
+ bizarAgentRegistry.reset();
37
+ swarmTopologyRegistry.reset();
38
+ });
39
+
40
+ test("BIZAR_TOOLS includes the 4 new tools (total ≥ 17)", () => {
41
+ const names = new Set(BIZAR_TOOLS.map((t) => t.name));
42
+ expect(names.has("agent_spawn")).toBe(true);
43
+ expect(names.has("agent_list")).toBe(true);
44
+ expect(names.has("agent_terminate")).toBe(true);
45
+ expect(names.has("swarm_init")).toBe(true);
46
+ expect(BIZAR_TOOLS.length).toBeGreaterThanOrEqual(17);
47
+ });
48
+
49
+ test("each new tool has the expected inputSchema", () => {
50
+ const spawn = findTool("agent_spawn");
51
+ expect(spawn.inputSchema).toMatchObject({
52
+ type: "string", name: "string", priority: "string", metadata: "string",
53
+ });
54
+
55
+ const list = findTool("agent_list");
56
+ expect(list.inputSchema).toMatchObject({
57
+ status: "string", type: "string", limit: "number", offset: "number",
58
+ });
59
+
60
+ const term = findTool("agent_terminate");
61
+ expect(term.inputSchema).toMatchObject({
62
+ agentId: "string", graceful: "boolean", reason: "string",
63
+ });
64
+
65
+ const swarm = findTool("swarm_init");
66
+ expect(swarm.inputSchema).toMatchObject({
67
+ topology: "string", maxAgents: "number", metadata: "string",
68
+ });
69
+ });
70
+
71
+ test("agent_spawn returns { agentId, status }", async () => {
72
+ const r = await call("agent_spawn", { type: "coder", name: "ada" });
73
+ expect(r).toMatchObject({ status: "active" });
74
+ expect((r as { agentId: string }).agentId).toMatch(/^agent-/);
75
+ });
76
+
77
+ test("agent_spawn parses metadata JSON when supplied", async () => {
78
+ const r = await call("agent_spawn", {
79
+ type: "coder",
80
+ metadata: JSON.stringify({ region: "us", priority: "high" }),
81
+ });
82
+ const id = (r as { agentId: string }).agentId;
83
+ const got = bizarAgentRegistry.getAgent(id);
84
+ expect(got?.metadata).toEqual({ region: "us", priority: "high" });
85
+ });
86
+
87
+ test("agent_spawn returns a structured error when metadata is not valid JSON", async () => {
88
+ const tool = findTool("agent_spawn");
89
+ const res = await tool.handler({ type: "coder", metadata: "not-json" }, {});
90
+ const text = (res.content[0] as { text: string }).text;
91
+ expect(text).toMatch(/^error:/);
92
+ expect(text).toMatch(/JSON/);
93
+ });
94
+
95
+ test("agent_spawn rejects types outside the AGENT_TYPES allowlist", async () => {
96
+ const tool = findTool("agent_spawn");
97
+ const res = await tool.handler({ type: "lawyer" }, {});
98
+ const text = (res.content[0] as { text: string }).text;
99
+ expect(text).toMatch(/^error:/);
100
+ expect(text).toMatch(/must be one of/);
101
+ });
102
+
103
+ test("agent_list returns active agents by default", async () => {
104
+ await call("agent_spawn", { type: "coder" });
105
+ await call("agent_spawn", { type: "reviewer" });
106
+ const r = await call("agent_list", {});
107
+ expect(r).toMatchObject({ total: 2 });
108
+ expect((r as { agents: unknown[] }).agents).toHaveLength(2);
109
+ });
110
+
111
+ test("agent_list filters by status", async () => {
112
+ const a = await call("agent_spawn", { type: "coder" }) as { agentId: string };
113
+ await call("agent_spawn", { type: "coder" });
114
+ await call("agent_terminate", { agentId: a.agentId, reason: "test" });
115
+ const terminated = await call("agent_list", { status: "terminated" });
116
+ expect(terminated).toMatchObject({ total: 1 });
117
+ const active = await call("agent_list", { status: "active" });
118
+ expect(active).toMatchObject({ total: 1 });
119
+ });
120
+
121
+ test("agent_list defaults status to 'all' for unrecognized values", async () => {
122
+ await call("agent_spawn", { type: "coder" });
123
+ const r = await call("agent_list", { status: "bogus" });
124
+ expect(r).toMatchObject({ total: 1 });
125
+ });
126
+
127
+ test("agent_terminate returns { agentId, status, terminatedAt }", async () => {
128
+ const spawned = await call("agent_spawn", { type: "coder" }) as { agentId: string };
129
+ const r = await call("agent_terminate", { agentId: spawned.agentId, reason: "done" });
130
+ expect(r).toMatchObject({
131
+ agentId: spawned.agentId,
132
+ status: "terminated",
133
+ });
134
+ expect(typeof (r as { terminatedAt: string }).terminatedAt).toBe("string");
135
+ });
136
+
137
+ test("agent_terminate surfaces a structured error for unknown agent", async () => {
138
+ const tool = findTool("agent_terminate");
139
+ const res = await tool.handler({ agentId: "agent-nope" }, {});
140
+ const text = (res.content[0] as { text: string }).text;
141
+ expect(text).toMatch(/^error:/);
142
+ expect(text).toMatch(/not found/);
143
+ });
144
+
145
+ test("swarm_init creates a swarm with defaults", async () => {
146
+ const r = await call("swarm_init", {}) as {
147
+ swarmId: string; topology: string; maxAgents: number; createdAt: string;
148
+ };
149
+ expect(r.swarmId).toMatch(/^swarm-\d+$/);
150
+ expect(r.topology).toBe("hierarchical-mesh");
151
+ expect(r.maxAgents).toBe(15);
152
+ expect(typeof r.createdAt).toBe("string");
153
+ });
154
+
155
+ test("swarm_init respects topology and maxAgents", async () => {
156
+ const r = await call("swarm_init", {
157
+ topology: "mesh",
158
+ maxAgents: 42,
159
+ metadata: JSON.stringify({ owner: "F-032" }),
160
+ }) as { topology: string; maxAgents: number; metadata?: Record<string, unknown> };
161
+ expect(r.topology).toBe("mesh");
162
+ expect(r.maxAgents).toBe(42);
163
+ expect(r.metadata).toEqual({ owner: "F-032" });
164
+ });
165
+
166
+ test("swarm_init clamps maxAgents to [1, 1000]", async () => {
167
+ const lo = await call("swarm_init", { maxAgents: 0 }) as { maxAgents: number };
168
+ const hi = await call("swarm_init", { maxAgents: 99999 }) as { maxAgents: number };
169
+ expect(lo.maxAgents).toBe(1);
170
+ expect(hi.maxAgents).toBe(1000);
171
+ });
172
+
173
+ test("swarm_init returns an error for invalid JSON metadata", async () => {
174
+ const tool = findTool("swarm_init");
175
+ const res = await tool.handler({ metadata: "{not-json" }, {});
176
+ const text = (res.content[0] as { text: string }).text;
177
+ expect(text).toMatch(/^error:/);
178
+ expect(text).toMatch(/JSON/);
179
+ });
180
+
181
+ test("end-to-end: spawn → list → terminate → list shows 1 active + 1 terminated", async () => {
182
+ const spawned = await call("agent_spawn", { type: "coder", name: "ada" }) as {
183
+ agentId: string;
184
+ };
185
+
186
+ const allAfterSpawn = await call("agent_list", { status: "all" }) as {
187
+ total: number; agents: { agentId: string; status: string }[];
188
+ };
189
+ expect(allAfterSpawn.total).toBe(1);
190
+ expect(allAfterSpawn.agents[0]?.status).toBe("active");
191
+ expect(allAfterSpawn.agents[0]?.agentId).toBe(spawned.agentId);
192
+
193
+ await call("agent_terminate", { agentId: spawned.agentId, reason: "smoke" });
194
+
195
+ const allAfterTerm = await call("agent_list", { status: "all" }) as {
196
+ total: number; agents: { status: string }[];
197
+ };
198
+ expect(allAfterTerm.total).toBe(1);
199
+ expect(allAfterTerm.agents[0]?.status).toBe("terminated");
200
+
201
+ // Sanity: the registry returns the same record (proves state lives in BizarAgentRegistry).
202
+ expect(bizarAgentRegistry.getAgent(spawned.agentId)?.status).toBe("terminated");
203
+ });
204
+ });