@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,322 @@
1
+ /**
2
+ * tests/memory-tab.test.mjs
3
+ *
4
+ * v4.7.0 — Tests for the dedicated Memory tab endpoints.
5
+ *
6
+ * Covers the new REST surface:
7
+ * GET /api/memory/health
8
+ * GET /api/memory/storage
9
+ * GET /api/memory/lightrag/stats
10
+ * GET /api/memory/obsidian/tree
11
+ * GET /api/memory/obsidian/backlinks
12
+ * POST /api/memory/semantic-search
13
+ * GET /api/memory/git/diff
14
+ *
15
+ * Plus the canonical CRUD endpoints and existing memory status surface, to
16
+ * verify nothing regressed when we added the new shape. Uses an isolated
17
+ * tmp projectRoot per test so the existing repo memory config is untouched.
18
+ */
19
+
20
+ import { test, describe, before, after } from 'node:test';
21
+ import assert from 'node:assert/strict';
22
+ import express from 'express';
23
+ import { tmpdir } from 'node:os';
24
+ import { join } from 'node:path';
25
+ import { mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs';
26
+
27
+ const { createMemoryRouter } = await import('../src/server/routes/memory.mjs');
28
+ const memoryStore = await import('../src/server/memory-store.mjs');
29
+
30
+ let server;
31
+ let baseUrl;
32
+ let tmpRoot;
33
+
34
+ function get(path) {
35
+ return fetch(`${baseUrl}${path}`);
36
+ }
37
+
38
+ function post(path, body) {
39
+ return fetch(`${baseUrl}${path}`, {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: body ? JSON.stringify(body) : undefined,
43
+ });
44
+ }
45
+
46
+ function put(path, body) {
47
+ return fetch(`${baseUrl}${path}`, {
48
+ method: 'PUT',
49
+ headers: { 'Content-Type': 'application/json' },
50
+ body: body ? JSON.stringify(body) : undefined,
51
+ });
52
+ }
53
+
54
+ function del(path) {
55
+ return fetch(`${baseUrl}${path}`, { method: 'DELETE' });
56
+ }
57
+
58
+ before(async () => {
59
+ tmpRoot = join(tmpdir(), `bizar-memtab-${Date.now()}`);
60
+ mkdirSync(join(tmpRoot, '.bizar'), { recursive: true });
61
+
62
+ // Initialise memory in local-only mode for tests.
63
+ memoryStore.saveConfig(tmpRoot, {
64
+ version: 1,
65
+ backend: 'bizar-local',
66
+ projectId: 'memtab-test',
67
+ memoryRepo: {
68
+ mode: 'local-only',
69
+ path: join(tmpRoot, '.obsidian'),
70
+ remote: null,
71
+ branch: 'main',
72
+ namespace: 'projects/memtab-test',
73
+ },
74
+ namespaces: {
75
+ project: 'projects/memtab-test',
76
+ global: 'global/bizar',
77
+ user: 'users/local',
78
+ },
79
+ lightrag: { enabled: false, host: '127.0.0.1', port: 9621 },
80
+ git: {
81
+ autoPullOnSessionStart: false,
82
+ autoCommitOnMemoryWrite: false,
83
+ autoPushOnSessionEnd: false,
84
+ commitAuthor: 'Test <test@local>',
85
+ commitMessageTemplate: 'memory(test): {summary}',
86
+ },
87
+ });
88
+ memoryStore.initVault(tmpRoot);
89
+
90
+ const app = express();
91
+ app.use(express.json({ limit: '2mb' }));
92
+ app.use(createMemoryRouter({ projectRoot: tmpRoot }));
93
+ await new Promise((resolve) => {
94
+ server = app.listen(0, () => resolve());
95
+ });
96
+ const { port } = server.address();
97
+ baseUrl = `http://127.0.0.1:${port}`;
98
+ });
99
+
100
+ after(async () => {
101
+ await new Promise((r) => server?.close(r));
102
+ try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
103
+ });
104
+
105
+ describe('GET /api/memory/health', () => {
106
+ test('returns score + checks + status', async () => {
107
+ const r = await get('/memory/health');
108
+ assert.equal(r.status, 200);
109
+ const body = await r.json();
110
+ assert.equal(typeof body.score, 'number');
111
+ assert.ok(['healthy', 'degraded', 'unhealthy', 'unconfigured'].includes(body.status));
112
+ assert.ok(Array.isArray(body.checks));
113
+ assert.equal(typeof body.message, 'string');
114
+ });
115
+
116
+ test('checks include vault_exists + vault_writable + git_clean', async () => {
117
+ const r = await get('/memory/health');
118
+ const body = await r.json();
119
+ const names = body.checks.map((c) => c.name);
120
+ assert.ok(names.includes('vault_exists'));
121
+ assert.ok(names.includes('vault_writable'));
122
+ // local-only mode marks git_clean as pass with detail "local-only mode"
123
+ const git = body.checks.find((c) => c.name === 'git_clean');
124
+ assert.ok(git);
125
+ assert.match(git.detail, /local-only/);
126
+ });
127
+ });
128
+
129
+ describe('GET /api/memory/storage', () => {
130
+ test('returns total + breakdown', async () => {
131
+ const r = await get('/memory/storage');
132
+ assert.equal(r.status, 200);
133
+ const body = await r.json();
134
+ assert.equal(typeof body.total, 'number');
135
+ assert.ok(Array.isArray(body.breakdown));
136
+ assert.equal(typeof body.mode, 'string');
137
+ });
138
+ });
139
+
140
+ describe('GET /api/memory/lightrag/stats', () => {
141
+ test('returns aggregate stats shape', async () => {
142
+ const r = await get('/memory/lightrag/stats');
143
+ assert.equal(r.status, 200);
144
+ const body = await r.json();
145
+ assert.equal(typeof body.running, 'boolean');
146
+ assert.equal(typeof body.host, 'string');
147
+ assert.equal(typeof body.port, 'number');
148
+ assert.equal(typeof body.indexedApprox, 'number');
149
+ assert.equal(typeof body.queryCountLast24h, 'number');
150
+ // lastReindexAt may be null in a fresh project
151
+ assert.ok('lastReindexAt' in body);
152
+ });
153
+ });
154
+
155
+ describe('GET /api/memory/obsidian/tree', () => {
156
+ test('returns a tree node (or null for empty vault)', async () => {
157
+ const r = await get('/memory/obsidian/tree');
158
+ assert.equal(r.status, 200);
159
+ const body = await r.json();
160
+ assert.ok('tree' in body);
161
+ });
162
+ });
163
+
164
+ describe('CRUD: POST/PUT/GET/DELETE /api/memory/notes/*', () => {
165
+ const notePath = 'daily/2026-07-05.md';
166
+ let createdBody;
167
+
168
+ const fullFrontmatter = {
169
+ memory_id: 'mem-test-2026-07-05',
170
+ project_id: 'memtab-test',
171
+ type: 'session_summary',
172
+ status: 'draft',
173
+ confidence: 'inferred',
174
+ created: '2026-07-05',
175
+ updated: '2026-07-05',
176
+ tags: ['test'],
177
+ };
178
+
179
+ test('POST creates a note', async () => {
180
+ const r = await post('/memory/notes', {
181
+ path: notePath,
182
+ frontmatter: { ...fullFrontmatter, title: 'Test note' },
183
+ body: 'This note links to [[2026-07-04]] and [[notes/other]].',
184
+ });
185
+ assert.equal(r.status, 201);
186
+ createdBody = await r.json();
187
+ assert.equal(createdBody.relPath, notePath);
188
+ assert.equal(createdBody.frontmatter.title, 'Test note');
189
+ });
190
+
191
+ test('GET /memory/notes/:path returns the note', async () => {
192
+ const r = await get(`/memory/notes/${encodeURI(notePath)}`);
193
+ assert.equal(r.status, 200);
194
+ const body = await r.json();
195
+ assert.equal(body.frontmatter.title, 'Test note');
196
+ });
197
+
198
+ test('PUT updates the note', async () => {
199
+ const r = await put(`/memory/notes/${encodeURI(notePath)}`, {
200
+ frontmatter: { ...fullFrontmatter, title: 'Updated test note', tags: ['test', 'updated'] },
201
+ body: 'Updated body. References [[2026-07-04]] again.',
202
+ });
203
+ assert.equal(r.status, 200);
204
+ const body = await r.json();
205
+ assert.equal(body.frontmatter.title, 'Updated test note');
206
+ assert.deepEqual(body.frontmatter.tags, ['test', 'updated']);
207
+ });
208
+
209
+ test('DELETE removes the note', async () => {
210
+ const r = await del(`/memory/notes/${encodeURI(notePath)}`);
211
+ assert.equal(r.status, 204);
212
+ const r2 = await get(`/memory/notes/${encodeURI(notePath)}`);
213
+ assert.equal(r2.status, 404);
214
+ });
215
+ });
216
+
217
+ describe('GET /api/memory/obsidian/backlinks', () => {
218
+ const fullFrontmatter = {
219
+ memory_id: 'mem-test-backlinks',
220
+ project_id: 'memtab-test',
221
+ type: 'session_summary',
222
+ status: 'draft',
223
+ confidence: 'inferred',
224
+ created: '2026-07-04',
225
+ updated: '2026-07-04',
226
+ tags: ['test'],
227
+ };
228
+
229
+ test('returns empty backlinks for an unknown note', async () => {
230
+ const r = await get('/memory/obsidian/backlinks?note=does-not-exist.md');
231
+ assert.equal(r.status, 200);
232
+ const body = await r.json();
233
+ assert.equal(body.note, 'does-not-exist.md');
234
+ assert.deepEqual(body.backlinks, []);
235
+ });
236
+
237
+ test('finds a backlink to a known target', async () => {
238
+ // Create the target note first.
239
+ await post('/memory/notes', {
240
+ path: 'daily/2026-07-04.md',
241
+ frontmatter: { ...fullFrontmatter, title: 'Yesterday' },
242
+ body: 'yesterday content',
243
+ });
244
+ // Create a note that links to it.
245
+ await post('/memory/notes', {
246
+ path: 'daily/2026-07-05-linker.md',
247
+ frontmatter: { ...fullFrontmatter, title: 'Today', memory_id: 'mem-test-linker' },
248
+ body: 'See [[2026-07-04]] for context.',
249
+ });
250
+ const r = await get('/memory/obsidian/backlinks?note=' + encodeURIComponent('daily/2026-07-04.md'));
251
+ const body = await r.json();
252
+ assert.ok(body.backlinks.length >= 1, 'expected at least one backlink');
253
+ const backlink = body.backlinks.find((b) => b.fromRelPath === 'daily/2026-07-05-linker.md');
254
+ assert.ok(backlink, 'expected backlink from 2026-07-05-linker.md');
255
+ assert.match(backlink.snippet, /\[\[2026-07-04\]\]/);
256
+ });
257
+
258
+ test('rejects missing note param', async () => {
259
+ const r = await get('/memory/obsidian/backlinks');
260
+ assert.equal(r.status, 400);
261
+ });
262
+ });
263
+
264
+ describe('POST /api/memory/semantic-search', () => {
265
+ test('returns merged lexical + lightrag results', async () => {
266
+ const r = await post('/memory/semantic-search', {
267
+ query: 'today',
268
+ limit: 5,
269
+ sources: ['obsidian'],
270
+ });
271
+ assert.equal(r.status, 200);
272
+ const body = await r.json();
273
+ assert.equal(body.query, 'today');
274
+ assert.ok(Array.isArray(body.results));
275
+ // lightrag is disabled by default → source should only contain obsidian
276
+ for (const result of body.results) {
277
+ assert.equal(result.source, 'obsidian');
278
+ }
279
+ });
280
+
281
+ test('deduplicates by (source, relPath)', async () => {
282
+ const r = await post('/memory/semantic-search', {
283
+ query: 'today',
284
+ limit: 5,
285
+ });
286
+ const body = await r.json();
287
+ const seen = new Set();
288
+ for (const result of body.results) {
289
+ const k = `${result.source}|${result.relPath || '_query_'}`;
290
+ assert.ok(!seen.has(k), `duplicate key ${k}`);
291
+ seen.add(k);
292
+ }
293
+ });
294
+
295
+ test('rejects empty query', async () => {
296
+ const r = await post('/memory/semantic-search', { query: '' });
297
+ assert.equal(r.status, 400);
298
+ });
299
+ });
300
+
301
+ describe('GET /api/memory/git/diff', () => {
302
+ test('returns hasDiff=false in local-only mode', async () => {
303
+ const r = await get('/memory/git/diff');
304
+ assert.equal(r.status, 200);
305
+ const body = await r.json();
306
+ assert.equal(body.mode, 'local-only');
307
+ assert.equal(body.hasDiff, false);
308
+ assert.ok(Array.isArray(body.lines));
309
+ assert.ok(Array.isArray(body.files));
310
+ });
311
+ });
312
+
313
+ describe('GET /api/memory/status', () => {
314
+ test('returns initialized=true', async () => {
315
+ const r = await get('/memory/status');
316
+ assert.equal(r.status, 200);
317
+ const body = await r.json();
318
+ assert.equal(body.initialized, true);
319
+ assert.equal(body.mode, 'local-only');
320
+ assert.ok(typeof body.noteCount === 'number');
321
+ });
322
+ });
@@ -0,0 +1,178 @@
1
+ /**
2
+ * minimax-chat-usage.test.mjs
3
+ *
4
+ * Verifies that chatCompletion() in minimax.mjs calls recordUsage()
5
+ * with the correct shape after every call (success, error, and cached).
6
+ *
7
+ * Run with: node --test bizar-dash/tests/minimax-chat-usage.test.mjs
8
+ */
9
+ import { describe, it, before, after, beforeEach } from 'node:test';
10
+ import assert from 'node:assert/strict';
11
+ import { mkdtempSync, rmSync, unlinkSync, existsSync, mkdirSync } from 'node:fs';
12
+ import { join, resolve } from 'node:path';
13
+ import { tmpdir } from 'node:os';
14
+
15
+ const REPO = resolve(import.meta.dirname, '..', '..');
16
+
17
+ let SANDBOX_HOME;
18
+ let ORIGINAL_BIZAR_STORE_HOME;
19
+
20
+ before(() => {
21
+ SANDBOX_HOME = mkdtempSync(join(tmpdir(), `bizar-chat-usage-test-${Date.now()}-`));
22
+ ORIGINAL_BIZAR_STORE_HOME = process.env.BIZAR_STORE_HOME;
23
+ // Set BIZAR_STORE_HOME so the store module resolves to our sandbox.
24
+ process.env.BIZAR_STORE_HOME = join(SANDBOX_HOME, '.local', 'share', 'bizar');
25
+ });
26
+
27
+ after(() => {
28
+ process.env.BIZAR_STORE_HOME = ORIGINAL_BIZAR_STORE_HOME ?? '';
29
+ try { rmSync(SANDBOX_HOME, { recursive: true, force: true }); } catch { /* ignore */ }
30
+ });
31
+
32
+ beforeEach(() => {
33
+ // Wipe the usage store before each test by directly unlinking the file.
34
+ const storeFile = join(process.env.BIZAR_STORE_HOME, 'usage.jsonl');
35
+ try { unlinkSync(storeFile); } catch { /* ignore */ }
36
+ });
37
+
38
+ /** Load the store module fresh each time with a unique cache-busting query param. */
39
+ async function getStore() {
40
+ const stamp = `${Date.now()}-${Math.random()}`;
41
+ return import(`../src/server/minimax-usage-store.mjs?nocache=${stamp}`);
42
+ }
43
+
44
+ // ─── Tests ─────────────────────────────────────────────────────────────────
45
+
46
+ /**
47
+ * We can't easily intercept recordUsage calls directly, but we CAN
48
+ * verify the effect: after calling chatCompletion, the JSONL file
49
+ * contains one more line, and the shape of the record is correct.
50
+ *
51
+ * We mock the globalThis.fetch to return a fake MiniMax response,
52
+ * and verify the recorded usage entry.
53
+ */
54
+ describe('chatCompletion records usage', () => {
55
+ it('records a success usage entry', async () => {
56
+ const store = await getStore();
57
+
58
+ // Mock fetch to return a fake MiniMax chat completion response.
59
+ const originalFetch = globalThis.fetch;
60
+ globalThis.fetch = function mockFetch(url, opts) {
61
+ if (String(url).includes('/chat/completions')) {
62
+ return Promise.resolve({
63
+ ok: true,
64
+ status: 200,
65
+ headers: { get: () => 'application/json' },
66
+ text: () => Promise.resolve(JSON.stringify({
67
+ id: 'msg_test',
68
+ model: 'MiniMax-M3',
69
+ choices: [{
70
+ finish_reason: 'stop',
71
+ message: { role: 'assistant', content: 'pong' },
72
+ }],
73
+ usage: {
74
+ prompt_tokens: 12,
75
+ completion_tokens: 3,
76
+ total_tokens: 15,
77
+ completion_tokens_details: { reasoning_tokens: 1 },
78
+ },
79
+ base_resp: { status_code: 0, status_msg: 'Success' },
80
+ })),
81
+ });
82
+ }
83
+ return originalFetch.call(this, url, opts);
84
+ };
85
+
86
+ try {
87
+ // Force re-import of minimax.mjs with the patched HOME.
88
+ const { chatCompletion } = await import(`../src/server/minimax.mjs?v=${Date.now()}`);
89
+ const result = await chatCompletion({ prompt: 'pong', model: 'MiniMax-M3', maxTokens: 16 });
90
+
91
+ assert.equal(result.ok, true);
92
+ assert.equal(result.model, 'MiniMax-M3');
93
+ assert.equal(result.content, 'pong');
94
+
95
+ // Now verify the usage was recorded.
96
+ const all = store.readAllRecords();
97
+ const entries = all.filter(r => r.requestId.startsWith('msg_'));
98
+ assert.ok(entries.length >= 1, `Expected at least 1 usage record, got ${entries.length}`);
99
+
100
+ const entry = entries[entries.length - 1];
101
+ assert.equal(entry.providerId, 'minimax');
102
+ assert.equal(entry.modelId, 'MiniMax-M3');
103
+ assert.equal(entry.endpoint, 'chat');
104
+ assert.equal(entry.promptTokens, 12);
105
+ assert.equal(entry.completionTokens, 3);
106
+ assert.equal(entry.totalTokens, 15);
107
+ assert.equal(entry.reasoningTokens, 1);
108
+ assert.equal(entry.finishReason, 'stop');
109
+ assert.equal(entry.error, null);
110
+ assert.equal(entry.cached, false);
111
+ } finally {
112
+ globalThis.fetch = originalFetch;
113
+ }
114
+ });
115
+
116
+ it('records an error usage entry on network failure', async () => {
117
+ const store = await getStore();
118
+
119
+ const originalFetch = globalThis.fetch;
120
+ globalThis.fetch = function mockFetch(url, opts) {
121
+ if (String(url).includes('/chat/completions')) {
122
+ return Promise.reject(new Error('ECONNREFUSED'));
123
+ }
124
+ return originalFetch.call(this, url, opts);
125
+ };
126
+
127
+ try {
128
+ const { chatCompletion } = await import(`../src/server/minimax.mjs?v=${Date.now() + 1}`);
129
+ const result = await chatCompletion({ prompt: 'ping', model: 'MiniMax-M3' });
130
+
131
+ assert.equal(result.ok, false);
132
+ assert.equal(result.error, 'network_error');
133
+
134
+ const all = store.readAllRecords();
135
+ const errorEntries = all.filter(r => r.error !== null);
136
+ assert.ok(errorEntries.length >= 1);
137
+ const entry = errorEntries[errorEntries.length - 1];
138
+ assert.equal(entry.error.code, 'network_error');
139
+ assert.equal(entry.endpoint, 'chat');
140
+ } finally {
141
+ globalThis.fetch = originalFetch;
142
+ }
143
+ });
144
+
145
+ it('records an error usage entry on HTTP 429', async () => {
146
+ const store = await getStore();
147
+
148
+ const originalFetch = globalThis.fetch;
149
+ globalThis.fetch = function mockFetch(url, opts) {
150
+ if (String(url).includes('/chat/completions')) {
151
+ return Promise.resolve({
152
+ ok: false,
153
+ status: 429,
154
+ headers: { get: () => 'application/json' },
155
+ text: () => Promise.resolve(JSON.stringify({
156
+ base_resp: { status_code: 429, status_msg: 'Rate limit exceeded' },
157
+ })),
158
+ });
159
+ }
160
+ return originalFetch.call(this, url, opts);
161
+ };
162
+
163
+ try {
164
+ const { chatCompletion } = await import(`../src/server/minimax.mjs?v=${Date.now() + 2}`);
165
+ const result = await chatCompletion({ prompt: 'ping', model: 'MiniMax-M3' });
166
+ assert.equal(result.ok, false);
167
+ assert.equal(result.error, 'http_429');
168
+
169
+ const all = store.readAllRecords();
170
+ const errorEntries = all.filter(r => r.error !== null);
171
+ assert.ok(errorEntries.length >= 1);
172
+ const entry = errorEntries[errorEntries.length - 1];
173
+ assert.equal(entry.error.code, 'http_429');
174
+ } finally {
175
+ globalThis.fetch = originalFetch;
176
+ }
177
+ });
178
+ });