@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.
- package/bizar-dash/dist/assets/icons-Bo0iH9EC.js +701 -0
- package/bizar-dash/dist/assets/icons-Bo0iH9EC.js.map +1 -0
- package/bizar-dash/dist/assets/{index-DU61awG3.js → index-DmpSFPJY.js} +1 -1
- package/bizar-dash/dist/assets/{index-DU61awG3.js.map → index-DmpSFPJY.js.map} +1 -1
- package/bizar-dash/dist/assets/main-C1cpttnv.js +19 -0
- package/bizar-dash/dist/assets/main-C1cpttnv.js.map +1 -0
- package/bizar-dash/dist/assets/main-DTkNlLrw.css +1 -0
- package/bizar-dash/dist/assets/markdown-DIquRulQ.js +29 -0
- package/bizar-dash/dist/assets/markdown-DIquRulQ.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-BtxQJftK.js +2 -0
- package/bizar-dash/dist/assets/mobile-BtxQJftK.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-DD-FZrTC.js +1 -0
- package/bizar-dash/dist/assets/{mobile-CL5uUQEC.js.map → mobile-DD-FZrTC.js.map} +1 -1
- package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js +40 -0
- package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js.map +1 -0
- package/bizar-dash/dist/index.html +6 -3
- package/bizar-dash/dist/mobile.html +5 -2
- package/bizar-dash/node_modules/.package-lock.json +6 -0
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/package-lock.json +6 -0
- package/bizar-dash/skills/eval/SKILL.md +237 -0
- package/bizar-dash/src/server/api.mjs +35 -0
- package/bizar-dash/src/server/auth.mjs +155 -1
- package/bizar-dash/src/server/diagnostics-store.mjs +452 -2
- package/bizar-dash/src/server/eval-store.mjs +226 -0
- package/bizar-dash/src/server/eval.mjs +347 -0
- package/bizar-dash/src/server/memory-store.mjs +46 -0
- package/bizar-dash/src/server/ocr.mjs +55 -0
- package/bizar-dash/src/server/plugins/registry.mjs +363 -0
- package/bizar-dash/src/server/plugins/sandbox.mjs +655 -0
- package/bizar-dash/src/server/plugins/store.mjs +659 -0
- package/bizar-dash/src/server/providers-store.mjs +11 -4
- package/bizar-dash/src/server/routes/_shared.mjs +2 -2
- package/bizar-dash/src/server/routes/clipboard.mjs +173 -0
- package/bizar-dash/src/server/routes/doctor.mjs +71 -0
- package/bizar-dash/src/server/routes/env-vars.mjs +67 -1
- package/bizar-dash/src/server/routes/eval.mjs +147 -0
- package/bizar-dash/src/server/routes/memory.mjs +3 -0
- package/bizar-dash/src/server/routes/ocr.mjs +182 -0
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +61 -42
- package/bizar-dash/src/server/routes/plugins.mjs +220 -0
- package/bizar-dash/src/server/routes/schedules.mjs +55 -0
- package/bizar-dash/src/server/routes/users.mjs +84 -0
- package/bizar-dash/src/server/routes/voice.mjs +131 -0
- package/bizar-dash/src/server/routes/workspaces.mjs +204 -0
- package/bizar-dash/src/server/serve-info.mjs +172 -0
- package/bizar-dash/src/server/voice-store.mjs +202 -0
- package/bizar-dash/src/server/voice-transcribe.mjs +72 -0
- package/bizar-dash/src/server/workspaces.mjs +626 -0
- package/bizar-dash/src/web/App.tsx +48 -11
- package/bizar-dash/src/web/components/AutosaveField.tsx +50 -0
- package/bizar-dash/src/web/components/DoctorPanel.tsx +160 -0
- package/bizar-dash/src/web/components/EnvVarManager.tsx +245 -62
- package/bizar-dash/src/web/components/InviteDialog.tsx +205 -0
- package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +71 -0
- package/bizar-dash/src/web/components/ScreenshotCapture.tsx +138 -0
- package/bizar-dash/src/web/components/ScreenshotOCR.tsx +42 -0
- package/bizar-dash/src/web/components/SettingsNav.tsx +101 -0
- package/bizar-dash/src/web/components/Sidebar.tsx +25 -1
- package/bizar-dash/src/web/components/StatusBadge.tsx +33 -2
- package/bizar-dash/src/web/components/Topbar.tsx +11 -0
- package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +182 -0
- package/bizar-dash/src/web/components/VoiceRecorder.tsx +104 -0
- package/bizar-dash/src/web/components/WorkspaceSelector.tsx +158 -0
- package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +64 -0
- package/bizar-dash/src/web/components/chat/Composer.tsx +1 -1
- package/bizar-dash/src/web/components/chat/useChat.ts +118 -1
- package/bizar-dash/src/web/hooks/useAutosave.ts +107 -0
- package/bizar-dash/src/web/lib/types.ts +134 -0
- package/bizar-dash/src/web/styles/chat.css +8 -5
- package/bizar-dash/src/web/styles/main.css +77 -2
- package/bizar-dash/src/web/styles/memory.css +82 -0
- package/bizar-dash/src/web/styles/settings.css +265 -0
- package/bizar-dash/src/web/views/Chat.tsx +15 -1
- package/bizar-dash/src/web/views/Doctor.tsx +317 -0
- package/bizar-dash/src/web/views/Memory.tsx +17 -2
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +1 -0
- package/bizar-dash/src/web/views/Schedules.tsx +94 -12
- package/bizar-dash/src/web/views/Settings.tsx +78 -45
- package/bizar-dash/src/web/views/Workspace.tsx +294 -0
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +114 -133
- package/bizar-dash/src/web/views/memory/FromScreenshotPanel.tsx +23 -0
- package/bizar-dash/src/web/views/memory/VaultFromClipboardPanel.tsx +101 -0
- package/bizar-dash/src/web/views/settings/AgentSection.tsx +23 -12
- package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +5 -6
- package/bizar-dash/src/web/views/settings/GeneralSection.tsx +38 -15
- package/bizar-dash/src/web/views/settings/MemorySection.tsx +92 -8
- package/bizar-dash/src/web/views/settings/WorkspacesSection.tsx +189 -0
- package/bizar-dash/tests/autosave.test.tsx +276 -0
- package/bizar-dash/tests/bundle-analysis.test.mjs +5 -2
- package/bizar-dash/tests/chat-composer.test.tsx +140 -0
- package/bizar-dash/tests/clipboard.test.mjs +147 -0
- package/bizar-dash/tests/components/doctor-panel.test.tsx +105 -0
- package/bizar-dash/tests/components/screenshot-ocr.test.tsx +75 -0
- package/bizar-dash/tests/components/workspace-selector.test.tsx +73 -0
- package/bizar-dash/tests/deploy-templates.test.mjs +100 -0
- package/bizar-dash/tests/diagnostics-store.test.mjs +206 -0
- package/bizar-dash/tests/env-vars-extended.test.mjs +190 -0
- package/bizar-dash/tests/eval/fixtures.test.mjs +141 -0
- package/bizar-dash/tests/eval/report.test.mjs +284 -0
- package/bizar-dash/tests/eval/runner.test.mjs +471 -0
- package/bizar-dash/tests/layout-spacing.test.mjs +101 -0
- package/bizar-dash/tests/memory-default-vault.test.mjs +98 -0
- package/bizar-dash/tests/minimax-bar.test.tsx +113 -0
- package/bizar-dash/tests/minimax-models.test.mjs +123 -0
- package/bizar-dash/tests/ocr.test.mjs +87 -0
- package/bizar-dash/tests/opencode-sessions-detail.test.mjs +158 -0
- package/bizar-dash/tests/plugins-registry.test.mjs +387 -0
- package/bizar-dash/tests/plugins-sandbox.test.mjs +374 -0
- package/bizar-dash/tests/plugins-store.test.mjs +455 -0
- package/bizar-dash/tests/routes-doctor.test.mjs +159 -0
- package/bizar-dash/tests/schedules-templates.test.mjs +144 -0
- package/bizar-dash/tests/settings-layout.test.tsx +129 -0
- package/bizar-dash/tests/settings-mode-wiring.test.tsx +151 -0
- package/bizar-dash/tests/settings-nav.test.tsx +126 -0
- package/bizar-dash/tests/users.test.mjs +108 -0
- package/bizar-dash/tests/voice-recorder.test.tsx +95 -0
- package/bizar-dash/tests/voice-store.test.mjs +148 -0
- package/bizar-dash/tests/voice-transcribe.test.mjs +87 -0
- package/bizar-dash/tests/workspaces.test.mjs +527 -0
- package/cli/bin.mjs +72 -2
- package/cli/commands/clip.mjs +146 -0
- package/cli/commands/deploy/cloudflare.mjs +250 -0
- package/cli/commands/deploy/docker.mjs +221 -0
- package/cli/commands/deploy/fly.mjs +161 -0
- package/cli/commands/deploy/vercel.mjs +225 -0
- package/cli/commands/deploy.mjs +240 -0
- package/cli/commands/eval.mjs +378 -0
- package/cli/commands/marketplace.mjs +64 -0
- package/cli/commands/minimax.mjs +5 -2
- package/cli/commands/ocr.mjs +165 -0
- package/cli/commands/plugin.mjs +358 -0
- package/cli/commands/voice.mjs +211 -0
- package/cli/commands/workspace.mjs +247 -0
- package/cli/tests/minimax-cli.test.mjs +79 -0
- package/config/agents/frigg.md +1 -1
- package/config/agents/heimdall.md +1 -1
- package/config/agents/mimir.md +1 -1
- package/config/agents/quick.md +1 -1
- package/config/agents/semble-search.md +1 -1
- package/config/agents/vor.md +1 -1
- package/config/opencode.json +28 -21
- package/config/opencode.json.template +8 -1
- package/package.json +12 -8
- package/plugins/bizar/index.ts +77 -0
- package/plugins/bizar/src/compaction.d.mts +48 -0
- package/plugins/bizar/src/compaction.mjs +192 -0
- package/plugins/bizar/tests/compaction.test.ts +264 -0
- package/templates/deploy/cloudflare/README.md +32 -0
- package/templates/deploy/cloudflare/functions-index.template.js +15 -0
- package/templates/deploy/cloudflare/wrangler.toml.template +9 -0
- package/templates/deploy/docker/.env.template +16 -0
- package/templates/deploy/docker/README.md +58 -0
- package/templates/deploy/docker/docker-compose.template.yml +23 -0
- package/templates/deploy/fly/README.md +35 -0
- package/templates/deploy/fly/fly.toml.template +28 -0
- package/templates/deploy/vercel/README.md +29 -0
- package/templates/deploy/vercel/api-index.template.js +18 -0
- package/templates/deploy/vercel/vercel.json.template +16 -0
- package/templates/eval-fixtures/README.md +58 -0
- package/templates/eval-fixtures/code-search-basic.json +28 -0
- package/templates/eval-fixtures/latency-bounds.json +16 -0
- package/templates/eval-fixtures/regression-suite.json +79 -0
- package/templates/eval-fixtures/response-format.json +30 -0
- package/templates/eval-fixtures/tool-call-correctness.json +24 -0
- package/templates/plugin-template/README.md +121 -0
- package/templates/plugin-template/index.js +66 -0
- package/templates/plugin-template/plugin.json +42 -0
- package/templates/plugin-template/tests/plugin.test.js +83 -0
- package/templates/schedules/daily-backup.json +12 -0
- package/templates/schedules/daily-cleanup.json +12 -0
- package/templates/schedules/hourly-health-check.json +12 -0
- package/templates/schedules/webhook-on-push.json +13 -0
- package/templates/schedules/weekly-digest.json +13 -0
- package/bizar-dash/dist/assets/main-DaC1Lc6q.js +0 -366
- package/bizar-dash/dist/assets/main-DaC1Lc6q.js.map +0 -1
- package/bizar-dash/dist/assets/main-DfmIfOUS.css +0 -1
- package/bizar-dash/dist/assets/mobile-CL5uUQEC.js +0 -1
- package/bizar-dash/dist/assets/mobile-D5WTWvuh.js +0 -338
- package/bizar-dash/dist/assets/mobile-D5WTWvuh.js.map +0 -1
- package/bizar-dash/src/web/views/settings/BackupSection.tsx +0 -16
- package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +0 -16
- 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
|
+
}
|