Files
waggle-os/packages/server/tests/dream-journal.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

153 lines
6.6 KiB
TypeScript

/**
* DreamJournal — event recording, deterministic summary composition,
* atomic persistence, quiet nights, narrative staleness, listing
* (DREAM-DIARY spec).
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
DreamJournal, QUIET_NIGHT_SUMMARY, composeSummary, localDateString,
} from '../src/local/dream-journal.js';
import type { DreamEvent } from '../src/local/dream-journal.js';
let dir: string;
beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-dreams-'));
});
afterEach(() => {
vi.restoreAllMocks();
fs.rmSync(dir, { recursive: true, force: true });
});
function evt(action: DreamEvent['action'], stats: Record<string, number>): DreamEvent {
return { action, at: new Date().toISOString(), stats };
}
describe('composeSummary', () => {
it('returns the quiet-night line when every counter is zero', () => {
expect(composeSummary([])).toBe(QUIET_NIGHT_SUMMARY);
expect(composeSummary([
evt('memory_compact', { temporaryPruned: 0, deprecatedPruned: 0, pframesMerged: 0 }),
evt('index_reconcile', { ftsFixed: 0, vecFixed: 0 }),
])).toBe(QUIET_NIGHT_SUMMARY);
});
it('names every kind of work with real counts and joins clauses with "and"', () => {
const summary = composeSummary([
evt('memory_compact', { temporaryPruned: 10, deprecatedPruned: 2, pframesMerged: 3 }),
evt('harvest_sync', { framesSaved: 5, itemsScanned: 40, sourcesScanned: 1, couldNotVerify: 0 }),
evt('memory_lane_extract', { framesProcessed: 20, factsWritten: 4, eventsWritten: 2, profilesWritten: 1 }),
evt('index_reconcile', { ftsFixed: 1, vecFixed: 1 }),
]);
expect(summary).toContain('merged 3 related memory fragments');
expect(summary).toContain('cleared 12 stale memories');
expect(summary).toContain('imported 5 new memories from 1 source');
expect(summary).toContain('distilled 7 quick-recall notes');
expect(summary).toContain('and repaired 2 search-index entries');
expect(summary.startsWith('Overnight I ')).toBe(true);
});
it('aggregates repeated runs of the same action across the day', () => {
const summary = composeSummary([
evt('memory_compact', { temporaryPruned: 1, deprecatedPruned: 0, pframesMerged: 0 }),
evt('memory_compact', { temporaryPruned: 2, deprecatedPruned: 1, pframesMerged: 0 }),
]);
expect(summary).toBe('Overnight I cleared 4 stale memories.');
});
it('surfaces erasure-suppressed items honestly', () => {
const summary = composeSummary([
evt('harvest_sync', { framesSaved: 3, itemsScanned: 10, sourcesScanned: 1, couldNotVerify: 2 }),
]);
expect(summary).toContain('left 2 items out (could not verify against your erasure list)');
});
it('uses singular nouns for count 1', () => {
const summary = composeSummary([evt('index_reconcile', { ftsFixed: 1, vecFixed: 0 })]);
expect(summary).toBe('Overnight I repaired 1 search-index entry.');
});
});
describe('DreamJournal record + persistence', () => {
it('records events into the local-date file and refreshes the summary', () => {
const j = new DreamJournal(dir);
const day = j.record('memory_compact', { temporaryPruned: 5, deprecatedPruned: 0, pframesMerged: 1 });
expect(day.date).toBe(localDateString());
expect(day.events).toHaveLength(1);
expect(day.summary).toContain('merged 1 related memory fragment');
// Fresh instance reads the same day from disk.
const again = new DreamJournal(dir).read(day.date);
expect(again?.events).toHaveLength(1);
expect(again?.summary).toBe(day.summary);
});
it('retries transient rename failures before persisting', () => {
const rename = vi.spyOn(fs, 'renameSync')
.mockImplementationOnce(() => { throw Object.assign(new Error('locked'), { code: 'EPERM' }); })
.mockImplementationOnce(() => { throw Object.assign(new Error('locked'), { code: 'EPERM' }); });
const j = new DreamJournal(dir);
expect(() => {
j.record('index_reconcile', { ftsFixed: 1, vecFixed: 0 }, new Date('2026-07-09T03:00:00'));
}).not.toThrow();
expect(rename).toHaveBeenCalledTimes(3);
expect(new DreamJournal(dir).read('2026-07-09')?.events).toHaveLength(1);
});
it('invalidates a stale narrative when new events arrive', () => {
const j = new DreamJournal(dir);
const day = j.record('memory_compact', { temporaryPruned: 1, deprecatedPruned: 0, pframesMerged: 0 });
j.setNarrative(day.date, 'I tidied one memory.');
expect(j.read(day.date)?.narrative).toBe('I tidied one memory.');
j.record('harvest_sync', { framesSaved: 9, itemsScanned: 9, sourcesScanned: 1, couldNotVerify: 0 });
expect(j.read(day.date)?.narrative).toBeUndefined();
});
it('setNarrative on a missing date is a no-op', () => {
const j = new DreamJournal(dir);
j.setNarrative('1999-01-01', 'ghost');
expect(j.read('1999-01-01')).toBeNull();
});
it('writes separate files per calendar day (rollover)', () => {
const j = new DreamJournal(dir);
j.record('memory_compact', { temporaryPruned: 1, deprecatedPruned: 0, pframesMerged: 0 }, new Date('2026-07-08T23:50:00'));
j.record('memory_compact', { temporaryPruned: 2, deprecatedPruned: 0, pframesMerged: 0 }, new Date('2026-07-09T00:10:00'));
expect(j.read('2026-07-08')?.events).toHaveLength(1);
expect(j.read('2026-07-09')?.events).toHaveLength(1);
});
it('survives a corrupt day file by treating it as absent', () => {
fs.mkdirSync(path.join(dir, 'dreams'), { recursive: true });
fs.writeFileSync(path.join(dir, 'dreams', '2026-07-09.json'), '{nope', 'utf8');
const j = new DreamJournal(dir);
expect(j.read('2026-07-09')).toBeNull();
// Recording over it heals the file.
const day = j.record('index_reconcile', { ftsFixed: 1, vecFixed: 0 }, new Date('2026-07-09T03:00:00'));
expect(day.events).toHaveLength(1);
});
});
describe('DreamJournal list', () => {
it('returns newest-first, limited to the requested window', () => {
const j = new DreamJournal(dir);
j.record('memory_compact', { temporaryPruned: 1, deprecatedPruned: 0, pframesMerged: 0 }, new Date('2026-07-05T03:00:00'));
j.record('memory_compact', { temporaryPruned: 2, deprecatedPruned: 0, pframesMerged: 0 }, new Date('2026-07-07T03:00:00'));
j.record('memory_compact', { temporaryPruned: 3, deprecatedPruned: 0, pframesMerged: 0 }, new Date('2026-07-09T03:00:00'));
const two = j.list(2);
expect(two.map(d => d.date)).toEqual(['2026-07-09', '2026-07-07']);
});
it('returns [] when nothing was ever recorded', () => {
expect(new DreamJournal(dir).list(7)).toEqual([]);
});
});