397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
163 lines
6.7 KiB
JavaScript
163 lines
6.7 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
||
import { extractVersion, extractCrossRefs, detectMismatches } from './cross-ref-checker.mjs';
|
||
|
||
describe('extractVersion', () => {
|
||
it('extracts version from header', () => {
|
||
const text = '# Pravila v1.30 от 18.05.2026';
|
||
expect(extractVersion(text)).toBe('1.30');
|
||
});
|
||
|
||
it('returns null if no version', () => {
|
||
expect(extractVersion('# Untitled')).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('extractCrossRefs', () => {
|
||
it('extracts cross-refs like "Pravila v1.29"', () => {
|
||
const text = 'See Pravila v1.29 and Tooling v2.15.';
|
||
const refs = extractCrossRefs(text);
|
||
expect(refs).toContainEqual({ name: 'Pravila', version: '1.29' });
|
||
expect(refs).toContainEqual({ name: 'Tooling', version: '2.15' });
|
||
});
|
||
|
||
it('link-based primary: first bold version after markdown-link to normative file', () => {
|
||
const text =
|
||
'| Правила | [docs/Pravila_raboty_Claude_v1_1.md](docs/Pravila_raboty_Claude_v1_1.md) (**v1.31 от 19.05.2026** — описание; v1.30 наследие; v1.29 наследие) |';
|
||
const refs = extractCrossRefs(text);
|
||
expect(refs).toEqual([{ name: 'Pravila', version: '1.31' }]);
|
||
});
|
||
|
||
it('link-based primary: extracts version even if bold contains preceding words (e.g. "Прил. Н")', () => {
|
||
const text =
|
||
'| Реестр | [docs/Tooling_v8_3.md](docs/Tooling_v8_3.md) (**Прил. Н v2.17 от 19.05.2026** — описание) |';
|
||
const refs = extractCrossRefs(text);
|
||
expect(refs).toEqual([{ name: 'Tooling', version: '2.17' }]);
|
||
});
|
||
|
||
it('link-based primary: §0 table with multiple normative files', () => {
|
||
const text = [
|
||
'| Pravila | [docs/Pravila_raboty_Claude_v1_1.md](docs/Pravila_raboty_Claude_v1_1.md) (**v1.31** ...) |',
|
||
'| Tooling | [docs/Tooling_v8_3.md](docs/Tooling_v8_3.md) (**Прил. Н v2.17** ...) |',
|
||
'| PSR_v1 | [docs/Plugin_stack_rules_v1.md](docs/Plugin_stack_rules_v1.md) (**v3.16** ...) |',
|
||
].join('\n');
|
||
const refs = extractCrossRefs(text);
|
||
expect(refs).toEqual([
|
||
{ name: 'Pravila', version: '1.31' },
|
||
{ name: 'Tooling', version: '2.17' },
|
||
{ name: 'PSR_v1', version: '3.16' },
|
||
]);
|
||
});
|
||
|
||
it('dedupe per target: only first ref per target name is kept', () => {
|
||
const text =
|
||
'header: Tooling v2.17; legacy: Tooling v2.16 наследие; older: Tooling v2.15 наследие';
|
||
const refs = extractCrossRefs(text);
|
||
expect(refs.filter((r) => r.name === 'Tooling')).toEqual([
|
||
{ name: 'Tooling', version: '2.17' },
|
||
]);
|
||
});
|
||
|
||
it('skips left side of arrow transition "v1.30→**v1.31**"', () => {
|
||
const text = 'cross-refs: Pravila v1.30→**v1.31**';
|
||
const refs = extractCrossRefs(text);
|
||
expect(refs.filter((r) => r.name === 'Pravila')).toHaveLength(0);
|
||
});
|
||
|
||
it('skips arrow with whitespace: "v1.30 → v1.31"', () => {
|
||
const text = 'transition: Pravila v1.30 → v1.31 happened today';
|
||
const refs = extractCrossRefs(text);
|
||
expect(refs.filter((r) => r.name === 'Pravila')).toHaveLength(0);
|
||
});
|
||
|
||
it('link-based wins over fallback when both present', () => {
|
||
const text = [
|
||
'Body mentions Pravila v1.25 in some sentence.',
|
||
'| Pravila | [docs/Pravila_raboty_Claude_v1_1.md](docs/Pravila_raboty_Claude_v1_1.md) (**v1.31** current) |',
|
||
].join('\n');
|
||
const refs = extractCrossRefs(text);
|
||
expect(refs.filter((r) => r.name === 'Pravila')).toEqual([
|
||
{ name: 'Pravila', version: '1.31' },
|
||
]);
|
||
});
|
||
|
||
it('fallback scope-cuts at first "**vN.M наследие**" history marker', () => {
|
||
const text =
|
||
'header refs: Tooling v2.17\n\n**v2.16 наследие:** old refs Tooling v2.15';
|
||
const refs = extractCrossRefs(text);
|
||
expect(refs).toEqual([{ name: 'Tooling', version: '2.17' }]);
|
||
});
|
||
|
||
it('fallback scope-cuts at first "**Что изменилось в vN.M относительно**" marker', () => {
|
||
const text = [
|
||
'**Версия:** v1.31',
|
||
'',
|
||
'**Что изменилось в v1.31 относительно v1.30:** ADR-011',
|
||
'',
|
||
'**Что изменилось в v1.29 относительно v1.28:** Связано: Tooling v2.15',
|
||
].join('\n');
|
||
const refs = extractCrossRefs(text);
|
||
expect(refs.filter((r) => r.name === 'Tooling')).toEqual([]);
|
||
});
|
||
|
||
it('fallback scope-cuts at first "**vN.M** — " bold-dash marker', () => {
|
||
const text = '**v3.16** — current changes\n**v3.15** — Связано: Tooling v2.15';
|
||
const refs = extractCrossRefs(text);
|
||
expect(refs.filter((r) => r.name === 'Tooling')).toEqual([]);
|
||
});
|
||
|
||
it('link-based catches refs even when scope cut would exclude them', () => {
|
||
const text = [
|
||
'shapka with **v2.17 наследие** description body',
|
||
'| Pravila | [docs/Pravila_raboty_Claude_v1_1.md](docs/Pravila_raboty_Claude_v1_1.md) (**v1.31** current) |',
|
||
].join('\n');
|
||
const refs = extractCrossRefs(text);
|
||
expect(refs).toContainEqual({ name: 'Pravila', version: '1.31' });
|
||
});
|
||
});
|
||
|
||
describe('detectMismatches', () => {
|
||
it('detects when fileA references fileB v1.29 but fileB header is v1.30', () => {
|
||
const files = {
|
||
'A.md': '# A v1.0 — cross-refs: Pravila v1.29',
|
||
'docs/Pravila_raboty_Claude_v1_1.md': '# Pravila v1.30',
|
||
};
|
||
const m = detectMismatches(files);
|
||
expect(m).toHaveLength(1);
|
||
expect(m[0].from).toBe('A.md');
|
||
expect(m[0].to).toBe('Pravila');
|
||
expect(m[0].expected).toBe('1.30');
|
||
expect(m[0].found).toBe('1.29');
|
||
});
|
||
|
||
it('passes when in sync', () => {
|
||
const files = {
|
||
'A.md': '# A v1.0 — Pravila v1.30',
|
||
'docs/Pravila_raboty_Claude_v1_1.md': '# Pravila v1.30',
|
||
};
|
||
expect(detectMismatches(files)).toHaveLength(0);
|
||
});
|
||
|
||
it('ignores «наследие» chain when current link-ref matches header', () => {
|
||
const files = {
|
||
'CLAUDE.md':
|
||
'**Версия:** 2.18 — §0 cross-refs Pravila v1.30→**v1.31** / Tooling v2.16→**v2.17**\n\n| Правила | [docs/Pravila_raboty_Claude_v1_1.md](docs/Pravila_raboty_Claude_v1_1.md) (**v1.31** current; v1.30 наследие; v1.29 наследие) |',
|
||
'docs/Pravila_raboty_Claude_v1_1.md': '**Версия:** v1.31 — current',
|
||
};
|
||
expect(detectMismatches(files)).toEqual([]);
|
||
});
|
||
|
||
it('detects real drift when link-based cross-ref is stale', () => {
|
||
const files = {
|
||
'CLAUDE.md':
|
||
'| Pravila | [docs/Pravila_raboty_Claude_v1_1.md](docs/Pravila_raboty_Claude_v1_1.md) (**v1.30** description) |',
|
||
'docs/Pravila_raboty_Claude_v1_1.md': '**Версия:** v1.31 — current',
|
||
};
|
||
const m = detectMismatches(files);
|
||
expect(m).toHaveLength(1);
|
||
expect(m[0].from).toBe('CLAUDE.md');
|
||
expect(m[0].to).toBe('Pravila');
|
||
expect(m[0].found).toBe('1.30');
|
||
expect(m[0].expected).toBe('1.31');
|
||
});
|
||
});
|