claude-harness/dsh/tests/install.test.mjs

720 lines
28 KiB
JavaScript
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 assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import { cleanup, git, tempDir, write } from './helpers.mjs'
const here = dirname(fileURLToPath(import.meta.url))
const script = join(here, '..', 'install.sh')
const testDir = dirname(fileURLToPath(import.meta.url))
const fixture = resolve(testDir, 'fixtures', 'superpowers-min')
test('install.sh copies skills and roles instead of linking into ~/.claude', () => {
const home = tempDir()
try {
const dshHome = join(home, '.dsh')
const cache = join(
home,
'.claude',
'plugins',
'cache',
'claude-plugins-official',
'superpowers',
'7.0.0',
'skills',
)
write(join(cache, 'using-superpowers', 'SKILL.md'), '---\nname: using-superpowers\n---\nbody\n')
write(join(cache, 'brainstorming', 'SKILL.md'), '---\nname: brainstorming\n---\nbody\n')
const env = { ...process.env, HOME: home, DSH_HOME: dshHome, DSH_SUPERPOWERS_REPO: 'file:///nonexistent' }
delete env.DSH_SUPERPOWERS_SOURCE
delete env.DSH_SUPERPOWERS_REF
const run = () => spawnSync('bash', [script, '--skills', '--roles'], { env, encoding: 'utf8' })
const first = run()
assert.equal(first.status, 0, first.stderr)
assert.match(first.stdout, /copied 2 superpowers skills/)
assert.match(first.stdout, /copied 2 local skills/)
assert.match(first.stdout, /installed 4 roles/)
const skill = join(dshHome, 'skills', 'using-superpowers')
assert.equal(lstatSync(skill).isSymbolicLink(), false)
assert.equal(lstatSync(skill).isDirectory(), true)
assert.match(readFileSync(join(skill, 'SKILL.md'), 'utf8'), /body/)
const roleSkill = join(dshHome, 'skills', 'role', 'SKILL.md')
assert.ok(existsSync(roleSkill), 'role skill was not installed')
assert.match(readFileSync(roleSkill, 'utf8'), /name: role/)
const sddSkill = join(dshHome, 'skills', 'sdd', 'SKILL.md')
assert.ok(existsSync(sddSkill), 'sdd skill was not installed')
const sddText = readFileSync(sddSkill, 'utf8')
assert.match(sddText, /name: sdd/)
assert.match(sddText, /\/approve/, 'the sdd skill must check the plan approval marker before dispatch')
assert.match(sddText, /approval\.md/, 'the sdd skill must name the approval marker file')
// T3.4: TDD order is part of the dispatch protocol: RED before the code.
assert.match(sddText, /падает \(RED\)/, 'the sdd skill must require a failing test (RED) before the code')
assert.match(sddText, /реализация без RED — `DONE_WITH_CONCERNS`/, 'a missing RED must degrade the report status')
for (const role of ['dev', 'admin', 'analyst', 'qa']) {
assert.ok(existsSync(join(dshHome, 'roles', `${role}.md`)), `missing ${role}.md`)
}
const devRole = join(dshHome, 'roles', 'dev.md')
const devText = readFileSync(devRole, 'utf8')
assert.match(devText, /В DSH-сессии исполнение плана идёт по skill `sdd`/)
assert.match(devText, /`maxDepth: 2`/)
assert.match(devText, /Бандл гейтит цикл/)
assert.match(devText, /\/approve/, 'the DSH note must point at /approve')
assert.match(devText, /approval-gates/, 'the DSH note must name the approval-gates deny prefix')
assert.match(first.stdout, /role dsh notes/)
assert.equal(countNotes(devText), 1, 'note must be inserted once')
const second = run()
assert.equal(second.status, 0, second.stderr)
assert.equal(lstatSync(skill).isSymbolicLink(), false)
assert.ok(existsSync(roleSkill))
assert.match(second.stdout, /role dsh notes/)
assert.equal(countNotes(readFileSync(devRole, 'utf8')), 1, 'note must not duplicate')
const check = spawnSync('bash', [script, '--check'], { env, encoding: 'utf8' })
assert.match(check.stdout, /ok: role dsh notes/)
} finally {
cleanup(home)
}
})
function countNotes(text) {
return (text.match(/В DSH-сессии исполнение плана/g) ?? []).length
}
function makeSuperpowers(root, body = 'body') {
for (const name of ['using-superpowers', 'brainstorming']) {
write(join(root, 'skills', name, 'SKILL.md'), `---\nname: ${name}\n---\n${body}\n`)
}
return root
}
function makeLegacyCache(home) {
const root = join(home, '.claude', 'plugins', 'cache', 'claude-plugins-official', 'superpowers', '7.0.0')
makeSuperpowers(root)
return join(root, 'skills')
}
/** A local git repo carrying the reduced upstream checkout on tag v7.0.1. */
function makePinnedRepo(work, marker = null, name = 'upstream-repo') {
const repo = join(work, name)
cpSync(fixture, repo, { recursive: true })
if (marker) {
const skill = join(repo, 'skills', 'using-superpowers', 'SKILL.md')
write(skill, `${readFileSync(skill, 'utf8')}\n<!-- pin-marker: ${marker} -->\n`)
}
try {
git(repo, ['init', '-b', 'main'])
} catch {
git(repo, ['init'])
git(repo, ['checkout', '-b', 'main'])
}
git(repo, ['config', 'user.email', 'dsh-harness@example.invalid'])
git(repo, ['config', 'user.name', 'DSH Harness Test'])
git(repo, ['add', '.'])
git(repo, ['commit', '-m', 'superpowers fixture'])
git(repo, ['tag', 'v7.0.1'])
return repo
}
/** A full 15-skill checkout copy (explicit sources sync it in place). */
function fixtureSuperpowers(work, marker = null, root = join(work, 'src', 'superpowers')) {
cpSync(fixture, root, { recursive: true })
if (marker) {
const skill = join(root, 'skills', 'using-superpowers', 'SKILL.md')
write(skill, `${readFileSync(skill, 'utf8')}\n<!-- install-source: ${marker} -->\n`)
}
return root
}
function walk(dir, acc = []) {
if (!existsSync(dir)) return acc
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name)
acc.push(full)
if (entry.isDirectory()) walk(full, acc)
}
return acc
}
function installerEnv(home) {
const env = { ...process.env, HOME: home, DSH_HOME: join(home, '.dsh') }
delete env.DSH_SUPERPOWERS_SOURCE
delete env.DSH_SUPERPOWERS_REPO
delete env.DSH_SUPERPOWERS_REF
return env
}
function runInstaller(home, args, extraEnv = {}) {
return spawnSync('bash', [script, ...args], {
env: { ...installerEnv(home), ...extraEnv },
encoding: 'utf8',
})
}
test('--skills clones the pinned ref, syncs, verifies and installs adapted skills', () => {
const work = tempDir()
try {
const home = join(work, 'home')
const repo = makePinnedRepo(work)
const run = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: `file://${repo}` })
const out = run.stdout + run.stderr
assert.equal(run.status, 0, out)
assert.match(run.stdout, /synced \d+ files/)
assert.match(run.stdout, /verified 15 skills/)
assert.match(run.stdout, /copied 15 superpowers skills/)
const skillFile = join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md')
const text = readFileSync(skillFile, 'utf8')
assert.match(text, /references\/dsh-tools\.md/)
assert.doesNotMatch(text, /superpowers:/)
assert.ok(
existsSync(join(home, '.dsh', 'skills', 'using-superpowers', 'references', 'dsh-tools.md')),
'the DSH tool mapping must be installed',
)
assert.ok(existsSync(join(home, '.dsh', 'skills', 'role', 'SKILL.md')), 'local skills still installed')
} finally {
cleanup(work)
}
})
test('--skills offline without a source fails with the clone hint', () => {
const home = tempDir()
try {
const run = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: 'file:///nonexistent' })
const out = run.stdout + run.stderr
assert.equal(run.status, 1, out)
assert.match(run.stderr, /git clone --depth 1 --branch v7\.0\.1 file:\/\/\/nonexistent/)
assert.match(out, /FAIL: no superpowers source/)
assert.equal(existsSync(join(home, '.dsh', 'skills', 'using-superpowers')), false)
} finally {
cleanup(home)
}
})
test('--skills re-run picks up a moved tag', () => {
const work = tempDir()
try {
const home = join(work, 'home')
const repo = makePinnedRepo(work)
const env = { DSH_SUPERPOWERS_REPO: `file://${repo}` }
const installed = join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md')
const first = runInstaller(home, ['--skills'], env)
assert.equal(first.status, 0, first.stdout + first.stderr)
assert.doesNotMatch(readFileSync(installed, 'utf8'), /move-marker: second/)
const upstreamSkill = join(repo, 'skills', 'using-superpowers', 'SKILL.md')
write(upstreamSkill, `${readFileSync(upstreamSkill, 'utf8')}\n<!-- move-marker: second -->\n`)
git(repo, ['add', 'skills/using-superpowers/SKILL.md'])
git(repo, ['commit', '--amend', '--no-edit'])
git(repo, ['tag', '-f', 'v7.0.1'])
const second = runInstaller(home, ['--skills'], env)
assert.equal(second.status, 0, second.stdout + second.stderr)
assert.match(readFileSync(installed, 'utf8'), /move-marker: second/)
} finally {
cleanup(work)
}
})
test('--skills updates an existing clone from DSH_SUPERPOWERS_REPO, not origin (T3.3)', () => {
const work = tempDir()
try {
const home = join(work, 'home')
const repoA = makePinnedRepo(work, 'from-a', 'upstream-a')
const repoB = makePinnedRepo(work, 'from-b', 'upstream-b')
const installed = join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md')
const first = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: `file://${repoA}` })
assert.equal(first.status, 0, first.stdout + first.stderr)
assert.match(readFileSync(installed, 'utf8'), /pin-marker: from-a/)
// Same ref, different repo: the update must come from the env repo, not
// from the clone's origin.
const second = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: `file://${repoB}` })
assert.equal(second.status, 0, second.stdout + second.stderr)
assert.match(readFileSync(installed, 'utf8'), /pin-marker: from-b/)
assert.doesNotMatch(readFileSync(installed, 'utf8'), /pin-marker: from-a/)
} finally {
cleanup(work)
}
})
test('after a repo switch, an offline run still reuses the clone on the ref (T3.5)', () => {
const work = tempDir()
try {
const home = join(work, 'home')
const repoA = makePinnedRepo(work, 'from-a', 'upstream-a')
const repoB = makePinnedRepo(work, 'from-b', 'upstream-b')
const installed = join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md')
const first = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: `file://${repoA}` })
assert.equal(first.status, 0, first.stdout + first.stderr)
// Online switch: the clone moves to repoB's v7.0.1 (T3.3 behavior).
const second = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: `file://${repoB}` })
assert.equal(second.status, 0, second.stdout + second.stderr)
assert.match(readFileSync(installed, 'utf8'), /pin-marker: from-b/)
// Offline afterwards: the local tag must have moved with the fetch, so
// the on-ref reuse succeeds with a WARN instead of a hard FAIL.
const third = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: 'file:///nonexistent' })
const out = third.stdout + third.stderr
assert.equal(third.status, 0, out)
assert.match(out, /offline; reusing the existing clone/)
assert.match(readFileSync(installed, 'utf8'), /pin-marker: from-b/)
} finally {
cleanup(work)
}
})
test('--skills removes a half-cloned dir after a failed fresh clone (RF5)', () => {
const home = tempDir()
try {
const cloneDir = join(home, '.dsh', 'superpowers')
mkdirSync(cloneDir, { recursive: true })
const run = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: 'file:///nonexistent' })
const out = run.stdout + run.stderr
assert.equal(run.status, 1, out)
assert.equal(existsSync(cloneDir), false, 'half-cloned dir must be removed')
} finally {
cleanup(home)
}
})
test('a foreign git repo in $DSH_HOME/superpowers is never touched (I-1)', () => {
const work = tempDir()
try {
const home = join(work, 'home')
const pinned = makePinnedRepo(work)
const foreign = join(home, '.dsh', 'superpowers')
mkdirSync(foreign, { recursive: true })
git(foreign, ['init', '-b', 'main'])
git(foreign, ['config', 'user.email', 'dsh-harness@example.invalid'])
git(foreign, ['config', 'user.name', 'DSH Harness Test'])
write(join(foreign, 'witness.txt'), 'do not touch\n')
git(foreign, ['add', 'witness.txt'])
git(foreign, ['commit', '-m', 'foreign repo'])
const headBefore = git(foreign, ['rev-parse', 'HEAD'])
const run = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: `file://${pinned}` })
const out = run.stdout + run.stderr
assert.equal(run.status, 1, out)
assert.match(out, /not a superpowers clone/)
assert.match(out, /move it away or use --superpowers-from/)
assert.equal(readFileSync(join(foreign, 'witness.txt'), 'utf8'), 'do not touch\n')
assert.equal(git(foreign, ['rev-parse', 'HEAD']), headBefore, 'HEAD must not move')
assert.equal(existsSync(join(foreign, 'skills')), false, 'no skills tree may appear')
// --check agrees: the foreign repo is not reported as a source.
const check = runInstaller(home, ['--check'], { DSH_SUPERPOWERS_REPO: `file://${pinned}` })
const checkOut = check.stdout + check.stderr
assert.equal(check.status, 0, checkOut)
assert.doesNotMatch(checkOut, /ok: superpowers source/)
} finally {
cleanup(work)
}
})
test('--skills reuses an on-ref clone when offline (WARN)', () => {
const work = tempDir()
try {
const home = join(work, 'home')
const repo = makePinnedRepo(work)
const first = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: `file://${repo}` })
assert.equal(first.status, 0, first.stdout + first.stderr)
// Go offline for real: the clone's origin must become unreachable, not
// just the DSH_SUPERPOWERS_REPO env (an existing clone fetches origin).
rmSync(repo, { recursive: true, force: true })
const second = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: 'file:///nonexistent' })
const out = second.stdout + second.stderr
assert.equal(second.status, 0, out)
assert.match(out, /offline; reusing the existing clone/)
assert.match(second.stdout, /copied 15 superpowers skills/)
} finally {
cleanup(work)
}
})
test('--skills leaves no staging garbage and records the ref in .sync-meta.json', () => {
const work = tempDir()
try {
const home = join(work, 'home')
const repo = makePinnedRepo(work)
const run = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: `file://${repo}` })
assert.equal(run.status, 0, run.stdout + run.stderr)
const cloneDir = join(home, '.dsh', 'superpowers')
const garbage = walk(cloneDir).filter((p) => p.includes('.tmp-'))
assert.deepEqual(garbage, [], `staging garbage left behind: ${garbage.join(', ')}`)
const meta = JSON.parse(readFileSync(join(cloneDir, 'skills.synced', '.sync-meta.json'), 'utf8'))
assert.equal(meta.ref, 'v7.0.1')
} finally {
cleanup(work)
}
})
test('explicit syncs stage outside the pinned clone; a later default run clones fresh (T3.1)', () => {
const work = tempDir()
try {
const home = join(work, 'home')
const repo = makePinnedRepo(work)
const src = fixtureSuperpowers(home, null, join(home, 'src', 'superpowers'))
const first = runInstaller(home, ['--skills', '--superpowers-from', src])
assert.equal(first.status, 0, first.stdout + first.stderr)
assert.ok(
existsSync(join(home, '.dsh', '.skills.synced', 'using-superpowers', 'SKILL.md')),
'explicit staging lives outside $DSH_HOME/superpowers',
)
assert.equal(
existsSync(join(home, '.dsh', 'superpowers')),
false,
'an explicit run must not create the pinned-clone dir',
)
const second = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: `file://${repo}` })
const out = second.stdout + second.stderr
assert.equal(second.status, 0, out)
assert.doesNotMatch(out, /no usable \.git/)
assert.match(second.stdout, /copied 15 superpowers skills/)
} finally {
cleanup(work)
}
})
test('a bare skills dir without a checkout root is invalid for --skills and --check alike (T3.2)', () => {
const home = tempDir()
try {
const bare = join(home, 'my-skills')
write(join(bare, 'using-superpowers', 'SKILL.md'), '---\nname: using-superpowers\n---\nbody\n')
const skills = runInstaller(home, ['--skills', '--superpowers-from', bare])
const check = runInstaller(home, ['--check'], { DSH_SUPERPOWERS_SOURCE: bare })
for (const run of [skills, check]) {
const out = run.stdout + run.stderr
assert.equal(run.status, 1, out)
assert.match(out, /checkout root or its skills\/ directory/)
}
assert.equal(existsSync(join(home, '.dsh', 'skills', 'using-superpowers')), false)
} finally {
cleanup(home)
}
})
test('--check is offline and reports the configured ref', () => {
const home = tempDir()
try {
const run = runInstaller(home, ['--check'], { DSH_SUPERPOWERS_REPO: 'file:///nonexistent' })
const out = run.stdout + run.stderr
assert.equal(run.status, 0, out)
assert.match(run.stdout, /ref v7\.0\.1/)
assert.doesNotMatch(out, /fatal:/, '--check must not attempt to clone')
} finally {
cleanup(home)
}
})
test('.claude/code-paths lists dsh/', () => {
const codePaths = resolve(testDir, '..', '..', '.claude', 'code-paths')
const text = readFileSync(codePaths, 'utf8')
assert.ok(text.split('\n').includes('dsh/'), 'code-paths must gate dsh/ under require-plan (D-5)')
})
test('--skills syncs an explicit --superpowers-from source in a clean HOME', () => {
const home = tempDir()
try {
const src = fixtureSuperpowers(home, null, join(home, 'src', 'superpowers'))
const run = runInstaller(home, ['--skills', '--superpowers-from', src])
const out = run.stdout + run.stderr
assert.equal(run.status, 0, out)
assert.match(run.stdout, /synced \d+ files/)
assert.match(run.stdout, /verified 15 skills/)
assert.match(run.stdout, /copied 15 superpowers skills/)
assert.ok(run.stdout.includes(`source: ${src}`), out)
assert.ok(existsSync(join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md')))
assert.ok(existsSync(join(home, '.dsh', 'skills', 'role', 'SKILL.md')))
assert.ok(existsSync(join(home, '.dsh', 'skills', 'sdd', 'SKILL.md')))
assert.equal(existsSync(join(home, '.claude')), false, 'clean HOME must stay clean')
assert.doesNotMatch(out, /legacy/)
} finally {
cleanup(home)
}
})
test('--superpowers-from accepts the skills directory itself', () => {
const home = tempDir()
try {
const src = fixtureSuperpowers(home, null, join(home, 'src', 'superpowers'))
const run = runInstaller(home, ['--skills', '--superpowers-from', join(src, 'skills')])
assert.equal(run.status, 0, run.stdout + run.stderr)
assert.match(run.stdout, /copied 15 superpowers skills/)
} finally {
cleanup(home)
}
})
test('--superpowers-from accepts a path with spaces', () => {
const home = tempDir()
try {
const src = fixtureSuperpowers(home, null, join(home, 'super powers'))
const run = runInstaller(home, ['--skills', '--superpowers-from', src])
assert.equal(run.status, 0, run.stdout + run.stderr)
assert.ok(existsSync(join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md')))
} finally {
cleanup(home)
}
})
test('--superpowers-from wins over DSH_SUPERPOWERS_SOURCE', () => {
const home = tempDir()
try {
const fromFlag = fixtureSuperpowers(home, 'from-flag', join(home, 'flag-source'))
const fromEnv = fixtureSuperpowers(home, 'from-env', join(home, 'env-source'))
const run = runInstaller(home, ['--skills', '--superpowers-from', fromFlag], {
DSH_SUPERPOWERS_SOURCE: fromEnv,
})
assert.equal(run.status, 0, run.stdout + run.stderr)
assert.match(
readFileSync(join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md'), 'utf8'),
/install-source: from-flag/,
)
} finally {
cleanup(home)
}
})
test('DSH_SUPERPOWERS_SOURCE is used when the flag is absent', () => {
const home = tempDir()
try {
const fromEnv = fixtureSuperpowers(home, 'from-env', join(home, 'env-source'))
const run = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_SOURCE: fromEnv })
assert.equal(run.status, 0, run.stdout + run.stderr)
assert.match(
readFileSync(join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md'), 'utf8'),
/install-source: from-env/,
)
} finally {
cleanup(home)
}
})
test('--skills fails when there is no source and nothing installed', () => {
const home = tempDir()
try {
const run = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: 'file:///nonexistent' })
const out = run.stdout + run.stderr
assert.equal(run.status, 1, out)
assert.match(out, /FAIL: no superpowers source/)
assert.match(out, /git clone --depth 1 --branch v7\.0\.1 file:\/\/\/nonexistent/)
assert.ok(existsSync(join(home, '.dsh', 'skills', 'role', 'SKILL.md')), 'local skills still installed')
assert.ok(existsSync(join(home, '.dsh', 'skills', 'sdd', 'SKILL.md')), 'local skills still installed')
assert.equal(existsSync(join(home, '.dsh', 'skills', 'using-superpowers')), false)
} finally {
cleanup(home)
}
})
test('--skills offline without a source fails but keeps the installed copies', () => {
const home = tempDir()
try {
const installed = join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md')
write(installed, '---\nname: using-superpowers\n---\ninstalled\n')
const run = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: 'file:///nonexistent' })
const out = run.stdout + run.stderr
assert.equal(run.status, 1, out)
assert.match(out, /FAIL: no superpowers source/)
assert.match(readFileSync(installed, 'utf8'), /installed/)
} finally {
cleanup(home)
}
})
test('empty DSH_SUPERPOWERS_SOURCE counts as unset', () => {
const home = tempDir()
try {
const run = runInstaller(home, ['--skills'], {
DSH_SUPERPOWERS_SOURCE: '',
DSH_SUPERPOWERS_REPO: 'file:///nonexistent',
})
const out = run.stdout + run.stderr
assert.equal(run.status, 1, out)
assert.match(out, /FAIL: no superpowers source/)
assert.doesNotMatch(out, /explicit superpowers source/)
} finally {
cleanup(home)
}
})
test('an invalid explicit source fails without touching the legacy cache', () => {
const home = tempDir()
try {
makeLegacyCache(home)
const empty = join(home, 'not-superpowers')
const run = runInstaller(home, ['--skills', '--superpowers-from', empty])
const out = run.stdout + run.stderr
assert.equal(run.status, 1, out)
assert.match(out, /FAIL: explicit superpowers source is invalid: .* has no using-superpowers\/SKILL\.md/)
assert.equal(existsSync(join(home, '.dsh', 'skills', 'brainstorming')), false)
} finally {
cleanup(home)
}
})
test('the legacy plugin cache still works and warns', () => {
const home = tempDir()
try {
makeLegacyCache(home)
const run = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: 'file:///nonexistent' })
const out = run.stdout + run.stderr
assert.equal(run.status, 0, out)
assert.match(out, /WARN: legacy superpowers source/)
assert.match(run.stdout, /copied 2 superpowers skills/)
assert.ok(existsSync(join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md')))
} finally {
cleanup(home)
}
})
test('--check reports the pinned source', () => {
const home = tempDir()
try {
const pinned = join(home, '.dsh', 'superpowers')
makeSuperpowers(pinned)
const run = runInstaller(home, ['--check'])
const out = run.stdout + run.stderr
assert.equal(run.status, 0, out)
assert.match(out, /ok: superpowers source \(.*superpowers\/skills\)/)
} finally {
cleanup(home)
}
})
test('--check fails on an invalid explicit source', () => {
const home = tempDir()
try {
const empty = join(home, 'not-superpowers')
const run = runInstaller(home, ['--check'], { DSH_SUPERPOWERS_SOURCE: empty })
const out = run.stdout + run.stderr
assert.equal(run.status, 1, out)
assert.match(out, /FAIL: explicit superpowers source/)
} finally {
cleanup(home)
}
})
test('--global installs DSH role paths and explains the instruction format', () => {
const home = tempDir()
try {
const globalSource = join(here, '..', '..', 'global', 'CLAUDE.md')
const before = readFileSync(globalSource, 'utf8')
const run = runInstaller(home, ['--global'])
assert.equal(run.status, 0, run.stdout + run.stderr)
const agents = readFileSync(join(home, '.dsh', 'AGENTS.md'), 'utf8')
assert.match(agents, /~\/\.dsh\/roles\/dev\.md/)
assert.doesNotMatch(agents, /~\/\.claude/)
assert.match(agents, /superpowers skills are ~\/\.dsh\/skills/)
assert.match(agents, /instruction-file format of this harness/)
assert.match(agents, /does not imply a Claude Code runtime/)
assert.equal(readFileSync(globalSource, 'utf8'), before, 'global/CLAUDE.md must stay untouched')
const second = runInstaller(home, ['--global'])
assert.equal(second.status, 0, second.stdout + second.stderr)
assert.match(second.stdout, /backup: /)
} finally {
cleanup(home)
}
})
test('--skills and --roles warn when DSH_HOME exists while HOME is isolated (O-2)', () => {
const work = tempDir()
try {
const home = join(work, 'home')
// An existing DSH_HOME outside the isolated HOME is the mixed state: the
// install would write outside the sandbox. The warning must not abort.
const sharedDsh = join(work, 'shared-dsh')
mkdirSync(home, { recursive: true })
mkdirSync(sharedDsh, { recursive: true })
const src = fixtureSuperpowers(home, null, join(home, 'src', 'superpowers'))
const warn = `WARN: DSH_HOME=${sharedDsh} points to an existing directory with a non-standard HOME; both HOME and DSH_HOME must be isolated together`
const skills = runInstaller(home, ['--skills', '--superpowers-from', src], { DSH_HOME: sharedDsh })
const skillsOut = skills.stdout + skills.stderr
assert.equal(skills.status, 0, skillsOut)
assert.ok(skillsOut.includes(warn), skillsOut)
assert.ok(
existsSync(join(sharedDsh, 'skills', 'using-superpowers', 'SKILL.md')),
'the install still runs after the warning',
)
const roles = runInstaller(home, ['--roles'], { DSH_HOME: sharedDsh })
const rolesOut = roles.stdout + roles.stderr
assert.equal(roles.status, 0, rolesOut)
assert.ok(rolesOut.includes(warn), rolesOut)
// Isolated together ($DSH_HOME=$HOME/.dsh): no warning.
const normal = runInstaller(home, ['--skills', '--superpowers-from', src])
const normalOut = normal.stdout + normal.stderr
assert.equal(normal.status, 0, normalOut)
assert.doesNotMatch(normalOut, /WARN: DSH_HOME=/)
} finally {
cleanup(work)
}
})
test('a legacy cache without using-superpowers is not a source', () => {
const home = tempDir()
try {
const cache = join(
home,
'.claude',
'plugins',
'cache',
'claude-plugins-official',
'superpowers',
'7.0.0',
'skills',
)
write(join(cache, 'brainstorming', 'SKILL.md'), '---\nname: brainstorming\n---\nbody\n')
const run = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: 'file:///nonexistent' })
const out = run.stdout + run.stderr
assert.equal(run.status, 1, out)
assert.match(out, /FAIL: no superpowers source/)
assert.doesNotMatch(out, /copied 0 superpowers/)
assert.equal(existsSync(join(home, '.dsh', 'skills', 'brainstorming')), false)
} finally {
cleanup(home)
}
})
test('an empty --superpowers-from is rejected, not ignored', () => {
const home = tempDir()
try {
makeLegacyCache(home)
for (const args of [['--skills', '--superpowers-from='], ['--skills', '--superpowers-from', '']]) {
const run = runInstaller(home, args)
const out = run.stdout + run.stderr
assert.equal(run.status, 2, out)
assert.match(out, /needs a non-empty directory/)
assert.equal(existsSync(join(home, '.dsh', 'skills', 'using-superpowers')), false)
}
} finally {
cleanup(home)
}
})