@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.
- package/bizar-dash/dist/assets/{EnvVarsSection-L413CQEI.js → EnvVarsSection-DoIzqrlb.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-L413CQEI.js.map → EnvVarsSection-DoIzqrlb.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-0kGQb1S6.js → MobileChat-CoUCUsbr.js} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-0kGQb1S6.js.map → MobileChat-CoUCUsbr.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-BeO04g52.js +1 -0
- package/bizar-dash/dist/assets/MobileSettings-BeO04g52.js.map +1 -0
- package/bizar-dash/dist/assets/{Toast-C6f1zFJ2.js → Toast-D9nv5N6m.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-C6f1zFJ2.js.map → Toast-D9nv5N6m.js.map} +1 -1
- package/bizar-dash/dist/assets/{icons-BsWXWkEF.js → icons-Btipv2pp.js} +100 -90
- 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-BklYy02e.css → main-UG4jKK-F.css} +1 -1
- package/bizar-dash/dist/assets/mobile-H5KH9cTL.js +1 -0
- package/bizar-dash/dist/assets/{mobile-CsaN4CxH.js.map → mobile-H5KH9cTL.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-layout-iFBdjEE4.js → mobile-layout-BqHxVb6_.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-layout-iFBdjEE4.js.map → mobile-layout-BqHxVb6_.js.map} +1 -1
- package/bizar-dash/dist/assets/{useSlashCommands-DGJcC9iQ.js → useSlashCommands-C24-O1e7.js} +2 -2
- package/bizar-dash/dist/assets/{useSlashCommands-DGJcC9iQ.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/bg-spawner.mjs +522 -305
- package/bizar-dash/src/server/headroom.mjs +3 -1
- package/bizar-dash/src/server/memory-lightrag.mjs +26 -2
- package/bizar-dash/src/server/memory-store.mjs +74 -28
- package/bizar-dash/src/server/opencode-sdk.mjs +63 -3
- package/bizar-dash/src/server/plugins/registry.mjs +1 -1
- package/bizar-dash/src/server/routes/activity.mjs +85 -0
- package/bizar-dash/src/server/routes/background.mjs +31 -13
- package/bizar-dash/src/server/routes/memory.mjs +91 -38
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +1 -1
- package/bizar-dash/src/server/server.mjs +4 -1
- package/bizar-dash/src/web/components/Sidebar.tsx +0 -1
- package/bizar-dash/src/web/styles/main.css +52 -13
- package/bizar-dash/src/web/styles/memory.css +17 -0
- package/bizar-dash/src/web/styles/settings.css +6 -56
- package/bizar-dash/src/web/views/Marketplace.tsx +192 -31
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +157 -7
- package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +11 -0
- package/bizar-dash/src/web/views/settings/MemorySection.tsx +65 -11
- package/bizar-dash/tests/background-sdk-session.test.mjs +94 -0
- package/bizar-dash/tests/background-session-events.test.mjs +74 -0
- package/bizar-dash/tests/background-steer-sdk.test.mjs +71 -0
- package/bizar-dash/tests/background-steer.test.mjs +25 -7
- 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-mode-wiring.test.tsx +1 -1
- package/bizar-dash/tests/settings-nav.test.tsx +0 -1
- package/bizar-dash/tests/views/Marketplace.test.tsx +200 -0
- package/bizar-dash/tests/views/Memory.test.tsx +274 -0
- package/bizar-dash/tests/views/sidebar.test.tsx +116 -0
- package/bizar-dash/tests/views/topbar.test.tsx +87 -0
- package/cli/commands/marketplace.mjs +1 -1
- package/cli/memory.mjs +1 -1
- package/package.json +1 -1
- package/plugins/bizar/src/background-state.ts +11 -0
- package/plugins/bizar/src/background.ts +212 -34
- package/plugins/bizar/src/opencode-runner.ts +80 -353
- package/plugins/bizar/src/tools/bg-send-message.ts +160 -61
- package/plugins/bizar/src/tools/bg-spawn.ts +235 -156
- package/plugins/bizar/tests/tools/bg-send-message.test.ts +84 -11
- package/plugins/bizar/tests/tools/bg-spawn-http.test.ts +223 -0
- package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js +0 -1
- package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js.map +0 -1
- package/bizar-dash/dist/assets/icons-BsWXWkEF.js.map +0 -1
- package/bizar-dash/dist/assets/main-CBTgYSeG.js +0 -18
- package/bizar-dash/dist/assets/main-CBTgYSeG.js.map +0 -1
- 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-
|
|
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, ...).
|