Files
waggle-os/packages/hive-mind-core/tests/mind/concept-tracker.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

210 lines
7.6 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { MindDB } from '../../src/mind/db.js';
import { ConceptTracker } from '../../src/mind/concept-tracker.js';
describe('ConceptTracker (F19)', () => {
let db: MindDB;
let tracker: ConceptTracker;
beforeEach(() => {
db = new MindDB(':memory:');
tracker = new ConceptTracker(db);
});
afterEach(() => {
db.close();
});
describe('upsertConcept', () => {
it('creates a new concept with defaults', () => {
const entry = tracker.upsertConcept('TypeScript generics');
expect(entry.concept).toBe('TypeScript generics');
expect(entry.mastery_level).toBe(1);
expect(entry.times_correct).toBe(0);
expect(entry.times_wrong).toBe(0);
expect(entry.notes).toBe('');
expect(entry.created_at).toBeDefined();
});
it('creates a concept with custom mastery level', () => {
const entry = tracker.upsertConcept('SQL joins', { mastery_level: 3 });
expect(entry.mastery_level).toBe(3);
});
it('creates a concept with notes', () => {
const entry = tracker.upsertConcept('React hooks', { notes: 'Focus on useEffect cleanup' });
expect(entry.notes).toBe('Focus on useEffect cleanup');
});
it('updates existing concept mastery level', () => {
tracker.upsertConcept('Git rebase', { mastery_level: 2 });
const updated = tracker.upsertConcept('Git rebase', { mastery_level: 4 });
expect(updated.mastery_level).toBe(4);
});
it('updates existing concept notes', () => {
tracker.upsertConcept('Docker', { notes: 'basics' });
const updated = tracker.upsertConcept('Docker', { notes: 'Dockerfile multi-stage builds' });
expect(updated.notes).toBe('Dockerfile multi-stage builds');
});
it('clamps mastery level to 1-5 range', () => {
const low = tracker.upsertConcept('test-low', { mastery_level: 0 });
expect(low.mastery_level).toBe(1);
const high = tracker.upsertConcept('test-high', { mastery_level: 10 });
expect(high.mastery_level).toBe(5);
});
});
describe('getConcept', () => {
it('returns a concept by name', () => {
tracker.upsertConcept('Rust ownership');
const found = tracker.getConcept('Rust ownership');
expect(found).toBeDefined();
expect(found!.concept).toBe('Rust ownership');
});
it('returns undefined for nonexistent concept', () => {
expect(tracker.getConcept('nonexistent')).toBeUndefined();
});
});
describe('listConcepts', () => {
it('lists all concepts', () => {
tracker.upsertConcept('A', { mastery_level: 1 });
tracker.upsertConcept('B', { mastery_level: 3 });
tracker.upsertConcept('C', { mastery_level: 5 });
expect(tracker.listConcepts()).toHaveLength(3);
});
it('filters by minimum mastery', () => {
tracker.upsertConcept('Low', { mastery_level: 1 });
tracker.upsertConcept('Mid', { mastery_level: 3 });
tracker.upsertConcept('High', { mastery_level: 5 });
const filtered = tracker.listConcepts(3);
expect(filtered).toHaveLength(2);
expect(filtered.every(c => c.mastery_level >= 3)).toBe(true);
});
it('filters by maximum mastery', () => {
tracker.upsertConcept('Low', { mastery_level: 1 });
tracker.upsertConcept('Mid', { mastery_level: 3 });
tracker.upsertConcept('High', { mastery_level: 5 });
const filtered = tracker.listConcepts(undefined, 2);
expect(filtered).toHaveLength(1);
expect(filtered[0].concept).toBe('Low');
});
it('filters by mastery range', () => {
tracker.upsertConcept('A', { mastery_level: 1 });
tracker.upsertConcept('B', { mastery_level: 2 });
tracker.upsertConcept('C', { mastery_level: 3 });
tracker.upsertConcept('D', { mastery_level: 4 });
const filtered = tracker.listConcepts(2, 3);
expect(filtered).toHaveLength(2);
});
});
describe('recordAnswer', () => {
it('increases mastery on correct answer', () => {
tracker.upsertConcept('Promises', { mastery_level: 2 });
const updated = tracker.recordAnswer('Promises', true);
expect(updated.mastery_level).toBe(3);
expect(updated.times_correct).toBe(1);
expect(updated.times_wrong).toBe(0);
expect(updated.last_tested).toBeDefined();
});
it('decreases mastery on wrong answer', () => {
tracker.upsertConcept('Closures', { mastery_level: 3 });
const updated = tracker.recordAnswer('Closures', false);
expect(updated.mastery_level).toBe(2);
expect(updated.times_wrong).toBe(1);
});
it('caps mastery at 5', () => {
tracker.upsertConcept('HTML', { mastery_level: 5 });
const updated = tracker.recordAnswer('HTML', true);
expect(updated.mastery_level).toBe(5);
expect(updated.times_correct).toBe(1);
});
it('floors mastery at 1', () => {
tracker.upsertConcept('Assembly', { mastery_level: 1 });
const updated = tracker.recordAnswer('Assembly', false);
expect(updated.mastery_level).toBe(1);
expect(updated.times_wrong).toBe(1);
});
it('auto-creates concept on first answer if not exists', () => {
const entry = tracker.recordAnswer('New concept', true);
expect(entry.concept).toBe('New concept');
expect(entry.mastery_level).toBe(2); // correct = start at 2
expect(entry.times_correct).toBe(1);
});
it('auto-creates concept with mastery 1 on wrong answer', () => {
const entry = tracker.recordAnswer('Hard concept', false);
expect(entry.mastery_level).toBe(1);
expect(entry.times_wrong).toBe(1);
});
it('accumulates correct and wrong counts', () => {
tracker.upsertConcept('CSS Grid', { mastery_level: 3 });
tracker.recordAnswer('CSS Grid', true);
tracker.recordAnswer('CSS Grid', true);
tracker.recordAnswer('CSS Grid', false);
const entry = tracker.getConcept('CSS Grid')!;
expect(entry.times_correct).toBe(2);
expect(entry.times_wrong).toBe(1);
// 3 + 1 + 1 - 1 = 4
expect(entry.mastery_level).toBe(4);
});
});
describe('getDueForReview', () => {
it('returns concepts with mastery < 4', () => {
tracker.upsertConcept('Easy', { mastery_level: 5 });
tracker.upsertConcept('Medium', { mastery_level: 3 });
tracker.upsertConcept('Hard', { mastery_level: 1 });
const due = tracker.getDueForReview();
expect(due).toHaveLength(2);
// Hard (1) should come before Medium (3)
expect(due[0].concept).toBe('Hard');
expect(due[1].concept).toBe('Medium');
});
it('excludes mastered concepts (level 4+)', () => {
tracker.upsertConcept('Mastered', { mastery_level: 4 });
tracker.upsertConcept('NotYet', { mastery_level: 2 });
const due = tracker.getDueForReview();
expect(due).toHaveLength(1);
expect(due[0].concept).toBe('NotYet');
});
it('respects limit parameter', () => {
for (let i = 0; i < 20; i++) {
tracker.upsertConcept(`Concept ${i}`, { mastery_level: 1 });
}
expect(tracker.getDueForReview(5)).toHaveLength(5);
});
it('returns empty array when all concepts are mastered', () => {
tracker.upsertConcept('A', { mastery_level: 4 });
tracker.upsertConcept('B', { mastery_level: 5 });
expect(tracker.getDueForReview()).toHaveLength(0);
});
});
describe('table idempotency', () => {
it('creating multiple ConceptTracker instances on same DB does not error', () => {
const tracker2 = new ConceptTracker(db);
tracker.upsertConcept('test');
expect(tracker2.getConcept('test')).toBeDefined();
});
});
});