@polderlabs/bizar 5.5.0 → 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.
Files changed (71) hide show
  1. package/bizar-dash/dist/assets/{EnvVarsSection-L413CQEI.js → EnvVarsSection-DoIzqrlb.js} +3 -3
  2. package/bizar-dash/dist/assets/{EnvVarsSection-L413CQEI.js.map → EnvVarsSection-DoIzqrlb.js.map} +1 -1
  3. package/bizar-dash/dist/assets/{MobileChat-0kGQb1S6.js → MobileChat-CoUCUsbr.js} +1 -1
  4. package/bizar-dash/dist/assets/{MobileChat-0kGQb1S6.js.map → MobileChat-CoUCUsbr.js.map} +1 -1
  5. package/bizar-dash/dist/assets/MobileSettings-BeO04g52.js +1 -0
  6. package/bizar-dash/dist/assets/MobileSettings-BeO04g52.js.map +1 -0
  7. package/bizar-dash/dist/assets/{Toast-C6f1zFJ2.js → Toast-D9nv5N6m.js} +1 -1
  8. package/bizar-dash/dist/assets/{Toast-C6f1zFJ2.js.map → Toast-D9nv5N6m.js.map} +1 -1
  9. package/bizar-dash/dist/assets/{icons-BsWXWkEF.js → icons-Btipv2pp.js} +100 -90
  10. package/bizar-dash/dist/assets/icons-Btipv2pp.js.map +1 -0
  11. package/bizar-dash/dist/assets/main-DHXPuddY.js +18 -0
  12. package/bizar-dash/dist/assets/main-DHXPuddY.js.map +1 -0
  13. package/bizar-dash/dist/assets/{main-BklYy02e.css → main-UG4jKK-F.css} +1 -1
  14. package/bizar-dash/dist/assets/mobile-H5KH9cTL.js +1 -0
  15. package/bizar-dash/dist/assets/{mobile-CsaN4CxH.js.map → mobile-H5KH9cTL.js.map} +1 -1
  16. package/bizar-dash/dist/assets/{mobile-layout-iFBdjEE4.js → mobile-layout-BqHxVb6_.js} +2 -2
  17. package/bizar-dash/dist/assets/{mobile-layout-iFBdjEE4.js.map → mobile-layout-BqHxVb6_.js.map} +1 -1
  18. package/bizar-dash/dist/assets/{useSlashCommands-DGJcC9iQ.js → useSlashCommands-C24-O1e7.js} +2 -2
  19. package/bizar-dash/dist/assets/{useSlashCommands-DGJcC9iQ.js.map → useSlashCommands-C24-O1e7.js.map} +1 -1
  20. package/bizar-dash/dist/index.html +7 -7
  21. package/bizar-dash/dist/mobile.html +3 -3
  22. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  23. package/bizar-dash/src/server/bg-spawner.mjs +522 -305
  24. package/bizar-dash/src/server/headroom.mjs +3 -1
  25. package/bizar-dash/src/server/memory-lightrag.mjs +26 -2
  26. package/bizar-dash/src/server/memory-store.mjs +74 -28
  27. package/bizar-dash/src/server/opencode-sdk.mjs +63 -3
  28. package/bizar-dash/src/server/plugins/registry.mjs +1 -1
  29. package/bizar-dash/src/server/routes/activity.mjs +85 -0
  30. package/bizar-dash/src/server/routes/background.mjs +31 -13
  31. package/bizar-dash/src/server/routes/memory.mjs +91 -38
  32. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +1 -1
  33. package/bizar-dash/src/server/server.mjs +4 -1
  34. package/bizar-dash/src/web/components/Sidebar.tsx +0 -1
  35. package/bizar-dash/src/web/styles/main.css +52 -13
  36. package/bizar-dash/src/web/styles/memory.css +17 -0
  37. package/bizar-dash/src/web/styles/settings.css +6 -56
  38. package/bizar-dash/src/web/views/Marketplace.tsx +192 -31
  39. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +157 -7
  40. package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +11 -0
  41. package/bizar-dash/src/web/views/settings/MemorySection.tsx +65 -11
  42. package/bizar-dash/tests/background-sdk-session.test.mjs +94 -0
  43. package/bizar-dash/tests/background-session-events.test.mjs +74 -0
  44. package/bizar-dash/tests/background-steer-sdk.test.mjs +71 -0
  45. package/bizar-dash/tests/background-steer.test.mjs +25 -7
  46. package/bizar-dash/tests/memory-namespace.test.mjs +2 -2
  47. package/bizar-dash/tests/memory-status-root.test.mjs +144 -0
  48. package/bizar-dash/tests/memory-store.test.mjs +5 -4
  49. package/bizar-dash/tests/memory-vault-config-endpoint.test.mjs +173 -0
  50. package/bizar-dash/tests/settings-mode-wiring.test.tsx +1 -1
  51. package/bizar-dash/tests/settings-nav.test.tsx +0 -1
  52. package/bizar-dash/tests/views/Marketplace.test.tsx +200 -0
  53. package/bizar-dash/tests/views/Memory.test.tsx +274 -0
  54. package/bizar-dash/tests/views/sidebar.test.tsx +116 -0
  55. package/bizar-dash/tests/views/topbar.test.tsx +87 -0
  56. package/cli/commands/marketplace.mjs +1 -1
  57. package/cli/memory.mjs +1 -1
  58. package/package.json +1 -1
  59. package/plugins/bizar/src/background-state.ts +11 -0
  60. package/plugins/bizar/src/background.ts +212 -34
  61. package/plugins/bizar/src/opencode-runner.ts +80 -353
  62. package/plugins/bizar/src/tools/bg-send-message.ts +160 -61
  63. package/plugins/bizar/src/tools/bg-spawn.ts +235 -156
  64. package/plugins/bizar/tests/tools/bg-send-message.test.ts +84 -11
  65. package/plugins/bizar/tests/tools/bg-spawn-http.test.ts +223 -0
  66. package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js +0 -1
  67. package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js.map +0 -1
  68. package/bizar-dash/dist/assets/icons-BsWXWkEF.js.map +0 -1
  69. package/bizar-dash/dist/assets/main-CBTgYSeG.js +0 -18
  70. package/bizar-dash/dist/assets/main-CBTgYSeG.js.map +0 -1
  71. package/bizar-dash/dist/assets/mobile-CsaN4CxH.js +0 -1
@@ -0,0 +1,200 @@
1
+ /**
2
+ * tests/views/Marketplace.test.tsx
3
+ *
4
+ * v6.x — Marketplace view tests. Verifies the new UX:
5
+ * - Loading state shows a "Fetching registry from <url>" message.
6
+ * - Success state surfaces the registry source URL.
7
+ * - Empty state shows a friendly "No plugins published yet" message.
8
+ * - Refresh button re-fetches the registry.
9
+ * - Category filter works (still works after the rewrite).
10
+ */
11
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
12
+ import { render, screen, waitFor } from '@testing-library/react';
13
+ import userEvent from '@testing-library/user-event';
14
+ import React from 'react';
15
+ import { ToastProvider } from '../../src/web/components/Toast';
16
+ import { ModalProvider } from '../../src/web/components/Modal';
17
+ import { Marketplace } from '../../src/web/views/Marketplace';
18
+ import { api, ApiError } from '../../src/web/lib/api';
19
+
20
+ vi.mock('../../src/web/lib/api', () => {
21
+ const get = vi.fn();
22
+ const post = vi.fn();
23
+ return {
24
+ api: { get, put: vi.fn(), post, del: vi.fn(), setToken: vi.fn(), probeAuthStatus: vi.fn() },
25
+ ApiError: class ApiError extends Error {
26
+ status: number;
27
+ data: unknown;
28
+ constructor(status: number, message: string, data: unknown = null) {
29
+ super(message);
30
+ this.status = status;
31
+ this.data = data;
32
+ }
33
+ },
34
+ };
35
+ });
36
+
37
+ const Harness = () => (
38
+ <ToastProvider>
39
+ <ModalProvider>
40
+ <Marketplace />
41
+ </ModalProvider>
42
+ </ToastProvider>
43
+ );
44
+
45
+ const samplePlugins = [
46
+ {
47
+ id: 'plugin-a',
48
+ name: 'Plugin A',
49
+ version: '1.0.0',
50
+ author: 'Alice',
51
+ description: 'First test plugin',
52
+ category: 'Utilities',
53
+ tags: ['tool'],
54
+ permissions: [],
55
+ },
56
+ {
57
+ id: 'plugin-b',
58
+ name: 'Plugin B',
59
+ version: '0.2.1',
60
+ author: 'Bob',
61
+ description: 'Second test plugin',
62
+ category: 'Integrations',
63
+ tags: ['github'],
64
+ permissions: ['net'],
65
+ },
66
+ ];
67
+
68
+ describe('Marketplace view', () => {
69
+ beforeEach(() => {
70
+ vi.clearAllMocks();
71
+ });
72
+
73
+ afterEach(() => {
74
+ vi.restoreAllMocks();
75
+ });
76
+
77
+ it('shows a loading state while the registry is being fetched', () => {
78
+ (api.get as ReturnType<typeof vi.fn>).mockReturnValue(new Promise(() => {})); // never resolves
79
+ render(<Harness />);
80
+ expect(screen.getByTestId('marketplace-loading')).toBeInTheDocument();
81
+ // Both the subtitle ("Fetching registry from the community server…")
82
+ // and the spinner paragraph mention "Fetching registry"; assert the
83
+ // spinner paragraph specifically.
84
+ const loading = screen.getByTestId('marketplace-loading');
85
+ expect(loading.textContent?.toLowerCase()).toContain('fetching');
86
+ });
87
+
88
+ it('surfaces the registry source URL after a successful fetch', async () => {
89
+ (api.get as ReturnType<typeof vi.fn>).mockResolvedValue({
90
+ plugins: samplePlugins,
91
+ registry: { source: 'https://github.com/DrB0rk/bizar-mods', updatedAt: null },
92
+ });
93
+
94
+ render(<Harness />);
95
+
96
+ await waitFor(() => {
97
+ const source = screen.getByTestId('marketplace-source');
98
+ expect(source.textContent).toContain('https://github.com/DrB0rk/bizar-mods');
99
+ });
100
+
101
+ // Plugin count pill
102
+ const count = screen.getByTestId('marketplace-count');
103
+ expect(count.textContent).toMatch(/2 plugins/);
104
+ });
105
+
106
+ it('shows a "cached" notice when the server reports cached data', async () => {
107
+ (api.get as ReturnType<typeof vi.fn>).mockResolvedValue({
108
+ plugins: samplePlugins,
109
+ registry: { source: 'https://github.com/DrB0rk/bizar-mods', updatedAt: null },
110
+ cached: true,
111
+ cacheTimestamp: Date.now() - 1000 * 60 * 30, // 30 min ago
112
+ });
113
+
114
+ render(<Harness />);
115
+
116
+ await waitFor(() => {
117
+ expect(screen.getByTestId('marketplace-cache-notice')).toBeInTheDocument();
118
+ });
119
+ expect(screen.getByTestId('marketplace-cache-notice').textContent?.toLowerCase()).toContain('cached');
120
+ });
121
+
122
+ it('renders a friendly empty state when the registry has zero plugins', async () => {
123
+ (api.get as ReturnType<typeof vi.fn>).mockResolvedValue({
124
+ plugins: [],
125
+ registry: { source: 'https://github.com/DrB0rk/bizar-mods', updatedAt: null },
126
+ });
127
+
128
+ render(<Harness />);
129
+
130
+ await waitFor(() => {
131
+ expect(screen.getByText(/no plugins published yet/i)).toBeInTheDocument();
132
+ });
133
+ });
134
+
135
+ it('renders a registry-unreachable error state on failure', async () => {
136
+ (api.get as ReturnType<typeof vi.fn>).mockRejectedValue(
137
+ new ApiError(502, 'all registry URLs failed', { registryUrl: 'https://github.com/DrB0rk/bizar-mods' }),
138
+ );
139
+
140
+ render(<Harness />);
141
+
142
+ await waitFor(() => {
143
+ expect(screen.getByText(/registry unreachable/i)).toBeInTheDocument();
144
+ });
145
+ // URL surfaced in error state
146
+ expect(screen.getByText(/github\.com\/DrB0rk\/bizar-mods/i)).toBeInTheDocument();
147
+ });
148
+
149
+ it('re-fetches the registry when the Refresh button is clicked', async () => {
150
+ (api.get as ReturnType<typeof vi.fn>).mockResolvedValue({
151
+ plugins: samplePlugins,
152
+ registry: { source: 'https://github.com/DrB0rk/bizar-mods', updatedAt: null },
153
+ });
154
+
155
+ const user = userEvent.setup();
156
+ render(<Harness />);
157
+
158
+ await waitFor(() => {
159
+ expect(screen.getByTestId('marketplace-source')).toBeInTheDocument();
160
+ });
161
+
162
+ (api.get as ReturnType<typeof vi.fn>).mockClear();
163
+ await user.click(screen.getByTestId('marketplace-refresh'));
164
+ await waitFor(() => {
165
+ expect(api.get).toHaveBeenCalled();
166
+ });
167
+ });
168
+
169
+ it('filters plugins by category when a chip is clicked', async () => {
170
+ (api.get as ReturnType<typeof vi.fn>).mockResolvedValue({
171
+ plugins: samplePlugins,
172
+ registry: { source: 'https://github.com/DrB0rk/bizar-mods', updatedAt: null },
173
+ });
174
+
175
+ const user = userEvent.setup();
176
+ render(<Harness />);
177
+
178
+ await waitFor(() => {
179
+ expect(screen.getByTestId('marketplace-grid')).toBeInTheDocument();
180
+ });
181
+
182
+ // Initially both plugins visible.
183
+ expect(screen.getByText('Plugin A')).toBeInTheDocument();
184
+ expect(screen.getByText('Plugin B')).toBeInTheDocument();
185
+
186
+ // Click the Integrations chip — only Plugin B should remain.
187
+ await user.click(screen.getByTestId('marketplace-category-integrations'));
188
+ await waitFor(() => {
189
+ expect(screen.queryByText('Plugin A')).not.toBeInTheDocument();
190
+ expect(screen.getByText('Plugin B')).toBeInTheDocument();
191
+ });
192
+
193
+ // Reset filter via "All" chip — both visible again.
194
+ await user.click(screen.getByTestId('marketplace-category-all'));
195
+ await waitFor(() => {
196
+ expect(screen.getByText('Plugin A')).toBeInTheDocument();
197
+ expect(screen.getByText('Plugin B')).toBeInTheDocument();
198
+ });
199
+ });
200
+ });
@@ -0,0 +1,274 @@
1
+ /**
2
+ * tests/views/Memory.test.tsx
3
+ *
4
+ * v6.x — Memory UI tests focused on the vault path loader fix.
5
+ * Before this fix, the ConfigPanel and MemorySection hardcoded the vault
6
+ * path to '' and rendered "(loading…)" forever. After the fix, they fetch
7
+ * /api/memory/status and render the actual path or a "not initialised"
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).
12
+ */
13
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
14
+ import { render, screen, waitFor } from '@testing-library/react';
15
+ import userEvent from '@testing-library/user-event';
16
+ import React from 'react';
17
+ import { ToastProvider } from '../../src/web/components/Toast';
18
+ import { ModalProvider } from '../../src/web/components/Modal';
19
+ import { MemoryOverview } from '../../src/web/views/memory/MemoryOverview';
20
+ import { ConfigPanel } from '../../src/web/views/memory/ConfigPanel';
21
+ import { MemorySection } from '../../src/web/views/settings/MemorySection';
22
+ import { api } from '../../src/web/lib/api';
23
+
24
+ vi.mock('../../src/web/lib/api', () => {
25
+ const get = vi.fn();
26
+ return {
27
+ api: {
28
+ get,
29
+ put: vi.fn(),
30
+ post: vi.fn(),
31
+ del: vi.fn(),
32
+ setToken: vi.fn(),
33
+ probeAuthStatus: vi.fn(),
34
+ },
35
+ ApiError: class ApiError extends Error {
36
+ status: number;
37
+ constructor(status: number, message: string) {
38
+ super(message);
39
+ this.status = status;
40
+ }
41
+ },
42
+ };
43
+ });
44
+
45
+ const Harness = ({ children }: { children: React.ReactNode }) => (
46
+ <ToastProvider>
47
+ <ModalProvider>{children}</ModalProvider>
48
+ </ToastProvider>
49
+ );
50
+
51
+ describe('MemoryOverview vault path', () => {
52
+ beforeEach(() => {
53
+ vi.clearAllMocks();
54
+ });
55
+
56
+ it('renders the resolved vault path from /memory/status', async () => {
57
+ (api.get as ReturnType<typeof vi.fn>).mockImplementation((url: string) => {
58
+ if (url === '/memory/status') return Promise.resolve({
59
+ initialized: true,
60
+ mode: 'local-only',
61
+ vaultRoot: '/Users/test/.bizar_memory',
62
+ projectVaultRoot: '/Users/test/.bizar_memory',
63
+ noteCount: 5,
64
+ gitClean: true,
65
+ branch: 'main',
66
+ });
67
+ if (url === '/memory/health') return Promise.resolve({
68
+ score: 90,
69
+ status: 'healthy',
70
+ checks: [],
71
+ message: 'all good',
72
+ });
73
+ if (url === '/memory/lightrag/stats') return Promise.resolve({
74
+ running: false, pid: null, host: '127.0.0.1', port: 9621, indexedApprox: 0, queryCountLast24h: 0, lastReindexAt: null,
75
+ });
76
+ if (url === '/memory/storage') return Promise.resolve({ total: 1024, breakdown: [] });
77
+ return Promise.resolve(null);
78
+ });
79
+
80
+ render(
81
+ <Harness>
82
+ <MemoryOverview refreshKey={0} onRefresh={vi.fn()} setActiveSubPanel={vi.fn()} />
83
+ </Harness>,
84
+ );
85
+
86
+ await waitFor(() => {
87
+ const el = screen.getByTestId('memory-overview-vault-path');
88
+ expect(el.textContent).toContain('/Users/test/.bizar_memory');
89
+ });
90
+ });
91
+
92
+ it('shows "not initialised" when status reports initialized=false', async () => {
93
+ (api.get as ReturnType<typeof vi.fn>).mockImplementation((url: string) => {
94
+ if (url === '/memory/status') return Promise.resolve({
95
+ initialized: false, mode: null, projectId: null, vaultRoot: null, projectVaultRoot: null,
96
+ });
97
+ if (url === '/memory/health') return Promise.resolve({
98
+ score: 0, status: 'unconfigured', checks: [], message: 'not initialised',
99
+ });
100
+ if (url === '/memory/lightrag/stats') return Promise.resolve({
101
+ running: false, pid: null, host: '127.0.0.1', port: 9621, indexedApprox: 0, queryCountLast24h: 0, lastReindexAt: null,
102
+ });
103
+ if (url === '/memory/storage') return Promise.resolve({ total: 0, breakdown: [] });
104
+ return Promise.resolve(null);
105
+ });
106
+
107
+ render(
108
+ <Harness>
109
+ <MemoryOverview refreshKey={0} onRefresh={vi.fn()} setActiveSubPanel={vi.fn()} />
110
+ </Harness>,
111
+ );
112
+
113
+ await waitFor(() => {
114
+ const el = screen.getByTestId('memory-overview-vault-path');
115
+ expect(el.textContent?.toLowerCase()).toContain('not initialised');
116
+ });
117
+ });
118
+ });
119
+
120
+ describe('ConfigPanel vault path', () => {
121
+ beforeEach(() => {
122
+ vi.clearAllMocks();
123
+ });
124
+
125
+ it('does not render "(loading…)" forever — shows actual vault path', async () => {
126
+ (api.get as ReturnType<typeof vi.fn>).mockImplementation((url: string) => {
127
+ if (url === '/memory/config/global') return Promise.resolve({
128
+ config: { git: { remoteUrl: '' } },
129
+ });
130
+ if (url === '/memory/status') return Promise.resolve({
131
+ initialized: true, vaultRoot: '/Users/test/.bizar_memory', mode: 'local-only',
132
+ });
133
+ if (url === '/memory/git/status') return Promise.resolve({ ok: false });
134
+ return Promise.resolve(null);
135
+ });
136
+
137
+ render(
138
+ <Harness>
139
+ <ConfigPanel refreshKey={0} />
140
+ </Harness>,
141
+ );
142
+
143
+ await waitFor(() => {
144
+ const el = screen.getByTestId('memory-current-vault-path');
145
+ expect(el.textContent).toContain('/Users/test/.bizar_memory');
146
+ });
147
+
148
+ // Sanity: the legacy "(loading…)" string is no longer rendered.
149
+ expect(screen.queryByText('(loading…)')).not.toBeInTheDocument();
150
+ });
151
+
152
+ it('shows an "Initialise default" button when not initialised', async () => {
153
+ (api.get as ReturnType<typeof vi.fn>).mockImplementation((url: string) => {
154
+ if (url === '/memory/config/global') return Promise.resolve({ config: {} });
155
+ if (url === '/memory/status') return Promise.resolve({ initialized: false, vaultRoot: null });
156
+ return Promise.resolve(null);
157
+ });
158
+
159
+ render(
160
+ <Harness>
161
+ <ConfigPanel refreshKey={0} />
162
+ </Harness>,
163
+ );
164
+
165
+ await waitFor(() => {
166
+ expect(screen.getByTestId('memory-init-vault')).toBeInTheDocument();
167
+ });
168
+ });
169
+
170
+ it('triggers /memory/init when "Initialise default" is clicked', async () => {
171
+ const post = vi.fn().mockResolvedValue({ ok: true });
172
+ (api.get as ReturnType<typeof vi.fn>).mockImplementation((url: string) => {
173
+ if (url === '/memory/config/global') return Promise.resolve({ config: {} });
174
+ if (url === '/memory/status') return Promise.resolve({ initialized: false, vaultRoot: null });
175
+ return Promise.resolve(null);
176
+ });
177
+ (api.post as ReturnType<typeof vi.fn>) = post;
178
+
179
+ const user = userEvent.setup();
180
+ render(
181
+ <Harness>
182
+ <ConfigPanel refreshKey={0} />
183
+ </Harness>,
184
+ );
185
+
186
+ await waitFor(() => {
187
+ expect(screen.getByTestId('memory-init-vault')).toBeInTheDocument();
188
+ });
189
+ await user.click(screen.getByTestId('memory-init-vault'));
190
+ expect(post).toHaveBeenCalledWith('/memory/init', expect.anything());
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
+ });
227
+ });
228
+
229
+ describe('MemorySection (settings) vault path', () => {
230
+ beforeEach(() => {
231
+ vi.clearAllMocks();
232
+ });
233
+
234
+ it('renders the vault path from /memory/status (was hardcoded to "")', async () => {
235
+ (api.get as ReturnType<typeof vi.fn>).mockImplementation((url: string) => {
236
+ if (url === '/memory/config/global') return Promise.resolve({ config: { git: { remoteUrl: '' } } });
237
+ if (url === '/memory/status') return Promise.resolve({
238
+ initialized: true, vaultRoot: '/home/test/.bizar_memory', mode: 'local-only',
239
+ });
240
+ return Promise.resolve(null);
241
+ });
242
+
243
+ render(
244
+ <Harness>
245
+ <MemorySection />
246
+ </Harness>,
247
+ );
248
+
249
+ await waitFor(() => {
250
+ const el = screen.getByTestId('settings-memory-vault-path');
251
+ expect(el.textContent).toContain('/home/test/.bizar_memory');
252
+ });
253
+ });
254
+
255
+ it('shows an Initialise button when the vault is not initialised', async () => {
256
+ (api.get as ReturnType<typeof vi.fn>).mockImplementation((url: string) => {
257
+ if (url === '/memory/config/global') return Promise.resolve({ config: {} });
258
+ if (url === '/memory/status') return Promise.resolve({ initialized: false, vaultRoot: null });
259
+ return Promise.resolve(null);
260
+ });
261
+
262
+ render(
263
+ <Harness>
264
+ <MemorySection />
265
+ </Harness>,
266
+ );
267
+
268
+ await waitFor(() => {
269
+ const el = screen.getByTestId('settings-memory-vault-path');
270
+ expect(el.textContent?.toLowerCase()).toContain('not initialised');
271
+ expect(screen.getByRole('button', { name: /initialise/i })).toBeInTheDocument();
272
+ });
273
+ });
274
+ });
@@ -0,0 +1,116 @@
1
+ /**
2
+ * tests/views/sidebar.test.tsx
3
+ *
4
+ * v6.x — Sidebar tests. Each settings section is its own top-level tab;
5
+ * there is no longer a `settingsMode` toggle or sub-nav render path.
6
+ * The Sidebar always renders the regular `.sidebar-tab` rail.
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, Bot } from 'lucide-react';
15
+
16
+ const TABS: TabDef[] = [
17
+ { id: 'overview', label: 'Overview', icon: LayoutDashboard },
18
+ { id: 'chat', label: 'Chat', icon: MessageSquare },
19
+ { id: 'agents', label: 'Agents', icon: Bot },
20
+ ];
21
+
22
+ const SETTINGS_TABS: TabDef[] = [
23
+ { id: 'settings-theme', label: 'Theme', icon: LayoutDashboard },
24
+ { id: 'settings-general', label: 'General', icon: MessageSquare },
25
+ ];
26
+
27
+ describe('Sidebar', () => {
28
+ const onTabChange = vi.fn();
29
+
30
+ beforeEach(() => {
31
+ vi.clearAllMocks();
32
+ });
33
+
34
+ it('renders the regular tab rail', () => {
35
+ render(
36
+ <Sidebar
37
+ tabs={TABS}
38
+ activeTab="overview"
39
+ onTabChange={onTabChange}
40
+ />,
41
+ );
42
+ expect(screen.getByRole('tablist')).toBeInTheDocument();
43
+ expect(screen.getByRole('tab', { name: /overview/i })).toBeInTheDocument();
44
+ expect(screen.getByRole('tab', { name: /chat/i })).toBeInTheDocument();
45
+ expect(screen.getByRole('tab', { name: /agents/i })).toBeInTheDocument();
46
+ });
47
+
48
+ it('does not render the legacy SettingsNav back button', () => {
49
+ render(
50
+ <Sidebar
51
+ tabs={TABS}
52
+ activeTab="overview"
53
+ onTabChange={onTabChange}
54
+ />,
55
+ );
56
+ expect(screen.queryByRole('button', { name: /exit settings/i })).not.toBeInTheDocument();
57
+ expect(screen.queryByRole('button', { name: /back/i })).not.toBeInTheDocument();
58
+ });
59
+
60
+ it('highlights the active tab', () => {
61
+ render(
62
+ <Sidebar
63
+ tabs={TABS}
64
+ activeTab="chat"
65
+ onTabChange={onTabChange}
66
+ />,
67
+ );
68
+ const chatTab = screen.getByRole('tab', { name: /chat/i });
69
+ expect(chatTab).toHaveClass('sidebar-tab-active');
70
+ });
71
+
72
+ it('calls onTabChange when a tab is clicked', async () => {
73
+ const user = userEvent.setup();
74
+ render(
75
+ <Sidebar
76
+ tabs={TABS}
77
+ activeTab="overview"
78
+ onTabChange={onTabChange}
79
+ />,
80
+ );
81
+ await user.click(screen.getByRole('tab', { name: /chat/i }));
82
+ expect(onTabChange).toHaveBeenCalledWith('chat');
83
+ });
84
+
85
+ it('renders settings sections as their own tabs', () => {
86
+ render(
87
+ <Sidebar
88
+ tabs={[...TABS, ...SETTINGS_TABS]}
89
+ activeTab="settings-theme"
90
+ onTabChange={onTabChange}
91
+ />,
92
+ );
93
+ expect(screen.getByRole('tab', { name: /theme/i })).toBeInTheDocument();
94
+ expect(screen.getByRole('tab', { name: /general/i })).toBeInTheDocument();
95
+ const themeTab = screen.getByRole('tab', { name: /theme/i });
96
+ expect(themeTab).toHaveClass('sidebar-tab-active');
97
+ });
98
+
99
+ it('renders mod views under a "Mods" divider', () => {
100
+ const modTab: TabDef & { isMod?: boolean } = {
101
+ id: 'graphify:web',
102
+ label: 'Graph',
103
+ icon: LayoutDashboard,
104
+ isMod: true,
105
+ };
106
+ render(
107
+ <Sidebar
108
+ tabs={[...TABS, modTab]}
109
+ activeTab="overview"
110
+ onTabChange={onTabChange}
111
+ />,
112
+ );
113
+ expect(screen.getByText('Mods')).toBeInTheDocument();
114
+ expect(screen.getByRole('tab', { name: /graph/i })).toBeInTheDocument();
115
+ });
116
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * tests/views/topbar.test.tsx
3
+ *
4
+ * v4.9.0 / v5.5.0 — Topbar tests. Verifies that:
5
+ * - The Settings tab appears as a single entry in the tab rail.
6
+ * - Clicking Settings invokes onTabChange with 'settings'.
7
+ * - The Settings tab shows the settings-mode-indicator when active.
8
+ * - All other tabs work normally.
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 { Topbar, TABS } from '../../src/web/components/Topbar';
15
+
16
+ describe('Topbar', () => {
17
+ const onTabChange = vi.fn();
18
+
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ });
22
+
23
+ const renderTopbar = (activeTab: string) =>
24
+ render(
25
+ <Topbar
26
+ activeTab={activeTab}
27
+ onTabChange={onTabChange}
28
+ wsStatus="connected"
29
+ version="v4.9.0"
30
+ activeProject={null}
31
+ projects={[]}
32
+ onProjectChange={vi.fn()}
33
+ onProjectsRefresh={vi.fn()}
34
+ onOpenSearch={vi.fn()}
35
+ />,
36
+ );
37
+
38
+ it('renders the Settings tab as a single entry', () => {
39
+ renderTopbar('overview');
40
+ expect(screen.getByRole('tab', { name: /settings/i })).toBeInTheDocument();
41
+ });
42
+
43
+ it('renders all main tabs', () => {
44
+ renderTopbar('overview');
45
+ expect(screen.getByRole('tab', { name: /overview/i })).toBeInTheDocument();
46
+ expect(screen.getByRole('tab', { name: /chat/i })).toBeInTheDocument();
47
+ expect(screen.getByRole('tab', { name: /agents/i })).toBeInTheDocument();
48
+ expect(screen.getByRole('tab', { name: /memory/i })).toBeInTheDocument();
49
+ expect(screen.getByRole('tab', { name: /settings/i })).toBeInTheDocument();
50
+ });
51
+
52
+ it('exposes the Settings tab id in TABS', () => {
53
+ const ids = TABS.map((t) => t.id);
54
+ expect(ids).toContain('settings');
55
+ // Settings should NOT have a prefix (it's a single entry, not per-section)
56
+ expect(ids).not.toContain('settings-theme');
57
+ expect(ids).not.toContain('settings-general');
58
+ });
59
+
60
+ it('calls onTabChange with "settings" when the Settings tab is clicked', async () => {
61
+ const user = userEvent.setup();
62
+ renderTopbar('overview');
63
+ await user.click(screen.getByRole('tab', { name: /settings/i }));
64
+ expect(onTabChange).toHaveBeenCalledWith('settings');
65
+ });
66
+
67
+ it('calls onTabChange with "overview" when the Overview tab is clicked', async () => {
68
+ const user = userEvent.setup();
69
+ renderTopbar('settings');
70
+ await user.click(screen.getByRole('tab', { name: /overview/i }));
71
+ expect(onTabChange).toHaveBeenCalledWith('overview');
72
+ });
73
+
74
+ it('highlights the active tab with `tab-active`', () => {
75
+ renderTopbar('settings');
76
+ const settingsTab = screen.getByRole('tab', { name: /settings/i });
77
+ expect(settingsTab).toHaveClass('tab-active');
78
+ });
79
+
80
+ it('does not render settings section tabs (they are in the sidebar when in settings mode)', () => {
81
+ renderTopbar('overview');
82
+ // These section tabs should NOT exist in the topbar
83
+ expect(screen.queryByRole('tab', { name: /theme/i })).not.toBeInTheDocument();
84
+ expect(screen.queryByRole('tab', { name: /general/i })).not.toBeInTheDocument();
85
+ expect(screen.queryByRole('tab', { name: /layout/i })).not.toBeInTheDocument();
86
+ });
87
+ });
@@ -29,7 +29,7 @@ export function showMarketplaceHelp() {
29
29
  Description:
30
30
  The marketplace is the public registry of plugins hosted at
31
31
  ${process.env.BIZAR_REGISTRY_URL ||
32
- 'https://raw.githubusercontent.com/DrB0rk/bizar-plugins/main/registry.json'}.
32
+ 'https://raw.githubusercontent.com/DrB0rk/bizar-mods/main/registry.json'}.
33
33
 
34
34
  This command is a thin alias for the \`plugin\` command — run
35
35
  \`bizar plugin --help\` for the full surface (config, update, invoke, ...).