Skip to content

Commit e8b81b3

Browse files
test(release-notes): add sync and page generation tests
主要变更: - 添加 release-notes-sync.test.mjs - Release 资产发现测试 - Archive 验证测试 - 配置解析测试 - Materialization 幂等性测试 - 添加 release-notes-pages.test.mjs - 双语详情页生成测试 - Landing 页辅助函数测试 Co-Authored-By: Hagicode <[email protected]> Signed-off-by: newbe36524 <[email protected]>
1 parent 7c54d6c commit e8b81b3

2 files changed

Lines changed: 366 additions & 0 deletions

File tree

tests/release-notes-pages.test.mjs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import assert from 'node:assert/strict';
2+
import { mkdtemp, readFile } from 'node:fs/promises';
3+
import path from 'node:path';
4+
import test from 'node:test';
5+
6+
import { materializeReleaseNotes, resolveReleaseNotesConfig } from '../scripts/release-notes-sync-lib.mjs';
7+
import {
8+
getReleaseNotesLandingCopy,
9+
getReleaseNotesLandingEntries,
10+
} from '../src/lib/release-notes.mjs';
11+
12+
function createSnapshot(tag = 'v1.0.0') {
13+
return {
14+
generatedAt: '2026-04-14T10:00:00.000Z',
15+
source: {
16+
repository: 'HagiCode-org/release-notes',
17+
githubApiBaseUrl: 'https://api.github.com',
18+
locales: ['zh-CN', 'en'],
19+
},
20+
entries: [
21+
{
22+
tag,
23+
displayTag: tag,
24+
sortVersion: '1.0.0',
25+
releaseDate: '2026-04-13',
26+
publishedAt: '2026-04-13T08:00:00.000Z',
27+
synchronizedAt: '2026-04-14T10:00:00.000Z',
28+
upstreamGeneratedAt: '2026-04-13T07:55:00.000Z',
29+
summary: {
30+
'zh-CN': '统一了 Code Server 的主要入口。',
31+
en: 'Unified the main Code Server entry flow.',
32+
},
33+
routes: {
34+
'zh-CN': `/release-notes/${tag}/`,
35+
en: `/en/release-notes/${tag}/`,
36+
},
37+
repositoryRanges: [
38+
{
39+
repository: 'web',
40+
path: 'repos/web',
41+
label: 'v0.9.0..v1.0.0',
42+
fromTag: 'v0.9.0',
43+
toTag: tag,
44+
range: 'v0.9.0..v1.0.0',
45+
commitCount: 3,
46+
},
47+
],
48+
totalCommitCount: 3,
49+
source: {
50+
releaseName: tag,
51+
releaseUrl: `https://example.test/releases/${tag}`,
52+
assetName: `release-notes-${tag}-history.zip`,
53+
assetUrl: `https://example.test/assets/${tag}.zip`,
54+
jsonPath: `artifacts/tags/${tag}/${tag}.json`,
55+
bodies: {
56+
'zh-CN': `published/${tag}.zh-CN.md`,
57+
en: `published/${tag}.en.md`,
58+
},
59+
},
60+
bodies: {
61+
'zh-CN': '# HagiCode\n\n- 统一了 Code Server 的主要入口。\n',
62+
en: '# HagiCode\n\n- Unified the main Code Server entry flow.\n',
63+
},
64+
},
65+
],
66+
skipped: [],
67+
counts: {
68+
releases: 1,
69+
discovered: 1,
70+
synchronized: 1,
71+
skipped: 0,
72+
},
73+
};
74+
}
75+
76+
test('materialization writes bilingual detail pages and locale-specific landing routes', async () => {
77+
const repoRoot = await mkdtemp(path.join(process.env.TMPDIR ?? '/tmp', 'docs-release-notes-pages-'));
78+
const config = resolveReleaseNotesConfig({ repoRoot });
79+
const snapshot = createSnapshot('v1.0.0');
80+
81+
await materializeReleaseNotes({ snapshot, config });
82+
83+
const zhDetail = await readFile(path.join(config.outputPaths.zhDir, 'v1.0.0.md'), 'utf8');
84+
const enDetail = await readFile(path.join(config.outputPaths.enDir, 'v1.0.0.md'), 'utf8');
85+
const zhLanding = await readFile(path.join(config.outputPaths.zhDir, 'index.mdx'), 'utf8');
86+
const enLanding = await readFile(path.join(config.outputPaths.enDir, 'index.mdx'), 'utf8');
87+
88+
assert.match(zhDetail, /> : 2026-04-13/);
89+
assert.match(zhDetail, /\[Read English\]\(\/en\/release-notes\/v1\.0\.0\/\)/);
90+
assert.match(enDetail, /> Release date: 2026-04-13/);
91+
assert.match(enDetail, /\[\]\(\/release-notes\/v1\.0\.0\/\)/);
92+
assert.match(zhLanding, /<ReleaseNotesLanding locale="zh-CN" \/>/);
93+
assert.match(enLanding, /<ReleaseNotesLanding locale="en" \/>/);
94+
});
95+
96+
test('landing helpers expose localized summaries and metadata counts', () => {
97+
const snapshot = createSnapshot('v1.0.0');
98+
const zhEntries = getReleaseNotesLandingEntries(snapshot, 'zh-CN');
99+
const enEntries = getReleaseNotesLandingEntries(snapshot, 'en');
100+
const zhCopy = getReleaseNotesLandingCopy('zh-CN');
101+
const enCopy = getReleaseNotesLandingCopy('en');
102+
103+
assert.equal(zhEntries[0].primaryRoute, '/release-notes/v1.0.0/');
104+
assert.equal(zhEntries[0].secondaryRoute, '/en/release-notes/v1.0.0/');
105+
assert.equal(zhEntries[0].summary, '统一了 Code Server 的主要入口。');
106+
assert.equal(zhEntries[0].repositoryCount, 1);
107+
assert.equal(zhEntries[0].totalCommitCount, 3);
108+
assert.equal(zhCopy.readPrimary, '查看中文');
109+
110+
assert.equal(enEntries[0].primaryRoute, '/en/release-notes/v1.0.0/');
111+
assert.equal(enEntries[0].secondaryRoute, '/release-notes/v1.0.0/');
112+
assert.equal(enEntries[0].summary, 'Unified the main Code Server entry flow.');
113+
assert.equal(enCopy.readPrimary, 'Read English');
114+
});

tests/release-notes-sync.test.mjs

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import assert from 'node:assert/strict';
2+
import { spawnSync } from 'node:child_process';
3+
import { mkdtemp, readFile, writeFile } from 'node:fs/promises';
4+
import fs from 'node:fs';
5+
import os from 'node:os';
6+
import path from 'node:path';
7+
import test from 'node:test';
8+
9+
import {
10+
discoverReleaseNoteAssets,
11+
inspectReleaseNoteArchive,
12+
materializeReleaseNotes,
13+
normalizeSynchronizedReleaseNotes,
14+
resolveReleaseNotesConfig,
15+
} from '../scripts/release-notes-sync-lib.mjs';
16+
17+
async function writeFixtureFile(filePath, contents) {
18+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
19+
await writeFile(filePath, contents, 'utf8');
20+
}
21+
22+
async function createArchiveFixture({
23+
tag = 'v1.0.0',
24+
payloadTag = tag,
25+
includeJson = true,
26+
includeZh = true,
27+
includeEn = true,
28+
repositories = {
29+
web: {
30+
path: 'repos/web',
31+
group: {
32+
range: 'v0.9.0..v1.0.0',
33+
commits: [
34+
{
35+
hash: 'abcdef',
36+
shortHash: 'abcdef',
37+
date: '2026-04-13',
38+
author: 'tester',
39+
subject: 'feat: ship release notes',
40+
},
41+
],
42+
},
43+
},
44+
},
45+
} = {}) {
46+
const root = await mkdtemp(path.join(os.tmpdir(), 'docs-release-notes-archive-'));
47+
const sourceRoot = path.join(root, 'source');
48+
const zipPath = path.join(root, `${tag}.zip`);
49+
50+
if (includeJson) {
51+
await writeFixtureFile(
52+
path.join(sourceRoot, 'artifacts', 'tags', tag, `${tag}.json`),
53+
`${JSON.stringify(
54+
{
55+
generatedAt: '2026-04-13T14:14:52.517Z',
56+
formatVersion: 1,
57+
tag: payloadTag,
58+
repositories,
59+
},
60+
null,
61+
2,
62+
)}\n`,
63+
);
64+
}
65+
66+
if (includeZh) {
67+
await writeFixtureFile(
68+
path.join(sourceRoot, 'published', `${tag}.zh-CN.md`),
69+
'# HagiCode\n\n- 统一了入口流程。\n',
70+
);
71+
}
72+
73+
if (includeEn) {
74+
await writeFixtureFile(
75+
path.join(sourceRoot, 'published', `${tag}.en.md`),
76+
'# HagiCode\n\n- Unified the entry flow.\n',
77+
);
78+
}
79+
80+
const result = spawnSync('zip', ['-qr', zipPath, '.'], {
81+
cwd: sourceRoot,
82+
encoding: 'utf8',
83+
});
84+
85+
if ((result.status ?? 0) !== 0) {
86+
throw new Error(`zip failed: ${result.stderr || result.stdout}`);
87+
}
88+
89+
return { root, zipPath };
90+
}
91+
92+
function createReleasePayload() {
93+
return [
94+
{
95+
id: 1,
96+
tag_name: 'v1.0.1',
97+
name: 'v1.0.1',
98+
published_at: '2026-04-14T08:00:00.000Z',
99+
html_url: 'https://example.test/releases/v1.0.1',
100+
assets: [
101+
{
102+
id: 10,
103+
name: 'release-notes-v1.0.1-history.zip',
104+
browser_download_url: 'https://example.test/assets/v1.0.1.zip',
105+
updated_at: '2026-04-14T08:10:00.000Z',
106+
},
107+
{
108+
id: 11,
109+
name: 'notes.txt',
110+
browser_download_url: 'https://example.test/assets/notes.txt',
111+
updated_at: '2026-04-14T08:10:00.000Z',
112+
},
113+
],
114+
},
115+
{
116+
id: 2,
117+
tag_name: '1.0.0',
118+
name: '1.0.0',
119+
published_at: '2026-04-13T08:00:00.000Z',
120+
html_url: 'https://example.test/releases/1.0.0',
121+
assets: [
122+
{
123+
id: 20,
124+
name: 'release-notes-1.0.0-history.zip',
125+
browser_download_url: 'https://example.test/assets/1.0.0.zip',
126+
updated_at: '2026-04-13T08:10:00.000Z',
127+
},
128+
],
129+
},
130+
];
131+
}
132+
133+
test('release discovery keeps only matching history bundle assets', () => {
134+
const discovered = discoverReleaseNoteAssets(createReleasePayload());
135+
136+
assert.deepEqual(
137+
discovered.map((entry) => entry.tag),
138+
['v1.0.1', '1.0.0'],
139+
);
140+
assert.equal(discovered[0].assetName, 'release-notes-v1.0.1-history.zip');
141+
});
142+
143+
test('config prefers explicit cross-repository release-notes tokens', () => {
144+
const config = resolveReleaseNotesConfig({
145+
repoRoot: '/tmp/docs',
146+
env: {
147+
DOCS_RELEASE_NOTES_TOKEN: 'release-notes-token',
148+
DOCS_GITHUB_TOKEN: 'docs-token',
149+
GITHUB_TOKEN: 'github-token',
150+
},
151+
});
152+
153+
assert.equal(config.token, 'release-notes-token');
154+
});
155+
156+
test('archive validation rejects missing required tag json', async () => {
157+
const fixture = await createArchiveFixture({ includeJson: false });
158+
const inspection = inspectReleaseNoteArchive({
159+
zipPath: fixture.zipPath,
160+
candidate: { tag: 'v1.0.0' },
161+
});
162+
163+
assert.equal(inspection.accepted, false);
164+
assert.equal(inspection.reason, 'missing_required_payload');
165+
});
166+
167+
test('archive validation rejects tag mismatches between asset and payload', async () => {
168+
const fixture = await createArchiveFixture({ payloadTag: 'v9.9.9' });
169+
const inspection = inspectReleaseNoteArchive({
170+
zipPath: fixture.zipPath,
171+
candidate: { tag: 'v1.0.0' },
172+
});
173+
174+
assert.equal(inspection.accepted, false);
175+
assert.equal(inspection.reason, 'tag_mismatch');
176+
});
177+
178+
test('archive validation requires both published locales', async () => {
179+
const fixture = await createArchiveFixture({ includeEn: false });
180+
const inspection = inspectReleaseNoteArchive({
181+
zipPath: fixture.zipPath,
182+
candidate: { tag: 'v1.0.0' },
183+
});
184+
185+
assert.equal(inspection.accepted, false);
186+
assert.equal(inspection.reason, 'missing_locale_body');
187+
assert.equal(inspection.locale, 'en');
188+
});
189+
190+
test('normalization preserves display tags, sorts semver consistently, and materialization is idempotent', async () => {
191+
const firstFixture = await createArchiveFixture({ tag: '1.0.0' });
192+
const secondFixture = await createArchiveFixture({ tag: 'v1.0.1' });
193+
const firstAccepted = inspectReleaseNoteArchive({
194+
zipPath: firstFixture.zipPath,
195+
candidate: {
196+
tag: '1.0.0',
197+
assetName: 'release-notes-1.0.0-history.zip',
198+
assetDownloadUrl: 'https://example.test/assets/1.0.0.zip',
199+
releasePublishedAt: '2026-04-13T08:00:00.000Z',
200+
releaseHtmlUrl: 'https://example.test/releases/1.0.0',
201+
releaseName: '1.0.0',
202+
},
203+
});
204+
const secondAccepted = inspectReleaseNoteArchive({
205+
zipPath: secondFixture.zipPath,
206+
candidate: {
207+
tag: 'v1.0.1',
208+
assetName: 'release-notes-v1.0.1-history.zip',
209+
assetDownloadUrl: 'https://example.test/assets/v1.0.1.zip',
210+
releasePublishedAt: '2026-04-14T08:00:00.000Z',
211+
releaseHtmlUrl: 'https://example.test/releases/v1.0.1',
212+
releaseName: 'v1.0.1',
213+
},
214+
});
215+
216+
assert.equal(firstAccepted.accepted, true);
217+
assert.equal(secondAccepted.accepted, true);
218+
219+
const repoRoot = await mkdtemp(path.join(os.tmpdir(), 'docs-release-notes-materialize-'));
220+
const config = resolveReleaseNotesConfig({ repoRoot });
221+
const snapshot = {
222+
...normalizeSynchronizedReleaseNotes({
223+
acceptedEntries: [firstAccepted, secondAccepted],
224+
config,
225+
synchronizedAt: '2026-04-14T10:00:00.000Z',
226+
}),
227+
skipped: [],
228+
counts: {
229+
releases: 2,
230+
discovered: 2,
231+
synchronized: 2,
232+
skipped: 0,
233+
},
234+
};
235+
236+
assert.deepEqual(
237+
snapshot.entries.map((entry) => entry.tag),
238+
['v1.0.1', '1.0.0'],
239+
);
240+
241+
const firstRun = await materializeReleaseNotes({ snapshot, config });
242+
const firstIndex = await readFile(config.outputPaths.indexJson, 'utf8');
243+
const firstZh = await readFile(path.join(config.outputPaths.zhDir, 'v1.0.1.md'), 'utf8');
244+
245+
const secondRun = await materializeReleaseNotes({ snapshot, config });
246+
const secondIndex = await readFile(config.outputPaths.indexJson, 'utf8');
247+
const secondZh = await readFile(path.join(config.outputPaths.zhDir, 'v1.0.1.md'), 'utf8');
248+
249+
assert.equal(firstIndex, secondIndex);
250+
assert.equal(firstZh, secondZh);
251+
assert.deepEqual(firstRun.writtenFiles, secondRun.writtenFiles);
252+
});

0 commit comments

Comments
 (0)