285 lines
8.9 KiB
JavaScript
285 lines
8.9 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { spawnSync } from 'node:child_process'
|
|
import { cpSync, readFileSync, rmSync } from 'node:fs'
|
|
import { dirname, join, resolve } from 'node:path'
|
|
import test from 'node:test'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { cleanup, tempDir, write } from './helpers.mjs'
|
|
|
|
const testDir = dirname(fileURLToPath(import.meta.url))
|
|
const repoRoot = resolve(testDir, '..', '..')
|
|
const syncScript = resolve(repoRoot, 'dsh/sync/sync-from-upstream.mjs')
|
|
const verifyScript = resolve(repoRoot, 'dsh/sync/verify-skills.mjs')
|
|
const fixture = resolve(testDir, 'fixtures', 'superpowers-min')
|
|
|
|
function run(script, args) {
|
|
return spawnSync(process.execPath, [script, ...args], { encoding: 'utf8' })
|
|
}
|
|
|
|
/**
|
|
* The tree every test starts from: Task 1's sync applied to the reduced
|
|
* upstream checkout — 15 skill directories plus the DSH `dsh-tools.md`
|
|
* override. Verify only ever sees a synced tree, never raw upstream text.
|
|
*/
|
|
function syncedTree(work) {
|
|
const out = join(work, 'synced')
|
|
const result = run(syncScript, [fixture, '--out', out, '--ref', 'v7.0.1'])
|
|
assert.equal(result.status, 0, `sync fixture failed: ${result.stderr}`)
|
|
return out
|
|
}
|
|
|
|
/** A private copy of a synced tree: the failing tests mutate it. */
|
|
function syncedCopy(work, dir) {
|
|
const copy = join(work, 'copy')
|
|
cpSync(dir, copy, { recursive: true })
|
|
return copy
|
|
}
|
|
|
|
function verify(dir) {
|
|
return run(verifyScript, [dir])
|
|
}
|
|
|
|
test('verify ok on synced tree', () => {
|
|
const work = tempDir('dsh-verify-ok-')
|
|
try {
|
|
const result = verify(syncedTree(work))
|
|
|
|
assert.equal(result.status, 0, `stderr: ${result.stderr}`)
|
|
assert.match(result.stdout, /^ok: using-superpowers$/m)
|
|
assert.match(result.stdout, /verified 15 skills/)
|
|
assert.equal((result.stdout.match(/^ok: /gm) ?? []).length, 15, 'one ok line per skill directory')
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('verify fails on missing description', () => {
|
|
const work = tempDir('dsh-verify-no-description-')
|
|
try {
|
|
const dir = syncedCopy(work, syncedTree(work))
|
|
const file = join(dir, 'brainstorming', 'SKILL.md')
|
|
write(file, readFileSync(file, 'utf8').replace(/^description:.*\n/m, ''))
|
|
|
|
const result = verify(dir)
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /brainstorming\/SKILL\.md/)
|
|
assert.match(result.stderr, /description/)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('verify fails on leftover superpowers: prefix in a body', () => {
|
|
const work = tempDir('dsh-verify-prefix-')
|
|
try {
|
|
const dir = syncedCopy(work, syncedTree(work))
|
|
const file = join(dir, 'systematic-debugging', 'SKILL.md')
|
|
write(file, `${readFileSync(file, 'utf8')}\nSee superpowers:brainstorming for the upstream wording.\n`)
|
|
|
|
const result = verify(dir)
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /systematic-debugging\/SKILL\.md/)
|
|
assert.match(result.stderr, /superpowers:/)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('verify fails on dangling relative reference', () => {
|
|
const work = tempDir('dsh-verify-dangling-')
|
|
try {
|
|
const dir = syncedCopy(work, syncedTree(work))
|
|
const file = join(dir, 'writing-plans', 'SKILL.md')
|
|
write(file, `${readFileSync(file, 'utf8')}\nSee [the missing file](../brainstorming/nope.md).\n`)
|
|
|
|
const result = verify(dir)
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /writing-plans\/SKILL\.md/)
|
|
assert.match(result.stderr, /\.\.\/brainstorming\/nope\.md/)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('verify ignores illustrative example script paths', () => {
|
|
const work = tempDir('dsh-verify-illustrative-')
|
|
try {
|
|
const dir = syncedCopy(work, syncedTree(work))
|
|
const file = join(dir, 'writing-skills', 'SKILL.md')
|
|
write(
|
|
file,
|
|
`${readFileSync(file, 'utf8')}\nInvoke bundled scripts in prose: \`bash scripts/tool.sh\`, \`node scripts/tool.js\`.\n`,
|
|
)
|
|
|
|
const result = verify(dir)
|
|
|
|
assert.equal(result.status, 0, `stderr: ${result.stderr}`)
|
|
assert.match(result.stdout, /verified 15 skills/)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('verify ok when superpowers: survives in a references file (T1.5)', () => {
|
|
const work = tempDir('dsh-verify-references-')
|
|
try {
|
|
const dir = syncedTree(work)
|
|
const reference = readFileSync(join(dir, 'using-superpowers', 'references', 'dsh-tools.md'), 'utf8')
|
|
assert.match(reference, /superpowers:/, 'the DSH tool mapping quotes the upstream namespace on purpose')
|
|
|
|
const result = verify(dir)
|
|
|
|
assert.equal(result.status, 0, `stderr: ${result.stderr}`)
|
|
assert.match(result.stdout, /verified 15 skills/)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('verify fails when a sibling skill script is missing', () => {
|
|
const work = tempDir('dsh-verify-missing-script-')
|
|
try {
|
|
const dir = syncedCopy(work, syncedTree(work))
|
|
rmSync(join(dir, 'subagent-driven-development', 'scripts', 'sdd-workspace'))
|
|
|
|
const result = verify(dir)
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /executing-plans\/SKILL\.md/)
|
|
assert.match(result.stderr, /sdd-workspace/)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('verify fails on a mistyped sibling skill name', () => {
|
|
const work = tempDir('dsh-verify-mistyped-skill-')
|
|
try {
|
|
const dir = syncedCopy(work, syncedTree(work))
|
|
const file = join(dir, 'executing-plans', 'SKILL.md')
|
|
write(
|
|
file,
|
|
`${readFileSync(file, 'utf8')}\nRun \`bash ../subagent-driven-developmen/scripts/sdd-workspace PLAN_FILE\`.\n`,
|
|
)
|
|
|
|
const result = verify(dir)
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /subagent-driven-developmen\/scripts\/sdd-workspace/)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('verify fails on a duplicate frontmatter key', () => {
|
|
const work = tempDir('dsh-verify-duplicate-key-')
|
|
try {
|
|
const dir = syncedCopy(work, syncedTree(work))
|
|
const file = join(dir, 'brainstorming', 'SKILL.md')
|
|
write(file, readFileSync(file, 'utf8').replace(/^name: brainstorming$/m, 'name: brainstorming\nname: duplicate'))
|
|
|
|
const result = verify(dir)
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /brainstorming\/SKILL\.md/)
|
|
assert.match(result.stderr, /duplicate key/)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('verify fails without frontmatter delimiters', () => {
|
|
const work = tempDir('dsh-verify-no-frontmatter-')
|
|
try {
|
|
const dir = syncedCopy(work, syncedTree(work))
|
|
write(join(dir, 'brainstorming', 'SKILL.md'), '# Brainstorming\n\nNo frontmatter here.\n')
|
|
|
|
const result = verify(dir)
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /brainstorming\/SKILL\.md/)
|
|
assert.match(result.stderr, /missing YAML frontmatter/)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('verify CLI without a directory exits 2', () => {
|
|
const result = verify('')
|
|
|
|
assert.equal(result.status, 2)
|
|
assert.match(result.stderr, /usage:/)
|
|
})
|
|
|
|
test('verify an empty skills directory reports zero skills', () => {
|
|
const work = tempDir('dsh-verify-empty-')
|
|
try {
|
|
const dir = join(work, 'empty')
|
|
write(join(dir, '.keep'), '')
|
|
|
|
const result = verify(dir)
|
|
|
|
assert.equal(result.status, 0, `stderr: ${result.stderr}`)
|
|
assert.match(result.stdout, /verified 0 skills/)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('verify ignores prefixes and paths inside fenced examples', () => {
|
|
const work = tempDir('dsh-verify-fenced-')
|
|
try {
|
|
const dir = syncedCopy(work, syncedTree(work))
|
|
const file = join(dir, 'writing-plans', 'SKILL.md')
|
|
const fence = [
|
|
'```',
|
|
'See superpowers:brainstorming, [x](../brainstorming/nope.md) and `references/nope.md`.',
|
|
'```',
|
|
].join('\n')
|
|
write(file, `${readFileSync(file, 'utf8')}\n${fence}\n`)
|
|
|
|
const result = verify(dir)
|
|
|
|
assert.equal(result.status, 0, `stderr: ${result.stderr}`)
|
|
assert.match(result.stdout, /verified 15 skills/)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('verify fails on a dangling inline reference (T2.7)', () => {
|
|
const work = tempDir('dsh-verify-dangling-inline-')
|
|
try {
|
|
const dir = syncedCopy(work, syncedTree(work))
|
|
const file = join(dir, 'using-superpowers', 'SKILL.md')
|
|
write(file, `${readFileSync(file, 'utf8')}\nSee \`references/nope.md\` for the full mapping.\n`)
|
|
|
|
const result = verify(dir)
|
|
|
|
assert.equal(result.status, 1, `stdout: ${result.stdout}`)
|
|
assert.match(result.stderr, /using-superpowers\/SKILL\.md/)
|
|
assert.match(result.stderr, /references\/nope\.md/)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('verify does not flag a resolving inline reference', () => {
|
|
const work = tempDir('dsh-verify-inline-reference-')
|
|
try {
|
|
const dir = syncedCopy(work, syncedTree(work))
|
|
const file = join(dir, 'using-superpowers', 'SKILL.md')
|
|
write(file, `${readFileSync(file, 'utf8')}\nSee \`references/dsh-tools.md\` again.\n`)
|
|
|
|
const result = verify(dir)
|
|
|
|
assert.equal(result.status, 0, `stderr: ${result.stderr}`)
|
|
assert.match(result.stdout, /verified 15 skills/)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|