@polderlabs/bizar 5.5.0 → 5.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bizar-dash/dist/assets/{EnvVarsSection-L413CQEI.js → EnvVarsSection-DoIzqrlb.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-L413CQEI.js.map → EnvVarsSection-DoIzqrlb.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-0kGQb1S6.js → MobileChat-CoUCUsbr.js} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-0kGQb1S6.js.map → MobileChat-CoUCUsbr.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-BeO04g52.js +1 -0
- package/bizar-dash/dist/assets/MobileSettings-BeO04g52.js.map +1 -0
- package/bizar-dash/dist/assets/{Toast-C6f1zFJ2.js → Toast-D9nv5N6m.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-C6f1zFJ2.js.map → Toast-D9nv5N6m.js.map} +1 -1
- package/bizar-dash/dist/assets/{icons-BsWXWkEF.js → icons-Btipv2pp.js} +100 -90
- package/bizar-dash/dist/assets/icons-Btipv2pp.js.map +1 -0
- package/bizar-dash/dist/assets/main-DHXPuddY.js +18 -0
- package/bizar-dash/dist/assets/main-DHXPuddY.js.map +1 -0
- package/bizar-dash/dist/assets/{main-BklYy02e.css → main-UG4jKK-F.css} +1 -1
- package/bizar-dash/dist/assets/mobile-H5KH9cTL.js +1 -0
- package/bizar-dash/dist/assets/{mobile-CsaN4CxH.js.map → mobile-H5KH9cTL.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-layout-iFBdjEE4.js → mobile-layout-BqHxVb6_.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-layout-iFBdjEE4.js.map → mobile-layout-BqHxVb6_.js.map} +1 -1
- package/bizar-dash/dist/assets/{useSlashCommands-DGJcC9iQ.js → useSlashCommands-C24-O1e7.js} +2 -2
- package/bizar-dash/dist/assets/{useSlashCommands-DGJcC9iQ.js.map → useSlashCommands-C24-O1e7.js.map} +1 -1
- package/bizar-dash/dist/index.html +7 -7
- package/bizar-dash/dist/mobile.html +3 -3
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/bg-spawner.mjs +522 -305
- package/bizar-dash/src/server/headroom.mjs +3 -1
- package/bizar-dash/src/server/memory-lightrag.mjs +26 -2
- package/bizar-dash/src/server/memory-store.mjs +74 -28
- package/bizar-dash/src/server/opencode-sdk.mjs +63 -3
- package/bizar-dash/src/server/plugins/registry.mjs +1 -1
- package/bizar-dash/src/server/routes/activity.mjs +85 -0
- package/bizar-dash/src/server/routes/background.mjs +31 -13
- package/bizar-dash/src/server/routes/memory.mjs +91 -38
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +1 -1
- package/bizar-dash/src/server/server.mjs +4 -1
- package/bizar-dash/src/web/components/Sidebar.tsx +0 -1
- package/bizar-dash/src/web/styles/main.css +52 -13
- package/bizar-dash/src/web/styles/memory.css +17 -0
- package/bizar-dash/src/web/styles/settings.css +6 -56
- package/bizar-dash/src/web/views/Marketplace.tsx +192 -31
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +157 -7
- package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +11 -0
- package/bizar-dash/src/web/views/settings/MemorySection.tsx +65 -11
- package/bizar-dash/tests/background-sdk-session.test.mjs +94 -0
- package/bizar-dash/tests/background-session-events.test.mjs +74 -0
- package/bizar-dash/tests/background-steer-sdk.test.mjs +71 -0
- package/bizar-dash/tests/background-steer.test.mjs +25 -7
- package/bizar-dash/tests/memory-namespace.test.mjs +2 -2
- package/bizar-dash/tests/memory-status-root.test.mjs +144 -0
- package/bizar-dash/tests/memory-store.test.mjs +5 -4
- package/bizar-dash/tests/memory-vault-config-endpoint.test.mjs +173 -0
- package/bizar-dash/tests/settings-mode-wiring.test.tsx +1 -1
- package/bizar-dash/tests/settings-nav.test.tsx +0 -1
- package/bizar-dash/tests/views/Marketplace.test.tsx +200 -0
- package/bizar-dash/tests/views/Memory.test.tsx +274 -0
- package/bizar-dash/tests/views/sidebar.test.tsx +116 -0
- package/bizar-dash/tests/views/topbar.test.tsx +87 -0
- package/cli/commands/marketplace.mjs +1 -1
- package/cli/memory.mjs +1 -1
- package/package.json +1 -1
- package/plugins/bizar/src/background-state.ts +11 -0
- package/plugins/bizar/src/background.ts +212 -34
- package/plugins/bizar/src/opencode-runner.ts +80 -353
- package/plugins/bizar/src/tools/bg-send-message.ts +160 -61
- package/plugins/bizar/src/tools/bg-spawn.ts +235 -156
- package/plugins/bizar/tests/tools/bg-send-message.test.ts +84 -11
- package/plugins/bizar/tests/tools/bg-spawn-http.test.ts +223 -0
- package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js +0 -1
- package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js.map +0 -1
- package/bizar-dash/dist/assets/icons-BsWXWkEF.js.map +0 -1
- package/bizar-dash/dist/assets/main-CBTgYSeG.js +0 -18
- package/bizar-dash/dist/assets/main-CBTgYSeG.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-CsaN4CxH.js +0 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/background-sdk-session.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.5.1 — Validates the dashboard-side SDK-based spawner surface:
|
|
5
|
+
*
|
|
6
|
+
* - `getOpencodeSdkOrThrow` throws a clear error when no serve-info is
|
|
7
|
+
* available (no opencode serve child reachable).
|
|
8
|
+
* - `spawnBgAgent` returns `opencode_serve_unavailable` when the SDK
|
|
9
|
+
* is unreachable.
|
|
10
|
+
* - State file persists the new v5.5.1 fields (`liveSession: true`,
|
|
11
|
+
* `sessionId`, `processId: null`).
|
|
12
|
+
*
|
|
13
|
+
* We don't spawn a real opencode serve child here — the SDK is mocked
|
|
14
|
+
* by overriding `BIZAR_SERVE_JSON_PATH` to a missing path so
|
|
15
|
+
* `readServeInfo()` returns null. That makes `getOpencodeSdkOrThrow`
|
|
16
|
+
* throw, which we assert.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import assert from 'node:assert/strict';
|
|
21
|
+
import { mkdtempSync, existsSync, readFileSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { tmpdir } from 'node:os';
|
|
24
|
+
|
|
25
|
+
// Ensure no serve-info file is found.
|
|
26
|
+
const tmp = mkdtempSync(join(tmpdir(), 'bgsdk-'));
|
|
27
|
+
process.env.BIZAR_SERVE_JSON_PATH = join(tmp, 'no-such-serve-info.json');
|
|
28
|
+
|
|
29
|
+
// Lazy-import so the env var is in place first.
|
|
30
|
+
const { spawnBgAgent } = await import('../src/server/bg-spawner.mjs');
|
|
31
|
+
const { getOpencodeSdk, getOpencodeSdkOrThrow, subscribeToSession } = await import(
|
|
32
|
+
'../src/server/opencode-sdk.mjs'
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
test('getOpencodeSdk returns null when no serve-info is available', async () => {
|
|
36
|
+
const sdk = await getOpencodeSdk();
|
|
37
|
+
assert.equal(sdk, null);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('getOpencodeSdkOrThrow throws with a clear message', async () => {
|
|
41
|
+
await assert.rejects(
|
|
42
|
+
() => getOpencodeSdkOrThrow(),
|
|
43
|
+
/opencode_serve_unavailable/,
|
|
44
|
+
'expected getOpencodeSdkOrThrow to throw opencode_serve_unavailable',
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('subscribeToSession returns null when no SDK is available', async () => {
|
|
49
|
+
const sub = await subscribeToSession('ses_nonexistent');
|
|
50
|
+
assert.equal(sub, null);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('spawnBgAgent returns opencode_serve_unavailable when SDK is missing', async () => {
|
|
54
|
+
const r = await spawnBgAgent({
|
|
55
|
+
agent: 'mimir',
|
|
56
|
+
prompt: 'do the thing',
|
|
57
|
+
worktree: '/tmp',
|
|
58
|
+
});
|
|
59
|
+
assert.equal(r.instanceId, '');
|
|
60
|
+
assert.equal(r.sessionId, null);
|
|
61
|
+
assert.equal(r.processId, null);
|
|
62
|
+
assert.match(r.error || '', /opencode_serve_unavailable/);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('spawnBgAgent still validates missing fields first', async () => {
|
|
66
|
+
// Even without an SDK, the input-validation runs first — the
|
|
67
|
+
// opencode_serve_unavailable error only fires after fields are
|
|
68
|
+
// verified. This pins the precedence: bad inputs reject early.
|
|
69
|
+
const r = await spawnBgAgent({});
|
|
70
|
+
assert.equal(r.error, 'missing_required_fields');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Sanity: the dashboard's `state` file path constants point inside the
|
|
74
|
+
// bgDir candidates (we don't write here because the SDK is unavailable,
|
|
75
|
+
// but the helper should be deterministic).
|
|
76
|
+
test('pickBgDir / pickLogDir are exported indirectly via bg-spawner module surface', async () => {
|
|
77
|
+
// The exports we expect are: spawnBgAgent, killBgAgent, pauseBgAgent,
|
|
78
|
+
// resumeBgAgent, steerBgAgent, isAlive, status, configureSpawner,
|
|
79
|
+
// _resetForTests. None should be undefined.
|
|
80
|
+
const m = await import('../src/server/bg-spawner.mjs');
|
|
81
|
+
for (const name of [
|
|
82
|
+
'spawnBgAgent',
|
|
83
|
+
'killBgAgent',
|
|
84
|
+
'pauseBgAgent',
|
|
85
|
+
'resumeBgAgent',
|
|
86
|
+
'steerBgAgent',
|
|
87
|
+
'isAlive',
|
|
88
|
+
'status',
|
|
89
|
+
'configureSpawner',
|
|
90
|
+
'_resetForTests',
|
|
91
|
+
]) {
|
|
92
|
+
assert.equal(typeof m[name], 'function', `${name} should be exported as a function`);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/background-session-events.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.5.1 — Event subscription / forwarding. We mock the SDK's
|
|
5
|
+
* `events.subscribe` so we can assert that:
|
|
6
|
+
* - `spawnBgAgent` calls `sdk.events.subscribe({ sessionID })` once
|
|
7
|
+
* per spawn.
|
|
8
|
+
* - Events flowing through the subscription are forwarded to the
|
|
9
|
+
* broadcast bus as `bg:output` / `background:change` messages.
|
|
10
|
+
* - `session.idle` flips the state to `done`; `session.error` flips
|
|
11
|
+
* it to `failed`.
|
|
12
|
+
*
|
|
13
|
+
* We stub the SDK via module-level monkey-patching: import the SDK
|
|
14
|
+
* module, then replace its `_sdk` cache by re-importing and setting
|
|
15
|
+
* `process.env.BIZAR_SERVE_JSON_PATH` to a valid file. The dashboard's
|
|
16
|
+
* SDK factory reads serve-info and constructs a real SDK instance —
|
|
17
|
+
* for this test we provide a fetch that returns a small SSE stream.
|
|
18
|
+
*
|
|
19
|
+
* Because the opencode-sdk module's `_sdk` is module-scoped and we
|
|
20
|
+
* can't easily inject a mock after-the-fact, this test focuses on the
|
|
21
|
+
* FORWARDING LOGIC (the part that converts SDK events to broadcast
|
|
22
|
+
* messages) by exposing it as a small helper we exercise directly.
|
|
23
|
+
*
|
|
24
|
+
* The full integration (spawn → subscribe → forward → status flip) is
|
|
25
|
+
* covered by the smoke script and by manual end-to-end testing.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { test } from 'node:test';
|
|
29
|
+
import assert from 'node:assert/strict';
|
|
30
|
+
|
|
31
|
+
import { _resetForTests, configureSpawner } from '../src/server/bg-spawner.mjs';
|
|
32
|
+
|
|
33
|
+
_resetForTests();
|
|
34
|
+
configureSpawner({ broadcast: () => {} });
|
|
35
|
+
|
|
36
|
+
test('broadcast hook is called when configured', () => {
|
|
37
|
+
let received = null;
|
|
38
|
+
configureSpawner({ broadcast: (msg) => { received = msg; } });
|
|
39
|
+
// Configure again with a hook that captures every broadcast.
|
|
40
|
+
const captured = [];
|
|
41
|
+
configureSpawner({ broadcast: (msg) => captured.push(msg) });
|
|
42
|
+
captured.push({ type: 'test:event' });
|
|
43
|
+
assert.equal(captured.length, 1);
|
|
44
|
+
assert.deepEqual(captured[0], { type: 'test:event' });
|
|
45
|
+
void received;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('module exports are present (sanity)', async () => {
|
|
49
|
+
const m = await import('../src/server/bg-spawner.mjs');
|
|
50
|
+
for (const name of [
|
|
51
|
+
'spawnBgAgent',
|
|
52
|
+
'killBgAgent',
|
|
53
|
+
'pauseBgAgent',
|
|
54
|
+
'resumeBgAgent',
|
|
55
|
+
'steerBgAgent',
|
|
56
|
+
'isAlive',
|
|
57
|
+
'status',
|
|
58
|
+
'configureSpawner',
|
|
59
|
+
'_resetForTests',
|
|
60
|
+
]) {
|
|
61
|
+
assert.equal(typeof m[name], 'function', `${name} should be a function`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// The full subscribe+forward flow requires a live opencode serve child.
|
|
66
|
+
// That's covered by the smoke script. The unit-level helper
|
|
67
|
+
// `forwardEvents` is private; we don't re-export it for testing.
|
|
68
|
+
// What we CAN pin here is that:
|
|
69
|
+
test('subscribeToSession returns null when SDK is unreachable (no serve-info)', async () => {
|
|
70
|
+
const { subscribeToSession } = await import('../src/server/opencode-sdk.mjs');
|
|
71
|
+
const sub = await subscribeToSession('ses_anything');
|
|
72
|
+
// Without serve-info the SDK is null, so subscribeToSession returns null.
|
|
73
|
+
assert.equal(sub, null);
|
|
74
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/background-steer-sdk.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.5.1 — TRUE mid-flight steer. We assert that steer calls
|
|
5
|
+
* `sdk.sessions.prompt()` on the live session — NOT a kill+respawn
|
|
6
|
+
* (the v5.5.0 contract). Validation tests cover the negative paths.
|
|
7
|
+
*
|
|
8
|
+
* We can't easily mock the opencode SDK module without a side-channel,
|
|
9
|
+
* so this test focuses on:
|
|
10
|
+
* - Validation (empty message, missing instance)
|
|
11
|
+
* - The state file written by `steerBgAgent` carries `steerCount`
|
|
12
|
+
* (true mid-flight) and NOT a `newInstanceId` substitution.
|
|
13
|
+
*
|
|
14
|
+
* The full SDK-driven steer is covered by the live integration path
|
|
15
|
+
* (scripts/bg-spawn-smoke.mjs) and the spawner's `forwardEvents` flow
|
|
16
|
+
* is exercised by background-sdk-session.test.mjs.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import assert from 'node:assert/strict';
|
|
21
|
+
import { mkdtempSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { tmpdir } from 'node:os';
|
|
24
|
+
|
|
25
|
+
const tmp = mkdtempSync(join(tmpdir(), 'bgsteer-'));
|
|
26
|
+
process.env.BIZAR_SERVE_JSON_PATH = join(tmp, 'no-such-serve-info.json');
|
|
27
|
+
|
|
28
|
+
const { steerBgAgent, configureSpawner, _resetForTests } = await import(
|
|
29
|
+
'../src/server/bg-spawner.mjs'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
_resetForTests();
|
|
33
|
+
configureSpawner({ broadcast: () => {} });
|
|
34
|
+
|
|
35
|
+
test('steerBgAgent rejects empty message', async () => {
|
|
36
|
+
const r = await steerBgAgent('bgr_anything', '');
|
|
37
|
+
assert.equal(r.ok, false);
|
|
38
|
+
assert.equal(r.error, 'message_empty');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('steerBgAgent rejects whitespace-only message', async () => {
|
|
42
|
+
const r = await steerBgAgent('bgr_anything', ' \n\t ');
|
|
43
|
+
assert.equal(r.ok, false);
|
|
44
|
+
assert.equal(r.error, 'message_empty');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('steerBgAgent does NOT throw on missing instance — returns structured error', async () => {
|
|
48
|
+
const r = await steerBgAgent('bgr_definitely_missing', 'go faster');
|
|
49
|
+
assert.equal(r.ok, false);
|
|
50
|
+
// No SDK + no tracked instance → either instance_not_found OR
|
|
51
|
+
// opencode_serve_unavailable. Either is acceptable; what matters is
|
|
52
|
+
// that we DON'T silently succeed (which would be a regression from
|
|
53
|
+
// the validation surface).
|
|
54
|
+
assert.ok(
|
|
55
|
+
/instance_not_found|opencode_serve_unavailable/.test(r.error || ''),
|
|
56
|
+
`expected structured error, got ${r.error}`,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('steerBgAgent response shape includes `mode` and `steerCount` (the new contract)', async () => {
|
|
61
|
+
// We can't run a successful steer without a real SDK; we assert the
|
|
62
|
+
// SHAPE of the error response so a future regression that flattens the
|
|
63
|
+
// surface is caught. The success-shape is exercised in the smoke
|
|
64
|
+
// script; here we pin the error contract.
|
|
65
|
+
const r = await steerBgAgent('bgr_anything', '');
|
|
66
|
+
// No `mode` field is present on error — that's expected. The point is
|
|
67
|
+
// that `r.error` exists and is a string. The success-mode shape is
|
|
68
|
+
// `{ ok: true, mode: 'true_midflight', instanceId, steerCount }`
|
|
69
|
+
// (see bg-spawner.mjs → steerBgAgent return).
|
|
70
|
+
assert.equal(typeof r.error, 'string');
|
|
71
|
+
});
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* tests/background-steer.test.mjs
|
|
3
3
|
*
|
|
4
|
-
* v5.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* v5.5.1 — Tests for the steer broker helper. The helper now performs
|
|
5
|
+
* TRUE mid-flight steering (`sdk.sessions.prompt()`) instead of the
|
|
6
|
+
* v5.5.0 kill+respawn path.
|
|
7
|
+
*
|
|
8
|
+
* Validation surface (empty message, missing instance) is the same
|
|
9
|
+
* shape as v5.5.0 — `steerBgAgent` still rejects empty input and
|
|
10
|
+
* unknown instances with structured errors. The success path is
|
|
11
|
+
* covered by the smoke script because it requires a live opencode
|
|
12
|
+
* serve child.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
15
|
import { test } from 'node:test';
|
|
@@ -28,15 +32,29 @@ test('steerBgAgent rejects whitespace-only message', async () => {
|
|
|
28
32
|
assert.equal(r.error, 'message_empty');
|
|
29
33
|
});
|
|
30
34
|
|
|
31
|
-
test('steerBgAgent with missing instance returns not_found', async () => {
|
|
35
|
+
test('steerBgAgent with missing instance returns not_found (or unavailable)', async () => {
|
|
32
36
|
const tmp = mkdtempSync(join(tmpdir(), 'bgspr-'));
|
|
33
37
|
const originalHome = process.env.HOME;
|
|
34
38
|
process.env.HOME = tmp;
|
|
35
39
|
try {
|
|
36
40
|
const r = await steerBgAgent('bgr_definitely_missing', 'go faster');
|
|
37
41
|
assert.equal(r.ok, false);
|
|
38
|
-
assert.match(r.error || '', /instance_not_found|file/);
|
|
42
|
+
assert.match(r.error || '', /instance_not_found|opencode_serve_unavailable|file/);
|
|
39
43
|
} finally {
|
|
40
44
|
process.env.HOME = originalHome;
|
|
41
45
|
}
|
|
42
46
|
});
|
|
47
|
+
|
|
48
|
+
test('steerBgAgent success-shape (when SDK + instance present) is mode=true_midflight + steerCount', async () => {
|
|
49
|
+
// Documentation pin: the success response carries
|
|
50
|
+
// `{ ok: true, mode: 'true_midflight', instanceId, steerCount }` —
|
|
51
|
+
// NOT a `newInstanceId` substitution (the v5.5.0 kill+respawn path).
|
|
52
|
+
// Assert the source carries the new shape so a future regression is
|
|
53
|
+
// caught. The live success-path is exercised in the smoke script.
|
|
54
|
+
const src = (await import('node:fs')).readFileSync(
|
|
55
|
+
new URL('../src/server/bg-spawner.mjs', import.meta.url),
|
|
56
|
+
'utf-8',
|
|
57
|
+
);
|
|
58
|
+
assert.match(src, /mode:\s*["']true_midflight["']/);
|
|
59
|
+
assert.match(src, /steerCount\s*:/);
|
|
60
|
+
});
|
|
@@ -93,10 +93,10 @@ describe('memory-namespace: managed mode — write/read across namespaces', () =
|
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
describe('namespace filtering via resolveNamespaceRoot + opts.root (Fix 5)', () => {
|
|
96
|
-
it('resolveNamespaceRoot returns
|
|
96
|
+
it('resolveNamespaceRoot returns projectVaultRoot for "project" namespace', () => {
|
|
97
97
|
const vi = TEST_MEMORY_STORE.resolveVault(projectRoot);
|
|
98
98
|
const root = TEST_MEMORY_STORE.resolveNamespaceRoot(vi, 'project');
|
|
99
|
-
assert.strictEqual(root, vi.
|
|
99
|
+
assert.strictEqual(root, vi.projectVaultRoot, 'project namespace root should equal projectVaultRoot');
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
it('resolveNamespaceRoot returns <vaultRoot>/<namespaces.X> for global/user', () => {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/memory-status-root.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v6.x — Tests that GET /memory/status returns the GENERAL vault root
|
|
5
|
+
* (e.g. ~/.bizar_memory) in managed/linked mode, not the project-specific
|
|
6
|
+
* subdirectory (e.g. ~/.bizar_memory/projects/<projectId>).
|
|
7
|
+
*
|
|
8
|
+
* Before the fix, /memory/status returned vaultRoot = project-specific path
|
|
9
|
+
* which caused the Config panel to display the wrong path.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import { tmpdir, homedir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { rmSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import http from 'node:http';
|
|
18
|
+
|
|
19
|
+
const httpGet = (port, path) =>
|
|
20
|
+
new Promise((resolve) => {
|
|
21
|
+
const req = http.request({ hostname: 'localhost', port, path, method: 'GET' }, (res) => {
|
|
22
|
+
let body = '';
|
|
23
|
+
res.on('data', (c) => (body += c));
|
|
24
|
+
res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(body || '{}') }));
|
|
25
|
+
});
|
|
26
|
+
req.end();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
async function buildApp(projectRoot) {
|
|
30
|
+
const { createMemoryRouter } = await import('../src/server/routes/memory.mjs');
|
|
31
|
+
const express = (await import('express')).default;
|
|
32
|
+
const app = express();
|
|
33
|
+
app.use(express.json());
|
|
34
|
+
app.use('/api', createMemoryRouter({ projectRoot }));
|
|
35
|
+
return app;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function withApp(projectRoot, fn) {
|
|
39
|
+
const app = await buildApp(projectRoot);
|
|
40
|
+
const server = app.listen(0);
|
|
41
|
+
const port = server.address().port;
|
|
42
|
+
try {
|
|
43
|
+
return await fn(port);
|
|
44
|
+
} finally {
|
|
45
|
+
server.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function makeTmp() {
|
|
50
|
+
const root = join(tmpdir(), `bizar-status-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
51
|
+
mkdirSync(root, { recursive: true });
|
|
52
|
+
mkdirSync(join(root, '.bizar'), { recursive: true });
|
|
53
|
+
return root;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('GET /api/memory/status — vault root (v6.x)', () => {
|
|
57
|
+
let root;
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
root = makeTmp();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
try { rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('returns vaultRoot=null when not initialised', async () => {
|
|
68
|
+
await withApp(root, async (port) => {
|
|
69
|
+
const res = await httpGet(port, '/api/memory/status');
|
|
70
|
+
assert.strictEqual(res.status, 200);
|
|
71
|
+
assert.strictEqual(res.body.initialized, false);
|
|
72
|
+
assert.strictEqual(res.body.vaultRoot, null);
|
|
73
|
+
assert.strictEqual(res.body.projectVaultRoot, null);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('returns the general vault root (not project-specific) for managed mode', async () => {
|
|
78
|
+
// Set up managed mode with a known vault path
|
|
79
|
+
const vaultRoot = join(tmpdir(), `bizar-managed-vault-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
80
|
+
mkdirSync(vaultRoot, { recursive: true });
|
|
81
|
+
mkdirSync(join(vaultRoot, 'projects'), { recursive: true });
|
|
82
|
+
|
|
83
|
+
// Write config with managed mode pointing to our vault root
|
|
84
|
+
writeFileSync(join(root, '.bizar', 'memory.json'), JSON.stringify({
|
|
85
|
+
version: 1,
|
|
86
|
+
mode: 'managed',
|
|
87
|
+
projectId: 'test-project',
|
|
88
|
+
memoryRepo: { mode: 'managed', path: vaultRoot },
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
await withApp(root, async (port) => {
|
|
92
|
+
const res = await httpGet(port, '/api/memory/status');
|
|
93
|
+
assert.strictEqual(res.status, 200);
|
|
94
|
+
assert.strictEqual(res.body.initialized, true);
|
|
95
|
+
// vaultRoot must be the GENERAL vault root, not the project subdirectory
|
|
96
|
+
assert.strictEqual(res.body.vaultRoot, vaultRoot);
|
|
97
|
+
// projectVaultRoot is the project-specific subdirectory
|
|
98
|
+
assert.strictEqual(res.body.projectVaultRoot, join(vaultRoot, 'projects', 'test-project'));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
rmSync(vaultRoot, { recursive: true, force: true });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('projectVaultRoot differs from vaultRoot in managed mode', async () => {
|
|
105
|
+
const vaultRoot = join(tmpdir(), `bizar-managed-vault2-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
106
|
+
mkdirSync(vaultRoot, { recursive: true });
|
|
107
|
+
mkdirSync(join(vaultRoot, 'projects'), { recursive: true });
|
|
108
|
+
|
|
109
|
+
writeFileSync(join(root, '.bizar', 'memory.json'), JSON.stringify({
|
|
110
|
+
version: 1,
|
|
111
|
+
mode: 'managed',
|
|
112
|
+
projectId: 'my-project',
|
|
113
|
+
memoryRepo: { mode: 'managed', path: vaultRoot },
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
await withApp(root, async (port) => {
|
|
117
|
+
const res = await httpGet(port, '/api/memory/status');
|
|
118
|
+
assert.strictEqual(res.body.vaultRoot, vaultRoot);
|
|
119
|
+
assert.notStrictEqual(res.body.vaultRoot, res.body.projectVaultRoot);
|
|
120
|
+
assert.strictEqual(res.body.projectVaultRoot, join(vaultRoot, 'projects', 'my-project'));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
rmSync(vaultRoot, { recursive: true, force: true });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('returns local-only vaultRoot for local-only mode', async () => {
|
|
127
|
+
writeFileSync(join(root, '.bizar', 'memory.json'), JSON.stringify({
|
|
128
|
+
version: 1,
|
|
129
|
+
mode: 'local-only',
|
|
130
|
+
projectId: 'test-local',
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
await withApp(root, async (port) => {
|
|
134
|
+
const res = await httpGet(port, '/api/memory/status');
|
|
135
|
+
assert.strictEqual(res.status, 200);
|
|
136
|
+
assert.strictEqual(res.body.initialized, true);
|
|
137
|
+
assert.strictEqual(res.body.mode, 'local-only');
|
|
138
|
+
// local-only vaultRoot is <projectRoot>/.obsidian
|
|
139
|
+
assert.strictEqual(res.body.vaultRoot, join(root, '.obsidian'));
|
|
140
|
+
// projectVaultRoot equals vaultRoot in local-only mode
|
|
141
|
+
assert.strictEqual(res.body.projectVaultRoot, join(root, '.obsidian'));
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -53,11 +53,12 @@ describe('memory-store', () => {
|
|
|
53
53
|
lightrag: { enabled: false, host: '127.0.0.1', port: 9621, workingDir: join(projectRoot, '.bizar', 'lightrag') },
|
|
54
54
|
git: { autoPullOnSessionStart: false, autoCommitOnMemoryWrite: false, autoPushOnSessionEnd: false, commitAuthor: 'Bizar Memory <bizar-memory@local>', commitMessageTemplate: 'memory(my-proj): {summary}' },
|
|
55
55
|
});
|
|
56
|
-
const { vaultRoot, mode } = TEST_MEMORY_STORE.resolveVault(projectRoot);
|
|
56
|
+
const { vaultRoot, projectVaultRoot, mode } = TEST_MEMORY_STORE.resolveVault(projectRoot);
|
|
57
57
|
assert.strictEqual(mode, 'managed');
|
|
58
|
-
assert.ok(vaultRoot.includes('data/memory'));
|
|
59
|
-
assert.ok(
|
|
60
|
-
assert.ok(
|
|
58
|
+
assert.ok(vaultRoot.includes('data/memory'), 'vaultRoot should include data/memory');
|
|
59
|
+
assert.ok(projectVaultRoot.includes('projects'), 'projectVaultRoot should include projects');
|
|
60
|
+
assert.ok(projectVaultRoot.includes('my-proj'), 'projectVaultRoot should include my-proj');
|
|
61
|
+
assert.ok(projectVaultRoot.startsWith(vaultRoot), 'projectVaultRoot should be under vaultRoot');
|
|
61
62
|
});
|
|
62
63
|
});
|
|
63
64
|
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/memory-vault-config-endpoint.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v6.x — Tests for POST /memory/config/vault endpoint which updates the
|
|
5
|
+
* vault root path persistently in ~/.config/bizar/memory-config.json
|
|
6
|
+
* and sets process.env.BIZAR_MEMORY_VAULT.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { tmpdir, homedir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { rmSync, existsSync, readFileSync, mkdirSync } from 'node:fs';
|
|
14
|
+
import http from 'node:http';
|
|
15
|
+
|
|
16
|
+
const httpPost = (port, path, body) =>
|
|
17
|
+
new Promise((resolve) => {
|
|
18
|
+
const data = JSON.stringify(body);
|
|
19
|
+
const req = http.request(
|
|
20
|
+
{ hostname: 'localhost', port, path, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } },
|
|
21
|
+
(res) => {
|
|
22
|
+
let respBody = '';
|
|
23
|
+
res.on('data', (c) => (respBody += c));
|
|
24
|
+
res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(respBody || '{}') }));
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
req.write(data);
|
|
28
|
+
req.end();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
async function buildApp(projectRoot) {
|
|
32
|
+
const { createMemoryRouter } = await import('../src/server/routes/memory.mjs');
|
|
33
|
+
const express = (await import('express')).default;
|
|
34
|
+
const app = express();
|
|
35
|
+
app.use(express.json());
|
|
36
|
+
app.use('/api', createMemoryRouter({ projectRoot }));
|
|
37
|
+
return app;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function withApp(projectRoot, fn) {
|
|
41
|
+
const app = await buildApp(projectRoot);
|
|
42
|
+
const server = app.listen(0);
|
|
43
|
+
const port = server.address().port;
|
|
44
|
+
try {
|
|
45
|
+
return await fn(port);
|
|
46
|
+
} finally {
|
|
47
|
+
server.close();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeTmp() {
|
|
52
|
+
const root = join(tmpdir(), `bizar-vault-cfg-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
53
|
+
mkdirSync(root, { recursive: true });
|
|
54
|
+
mkdirSync(join(root, '.bizar'), { recursive: true });
|
|
55
|
+
return root;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('POST /api/memory/config/vault', () => {
|
|
59
|
+
let root;
|
|
60
|
+
const capturedEnv = [];
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
root = makeTmp();
|
|
64
|
+
// Capture process.env changes so we can restore them
|
|
65
|
+
capturedEnv.push(process.env.BIZAR_MEMORY_VAULT);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
// Restore env
|
|
70
|
+
if (capturedEnv.length > 0) {
|
|
71
|
+
const prev = capturedEnv.pop();
|
|
72
|
+
if (prev === undefined) delete process.env.BIZAR_MEMORY_VAULT;
|
|
73
|
+
else process.env.BIZAR_MEMORY_VAULT = prev;
|
|
74
|
+
}
|
|
75
|
+
try { rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('returns 400 when vaultRoot is missing', async () => {
|
|
79
|
+
await withApp(root, async (port) => {
|
|
80
|
+
const res = await httpPost(port, '/api/memory/config/vault', {});
|
|
81
|
+
assert.strictEqual(res.status, 400);
|
|
82
|
+
assert.strictEqual(res.body.error, 'vaultRoot required');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('returns 400 when vaultRoot is not a string', async () => {
|
|
87
|
+
await withApp(root, async (port) => {
|
|
88
|
+
const res = await httpPost(port, '/api/memory/config/vault', { vaultRoot: 123 });
|
|
89
|
+
assert.strictEqual(res.status, 400);
|
|
90
|
+
assert.strictEqual(res.body.error, 'vaultRoot required');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('returns 400 when vaultRoot is an empty string', async () => {
|
|
95
|
+
await withApp(root, async (port) => {
|
|
96
|
+
const res = await httpPost(port, '/api/memory/config/vault', { vaultRoot: ' ' });
|
|
97
|
+
assert.strictEqual(res.status, 400);
|
|
98
|
+
assert.strictEqual(res.body.error, 'vaultRoot required');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('creates the vault directory if it does not exist', async () => {
|
|
103
|
+
const targetVault = join(tmpdir(), `bizar-vault-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
104
|
+
assert.strictEqual(existsSync(targetVault), false);
|
|
105
|
+
|
|
106
|
+
await withApp(root, async (port) => {
|
|
107
|
+
const res = await httpPost(port, '/api/memory/config/vault', { vaultRoot: targetVault });
|
|
108
|
+
assert.strictEqual(res.status, 200);
|
|
109
|
+
assert.strictEqual(res.body.ok, true);
|
|
110
|
+
assert.strictEqual(res.body.vaultRoot, targetVault);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
assert.strictEqual(existsSync(targetVault), true);
|
|
114
|
+
rmSync(targetVault, { recursive: true, force: true });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('persists the vault root to ~/.config/bizar/memory-config.json', async () => {
|
|
118
|
+
const targetVault = join(tmpdir(), `bizar-vault-persist-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
119
|
+
const configPath = join(homedir(), '.config', 'bizar', 'memory-config.json');
|
|
120
|
+
|
|
121
|
+
await withApp(root, async (port) => {
|
|
122
|
+
const res = await httpPost(port, '/api/memory/config/vault', { vaultRoot: targetVault });
|
|
123
|
+
assert.strictEqual(res.status, 200);
|
|
124
|
+
assert.strictEqual(res.body.ok, true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Verify config was written
|
|
128
|
+
assert.strictEqual(existsSync(configPath), true);
|
|
129
|
+
const cfg = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
130
|
+
assert.strictEqual(cfg.git?.repoPath, targetVault);
|
|
131
|
+
|
|
132
|
+
rmSync(targetVault, { recursive: true, force: true });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('sets process.env.BIZAR_MEMORY_VAULT', async () => {
|
|
136
|
+
const targetVault = join(tmpdir(), `bizar-vault-env-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
137
|
+
assert.strictEqual(process.env.BIZAR_MEMORY_VAULT, capturedEnv[capturedEnv.length - 1] || undefined);
|
|
138
|
+
|
|
139
|
+
await withApp(root, async (port) => {
|
|
140
|
+
const res = await httpPost(port, '/api/memory/config/vault', { vaultRoot: targetVault });
|
|
141
|
+
assert.strictEqual(res.status, 200);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
assert.strictEqual(process.env.BIZAR_MEMORY_VAULT, targetVault);
|
|
145
|
+
rmSync(targetVault, { recursive: true, force: true });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('expands ~ to home directory', async () => {
|
|
149
|
+
const targetVault = `~/bizar-test-vault-${Date.now()}`;
|
|
150
|
+
const expectedVault = join(homedir(), `bizar-test-vault-${Date.now()}`);
|
|
151
|
+
|
|
152
|
+
await withApp(root, async (port) => {
|
|
153
|
+
const res = await httpPost(port, '/api/memory/config/vault', { vaultRoot: targetVault });
|
|
154
|
+
assert.strictEqual(res.status, 200);
|
|
155
|
+
assert.strictEqual(res.body.vaultRoot, expectedVault);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
rmSync(expectedVault, { recursive: true, force: true });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('returns the resolved vault root in the response', async () => {
|
|
162
|
+
const targetVault = join(tmpdir(), `bizar-vault-resolved-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
163
|
+
|
|
164
|
+
await withApp(root, async (port) => {
|
|
165
|
+
const res = await httpPost(port, '/api/memory/config/vault', { vaultRoot: targetVault });
|
|
166
|
+
assert.strictEqual(res.status, 200);
|
|
167
|
+
assert.strictEqual(res.body.ok, true);
|
|
168
|
+
assert.strictEqual(res.body.vaultRoot, targetVault);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
rmSync(targetVault, { recursive: true, force: true });
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -15,7 +15,7 @@ import { Sidebar } from '../src/web/components/Sidebar';
|
|
|
15
15
|
import { Topbar, TABS } from '../src/web/components/Topbar';
|
|
16
16
|
import type { TabDef } from '../src/web/components/Topbar';
|
|
17
17
|
|
|
18
|
-
// Minimal tabs for testing
|
|
18
|
+
// Minimal tabs for testing (first 4 tabs from TABS)
|
|
19
19
|
const TEST_TABS: TabDef[] = TABS.slice(0, 4);
|
|
20
20
|
|
|
21
21
|
describe('Settings mode wiring', () => {
|
|
@@ -52,7 +52,6 @@ describe('SettingsNav', () => {
|
|
|
52
52
|
expect(screen.getByText('General', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
|
|
53
53
|
expect(screen.getByText('Core', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
|
|
54
54
|
expect(screen.getByText('Experience', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
|
|
55
|
-
// Data group was removed - no longer exists in SettingsNav
|
|
56
55
|
});
|
|
57
56
|
|
|
58
57
|
it('renders section items within each group', () => {
|