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

215 lines
8.3 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 { join } from 'node:path'
import test from 'node:test'
import {
buildHarnessCheckText,
buildRoleHintText,
detectAdoption,
detectRole,
isHarnessMessage,
makePrestepMarker,
} from '../bundle/lib/prestep.js'
import { cleanup, tempDir, write } from './helpers.mjs'
const SECTION = '## OpenSpec и superpowers\n\nСекция подключения.\n'
/**
* A throwaway directory tree that looks like a harness-adopted repository:
* `.git/`, `openspec/`, `.codegraph/`, and the CLAUDE.md section.
*/
function adoptedRepo() {
const repo = tempDir('dsh-harness-adopted-')
write(join(repo, '.git', 'HEAD'), 'ref: refs/heads/main\n')
write(join(repo, 'openspec', 'specs', '.keep'), '')
write(join(repo, '.codegraph', '.keep'), '')
write(join(repo, 'CLAUDE.md'), `# Проект\n\n${SECTION}`)
return repo
}
// ---------------------------------------------------------------------------
// detectAdoption
// ---------------------------------------------------------------------------
test('detectAdoption: adopted repo reports adopted with no missing signals', () => {
const repo = adoptedRepo()
try {
assert.deepEqual(detectAdoption(repo), { adopted: true, root: repo, missing: [] })
} finally {
cleanup(repo)
}
})
test('detectAdoption: repo without openspec/ and the CLAUDE.md section is not adopted', () => {
const repo = tempDir('dsh-harness-partial-')
try {
write(join(repo, '.git', 'HEAD'), 'ref: refs/heads/main\n')
write(join(repo, '.codegraph', '.keep'), '')
write(join(repo, 'CLAUDE.md'), '# Проект без секции\n')
assert.deepEqual(detectAdoption(repo), {
adopted: false,
root: repo,
missing: ['openspec/', 'секция «OpenSpec и superpowers» в CLAUDE.md'],
})
} finally {
cleanup(repo)
}
})
test('detectAdoption: cwd in a subdirectory walks up to the repo root', () => {
const repo = adoptedRepo()
try {
const subdir = join(repo, 'dsh', 'bundle', 'lib')
write(join(subdir, 'module.js'), 'export {}\n')
assert.deepEqual(detectAdoption(subdir), { adopted: true, root: repo, missing: [] })
} finally {
cleanup(repo)
}
})
test('detectAdoption: cwd outside any git repo stays silent', () => {
const outside = tempDir('dsh-harness-outside-')
try {
assert.deepEqual(detectAdoption(outside), { adopted: true, root: null, missing: [] })
} finally {
cleanup(outside)
}
})
test('detectAdoption: a .git file (worktree checkout) is a repo root too', () => {
const repo = tempDir('dsh-harness-worktree-')
try {
write(join(repo, '.git'), `gitdir: ${join(repo, '..', 'main', '.git', 'worktrees', 'x')}\n`)
write(join(repo, 'openspec', 'specs', '.keep'), '')
write(join(repo, '.codegraph', '.keep'), '')
write(join(repo, 'CLAUDE.md'), SECTION)
assert.deepEqual(detectAdoption(join(repo, 'openspec')), {
adopted: true,
root: repo,
missing: [],
})
} finally {
cleanup(repo)
}
})
// ---------------------------------------------------------------------------
// buildHarnessCheckText
// ---------------------------------------------------------------------------
test('buildHarnessCheckText names the root, missing signals, and next steps', () => {
const text = buildHarnessCheckText('/tmp/proj', [
'openspec/',
'секция «OpenSpec и superpowers» в CLAUDE.md',
])
assert.match(
text,
/^\[dsh-harness\] Проект \/tmp\/proj не подключён к обвязке: нет: openspec\/; секция «OpenSpec и superpowers» в CLAUDE\.md\. /,
)
assert.match(text, /openspec init --tools agents/)
assert.match(text, /codegraph init/)
assert.match(text, /Предложи владельцу настройку/)
})
// ---------------------------------------------------------------------------
// detectRole
// ---------------------------------------------------------------------------
test('detectRole: admin keywords (jira, гант, спринт)', () => {
assert.equal(detectRole('Разбери jira-задачу'), 'admin')
assert.equal(detectRole('Покажи гант проекта'), 'admin')
assert.equal(detectRole('Новый спринт начинается'), 'admin')
assert.equal(detectRole('JIRA-тикет'), 'admin')
})
test('detectRole: qa keywords (стенд, прогон, скриншот, макет)', () => {
assert.equal(detectRole('Подними стенд'), 'qa')
assert.equal(detectRole('Нужен прогон сценария'), 'qa')
assert.equal(detectRole('Сделай скриншот'), 'qa')
assert.equal(detectRole('Сверь с макетом'), 'qa')
})
test('detectRole: analyst keywords (тз, oq, спека, методика)', () => {
assert.equal(detectRole('Оформи ТЗ'), 'analyst')
assert.equal(detectRole('Закрой OQ'), 'analyst')
assert.equal(detectRole('Спека готова'), 'analyst')
assert.equal(detectRole('Нужна методика'), 'analyst')
})
test('detectRole: dev keywords (срез, issue, pr, тест, миграция)', () => {
assert.equal(detectRole('Сними срез метрик'), 'dev')
assert.equal(detectRole('Открой issue'), 'dev')
assert.equal(detectRole('Проверь тест'), 'dev')
assert.equal(detectRole('Нужна миграция'), 'dev')
})
test('detectRole: pr matches only as a whole word', () => {
assert.equal(detectRole('сделай PR'), 'dev')
assert.equal(detectRole('просто посмотри'), null)
assert.equal(detectRole('preview the branch'), null)
})
test('detectRole: priority is admin before qa, analyst, and dev', () => {
assert.equal(detectRole('спринт и тест'), 'admin')
assert.equal(detectRole('прогон и issue'), 'qa')
assert.equal(detectRole('спека и тест'), 'analyst')
})
test('detectRole: no keyword or empty input yields null', () => {
assert.equal(detectRole('Привет, как дела?'), null)
assert.equal(detectRole(''), null)
})
// ---------------------------------------------------------------------------
// buildRoleHintText
// ---------------------------------------------------------------------------
test('buildRoleHintText: known role names it and asks to load the skill', () => {
const text = buildRoleHintText('qa')
assert.ok(text.startsWith('[dsh-harness]'))
assert.match(text, /Роль сессии: qa\./)
assert.match(text, /Вызови skill role\./)
})
test('buildRoleHintText: null asks one question with all four roles', () => {
const text = buildRoleHintText(null)
assert.ok(text.startsWith('[dsh-harness]'))
assert.match(text, /Роль сессии неясна\./)
for (const role of ['dev', 'admin', 'analyst', 'qa']) {
assert.ok(text.includes(`${role} —`), `${role} variant is listed`)
}
assert.match(text, /четырьмя вариантами/)
})
// ---------------------------------------------------------------------------
// isHarnessMessage
// ---------------------------------------------------------------------------
test('isHarnessMessage: bootstrap, DSH, and legacy claude-harness texts are harness messages', () => {
assert.equal(isHarnessMessage('<!-- DSH-HARNESS-BOOTSTRAP:abc123 -->\nYou have superpowers.'), true)
assert.equal(isHarnessMessage('[dsh-harness] Роль сессии: dev.'), true)
assert.equal(isHarnessMessage('[claude-harness] Роль сессии: dev'), true)
})
test('isHarnessMessage: plain user text is not a harness message', () => {
assert.equal(isHarnessMessage('Привет! Разбери задачу.'), false)
assert.equal(isHarnessMessage(''), false)
})
// ---------------------------------------------------------------------------
// makePrestepMarker
// ---------------------------------------------------------------------------
test('makePrestepMarker: marker format, body prefix, determinism, uniqueness', () => {
const { marker, body } = makePrestepMarker('Привет')
assert.match(marker, /^<!-- DSH-HARNESS-PRESTEP:[0-9a-f]{16} -->$/)
assert.ok(body.startsWith(marker))
assert.equal(body, `${marker}\nПривет`)
assert.equal(makePrestepMarker('Привет').marker, marker, 'same text, same marker')
assert.notEqual(makePrestepMarker('Пока').marker, marker, 'different text, different marker')
assert.notEqual(makePrestepMarker('привет').marker, marker, 'case matters')
})