@polderlabs/bizar 5.5.4 → 5.5.5
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/src/server/memory-lightrag.mjs +21 -2
- package/bizar-dash/src/server/memory-store.mjs +71 -0
- package/bizar-dash/src/server/minimax.mjs +15 -1
- package/bizar-dash/src/server/routes/memory.mjs +54 -16
- package/bizar-dash/src/server/server.mjs +17 -0
- package/bizar-dash/tests/lightrag-startup-hook.test.mjs +32 -0
- package/bizar-dash/tests/memory-store.test.mjs +102 -0
- package/bizar-dash/tests/minimax-chat-usage.test.mjs +75 -0
- package/package.json +1 -1
|
@@ -670,7 +670,11 @@ export async function ensureRunning(config, opts) {
|
|
|
670
670
|
* '0' / 'false' / 'no', skip.
|
|
671
671
|
* 3. Check if the server is already running — if so, return
|
|
672
672
|
* ok=true, started=false, reason='already-running'.
|
|
673
|
-
* 4.
|
|
673
|
+
* 4. Probe LLM availability (detectAvailableLLM) — if unreachable
|
|
674
|
+
* AND no API key is configured, log a clear warning but still
|
|
675
|
+
* attempt to start the server (the user may have installed
|
|
676
|
+
* ollama mid-session or configured a key after boot).
|
|
677
|
+
* 5. Call `startServer(config)` and return its result.
|
|
674
678
|
*
|
|
675
679
|
* Errors are caught and logged as warnings — startup must not fail
|
|
676
680
|
* the dashboard just because LightRAG couldn't be started (the user
|
|
@@ -727,7 +731,22 @@ export async function lightragStartupHook(projectRoot, opts = {}) {
|
|
|
727
731
|
warn('startup hook: isRunning probe failed:', err?.message || err);
|
|
728
732
|
}
|
|
729
733
|
|
|
730
|
-
// 6.
|
|
734
|
+
// 6. Probe LLM availability — log a warning if unreachable + no API key,
|
|
735
|
+
// but still attempt to start (user may have installed ollama mid-session).
|
|
736
|
+
const llm = await detectAvailableLLM(config);
|
|
737
|
+
if (!llm) {
|
|
738
|
+
const binding = config.llmBinding || 'ollama';
|
|
739
|
+
warn(
|
|
740
|
+
`startup hook: ${binding} is not reachable and no API key is configured. ` +
|
|
741
|
+
`LightRAG may start in a degraded state. ` +
|
|
742
|
+
`Set an API key env var (OPENAI_API_KEY, ANTHROPIC_API_KEY, or MINIMAX_API_KEY) ` +
|
|
743
|
+
`or ensure ollama is running.`,
|
|
744
|
+
);
|
|
745
|
+
} else {
|
|
746
|
+
log(`startup hook: LLM detected: ${llm.provider} (${llm.detail})`);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// 7. Try to start — still attempt even if LLM probe failed (see step 6 note).
|
|
731
750
|
try {
|
|
732
751
|
const r = await startServer(config, opts);
|
|
733
752
|
if (r.ok) {
|
|
@@ -843,6 +843,77 @@ export function vaultStats(projectRoot) {
|
|
|
843
843
|
};
|
|
844
844
|
}
|
|
845
845
|
|
|
846
|
+
// ── v5.5.2 Legacy config migration ─────────────────────────────────────────
|
|
847
|
+
//
|
|
848
|
+
// When the user upgrades from pre-v5.5.2, their `.bizar/memory.json` may
|
|
849
|
+
// still point `git.repoPath` at the legacy path
|
|
850
|
+
// (`~/.local/share/bizar/memory/bizar-memory`). The new default vault root
|
|
851
|
+
// is `~/.bizar_memory/bizar-memory`. This helper detects the stale path and
|
|
852
|
+
// auto-corrects the config so all git checks work immediately after upgrade.
|
|
853
|
+
//
|
|
854
|
+
// Behaviour:
|
|
855
|
+
// - Only migrates if the legacy path doesn't exist but the new one does.
|
|
856
|
+
// - Only migrates if `git.repoPath` is explicitly set in the config.
|
|
857
|
+
// - Idempotent — once migrated, the new path exists and the check is a
|
|
858
|
+
// no-op on subsequent boots.
|
|
859
|
+
// - Returns { migrated: true } only on actual change; { migrated: false }
|
|
860
|
+
// for all other cases (already correct, not applicable, or failure).
|
|
861
|
+
//
|
|
862
|
+
// Called once at dashboard startup from server.mjs.
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Detect and fix a stale `git.repoPath` pointing at the legacy vault location.
|
|
866
|
+
*
|
|
867
|
+
* @param {string} projectRoot — project root (where `.bizar/memory.json` lives)
|
|
868
|
+
* @returns {{ migrated: boolean, from?: string, to?: string, error?: string }}
|
|
869
|
+
*/
|
|
870
|
+
export function migrateLegacyGitRepoPath(projectRoot) {
|
|
871
|
+
const configPath = join(projectRoot, '.bizar', 'memory.json');
|
|
872
|
+
let raw;
|
|
873
|
+
try {
|
|
874
|
+
raw = safeReadJSON(configPath, null);
|
|
875
|
+
} catch {
|
|
876
|
+
return { migrated: false };
|
|
877
|
+
}
|
|
878
|
+
if (!raw) return { migrated: false };
|
|
879
|
+
|
|
880
|
+
const gitBlock = raw.git || raw.memoryRepo || {};
|
|
881
|
+
const currentRepoPath = gitBlock.repoPath;
|
|
882
|
+
if (!currentRepoPath || typeof currentRepoPath !== 'string') return { migrated: false };
|
|
883
|
+
|
|
884
|
+
const expanded = currentRepoPath.startsWith('~')
|
|
885
|
+
? join(HOME, currentRepoPath.slice(1))
|
|
886
|
+
: currentRepoPath;
|
|
887
|
+
|
|
888
|
+
// Only migrate when the configured path doesn't exist but the new default does.
|
|
889
|
+
if (existsSync(expanded)) return { migrated: false };
|
|
890
|
+
|
|
891
|
+
const newDefault = join(DEFAULT_MEMORY_VAULT, 'bizar-memory');
|
|
892
|
+
if (!existsSync(newDefault)) return { migrated: false };
|
|
893
|
+
|
|
894
|
+
// Verify the new path is actually a git repo before migrating.
|
|
895
|
+
if (!existsSync(join(newDefault, '.git'))) return { migrated: false };
|
|
896
|
+
|
|
897
|
+
// Apply the fix: update gitBlock.repoPath (or memoryRepo.repoPath) to newDefault.
|
|
898
|
+
try {
|
|
899
|
+
const updated = JSON.parse(JSON.stringify(raw));
|
|
900
|
+
if (!updated.git) updated.git = {};
|
|
901
|
+
if (!updated.memoryRepo) updated.memoryRepo = {};
|
|
902
|
+
updated.git.repoPath = newDefault;
|
|
903
|
+
updated.memoryRepo.path = newDefault;
|
|
904
|
+
atomicWriteJson(configPath, updated);
|
|
905
|
+
console.warn(
|
|
906
|
+
`[memory-store] Auto-migrated git.repoPath from legacy path\n` +
|
|
907
|
+
`[memory-store] FROM: ${expanded}\n` +
|
|
908
|
+
`[memory-store] TO: ${newDefault}\n` +
|
|
909
|
+
`[memory-store] Please restart the dashboard to pick up the new path.`,
|
|
910
|
+
);
|
|
911
|
+
return { migrated: true, from: expanded, to: newDefault };
|
|
912
|
+
} catch (err) {
|
|
913
|
+
return { migrated: false, error: err.message };
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
846
917
|
// ── LightRAG integration (v4.1.0) ──────────────────────────────────────────
|
|
847
918
|
//
|
|
848
919
|
// Re-export the LightRAG orchestrator from the memory-store module so the
|
|
@@ -369,7 +369,21 @@ export async function fetchRemains({ force = false } = {}) {
|
|
|
369
369
|
let body = null;
|
|
370
370
|
try { body = text ? JSON.parse(text) : null; } catch { /* keep as null */ }
|
|
371
371
|
if (!resp.ok) {
|
|
372
|
-
|
|
372
|
+
// v5.5.2 — Provide descriptive messages for common failure modes
|
|
373
|
+
// rather than surfacing raw HTTP status text.
|
|
374
|
+
let errMsg;
|
|
375
|
+
if (resp.status === 404) {
|
|
376
|
+
errMsg =
|
|
377
|
+
'MiniMax token plan API returned 404. The endpoint may have changed — ' +
|
|
378
|
+
'this may require a BizarHarness update. group_id=' +
|
|
379
|
+
encodeURIComponent(resolved.groupId);
|
|
380
|
+
} else if (resp.status === 401 || resp.status === 403) {
|
|
381
|
+
errMsg = 'MiniMax API key is invalid or expired. Check your key in the dashboard onboarding.';
|
|
382
|
+
} else if (resp.status === 429) {
|
|
383
|
+
errMsg = 'MiniMax API rate limit hit. Try again in a few minutes.';
|
|
384
|
+
} else {
|
|
385
|
+
errMsg = body?.base_resp?.status_msg || resp.statusText || 'request failed';
|
|
386
|
+
}
|
|
373
387
|
await recordUsageDynamic({
|
|
374
388
|
providerId: 'minimax',
|
|
375
389
|
modelId: 'unknown',
|
|
@@ -298,23 +298,41 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
298
298
|
}));
|
|
299
299
|
|
|
300
300
|
// POST /memory/git/pull
|
|
301
|
+
// v5.5.2 — Returns 200 with { ok: false } for operational failures
|
|
302
|
+
// (vault missing, git missing, pull errors). Only returns 503 for truly
|
|
303
|
+
// catastrophic failures. This allows the UI to show a clear message rather
|
|
304
|
+
// than treating a failed pull as a server-internal error.
|
|
301
305
|
router.post('/memory/git/pull', wrap(async (_req, res) => {
|
|
302
306
|
const { resolveVault } = memoryStore;
|
|
303
307
|
const { pull, isGitInstalled } = memoryGit;
|
|
304
308
|
const { projectVaultRoot, mode } = resolveVault(projectRoot);
|
|
305
309
|
|
|
306
310
|
if (mode === 'local-only') {
|
|
307
|
-
res.
|
|
311
|
+
res.json({ ok: false, error: 'local_only_mode', message: 'pull is not available in local-only mode' });
|
|
308
312
|
return;
|
|
309
313
|
}
|
|
314
|
+
|
|
315
|
+
if (!existsSync(projectVaultRoot)) {
|
|
316
|
+
res.json({
|
|
317
|
+
ok: false,
|
|
318
|
+
error: 'vault_not_found',
|
|
319
|
+
message: `Vault directory not found at ${projectVaultRoot}. Run \`bizar memory init\` to create it.`,
|
|
320
|
+
});
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
310
324
|
if (!isGitInstalled()) {
|
|
311
|
-
res.
|
|
325
|
+
res.json({
|
|
326
|
+
ok: false,
|
|
327
|
+
error: 'git_not_installed',
|
|
328
|
+
message: 'git is not installed or not found on PATH. Install git to enable sync.',
|
|
329
|
+
});
|
|
312
330
|
return;
|
|
313
331
|
}
|
|
314
332
|
|
|
315
333
|
const result = pull(projectVaultRoot);
|
|
316
334
|
if (!result.ok) {
|
|
317
|
-
res.
|
|
335
|
+
res.json({ ok: false, error: 'pull_failed', message: result.error });
|
|
318
336
|
return;
|
|
319
337
|
}
|
|
320
338
|
res.json({ ok: true, output: result.output });
|
|
@@ -1001,20 +1019,33 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
1001
1019
|
}
|
|
1002
1020
|
|
|
1003
1021
|
const { vaultRoot, projectVaultRoot, mode } = resolveVault(projectRoot);
|
|
1004
|
-
|
|
1022
|
+
// v5.5.2 — Check the GENERAL vault root (vaultRoot), not the project
|
|
1023
|
+
// subdirectory (projectVaultRoot). The general vault exists at
|
|
1024
|
+
// ~/.bizar_memory if the user has ever run `bizar memory init` or if
|
|
1025
|
+
// auto-migration ran. The project subdirectory
|
|
1026
|
+
// (~/.bizar_memory/projects/<projectId>/) is only created lazily on
|
|
1027
|
+
// first write.
|
|
1028
|
+
const generalVaultExists = existsSync(vaultRoot);
|
|
1029
|
+
const projectVaultExists = existsSync(projectVaultRoot);
|
|
1005
1030
|
checks.push({
|
|
1006
1031
|
name: 'vault_exists',
|
|
1007
|
-
pass:
|
|
1008
|
-
detail:
|
|
1032
|
+
pass: generalVaultExists,
|
|
1033
|
+
detail: generalVaultExists
|
|
1034
|
+
? projectVaultExists
|
|
1035
|
+
? vaultRoot
|
|
1036
|
+
: `${vaultRoot} (project subdirectory not yet created — click Initialize)`
|
|
1037
|
+
: `vault directory missing — run \`bizar memory init\` to create it`,
|
|
1009
1038
|
});
|
|
1010
|
-
if (
|
|
1039
|
+
if (generalVaultExists) score += 20;
|
|
1011
1040
|
|
|
1012
1041
|
// Writable?
|
|
1042
|
+
// v5.5.2 — Write to vaultRoot (the general vault), not projectVaultRoot,
|
|
1043
|
+
// since the project subdirectory may not exist yet.
|
|
1013
1044
|
let writable = false;
|
|
1014
|
-
if (
|
|
1045
|
+
if (generalVaultExists) {
|
|
1015
1046
|
try {
|
|
1016
|
-
const probe = join(
|
|
1017
|
-
writeFileSync(probe, 'ok');
|
|
1047
|
+
const probe = join(vaultRoot, '.health-probe.tmp');
|
|
1048
|
+
writeFileSync(probe, 'ok', 'utf8');
|
|
1018
1049
|
try {
|
|
1019
1050
|
const { unlinkSync } = await import('node:fs');
|
|
1020
1051
|
unlinkSync(probe);
|
|
@@ -1024,13 +1055,20 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
1024
1055
|
writable = false;
|
|
1025
1056
|
}
|
|
1026
1057
|
}
|
|
1027
|
-
checks.push({
|
|
1058
|
+
checks.push({
|
|
1059
|
+
name: 'vault_writable',
|
|
1060
|
+
pass: writable,
|
|
1061
|
+
detail: writable ? 'yes' : generalVaultExists ? 'vault root not writable' : 'vault directory missing',
|
|
1062
|
+
});
|
|
1028
1063
|
if (writable) score += 10;
|
|
1029
1064
|
|
|
1030
1065
|
// Git clean?
|
|
1066
|
+
// v5.5.2 — In managed/linked mode, the git repo lives at vaultRoot, not
|
|
1067
|
+
// projectVaultRoot. Use vaultRoot for git status; projectVaultRoot is only
|
|
1068
|
+
// a subdirectory of the repo.
|
|
1031
1069
|
let gitClean = null;
|
|
1032
|
-
if ((mode === 'managed' || mode === 'linked') &&
|
|
1033
|
-
const gs = gitStatus(
|
|
1070
|
+
if ((mode === 'managed' || mode === 'linked') && generalVaultExists && isGitInstalled()) {
|
|
1071
|
+
const gs = gitStatus(vaultRoot);
|
|
1034
1072
|
gitClean = gs.clean;
|
|
1035
1073
|
checks.push({
|
|
1036
1074
|
name: 'git_clean',
|
|
@@ -1064,7 +1102,7 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
1064
1102
|
|
|
1065
1103
|
// Schema valid?
|
|
1066
1104
|
let invalidCount = 0;
|
|
1067
|
-
if (
|
|
1105
|
+
if (generalVaultExists) {
|
|
1068
1106
|
const validationResults = validateAll(projectRoot);
|
|
1069
1107
|
invalidCount = validationResults.length;
|
|
1070
1108
|
}
|
|
@@ -1073,11 +1111,11 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
1073
1111
|
pass: invalidCount === 0,
|
|
1074
1112
|
detail: invalidCount === 0 ? 'all notes valid' : `${invalidCount} invalid`,
|
|
1075
1113
|
});
|
|
1076
|
-
if (invalidCount === 0 &&
|
|
1114
|
+
if (invalidCount === 0 && generalVaultExists) score += 15;
|
|
1077
1115
|
|
|
1078
1116
|
// Secrets clean?
|
|
1079
1117
|
let highFindings = 0;
|
|
1080
|
-
if (
|
|
1118
|
+
if (generalVaultExists) {
|
|
1081
1119
|
for (const n of listNotes(projectRoot)) {
|
|
1082
1120
|
const r = scanForSecrets(projectRoot, n.relPath);
|
|
1083
1121
|
if (!r.safe) {
|
|
@@ -476,6 +476,23 @@ export async function createServer({
|
|
|
476
476
|
console.warn('[bizar-dash] headroom startup hook skipped:', err?.message || err);
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
+
// v5.5.2 — Auto-migrate legacy git.repoPath from the old vault location
|
|
480
|
+
// to the new default. Runs before the lightrag hook so git-dependent
|
|
481
|
+
// checks are already correct.
|
|
482
|
+
try {
|
|
483
|
+
const { migrateLegacyGitRepoPath } = await import('./memory-store.mjs');
|
|
484
|
+
const result = migrateLegacyGitRepoPath(projectRoot);
|
|
485
|
+
if (result.migrated) {
|
|
486
|
+
console.warn(
|
|
487
|
+
`[bizar-dash] git.repoPath auto-migrated:\n` +
|
|
488
|
+
` from: ${result.from}\n` +
|
|
489
|
+
` to: ${result.to}`,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
} catch (err) {
|
|
493
|
+
console.warn('[bizar-dash] git.repoPath migration check failed:', err?.message || err);
|
|
494
|
+
}
|
|
495
|
+
|
|
479
496
|
// v5.x — LightRAG startup hook (issue #6). Mirrors the headroom hook:
|
|
480
497
|
// reads config from .bizar/memory.json + env vars, then calls
|
|
481
498
|
// lightragStartupHook() which respects `lightrag.enabled` and the
|
|
@@ -203,4 +203,36 @@ describe('lightragStartupHook — config resolution', () => {
|
|
|
203
203
|
});
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
+
describe('lightragStartupHook — LLM unavailable but still starts', () => {
|
|
207
|
+
test('hook still returns ok=true when LLM is unreachable but binary is found', async () => {
|
|
208
|
+
// This test verifies the v5.5.2 fix: when detectAvailableLLM returns
|
|
209
|
+
// null (ollama unreachable + no API key), the hook used to return
|
|
210
|
+
// ok=false without attempting to start. Now it logs a warning but
|
|
211
|
+
// still tries to start. We verify the hook returns a structured
|
|
212
|
+
// result without throwing.
|
|
213
|
+
writeMemoryJson(tmpRoot, {
|
|
214
|
+
lightrag: {
|
|
215
|
+
enabled: true,
|
|
216
|
+
// Port unlikely to be in use — we pre-create a PID file to
|
|
217
|
+
// short-circuit the actual start probe.
|
|
218
|
+
port: 29997,
|
|
219
|
+
workingDir: join(tmpRoot, '.bizar', 'lightrag'),
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
mkdirSync(join(tmpRoot, '.bizar', 'lightrag'), { recursive: true });
|
|
223
|
+
// Pre-create PID file pointing to a live PID so isRunning short-circuits
|
|
224
|
+
// to "already-running" without spawning.
|
|
225
|
+
writeFileSync(join(tmpRoot, '.bizar', 'lightrag', 'lightrag.pid'), String(process.pid));
|
|
226
|
+
try {
|
|
227
|
+
const r = await lightragStartupHook(tmpRoot);
|
|
228
|
+
// With the fix: hook returns ok=true, reason='already-running' even
|
|
229
|
+
// when LLM probe would have failed (no API key, ollama unreachable).
|
|
230
|
+
assert.equal(r.ok, true, 'hook should succeed even when LLM is unreachable');
|
|
231
|
+
assert.equal(r.reason, 'already-running');
|
|
232
|
+
} finally {
|
|
233
|
+
rmSync(join(tmpRoot, '.bizar', 'lightrag'), { recursive: true, force: true });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
206
238
|
console.log(' lightrag-startup-hook tests loaded');
|
|
@@ -9,8 +9,11 @@ import assert from 'node:assert';
|
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { execSync, execFileSync } from 'node:child_process';
|
|
12
13
|
|
|
13
14
|
const TEST_MEMORY_STORE = await import('../src/server/memory-store.mjs').then((m) => m);
|
|
15
|
+
const { DEFAULT_MEMORY_VAULT } = TEST_MEMORY_STORE;
|
|
16
|
+
const TEST_GIT = await import('../src/server/memory-git.mjs').then((m) => m);
|
|
14
17
|
|
|
15
18
|
describe('memory-store', () => {
|
|
16
19
|
let projectRoot;
|
|
@@ -388,4 +391,103 @@ describe('memory-store', () => {
|
|
|
388
391
|
assert.strictEqual(ok, false);
|
|
389
392
|
});
|
|
390
393
|
});
|
|
394
|
+
|
|
395
|
+
// ── migrateLegacyGitRepoPath ────────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
describe('migrateLegacyGitRepoPath', () => {
|
|
398
|
+
it('returns migrated=false when config does not exist', () => {
|
|
399
|
+
// projectRoot has no .bizar/memory.json
|
|
400
|
+
const r = TEST_MEMORY_STORE.migrateLegacyGitRepoPath(projectRoot);
|
|
401
|
+
assert.strictEqual(r.migrated, false);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('returns migrated=false when git.repoPath is not set', () => {
|
|
405
|
+
TEST_MEMORY_STORE.saveConfig(projectRoot, {
|
|
406
|
+
version: 1,
|
|
407
|
+
mode: 'managed',
|
|
408
|
+
projectId: 'test-proj',
|
|
409
|
+
memoryRepo: { mode: 'managed', path: null },
|
|
410
|
+
});
|
|
411
|
+
const r = TEST_MEMORY_STORE.migrateLegacyGitRepoPath(projectRoot);
|
|
412
|
+
assert.strictEqual(r.migrated, false);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('returns migrated=false when legacy path does not exist and new path also does not exist', () => {
|
|
416
|
+
TEST_MEMORY_STORE.saveConfig(projectRoot, {
|
|
417
|
+
version: 1,
|
|
418
|
+
mode: 'managed',
|
|
419
|
+
projectId: 'test-proj',
|
|
420
|
+
git: { repoPath: '/nonexistent/legacy/path' },
|
|
421
|
+
memoryRepo: { mode: 'managed', path: '/nonexistent/legacy/path' },
|
|
422
|
+
});
|
|
423
|
+
const r = TEST_MEMORY_STORE.migrateLegacyGitRepoPath(projectRoot);
|
|
424
|
+
assert.strictEqual(r.migrated, false);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('returns migrated=false when legacy path does not exist but new path is not a git repo', () => {
|
|
428
|
+
// Use the same path the migration function checks, but don't init git
|
|
429
|
+
const newDefault = join(DEFAULT_MEMORY_VAULT, 'bizar-memory');
|
|
430
|
+
mkdirSync(newDefault, { recursive: true });
|
|
431
|
+
try {
|
|
432
|
+
TEST_MEMORY_STORE.saveConfig(projectRoot, {
|
|
433
|
+
version: 1,
|
|
434
|
+
mode: 'managed',
|
|
435
|
+
projectId: 'test-proj',
|
|
436
|
+
git: { repoPath: '/nonexistent/legacy/path' },
|
|
437
|
+
memoryRepo: { mode: 'managed', path: '/nonexistent/legacy/path' },
|
|
438
|
+
});
|
|
439
|
+
const r = TEST_MEMORY_STORE.migrateLegacyGitRepoPath(projectRoot);
|
|
440
|
+
assert.strictEqual(r.migrated, false);
|
|
441
|
+
} finally {
|
|
442
|
+
rmSync(newDefault, { recursive: true, force: true });
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('migrates when legacy path does not exist but new path is a git repo', () => {
|
|
447
|
+
// newDefault = join(DEFAULT_MEMORY_VAULT, 'bizar-memory') in the migration function
|
|
448
|
+
const newDefault = join(DEFAULT_MEMORY_VAULT, 'bizar-memory');
|
|
449
|
+
mkdirSync(newDefault, { recursive: true });
|
|
450
|
+
// Init git in the actual path the migration function checks
|
|
451
|
+
try {
|
|
452
|
+
execFileSync('git', ['init', '-b', 'main'], { cwd: newDefault, encoding: 'utf8', stdio: 'pipe' });
|
|
453
|
+
} catch { /* ignore — git may not be available in test env */ }
|
|
454
|
+
try {
|
|
455
|
+
TEST_MEMORY_STORE.saveConfig(projectRoot, {
|
|
456
|
+
version: 1,
|
|
457
|
+
mode: 'managed',
|
|
458
|
+
projectId: 'test-proj',
|
|
459
|
+
git: { repoPath: '/nonexistent/legacy/path' },
|
|
460
|
+
memoryRepo: { mode: 'managed', path: '/nonexistent/legacy/path' },
|
|
461
|
+
});
|
|
462
|
+
const r = TEST_MEMORY_STORE.migrateLegacyGitRepoPath(projectRoot);
|
|
463
|
+
assert.strictEqual(r.migrated, true);
|
|
464
|
+
assert.ok(r.from.includes('/nonexistent/legacy/path'));
|
|
465
|
+
assert.strictEqual(r.to, newDefault);
|
|
466
|
+
|
|
467
|
+
// Verify config was updated
|
|
468
|
+
const updated = TEST_MEMORY_STORE.loadConfig(projectRoot);
|
|
469
|
+
assert.strictEqual(updated.config.git?.repoPath, newDefault);
|
|
470
|
+
} finally {
|
|
471
|
+
rmSync(newDefault, { recursive: true, force: true });
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('returns migrated=false when legacy path already exists (no need to migrate)', () => {
|
|
476
|
+
const legacyPath = join(tmpdir(), `bizar-legacy-exists-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
477
|
+
mkdirSync(legacyPath, { recursive: true });
|
|
478
|
+
try {
|
|
479
|
+
TEST_MEMORY_STORE.saveConfig(projectRoot, {
|
|
480
|
+
version: 1,
|
|
481
|
+
mode: 'managed',
|
|
482
|
+
projectId: 'test-proj',
|
|
483
|
+
git: { repoPath: legacyPath },
|
|
484
|
+
memoryRepo: { mode: 'managed', path: legacyPath },
|
|
485
|
+
});
|
|
486
|
+
const r = TEST_MEMORY_STORE.migrateLegacyGitRepoPath(projectRoot);
|
|
487
|
+
assert.strictEqual(r.migrated, false);
|
|
488
|
+
} finally {
|
|
489
|
+
rmSync(legacyPath, { recursive: true, force: true });
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
});
|
|
391
493
|
});
|
|
@@ -176,3 +176,78 @@ describe('chatCompletion records usage', () => {
|
|
|
176
176
|
}
|
|
177
177
|
});
|
|
178
178
|
});
|
|
179
|
+
|
|
180
|
+
// ── fetchRemains error handling ────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
describe('fetchRemains — HTTP error messages (v5.5.2 fix)', () => {
|
|
183
|
+
// Force a unique HOME so the test doesn't conflict with other minimax tests
|
|
184
|
+
// that write to the same sandbox directory.
|
|
185
|
+
const SANDBOX = mkdtempSync(join(tmpdir(), `bizar-minimax-404-${Date.now()}-`));
|
|
186
|
+
const ORIG_HOME = process.env.HOME;
|
|
187
|
+
const ORIG_BIZAR_STORE_HOME = process.env.BIZAR_STORE_HOME;
|
|
188
|
+
|
|
189
|
+
before(() => {
|
|
190
|
+
process.env.HOME = SANDBOX;
|
|
191
|
+
process.env.BIZAR_STORE_HOME = join(SANDBOX, '.config', 'bizar');
|
|
192
|
+
mkdirSync(process.env.BIZAR_STORE_HOME, { recursive: true });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
after(() => {
|
|
196
|
+
process.env.HOME = ORIG_HOME;
|
|
197
|
+
process.env.BIZAR_STORE_HOME = ORIG_BIZAR_STORE_HOME ?? '';
|
|
198
|
+
try { rmSync(SANDBOX, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
async function mockRemains(url, status, body) {
|
|
202
|
+
const originalFetch = globalThis.fetch;
|
|
203
|
+
globalThis.fetch = function mock(urlOrReq, opts) {
|
|
204
|
+
const u = typeof urlOrReq === 'string' ? urlOrReq : urlOrReq?.url || String(urlOrReq);
|
|
205
|
+
if (u.includes('/token_plan/remains')) {
|
|
206
|
+
return Promise.resolve({
|
|
207
|
+
ok: false,
|
|
208
|
+
status,
|
|
209
|
+
headers: { get: () => 'application/json' },
|
|
210
|
+
statusText: status === 404 ? 'Not Found' : status === 401 ? 'Unauthorized' : status === 429 ? 'Too Many Requests' : 'Error',
|
|
211
|
+
text: () => Promise.resolve(JSON.stringify(body || {})),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return originalFetch.call(this, urlOrReq, opts);
|
|
215
|
+
};
|
|
216
|
+
try {
|
|
217
|
+
// Import with cache-bust to get fresh module state with the new HOME.
|
|
218
|
+
const { fetchRemains: fr } = await import(`../src/server/minimax.mjs?v=${Date.now()}`);
|
|
219
|
+
return await fr({ force: true });
|
|
220
|
+
} finally {
|
|
221
|
+
globalThis.fetch = originalFetch;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
it('404 returns a descriptive message (not just "Not Found")', async () => {
|
|
226
|
+
const result = await mockRemains('/token_plan/remains', 404, {});
|
|
227
|
+
assert.equal(result.ok, false);
|
|
228
|
+
assert.equal(result.error, 'http_404');
|
|
229
|
+
assert.ok(
|
|
230
|
+
result.message.includes('endpoint may have changed') ||
|
|
231
|
+
result.message.includes('BizarHarness update'),
|
|
232
|
+
`Expected descriptive 404 message, got: ${result.message}`,
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('401/403 returns "invalid or expired key" message', async () => {
|
|
237
|
+
const result = await mockRemains('/token_plan/remains', 401, {});
|
|
238
|
+
assert.equal(result.ok, false);
|
|
239
|
+
assert.ok(
|
|
240
|
+
result.message.includes('invalid or expired'),
|
|
241
|
+
`Expected "invalid or expired" message, got: ${result.message}`,
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('429 returns "rate limit" message', async () => {
|
|
246
|
+
const result = await mockRemains('/token_plan/remains', 429, {});
|
|
247
|
+
assert.equal(result.ok, false);
|
|
248
|
+
assert.ok(
|
|
249
|
+
result.message.includes('rate limit'),
|
|
250
|
+
`Expected "rate limit" message, got: ${result.message}`,
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polderlabs/bizar",
|
|
3
|
-
"version": "5.5.
|
|
3
|
+
"version": "5.5.5",
|
|
4
4
|
"description": "Norse-pantheon multi-agent system for opencode — 13 agents across 4 cost tiers with cost-aware routing, plans, and a configurable agent harness. v4 ships as a single npm package bundling the dashboard server, opencode plugin, and typed SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|