87 lines
3.7 KiB
JavaScript
87 lines
3.7 KiB
JavaScript
import { test, before } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { mkdtempSync, readFileSync, existsSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const fakeDsh = fileURLToPath(new URL('./fake-dsh.mjs', import.meta.url))
|
|
let ws
|
|
before(() => {
|
|
process.env.DSHO_HOME = mkdtempSync(join(tmpdir(), 'dsho-runner-'))
|
|
process.env.DSHO_DSH_CMD = fakeDsh
|
|
process.env.FAKE_DSH_LOG = join(process.env.DSHO_HOME, 'fake.log')
|
|
ws = mkdtempSync(join(tmpdir(), 'dsho-ws-'))
|
|
})
|
|
|
|
const store = await import('../src/store.mjs')
|
|
const { createRunnableJob, runJob, cancelJob } = await import('../src/runner.mjs')
|
|
const { runBatch } = await import('../src/supervisor.mjs')
|
|
|
|
test('runJob completes, stores result, session and events', async () => {
|
|
const job = createRunnableJob({ prompt: 'Hello world', cwd: join(ws, 'a') }, 'Hello world\nWRITE out.txt')
|
|
const done = await runJob(job.id)
|
|
assert.equal(done.status, 'done')
|
|
assert.equal(done.exitCode, 0)
|
|
assert.match(done.sessionId, /^session-fake-/)
|
|
assert.equal(done.progress.toolCalls, 1)
|
|
assert.equal(done.usage.inputTokens, 200)
|
|
assert.equal(readFileSync(join(process.env.DSHO_HOME, 'jobs', job.id, 'result.md'), 'utf8'), 'FAKE-RESULT: Hello world')
|
|
assert.ok(existsSync(join(ws, 'a', 'out.txt')))
|
|
assert.ok(readFileSync(join(process.env.DSHO_HOME, 'jobs', job.id, 'events.jsonl'), 'utf8').includes('"type":"final"'))
|
|
})
|
|
|
|
test('runJob resumes a session when resume is set', async () => {
|
|
const job = createRunnableJob({ prompt: 'follow', cwd: join(ws, 'a'), resume: 'session-fake-abc' }, 'follow')
|
|
const done = await runJob(job.id)
|
|
assert.equal(done.sessionId, 'session-fake-abc')
|
|
assert.match(readFileSync(join(process.env.DSHO_HOME, 'jobs', job.id, 'result.md'), 'utf8'), /resumed session-fake-abc/)
|
|
})
|
|
|
|
test('runJob marks failures', async () => {
|
|
const job = createRunnableJob({ prompt: 'broken', cwd: ws }, 'broken FAIL')
|
|
const done = await runJob(job.id)
|
|
assert.equal(done.status, 'failed')
|
|
assert.equal(done.exitCode, 1)
|
|
assert.match(done.error, /Fake failure/)
|
|
})
|
|
|
|
test('runJob enforces the timeout', async () => {
|
|
const job = createRunnableJob({ prompt: 'slow', cwd: ws, timeoutSec: 1 }, 'slow SLEEP 10')
|
|
const t0 = Date.now()
|
|
const done = await runJob(job.id)
|
|
assert.equal(done.status, 'timeout')
|
|
assert.ok(Date.now() - t0 < 8000)
|
|
})
|
|
|
|
test('cancelJob stops a running job', async () => {
|
|
const job = createRunnableJob({ prompt: 'cancel me', cwd: ws }, 'cancel me SLEEP 10')
|
|
const running = runJob(job.id)
|
|
await new Promise((r) => setTimeout(r, 700))
|
|
cancelJob(job.id)
|
|
const done = await running
|
|
assert.equal(done.status, 'cancelled')
|
|
})
|
|
|
|
test('runBatch respects the parallel limit', async () => {
|
|
const jobs = Array.from({ length: 6 }, (_, i) =>
|
|
createRunnableJob({ prompt: `batch ${i}`, cwd: join(ws, `b${i}`), index: i + 1 }, `batch-${i} SLEEP 0.6`),
|
|
)
|
|
const batch = store.createBatch({ name: 'test-batch', root: ws, parallel: 3, jobs: jobs.map((j) => j.id) })
|
|
for (const j of jobs) store.updateJob(j.id, { batch: batch.id })
|
|
await runBatch(batch.id, { staggerMs: 0 })
|
|
const states = jobs.map((j) => store.readJob(j.id).status)
|
|
assert.deepEqual(states, Array(6).fill('done'))
|
|
const log = readFileSync(process.env.FAKE_DSH_LOG, 'utf8').trim().split('\n').map(JSON.parse)
|
|
.filter((e) => e.task.startsWith('batch-'))
|
|
let active = 0
|
|
let peak = 0
|
|
for (const e of log.sort((a, b) => a.t - b.t || (a.event === 'end' ? -1 : 1))) {
|
|
active += e.event === 'start' ? 1 : -1
|
|
peak = Math.max(peak, active)
|
|
}
|
|
assert.ok(peak <= 3, `peak ${peak}`)
|
|
assert.ok(peak >= 2, `peak ${peak}`)
|
|
assert.ok(store.readBatch(batch.id).endedAt)
|
|
})
|