import { describe, expect, it } from 'vitest' import type { DirectiveEntry } from '../directives/record-directive-v0.js' import type { Focus, ScheduledItem } from '../scheduler/scheduler-v0.js' import { activeDirectives, assembleHotContext, estimateTokens, HOT_CONTEXT_BUDGET_TOKENS, } from './memory-v0.js' const item = (number: number, title: string): ScheduledItem => ({ number, title, labels: [], order: 0, startDay: 0, endDay: 1, durationDays: 1, blockedBy: [], blocks: [], critical: false, rationale: '' }) const focus: Focus = { now: item(7, 'Fix lifecycle inference'), next: item(8, 'Webhook listener'), later: null } const directive = (over: Partial): DirectiveEntry => ({ id: 'd1', ts: '2026-02-10T09:00:00Z', status: 'accepted', kind: 'note', quote: 'pilots come first', ...over, }) describe('memory-v0 (#27)', () => { it('estimateTokens is a slight over-estimate (~4 chars/token)', () => { expect(estimateTokens('')).toBe(0) expect(estimateTokens('abcd')).toBe(1) expect(estimateTokens('a'.repeat(4001))).toBe(1001) }) it('activeDirectives keeps accepted/amended, most-recent-first', () => { const ds = [ directive({ id: 'a', ts: '2026-02-01T00:00:00Z', status: 'accepted', quote: 'old' }), directive({ id: 'b', ts: '2026-02-11T00:00:00Z', status: 'amended', quote: 'new' }), directive({ id: 'c', ts: '2026-02-12T00:00:00Z', status: 'withdrawn', quote: 'gone' }), directive({ id: 'd', ts: '2026-02-09T00:00:00Z', status: 'proposed', quote: 'maybe' }), ] expect(activeDirectives(ds).map((d) => d.quote)).toEqual(['new', 'old']) }) it('assembles hot context under the 2k budget even with a huge charter', () => { const huge = 'Charter line that goes on and on. '.repeat(2000) // ~34k tokens const ds = Array.from({ length: 50 }, (_, i) => directive({ id: `d${i}`, ts: `2026-02-${String((i % 27) + 1).padStart(2, '0')}T00:00:00Z`, quote: `directive number ${i}` }), ) const out = assembleHotContext({ charter: huge, directives: ds, focus }) expect(estimateTokens(out)).toBeLessThanOrEqual(HOT_CONTEXT_BUDGET_TOKENS) // focus (tiny) is always kept; the charter is the part that gets truncated expect(out).toContain('## Focus') expect(out).toContain('#7 Fix lifecycle inference') expect(out).toContain('…') // charter was clamped // at least some recent directives survived expect(out).toContain('## Active directives') }) it('never inlines ticket bodies — only numbers + titles appear for focus', () => { // The assembler takes no issue bodies by construction; focus shows #number title only. const out = assembleHotContext({ charter: 'Ship the beta.', directives: [directive({})], focus }) expect(out).toContain('Now: #7 Fix lifecycle inference') expect(out).toContain('[note] pilots come first') expect(out).not.toMatch(/body|description|comment/i) }) it('degrades to just focus when there is no charter or directives', () => { const out = assembleHotContext({ charter: '', directives: [], focus }) expect(out).toBe(['## Focus', 'Now: #7 Fix lifecycle inference', 'Next: #8 Webhook listener', 'Later: —'].join('\n')) }) })