Skip to content

Commit ff7ec01

Browse files
committed
test(store): add missing coverage for utilities, embedding dims, epics reorder
- bigint.ts: tests for likeEscape, assertEmbeddingDim, chunk, safeJson (was 70% → 100%) - search.ts: test assertIdentifier SQL injection guard - migrate.ts: test invalid migration version rejection - epics: reorder, version conflict, embedding update, tag filter, dim mismatch - tasks/knowledge: embedding dimension mismatch on create and update
1 parent bd2b39a commit ff7ec01

6 files changed

Lines changed: 152 additions & 1 deletion

File tree

src/tests/store/contract/epics.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,49 @@ describe('EpicsStore contract', () => {
144144
expect(epics.get(epic.id)!.progress.total).toBe(1);
145145
});
146146

147+
// --- Reorder ---
148+
149+
it('reorders an epic', () => {
150+
const epic = epics.create({ title: 'E', description: '' }, seedEmbedding(1));
151+
const reordered = epics.reorder(epic.id, 5000);
152+
expect(reordered.order).toBe(5000);
153+
expect(reordered.version).toBe(2);
154+
});
155+
156+
// --- Version conflict ---
157+
158+
it('update throws on version conflict', () => {
159+
const epic = epics.create({ title: 'E', description: '' }, seedEmbedding(1));
160+
expect(() => epics.update(epic.id, { title: 'X' }, null, undefined, 99)).toThrow('Version conflict');
161+
});
162+
163+
it('update with embedding replaces vec0', () => {
164+
const epic = epics.create({ title: 'E', description: '' }, seedEmbedding(1));
165+
const updated = epics.update(epic.id, { title: 'E2' }, seedEmbedding(2));
166+
expect(updated.title).toBe('E2');
167+
// Verify vector search still works after replacement
168+
const results = epics.search({ embedding: seedEmbedding(2), searchMode: 'vector' });
169+
expect(results.length).toBeGreaterThan(0);
170+
expect(results[0].id).toBe(epic.id);
171+
});
172+
173+
// --- Tag filter ---
174+
175+
it('list with tag filter', () => {
176+
epics.create({ title: 'Tagged', description: '', tags: ['important'] }, seedEmbedding(1));
177+
epics.create({ title: 'Untagged', description: '' }, seedEmbedding(2));
178+
179+
const result = epics.list({ tag: 'important' });
180+
expect(result.results.length).toBe(1);
181+
expect(result.results[0].title).toBe('Tagged');
182+
});
183+
184+
// --- Embedding dim mismatch ---
185+
186+
it('create throws on wrong embedding dimension', () => {
187+
expect(() => epics.create({ title: 'Bad', description: '' }, [1, 2, 3])).toThrow('Embedding dimension mismatch');
188+
});
189+
147190
// --- Hybrid search ---
148191

149192
it('hybrid search combines keyword and vector', () => {

src/tests/store/contract/knowledge.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,15 @@ describe('KnowledgeStore contract', () => {
219219
expect(knowledge.getMeta('key')).toBe('val');
220220
expect(store.getMeta('key')).toBeNull();
221221
});
222+
223+
// --- Embedding dimension ---
224+
225+
it('create throws on wrong embedding dimension', () => {
226+
expect(() => knowledge.create({ title: 'Bad', content: '' }, [1, 2, 3])).toThrow('Embedding dimension mismatch');
227+
});
228+
229+
it('update throws on wrong embedding dimension', () => {
230+
const note = knowledge.create({ title: 'N', content: '' }, seedEmbedding(1));
231+
expect(() => knowledge.update(note.id, { title: 'N2' }, [1, 2, 3])).toThrow('Embedding dimension mismatch');
232+
});
222233
});

src/tests/store/contract/tasks.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,4 +282,15 @@ describe('TasksStore contract', () => {
282282
const after = Number((db.prepare("SELECT COUNT(*) AS c FROM tags WHERE project_id = ? AND name = 'old-tag'").get(projectId) as { c: bigint }).c);
283283
expect(after).toBe(0);
284284
});
285+
286+
// --- Embedding dimension ---
287+
288+
it('create throws on wrong embedding dimension', () => {
289+
expect(() => tasks.create({ title: 'Bad', description: '' }, [1, 2, 3])).toThrow('Embedding dimension mismatch');
290+
});
291+
292+
it('update throws on wrong embedding dimension', () => {
293+
const task = tasks.create({ title: 'T', description: '' }, seedEmbedding(1));
294+
expect(() => tasks.update(task.id, { title: 'T2' }, [1, 2, 3])).toThrow('Embedding dimension mismatch');
295+
});
285296
});

src/tests/store/sqlite/bigint.test.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { num, now } from '@/store/sqlite/lib/bigint';
1+
import { num, now, likeEscape, assertEmbeddingDim, chunk, safeJson } from '@/store/sqlite/lib/bigint';
22

33
describe('BigInt helpers', () => {
44
it('num converts BigInt to number', () => {
@@ -22,3 +22,74 @@ describe('BigInt helpers', () => {
2222
expect(ts <= after).toBe(true);
2323
});
2424
});
25+
26+
describe('likeEscape', () => {
27+
it('escapes % and _ characters', () => {
28+
expect(likeEscape('100%')).toBe('100\\%');
29+
expect(likeEscape('file_name')).toBe('file\\_name');
30+
});
31+
32+
it('escapes backslashes', () => {
33+
expect(likeEscape('path\\to')).toBe('path\\\\to');
34+
});
35+
36+
it('handles combined special characters', () => {
37+
expect(likeEscape('50%_done\\ok')).toBe('50\\%\\_done\\\\ok');
38+
});
39+
40+
it('returns plain text unchanged', () => {
41+
expect(likeEscape('hello world')).toBe('hello world');
42+
});
43+
});
44+
45+
describe('assertEmbeddingDim', () => {
46+
it('passes for correct dimension', () => {
47+
expect(() => assertEmbeddingDim([1, 2, 3], 3)).not.toThrow();
48+
});
49+
50+
it('throws for wrong dimension', () => {
51+
expect(() => assertEmbeddingDim([1, 2], 3)).toThrow('Embedding dimension mismatch: expected 3, got 2');
52+
});
53+
54+
it('throws for empty embedding', () => {
55+
expect(() => assertEmbeddingDim([], 384)).toThrow('expected 384, got 0');
56+
});
57+
});
58+
59+
describe('chunk', () => {
60+
it('returns single chunk for small array', () => {
61+
const result = chunk([1, 2, 3], 10);
62+
expect(result).toEqual([[1, 2, 3]]);
63+
});
64+
65+
it('splits array into multiple chunks', () => {
66+
const result = chunk([1, 2, 3, 4, 5], 2);
67+
expect(result).toEqual([[1, 2], [3, 4], [5]]);
68+
});
69+
70+
it('handles empty array', () => {
71+
const result = chunk([], 10);
72+
expect(result).toEqual([[]]);
73+
});
74+
75+
it('handles exact chunk size', () => {
76+
const result = chunk([1, 2, 3, 4], 2);
77+
expect(result).toEqual([[1, 2], [3, 4]]);
78+
});
79+
});
80+
81+
describe('safeJson', () => {
82+
it('parses valid JSON', () => {
83+
expect(safeJson('["a","b"]', [])).toEqual(['a', 'b']);
84+
expect(safeJson('{"x":1}', {})).toEqual({ x: 1 });
85+
});
86+
87+
it('returns fallback for invalid JSON', () => {
88+
expect(safeJson('not json', [])).toEqual([]);
89+
expect(safeJson('{broken', 'default')).toBe('default');
90+
});
91+
92+
it('returns fallback for empty string', () => {
93+
expect(safeJson('', null)).toBeNull();
94+
});
95+
});

src/tests/store/sqlite/migrate.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,12 @@ describe('SQLite migrations', () => {
8787
expect(applied).toBe(0);
8888
expect(getSchemaVersion(db)).toBe(v1);
8989
});
90+
91+
it('rejects invalid migration version', () => {
92+
const db = store.getDb();
93+
const { runMigrations } = require('@/store/sqlite/lib/migrate');
94+
95+
expect(() => runMigrations(db, [{ version: 1.5, sql: 'SELECT 1' }])).toThrow('Invalid migration version');
96+
expect(() => runMigrations(db, [{ version: 999.5, sql: 'SELECT 1' }])).toThrow('Invalid migration version');
97+
});
9098
});

src/tests/store/sqlite/search.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,13 @@ describe('hybridSearch', () => {
178178
expect(hybrid.map(r => r.id)).toEqual(keyword.map(r => r.id));
179179
});
180180

181+
it('throws on invalid table identifier', () => {
182+
expect(() => hybridSearch(db, {
183+
...config,
184+
ftsTable: 'test; DROP TABLE test_items',
185+
}, { text: 'hello', searchMode: 'keyword' }, projectId)).toThrow('Invalid ftsTable');
186+
});
187+
181188
it('hybrid with only embedding (no text) behaves like vector', () => {
182189
const hybrid = hybridSearch(db, config, {
183190
embedding: seedEmbedding(2),

0 commit comments

Comments
 (0)