129 lines
3.6 KiB
JavaScript
129 lines
3.6 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import test from 'node:test'
|
|
import { checkTool, forDshReason } from '../bundle/lib/guards.js'
|
|
import { cleanup, git, makeRepo, write } from './helpers.mjs'
|
|
|
|
const hooksDir = fileURLToPath(new URL('../bundle/hooks/', import.meta.url))
|
|
|
|
function call(name, args) {
|
|
return { name, arguments: args }
|
|
}
|
|
|
|
test('guard-main blocks a write on main', () => {
|
|
const repo = makeRepo()
|
|
try {
|
|
const reason = checkTool({
|
|
exec: call('write', { path: join(repo, 'a.txt'), content: 'x' }),
|
|
cwd: repo,
|
|
hooksDir,
|
|
})
|
|
assert.match(reason ?? '', /BLOCKED \[guard-main\]/)
|
|
assert.doesNotMatch(reason ?? '', /superpowers:/)
|
|
assert.match(reason ?? '', /using-git-worktrees/)
|
|
} finally {
|
|
cleanup(repo)
|
|
}
|
|
})
|
|
|
|
test('forDshReason removes the Claude skill namespace from hook messages', () => {
|
|
assert.equal(
|
|
forDshReason('Run superpowers:brainstorming; use superpowers:using-git-worktrees.'),
|
|
'Run brainstorming; use using-git-worktrees.',
|
|
)
|
|
})
|
|
|
|
test('guard-main allows a write on a feature branch', () => {
|
|
const repo = makeRepo()
|
|
try {
|
|
git(repo, ['checkout', '-b', 'feat/x'])
|
|
const reason = checkTool({
|
|
exec: call('write', { path: join(repo, 'a.txt'), content: 'x' }),
|
|
cwd: repo,
|
|
hooksDir,
|
|
})
|
|
assert.equal(reason, undefined)
|
|
} finally {
|
|
cleanup(repo)
|
|
}
|
|
})
|
|
|
|
test('guard-main inspects bash commands on main', () => {
|
|
const repo = makeRepo()
|
|
try {
|
|
const blocked = checkTool({
|
|
exec: call('bash', { command: 'echo hi > a.txt' }),
|
|
cwd: repo,
|
|
hooksDir,
|
|
})
|
|
assert.match(blocked ?? '', /BLOCKED \[guard-main\]/)
|
|
|
|
const allowed = checkTool({
|
|
exec: call('bash', { command: 'git status --short' }),
|
|
cwd: repo,
|
|
hooksDir,
|
|
})
|
|
assert.equal(allowed, undefined)
|
|
} finally {
|
|
cleanup(repo)
|
|
}
|
|
})
|
|
|
|
test('require-plan blocks src edits without a plan or .bounded', () => {
|
|
const repo = makeRepo()
|
|
try {
|
|
git(repo, ['checkout', '-b', 'feat/x'])
|
|
write(join(repo, 'src', '.keep'), '')
|
|
const blocked = checkTool({
|
|
exec: call('write', { path: join(repo, 'src', 'a.ts'), content: 'export {}\n' }),
|
|
cwd: repo,
|
|
hooksDir,
|
|
})
|
|
assert.match(blocked ?? '', /BLOCKED \[require-plan\]/)
|
|
assert.doesNotMatch(blocked ?? '', /superpowers:/)
|
|
assert.match(blocked ?? '', /brainstorming/)
|
|
|
|
write(join(repo, '.bounded'), '')
|
|
const bounded = checkTool({
|
|
exec: call('write', { path: join(repo, 'src', 'a.ts'), content: 'export {}\n' }),
|
|
cwd: repo,
|
|
hooksDir,
|
|
})
|
|
assert.equal(bounded, undefined)
|
|
} finally {
|
|
cleanup(repo)
|
|
}
|
|
})
|
|
|
|
test('require-plan accepts a plan that names the branch', () => {
|
|
const repo = makeRepo()
|
|
try {
|
|
git(repo, ['checkout', '-b', 'feat/x'])
|
|
write(join(repo, 'src', '.keep'), '')
|
|
write(join(repo, 'docs', 'superpowers', 'plans', 'test.md'), '# plan for feat/x\n')
|
|
const reason = checkTool({
|
|
exec: call('edit', { path: join(repo, 'src', 'a.ts'), old_string: 'a', new_string: 'b' }),
|
|
cwd: repo,
|
|
hooksDir,
|
|
})
|
|
assert.equal(reason, undefined)
|
|
} finally {
|
|
cleanup(repo)
|
|
}
|
|
})
|
|
|
|
test('guard-main blocks an edit on main before require-plan runs', () => {
|
|
const repo = makeRepo()
|
|
try {
|
|
const reason = checkTool({
|
|
exec: call('edit', { path: join(repo, 'README.md'), old_string: 'test', new_string: 'changed' }),
|
|
cwd: repo,
|
|
hooksDir,
|
|
})
|
|
assert.match(reason ?? '', /BLOCKED \[guard-main\]/)
|
|
} finally {
|
|
cleanup(repo)
|
|
}
|
|
})
|