@polderlabs/bizar 4.4.13 → 4.5.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.
Files changed (118) hide show
  1. package/bizar-dash/CHANGELOG.md +37 -276
  2. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +2 -0
  3. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +1 -0
  4. package/bizar-dash/dist/assets/main-eWZ4NlCL.css +1 -0
  5. package/bizar-dash/dist/assets/main-usWhlPWa.js +362 -0
  6. package/bizar-dash/dist/assets/main-usWhlPWa.js.map +1 -0
  7. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile-DQLFCjwJ.js} +2 -2
  8. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile-DQLFCjwJ.js.map} +1 -1
  9. package/bizar-dash/dist/assets/mobile-O6ANdD4W.js +352 -0
  10. package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +1 -0
  11. package/bizar-dash/dist/index.html +3 -3
  12. package/bizar-dash/dist/mobile.html +2 -2
  13. package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
  14. package/bizar-dash/skills/bizar/SKILL.md +116 -0
  15. package/bizar-dash/skills/chat/SKILL.md +74 -0
  16. package/bizar-dash/skills/headroom/SKILL.md +94 -0
  17. package/bizar-dash/skills/lightrag/SKILL.md +86 -0
  18. package/bizar-dash/skills/minimax/SKILL.md +80 -0
  19. package/bizar-dash/skills/obsidian/SKILL.md +68 -0
  20. package/bizar-dash/skills/providers/SKILL.md +75 -0
  21. package/bizar-dash/skills/sdk/SKILL.md +138 -0
  22. package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
  23. package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
  24. package/bizar-dash/skills/usage/SKILL.md +62 -0
  25. package/bizar-dash/src/server/api.mjs +18 -0
  26. package/bizar-dash/src/server/headroom.mjs +645 -0
  27. package/bizar-dash/src/server/memory-lightrag.mjs +272 -2
  28. package/bizar-dash/src/server/memory-obsidian.mjs +230 -0
  29. package/bizar-dash/src/server/memory-store.mjs +189 -0
  30. package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
  31. package/bizar-dash/src/server/minimax.mjs +196 -5
  32. package/bizar-dash/src/server/providers-store.mjs +956 -0
  33. package/bizar-dash/src/server/routes/_shared.mjs +17 -0
  34. package/bizar-dash/src/server/routes/config.mjs +52 -1
  35. package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
  36. package/bizar-dash/src/server/routes/headroom.mjs +126 -0
  37. package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
  38. package/bizar-dash/src/server/routes/memory.mjs +668 -1
  39. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
  40. package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
  41. package/bizar-dash/src/server/routes/providers.mjs +266 -5
  42. package/bizar-dash/src/server/routes/skills.mjs +32 -43
  43. package/bizar-dash/src/server/routes/update.mjs +340 -0
  44. package/bizar-dash/src/server/routes/usage.mjs +136 -0
  45. package/bizar-dash/src/server/serve-info.mjs +135 -4
  46. package/bizar-dash/src/server/server.mjs +20 -0
  47. package/bizar-dash/src/server/skills-store.mjs +152 -262
  48. package/bizar-dash/src/web/App.tsx +120 -29
  49. package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
  50. package/bizar-dash/src/web/components/HeadroomSettings.tsx +418 -0
  51. package/bizar-dash/src/web/components/HeadroomStatus.tsx +158 -0
  52. package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
  53. package/bizar-dash/src/web/components/Topbar.tsx +2 -1
  54. package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
  55. package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
  56. package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
  57. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
  58. package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
  59. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
  60. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
  61. package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
  62. package/bizar-dash/src/web/lib/api.ts +43 -0
  63. package/bizar-dash/src/web/lib/types.ts +16 -0
  64. package/bizar-dash/src/web/main.tsx +2 -0
  65. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
  66. package/bizar-dash/src/web/styles/chat.css +135 -1
  67. package/bizar-dash/src/web/styles/main.css +46 -0
  68. package/bizar-dash/src/web/styles/memory.css +955 -0
  69. package/bizar-dash/src/web/styles/minimax-usage.css +335 -0
  70. package/bizar-dash/src/web/styles/settings.css +418 -0
  71. package/bizar-dash/src/web/styles/skills.css +302 -0
  72. package/bizar-dash/src/web/styles/tasks.css +288 -0
  73. package/bizar-dash/src/web/views/Chat.tsx +276 -48
  74. package/bizar-dash/src/web/views/Config.tsx +3 -2065
  75. package/bizar-dash/src/web/views/Memory.tsx +140 -0
  76. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +476 -461
  77. package/bizar-dash/src/web/views/Overview.tsx +3 -0
  78. package/bizar-dash/src/web/views/Settings.tsx +36 -0
  79. package/bizar-dash/src/web/views/Skills.tsx +208 -260
  80. package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
  81. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +289 -0
  82. package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +220 -0
  83. package/bizar-dash/src/web/views/memory/LightragPanel.tsx +307 -0
  84. package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +354 -0
  85. package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +160 -0
  86. package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +642 -0
  87. package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +194 -0
  88. package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
  89. package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
  90. package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
  91. package/bizar-dash/tests/headroom-install.test.mjs +173 -0
  92. package/bizar-dash/tests/headroom-settings.test.mjs +126 -0
  93. package/bizar-dash/tests/headroom-status.test.mjs +117 -0
  94. package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
  95. package/bizar-dash/tests/memory-lightrag-extended.test.mjs +162 -0
  96. package/bizar-dash/tests/memory-obsidian.test.mjs +269 -0
  97. package/bizar-dash/tests/memory-tab.test.mjs +322 -0
  98. package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
  99. package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
  100. package/bizar-dash/tests/mod-upgrade.node.test.mjs +1 -1
  101. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
  102. package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
  103. package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
  104. package/bizar-dash/tests/skills-list.test.mjs +232 -0
  105. package/bizar-dash/tests/skills-search.test.mjs +222 -0
  106. package/bizar-dash/tests/submit-feedback.test.mjs +6 -6
  107. package/bizar-dash/tests/tasks-create.test.mjs +187 -0
  108. package/bizar-dash/tests/update-check.test.mjs +127 -0
  109. package/bizar-dash/tests/update-run.test.mjs +266 -0
  110. package/cli/bin.mjs +247 -1
  111. package/cli/provision.mjs +118 -4
  112. package/config/agents/_shared/SKILLS.md +109 -0
  113. package/package.json +1 -1
  114. package/bizar-dash/dist/assets/main-BB5mJurD.js +0 -352
  115. package/bizar-dash/dist/assets/main-BB5mJurD.js.map +0 -1
  116. package/bizar-dash/dist/assets/main-BsnQLXdh.css +0 -1
  117. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
  118. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
@@ -0,0 +1,293 @@
1
+ /**
2
+ * minimax-usage-store.node.test.mjs
3
+ *
4
+ * Tests for the JSONL usage store.
5
+ *
6
+ * Run with: node --test bizar-dash/tests/minimax-usage-store.node.test.mjs
7
+ *
8
+ * Strategy: we avoid ESM dynamic-import cache issues by writing JSONL lines
9
+ * directly to the store file path (derived from BIZAR_STORE_HOME env var)
10
+ * and importing the store module only to call its pure/stateless helper
11
+ * functions (computeTotals, computePerModel, etc.). recordUsage and queryUsage
12
+ * are tested via direct file I/O so the ESM module loader never interferes.
13
+ */
14
+ import { describe, it, before, after, beforeEach } from 'node:test';
15
+ import assert from 'node:assert/strict';
16
+ import { mkdtempSync, writeFileSync, rmSync, unlinkSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { tmpdir } from 'node:os';
19
+
20
+ // Allow tests to run without polluting the real store.
21
+ // NOTE: STORE_FILE is computed lazily inside functions, NOT at module level,
22
+ // because process.env.BIZAR_STORE_HOME is set in the `before` hook which
23
+ // runs AFTER module-level code.
24
+ let SANDBOX_HOME;
25
+ let ORIGINAL_BIZAR_STORE_HOME;
26
+
27
+ /** The store file path, resolved after BIZAR_STORE_HOME is set by `before`. */
28
+ function storeFile() {
29
+ return join(process.env.BIZAR_STORE_HOME, 'usage.jsonl');
30
+ }
31
+
32
+ const RECORD_BASE = {
33
+ ts: Date.now(), // required for queryUsage range filters
34
+ providerId: 'minimax',
35
+ modelId: 'MiniMax-M3',
36
+ endpoint: 'chat',
37
+ requestId: 'msg_test1',
38
+ promptTokens: 100,
39
+ completionTokens: 200,
40
+ totalTokens: 300,
41
+ cachedTokens: 0,
42
+ reasoningTokens: 50,
43
+ latencyMs: 1234,
44
+ finishReason: 'stop',
45
+ error: null,
46
+ keyEnvVar: 'env:MINIMAX_API_KEY',
47
+ isBackup: false,
48
+ cached: false,
49
+ };
50
+
51
+ before(() => {
52
+ SANDBOX_HOME = mkdtempSync(join(tmpdir(), `bizar-usage-test-${Date.now()}-`));
53
+ ORIGINAL_BIZAR_STORE_HOME = process.env.BIZAR_STORE_HOME;
54
+ // Set BIZAR_STORE_HOME before the store module is ever imported so its
55
+ // top-level STORE_FILE computation picks up the sandbox path.
56
+ process.env.BIZAR_STORE_HOME = join(SANDBOX_HOME, '.local', 'share', 'bizar');
57
+ });
58
+
59
+ after(() => {
60
+ process.env.BIZAR_STORE_HOME = ORIGINAL_BIZAR_STORE_HOME ?? '';
61
+ try { rmSync(SANDBOX_HOME, { recursive: true, force: true }); } catch { /* ignore */ }
62
+ });
63
+
64
+ beforeEach(() => {
65
+ // Wipe the JSONL store before each test.
66
+ try { unlinkSync(storeFile()); } catch { /* ignore */ }
67
+ });
68
+
69
+ function makeRecord(overrides = {}) {
70
+ return { ...RECORD_BASE, ...overrides };
71
+ }
72
+
73
+ /**
74
+ * Write a JSONL line directly to the store file (bypassing the store module
75
+ * to avoid any ESM import ordering issues).
76
+ */
77
+ function appendJsonl(record) {
78
+ writeFileSync(storeFile(), JSON.stringify(record) + '\n', { flag: 'a', encoding: 'utf8' });
79
+ }
80
+
81
+ /**
82
+ * Load the store module. Uses a fresh import each time via unique cache-busting query param.
83
+ * The BIZAR_STORE_HOME env var is set in the `before` hook before any imports happen.
84
+ */
85
+ async function getStore() {
86
+ const stamp = `${Date.now()}-${Math.random()}`;
87
+ return import(`../src/server/minimax-usage-store.mjs?nocache=${stamp}`);
88
+ }
89
+
90
+ // ─── recordUsage + queryUsage ─────────────────────────────────────────
91
+
92
+ describe('recordUsage + queryUsage', () => {
93
+ it('records a single chat usage record', async () => {
94
+ const store = await getStore();
95
+ store.recordUsage(makeRecord());
96
+ const result = store.queryUsage({ range: '24h' });
97
+ assert.equal(result.totals.requests, 1);
98
+ assert.equal(result.totals.totalTokens, 300);
99
+ assert.equal(result.totals.promptTokens, 100);
100
+ assert.equal(result.totals.completionTokens, 200);
101
+ assert.equal(result.totals.reasoningTokens, 50);
102
+ assert.equal(result.totals.avgLatencyMs, 1234);
103
+ });
104
+
105
+ it('aggregates multiple records', async () => {
106
+ const store = await getStore();
107
+ store.recordUsage(makeRecord({ requestId: 'r1', totalTokens: 100 }));
108
+ store.recordUsage(makeRecord({ requestId: 'r2', totalTokens: 200 }));
109
+ store.recordUsage(makeRecord({ requestId: 'r3', totalTokens: 300 }));
110
+ const result = store.queryUsage({ range: '24h' });
111
+ assert.equal(result.totals.requests, 3);
112
+ assert.equal(result.totals.totalTokens, 600);
113
+ });
114
+
115
+ it('filters by range — 24h excludes old records', async () => {
116
+ const store = await getStore();
117
+ // Write a recent record via the store.
118
+ store.recordUsage(makeRecord({ requestId: 'recent', ts: Date.now() - 60_000 }));
119
+ // Manually write an 8-day-old record to the file.
120
+ appendJsonl(makeRecord({ requestId: 'old', ts: Date.now() - 8 * 86_400_000 }));
121
+ const result = store.queryUsage({ range: '24h' });
122
+ assert.equal(result.totals.requests, 1);
123
+ assert.equal(result.daily.length, 1);
124
+ });
125
+
126
+ it('supports range=7d', async () => {
127
+ const store = await getStore();
128
+ store.recordUsage(makeRecord({ requestId: 'r1', ts: Date.now() - 2 * 86_400_000 }));
129
+ store.recordUsage(makeRecord({ requestId: 'r2', ts: Date.now() - 6 * 86_400_000 }));
130
+ const result = store.queryUsage({ range: '7d' });
131
+ assert.equal(result.totals.requests, 2);
132
+ });
133
+
134
+ it('computes avgLatencyMs and p95LatencyMs', async () => {
135
+ const store = await getStore();
136
+ for (let i = 0; i < 20; i++) {
137
+ store.recordUsage(makeRecord({ requestId: `r${i}`, latencyMs: (i + 1) * 100 }));
138
+ }
139
+ const result = store.queryUsage({ range: '24h' });
140
+ assert.equal(result.totals.avgLatencyMs, 1050);
141
+ assert.equal(result.totals.p95LatencyMs, 2000);
142
+ });
143
+
144
+ it('records errors correctly', async () => {
145
+ const store = await getStore();
146
+ store.recordUsage(makeRecord({ requestId: 'e1', error: { code: 'http_429', message: 'rate limited' } }));
147
+ store.recordUsage(makeRecord({ requestId: 'e2', error: { code: 'http_500', message: 'server error' } }));
148
+ const result = store.queryUsage({ range: '24h' });
149
+ assert.equal(result.totals.errors, 2);
150
+ assert.equal(result.totals.requests, 2);
151
+ assert.equal(result.errors.length, 2);
152
+ });
153
+
154
+ it('records cached=true correctly', async () => {
155
+ const store = await getStore();
156
+ // Both records are cached so cost is 0 (uncached record would cost > 0).
157
+ store.recordUsage(makeRecord({ requestId: 'c1', cached: true }));
158
+ store.recordUsage(makeRecord({ requestId: 'c2', cached: true }));
159
+ const result = store.queryUsage({ range: '24h' });
160
+ assert.equal(result.totals.requests, 2);
161
+ // Cost should be 0 for cached records.
162
+ assert.equal(result.totals.costEstimate, 0);
163
+ });
164
+
165
+ it('computes per-model buckets', async () => {
166
+ const store = await getStore();
167
+ store.recordUsage(makeRecord({ modelId: 'MiniMax-M3', totalTokens: 100 }));
168
+ store.recordUsage(makeRecord({ modelId: 'MiniMax-M3', totalTokens: 200 }));
169
+ store.recordUsage(makeRecord({ modelId: 'MiniMax-M2.7', totalTokens: 50 }));
170
+ const result = store.queryUsage({ range: '24h' });
171
+ assert.equal(result.perModel.length, 2);
172
+ const m3 = result.perModel.find(m => m.modelId === 'MiniMax-M3');
173
+ assert.ok(m3);
174
+ assert.equal(m3.totalTokens, 300);
175
+ assert.equal(m3.requests, 2);
176
+ });
177
+
178
+ it('computes per-key buckets', async () => {
179
+ const store = await getStore();
180
+ store.recordUsage(makeRecord({ keyEnvVar: 'MINIMAX_API_KEY', isBackup: false, totalTokens: 100 }));
181
+ store.recordUsage(makeRecord({ keyEnvVar: 'MINIMAX_API_KEY_BACKUP', isBackup: true, totalTokens: 50 }));
182
+ const result = store.queryUsage({ range: '24h' });
183
+ assert.equal(result.perKey.length, 2);
184
+ const primary = result.perKey.find(k => k.keyEnvVar === 'MINIMAX_API_KEY' && !k.isBackup);
185
+ assert.ok(primary);
186
+ assert.equal(primary.requests, 1);
187
+ });
188
+ });
189
+
190
+ // ─── getUsageSummary ──────────────────────────────────────────────────
191
+
192
+ describe('getUsageSummary', () => {
193
+ it('returns zeros when no records', async () => {
194
+ const store = await getStore();
195
+ const s = store.getUsageSummary('minimax');
196
+ assert.equal(s.requests, 0);
197
+ assert.equal(s.tokens, 0);
198
+ });
199
+
200
+ it('returns last-5-min rolling totals', async () => {
201
+ const store = await getStore();
202
+ // 10 minutes ago — should NOT be in 5-min window.
203
+ store.recordUsage(makeRecord({ requestId: 'old', ts: Date.now() - 10 * 60_000, totalTokens: 9999 }));
204
+ // 2 minutes ago — should be in 5-min window.
205
+ store.recordUsage(makeRecord({ requestId: 'recent', ts: Date.now() - 2 * 60_000, totalTokens: 500 }));
206
+ const s = store.getUsageSummary('minimax');
207
+ assert.equal(s.requests, 1);
208
+ assert.equal(s.tokens, 500);
209
+ });
210
+ });
211
+
212
+ // ─── getUsageLimitsForAgent ───────────────────────────────────────────
213
+
214
+ describe('getUsageLimitsForAgent', () => {
215
+ it('returns a valid summary', async () => {
216
+ const store = await getStore();
217
+ store.recordUsage(makeRecord({ totalTokens: 10_000 }));
218
+ const s = await store.getUsageLimitsForAgent('minimax');
219
+ assert.equal(s.provider, 'minimax');
220
+ assert.equal(s.tokensLast24h, 10_000);
221
+ assert.equal(s.requestsLast24h, 1);
222
+ assert.ok(s.limits);
223
+ assert.equal(typeof s.percentUsed24h, 'number');
224
+ assert.ok(s.estimatedTimeUntilReset === null || typeof s.estimatedTimeUntilReset === 'string');
225
+ assert.ok(s.warning === null || typeof s.warning === 'string');
226
+ });
227
+
228
+ it('warns when 24h usage exceeds 80pct of heuristic limit', async () => {
229
+ const store = await getStore();
230
+ // Exceed 80% of 1_000_000 tokens.
231
+ store.recordUsage(makeRecord({ totalTokens: 850_000 }));
232
+ const s = await store.getUsageLimitsForAgent('minimax');
233
+ assert.equal(s.warning, 'approaching_daily_limit');
234
+ });
235
+ });
236
+
237
+ // ─── pruneUsage ──────────────────────────────────────────────────────
238
+
239
+ describe('pruneUsage', () => {
240
+ it('removes records older than the cutoff', async () => {
241
+ const store = await getStore();
242
+ store.recordUsage(makeRecord({ requestId: 'old', ts: Date.now() - 48 * 86_400_000 }));
243
+ store.recordUsage(makeRecord({ requestId: 'recent', ts: Date.now() - 1 * 86_400_000 }));
244
+ const removed = store.pruneUsage({ olderThanMs: 24 * 86_400_000 });
245
+ assert.equal(removed, 1);
246
+ const result = store.queryUsage({ range: '30d' });
247
+ assert.equal(result.totals.requests, 1);
248
+ });
249
+
250
+ it('returns 0 when nothing to prune', async () => {
251
+ const store = await getStore();
252
+ store.recordUsage(makeRecord());
253
+ const removed = store.pruneUsage({ olderThanMs: 24 * 86_400_000 });
254
+ assert.equal(removed, 0);
255
+ });
256
+ });
257
+
258
+ // ─── __resetStoreForTests ────────────────────────────────────────────
259
+
260
+ describe('__resetStoreForTests', () => {
261
+ it('wipes all records', async () => {
262
+ const store = await getStore();
263
+ store.recordUsage(makeRecord());
264
+ store.recordUsage(makeRecord({ requestId: 'another' }));
265
+ assert.equal(store.queryUsage({ range: '24h' }).totals.requests, 2);
266
+ store.__resetStoreForTests();
267
+ assert.equal(store.queryUsage({ range: '24h' }).totals.requests, 0);
268
+ });
269
+ });
270
+
271
+ // ─── cost estimation ─────────────────────────────────────────────────
272
+
273
+ describe('costEstimate', () => {
274
+ it('estimates cost for MiniMax-M3', async () => {
275
+ const store = await getStore();
276
+ store.recordUsage(makeRecord({
277
+ modelId: 'MiniMax-M3',
278
+ promptTokens: 1_000_000,
279
+ completionTokens: 1_000_000,
280
+ totalTokens: 2_000_000,
281
+ }));
282
+ const result = store.queryUsage({ range: '24h' });
283
+ // 1M prompt @ $1/M = $1, 1M completion @ $3/M = $3 → $4 total
284
+ assert.equal(result.totals.costEstimate, 4);
285
+ });
286
+
287
+ it('returns 0 for unknown models', async () => {
288
+ const store = await getStore();
289
+ store.recordUsage(makeRecord({ modelId: 'unknown-model', totalTokens: 1_000_000 }));
290
+ const result = store.queryUsage({ range: '24h' });
291
+ assert.equal(result.totals.costEstimate, 0);
292
+ });
293
+ });
@@ -172,7 +172,7 @@ describe('upgradeFromRegistry — end-to-end with stubbed registry', () => {
172
172
  it('upgrades in place when the registry returns a newer version', async () => {
173
173
  // Install v1.0.0 from a source folder.
174
174
  const v1Source = buildTestMod(TEST_MOD_ID, '1.0.0');
175
- const installed = modsLoader.installFromPath(v1Source);
175
+ const installed = await modsLoader.installFromPath(v1Source);
176
176
  assert.equal(installed.version, '1.0.0');
177
177
 
178
178
  // Build a fake registry entry for v2.0.0 and stub installFromRegistry
@@ -18,7 +18,8 @@ import {
18
18
  writeFileSync,
19
19
  rmSync,
20
20
  existsSync,
21
- renameSync,
21
+ copyFileSync,
22
+ unlinkSync,
22
23
  readFileSync,
23
24
  mkdirSync,
24
25
  } from 'node:fs';
@@ -91,23 +92,21 @@ async function startDashboard() {
91
92
  }
92
93
 
93
94
  let dashboardServer, dashboardBaseUrl, tmpDir;
94
- const SERVE_JSON_PATH = join(homedir(), '.cache', 'bizar', 'serve.json');
95
+ let SERVE_JSON_PATH;
95
96
  let originalServeJson;
96
97
 
97
98
  before(async () => {
99
+ SERVE_JSON_PATH = `${tmpdir()}/bizar-test-serve-detail-${process.pid}-${Date.now()}.json`;
100
+ process.env.BIZAR_SERVE_JSON_PATH = SERVE_JSON_PATH;
98
101
  await startUpstream();
99
102
  await startDashboard();
100
103
  tmpDir = mkdtempSync(join(tmpdir(), 'opencode-sessions-detail-'));
101
- // Snapshot any existing serve.json so we can restore it.
102
104
  if (existsSync(SERVE_JSON_PATH)) {
103
105
  originalServeJson = readFileSync(SERVE_JSON_PATH, 'utf8');
104
106
  }
105
- mkdirSync(join(homedir(), '.cache', 'bizar'), { recursive: true });
106
107
  });
107
108
 
108
109
  after(async () => {
109
- // Restore the real serve.json (or remove ours) before exit so we
110
- // never leave a fake file behind in the user's home.
111
110
  if (originalServeJson !== undefined) {
112
111
  writeFileSync(SERVE_JSON_PATH, originalServeJson, 'utf8');
113
112
  } else if (existsSync(SERVE_JSON_PATH)) {
@@ -116,6 +115,7 @@ after(async () => {
116
115
  if (dashboardServer) await new Promise((r) => dashboardServer.close(r));
117
116
  if (upstreamServer) await new Promise((r) => upstreamServer.close(r));
118
117
  if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
118
+ delete process.env.BIZAR_SERVE_JSON_PATH;
119
119
  });
120
120
 
121
121
  beforeEach(() => {
@@ -128,11 +128,14 @@ function writeServeJson(worktree) {
128
128
  writeFileSync(SERVE_JSON_PATH, JSON.stringify({ port: upstreamPort, password: 'test-pw', worktree, pid: 99999, startedAt: Date.now() }), 'utf8');
129
129
  }
130
130
 
131
- /** Move serve.json aside so readServeInfo() returns null. */
131
+ /** Move serve.json aside so readServeInfo() returns null.
132
+ * Uses copy + unlink because rename across filesystems (e.g.
133
+ * /home/* → /tmp/*) throws EXDEV on Linux. */
132
134
  function moveServeJsonAside() {
133
135
  if (!existsSync(SERVE_JSON_PATH)) return;
134
- const tmp = join(tmpDir, `serve-aside-${Date.now()}.json`);
135
- renameSync(SERVE_JSON_PATH, tmp);
136
+ const tmp = join(tmpDir, `serve-aside-${Date.now()}-${process.pid}.json`);
137
+ copyFileSync(SERVE_JSON_PATH, tmp);
138
+ unlinkSync(SERVE_JSON_PATH);
136
139
  }
137
140
 
138
141
  function parseSseStream(text) {