This commit is contained in:
201
packages/hive-mind-core/tests/mind/improvement-signals.test.ts
Normal file
201
packages/hive-mind-core/tests/mind/improvement-signals.test.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { MindDB } from '../../src/mind/db.js';
|
||||
import {
|
||||
ImprovementSignalStore,
|
||||
type SignalCategory,
|
||||
} from '../../src/mind/improvement-signals.js';
|
||||
|
||||
describe('ImprovementSignalStore', () => {
|
||||
let db: MindDB;
|
||||
let store: ImprovementSignalStore;
|
||||
|
||||
beforeEach(() => {
|
||||
db = new MindDB(':memory:');
|
||||
store = new ImprovementSignalStore(db);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
// ── record ─────────────────────────────────────────────────
|
||||
|
||||
describe('record', () => {
|
||||
it('inserts a new signal on first record', () => {
|
||||
const signal = store.record('capability_gap', 'missing:pdf_reader');
|
||||
expect(signal.id).toBeDefined();
|
||||
expect(signal.category).toBe('capability_gap');
|
||||
expect(signal.pattern_key).toBe('missing:pdf_reader');
|
||||
expect(signal.count).toBe(1);
|
||||
expect(signal.surfaced).toBe(0);
|
||||
});
|
||||
|
||||
it('increments count on duplicate (category, pattern_key)', () => {
|
||||
store.record('correction', 'tone:too_formal');
|
||||
store.record('correction', 'tone:too_formal');
|
||||
const signal = store.record('correction', 'tone:too_formal');
|
||||
expect(signal.count).toBe(3);
|
||||
});
|
||||
|
||||
it('updates detail on upsert when new detail is non-empty', () => {
|
||||
store.record('correction', 'format:headers', 'Use ## not ###');
|
||||
const updated = store.record('correction', 'format:headers', 'Use h2 not h3');
|
||||
expect(updated.detail).toBe('Use h2 not h3');
|
||||
});
|
||||
|
||||
it('preserves existing detail when new detail is empty', () => {
|
||||
store.record('correction', 'format:headers', 'Use ## not ###');
|
||||
const updated = store.record('correction', 'format:headers');
|
||||
expect(updated.detail).toBe('Use ## not ###');
|
||||
});
|
||||
|
||||
it('stores metadata as JSON', () => {
|
||||
const signal = store.record('workflow_pattern', 'shape:research', undefined, {
|
||||
lastTask: 'analyze competitor',
|
||||
avgSteps: 4,
|
||||
});
|
||||
const parsed = JSON.parse(signal.metadata);
|
||||
expect(parsed.lastTask).toBe('analyze competitor');
|
||||
expect(parsed.avgSteps).toBe(4);
|
||||
});
|
||||
|
||||
it('keeps different pattern_keys separate within same category', () => {
|
||||
store.record('capability_gap', 'missing:pdf_reader');
|
||||
store.record('capability_gap', 'missing:web_search');
|
||||
const gaps = store.getByCategory('capability_gap');
|
||||
expect(gaps).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ── getByCategory ──────────────────────────────────────────
|
||||
|
||||
describe('getByCategory', () => {
|
||||
it('returns only signals for the requested category', () => {
|
||||
store.record('capability_gap', 'missing:pdf');
|
||||
store.record('correction', 'tone:casual');
|
||||
store.record('capability_gap', 'missing:search');
|
||||
|
||||
const gaps = store.getByCategory('capability_gap');
|
||||
expect(gaps).toHaveLength(2);
|
||||
expect(gaps.every(s => s.category === 'capability_gap')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns signals ordered by count descending', () => {
|
||||
store.record('correction', 'a');
|
||||
store.record('correction', 'b');
|
||||
store.record('correction', 'b');
|
||||
store.record('correction', 'b');
|
||||
store.record('correction', 'a');
|
||||
|
||||
const corrections = store.getByCategory('correction');
|
||||
expect(corrections[0].pattern_key).toBe('b');
|
||||
expect(corrections[0].count).toBe(3);
|
||||
expect(corrections[1].pattern_key).toBe('a');
|
||||
expect(corrections[1].count).toBe(2);
|
||||
});
|
||||
|
||||
it('returns empty array for category with no signals', () => {
|
||||
expect(store.getByCategory('workflow_pattern')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ── getActionable ──────────────────────────────────────────
|
||||
|
||||
describe('getActionable', () => {
|
||||
it('returns signals above default thresholds', () => {
|
||||
// capability_gap threshold = 2
|
||||
store.record('capability_gap', 'missing:pdf');
|
||||
store.record('capability_gap', 'missing:pdf');
|
||||
|
||||
const actionable = store.getActionable();
|
||||
expect(actionable).toHaveLength(1);
|
||||
expect(actionable[0].pattern_key).toBe('missing:pdf');
|
||||
expect(actionable[0].parsedMetadata).toBeDefined();
|
||||
});
|
||||
|
||||
it('excludes signals below threshold', () => {
|
||||
// correction threshold = 3, only recorded twice
|
||||
store.record('correction', 'tone:casual');
|
||||
store.record('correction', 'tone:casual');
|
||||
|
||||
const actionable = store.getActionable();
|
||||
expect(actionable).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('excludes already-surfaced signals', () => {
|
||||
store.record('capability_gap', 'missing:pdf');
|
||||
const signal = store.record('capability_gap', 'missing:pdf');
|
||||
store.markSurfaced(signal.id);
|
||||
|
||||
const actionable = store.getActionable();
|
||||
expect(actionable).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('caps at 3 results (MAX_ACTIONABLE)', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const key = `gap_${i}`;
|
||||
store.record('capability_gap', key);
|
||||
store.record('capability_gap', key);
|
||||
}
|
||||
|
||||
const actionable = store.getActionable();
|
||||
expect(actionable.length).toBeLessThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('accepts custom thresholds', () => {
|
||||
store.record('correction', 'tone:casual'); // count = 1
|
||||
|
||||
// Lower threshold to 1 — should now be actionable
|
||||
const actionable = store.getActionable({ correction: 1 });
|
||||
expect(actionable.some(s => s.pattern_key === 'tone:casual')).toBe(true);
|
||||
});
|
||||
|
||||
it('includes parsedMetadata on results', () => {
|
||||
store.record('capability_gap', 'missing:pdf', undefined, { tool: 'pdf_reader' });
|
||||
store.record('capability_gap', 'missing:pdf');
|
||||
|
||||
const actionable = store.getActionable();
|
||||
expect(actionable[0].parsedMetadata).toEqual({ tool: 'pdf_reader' });
|
||||
});
|
||||
});
|
||||
|
||||
// ── markSurfaced ───────────────────────────────────────────
|
||||
|
||||
describe('markSurfaced', () => {
|
||||
it('sets surfaced=1 and surfaced_at', () => {
|
||||
const signal = store.record('correction', 'tone:casual');
|
||||
store.markSurfaced(signal.id);
|
||||
|
||||
const updated = store.get(signal.id);
|
||||
expect(updated?.surfaced).toBe(1);
|
||||
expect(updated?.surfaced_at).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ── getByKey ───────────────────────────────────────────────
|
||||
|
||||
describe('getByKey', () => {
|
||||
it('returns signal by category + pattern_key', () => {
|
||||
store.record('workflow_pattern', 'shape:research');
|
||||
const found = store.getByKey('workflow_pattern', 'shape:research');
|
||||
expect(found).toBeDefined();
|
||||
expect(found?.pattern_key).toBe('shape:research');
|
||||
});
|
||||
|
||||
it('returns undefined for non-existent key', () => {
|
||||
expect(store.getByKey('correction', 'nonexistent')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ── ensureTable (backward compat) ──────────────────────────
|
||||
|
||||
describe('ensureTable', () => {
|
||||
it('creates table even on databases without it in schema', () => {
|
||||
// The :memory: DB already has the table from schema.ts,
|
||||
// but ensureTable should handle it gracefully
|
||||
const store2 = new ImprovementSignalStore(db);
|
||||
const signal = store2.record('capability_gap', 'test:compat');
|
||||
expect(signal.count).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user