228 lines
8.9 KiB
JavaScript
228 lines
8.9 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { spawnSync } from 'node:child_process'
|
|
import { cpSync, existsSync, readFileSync, readdirSync, 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 script = resolve(repoRoot, 'dsh/sync/sync-from-upstream.mjs')
|
|
const fixture = resolve(testDir, 'fixtures', 'superpowers-min')
|
|
const bundleDshTools = resolve(repoRoot, 'dsh/bundle/dsh-tools.md')
|
|
|
|
/** A private copy of the reduced upstream checkout: tests mutate it. */
|
|
function fixtureCopy(work) {
|
|
const upstream = join(work, 'upstream')
|
|
cpSync(fixture, upstream, { recursive: true })
|
|
return upstream
|
|
}
|
|
|
|
function runSync(args) {
|
|
return spawnSync(process.execPath, [script, ...args], { encoding: 'utf8' })
|
|
}
|
|
|
|
test('sync happy path: transforms, namespace strip, override, meta', () => {
|
|
const work = tempDir('dsh-sync-happy-')
|
|
try {
|
|
const upstream = fixtureCopy(work)
|
|
const out = join(work, 'out')
|
|
const result = runSync([upstream, '--out', out, '--ref', 'v7.0.1'])
|
|
|
|
assert.equal(result.status, 0, `stderr: ${result.stderr}`)
|
|
assert.match(result.stdout, /synced \d+ files/)
|
|
assert.match(result.stdout, /transforms applied: 16/)
|
|
|
|
const skillDirs = readdirSync(out).filter((name) => name !== '.sync-meta.json')
|
|
assert.equal(skillDirs.length, 15, `expected all 15 upstream skills, got ${skillDirs.join(', ')}`)
|
|
|
|
const using = readFileSync(join(out, 'using-superpowers', 'SKILL.md'), 'utf8')
|
|
assert.match(using, /references\/dsh-tools\.md/)
|
|
assert.doesNotMatch(using, /superpowers:/)
|
|
assert.doesNotMatch(using, /<SUBAGENT-STOP>/)
|
|
|
|
const override = readFileSync(join(out, 'using-superpowers', 'references', 'dsh-tools.md'), 'utf8')
|
|
assert.equal(override, readFileSync(bundleDshTools, 'utf8'))
|
|
|
|
const brainstorming = readFileSync(join(out, 'brainstorming', 'SKILL.md'), 'utf8')
|
|
assert.doesNotMatch(brainstorming, /superpowers:/)
|
|
assert.match(brainstorming, /writing-plans/)
|
|
|
|
const meta = JSON.parse(readFileSync(join(out, '.sync-meta.json'), 'utf8'))
|
|
assert.deepEqual(meta, { ref: 'v7.0.1', version: '7.0.1' })
|
|
|
|
assert.equal(existsSync(join(out, 'using-superpowers', 'references', 'claude-code-tools.md')), false)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('sync fails loud on a broken transform anchor and leaves --out untouched', () => {
|
|
const work = tempDir('dsh-sync-anchor-')
|
|
try {
|
|
const upstream = fixtureCopy(work)
|
|
const usingPath = join(upstream, 'skills', 'using-superpowers', 'SKILL.md')
|
|
const anchor = 'If your harness appears here, read its reference file for special instructions:'
|
|
const original = readFileSync(usingPath, 'utf8')
|
|
assert.ok(original.includes(anchor), 'fixture must carry the upstream anchor verbatim')
|
|
write(usingPath, original.replace(anchor, 'If your harness appears here, read the reference file:'))
|
|
|
|
const out = join(work, 'out')
|
|
write(join(out, 'keep.txt'), 'sentinel\n')
|
|
|
|
const result = runSync([upstream, '--out', out, '--ref', 'v7.0.1'])
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /DSH platform reference/)
|
|
assert.equal(readFileSync(join(out, 'keep.txt'), 'utf8'), 'sentinel\n')
|
|
assert.deepEqual(readdirSync(out), ['keep.txt'], 'failed sync must not swap a partial tree in')
|
|
assert.deepEqual(
|
|
readdirSync(work).filter((name) => name.startsWith('out.tmp-')),
|
|
[],
|
|
'failed sync must remove its staging tree',
|
|
)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('sync fails loud on a broken anchor of another transform', () => {
|
|
const work = tempDir('dsh-sync-anchor-executing-plans-')
|
|
try {
|
|
const upstream = fixtureCopy(work)
|
|
const skillPath = join(upstream, 'skills', 'executing-plans', 'SKILL.md')
|
|
const anchor = 'Run `../subagent-driven-development/scripts/review-package PLAN_FILE MERGE_BASE HEAD`'
|
|
const original = readFileSync(skillPath, 'utf8')
|
|
assert.ok(original.includes(anchor), 'fixture must carry the review-package anchor verbatim')
|
|
write(skillPath, original.replace(anchor, 'Run `../subagent-driven-development/scripts/review-package PLAN_FILE HEAD`'))
|
|
|
|
const result = runSync([upstream, '--out', join(work, 'out'), '--ref', 'v7.0.1'])
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /invoke review-package through bash/)
|
|
assert.equal(existsSync(join(work, 'out')), false)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('sync refuses --out that collides with the upstream checkout', () => {
|
|
const work = tempDir('dsh-sync-collision-')
|
|
try {
|
|
const upstream = fixtureCopy(work)
|
|
write(join(upstream, 'sentinel.txt'), 'source stays\n')
|
|
|
|
// `--out` equal to the checkout, an ancestor of it, and the skills/ tree
|
|
// itself — each would delete the source the sync is reading.
|
|
for (const out of [upstream, work, join(upstream, 'skills')]) {
|
|
const result = runSync([upstream, '--out', out, '--ref', 'v7.0.1'])
|
|
|
|
assert.equal(result.status, 1, `--out ${out} must be refused`)
|
|
assert.match(result.stderr, /--out collides with the upstream checkout/)
|
|
}
|
|
|
|
assert.equal(readFileSync(join(upstream, 'sentinel.txt'), 'utf8'), 'source stays\n')
|
|
assert.equal(existsSync(join(upstream, '.claude-plugin', 'plugin.json')), true)
|
|
assert.equal(readdirSync(join(upstream, 'skills')).length, 15)
|
|
assert.deepEqual(
|
|
readdirSync(work).filter((name) => name.includes('.tmp-')),
|
|
[],
|
|
'a refused sync must not create its staging tree',
|
|
)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('sync fails on a transform whose source file upstream dropped', () => {
|
|
const work = tempDir('dsh-sync-missing-transform-file-')
|
|
try {
|
|
const upstream = fixtureCopy(work)
|
|
rmSync(join(upstream, 'skills', 'brainstorming', 'visual-companion.md'))
|
|
|
|
const result = runSync([upstream, '--out', join(work, 'out'), '--ref', 'v7.0.1'])
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /never matched/)
|
|
assert.match(result.stderr, /DSH platform note for the visual companion/)
|
|
assert.equal(existsSync(join(work, 'out')), false)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('sync fails when an anchor matches twice', () => {
|
|
const work = tempDir('dsh-sync-duplicate-anchor-')
|
|
try {
|
|
const upstream = fixtureCopy(work)
|
|
const skillPath = join(upstream, 'skills', 'executing-plans', 'SKILL.md')
|
|
const anchor = "Run this skill's `scripts/task-start PLAN_FILE N`."
|
|
const original = readFileSync(skillPath, 'utf8')
|
|
assert.ok(original.includes(anchor), 'fixture must carry the task-start anchor verbatim')
|
|
write(skillPath, original.replace(anchor, `${anchor}\n\n${anchor}`))
|
|
|
|
const result = runSync([upstream, '--out', join(work, 'out'), '--ref', 'v7.0.1'])
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /matched 2 times/)
|
|
assert.match(result.stderr, /invoke task-start through bash/)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('sync warns, but succeeds, when the optional transform anchor is gone', () => {
|
|
const work = tempDir('dsh-sync-optional-anchor-')
|
|
try {
|
|
const upstream = fixtureCopy(work)
|
|
const skillPath = join(upstream, 'skills', 'subagent-driven-development', 'SKILL.md')
|
|
const anchor = 'You: "User level (~/.config/superpowers/hooks/)"'
|
|
const original = readFileSync(skillPath, 'utf8')
|
|
assert.ok(original.includes(anchor), 'fixture must carry the optional anchor verbatim')
|
|
write(skillPath, original.replace(anchor, 'You: "Install the hook at the project level"'))
|
|
|
|
const out = join(work, 'out')
|
|
const result = runSync([upstream, '--out', out, '--ref', 'v7.0.1'])
|
|
|
|
assert.equal(result.status, 0, `stderr: ${result.stderr}`)
|
|
assert.match(result.stderr, /\[warn\] optional transform/)
|
|
assert.match(result.stdout, /transforms applied: 15/)
|
|
assert.equal(existsSync(join(out, '.sync-meta.json')), true)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('sync fails on a changed skill set', () => {
|
|
const work = tempDir('dsh-sync-skill-set-')
|
|
try {
|
|
const upstream = fixtureCopy(work)
|
|
rmSync(join(upstream, 'skills', 'brainstorming'), { recursive: true, force: true })
|
|
|
|
const result = runSync([upstream, '--out', join(work, 'out')])
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /removed upstream:\s*brainstorming/)
|
|
assert.equal(existsSync(join(work, 'out')), false)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|
|
|
|
test('sync fails when the checkout has no skills/ directory', () => {
|
|
const work = tempDir('dsh-sync-no-skills-')
|
|
try {
|
|
const upstream = fixtureCopy(work)
|
|
rmSync(join(upstream, 'skills'), { recursive: true, force: true })
|
|
|
|
const result = runSync([upstream, '--out', join(work, 'out')])
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /no skills\/ directory/)
|
|
assert.equal(existsSync(join(work, 'out')), false)
|
|
} finally {
|
|
cleanup(work)
|
|
}
|
|
})
|