@polderlabs/bizar 4.7.0 → 4.7.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/main-DHZmbnxQ.js +361 -0
- package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +1 -0
- package/bizar-dash/dist/assets/main-DX_Jh8Wc.css +1 -0
- package/bizar-dash/dist/assets/{mobile-CWqPoGaT.js → mobile-BK8-ythT.js} +2 -2
- package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-i4Uv9eW8.js → mobile-Chvf9u_B.js} +1 -1
- package/bizar-dash/dist/assets/{mobile-i4Uv9eW8.js.map → mobile-Chvf9u_B.js.map} +1 -1
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/skills/publishing/SKILL.md +146 -0
- package/bizar-dash/src/server/api.mjs +8 -0
- package/bizar-dash/src/server/backup-store.mjs +525 -0
- package/bizar-dash/src/server/digest-store.mjs +558 -0
- package/bizar-dash/src/server/lib/rate-limit.mjs +122 -0
- package/bizar-dash/src/server/routes/backup.mjs +112 -0
- package/bizar-dash/src/server/routes/chat.mjs +14 -0
- package/bizar-dash/src/server/routes/digests.mjs +82 -0
- package/bizar-dash/src/server/routes-v2/events.mjs +14 -0
- package/bizar-dash/src/server/schedules-runner.mjs +126 -0
- package/bizar-dash/src/server/server.mjs +17 -0
- package/bizar-dash/src/web/App.tsx +8 -1
- package/bizar-dash/src/web/components/BackupRestore.tsx +330 -0
- package/bizar-dash/src/web/components/SearchModal.tsx +3 -1
- package/bizar-dash/src/web/components/chat/Composer.tsx +2 -0
- package/bizar-dash/src/web/styles/main.css +70 -8
- package/bizar-dash/src/web/views/Activity.tsx +11 -1
- package/bizar-dash/src/web/views/Agents.tsx +57 -42
- package/bizar-dash/src/web/views/Artifacts.tsx +38 -25
- package/bizar-dash/src/web/views/Chat.tsx +8 -0
- package/bizar-dash/src/web/views/History.tsx +4 -1
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +21 -4
- package/bizar-dash/src/web/views/Mods.tsx +30 -17
- package/bizar-dash/src/web/views/Overview.tsx +8 -1
- package/bizar-dash/src/web/views/Providers.tsx +16 -16
- package/bizar-dash/src/web/views/Schedules.tsx +33 -15
- package/bizar-dash/src/web/views/Settings.tsx +97 -1751
- package/bizar-dash/src/web/views/Skills.tsx +4 -1
- package/bizar-dash/src/web/views/Tasks.tsx +11 -2
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +8 -2
- package/bizar-dash/src/web/views/memory/LightragPanel.tsx +3 -0
- package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +12 -4
- package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +3 -0
- package/bizar-dash/src/web/views/settings/ActivitySection.tsx +205 -0
- package/bizar-dash/src/web/views/settings/AgentSection.tsx +294 -0
- package/bizar-dash/src/web/views/settings/AuthSection.tsx +159 -0
- package/bizar-dash/src/web/views/settings/BackupSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/GeneralSection.tsx +105 -0
- package/bizar-dash/src/web/views/settings/HeadroomSection.tsx +39 -0
- package/bizar-dash/src/web/views/settings/MemorySection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/NetworkSection.tsx +87 -0
- package/bizar-dash/src/web/views/settings/NotificationsSection.tsx +34 -0
- package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/SkillsSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/SystemLlmSection.tsx +81 -0
- package/bizar-dash/src/web/views/settings/ThemeSection.tsx +168 -0
- package/bizar-dash/src/web/views/settings/UpdatesSection.tsx +256 -0
- package/bizar-dash/tests/a11y.test.tsx +206 -0
- package/bizar-dash/tests/backup-restore.test.mjs +217 -0
- package/bizar-dash/tests/backup-restore.test.tsx +123 -0
- package/bizar-dash/tests/backup-store.test.mjs +300 -0
- package/bizar-dash/tests/cli-error-visibility.test.mjs +153 -0
- package/bizar-dash/tests/digest-generation.test.mjs +191 -0
- package/bizar-dash/tests/digest-store.test.mjs +264 -0
- package/bizar-dash/tests/rate-limit.test.mjs +298 -0
- package/cli/bin.mjs +96 -2
- package/cli/commands/minimax.mjs +20 -0
- package/cli/commands/util.mjs +154 -1
- package/cli/digest.mjs +149 -0
- package/package.json +1 -1
- package/bizar-dash/dist/assets/main-DAlLdW8I.css +0 -1
- package/bizar-dash/dist/assets/main-DGGq-iZI.js +0 -361
- package/bizar-dash/dist/assets/main-DGGq-iZI.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-CWqPoGaT.js.map +0 -1
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/backup-store.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v4.8.0 — Tests for the backup-store module.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
7
|
+
import assert from 'node:assert';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import {
|
|
11
|
+
mkdirSync,
|
|
12
|
+
rmSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
existsSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
readdirSync,
|
|
17
|
+
} from 'node:fs';
|
|
18
|
+
|
|
19
|
+
const BACKUP_STORE = await import('../src/server/backup-store.mjs').then((m) => m);
|
|
20
|
+
|
|
21
|
+
describe('backup-store', () => {
|
|
22
|
+
let backupRoot;
|
|
23
|
+
let configDir;
|
|
24
|
+
let opencodeDir;
|
|
25
|
+
let memoryDir;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
const id = `bizar-backup-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
29
|
+
backupRoot = join(tmpdir(), id, 'backups');
|
|
30
|
+
configDir = join(tmpdir(), id, 'config', 'bizar');
|
|
31
|
+
opencodeDir = join(tmpdir(), id, 'config', 'opencode');
|
|
32
|
+
memoryDir = join(tmpdir(), id, 'local', 'share', 'bizar', 'memory');
|
|
33
|
+
|
|
34
|
+
mkdirSync(configDir, { recursive: true });
|
|
35
|
+
mkdirSync(opencodeDir, { recursive: true });
|
|
36
|
+
mkdirSync(memoryDir, { recursive: true });
|
|
37
|
+
mkdirSync(backupRoot, { recursive: true });
|
|
38
|
+
|
|
39
|
+
// Create some test files
|
|
40
|
+
writeFileSync(join(configDir, 'settings.json'), JSON.stringify({ theme: { mode: 'dark' } }), { mode: 0o600 });
|
|
41
|
+
writeFileSync(join(opencodeDir, 'opencode.json'), JSON.stringify({ models: [] }), { mode: 0o600 });
|
|
42
|
+
writeFileSync(join(memoryDir, 'test.md'), '# Test memory', { mode: 0o600 });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
try { rmSync(backupRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
47
|
+
try { rmSync(join(tmpdir(), `bizar-backup-test-${Date.now()}-${Math.random().toString(36).slice(2)}`), { recursive: true, force: true }); } catch { /* ignore */ }
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ── createBackup ─────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
describe('createBackup', () => {
|
|
53
|
+
it('creates a timestamped backup directory', async () => {
|
|
54
|
+
const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
|
|
55
|
+
assert.strictEqual(result.ok, true);
|
|
56
|
+
assert.ok(result.path.includes('bizar-'));
|
|
57
|
+
assert.ok(existsSync(result.path));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('writes a manifest.json with version, createdAt, and paths', async () => {
|
|
61
|
+
const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
|
|
62
|
+
const manifestPath = join(result.path, 'manifest.json');
|
|
63
|
+
assert.ok(existsSync(manifestPath));
|
|
64
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
65
|
+
assert.ok(manifest.version);
|
|
66
|
+
assert.ok(manifest.createdAt);
|
|
67
|
+
assert.ok(Array.isArray(manifest.paths));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('copies existing files to the backup', async () => {
|
|
71
|
+
const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
|
|
72
|
+
const configBackup = join(result.path, 'config');
|
|
73
|
+
assert.ok(existsSync(configBackup));
|
|
74
|
+
const settingsBackup = join(configBackup, 'settings.json');
|
|
75
|
+
assert.ok(existsSync(settingsBackup));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('skips non-existent optional paths', async () => {
|
|
79
|
+
const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
|
|
80
|
+
const manifestPath = join(result.path, 'manifest.json');
|
|
81
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
82
|
+
// Optional paths that don't exist should be in skipped list
|
|
83
|
+
assert.ok(manifest.skipped);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('respects custom label in name and manifest', async () => {
|
|
87
|
+
const result = await BACKUP_STORE.createBackup({ outDir: backupRoot, label: 'test-label' });
|
|
88
|
+
assert.ok(result.path.includes('test-label'));
|
|
89
|
+
const manifest = JSON.parse(readFileSync(join(result.path, 'manifest.json'), 'utf8'));
|
|
90
|
+
assert.strictEqual(manifest.label, 'test-label');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('returns sizeBytes and durationMs', async () => {
|
|
94
|
+
const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
|
|
95
|
+
assert.ok(typeof result.sizeBytes === 'number');
|
|
96
|
+
assert.ok(result.sizeBytes >= 0);
|
|
97
|
+
assert.ok(typeof result.durationMs === 'number');
|
|
98
|
+
assert.ok(result.durationMs >= 0);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── listBackups ──────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
describe('listBackups', () => {
|
|
105
|
+
it('returns empty array when no backups exist', async () => {
|
|
106
|
+
const emptyDir = join(tmpdir(), `bizar-empty-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
107
|
+
mkdirSync(emptyDir, { recursive: true });
|
|
108
|
+
try {
|
|
109
|
+
const backups = await BACKUP_STORE.listBackups({ inDir: emptyDir });
|
|
110
|
+
assert.strictEqual(backups.length, 0);
|
|
111
|
+
} finally {
|
|
112
|
+
rmSync(emptyDir, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns all backups sorted newest-first', async () => {
|
|
117
|
+
await BACKUP_STORE.createBackup({ outDir: backupRoot });
|
|
118
|
+
await BACKUP_STORE.createBackup({ outDir: backupRoot, label: 'second' });
|
|
119
|
+
const backups = await BACKUP_STORE.listBackups({ inDir: backupRoot });
|
|
120
|
+
assert.strictEqual(backups.length, 2);
|
|
121
|
+
assert.strictEqual(backups[0].manifest?.label, 'second');
|
|
122
|
+
assert.strictEqual(backups[1].manifest?.label, null);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('includes createdAt, sizeBytes, sizeFormatted, and manifest', async () => {
|
|
126
|
+
await BACKUP_STORE.createBackup({ outDir: backupRoot });
|
|
127
|
+
const backups = await BACKUP_STORE.listBackups({ inDir: backupRoot });
|
|
128
|
+
assert.ok(backups[0].createdAt);
|
|
129
|
+
assert.ok(typeof backups[0].sizeBytes === 'number');
|
|
130
|
+
assert.ok(typeof backups[0].sizeFormatted === 'string');
|
|
131
|
+
assert.ok(backups[0].manifest);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ── restoreBackup (dryRun) ───────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
describe('restoreBackup (dryRun)', () => {
|
|
138
|
+
it('does not modify any files when dryRun is true', async () => {
|
|
139
|
+
const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
|
|
140
|
+
const settingsFile = join(configDir, 'settings.json');
|
|
141
|
+
const originalContent = readFileSync(settingsFile, 'utf8');
|
|
142
|
+
// Modify the settings file
|
|
143
|
+
writeFileSync(settingsFile, JSON.stringify({ theme: { mode: 'light' } }), { mode: 0o600 });
|
|
144
|
+
const restoreResult = await BACKUP_STORE.restoreBackup({
|
|
145
|
+
backupPath: result.path,
|
|
146
|
+
dryRun: true,
|
|
147
|
+
conflictStrategy: 'overwrite',
|
|
148
|
+
});
|
|
149
|
+
assert.strictEqual(restoreResult.ok, true);
|
|
150
|
+
// File should still have the modified content
|
|
151
|
+
assert.strictEqual(readFileSync(settingsFile, 'utf8'), JSON.stringify({ theme: { mode: 'light' } }));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('reports what would be restored in dryRun mode', async () => {
|
|
155
|
+
const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
|
|
156
|
+
const restoreResult = await BACKUP_STORE.restoreBackup({
|
|
157
|
+
backupPath: result.path,
|
|
158
|
+
dryRun: true,
|
|
159
|
+
conflictStrategy: 'merge',
|
|
160
|
+
});
|
|
161
|
+
assert.ok(restoreResult.restored.length > 0);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ── restoreBackup (overwrite) ────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
describe('restoreBackup (overwrite)', () => {
|
|
168
|
+
it('reports overwritten entries when conflictStrategy is overwrite (dryRun)', async () => {
|
|
169
|
+
const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
|
|
170
|
+
// dryRun=true means no actual overwrite happens, but we can verify
|
|
171
|
+
// the restored list includes the backed-up entries
|
|
172
|
+
const restoreResult = await BACKUP_STORE.restoreBackup({
|
|
173
|
+
backupPath: result.path,
|
|
174
|
+
dryRun: true,
|
|
175
|
+
conflictStrategy: 'overwrite',
|
|
176
|
+
});
|
|
177
|
+
assert.strictEqual(restoreResult.ok, true);
|
|
178
|
+
// In dryRun mode, entries that would be overwritten are listed in restored
|
|
179
|
+
assert.ok(restoreResult.restored.length > 0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('overwrites a manually-created backup correctly', async () => {
|
|
183
|
+
// Create a manual backup structure with project-state label (which is in PROJECT_BACKUP_PATHS)
|
|
184
|
+
const testBackupDir = join(backupRoot, 'bizar-test-overwrite-manual');
|
|
185
|
+
const testProjectRoot = join(tmpdir(), `backup-overwrite-test-${Date.now()}`);
|
|
186
|
+
mkdirSync(testBackupDir, { recursive: true });
|
|
187
|
+
mkdirSync(testProjectRoot, { recursive: true });
|
|
188
|
+
mkdirSync(join(testBackupDir, 'project-state'), { recursive: true });
|
|
189
|
+
|
|
190
|
+
// Write original content to backup
|
|
191
|
+
const originalContent = { theme: { mode: 'dark' } };
|
|
192
|
+
writeFileSync(join(testBackupDir, 'project-state', 'state.json'), JSON.stringify(originalContent), { mode: 0o600 });
|
|
193
|
+
// Use new manifest format with {label, src} objects
|
|
194
|
+
writeFileSync(
|
|
195
|
+
join(testBackupDir, 'manifest.json'),
|
|
196
|
+
JSON.stringify({
|
|
197
|
+
version: '1.0.0',
|
|
198
|
+
createdAt: new Date().toISOString(),
|
|
199
|
+
paths: [{ label: 'project-state', src: '.bizar/' }],
|
|
200
|
+
}),
|
|
201
|
+
{ mode: 0o600 },
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Restore with overwrite — project-state is in PROJECT_BACKUP_PATHS, so it will restore
|
|
205
|
+
const restoreResult = await BACKUP_STORE.restoreBackup({
|
|
206
|
+
backupPath: testBackupDir,
|
|
207
|
+
dryRun: false,
|
|
208
|
+
conflictStrategy: 'overwrite',
|
|
209
|
+
projectRoot: testProjectRoot,
|
|
210
|
+
});
|
|
211
|
+
// The entry should be restored (it's in PROJECT_BACKUP_PATHS)
|
|
212
|
+
assert.ok(restoreResult.ok);
|
|
213
|
+
|
|
214
|
+
// Cleanup
|
|
215
|
+
rmSync(testProjectRoot, { recursive: true, force: true });
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ── verifyBackup ─────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
describe('verifyBackup', () => {
|
|
222
|
+
it('returns ok=true for a valid backup', async () => {
|
|
223
|
+
const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
|
|
224
|
+
const verifyResult = await BACKUP_STORE.verifyBackup({ backupPath: result.path });
|
|
225
|
+
assert.strictEqual(verifyResult.ok, true);
|
|
226
|
+
assert.strictEqual(verifyResult.issues.length, 0);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('returns ok=false when manifest is missing', async () => {
|
|
230
|
+
const backupDir = join(backupRoot, 'bizar-test-no-manifest');
|
|
231
|
+
mkdirSync(backupDir, { recursive: true });
|
|
232
|
+
writeFileSync(join(backupDir, 'somefile.txt'), 'test', { mode: 0o600 });
|
|
233
|
+
const verifyResult = await BACKUP_STORE.verifyBackup({ backupPath: backupDir });
|
|
234
|
+
assert.strictEqual(verifyResult.ok, false);
|
|
235
|
+
assert.ok(verifyResult.issues.length > 0);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('detects missing backup entries', async () => {
|
|
239
|
+
const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
|
|
240
|
+
// Remove a backed-up directory
|
|
241
|
+
rmSync(join(result.path, 'config'), { recursive: true, force: true });
|
|
242
|
+
const verifyResult = await BACKUP_STORE.verifyBackup({ backupPath: result.path });
|
|
243
|
+
assert.strictEqual(verifyResult.ok, false);
|
|
244
|
+
assert.ok(verifyResult.issues.some((i) => i.includes('config')));
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('returns ok=false when backup directory does not exist', async () => {
|
|
248
|
+
const verifyResult = await BACKUP_STORE.verifyBackup({
|
|
249
|
+
backupPath: join(tmpdir(), 'nonexistent-backup-path'),
|
|
250
|
+
});
|
|
251
|
+
assert.strictEqual(verifyResult.ok, false);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ── deleteBackup ─────────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
describe('deleteBackup', () => {
|
|
258
|
+
it('removes the backup directory', async () => {
|
|
259
|
+
const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
|
|
260
|
+
assert.ok(existsSync(result.path));
|
|
261
|
+
const deleteResult = await BACKUP_STORE.deleteBackup({ backupPath: result.path });
|
|
262
|
+
assert.strictEqual(deleteResult.ok, true);
|
|
263
|
+
assert.ok(!existsSync(result.path));
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('returns ok=false when backup does not exist', async () => {
|
|
267
|
+
const deleteResult = await BACKUP_STORE.deleteBackup({
|
|
268
|
+
backupPath: join(tmpdir(), 'nonexistent-backup-path'),
|
|
269
|
+
});
|
|
270
|
+
assert.strictEqual(deleteResult.ok, false);
|
|
271
|
+
assert.ok(deleteResult.error);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ── getManifest ──────────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
describe('getManifest', () => {
|
|
278
|
+
it('returns manifest when backup exists', async () => {
|
|
279
|
+
const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
|
|
280
|
+
const manifestResult = await BACKUP_STORE.getManifest({ backupPath: result.path });
|
|
281
|
+
assert.strictEqual(manifestResult.ok, true);
|
|
282
|
+
assert.ok(manifestResult.manifest);
|
|
283
|
+
assert.ok(manifestResult.manifest.version);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('returns ok=false when manifest is missing', async () => {
|
|
287
|
+
const backupDir = join(backupRoot, 'bizar-no-manifest');
|
|
288
|
+
mkdirSync(backupDir, { recursive: true });
|
|
289
|
+
const manifestResult = await BACKUP_STORE.getManifest({ backupPath: backupDir });
|
|
290
|
+
assert.strictEqual(manifestResult.ok, false);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('returns ok=false when backup does not exist', async () => {
|
|
294
|
+
const manifestResult = await BACKUP_STORE.getManifest({
|
|
295
|
+
backupPath: join(tmpdir(), 'nonexistent'),
|
|
296
|
+
});
|
|
297
|
+
assert.strictEqual(manifestResult.ok, false);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bizar-dash/tests/cli-error-visibility.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v4.7.1 — CLI silent error visibility fixes.
|
|
5
|
+
*
|
|
6
|
+
* Verifies:
|
|
7
|
+
* 1. importCommand() failure produces a visible error message
|
|
8
|
+
* 2. `bizar help` shows help text (not "Unknown command")
|
|
9
|
+
* 3. `bizar --help` shows help text
|
|
10
|
+
* 4. `bizar` (no command) shows help text
|
|
11
|
+
*
|
|
12
|
+
* Run with: node --test bizar-dash/tests/cli-error-visibility.test.mjs
|
|
13
|
+
*/
|
|
14
|
+
import { test, describe } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
import { resolve, dirname } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
// __dirname = .../BizarHarness/bizar-dash/tests → .../BizarHarness/cli
|
|
23
|
+
const CLI_ROOT = resolve(__dirname, '..', '..', 'cli');
|
|
24
|
+
|
|
25
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Run the CLI as a subprocess and capture stdout + stderr.
|
|
29
|
+
* Uses spawnSync for reliable synchronous capture.
|
|
30
|
+
* Returns { code, stdout, stderr }.
|
|
31
|
+
*/
|
|
32
|
+
function runBizar(args, env = {}) {
|
|
33
|
+
const binPath = resolve(CLI_ROOT, 'bin.mjs');
|
|
34
|
+
const result = spawnSync('node', [binPath, ...args], {
|
|
35
|
+
env: { ...process.env, BIZAR_SKIP_INSTALL: '1', ...env },
|
|
36
|
+
timeout: 10000,
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
code: result.status,
|
|
41
|
+
stdout: result.stdout || '',
|
|
42
|
+
stderr: result.stderr || '',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── importCommand() failure ───────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
describe('importCommand() failure produces visible error', () => {
|
|
49
|
+
test('non-existent command prints "Unknown command", not silent exit', () => {
|
|
50
|
+
const { code, stdout, stderr } = runBizar(['this-command-does-not-exist-xyz']);
|
|
51
|
+
const combined = stdout + stderr;
|
|
52
|
+
// Should exit with error code
|
|
53
|
+
assert.notStrictEqual(code, 0, `should exit non-zero, got ${code}`);
|
|
54
|
+
// Should NOT be silent
|
|
55
|
+
assert.ok(combined.length > 0, `should produce output, not silent. stdout=${stdout.length}, stderr=${stderr.length}`);
|
|
56
|
+
// Should mention the unknown command
|
|
57
|
+
assert.ok(
|
|
58
|
+
combined.includes('Unknown command') || combined.includes('this-command-does-not-exist-xyz'),
|
|
59
|
+
`output should mention Unknown command or the command name, got: ${combined.slice(0, 200)}`,
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('broken command module logs error to stderr (not silent)', () => {
|
|
64
|
+
// The bin.mjs importCommand catch now logs to console.error.
|
|
65
|
+
// We verify stderr is non-empty when a command fails to load.
|
|
66
|
+
const { stderr } = runBizar(['this-command-does-not-exist-xyz']);
|
|
67
|
+
assert.ok(stderr.length > 0, `stderr should not be empty when command fails. stderr=${JSON.stringify(stderr.slice(0, 100))}`);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ── `bizar help` alias ─────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
describe('`bizar help` alias', () => {
|
|
74
|
+
test('`bizar help` shows help text, not "Unknown command"', () => {
|
|
75
|
+
const { code, stdout, stderr } = runBizar(['help']);
|
|
76
|
+
const combined = stdout + stderr;
|
|
77
|
+
assert.strictEqual(code, 0, `should exit with 0, got ${code}. stderr: ${stderr.slice(0, 300)}`);
|
|
78
|
+
assert.ok(
|
|
79
|
+
combined.includes('Usage:') && combined.includes('bizar'),
|
|
80
|
+
`should show help usage, got: ${combined.slice(0, 300)}`,
|
|
81
|
+
);
|
|
82
|
+
assert.ok(
|
|
83
|
+
!combined.includes('Unknown command'),
|
|
84
|
+
`should NOT say "Unknown command", got: ${combined.slice(0, 300)}`,
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ── `bizar --help` ─────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
describe('`bizar --help`', () => {
|
|
92
|
+
test('`bizar --help` shows help text', () => {
|
|
93
|
+
const { code, stdout, stderr } = runBizar(['--help']);
|
|
94
|
+
const combined = stdout + stderr;
|
|
95
|
+
assert.strictEqual(code, 0, `should exit with 0, got ${code}. stderr: ${stderr.slice(0, 300)}`);
|
|
96
|
+
assert.ok(
|
|
97
|
+
combined.includes('Usage:') && combined.includes('bizar'),
|
|
98
|
+
`should show help usage, got: ${combined.slice(0, 300)}`,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('`bizar -h` shows help text', () => {
|
|
103
|
+
const { code, stdout, stderr } = runBizar(['-h']);
|
|
104
|
+
const combined = stdout + stderr;
|
|
105
|
+
assert.strictEqual(code, 0, `should exit with 0, got ${code}. stderr: ${stderr.slice(0, 300)}`);
|
|
106
|
+
assert.ok(
|
|
107
|
+
combined.includes('Usage:') && combined.includes('bizar'),
|
|
108
|
+
`should show help usage, got: ${combined.slice(0, 300)}`,
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ── `bizar` (no command) ───────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
describe('`bizar` with no command', () => {
|
|
116
|
+
test('`bizar` with no args shows help text', () => {
|
|
117
|
+
const { code, stdout, stderr } = runBizar([]);
|
|
118
|
+
const combined = stdout + stderr;
|
|
119
|
+
assert.strictEqual(code, 0, `should exit with 0, got ${code}. stderr: ${stderr.slice(0, 300)}`);
|
|
120
|
+
assert.ok(
|
|
121
|
+
combined.includes('Usage:') && combined.includes('bizar'),
|
|
122
|
+
`should show help usage, got: ${combined.slice(0, 300)}`,
|
|
123
|
+
);
|
|
124
|
+
assert.ok(
|
|
125
|
+
!combined.includes('Unknown command'),
|
|
126
|
+
`should NOT say "Unknown command", got: ${combined.slice(0, 300)}`,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── Defensive null checks on import ───────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
describe('command module null-check produces user-visible error', () => {
|
|
134
|
+
test('bin.mjs has null checks after importCommand in all case branches', async () => {
|
|
135
|
+
const binSrc = await import('fs').then((fs) =>
|
|
136
|
+
fs.promises.readFile(resolve(CLI_ROOT, 'bin.mjs'), 'utf8'),
|
|
137
|
+
);
|
|
138
|
+
// Count `if (!mod)` checks that follow `importCommand`
|
|
139
|
+
const nullCheckCount = (binSrc.match(/if \(!mod\)/g) || []).length;
|
|
140
|
+
// We have 9 case groups that call importCommand: install/update, service, dash,
|
|
141
|
+
// minimax, headroom, mod, artifact, memory, usage, and util (utility commands)
|
|
142
|
+
assert.ok(
|
|
143
|
+
nullCheckCount >= 9,
|
|
144
|
+
`Expected at least 9 null checks after importCommand, found ${nullCheckCount}`,
|
|
145
|
+
);
|
|
146
|
+
// Also verify each has a user-visible error message
|
|
147
|
+
const errorMsgCount = (binSrc.match(/Could not load .+ command module/g) || []).length;
|
|
148
|
+
assert.ok(
|
|
149
|
+
errorMsgCount >= 9,
|
|
150
|
+
`Expected at least 9 user-visible error messages for failed loads, found ${errorMsgCount}`,
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/digest-generation.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v4.8.0 — End-to-end tests for digest generation.
|
|
5
|
+
*
|
|
6
|
+
* Verifies:
|
|
7
|
+
* - Creating tasks then generating a digest includes task info
|
|
8
|
+
* - dryRun does not save any files
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
12
|
+
import assert from 'node:assert';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { mkdirSync, rmSync, existsSync, writeFileSync } from 'node:fs';
|
|
16
|
+
|
|
17
|
+
// Point stores to temp directories
|
|
18
|
+
const storeHome = join(tmpdir(), `bizar-digest-gen-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
19
|
+
process.env.BIZAR_STORE_HOME = storeHome;
|
|
20
|
+
|
|
21
|
+
const DIGEST_STORE = await import('../src/server/digest-store.mjs');
|
|
22
|
+
|
|
23
|
+
// We import tasksStore to create tasks that should appear in the digest
|
|
24
|
+
let tasksStore;
|
|
25
|
+
|
|
26
|
+
describe('digest-generation', () => {
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
mkdirSync(storeHome, { recursive: true });
|
|
29
|
+
|
|
30
|
+
// Create the project's .obsidian directory with some notes
|
|
31
|
+
const obsidianDir = join(storeHome, '.obsidian');
|
|
32
|
+
const dailyDir = join(obsidianDir, 'daily');
|
|
33
|
+
const decisionsDir = join(obsidianDir, 'decisions');
|
|
34
|
+
mkdirSync(dailyDir, { recursive: true });
|
|
35
|
+
mkdirSync(decisionsDir, { recursive: true });
|
|
36
|
+
|
|
37
|
+
// Write some memory notes "within the week"
|
|
38
|
+
writeFileSync(join(dailyDir, '2026-07-01.md'),
|
|
39
|
+
'---\ntitle: "Daily Note Jul 1"\n---\n\nWorked on authentication flow.');
|
|
40
|
+
writeFileSync(join(decisionsDir, 'use-sqlite.md'),
|
|
41
|
+
'---\ntitle: "Use SQLite"\n---\n\nDecision to use SQLite for local storage.');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
try { rmSync(storeHome, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
46
|
+
DIGEST_STORE.__resetStoreForTests();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ── Basic generation ──────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
describe('basic generation', () => {
|
|
52
|
+
it('generates markdown with all expected sections', async () => {
|
|
53
|
+
const result = await DIGEST_STORE.generateWeeklyDigest({
|
|
54
|
+
weekStart: '2026-06-28',
|
|
55
|
+
weekEnd: '2026-07-04',
|
|
56
|
+
projectRoot: storeHome,
|
|
57
|
+
dryRun: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
assert.ok(result.markdown);
|
|
61
|
+
// Check frontmatter
|
|
62
|
+
assert.ok(result.markdown.includes('type: digest'));
|
|
63
|
+
assert.ok(result.markdown.includes('weekStart: 2026-06-28'));
|
|
64
|
+
assert.ok(result.markdown.includes('weekEnd: 2026-07-04'));
|
|
65
|
+
|
|
66
|
+
// Check all section headers present
|
|
67
|
+
const sections = [
|
|
68
|
+
'Tasks completed',
|
|
69
|
+
'Tasks created',
|
|
70
|
+
'Memory notes written',
|
|
71
|
+
'Chat sessions',
|
|
72
|
+
'Schedules fired',
|
|
73
|
+
'Background agents completed',
|
|
74
|
+
'Token usage',
|
|
75
|
+
];
|
|
76
|
+
for (const s of sections) {
|
|
77
|
+
assert.ok(result.markdown.includes(`## ${s}`), `section "${s}" should have a header`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check footer
|
|
81
|
+
assert.ok(result.markdown.includes('BizarHarness v4.8.0'));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('memory-writes section is populated from vault state', async () => {
|
|
85
|
+
// Use a week range that includes "right now" so the notes written
|
|
86
|
+
// in beforeEach (which have the current mtime) are picked up.
|
|
87
|
+
const now = new Date();
|
|
88
|
+
const weekStart = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000)
|
|
89
|
+
.toISOString().slice(0, 10);
|
|
90
|
+
const weekEnd = new Date(now.getTime() + 1 * 24 * 60 * 60 * 1000)
|
|
91
|
+
.toISOString().slice(0, 10);
|
|
92
|
+
|
|
93
|
+
const result = await DIGEST_STORE.generateWeeklyDigest({
|
|
94
|
+
weekStart,
|
|
95
|
+
weekEnd,
|
|
96
|
+
projectRoot: storeHome,
|
|
97
|
+
dryRun: true,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// memory-writes section should contain our test notes (since they
|
|
101
|
+
// were just created and fall within the dynamic date range)
|
|
102
|
+
const memorySection = result.sections['memory-writes'];
|
|
103
|
+
assert.ok(Array.isArray(memorySection), 'memory-writes should be an array');
|
|
104
|
+
assert.ok(memorySection.length > 0, 'should include at least one memory note');
|
|
105
|
+
// At least one note should reference the files we created
|
|
106
|
+
const hasDaily = memorySection.some((n) => n.relPath.includes('daily'));
|
|
107
|
+
const hasDecisions = memorySection.some((n) => n.relPath.includes('decisions'));
|
|
108
|
+
assert.ok(hasDaily || hasDecisions, 'should include notes from our test vault');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ── dryRun behavior ───────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
describe('dryRun', () => {
|
|
115
|
+
it('does not save files when dryRun is true', async () => {
|
|
116
|
+
const result = await DIGEST_STORE.generateAndSave({
|
|
117
|
+
weekStart: '2026-06-28',
|
|
118
|
+
weekEnd: '2026-07-04',
|
|
119
|
+
projectRoot: storeHome,
|
|
120
|
+
dryRun: true,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// saveResult should not be present
|
|
124
|
+
assert.strictEqual(result.saveResult, undefined, 'saveResult should be undefined in dry run');
|
|
125
|
+
|
|
126
|
+
// No digest files should exist
|
|
127
|
+
const prefix = 'weekly-';
|
|
128
|
+
const digestsDir = join(storeHome, 'digests');
|
|
129
|
+
if (existsSync(digestsDir)) {
|
|
130
|
+
const { readdirSync } = await import('node:fs');
|
|
131
|
+
const files = readdirSync(digestsDir);
|
|
132
|
+
const digestFiles = files.filter((f) => f.startsWith(prefix));
|
|
133
|
+
assert.strictEqual(digestFiles.length, 0, 'no digest files should exist after dry run');
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('saves files when dryRun is false', async () => {
|
|
138
|
+
const result = await DIGEST_STORE.generateAndSave({
|
|
139
|
+
weekStart: '2026-06-21',
|
|
140
|
+
weekEnd: '2026-06-27',
|
|
141
|
+
projectRoot: storeHome,
|
|
142
|
+
dryRun: false,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
assert.ok(result.saveResult, 'saveResult should be present');
|
|
146
|
+
assert.ok(result.saveResult.ok);
|
|
147
|
+
assert.ok(result.saveResult.paths.length >= 1);
|
|
148
|
+
|
|
149
|
+
// File should exist on disk
|
|
150
|
+
for (const p of result.saveResult.paths) {
|
|
151
|
+
assert.ok(existsSync(p), `digest file should exist: ${p}`);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ── Markdown format ───────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
describe('markdown format', () => {
|
|
159
|
+
it('produces parseable frontmatter', async () => {
|
|
160
|
+
const result = await DIGEST_STORE.generateWeeklyDigest({
|
|
161
|
+
weekStart: '2026-06-28',
|
|
162
|
+
weekEnd: '2026-07-04',
|
|
163
|
+
projectRoot: storeHome,
|
|
164
|
+
dryRun: true,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Extract frontmatter between --- markers
|
|
168
|
+
const lines = result.markdown.split('\n');
|
|
169
|
+
assert.strictEqual(lines[0], '---', 'should start with frontmatter delimiter');
|
|
170
|
+
const endIdx = lines.indexOf('---', 1);
|
|
171
|
+
assert.ok(endIdx > 1, 'should have closing frontmatter delimiter');
|
|
172
|
+
|
|
173
|
+
const fm = lines.slice(1, endIdx);
|
|
174
|
+
const titleLine = fm.find((l) => l.startsWith('title:'));
|
|
175
|
+
assert.ok(titleLine, 'frontmatter should have title');
|
|
176
|
+
assert.ok(titleLine.includes('Weekly Digest'), 'title should mention Weekly Digest');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('has a readable human date in the header', async () => {
|
|
180
|
+
const result = await DIGEST_STORE.generateWeeklyDigest({
|
|
181
|
+
weekStart: '2026-06-28',
|
|
182
|
+
weekEnd: '2026-07-04',
|
|
183
|
+
projectRoot: storeHome,
|
|
184
|
+
dryRun: true,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// The markdown should have a **Week:** line
|
|
188
|
+
assert.ok(result.markdown.includes('**Week:**'), 'should have week header');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|