Files

133 lines
5.2 KiB
JavaScript

import { test, before, after } from 'node:test'
import assert from 'node:assert/strict'
import { spawn } from 'node:child_process'
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { startFakeWeb } from './fake-web.mjs'
const BIN = fileURLToPath(new URL('../bin/dsho.mjs', import.meta.url))
let fake
let env
let ws
before(async () => {
fake = await startFakeWeb()
const home = mkdtempSync(join(tmpdir(), 'dsho-web-'))
ws = mkdtempSync(join(tmpdir(), 'dsho-web-ws-'))
const tokenFile = join(home, 'last-url')
writeFileSync(tokenFile, `${fake.base}/?token=T\n`)
env = {
...process.env,
DSHO_HOME: home,
DSHO_WEB_URL: fake.base,
DSHO_WEB_TOKEN_FILE: tokenFile,
DSHO_WEB_POLL_MS: '150',
DSHO_BACKEND: 'auto',
DSHO_WORKSPACES: join(ws, 'auto'),
DSHO_DSH_CMD: '/bin/false',
}
})
after(() => fake.close())
// Async spawn: the fake server lives in this process and must keep serving.
function dsho(args) {
return new Promise((resolve) => {
const child = spawn(process.execPath, [BIN, ...args], { env })
let out = ''
let err = ''
child.stdout.on('data', (c) => (out += c))
child.stderr.on('data', (c) => (err += c))
child.stdin.end()
child.on('close', (code) => {
let json = null
try {
json = JSON.parse(out)
} catch {
// Text output.
}
resolve({ code, out, err, json })
})
})
}
test('auto backend runs a job inside the web UI and names the session', async () => {
const r = await dsho(['run', '-C', join(ws, 'one'), '-w', '--json', '--name', 'My web job', 'Hello web TOOL'])
assert.equal(r.code, 0, r.err + r.out)
assert.equal(r.json.job.backend, 'web')
assert.equal(r.json.result, 'WEB-RESULT: Hello web TOOL')
assert.equal(r.json.job.web.workspaceTitle, 'one')
assert.equal(r.json.job.progress.toolCalls, 1)
assert.equal(r.json.job.usage.inputTokens, 30)
const session = fake.sessions.get(r.json.job.sessionId)
assert.equal(session.title, 'My web job')
assert.equal(session.cwd, join(ws, 'one'))
const logs = await dsho(['logs', r.json.job.id])
assert.match(logs.out, /→ bash: echo web/)
assert.match(logs.out, /final: WEB-RESULT/)
})
test('a web batch becomes one workspace with one session per job', async () => {
const r = await dsho(['research', 'Question A', 'Question B', '-C', join(ws, 'research'), '-w', '--json'])
assert.equal(r.code, 0, r.err + r.out)
assert.equal(r.json.summary.status, 'done')
const root = join(ws, 'research')
const wsRow = [...fake.workspaces.values()].find((w) => w.path === root)
assert.equal(wsRow.sessionIds.length, 2)
for (const job of r.json.jobs) {
assert.equal(job.cwd, root)
assert.ok(job.outDir.startsWith(root + '/0'))
assert.match(fake.sessions.get(job.sessionId).title, /^0\d · Research: Question/)
assert.match(readFileSync(join(env.DSHO_HOME, 'jobs', job.id, 'task.md'), 'utf8'), new RegExp(`^Working folder for this task: ${job.outDir}`))
}
})
test('continue prompts the same web session', async () => {
const first = await dsho(['run', '-C', join(ws, 'follow'), '-w', '--json', 'Begin'])
const creates = fake.calls.filter((c) => c.method === 'session/create').length
const next = await dsho(['continue', first.json.job.id, 'Carry on', '-w', '--json'])
assert.equal(next.code, 0, next.err + next.out)
assert.equal(next.json.job.sessionId, first.json.job.sessionId)
assert.equal(next.json.result, 'WEB-RESULT: Carry on')
assert.equal(fake.calls.filter((c) => c.method === 'session/create').length, creates)
})
test('model override selects the model on the web session', async () => {
const r = await dsho(['run', '-C', join(ws, 'model'), '-m', 'pro', '--effort', 'high', '-w', '--json', 'Model test'])
assert.equal(r.code, 0, r.err + r.out)
const sel = fake.sessions.get(r.json.job.sessionId).model
assert.equal(sel.model, 'deepseek-v4-pro')
assert.equal(sel.reasoningEffort, 'high')
})
test('cancel aborts the web turn', async () => {
const start = await dsho(['run', '-C', join(ws, 'abort'), '--json', 'Long SLEEP 30'])
await new Promise((r) => setTimeout(r, 800))
const c = await dsho(['cancel', start.json.job.id, '--json'])
assert.equal(c.code, 0, c.err)
assert.equal(c.json.jobs[0].status, 'cancelled')
assert.ok(fake.calls.some((x) => x.method === 'session/cancel'))
})
test('failed web turns map to failed jobs', async () => {
const r = await dsho(['run', '-C', join(ws, 'failure'), '-w', '--json', 'Please FAIL'])
assert.equal(r.code, 1)
assert.equal(r.json.job.status, 'failed')
assert.match(r.json.job.error, /Web failure/)
})
test('backend headless is honoured even when the UI runs', async () => {
const r = await dsho(['run', '-C', join(ws, 'headless'), '-b', 'headless', '--json', 'anything'])
assert.equal(r.json.job.backend, 'headless')
await dsho(['cancel', r.json.job.id])
})
test('ui login stores a token URL and rejects a wrong one', async () => {
const good = await dsho(['ui', 'login', `${fake.base}/?token=T`, '--json'])
assert.equal(good.code, 0, good.err + good.out)
assert.equal(good.json.url, fake.base)
const bad = await dsho(['ui', 'login', `${fake.base}/?token=WRONG`, '--json'])
assert.equal(bad.code, 5)
})