maestro/src/engine/notes-inject.test.ts
2026-06-03 05:08:00 +00:00

95 lines
5.5 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import Database from 'better-sqlite3';
import { mkdtempSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { runMigrations } from '../db/migrate.js';
import { NotesRepository } from '../notes/notes-repository.js';
import { NotesService } from '../notes/notes-service.js';
import { buildInjectSection, InjectConfig } from './notes-inject.js';
describe('buildInjectSection', () => {
let tmpRoot: string;
let db: Database.Database;
let service: NotesService;
let bobUser: any;
beforeEach(() => {
tmpRoot = mkdtempSync(join(tmpdir(), 'inject-test-'));
db = new Database(join(tmpRoot, 'test.db'));
runMigrations(db);
db.prepare(`INSERT INTO users (id, email, name) VALUES ('alice','a@x.com','Alice'),('bob','b@x.com','Bob')`).run();
const repo = new NotesRepository(db);
service = new NotesService({ db, repo, userFolderRoot: tmpRoot, getUserOrgIds: () => ['team1'] });
bobUser = { id: 'bob', role: 'user', orgIds: [] };
});
afterEach(() => { db.close(); rmSync(tmpRoot, { recursive: true, force: true }); });
it('returns empty string when no inject subscriptions', () => {
const out = buildInjectSection({ user: bobUser, service, config: { perNoteMaxKb: 8, totalMaxKb: 32, overBudgetStrategy: 'skip_remaining' } });
expect(out).toBe('');
});
it('emits section header and one note when subscribed', () => {
service.writeNote({ ownerId: 'alice', folder: 'runbooks', fileName: 'failover.md', content: '---\nvisibility: public\n---\nstep 1 do X' });
service.upsertSubscription({ consumerUser: bobUser, publisherUserId: 'alice', folder: 'runbooks', mode: 'inject', enabled: 1 });
const out = buildInjectSection({ user: bobUser, service, config: { perNoteMaxKb: 8, totalMaxKb: 32, overBudgetStrategy: 'skip_remaining' } });
expect(out).toContain('## Subscribed Notes');
expect(out).toContain('### From Alice/runbooks/failover.md');
expect(out).toContain('step 1 do X');
});
it('skips notes over per_note_max_kb', () => {
const big = 'x'.repeat(10 * 1024); // 10 KB body
service.writeNote({ ownerId: 'alice', folder: 'big', fileName: 'huge.md', content: `---\nvisibility: public\n---\n${big}` });
service.upsertSubscription({ consumerUser: bobUser, publisherUserId: 'alice', folder: 'big', mode: 'inject', enabled: 1 });
const out = buildInjectSection({ user: bobUser, service, config: { perNoteMaxKb: 4, totalMaxKb: 32, overBudgetStrategy: 'skip_remaining' } });
expect(out).not.toContain('xxxx');
});
it('applies skip_remaining over total budget', () => {
const med = 'y'.repeat(3 * 1024); // 3 KB
service.writeNote({ ownerId: 'alice', folder: 'a', fileName: 'one.md', content: `---\nvisibility: public\n---\n${med}` });
service.writeNote({ ownerId: 'alice', folder: 'a', fileName: 'two.md', content: `---\nvisibility: public\n---\n${med}` });
service.writeNote({ ownerId: 'alice', folder: 'a', fileName: 'three.md', content: `---\nvisibility: public\n---\n${med}` });
service.upsertSubscription({ consumerUser: bobUser, publisherUserId: 'alice', folder: 'a', mode: 'inject', enabled: 1 });
const out = buildInjectSection({ user: bobUser, service, config: { perNoteMaxKb: 8, totalMaxKb: 4, overBudgetStrategy: 'skip_remaining' } });
// Only first note fits (≈ 3 KB, second would push past 4 KB total)
const occurrences = (out.match(/### From Alice\//g) || []).length;
expect(occurrences).toBe(1);
});
it('degrade_to_search emits placeholder with skipped note names when over budget', () => {
const med = 'z'.repeat(3 * 1024); // 3 KB each
service.writeNote({ ownerId: 'alice', folder: 'kb', fileName: 'first.md', content: `---\nvisibility: public\n---\n${med}` });
service.writeNote({ ownerId: 'alice', folder: 'kb', fileName: 'second.md', content: `---\nvisibility: public\n---\n${med}` });
service.upsertSubscription({ consumerUser: bobUser, publisherUserId: 'alice', folder: 'kb', mode: 'inject', enabled: 1 });
// totalMaxKb=4 means the second note won't fit
const out = buildInjectSection({
user: bobUser, service,
config: { perNoteMaxKb: 8, totalMaxKb: 4, overBudgetStrategy: 'degrade_to_search' },
});
expect(out).toContain('## Subscribed Notes');
// First note fits, second goes into the placeholder
expect(out).toContain('use SearchNotes');
// The skipped note should be listed in the placeholder
expect(out).toContain('second.md');
});
it('returns deterministic order on equal updated_at (tiebreak by owner/folder/file)', () => {
// Insert 2 notes at the same timestamp by direct DB manipulation
db.prepare(`INSERT INTO note_index (owner_id, folder, file_name, title, visibility, visibility_scope_org_id, mode_hint, tags_json, content_size, content_hash, body, updated_at) VALUES
('alice','f','b.md','B','public',NULL,NULL,'[]', 10, 'h', 'body B', 999),
('alice','f','a.md','A','public',NULL,NULL,'[]', 10, 'h', 'body A', 999)
`).run();
service.upsertSubscription({ consumerUser: bobUser, publisherUserId: 'alice', folder: 'f', mode: 'inject', enabled: 1 });
const out = buildInjectSection({ user: bobUser, service, config: { perNoteMaxKb: 8, totalMaxKb: 32, overBudgetStrategy: 'skip_remaining' } });
const aIdx = out.indexOf('### From Alice/f/a.md');
const bIdx = out.indexOf('### From Alice/f/b.md');
expect(aIdx).toBeGreaterThan(-1);
expect(bIdx).toBeGreaterThan(-1);
expect(aIdx).toBeLessThan(bIdx);
});
});