This commit is contained in:
196
packages/agent/tests/conflict-detection.test.ts
Normal file
196
packages/agent/tests/conflict-detection.test.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Conflict detection tests — post-Milestone-B trust slice.
|
||||
*
|
||||
* Tests the detectConflict heuristic and its integration with
|
||||
* CombinedRetrieval and formatCombinedResult.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import {
|
||||
detectConflict,
|
||||
CombinedRetrieval,
|
||||
type CombinedResult,
|
||||
type MemorySearchLike,
|
||||
type MemorySearchResultLike,
|
||||
} from '../src/combined-retrieval.js';
|
||||
import { formatCombinedResult } from '../src/tools.js';
|
||||
import type { KvarkClientLike, KvarkSearchResponseLike } from '../src/kvark-tools.js';
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
function makeResult(source: 'workspace' | 'kvark', content: string, score: number): CombinedResult {
|
||||
return {
|
||||
content,
|
||||
source,
|
||||
attribution: source === 'workspace' ? '[workspace memory]' : '[KVARK: doc]',
|
||||
score,
|
||||
metadata: {},
|
||||
};
|
||||
}
|
||||
|
||||
function makeMemoryResult(id: number, content: string, score: number): MemorySearchResultLike {
|
||||
return { frame: { id, content, frame_type: 'fact', importance: 'normal' }, finalScore: score };
|
||||
}
|
||||
|
||||
function makeSearch(results: MemorySearchResultLike[]): MemorySearchLike {
|
||||
return { search: vi.fn().mockResolvedValue(results) };
|
||||
}
|
||||
|
||||
function makeKvarkClient(results: Array<{ title: string; snippet: string; score: number }>): KvarkClientLike {
|
||||
const response: KvarkSearchResponseLike = {
|
||||
query: 'test',
|
||||
total: results.length,
|
||||
results: results.map((r, i) => ({
|
||||
document_id: i + 1,
|
||||
title: r.title,
|
||||
snippet: r.snippet,
|
||||
score: r.score,
|
||||
document_type: null,
|
||||
})),
|
||||
};
|
||||
return {
|
||||
search: vi.fn().mockResolvedValue(response),
|
||||
askDocument: vi.fn().mockResolvedValue({ answer: '', sources: [] }),
|
||||
};
|
||||
}
|
||||
|
||||
// ── detectConflict unit tests ────────────────────────────────────────────
|
||||
|
||||
describe('detectConflict', () => {
|
||||
it('returns null when only workspace results are present', () => {
|
||||
const ws = [makeResult('workspace', 'project approved by board', 0.9)];
|
||||
expect(detectConflict(ws, [])).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when only KVARK results are present', () => {
|
||||
const kvark = [makeResult('kvark', 'project cancelled per policy', 0.8)];
|
||||
expect(detectConflict([], kvark)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when both sources agree (both positive)', () => {
|
||||
const ws = [makeResult('workspace', 'The migration was approved last week', 0.85)];
|
||||
const kvark = [makeResult('kvark', 'Migration approved and confirmed by CTO', 0.9)];
|
||||
expect(detectConflict(ws, kvark)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when both sources agree (both negative)', () => {
|
||||
const ws = [makeResult('workspace', 'Feature was rejected in review', 0.8)];
|
||||
const kvark = [makeResult('kvark', 'Feature rejected — not aligned with roadmap', 0.85)];
|
||||
expect(detectConflict(ws, kvark)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when neither source has status language', () => {
|
||||
const ws = [makeResult('workspace', 'The project uses PostgreSQL for data storage', 0.9)];
|
||||
const kvark = [makeResult('kvark', 'Database architecture uses Oracle Enterprise', 0.85)];
|
||||
expect(detectConflict(ws, kvark)).toBeNull();
|
||||
});
|
||||
|
||||
it('flags conflict when workspace is positive and KVARK is negative', () => {
|
||||
const ws = [makeResult('workspace', 'We decided to use the new API and approved the integration', 0.9)];
|
||||
const kvark = [makeResult('kvark', 'API integration has been cancelled due to security review', 0.85)];
|
||||
const result = detectConflict(ws, kvark);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('affirmative language');
|
||||
expect(result).toContain('contradictory language');
|
||||
expect(result).toContain('out of sync');
|
||||
});
|
||||
|
||||
it('flags conflict when workspace is negative and KVARK is positive', () => {
|
||||
const ws = [makeResult('workspace', 'The proposal was rejected by the committee', 0.8)];
|
||||
const kvark = [makeResult('kvark', 'Proposal approved and budget confirmed for Q2', 0.9)];
|
||||
const result = detectConflict(ws, kvark);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('enterprise source may be more current');
|
||||
});
|
||||
|
||||
it('returns null when results are below score threshold', () => {
|
||||
const ws = [makeResult('workspace', 'project approved', 0.4)];
|
||||
const kvark = [makeResult('kvark', 'project cancelled', 0.5)];
|
||||
expect(detectConflict(ws, kvark)).toBeNull();
|
||||
});
|
||||
|
||||
it('handles mixed polarity within same source (neutral)', () => {
|
||||
const ws = [makeResult('workspace', 'Some features approved, others rejected during review', 0.8)];
|
||||
const kvark = [makeResult('kvark', 'Project cancelled after initial approval', 0.85)];
|
||||
// workspace has both positive AND negative => neutral => no conflict
|
||||
expect(detectConflict(ws, kvark)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Integration: CombinedRetrieval with conflict detection ──────────────
|
||||
|
||||
describe('CombinedRetrieval conflict integration', () => {
|
||||
it('sets hasConflict=false when KVARK is not called', async () => {
|
||||
const cr = new CombinedRetrieval({
|
||||
workspaceSearch: makeSearch([makeMemoryResult(1, 'approved by team', 0.9)]),
|
||||
personalSearch: makeSearch([]),
|
||||
kvarkClient: null,
|
||||
});
|
||||
const result = await cr.search('test');
|
||||
expect(result.hasConflict).toBe(false);
|
||||
expect(result.conflictNote).toBeUndefined();
|
||||
});
|
||||
|
||||
it('sets hasConflict=true when workspace and KVARK results conflict', async () => {
|
||||
const cr = new CombinedRetrieval({
|
||||
workspaceSearch: makeSearch([makeMemoryResult(1, 'The vendor was selected and approved', 0.85)]),
|
||||
personalSearch: makeSearch([]),
|
||||
kvarkClient: makeKvarkClient([
|
||||
{ title: 'Vendor Policy', snippet: 'Vendor agreement has been cancelled effective immediately', score: 0.9 },
|
||||
]),
|
||||
});
|
||||
const result = await cr.search('vendor status');
|
||||
expect(result.hasConflict).toBe(true);
|
||||
expect(result.conflictNote).toBeDefined();
|
||||
expect(result.conflictNote).toContain('out of sync');
|
||||
});
|
||||
|
||||
it('sets hasConflict=false when sources do not conflict', async () => {
|
||||
const cr = new CombinedRetrieval({
|
||||
workspaceSearch: makeSearch([makeMemoryResult(1, 'project status is active and healthy', 0.8)]),
|
||||
personalSearch: makeSearch([]),
|
||||
kvarkClient: makeKvarkClient([
|
||||
{ title: 'Status', snippet: 'Project confirmed active with full funding', score: 0.85 },
|
||||
]),
|
||||
});
|
||||
const result = await cr.search('project status');
|
||||
expect(result.hasConflict).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── formatCombinedResult with conflict ──────────────────────────────────
|
||||
|
||||
describe('formatCombinedResult conflict rendering', () => {
|
||||
it('includes Source Conflict section when hasConflict is true', () => {
|
||||
const output = formatCombinedResult({
|
||||
query: 'test',
|
||||
workspaceResults: [makeResult('workspace', 'approved', 0.9)],
|
||||
personalResults: [],
|
||||
kvarkResults: [makeResult('kvark', 'cancelled', 0.85)],
|
||||
kvarkAvailable: true,
|
||||
kvarkSkipped: false,
|
||||
hasConflict: true,
|
||||
conflictNote: 'Sources may disagree.',
|
||||
}, true);
|
||||
|
||||
expect(output).toContain('## Source Conflict');
|
||||
expect(output).toContain('Sources may disagree.');
|
||||
expect(output).toContain('Review both sources carefully');
|
||||
});
|
||||
|
||||
it('omits Source Conflict section when hasConflict is false', () => {
|
||||
const output = formatCombinedResult({
|
||||
query: 'test',
|
||||
workspaceResults: [makeResult('workspace', 'some fact', 0.9)],
|
||||
personalResults: [],
|
||||
kvarkResults: [makeResult('kvark', 'some other fact', 0.85)],
|
||||
kvarkAvailable: true,
|
||||
kvarkSkipped: false,
|
||||
hasConflict: false,
|
||||
}, true);
|
||||
|
||||
expect(output).not.toContain('## Source Conflict');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user