215 lines
8.3 KiB
JavaScript
215 lines
8.3 KiB
JavaScript
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')
|
||
})
|