@polderlabs/bizar 5.5.1 → 5.5.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/{EnvVarsSection-BUQnyVQD.js → EnvVarsSection-DoIzqrlb.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-BUQnyVQD.js.map → EnvVarsSection-DoIzqrlb.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-8FupFgTv.js → MobileChat-CoUCUsbr.js} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-8FupFgTv.js.map → MobileChat-CoUCUsbr.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-BeO04g52.js +1 -0
- package/bizar-dash/dist/assets/{MobileSettings-BkXtglA6.js.map → MobileSettings-BeO04g52.js.map} +1 -1
- package/bizar-dash/dist/assets/{Toast-CUwkLRms.js → Toast-D9nv5N6m.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-CUwkLRms.js.map → Toast-D9nv5N6m.js.map} +1 -1
- package/bizar-dash/dist/assets/{icons-COpQr35l.js → icons-Btipv2pp.js} +65 -70
- package/bizar-dash/dist/assets/icons-Btipv2pp.js.map +1 -0
- package/bizar-dash/dist/assets/main-DHXPuddY.js +18 -0
- package/bizar-dash/dist/assets/main-DHXPuddY.js.map +1 -0
- package/bizar-dash/dist/assets/{main-Bz_zWFCw.css → main-UG4jKK-F.css} +1 -1
- package/bizar-dash/dist/assets/mobile-H5KH9cTL.js +1 -0
- package/bizar-dash/dist/assets/{mobile-B7dAkmYd.js.map → mobile-H5KH9cTL.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-layout-ClC6Qa42.js → mobile-layout-BqHxVb6_.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-layout-ClC6Qa42.js.map → mobile-layout-BqHxVb6_.js.map} +1 -1
- package/bizar-dash/dist/assets/{useSlashCommands-B3MZ1JXa.js → useSlashCommands-C24-O1e7.js} +2 -2
- package/bizar-dash/dist/assets/{useSlashCommands-B3MZ1JXa.js.map → useSlashCommands-C24-O1e7.js.map} +1 -1
- package/bizar-dash/dist/index.html +7 -7
- package/bizar-dash/dist/mobile.html +3 -3
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/memory-store.mjs +74 -28
- package/bizar-dash/src/server/routes/memory.mjs +91 -38
- package/bizar-dash/src/web/App.tsx +36 -43
- package/bizar-dash/src/web/components/SettingsNav.tsx +92 -0
- package/bizar-dash/src/web/components/Sidebar.tsx +26 -7
- package/bizar-dash/src/web/components/Topbar.tsx +11 -48
- package/bizar-dash/src/web/styles/main.css +18 -3
- package/bizar-dash/src/web/styles/settings.css +115 -3
- package/bizar-dash/src/web/views/Settings.tsx +184 -205
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +4 -4
- package/bizar-dash/tests/memory-namespace.test.mjs +2 -2
- package/bizar-dash/tests/memory-status-root.test.mjs +144 -0
- package/bizar-dash/tests/memory-store.test.mjs +5 -4
- package/bizar-dash/tests/memory-vault-config-endpoint.test.mjs +173 -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 +127 -0
- package/bizar-dash/tests/views/Memory.test.tsx +40 -1
- package/bizar-dash/tests/views/topbar.test.tsx +35 -50
- package/cli/memory.mjs +1 -1
- package/package.json +1 -1
- package/bizar-dash/dist/assets/MobileSettings-BkXtglA6.js +0 -1
- package/bizar-dash/dist/assets/icons-COpQr35l.js.map +0 -1
- package/bizar-dash/dist/assets/main-gqVwliHz.js +0 -18
- package/bizar-dash/dist/assets/main-gqVwliHz.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-B7dAkmYd.js +0 -1
- package/bizar-dash/tests/views/Settings.test.tsx +0 -160
|
@@ -53,11 +53,12 @@ describe('memory-store', () => {
|
|
|
53
53
|
lightrag: { enabled: false, host: '127.0.0.1', port: 9621, workingDir: join(projectRoot, '.bizar', 'lightrag') },
|
|
54
54
|
git: { autoPullOnSessionStart: false, autoCommitOnMemoryWrite: false, autoPushOnSessionEnd: false, commitAuthor: 'Bizar Memory <bizar-memory@local>', commitMessageTemplate: 'memory(my-proj): {summary}' },
|
|
55
55
|
});
|
|
56
|
-
const { vaultRoot, mode } = TEST_MEMORY_STORE.resolveVault(projectRoot);
|
|
56
|
+
const { vaultRoot, projectVaultRoot, mode } = TEST_MEMORY_STORE.resolveVault(projectRoot);
|
|
57
57
|
assert.strictEqual(mode, 'managed');
|
|
58
|
-
assert.ok(vaultRoot.includes('data/memory'));
|
|
59
|
-
assert.ok(
|
|
60
|
-
assert.ok(
|
|
58
|
+
assert.ok(vaultRoot.includes('data/memory'), 'vaultRoot should include data/memory');
|
|
59
|
+
assert.ok(projectVaultRoot.includes('projects'), 'projectVaultRoot should include projects');
|
|
60
|
+
assert.ok(projectVaultRoot.includes('my-proj'), 'projectVaultRoot should include my-proj');
|
|
61
|
+
assert.ok(projectVaultRoot.startsWith(vaultRoot), 'projectVaultRoot should be under vaultRoot');
|
|
61
62
|
});
|
|
62
63
|
});
|
|
63
64
|
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/memory-vault-config-endpoint.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v6.x — Tests for POST /memory/config/vault endpoint which updates the
|
|
5
|
+
* vault root path persistently in ~/.config/bizar/memory-config.json
|
|
6
|
+
* and sets process.env.BIZAR_MEMORY_VAULT.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { tmpdir, homedir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { rmSync, existsSync, readFileSync, mkdirSync } from 'node:fs';
|
|
14
|
+
import http from 'node:http';
|
|
15
|
+
|
|
16
|
+
const httpPost = (port, path, body) =>
|
|
17
|
+
new Promise((resolve) => {
|
|
18
|
+
const data = JSON.stringify(body);
|
|
19
|
+
const req = http.request(
|
|
20
|
+
{ hostname: 'localhost', port, path, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } },
|
|
21
|
+
(res) => {
|
|
22
|
+
let respBody = '';
|
|
23
|
+
res.on('data', (c) => (respBody += c));
|
|
24
|
+
res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(respBody || '{}') }));
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
req.write(data);
|
|
28
|
+
req.end();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
async function buildApp(projectRoot) {
|
|
32
|
+
const { createMemoryRouter } = await import('../src/server/routes/memory.mjs');
|
|
33
|
+
const express = (await import('express')).default;
|
|
34
|
+
const app = express();
|
|
35
|
+
app.use(express.json());
|
|
36
|
+
app.use('/api', createMemoryRouter({ projectRoot }));
|
|
37
|
+
return app;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function withApp(projectRoot, fn) {
|
|
41
|
+
const app = await buildApp(projectRoot);
|
|
42
|
+
const server = app.listen(0);
|
|
43
|
+
const port = server.address().port;
|
|
44
|
+
try {
|
|
45
|
+
return await fn(port);
|
|
46
|
+
} finally {
|
|
47
|
+
server.close();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeTmp() {
|
|
52
|
+
const root = join(tmpdir(), `bizar-vault-cfg-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
53
|
+
mkdirSync(root, { recursive: true });
|
|
54
|
+
mkdirSync(join(root, '.bizar'), { recursive: true });
|
|
55
|
+
return root;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('POST /api/memory/config/vault', () => {
|
|
59
|
+
let root;
|
|
60
|
+
const capturedEnv = [];
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
root = makeTmp();
|
|
64
|
+
// Capture process.env changes so we can restore them
|
|
65
|
+
capturedEnv.push(process.env.BIZAR_MEMORY_VAULT);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
// Restore env
|
|
70
|
+
if (capturedEnv.length > 0) {
|
|
71
|
+
const prev = capturedEnv.pop();
|
|
72
|
+
if (prev === undefined) delete process.env.BIZAR_MEMORY_VAULT;
|
|
73
|
+
else process.env.BIZAR_MEMORY_VAULT = prev;
|
|
74
|
+
}
|
|
75
|
+
try { rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('returns 400 when vaultRoot is missing', async () => {
|
|
79
|
+
await withApp(root, async (port) => {
|
|
80
|
+
const res = await httpPost(port, '/api/memory/config/vault', {});
|
|
81
|
+
assert.strictEqual(res.status, 400);
|
|
82
|
+
assert.strictEqual(res.body.error, 'vaultRoot required');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('returns 400 when vaultRoot is not a string', async () => {
|
|
87
|
+
await withApp(root, async (port) => {
|
|
88
|
+
const res = await httpPost(port, '/api/memory/config/vault', { vaultRoot: 123 });
|
|
89
|
+
assert.strictEqual(res.status, 400);
|
|
90
|
+
assert.strictEqual(res.body.error, 'vaultRoot required');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('returns 400 when vaultRoot is an empty string', async () => {
|
|
95
|
+
await withApp(root, async (port) => {
|
|
96
|
+
const res = await httpPost(port, '/api/memory/config/vault', { vaultRoot: ' ' });
|
|
97
|
+
assert.strictEqual(res.status, 400);
|
|
98
|
+
assert.strictEqual(res.body.error, 'vaultRoot required');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('creates the vault directory if it does not exist', async () => {
|
|
103
|
+
const targetVault = join(tmpdir(), `bizar-vault-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
104
|
+
assert.strictEqual(existsSync(targetVault), false);
|
|
105
|
+
|
|
106
|
+
await withApp(root, async (port) => {
|
|
107
|
+
const res = await httpPost(port, '/api/memory/config/vault', { vaultRoot: targetVault });
|
|
108
|
+
assert.strictEqual(res.status, 200);
|
|
109
|
+
assert.strictEqual(res.body.ok, true);
|
|
110
|
+
assert.strictEqual(res.body.vaultRoot, targetVault);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
assert.strictEqual(existsSync(targetVault), true);
|
|
114
|
+
rmSync(targetVault, { recursive: true, force: true });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('persists the vault root to ~/.config/bizar/memory-config.json', async () => {
|
|
118
|
+
const targetVault = join(tmpdir(), `bizar-vault-persist-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
119
|
+
const configPath = join(homedir(), '.config', 'bizar', 'memory-config.json');
|
|
120
|
+
|
|
121
|
+
await withApp(root, async (port) => {
|
|
122
|
+
const res = await httpPost(port, '/api/memory/config/vault', { vaultRoot: targetVault });
|
|
123
|
+
assert.strictEqual(res.status, 200);
|
|
124
|
+
assert.strictEqual(res.body.ok, true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Verify config was written
|
|
128
|
+
assert.strictEqual(existsSync(configPath), true);
|
|
129
|
+
const cfg = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
130
|
+
assert.strictEqual(cfg.git?.repoPath, targetVault);
|
|
131
|
+
|
|
132
|
+
rmSync(targetVault, { recursive: true, force: true });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('sets process.env.BIZAR_MEMORY_VAULT', async () => {
|
|
136
|
+
const targetVault = join(tmpdir(), `bizar-vault-env-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
137
|
+
assert.strictEqual(process.env.BIZAR_MEMORY_VAULT, capturedEnv[capturedEnv.length - 1] || undefined);
|
|
138
|
+
|
|
139
|
+
await withApp(root, async (port) => {
|
|
140
|
+
const res = await httpPost(port, '/api/memory/config/vault', { vaultRoot: targetVault });
|
|
141
|
+
assert.strictEqual(res.status, 200);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
assert.strictEqual(process.env.BIZAR_MEMORY_VAULT, targetVault);
|
|
145
|
+
rmSync(targetVault, { recursive: true, force: true });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('expands ~ to home directory', async () => {
|
|
149
|
+
const targetVault = `~/bizar-test-vault-${Date.now()}`;
|
|
150
|
+
const expectedVault = join(homedir(), `bizar-test-vault-${Date.now()}`);
|
|
151
|
+
|
|
152
|
+
await withApp(root, async (port) => {
|
|
153
|
+
const res = await httpPost(port, '/api/memory/config/vault', { vaultRoot: targetVault });
|
|
154
|
+
assert.strictEqual(res.status, 200);
|
|
155
|
+
assert.strictEqual(res.body.vaultRoot, expectedVault);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
rmSync(expectedVault, { recursive: true, force: true });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('returns the resolved vault root in the response', async () => {
|
|
162
|
+
const targetVault = join(tmpdir(), `bizar-vault-resolved-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
163
|
+
|
|
164
|
+
await withApp(root, async (port) => {
|
|
165
|
+
const res = await httpPost(port, '/api/memory/config/vault', { vaultRoot: targetVault });
|
|
166
|
+
assert.strictEqual(res.status, 200);
|
|
167
|
+
assert.strictEqual(res.body.ok, true);
|
|
168
|
+
assert.strictEqual(res.body.vaultRoot, targetVault);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
rmSync(targetVault, { recursive: true, force: true });
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -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: /exit settings/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: /env vars/i }));
|
|
113
|
+
expect(onSettingsSectionChange).toHaveBeenCalledWith('env-vars');
|
|
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 (first 4 tabs from TABS)
|
|
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,127 @@
|
|
|
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: /exit settings/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
|
+
});
|
|
56
|
+
|
|
57
|
+
it('renders section items within each group', () => {
|
|
58
|
+
render(
|
|
59
|
+
<SettingsNav
|
|
60
|
+
activeSection={null}
|
|
61
|
+
onSectionChange={onSectionChange}
|
|
62
|
+
onExitSettings={onExitSettings}
|
|
63
|
+
/>,
|
|
64
|
+
);
|
|
65
|
+
// General group
|
|
66
|
+
expect(screen.getByRole('button', { name: /theme/i })).toBeInTheDocument();
|
|
67
|
+
expect(screen.getByRole('button', { name: /layout/i })).toBeInTheDocument();
|
|
68
|
+
expect(screen.getByRole('button', { name: /general/i })).toBeInTheDocument();
|
|
69
|
+
// Core group
|
|
70
|
+
expect(screen.getByRole('button', { name: /env vars/i })).toBeInTheDocument();
|
|
71
|
+
expect(screen.getByRole('button', { name: /system llm/i })).toBeInTheDocument();
|
|
72
|
+
// Experience group
|
|
73
|
+
expect(screen.getByRole('button', { name: /updates/i })).toBeInTheDocument();
|
|
74
|
+
expect(screen.getByRole('button', { name: /headroom/i })).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('calls onSectionChange with correct id when a section is clicked', async () => {
|
|
78
|
+
const user = userEvent.setup();
|
|
79
|
+
render(
|
|
80
|
+
<SettingsNav
|
|
81
|
+
activeSection={null}
|
|
82
|
+
onSectionChange={onSectionChange}
|
|
83
|
+
onExitSettings={onExitSettings}
|
|
84
|
+
/>,
|
|
85
|
+
);
|
|
86
|
+
await user.click(screen.getByRole('button', { name: /theme/i }));
|
|
87
|
+
expect(onSectionChange).toHaveBeenCalledWith('theme');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('highlights the active section', () => {
|
|
91
|
+
render(
|
|
92
|
+
<SettingsNav
|
|
93
|
+
activeSection="theme"
|
|
94
|
+
onSectionChange={onSectionChange}
|
|
95
|
+
onExitSettings={onExitSettings}
|
|
96
|
+
/>,
|
|
97
|
+
);
|
|
98
|
+
const themeBtn = screen.getByRole('button', { name: /theme/i });
|
|
99
|
+
expect(themeBtn).toHaveClass('settings-nav-item-active');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('toggles off the active section when clicking it again', async () => {
|
|
103
|
+
const user = userEvent.setup();
|
|
104
|
+
render(
|
|
105
|
+
<SettingsNav
|
|
106
|
+
activeSection="theme"
|
|
107
|
+
onSectionChange={onSectionChange}
|
|
108
|
+
onExitSettings={onExitSettings}
|
|
109
|
+
/>,
|
|
110
|
+
);
|
|
111
|
+
await user.click(screen.getByRole('button', { name: /theme/i }));
|
|
112
|
+
expect(onSectionChange).toHaveBeenCalledWith(null); // toggles off
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('renders chevron on the active section', () => {
|
|
116
|
+
render(
|
|
117
|
+
<SettingsNav
|
|
118
|
+
activeSection="theme"
|
|
119
|
+
onSectionChange={onSectionChange}
|
|
120
|
+
onExitSettings={onExitSettings}
|
|
121
|
+
/>,
|
|
122
|
+
);
|
|
123
|
+
const themeBtn = screen.getByRole('button', { name: /theme/i });
|
|
124
|
+
// The chevron is inside the button as an SVG (aria-hidden), so we just verify the button exists
|
|
125
|
+
expect(themeBtn).toBeInTheDocument();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* path to '' and rendered "(loading…)" forever. After the fix, they fetch
|
|
7
7
|
* /api/memory/status and render the actual path or a "not initialised"
|
|
8
8
|
* placeholder, plus an Initialise button.
|
|
9
|
+
*
|
|
10
|
+
* v6.x also adds tests for the Save Path button calling
|
|
11
|
+
* POST /memory/config/vault (instead of just showing a message).
|
|
9
12
|
*/
|
|
10
13
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
11
14
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
@@ -56,6 +59,7 @@ describe('MemoryOverview vault path', () => {
|
|
|
56
59
|
initialized: true,
|
|
57
60
|
mode: 'local-only',
|
|
58
61
|
vaultRoot: '/Users/test/.bizar_memory',
|
|
62
|
+
projectVaultRoot: '/Users/test/.bizar_memory',
|
|
59
63
|
noteCount: 5,
|
|
60
64
|
gitClean: true,
|
|
61
65
|
branch: 'main',
|
|
@@ -88,7 +92,7 @@ describe('MemoryOverview vault path', () => {
|
|
|
88
92
|
it('shows "not initialised" when status reports initialized=false', async () => {
|
|
89
93
|
(api.get as ReturnType<typeof vi.fn>).mockImplementation((url: string) => {
|
|
90
94
|
if (url === '/memory/status') return Promise.resolve({
|
|
91
|
-
initialized: false, mode: null, projectId: null, vaultRoot: null,
|
|
95
|
+
initialized: false, mode: null, projectId: null, vaultRoot: null, projectVaultRoot: null,
|
|
92
96
|
});
|
|
93
97
|
if (url === '/memory/health') return Promise.resolve({
|
|
94
98
|
score: 0, status: 'unconfigured', checks: [], message: 'not initialised',
|
|
@@ -185,6 +189,41 @@ describe('ConfigPanel vault path', () => {
|
|
|
185
189
|
await user.click(screen.getByTestId('memory-init-vault'));
|
|
186
190
|
expect(post).toHaveBeenCalledWith('/memory/init', expect.anything());
|
|
187
191
|
});
|
|
192
|
+
|
|
193
|
+
// v6.x — Save Path now calls POST /memory/config/vault
|
|
194
|
+
it('calls POST /memory/config/vault when Save path is clicked (already initialised)', async () => {
|
|
195
|
+
const post = vi.fn().mockResolvedValue({ ok: true, vaultRoot: '/new/path/.bizar_memory' });
|
|
196
|
+
(api.get as ReturnType<typeof vi.fn>).mockImplementation((url: string) => {
|
|
197
|
+
if (url === '/memory/config/global') return Promise.resolve({ config: { git: { remoteUrl: '' } } });
|
|
198
|
+
if (url === '/memory/status') return Promise.resolve({
|
|
199
|
+
initialized: true, vaultRoot: '/old/path/.bizar_memory', mode: 'managed',
|
|
200
|
+
});
|
|
201
|
+
if (url === '/memory/git/status') return Promise.resolve({ ok: true, clean: true, branch: 'main' });
|
|
202
|
+
return Promise.resolve(null);
|
|
203
|
+
});
|
|
204
|
+
(api.post as ReturnType<typeof vi.fn>) = post;
|
|
205
|
+
|
|
206
|
+
const user = userEvent.setup();
|
|
207
|
+
render(
|
|
208
|
+
<Harness>
|
|
209
|
+
<ConfigPanel refreshKey={0} />
|
|
210
|
+
</Harness>,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
await waitFor(() => {
|
|
214
|
+
expect(screen.getByTestId('memory-save-vault-path')).toBeInTheDocument();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Change the vault path input
|
|
218
|
+
const input = screen.getByTestId('memory-vault-path-input');
|
|
219
|
+
await user.clear(input);
|
|
220
|
+
await user.type(input, '/new/path/.bizar_memory');
|
|
221
|
+
|
|
222
|
+
await user.click(screen.getByTestId('memory-save-vault-path'));
|
|
223
|
+
|
|
224
|
+
// v6.x: should call /memory/config/vault (not just show a message)
|
|
225
|
+
expect(post).toHaveBeenCalledWith('/memory/config/vault', { vaultRoot: '/new/path/.bizar_memory' });
|
|
226
|
+
});
|
|
188
227
|
});
|
|
189
228
|
|
|
190
229
|
describe('MemorySection (settings) vault path', () => {
|