Files
waggle-os/packages/agent/tests/kvark-pipeline-smoke.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

139 lines
6.7 KiB
TypeScript

/**
* KVARK Pipeline Smoke - assembled path validation (Milestone E1).
* Exercises real B1-B4 + conflict + C pipeline code.
* Only KVARK HTTP boundary is mocked via KvarkClientLike.
*/
import { describe, it, expect, vi } from 'vitest';
import { CombinedRetrieval, detectConflict, type MemorySearchLike, type MemorySearchResultLike } from '../src/combined-retrieval.js';
import { formatCombinedResult } from '../src/tools.js';
import { createKvarkTools, type KvarkClientLike, type KvarkSearchResponseLike } from '../src/kvark-tools.js';
function memStub(r: Array<{ id: number; content: string; score: number; type?: string }>): MemorySearchLike {
const m: MemorySearchResultLike[] = r.map(x => ({ frame: { id: x.id, content: x.content, frame_type: x.type ?? 'fact', importance: 'normal' }, finalScore: x.score }));
return { search: vi.fn().mockResolvedValue(m) };
}
function kStub(r: Array<{ id: number; title: string; snippet: string; score: number; type?: string }>): KvarkClientLike {
const resp: KvarkSearchResponseLike = { query: 'q', total: r.length, results: r.map(x => ({ document_id: x.id, title: x.title, snippet: x.snippet, score: x.score, document_type: x.type ?? null })) };
return { search: vi.fn().mockResolvedValue(resp), askDocument: vi.fn().mockResolvedValue({ answer: '', sources: [] }), feedback: vi.fn().mockResolvedValue({ ok: true }) };
}
describe('Pipeline: happy path', () => {
it('all sources produce formatted output with attribution', async () => {
const cr = new CombinedRetrieval({
workspaceSearch: memStub([{ id: 1, content: 'We use PostgreSQL', score: 0.75, type: 'decision' }]),
personalSearch: memStub([{ id: 2, content: 'User prefers concise', score: 0.5 }]),
kvarkClient: kStub([{ id: 42, title: 'ADR', snippet: 'PostgreSQL selected', score: 0.88, type: 'pdf' }]),
});
const r = await cr.search('db');
const o = formatCombinedResult(r, true);
expect(o).toContain('## Workspace Memory');
expect(o).toContain('## Personal Memory');
expect(o).toContain('## Enterprise Knowledge (KVARK)');
expect(o).toContain('[workspace memory]');
expect(o).toContain('[personal memory]');
expect(o).toContain('[KVARK: pdf: ADR]');
expect(o).not.toContain('## Source Conflict');
expect(r.hasConflict).toBe(false);
});
});
describe('Pipeline: conflict detection', () => {
it('approved vs cancelled triggers conflict', async () => {
const cr = new CombinedRetrieval({
workspaceSearch: memStub([{ id: 1, content: 'Vendor approved and confirmed', score: 0.85 }]),
personalSearch: memStub([]),
kvarkClient: kStub([{ id: 99, title: 'Policy', snippet: 'Vendor cancelled due to compliance', score: 0.9 }]),
});
const r = await cr.search('vendor');
expect(r.hasConflict).toBe(true);
const o = formatCombinedResult(r, true);
expect(o).toContain('## Source Conflict');
expect(o).toContain('## Workspace Memory');
expect(o).toContain('## Enterprise Knowledge');
});
it('detectConflict consistent with CombinedRetrieval', async () => {
const cr = new CombinedRetrieval({
workspaceSearch: memStub([{ id: 1, content: 'Project rejected', score: 0.8 }]),
personalSearch: memStub([]),
kvarkClient: kStub([{ id: 50, title: 'Minutes', snippet: 'Project approved', score: 0.85 }]),
});
const r = await cr.search('status');
const d = detectConflict(r.workspaceResults, r.kvarkResults);
expect(r.hasConflict).toBe(d !== null);
});
});
describe('Pipeline: KVARK skip', () => {
it('strong local results skip KVARK', async () => {
const k = kStub([{ id: 1, title: 'D', snippet: 't', score: 0.9 }]);
const cr = new CombinedRetrieval({
workspaceSearch: memStub([{ id: 1, content: 'A', score: 0.9 }, { id: 2, content: 'B', score: 0.85 }, { id: 3, content: 'C', score: 0.75 }]),
personalSearch: memStub([]), kvarkClient: k,
});
const r = await cr.search('test');
expect(r.kvarkSkipped).toBe(true);
expect(k.search).not.toHaveBeenCalled();
expect(formatCombinedResult(r, true)).not.toContain('Enterprise');
});
});
describe('Pipeline: KVARK failure', () => {
it('local results preserved on error', async () => {
const cr = new CombinedRetrieval({
workspaceSearch: memStub([{ id: 1, content: 'Local fact', score: 0.6 }]),
personalSearch: memStub([{ id: 2, content: 'Note', score: 0.4 }]),
kvarkClient: { search: vi.fn().mockRejectedValue(new Error('down')), askDocument: vi.fn() },
});
const r = await cr.search('x');
const o = formatCombinedResult(r, true);
expect(r.kvarkError).toBe('down');
expect(r.hasConflict).toBe(false);
expect(o).toContain('Local fact');
expect(o).toContain('error');
});
});
describe('Pipeline: feedback round-trip', () => {
it('search doc IDs valid for feedback', async () => {
const fb = vi.fn().mockResolvedValue({ ok: true });
const k = kStub([{ id: 42, title: 'Q3', snippet: 'Rev up', score: 0.9, type: 'pdf' }]);
k.feedback = fb;
const cr = new CombinedRetrieval({ workspaceSearch: memStub([]), personalSearch: memStub([]), kvarkClient: k });
const s = await cr.search('rev');
const did = s.kvarkResults[0].metadata.documentId;
expect(did).toBe(42);
const t = createKvarkTools({ client: k }).find(x => x.name === 'kvark_feedback')!;
const o = await t.execute({ document_id: did, query: 'rev', useful: true });
expect(fb).toHaveBeenCalledWith(42, 'rev', true, undefined);
expect(o).toContain('Feedback');
});
});
describe('Pipeline: scope filtering', () => {
it('personal skips workspace and KVARK', async () => {
const w = memStub([{ id: 1, content: 'ws', score: 0.8 }]);
const p = memStub([{ id: 2, content: 'pers', score: 0.7 }]);
const k = kStub([{ id: 42, title: 'D', snippet: 'e', score: 0.9 }]);
const cr = new CombinedRetrieval({ workspaceSearch: w, personalSearch: p, kvarkClient: k });
const r = await cr.search('t', { scope: 'personal' });
expect(r.workspaceResults).toHaveLength(0);
expect(r.kvarkResults).toHaveLength(0);
expect(w.search).not.toHaveBeenCalled();
expect(k.search).not.toHaveBeenCalled();
});
it('workspace skips personal and KVARK', async () => {
const w = memStub([{ id: 1, content: 'ws', score: 0.8 }]);
const p = memStub([{ id: 2, content: 'pers', score: 0.7 }]);
const k = kStub([{ id: 42, title: 'D', snippet: 'e', score: 0.9 }]);
const cr = new CombinedRetrieval({ workspaceSearch: w, personalSearch: p, kvarkClient: k });
const r = await cr.search('t', { scope: 'workspace' });
expect(r.personalResults).toHaveLength(0);
expect(r.kvarkResults).toHaveLength(0);
expect(p.search).not.toHaveBeenCalled();
expect(k.search).not.toHaveBeenCalled();
});
});