@polderlabs/bizar 4.9.0 → 5.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 (183) hide show
  1. package/bizar-dash/dist/assets/icons-Bo0iH9EC.js +701 -0
  2. package/bizar-dash/dist/assets/icons-Bo0iH9EC.js.map +1 -0
  3. package/bizar-dash/dist/assets/{index-DU61awG3.js → index-DmpSFPJY.js} +1 -1
  4. package/bizar-dash/dist/assets/{index-DU61awG3.js.map → index-DmpSFPJY.js.map} +1 -1
  5. package/bizar-dash/dist/assets/main-C1cpttnv.js +19 -0
  6. package/bizar-dash/dist/assets/main-C1cpttnv.js.map +1 -0
  7. package/bizar-dash/dist/assets/main-DTkNlLrw.css +1 -0
  8. package/bizar-dash/dist/assets/markdown-DIquRulQ.js +29 -0
  9. package/bizar-dash/dist/assets/markdown-DIquRulQ.js.map +1 -0
  10. package/bizar-dash/dist/assets/mobile-BtxQJftK.js +2 -0
  11. package/bizar-dash/dist/assets/mobile-BtxQJftK.js.map +1 -0
  12. package/bizar-dash/dist/assets/mobile-DD-FZrTC.js +1 -0
  13. package/bizar-dash/dist/assets/{mobile-CL5uUQEC.js.map → mobile-DD-FZrTC.js.map} +1 -1
  14. package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js +40 -0
  15. package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js.map +1 -0
  16. package/bizar-dash/dist/index.html +6 -3
  17. package/bizar-dash/dist/mobile.html +5 -2
  18. package/bizar-dash/node_modules/.package-lock.json +6 -0
  19. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  20. package/bizar-dash/package-lock.json +6 -0
  21. package/bizar-dash/skills/eval/SKILL.md +237 -0
  22. package/bizar-dash/src/server/api.mjs +35 -0
  23. package/bizar-dash/src/server/auth.mjs +155 -1
  24. package/bizar-dash/src/server/diagnostics-store.mjs +452 -2
  25. package/bizar-dash/src/server/eval-store.mjs +226 -0
  26. package/bizar-dash/src/server/eval.mjs +347 -0
  27. package/bizar-dash/src/server/memory-store.mjs +46 -0
  28. package/bizar-dash/src/server/ocr.mjs +55 -0
  29. package/bizar-dash/src/server/plugins/registry.mjs +363 -0
  30. package/bizar-dash/src/server/plugins/sandbox.mjs +655 -0
  31. package/bizar-dash/src/server/plugins/store.mjs +659 -0
  32. package/bizar-dash/src/server/providers-store.mjs +11 -4
  33. package/bizar-dash/src/server/routes/_shared.mjs +2 -2
  34. package/bizar-dash/src/server/routes/clipboard.mjs +173 -0
  35. package/bizar-dash/src/server/routes/doctor.mjs +71 -0
  36. package/bizar-dash/src/server/routes/env-vars.mjs +67 -1
  37. package/bizar-dash/src/server/routes/eval.mjs +147 -0
  38. package/bizar-dash/src/server/routes/memory.mjs +3 -0
  39. package/bizar-dash/src/server/routes/ocr.mjs +182 -0
  40. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +61 -42
  41. package/bizar-dash/src/server/routes/plugins.mjs +220 -0
  42. package/bizar-dash/src/server/routes/schedules.mjs +55 -0
  43. package/bizar-dash/src/server/routes/users.mjs +84 -0
  44. package/bizar-dash/src/server/routes/voice.mjs +131 -0
  45. package/bizar-dash/src/server/routes/workspaces.mjs +204 -0
  46. package/bizar-dash/src/server/serve-info.mjs +172 -0
  47. package/bizar-dash/src/server/voice-store.mjs +202 -0
  48. package/bizar-dash/src/server/voice-transcribe.mjs +72 -0
  49. package/bizar-dash/src/server/workspaces.mjs +626 -0
  50. package/bizar-dash/src/web/App.tsx +48 -11
  51. package/bizar-dash/src/web/components/AutosaveField.tsx +50 -0
  52. package/bizar-dash/src/web/components/DoctorPanel.tsx +160 -0
  53. package/bizar-dash/src/web/components/EnvVarManager.tsx +245 -62
  54. package/bizar-dash/src/web/components/InviteDialog.tsx +205 -0
  55. package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +71 -0
  56. package/bizar-dash/src/web/components/ScreenshotCapture.tsx +138 -0
  57. package/bizar-dash/src/web/components/ScreenshotOCR.tsx +42 -0
  58. package/bizar-dash/src/web/components/SettingsNav.tsx +101 -0
  59. package/bizar-dash/src/web/components/Sidebar.tsx +25 -1
  60. package/bizar-dash/src/web/components/StatusBadge.tsx +33 -2
  61. package/bizar-dash/src/web/components/Topbar.tsx +11 -0
  62. package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +182 -0
  63. package/bizar-dash/src/web/components/VoiceRecorder.tsx +104 -0
  64. package/bizar-dash/src/web/components/WorkspaceSelector.tsx +158 -0
  65. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +64 -0
  66. package/bizar-dash/src/web/components/chat/Composer.tsx +1 -1
  67. package/bizar-dash/src/web/components/chat/useChat.ts +118 -1
  68. package/bizar-dash/src/web/hooks/useAutosave.ts +107 -0
  69. package/bizar-dash/src/web/lib/types.ts +134 -0
  70. package/bizar-dash/src/web/styles/chat.css +8 -5
  71. package/bizar-dash/src/web/styles/main.css +77 -2
  72. package/bizar-dash/src/web/styles/memory.css +82 -0
  73. package/bizar-dash/src/web/styles/settings.css +265 -0
  74. package/bizar-dash/src/web/views/Chat.tsx +15 -1
  75. package/bizar-dash/src/web/views/Doctor.tsx +317 -0
  76. package/bizar-dash/src/web/views/Memory.tsx +17 -2
  77. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +1 -0
  78. package/bizar-dash/src/web/views/Schedules.tsx +94 -12
  79. package/bizar-dash/src/web/views/Settings.tsx +78 -45
  80. package/bizar-dash/src/web/views/Workspace.tsx +294 -0
  81. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +114 -133
  82. package/bizar-dash/src/web/views/memory/FromScreenshotPanel.tsx +23 -0
  83. package/bizar-dash/src/web/views/memory/VaultFromClipboardPanel.tsx +101 -0
  84. package/bizar-dash/src/web/views/settings/AgentSection.tsx +23 -12
  85. package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +5 -6
  86. package/bizar-dash/src/web/views/settings/GeneralSection.tsx +38 -15
  87. package/bizar-dash/src/web/views/settings/MemorySection.tsx +92 -8
  88. package/bizar-dash/src/web/views/settings/WorkspacesSection.tsx +189 -0
  89. package/bizar-dash/tests/autosave.test.tsx +276 -0
  90. package/bizar-dash/tests/bundle-analysis.test.mjs +5 -2
  91. package/bizar-dash/tests/chat-composer.test.tsx +140 -0
  92. package/bizar-dash/tests/clipboard.test.mjs +147 -0
  93. package/bizar-dash/tests/components/doctor-panel.test.tsx +105 -0
  94. package/bizar-dash/tests/components/screenshot-ocr.test.tsx +75 -0
  95. package/bizar-dash/tests/components/workspace-selector.test.tsx +73 -0
  96. package/bizar-dash/tests/deploy-templates.test.mjs +100 -0
  97. package/bizar-dash/tests/diagnostics-store.test.mjs +206 -0
  98. package/bizar-dash/tests/env-vars-extended.test.mjs +190 -0
  99. package/bizar-dash/tests/eval/fixtures.test.mjs +141 -0
  100. package/bizar-dash/tests/eval/report.test.mjs +284 -0
  101. package/bizar-dash/tests/eval/runner.test.mjs +471 -0
  102. package/bizar-dash/tests/layout-spacing.test.mjs +101 -0
  103. package/bizar-dash/tests/memory-default-vault.test.mjs +98 -0
  104. package/bizar-dash/tests/minimax-bar.test.tsx +113 -0
  105. package/bizar-dash/tests/minimax-models.test.mjs +123 -0
  106. package/bizar-dash/tests/ocr.test.mjs +87 -0
  107. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +158 -0
  108. package/bizar-dash/tests/plugins-registry.test.mjs +387 -0
  109. package/bizar-dash/tests/plugins-sandbox.test.mjs +374 -0
  110. package/bizar-dash/tests/plugins-store.test.mjs +455 -0
  111. package/bizar-dash/tests/routes-doctor.test.mjs +159 -0
  112. package/bizar-dash/tests/schedules-templates.test.mjs +144 -0
  113. package/bizar-dash/tests/settings-layout.test.tsx +129 -0
  114. package/bizar-dash/tests/settings-mode-wiring.test.tsx +151 -0
  115. package/bizar-dash/tests/settings-nav.test.tsx +126 -0
  116. package/bizar-dash/tests/users.test.mjs +108 -0
  117. package/bizar-dash/tests/voice-recorder.test.tsx +95 -0
  118. package/bizar-dash/tests/voice-store.test.mjs +148 -0
  119. package/bizar-dash/tests/voice-transcribe.test.mjs +87 -0
  120. package/bizar-dash/tests/workspaces.test.mjs +527 -0
  121. package/cli/bin.mjs +72 -2
  122. package/cli/commands/clip.mjs +146 -0
  123. package/cli/commands/deploy/cloudflare.mjs +250 -0
  124. package/cli/commands/deploy/docker.mjs +221 -0
  125. package/cli/commands/deploy/fly.mjs +161 -0
  126. package/cli/commands/deploy/vercel.mjs +225 -0
  127. package/cli/commands/deploy.mjs +240 -0
  128. package/cli/commands/eval.mjs +378 -0
  129. package/cli/commands/marketplace.mjs +64 -0
  130. package/cli/commands/minimax.mjs +5 -2
  131. package/cli/commands/ocr.mjs +165 -0
  132. package/cli/commands/plugin.mjs +358 -0
  133. package/cli/commands/voice.mjs +211 -0
  134. package/cli/commands/workspace.mjs +247 -0
  135. package/cli/tests/minimax-cli.test.mjs +79 -0
  136. package/config/agents/frigg.md +1 -1
  137. package/config/agents/heimdall.md +1 -1
  138. package/config/agents/mimir.md +1 -1
  139. package/config/agents/quick.md +1 -1
  140. package/config/agents/semble-search.md +1 -1
  141. package/config/agents/vor.md +1 -1
  142. package/config/opencode.json +28 -21
  143. package/config/opencode.json.template +8 -1
  144. package/package.json +12 -8
  145. package/plugins/bizar/index.ts +77 -0
  146. package/plugins/bizar/src/compaction.d.mts +48 -0
  147. package/plugins/bizar/src/compaction.mjs +192 -0
  148. package/plugins/bizar/tests/compaction.test.ts +264 -0
  149. package/templates/deploy/cloudflare/README.md +32 -0
  150. package/templates/deploy/cloudflare/functions-index.template.js +15 -0
  151. package/templates/deploy/cloudflare/wrangler.toml.template +9 -0
  152. package/templates/deploy/docker/.env.template +16 -0
  153. package/templates/deploy/docker/README.md +58 -0
  154. package/templates/deploy/docker/docker-compose.template.yml +23 -0
  155. package/templates/deploy/fly/README.md +35 -0
  156. package/templates/deploy/fly/fly.toml.template +28 -0
  157. package/templates/deploy/vercel/README.md +29 -0
  158. package/templates/deploy/vercel/api-index.template.js +18 -0
  159. package/templates/deploy/vercel/vercel.json.template +16 -0
  160. package/templates/eval-fixtures/README.md +58 -0
  161. package/templates/eval-fixtures/code-search-basic.json +28 -0
  162. package/templates/eval-fixtures/latency-bounds.json +16 -0
  163. package/templates/eval-fixtures/regression-suite.json +79 -0
  164. package/templates/eval-fixtures/response-format.json +30 -0
  165. package/templates/eval-fixtures/tool-call-correctness.json +24 -0
  166. package/templates/plugin-template/README.md +121 -0
  167. package/templates/plugin-template/index.js +66 -0
  168. package/templates/plugin-template/plugin.json +42 -0
  169. package/templates/plugin-template/tests/plugin.test.js +83 -0
  170. package/templates/schedules/daily-backup.json +12 -0
  171. package/templates/schedules/daily-cleanup.json +12 -0
  172. package/templates/schedules/hourly-health-check.json +12 -0
  173. package/templates/schedules/webhook-on-push.json +13 -0
  174. package/templates/schedules/weekly-digest.json +13 -0
  175. package/bizar-dash/dist/assets/main-DaC1Lc6q.js +0 -366
  176. package/bizar-dash/dist/assets/main-DaC1Lc6q.js.map +0 -1
  177. package/bizar-dash/dist/assets/main-DfmIfOUS.css +0 -1
  178. package/bizar-dash/dist/assets/mobile-CL5uUQEC.js +0 -1
  179. package/bizar-dash/dist/assets/mobile-D5WTWvuh.js +0 -338
  180. package/bizar-dash/dist/assets/mobile-D5WTWvuh.js.map +0 -1
  181. package/bizar-dash/src/web/views/settings/BackupSection.tsx +0 -16
  182. package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +0 -16
  183. package/bizar-dash/src/web/views/settings/SkillsSection.tsx +0 -16
@@ -0,0 +1,471 @@
1
+ /**
2
+ * tests/eval/runner.test.mjs
3
+ *
4
+ * v5.0.0 — Tests for the eval runner.
5
+ *
6
+ * Verifies:
7
+ * - runFixture returns correct shape
8
+ * - All check types work (contains, notContains, regex, jsonSchema, maxTokens, maxLatencyMs)
9
+ * - Latency tracking
10
+ * - Timeout enforcement
11
+ * - Suite parallelism
12
+ */
13
+
14
+ import { describe, it, beforeEach, afterEach } from 'node:test';
15
+ import assert from 'node:assert';
16
+ import { tmpdir } from 'node:os';
17
+ import { join, dirname } from 'node:path';
18
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const __dirname = dirname(fileURLToPath(import.meta.url));
22
+ const EVAL = await import('../../src/server/eval.mjs');
23
+
24
+ // ── Mock LLM call helpers ──────────────────────────────────────────────────────
25
+
26
+ function mockLlmCall({ content = 'ok', latencyMs = 10, tokens = 100 } = {}) {
27
+ return async (prompt, opts) => {
28
+ await new Promise((r) => setTimeout(r, latencyMs));
29
+ return {
30
+ content,
31
+ usage: {
32
+ inputTokens: tokens,
33
+ outputTokens: tokens,
34
+ totalTokens: tokens * 2,
35
+ },
36
+ };
37
+ };
38
+ }
39
+
40
+ function failingLlmCall() {
41
+ return async (prompt, opts) => {
42
+ throw new Error('LLM error');
43
+ };
44
+ }
45
+
46
+ // ── Fixtures ───────────────────────────────────────────────────────────────────
47
+
48
+ describe('eval runner', () => {
49
+
50
+ describe('runFixture', () => {
51
+
52
+ it('returns correct shape', async () => {
53
+ const fixture = {
54
+ id: 'test-fixture',
55
+ name: 'Test',
56
+ description: 'Test fixture',
57
+ agent: 'thor',
58
+ prompt: 'Say hello',
59
+ expected: {},
60
+ };
61
+
62
+ const result = await EVAL.runFixture(fixture, {
63
+ llmCall: mockLlmCall({ content: 'hello world' }),
64
+ });
65
+
66
+ assert.strictEqual(result.fixtureId, 'test-fixture');
67
+ assert.strictEqual(typeof result.ok, 'boolean');
68
+ assert.ok(Array.isArray(result.checks));
69
+ assert.ok(typeof result.latencyMs === 'number');
70
+ assert.strictEqual(result.content, 'hello world');
71
+ assert.ok(result.usage);
72
+ assert.strictEqual(result.usage.totalTokens, 200);
73
+ });
74
+
75
+ describe('contains check', () => {
76
+ it('passes when content includes expected substring', async () => {
77
+ const fixture = {
78
+ id: 'test',
79
+ agent: 'thor',
80
+ prompt: 'test',
81
+ expected: { contains: ['hello', 'world'] },
82
+ };
83
+ const result = await EVAL.runFixture(fixture, {
84
+ llmCall: mockLlmCall({ content: 'hello world' }),
85
+ });
86
+ assert.strictEqual(result.ok, true);
87
+ const containsCheck = result.checks.find((c) => c.kind === 'contains');
88
+ assert.ok(containsCheck);
89
+ assert.strictEqual(containsCheck.ok, true);
90
+ });
91
+
92
+ it('fails when content does not include expected substring', async () => {
93
+ const fixture = {
94
+ id: 'test',
95
+ agent: 'thor',
96
+ prompt: 'test',
97
+ expected: { contains: ['goodbye'] },
98
+ };
99
+ const result = await EVAL.runFixture(fixture, {
100
+ llmCall: mockLlmCall({ content: 'hello world' }),
101
+ });
102
+ assert.strictEqual(result.ok, false);
103
+ const containsCheck = result.checks.find((c) => c.kind === 'contains');
104
+ assert.ok(containsCheck);
105
+ assert.strictEqual(containsCheck.ok, false);
106
+ assert.ok(containsCheck.message.includes('goodbye'));
107
+ });
108
+ });
109
+
110
+ describe('notContains check', () => {
111
+ it('passes when content does NOT include forbidden substring', async () => {
112
+ const fixture = {
113
+ id: 'test',
114
+ agent: 'thor',
115
+ prompt: 'test',
116
+ expected: { notContains: ['error', 'fail'] },
117
+ };
118
+ const result = await EVAL.runFixture(fixture, {
119
+ llmCall: mockLlmCall({ content: 'hello world' }),
120
+ });
121
+ assert.strictEqual(result.ok, true);
122
+ const notContainsCheck = result.checks.find((c) => c.kind === 'notContains');
123
+ assert.ok(notContainsCheck);
124
+ assert.strictEqual(notContainsCheck.ok, true);
125
+ });
126
+
127
+ it('fails when content includes forbidden substring', async () => {
128
+ const fixture = {
129
+ id: 'test',
130
+ agent: 'thor',
131
+ prompt: 'test',
132
+ expected: { notContains: ['error'] },
133
+ };
134
+ const result = await EVAL.runFixture(fixture, {
135
+ llmCall: mockLlmCall({ content: 'hello error world' }),
136
+ });
137
+ assert.strictEqual(result.ok, false);
138
+ const notContainsCheck = result.checks.find((c) => c.kind === 'notContains');
139
+ assert.ok(notContainsCheck);
140
+ assert.strictEqual(notContainsCheck.ok, false);
141
+ });
142
+ });
143
+
144
+ describe('regex check', () => {
145
+ it('passes when regex matches', async () => {
146
+ const fixture = {
147
+ id: 'test',
148
+ agent: 'thor',
149
+ prompt: 'test',
150
+ expected: { regex: ['hello\\s+world'] },
151
+ };
152
+ const result = await EVAL.runFixture(fixture, {
153
+ llmCall: mockLlmCall({ content: 'hello world' }),
154
+ });
155
+ assert.strictEqual(result.ok, true);
156
+ const regexCheck = result.checks.find((c) => c.kind === 'regex');
157
+ assert.ok(regexCheck);
158
+ assert.strictEqual(regexCheck.ok, true);
159
+ });
160
+
161
+ it('fails when regex does not match', async () => {
162
+ const fixture = {
163
+ id: 'test',
164
+ agent: 'thor',
165
+ prompt: 'test',
166
+ expected: { regex: ['goodbye\\s+world'] },
167
+ };
168
+ const result = await EVAL.runFixture(fixture, {
169
+ llmCall: mockLlmCall({ content: 'hello world' }),
170
+ });
171
+ assert.strictEqual(result.ok, false);
172
+ const regexCheck = result.checks.find((c) => c.kind === 'regex');
173
+ assert.ok(regexCheck);
174
+ assert.strictEqual(regexCheck.ok, false);
175
+ });
176
+
177
+ it('fails gracefully on invalid regex', async () => {
178
+ const fixture = {
179
+ id: 'test',
180
+ agent: 'thor',
181
+ prompt: 'test',
182
+ expected: { regex: ['[invalid'] },
183
+ };
184
+ const result = await EVAL.runFixture(fixture, {
185
+ llmCall: mockLlmCall({ content: 'hello' }),
186
+ });
187
+ assert.strictEqual(result.ok, false);
188
+ const regexCheck = result.checks.find((c) => c.kind === 'regex');
189
+ assert.ok(regexCheck);
190
+ assert.strictEqual(regexCheck.ok, false);
191
+ assert.ok(regexCheck.message.includes('invalid regex'));
192
+ });
193
+ });
194
+
195
+ describe('jsonSchema check', () => {
196
+ it('passes when response is valid JSON matching schema', async () => {
197
+ const fixture = {
198
+ id: 'test',
199
+ agent: 'thor',
200
+ prompt: 'test',
201
+ expected: {
202
+ jsonSchema: {
203
+ type: 'object',
204
+ required: ['status'],
205
+ properties: { status: { type: 'string' } },
206
+ },
207
+ },
208
+ };
209
+ const result = await EVAL.runFixture(fixture, {
210
+ llmCall: mockLlmCall({ content: '{"status": "ok"}' }),
211
+ });
212
+ assert.strictEqual(result.ok, true);
213
+ const schemaCheck = result.checks.find((c) => c.kind === 'jsonSchema');
214
+ assert.ok(schemaCheck);
215
+ assert.strictEqual(schemaCheck.ok, true);
216
+ });
217
+
218
+ it('fails when response is not valid JSON', async () => {
219
+ const fixture = {
220
+ id: 'test',
221
+ agent: 'thor',
222
+ prompt: 'test',
223
+ expected: { jsonSchema: { type: 'object' } },
224
+ };
225
+ const result = await EVAL.runFixture(fixture, {
226
+ llmCall: mockLlmCall({ content: 'not json' }),
227
+ });
228
+ assert.strictEqual(result.ok, false);
229
+ const schemaCheck = result.checks.find((c) => c.kind === 'jsonSchema');
230
+ assert.ok(schemaCheck);
231
+ assert.strictEqual(schemaCheck.ok, false);
232
+ });
233
+
234
+ it('fails when required property is missing', async () => {
235
+ const fixture = {
236
+ id: 'test',
237
+ agent: 'thor',
238
+ prompt: 'test',
239
+ expected: {
240
+ jsonSchema: {
241
+ type: 'object',
242
+ required: ['version'],
243
+ properties: { version: { type: 'string' } },
244
+ },
245
+ },
246
+ };
247
+ const result = await EVAL.runFixture(fixture, {
248
+ llmCall: mockLlmCall({ content: '{"status": "ok"}' }),
249
+ });
250
+ assert.strictEqual(result.ok, false);
251
+ const schemaCheck = result.checks.find((c) => c.kind === 'jsonSchema');
252
+ assert.ok(schemaCheck);
253
+ assert.strictEqual(schemaCheck.ok, false);
254
+ assert.ok(schemaCheck.message.includes('missing required property'));
255
+ });
256
+ });
257
+
258
+ describe('maxTokens check', () => {
259
+ it('passes when token count is under limit', async () => {
260
+ const fixture = {
261
+ id: 'test',
262
+ agent: 'thor',
263
+ prompt: 'test',
264
+ expected: { maxTokens: 1000 },
265
+ };
266
+ const result = await EVAL.runFixture(fixture, {
267
+ llmCall: mockLlmCall({ tokens: 100 }),
268
+ });
269
+ assert.strictEqual(result.ok, true);
270
+ const tokenCheck = result.checks.find((c) => c.kind === 'maxTokens');
271
+ assert.ok(tokenCheck);
272
+ assert.strictEqual(tokenCheck.ok, true);
273
+ });
274
+
275
+ it('fails when token count exceeds limit', async () => {
276
+ const fixture = {
277
+ id: 'test',
278
+ agent: 'thor',
279
+ prompt: 'test',
280
+ expected: { maxTokens: 50 },
281
+ };
282
+ const result = await EVAL.runFixture(fixture, {
283
+ llmCall: mockLlmCall({ tokens: 100 }),
284
+ });
285
+ assert.strictEqual(result.ok, false);
286
+ const tokenCheck = result.checks.find((c) => c.kind === 'maxTokens');
287
+ assert.ok(tokenCheck);
288
+ assert.strictEqual(tokenCheck.ok, false);
289
+ assert.ok(tokenCheck.message.includes('exceeds max'));
290
+ });
291
+ });
292
+
293
+ describe('maxLatencyMs check', () => {
294
+ it('passes when latency is under limit', async () => {
295
+ const fixture = {
296
+ id: 'test',
297
+ agent: 'thor',
298
+ prompt: 'test',
299
+ expected: { maxLatencyMs: 5000 },
300
+ };
301
+ const result = await EVAL.runFixture(fixture, {
302
+ llmCall: mockLlmCall({ latencyMs: 10 }),
303
+ });
304
+ assert.strictEqual(result.ok, true);
305
+ const latencyCheck = result.checks.find((c) => c.kind === 'maxLatencyMs');
306
+ assert.ok(latencyCheck);
307
+ assert.strictEqual(latencyCheck.ok, true);
308
+ });
309
+
310
+ it('fails when latency exceeds limit', async () => {
311
+ const fixture = {
312
+ id: 'test',
313
+ agent: 'thor',
314
+ prompt: 'test',
315
+ expected: { maxLatencyMs: 5 },
316
+ };
317
+ const result = await EVAL.runFixture(fixture, {
318
+ llmCall: mockLlmCall({ latencyMs: 100 }),
319
+ });
320
+ assert.strictEqual(result.ok, false);
321
+ const latencyCheck = result.checks.find((c) => c.kind === 'maxLatencyMs');
322
+ assert.ok(latencyCheck);
323
+ assert.strictEqual(latencyCheck.ok, false);
324
+ assert.ok(latencyCheck.message.includes('exceeds max'));
325
+ });
326
+ });
327
+
328
+ describe('timeout enforcement', () => {
329
+ it('returns timeout check on timeout', async () => {
330
+ const fixture = {
331
+ id: 'test',
332
+ agent: 'thor',
333
+ prompt: 'test',
334
+ expected: {},
335
+ };
336
+ const result = await EVAL.runFixture(fixture, {
337
+ llmCall: async () => {
338
+ await new Promise((r) => setTimeout(r, 200));
339
+ return { content: 'ok', usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 } };
340
+ },
341
+ timeoutMs: 50,
342
+ });
343
+ assert.strictEqual(result.ok, false);
344
+ const timeoutCheck = result.checks.find((c) => c.kind === 'timeout');
345
+ assert.ok(timeoutCheck);
346
+ assert.strictEqual(timeoutCheck.ok, false);
347
+ });
348
+ });
349
+
350
+ describe('error handling', () => {
351
+ it('returns error check when LLM call throws', async () => {
352
+ const fixture = {
353
+ id: 'test',
354
+ agent: 'thor',
355
+ prompt: 'test',
356
+ expected: {},
357
+ };
358
+ const result = await EVAL.runFixture(fixture, {
359
+ llmCall: failingLlmCall(),
360
+ });
361
+ assert.strictEqual(result.ok, false);
362
+ const errorCheck = result.checks.find((c) => c.kind === 'error');
363
+ assert.ok(errorCheck);
364
+ assert.strictEqual(errorCheck.ok, false);
365
+ assert.ok(errorCheck.message.includes('LLM error'));
366
+ });
367
+ });
368
+ });
369
+
370
+ describe('runSuite', () => {
371
+ const suiteDir = join(tmpdir(), `eval-suite-${Date.now()}-${Math.random().toString(36).slice(2)}`);
372
+
373
+ beforeEach(() => {
374
+ mkdirSync(suiteDir, { recursive: true });
375
+ writeFileSync(join(suiteDir, 'fix1.json'), JSON.stringify({
376
+ id: 'fix1',
377
+ name: 'Fix 1',
378
+ description: 'First fixture',
379
+ agent: 'thor',
380
+ prompt: 'Say hello',
381
+ expected: { contains: ['hello'] },
382
+ }));
383
+ writeFileSync(join(suiteDir, 'fix2.json'), JSON.stringify({
384
+ id: 'fix2',
385
+ name: 'Fix 2',
386
+ description: 'Second fixture',
387
+ agent: 'thor',
388
+ prompt: 'Say goodbye',
389
+ expected: { contains: ['goodbye'] },
390
+ }));
391
+ });
392
+
393
+ afterEach(() => {
394
+ rmSync(suiteDir, { recursive: true, force: true });
395
+ });
396
+
397
+ it('runs all fixtures in a directory', async () => {
398
+ const result = await EVAL.runSuite(suiteDir, {
399
+ llmCall: mockLlmCall({ content: 'hello and goodbye' }),
400
+ concurrency: 5,
401
+ });
402
+ assert.strictEqual(result.total, 2);
403
+ assert.strictEqual(result.passed, 2);
404
+ assert.strictEqual(result.failed, 0);
405
+ assert.strictEqual(result.results.length, 2);
406
+ });
407
+
408
+ it('reports failures correctly', async () => {
409
+ const result = await EVAL.runSuite(suiteDir, {
410
+ llmCall: mockLlmCall({ content: 'hello only' }),
411
+ concurrency: 5,
412
+ });
413
+ assert.strictEqual(result.total, 2);
414
+ assert.strictEqual(result.passed, 1);
415
+ assert.strictEqual(result.failed, 1);
416
+ });
417
+
418
+ it('runs fixtures in parallel batches', async () => {
419
+ const start = Date.now();
420
+ // Create 6 fixtures with unique IDs to test batching
421
+ for (let i = 0; i < 6; i++) {
422
+ writeFileSync(join(suiteDir, `batch${i}.json`), JSON.stringify({
423
+ id: `batch${i}`,
424
+ name: `Batch ${i}`,
425
+ description: 'Test fixture',
426
+ agent: 'thor',
427
+ prompt: 'Say hello',
428
+ expected: { contains: ['hello'] },
429
+ }));
430
+ }
431
+ const result = await EVAL.runSuite(suiteDir, {
432
+ llmCall: mockLlmCall({ latencyMs: 50 }),
433
+ concurrency: 3,
434
+ });
435
+ // 2 original (fix1, fix2) + 6 new (batch0-5) = 8
436
+ assert.ok(result.total >= 8, `Expected at least 8 fixtures, got ${result.total}`);
437
+ // Parallel batches should complete faster than sequential
438
+ const elapsed = Date.now() - start;
439
+ assert.ok(elapsed < 500, `Should complete quickly, took ${elapsed}ms`);
440
+ });
441
+
442
+ it('handles suite with no fixtures', async () => {
443
+ const emptyDir = join(tmpdir(), `eval-empty-${Date.now()}`);
444
+ mkdirSync(emptyDir, { recursive: true });
445
+ const result = await EVAL.runSuite(emptyDir, {
446
+ llmCall: mockLlmCall(),
447
+ concurrency: 5,
448
+ });
449
+ assert.strictEqual(result.total, 0);
450
+ assert.strictEqual(result.passed, 0);
451
+ assert.strictEqual(result.failed, 0);
452
+ rmSync(emptyDir, { recursive: true, force: true });
453
+ });
454
+
455
+ it('loads suite JSON with array of fixtures', async () => {
456
+ const suiteFile = join(suiteDir, 'suite.json');
457
+ writeFileSync(suiteFile, JSON.stringify({
458
+ id: 'my-suite',
459
+ fixtures: [
460
+ { id: 'f1', name: 'F1', description: 'F1', agent: 'thor', prompt: 'hi', expected: { contains: ['hi'] } },
461
+ { id: 'f2', name: 'F2', description: 'F2', agent: 'thor', prompt: 'bye', expected: { contains: ['bye'] } },
462
+ ],
463
+ }));
464
+ const result = await EVAL.runSuite(suiteDir, {
465
+ llmCall: mockLlmCall({ content: 'hi and bye' }),
466
+ concurrency: 5,
467
+ });
468
+ assert.strictEqual(result.total, 4); // 2 individual + 2 in suite
469
+ });
470
+ });
471
+ });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * tests/layout-spacing.test.mjs
3
+ *
4
+ * v4.x.x — Smoke test for page-level CSS spacing.
5
+ * Parses main.css and verifies the .view class has sufficient top padding
6
+ * so content isn't squished against the topbar.
7
+ */
8
+ import { readFileSync } from 'node:fs';
9
+ import { resolve } from 'node:path';
10
+
11
+ const css = readFileSync(
12
+ resolve(import.meta.dirname, '../src/web/styles/main.css'),
13
+ 'utf8',
14
+ );
15
+
16
+ // Extract the .view rule's padding value.
17
+ // Handles both `padding: 24px` (shorthand) and `padding: var(--space-6)` (CSS variable).
18
+ // We extract the first token (top padding) and resolve CSS var to its px value.
19
+ const viewPaddingMatch = css.match(/\.view\s*\{[^}]*padding:\s*([^;]+)(?:\s|;)/);
20
+ if (!viewPaddingMatch) throw new Error('Could not find .view { padding: ... } in main.css');
21
+
22
+ // The padding shorthand may be "var(--space-6) var(--space-6) 0" (top right bottom).
23
+ // We split by whitespace and resolve the first token.
24
+ const paddingTokens = viewPaddingMatch[1].trim().split(/\s+/);
25
+ const paddingValue = paddingTokens[0];
26
+ // CSS var: var(--space-6) = 24px, var(--space-4) = 16px, var(--space-3) = 12px
27
+ const CSS_VAR_MAP = {
28
+ 'var(--space-1)': 4,
29
+ 'var(--space-2)': 8,
30
+ 'var(--space-3)': 12,
31
+ 'var(--space-4)': 16,
32
+ 'var(--space-5)': 20,
33
+ 'var(--space-6)': 24,
34
+ 'var(--space-8)': 32,
35
+ };
36
+
37
+ let paddingNum;
38
+ if (paddingValue.startsWith('var(')) {
39
+ paddingNum = CSS_VAR_MAP[paddingValue] ?? NaN;
40
+ } else {
41
+ paddingNum = parseInt(paddingValue, 10);
42
+ }
43
+ if (isNaN(paddingNum)) throw new Error(`Unexpected padding value: "${paddingValue}"`);
44
+
45
+ const MIN_PADDING = 16;
46
+ if (paddingNum < MIN_PADDING) {
47
+ throw new Error(
48
+ `.view padding-top too small: ${paddingNum}px (expected >= ${MIN_PADDING}px). ` +
49
+ `Content will be squished against the topbar.`,
50
+ );
51
+ }
52
+
53
+ console.log(`✓ .view padding-top: ${paddingNum}px (minimum: ${MIN_PADDING}px)`);
54
+
55
+ // Also verify .bg-active-view has adequate top padding
56
+ const bgMatch = css.match(/\.bg-active-view\s*\{[^}]*padding:\s*([^;]+)(?:\s|;)/);
57
+ if (bgMatch) {
58
+ const bgTokens = bgMatch[1].trim().split(/\s+/);
59
+ const bgVal = bgTokens[0];
60
+ let bgPadding;
61
+ if (bgVal.startsWith('var(')) {
62
+ bgPadding = CSS_VAR_MAP[bgVal] ?? NaN;
63
+ } else {
64
+ bgPadding = parseInt(bgVal, 10);
65
+ }
66
+ if (!isNaN(bgPadding) && bgPadding < MIN_PADDING) {
67
+ throw new Error(
68
+ `.bg-active-view padding-top too small: ${bgPadding}px (expected >= ${MIN_PADDING}px)`,
69
+ );
70
+ }
71
+ if (!isNaN(bgPadding)) console.log(`✓ .bg-active-view padding-top: ${bgPadding}px`);
72
+ }
73
+
74
+ // Verify .view gap is at least 16px
75
+ const gapMatch = css.match(/\.view\s*\{[^}]*gap:\s*([^;]+)(?:\s|;)/);
76
+ if (gapMatch) {
77
+ const gapVal = gapMatch[1].trim();
78
+ let gapNum;
79
+ if (gapVal.startsWith('var(')) {
80
+ gapNum = CSS_VAR_MAP[gapVal] ?? NaN;
81
+ } else {
82
+ gapNum = parseInt(gapVal, 10);
83
+ }
84
+ const MIN_GAP = 16;
85
+ if (!isNaN(gapNum) && gapNum < MIN_GAP) {
86
+ throw new Error(`.view gap too small: ${gapNum}px (expected >= ${MIN_GAP}px)`);
87
+ }
88
+ if (!isNaN(gapNum)) console.log(`✓ .view gap: ${gapNum}px`);
89
+ }
90
+
91
+ // Verify .page-header exists (margin-bottom for spacing below page headers)
92
+ if (!css.includes('.page-header')) {
93
+ throw new Error('.page-header not found in main.css — page header spacing missing');
94
+ }
95
+ console.log('✓ .page-header rule present');
96
+
97
+ // Verify .card + .card rule for sibling card spacing
98
+ if (!css.includes('.card + .card')) {
99
+ throw new Error('.card + .card sibling rule not found — card spacing may be cramped');
100
+ }
101
+ console.log('✓ .card + .card sibling rule present');
@@ -0,0 +1,98 @@
1
+ /**
2
+ * tests/memory-default-vault.test.mjs
3
+ *
4
+ * Tests for the default vault location, currentVault(), and ensureVaultExists().
5
+ */
6
+
7
+ import { describe, it, beforeEach, afterEach } from 'node:test';
8
+ import assert from 'node:assert';
9
+ import { tmpdir, homedir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { rmSync, existsSync, readFileSync } from 'node:fs';
12
+
13
+ const TEST_STORE = await import('../src/server/memory-store.mjs').then((m) => m);
14
+
15
+ describe('DEFAULT_MEMORY_VAULT', () => {
16
+ it('is ~/.local/share/bizar/memory', () => {
17
+ const expected = join(homedir(), '.local', 'share', 'bizar', 'memory');
18
+ assert.strictEqual(TEST_STORE.DEFAULT_MEMORY_VAULT, expected);
19
+ });
20
+ });
21
+
22
+ describe('DEFAULT_GIT_REMOTE', () => {
23
+ it('is null by default', () => {
24
+ assert.strictEqual(TEST_STORE.DEFAULT_GIT_REMOTE, null);
25
+ });
26
+ });
27
+
28
+ describe('currentVault', () => {
29
+ it('returns DEFAULT_MEMORY_VAULT when no env override', () => {
30
+ assert.strictEqual(TEST_STORE.currentVault(), TEST_STORE.DEFAULT_MEMORY_VAULT);
31
+ });
32
+
33
+ it('returns BIZAR_MEMORY_VAULT env var when set', () => {
34
+ const orig = process.env.BIZAR_MEMORY_VAULT;
35
+ process.env.BIZAR_MEMORY_VAULT = '/tmp/test-current-vault';
36
+ try {
37
+ assert.strictEqual(TEST_STORE.currentVault(), '/tmp/test-current-vault');
38
+ } finally {
39
+ if (orig === undefined) delete process.env.BIZAR_MEMORY_VAULT;
40
+ else process.env.BIZAR_MEMORY_VAULT = orig;
41
+ }
42
+ });
43
+ });
44
+
45
+ describe('ensureVaultExists', () => {
46
+ // Use a real temp path so we actually test the function
47
+ let testVaultRoot;
48
+
49
+ beforeEach(() => {
50
+ testVaultRoot = join(
51
+ tmpdir(),
52
+ `bizar-test-vault-${Date.now()}-${Math.random().toString(36).slice(2)}`,
53
+ );
54
+ // Set env var before importing / calling
55
+ process.env.BIZAR_MEMORY_VAULT = testVaultRoot;
56
+ });
57
+
58
+ afterEach(() => {
59
+ delete process.env.BIZAR_MEMORY_VAULT;
60
+ try { rmSync(testVaultRoot, { recursive: true, force: true }); } catch { /* ignore */ }
61
+ });
62
+
63
+ it('creates the vault directory if missing', () => {
64
+ assert.strictEqual(existsSync(testVaultRoot), false);
65
+ const result = TEST_STORE.ensureVaultExists();
66
+ assert.strictEqual(existsSync(testVaultRoot), true);
67
+ assert.strictEqual(result, testVaultRoot);
68
+ });
69
+
70
+ it('is idempotent — calling twice does not throw', () => {
71
+ TEST_STORE.ensureVaultExists();
72
+ const result = TEST_STORE.ensureVaultExists();
73
+ assert.strictEqual(result, testVaultRoot);
74
+ assert.strictEqual(existsSync(testVaultRoot), true);
75
+ });
76
+
77
+ it('inits git repo on first run when .git is absent', () => {
78
+ assert.strictEqual(existsSync(join(testVaultRoot, '.git')), false);
79
+ TEST_STORE.ensureVaultExists();
80
+ assert.strictEqual(existsSync(join(testVaultRoot, '.git')), true);
81
+ });
82
+
83
+ it('skips git init if .git already exists', () => {
84
+ TEST_STORE.ensureVaultExists();
85
+ assert.strictEqual(existsSync(join(testVaultRoot, '.git')), true);
86
+ // second call must not throw
87
+ TEST_STORE.ensureVaultExists();
88
+ assert.strictEqual(existsSync(join(testVaultRoot, '.git')), true);
89
+ });
90
+
91
+ it('sets git user.email config', () => {
92
+ TEST_STORE.ensureVaultExists();
93
+ const configFile = join(testVaultRoot, '.git', 'config');
94
+ assert.strictEqual(existsSync(configFile), true);
95
+ const content = readFileSync(configFile, 'utf8');
96
+ assert.match(content, /bizar@localhost/);
97
+ });
98
+ });