Files
houseplan-card/test/reviews-archive.test.mjs
T
Claudeandclaude[bot] 0991c45374 fix(tools): архив переписывает относительные ссылки перенесённых документов (#682)
Ревью #682 r1, Medium: перенос добавляет документу уровень вложенности
(`docs/reviews/X.md` → `legacy/reviews/<тег>/X.md`, `docs/specs/` →
`legacy/specs/`), а относительные ссылки внутри перенесённых документов и в
соседях, ссылавшихся на них, никто не пересчитывал — на `97d19268` 53 битые
ссылки в 46 файлах (заявление «все 26 резолвятся» в `7feb6177` было верно
только до переноса документов ревью). Гейты архив не смотрят.

`reviews-archive.mjs`: `repairLinks` пересчитывает ссылку, если она не
резолвится от нового места, а цель находится от нового или старого места
через карту переносов; битая и до переноса ссылка не трогается. `--apply`
делает это само, `--repair-links=<rev>` — для всех переименований
`<rev>..HEAD`, `--check-links` печатает битые. Этим коммитом
`--repair-links=origin/dev` переписал ровно 53 ссылки в 46 файлах; остались
две прежние «...»-заглушки в CODE-REVIEW-448-r2 (битые и на dev). Тесты:
перенесённый документ, сосед со ссылкой в архив, ТЗ со ссылкой на позже
перенесённое ревью, битая-до-переноса не трогается, в `legacy/` битых нет;
мутант `reviews-archive-links-from-new-place-only`. PROCESS §2.10 и
DEVELOPMENT › Release называют переписывание и `--check-links`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:47 +00:00

121 lines
7.9 KiB
JavaScript

// #682: архив документов ревью выпущенных линий — кому куда, решают трейлеры.
import assert from 'node:assert/strict';
import test from 'node:test';
import { ARCHIVE_DIR, LIVE_DIR, archivePlan, brokenLinks, renderPlan, repairLinks, stableTagsThrough } from '../scripts/reviews-archive.mjs';
const lines = [
{ tag: 'v1.76.0', issues: [500, 510, 520] },
{ tag: 'v1.77.0', issues: [520, 530, 540] },
];
const names = [
'INDEX.md',
'SPEC-REVIEW-500-r1.md', 'CODE-REVIEW-500-r1.md', 'CODE-REVIEW-500-r2.md',
'SPEC-REVIEW-520-r1.md', 'CODE-REVIEW-520-r1.md',
'CODE-REVIEW-540-r1.md', 'CODE-REVIEW-540-r2.md',
'CODE-REVIEW-600-r1.md',
'CODE-REVIEW-issue-068-2026-08-12.md',
'RELEASE-REVIEW-v1.77.0.md', 'RELEASE-REVIEW-v1.78.0.md',
'notes.md',
];
const where = (plan, name) => plan.moves.find((m) => m.name === name)?.to
?? plan.kept.find((k) => k.name === name)?.reason;
test('#682 архив: задача уходит в каталог последней своей линии, документы не разъезжаются', () => {
const plan = archivePlan({ names, lines, open: [600], through: 'v1.77.0' });
assert.equal(where(plan, 'CODE-REVIEW-500-r2.md'), `${ARCHIVE_DIR}/v1.76.0/CODE-REVIEW-500-r2.md`);
// #520 — трейлеры в обеих линиях: все раунды в последней.
assert.equal(where(plan, 'SPEC-REVIEW-520-r1.md'), `${ARCHIVE_DIR}/v1.77.0/SPEC-REVIEW-520-r1.md`);
assert.equal(where(plan, 'CODE-REVIEW-520-r1.md'), `${ARCHIVE_DIR}/v1.77.0/CODE-REVIEW-520-r1.md`);
assert.equal(where(plan, 'RELEASE-REVIEW-v1.77.0.md'), `${ARCHIVE_DIR}/v1.77.0/RELEASE-REVIEW-v1.77.0.md`);
assert.ok(plan.moves.every((m) => m.from === `${LIVE_DIR}/${m.name}`));
assert.ok(!plan.moves.some((m) => m.name === 'INDEX.md') && !plan.kept.some((k) => k.name === 'INDEX.md'));
});
test('#682 архив: задача из открытой линии остаётся целиком — её раунды ссылаются на прошлые', () => {
// #540 выпущена в v1.77.0, но у неё есть трейлер и после тега.
const plan = archivePlan({ names, lines, open: [540, 600], through: 'v1.77.0' });
assert.equal(where(plan, 'CODE-REVIEW-540-r1.md'), 'задача есть в открытой линии');
assert.equal(where(plan, 'CODE-REVIEW-540-r2.md'), 'задача есть в открытой линии');
assert.equal(where(plan, 'CODE-REVIEW-600-r1.md'), 'задача есть в открытой линии');
assert.equal(where(plan, 'RELEASE-REVIEW-v1.78.0.md'), 'ревью линии v1.78.0 не входит в архивируемые линии');
});
test('#682 архив: без трейлера — по линии добавления документа, иначе остаётся; чужое имя не трогается', () => {
const bare = archivePlan({ names, lines, open: [600], through: 'v1.77.0' });
assert.equal(where(bare, 'CODE-REVIEW-issue-068-2026-08-12.md'), 'нет трейлера ни в одной линии');
assert.equal(where(bare, 'notes.md'), 'вне схемы имён');
const resolved = archivePlan({
names, lines, open: [600], through: 'v1.77.0',
addedIn: new Map([['CODE-REVIEW-issue-068-2026-08-12.md', 'v1.76.0'], ['notes.md', 'v1.76.0']]),
});
assert.equal(where(resolved, 'CODE-REVIEW-issue-068-2026-08-12.md'), `${ARCHIVE_DIR}/v1.76.0/CODE-REVIEW-issue-068-2026-08-12.md`);
assert.equal(where(resolved, 'notes.md'), 'вне схемы имён');
// Тег добавления вне архивируемых линий (бета, будущая линия) — не повод переносить.
const future = archivePlan({ names, lines, open: [600], through: 'v1.77.0', addedIn: new Map([['CODE-REVIEW-issue-068-2026-08-12.md', 'v1.78.0']]) });
assert.equal(where(future, 'CODE-REVIEW-issue-068-2026-08-12.md'), 'нет трейлера ни в одной линии');
const report = renderPlan({ ...bare, through: 'v1.77.0' });
assert.match(report, /переносится 8 \(задач 3\), остаётся 4/);
assert.match(report, /CODE-REVIEW-issue-068-2026-08-12\.md — нет трейлера/);
});
test('#682 архив: линии — только стабильные теги не новее границы', () => {
assert.deepEqual(
stableTagsThrough(['v1.78.0-beta.1', 'v1.77.0', 'v1.76.0', 'v1.10.0', 'v1.9.0', 'v1.78.0', 'x'], 'v1.77.0'),
['v1.9.0', 'v1.10.0', 'v1.76.0', 'v1.77.0'],
);
assert.throws(() => stableTagsThrough([], 'v1.78.0-beta.1'), /not a stable release tag/);
assert.throws(() => archivePlan({ names: [], lines: [{ tag: 'v1.78.0', issues: [] }], open: [], through: 'v1.77.0' }), /newer than/);
});
// Ревью #682 r1 (Medium): перенос добавляет уровень вложенности, и относительные
// ссылки внутри перенесённых документов и в соседях, которые на них ссылаются,
// ломались молча — ни один гейт не смотрит в архив.
const tree = new Set([
'docs/specs/089-stage1.md',
'docs/reviews/CODE-REVIEW-635-r1.md',
'legacy/reviews/v1.77.0/CODE-REVIEW-594-r1.md',
'legacy/reviews/v1.77.0/CODE-REVIEW-594-r2.md',
'legacy/reviews/v1.68.0/SPEC-REVIEW-262-r1.md',
'legacy/specs/262-readd.md',
]);
const exists = (path) => tree.has(path);
const moved = new Map([
['docs/reviews/CODE-REVIEW-594-r1.md', 'legacy/reviews/v1.77.0/CODE-REVIEW-594-r1.md'],
['docs/reviews/CODE-REVIEW-594-r2.md', 'legacy/reviews/v1.77.0/CODE-REVIEW-594-r2.md'],
['docs/reviews/SPEC-REVIEW-262-r1.md', 'legacy/reviews/v1.68.0/SPEC-REVIEW-262-r1.md'],
['docs/specs/262-readd.md', 'legacy/specs/262-readd.md'],
]);
test('#682 r1 ссылки: перенесённый документ пересчитывает свои ссылки от нового места', () => {
const result = repairLinks({
text: 'ТЗ: [s](../specs/089-stage1.md#ac2); прошлый раунд: [r1](CODE-REVIEW-594-r1.md); сайт: [x](https://example.org/a.md)',
path: 'legacy/reviews/v1.77.0/CODE-REVIEW-594-r2.md',
oldPath: 'docs/reviews/CODE-REVIEW-594-r2.md',
moved, exists,
});
assert.equal(result.text, 'ТЗ: [s](../../../docs/specs/089-stage1.md#ac2); прошлый раунд: [r1](CODE-REVIEW-594-r1.md); сайт: [x](https://example.org/a.md)');
assert.equal(result.fixed, 1);
});
test('#682 r1 ссылки: сосед, ссылавшийся на перенесённый документ, ведёт в архив', () => {
const live = repairLinks({ text: '[r1](CODE-REVIEW-594-r1.md)', path: 'docs/reviews/CODE-REVIEW-635-r1.md', moved, exists });
assert.equal(live.text, '[r1](../../legacy/reviews/v1.77.0/CODE-REVIEW-594-r1.md)');
// Ссылка, уже переписанная прошлым переносом (ТЗ ушло в legacy/specs раньше документа ревью).
const spec = repairLinks({
text: '[r](../../docs/reviews/SPEC-REVIEW-262-r1.md)', path: 'legacy/specs/262-readd.md', oldPath: 'docs/specs/262-readd.md', moved, exists,
});
assert.equal(spec.text, '[r](../reviews/v1.68.0/SPEC-REVIEW-262-r1.md)');
});
test('#682 r1 ссылки: битая и до переноса ссылка не «чинится» наугад', () => {
const result = repairLinks({ text: '[x](...), [y](nowhere.md)', path: 'legacy/reviews/v1.72.0/CODE-REVIEW-448-r2.md', oldPath: 'docs/reviews/CODE-REVIEW-448-r2.md', moved, exists });
assert.equal(result.fixed, 0);
assert.equal(result.text, '[x](...), [y](nowhere.md)');
});
test('#682 r1 архив legacy/: относительные ссылки резолвятся (кроме известных «...»-заглушек)', () => {
const broken = brokenLinks({ roots: ['legacy/reviews', 'legacy/specs'] }).filter((item) => item.target !== '...');
assert.deepEqual(broken, []);
});