acd9bdc479
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>
114 lines
5.0 KiB
JavaScript
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' }]);
|
|
});
|
|
});
|