Loading files
8035964e1This file is pinned to the selected release. Download the original to open it in your design tool.
// `harness dsh install` on real (local) git repositories: a harness that uses a viewer package
// installs the viewer too, and narrates the viewer's phases under its own id.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { execFileSync } from 'node:child_process'
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { env } from '../config/env.js'
import { DOCTOR_TIMEOUT_MS, installDsh, isTransientGitFailure, removeDsh, resolveInstallSource, runDshDoctor, type DshInstallProgress } from './install.js'
import { dshInstallDir, installedDsh, invalidateInstalledDsh, listInstalledDsh, readInstalledIndex, type InstalledDsh } from './installed.js'
import { HARNESS_MONOREPO, type DshRegistryEntry } from './registry.js'
function gitRepo(dir: string, files: Record<string, string>): string {
mkdirSync(dir, { recursive: true })
for (const [name, body] of Object.entries(files)) {
mkdirSync(join(dir, name, '..'), { recursive: true })
// a script is committed executable, the way a real repo carries it; a clone keeps the bit
writeFileSync(join(dir, name), body, { mode: name.endsWith('.sh') ? 0o755 : 0o644 })
}
const git = (...args: string[]) => execFileSync('git', ['-C', dir, ...args], { stdio: 'ignore', env: { ...process.env, GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t' } })
git('init', '-q', '-b', 'main'); git('add', '-A'); git('commit', '-q', '-m', 'fixture')
return dir
}
describe('installDsh with a used viewer', () => {
let root: string
let savedDshDir: string
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'dsh-install-'))
savedDshDir = env.DSH_DIR
env.DSH_DIR = join(root, 'installed')
invalidateInstalledDsh()
})
afterEach(() => {
env.DSH_DIR = savedDshDir
invalidateInstalledDsh()
rmSync(root, { recursive: true, force: true })
})
it('installs the viewer package the harness uses, from the registry, and narrates it under the harness', async () => {
const viewerRepo = gitRepo(join(root, 'src', 'viewer'), {
'harness.json': JSON.stringify({ spec: 1, kind: 'viewer', id: 'acme/viewer', name: 'Viewer', toolchain: { doctor: './doctor.sh' }, viewer: { command: './viewer.sh', url: 'http://127.0.0.1:${port}/?file=${artifact}', artifactExtensions: ['.step'] } }),
'doctor.sh': '#!/bin/sh\necho "ok viewer"\n',
'viewer.sh': '#!/bin/sh\nsleep 1000\n',
})
const harnessRepo = gitRepo(join(root, 'src', 'thing'), {
'harness.json': JSON.stringify({ spec: 1, id: 'acme/thing', name: 'Thing', engine: 'claude', agent: { instructions: 'AGENTS.md' }, viewer: { use: 'acme/viewer' } }),
'AGENTS.md': '# Thing\n',
})
const registry = (id: string): DshRegistryEntry | undefined => (id === 'acme/viewer' ? { id, kind: 'viewer', name: 'Viewer', repo: viewerRepo, tier: 2 } : undefined)
const frames: DshInstallProgress[] = []
const result = await installDsh({ source: harnessRepo, registry, onProgress: (p) => frames.push(p) })
expect(result.ok, JSON.stringify(result)).toBe(true)
// both are installed, each under its own id
expect(listInstalledDsh().map((d) => d.id).sort()).toEqual(['acme/thing', 'acme/viewer'])
expect(installedDsh('acme/viewer')?.manifest.kind).toBe('viewer')
// every frame the dialog sees carries the harness's id; the viewer's phases ride in the detail
expect(frames.every((f) => f.id === null || f.id === 'acme/thing'), JSON.stringify(frames)).toBe(true)
const viewerFrames = frames.filter((f) => f.detail?.startsWith('viewer acme/viewer'))
expect(viewerFrames.map((f) => f.phase)).toEqual(expect.arrayContaining(['clone', 'doctor', 'setup']))
expect(viewerFrames.some((f) => f.detail?.includes('installed'))).toBe(true)
// the harness's own done is the last word
expect(frames.at(-1)).toMatchObject({ id: 'acme/thing', phase: 'done' })
})
it('a used viewer that is already installed is not installed again', async () => {
const viewerRepo = gitRepo(join(root, 'src', 'viewer'), {
'harness.json': JSON.stringify({ spec: 1, kind: 'viewer', id: 'acme/viewer', name: 'Viewer', viewer: { command: './viewer.sh', url: 'http://127.0.0.1:${port}/' } }),
'viewer.sh': '#!/bin/sh\nsleep 1000\n',
})
const first = await installDsh({ source: viewerRepo })
expect(first.ok).toBe(true)
const harnessRepo = gitRepo(join(root, 'src', 'thing'), {
'harness.json': JSON.stringify({ spec: 1, id: 'acme/thing', name: 'Thing', engine: 'claude', viewer: { use: 'acme/viewer' } }),
})
let asked = 0
const result = await installDsh({ source: harnessRepo, registry: () => { asked++; return undefined } })
expect(result.ok).toBe(true)
expect(asked).toBe(0)
})
it('can install a whole repository at the published commit after its branch advances', async () => {
const repo = gitRepo(join(root, 'pinned'), { 'harness.json': JSON.stringify({ spec: 1, id: 'acme/pinned', name: 'Published', engine: 'claude' }) })
const ref = execFileSync('git', ['-C', repo, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim()
writeFileSync(join(repo, 'harness.json'), JSON.stringify({ spec: 1, id: 'acme/pinned', name: 'Unpublished', engine: 'claude' }))
execFileSync('git', ['-C', repo, '-c', 'user.name=test', '-c', 'user.email=test@example.test', 'commit', '-qam', 'later version'])
const result = await installDsh({ source: repo, ref, expectedId: 'acme/pinned' })
expect(result.ok, JSON.stringify(result)).toBe(true)
expect(installedDsh('acme/pinned')?.commit).toBe(ref)
expect(installedDsh('acme/pinned')?.manifest.name).toBe('Published')
})
it('rejects a mismatched catalog identity before installing or running setup', async () => {
const repo = gitRepo(join(root, 'wrong-id'), {
'harness.json': JSON.stringify({ spec: 1, id: 'acme/surprise', name: 'Wrong', engine: 'claude', toolchain: { setup: './setup.sh' } }),
'setup.sh': '#!/bin/sh\ntouch did-run\n',
})
expect(await installDsh({ source: repo, expectedId: 'acme/expected' })).toMatchObject({ ok: false, error: 'PACKAGE_ID_MISMATCH' })
expect(listInstalledDsh()).toEqual([])
expect(readdirSync(env.DSH_DIR).filter(name => name.startsWith('.tmp-'))).toEqual([])
})
it('a used viewer that is neither installed nor in the registry is said, and the harness still installs', async () => {
const harnessRepo = gitRepo(join(root, 'src', 'thing'), {
'harness.json': JSON.stringify({ spec: 1, id: 'acme/thing', name: 'Thing', engine: 'claude', viewer: { use: 'acme/missing' } }),
})
const lines: string[] = []
const result = await installDsh({ source: harnessRepo, registry: () => undefined, onLine: (l) => lines.push(l) })
expect(result.ok).toBe(true)
expect(lines.join('\n')).toContain('viewer acme/missing is not installed and not in the registry')
})
})
describe('installDsh from one folder of a repo (the built-in shelf)', () => {
let root: string
let savedDshDir: string
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'dsh-install-path-'))
savedDshDir = env.DSH_DIR
env.DSH_DIR = join(root, 'installed')
invalidateInstalledDsh()
})
afterEach(() => {
env.DSH_DIR = savedDshDir
invalidateInstalledDsh()
rmSync(root, { recursive: true, force: true })
})
// A monorepo in miniature: an app beside the store, two packages that use each other.
const monorepo = (): string => gitRepo(join(root, 'src', 'mono'), {
'README.md': '# the monorepo\n',
'cli/src/index.ts': 'export {}\n',
'store/agents/thing/harness.json': JSON.stringify({ spec: 1, id: 'acme/thing', name: 'Thing', engine: 'claude', agent: { instructions: 'AGENTS.md', skills: ['skills'] }, toolchain: { setup: './setup.sh' }, viewer: { use: 'acme/viewer' } }),
'store/agents/thing/AGENTS.md': '# Thing\n',
'store/agents/thing/skills/draw/SKILL.md': '---\nname: draw\n---\n',
'store/agents/thing/setup.sh': '#!/bin/sh\ntouch set-up\n',
'store/viewers/viewer/harness.json': JSON.stringify({ spec: 1, kind: 'viewer', id: 'acme/viewer', name: 'Viewer', viewer: { command: './viewer.sh', url: 'http://127.0.0.1:${port}/' } }),
'store/viewers/viewer/viewer.sh': '#!/bin/sh\nsleep 1000\n',
})
it('installs only that folder, laid out like a whole-repo install, and records where it came from', async () => {
const repo = monorepo()
const registry = (id: string): DshRegistryEntry | undefined => (id === 'acme/viewer'
? { id, kind: 'viewer', name: 'Viewer', repo, ref: 'main', path: 'store/viewers/viewer' }
: undefined)
const frames: DshInstallProgress[] = []
const result = await installDsh({ source: repo, ref: 'main', path: 'store/agents/thing', registry, onProgress: (p) => frames.push(p) })
expect(result.ok, JSON.stringify(result)).toBe(true)
if (!result.ok) return
// the manifest sits at the install's root; nothing else of the monorepo came along, not even .git
const dir = result.installed.realDir
expect(readdirSync(dir).sort()).toEqual(['AGENTS.md', 'harness.json', 'set-up', 'setup.sh', 'skills'])
expect(existsSync(join(dir, 'skills', 'draw', 'SKILL.md'))).toBe(true)
// setup ran in the folder, and the script kept its executable bit through the sparse clone
expect(existsSync(join(dir, 'set-up'))).toBe(true)
// the viewer it uses came from ITS folder of the same repo
expect(installedDsh('acme/viewer')?.manifest.kind).toBe('viewer')
expect(readdirSync(installedDsh('acme/viewer')!.realDir).sort()).toEqual(['harness.json', 'viewer.sh'])
const row = readInstalledIndex().find((r) => r.id === 'acme/thing')
expect(row).toMatchObject({ source: repo, ref: 'main', path: 'store/agents/thing', linked: false })
expect(row?.commit).toMatch(/^[0-9a-f]{40}$/)
expect(frames[0]?.detail).toContain('store/agents/thing')
// no temporary clone is left in the install root
expect(readdirSync(env.DSH_DIR).filter((name) => name.startsWith('.tmp-'))).toEqual([])
})
it('a folder the repo does not have fails cleanly and leaves nothing behind', async () => {
const repo = monorepo()
const result = await installDsh({ source: repo, path: 'store/agents/nope' })
expect(result).toMatchObject({ ok: false, error: 'CLONE_FAILED' })
if (!result.ok) expect(result.detail).toContain('has no folder store/agents/nope')
expect(listInstalledDsh()).toEqual([])
expect(readdirSync(env.DSH_DIR).filter((name) => name.startsWith('.tmp-'))).toEqual([])
})
it('refuses a path that climbs out of the repo before cloning anything', async () => {
for (const path of ['../elsewhere', '/abs', 'store/../..', 'store/agents/thing/']) {
const result = await installDsh({ source: join(root, 'never-cloned'), path })
expect(result, path).toMatchObject({ ok: false, error: 'INVALID_SOURCE' })
}
})
})
describe('installDsh, every way it can go', () => {
let root: string
let savedDshDir: string
let savedPath: string | undefined
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'dsh-install-each-'))
savedDshDir = env.DSH_DIR
savedPath = process.env.PATH
env.DSH_DIR = join(root, 'installed')
invalidateInstalledDsh()
})
afterEach(() => {
vi.useRealTimers()
process.env.PATH = savedPath
env.DSH_DIR = savedDshDir
invalidateInstalledDsh()
rmSync(root, { recursive: true, force: true })
})
const REAL_GIT = execFileSync('/bin/sh', ['-c', 'command -v git'], { encoding: 'utf8' }).trim()
/** Put a `git` first on PATH that runs `script` (sh), which may hand anything on to the real git. */
const stubGit = (script: string): void => {
const bin = join(root, 'bin')
mkdirSync(bin, { recursive: true })
writeFileSync(join(bin, 'git'), `#!/bin/sh\nREAL_GIT='${REAL_GIT}'\n${script}\n`, { mode: 0o755 })
process.env.PATH = `${bin}:${savedPath}`
}
const leftovers = (): string[] => (existsSync(env.DSH_DIR) ? readdirSync(env.DSH_DIR).filter((name) => name.startsWith('.tmp-')) : [])
const thing = (extra: Record<string, unknown> = {}, files: Record<string, string> = {}): string => gitRepo(join(root, 'src', 'thing'), {
'harness.json': JSON.stringify({ spec: 1, id: 'acme/thing', name: 'Thing', engine: 'claude', ...extra }),
...files,
})
describe('resolveInstallSource', () => {
it('a registry id is its repo, ref and folder; a URL or path is itself; an empty or control-character source is nothing', () => {
const entries: Record<string, DshRegistryEntry> = {
'acme/whole': { id: 'acme/whole', name: 'Whole', repo: 'https://example.com/whole.git', ref: 'v1', engine: 'claude' },
'acme/folder': { id: 'acme/folder', name: 'Folder', repo: HARNESS_MONOREPO, ref: 'main', path: 'store/agents/folder', engine: 'claude' },
}
const registry = (id: string): DshRegistryEntry | undefined => entries[id]
expect(resolveInstallSource('acme/whole', registry)).toEqual({ source: 'https://example.com/whole.git', ref: 'v1', id: 'acme/whole' })
expect(resolveInstallSource('acme/folder', registry)).toEqual({ source: HARNESS_MONOREPO, ref: 'main', path: 'store/agents/folder', id: 'acme/folder' })
expect(resolveInstallSource('/Users/example/code/thing', registry)).toEqual({ source: '/Users/example/code/thing' })
expect(resolveInstallSource('', registry)).toBeNull()
expect(resolveInstallSource('https://example.com/x', registry)).toBeNull()
})
})
describe('--link', () => {
it('links a checkout in place, records it as linked with its commit, and a second link replaces the first', async () => {
const repo = thing()
const frames: DshInstallProgress[] = []
const result = await installDsh({ source: repo, link: true, onProgress: (p) => frames.push(p) })
expect(result.ok, JSON.stringify(result)).toBe(true)
expect(frames[0]).toEqual({ id: null, phase: 'clone', detail: `linking ${repo}` })
const dir = dshInstallDir('acme/thing')
expect(lstatSync(dir).isSymbolicLink()).toBe(true)
const row = readInstalledIndex()[0]
expect(row).toMatchObject({ id: 'acme/thing', dir, source: repo, ref: null, linked: true })
expect(row).not.toHaveProperty('path')
expect(row.commit).toMatch(/^[0-9a-f]{40}$/)
expect(result.ok && result.installed.realDir).toBe(realpathSync(repo))
const again = await installDsh({ source: repo, link: true, path: 'ignored/for/a/link' })
expect(again.ok).toBe(true)
expect(readInstalledIndex()).toHaveLength(1)
})
it('a folder that is not a git checkout links with no commit', async () => {
const plain = join(root, 'plain')
mkdirSync(plain)
writeFileSync(join(plain, 'harness.json'), JSON.stringify({ spec: 1, id: 'acme/plain', name: 'Plain', engine: 'codex' }))
const result = await installDsh({ source: plain, link: true })
expect(result.ok).toBe(true)
expect(readInstalledIndex()[0]?.commit).toBeNull()
})
it('a git that answers rev-parse with nothing is no commit, not an empty one', async () => {
const repo = thing()
stubGit('if [ "$3" = "rev-parse" ]; then exit 0; fi\nexec "$REAL_GIT" "$@"')
expect((await installDsh({ source: repo, link: true })).ok).toBe(true)
expect(readInstalledIndex()[0]?.commit).toBeNull()
})
it('refuses a relative path, a path that does not exist, and a folder with no manifest, before touching anything', async () => {
const frames: DshInstallProgress[] = []
expect(await installDsh({ source: 'relative/thing', link: true, onProgress: (p) => frames.push(p) }))
.toEqual({ ok: false, error: 'INVALID_SOURCE', detail: '--link needs an absolute path to a checkout' })
expect(frames.at(-1)).toEqual({ id: null, phase: 'failed', detail: '--link needs an absolute path to a checkout', error: 'INVALID_SOURCE' })
expect(await installDsh({ source: join(root, 'nope'), link: true })).toEqual({ ok: false, error: 'SOURCE_NOT_FOUND', detail: `${join(root, 'nope')} does not exist` })
mkdirSync(join(root, 'empty'))
expect(await installDsh({ source: join(root, 'empty'), link: true })).toMatchObject({ ok: false, error: 'INVALID_MANIFEST' })
expect(existsSync(env.DSH_DIR)).toBe(false)
})
it('refuses to link the installed copy onto itself, which would delete it first', async () => {
// Found by this spec: placing the link clears the install directory, so linking a clone that is
// already installed (or a folder inside it) deleted the only copy and left a link to nothing.
const repo = thing({}, { 'AGENTS.md': '# mine\n' })
expect((await installDsh({ source: repo })).ok).toBe(true)
const installed = dshInstallDir('acme/thing')
writeFileSync(join(installed, 'notes.md'), 'work in progress\n')
expect(await installDsh({ source: installed, link: true })).toMatchObject({ ok: false, error: 'INVALID_SOURCE' })
mkdirSync(join(installed, 'nested'))
writeFileSync(join(installed, 'nested', 'harness.json'), readFileSync(join(installed, 'harness.json')))
expect(await installDsh({ source: join(installed, 'nested'), link: true })).toMatchObject({ ok: false, error: 'INVALID_SOURCE' })
expect(lstatSync(installed).isDirectory()).toBe(true)
expect(readFileSync(join(installed, 'notes.md'), 'utf8')).toBe('work in progress\n')
expect(readInstalledIndex()[0]).toMatchObject({ id: 'acme/thing', linked: false })
})
it('linking a checkout elsewhere replaces an installed clone of the same id', async () => {
const repo = thing()
expect((await installDsh({ source: repo })).ok).toBe(true)
expect(lstatSync(dshInstallDir('acme/thing')).isDirectory()).toBe(true)
expect((await installDsh({ source: repo, link: true })).ok).toBe(true)
expect(lstatSync(dshInstallDir('acme/thing')).isSymbolicLink()).toBe(true)
expect(readInstalledIndex()[0]).toMatchObject({ id: 'acme/thing', linked: true })
})
})
describe('cloning', () => {
it('clones a whole repo at a ref, replacing an earlier install of the same id', async () => {
const repo = thing()
execFileSync('git', ['-C', repo, 'branch', 'v2'])
expect((await installDsh({ source: repo })).ok).toBe(true)
writeFileSync(join(dshInstallDir('acme/thing'), 'stale.txt'), 'from the first install')
const result = await installDsh({ source: repo, ref: 'v2' })
expect(result.ok, JSON.stringify(result)).toBe(true)
expect(existsSync(join(dshInstallDir('acme/thing'), 'stale.txt'))).toBe(false)
expect(readInstalledIndex()[0]).toMatchObject({ ref: 'v2', linked: false })
expect(leftovers()).toEqual([])
})
it('a repo that cannot be cloned, or has no manifest, fails with what git or the manifest said', async () => {
const missing = await installDsh({ source: join(root, 'no-such-repo') })
expect(missing).toMatchObject({ ok: false, error: 'CLONE_FAILED' })
expect(!missing.ok && missing.detail).toMatch(/^git clone exited 128: .*no-such-repo/)
const bare = gitRepo(join(root, 'src', 'bare'), { 'README.md': '# no manifest\n' })
const noManifest = await installDsh({ source: bare })
expect(noManifest).toMatchObject({ ok: false, error: 'INVALID_MANIFEST' })
expect(!noManifest.ok && noManifest.detail).toMatch(/^no harness\.json in /)
const sparse = await installDsh({ source: join(root, 'no-such-repo'), ref: 'main', path: 'store/agents/x' })
expect(sparse).toMatchObject({ ok: false, error: 'CLONE_FAILED' })
expect(leftovers()).toEqual([])
})
it('a folder the repo lacks at a ref is said with the ref; a path that is a file fails at the sparse checkout', async () => {
const repo = gitRepo(join(root, 'src', 'mono'), { 'store/agents/file': 'not a folder\n' })
expect(await installDsh({ source: repo, ref: 'main', path: 'store/agents/nope' }))
.toEqual({ ok: false, error: 'CLONE_FAILED', detail: `${repo} at main has no folder store/agents/nope` })
const file = await installDsh({ source: repo, path: 'store/agents/file' })
expect(file).toMatchObject({ ok: false, error: 'CLONE_FAILED' })
expect(!file.ok && file.detail).toMatch(/^git sparse-checkout exited 128: .*not a directory/)
expect(leftovers()).toEqual([])
})
it('streams git\'s lines as they land, and reports the last three that are not progress', async () => {
stubGit([
`printf 'Cloning into x...\\n\\n' >&2`,
`i=0; while [ $i -lt 25 ]; do printf 'remote: Counting objects: %s\\r' $i >&2; i=$((i+1)); done`,
`printf '\\nReceiving objects: 100%%\\r\\n' >&2`,
`printf 'fatal: first\\nfatal: second\\nfatal: third\\nfatal: no newline' >&2`,
'exit 128',
].join('\n'))
const lines: string[] = []
const result = await installDsh({ source: 'https://example.com/thing.git', onLine: (line) => lines.push(line) })
expect(result).toEqual({ ok: false, error: 'CLONE_FAILED', detail: 'git clone exited 128: fatal: second · fatal: third · fatal: no newline' })
expect(lines[0]).toBe('Cloning into x...')
expect(lines.filter((line) => line.startsWith('remote: Counting objects'))).toHaveLength(25)
expect(lines.slice(-4)).toEqual(['fatal: first', 'fatal: second', 'fatal: third', 'fatal: no newline'])
expect(lines).not.toContain('')
})
it('when git said nothing but progress, the last progress line is the detail', async () => {
stubGit(`printf 'Receiving objects: 10%%\\rReceiving objects: 20%%\\r' >&2\nexit 1`)
expect(await installDsh({ source: 'https://example.com/thing.git' })).toEqual({ ok: false, error: 'CLONE_FAILED', detail: 'git clone exited 1: Receiving objects: 20%' })
})
it('no git at all is a failed clone that says so', async () => {
const empty = join(root, 'empty-bin')
mkdirSync(empty)
process.env.PATH = empty
const result = await installDsh({ source: 'https://example.com/thing.git' })
expect(result).toMatchObject({ ok: false, error: 'CLONE_FAILED' })
expect(!result.ok && result.detail).toContain('ENOENT')
})
it('a clone still running after ten minutes is stopped, and said to have been', async () => {
stubGit('echo "Cloning into x..." >&2\nexec sleep 30')
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
let said!: () => void
const saidSomething = new Promise<void>((resolve) => { said = resolve })
const pending = installDsh({ source: 'https://example.com/thing.git', onLine: () => said() })
await saidSomething
vi.advanceTimersByTime(10 * 60_000)
vi.useRealTimers()
expect(await pending).toEqual({ ok: false, error: 'CLONE_FAILED', detail: 'git clone was still running after 10 min: Cloning into x...' })
})
it('a fetch that failed the way a bad connection fails is tried again, from nothing, and the retry is narrated', async () => {
const repo = thing()
const marker = join(root, 'attempts')
// The first clone stalls out the way curl reports it; every later call is the real git.
stubGit(`if [ "$1" = clone ] && [ ! -e '${marker}' ]; then touch '${marker}'; printf 'Cloning into x...\\nerror: RPC failed; curl 28 Operation too slow. Less than 1024 bytes/sec transferred the last 60 seconds\\nfatal: early EOF\\n' >&2; exit 128; fi\nexec "$REAL_GIT" "$@"`)
const lines: string[] = []
const frames: DshInstallProgress[] = []
const result = await installDsh({ source: repo, cloneRetryDelaysMs: [0, 0], onLine: (line) => lines.push(line), onProgress: (p) => frames.push(p) })
expect(result.ok, JSON.stringify(result)).toBe(true)
expect(lines.find((line) => line.startsWith('fetch failed'))).toBe('fetch failed · git clone exited 128: Cloning into x... · error: RPC failed; curl 28 Operation too slow. Less than 1024 bytes/sec transferred the last 60 seconds · fatal: early EOF · retrying (2/3)')
expect(frames.map((f) => f.phase)).toEqual(['clone', 'doctor', 'done'])
expect(leftovers()).toEqual([])
})
it('a connection that never recovers is given up after three attempts, and said to have been', async () => {
stubGit(`printf 'fatal: unable to access https://example.com/thing.git/: Could not resolve host: example.com\\n' >&2\nexit 128`)
const lines: string[] = []
const frames: DshInstallProgress[] = []
const result = await installDsh({ source: 'https://example.com/thing.git', cloneRetryDelaysMs: [0, 0], onLine: (line) => lines.push(line), onProgress: (p) => frames.push(p) })
expect(result).toEqual({ ok: false, error: 'CLONE_FAILED', detail: 'git clone exited 128: fatal: unable to access https://example.com/thing.git/: Could not resolve host: example.com · gave up after 3 attempts' })
expect(lines.filter((line) => line.startsWith('fetch failed')).map((line) => line.slice(-'retrying (2/3)'.length))).toEqual(['retrying (2/3)', 'retrying (3/3)'])
expect(frames.at(-1)).toMatchObject({ phase: 'failed', error: 'CLONE_FAILED', detail: expect.stringContaining('gave up after 3 attempts') })
expect(leftovers()).toEqual([])
})
it('a repository that is not there is not tried again', async () => {
const marker = join(root, 'attempts')
stubGit(`echo x >> '${marker}'\nprintf 'remote: Repository not found.\\nfatal: repository https://example.com/thing.git/ not found\\n' >&2\nexit 128`)
const lines: string[] = []
const result = await installDsh({ source: 'https://example.com/thing.git', cloneRetryDelaysMs: [0, 0], onLine: (line) => lines.push(line) })
expect(result).toEqual({ ok: false, error: 'CLONE_FAILED', detail: 'git clone exited 128: fatal: repository https://example.com/thing.git/ not found' })
expect(readFileSync(marker, 'utf8')).toBe('x\n')
expect(lines.some((line) => line.startsWith('fetch failed'))).toBe(false)
})
it('git is run with a stall limit and no credential prompt, on top of the daemon\'s environment', async () => {
stubGit(`printf 'env %s %s %s %s\\n' "$GIT_HTTP_LOW_SPEED_LIMIT" "$GIT_HTTP_LOW_SPEED_TIME" "$GIT_TERMINAL_PROMPT" "$DSH_SPEC_MARK" >&2\nexit 1`)
process.env.DSH_SPEC_MARK = 'still-here'
try {
const lines: string[] = []
await installDsh({ source: 'https://example.com/thing.git', onLine: (line) => lines.push(line) })
expect(lines).toContain('env 1024 60 0 still-here')
} finally {
delete process.env.DSH_SPEC_MARK
}
})
it('isTransientGitFailure: the network is, the repository and our own stop are not', () => {
for (const said of [
'git clone exited 128: error: RPC failed; curl 28 Operation too slow',
'git clone exited 128: fatal: early EOF',
'git fetch exited 128: fatal: the remote end hung up unexpectedly',
'git clone exited 128: fatal: unable to access https://x/: Could not resolve host: x',
'git clone exited 128: fatal: unable to access https://x/: Failed to connect to x port 443: Connection refused',
'git clone exited 128: error: The requested URL returned error: 502',
'git clone exited 128: fatal: unable to access https://x/: GnuTLS recv error (-110)',
'git clone exited 128: fatal: unable to access https://x/: OpenSSL SSL_read: Connection reset by peer, errno 104',
]) expect(isTransientGitFailure(said), said).toBe(true)
for (const said of [
'git clone was still running after 10 min: Receiving objects: 40%',
'git clone exited 128: fatal: repository https://x/ not found',
'git clone exited 128: error: RPC failed; curl 22 The requested URL returned error: 404',
'git clone exited 128: error: RPC failed; HTTP 401 curl 22 The requested URL returned error: 401',
'git clone exited 128: fatal: Authentication failed for https://x/',
'git clone exited 128: git@x: Permission denied (publickey). · fatal: Could not read from remote repository.',
'git clone exited 128: fatal: could not read Username for https://x: terminal prompts disabled',
'git clone exited 128: fatal: Remote branch nope not found in upstream origin',
'https://x at main has no folder store/agents/nope',
'git clone exited 2',
]) expect(isTransientGitFailure(said), said).toBe(false)
})
it('a git that fails without a word is reported by how it ended alone, a code or a signal', async () => {
stubGit('exit 2')
expect(await installDsh({ source: 'https://example.com/thing.git' })).toEqual({ ok: false, error: 'CLONE_FAILED', detail: 'git clone exited 2' })
stubGit('kill -KILL $$')
expect(await installDsh({ source: 'https://example.com/thing.git' })).toEqual({ ok: false, error: 'CLONE_FAILED', detail: 'git clone exited SIGKILL' })
})
})
describe('setup, the used viewer, and the doctor', () => {
it('a bare script name is the harness\'s own script, as `harness dsh check` reads it, not a PATH lookup', async () => {
// Found by this spec: `"setup": "setup.sh"` (the shape spec/README.md shows for a viewer package)
// passed the check, which looks for the file, and then failed install with "command not found".
const repo = thing({ toolchain: { setup: 'setup.sh', doctor: 'doctor.sh' } }, {
'setup.sh': '#!/bin/sh\ntouch set-up\n',
'doctor.sh': '#!/bin/sh\necho "ok bare doctor ran"\n',
})
const result = await installDsh({ source: repo })
expect(result.ok, JSON.stringify(result)).toBe(true)
expect(result.ok && result.doctor.lines).toContain('ok bare doctor ran')
expect(existsSync(join(dshInstallDir('acme/thing'), 'set-up'))).toBe(true)
})
it('a setup that fails says how, with its last five lines', async () => {
const repo = thing({ toolchain: { setup: 'setup.sh' } }, { 'setup.sh': '#!/bin/sh\nfor l in l1 l2 l3 l4 l5 l6; do echo $l; done\nexit 2\n' })
const frames: DshInstallProgress[] = []
const result = await installDsh({ source: repo, onProgress: (p) => frames.push(p) })
expect(result).toMatchObject({ ok: false, error: 'SETUP_FAILED' })
expect(!result.ok && result.detail).toMatch(/^setup exited 2 · (.* · )?l2 · l3 · l4 · l5 · l6$/)
expect(frames.map((f) => f.phase)).toEqual(['clone', 'setup', 'failed'])
expect(readInstalledIndex()).toEqual([])
})
it('a setup killed by a signal, or past its timeout, says that instead', async () => {
const killed = await installDsh({ source: thing({ toolchain: { setup: 'kill -KILL $$' } }) })
expect(!killed.ok && killed.detail).toMatch(/^setup exited SIGKILL · /)
rmSync(join(root, 'src'), { recursive: true, force: true })
const slow = await installDsh({ source: thing({ toolchain: { setup: 'sleep 30' } }), setupTimeoutMs: 300 })
expect(slow).toEqual({ ok: false, error: 'SETUP_FAILED', detail: 'setup timed out' })
})
it('a used viewer that fails to install fails the harness, said once and under the harness', async () => {
const repo = thing({ viewer: { use: 'acme/viewer' } })
const registry = (id: string): DshRegistryEntry | undefined => (id === 'acme/viewer' ? { id, kind: 'viewer', name: 'Viewer', repo: join(root, 'no-viewer-repo') } : undefined)
const frames: DshInstallProgress[] = []
const lines: string[] = []
const result = await installDsh({ source: repo, registry, onProgress: (p) => frames.push(p), onLine: (line) => lines.push(line) })
expect(result).toMatchObject({ ok: false, error: 'CLONE_FAILED' })
expect(!result.ok && result.detail).toMatch(/^viewer acme\/viewer · git clone exited 128: /)
expect(frames.filter((f) => f.phase === 'failed')).toEqual([{ id: 'acme/thing', phase: 'failed', detail: !result.ok && result.detail, error: 'CLONE_FAILED' }])
expect(frames.every((f) => f.id === null || f.id === 'acme/thing')).toBe(true)
expect(lines).toContain('viewer acme/viewer · installing')
expect(readInstalledIndex()).toEqual([])
})
it('a used viewer the bundled registry does not know is said, and the harness still installs', async () => {
const lines: string[] = []
const result = await installDsh({ source: thing({ viewer: { use: 'acme/nowhere-to-be-found' } }), onLine: (line) => lines.push(line) })
expect(result.ok).toBe(true)
expect(lines).toContain('miss viewer acme/nowhere-to-be-found is not installed and not in the registry · install it first')
})
it('a doctor that fails still records the install, and names what is missing', async () => {
const repo = thing({ toolchain: { doctor: 'doctor.sh' } }, { 'doctor.sh': '#!/bin/sh\necho "ok git"\necho "miss typst on PATH"\necho "miss fonts"\nexit 1\n' })
const frames: DshInstallProgress[] = []
const result = await installDsh({ source: repo, onProgress: (p) => frames.push(p) })
expect(result).toEqual({ ok: false, error: 'DOCTOR_FAILED', detail: 'doctor failed · miss typst on PATH · miss fonts' })
expect(frames.at(-1)).toEqual({ id: 'acme/thing', phase: 'failed', detail: 'doctor failed · miss typst on PATH · miss fonts', error: 'DOCTOR_FAILED' })
expect(readInstalledIndex().map((row) => row.id)).toEqual(['acme/thing'])
})
it('a doctor that fails without a miss line is reported by its last three lines', async () => {
const repo = thing({ toolchain: { doctor: 'doctor.sh' } }, { 'doctor.sh': '#!/bin/sh\nfor l in a b c d; do echo "$l"; done\nexit 3\n' })
const result = await installDsh({ source: repo })
expect(!result.ok && result.detail).toMatch(/^doctor failed · .*b · c · d$/)
})
it('runDshDoctor: no doctor is ready; one still running after five minutes is stopped and said to be', async () => {
const base = { id: 'acme/thing', dir: root, realDir: root, source: root, ref: null, commit: null, linked: true, installedAt: 0 }
const none: InstalledDsh = { ...base, manifest: { spec: 1, id: 'acme/thing', name: 'Thing', engine: 'claude' } }
expect(await runDshDoctor(none)).toEqual({ ok: true, lines: [] })
const slow: InstalledDsh = { ...base, manifest: { ...none.manifest, toolchain: { doctor: 'echo "ok started"; sleep 30' } } }
const said = 'miss doctor still running after 5 min — stopped; run `harness dsh doctor acme/thing` again'
const seen: string[] = []
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
const pending = runDshDoctor(slow, (line) => seen.push(line))
vi.advanceTimersByTime(DOCTOR_TIMEOUT_MS)
vi.useRealTimers()
const result = await pending
expect(result.ok).toBe(false)
expect(result.lines.at(-1)).toBe(said)
expect(seen.at(-1)).toBe(said)
// and with no one listening for lines
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
const quiet = runDshDoctor(slow)
vi.advanceTimersByTime(DOCTOR_TIMEOUT_MS)
vi.useRealTimers()
expect((await quiet).lines.at(-1)).toBe(said)
})
})
describe('removeDsh', () => {
it('removes a clone and its row; a link, never the checkout it points at; a row whose folder is already gone', async () => {
expect(removeDsh('acme/thing')).toEqual({ ok: false, error: 'NOT_INSTALLED', detail: 'acme/thing is not installed' })
const repo = thing()
await installDsh({ source: repo })
expect(removeDsh('acme/thing')).toEqual({ ok: true })
expect(existsSync(dshInstallDir('acme/thing'))).toBe(false)
expect(readInstalledIndex()).toEqual([])
await installDsh({ source: repo, link: true })
expect(removeDsh('acme/thing')).toEqual({ ok: true })
expect(existsSync(dshInstallDir('acme/thing'))).toBe(false)
expect(existsSync(join(repo, 'harness.json'))).toBe(true)
await installDsh({ source: repo })
rmSync(dshInstallDir('acme/thing'), { recursive: true })
expect(removeDsh('acme/thing')).toEqual({ ok: true })
expect(readInstalledIndex()).toEqual([])
})
it('a folder that cannot be removed is a failure that keeps the row', async () => {
await installDsh({ source: thing() })
const owner = join(env.DSH_DIR, 'acme')
chmodSync(owner, 0o500)
try {
const result = removeDsh('acme/thing')
expect(result).toMatchObject({ ok: false, error: 'REMOVE_FAILED' })
expect(!result.ok && result.detail).toContain('EACCES')
expect(readInstalledIndex().map((row) => row.id)).toEqual(['acme/thing'])
} finally {
chmodSync(owner, 0o700)
}
})
})
})