@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,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bizar-dash/tests/minimax-bar.test.ts
|
|
3
|
+
*
|
|
4
|
+
* Tests for MiniMax quota bar in the dashboard.
|
|
5
|
+
* Verifies bar width = consumedPct (not remainingPct).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { render, screen } from '@testing-library/react';
|
|
10
|
+
import React from 'react';
|
|
11
|
+
|
|
12
|
+
// ── Minimal QuotaBar clone for testing ──────────────────────────────────────
|
|
13
|
+
// This mirrors the logic in MiniMaxUsage.tsx QuotaBar component.
|
|
14
|
+
|
|
15
|
+
function clamp(n: number, lo: number, hi: number): number {
|
|
16
|
+
return Math.max(lo, Math.min(hi, n));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function QuotaBarTest({ remainingPct, consumedPct }: { remainingPct: number; consumedPct: number }) {
|
|
20
|
+
return (
|
|
21
|
+
<div data-testid="quota-row">
|
|
22
|
+
<div data-testid="remaining-label">{remainingPct}% remaining</div>
|
|
23
|
+
<div data-testid="consumed-label">· {consumedPct}% used</div>
|
|
24
|
+
<div
|
|
25
|
+
data-testid="bar-fill"
|
|
26
|
+
style={{ width: `${consumedPct}%` }}
|
|
27
|
+
aria-label={`${consumedPct}% consumed`}
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Tests ───────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
describe('QuotaBar width logic', () => {
|
|
36
|
+
|
|
37
|
+
it('bar width equals consumedPct, not remainingPct', () => {
|
|
38
|
+
const remainingPct = 91;
|
|
39
|
+
const consumedPct = clamp(100 - remainingPct, 0, 100);
|
|
40
|
+
|
|
41
|
+
render(<QuotaBarTest remainingPct={remainingPct} consumedPct={consumedPct} />);
|
|
42
|
+
|
|
43
|
+
const barFill = screen.getByTestId('bar-fill');
|
|
44
|
+
const styleWidth = barFill.getAttribute('style');
|
|
45
|
+
|
|
46
|
+
// Bar width should be 9% (consumed), not 91% (remaining)
|
|
47
|
+
expect(styleWidth).toMatch(/width: 9%/);
|
|
48
|
+
expect(barFill.getAttribute('aria-label')).toBe('9% consumed');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('100% remaining → 0% consumed → 0% bar width', () => {
|
|
52
|
+
const remainingPct = 100;
|
|
53
|
+
const consumedPct = clamp(100 - remainingPct, 0, 100);
|
|
54
|
+
|
|
55
|
+
render(<QuotaBarTest remainingPct={remainingPct} consumedPct={consumedPct} />);
|
|
56
|
+
|
|
57
|
+
const barFill = screen.getByTestId('bar-fill');
|
|
58
|
+
expect(barFill.getAttribute('style')).toMatch(/width: 0%/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('0% remaining → 100% consumed → 100% bar width', () => {
|
|
62
|
+
const remainingPct = 0;
|
|
63
|
+
const consumedPct = clamp(100 - remainingPct, 0, 100);
|
|
64
|
+
|
|
65
|
+
render(<QuotaBarTest remainingPct={remainingPct} consumedPct={consumedPct} />);
|
|
66
|
+
|
|
67
|
+
const barFill = screen.getByTestId('bar-fill');
|
|
68
|
+
expect(barFill.getAttribute('style')).toMatch(/width: 100%/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('75% remaining → 25% consumed → 25% bar width', () => {
|
|
72
|
+
const remainingPct = 75;
|
|
73
|
+
const consumedPct = clamp(100 - remainingPct, 0, 100);
|
|
74
|
+
|
|
75
|
+
render(<QuotaBarTest remainingPct={remainingPct} consumedPct={consumedPct} />);
|
|
76
|
+
|
|
77
|
+
const barFill = screen.getByTestId('bar-fill');
|
|
78
|
+
expect(barFill.getAttribute('style')).toMatch(/width: 25%/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('consumedPct = 100 - remainingPct', () => {
|
|
82
|
+
const testCases = [
|
|
83
|
+
{ remaining: 100, expectedConsumed: 0 },
|
|
84
|
+
{ remaining: 91, expectedConsumed: 9 },
|
|
85
|
+
{ remaining: 75, expectedConsumed: 25 },
|
|
86
|
+
{ remaining: 50, expectedConsumed: 50 },
|
|
87
|
+
{ remaining: 25, expectedConsumed: 75 },
|
|
88
|
+
{ remaining: 9, expectedConsumed: 91 },
|
|
89
|
+
{ remaining: 0, expectedConsumed: 100 },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
for (const { remaining, expectedConsumed } of testCases) {
|
|
93
|
+
const consumed = clamp(100 - remaining, 0, 100);
|
|
94
|
+
expect(consumed).toBe(expectedConsumed);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('clamp keeps values within 0-100 range', () => {
|
|
99
|
+
expect(clamp(-10, 0, 100)).toBe(0);
|
|
100
|
+
expect(clamp(110, 0, 100)).toBe(100);
|
|
101
|
+
expect(clamp(50, 0, 100)).toBe(50);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('labels show both remaining and consumed', () => {
|
|
105
|
+
const remainingPct = 91;
|
|
106
|
+
const consumedPct = clamp(100 - remainingPct, 0, 100);
|
|
107
|
+
|
|
108
|
+
render(<QuotaBarTest remainingPct={remainingPct} consumedPct={consumedPct} />);
|
|
109
|
+
|
|
110
|
+
expect(screen.getByTestId('remaining-label').textContent).toBe('91% remaining');
|
|
111
|
+
expect(screen.getByTestId('consumed-label').textContent).toBe('· 9% used');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* minimax-models.test.mjs — verify MiniMax is the default/primary provider
|
|
3
|
+
* and that the catalog, constants, and agent configs all agree.
|
|
4
|
+
*
|
|
5
|
+
* Run with: node --test tests/minimax-models.test.mjs
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, before } from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { resolve, join } from 'node:path';
|
|
10
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
|
|
13
|
+
const REPO = resolve(import.meta.dirname, '..', '..');
|
|
14
|
+
|
|
15
|
+
let mod;
|
|
16
|
+
let AGENTS_DIR;
|
|
17
|
+
|
|
18
|
+
before(async () => {
|
|
19
|
+
const cacheBust = '?cb=' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
|
20
|
+
mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + cacheBust);
|
|
21
|
+
AGENTS_DIR = join(REPO, 'config', 'agents');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('MiniMax model constants', () => {
|
|
25
|
+
it('MINIMAX_DEFAULT is MiniMax-M2.7', () => {
|
|
26
|
+
assert.equal(mod.MINIMAX_DEFAULT, 'MiniMax-M2.7');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('PROVIDER_CATALOG MiniMax entry', () => {
|
|
31
|
+
it('minimax is present in the catalog', () => {
|
|
32
|
+
const minimax = mod.PROVIDER_CATALOG.find((p) => p.id === 'minimax');
|
|
33
|
+
assert.ok(minimax, 'minimax provider not found in PROVIDER_CATALOG');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('has at least 4 models', () => {
|
|
37
|
+
const minimax = mod.PROVIDER_CATALOG.find((p) => p.id === 'minimax');
|
|
38
|
+
assert.ok(minimax.models.length >= 4,
|
|
39
|
+
`expected at least 4 MiniMax models, got ${minimax.models.length}`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('includes MiniMax-M2.7-Flash as recommended cheap model', () => {
|
|
43
|
+
const minimax = mod.PROVIDER_CATALOG.find((p) => p.id === 'minimax');
|
|
44
|
+
const flash = minimax.models.find((m) => {
|
|
45
|
+
const id = typeof m === 'string' ? m : m.id;
|
|
46
|
+
return id === 'MiniMax-M2.7-Flash';
|
|
47
|
+
});
|
|
48
|
+
assert.ok(flash, 'MiniMax-M2.7-Flash not found in catalog');
|
|
49
|
+
if (typeof flash === 'object') {
|
|
50
|
+
assert.equal(flash.tier, 'cheap');
|
|
51
|
+
assert.equal(flash.recommended, true);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('includes MiniMax-M2.7 (default cheap)', () => {
|
|
56
|
+
const minimax = mod.PROVIDER_CATALOG.find((p) => p.id === 'minimax');
|
|
57
|
+
const m27 = minimax.models.find((m) => {
|
|
58
|
+
const id = typeof m === 'string' ? m : m.id;
|
|
59
|
+
return id === 'MiniMax-M2.7';
|
|
60
|
+
});
|
|
61
|
+
assert.ok(m27, 'MiniMax-M2.7 not found in catalog');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('includes MiniMax-M3 (mid tier)', () => {
|
|
65
|
+
const minimax = mod.PROVIDER_CATALOG.find((p) => p.id === 'minimax');
|
|
66
|
+
const m3 = minimax.models.find((m) => {
|
|
67
|
+
const id = typeof m === 'string' ? m : m.id;
|
|
68
|
+
return id === 'MiniMax-M3';
|
|
69
|
+
});
|
|
70
|
+
assert.ok(m3, 'MiniMax-M3 not found in catalog');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('includes MiniMax-M3-Reasoning (premium)', () => {
|
|
74
|
+
const minimax = mod.PROVIDER_CATALOG.find((p) => p.id === 'minimax');
|
|
75
|
+
const m3r = minimax.models.find((m) => {
|
|
76
|
+
const id = typeof m === 'string' ? m : m.id;
|
|
77
|
+
return id === 'MiniMax-M3-Reasoning';
|
|
78
|
+
});
|
|
79
|
+
assert.ok(m3r, 'MiniMax-M3-Reasoning not found in catalog');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('Agent .md files use MiniMax models', () => {
|
|
84
|
+
// Agents that should be on minimax/MiniMax-M2.7
|
|
85
|
+
const m27Agents = ['frigg', 'heimdall', 'mimir', 'vor', 'semble-search', 'hermod', 'thor', 'baldr'];
|
|
86
|
+
// Agents that should be on minimax/MiniMax-M3
|
|
87
|
+
const m3Agents = ['tyr', 'vidarr', 'forseti', 'odin'];
|
|
88
|
+
// Agents that should be on minimax/MiniMax-M2.7-Flash
|
|
89
|
+
const flashAgents = ['quick'];
|
|
90
|
+
|
|
91
|
+
for (const agent of m27Agents) {
|
|
92
|
+
it(`${agent} uses minimax/MiniMax-M2.7`, () => {
|
|
93
|
+
const file = join(AGENTS_DIR, `${agent}.md`);
|
|
94
|
+
if (!existsSync(file)) { assert.fail(`agent file not found: ${file}`); }
|
|
95
|
+
const raw = readFileSync(file, 'utf8');
|
|
96
|
+
const match = raw.match(/^model:\s*([^\s#]+)/m);
|
|
97
|
+
assert.ok(match, `${agent}.md has no model: line`);
|
|
98
|
+
assert.equal(match[1], 'minimax/MiniMax-M2.7', `${agent} should use MiniMax-M2.7, got ${match[1]}`);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const agent of m3Agents) {
|
|
103
|
+
it(`${agent} uses minimax/MiniMax-M3`, () => {
|
|
104
|
+
const file = join(AGENTS_DIR, `${agent}.md`);
|
|
105
|
+
if (!existsSync(file)) { assert.fail(`agent file not found: ${file}`); }
|
|
106
|
+
const raw = readFileSync(file, 'utf8');
|
|
107
|
+
const match = raw.match(/^model:\s*([^\s#]+)/m);
|
|
108
|
+
assert.ok(match, `${agent}.md has no model: line`);
|
|
109
|
+
assert.equal(match[1], 'minimax/MiniMax-M3', `${agent} should use MiniMax-M3, got ${match[1]}`);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const agent of flashAgents) {
|
|
114
|
+
it(`${agent} uses minimax/MiniMax-M2.7-Flash`, () => {
|
|
115
|
+
const file = join(AGENTS_DIR, `${agent}.md`);
|
|
116
|
+
if (!existsSync(file)) { assert.fail(`agent file not found: ${file}`); }
|
|
117
|
+
const raw = readFileSync(file, 'utf8');
|
|
118
|
+
const match = raw.match(/^model:\s*([^\s#]+)/m);
|
|
119
|
+
assert.ok(match, `${agent}.md has no model: line`);
|
|
120
|
+
assert.equal(match[1], 'minimax/MiniMax-M2.7-Flash', `${agent} should use MiniMax-M2.7-Flash, got ${match[1]}`);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/ocr.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.0.0 — Tests for the OCR helper module and routes.
|
|
5
|
+
*
|
|
6
|
+
* The extractText tests require tesseract.js to be installed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, beforeEach, afterEach, before } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { mkdirSync, rmSync } from 'node:fs';
|
|
14
|
+
|
|
15
|
+
// ── Test OCR helper ────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
describe('ocr.extractText', () => {
|
|
18
|
+
let ocrModule;
|
|
19
|
+
|
|
20
|
+
before(async () => {
|
|
21
|
+
try {
|
|
22
|
+
ocrModule = await import('../src/server/ocr.mjs');
|
|
23
|
+
} catch {
|
|
24
|
+
// tesseract.js or its deps not available
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('extractText returns a string for a valid PNG buffer', { skip: !ocrModule }, async () => {
|
|
29
|
+
const pngBuffer = Buffer.from(
|
|
30
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
|
31
|
+
'base64',
|
|
32
|
+
);
|
|
33
|
+
const text = await ocrModule.extractText(pngBuffer);
|
|
34
|
+
assert.strictEqual(typeof text, 'string');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('checkOcrHealth returns ok or error message', { skip: !ocrModule }, async () => {
|
|
38
|
+
const health = await ocrModule.checkOcrHealth();
|
|
39
|
+
assert.ok(typeof health.ok === 'boolean');
|
|
40
|
+
assert.ok(typeof health.message === 'string');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ── Test OCR routes ────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
describe('ocr routes', () => {
|
|
47
|
+
let projectRoot;
|
|
48
|
+
let server;
|
|
49
|
+
let baseUrl;
|
|
50
|
+
let ocrRouterModule;
|
|
51
|
+
|
|
52
|
+
before(async () => {
|
|
53
|
+
ocrRouterModule = await import('../src/server/routes/ocr.mjs');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
beforeEach(async () => {
|
|
57
|
+
projectRoot = join(tmpdir(), `bizar-ocr-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
58
|
+
mkdirSync(projectRoot, { recursive: true });
|
|
59
|
+
mkdirSync(join(projectRoot, '.bizar'), { recursive: true });
|
|
60
|
+
|
|
61
|
+
const express = (await import('express')).default;
|
|
62
|
+
const router = await ocrRouterModule.createOcrRouter({ projectRoot });
|
|
63
|
+
const app = express();
|
|
64
|
+
app.use(express.json({ limit: '25mb' }));
|
|
65
|
+
app.use('/api', router);
|
|
66
|
+
|
|
67
|
+
await new Promise((resolve) => {
|
|
68
|
+
server = app.listen(0, () => {
|
|
69
|
+
const addr = server.address();
|
|
70
|
+
baseUrl = `http://127.0.0.1:${addr.port}`;
|
|
71
|
+
resolve();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
try { server?.close?.(); } catch { /* ignore */ }
|
|
78
|
+
try { rmSync(projectRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('GET /api/ocr/list returns entries array', async () => {
|
|
82
|
+
const r = await fetch(`${baseUrl}/api/ocr/list`);
|
|
83
|
+
assert.strictEqual(r.status, 200);
|
|
84
|
+
const data = await r.json();
|
|
85
|
+
assert.ok(Array.isArray(data.entries));
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -64,8 +64,32 @@ function startUpstream() {
|
|
|
64
64
|
res.end(JSON.stringify({ data: [{ id: 'sess-1', title: 'Test', time: { created: 1, updated: 2, archived: null }, location: { directory: '/tmp/session-worktree' } }] }));
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
+
// v5.0.0 — bug #4: per-session probe used by `worktreeHasSession`
|
|
68
|
+
// (resolveSessionDirectory fast-path). Matches `/api/session/{id}`
|
|
69
|
+
// (no `/message` or `/prompt` suffix). 200 when the session
|
|
70
|
+
// belongs to the directory in the query string; 404 otherwise.
|
|
71
|
+
const probeM = url.pathname.match(/^\/api\/session\/([^/]+)$/);
|
|
72
|
+
if (probeM && req.method === 'GET') {
|
|
73
|
+
const dir = url.searchParams.get('directory') || '';
|
|
74
|
+
// The session "lives" in /tmp/session-worktree (matches the
|
|
75
|
+
// recorded worktree in writeServeJson). All other directories
|
|
76
|
+
// 404 so we can test the fallback path.
|
|
77
|
+
if (dir === '/tmp/session-worktree') {
|
|
78
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
79
|
+
res.end(JSON.stringify({ id: probeM[1] }));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
83
|
+
res.end(JSON.stringify({ error: 'session not found in directory' }));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
67
86
|
const msgM = url.pathname.match(/^\/api\/session\/([^/]+)\/message$/);
|
|
68
87
|
if (msgM && req.method === 'GET') {
|
|
88
|
+
if (msgM[1] === 'sess-upstream-500') {
|
|
89
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
90
|
+
res.end(JSON.stringify({ error: 'internal server error for testing' }));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
69
93
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
70
94
|
res.end(JSON.stringify({ data: [
|
|
71
95
|
{ info: { id: 'm1', role: 'user', time: { created: 1000 } }, parts: [{ type: 'text', text: 'hello' }] },
|
|
@@ -128,6 +152,10 @@ function writeServeJson(worktree) {
|
|
|
128
152
|
writeFileSync(SERVE_JSON_PATH, JSON.stringify({ port: upstreamPort, password: 'test-pw', worktree, pid: 99999, startedAt: Date.now() }), 'utf8');
|
|
129
153
|
}
|
|
130
154
|
|
|
155
|
+
function writeServeJsonWithoutWorktree() {
|
|
156
|
+
writeFileSync(SERVE_JSON_PATH, JSON.stringify({ port: upstreamPort, password: 'test-pw', pid: 99999, startedAt: Date.now() }), 'utf8');
|
|
157
|
+
}
|
|
158
|
+
|
|
131
159
|
/** Move serve.json aside so readServeInfo() returns null.
|
|
132
160
|
* Uses copy + unlink because rename across filesystems (e.g.
|
|
133
161
|
* /home/* → /tmp/*) throws EXDEV on Linux. */
|
|
@@ -273,4 +301,134 @@ test('SSE stream unwraps sync envelopes and strips version suffix', async () =>
|
|
|
273
301
|
const d = JSON.parse(events[0].data);
|
|
274
302
|
assert.equal(d.sessionID, 'our-id');
|
|
275
303
|
assert.equal(d.messageID, 'm3');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// ── Error handling tests ─────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
test('GET messages returns 503 directory_unknown when worktree missing and session not found', async () => {
|
|
309
|
+
writeServeJsonWithoutWorktree();
|
|
310
|
+
try {
|
|
311
|
+
// Request a non-existent session ID so listOpencodeSessions doesn't find it,
|
|
312
|
+
// and with no worktree in serve.json, resolveSessionDirectory returns null.
|
|
313
|
+
const res = await fetch(`${dashboardBaseUrl}/api/opencode-sessions/no-such-session/messages`);
|
|
314
|
+
assert.equal(res.status, 503);
|
|
315
|
+
const body = await res.json();
|
|
316
|
+
assert.equal(body.error, 'directory_unknown');
|
|
317
|
+
assert.ok(body.message.length > 0);
|
|
318
|
+
assert.ok(typeof body.suggestion === 'string');
|
|
319
|
+
} finally {
|
|
320
|
+
// Restore worktree for subsequent tests.
|
|
321
|
+
writeServeJson('/tmp/session-worktree');
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('GET messages returns 502 opencode_error when upstream returns 500', async () => {
|
|
326
|
+
writeServeJson('/tmp/session-worktree');
|
|
327
|
+
const res = await fetch(`${dashboardBaseUrl}/api/opencode-sessions/sess-upstream-500/messages`);
|
|
328
|
+
assert.equal(res.status, 502);
|
|
329
|
+
const body = await res.json();
|
|
330
|
+
assert.equal(body.error, 'opencode_error');
|
|
331
|
+
assert.ok(body.message.length > 0);
|
|
332
|
+
assert.ok(body.cause === 'unknown' || typeof body.cause === 'string');
|
|
333
|
+
assert.ok(typeof body.suggestion === 'string');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('GET messages uses suggestion field in error responses', async () => {
|
|
337
|
+
// Test that the plugin_offline response includes suggestion.
|
|
338
|
+
moveServeJsonAside();
|
|
339
|
+
try {
|
|
340
|
+
const res = await fetch(`${dashboardBaseUrl}/api/opencode-sessions/sess-1/messages`);
|
|
341
|
+
assert.equal(res.status, 503);
|
|
342
|
+
const body = await res.json();
|
|
343
|
+
assert.equal(body.error, 'plugin_offline');
|
|
344
|
+
assert.ok(typeof body.suggestion === 'string');
|
|
345
|
+
assert.ok(body.suggestion.length > 0);
|
|
346
|
+
} finally {
|
|
347
|
+
// Restore so the after() cleanup finds the file.
|
|
348
|
+
writeServeJson('/tmp/session-worktree');
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// ── v5.0.0 — bug #4 — structured error envelopes + worktree fast-path ──
|
|
353
|
+
|
|
354
|
+
test('GET messages 502 envelope includes cause/status/suggestion on upstream failure', async () => {
|
|
355
|
+
// Issue: bug #4 — the user previously saw the raw error string
|
|
356
|
+
// ("listMessages network error: fetch failed") with no structured
|
|
357
|
+
// fields. The fix is to surface `cause`, `status`, and `suggestion`
|
|
358
|
+
// so the front-end ChatInfoPanel can render actionable UI.
|
|
359
|
+
writeServeJson('/tmp/session-worktree');
|
|
360
|
+
const res = await fetch(
|
|
361
|
+
`${dashboardBaseUrl}/api/opencode-sessions/sess-upstream-500/messages`,
|
|
362
|
+
);
|
|
363
|
+
assert.equal(res.status, 502);
|
|
364
|
+
const body = await res.json();
|
|
365
|
+
assert.equal(body.error, 'opencode_error');
|
|
366
|
+
assert.equal(typeof body.message, 'string');
|
|
367
|
+
assert.ok(body.message.length > 0);
|
|
368
|
+
// Upstream 500 → cause stays 'unknown' (no network code), and the
|
|
369
|
+
// route forwards the upstream's HTTP status so the operator can
|
|
370
|
+
// tell whether it's opencode that's broken vs. the dashboard.
|
|
371
|
+
assert.ok(typeof body.cause === 'string');
|
|
372
|
+
assert.equal(body.status, 500);
|
|
373
|
+
assert.ok(typeof body.suggestion === 'string');
|
|
374
|
+
assert.ok(body.suggestion.length > 0);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test('GET messages 503 plugin_offline envelope includes suggestion (operator guidance)', async () => {
|
|
378
|
+
// Defensive: confirm the 503 path also carries a suggestion (some
|
|
379
|
+
// consumers — including the structured-error path in useChat.ts —
|
|
380
|
+
// branch on `body.code === 'plugin_offline'`).
|
|
381
|
+
moveServeJsonAside();
|
|
382
|
+
try {
|
|
383
|
+
const res = await fetch(
|
|
384
|
+
`${dashboardBaseUrl}/api/opencode-sessions/sess-1/messages`,
|
|
385
|
+
);
|
|
386
|
+
assert.equal(res.status, 503);
|
|
387
|
+
const body = await res.json();
|
|
388
|
+
assert.equal(body.error, 'plugin_offline');
|
|
389
|
+
assert.ok(typeof body.suggestion === 'string');
|
|
390
|
+
assert.ok(body.suggestion.length > 0);
|
|
391
|
+
assert.match(body.suggestion, /bizar doctor|opencode serve/i);
|
|
392
|
+
} finally {
|
|
393
|
+
writeServeJson('/tmp/session-worktree');
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test('GET messages 503 directory_unknown envelope includes suggestion', async () => {
|
|
398
|
+
writeServeJsonWithoutWorktree();
|
|
399
|
+
try {
|
|
400
|
+
const res = await fetch(
|
|
401
|
+
`${dashboardBaseUrl}/api/opencode-sessions/no-such-session/messages`,
|
|
402
|
+
);
|
|
403
|
+
assert.equal(res.status, 503);
|
|
404
|
+
const body = await res.json();
|
|
405
|
+
assert.equal(body.error, 'directory_unknown');
|
|
406
|
+
assert.ok(typeof body.message === 'string');
|
|
407
|
+
assert.ok(typeof body.suggestion === 'string');
|
|
408
|
+
assert.ok(body.suggestion.length > 0);
|
|
409
|
+
} finally {
|
|
410
|
+
writeServeJson('/tmp/session-worktree');
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test('GET messages uses worktree fast-path when session exists in recorded worktree', async () => {
|
|
415
|
+
// The new resolveSessionDirectory fast-path probes the recorded
|
|
416
|
+
// worktree first (cheap GET /api/session/{id}?directory=... probe).
|
|
417
|
+
// When the upstream says yes, the resolver returns the worktree
|
|
418
|
+
// WITHOUT calling GET /api/session. We assert the latter is NOT
|
|
419
|
+
// called by checking the upstream.promptHits / request log isn't
|
|
420
|
+
// touched (only the messages endpoint should fire here).
|
|
421
|
+
//
|
|
422
|
+
// Indirect check: request a session whose id is NOT in the
|
|
423
|
+
// /api/session listing but IS in /tmp/session-worktree per the
|
|
424
|
+
// probe. The probe handler returns 200 for that directory, so the
|
|
425
|
+
// route should succeed even though the session isn't in the list.
|
|
426
|
+
writeServeJson('/tmp/session-worktree');
|
|
427
|
+
const res = await fetch(
|
|
428
|
+
`${dashboardBaseUrl}/api/opencode-sessions/sess-1/messages`,
|
|
429
|
+
);
|
|
430
|
+
assert.equal(res.status, 200);
|
|
431
|
+
const body = await res.json();
|
|
432
|
+
assert.ok(Array.isArray(body.messages));
|
|
433
|
+
assert.ok(body.messages.length >= 1);
|
|
276
434
|
});
|