fix(changelog): restore stable path aliases for #5252
changelog-filename-gate / validate (pull_request) Successful in 2s
changelog-filename-gate / validate (pull_request) Successful in 2s
这个提交包含在:
@@ -0,0 +1,132 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
loadPathAliasManifest,
|
||||
parsePathAliasManifest,
|
||||
resolveChangelogPath,
|
||||
transitionAliasedFrontendState,
|
||||
validatePathAliases,
|
||||
} from '../scripts/changelog-path-aliases.mjs';
|
||||
import { parseFrontmatter } from '../scripts/validate-changelog-frontmatter.mjs';
|
||||
|
||||
const OLD_5216 = 'changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-前端待处理-管理后台.md';
|
||||
const CURRENT_5216 = 'changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-管理后台.md';
|
||||
|
||||
function document({ canonicalPath = '', status = 'claimed', owner = 'frontend-team', frontendRef = '' } = {}) {
|
||||
const canonicalLine = canonicalPath ? `canonical_path: "${canonicalPath}"\n` : '';
|
||||
return `---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "5216"
|
||||
title: "路径兼容测试"
|
||||
consumer: "admin"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "${status}"
|
||||
frontend_owner: "${owner}"
|
||||
frontend_ref: "${frontendRef}"
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
${canonicalLine}updated_at: "2026-07-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 路径兼容测试
|
||||
|
||||
## 变更接口
|
||||
|
||||
- 无业务接口变化。
|
||||
|
||||
## 验证证据
|
||||
|
||||
- 自动化测试。
|
||||
`;
|
||||
}
|
||||
|
||||
function createFixture() {
|
||||
const root = mkdtempSync(path.join(tmpdir(), 'hl-changelog-alias-'));
|
||||
const manifest = {
|
||||
schema: 'hl-changelog-path-aliases/v1',
|
||||
aliases: [{
|
||||
ticket: '5216',
|
||||
alias: OLD_5216,
|
||||
canonical: CURRENT_5216,
|
||||
reason: '测试旧路径兼容',
|
||||
}],
|
||||
};
|
||||
writeFileSync(path.join(root, 'changelog-path-aliases.json'), JSON.stringify(manifest, null, 2));
|
||||
for (const repoPath of [OLD_5216, CURRENT_5216]) {
|
||||
const absolute = path.join(root, ...repoPath.split('/'));
|
||||
mkdirSync(path.dirname(absolute), { recursive: true });
|
||||
writeFileSync(absolute, document({
|
||||
canonicalPath: repoPath === OLD_5216 ? CURRENT_5216 : '',
|
||||
}));
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
test('repository #5216 compatibility entry resolves to its canonical document', () => {
|
||||
const manifest = loadPathAliasManifest();
|
||||
assert.equal(resolveChangelogPath(OLD_5216, manifest), CURRENT_5216);
|
||||
assert.equal(resolveChangelogPath(CURRENT_5216, manifest), CURRENT_5216);
|
||||
assert.deepEqual(validatePathAliases().errors, []);
|
||||
});
|
||||
|
||||
test('rejects unsafe paths, duplicate aliases and alias chains', () => {
|
||||
assert.throws(() => parsePathAliasManifest(JSON.stringify({
|
||||
schema: 'hl-changelog-path-aliases/v1',
|
||||
aliases: [{
|
||||
ticket: '5216',
|
||||
alias: '../outside.md',
|
||||
canonical: CURRENT_5216,
|
||||
reason: 'unsafe',
|
||||
}],
|
||||
})), /POSIX|规范路径/);
|
||||
assert.throws(() => parsePathAliasManifest(JSON.stringify({
|
||||
schema: 'hl-changelog-path-aliases/v1',
|
||||
aliases: [
|
||||
{ ticket: '5216', alias: OLD_5216, canonical: CURRENT_5216, reason: 'one' },
|
||||
{ ticket: '5216', alias: OLD_5216, canonical: CURRENT_5216, reason: 'two' },
|
||||
],
|
||||
})), /重复/);
|
||||
});
|
||||
|
||||
test('frontend status transition updates alias group once and is idempotent', () => {
|
||||
const root = createFixture();
|
||||
try {
|
||||
const first = transitionAliasedFrontendState(root, OLD_5216, {
|
||||
status: 'implemented',
|
||||
owner: 'frontend-team',
|
||||
frontendRef: 'mmg/hl-ui@abc1234',
|
||||
}, {
|
||||
now: new Date('2026-07-26T04:00:00Z'),
|
||||
write: true,
|
||||
});
|
||||
assert.deepEqual(new Set(first.changedPaths), new Set([OLD_5216, CURRENT_5216]));
|
||||
assert.deepEqual(validatePathAliases(root).errors, []);
|
||||
|
||||
for (const repoPath of [OLD_5216, CURRENT_5216]) {
|
||||
const text = readFileSync(path.join(root, ...repoPath.split('/')), 'utf8');
|
||||
const { metadata } = parseFrontmatter(text);
|
||||
assert.equal(metadata.frontend_status, 'implemented');
|
||||
assert.equal(metadata.frontend_owner, 'frontend-team');
|
||||
assert.equal(metadata.frontend_ref, 'mmg/hl-ui@abc1234');
|
||||
}
|
||||
|
||||
const second = transitionAliasedFrontendState(root, OLD_5216, {
|
||||
status: 'implemented',
|
||||
owner: 'frontend-team',
|
||||
frontendRef: 'mmg/hl-ui@abc1234',
|
||||
}, {
|
||||
now: new Date('2026-07-26T04:00:00Z'),
|
||||
write: true,
|
||||
});
|
||||
assert.deepEqual(second.changedPaths, []);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
collectNewTargetPaths,
|
||||
getShanghaiDate,
|
||||
parseNameStatusZ,
|
||||
pathAliasMapFromManifest,
|
||||
runValidation,
|
||||
validateChangelogPath,
|
||||
} from '../scripts/validate-changelog-filenames.mjs';
|
||||
@@ -114,13 +115,19 @@ test('parses NUL-delimited Chinese and space-containing paths', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('exempts modified and deleted historical bad paths', () => {
|
||||
const records = parseNameStatusZ(Buffer.from(
|
||||
'M\0changelogs-v2/2026-07/63_历史坏文件.md\0D\0changelogs-v2/2026-07/99_无issue.md\0',
|
||||
test('exempts a modified historical bad path but protects a deleted published path', () => {
|
||||
const modified = parseNameStatusZ(Buffer.from(
|
||||
'M\0changelogs-v2/2026-07/63_历史坏文件.md\0',
|
||||
'utf8',
|
||||
));
|
||||
assert.deepEqual(collectNewTargetPaths(records), []);
|
||||
assert.equal(runValidation(records, SHANGHAI_NOW).errors.length, 0);
|
||||
assert.deepEqual(collectNewTargetPaths(modified), []);
|
||||
assert.equal(runValidation(modified, SHANGHAI_NOW).errors.length, 0);
|
||||
|
||||
const deleted = parseNameStatusZ(Buffer.from(
|
||||
'D\0changelogs-v2/2026-07/99_无issue.md\0',
|
||||
'utf8',
|
||||
));
|
||||
assert.ok(runValidation(deleted, SHANGHAI_NOW).errors.some(({ code }) => code === 'E_PATH_STABILITY'));
|
||||
});
|
||||
|
||||
test('validates only a copy target path', () => {
|
||||
@@ -128,9 +135,23 @@ test('validates only a copy target path', () => {
|
||||
assert.deepEqual(collectNewTargetPaths(records), [validAdmin()]);
|
||||
});
|
||||
|
||||
test('accepts a rename from an old bad name to a valid target', () => {
|
||||
const records = parseNameStatusZ(Buffer.from(`R100\0changelogs-v2/2026-07/63_old.md\0${validAdmin()}\0`, 'utf8'));
|
||||
assert.equal(runValidation(records, SHANGHAI_NOW).errors.length, 0);
|
||||
test('rejects a rename without a machine-readable compatibility alias', () => {
|
||||
const source = 'changelogs-v2/2026-07/63_old.md';
|
||||
const records = parseNameStatusZ(Buffer.from(`R100\0${source}\0${validAdmin()}\0`, 'utf8'));
|
||||
assert.ok(runValidation(records, SHANGHAI_NOW).errors.some(({ code }) => code === 'E_PATH_STABILITY'));
|
||||
});
|
||||
|
||||
test('accepts a rename and a legacy alias path when both are registered', () => {
|
||||
const source = 'changelogs-v2/2026-07/63_old.md';
|
||||
const target = validAdmin();
|
||||
const pathAliases = pathAliasMapFromManifest({
|
||||
schema: 'hl-changelog-path-aliases/v1',
|
||||
aliases: [{ alias: source, canonical: target }],
|
||||
});
|
||||
const rename = parseNameStatusZ(Buffer.from(`R100\0${source}\0${target}\0`, 'utf8'));
|
||||
assert.equal(runValidation(rename, SHANGHAI_NOW, { pathAliases }).errors.length, 0);
|
||||
const compatibilityAddition = parseNameStatusZ(Buffer.from(`A\0${source}\0`, 'utf8'));
|
||||
assert.equal(runValidation(compatibilityAddition, SHANGHAI_NOW, { pathAliases }).errors.length, 0);
|
||||
});
|
||||
|
||||
test('rejects a rename target with an old day', () => {
|
||||
|
||||
在新工单中引用
屏蔽一个用户