gsd-pi 2.75.0-dev.2203010a0 → 2.75.0-dev.96d4bb599
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/dist/onboarding.d.ts +5 -1
- package/dist/onboarding.js +5 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +23 -19
- package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +128 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +17 -4
- package/dist/resources/extensions/gsd/commands/handlers/onboarding.js +52 -68
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
- package/dist/resources/extensions/gsd/commands-memory.js +462 -0
- package/dist/resources/extensions/gsd/gsd-db.js +237 -4
- package/dist/resources/extensions/gsd/memory-embeddings.js +219 -0
- package/dist/resources/extensions/gsd/memory-extractor.js +78 -27
- package/dist/resources/extensions/gsd/memory-ingest.js +218 -0
- package/dist/resources/extensions/gsd/memory-relations.js +189 -0
- package/dist/resources/extensions/gsd/memory-source-store.js +113 -0
- package/dist/resources/extensions/gsd/memory-store.js +299 -6
- package/dist/resources/extensions/gsd/model-router.js +9 -5
- package/dist/resources/extensions/gsd/notification-overlay.js +7 -22
- package/dist/resources/extensions/gsd/tools/memory-tools.js +306 -0
- package/dist/resources/extensions/gsd/tools/skip-slice.js +78 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +12 -10
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/session-manager.d.ts.map +1 -1
- package/packages/mcp-server/dist/session-manager.js +8 -1
- package/packages/mcp-server/dist/session-manager.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts +1 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +113 -14
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +40 -4
- package/packages/mcp-server/src/server.ts +12 -10
- package/packages/mcp-server/src/session-manager.ts +10 -3
- package/packages/mcp-server/src/workflow-tools.test.ts +91 -1
- package/packages/mcp-server/src/workflow-tools.ts +128 -18
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +24 -20
- package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +158 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +20 -4
- package/src/resources/extensions/gsd/commands/handlers/onboarding.ts +65 -98
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
- package/src/resources/extensions/gsd/commands-memory.ts +551 -0
- package/src/resources/extensions/gsd/gsd-db.ts +273 -4
- package/src/resources/extensions/gsd/memory-embeddings.ts +235 -0
- package/src/resources/extensions/gsd/memory-extractor.ts +100 -34
- package/src/resources/extensions/gsd/memory-ingest.ts +286 -0
- package/src/resources/extensions/gsd/memory-relations.ts +240 -0
- package/src/resources/extensions/gsd/memory-source-store.ts +138 -0
- package/src/resources/extensions/gsd/memory-store.ts +351 -7
- package/src/resources/extensions/gsd/model-router.ts +10 -5
- package/src/resources/extensions/gsd/notification-overlay.ts +9 -19
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/memory-embeddings.test.ts +213 -0
- package/src/resources/extensions/gsd/tests/memory-ingest.test.ts +153 -0
- package/src/resources/extensions/gsd/tests/memory-maintenance.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/memory-relations.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/memory-tools.test.ts +295 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +56 -37
- package/src/resources/extensions/gsd/tests/skip-slice-cascades-tasks.test.ts +125 -0
- package/src/resources/extensions/gsd/tools/memory-tools.ts +380 -0
- package/src/resources/extensions/gsd/tools/skip-slice.ts +133 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +3 -1
- /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → o61X3klsB6C0UE0X1x3PA}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → o61X3klsB6C0UE0X1x3PA}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { _getAdapter, closeDatabase, openDatabase } from '../gsd-db.ts';
|
|
5
|
+
import { createMemory, queryMemoriesRanked, supersedeMemory } from '../memory-store.ts';
|
|
6
|
+
import {
|
|
7
|
+
cosineSimilarity,
|
|
8
|
+
embedMemories,
|
|
9
|
+
getEmbeddingForMemory,
|
|
10
|
+
loadAllEmbeddings,
|
|
11
|
+
packFloat32,
|
|
12
|
+
removeEmbedding,
|
|
13
|
+
saveEmbedding,
|
|
14
|
+
unpackFloat32,
|
|
15
|
+
} from '../memory-embeddings.ts';
|
|
16
|
+
|
|
17
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18
|
+
// Float32 packing + cosine math
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
+
|
|
21
|
+
test('memory-embeddings: packFloat32 / unpackFloat32 round-trip', () => {
|
|
22
|
+
const vec = new Float32Array([0.1, -0.5, 0.25, 0.75]);
|
|
23
|
+
const packed = packFloat32(vec);
|
|
24
|
+
const restored = unpackFloat32(packed);
|
|
25
|
+
assert.ok(restored);
|
|
26
|
+
assert.equal(restored!.length, vec.length);
|
|
27
|
+
for (let i = 0; i < vec.length; i++) {
|
|
28
|
+
assert.ok(Math.abs(restored![i] - vec[i]) < 1e-6);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('memory-embeddings: cosineSimilarity identities', () => {
|
|
33
|
+
const a = new Float32Array([1, 0, 0]);
|
|
34
|
+
const b = new Float32Array([1, 0, 0]);
|
|
35
|
+
const c = new Float32Array([0, 1, 0]);
|
|
36
|
+
assert.ok(Math.abs(cosineSimilarity(a, b) - 1) < 1e-6);
|
|
37
|
+
assert.ok(Math.abs(cosineSimilarity(a, c)) < 1e-6);
|
|
38
|
+
assert.equal(cosineSimilarity(new Float32Array([]), new Float32Array([])), 0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
42
|
+
// memory_embeddings table round-trip
|
|
43
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
44
|
+
|
|
45
|
+
test('memory-embeddings: saveEmbedding then getEmbeddingForMemory', () => {
|
|
46
|
+
openDatabase(':memory:');
|
|
47
|
+
const id = createMemory({ category: 'pattern', content: 'embedding persistence test' });
|
|
48
|
+
assert.ok(id);
|
|
49
|
+
|
|
50
|
+
const vec = new Float32Array([0.1, 0.2, 0.3]);
|
|
51
|
+
assert.ok(saveEmbedding(id!, vec, 'test-model'));
|
|
52
|
+
|
|
53
|
+
const row = getEmbeddingForMemory(id!);
|
|
54
|
+
assert.ok(row);
|
|
55
|
+
assert.equal(row!.model, 'test-model');
|
|
56
|
+
assert.equal(row!.dim, 3);
|
|
57
|
+
assert.equal(row!.vector.length, 3);
|
|
58
|
+
|
|
59
|
+
assert.ok(removeEmbedding(id!));
|
|
60
|
+
assert.equal(getEmbeddingForMemory(id!), null);
|
|
61
|
+
|
|
62
|
+
closeDatabase();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('memory-embeddings: loadAllEmbeddings skips superseded memories', () => {
|
|
66
|
+
openDatabase(':memory:');
|
|
67
|
+
createMemory({ category: 'pattern', content: 'mem A' });
|
|
68
|
+
createMemory({ category: 'pattern', content: 'mem B' });
|
|
69
|
+
saveEmbedding('MEM001', new Float32Array([1, 0, 0]), 'm');
|
|
70
|
+
saveEmbedding('MEM002', new Float32Array([0, 1, 0]), 'm');
|
|
71
|
+
|
|
72
|
+
const all = loadAllEmbeddings();
|
|
73
|
+
assert.equal(all.length, 2);
|
|
74
|
+
|
|
75
|
+
// supersede MEM001
|
|
76
|
+
supersedeMemory('MEM001', 'MEM002');
|
|
77
|
+
|
|
78
|
+
const remaining = loadAllEmbeddings();
|
|
79
|
+
assert.equal(remaining.length, 1);
|
|
80
|
+
assert.equal(remaining[0].memoryId, 'MEM002');
|
|
81
|
+
|
|
82
|
+
closeDatabase();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('memory-embeddings: embedMemories returns 0 when embedFn is null', async () => {
|
|
86
|
+
const n = await embedMemories([{ id: 'MEM001', content: 'x' }], null, 'none');
|
|
87
|
+
assert.equal(n, 0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('memory-embeddings: embedMemories persists fetched vectors', async () => {
|
|
91
|
+
openDatabase(':memory:');
|
|
92
|
+
createMemory({ category: 'pattern', content: 'first' });
|
|
93
|
+
createMemory({ category: 'pattern', content: 'second' });
|
|
94
|
+
|
|
95
|
+
const fn = async (texts: string[]): Promise<Float32Array[]> =>
|
|
96
|
+
texts.map((_, i) => new Float32Array([i, i + 1, i + 2]));
|
|
97
|
+
|
|
98
|
+
const count = await embedMemories(
|
|
99
|
+
[
|
|
100
|
+
{ id: 'MEM001', content: 'first' },
|
|
101
|
+
{ id: 'MEM002', content: 'second' },
|
|
102
|
+
],
|
|
103
|
+
fn,
|
|
104
|
+
'mock-embedder',
|
|
105
|
+
);
|
|
106
|
+
assert.equal(count, 2);
|
|
107
|
+
|
|
108
|
+
const first = getEmbeddingForMemory('MEM001');
|
|
109
|
+
assert.ok(first);
|
|
110
|
+
assert.equal(first!.model, 'mock-embedder');
|
|
111
|
+
assert.equal(first!.vector.length, 3);
|
|
112
|
+
|
|
113
|
+
closeDatabase();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
117
|
+
// queryMemoriesRanked + FTS5 keyword path
|
|
118
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
119
|
+
|
|
120
|
+
test('memory-store: queryMemoriesRanked returns keyword hits', () => {
|
|
121
|
+
openDatabase(':memory:');
|
|
122
|
+
createMemory({ category: 'gotcha', content: 'sql.js FTS5 virtual tables need triggers' });
|
|
123
|
+
createMemory({ category: 'pattern', content: 'prefer prepared statements for hot paths' });
|
|
124
|
+
createMemory({ category: 'convention', content: 'atomic writes use tmp + rename' });
|
|
125
|
+
|
|
126
|
+
const ranked = queryMemoriesRanked({ query: 'sql triggers', k: 5 });
|
|
127
|
+
assert.ok(ranked.length >= 1);
|
|
128
|
+
assert.equal(ranked[0].memory.id, 'MEM001');
|
|
129
|
+
assert.ok(ranked[0].keywordRank !== null);
|
|
130
|
+
assert.equal(ranked[0].reason, 'keyword');
|
|
131
|
+
|
|
132
|
+
closeDatabase();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('memory-store: queryMemoriesRanked respects scope + tag filters', () => {
|
|
136
|
+
openDatabase(':memory:');
|
|
137
|
+
createMemory({ category: 'gotcha', content: 'scoped fact one', scope: 'project', tags: ['a'] });
|
|
138
|
+
createMemory({ category: 'gotcha', content: 'scoped fact two', scope: 'global', tags: ['a', 'b'] });
|
|
139
|
+
|
|
140
|
+
const projectOnly = queryMemoriesRanked({ query: 'scoped fact', scope: 'project' });
|
|
141
|
+
assert.equal(projectOnly.length, 1);
|
|
142
|
+
assert.equal(projectOnly[0].memory.scope, 'project');
|
|
143
|
+
|
|
144
|
+
const tagged = queryMemoriesRanked({ query: 'scoped fact', tag: 'b' });
|
|
145
|
+
assert.equal(tagged.length, 1);
|
|
146
|
+
assert.equal(tagged[0].memory.id, 'MEM002');
|
|
147
|
+
|
|
148
|
+
closeDatabase();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('memory-store: queryMemoriesRanked fuses keyword + semantic hits', () => {
|
|
152
|
+
openDatabase(':memory:');
|
|
153
|
+
createMemory({ category: 'pattern', content: 'foo bar baz' });
|
|
154
|
+
createMemory({ category: 'pattern', content: 'completely unrelated' });
|
|
155
|
+
|
|
156
|
+
// Embed memory 2 so semantic scoring picks it up even though the keyword
|
|
157
|
+
// "foo" does not appear in its content.
|
|
158
|
+
saveEmbedding('MEM001', new Float32Array([1, 0, 0, 0]), 'mock');
|
|
159
|
+
saveEmbedding('MEM002', new Float32Array([0.9, 0.1, 0, 0]), 'mock');
|
|
160
|
+
|
|
161
|
+
const queryVec = new Float32Array([0.95, 0.05, 0, 0]);
|
|
162
|
+
const ranked = queryMemoriesRanked({ query: 'foo', queryVector: queryVec, k: 5 });
|
|
163
|
+
const reasons = ranked.map((r) => r.reason);
|
|
164
|
+
assert.ok(reasons.includes('both') || reasons.includes('keyword'));
|
|
165
|
+
const ids = ranked.map((r) => r.memory.id);
|
|
166
|
+
assert.ok(ids.includes('MEM002'), 'semantic-only hit should surface even without keyword match');
|
|
167
|
+
|
|
168
|
+
closeDatabase();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('memory-store: queryMemoriesRanked falls back to ranked listing when query is empty', () => {
|
|
172
|
+
openDatabase(':memory:');
|
|
173
|
+
createMemory({ category: 'gotcha', content: 'first', confidence: 0.9 });
|
|
174
|
+
createMemory({ category: 'pattern', content: 'second', confidence: 0.5 });
|
|
175
|
+
|
|
176
|
+
const ranked = queryMemoriesRanked({ query: ' ' });
|
|
177
|
+
assert.equal(ranked.length, 2);
|
|
178
|
+
assert.equal(ranked[0].memory.id, 'MEM001');
|
|
179
|
+
assert.equal(ranked[0].reason, 'ranked');
|
|
180
|
+
|
|
181
|
+
closeDatabase();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('memory-store: queryMemoriesRanked excludes superseded by default', () => {
|
|
185
|
+
openDatabase(':memory:');
|
|
186
|
+
createMemory({ category: 'gotcha', content: 'stale insight' });
|
|
187
|
+
createMemory({ category: 'gotcha', content: 'fresh insight' });
|
|
188
|
+
|
|
189
|
+
supersedeMemory('MEM001', 'MEM002');
|
|
190
|
+
|
|
191
|
+
const active = queryMemoriesRanked({ query: 'insight' });
|
|
192
|
+
assert.ok(active.every((r) => r.memory.superseded_by == null));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
196
|
+
// FTS5 triggers keep memories_fts in sync with memories
|
|
197
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
198
|
+
|
|
199
|
+
test('memory-store schema: memories_fts stays in sync with memories', () => {
|
|
200
|
+
openDatabase(':memory:');
|
|
201
|
+
createMemory({ category: 'pattern', content: 'alpha beta gamma' });
|
|
202
|
+
const adapter = _getAdapter();
|
|
203
|
+
assert.ok(adapter);
|
|
204
|
+
const fts = adapter!.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
|
|
205
|
+
if (!fts) {
|
|
206
|
+
// Build without FTS5 — skip.
|
|
207
|
+
closeDatabase();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const rows = adapter!.prepare(`SELECT content FROM memories_fts WHERE memories_fts MATCH 'beta'`).all();
|
|
211
|
+
assert.ok(rows.some((row) => (row['content'] as string).includes('alpha beta gamma')));
|
|
212
|
+
closeDatabase();
|
|
213
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { _getAdapter, closeDatabase, openDatabase } from '../gsd-db.ts';
|
|
8
|
+
import {
|
|
9
|
+
createMemorySource,
|
|
10
|
+
deleteMemorySource,
|
|
11
|
+
getMemorySource,
|
|
12
|
+
hashContent,
|
|
13
|
+
listMemorySources,
|
|
14
|
+
} from '../memory-source-store.ts';
|
|
15
|
+
import { ingestFile, ingestNote, summarizeIngest } from '../memory-ingest.ts';
|
|
16
|
+
|
|
17
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18
|
+
// memory-source-store
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
+
|
|
21
|
+
test('memory-sources: content_hash is stable', () => {
|
|
22
|
+
const a = hashContent('hello');
|
|
23
|
+
const b = hashContent('hello');
|
|
24
|
+
assert.equal(a, b);
|
|
25
|
+
assert.notEqual(a, hashContent('world'));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('memory-sources: createMemorySource is idempotent on content_hash', () => {
|
|
29
|
+
openDatabase(':memory:');
|
|
30
|
+
|
|
31
|
+
const first = createMemorySource({
|
|
32
|
+
kind: 'note',
|
|
33
|
+
content: 'sql.js FTS5 triggers must be kept in sync with the base table',
|
|
34
|
+
tags: ['db', 'fts'],
|
|
35
|
+
});
|
|
36
|
+
assert.ok(first);
|
|
37
|
+
assert.equal(first!.duplicate, false);
|
|
38
|
+
|
|
39
|
+
const second = createMemorySource({
|
|
40
|
+
kind: 'note',
|
|
41
|
+
content: 'sql.js FTS5 triggers must be kept in sync with the base table',
|
|
42
|
+
tags: ['db', 'fts'],
|
|
43
|
+
});
|
|
44
|
+
assert.ok(second);
|
|
45
|
+
assert.equal(second!.duplicate, true);
|
|
46
|
+
assert.equal(second!.id, first!.id);
|
|
47
|
+
|
|
48
|
+
const sources = listMemorySources();
|
|
49
|
+
assert.equal(sources.length, 1);
|
|
50
|
+
assert.equal(sources[0].kind, 'note');
|
|
51
|
+
assert.deepEqual(sources[0].tags, ['db', 'fts']);
|
|
52
|
+
|
|
53
|
+
closeDatabase();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('memory-sources: getMemorySource and deleteMemorySource round-trip', () => {
|
|
57
|
+
openDatabase(':memory:');
|
|
58
|
+
|
|
59
|
+
const created = createMemorySource({ kind: 'note', content: 'note to delete' });
|
|
60
|
+
assert.ok(created);
|
|
61
|
+
const fetched = getMemorySource(created!.id);
|
|
62
|
+
assert.ok(fetched);
|
|
63
|
+
assert.equal(fetched!.content, 'note to delete');
|
|
64
|
+
|
|
65
|
+
const removed = deleteMemorySource(created!.id);
|
|
66
|
+
assert.ok(removed);
|
|
67
|
+
assert.equal(getMemorySource(created!.id), null);
|
|
68
|
+
|
|
69
|
+
closeDatabase();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
73
|
+
// memory-ingest
|
|
74
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
75
|
+
|
|
76
|
+
test('memory-ingest: ingestNote persists a note source', async () => {
|
|
77
|
+
openDatabase(':memory:');
|
|
78
|
+
|
|
79
|
+
const result = await ingestNote('The CAPTURES.md pipeline runs on triage, not dispatch.', null, {
|
|
80
|
+
tags: ['captures'],
|
|
81
|
+
scope: 'project',
|
|
82
|
+
extract: false,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
assert.ok(result.sourceId.startsWith('SRC-'));
|
|
86
|
+
assert.equal(result.duplicate, false);
|
|
87
|
+
assert.equal(result.kind, 'note');
|
|
88
|
+
assert.equal(result.extracted.length, 0);
|
|
89
|
+
assert.match(summarizeIngest(result), /no memories extracted/);
|
|
90
|
+
|
|
91
|
+
closeDatabase();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('memory-ingest: ingestNote rejects empty input', async () => {
|
|
95
|
+
openDatabase(':memory:');
|
|
96
|
+
|
|
97
|
+
const result = await ingestNote(' ', null, { extract: false });
|
|
98
|
+
assert.equal(result.sourceId, '');
|
|
99
|
+
|
|
100
|
+
closeDatabase();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('memory-ingest: ingestNote marks duplicates on replay', async () => {
|
|
104
|
+
openDatabase(':memory:');
|
|
105
|
+
|
|
106
|
+
const first = await ingestNote('same note body', null, { extract: false });
|
|
107
|
+
const second = await ingestNote('same note body', null, { extract: false });
|
|
108
|
+
assert.equal(first.sourceId, second.sourceId);
|
|
109
|
+
assert.equal(second.duplicate, true);
|
|
110
|
+
|
|
111
|
+
closeDatabase();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('memory-ingest: ingestFile persists a file source', async () => {
|
|
115
|
+
openDatabase(':memory:');
|
|
116
|
+
|
|
117
|
+
const dir = mkdtempSync(join(tmpdir(), 'gsd-memory-ingest-'));
|
|
118
|
+
const file = join(dir, 'sample.md');
|
|
119
|
+
writeFileSync(file, '# Architecture\n\nWe use SQLite with WAL journaling.', 'utf-8');
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const result = await ingestFile(file, null, { tags: ['arch'], extract: false });
|
|
123
|
+
assert.ok(result.sourceId.startsWith('SRC-'));
|
|
124
|
+
assert.equal(result.kind, 'file');
|
|
125
|
+
assert.equal(result.title, 'sample.md');
|
|
126
|
+
assert.equal(result.uri, file);
|
|
127
|
+
} finally {
|
|
128
|
+
rmSync(dir, { recursive: true, force: true });
|
|
129
|
+
closeDatabase();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('memory-ingest: ingestFile rejects missing paths', async () => {
|
|
134
|
+
openDatabase(':memory:');
|
|
135
|
+
|
|
136
|
+
await assert.rejects(() => ingestFile('/no/such/file/exists.md', null, { extract: false }), /File not found/);
|
|
137
|
+
|
|
138
|
+
closeDatabase();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
142
|
+
// schema migration + scope/tags columns on memories
|
|
143
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
144
|
+
|
|
145
|
+
test('memory-store schema: scope and tags columns exist on memories', () => {
|
|
146
|
+
openDatabase(':memory:');
|
|
147
|
+
const adapter = _getAdapter();
|
|
148
|
+
const info = adapter!.prepare('PRAGMA table_info(memories)').all();
|
|
149
|
+
const names = info.map((row: Record<string, unknown>) => row.name);
|
|
150
|
+
assert.ok(names.includes('scope'));
|
|
151
|
+
assert.ok(names.includes('tags'));
|
|
152
|
+
closeDatabase();
|
|
153
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { _getAdapter, closeDatabase, openDatabase } from '../gsd-db.ts';
|
|
5
|
+
import {
|
|
6
|
+
createMemory,
|
|
7
|
+
decayStaleMemories,
|
|
8
|
+
enforceMemoryCap,
|
|
9
|
+
markUnitProcessed,
|
|
10
|
+
} from '../memory-store.ts';
|
|
11
|
+
import { createMemoryRelation, listRelationsFor } from '../memory-relations.ts';
|
|
12
|
+
import { saveEmbedding, getEmbeddingForMemory } from '../memory-embeddings.ts';
|
|
13
|
+
|
|
14
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
15
|
+
// enforceMemoryCap — cascade cleanup of embeddings and relations
|
|
16
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
17
|
+
|
|
18
|
+
test('memory-cap: supersedes lowest-ranked memories and cascades cleanup', () => {
|
|
19
|
+
openDatabase(':memory:');
|
|
20
|
+
|
|
21
|
+
// Create 6 memories with descending confidence so the first is lowest.
|
|
22
|
+
createMemory({ category: 'pattern', content: 'weakest', confidence: 0.2 });
|
|
23
|
+
createMemory({ category: 'pattern', content: 'mid-1', confidence: 0.5 });
|
|
24
|
+
createMemory({ category: 'pattern', content: 'mid-2', confidence: 0.6 });
|
|
25
|
+
createMemory({ category: 'pattern', content: 'strong-1', confidence: 0.9 });
|
|
26
|
+
createMemory({ category: 'pattern', content: 'strong-2', confidence: 0.92 });
|
|
27
|
+
createMemory({ category: 'pattern', content: 'strongest', confidence: 0.95 });
|
|
28
|
+
|
|
29
|
+
saveEmbedding('MEM001', new Float32Array([1, 0, 0]), 'm');
|
|
30
|
+
saveEmbedding('MEM006', new Float32Array([0, 1, 0]), 'm');
|
|
31
|
+
createMemoryRelation('MEM001', 'MEM002', 'related_to');
|
|
32
|
+
|
|
33
|
+
// Cap at 5 — MEM001 should be superseded and its embedding + relations purged.
|
|
34
|
+
enforceMemoryCap(5);
|
|
35
|
+
|
|
36
|
+
const adapter = _getAdapter();
|
|
37
|
+
const row = adapter!
|
|
38
|
+
.prepare("SELECT superseded_by FROM memories WHERE id = 'MEM001'")
|
|
39
|
+
.get();
|
|
40
|
+
assert.equal(row?.['superseded_by'], 'CAP_EXCEEDED');
|
|
41
|
+
assert.equal(getEmbeddingForMemory('MEM001'), null);
|
|
42
|
+
assert.equal(listRelationsFor('MEM001').length, 0);
|
|
43
|
+
|
|
44
|
+
// MEM006 was not victimised — its embedding should still exist.
|
|
45
|
+
assert.ok(getEmbeddingForMemory('MEM006'));
|
|
46
|
+
|
|
47
|
+
closeDatabase();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('memory-cap: is a no-op when count is already under the cap', () => {
|
|
51
|
+
openDatabase(':memory:');
|
|
52
|
+
createMemory({ category: 'pattern', content: 'one' });
|
|
53
|
+
createMemory({ category: 'pattern', content: 'two' });
|
|
54
|
+
|
|
55
|
+
enforceMemoryCap(10);
|
|
56
|
+
|
|
57
|
+
const adapter = _getAdapter();
|
|
58
|
+
const count = adapter!
|
|
59
|
+
.prepare("SELECT count(*) as cnt FROM memories WHERE superseded_by IS NULL")
|
|
60
|
+
.get();
|
|
61
|
+
assert.equal(count?.['cnt'], 2);
|
|
62
|
+
|
|
63
|
+
closeDatabase();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
67
|
+
// decayStaleMemories — returns list of decayed IDs
|
|
68
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
69
|
+
|
|
70
|
+
test('memory-decay: returns decayed memory IDs', () => {
|
|
71
|
+
openDatabase(':memory:');
|
|
72
|
+
|
|
73
|
+
// Insert processed units — decayStaleMemories needs at least N rows.
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
for (let i = 0; i < 21; i++) {
|
|
76
|
+
markUnitProcessed(`unit/${i}`, `file-${i}`);
|
|
77
|
+
// small spacing to create deterministic ordering
|
|
78
|
+
const row = _getAdapter()!
|
|
79
|
+
.prepare('UPDATE memory_processed_units SET processed_at = :ts WHERE unit_key = :key');
|
|
80
|
+
row.run({ ':ts': new Date(now + i * 1000).toISOString(), ':key': `unit/${i}` });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Create memory with updated_at in the distant past
|
|
84
|
+
createMemory({ category: 'pattern', content: 'stale entry', confidence: 0.9 });
|
|
85
|
+
_getAdapter()!
|
|
86
|
+
.prepare("UPDATE memories SET updated_at = '2000-01-01T00:00:00Z' WHERE id = 'MEM001'")
|
|
87
|
+
.run({});
|
|
88
|
+
|
|
89
|
+
const decayed = decayStaleMemories(20);
|
|
90
|
+
assert.ok(decayed.includes('MEM001'));
|
|
91
|
+
|
|
92
|
+
// Confidence should have dropped.
|
|
93
|
+
const row = _getAdapter()!
|
|
94
|
+
.prepare("SELECT confidence FROM memories WHERE id = 'MEM001'")
|
|
95
|
+
.get();
|
|
96
|
+
assert.ok((row?.['confidence'] as number) < 0.9);
|
|
97
|
+
|
|
98
|
+
closeDatabase();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('memory-decay: returns empty when there are fewer processed units than the threshold', () => {
|
|
102
|
+
openDatabase(':memory:');
|
|
103
|
+
createMemory({ category: 'pattern', content: 'fresh' });
|
|
104
|
+
const decayed = decayStaleMemories(20);
|
|
105
|
+
assert.deepEqual(decayed, []);
|
|
106
|
+
closeDatabase();
|
|
107
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { closeDatabase, openDatabase } from '../gsd-db.ts';
|
|
5
|
+
import { applyMemoryActions, createMemory, supersedeMemory } from '../memory-store.ts';
|
|
6
|
+
import {
|
|
7
|
+
createMemoryRelation,
|
|
8
|
+
isValidRelation,
|
|
9
|
+
listRelationsFor,
|
|
10
|
+
removeMemoryRelationsFor,
|
|
11
|
+
traverseGraph,
|
|
12
|
+
VALID_RELATIONS,
|
|
13
|
+
} from '../memory-relations.ts';
|
|
14
|
+
import { executeGsdGraph } from '../tools/memory-tools.ts';
|
|
15
|
+
|
|
16
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
17
|
+
// Validation
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
test('memory-relations: isValidRelation recognises the canonical set', () => {
|
|
21
|
+
for (const rel of VALID_RELATIONS) {
|
|
22
|
+
assert.ok(isValidRelation(rel));
|
|
23
|
+
}
|
|
24
|
+
assert.equal(isValidRelation('bogus'), false);
|
|
25
|
+
assert.equal(isValidRelation(null), false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
29
|
+
// createMemoryRelation
|
|
30
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
31
|
+
|
|
32
|
+
test('memory-relations: createMemoryRelation round-trips', () => {
|
|
33
|
+
openDatabase(':memory:');
|
|
34
|
+
createMemory({ category: 'architecture', content: 'auth lives in src/auth' });
|
|
35
|
+
createMemory({ category: 'pattern', content: 'jwt tokens signed with HS256' });
|
|
36
|
+
|
|
37
|
+
const ok = createMemoryRelation('MEM001', 'MEM002', 'elaborates', 0.9);
|
|
38
|
+
assert.equal(ok, true);
|
|
39
|
+
|
|
40
|
+
const relations = listRelationsFor('MEM001');
|
|
41
|
+
assert.equal(relations.length, 1);
|
|
42
|
+
assert.equal(relations[0].from, 'MEM001');
|
|
43
|
+
assert.equal(relations[0].to, 'MEM002');
|
|
44
|
+
assert.equal(relations[0].rel, 'elaborates');
|
|
45
|
+
assert.equal(relations[0].confidence, 0.9);
|
|
46
|
+
|
|
47
|
+
closeDatabase();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('memory-relations: rejects self-loops and unknown memories', () => {
|
|
51
|
+
openDatabase(':memory:');
|
|
52
|
+
createMemory({ category: 'gotcha', content: 'only one memory exists' });
|
|
53
|
+
|
|
54
|
+
assert.equal(createMemoryRelation('MEM001', 'MEM001', 'related_to'), false);
|
|
55
|
+
assert.equal(createMemoryRelation('MEM001', 'MEM999', 'related_to'), false);
|
|
56
|
+
assert.equal(createMemoryRelation('MEM001', 'MEM002', 'bogus-rel' as never), false);
|
|
57
|
+
|
|
58
|
+
closeDatabase();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('memory-relations: duplicate (from, to, rel) upserts instead of erroring', () => {
|
|
62
|
+
openDatabase(':memory:');
|
|
63
|
+
createMemory({ category: 'pattern', content: 'A' });
|
|
64
|
+
createMemory({ category: 'pattern', content: 'B' });
|
|
65
|
+
|
|
66
|
+
assert.equal(createMemoryRelation('MEM001', 'MEM002', 'related_to', 0.7), true);
|
|
67
|
+
assert.equal(createMemoryRelation('MEM001', 'MEM002', 'related_to', 0.9), true);
|
|
68
|
+
|
|
69
|
+
const relations = listRelationsFor('MEM001');
|
|
70
|
+
assert.equal(relations.length, 1);
|
|
71
|
+
assert.equal(relations[0].confidence, 0.9);
|
|
72
|
+
|
|
73
|
+
closeDatabase();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('memory-relations: removeMemoryRelationsFor wipes both directions', () => {
|
|
77
|
+
openDatabase(':memory:');
|
|
78
|
+
createMemory({ category: 'pattern', content: 'A' });
|
|
79
|
+
createMemory({ category: 'pattern', content: 'B' });
|
|
80
|
+
createMemory({ category: 'pattern', content: 'C' });
|
|
81
|
+
createMemoryRelation('MEM001', 'MEM002', 'related_to');
|
|
82
|
+
createMemoryRelation('MEM003', 'MEM002', 'elaborates');
|
|
83
|
+
|
|
84
|
+
removeMemoryRelationsFor('MEM002');
|
|
85
|
+
|
|
86
|
+
assert.equal(listRelationsFor('MEM001').length, 0);
|
|
87
|
+
assert.equal(listRelationsFor('MEM002').length, 0);
|
|
88
|
+
assert.equal(listRelationsFor('MEM003').length, 0);
|
|
89
|
+
|
|
90
|
+
closeDatabase();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
94
|
+
// LINK action via applyMemoryActions
|
|
95
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
96
|
+
|
|
97
|
+
test('memory-relations: applyMemoryActions processes LINK actions', () => {
|
|
98
|
+
openDatabase(':memory:');
|
|
99
|
+
createMemory({ category: 'architecture', content: 'core lives in src/core' });
|
|
100
|
+
createMemory({ category: 'pattern', content: 'events flow through EventEmitter' });
|
|
101
|
+
|
|
102
|
+
applyMemoryActions([
|
|
103
|
+
{ action: 'LINK', from: 'MEM001', to: 'MEM002', rel: 'related_to', confidence: 0.75 },
|
|
104
|
+
// unknown rel type — should be skipped silently
|
|
105
|
+
{ action: 'LINK', from: 'MEM001', to: 'MEM002', rel: 'nonsense-rel' as never },
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const relations = listRelationsFor('MEM001');
|
|
109
|
+
assert.equal(relations.length, 1);
|
|
110
|
+
assert.equal(relations[0].rel, 'related_to');
|
|
111
|
+
|
|
112
|
+
closeDatabase();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
116
|
+
// traverseGraph + gsd_graph tool
|
|
117
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
118
|
+
|
|
119
|
+
test('memory-relations: traverseGraph walks multi-hop edges', () => {
|
|
120
|
+
openDatabase(':memory:');
|
|
121
|
+
createMemory({ category: 'pattern', content: 'A' });
|
|
122
|
+
createMemory({ category: 'pattern', content: 'B' });
|
|
123
|
+
createMemory({ category: 'pattern', content: 'C' });
|
|
124
|
+
createMemoryRelation('MEM001', 'MEM002', 'related_to');
|
|
125
|
+
createMemoryRelation('MEM002', 'MEM003', 'depends_on');
|
|
126
|
+
|
|
127
|
+
const hop1 = traverseGraph('MEM001', 1);
|
|
128
|
+
assert.equal(hop1.nodes.length, 2);
|
|
129
|
+
assert.equal(hop1.edges.filter((e) => e.rel !== 'supersedes').length, 1);
|
|
130
|
+
|
|
131
|
+
const hop2 = traverseGraph('MEM001', 2);
|
|
132
|
+
assert.equal(hop2.nodes.length, 3);
|
|
133
|
+
assert.equal(hop2.edges.filter((e) => e.rel !== 'supersedes').length, 2);
|
|
134
|
+
|
|
135
|
+
closeDatabase();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('memory-relations: traverseGraph still reports supersedes edges', () => {
|
|
139
|
+
openDatabase(':memory:');
|
|
140
|
+
createMemory({ category: 'convention', content: 'old style' });
|
|
141
|
+
createMemory({ category: 'convention', content: 'new style' });
|
|
142
|
+
supersedeMemory('MEM001', 'MEM002');
|
|
143
|
+
|
|
144
|
+
const graph = traverseGraph('MEM001', 1);
|
|
145
|
+
const ids = graph.nodes.map((n) => n.id).sort();
|
|
146
|
+
assert.deepEqual(ids, ['MEM001', 'MEM002']);
|
|
147
|
+
assert.ok(graph.edges.some((e) => e.rel === 'supersedes' && e.from === 'MEM001' && e.to === 'MEM002'));
|
|
148
|
+
|
|
149
|
+
closeDatabase();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('memory-tools: gsd_graph returns LINK edges and filters by rel', () => {
|
|
153
|
+
openDatabase(':memory:');
|
|
154
|
+
createMemory({ category: 'pattern', content: 'alpha' });
|
|
155
|
+
createMemory({ category: 'pattern', content: 'beta' });
|
|
156
|
+
createMemory({ category: 'pattern', content: 'gamma' });
|
|
157
|
+
createMemoryRelation('MEM001', 'MEM002', 'related_to');
|
|
158
|
+
createMemoryRelation('MEM001', 'MEM003', 'contradicts');
|
|
159
|
+
|
|
160
|
+
const all = executeGsdGraph({ mode: 'query', memoryId: 'MEM001', depth: 1 });
|
|
161
|
+
assert.ok(!all.isError);
|
|
162
|
+
const allEdges = all.details.edges as Array<{ rel: string }>;
|
|
163
|
+
const relTypes = new Set(allEdges.map((e) => e.rel));
|
|
164
|
+
assert.ok(relTypes.has('related_to'));
|
|
165
|
+
assert.ok(relTypes.has('contradicts'));
|
|
166
|
+
|
|
167
|
+
const filtered = executeGsdGraph({ mode: 'query', memoryId: 'MEM001', depth: 1, rel: 'related_to' });
|
|
168
|
+
assert.ok(!filtered.isError);
|
|
169
|
+
const fEdges = filtered.details.edges as Array<{ rel: string; to: string }>;
|
|
170
|
+
assert.equal(fEdges.length, 1);
|
|
171
|
+
assert.equal(fEdges[0].rel, 'related_to');
|
|
172
|
+
assert.equal(fEdges[0].to, 'MEM002');
|
|
173
|
+
|
|
174
|
+
closeDatabase();
|
|
175
|
+
});
|
|
@@ -323,9 +323,9 @@ test('memory-store: schema includes memories table', () => {
|
|
|
323
323
|
const viewCount = adapter.prepare('SELECT count(*) as cnt FROM active_memories').get();
|
|
324
324
|
assert.deepStrictEqual(viewCount?.['cnt'], 0, 'active_memories view should exist');
|
|
325
325
|
|
|
326
|
-
// Verify schema version is
|
|
326
|
+
// Verify schema version is 20 (memory Phase 4 relations included)
|
|
327
327
|
const version = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
|
|
328
|
-
assert.deepStrictEqual(version?.['v'],
|
|
328
|
+
assert.deepStrictEqual(version?.['v'], 20, 'schema version should be 20');
|
|
329
329
|
|
|
330
330
|
closeDatabase();
|
|
331
331
|
});
|