MEGAINFRA-WEBAPP/foreman/mod/tests/foreman.test.tsx
DEls 7719e4f36a feat(foreman): scheduler updates for the runner and mod hooks
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-10 23:52:12 +02:00

247 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, mock, test } from 'claude-code/testing'
import type { On } from 'claude-code'
const ROOT = '/Volumes/EXT/DEVELOPMENT/MEGAIFRASTRUCTURE/foreman'
const SURFACES = ['terminal', 'desktop'] as const
const PANE = {
component: 'Pane',
requestId: 'foreman',
// scroll and view are engine-filled; the pane never reads them.
props: { title: '👷 Foreman', isFocused: false, bodyColumns: 120, placement: 'dock' } as never,
} as const
// An in-memory copy of Foreman's folder, answering the plugin's $.fs calls.
function fakeDisk(on: On, files: Record<string, string>, onRun?: (argv: string[], disk: Map<string, string>) => void) {
const disk = new Map(Object.entries(files))
const runs: string[][] = []
on('fs.read', (_$, e) => (disk.has(e.path) ? { value: disk.get(e.path)! } : { deny: 'ENOENT' }))
on('fs.write', (_$, e) => {
disk.set(e.path, e.text)
return { value: undefined }
})
on('fs.list', (_$, e) => {
const prefix = `${e.path}/`
const names = new Set<string>()
for (const p of disk.keys()) if (p.startsWith(prefix)) names.add(p.slice(prefix.length).split('/')[0]!)
if (names.size === 0) return { deny: 'ENOENT' }
return { value: [...names].map(name => ({ name, kind: disk.has(prefix + name) ? ('file' as const) : ('dir' as const), size: 0, mtimeMs: 0, isLink: false })) }
})
on('process.run', (_$, e) => {
runs.push([...e.argv])
onRun?.([...e.argv], disk)
return { value: { exitCode: 0, stdout: '', stderr: '', isStdoutTruncated: false, isStderrTruncated: false } }
})
on('ui.open', () => ({ value: { isPlaced: true as const } }))
return { disk, runs }
}
const jobsJson = (jobs: unknown[]) => JSON.stringify({ paused: false, node: '/usr/local/bin/node', jobs })
const NOW = new Date(2026, 9, 9, 10, 30).getTime() // Fri 9 Oct 2026 10:30
test('an empty Foreman draws his guide on every surface', async ($, on) => {
mock.clock(on, { now: NOW })
mock.store(on)
fakeDisk(on, { [`${ROOT}/jobs.json`]: jobsJson([]) })
await $.command.run({ command: 'foreman', args: '' } as never)
for (const surface of SURFACES) {
const ui = await $.ui.mount({ plugin: 'foreman', surface, ...PANE })
expect(await ui.find({ type: 'Text', text: /^Foreman$/ })).toBeDefined()
expect(await ui.find({ type: 'Text', text: /No jobs yet/ })).toBeDefined()
expect(await ui.find({ type: 'Text', text: /Scheduler not running/ })).toBeDefined()
await ui.unmount()
}
})
test('the board shows what is next, what needs attention, and Reviewed clears it', async ($, on) => {
mock.clock(on, { now: NOW })
mock.store(on)
const run = {
runId: 'notes-2026-10-09-08-00', trigger: 'schedule', startedAt: new Date(2026, 9, 9, 8, 0).toISOString(),
status: 'attention', attentionCount: 2, summary: 'Two delivery notes have no purchase order.', report: 'runs/notes/x.md',
}
const { disk } = fakeDisk(on, {
[`${ROOT}/jobs.json`]: jobsJson([{ id: 'notes', name: 'Delivery notes check', skill: 'skills/notes/SKILL.md', schedule: 'mon-fri 08:00 11:00 14:00', enabled: true }]),
[`${ROOT}/state/notes.json`]: JSON.stringify({ id: 'notes', status: 'attention', lastRun: run, history: [run] }),
[`${ROOT}/state/scheduler.json`]: JSON.stringify({ lastTick: new Date(NOW - 30_000).toISOString() }),
[`${ROOT}/runs/notes/x.md`]: '# Delivery notes check\n\nVERDICT: ATTENTION (2 items)\n\n## Summary\nTwo notes lack a PO.',
[`${ROOT}/skills/notes/SKILL.md`]: '---\nname: notes\ndescription: Checks delivery notes\n---\nbody',
})
await $.command.run({ command: 'foreman', args: '' } as never)
for (const surface of SURFACES) {
const ui = await $.ui.mount({ plugin: 'foreman', surface, ...PANE })
expect(await ui.find({ type: 'Text', text: /^UP NEXT · 1$/ })).toBeDefined()
expect(await ui.find({ type: 'Text', text: /today 11:00/ })).toBeDefined()
expect(await ui.find({ type: 'Text', text: /^NEEDS ATTENTION · 1$/ })).toBeDefined()
expect(await ui.find({ type: 'Text', text: /Scheduler on/ })).toBeDefined()
await ui.unmount()
}
const ui = await $.ui.mount({ plugin: 'foreman', surface: 'desktop', ...PANE })
await ui.press({ key: `rep-${run.runId}` })
expect(await ui.find({ type: 'Text', text: /VERDICT: ATTENTION/ })).toBeDefined()
await ui.press({ key: 'rack' })
expect(JSON.parse(disk.get(`${ROOT}/state/acks.json`) ?? '[]')).toContain(run.runId)
await ui.press({ key: 'rback' })
expect(await ui.find({ type: 'Text', text: /^NEEDS ATTENTION · 0$/ })).toBeDefined()
await ui.unmount()
})
test('a skill goes on the board, its schedule is edited, and Run now starts the runner', async ($, on) => {
mock.clock(on, { now: NOW })
mock.store(on)
const { disk, runs } = fakeDisk(on, {
[`${ROOT}/jobs.json`]: jobsJson([]),
[`${ROOT}/skills/notes/SKILL.md`]: '---\nname: Delivery notes check\ndescription: Checks delivery notes\n---\nbody',
})
await $.command.run({ command: 'foreman', args: '' } as never)
const ui = await $.ui.mount({ plugin: 'foreman', surface: 'desktop', ...PANE })
await ui.press({ key: 'nav-skills' })
await ui.press({ key: 'addskill-foreman-notes' })
const jobs = () => JSON.parse(disk.get(`${ROOT}/jobs.json`)!).jobs as Array<Record<string, unknown>>
expect(jobs()[0]).toMatchObject({ id: 'delivery-notes-check', skill: 'skills/notes/SKILL.md', enabled: false })
await ui.input({ key: 'sched-delivery-notes-check', text: 'banana 8:00' })
expect(await ui.find({ type: 'Text', text: /Didn't understand "banana"/ })).toBeDefined()
expect(jobs()[0]?.schedule).toBe('mon-fri 08:00')
await ui.input({ key: 'sched-delivery-notes-check', text: 'every 2h mon-fri 07:00-17:00' })
expect(jobs()[0]?.schedule).toBe('every 2h mon-fri 07:00-17:00')
expect(await ui.find({ type: 'Text', text: /Every 2h, Mon–Fri, 07:00–17:00/ })).toBeDefined()
await ui.press({ key: 'etoggle-delivery-notes-check' })
expect(jobs()[0]?.enabled).toBe(true)
await ui.press({ key: 'erun-delivery-notes-check' })
expect(runs.at(-1)?.join(' ')).toContain('runner.mjs')
expect(runs.at(-1)?.join(' ')).toContain('run delivery-notes-check')
await ui.unmount()
})
test('the theme button cycles through the bright themes', async ($, on) => {
mock.clock(on, { now: NOW })
mock.store(on)
fakeDisk(on, { [`${ROOT}/jobs.json`]: jobsJson([]) })
for (const surface of SURFACES) {
const ui = await $.ui.mount({ plugin: 'foreman', surface, ...PANE })
const before = (await ui.find({ key: 'theme' }))?.text ?? ''
await ui.press({ key: 'theme' })
expect((await ui.find({ key: 'theme' }))?.text).not.toBe(before)
await ui.unmount()
}
})
test('dark themes (Dracula, Monokai) still draw every control', async ($, on) => {
mock.clock(on, { now: NOW })
mock.store(on)
fakeDisk(on, {
[`${ROOT}/jobs.json`]: jobsJson([{ id: 'notes', name: 'Delivery notes check', skill: 'skills/notes/SKILL.md', schedule: 'mon-fri 08:00', enabled: true }]),
})
await $.command.run({ command: 'foreman', args: '' } as never)
for (const surface of SURFACES) {
const ui = await $.ui.mount({ plugin: 'foreman', surface, ...PANE })
const seen: string[] = []
for (let i = 0; i < 8; i++) {
const label = (await ui.find({ key: 'theme' }))?.text ?? ''
seen.push(label)
if (/Dracula|Monokai/.test(label)) {
expect(await ui.find({ key: 'run-notes' })).toBeDefined()
expect(await ui.find({ key: 'toggle-notes' })).toBeDefined()
expect(await ui.find({ key: 'nav-skills' })).toBeDefined()
}
await ui.press({ key: 'theme' })
}
expect(seen.some(l => l.includes('Dracula'))).toBe(true)
expect(seen.some(l => l.includes('Monokai'))).toBe(true)
await ui.unmount()
}
})
// Stands in for bin/runner.mjs: `node runner.mjs run <id>` writes a finished state and report.
const fakeRunner = (status: 'clear' | 'attention') => (argv: string[], disk: Map<string, string>) => {
const i = argv.indexOf('run')
if (i < 0 || !argv[0]?.endsWith('node')) return
const id = argv[i + 1]!
const report = `runs/${id}/r.md`
disk.set(`${ROOT}/${report}`, `# Task\n\nVERDICT: ${status === 'clear' ? 'CLEAR' : 'ATTENTION (1 items)'}\n\n## Summary\nChecked 4 notes.`)
const run = { runId: `${id}-r`, trigger: 'manual', startedAt: new Date(NOW).toISOString(), status, attentionCount: status === 'clear' ? 0 : 1, report, toolCount: 7 }
disk.set(`${ROOT}/state/${id}.json`, JSON.stringify({ id, status, lastRun: run, history: [run] }))
}
test('an agent hands Foreman a task and waits for the report', async ($, on) => {
mock.clock(on, { now: NOW })
mock.store(on)
const { disk, runs } = fakeDisk(on, { [`${ROOT}/jobs.json`]: jobsJson([]) }, fakeRunner('attention'))
const done = await $.tool.call({ tool: 'mcp__foreman__task', task: 'Check this week\'s Afrisam delivery notes against their POs.', title: 'Afrisam notes check' })
const text = String(done.result)
expect(text).toContain('ATTENTION (1 items)')
expect(text).toContain('Checked 4 notes.')
const job = (JSON.parse(disk.get(`${ROOT}/jobs.json`)!).jobs as Array<Record<string, unknown>>)[0]!
expect(job).toMatchObject({ oneOff: true, schedule: 'manual', enabled: false, permission: 'smart', effort: 'xhigh' })
expect(String(job.id)).toMatch(/^task-\d{8}-\d{6}-afrisam-notes-check$/)
expect(disk.get(`${ROOT}/${job.skill}`)).toContain('only read and report')
expect(runs.at(-1)).toEqual([expect.stringContaining('node'), `${ROOT}/bin/runner.mjs`, 'run', job.id])
for (const surface of SURFACES) {
const ui = await $.ui.mount({ plugin: 'foreman', surface, ...PANE })
expect(await ui.find({ type: 'Text', text: /^TASKS FROM AGENTS · 1$/ })).toBeDefined()
expect(await ui.find({ type: 'Text', text: /Afrisam notes check/ })).toBeDefined()
expect(await ui.find({ type: 'Text', text: /^SCHEDULED JOBS · 0$/ })).toBeDefined()
await ui.unmount()
}
})
test('a background task starts at once and its report is collected later', async ($, on) => {
mock.clock(on, { now: NOW })
mock.store(on)
const { disk, runs } = fakeDisk(on, { [`${ROOT}/jobs.json`]: jobsJson([]) })
const started = await $.tool.call({ tool: 'mcp__foreman__task', task: 'Summarise open POs for The Hills.', wait: false, allow_changes: true })
const id = /task-[a-z0-9-]+/.exec(String(started.result))![0]
expect(runs.at(-1)?.join(' ')).toContain(`nohup`)
expect((JSON.parse(disk.get(`${ROOT}/jobs.json`)!).jobs as Array<Record<string, unknown>>)[0]?.permission).toBe('full')
expect(String((await $.tool.call({ tool: 'mcp__foreman__task_result', id })).result)).toContain('not started yet')
fakeRunner('clear')(['/usr/local/bin/node', 'runner.mjs', 'run', id], disk)
expect(String((await $.tool.call({ tool: 'mcp__foreman__task_result', id })).result)).toContain(`Foreman finished task ${id}: CLEAR`)
expect((await $.tool.call({ tool: 'mcp__foreman__task', task: ' ' })).deny).toContain('needs a task')
})
test('"once tomorrow 08:00" is saved as a fixed date', async ($, on) => {
mock.clock(on, { now: NOW })
mock.store(on)
const { disk } = fakeDisk(on, {
[`${ROOT}/jobs.json`]: jobsJson([{ id: 'notes', name: 'Notes', skill: 'skills/notes/SKILL.md', schedule: 'manual', enabled: true }]),
})
await $.command.run({ command: 'foreman', args: '' } as never)
const ui = await $.ui.mount({ plugin: 'foreman', surface: 'desktop', ...PANE })
await ui.press({ key: 'edit-notes' })
await ui.input({ key: 'sched-notes', text: 'once tomorrow 08:00' })
expect(JSON.parse(disk.get(`${ROOT}/jobs.json`)!).jobs[0].schedule).toBe('once 2026-10-10 08:00')
expect(await ui.find({ type: 'Text', text: /Once on Sat, 10 Oct 2026 at 08:00/ })).toBeDefined()
await ui.unmount()
})
test('one-click schedules turn a job on, and a skill can be deleted to the archive', async ($, on) => {
mock.clock(on, { now: NOW })
mock.store(on)
const { disk, runs } = fakeDisk(on, {
[`${ROOT}/jobs.json`]: jobsJson([]),
[`${ROOT}/skills/notes/SKILL.md`]: '---\nname: Delivery notes check\ndescription: Checks delivery notes\n---\nbody',
})
await $.command.run({ command: 'foreman', args: '' } as never)
const jobs = () => JSON.parse(disk.get(`${ROOT}/jobs.json`)!).jobs as Array<Record<string, unknown>>
for (const surface of SURFACES) {
disk.set(`${ROOT}/jobs.json`, jobsJson([]))
const ui = await $.ui.mount({ plugin: 'foreman', surface, ...PANE })
await ui.press({ key: 'nav-skills' })
await ui.press({ key: 'addskill-foreman-notes' })
await ui.press({ key: 'preset-delivery-notes-check-every-hour-work-hours' })
expect(jobs()[0]).toMatchObject({ schedule: 'every 1h mon-fri 07:00-17:00', enabled: true })
await ui.press({ key: 'preset-delivery-notes-check-once-tomorrow-08-00' })
expect(jobs()[0]?.schedule).toBe('once 2026-10-10 08:00')
await ui.press({ key: 'nav-skills' })
expect(await ui.find({ key: 'schedskill-foreman-notes' })).toBeDefined()
await ui.press({ key: 'delskill-notes' })
await ui.press({ key: 'delskill-yes-notes' })
expect(runs.some(a => a[0] === '/bin/mv' && a[1] === `${ROOT}/skills/notes` && String(a[2]).startsWith(`${ROOT}/skills/_archive/notes-`))).toBe(true)
expect(jobs()).toHaveLength(0)
await ui.unmount()
}
})