import { describe, expect, it } from 'vitest' import type { ContentRule, ResearchRule } from './parse' import { budgetFor, canSchedule, configFor, needsTwoSources, productionError, ruleOf, VIDEO, type ExpressAgent } from './schedules' const rule = (kind: ContentRule['kind'], count = 2, per: ContentRule['per'] = 'week', topic = 'onboarding', language: ContentRule['language'] = 'English'): ContentRule => ({ type: 'content', kind, count, per, topic, language }) // What the engine sends back for an agent created from configFor. function agentFrom(config: ReturnType): ExpressAgent { const c = config as Record return { id: 1, status: 'active', next_run_at: null, created_at: '2026-10-01T00:00:00Z', auto_approve_sources: false, name: c.name as string, goal: c.goal as string, instructions: c.instructions as string, enabled_tools: c.enabled_tools as string[], material_outputs: (c.material_outputs as ExpressAgent['material_outputs']) ?? [], content_goal: (c.content_goal as ExpressAgent['content_goal']) ?? null, video_options: (c.video_options as ExpressAgent['video_options']) ?? null, run_schedule: (c.run_schedule as ExpressAgent['run_schedule']) ?? null, run_budget: c.run_budget as number, monthly_budget: c.monthly_budget as number, } } describe('configFor', () => { it('makes an audio schedule a narration summary', () => { const config = configFor(rule('audio')) as Record expect(config.material_outputs).toEqual([{ kind: 'summary', options: {} }]) expect(config.instructions).toMatch(/narration script/) expect(config.name).toBe('Express · 2 audio per week · onboarding') }) it('makes a SCORM schedule a lesson + quiz pair', () => { const config = configFor(rule('scorm')) as Record expect((config.material_outputs as { kind: string }[]).map(o => o.kind)).toEqual(['summary', 'quiz']) }) it('makes a video schedule a run-scheduled Video Studio agent', () => { const config = configFor(rule('video', 1, 'day'), 100) as Record expect(config.enabled_tools).toContain('studio.create_video') expect(config).not.toHaveProperty('content_goal') expect(config.run_schedule).toMatchObject({ cadence: 'daily' }) expect(config.video_options).toMatchObject({ max_scenes: VIDEO.scenes, max_duration_seconds: VIDEO.seconds }) expect(config.run_budget).toBe(110) expect(config.monthly_budget).toBe(110 * 31) }) it('keeps budgets ordered run ≤ weekly ≤ monthly', () => { for (const kind of ['summary', 'quiz', 'audio', 'scorm'] as const) { for (const count of [1, 5, 50]) { const b = budgetFor(rule(kind, count)) expect(b.run).toBeLessThanOrEqual(b.weekly!) expect(b.weekly!).toBeLessThanOrEqual(b.monthly) } } }) }) describe('content language', () => { it('writes the chosen language into the brief and the video options', () => { expect((configFor(rule('summary', 2, 'week', 'sales', '中文')) as { instructions: string }).instructions) .toMatch(/Write all content in Simplified Chinese \(中文\)\./) expect((configFor(rule('video', 1, 'week', 'sales', 'Bahasa Indonesia'), 50) as { video_options: { language: string } }).video_options.language) .toBe('Bahasa Indonesia') }) }) describe('chosen material', () => { it('marks a schedule made from chosen material in its brief, and reads it back as chosen', () => { const r = rule('summary', 2, 'week', 'sales', 'Bahasa Indonesia') const config = configFor(r, 0, true) expect(config.instructions).toMatch(/Use only the Brain material chosen for this schedule\./) expect(configFor(r).instructions).not.toMatch(/chosen/) expect(ruleOf(agentFrom(config))).toEqual({ ...r, chosen: true }) expect(ruleOf(agentFrom(configFor(r)))).toEqual(r) }) }) describe('ruleOf', () => { it.each([ rule('summary', 3), rule('audio', 2), rule('scorm', 1), rule('quiz', 1, 'day'), rule('video', 1, 'week'), rule('video', 1, 'day'), rule('summary', 2, 'week', 'sales', 'Bahasa Indonesia'), rule('video', 1, 'week', 'sales', '中文'), ])('reads back the rule a schedule was created from: $count $kind per $per', r => { expect(ruleOf(agentFrom(configFor(r, 50)))).toEqual(r) }) it('reads back a research rule', () => { const r: ResearchRule = { type: 'research', cadence: 'weekly', weekday: 2, topic: 'sales', autoApprove: true } const agent = { ...agentFrom(configFor(r)), auto_approve_sources: true } expect(ruleOf(agent)).toEqual(r) }) }) describe('canSchedule', () => { it('accepts every kind within limits', () => { expect(canSchedule(rule('audio', 5))).toBe(true) expect(canSchedule(rule('scorm', 2))).toBe(true) expect(canSchedule(rule('video', 1))).toBe(true) }) it('rejects video counts without a cadence and production over 50 a week', () => { expect(canSchedule(rule('video', 3))).toBe(false) expect(canSchedule(rule('summary', 8, 'day'))).toBe(false) expect(canSchedule(rule('summary', 0))).toBe(false) }) }) describe('production sources', () => { it('needs two sources for planned content, not for video or research', () => { expect(needsTwoSources(rule('quiz'))).toBe(true) expect(needsTwoSources(rule('video', 1))).toBe(false) expect(needsTwoSources({ type: 'research', cadence: 'weekly', weekday: 2, topic: 'sales', autoApprove: false })).toBe(false) }) it("explains the engine's refusal for too few sources and passes others through", () => { expect(productionError('At least two ready sources with complementary evidence are required')).toMatch(/2/) expect(productionError('At least two ready sources with complementary evidence are required')).not.toMatch(/complementary/) expect(productionError('Something else')).toBe('Something else') }) })