import assert from 'node:assert/strict'; import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { parseFrontmatter, runFrontmatterValidation, validateFrontendState, validateFrontendTransition, validateV2Document, } from '../scripts/validate-changelog-frontmatter.mjs'; const FILE = 'changelogs-v2/2026-07/24_5218_工作流治理-修改接口-管理后台.md'; function metadata(overrides = {}) { return { schema: 'hl-changelog/v2', ticket: '5218', title: '工作流治理', consumer: 'admin', change_type: '修改接口', backend_status: 'deployed', gateway_status: 'verified', frontend_status: 'pending', frontend_owner: '', frontend_ref: '', target_release: '', verified_at: '', status_note: '', updated_at: '2026-07-24', base: 'dev-v3', ...overrides, }; } function document(overrides = {}) { const fields = metadata(overrides); const frontmatter = Object.entries(fields) .map(([key, value]) => `${key}: "${value}"`) .join('\n'); return `---\n${frontmatter}\n---\n\n# 工作流治理\n\n## 变更接口\n\n- 无业务接口变化。\n\n## 验证证据\n\n- 自动化测试通过。\n`; } test('parses quoted flat YAML frontmatter', () => { const parsed = parseFrontmatter(document()); assert.equal(parsed.metadata.schema, 'hl-changelog/v2'); assert.equal(parsed.metadata.frontend_status, 'pending'); }); test('accepts a complete v2 handoff with pending frontend consumption', () => { assert.deepEqual(validateV2Document(FILE, document(), { requireV2: true }), []); }); test('rejects backend and gateway pending at publication', () => { const errors = validateV2Document( FILE, document({ backend_status: 'pending', gateway_status: 'pending' }), { requireV2: true }, ); assert.ok(errors.some(({ code }) => code === 'E_BACKEND_PENDING')); assert.ok(errors.some(({ code }) => code === 'E_GATEWAY_PENDING')); }); test('requires frontend evidence as status advances', () => { assert.deepEqual( validateFrontendState(metadata({ frontend_status: 'claimed' })), ['claimed 必须填写 frontend_owner'], ); assert.deepEqual( validateFrontendState(metadata({ frontend_status: 'implemented', frontend_owner: 'frontend-team', frontend_ref: 'mmg/hl-ui@abc1234', })), [], ); assert.ok( validateFrontendState(metadata({ frontend_status: 'verified', frontend_owner: 'frontend-team', frontend_ref: 'mmg/hl-ui@abc1234', target_release: 'prod-2026.07.24', verified_at: '2026-02-30', })).includes('verified_at 必须是 ISO 日期或时间'), ); }); test('rejects skipped transitions and requires a rollback reason', () => { assert.ok(validateFrontendTransition('pending', 'implemented').length > 0); assert.deepEqual(validateFrontendTransition('pending', 'claimed'), []); assert.ok(validateFrontendTransition('released', 'implemented').length > 0); assert.deepEqual( validateFrontendTransition('released', 'implemented', '测试发布已回滚'), [], ); }); test('new changelog requires v2 while a modified legacy file remains compatible', () => { const root = mkdtempSync(path.join(tmpdir(), 'hl-frontmatter-')); try { const file = path.join(root, ...FILE.split('/')); mkdirSync(path.dirname(file), { recursive: true }); writeFileSync(file, '# legacy\n'); const newResult = runFrontmatterValidation([{ status: 'A', targetPath: FILE }], root); assert.ok(newResult.errors.some(({ code }) => code === 'E_FRONTMATTER')); const modifiedResult = runFrontmatterValidation([{ status: 'M', targetPath: FILE }], root); assert.deepEqual(modifiedResult.errors, []); } finally { rmSync(root, { recursive: true, force: true }); } }); test('detects metadata and filename mismatches', () => { const errors = validateV2Document( FILE, document({ ticket: '9999', change_type: '新增接口' }), { requireV2: true }, ); assert.ok(errors.some(({ code }) => code === 'E_TICKET_MISMATCH')); assert.ok(errors.some(({ code }) => code === 'E_TYPE_MISMATCH')); }); test('rejects unsupported consumers and impossible dates', () => { const errors = validateV2Document( FILE, document({ consumer: 'browser', updated_at: '2026-02-30' }), { requireV2: true }, ); assert.ok(errors.some(({ code }) => code === 'E_CONSUMER')); assert.ok(errors.some(({ code }) => code === 'E_UPDATED_AT')); });