Files
brain/tools/capability-vocabulary.test.mjs
T
Дмитрий acd9bdc479 feat(registry): токенизация needs/produces — батч superpowers (этап 2b, роутер-реестр)
Phase 2b батч 1: 14 superpowers-контрактов переведены с прозы на токены словаря.
Словарь +13 (атомарные выходы + данности), всего 39 токенов, v0.3.0.

Граф ожил для рабочих цепочек (рёбра producer->consumer):
- brainstorming -> writing-plans -> executing-plans / subagent-driven
- test-driven-development -> requesting-code-review -> receiving-code-review
- finishing-a-development-branch (needs completed-change)

Тесты: новый замок-тест батча (14 контрактов проходят словарь + рёбра графа);
m3c-coverage-invariants просьба обновлена на токен; capability-vocabulary
счётчик -> >= (словарь живой). Регрессия 4369 passed, exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:03:22 +03:00

114 lines
5.0 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import { validateVocabulary, loadVocabulary, unknownTokens } from './capability-vocabulary.mjs';
describe('capability-vocabulary.json — реальный файл словаря (этап 2a)', () => {
const raw = JSON.parse(fs.readFileSync('docs/registry/capability-vocabulary.json', 'utf8'));
const r = validateVocabulary(raw);
it('форма валидна → ok, без ошибок', () => {
expect(r.errors).toEqual([]);
expect(r.ok).toBe(true);
});
it('фундамент-набор присутствует (A8 + мосты + данности)', () => {
// фундамент = 26 токенов; словарь живой, дорастает per-batch в 2b → >= не ==
expect(r.tokens.size).toBeGreaterThanOrEqual(26);
// A8-цепочка
for (const t of ['running-portal', 'dast-report', 'go-live-verdict']) expect(r.tokens.has(t)).toBe(true);
// мосты рабочих цепочек (создают рёбра графа)
for (const t of ['feature-spec', 'implementation-plan', 'completed-change', 'code-review-feedback', 'raw-research', 'ui-design', 'marketing-draft', 'content-framework', 'close-entries'])
expect(r.tokens.has(t)).toBe(true);
// данности задачи (→ initialInputs в 2c)
for (const t of ['feature-intent', 'feature-or-bugfix', 'bug-or-failure', 'ui-task'])
expect(r.tokens.has(t)).toBe(true);
});
});
describe('validateVocabulary — форма словаря', () => {
const good = { version: '0.1.0', tokens: [
{ token: 'dast-report', label: 'отчёт DAST', description: 'результат динамики' },
{ token: 'stride-model', label: 'STRIDE', description: 'модель угроз' },
] };
it('валидный словарь → ok + Set токенов', () => {
const r = validateVocabulary(good);
expect(r.ok).toBe(true);
expect(r.errors).toEqual([]);
expect(r.tokens.has('dast-report')).toBe(true);
expect(r.tokens.size).toBe(2);
});
it('не-объект / нет tokens-массива → ошибка', () => {
expect(validateVocabulary(null).ok).toBe(false);
expect(validateVocabulary({ version: '1' }).ok).toBe(false);
});
it('токен не kebab-case → ошибка', () => {
const r = validateVocabulary({ tokens: [{ token: 'DAST_Report', label: 'x', description: 'y' }] });
expect(r.ok).toBe(false);
expect(r.errors.join(' ')).toMatch(/kebab-case/);
});
it('дубль токена → ошибка', () => {
const r = validateVocabulary({ tokens: [
{ token: 'a-b', label: 'x', description: 'y' },
{ token: 'a-b', label: 'x2', description: 'y2' },
] });
expect(r.ok).toBe(false);
expect(r.errors.join(' ')).toMatch(/duplicate/);
});
it('пустой label/description → ошибка', () => {
const r = validateVocabulary({ tokens: [{ token: 'a-b', label: '', description: '' }] });
expect(r.ok).toBe(false);
});
});
describe('loadVocabulary — fs-инъекция', () => {
it('читает файл и возвращает валидный словарь', () => {
const stub = { readFileSync: () => JSON.stringify({ tokens: [{ token: 'a-b', label: 'x', description: 'y' }] }) };
const r = loadVocabulary({ path: 'fake.json', fsImpl: stub });
expect(r.ok).toBe(true);
expect(r.tokens.has('a-b')).toBe(true);
});
it('бросает при битом JSON', () => {
const stub = { readFileSync: () => 'not-json' };
expect(() => loadVocabulary({ path: 'bad.json', fsImpl: stub })).toThrow();
});
});
describe('unknownTokens — сверка токенов контракта со словарём', () => {
const set = new Set(['running-portal', 'dast-report']);
it('все токены в словаре → пусто', () => {
const c = { needs: ['running-portal'], produces: ['dast-report'] };
expect(unknownTokens(c, set)).toEqual([]);
});
it('неизвестный токен в needs → запись {field, token}', () => {
const c = { needs: ['no-such-token'], produces: ['dast-report'] };
expect(unknownTokens(c, set)).toEqual([{ field: 'needs', token: 'no-such-token' }]);
});
it('неизвестный токен в produces → запись', () => {
const c = { needs: ['running-portal'], produces: ['ghost'] };
expect(unknownTokens(c, set)).toEqual([{ field: 'produces', token: 'ghost' }]);
});
it('пустые needs/produces → пусто (нечего сверять)', () => {
expect(unknownTokens({ needs: [], produces: [] }, set)).toEqual([]);
});
it('contract null/undefined → пусто (защита optional-chain)', () => {
expect(unknownTokens(null, set)).toEqual([]);
expect(unknownTokens(undefined, set)).toEqual([]);
});
it('tokenSet как массив (не Set) → нормализуется', () => {
const c = { needs: ['running-portal'], produces: ['ghost'] };
expect(unknownTokens(c, ['running-portal'])).toEqual([{ field: 'produces', token: 'ghost' }]);
});
});