@polderlabs/bizar 5.0.0 → 5.0.2
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-CFqu2M-c.js → icons-Bo0iH9EC.js} +166 -121
- package/bizar-dash/dist/assets/icons-Bo0iH9EC.js.map +1 -0
- 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/mobile-BtxQJftK.js +2 -0
- package/bizar-dash/dist/assets/mobile-BtxQJftK.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-DHXXbn1A.js → mobile-DD-FZrTC.js} +1 -1
- package/bizar-dash/dist/assets/{mobile-DHXXbn1A.js.map → mobile-DD-FZrTC.js.map} +1 -1
- package/bizar-dash/dist/index.html +4 -4
- package/bizar-dash/dist/mobile.html +3 -3
- 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/src/server/api.mjs +7 -0
- package/bizar-dash/src/server/diagnostics-store.mjs +452 -2
- package/bizar-dash/src/server/memory-store.mjs +46 -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/doctor.mjs +71 -0
- package/bizar-dash/src/server/routes/env-vars.mjs +67 -1
- package/bizar-dash/src/server/routes/memory.mjs +3 -0
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +61 -42
- package/bizar-dash/src/server/routes/schedules.mjs +55 -0
- package/bizar-dash/src/server/serve-info.mjs +172 -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/ScheduleTemplateCard.tsx +71 -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/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/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/MiniMaxUsage.tsx +1 -0
- package/bizar-dash/src/web/views/Schedules.tsx +94 -12
- package/bizar-dash/src/web/views/Settings.tsx +76 -46
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +114 -133
- 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/tests/autosave.test.tsx +276 -0
- package/bizar-dash/tests/chat-composer.test.tsx +140 -0
- package/bizar-dash/tests/components/doctor-panel.test.tsx +105 -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/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/opencode-sessions-detail.test.mjs +158 -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/cli/commands/dash.mjs +5 -1
- package/cli/commands/minimax.mjs +5 -2
- 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 +1 -1
- 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/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/icons-CFqu2M-c.js.map +0 -1
- package/bizar-dash/dist/assets/main-Dl8yY5_H.js +0 -16
- package/bizar-dash/dist/assets/main-Dl8yY5_H.js.map +0 -1
- package/bizar-dash/dist/assets/main-ZAfGKENE.css +0 -1
- package/bizar-dash/dist/assets/mobile-C2gysFOZ.js +0 -2
- package/bizar-dash/dist/assets/mobile-C2gysFOZ.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,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bizar-dash/tests/schedules-templates.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* Verify schedule template loading and from-template endpoint.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it } from 'node:test';
|
|
7
|
+
import assert from 'node:assert';
|
|
8
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
9
|
+
import { join, dirname } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
13
|
+
const PROJECT_ROOT = join(__dirname, '..', '..');
|
|
14
|
+
const TEMPLATES_DIR = join(PROJECT_ROOT, 'templates', 'schedules');
|
|
15
|
+
|
|
16
|
+
describe('Schedule templates', () => {
|
|
17
|
+
it('templates/schedules directory exists', () => {
|
|
18
|
+
assert.ok(existsSync(TEMPLATES_DIR), 'Missing templates/schedules directory');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('at least 4 template files exist', () => {
|
|
22
|
+
const files = readdirSync(TEMPLATES_DIR).filter((f) => f.endsWith('.json'));
|
|
23
|
+
assert.ok(files.length >= 4, `Expected >=4 templates, found ${files.length}: ${files.join(', ')}`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('each template is valid JSON with required fields', () => {
|
|
27
|
+
const files = readdirSync(TEMPLATES_DIR).filter((f) => f.endsWith('.json'));
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
const content = readFileSync(join(TEMPLATES_DIR, file), 'utf8');
|
|
30
|
+
const template = JSON.parse(content);
|
|
31
|
+
assert.ok(template.id, `${file}: missing id`);
|
|
32
|
+
assert.ok(template.name, `${file}: missing name`);
|
|
33
|
+
assert.ok(template.description, `${file}: missing description`);
|
|
34
|
+
assert.ok(template.type, `${file}: missing type`);
|
|
35
|
+
assert.ok(template.action, `${file}: missing action`);
|
|
36
|
+
assert.strictEqual(template.source, undefined, `${file}: should NOT have source field (added at load time)`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('daily-backup.json is a valid cron schedule template', () => {
|
|
41
|
+
const content = readFileSync(join(TEMPLATES_DIR, 'daily-backup.json'), 'utf8');
|
|
42
|
+
const t = JSON.parse(content);
|
|
43
|
+
assert.strictEqual(t.type, 'cron');
|
|
44
|
+
assert.strictEqual(t.schedule, '0 3 * * *');
|
|
45
|
+
assert.strictEqual(t.action.type, 'command');
|
|
46
|
+
assert.ok(t.action.target.includes('bizar backup'));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('weekly-digest.json is a valid agent template', () => {
|
|
50
|
+
const content = readFileSync(join(TEMPLATES_DIR, 'weekly-digest.json'), 'utf8');
|
|
51
|
+
const t = JSON.parse(content);
|
|
52
|
+
assert.strictEqual(t.type, 'cron');
|
|
53
|
+
assert.strictEqual(t.action.type, 'agent');
|
|
54
|
+
assert.ok(t.action.prompt);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('hourly-health-check.json is a valid interval template', () => {
|
|
58
|
+
const content = readFileSync(join(TEMPLATES_DIR, 'hourly-health-check.json'), 'utf8');
|
|
59
|
+
const t = JSON.parse(content);
|
|
60
|
+
assert.strictEqual(t.type, 'interval');
|
|
61
|
+
assert.strictEqual(t.action.type, 'command');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('webhook-on-push.json has webhook action', () => {
|
|
65
|
+
const content = readFileSync(join(TEMPLATES_DIR, 'webhook-on-push.json'), 'utf8');
|
|
66
|
+
const t = JSON.parse(content);
|
|
67
|
+
assert.strictEqual(t.action.type, 'webhook');
|
|
68
|
+
assert.ok(t.action.target);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('loadTemplates (unit)', () => {
|
|
73
|
+
// Re-implement loadTemplates here to test in isolation
|
|
74
|
+
function loadTemplates() {
|
|
75
|
+
if (!existsSync(TEMPLATES_DIR)) return [];
|
|
76
|
+
return readdirSync(TEMPLATES_DIR)
|
|
77
|
+
.filter((f) => f.endsWith('.json'))
|
|
78
|
+
.map((f) => {
|
|
79
|
+
const content = JSON.parse(readFileSync(join(TEMPLATES_DIR, f), 'utf8'));
|
|
80
|
+
return { ...content, source: 'template', templateFile: f };
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
it('returns array with source=template and templateFile set', () => {
|
|
85
|
+
const templates = loadTemplates();
|
|
86
|
+
assert.ok(Array.isArray(templates));
|
|
87
|
+
assert.ok(templates.length > 0);
|
|
88
|
+
for (const t of templates) {
|
|
89
|
+
assert.strictEqual(t.source, 'template', 'each template should have source=template');
|
|
90
|
+
assert.ok(t.templateFile.endsWith('.json'), 'templateFile should be a .json file');
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('missing templates dir returns empty array', () => {
|
|
95
|
+
// Use a path that definitely doesn't exist
|
|
96
|
+
const fakeDir = join(TEMPLATES_DIR, '..', 'schedules-does-not-exist');
|
|
97
|
+
function loadFromFake() {
|
|
98
|
+
if (!existsSync(fakeDir)) return [];
|
|
99
|
+
return readdirSync(fakeDir).filter((f) => f.endsWith('.json'));
|
|
100
|
+
}
|
|
101
|
+
const result = loadFromFake();
|
|
102
|
+
assert.deepStrictEqual(result, []);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('from-template endpoint contract', () => {
|
|
107
|
+
it('template payload can be used to create a valid schedule payload', () => {
|
|
108
|
+
const content = readFileSync(join(TEMPLATES_DIR, 'daily-backup.json'), 'utf8');
|
|
109
|
+
const template = JSON.parse(content);
|
|
110
|
+
|
|
111
|
+
// Simulate what the route does
|
|
112
|
+
const schedulePayload = {
|
|
113
|
+
name: template.name,
|
|
114
|
+
type: template.type,
|
|
115
|
+
schedule: template.schedule,
|
|
116
|
+
timezone: template.timezone || 'UTC',
|
|
117
|
+
action: template.action,
|
|
118
|
+
enabled: true,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
assert.ok(schedulePayload.name);
|
|
122
|
+
assert.ok(['cron', 'interval', 'once'].includes(schedulePayload.type));
|
|
123
|
+
assert.ok(schedulePayload.schedule);
|
|
124
|
+
assert.ok(schedulePayload.action);
|
|
125
|
+
assert.strictEqual(schedulePayload.enabled, true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('custom name can override template name', () => {
|
|
129
|
+
const content = readFileSync(join(TEMPLATES_DIR, 'daily-backup.json'), 'utf8');
|
|
130
|
+
const template = JSON.parse(content);
|
|
131
|
+
const customName = 'My Custom Backup';
|
|
132
|
+
|
|
133
|
+
const schedulePayload = {
|
|
134
|
+
name: customName || template.name,
|
|
135
|
+
type: template.type,
|
|
136
|
+
schedule: template.schedule,
|
|
137
|
+
timezone: template.timezone || 'UTC',
|
|
138
|
+
action: template.action,
|
|
139
|
+
enabled: true,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
assert.strictEqual(schedulePayload.name, customName);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/settings-layout.test.tsx
|
|
3
|
+
*
|
|
4
|
+
* v4.9.0 — Integration tests for the settings sidebar layout mode.
|
|
5
|
+
* Verifies that when settingsMode is active, the sidebar shows SettingsNav
|
|
6
|
+
* and clicking sections updates the active section.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
9
|
+
import { render, screen } from '@testing-library/react';
|
|
10
|
+
import userEvent from '@testing-library/user-event';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { Sidebar } from '../src/web/components/Sidebar';
|
|
13
|
+
import type { TabDef } from '../src/web/components/Topbar';
|
|
14
|
+
import { LayoutDashboard, MessageSquare, Sliders } from 'lucide-react';
|
|
15
|
+
|
|
16
|
+
const TABS: TabDef[] = [
|
|
17
|
+
{ id: 'overview', label: 'Overview', icon: LayoutDashboard },
|
|
18
|
+
{ id: 'chat', label: 'Chat', icon: MessageSquare },
|
|
19
|
+
{ id: 'settings', label: 'Settings', icon: Sliders },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
describe('Sidebar — settingsMode', () => {
|
|
23
|
+
const onTabChange = vi.fn();
|
|
24
|
+
const onSettingsSectionChange = vi.fn();
|
|
25
|
+
const onExitSettings = vi.fn();
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('renders normal tab rail when settingsMode is false', () => {
|
|
32
|
+
render(
|
|
33
|
+
<Sidebar
|
|
34
|
+
tabs={TABS}
|
|
35
|
+
activeTab="overview"
|
|
36
|
+
onTabChange={onTabChange}
|
|
37
|
+
settingsMode={false}
|
|
38
|
+
/>,
|
|
39
|
+
);
|
|
40
|
+
expect(screen.getByRole('tablist')).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByRole('tab', { name: /overview/i })).toBeInTheDocument();
|
|
42
|
+
expect(screen.getByRole('tab', { name: /chat/i })).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('renders SettingsNav instead of tab rail when settingsMode is true', () => {
|
|
46
|
+
render(
|
|
47
|
+
<Sidebar
|
|
48
|
+
tabs={TABS}
|
|
49
|
+
activeTab="settings"
|
|
50
|
+
onTabChange={onTabChange}
|
|
51
|
+
settingsMode={true}
|
|
52
|
+
settingsActiveSection={null}
|
|
53
|
+
onSettingsSectionChange={onSettingsSectionChange}
|
|
54
|
+
onExitSettings={onExitSettings}
|
|
55
|
+
/>,
|
|
56
|
+
);
|
|
57
|
+
// Should NOT show the normal tab rail
|
|
58
|
+
expect(screen.queryByRole('tablist')).not.toBeInTheDocument();
|
|
59
|
+
// Should show the back button
|
|
60
|
+
expect(screen.getByRole('button', { name: /back/i })).toBeInTheDocument();
|
|
61
|
+
// Should show section groups (use selector to avoid duplicate text matches)
|
|
62
|
+
expect(screen.getByText('General', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
|
|
63
|
+
expect(screen.getByText('Core', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('shows Back button that calls onExitSettings', async () => {
|
|
67
|
+
const user = userEvent.setup();
|
|
68
|
+
render(
|
|
69
|
+
<Sidebar
|
|
70
|
+
tabs={TABS}
|
|
71
|
+
activeTab="settings"
|
|
72
|
+
onTabChange={onTabChange}
|
|
73
|
+
settingsMode={true}
|
|
74
|
+
settingsActiveSection={null}
|
|
75
|
+
onSettingsSectionChange={onSettingsSectionChange}
|
|
76
|
+
onExitSettings={onExitSettings}
|
|
77
|
+
/>,
|
|
78
|
+
);
|
|
79
|
+
await user.click(screen.getByText('Back'));
|
|
80
|
+
expect(onExitSettings).toHaveBeenCalledTimes(1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('highlights the active section when settingsActiveSection is set', () => {
|
|
84
|
+
render(
|
|
85
|
+
<Sidebar
|
|
86
|
+
tabs={TABS}
|
|
87
|
+
activeTab="settings"
|
|
88
|
+
onTabChange={onTabChange}
|
|
89
|
+
settingsMode={true}
|
|
90
|
+
settingsActiveSection="theme"
|
|
91
|
+
onSettingsSectionChange={onSettingsSectionChange}
|
|
92
|
+
onExitSettings={onExitSettings}
|
|
93
|
+
/>,
|
|
94
|
+
);
|
|
95
|
+
const themeBtn = screen.getByRole('button', { name: /theme/i });
|
|
96
|
+
expect(themeBtn).toHaveClass('settings-nav-item-active');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('calls onSettingsSectionChange when a section is clicked', async () => {
|
|
100
|
+
const user = userEvent.setup();
|
|
101
|
+
render(
|
|
102
|
+
<Sidebar
|
|
103
|
+
tabs={TABS}
|
|
104
|
+
activeTab="settings"
|
|
105
|
+
onTabChange={onTabChange}
|
|
106
|
+
settingsMode={true}
|
|
107
|
+
settingsActiveSection={null}
|
|
108
|
+
onSettingsSectionChange={onSettingsSectionChange}
|
|
109
|
+
onExitSettings={onExitSettings}
|
|
110
|
+
/>,
|
|
111
|
+
);
|
|
112
|
+
await user.click(screen.getByRole('button', { name: /memory/i }));
|
|
113
|
+
expect(onSettingsSectionChange).toHaveBeenCalledWith('memory');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('calls onTabChange when a normal sidebar tab is clicked', async () => {
|
|
117
|
+
const user = userEvent.setup();
|
|
118
|
+
render(
|
|
119
|
+
<Sidebar
|
|
120
|
+
tabs={TABS}
|
|
121
|
+
activeTab="overview"
|
|
122
|
+
onTabChange={onTabChange}
|
|
123
|
+
settingsMode={false}
|
|
124
|
+
/>,
|
|
125
|
+
);
|
|
126
|
+
await user.click(screen.getByRole('tab', { name: /chat/i }));
|
|
127
|
+
expect(onTabChange).toHaveBeenCalledWith('chat');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/settings-mode-wiring.test.tsx
|
|
3
|
+
*
|
|
4
|
+
* v4.9.0 — Wiring tests for settings mode.
|
|
5
|
+
* Verifies:
|
|
6
|
+
* 1. Topbar Settings tab calls onTabChange('settings') when clicked
|
|
7
|
+
* 2. Sidebar renders SettingsNav when settingsMode is true
|
|
8
|
+
* 3. Sidebar renders normal tab rail when settingsMode is false
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
11
|
+
import { render, screen } from '@testing-library/react';
|
|
12
|
+
import userEvent from '@testing-library/user-event';
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { Sidebar } from '../src/web/components/Sidebar';
|
|
15
|
+
import { Topbar, TABS } from '../src/web/components/Topbar';
|
|
16
|
+
import type { TabDef } from '../src/web/components/Topbar';
|
|
17
|
+
|
|
18
|
+
// Minimal tabs for testing
|
|
19
|
+
const TEST_TABS: TabDef[] = TABS.slice(0, 4);
|
|
20
|
+
|
|
21
|
+
describe('Settings mode wiring', () => {
|
|
22
|
+
describe('Topbar', () => {
|
|
23
|
+
it('calls onTabChange with "settings" when the Settings tab is clicked', async () => {
|
|
24
|
+
const onTabChange = vi.fn();
|
|
25
|
+
const user = userEvent.setup();
|
|
26
|
+
|
|
27
|
+
render(
|
|
28
|
+
<Topbar
|
|
29
|
+
activeTab="overview"
|
|
30
|
+
onTabChange={onTabChange}
|
|
31
|
+
wsStatus="connected"
|
|
32
|
+
version="v4.9.0"
|
|
33
|
+
activeProject={null}
|
|
34
|
+
projects={[]}
|
|
35
|
+
onProjectChange={vi.fn()}
|
|
36
|
+
onProjectsRefresh={vi.fn()}
|
|
37
|
+
onOpenSearch={vi.fn()}
|
|
38
|
+
/>,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Find and click the Settings tab
|
|
42
|
+
const settingsTab = screen.getByRole('tab', { name: /settings/i });
|
|
43
|
+
await user.click(settingsTab);
|
|
44
|
+
|
|
45
|
+
expect(onTabChange).toHaveBeenCalledWith('settings');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('calls onTabChange with "overview" when the Overview tab is clicked', async () => {
|
|
49
|
+
const onTabChange = vi.fn();
|
|
50
|
+
const user = userEvent.setup();
|
|
51
|
+
|
|
52
|
+
render(
|
|
53
|
+
<Topbar
|
|
54
|
+
activeTab="settings"
|
|
55
|
+
onTabChange={onTabChange}
|
|
56
|
+
wsStatus="connected"
|
|
57
|
+
version="v4.9.0"
|
|
58
|
+
activeProject={null}
|
|
59
|
+
projects={[]}
|
|
60
|
+
onProjectChange={vi.fn()}
|
|
61
|
+
onProjectsRefresh={vi.fn()}
|
|
62
|
+
onOpenSearch={vi.fn()}
|
|
63
|
+
/>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const overviewTab = screen.getByRole('tab', { name: /overview/i });
|
|
67
|
+
await user.click(overviewTab);
|
|
68
|
+
|
|
69
|
+
expect(onTabChange).toHaveBeenCalledWith('overview');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('Sidebar', () => {
|
|
74
|
+
const onTabChange = vi.fn();
|
|
75
|
+
const onSettingsSectionChange = vi.fn();
|
|
76
|
+
const onExitSettings = vi.fn();
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
vi.clearAllMocks();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('renders SettingsNav when settingsMode is true', () => {
|
|
83
|
+
render(
|
|
84
|
+
<Sidebar
|
|
85
|
+
tabs={TEST_TABS}
|
|
86
|
+
activeTab="settings"
|
|
87
|
+
onTabChange={onTabChange}
|
|
88
|
+
settingsMode={true}
|
|
89
|
+
settingsActiveSection={null}
|
|
90
|
+
onSettingsSectionChange={onSettingsSectionChange}
|
|
91
|
+
onExitSettings={onExitSettings}
|
|
92
|
+
/>,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// SettingsNav should be visible (renders the back button)
|
|
96
|
+
expect(screen.getByRole('button', { name: /exit settings/i })).toBeInTheDocument();
|
|
97
|
+
// Normal tab rail should NOT be visible
|
|
98
|
+
expect(screen.queryByRole('tablist')).not.toBeInTheDocument();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('renders normal tab rail when settingsMode is false', () => {
|
|
102
|
+
render(
|
|
103
|
+
<Sidebar
|
|
104
|
+
tabs={TEST_TABS}
|
|
105
|
+
activeTab="overview"
|
|
106
|
+
onTabChange={onTabChange}
|
|
107
|
+
settingsMode={false}
|
|
108
|
+
/>,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Tab rail should be visible
|
|
112
|
+
expect(screen.getByRole('tablist')).toBeInTheDocument();
|
|
113
|
+
// SettingsNav should NOT be visible
|
|
114
|
+
expect(screen.queryByRole('button', { name: /back/i })).not.toBeInTheDocument();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('exits settings mode when back button is clicked', async () => {
|
|
118
|
+
const user = userEvent.setup();
|
|
119
|
+
render(
|
|
120
|
+
<Sidebar
|
|
121
|
+
tabs={TEST_TABS}
|
|
122
|
+
activeTab="settings"
|
|
123
|
+
onTabChange={onTabChange}
|
|
124
|
+
settingsMode={true}
|
|
125
|
+
settingsActiveSection={null}
|
|
126
|
+
onSettingsSectionChange={onSettingsSectionChange}
|
|
127
|
+
onExitSettings={onExitSettings}
|
|
128
|
+
/>,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
await user.click(screen.getByRole('button', { name: /exit settings/i }));
|
|
132
|
+
expect(onExitSettings).toHaveBeenCalledTimes(1);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('calls onTabChange when a tab is clicked while in settingsMode=false', async () => {
|
|
136
|
+
const user = userEvent.setup();
|
|
137
|
+
render(
|
|
138
|
+
<Sidebar
|
|
139
|
+
tabs={TEST_TABS}
|
|
140
|
+
activeTab="overview"
|
|
141
|
+
onTabChange={onTabChange}
|
|
142
|
+
settingsMode={false}
|
|
143
|
+
/>,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Click the Chat tab
|
|
147
|
+
await user.click(screen.getByRole('tab', { name: /chat/i }));
|
|
148
|
+
expect(onTabChange).toHaveBeenCalledWith('chat');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/settings-nav.test.tsx
|
|
3
|
+
*
|
|
4
|
+
* v4.9.0 — Component tests for SettingsNav sidebar component.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
7
|
+
import { render, screen } from '@testing-library/react';
|
|
8
|
+
import userEvent from '@testing-library/user-event';
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { SettingsNav } from '../src/web/components/SettingsNav';
|
|
11
|
+
|
|
12
|
+
describe('SettingsNav', () => {
|
|
13
|
+
const onSectionChange = vi.fn();
|
|
14
|
+
const onExitSettings = vi.fn();
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('renders the back button', () => {
|
|
21
|
+
render(
|
|
22
|
+
<SettingsNav
|
|
23
|
+
activeSection={null}
|
|
24
|
+
onSectionChange={onSectionChange}
|
|
25
|
+
onExitSettings={onExitSettings}
|
|
26
|
+
/>,
|
|
27
|
+
);
|
|
28
|
+
expect(screen.getByRole('button', { name: /back/i })).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('calls onExitSettings when back button is clicked', async () => {
|
|
32
|
+
const user = userEvent.setup();
|
|
33
|
+
render(
|
|
34
|
+
<SettingsNav
|
|
35
|
+
activeSection={null}
|
|
36
|
+
onSectionChange={onSectionChange}
|
|
37
|
+
onExitSettings={onExitSettings}
|
|
38
|
+
/>,
|
|
39
|
+
);
|
|
40
|
+
await user.click(screen.getByText('Back'));
|
|
41
|
+
expect(onExitSettings).toHaveBeenCalledTimes(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('renders section groups with group-label class', () => {
|
|
45
|
+
render(
|
|
46
|
+
<SettingsNav
|
|
47
|
+
activeSection={null}
|
|
48
|
+
onSectionChange={onSectionChange}
|
|
49
|
+
onExitSettings={onExitSettings}
|
|
50
|
+
/>,
|
|
51
|
+
);
|
|
52
|
+
expect(screen.getByText('General', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
|
|
53
|
+
expect(screen.getByText('Core', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
|
|
54
|
+
expect(screen.getByText('Experience', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
|
|
55
|
+
expect(screen.getByText('Data', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('renders section items within each group', () => {
|
|
59
|
+
render(
|
|
60
|
+
<SettingsNav
|
|
61
|
+
activeSection={null}
|
|
62
|
+
onSectionChange={onSectionChange}
|
|
63
|
+
onExitSettings={onExitSettings}
|
|
64
|
+
/>,
|
|
65
|
+
);
|
|
66
|
+
expect(screen.getByRole('button', { name: /theme/i })).toBeInTheDocument();
|
|
67
|
+
expect(screen.getByRole('button', { name: /env vars/i })).toBeInTheDocument();
|
|
68
|
+
expect(screen.getByRole('button', { name: /memory/i })).toBeInTheDocument();
|
|
69
|
+
expect(screen.getByRole('button', { name: /system llm/i })).toBeInTheDocument();
|
|
70
|
+
expect(screen.getByRole('button', { name: /updates/i })).toBeInTheDocument();
|
|
71
|
+
expect(screen.getByRole('button', { name: /skills/i })).toBeInTheDocument();
|
|
72
|
+
expect(screen.getByRole('button', { name: /headroom/i })).toBeInTheDocument();
|
|
73
|
+
expect(screen.getByRole('button', { name: /backup/i })).toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('calls onSectionChange with correct id when a section is clicked', async () => {
|
|
77
|
+
const user = userEvent.setup();
|
|
78
|
+
render(
|
|
79
|
+
<SettingsNav
|
|
80
|
+
activeSection={null}
|
|
81
|
+
onSectionChange={onSectionChange}
|
|
82
|
+
onExitSettings={onExitSettings}
|
|
83
|
+
/>,
|
|
84
|
+
);
|
|
85
|
+
await user.click(screen.getByRole('button', { name: /theme/i }));
|
|
86
|
+
expect(onSectionChange).toHaveBeenCalledWith('theme');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('highlights the active section', () => {
|
|
90
|
+
render(
|
|
91
|
+
<SettingsNav
|
|
92
|
+
activeSection="theme"
|
|
93
|
+
onSectionChange={onSectionChange}
|
|
94
|
+
onExitSettings={onExitSettings}
|
|
95
|
+
/>,
|
|
96
|
+
);
|
|
97
|
+
const themeBtn = screen.getByRole('button', { name: /theme/i });
|
|
98
|
+
expect(themeBtn).toHaveClass('settings-nav-item-active');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('toggles off the active section when clicking it again', async () => {
|
|
102
|
+
const user = userEvent.setup();
|
|
103
|
+
render(
|
|
104
|
+
<SettingsNav
|
|
105
|
+
activeSection="theme"
|
|
106
|
+
onSectionChange={onSectionChange}
|
|
107
|
+
onExitSettings={onExitSettings}
|
|
108
|
+
/>,
|
|
109
|
+
);
|
|
110
|
+
await user.click(screen.getByRole('button', { name: /theme/i }));
|
|
111
|
+
expect(onSectionChange).toHaveBeenCalledWith(null); // toggles off
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('renders chevron on the active section', () => {
|
|
115
|
+
render(
|
|
116
|
+
<SettingsNav
|
|
117
|
+
activeSection="theme"
|
|
118
|
+
onSectionChange={onSectionChange}
|
|
119
|
+
onExitSettings={onExitSettings}
|
|
120
|
+
/>,
|
|
121
|
+
);
|
|
122
|
+
const themeBtn = screen.getByRole('button', { name: /theme/i });
|
|
123
|
+
// The chevron is inside the button as an SVG (aria-hidden), so we just verify the button exists
|
|
124
|
+
expect(themeBtn).toBeInTheDocument();
|
|
125
|
+
});
|
|
126
|
+
});
|
package/cli/commands/dash.mjs
CHANGED
|
@@ -129,7 +129,11 @@ export async function runDash(dashArgs) {
|
|
|
129
129
|
switch (sub) {
|
|
130
130
|
case 'start':
|
|
131
131
|
if (subOpts.bg) {
|
|
132
|
-
|
|
132
|
+
// Pass --port, --host, --bind through to the child process
|
|
133
|
+
const bgArgs = ['start', ...(subOpts.subArgs || [])];
|
|
134
|
+
if (subOpts.port) bgArgs.push('--port', String(subOpts.port));
|
|
135
|
+
if (process.env.BIZAR_DASHBOARD_BIND) bgArgs.push('--bind', process.env.BIZAR_DASHBOARD_BIND);
|
|
136
|
+
await dashModule.startInBackground(bgArgs);
|
|
133
137
|
} else {
|
|
134
138
|
await dashModule.start(subOpts);
|
|
135
139
|
}
|
package/cli/commands/minimax.mjs
CHANGED
|
@@ -148,8 +148,11 @@ async function runMinimaxCommand(minimaxArgs) {
|
|
|
148
148
|
for (const m of data.models || []) {
|
|
149
149
|
const five = m.current_interval_remaining_percent;
|
|
150
150
|
const week = m.current_weekly_remaining_percent;
|
|
151
|
-
|
|
152
|
-
const
|
|
151
|
+
// Bar shows CONSUMED as filled (inverted from remaining)
|
|
152
|
+
const fiveConsumed = 100 - five;
|
|
153
|
+
const weekConsumed = 100 - week;
|
|
154
|
+
const fiveBar = '█'.repeat(Math.round(fiveConsumed / 5)) + '░'.repeat(20 - Math.round(fiveConsumed / 5));
|
|
155
|
+
const weekBar = '█'.repeat(Math.round(weekConsumed / 5)) + '░'.repeat(20 - Math.round(weekConsumed / 5));
|
|
153
156
|
const fiveColor = five >= 75 ? chalk.green : five >= 25 ? chalk.yellow : chalk.red;
|
|
154
157
|
const weekColor = week >= 75 ? chalk.green : week >= 25 ? chalk.yellow : chalk.red;
|
|
155
158
|
console.log(` ${chalk.bold(m.model_name)}`);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/tests/minimax-cli.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* Tests for MiniMax CLI bar rendering.
|
|
5
|
+
* Verifies bar shows CONSUMED quota as filled blocks (not remaining).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { test, describe } from 'node:test';
|
|
9
|
+
import assert from 'node:assert';
|
|
10
|
+
|
|
11
|
+
// ── Bar rendering helper (mirrors minimax.mjs logic) ──────────────────────────
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render a quota bar string.
|
|
15
|
+
* @param {number} remainingPct - percentage of quota remaining (0-100)
|
|
16
|
+
* @returns {string} bar string like '██░░░░░░░░░░░░░░░░░' (20 chars total)
|
|
17
|
+
*/
|
|
18
|
+
function renderBar(remainingPct) {
|
|
19
|
+
const consumed = 100 - remainingPct;
|
|
20
|
+
const filled = Math.round(consumed / 5);
|
|
21
|
+
const empty = 20 - filled;
|
|
22
|
+
return '█'.repeat(filled) + '░'.repeat(empty);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Tests ───────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
describe('MiniMax CLI bar rendering', () => {
|
|
28
|
+
|
|
29
|
+
test('91% remaining → 9% consumed → 2 filled blocks (9/5 rounds to 2)', () => {
|
|
30
|
+
const bar = renderBar(91);
|
|
31
|
+
const filledCount = (bar.match(/█/g) || []).length;
|
|
32
|
+
assert.strictEqual(filledCount, 2, `Expected 2 filled blocks for 91% remaining, got ${filledCount}`);
|
|
33
|
+
assert.strictEqual(bar.length, 20, `Bar should be exactly 20 chars`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('100% remaining → 0% consumed → 0 filled blocks', () => {
|
|
37
|
+
const bar = renderBar(100);
|
|
38
|
+
const filledCount = (bar.match(/█/g) || []).length;
|
|
39
|
+
assert.strictEqual(filledCount, 0, `Expected 0 filled blocks for 100% remaining`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('0% remaining → 100% consumed → 20 filled blocks', () => {
|
|
43
|
+
const bar = renderBar(0);
|
|
44
|
+
const filledCount = (bar.match(/█/g) || []).length;
|
|
45
|
+
assert.strictEqual(filledCount, 20, `Expected 20 filled blocks for 0% remaining`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('75% remaining → 25% consumed → 5 filled blocks', () => {
|
|
49
|
+
const bar = renderBar(75);
|
|
50
|
+
const filledCount = (bar.match(/█/g) || []).length;
|
|
51
|
+
assert.strictEqual(filledCount, 5, `Expected 5 filled blocks for 75% remaining`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('25% remaining → 75% consumed → 15 filled blocks', () => {
|
|
55
|
+
const bar = renderBar(25);
|
|
56
|
+
const filledCount = (bar.match(/█/g) || []).length;
|
|
57
|
+
assert.strictEqual(filledCount, 15, `Expected 15 filled blocks for 25% remaining`);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('bar is always exactly 20 characters', () => {
|
|
61
|
+
for (const remaining of [0, 10, 25, 50, 75, 90, 100]) {
|
|
62
|
+
const bar = renderBar(remaining);
|
|
63
|
+
assert.strictEqual(bar.length, 20, `Bar for ${remaining}% remaining should be 20 chars, got ${bar.length}`);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('filled blocks increase as remaining decreases', () => {
|
|
68
|
+
const bars = [100, 91, 75, 50, 25, 10, 0].map(r => ({
|
|
69
|
+
remaining: r,
|
|
70
|
+
filled: (renderBar(r).match(/█/g) || []).length
|
|
71
|
+
}));
|
|
72
|
+
for (let i = 1; i < bars.length; i++) {
|
|
73
|
+
assert.ok(
|
|
74
|
+
bars[i].filled >= bars[i-1].filled,
|
|
75
|
+
`${bars[i].remaining}% remaining (${bars[i].filled} blocks) should have >= filled blocks than ${bars[i-1].remaining}% remaining (${bars[i-1].filled} blocks)`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
package/config/agents/frigg.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Frigg — Read-only codebase Q&A. Answers questions about the project with file references, never modifies anything. Routes to no one.
|
|
3
3
|
mode: subagent
|
|
4
|
-
model:
|
|
4
|
+
model: minimax/MiniMax-M2.7
|
|
5
5
|
color: "#f472b6"
|
|
6
6
|
permission:
|
|
7
7
|
read: allow
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Heimdall — Simple, routine, and deterministic tasks using DeepSeek. Quick edits, mechanical work, file operations. The ever-watchful guardian.
|
|
3
3
|
mode: subagent
|
|
4
|
-
model:
|
|
4
|
+
model: minimax/MiniMax-M2.7
|
|
5
5
|
color: "#10b981"
|
|
6
6
|
permission:
|
|
7
7
|
read: allow
|