@polderlabs/bizar 4.5.2 → 4.7.1
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-lbH6szyX.js → mobile-BK8-ythT.js} +18 -18
- package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js → mobile-Chvf9u_B.js} +1 -1
- package/bizar-dash/dist/assets/{mobile-BRhoDOUz.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 -0
- 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/logger.mjs +71 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +45 -6
- package/bizar-dash/src/server/metrics.mjs +193 -0
- package/bizar-dash/src/server/routes/backup.mjs +112 -0
- package/bizar-dash/src/server/routes/chat.mjs +20 -3
- package/bizar-dash/src/server/routes/digests.mjs +82 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +3 -2
- package/bizar-dash/src/server/routes/memory.mjs +5 -4
- package/bizar-dash/src/server/routes/misc.mjs +2 -1
- package/bizar-dash/src/server/routes/overview.mjs +2 -1
- 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 +79 -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 +6 -3
- package/bizar-dash/src/web/components/VirtualList.tsx +53 -0
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +17 -11
- package/bizar-dash/src/web/components/chat/Composer.tsx +2 -0
- package/bizar-dash/src/web/hooks/useI18n.ts +13 -0
- package/bizar-dash/src/web/lib/i18n.ts +25 -0
- package/bizar-dash/src/web/locales/en.json +52 -0
- package/bizar-dash/src/web/main.tsx +5 -0
- package/bizar-dash/src/web/styles/main.css +70 -8
- package/bizar-dash/src/web/views/Activity.tsx +35 -18
- 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 +94 -76
- 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 +19 -9
- 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 -1745
- 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-bugfixes.test.mjs +4 -4
- package/bizar-dash/tests/cli-error-visibility.test.mjs +153 -0
- package/bizar-dash/tests/cli-refactor.test.mjs +184 -0
- package/bizar-dash/tests/components/Button.test.tsx +41 -0
- package/bizar-dash/tests/components/Card.test.tsx +42 -0
- package/bizar-dash/tests/components/Modal.test.tsx +104 -0
- package/bizar-dash/tests/components/Spinner.test.tsx +32 -0
- package/bizar-dash/tests/components/StatusBadge.test.tsx +35 -0
- package/bizar-dash/tests/components/Toast.test.tsx +108 -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/hooks/useModal.test.tsx +84 -0
- package/bizar-dash/tests/hooks/useToast.test.tsx +50 -0
- package/bizar-dash/tests/lib/i18n.test.ts +46 -0
- package/bizar-dash/tests/lib/utils.test.ts +194 -0
- package/bizar-dash/tests/logger.test.mjs +207 -0
- package/bizar-dash/tests/metrics.test.mjs +183 -0
- package/bizar-dash/tests/rate-limit.test.mjs +298 -0
- package/bizar-dash/tests/server-bugfixes.test.mjs +2 -2
- package/bizar-dash/tests/setup.ts +7 -0
- package/bizar-dash/vitest.config.ts +13 -0
- package/cli/artifact-cli.mjs +605 -0
- package/cli/artifact-render.mjs +621 -0
- package/cli/artifact-server.mjs +847 -0
- package/cli/artifact.mjs +38 -2096
- package/cli/bg.mjs +5 -13
- package/cli/bin.mjs +221 -1350
- package/cli/commands/artifact.mjs +20 -0
- package/cli/commands/dash.mjs +160 -0
- package/cli/commands/headroom.mjs +204 -0
- package/cli/commands/install.mjs +169 -0
- package/cli/commands/memory.mjs +25 -0
- package/cli/commands/minimax.mjs +285 -0
- package/cli/commands/mod.mjs +185 -0
- package/cli/commands/service.mjs +65 -0
- package/cli/commands/usage.mjs +109 -0
- package/cli/commands/util.mjs +459 -0
- package/cli/digest.mjs +149 -0
- package/cli/doctor.mjs +1 -13
- package/cli/provision.mjs +2 -13
- package/cli/service-controller.mjs +1 -11
- package/cli/service.mjs +1 -11
- package/cli/utils.mjs +41 -1
- package/package.json +6 -1
- package/bizar-dash/dist/assets/main-B4OfGAwz.js +0 -361
- package/bizar-dash/dist/assets/main-B4OfGAwz.js.map +0 -1
- package/bizar-dash/dist/assets/main-DAlLdW8I.css +0 -1
- package/bizar-dash/dist/assets/mobile-lbH6szyX.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
|
+
});
|
|
@@ -20,7 +20,7 @@ const REPO = join(process.cwd());
|
|
|
20
20
|
describe('B1 — parseWithModsFlag', () => {
|
|
21
21
|
it('exits with code 2 when --with-mods is the last arg', async () => {
|
|
22
22
|
// Import fresh each time to avoid module-level side effects
|
|
23
|
-
const { parseWithModsFlag } = await import('../../cli/
|
|
23
|
+
const { parseWithModsFlag } = await import('../../cli/commands/install.mjs');
|
|
24
24
|
let exitCode = null;
|
|
25
25
|
const origExit = process.exit;
|
|
26
26
|
// Mock process.exit to capture the code without terminating the test
|
|
@@ -36,7 +36,7 @@ describe('B1 — parseWithModsFlag', () => {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
it('exits with code 2 when next arg is another flag', async () => {
|
|
39
|
-
const { parseWithModsFlag } = await import('../../cli/
|
|
39
|
+
const { parseWithModsFlag } = await import('../../cli/commands/install.mjs');
|
|
40
40
|
let exitCode = null;
|
|
41
41
|
const origExit = process.exit;
|
|
42
42
|
try {
|
|
@@ -51,7 +51,7 @@ describe('B1 — parseWithModsFlag', () => {
|
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
it('returns null when --with-mods is not present', async () => {
|
|
54
|
-
const { parseWithModsFlag } = await import('../../cli/
|
|
54
|
+
const { parseWithModsFlag } = await import('../../cli/commands/install.mjs');
|
|
55
55
|
let exitCode = null;
|
|
56
56
|
const origExit = process.exit;
|
|
57
57
|
try {
|
|
@@ -65,7 +65,7 @@ describe('B1 — parseWithModsFlag', () => {
|
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
it('returns array of mod ids when value is provided', async () => {
|
|
68
|
-
const { parseWithModsFlag } = await import('../../cli/
|
|
68
|
+
const { parseWithModsFlag } = await import('../../cli/commands/install.mjs');
|
|
69
69
|
let exitCode = null;
|
|
70
70
|
const origExit = process.exit;
|
|
71
71
|
try {
|
|
@@ -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,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bizar-dash/tests/cli-refactor.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v4.6 — CLI refactor tests.
|
|
5
|
+
*
|
|
6
|
+
* Verifies:
|
|
7
|
+
* 1. cli/bin.mjs imports from cli/commands/*.mjs correctly
|
|
8
|
+
* 2. Each command module exports a `run` function
|
|
9
|
+
* 3. cli/utils.mjs exports `which()` and `bizarConfigDir()`
|
|
10
|
+
* 4. cli/artifact-cli.mjs, cli/artifact-server.mjs, cli/artifact-render.mjs
|
|
11
|
+
* exist and export the right things
|
|
12
|
+
* 5. cli/artifact.mjs is a backward-compatible re-export shell
|
|
13
|
+
*/
|
|
14
|
+
import { test, describe } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import { resolve, dirname } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { readdirSync } from 'node:fs';
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const CLI_ROOT = resolve(__dirname, '..', '..', 'cli');
|
|
22
|
+
const COMMANDS_DIR = resolve(CLI_ROOT, 'commands');
|
|
23
|
+
|
|
24
|
+
// ── utils.mjs ────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
describe('cli/utils.mjs exports', () => {
|
|
27
|
+
test('exports which()', async () => {
|
|
28
|
+
const { which } = await import(`${CLI_ROOT}/utils.mjs`);
|
|
29
|
+
assert.equal(typeof which, 'function');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('exports whichPath()', async () => {
|
|
33
|
+
const { whichPath } = await import(`${CLI_ROOT}/utils.mjs`);
|
|
34
|
+
assert.equal(typeof whichPath, 'function');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('exports bizarConfigDir()', async () => {
|
|
38
|
+
const { bizarConfigDir } = await import(`${CLI_ROOT}/utils.mjs`);
|
|
39
|
+
assert.equal(typeof bizarConfigDir, 'function');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('bizarConfigDir() returns a non-empty string', async () => {
|
|
43
|
+
const { bizarConfigDir } = await import(`${CLI_ROOT}/utils.mjs`);
|
|
44
|
+
const dir = bizarConfigDir();
|
|
45
|
+
assert.equal(typeof dir, 'string');
|
|
46
|
+
assert.ok(dir.length > 0);
|
|
47
|
+
assert.ok(dir.includes('bizar'));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('which() returns boolean', async () => {
|
|
51
|
+
const { which } = await import(`${CLI_ROOT}/utils.mjs`);
|
|
52
|
+
const result = which('node');
|
|
53
|
+
assert.equal(typeof result, 'boolean');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('which() returns false for nonexistent command', async () => {
|
|
57
|
+
const { which } = await import(`${CLI_ROOT}/utils.mjs`);
|
|
58
|
+
const result = which('this-command-does-not-exist-xyz');
|
|
59
|
+
assert.equal(result, false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('whichPath() returns null for nonexistent command', async () => {
|
|
63
|
+
const { whichPath } = await import(`${CLI_ROOT}/utils.mjs`);
|
|
64
|
+
const result = whichPath('this-command-does-not-exist-xyz');
|
|
65
|
+
assert.equal(result, null);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ── Command modules ────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
const COMMAND_FILES = [
|
|
72
|
+
'install',
|
|
73
|
+
'service',
|
|
74
|
+
'dash',
|
|
75
|
+
'minimax',
|
|
76
|
+
'headroom',
|
|
77
|
+
'mod',
|
|
78
|
+
'artifact',
|
|
79
|
+
'memory',
|
|
80
|
+
'usage',
|
|
81
|
+
'util',
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
describe('cli/commands/*.mjs exports', () => {
|
|
85
|
+
for (const name of COMMAND_FILES) {
|
|
86
|
+
test(`${name}.mjs exports run()`, async () => {
|
|
87
|
+
const mod = await import(`${COMMANDS_DIR}/${name}.mjs`);
|
|
88
|
+
assert.equal(typeof mod.run, 'function', `${name}.mjs should export run()`);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Note: calling run() directly would execute the command (network calls, etc.)
|
|
92
|
+
// which is not appropriate in a module structure test. The run() signature
|
|
93
|
+
// is validated via TypeScript / node --check instead.
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── Artifact split ────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
describe('artifact module split', () => {
|
|
100
|
+
test('artifact-cli.mjs exists and exports runArtifact', async () => {
|
|
101
|
+
const mod = await import(`${CLI_ROOT}/artifact-cli.mjs`);
|
|
102
|
+
assert.equal(typeof mod.runArtifact, 'function');
|
|
103
|
+
assert.equal(typeof mod.default, 'function');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('artifact-cli.mjs exports showHelp', async () => {
|
|
107
|
+
const mod = await import(`${CLI_ROOT}/artifact-cli.mjs`);
|
|
108
|
+
assert.equal(typeof mod.showHelp, 'function');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('artifact-cli.mjs exports regenerateHtml', async () => {
|
|
112
|
+
const mod = await import(`${CLI_ROOT}/artifact-cli.mjs`);
|
|
113
|
+
assert.equal(typeof mod.regenerateHtml, 'function');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('artifact-server.mjs exists and exports startServer', async () => {
|
|
117
|
+
const mod = await import(`${CLI_ROOT}/artifact-server.mjs`);
|
|
118
|
+
assert.equal(typeof mod.startServer, 'function');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('artifact-render.mjs exports CANVAS_SCHEMA_VERSION', async () => {
|
|
122
|
+
const mod = await import(`${CLI_ROOT}/artifact-render.mjs`);
|
|
123
|
+
assert.equal(mod.CANVAS_SCHEMA_VERSION, 2);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('artifact-render.mjs exports canvas helpers', async () => {
|
|
127
|
+
const mod = await import(`${CLI_ROOT}/artifact-render.mjs`);
|
|
128
|
+
assert.equal(typeof mod.emptyCanvas, 'function');
|
|
129
|
+
assert.equal(typeof mod.readCanvasFile, 'function');
|
|
130
|
+
assert.equal(typeof mod.writeCanvasFile, 'function');
|
|
131
|
+
assert.equal(typeof mod.loadOrMigrateCanvas, 'function');
|
|
132
|
+
assert.equal(typeof mod.canvasToMarkdown, 'function');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('artifact-render.mjs exports ID generators', async () => {
|
|
136
|
+
const mod = await import(`${CLI_ROOT}/artifact-render.mjs`);
|
|
137
|
+
assert.equal(typeof mod.makeElementId, 'function');
|
|
138
|
+
assert.equal(typeof mod.makeConnectionId, 'function');
|
|
139
|
+
assert.equal(typeof mod.makeCommentId, 'function');
|
|
140
|
+
assert.equal(typeof mod.makeReplyId, 'function');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('artifact-render.mjs exports HTML renderers', async () => {
|
|
144
|
+
const mod = await import(`${CLI_ROOT}/artifact-render.mjs`);
|
|
145
|
+
assert.equal(typeof mod.renderElementHTML, 'function');
|
|
146
|
+
assert.equal(typeof mod.renderConnectionHTML, 'function');
|
|
147
|
+
assert.equal(typeof mod.renderCommentPinHTML, 'function');
|
|
148
|
+
assert.equal(typeof mod.escapeHtml, 'function');
|
|
149
|
+
assert.equal(typeof mod.formatDate, 'function');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('artifact.mjs is a re-export shell (backward compat)', async () => {
|
|
153
|
+
const mod = await import(`${CLI_ROOT}/artifact.mjs`);
|
|
154
|
+
// Should re-export runArtifact
|
|
155
|
+
assert.equal(typeof mod.runArtifact, 'function');
|
|
156
|
+
// Should re-export runPlan as alias for runArtifact (backward compat)
|
|
157
|
+
assert.equal(typeof mod.runPlan, 'function');
|
|
158
|
+
// Should re-export startServer
|
|
159
|
+
assert.equal(typeof mod.startServer, 'function');
|
|
160
|
+
// Should re-export canvas helpers
|
|
161
|
+
assert.equal(typeof mod.emptyCanvas, 'function');
|
|
162
|
+
assert.equal(typeof mod.canvasToMarkdown, 'function');
|
|
163
|
+
// Should re-export render helpers
|
|
164
|
+
assert.equal(typeof mod.renderElementHTML, 'function');
|
|
165
|
+
assert.equal(typeof mod.escapeHtml, 'function');
|
|
166
|
+
// Should re-export regenerateHtml
|
|
167
|
+
assert.equal(typeof mod.regenerateHtml, 'function');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ── bin.mjs structure ─────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
describe('cli/bin.mjs structure', () => {
|
|
174
|
+
test('bin.mjs is a valid ESM module', async () => {
|
|
175
|
+
// Just verify it can be parsed as ESM — don't run main()
|
|
176
|
+
const { readFileSync } = await import('node:fs');
|
|
177
|
+
const src = readFileSync(`${CLI_ROOT}/bin.mjs`, 'utf8');
|
|
178
|
+
assert.ok(src.includes('#!/usr/bin/env node'));
|
|
179
|
+
assert.ok(src.includes('export'));
|
|
180
|
+
assert.ok(src.includes('import'));
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
console.log(' cli-refactor.test.mjs loaded — run with: node --test bizar-dash/tests/cli-refactor.test.mjs');
|