95 lines
5.5 KiB
TypeScript
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);
|
|
});
|
|
});
|