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('\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, /^$/) 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') })