@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.
- package/bizar-dash/CHANGELOG.md +37 -276
- package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +2 -0
- package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +1 -0
- package/bizar-dash/dist/assets/main-eWZ4NlCL.css +1 -0
- package/bizar-dash/dist/assets/main-usWhlPWa.js +362 -0
- package/bizar-dash/dist/assets/main-usWhlPWa.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile-DQLFCjwJ.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile-DQLFCjwJ.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-O6ANdD4W.js +352 -0
- package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +1 -0
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
- package/bizar-dash/skills/bizar/SKILL.md +116 -0
- package/bizar-dash/skills/chat/SKILL.md +74 -0
- package/bizar-dash/skills/headroom/SKILL.md +94 -0
- package/bizar-dash/skills/lightrag/SKILL.md +86 -0
- package/bizar-dash/skills/minimax/SKILL.md +80 -0
- package/bizar-dash/skills/obsidian/SKILL.md +68 -0
- package/bizar-dash/skills/providers/SKILL.md +75 -0
- package/bizar-dash/skills/sdk/SKILL.md +138 -0
- package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
- package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
- package/bizar-dash/skills/usage/SKILL.md +62 -0
- package/bizar-dash/src/server/api.mjs +18 -0
- package/bizar-dash/src/server/headroom.mjs +645 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +272 -2
- package/bizar-dash/src/server/memory-obsidian.mjs +230 -0
- package/bizar-dash/src/server/memory-store.mjs +189 -0
- package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
- package/bizar-dash/src/server/minimax.mjs +196 -5
- package/bizar-dash/src/server/providers-store.mjs +956 -0
- package/bizar-dash/src/server/routes/_shared.mjs +17 -0
- package/bizar-dash/src/server/routes/config.mjs +52 -1
- package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
- package/bizar-dash/src/server/routes/headroom.mjs +126 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
- package/bizar-dash/src/server/routes/memory.mjs +668 -1
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
- package/bizar-dash/src/server/routes/providers.mjs +266 -5
- package/bizar-dash/src/server/routes/skills.mjs +32 -43
- package/bizar-dash/src/server/routes/update.mjs +340 -0
- package/bizar-dash/src/server/routes/usage.mjs +136 -0
- package/bizar-dash/src/server/serve-info.mjs +135 -4
- package/bizar-dash/src/server/server.mjs +20 -0
- package/bizar-dash/src/server/skills-store.mjs +152 -262
- package/bizar-dash/src/web/App.tsx +120 -29
- package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
- package/bizar-dash/src/web/components/HeadroomSettings.tsx +418 -0
- package/bizar-dash/src/web/components/HeadroomStatus.tsx +158 -0
- package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
- package/bizar-dash/src/web/components/Topbar.tsx +2 -1
- package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
- package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
- package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
- package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
- package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
- package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
- package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
- package/bizar-dash/src/web/lib/api.ts +43 -0
- package/bizar-dash/src/web/lib/types.ts +16 -0
- package/bizar-dash/src/web/main.tsx +2 -0
- package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
- package/bizar-dash/src/web/styles/chat.css +135 -1
- package/bizar-dash/src/web/styles/main.css +46 -0
- package/bizar-dash/src/web/styles/memory.css +955 -0
- package/bizar-dash/src/web/styles/minimax-usage.css +335 -0
- package/bizar-dash/src/web/styles/settings.css +418 -0
- package/bizar-dash/src/web/styles/skills.css +302 -0
- package/bizar-dash/src/web/styles/tasks.css +288 -0
- package/bizar-dash/src/web/views/Chat.tsx +276 -48
- package/bizar-dash/src/web/views/Config.tsx +3 -2065
- package/bizar-dash/src/web/views/Memory.tsx +140 -0
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +476 -461
- package/bizar-dash/src/web/views/Overview.tsx +3 -0
- package/bizar-dash/src/web/views/Settings.tsx +36 -0
- package/bizar-dash/src/web/views/Skills.tsx +208 -260
- package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +289 -0
- package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +220 -0
- package/bizar-dash/src/web/views/memory/LightragPanel.tsx +307 -0
- package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +354 -0
- package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +160 -0
- package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +642 -0
- package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +194 -0
- package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
- package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
- package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
- package/bizar-dash/tests/headroom-install.test.mjs +173 -0
- package/bizar-dash/tests/headroom-settings.test.mjs +126 -0
- package/bizar-dash/tests/headroom-status.test.mjs +117 -0
- package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
- package/bizar-dash/tests/memory-lightrag-extended.test.mjs +162 -0
- package/bizar-dash/tests/memory-obsidian.test.mjs +269 -0
- package/bizar-dash/tests/memory-tab.test.mjs +322 -0
- package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
- package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
- package/bizar-dash/tests/mod-upgrade.node.test.mjs +1 -1
- package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
- package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
- package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
- package/bizar-dash/tests/skills-list.test.mjs +232 -0
- package/bizar-dash/tests/skills-search.test.mjs +222 -0
- package/bizar-dash/tests/submit-feedback.test.mjs +6 -6
- package/bizar-dash/tests/tasks-create.test.mjs +187 -0
- package/bizar-dash/tests/update-check.test.mjs +127 -0
- package/bizar-dash/tests/update-run.test.mjs +266 -0
- package/cli/bin.mjs +247 -1
- package/cli/provision.mjs +118 -4
- package/config/agents/_shared/SKILLS.md +109 -0
- package/package.json +1 -1
- package/bizar-dash/dist/assets/main-BB5mJurD.js +0 -352
- package/bizar-dash/dist/assets/main-BB5mJurD.js.map +0 -1
- package/bizar-dash/dist/assets/main-BsnQLXdh.css +0 -1
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/memory-lightrag-extended.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v4.7.0 — Tests for the new LightRAG stats / rebuild-graph / recordQuery
|
|
5
|
+
* helpers added for the Memory tab.
|
|
6
|
+
*
|
|
7
|
+
* These helpers do NOT require a live lightrag-server — they exercise the
|
|
8
|
+
* pure helpers against an isolated tmp project root.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
resolveLightRAGConfig,
|
|
19
|
+
stats,
|
|
20
|
+
recordQuery,
|
|
21
|
+
rebuildGraph,
|
|
22
|
+
} = await import('../src/server/memory-lightrag.mjs');
|
|
23
|
+
|
|
24
|
+
let tmpRoot;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
tmpRoot = join(tmpdir(), `bizar-memlight-ext-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
28
|
+
mkdirSync(join(tmpRoot, '.bizar', 'memory-cache'), { recursive: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('resolveLightRAGConfig', () => {
|
|
36
|
+
test('returns sensible defaults for empty project', () => {
|
|
37
|
+
const cfg = resolveLightRAGConfig(tmpRoot);
|
|
38
|
+
assert.equal(cfg.enabled, true);
|
|
39
|
+
assert.equal(cfg.host, '127.0.0.1');
|
|
40
|
+
assert.equal(cfg.port, 9621);
|
|
41
|
+
assert.ok(cfg.workingDir.includes('.bizar/lightrag'));
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('stats', () => {
|
|
46
|
+
test('returns the documented shape for a fresh project', async () => {
|
|
47
|
+
const s = await stats(tmpRoot);
|
|
48
|
+
assert.equal(typeof s.running, 'boolean');
|
|
49
|
+
assert.equal(s.pid, null); // no server running in tests
|
|
50
|
+
assert.equal(s.host, '127.0.0.1');
|
|
51
|
+
assert.equal(s.port, 9621);
|
|
52
|
+
assert.equal(typeof s.workingDir, 'string');
|
|
53
|
+
assert.equal(s.lastReindexAt, null);
|
|
54
|
+
assert.equal(s.lastReindexOk, null);
|
|
55
|
+
assert.equal(s.lastReindexInserted, null);
|
|
56
|
+
assert.equal(s.lastReindexFailed, null);
|
|
57
|
+
assert.equal(typeof s.noteCount, 'number');
|
|
58
|
+
assert.equal(typeof s.indexedApprox, 'number');
|
|
59
|
+
assert.equal(typeof s.queryCountLast24h, 'number');
|
|
60
|
+
assert.ok(s.avgResponseMs === null || typeof s.avgResponseMs === 'number');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('reflects noteCount from the vault', async () => {
|
|
64
|
+
// Set up a memory.json in managed mode + a vault with 2 notes.
|
|
65
|
+
mkdirSync(join(tmpRoot, '.bizar', 'memory', 'projects', 'extproj'), { recursive: true });
|
|
66
|
+
writeFileSync(join(tmpRoot, '.bizar', 'memory.json'), JSON.stringify({
|
|
67
|
+
projectId: 'extproj',
|
|
68
|
+
memoryRepo: {
|
|
69
|
+
mode: 'managed',
|
|
70
|
+
path: join(tmpRoot, '.bizar', 'memory'),
|
|
71
|
+
remote: null,
|
|
72
|
+
branch: 'main',
|
|
73
|
+
},
|
|
74
|
+
}));
|
|
75
|
+
writeFileSync(
|
|
76
|
+
join(tmpRoot, '.bizar', 'memory', 'projects', 'extproj', 'a.md'),
|
|
77
|
+
'---\ntitle: A\n---\n\nBody A.',
|
|
78
|
+
);
|
|
79
|
+
writeFileSync(
|
|
80
|
+
join(tmpRoot, '.bizar', 'memory', 'projects', 'extproj', 'b.md'),
|
|
81
|
+
'---\ntitle: B\n---\n\nBody B.',
|
|
82
|
+
);
|
|
83
|
+
const s = await stats(tmpRoot);
|
|
84
|
+
assert.equal(s.noteCount, 2);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('indexedApprox sums kv_store_doc_status + kv_store_full_docs', async () => {
|
|
88
|
+
const cfg = resolveLightRAGConfig(tmpRoot);
|
|
89
|
+
mkdirSync(cfg.workingDir, { recursive: true });
|
|
90
|
+
writeFileSync(
|
|
91
|
+
join(cfg.workingDir, 'kv_store_doc_status.json'),
|
|
92
|
+
JSON.stringify({ a: 1, b: 2, c: 3 }),
|
|
93
|
+
);
|
|
94
|
+
writeFileSync(
|
|
95
|
+
join(cfg.workingDir, 'kv_store_full_docs.json'),
|
|
96
|
+
JSON.stringify({ x: 1, y: 2 }),
|
|
97
|
+
);
|
|
98
|
+
const s = await stats(tmpRoot);
|
|
99
|
+
assert.equal(s.indexedApprox, 5);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('recordQuery', () => {
|
|
104
|
+
test('writes to lightrag-query-log.jsonl and stats picks it up', async () => {
|
|
105
|
+
recordQuery(tmpRoot, 42);
|
|
106
|
+
recordQuery(tmpRoot, 84);
|
|
107
|
+
recordQuery(tmpRoot, 1000);
|
|
108
|
+
const logPath = join(tmpRoot, '.bizar', 'memory-cache', 'lightrag-query-log.jsonl');
|
|
109
|
+
assert.ok(existsSync(logPath), 'log file should exist');
|
|
110
|
+
const content = readFileSync(logPath, 'utf8').trim().split('\n');
|
|
111
|
+
assert.equal(content.length, 3);
|
|
112
|
+
for (const line of content) {
|
|
113
|
+
const rec = JSON.parse(line);
|
|
114
|
+
assert.equal(typeof rec.ts, 'number');
|
|
115
|
+
assert.equal(typeof rec.ms, 'number');
|
|
116
|
+
}
|
|
117
|
+
const s = await stats(tmpRoot);
|
|
118
|
+
assert.equal(s.queryCountLast24h, 3);
|
|
119
|
+
assert.equal(s.avgResponseMs, Math.round((42 + 84 + 1000) / 3));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('ignores queries older than 24h', async () => {
|
|
123
|
+
const logPath = join(tmpRoot, '.bizar', 'memory-cache', 'lightrag-query-log.jsonl');
|
|
124
|
+
mkdirSync(join(tmpRoot, '.bizar', 'memory-cache'), { recursive: true });
|
|
125
|
+
const old = { ts: Date.now() - 48 * 60 * 60 * 1000, ms: 99 };
|
|
126
|
+
const fresh = { ts: Date.now() - 60 * 1000, ms: 50 };
|
|
127
|
+
writeFileSync(logPath, JSON.stringify(old) + '\n' + JSON.stringify(fresh) + '\n');
|
|
128
|
+
const s = await stats(tmpRoot);
|
|
129
|
+
assert.equal(s.queryCountLast24h, 1);
|
|
130
|
+
assert.equal(s.avgResponseMs, 50);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('handles missing log file gracefully', async () => {
|
|
134
|
+
const s = await stats(tmpRoot);
|
|
135
|
+
assert.equal(s.queryCountLast24h, 0);
|
|
136
|
+
assert.equal(s.avgResponseMs, null);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('rebuildGraph', () => {
|
|
141
|
+
test('is idempotent when called against a non-running server', async () => {
|
|
142
|
+
// We don't have a live lightrag-server in tests, so rebuildGraph will
|
|
143
|
+
// ultimately return ok=false with error="lightrag disabled in .bizar/memory.json"
|
|
144
|
+
// OR ok=false with error about not running. Either way, it must NOT throw
|
|
145
|
+
// and must return a structured result.
|
|
146
|
+
mkdirSync(join(tmpRoot, '.bizar'), { recursive: true });
|
|
147
|
+
writeFileSync(join(tmpRoot, '.bizar', 'memory.json'), JSON.stringify({
|
|
148
|
+
projectId: 'rbproj',
|
|
149
|
+
memoryRepo: { mode: 'local-only', path: join(tmpRoot, '.obsidian') },
|
|
150
|
+
lightrag: { enabled: false },
|
|
151
|
+
}));
|
|
152
|
+
let result;
|
|
153
|
+
try {
|
|
154
|
+
result = await rebuildGraph(tmpRoot, {});
|
|
155
|
+
} catch (err) {
|
|
156
|
+
assert.fail(`rebuildGraph threw: ${err.message}`);
|
|
157
|
+
}
|
|
158
|
+
assert.ok(result);
|
|
159
|
+
assert.equal(typeof result.ok, 'boolean');
|
|
160
|
+
// workingDir should have been wiped and re-created (or attempted).
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/memory-obsidian.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v4.7.0 — Tests for the memory-obsidian façade: CRUD re-exports + the
|
|
5
|
+
* higher-level helpers (tree, listBacklinks, linkGraph, diffVault) used by
|
|
6
|
+
* the Memory tab Obsidian panel.
|
|
7
|
+
*
|
|
8
|
+
* Uses an isolated tmp projectRoot per describe so the rest of the repo's
|
|
9
|
+
* memory config is untouched.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
17
|
+
|
|
18
|
+
const obsidian = await import('../src/server/memory-obsidian.mjs');
|
|
19
|
+
const memoryStore = await import('../src/server/memory-store.mjs');
|
|
20
|
+
|
|
21
|
+
let tmpRoot;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
tmpRoot = join(tmpdir(), `bizar-memobs-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
25
|
+
mkdirSync(join(tmpRoot, '.bizar'), { recursive: true });
|
|
26
|
+
memoryStore.saveConfig(tmpRoot, {
|
|
27
|
+
version: 1,
|
|
28
|
+
backend: 'bizar-local',
|
|
29
|
+
projectId: 'memobs-test',
|
|
30
|
+
memoryRepo: {
|
|
31
|
+
mode: 'local-only',
|
|
32
|
+
path: join(tmpRoot, '.obsidian'),
|
|
33
|
+
remote: null,
|
|
34
|
+
branch: 'main',
|
|
35
|
+
namespace: 'projects/memobs-test',
|
|
36
|
+
},
|
|
37
|
+
namespaces: {
|
|
38
|
+
project: 'projects/memobs-test',
|
|
39
|
+
global: 'global/bizar',
|
|
40
|
+
user: 'users/local',
|
|
41
|
+
},
|
|
42
|
+
lightrag: { enabled: false, host: '127.0.0.1', port: 9621 },
|
|
43
|
+
git: {
|
|
44
|
+
autoPullOnSessionStart: false,
|
|
45
|
+
autoCommitOnMemoryWrite: false,
|
|
46
|
+
autoPushOnSessionEnd: false,
|
|
47
|
+
commitAuthor: 'Test <test@local>',
|
|
48
|
+
commitMessageTemplate: 'memory(test): {summary}',
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
memoryStore.initVault(tmpRoot);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ── Re-exports sanity ─────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
describe('memory-obsidian façade', () => {
|
|
61
|
+
test('re-exports the canonical CRUD surface', () => {
|
|
62
|
+
assert.equal(typeof obsidian.listNotes, 'function');
|
|
63
|
+
assert.equal(typeof obsidian.readNote, 'function');
|
|
64
|
+
assert.equal(typeof obsidian.writeNote, 'function');
|
|
65
|
+
assert.equal(typeof obsidian.deleteNote, 'function');
|
|
66
|
+
assert.equal(typeof obsidian.searchVault, 'function');
|
|
67
|
+
assert.equal(typeof obsidian.vaultStats, 'function');
|
|
68
|
+
assert.equal(typeof obsidian.resolveVault, 'function');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('exposes the Memory-tab specific helpers', () => {
|
|
72
|
+
assert.equal(typeof obsidian.tree, 'function');
|
|
73
|
+
assert.equal(typeof obsidian.listBacklinks, 'function');
|
|
74
|
+
assert.equal(typeof obsidian.linkGraph, 'function');
|
|
75
|
+
assert.equal(typeof obsidian.diffVault, 'function');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ── tree ──────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
describe('tree', () => {
|
|
82
|
+
test('returns null when vault does not exist', () => {
|
|
83
|
+
rmSync(join(tmpRoot, '.obsidian'), { recursive: true, force: true });
|
|
84
|
+
const result = obsidian.tree(tmpRoot);
|
|
85
|
+
assert.equal(result, null);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('returns a folder node containing nested notes', () => {
|
|
89
|
+
memoryStore.writeNote(tmpRoot, 'daily/2026-07-04.md', {
|
|
90
|
+
frontmatter: fullFM({ title: 'Day 1' }),
|
|
91
|
+
body: 'first day',
|
|
92
|
+
});
|
|
93
|
+
memoryStore.writeNote(tmpRoot, 'daily/2026-07-05.md', {
|
|
94
|
+
frontmatter: fullFM({ title: 'Day 2' }),
|
|
95
|
+
body: 'second day',
|
|
96
|
+
});
|
|
97
|
+
memoryStore.writeNote(tmpRoot, 'patterns/auth.md', {
|
|
98
|
+
frontmatter: fullFM({ title: 'Auth pattern' }),
|
|
99
|
+
body: 'use bcrypt',
|
|
100
|
+
});
|
|
101
|
+
const root = obsidian.tree(tmpRoot);
|
|
102
|
+
assert.ok(root);
|
|
103
|
+
assert.equal(root.type, 'folder');
|
|
104
|
+
assert.ok(Array.isArray(root.children));
|
|
105
|
+
const folders = root.children.filter((c) => c.type === 'folder').map((c) => c.name);
|
|
106
|
+
assert.ok(folders.includes('daily'));
|
|
107
|
+
assert.ok(folders.includes('patterns'));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ── listBacklinks ─────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
describe('listBacklinks', () => {
|
|
114
|
+
test('returns empty for a note with no incoming links', () => {
|
|
115
|
+
memoryStore.writeNote(tmpRoot, 'target.md', {
|
|
116
|
+
frontmatter: fullFM({ title: 'Target' }),
|
|
117
|
+
body: 'no one links here',
|
|
118
|
+
});
|
|
119
|
+
const links = obsidian.listBacklinks(tmpRoot, 'target.md');
|
|
120
|
+
assert.deepEqual(links, []);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('finds incoming wikilinks by basename', () => {
|
|
124
|
+
memoryStore.writeNote(tmpRoot, 'projects/api.md', {
|
|
125
|
+
frontmatter: fullFM({ title: 'API' }),
|
|
126
|
+
body: 'API details.',
|
|
127
|
+
});
|
|
128
|
+
memoryStore.writeNote(tmpRoot, 'daily/today.md', {
|
|
129
|
+
frontmatter: fullFM({ title: 'Today' }),
|
|
130
|
+
body: 'Working on [[api]] and [[projects/api]].',
|
|
131
|
+
});
|
|
132
|
+
const links = obsidian.listBacklinks(tmpRoot, 'projects/api.md');
|
|
133
|
+
assert.ok(links.length >= 1, 'expected at least one backlink');
|
|
134
|
+
const src = links.find((l) => l.fromRelPath === 'daily/today.md');
|
|
135
|
+
assert.ok(src);
|
|
136
|
+
assert.match(src.snippet, /\[\[api\]\]/);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('handles wikilinks with aliases and headings', () => {
|
|
140
|
+
memoryStore.writeNote(tmpRoot, 'a.md', { frontmatter: fullFM({ title: 'A' }), body: 'a body' });
|
|
141
|
+
memoryStore.writeNote(tmpRoot, 'b.md', {
|
|
142
|
+
frontmatter: fullFM({ title: 'B' }),
|
|
143
|
+
body: 'See [[a|the A note]] for context.',
|
|
144
|
+
});
|
|
145
|
+
const links = obsidian.listBacklinks(tmpRoot, 'a.md');
|
|
146
|
+
assert.equal(links.length, 1);
|
|
147
|
+
assert.match(links[0].snippet, /\[\[a\|the A note\]\]/);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('does not include the target itself', () => {
|
|
151
|
+
memoryStore.writeNote(tmpRoot, 'self.md', {
|
|
152
|
+
frontmatter: fullFM({ title: 'Self' }),
|
|
153
|
+
body: 'See [[self]] for context.',
|
|
154
|
+
});
|
|
155
|
+
const links = obsidian.listBacklinks(tmpRoot, 'self.md');
|
|
156
|
+
assert.deepEqual(links, []);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ── linkGraph ─────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
describe('linkGraph', () => {
|
|
163
|
+
test('returns nodes for each note + edges for each wikilink', () => {
|
|
164
|
+
memoryStore.writeNote(tmpRoot, 'a.md', { frontmatter: fullFM({ title: 'A' }), body: 'links to [[b]]' });
|
|
165
|
+
memoryStore.writeNote(tmpRoot, 'b.md', { frontmatter: fullFM({ title: 'B' }), body: 'links to [[c]]' });
|
|
166
|
+
memoryStore.writeNote(tmpRoot, 'c.md', { frontmatter: fullFM({ title: 'C' }), body: 'no links' });
|
|
167
|
+
const g = obsidian.linkGraph(tmpRoot);
|
|
168
|
+
assert.ok(g.nodes.length >= 3);
|
|
169
|
+
assert.ok(g.edges.length >= 2);
|
|
170
|
+
for (const e of g.edges) {
|
|
171
|
+
assert.equal(typeof e.from, 'string');
|
|
172
|
+
assert.equal(typeof e.to, 'string');
|
|
173
|
+
assert.equal(typeof e.dangling, 'boolean');
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('marks non-existent targets as dangling', () => {
|
|
178
|
+
memoryStore.writeNote(tmpRoot, 'a.md', { frontmatter: fullFM({ title: 'A' }), body: 'links to [[ghost-note]]' });
|
|
179
|
+
const g = obsidian.linkGraph(tmpRoot);
|
|
180
|
+
const edge = g.edges.find((e) => e.raw === 'ghost-note');
|
|
181
|
+
assert.ok(edge, 'expected edge for ghost-note');
|
|
182
|
+
assert.equal(edge.dangling, true);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ── diffVault ─────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
describe('diffVault', () => {
|
|
189
|
+
test('returns empty diff when notes are identical', () => {
|
|
190
|
+
memoryStore.writeNote(tmpRoot, 'a.md', { frontmatter: fullFM({ title: 'A' }), body: 'same body' });
|
|
191
|
+
memoryStore.writeNote(tmpRoot, 'b.md', { frontmatter: fullFM({ title: 'B' }), body: 'same body' });
|
|
192
|
+
const d = obsidian.diffVault(tmpRoot, 'a.md', 'b.md');
|
|
193
|
+
assert.ok(d.lines.length > 0);
|
|
194
|
+
for (const line of d.lines) {
|
|
195
|
+
assert.notEqual(line.kind, 'add');
|
|
196
|
+
assert.notEqual(line.kind, 'del');
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('returns add + del entries for differing notes', () => {
|
|
201
|
+
memoryStore.writeNote(tmpRoot, 'old.md', { frontmatter: fullFM({ title: 'Old' }), body: 'line1\nline2\nline3' });
|
|
202
|
+
memoryStore.writeNote(tmpRoot, 'new.md', { frontmatter: fullFM({ title: 'New' }), body: 'line1\nline4\nline5' });
|
|
203
|
+
const d = obsidian.diffVault(tmpRoot, 'old.md', 'new.md');
|
|
204
|
+
const adds = d.lines.filter((l) => l.kind === 'add').map((l) => l.text);
|
|
205
|
+
const dels = d.lines.filter((l) => l.kind === 'del').map((l) => l.text);
|
|
206
|
+
assert.ok(adds.includes('line4'), 'expected line4 added');
|
|
207
|
+
assert.ok(adds.includes('line5'), 'expected line5 added');
|
|
208
|
+
assert.ok(dels.includes('line2'), 'expected line2 deleted');
|
|
209
|
+
assert.ok(dels.includes('line3'), 'expected line3 deleted');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('handles null fromPath gracefully', () => {
|
|
213
|
+
memoryStore.writeNote(tmpRoot, 'new.md', { frontmatter: fullFM({ title: 'New' }), body: 'fresh content' });
|
|
214
|
+
const d = obsidian.diffVault(tmpRoot, '', 'new.md');
|
|
215
|
+
assert.ok(d.lines.length > 0);
|
|
216
|
+
for (const line of d.lines) {
|
|
217
|
+
assert.ok(['add', 'same'].includes(line.kind));
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ── CRUD via the façade ───────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
describe('CRUD via memory-obsidian façade', () => {
|
|
225
|
+
test('writeNote + readNote + deleteNote round-trip', () => {
|
|
226
|
+
const created = obsidian.writeNote(tmpRoot, 'foo/bar.md', {
|
|
227
|
+
frontmatter: fullFM({ title: 'Foo' }),
|
|
228
|
+
body: 'hello world',
|
|
229
|
+
});
|
|
230
|
+
assert.equal(created.relPath, 'foo/bar.md');
|
|
231
|
+
const read = obsidian.readNote(tmpRoot, 'foo/bar.md');
|
|
232
|
+
assert.ok(read);
|
|
233
|
+
assert.equal(read.frontmatter.title, 'Foo');
|
|
234
|
+
assert.equal(read.body, 'hello world');
|
|
235
|
+
const ok = obsidian.deleteNote(tmpRoot, 'foo/bar.md');
|
|
236
|
+
assert.equal(ok, true);
|
|
237
|
+
const after = obsidian.readNote(tmpRoot, 'foo/bar.md');
|
|
238
|
+
assert.equal(after, null);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('searchVault finds notes by content', () => {
|
|
242
|
+
obsidian.writeNote(tmpRoot, 'search/a.md', { frontmatter: fullFM({ title: 'A' }), body: 'contains the magic word auth' });
|
|
243
|
+
obsidian.writeNote(tmpRoot, 'search/b.md', { frontmatter: fullFM({ title: 'B' }), body: 'no match here' });
|
|
244
|
+
const r = obsidian.searchVault(tmpRoot, 'auth', { limit: 5 });
|
|
245
|
+
assert.ok(r.length >= 1);
|
|
246
|
+
assert.ok(r[0].relPath.startsWith('search/'));
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Build a schema-valid frontmatter object with overrides for the test note.
|
|
254
|
+
* The memory-store schema requires memory_id, project_id, type, status,
|
|
255
|
+
* confidence, created, updated, tags.
|
|
256
|
+
*/
|
|
257
|
+
function fullFM(overrides = {}) {
|
|
258
|
+
return {
|
|
259
|
+
memory_id: `mem-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
260
|
+
project_id: 'memobs-test',
|
|
261
|
+
type: 'session_summary',
|
|
262
|
+
status: 'draft',
|
|
263
|
+
confidence: 'inferred',
|
|
264
|
+
created: '2026-07-05',
|
|
265
|
+
updated: '2026-07-05',
|
|
266
|
+
tags: ['test'],
|
|
267
|
+
...overrides,
|
|
268
|
+
};
|
|
269
|
+
}
|