@polderlabs/bizar 6.3.0 → 7.0.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 (245) 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/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js → EnvVarsSection-CW6fWKds.js} +3 -3
  5. package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js.map → EnvVarsSection-CW6fWKds.js.map} +1 -1
  6. package/bizar-dash/dist/assets/MobileChat-D7zRhU9K.js +1 -0
  7. package/bizar-dash/dist/assets/{MobileChat-BJrqwVDd.js.map → MobileChat-D7zRhU9K.js.map} +1 -1
  8. package/bizar-dash/dist/assets/MobileSettings-K-sNsn_r.js +1 -0
  9. package/bizar-dash/dist/assets/{MobileSettings-CEQNJNLJ.js.map → MobileSettings-K-sNsn_r.js.map} +1 -1
  10. package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js → Toast-WTxc3m2g.js} +1 -1
  11. package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js.map → Toast-WTxc3m2g.js.map} +1 -1
  12. package/bizar-dash/dist/assets/flow-BpU0-gK2.js +1 -0
  13. package/bizar-dash/dist/assets/flow-BpU0-gK2.js.map +1 -0
  14. package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js +1 -0
  15. package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js.map +1 -0
  16. package/bizar-dash/dist/assets/{icons-Do5N2jmX.js → icons-B0AKnYI3.js} +98 -103
  17. package/bizar-dash/dist/assets/icons-B0AKnYI3.js.map +1 -0
  18. package/bizar-dash/dist/assets/main-C-Md3DaY.css +1 -0
  19. package/bizar-dash/dist/assets/main-CO8fIxQK.js +18 -0
  20. package/bizar-dash/dist/assets/main-CO8fIxQK.js.map +1 -0
  21. package/bizar-dash/dist/assets/markdown-w2bDDkAH.js +1 -0
  22. package/bizar-dash/dist/assets/{markdown-tOLaD6nm.js.map → markdown-w2bDDkAH.js.map} +1 -1
  23. package/bizar-dash/dist/assets/mobile-DXYCA0hC.js +1 -0
  24. package/bizar-dash/dist/assets/{mobile-DYCHcUpq.js.map → mobile-DXYCA0hC.js.map} +1 -1
  25. package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js +2 -0
  26. package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js.map +1 -0
  27. package/bizar-dash/dist/assets/react-vendor-BnuhLJ6X.css +1 -0
  28. package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js +46 -0
  29. package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js.map +1 -0
  30. package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js +2 -0
  31. package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js.map +1 -0
  32. package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js +45 -0
  33. package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js.map +1 -0
  34. package/bizar-dash/dist/index.html +12 -10
  35. package/bizar-dash/dist/mobile.html +7 -4
  36. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  37. package/bizar-dash/src/server/api.mjs +11 -0
  38. package/bizar-dash/src/server/memory-consolidator.mjs +218 -0
  39. package/bizar-dash/src/server/memory-distillation.mjs +296 -0
  40. package/bizar-dash/src/server/routes/distill.mjs +129 -0
  41. package/bizar-dash/src/server/routes/goal-planner.mjs +236 -0
  42. package/bizar-dash/src/web/App.tsx +4 -0
  43. package/bizar-dash/src/web/components/Tag.tsx +18 -5
  44. package/bizar-dash/src/web/components/Topbar.tsx +3 -0
  45. package/bizar-dash/src/web/components/agents/CommunicationLog.tsx +126 -0
  46. package/bizar-dash/src/web/components/agents/DependencyGraph.tsx +163 -0
  47. package/bizar-dash/src/web/components/agents/QualityGates.tsx +123 -0
  48. package/bizar-dash/src/web/components/agents/RealTimeEventLog.tsx +127 -0
  49. package/bizar-dash/src/web/components/agents/RoutingDecisions.test.tsx +148 -0
  50. package/bizar-dash/src/web/components/agents/RoutingDecisions.tsx +160 -0
  51. package/bizar-dash/src/web/components/goals/GoalInput.tsx +86 -0
  52. package/bizar-dash/src/web/components/goals/PlanVisualization.tsx +111 -0
  53. package/bizar-dash/src/web/lib/goapPlanner.ts +614 -0
  54. package/bizar-dash/src/web/lib/types.ts +25 -0
  55. package/bizar-dash/src/web/styles/agents-redesign.css +311 -0
  56. package/bizar-dash/src/web/styles/main.css +251 -0
  57. package/bizar-dash/src/web/styles/tasks-redesign.css +231 -0
  58. package/bizar-dash/src/web/ui/controls/Button.tsx +77 -0
  59. package/bizar-dash/src/web/ui/controls/Checkbox.tsx +95 -0
  60. package/bizar-dash/src/web/ui/controls/IconButton.tsx +66 -0
  61. package/bizar-dash/src/web/ui/controls/Kbd.tsx +24 -0
  62. package/bizar-dash/src/web/ui/controls/NumberInput.tsx +77 -0
  63. package/bizar-dash/src/web/ui/controls/RadioGroup.tsx +109 -0
  64. package/bizar-dash/src/web/ui/controls/SearchInput.tsx +91 -0
  65. package/bizar-dash/src/web/ui/controls/Select.tsx +92 -0
  66. package/bizar-dash/src/web/ui/controls/Slider.tsx +70 -0
  67. package/bizar-dash/src/web/ui/controls/TextInput.tsx +67 -0
  68. package/bizar-dash/src/web/ui/controls/Toggle.tsx +79 -0
  69. package/bizar-dash/src/web/ui/controls/controls.css +454 -0
  70. package/bizar-dash/src/web/ui/controls/index.ts +33 -0
  71. package/bizar-dash/src/web/ui/data/BarChart.tsx +60 -0
  72. package/bizar-dash/src/web/ui/data/DataTable.tsx +216 -0
  73. package/bizar-dash/src/web/ui/data/EmptyState.tsx +60 -0
  74. package/bizar-dash/src/web/ui/data/ErrorState.tsx +56 -0
  75. package/bizar-dash/src/web/ui/data/KeyValueList.tsx +106 -0
  76. package/bizar-dash/src/web/ui/data/LoadingState.tsx +65 -0
  77. package/bizar-dash/src/web/ui/data/Sparkline.tsx +87 -0
  78. package/bizar-dash/src/web/ui/data/StatTile.tsx +87 -0
  79. package/bizar-dash/src/web/ui/data/data.css +535 -0
  80. package/bizar-dash/src/web/ui/data/index.ts +31 -0
  81. package/bizar-dash/src/web/ui/feedback/Badge.tsx +49 -0
  82. package/bizar-dash/src/web/ui/feedback/Dialog.tsx +209 -0
  83. package/bizar-dash/src/web/ui/feedback/ProgressBar.tsx +69 -0
  84. package/bizar-dash/src/web/ui/feedback/StatusDot.tsx +50 -0
  85. package/bizar-dash/src/web/ui/feedback/Toast.tsx +200 -0
  86. package/bizar-dash/src/web/ui/feedback/Tooltip.tsx +126 -0
  87. package/bizar-dash/src/web/ui/feedback/feedback.css +314 -0
  88. package/bizar-dash/src/web/ui/feedback/index.ts +43 -0
  89. package/bizar-dash/src/web/ui/index.ts +56 -0
  90. package/bizar-dash/src/web/ui/layout/AppShell.tsx +51 -0
  91. package/bizar-dash/src/web/ui/layout/Breadcrumbs.tsx +97 -0
  92. package/bizar-dash/src/web/ui/layout/Panel.tsx +73 -0
  93. package/bizar-dash/src/web/ui/layout/PanelHeader.tsx +39 -0
  94. package/bizar-dash/src/web/ui/layout/Sidebar.tsx +131 -0
  95. package/bizar-dash/src/web/ui/layout/Tabs.tsx +133 -0
  96. package/bizar-dash/src/web/ui/layout/Topbar.tsx +42 -0
  97. package/bizar-dash/src/web/ui/layout/index.ts +33 -0
  98. package/bizar-dash/src/web/ui/layout/layout.css +413 -0
  99. package/bizar-dash/src/web/ui/navigation/CommandPalette.tsx +199 -0
  100. package/bizar-dash/src/web/ui/navigation/NavGroup.tsx +95 -0
  101. package/bizar-dash/src/web/ui/navigation/NavLink.tsx +93 -0
  102. package/bizar-dash/src/web/ui/navigation/index.ts +17 -0
  103. package/bizar-dash/src/web/ui/navigation/navigation.css +343 -0
  104. package/bizar-dash/src/web/ui/primitives/Box.tsx +91 -0
  105. package/bizar-dash/src/web/ui/primitives/Grid.tsx +49 -0
  106. package/bizar-dash/src/web/ui/primitives/Inline.tsx +55 -0
  107. package/bizar-dash/src/web/ui/primitives/Separator.tsx +46 -0
  108. package/bizar-dash/src/web/ui/primitives/Stack.tsx +54 -0
  109. package/bizar-dash/src/web/ui/primitives/VisuallyHidden.tsx +26 -0
  110. package/bizar-dash/src/web/ui/primitives/index.ts +23 -0
  111. package/bizar-dash/src/web/ui/primitives/primitives.css +183 -0
  112. package/bizar-dash/src/web/ui/styles/globals.css +86 -0
  113. package/bizar-dash/src/web/ui/styles/reset.css +125 -0
  114. package/bizar-dash/src/web/ui/styles/tokens.css +195 -0
  115. package/bizar-dash/src/web/ui/theme/ThemeProvider.tsx +142 -0
  116. package/bizar-dash/src/web/ui/theme/tokens.ts +131 -0
  117. package/bizar-dash/src/web/ui/theme/useTheme.ts +22 -0
  118. package/bizar-dash/src/web/ui/utils/cx.ts +42 -0
  119. package/bizar-dash/src/web/views/Agents.tsx +506 -266
  120. package/bizar-dash/src/web/views/GoalPlanner.tsx +158 -0
  121. package/bizar-dash/src/web/views/Overview.tsx +996 -448
  122. package/bizar-dash/src/web/views/Tasks.tsx +277 -273
  123. package/bizar-dash/tests/communication-log.test.tsx +67 -0
  124. package/bizar-dash/tests/dep-graph.test.tsx +40 -0
  125. package/bizar-dash/tests/event-log.test.tsx +61 -0
  126. package/bizar-dash/tests/goal-input.test.tsx +72 -0
  127. package/bizar-dash/tests/goap-planner.test.ts +68 -0
  128. package/bizar-dash/tests/quality-gates.test.tsx +59 -0
  129. package/bizar-dash/tests/setup.ts +31 -1
  130. package/bizar-dash/tests/ui/controls/Button.test.tsx +50 -0
  131. package/bizar-dash/tests/ui/controls/Checkbox.test.tsx +44 -0
  132. package/bizar-dash/tests/ui/controls/IconButton.test.tsx +37 -0
  133. package/bizar-dash/tests/ui/controls/Kbd.test.tsx +39 -0
  134. package/bizar-dash/tests/ui/controls/NumberInput.test.tsx +56 -0
  135. package/bizar-dash/tests/ui/controls/RadioGroup.test.tsx +58 -0
  136. package/bizar-dash/tests/ui/controls/SearchInput.test.tsx +52 -0
  137. package/bizar-dash/tests/ui/controls/Select.test.tsx +66 -0
  138. package/bizar-dash/tests/ui/controls/Slider.test.tsx +41 -0
  139. package/bizar-dash/tests/ui/controls/TextInput.test.tsx +44 -0
  140. package/bizar-dash/tests/ui/controls/Toggle.test.tsx +39 -0
  141. package/bizar-dash/tests/ui/data/BarChart.test.tsx +55 -0
  142. package/bizar-dash/tests/ui/data/DataTable.test.tsx +112 -0
  143. package/bizar-dash/tests/ui/data/EmptyState.test.tsx +57 -0
  144. package/bizar-dash/tests/ui/data/ErrorState.test.tsx +47 -0
  145. package/bizar-dash/tests/ui/data/KeyValueList.test.tsx +59 -0
  146. package/bizar-dash/tests/ui/data/LoadingState.test.tsx +37 -0
  147. package/bizar-dash/tests/ui/data/Sparkline.test.tsx +56 -0
  148. package/bizar-dash/tests/ui/data/StatTile.test.tsx +69 -0
  149. package/bizar-dash/tests/ui/feedback/Badge.test.tsx +41 -0
  150. package/bizar-dash/tests/ui/feedback/Dialog.test.tsx +99 -0
  151. package/bizar-dash/tests/ui/feedback/ProgressBar.test.tsx +66 -0
  152. package/bizar-dash/tests/ui/feedback/StatusDot.test.tsx +41 -0
  153. package/bizar-dash/tests/ui/feedback/Toast.test.tsx +124 -0
  154. package/bizar-dash/tests/ui/feedback/Tooltip.test.tsx +102 -0
  155. package/bizar-dash/tests/ui/layout/AppShell.test.tsx +50 -0
  156. package/bizar-dash/tests/ui/layout/Breadcrumbs.test.tsx +85 -0
  157. package/bizar-dash/tests/ui/layout/Panel.test.tsx +52 -0
  158. package/bizar-dash/tests/ui/layout/PanelHeader.test.tsx +32 -0
  159. package/bizar-dash/tests/ui/layout/Sidebar.test.tsx +95 -0
  160. package/bizar-dash/tests/ui/layout/Tabs.test.tsx +87 -0
  161. package/bizar-dash/tests/ui/layout/Topbar.test.tsx +41 -0
  162. package/bizar-dash/tests/ui/navigation/CommandPalette.test.tsx +105 -0
  163. package/bizar-dash/tests/ui/navigation/NavGroup.test.tsx +55 -0
  164. package/bizar-dash/tests/ui/navigation/NavLink.test.tsx +61 -0
  165. package/bizar-dash/tests/ui/primitives/Box.test.tsx +49 -0
  166. package/bizar-dash/tests/ui/primitives/Grid.test.tsx +37 -0
  167. package/bizar-dash/tests/ui/primitives/Inline.test.tsx +33 -0
  168. package/bizar-dash/tests/ui/primitives/Separator.test.tsx +34 -0
  169. package/bizar-dash/tests/ui/primitives/Stack.test.tsx +45 -0
  170. package/bizar-dash/tests/ui/primitives/VisuallyHidden.test.tsx +27 -0
  171. package/bizar-dash/tests/views/Agents.test.tsx +359 -0
  172. package/bizar-dash/tests/views/Overview.test.tsx +413 -0
  173. package/bizar-dash/tests/views/Tasks.test.tsx +250 -0
  174. package/bizar-dash/vitest.config.ts +3 -1
  175. package/cli/__tests__/cost-gate.test.mjs +301 -0
  176. package/cli/__tests__/feature-list-bridge.test.mjs +371 -0
  177. package/cli/bin.mjs +38 -0
  178. package/cli/commands/claim.mjs +282 -0
  179. package/cli/commands/cost.mjs +279 -0
  180. package/cli/cost-gate.mjs +501 -0
  181. package/cli/feature-list-bridge.mjs +400 -0
  182. package/cli/memory-constants.mjs +17 -0
  183. package/cli/worker-dispatcher.mjs +259 -0
  184. package/cli/worker-dispatcher.test.mjs +204 -0
  185. package/config/trigger-patterns.json +186 -0
  186. package/package.json +2 -2
  187. package/packages/sdk/package.json +1 -1
  188. package/packages/sdk/src/agent-registry.ts +304 -0
  189. package/packages/sdk/src/consensus/byzantine.ts +587 -0
  190. package/packages/sdk/src/consensus/index.ts +147 -0
  191. package/packages/sdk/src/consensus/queen.ts +173 -0
  192. package/packages/sdk/src/consensus/types.ts +199 -0
  193. package/packages/sdk/src/federation/audit.ts +156 -0
  194. package/packages/sdk/src/federation/budget.ts +269 -0
  195. package/packages/sdk/src/federation/envelope.ts +144 -0
  196. package/packages/sdk/src/federation/hmac.ts +160 -0
  197. package/packages/sdk/src/federation/index.ts +443 -0
  198. package/packages/sdk/src/federation/pii.ts +241 -0
  199. package/packages/sdk/src/federation/policy.ts +112 -0
  200. package/packages/sdk/src/federation/trust.ts +146 -0
  201. package/packages/sdk/src/index.ts +67 -0
  202. package/packages/sdk/src/mcp/server.ts +351 -1
  203. package/packages/sdk/src/router/codemod-intent.ts +125 -0
  204. package/packages/sdk/src/router/index.ts +247 -0
  205. package/packages/sdk/src/router/memory-distillation-shim.mjs +18 -0
  206. package/packages/sdk/src/router/memory-distillation.ts +249 -0
  207. package/packages/sdk/src/router/model-router.ts +235 -0
  208. package/packages/sdk/src/router/q-learning-router.ts +304 -0
  209. package/packages/sdk/src/swarm-topology.ts +279 -0
  210. package/packages/sdk/tests/agent-registry.test.ts +285 -0
  211. package/packages/sdk/tests/consensus.test.ts +376 -0
  212. package/packages/sdk/tests/federation/audit.test.ts +164 -0
  213. package/packages/sdk/tests/federation/budget.test.ts +189 -0
  214. package/packages/sdk/tests/federation/envelope.test.ts +120 -0
  215. package/packages/sdk/tests/federation/hmac.test.ts +194 -0
  216. package/packages/sdk/tests/federation/orchestrator.test.ts +176 -0
  217. package/packages/sdk/tests/federation/pii.test.ts +106 -0
  218. package/packages/sdk/tests/federation/policy.test.ts +140 -0
  219. package/packages/sdk/tests/federation/trust.test.ts +124 -0
  220. package/packages/sdk/tests/mcp-tools.test.ts +204 -0
  221. package/packages/sdk/tests/memory-distillation.test.mjs +186 -0
  222. package/packages/sdk/tests/model-router.test.mjs +108 -0
  223. package/packages/sdk/tests/q-learning-router.test.mjs +100 -0
  224. package/packages/sdk/tests/router-orchestrator.test.mjs +172 -0
  225. package/packages/sdk/tests/router.test.mjs +91 -0
  226. package/packages/sdk/tests/sdk.test.mjs +26 -2
  227. package/packages/sdk/tests/swarm-topology.test.ts +258 -0
  228. package/scripts/bh-full-e2e.mjs +11 -15
  229. package/bizar-dash/dist/assets/MobileChat-BJrqwVDd.js +0 -1
  230. package/bizar-dash/dist/assets/MobileSettings-CEQNJNLJ.js +0 -1
  231. package/bizar-dash/dist/assets/icons-Do5N2jmX.js.map +0 -1
  232. package/bizar-dash/dist/assets/main-IvfQAOfy.js +0 -18
  233. package/bizar-dash/dist/assets/main-IvfQAOfy.js.map +0 -1
  234. package/bizar-dash/dist/assets/main-ietCEg_R.css +0 -1
  235. package/bizar-dash/dist/assets/markdown-tOLaD6nm.js +0 -1
  236. package/bizar-dash/dist/assets/mobile-DYCHcUpq.js +0 -1
  237. package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js +0 -2
  238. package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js.map +0 -1
  239. package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js +0 -40
  240. package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js.map +0 -1
  241. package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js +0 -2
  242. package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js.map +0 -1
  243. package/bizar-dash/dist/assets/vendor-C843201K.js +0 -29
  244. package/bizar-dash/dist/assets/vendor-C843201K.js.map +0 -1
  245. package/packages/sdk/package-lock.json +0 -32
@@ -0,0 +1,376 @@
1
+ /**
2
+ * consensus.test.ts — Unit tests for the F-039 consensus layer.
3
+ *
4
+ * Covers:
5
+ * • Happy path: 5 agents, 3-of-5 agree → commit
6
+ * • Split: 2-2-1 → no consensus, view-change triggers
7
+ * • Faulty proposer: proposer votes "no" → view-change to next peer
8
+ * • Replay protection: same proposalId twice → second is no-op
9
+ * • Empty quorum: 0 votes → expiry after timeoutMs
10
+ * • QueenCoordinator round-robin + fault-skip
11
+ * • Determinism with proposerSeed
12
+ * • Status snapshot shape
13
+ * • Singleton + resetSharedConsensus()
14
+ * • MCP-tool-equivalent propose → castVote cycle
15
+ */
16
+
17
+ import { describe, test, expect, beforeEach } from "vitest";
18
+
19
+ import {
20
+ ByzantineConsensus,
21
+ QueenCoordinator,
22
+ createConsensus,
23
+ getSharedConsensus,
24
+ resetSharedConsensus,
25
+ DEFAULT_QUORUM,
26
+ DEFAULT_PEER_COUNT,
27
+ DEFAULT_MAX_FAULTS,
28
+ PHASE_ORDER,
29
+ type ConsensusHandle,
30
+ } from "../src/consensus/index.js";
31
+ import {
32
+ type ConsensusStatus,
33
+ type ProposeResult,
34
+ type VoteResult,
35
+ } from "../src/consensus/types.js";
36
+
37
+ // ─── Helpers ───────────────────────────────────────────────────────────
38
+
39
+ const FIVE_PEERS = ["odin", "frigg", "vor", "mimir", "heimdall"] as const;
40
+
41
+ function newFivePeerConsensus(opts: Partial<ConstructorParameters<typeof ByzantineConsensus>[0]> = {}): ByzantineConsensus {
42
+ return new ByzantineConsensus({
43
+ localAgentId: "odin",
44
+ peers: [...FIVE_PEERS],
45
+ quorum: 3,
46
+ maxFaults: 1,
47
+ ...opts,
48
+ });
49
+ }
50
+
51
+ // ─── Tests ─────────────────────────────────────────────────────────────
52
+
53
+ describe("ByzantineConsensus — happy path (3-of-5)", () => {
54
+ let c: ByzantineConsensus;
55
+ beforeEach(() => { c = newFivePeerConsensus(); });
56
+
57
+ test("5 agents, 3-of-5 agree → commit", () => {
58
+ const { proposalId, phase } = c.propose({ action: "ship", version: 1 });
59
+ expect(phase).toBe("prepare");
60
+ expect(proposalId).toMatch(/^bft-v\d+-[\da-f]+$/);
61
+
62
+ const r1 = c.castVote(proposalId, "odin", "yes");
63
+ expect(r1.approvals).toBe(1);
64
+ expect(r1.committed).toBe(false);
65
+
66
+ const r2 = c.castVote(proposalId, "frigg", "yes");
67
+ expect(r2.approvals).toBe(2);
68
+ expect(r2.committed).toBe(false);
69
+
70
+ const r3 = c.castVote(proposalId, "vor", "yes");
71
+ expect(r3.approvals).toBe(3);
72
+ expect(r3.committed).toBe(true);
73
+ expect(r3.phase).toBe("reply");
74
+ expect(r3.status).toBe("committed");
75
+ });
76
+
77
+ test("exactly 3 yes + 2 abstain → commit (3-of-5 simple majority)", () => {
78
+ const { proposalId } = c.propose({ a: 1 });
79
+ c.castVote(proposalId, "odin", "yes");
80
+ c.castVote(proposalId, "frigg", "yes");
81
+ c.castVote(proposalId, "vor", "yes");
82
+ const r = c.castVote(proposalId, "mimir", "abstain");
83
+ expect(r.committed).toBe(true);
84
+ expect(r.approvals).toBe(3);
85
+ });
86
+
87
+ test("2 yes + 3 no (all 5 votes) → rejected (cannot reach quorum)", () => {
88
+ const { proposalId } = c.propose({ a: 1 });
89
+ c.castVote(proposalId, "odin", "yes");
90
+ c.castVote(proposalId, "frigg", "yes");
91
+ c.castVote(proposalId, "vor", "no");
92
+ c.castVote(proposalId, "mimir", "no");
93
+ const r = c.castVote(proposalId, "heimdall", "no");
94
+ expect(r.committed).toBe(false);
95
+ expect(r.status).toBe("rejected");
96
+ expect(r.phase).toBe("reply");
97
+ expect(r.approvals).toBe(2);
98
+ expect(r.rejections).toBe(3);
99
+ });
100
+
101
+ test("late vote after commit is a no-op (returns current snapshot)", () => {
102
+ const { proposalId } = c.propose({ x: 1 });
103
+ c.castVote(proposalId, "odin", "yes");
104
+ c.castVote(proposalId, "frigg", "yes");
105
+ const committed = c.castVote(proposalId, "vor", "yes");
106
+ expect(committed.committed).toBe(true);
107
+ const late = c.castVote(proposalId, "mimir", "yes");
108
+ expect(late.committed).toBe(true);
109
+ expect(late.phase).toBe("reply");
110
+ });
111
+ });
112
+
113
+ describe("ByzantineConsensus — view-change / faulty proposer", () => {
114
+ test("proposer voting 'no' triggers view-change (self-fault)", () => {
115
+ const c = newFivePeerConsensus();
116
+ const { proposalId, phase } = c.propose({ payload: "bad" });
117
+ expect(phase).toBe("prepare");
118
+ const r = c.castVote(proposalId, c.getCurrentProposer(), "no");
119
+ expect(r.status).toBe("view-change");
120
+ expect(r.phase).toBe("reply");
121
+ expect(r.committed).toBe(false);
122
+ expect(c.getViewNumber()).toBeGreaterThanOrEqual(1);
123
+ });
124
+
125
+ test("explicit viewChange() advances proposer + view number", () => {
126
+ const c = newFivePeerConsensus();
127
+ const before = c.getCurrentProposer();
128
+ const result = c.viewChange("test-fault", "irrelevant");
129
+ expect(result.previousProposer).toBe(before);
130
+ expect(result.newProposer).not.toBe(before);
131
+ expect(result.viewNumber).toBe(1);
132
+ expect(c.getViewNumber()).toBe(1);
133
+ expect(c.getCurrentProposer()).toBe(result.newProposer);
134
+ });
135
+
136
+ test("viewChange() on a pending proposal allows the new view to re-propose", () => {
137
+ const c = newFivePeerConsensus();
138
+ const { proposalId } = c.propose({ same: true });
139
+ c.viewChange("first-proposer-faulty", proposalId);
140
+ // Same payload under the new view must produce a new proposalId.
141
+ const second = c.propose({ same: true });
142
+ expect(second.proposalId).not.toBe(proposalId);
143
+ });
144
+
145
+ test("tie-break (2-2-1) does not commit (only 2 yes)", () => {
146
+ const c = newFivePeerConsensus();
147
+ const { proposalId } = c.propose({ split: true });
148
+ c.castVote(proposalId, "odin", "yes");
149
+ c.castVote(proposalId, "frigg", "yes");
150
+ c.castVote(proposalId, "vor", "no");
151
+ const r = c.castVote(proposalId, "mimir", "no");
152
+ expect(r.committed).toBe(false);
153
+ expect(r.approvals).toBe(2);
154
+ expect(r.rejections).toBe(2);
155
+ });
156
+ });
157
+
158
+ describe("ByzantineConsensus — replay protection", () => {
159
+ test("same payload in same view returns cached proposalId", () => {
160
+ const c = newFivePeerConsensus();
161
+ const first = c.propose({ action: "noop", v: 1 });
162
+ const second = c.propose({ action: "noop", v: 1 });
163
+ expect(second.proposalId).toBe(first.proposalId);
164
+ });
165
+
166
+ test("different payloads in same view get different proposalIds", () => {
167
+ const c = newFivePeerConsensus();
168
+ const a = c.propose({ x: 1 });
169
+ const b = c.propose({ x: 2 });
170
+ expect(a.proposalId).not.toBe(b.proposalId);
171
+ });
172
+
173
+ test("payload digest keys on viewNumber — view-change produces fresh id", () => {
174
+ const c = newFivePeerConsensus();
175
+ const first = c.propose({ same: "payload" });
176
+ c.viewChange("manual", first.proposalId);
177
+ const second = c.propose({ same: "payload" });
178
+ expect(second.proposalId).not.toBe(first.proposalId);
179
+ });
180
+ });
181
+
182
+ describe("ByzantineConsensus — empty quorum / expiry", () => {
183
+ test("0 votes → expires after timeoutMs (status flips to expired)", async () => {
184
+ let nowMs = 1_000_000_000_000;
185
+ const now = () => new Date(nowMs);
186
+ const c = newFivePeerConsensus({ timeoutMs: 50, now });
187
+ const { proposalId } = c.propose({ stale: true });
188
+ expect(c.getProposal(proposalId)?.status).toBe("prepare");
189
+ nowMs += 100;
190
+ expect(c.getProposal(proposalId)?.status).toBe("expired");
191
+ // Subsequent castVote on an expired proposal is a no-op snapshot.
192
+ const r = c.castVote(proposalId, "odin", "yes");
193
+ expect(r.committed).toBe(false);
194
+ });
195
+ });
196
+
197
+ describe("ByzantineConsensus — validation", () => {
198
+ test("missing localAgentId throws", () => {
199
+ expect(() =>
200
+ new ByzantineConsensus({ localAgentId: "", peers: [...FIVE_PEERS] }),
201
+ ).toThrow(/localAgentId/);
202
+ });
203
+
204
+ test("localAgentId not in peer list throws", () => {
205
+ expect(() =>
206
+ new ByzantineConsensus({ localAgentId: "loki", peers: [...FIVE_PEERS] }),
207
+ ).toThrow(/must be in the peer list/);
208
+ });
209
+
210
+ test("peer count < 2 throws", () => {
211
+ expect(() =>
212
+ new ByzantineConsensus({ localAgentId: "odin", peers: ["odin"] }),
213
+ ).toThrow(/>=2/);
214
+ });
215
+
216
+ test("castVote on unknown proposalId throws", () => {
217
+ const c = newFivePeerConsensus();
218
+ expect(() => c.castVote("missing", "odin", "yes")).toThrow(/unknown proposalId/);
219
+ });
220
+
221
+ test("castVote from non-peer throws", () => {
222
+ const c = newFivePeerConsensus();
223
+ const { proposalId } = c.propose({ x: 1 });
224
+ expect(() => c.castVote(proposalId, "loki", "yes")).toThrow(/not in the peer list/);
225
+ });
226
+ });
227
+
228
+ describe("QueenCoordinator — round-robin + fault-skip", () => {
229
+ test("currentProposer advances through peer list on advance()", () => {
230
+ const q = new QueenCoordinator({ peers: ["a", "b", "c", "d", "e"] });
231
+ expect(q.currentProposer()).toBe("a");
232
+ q.advance();
233
+ expect(q.currentProposer()).toBe("b");
234
+ q.advance(); q.advance(); q.advance(); q.advance();
235
+ expect(q.currentProposer()).toBe("a");
236
+ });
237
+
238
+ test("recordFault adds skip; the peer is skipped until penalty clears", () => {
239
+ const q = new QueenCoordinator({ peers: ["a", "b", "c", "d", "e"] });
240
+ q.recordFault("a");
241
+ q.recordFault("a");
242
+ // advance() walks one step per call, decrementing any peer's skip
243
+ // counter it lands on. With skip[a]=2, two advances must "consume"
244
+ // a before a becomes proposer again.
245
+ // 1st: cursor 0→1, "b" skip=0 → "b" (a is still current; not yet proposed)
246
+ expect(q.advance()).toBe("b");
247
+ // 2nd: cursor 1→2, "c" → "c"
248
+ expect(q.advance()).toBe("c");
249
+ // 3rd: cursor 2→3, "d" → "d"
250
+ expect(q.advance()).toBe("d");
251
+ // 4th: cursor 3→4, "e" → "e"
252
+ expect(q.advance()).toBe("e");
253
+ // 5th: cursor 4→0, "a" skip=2 → decrement to 1, continue. cursor 0→1, "b" skip=0 → "b"
254
+ expect(q.advance()).toBe("b");
255
+ // 6th: cursor 1→2, "c" → "c"
256
+ expect(q.advance()).toBe("c");
257
+ // 7th: cursor 2→3, "d" → "d"
258
+ expect(q.advance()).toBe("d");
259
+ // 8th: cursor 3→4, "e" → "e"
260
+ expect(q.advance()).toBe("e");
261
+ // 9th: cursor 4→0, "a" skip=1 → decrement to 0, continue. cursor 0→1, "b" → "b"
262
+ expect(q.advance()).toBe("b");
263
+ // 10th: cursor 1→2, "c" → "c"
264
+ expect(q.advance()).toBe("c");
265
+ // Eventually "a" becomes eligible again.
266
+ // (proves the skip penalty is bounded, not permanent.)
267
+ let sawA = false;
268
+ for (let i = 0; i < 10; i++) {
269
+ if (q.advance() === "a") { sawA = true; break; }
270
+ }
271
+ expect(sawA).toBe(true);
272
+ });
273
+
274
+ test("seed produces deterministic initial cursor", () => {
275
+ const q1 = new QueenCoordinator({ peers: ["a", "b", "c", "d", "e"], seed: "fixed-seed" });
276
+ const q2 = new QueenCoordinator({ peers: ["a", "b", "c", "d", "e"], seed: "fixed-seed" });
277
+ expect(q1.currentProposer()).toBe(q2.currentProposer());
278
+ });
279
+
280
+ test("different seeds produce (likely) different initial cursors", () => {
281
+ const q1 = new QueenCoordinator({ peers: ["a", "b", "c", "d", "e", "f", "g"], seed: "alpha" });
282
+ const q2 = new QueenCoordinator({ peers: ["a", "b", "c", "d", "e", "f", "g"], seed: "omega" });
283
+ expect(q1.currentProposer()).not.toBe(q2.currentProposer());
284
+ });
285
+
286
+ test("reset clears faults and cursor", () => {
287
+ const q = new QueenCoordinator({ peers: ["a", "b", "c"] });
288
+ q.recordFault("a");
289
+ q.advance();
290
+ q.reset();
291
+ expect(q.currentProposer()).toBe("a");
292
+ expect(q.faultCounts()).toEqual({ a: 0, b: 0, c: 0 });
293
+ });
294
+
295
+ test("duplicate peer ids are rejected", () => {
296
+ expect(() => new QueenCoordinator({ peers: ["a", "b", "a"] })).toThrow(/duplicate/);
297
+ });
298
+ });
299
+
300
+ describe("createConsensus orchestrator", () => {
301
+ test("returns a handle with the expected surface", () => {
302
+ const c: ConsensusHandle = createConsensus({
303
+ localAgentId: "odin",
304
+ peers: [...FIVE_PEERS],
305
+ });
306
+ expect(c.localAgentId).toBe("odin");
307
+ expect(c.getQuorum()).toBe(3);
308
+ expect(c.getPeers()).toEqual([...FIVE_PEERS]);
309
+ expect(c.getCurrentProposer()).toBe("odin"); // head of seeded round-robin
310
+ });
311
+
312
+ test("full lifecycle: propose → castVote × 3 → committed + status reflects", () => {
313
+ const c = createConsensus({ localAgentId: "odin", peers: [...FIVE_PEERS] });
314
+ const { proposalId }: ProposeResult = c.propose({ task: "approve-pr" });
315
+ c.castVote(proposalId, "odin", "yes");
316
+ c.castVote(proposalId, "frigg", "yes");
317
+ const final = c.castVote(proposalId, "vor", "yes");
318
+ expect(final.committed).toBe(true);
319
+ const s: ConsensusStatus = c.status();
320
+ expect(s.committed).toBe(1);
321
+ expect(s.rejected).toBe(0);
322
+ expect(s.proposals.some((p) => p.id === proposalId && p.status === "committed")).toBe(true);
323
+ });
324
+
325
+ test("viewChange() advances view number and proposer", () => {
326
+ const c = createConsensus({ localAgentId: "odin", peers: [...FIVE_PEERS] });
327
+ const before = c.getCurrentProposer();
328
+ const r = c.viewChange("test-fault");
329
+ expect(r.previousProposer).toBe(before);
330
+ expect(r.viewNumber).toBe(1);
331
+ expect(c.getViewNumber()).toBe(1);
332
+ });
333
+ });
334
+
335
+ describe("getSharedConsensus — singleton", () => {
336
+ beforeEach(() => { resetSharedConsensus(); });
337
+
338
+ test("returns the same instance on repeated calls", () => {
339
+ const a = getSharedConsensus();
340
+ const b = getSharedConsensus();
341
+ expect(a).toBe(b);
342
+ });
343
+
344
+ test("default roster is the 5-agent Norse set", () => {
345
+ const c = getSharedConsensus();
346
+ expect(c.getPeers()).toEqual(["odin", "frigg", "vor", "mimir", "heimdall"]);
347
+ expect(c.getQuorum()).toBe(DEFAULT_QUORUM);
348
+ expect(c.getPeers().length).toBe(DEFAULT_PEER_COUNT);
349
+ expect(c.localAgentId).toBe("odin");
350
+ });
351
+
352
+ test("resetSharedConsensus drops the singleton", () => {
353
+ const a = getSharedConsensus();
354
+ resetSharedConsensus();
355
+ const b = getSharedConsensus();
356
+ expect(a).not.toBe(b);
357
+ });
358
+ });
359
+
360
+ describe("PHASE_ORDER constant", () => {
361
+ test("walks pre-prepare → prepare → commit → reply in order", () => {
362
+ expect(PHASE_ORDER).toEqual(["pre-prepare", "prepare", "commit", "reply"]);
363
+ });
364
+ });
365
+
366
+ describe("DEFAULT_* constants", () => {
367
+ test("default quorum is 3 (3-of-5 simple majority)", () => {
368
+ expect(DEFAULT_QUORUM).toBe(3);
369
+ });
370
+ test("default peer count is 5", () => {
371
+ expect(DEFAULT_PEER_COUNT).toBe(5);
372
+ });
373
+ test("default max faults is 1", () => {
374
+ expect(DEFAULT_MAX_FAULTS).toBe(1);
375
+ });
376
+ });
@@ -0,0 +1,164 @@
1
+ /**
2
+ * federation/audit.test.ts — F-038 AuditService tests.
3
+ *
4
+ * Covers:
5
+ * - record() appends NDJSON lines to the file.
6
+ * - size() reports on-disk bytes.
7
+ * - tail() returns parsed entries from the tail.
8
+ * - rotate() moves the file to .log.1.
9
+ * - reset() wipes the log file.
10
+ * - Constructor throws on missing nodeId.
11
+ */
12
+
13
+ import { describe, test, expect, beforeEach, afterEach } from "vitest";
14
+ import { mkdtempSync, rmSync, existsSync, statSync, readFileSync, mkdirSync, writeFileSync } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
17
+ import { AuditService, DEFAULT_AUDIT_PATH } from "../../src/federation/audit.js";
18
+
19
+ describe("federation/audit — AuditService", () => {
20
+ let dir: string;
21
+ let path: string;
22
+ let audit: AuditService;
23
+
24
+ beforeEach(() => {
25
+ dir = mkdtempSync(join(tmpdir(), "bizar-audit-"));
26
+ path = join(dir, "federation-audit.log");
27
+ audit = new AuditService({ nodeId: "test-node", path });
28
+ });
29
+
30
+ afterEach(() => {
31
+ rmSync(dir, { recursive: true, force: true });
32
+ });
33
+
34
+ test("record() appends an NDJSON line", () => {
35
+ audit.record({
36
+ ts: new Date().toISOString(),
37
+ envelopeId: "env-1",
38
+ sourceNodeId: "node-A",
39
+ targetNodeId: "node-B",
40
+ messageType: "heartbeat",
41
+ nonce: "n1",
42
+ allowed: true,
43
+ reason: "ok",
44
+ layer: "sent",
45
+ });
46
+ expect(existsSync(path)).toBe(true);
47
+ const lines = readFileSync(path, "utf-8").split("\n").filter((l) => l.length > 0);
48
+ expect(lines.length).toBe(1);
49
+ const parsed = JSON.parse(lines[0]);
50
+ expect(parsed.envelopeId).toBe("env-1");
51
+ expect(parsed.nodeId).toBe("test-node");
52
+ });
53
+
54
+ test("size() reports on-disk bytes", () => {
55
+ expect(audit.size()).toBe(0);
56
+ audit.record({
57
+ ts: new Date().toISOString(),
58
+ envelopeId: "env-1",
59
+ sourceNodeId: "node-A",
60
+ targetNodeId: "node-B",
61
+ messageType: "heartbeat",
62
+ nonce: "n1",
63
+ allowed: true,
64
+ reason: "ok",
65
+ layer: "sent",
66
+ });
67
+ expect(audit.size()).toBeGreaterThan(0);
68
+ });
69
+
70
+ test("tail() returns parsed entries from the end of the file", () => {
71
+ for (let i = 0; i < 5; i++) {
72
+ audit.record({
73
+ ts: new Date().toISOString(),
74
+ envelopeId: `env-${i}`,
75
+ sourceNodeId: "node-A",
76
+ targetNodeId: "node-B",
77
+ messageType: "heartbeat",
78
+ nonce: `n${i}`,
79
+ allowed: true,
80
+ reason: `entry-${i}`,
81
+ layer: "sent",
82
+ });
83
+ }
84
+ const tail = audit.tail(3);
85
+ expect(tail.length).toBe(3);
86
+ expect(tail[2].envelopeId).toBe("env-4");
87
+ expect(tail[2].reason).toBe("entry-4");
88
+ });
89
+
90
+ test("tail() returns [] when file missing", () => {
91
+ const a = new AuditService({ nodeId: "x", path: join(dir, "never-created.log") });
92
+ expect(a.tail()).toEqual([]);
93
+ });
94
+
95
+ test("rotate() moves the current log to .log.1", () => {
96
+ audit.record({
97
+ ts: new Date().toISOString(),
98
+ envelopeId: "env-1",
99
+ sourceNodeId: "node-A",
100
+ targetNodeId: "node-B",
101
+ messageType: "heartbeat",
102
+ nonce: "n1",
103
+ allowed: true,
104
+ reason: "ok",
105
+ layer: "sent",
106
+ });
107
+ audit.rotate();
108
+ expect(existsSync(path)).toBe(false);
109
+ expect(existsSync(`${path}.1`)).toBe(true);
110
+ });
111
+
112
+ test("getPath() returns the configured path", () => {
113
+ expect(audit.getPath()).toBe(path);
114
+ });
115
+
116
+ test("constructor requires nodeId", () => {
117
+ expect(() => new AuditService({ nodeId: "" as unknown as string })).toThrow(/nodeId/);
118
+ });
119
+
120
+ test("reset() removes the file (rename to .deleted)", () => {
121
+ audit.record({
122
+ ts: new Date().toISOString(),
123
+ envelopeId: "env-1",
124
+ sourceNodeId: "node-A",
125
+ targetNodeId: "node-B",
126
+ messageType: "heartbeat",
127
+ nonce: "n1",
128
+ allowed: true,
129
+ reason: "ok",
130
+ layer: "sent",
131
+ });
132
+ audit.reset();
133
+ // After reset, the file was renamed to .deleted (or removed on race).
134
+ expect(existsSync(path)).toBe(false);
135
+ });
136
+
137
+ test("DEFAULT_AUDIT_PATH is the harness-local path", () => {
138
+ expect(DEFAULT_AUDIT_PATH).toBe(".harness/federation-audit.log");
139
+ });
140
+
141
+ test("rotation triggered when appending would exceed threshold", () => {
142
+ // Pre-fill the file to just below the threshold.
143
+ const huge = "x".repeat(10 * 1024 * 1024 - 200);
144
+ mkdirSync(dir, { recursive: true });
145
+ writeFileSync(path, huge, "utf-8");
146
+ expect(statSync(path).size).toBeGreaterThan(10 * 1024 * 1024 - 1024);
147
+ audit.record({
148
+ ts: new Date().toISOString(),
149
+ envelopeId: "env-1",
150
+ sourceNodeId: "node-A",
151
+ targetNodeId: "node-B",
152
+ messageType: "heartbeat",
153
+ nonce: "n1",
154
+ allowed: true,
155
+ reason: "ok",
156
+ layer: "sent",
157
+ });
158
+ // After the write, the previous file should have been rotated to .log.1
159
+ // and the new entry written to the fresh .log.
160
+ expect(existsSync(`${path}.1`)).toBe(true);
161
+ const newSize = statSync(path).size;
162
+ expect(newSize).toBeLessThan(1024);
163
+ });
164
+ });