@polderlabs/bizar 4.4.13 → 4.5.0

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 (90) hide show
  1. package/bizar-dash/CHANGELOG.md +37 -276
  2. package/bizar-dash/dist/assets/main-CDFKHzBg.css +1 -0
  3. package/bizar-dash/dist/assets/main-NYFpS2wY.js +312 -0
  4. package/bizar-dash/dist/assets/main-NYFpS2wY.js.map +1 -0
  5. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile--0FBIKX3.js} +2 -2
  6. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile--0FBIKX3.js.map} +1 -1
  7. package/bizar-dash/dist/assets/mobile-OgRp8VIb.js +352 -0
  8. package/bizar-dash/dist/assets/mobile-OgRp8VIb.js.map +1 -0
  9. package/bizar-dash/dist/index.html +3 -3
  10. package/bizar-dash/dist/mobile.html +2 -2
  11. package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
  12. package/bizar-dash/skills/bizar/SKILL.md +96 -0
  13. package/bizar-dash/skills/chat/SKILL.md +74 -0
  14. package/bizar-dash/skills/lightrag/SKILL.md +75 -0
  15. package/bizar-dash/skills/minimax/SKILL.md +80 -0
  16. package/bizar-dash/skills/obsidian/SKILL.md +55 -0
  17. package/bizar-dash/skills/providers/SKILL.md +75 -0
  18. package/bizar-dash/skills/sdk/SKILL.md +138 -0
  19. package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
  20. package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
  21. package/bizar-dash/skills/usage/SKILL.md +62 -0
  22. package/bizar-dash/src/server/api.mjs +12 -0
  23. package/bizar-dash/src/server/memory-lightrag.mjs +5 -2
  24. package/bizar-dash/src/server/memory-store.mjs +38 -0
  25. package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
  26. package/bizar-dash/src/server/minimax.mjs +196 -5
  27. package/bizar-dash/src/server/providers-store.mjs +956 -0
  28. package/bizar-dash/src/server/routes/config.mjs +52 -1
  29. package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
  30. package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
  31. package/bizar-dash/src/server/routes/memory.mjs +241 -1
  32. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
  33. package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
  34. package/bizar-dash/src/server/routes/providers.mjs +266 -5
  35. package/bizar-dash/src/server/routes/skills.mjs +32 -43
  36. package/bizar-dash/src/server/routes/update.mjs +340 -0
  37. package/bizar-dash/src/server/routes/usage.mjs +136 -0
  38. package/bizar-dash/src/server/serve-info.mjs +135 -4
  39. package/bizar-dash/src/server/server.mjs +4 -0
  40. package/bizar-dash/src/server/skills-store.mjs +152 -262
  41. package/bizar-dash/src/web/App.tsx +118 -29
  42. package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
  43. package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
  44. package/bizar-dash/src/web/components/Topbar.tsx +0 -1
  45. package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
  46. package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
  47. package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
  48. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
  49. package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
  50. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
  51. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
  52. package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
  53. package/bizar-dash/src/web/lib/api.ts +43 -0
  54. package/bizar-dash/src/web/main.tsx +1 -0
  55. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
  56. package/bizar-dash/src/web/styles/chat.css +135 -1
  57. package/bizar-dash/src/web/styles/main.css +46 -0
  58. package/bizar-dash/src/web/styles/minimax-usage.css +335 -0
  59. package/bizar-dash/src/web/styles/settings.css +418 -0
  60. package/bizar-dash/src/web/styles/skills.css +302 -0
  61. package/bizar-dash/src/web/styles/tasks.css +288 -0
  62. package/bizar-dash/src/web/views/Chat.tsx +276 -48
  63. package/bizar-dash/src/web/views/Config.tsx +3 -2065
  64. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +476 -461
  65. package/bizar-dash/src/web/views/Settings.tsx +6 -0
  66. package/bizar-dash/src/web/views/Skills.tsx +208 -260
  67. package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
  68. package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
  69. package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
  70. package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
  71. package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
  72. package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
  73. package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
  74. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
  75. package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
  76. package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
  77. package/bizar-dash/tests/skills-list.test.mjs +232 -0
  78. package/bizar-dash/tests/skills-search.test.mjs +222 -0
  79. package/bizar-dash/tests/tasks-create.test.mjs +187 -0
  80. package/bizar-dash/tests/update-check.test.mjs +127 -0
  81. package/bizar-dash/tests/update-run.test.mjs +266 -0
  82. package/cli/bin.mjs +82 -1
  83. package/cli/provision.mjs +118 -4
  84. package/config/agents/_shared/SKILLS.md +109 -0
  85. package/package.json +1 -1
  86. package/bizar-dash/dist/assets/main-BB5mJurD.js +0 -352
  87. package/bizar-dash/dist/assets/main-BB5mJurD.js.map +0 -1
  88. package/bizar-dash/dist/assets/main-BsnQLXdh.css +0 -1
  89. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
  90. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
@@ -0,0 +1,216 @@
1
+ /**
2
+ * env-vars-store.test.mjs — tests for the env-vars route and loadEnvJson.
3
+ *
4
+ * Run with: node --test bizar-dash/tests/env-vars-store.test.mjs
5
+ */
6
+ import { describe, it, before, after, beforeEach } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import { mkdtempSync, writeFileSync, readFileSync, rmSync, chmodSync, statSync } from 'node:fs';
9
+ import { join, resolve } from 'node:path';
10
+ import { tmpdir } from 'node:os';
11
+
12
+ const REPO = resolve(import.meta.dirname, '..', '..');
13
+
14
+ // Sandbox process.env for isolation
15
+ let SANDBOX_HOME;
16
+ let ORIGINAL_HOME;
17
+ let BIZAR_DIR;
18
+ let ENV_FILE;
19
+
20
+ before(() => {
21
+ SANDBOX_HOME = mkdtempSync(join(tmpdir(), `bizar-env-vars-test-${Date.now()}-`));
22
+ ORIGINAL_HOME = process.env.HOME;
23
+ process.env.HOME = SANDBOX_HOME;
24
+ BIZAR_DIR = join(SANDBOX_HOME, '.config', 'bizar');
25
+ ENV_FILE = join(BIZAR_DIR, 'env.json');
26
+ });
27
+
28
+ after(() => {
29
+ process.env.HOME = ORIGINAL_HOME;
30
+ // Clean up any lingering BIZAR_* test vars
31
+ for (const k of Object.keys(process.env)) {
32
+ if (k.startsWith('BIZAR_TEST_')) delete process.env[k];
33
+ }
34
+ try { rmSync(SANDBOX_HOME, { recursive: true, force: true }); } catch { /* ignore */ }
35
+ });
36
+
37
+ // Helper to create a supertest-like request helper via native http
38
+ async function request(app, method, path, body) {
39
+ const http = await import('node:http');
40
+ return new Promise((resolve, reject) => {
41
+ const server = app.listen(0, '127.0.0.1', () => {
42
+ const { port } = server.address();
43
+ const bodyStr = body ? JSON.stringify(body) : '';
44
+ const opts = {
45
+ hostname: '127.0.0.1',
46
+ port,
47
+ path,
48
+ method,
49
+ headers: {
50
+ 'Content-Type': 'application/json',
51
+ 'Content-Length': Buffer.byteLength(bodyStr),
52
+ },
53
+ };
54
+ const req = http.request(opts, (res) => {
55
+ let data = '';
56
+ res.on('data', (chunk) => { data += chunk; });
57
+ res.on('end', () => {
58
+ server.close();
59
+ try {
60
+ const parsed = data ? JSON.parse(data) : {};
61
+ resolve({ status: res.statusCode, body: parsed, raw: data });
62
+ } catch {
63
+ resolve({ status: res.statusCode, body: data, raw: data });
64
+ }
65
+ });
66
+ });
67
+ req.on('error', (err) => { server.close(); reject(err); });
68
+ if (bodyStr) req.write(bodyStr);
69
+ req.end();
70
+ });
71
+ });
72
+ }
73
+
74
+ describe('env-vars.mjs', () => {
75
+ // Build a minimal express app with just the env-vars router
76
+ let app;
77
+ beforeEach(async () => {
78
+ // Reset module state between tests by deleting the env file
79
+ try { rmSync(ENV_FILE); } catch { /* ignore */ }
80
+ // Force-reload the module for fresh state
81
+ const express = (await import('express')).default;
82
+ const { createEnvVarsRouter, loadEnvJson, resetStore } = await import(`${REPO}/bizar-dash/src/server/routes/env-vars.mjs`);
83
+ resetStore();
84
+ app = express();
85
+ app.use(express.json());
86
+ app.use('/api', createEnvVarsRouter());
87
+ // Clear any BIZAR_TEST_ vars left from previous tests
88
+ for (const k of Object.keys(process.env)) {
89
+ if (k.startsWith('BIZAR_TEST_')) delete process.env[k];
90
+ }
91
+ app.use(createEnvVarsRouter());
92
+ });
93
+
94
+ it('GET /api/env-vars returns empty list when file does not exist', async () => {
95
+ const res = await request(app, 'GET', '/api/env-vars');
96
+ assert.equal(res.status, 200);
97
+ assert.ok(Array.isArray(res.body), 'response should be an array');
98
+ assert.equal(res.body.length, 0);
99
+ });
100
+
101
+ it('POST creates entry, GET lists it with masked value', async () => {
102
+ const create = await request(app, 'POST', '/api/env-vars', {
103
+ name: 'BIZAR_TEST_KEY',
104
+ value: 'super-secret-value-123',
105
+ });
106
+ assert.equal(create.status, 201);
107
+ assert.equal(create.body.name, 'BIZAR_TEST_KEY');
108
+ assert.ok(create.body.value.includes('*'), 'value should be masked');
109
+ assert.ok(create.body.value.endsWith('-123'), 'masked value should include last 4 chars');
110
+ assert.ok(create.body.createdAt, 'should have createdAt');
111
+
112
+ const list = await request(app, 'GET', '/api/env-vars');
113
+ assert.equal(list.status, 200);
114
+ const entry = list.body.find((e) => e.name === 'BIZAR_TEST_KEY');
115
+ assert.ok(entry, 'entry should be in list');
116
+ assert.equal(entry.value, create.body.value, 'value should be masked on list');
117
+ });
118
+
119
+ it('PUT updates entry, DELETE removes it', async () => {
120
+ await request(app, 'POST', '/api/env-vars', { name: 'BIZAR_TEST_PUT', value: 'original' });
121
+ const update = await request(app, 'PUT', '/api/env-vars/BIZAR_TEST_PUT', { value: 'updated-value' });
122
+ assert.equal(update.status, 200);
123
+ assert.ok(update.body.value.endsWith('lue'), 'updated value should be masked');
124
+
125
+ // Verify in list
126
+ const list = await request(app, 'GET', '/api/env-vars');
127
+ const entry = list.body.find((e) => e.name === 'BIZAR_TEST_PUT');
128
+ assert.ok(entry);
129
+
130
+ // Delete
131
+ const del = await request(app, 'DELETE', '/api/env-vars/BIZAR_TEST_PUT');
132
+ assert.equal(del.status, 200);
133
+ assert.ok(del.body.ok);
134
+
135
+ // Verify gone
136
+ const listAfter = await request(app, 'GET', '/api/env-vars');
137
+ const gone = listAfter.body.find((e) => e.name === 'BIZAR_TEST_PUT');
138
+ assert.equal(gone, undefined, 'entry should be deleted');
139
+ });
140
+
141
+ it('rejects invalid name (must match /^BIZAR_[A-Z0-9_]+$/)', async () => {
142
+ const r1 = await request(app, 'POST', '/api/env-vars', { name: 'INVALID', value: 'x' });
143
+ assert.equal(r1.status, 400);
144
+ assert.ok(r1.body.error === 'invalid_name');
145
+
146
+ const r2 = await request(app, 'POST', '/api/env-vars', { name: 'bizar_lowercase', value: 'x' });
147
+ assert.equal(r2.status, 400);
148
+
149
+ const r3 = await request(app, 'POST', '/api/env-vars', { name: 'BIZAR HAS_SPACE', value: 'x' });
150
+ assert.equal(r3.status, 400);
151
+ });
152
+
153
+ it('creates env.json with mode 0600', async () => {
154
+ await request(app, 'POST', '/api/env-vars', { name: 'BIZAR_TEST_PERM', value: 'perm-check' });
155
+ const mode = statSync(ENV_FILE).mode & 0o777;
156
+ assert.equal(mode, 0o600, `expected 0o600, got ${octal(mode)}`);
157
+ });
158
+
159
+ it('loadEnvJson sets process.env', async () => {
160
+ // Create an env var
161
+ await request(app, 'POST', '/api/env-vars', { name: 'BIZAR_TEST_LOADENV', value: 'env-loaded-value' });
162
+
163
+ // Delete from process.env to simulate fresh start
164
+ delete process.env.BIZAR_TEST_LOADENV;
165
+
166
+ // Call loadEnvJson
167
+ const { loadEnvJson } = await import(`${REPO}/bizar-dash/src/server/routes/env-vars.mjs`);
168
+ loadEnvJson();
169
+
170
+ assert.equal(process.env.BIZAR_TEST_LOADENV, 'env-loaded-value',
171
+ 'process.env should be set after loadEnvJson');
172
+ });
173
+
174
+ it('DELETE removes from process.env', async () => {
175
+ // Create
176
+ await request(app, 'POST', '/api/env-vars', { name: 'BIZAR_TEST_DELPROCESS', value: 'to-be-deleted' });
177
+ assert.equal(process.env.BIZAR_TEST_DELPROCESS, 'to-be-deleted');
178
+
179
+ // Delete via API
180
+ await request(app, 'DELETE', '/api/env-vars/BIZAR_TEST_DELPROCESS');
181
+
182
+ assert.equal(process.env.BIZAR_TEST_DELPROCESS, undefined,
183
+ 'process.env should be cleared after DELETE');
184
+ });
185
+
186
+ it('POST /api/env-vars/:name/test returns referenced and inProcessEnv', async () => {
187
+ // Not in process.env yet
188
+ const r1 = await request(app, 'POST', '/api/env-vars', { name: 'BIZAR_TEST_REFS', value: 'refs-val' });
189
+ assert.equal(r1.status, 201);
190
+
191
+ const test = await request(app, 'POST', '/api/env-vars/BIZAR_TEST_REFS/test');
192
+ assert.equal(test.status, 200);
193
+ assert.ok(typeof test.body.referenced === 'boolean');
194
+ assert.ok(typeof test.body.inProcessEnv === 'boolean');
195
+ assert.equal(test.body.inProcessEnv, true, 'should be in process.env after creation');
196
+ });
197
+
198
+ it('409 on duplicate POST', async () => {
199
+ await request(app, 'POST', '/api/env-vars', { name: 'BIZAR_TEST_DUP', value: 'first' });
200
+ const r = await request(app, 'POST', '/api/env-vars', { name: 'BIZAR_TEST_DUP', value: 'second' });
201
+ assert.equal(r.status, 409);
202
+ assert.ok(r.body.error === 'already_exists');
203
+ });
204
+
205
+ it('404 on PUT/DELETE non-existent var', async () => {
206
+ const put = await request(app, 'PUT', '/api/env-vars/BIZAR_NONEXISTENT', { value: 'x' });
207
+ assert.equal(put.status, 404);
208
+
209
+ const del = await request(app, 'DELETE', '/api/env-vars/BIZAR_NONEXISTENT');
210
+ assert.equal(del.status, 404);
211
+ });
212
+ });
213
+
214
+ function octal(n) {
215
+ return '0o' + n.toString(8);
216
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * lightrag-defaults.node.test.mjs — regression tests for the v4.6.0
3
+ * LightRAG defaults.
4
+ *
5
+ * The memory service's lightrag subsystem starts with the free opencode
6
+ * Zen models by default (no API key required). Operators can override
7
+ * via env vars `BIZAR_LIGHTRAG_LLM` and `BIZAR_LIGHTRAG_EMBEDDING`.
8
+ *
9
+ * The defaults surface:
10
+ * - GET /api/lightrag/defaults (lightrag settings view)
11
+ * - GET /api/memory/lightrag/status (existing memory surface)
12
+ *
13
+ * Run with: node --test tests/lightrag-defaults.node.test.mjs
14
+ */
15
+ import { describe, it, before, after } from 'node:test';
16
+ import assert from 'node:assert/strict';
17
+ import { resolve } from 'node:path';
18
+
19
+ const REPO = resolve(import.meta.dirname, '..', '..');
20
+
21
+ let memoryStore;
22
+ let memoryLightrag;
23
+ let ORIGINAL_ENV;
24
+
25
+ before(async () => {
26
+ const cb = '?cb=' + Date.now() + '-' + Math.random().toString(36).slice(2);
27
+ memoryStore = await import(join(REPO, 'bizar-dash/src/server/memory-store.mjs') + cb);
28
+ memoryLightrag = await import(join(REPO, 'bizar-dash/src/server/memory-lightrag.mjs') + cb);
29
+ ORIGINAL_ENV = {
30
+ BIZAR_LIGHTRAG_LLM: process.env.BIZAR_LIGHTRAG_LLM,
31
+ BIZAR_LIGHTRAG_EMBEDDING: process.env.BIZAR_LIGHTRAG_EMBEDDING,
32
+ };
33
+ delete process.env.BIZAR_LIGHTRAG_LLM;
34
+ delete process.env.BIZAR_LIGHTRAG_EMBEDDING;
35
+ });
36
+
37
+ after(() => {
38
+ for (const [k, v] of Object.entries(ORIGINAL_ENV)) {
39
+ if (v === undefined) delete process.env[k];
40
+ else process.env[k] = v;
41
+ }
42
+ });
43
+
44
+ import { join } from 'node:path';
45
+
46
+ describe('LIGHTRAG_DEFAULT_* constants (v4.6.0)', () => {
47
+ it('default LLM is opencode/gpt-5-nano', () => {
48
+ assert.equal(memoryStore.LIGHTRAG_DEFAULT_LLM, 'opencode/gpt-5-nano');
49
+ });
50
+ it('default embedding is opencode/text-embedding-3-small', () => {
51
+ assert.equal(memoryStore.LIGHTRAG_DEFAULT_EMBEDDING, 'opencode/text-embedding-3-small');
52
+ });
53
+ });
54
+
55
+ describe('getDefaultLightRAGConfig()', () => {
56
+ it('returns opencode-free defaults when no env overrides', () => {
57
+ delete process.env.BIZAR_LIGHTRAG_LLM;
58
+ delete process.env.BIZAR_LIGHTRAG_EMBEDDING;
59
+ const cfg = memoryStore.getDefaultLightRAGConfig();
60
+ assert.equal(cfg.llm, 'opencode/gpt-5-nano');
61
+ assert.equal(cfg.embedding, 'opencode/text-embedding-3-small');
62
+ assert.equal(cfg.source, 'opencode-free');
63
+ assert.equal(cfg.llmSource, 'default');
64
+ assert.equal(cfg.embeddingSource, 'default');
65
+ });
66
+
67
+ it('env BIZAR_LIGHTRAG_LLM overrides the default LLM', () => {
68
+ process.env.BIZAR_LIGHTRAG_LLM = 'anthropic/claude-haiku-4-5';
69
+ const cfg = memoryStore.getDefaultLightRAGConfig();
70
+ assert.equal(cfg.llm, 'anthropic/claude-haiku-4-5');
71
+ assert.equal(cfg.embedding, 'opencode/text-embedding-3-small');
72
+ assert.equal(cfg.source, 'env');
73
+ assert.equal(cfg.llmSource, 'env');
74
+ assert.equal(cfg.embeddingSource, 'default');
75
+ delete process.env.BIZAR_LIGHTRAG_LLM;
76
+ });
77
+
78
+ it('env BIZAR_LIGHTRAG_EMBEDDING overrides the default embedding', () => {
79
+ process.env.BIZAR_LIGHTRAG_EMBEDDING = 'openai/text-embedding-3-large';
80
+ const cfg = memoryStore.getDefaultLightRAGConfig();
81
+ assert.equal(cfg.llm, 'opencode/gpt-5-nano');
82
+ assert.equal(cfg.embedding, 'openai/text-embedding-3-large');
83
+ assert.equal(cfg.embeddingSource, 'env');
84
+ delete process.env.BIZAR_LIGHTRAG_EMBEDDING;
85
+ });
86
+
87
+ it('both env vars override together', () => {
88
+ process.env.BIZAR_LIGHTRAG_LLM = 'minimax/MiniMax-M3';
89
+ process.env.BIZAR_LIGHTRAG_EMBEDDING = 'openai/text-embedding-3-small';
90
+ const cfg = memoryStore.getDefaultLightRAGConfig();
91
+ assert.equal(cfg.llm, 'minimax/MiniMax-M3');
92
+ assert.equal(cfg.embedding, 'openai/text-embedding-3-small');
93
+ assert.equal(cfg.source, 'env');
94
+ assert.equal(cfg.llmSource, 'env');
95
+ assert.equal(cfg.embeddingSource, 'env');
96
+ delete process.env.BIZAR_LIGHTRAG_LLM;
97
+ delete process.env.BIZAR_LIGHTRAG_EMBEDDING;
98
+ });
99
+
100
+ it('treats empty env vars as unset (falls back to defaults)', () => {
101
+ process.env.BIZAR_LIGHTRAG_LLM = '';
102
+ process.env.BIZAR_LIGHTRAG_EMBEDDING = ' ';
103
+ const cfg = memoryStore.getDefaultLightRAGConfig();
104
+ assert.equal(cfg.llm, 'opencode/gpt-5-nano');
105
+ assert.equal(cfg.embedding, 'opencode/text-embedding-3-small');
106
+ delete process.env.BIZAR_LIGHTRAG_LLM;
107
+ delete process.env.BIZAR_LIGHTRAG_EMBEDDING;
108
+ });
109
+ });
110
+
111
+ describe('memory-lightrag.mjs LIGHTRAG_DEFAULTS (v4.6.0)', () => {
112
+ it('default llmModel matches the opencode-Zen-free default', () => {
113
+ assert.equal(memoryLightrag.LIGHTRAG_DEFAULTS.llmModel, 'opencode/gpt-5-nano');
114
+ });
115
+ it('default embeddingModel matches the opencode-Zen-free default', () => {
116
+ assert.equal(memoryLightrag.LIGHTRAG_DEFAULTS.embeddingModel, 'opencode/text-embedding-3-small');
117
+ });
118
+ });
@@ -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
+ });