/** * The hand-written path. * * Its whole reason for existing is that it does NOT depend on the machinery the * rest of memory depends on: no conversation to read, and no model to read it * with. An instance where recording has never worked — no model configured, which * is the default until someone visits a settings page — is exactly the instance * whose graph is empty, and therefore exactly the one where a person needs to be * able to write something down. * * So the model is optional at every step, and what it produces is a DRAFT rather * than a row: what the user types is one field and what the graph stores is six, * and filling the other five without showing the result would mean a store whose * contents nobody had read. */ import { beforeEach, describe, expect, it, mock } from 'bun:test'; import { Database } from 'bun:sqlite'; import type { DatabaseConnection } from '$shared/types/database/connection'; import * as migration066 from '$backend/database/migrations/066_create_memory_graph'; import * as migration076 from '$backend/database/migrations/076_remove_memory_code_graph'; let db: Database; // The whole module surface, not just `getDatabase`. `mock.module` replaces the // module for the entire test PROCESS, so a partial stub leaves any file that runs // afterwards unable to import the missing names — surfacing as // "Export named 'closeDatabase' not found" in a test that never touched memory. mock.module('$backend/database', () => ({ getDatabase: () => db, initializeDatabase: async () => db, closeDatabase: () => {}, resetDatabase: async () => {}, getDatabaseInfo: async () => ({}), vacuumDatabase: async () => {} })); /** No model, which is both the default and the case worth protecting. */ mock.module('./config', () => ({ getMemoryConfig: () => ({ enabled: true, recordMemories: true, autoRecall: true, recallBudget: 2_400, model: null }), setMemoryConfig: () => {} })); mock.module('./indexer', () => ({ scheduleVectorIndexing: () => {} })); mock.module('./notify', () => ({ notifyGraphChanged: () => {}, notifyMemoryStatus: () => {} })); const { graphQueries } = await import('$backend/database/queries/graph-queries'); const { draftMemory, createMemory } = await import('./compose'); const { retrieve } = await import('./retrieval'); const PROJECT = 'project-a'; beforeEach(() => { db = new Database(':memory:'); db.exec('PRAGMA foreign_keys = ON'); migration066.up(db as unknown as DatabaseConnection); migration076.up(db as unknown as DatabaseConnection); }); describe('drafting', () => { it('works with no model configured, and says so', async () => { const draft = (await draftMemory({ text: 'Never touch the vendored directory.\nIt is generated by the build and any edit is lost.', projectId: PROJECT }))!; expect(draft.structured).toBe(false); expect(draft.label).toBe('Never touch the vendored directory.'); expect(draft.body).toContain('generated by the build'); expect(draft.note).toContain('No memory model is configured'); }); it('splits a single paragraph at the first sentence rather than at a character count', () => { // A label is a claim. The first 140 characters of a paragraph is not one. const text = 'Staging runs the old schema until March. Anything that assumes the new columns has to be feature-flagged until the cutover lands.'; return draftMemory({ text, projectId: PROJECT }).then(draft => { expect(draft!.label).toBe('Staging runs the old schema until March.'); expect(draft!.body).toContain('feature-flagged'); }); }); it('redacts a secret the user pasted in passing', async () => { const draft = (await draftMemory({ text: 'Remember the deploy key is AKIAIOSFODNN7EXAMPLE for the staging bucket.', projectId: PROJECT }))!; expect(draft.label + draft.body).not.toContain('AKIAIOSFODNN7EXAMPLE'); }); it('returns nothing for nothing', async () => { expect(await draftMemory({ text: ' ', projectId: PROJECT })).toBeNull(); }); }); describe('saving', () => { it('records it as the user\'s, which is what exempts it from every automatic removal', () => { const node = createMemory({ subkind: 'preference', scope: 'global', label: 'Prefers tabs', body: 'Stated outright.', projectId: PROJECT })!; expect(node.source).toBe('user'); expect(node.confidence).toBe(0.95); // Global memories belong to the person, not to the repository they were // noticed in — which is what lets them apply before any project is selected. expect(node.projectId).toBeNull(); }); it('records the entities it names as an attribute, not as nodes', () => { // Entities used to be stub nodes so statements about one subject would // converge. On a real graph that made 115 of 208 nodes empty-bodied names, // with `about` pointing at a stub 220 times and at a file 6 — the graph was // "memories about the names of technologies" rather than about code. const node = createMemory({ subkind: 'observation', scope: 'project', label: 'Phoenix owns the billing cutover', body: 'Agreed in the planning call.', entities: ['Phoenix'], projectId: PROJECT })!; expect(graphQueries.entityNamesOf(node.id)).toContain('Phoenix'); expect(graphQueries.neighbours(node.id, 1).some(n => n.node.subkind === 'entity')).toBe(false); }); it('finds the memory by the entity it named, without a hop', () => { const node = createMemory({ subkind: 'observation', scope: 'project', label: 'The billing cutover is scheduled for March', body: 'Agreed in the planning call.', entities: ['Phoenix'], projectId: PROJECT })!; // The name is not in the prose at all — it is only an attribute — so this // only passes because entity names are folded into the lexical index. const hits = retrieve({ query: 'Phoenix', projectId: PROJECT, expandHops: 0 }).hits; expect(hits.map(h => h.node.id)).toContain(node.id); expect(graphQueries.memoriesAboutEntity('phoenix').map(n => n.id)).toContain(node.id); }); it('reinforces an existing memory instead of storing a second copy', () => { // Agreeing with something already stored should make it stronger, not add a // near-copy that competes with it for the same recall budget. const existing = graphQueries.upsert({ subkind: 'preference', projectId: PROJECT, label: 'Prefers tabs', confidence: 0.6 }); const node = createMemory({ subkind: 'preference', scope: 'project', label: 'Prefers tabs over spaces', body: '', projectId: PROJECT, reinforceId: existing.id })!; expect(node.id).toBe(existing.id); expect(node.confidence).toBe(0.95); expect(graphQueries.count({ projectId: PROJECT })).toBe(1); }); it('makes it findable at once', () => { const node = createMemory({ subkind: 'observation', scope: 'project', label: 'The vendored directory is generated', body: 'Any edit is lost on the next build.', projectId: PROJECT })!; const row = db .prepare(`SELECT node_id FROM graph_nodes_fts WHERE graph_nodes_fts MATCH ?`) .get('"vendored"*') as { node_id: string } | null; expect(row?.node_id).toBe(node.id); }); it('refuses an empty claim', () => { expect(createMemory({ subkind: 'observation', scope: 'project', label: ' ', body: '', projectId: PROJECT })).toBeNull(); }); });