mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
Вход агента до первого файла кода стоил ≈ 26 700 слов (аудит 22.09). - docs/process/AUTHOR.md и REVIEWER.md — выжимки PROCESS.md: каждый пункт ссылается на раздел канона, ключевые формулировки дословные; test/process-digests.test.mjs сверяет якоря, ссылки и правила. - scripts/entry-cost.mjs — маршрут чтения по роли и бюджет (автор ≤ 12 000 слов, AC1); AGENTS.md «Read this first» называет те же маршруты. - docs/STATUS.md: блок Snapshot генерирует scripts/status-snapshot.mjs (версии — release-contract, счётчики — inventory, теги — git); feature surface и ранние milestones перенесены дословно в docs/STATUS-FEATURES.md. - docs/TESTING.md — действующая инструкция (684 строки, AC3); ручные чек-листы и приложения по issue перенесены дословно в docs/testing-notes/ с индексом и тестом на полноту. - Промпт ревьюера в process.yml читает конспект вместо пересказа правил; машинные требования (строка вердикта, REVIEW_DOC, запрет fetch, таблица «чем краснеет», разделы повторного раунда) сохранены и закреплены тестом. - PROCESS.md: правила не менялись; добавлены ссылка на конспекты в шапке и уточнение в §10.4, что ревьюер конвейера читает конспект. - 7 мутантов в реестре. Issue: #634 User-Visible: no
116 lines
6.7 KiB
JavaScript
116 lines
6.7 KiB
JavaScript
// #634: таблица Snapshot в docs/STATUS.md генерируется из дерева и git.
|
|
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { dirname, join } from 'node:path';
|
|
import test from 'node:test';
|
|
import {
|
|
BEGIN, END, VERSION_FILES, blockDate, collectSnapshot, latestTags, renderSnapshot, replaceBlock, summarizeVersions,
|
|
} from '../scripts/status-snapshot.mjs';
|
|
|
|
const put = (root, rel, text) => {
|
|
mkdirSync(dirname(join(root, rel)), { recursive: true });
|
|
writeFileSync(join(root, rel), text);
|
|
};
|
|
const git = (root, args, date) => {
|
|
const env = { ...process.env, GIT_AUTHOR_DATE: date, GIT_COMMITTER_DATE: date,
|
|
GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t' };
|
|
const run = spawnSync('git', args, { cwd: root, encoding: 'utf8', env });
|
|
assert.equal(run.status, 0, run.stderr);
|
|
};
|
|
// Фикстура кладёт каждый источник версии по пути из VERSION_FILES — того же
|
|
// списка, по которому читает генератор.
|
|
const writeVersions = (root, version) => {
|
|
const content = {
|
|
packageJson: JSON.stringify({ name: 'x', version }),
|
|
packageLock: JSON.stringify({ version, packages: { '': { version } } }),
|
|
manifest: JSON.stringify({ version }),
|
|
constSource: `DOMAIN = "houseplan"\nVERSION = "${version}"\n`,
|
|
cardSource: `const CARD_VERSION = '${version}';\n`,
|
|
editorRuntimeSource: `const CARD_VERSION = '${version}';\n`,
|
|
};
|
|
assert.deepEqual(Object.keys(content).sort(), Object.keys(VERSION_FILES).sort());
|
|
for (const [key, rel] of Object.entries(VERSION_FILES)) put(root, rel, content[key]);
|
|
};
|
|
function fixture() {
|
|
const root = mkdtempSync(join(tmpdir(), 'hp-status-'));
|
|
writeVersions(root, '1.2.0');
|
|
put(root, 'test/a.test.mjs', "test('a', () => {});\ntest('b', () => {});\n");
|
|
put(root, 'tests_backend/test_pure.py', 'def test_one():\n pass\n');
|
|
put(root, 'tests_backend/test_ha_x.py', 'async def test_ha():\n pass\n');
|
|
put(root, 'demo/smoke_one.mjs', '');
|
|
put(root, 'demo/smoke_two.mjs', '');
|
|
git(root, ['init', '-q']);
|
|
git(root, ['add', '-A']);
|
|
git(root, ['commit', '-qm', 'one'], '2026-09-01T10:00:00Z');
|
|
git(root, ['tag', '-a', 'v1.2.0', '-m', 'stable'], '2026-09-01T10:00:00Z');
|
|
writeVersions(root, '1.3.0-beta.1');
|
|
git(root, ['commit', '-qam', 'two'], '2026-09-02T10:00:00Z');
|
|
git(root, ['tag', '-a', 'v1.3.0-beta.1', '-m', 'beta'], '2026-09-02T10:00:00Z');
|
|
put(root, 'README.md', 'x\n');
|
|
git(root, ['add', '-A']);
|
|
git(root, ['commit', '-qm', 'three'], '2026-09-03T10:00:00Z');
|
|
return root;
|
|
}
|
|
|
|
test('#634 status-snapshot: версии, теги и счётчики берутся из дерева и git', () => {
|
|
const root = fixture();
|
|
try {
|
|
const snapshot = collectSnapshot(root, { date: '2026-09-24' });
|
|
assert.deepEqual(snapshot.tags, { stable: 'v1.2.0', prerelease: 'v1.3.0-beta.1' });
|
|
assert.deepEqual(snapshot.versions, { version: '1.3.0-beta.1', sources: 7, mismatches: [] });
|
|
assert.deepEqual(snapshot.tests.map((row) => row.count), [2, 1, 1, 2]);
|
|
const block = renderSnapshot(snapshot);
|
|
assert.ok(block.startsWith(`${BEGIN}\n`) && block.endsWith(`\n${END}`));
|
|
assert.match(block, /\| Version \| \*\*1\.3\.0-beta\.1\*\* in all 7 version sources/);
|
|
assert.match(block, /\| Latest stable tag \| `v1\.2\.0` \|/);
|
|
assert.match(block, /\| Latest prerelease tag \| `v1\.3\.0-beta\.1` \|/);
|
|
assert.match(block, /\| Tests \| Node unit 2 · pure backend 1 · HA-harness backend 1 · browser smokes 2 /);
|
|
assert.equal(blockDate(block), '2026-09-24');
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('#634 status-snapshot: рассинхрон версий виден в таблице, а не сглажен', () => {
|
|
const sources = { 'package.json': '1.3.0', 'custom_components/houseplan/const.py': '1.2.9', 'package-lock.json': '1.3.0' };
|
|
const summary = summarizeVersions(sources);
|
|
assert.deepEqual(summary, { version: '1.3.0', sources: 3, mismatches: ['custom_components/houseplan/const.py="1.2.9"'] });
|
|
const block = renderSnapshot({ date: '2026-09-24', versions: summary, tags: { stable: null, prerelease: null }, tests: [] });
|
|
assert.match(block, /\| Version \| \*\*1\.3\.0\*\* — NOT synchronized: custom_components\/houseplan\/const\.py="1\.2\.9" \|/);
|
|
assert.match(block, /\| Latest stable tag \| n\/a — tags are not available/);
|
|
});
|
|
|
|
test('#634 status-snapshot: последние теги — по дате создания, стабильный отдельно от пре-релиза', () => {
|
|
assert.deepEqual(latestTags(['v1.78.0-beta.1', 'v1.77.0', 'v1.77.0-beta.5', 'not-a-tag']),
|
|
{ stable: 'v1.77.0', prerelease: 'v1.78.0-beta.1' });
|
|
assert.deepEqual(latestTags([]), { stable: null, prerelease: null });
|
|
});
|
|
|
|
test('#634 status-snapshot: --write заменяет только блок, без маркеров — отказ', () => {
|
|
const block = `${BEGIN}\n| Item | State |\n${END}`;
|
|
const text = `# S\n\n${BEGIN}\nold\n${END}\n\n## Rest\nkept\n`;
|
|
const next = replaceBlock(text, block);
|
|
assert.equal(next, `# S\n\n${block}\n\n## Rest\nkept\n`);
|
|
assert.equal(replaceBlock(next, block), next, 'идемпотентно');
|
|
assert.throws(() => replaceBlock('# S\n', block), /expected exactly one/);
|
|
assert.throws(() => replaceBlock(`${text}${BEGIN}\n${END}\n`, block), /expected exactly one/);
|
|
});
|
|
|
|
test('#634 status-snapshot: docs/STATUS.md несёт один сгенерированный блок той же формы', () => {
|
|
const status = readFileSync(new URL('../docs/STATUS.md', import.meta.url), 'utf8').replace(/\r\n?/g, '\n');
|
|
const start = status.indexOf(BEGIN);
|
|
const end = status.indexOf(END);
|
|
assert.ok(start > 0 && end > start, 'блок на месте');
|
|
assert.equal(status.indexOf(BEGIN, start + 1), -1, 'блок один');
|
|
const committed = status.slice(start, end + END.length);
|
|
const labels = (block) => block.split('\n').filter((line) => line.startsWith('| ')).map((line) => line.split('|')[1].trim());
|
|
const fresh = renderSnapshot({ date: blockDate(committed), versions: { version: 'x', sources: 7, mismatches: [] },
|
|
tags: { stable: null, prerelease: null }, tests: [] });
|
|
assert.deepEqual(labels(committed), labels(fresh), 'строки блока — ровно те, что рисует генератор');
|
|
assert.ok(blockDate(committed), 'дата генерации в блоке');
|
|
// Статус задач живёт только в метках: генератор меток не читает и в прозе их нет.
|
|
assert.doesNotMatch(committed, /S[1-8]-[a-z]/);
|
|
});
|