This commit is contained in:
138
packages/agent/tests/kvark-pipeline-smoke.test.ts
Normal file
138
packages/agent/tests/kvark-pipeline-smoke.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user