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

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)
}
})