@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,206 @@
1
+ /**
2
+ * tests/diagnostics-store.test.mjs
3
+ *
4
+ * v6.0.0 — Tests for the Doctor surface of the diagnostics store:
5
+ * - getRecentErrors({ since }) — timestamp-filtered error feed
6
+ * - runCheck(name) — single-check dispatch
7
+ * - health() — rolled-up status + groups
8
+ * - collectDiagnostics() — full snapshot shape
9
+ * - legacy snapshot() / health() — back-compat with v3 /api/diagnostics
10
+ *
11
+ * The store reads from `~/.config/bizar/service.log` and TCP-probes
12
+ * local services. Tests that exercise I/O use a temporary BIZAR_HOME
13
+ * so they don't touch the real install.
14
+ */
15
+ import { describe, it, beforeEach, afterEach } from 'node:test';
16
+ import assert from 'node:assert';
17
+ import { tmpdir } from 'node:os';
18
+ import { join } from 'node:path';
19
+ import { mkdtempSync, rmSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
20
+
21
+ let tmpHome;
22
+
23
+ const STORE = await import('../src/server/diagnostics-store.mjs');
24
+
25
+ beforeEach(() => {
26
+ tmpHome = mkdtempSync(join(tmpdir(), 'bizar-doctor-store-'));
27
+ // The store reads `~/.config/bizar/service.log` via the homedir()
28
+ // helper. We can't change homedir() at runtime, so we redirect
29
+ // BIZAR-related env by writing an empty service log to the real
30
+ // ~/.config/bizar path... actually the store uses homedir()
31
+ // unconditionally, so tests that need a custom log must write
32
+ // before each test. We work around by NOT requiring log presence
33
+ // — getRecentErrors() returns [] on missing files.
34
+ });
35
+
36
+ afterEach(() => {
37
+ if (tmpHome) {
38
+ try { rmSync(tmpHome, { recursive: true, force: true }); } catch { /* ignore */ }
39
+ }
40
+ });
41
+
42
+ // ── getRecentErrors ────────────────────────────────────────────────────────
43
+
44
+ describe('getRecentErrors', () => {
45
+ it('returns [] when no service log exists', () => {
46
+ // The real ~/.config/bizar/service.log may or may not exist;
47
+ // either way, the function must return an array (possibly empty).
48
+ const out = STORE.getRecentErrors();
49
+ assert.ok(Array.isArray(out));
50
+ // No crash, no throw — that's the contract.
51
+ });
52
+
53
+ it('respects the limit option', () => {
54
+ const out = STORE.getRecentErrors({ limit: 5 });
55
+ assert.ok(Array.isArray(out));
56
+ assert.ok(out.length <= 5);
57
+ });
58
+
59
+ it('returns entries with the documented shape', () => {
60
+ const out = STORE.getRecentErrors({ limit: 1 });
61
+ if (out.length === 0) return; // nothing to check
62
+ const row = out[0];
63
+ assert.strictEqual(typeof row.line, 'string');
64
+ assert.ok(row.ts === null || typeof row.ts === 'string');
65
+ assert.ok(row.tsMs === null || typeof row.tsMs === 'number');
66
+ });
67
+
68
+ it('filters out lines without error keywords', () => {
69
+ // Drop the result into a sink — we just want to make sure the
70
+ // function never returns a non-error line.
71
+ const out = STORE.getRecentErrors({ limit: 200 });
72
+ for (const row of out) {
73
+ assert.ok(
74
+ /\b(failed|error|err)\b/i.test(row.line),
75
+ `expected an error keyword in: ${row.line}`,
76
+ );
77
+ }
78
+ });
79
+ });
80
+
81
+ // ── runCheck ───────────────────────────────────────────────────────────────
82
+
83
+ describe('runCheck', () => {
84
+ it('returns the named check for a known name', async () => {
85
+ const result = await STORE.runCheck('node');
86
+ assert.strictEqual(result.name, 'node');
87
+ assert.ok(['ok', 'warn', 'fail'].includes(result.status));
88
+ assert.strictEqual(typeof result.message, 'string');
89
+ });
90
+
91
+ it('returns fail for an unknown check name', async () => {
92
+ const result = await STORE.runCheck('not-a-real-check');
93
+ assert.strictEqual(result.status, 'fail');
94
+ assert.strictEqual(result.message, 'unknown check');
95
+ assert.match(result.error, /Available: /);
96
+ });
97
+
98
+ it('handles empty / non-string input', async () => {
99
+ const result = await STORE.runCheck('');
100
+ assert.strictEqual(result.status, 'fail');
101
+ assert.match(result.message, /unknown check|checkName required/);
102
+ });
103
+
104
+ it('returns the same set of names registered', async () => {
105
+ // Smoke test: a system check name (memory) and a config check
106
+ // name (opencode-config) and a service check name (dashboard)
107
+ // must all be reachable.
108
+ for (const name of ['memory', 'opencode-config', 'dashboard']) {
109
+ const r = await STORE.runCheck(name);
110
+ assert.notStrictEqual(r.message, 'unknown check', `${name} should be a known check`);
111
+ assert.ok(['ok', 'warn', 'fail'].includes(r.status));
112
+ }
113
+ });
114
+ });
115
+
116
+ // ── health ─────────────────────────────────────────────────────────────────
117
+
118
+ describe('health', () => {
119
+ it('returns the rolled-up status object', async () => {
120
+ const h = await STORE.health();
121
+ assert.strictEqual(typeof h.ts, 'string');
122
+ assert.ok(['ok', 'warn', 'fail'].includes(h.status));
123
+ assert.ok(Array.isArray(h.issues));
124
+ assert.ok(h.groups && typeof h.groups === 'object');
125
+ assert.ok(Array.isArray(h.groups.system));
126
+ assert.ok(Array.isArray(h.groups.config));
127
+ assert.ok(Array.isArray(h.groups.services));
128
+ });
129
+
130
+ it('derives status from issues (fail > warn > ok)', async () => {
131
+ const h = await STORE.health();
132
+ const has = (s) => h.issues.some((i) => i.status === s);
133
+ if (has('fail')) assert.strictEqual(h.status, 'fail');
134
+ else if (has('warn')) assert.strictEqual(h.status, 'warn');
135
+ else assert.strictEqual(h.status, 'ok');
136
+ });
137
+ });
138
+
139
+ // ── collectDiagnostics ─────────────────────────────────────────────────────
140
+
141
+ describe('collectDiagnostics', () => {
142
+ it('returns a snapshot with the documented top-level keys', async () => {
143
+ const snap = await STORE.collectDiagnostics();
144
+ assert.strictEqual(typeof snap.timestamp, 'string');
145
+ assert.strictEqual(typeof snap.bizarVersion, 'string');
146
+ assert.strictEqual(snap.nodeVersion, process.version);
147
+ assert.strictEqual(snap.platform, process.platform);
148
+ assert.strictEqual(snap.arch, process.arch);
149
+ assert.strictEqual(typeof snap.uptime, 'number');
150
+ assert.ok(snap.memory && typeof snap.memory.rss === 'number');
151
+ assert.ok(snap.disk && typeof snap.disk.exists === 'boolean');
152
+ assert.ok(snap.services);
153
+ assert.ok(snap.services.dashboard);
154
+ assert.ok(snap.services.headroom);
155
+ assert.ok(snap.services.lightrag);
156
+ assert.ok(snap.services.opencode);
157
+ assert.ok(snap.counts);
158
+ assert.ok(Array.isArray(snap.recentErrors));
159
+ assert.ok(Array.isArray(snap.configHealth));
160
+ assert.ok(snap.opencode);
161
+ assert.ok(snap.checks);
162
+ assert.ok(snap.health);
163
+ });
164
+
165
+ it('counts all expected numeric fields', async () => {
166
+ const snap = await STORE.collectDiagnostics();
167
+ const c = snap.counts;
168
+ for (const key of [
169
+ 'tasks', 'schedules', 'mods', 'providers', 'mcps', 'agents',
170
+ 'projects', 'workspaces', 'voiceNotes', 'evalRuns', 'backups',
171
+ ]) {
172
+ assert.strictEqual(typeof c[key], 'number', `counts.${key} should be a number`);
173
+ assert.ok(c[key] >= 0, `counts.${key} should be >= 0`);
174
+ }
175
+ });
176
+
177
+ it('returns recent errors within the requested window', async () => {
178
+ const snap = await STORE.collectDiagnostics();
179
+ for (const err of snap.recentErrors) {
180
+ assert.ok(err.tsMs === null || err.tsMs >= Date.now() - 3700_000,
181
+ `recent error outside the last-hour window: ${err.line}`);
182
+ }
183
+ });
184
+ });
185
+
186
+ // ── legacy snapshot / health (back-compat with v3 /api/diagnostics) ───────
187
+
188
+ describe('diagnosticsStore (legacy v3)', () => {
189
+ it('snapshot() returns the legacy shape', () => {
190
+ const snap = STORE.diagnosticsStore.snapshot();
191
+ assert.strictEqual(typeof snap.version, 'string');
192
+ assert.strictEqual(typeof snap.uptime, 'number');
193
+ assert.strictEqual(snap.nodeVersion, process.version);
194
+ assert.strictEqual(snap.platform, process.platform);
195
+ assert.ok(snap.memory);
196
+ assert.ok(snap.counts);
197
+ assert.ok(Array.isArray(snap.errors));
198
+ assert.ok(snap.service);
199
+ });
200
+
201
+ it('legacy health() returns checks array', () => {
202
+ const h = STORE.diagnosticsStore.health();
203
+ assert.strictEqual(typeof h.ts, 'string');
204
+ assert.ok(Array.isArray(h.checks));
205
+ });
206
+ });
@@ -0,0 +1,190 @@
1
+ /**
2
+ * env-vars-extended.test.mjs — tests for bulk-import, export, grouped endpoints.
3
+ *
4
+ * Run with: node --test bizar-dash/tests/env-vars-extended.test.mjs
5
+ */
6
+ import { describe, it, before, after, beforeEach, afterEach } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import { mkdtempSync, rmSync, readFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { tmpdir } from 'node:os';
11
+
12
+ const REPO = join(import.meta.dirname, '..', '..');
13
+
14
+ let SANDBOX_HOME;
15
+ let ORIGINAL_HOME;
16
+ let ENV_FILE;
17
+
18
+ before(() => {
19
+ SANDBOX_HOME = mkdtempSync(join(tmpdir(), `bizar-env-vars-ext-${Date.now()}-`));
20
+ ORIGINAL_HOME = process.env.HOME;
21
+ process.env.HOME = SANDBOX_HOME;
22
+ const BIZAR_DIR = join(SANDBOX_HOME, '.config', 'bizar');
23
+ ENV_FILE = join(BIZAR_DIR, 'env.json');
24
+ });
25
+
26
+ after(() => {
27
+ process.env.HOME = ORIGINAL_HOME;
28
+ for (const k of Object.keys(process.env)) {
29
+ if (k.startsWith('BIZAR_EXT_')) delete process.env[k];
30
+ }
31
+ try { rmSync(SANDBOX_HOME, { recursive: true, force: true }); } catch { /* ignore */ }
32
+ });
33
+
34
+ async function request(app, method, path, body) {
35
+ const http = await import('node:http');
36
+ return new Promise((resolve, reject) => {
37
+ const server = app.listen(0, '127.0.0.1', () => {
38
+ const { port } = server.address();
39
+ const bodyStr = body ? JSON.stringify(body) : '';
40
+ const opts = {
41
+ hostname: '127.0.0.1', port, path, method,
42
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr) },
43
+ };
44
+ const req = http.request(opts, (res) => {
45
+ let data = '';
46
+ res.on('data', (chunk) => { data += chunk; });
47
+ res.on('end', () => {
48
+ server.close();
49
+ try { resolve({ status: res.statusCode, body: JSON.parse(data), raw: data, headers: res.headers }); }
50
+ catch { resolve({ status: res.statusCode, body: data, raw: data, headers: res.headers }); }
51
+ });
52
+ });
53
+ req.on('error', (err) => { server.close(); reject(err); });
54
+ if (bodyStr) req.write(bodyStr);
55
+ req.end();
56
+ });
57
+ });
58
+ }
59
+
60
+ // Track which keys this test file uses so we can clean them up
61
+ const TEST_KEYS = new Set();
62
+
63
+ describe('env-vars extended endpoints', () => {
64
+ let app;
65
+
66
+ beforeEach(async () => {
67
+ // Delete env file to start fresh
68
+ try { rmSync(ENV_FILE); } catch { /* ignore */ }
69
+ // Import fresh module
70
+ const express = (await import('express')).default;
71
+ const { createEnvVarsRouter } = await import(`${REPO}/bizar-dash/src/server/routes/env-vars.mjs`);
72
+ // Force store re-init
73
+ const { resetStore } = await import(`${REPO}/bizar-dash/src/server/routes/env-vars.mjs`);
74
+ resetStore();
75
+ app = express();
76
+ app.use(express.json());
77
+ app.use('/api', createEnvVarsRouter());
78
+ // Clear test keys from process.env
79
+ for (const k of Object.keys(process.env)) {
80
+ if (k.startsWith('BIZAR_EXT_')) delete process.env[k];
81
+ }
82
+ });
83
+
84
+ afterEach(() => {
85
+ for (const k of Object.keys(process.env)) {
86
+ if (k.startsWith('BIZAR_EXT_')) delete process.env[k];
87
+ }
88
+ });
89
+
90
+ // ─── POST /api/env-vars/bulk-import ───────────────────────────────────────
91
+
92
+ it('bulk-import parses KEY=value lines', async () => {
93
+ const res = await request(app, 'POST', '/api/env-vars/bulk-import', {
94
+ envContent: 'BIZAR_EXT_A=value1\nBIZAR_EXT_B=value2',
95
+ });
96
+ assert.equal(res.status, 200);
97
+ assert.equal(res.body.ok, true);
98
+ assert.equal(res.body.imported, 2);
99
+ assert.equal(res.body.skipped, 0);
100
+ assert.deepEqual(res.body.errors, []);
101
+ });
102
+
103
+ it('bulk-import skips existing keys', async () => {
104
+ // Pre-create one
105
+ const create = await request(app, 'POST', '/api/env-vars', {
106
+ name: 'BIZAR_EXT_EXISTING', value: 'original',
107
+ });
108
+ assert.equal(create.status, 201);
109
+
110
+ const bulk = await request(app, 'POST', '/api/env-vars/bulk-import', {
111
+ envContent: 'BIZAR_EXT_EXISTING=ignored\nBIZAR_EXT_NEWBULK=fresh',
112
+ });
113
+ assert.equal(bulk.body.imported, 1, 'should import only the new key');
114
+ assert.equal(bulk.body.skipped, 1, 'should skip the existing key');
115
+ assert.ok(bulk.body.errors.some((e) => e.includes('Already exists')));
116
+ });
117
+
118
+ it('bulk-import reports invalid lines in errors', async () => {
119
+ const res = await request(app, 'POST', '/api/env-vars/bulk-import', {
120
+ envContent: 'BIZAR_ext_invalid=value\nBIZAR_EXT_VALID=good\nBIZAR_EXT_NOEquals\nBOGUS_PREFIX=bad',
121
+ });
122
+ assert.equal(res.body.imported, 1, 'only valid entries are imported');
123
+ assert.ok(res.body.errors.length >= 2, 'should have errors for invalid lines');
124
+ });
125
+
126
+ it('bulk-import skips blank lines and comments', async () => {
127
+ const res = await request(app, 'POST', '/api/env-vars/bulk-import', {
128
+ envContent: '# comment\n\n \nBIZAR_EXT_TRIMMED=value',
129
+ });
130
+ assert.equal(res.body.imported, 1);
131
+ assert.equal(res.body.skipped, 3, 'comment and two blank/whitespace lines are skipped');
132
+ });
133
+
134
+ it('bulk-import requires envContent string', async () => {
135
+ const res = await request(app, 'POST', '/api/env-vars/bulk-import', {});
136
+ assert.equal(res.status, 400);
137
+ assert.equal(res.body.error, 'envContent_required');
138
+ });
139
+
140
+ // ─── GET /api/env-vars/export ─────────────────────────────────────────────
141
+
142
+ it('export produces valid .env format', async () => {
143
+ await request(app, 'POST', '/api/env-vars', { name: 'BIZAR_EXT_EXPORT1', value: 'val-one' });
144
+ await request(app, 'POST', '/api/env-vars', { name: 'BIZAR_EXT_EXPORT2', value: 'val-two' });
145
+
146
+ const res = await request(app, 'GET', '/api/env-vars/export');
147
+ assert.equal(res.status, 200);
148
+ assert.ok(res.headers['content-type']?.includes('text/plain'), 'should be text/plain');
149
+ assert.ok(res.headers['content-disposition']?.includes('.env'), 'should have .env content-disposition');
150
+ const lines = res.raw.split('\n').filter(Boolean);
151
+ assert.ok(lines.some((l) => l === 'BIZAR_EXT_EXPORT1=val-one'));
152
+ assert.ok(lines.some((l) => l === 'BIZAR_EXT_EXPORT2=val-two'));
153
+ });
154
+
155
+ it('export returns empty string when no vars', async () => {
156
+ const res = await request(app, 'GET', '/api/env-vars/export');
157
+ assert.equal(res.status, 200);
158
+ assert.equal(res.raw.trim(), '');
159
+ });
160
+
161
+ // ─── GET /api/env-vars/grouped ───────────────────────────────────────────
162
+
163
+ it('grouping splits vars by prefix', async () => {
164
+ // All vars must start with BIZAR_ per NAME_RE
165
+ // Grouping is by the "prefix" after BIZAR_ (e.g. EXT_, PROVIDER_, MODEL_)
166
+ await request(app, 'POST', '/api/env-vars', { name: 'BIZAR_EXT_A', value: 'v1' });
167
+ await request(app, 'POST', '/api/env-vars', { name: 'BIZAR_EXT_B', value: 'v2' });
168
+ await request(app, 'POST', '/api/env-vars', { name: 'BIZAR_PROVIDER_KEY', value: 'pv' });
169
+ await request(app, 'POST', '/api/env-vars', { name: 'BIZAR_MODEL_SETTING', value: 'ms' });
170
+
171
+ const res = await request(app, 'GET', '/api/env-vars/grouped');
172
+ assert.equal(res.status, 200);
173
+ const groups = res.body;
174
+ const groupKeys = Object.keys(groups).sort();
175
+ // BIZAR_EXT_* -> EXT_ prefix, BIZAR_PROVIDER_* -> PROVIDER_, BIZAR_MODEL_* -> MODEL_
176
+ assert.ok(groupKeys.includes('EXT_'), `should have EXT_ group, got: ${groupKeys.join(',')}`);
177
+ assert.ok(groupKeys.includes('PROVIDER_'), `should have PROVIDER_ group, got: ${groupKeys.join(',')}`);
178
+ assert.ok(groupKeys.includes('MODEL_'), `should have MODEL_ group, got: ${groupKeys.join(',')}`);
179
+ assert.equal(groups['EXT_'].length, 2, 'BIZAR_EXT_* vars');
180
+ assert.equal(groups['PROVIDER_'].length, 1);
181
+ assert.equal(groups['MODEL_'].length, 1);
182
+ assert.ok(groups['EXT_'][0].value.includes('*'), 'values should be masked');
183
+ });
184
+
185
+ it('grouped returns empty object when no vars', async () => {
186
+ const res = await request(app, 'GET', '/api/env-vars/grouped');
187
+ assert.equal(res.status, 200);
188
+ assert.deepEqual(res.body, {});
189
+ });
190
+ });
@@ -0,0 +1,141 @@
1
+ /**
2
+ * tests/eval/fixtures.test.mjs
3
+ *
4
+ * v5.0.0 — Tests for fixture loading.
5
+ *
6
+ * Verifies:
7
+ * - Example fixtures load without errors
8
+ * - Fixture JSON is valid and has required fields
9
+ * - loadFixtures correctly loads from a directory
10
+ */
11
+
12
+ import { describe, it } from 'node:test';
13
+ import assert from 'node:assert';
14
+ import { join, dirname } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const EVAL = await import('../../src/server/eval.mjs');
19
+
20
+ // Fixture files to validate
21
+ const EXAMPLE_FIXTURES = [
22
+ '../../../templates/eval-fixtures/code-search-basic.json',
23
+ '../../../templates/eval-fixtures/tool-call-correctness.json',
24
+ '../../../templates/eval-fixtures/response-format.json',
25
+ '../../../templates/eval-fixtures/latency-bounds.json',
26
+ '../../../templates/eval-fixtures/regression-suite.json',
27
+ ];
28
+
29
+ const REQUIRED_FIELDS = ['id', 'name', 'description', 'agent', 'prompt', 'expected'];
30
+ const REQUIRED_EXPECTED = ['contains', 'notContains', 'regex', 'jsonSchema', 'maxTokens', 'maxLatencyMs'];
31
+
32
+ describe('eval fixtures', () => {
33
+
34
+ describe('example fixtures from templates/', () => {
35
+
36
+ for (const fixturePath of EXAMPLE_FIXTURES) {
37
+ const fullPath = join(__dirname, fixturePath);
38
+ it(`loads and validates: ${fixturePath}`, async () => {
39
+ // Dynamically import so we can check it parses
40
+ const { readFileSync } = await import('node:fs');
41
+ const raw = readFileSync(fullPath, 'utf8');
42
+ const fixture = JSON.parse(raw);
43
+
44
+ // Should have id
45
+ assert.ok(fixture.id, 'fixture must have an id');
46
+
47
+ // If it's a suite with fixtures array, validate each
48
+ if (fixture.fixtures && Array.isArray(fixture.fixtures)) {
49
+ assert.ok(fixture.fixtures.length > 0, 'suite must have at least one fixture');
50
+ for (const f of fixture.fixtures) {
51
+ validateFixture(f);
52
+ }
53
+ } else {
54
+ // Single fixture
55
+ validateFixture(fixture);
56
+ }
57
+ });
58
+ }
59
+ });
60
+
61
+ describe('loadFixtures', () => {
62
+ it('loads fixtures from a directory', async () => {
63
+ const fixturesDir = join(__dirname, '../../../templates/eval-fixtures');
64
+ const fixtures = EVAL.loadFixtures(fixturesDir);
65
+ assert.ok(fixtures.length >= 4, `Expected at least 4 fixtures, got ${fixtures.length}`);
66
+ });
67
+
68
+ it('skips non-JSON files', async () => {
69
+ const { mkdirSync, writeFileSync, rmSync } = await import('node:fs');
70
+ const { tmpdir } = await import('node:os');
71
+ const testDir = join(tmpdir(), `eval-load-test-${Date.now()}`);
72
+ mkdirSync(testDir, { recursive: true });
73
+ writeFileSync(join(testDir, 'readme.md'), '# readme');
74
+ writeFileSync(join(testDir, 'valid.json'), JSON.stringify({
75
+ id: 'test',
76
+ name: 'Test',
77
+ description: 'Test',
78
+ agent: 'thor',
79
+ prompt: 'test',
80
+ expected: { contains: [] },
81
+ }));
82
+ const fixtures = EVAL.loadFixtures(testDir);
83
+ rmSync(testDir, { recursive: true, force: true });
84
+ assert.strictEqual(fixtures.length, 1);
85
+ assert.strictEqual(fixtures[0].id, 'test');
86
+ });
87
+
88
+ it('skips invalid JSON files', async () => {
89
+ const { mkdirSync, writeFileSync, rmSync } = await import('node:fs');
90
+ const { tmpdir } = await import('node:os');
91
+ const testDir = join(tmpdir(), `eval-invalid-test-${Date.now()}`);
92
+ mkdirSync(testDir, { recursive: true });
93
+ writeFileSync(join(testDir, 'invalid.json'), 'not valid json {');
94
+ writeFileSync(join(testDir, 'valid.json'), JSON.stringify({
95
+ id: 'test2',
96
+ name: 'Test2',
97
+ description: 'Test2',
98
+ agent: 'thor',
99
+ prompt: 'test',
100
+ expected: { contains: [] },
101
+ }));
102
+ const fixtures = EVAL.loadFixtures(testDir);
103
+ rmSync(testDir, { recursive: true, force: true });
104
+ assert.strictEqual(fixtures.length, 1);
105
+ assert.strictEqual(fixtures[0].id, 'test2');
106
+ });
107
+
108
+ it('returns empty array for non-existent directory', async () => {
109
+ const fixtures = EVAL.loadFixtures('/nonexistent/path/xyz');
110
+ assert.strictEqual(fixtures.length, 0);
111
+ });
112
+ });
113
+ });
114
+
115
+ /**
116
+ * Validate a fixture object has required fields
117
+ * @param {object} fixture
118
+ */
119
+ function validateFixture(fixture) {
120
+ for (const field of REQUIRED_FIELDS) {
121
+ assert.ok(
122
+ fixture[field] !== undefined && fixture[field] !== null,
123
+ `fixture ${fixture.id || 'unknown'} missing required field: ${field}`,
124
+ );
125
+ }
126
+ assert.ok(
127
+ typeof fixture.expected === 'object',
128
+ `fixture ${fixture.id} expected must be an object`,
129
+ );
130
+ // expected should have at least one check type
131
+ const expected = fixture.expected;
132
+ const hasCheckType = [
133
+ expected.contains,
134
+ expected.notContains,
135
+ expected.regex,
136
+ expected.jsonSchema,
137
+ expected.maxTokens,
138
+ expected.maxLatencyMs,
139
+ ].some((v) => v !== undefined && v !== null);
140
+ assert.ok(hasCheckType, `fixture ${fixture.id} expected must have at least one check type`);
141
+ }