Skip to content

Commit a314597

Browse files
committed
test: add epic link/unlink and embedding dimension validation tests
- Epic link/unlink: idempotent linking, non-existent epic/task throws, multiple tasks per epic, progress tracking, cancelled task exclusion - Embedding validation: wrong dimension rejected for all stores, empty embedding rejected +14 new tests (1524 → 1538)
1 parent 3ee5c80 commit a314597

1 file changed

Lines changed: 187 additions & 0 deletions

File tree

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { createSqliteStoreFactory, seedEmbedding, TEST_DIM } from '../helpers';
2+
import type { SqliteStore } from '@/store';
3+
4+
describe('Epics link/unlink tasks', () => {
5+
const factory = createSqliteStoreFactory();
6+
let store: SqliteStore;
7+
let cleanup: () => void;
8+
let projectId: number;
9+
10+
beforeEach(() => {
11+
({ store, cleanup } = factory());
12+
const project = store.projects.create({ slug: 'epic-link', name: 'Epic Link', directory: '/tmp/epic' });
13+
projectId = project.id;
14+
});
15+
16+
afterEach(() => cleanup());
17+
18+
it('links a task to an epic', () => {
19+
const scoped = store.project(projectId);
20+
const emb = seedEmbedding(1, TEST_DIM);
21+
22+
const epic = scoped.epics.create({ title: 'Epic 1', description: '' }, emb);
23+
const task = scoped.tasks.create({ title: 'Task 1', description: '' }, emb);
24+
25+
scoped.epics.linkTask(epic.id, task.id);
26+
27+
const tasks = scoped.epics.listTasks(epic.id);
28+
expect(tasks).toContain(task.id);
29+
});
30+
31+
it('unlinks a task from an epic', () => {
32+
const scoped = store.project(projectId);
33+
const emb = seedEmbedding(1, TEST_DIM);
34+
35+
const epic = scoped.epics.create({ title: 'Epic 1', description: '' }, emb);
36+
const task = scoped.tasks.create({ title: 'Task 1', description: '' }, emb);
37+
38+
scoped.epics.linkTask(epic.id, task.id);
39+
scoped.epics.unlinkTask(epic.id, task.id);
40+
41+
expect(scoped.epics.listTasks(epic.id)).toHaveLength(0);
42+
});
43+
44+
it('linking same task twice is idempotent (INSERT OR IGNORE)', () => {
45+
const scoped = store.project(projectId);
46+
const emb = seedEmbedding(1, TEST_DIM);
47+
48+
const epic = scoped.epics.create({ title: 'Epic 1', description: '' }, emb);
49+
const task = scoped.tasks.create({ title: 'Task 1', description: '' }, emb);
50+
51+
scoped.epics.linkTask(epic.id, task.id);
52+
scoped.epics.linkTask(epic.id, task.id); // duplicate — should not throw
53+
54+
expect(scoped.epics.listTasks(epic.id)).toHaveLength(1);
55+
});
56+
57+
it('unlinking non-linked task is no-op', () => {
58+
const scoped = store.project(projectId);
59+
const emb = seedEmbedding(1, TEST_DIM);
60+
61+
const epic = scoped.epics.create({ title: 'Epic 1', description: '' }, emb);
62+
// No task linked — should not throw
63+
scoped.epics.unlinkTask(epic.id, 99999);
64+
});
65+
66+
it('throws when linking to non-existent epic', () => {
67+
const scoped = store.project(projectId);
68+
const emb = seedEmbedding(1, TEST_DIM);
69+
const task = scoped.tasks.create({ title: 'Task 1', description: '' }, emb);
70+
71+
expect(() => scoped.epics.linkTask(99999, task.id)).toThrow(/not found/i);
72+
});
73+
74+
it('throws when linking non-existent task', () => {
75+
const scoped = store.project(projectId);
76+
const emb = seedEmbedding(1, TEST_DIM);
77+
const epic = scoped.epics.create({ title: 'Epic 1', description: '' }, emb);
78+
79+
expect(() => scoped.epics.linkTask(epic.id, 99999)).toThrow(/not found/i);
80+
});
81+
82+
it('multiple tasks can be linked to one epic', () => {
83+
const scoped = store.project(projectId);
84+
const emb = seedEmbedding(1, TEST_DIM);
85+
86+
const epic = scoped.epics.create({ title: 'Epic 1', description: '' }, emb);
87+
const t1 = scoped.tasks.create({ title: 'Task 1', description: '' }, emb);
88+
const t2 = scoped.tasks.create({ title: 'Task 2', description: '' }, emb);
89+
const t3 = scoped.tasks.create({ title: 'Task 3', description: '' }, emb);
90+
91+
scoped.epics.linkTask(epic.id, t1.id);
92+
scoped.epics.linkTask(epic.id, t2.id);
93+
scoped.epics.linkTask(epic.id, t3.id);
94+
95+
const tasks = scoped.epics.listTasks(epic.id);
96+
expect(tasks).toHaveLength(3);
97+
expect(tasks).toContain(t1.id);
98+
expect(tasks).toContain(t2.id);
99+
expect(tasks).toContain(t3.id);
100+
});
101+
102+
it('epic progress reflects linked tasks', () => {
103+
const scoped = store.project(projectId);
104+
const emb = seedEmbedding(1, TEST_DIM);
105+
106+
const epic = scoped.epics.create({ title: 'Epic 1', description: '' }, emb);
107+
const t1 = scoped.tasks.create({ title: 'Task 1', description: '' }, emb);
108+
const t2 = scoped.tasks.create({ title: 'Task 2', description: '' }, emb);
109+
110+
scoped.epics.linkTask(epic.id, t1.id);
111+
scoped.epics.linkTask(epic.id, t2.id);
112+
113+
// Move one to done
114+
scoped.tasks.move(t1.id, 'done');
115+
116+
const fetched = scoped.epics.get(epic.id)!;
117+
expect(fetched.progress.total).toBe(2);
118+
expect(fetched.progress.done).toBe(1);
119+
});
120+
121+
it('cancelled tasks excluded from progress total', () => {
122+
const scoped = store.project(projectId);
123+
const emb = seedEmbedding(1, TEST_DIM);
124+
125+
const epic = scoped.epics.create({ title: 'Epic 1', description: '' }, emb);
126+
const t1 = scoped.tasks.create({ title: 'Task 1', description: '' }, emb);
127+
const t2 = scoped.tasks.create({ title: 'Task 2', description: '' }, emb);
128+
129+
scoped.epics.linkTask(epic.id, t1.id);
130+
scoped.epics.linkTask(epic.id, t2.id);
131+
132+
scoped.tasks.move(t2.id, 'cancelled');
133+
134+
const fetched = scoped.epics.get(epic.id)!;
135+
// Cancelled tasks excluded from total
136+
expect(fetched.progress.total).toBe(1);
137+
expect(fetched.progress.done).toBe(0);
138+
});
139+
});
140+
141+
describe('Embedding dimension validation', () => {
142+
const factory = createSqliteStoreFactory();
143+
let store: SqliteStore;
144+
let cleanup: () => void;
145+
let projectId: number;
146+
147+
beforeEach(() => {
148+
({ store, cleanup } = factory());
149+
const project = store.projects.create({ slug: 'dim-test', name: 'Dim Test', directory: '/tmp/dim' });
150+
projectId = project.id;
151+
});
152+
153+
afterEach(() => cleanup());
154+
155+
it('rejects embedding with wrong dimension for knowledge', () => {
156+
const scoped = store.project(projectId);
157+
const wrongDim = new Array(64).fill(0.1); // Wrong dimension (should be TEST_DIM=384)
158+
159+
expect(() => scoped.knowledge.create({ title: 'x', content: 'c' }, wrongDim)).toThrow(/embedding/i);
160+
});
161+
162+
it('rejects embedding with wrong dimension for tasks', () => {
163+
const scoped = store.project(projectId);
164+
const wrongDim = new Array(64).fill(0.1);
165+
166+
expect(() => scoped.tasks.create({ title: 'x', description: '' }, wrongDim)).toThrow(/embedding/i);
167+
});
168+
169+
it('rejects embedding with wrong dimension for skills', () => {
170+
const scoped = store.project(projectId);
171+
const wrongDim = new Array(64).fill(0.1);
172+
173+
expect(() => scoped.skills.create({ title: 'x', description: '' }, wrongDim)).toThrow(/embedding/i);
174+
});
175+
176+
it('rejects embedding with wrong dimension for epics', () => {
177+
const scoped = store.project(projectId);
178+
const wrongDim = new Array(64).fill(0.1);
179+
180+
expect(() => scoped.epics.create({ title: 'x', description: '' }, wrongDim)).toThrow(/embedding/i);
181+
});
182+
183+
it('rejects empty embedding', () => {
184+
const scoped = store.project(projectId);
185+
expect(() => scoped.knowledge.create({ title: 'x', content: 'c' }, [])).toThrow(/embedding/i);
186+
});
187+
});

0 commit comments

Comments
 (0)