foreman/ is a Claude Code mod (job runner + hooks) that imports the claude-code runtime types, so tsc over the app failed on it. Excluding it from tsconfig keeps the Next build green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
155 lines
7.5 KiB
TypeScript
155 lines
7.5 KiB
TypeScript
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>) {
|
||
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])
|
||
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-${ROOT}/skills/notes/SKILL.md` })
|
||
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()
|
||
}
|
||
})
|