import test from "node:test"; import assert from "node:assert/strict"; import { buildFallbackReleaseSummary, classifyCommit, normalizeSectionTitle, normalizeVersion, resolveChannel } from "../../apps/desktop/scripts/generate-release-summary.mjs"; import { GITHUB_RELEASE_BODY_MAX_LENGTH, RELEASE_NOTES_TRUNCATION_NOTICE } from "../../apps/desktop/scripts/lib/githubReleaseBody.mjs"; import { resolvePreviousReleaseTag } from "../../apps/desktop/scripts/lib/previousReleaseTag.mjs"; import { REVIEW_END, REVIEW_START, SECTION_END, SECTION_START, SUGGESTION_START, buildApprovedReleaseSummary, buildUpdatedReleaseBody } from "../../apps/desktop/scripts/upsert-release-summary.mjs"; import { validateReleaseSummary } from "../../apps/desktop/scripts/validate-release-summary.mjs"; function validReleaseSummary(overrides = {}) { return { schemaVersion: "tutti.desktop.release.summary.v1", tag: "v1.2.4-rc.1", version: "1.2.4-rc.1", channel: "rc", prerelease: true, targetCommit: "0123456789abcdef", compare: { from: "v1.2.4-rc.0", range: "v1.2.4-rc.0..HEAD", to: "HEAD" }, generatedAt: "2026-07-21T00:00:00.000Z", summarySource: "fallback", zh: { headline: "本次版本优化发布流程。", sections: [{ title: "发布与更新", items: ["验证候选版本。"] }], qaFocus: ["验证更新。"] }, en: { headline: "This release improves the release flow.", sections: [ { title: "Release and Updates", items: ["Validate the candidate."] } ], qaFocus: ["Verify updates."] }, ...overrides }; } test("desktop release summary classifies commit messages for human sections", () => { assert.equal( classifyCommit("abc1234 feat(update): add channel picker"), "功能变更" ); assert.equal( classifyCommit("abc1234 fix(release): avoid rc latest"), "问题修复" ); assert.equal( classifyCommit("abc1234 chore(release): update desktop workflow"), "发布与更新" ); }); test("desktop release summary maps technical headings to user-facing sections", () => { assert.equal(normalizeSectionTitle("核心功能与架构", "zh"), "功能变更"); assert.equal(normalizeSectionTitle("后端与服务端改进", "zh"), "体验优化"); assert.equal( normalizeSectionTitle("Core Features & Architecture", "en"), "Feature Updates" ); assert.equal( normalizeSectionTitle("Backend & Service Enhancements", "en"), "Experience Improvements" ); }); test("desktop release summary resolves channel from version shape", () => { assert.equal(normalizeVersion("v1.2.4"), "1.2.4"); assert.equal(normalizeVersion("tutti-desktop-v1.2.4-rc.1"), "1.2.4-rc.1"); assert.equal(normalizeVersion("tutti-desktop-v1.2.4-beta.1"), "1.2.4-beta.1"); assert.equal(resolveChannel({ version: "1.2.4" }), "stable"); assert.equal(resolveChannel({ version: "1.2.4-rc.1" }), "rc"); assert.equal(resolveChannel({ version: "1.2.4-beta.1" }), "beta"); }); test("GitHub release notes compare prereleases against the latest same-channel tag", () => { const previousTag = resolvePreviousReleaseTag({ channel: "rc", tag: "v0.2.2-rc.9", tags: ["v0.2.2-rc.9", "v0.2.2-rc.8", "v0.2.2-beta.5", "v0.2.0"], version: "0.2.2-rc.9" }); assert.equal(previousTag, "v0.2.2-rc.8"); }); test("GitHub release notes fall back to the latest stable tag", () => { const previousTag = resolvePreviousReleaseTag({ channel: "beta", tag: "v0.3.0-beta.0", tags: ["v0.3.0-beta.0", "v0.2.2-rc.8", "v0.2.1", "v0.2.0"], version: "0.3.0-beta.0" }); assert.equal(previousTag, "v0.2.1"); }); test("desktop release summary fallback emits zh and en sections", () => { const summary = buildFallbackReleaseSummary({ commits: [ "abc1234 feat(update): add release channel picker", "def5678 fix(release): keep rc out of latest metadata" ] }); assert.equal(summary.source, "fallback"); assert.match(summary.zh.headline, /桌面端/); assert.ok(summary.zh.sections.length >= 2); assert.ok(summary.en.sections.length >= 2); }); test("desktop release summary creates editable review and replaceable suggestion sections", () => { const nextBody = buildUpdatedReleaseBody({ existingBody: [ "## What's Changed", "- Raw GitHub note", "", SECTION_START, "old summary", SECTION_END ].join("\n"), summary: { zh: { headline: "本次版本聚焦发布链路稳定性。", sections: [ { title: "发布与更新", items: ["稳定包入口只指向正式版。"] } ], qaFocus: ["验证下载入口。"] }, en: { headline: "This release focuses on release stability.", sections: [ { title: "Release and Updates", items: ["Stable downloads only point to official releases."] } ], qaFocus: ["Verify the download entry."] } } }); assert.equal(nextBody.match(new RegExp(REVIEW_START, "g"))?.length, 1); assert.equal(nextBody.match(new RegExp(SUGGESTION_START, "g"))?.length, 1); assert.match(nextBody, /本次版本聚焦发布链路稳定性/); assert.match(nextBody, /Stable downloads only point to official releases/); assert.match(nextBody, /Release Notes/); assert.doesNotMatch(nextBody, /QA Focus/); assert.doesNotMatch(nextBody, /Verify the download entry/); assert.match(nextBody, /Raw GitHub note/); assert.doesNotMatch(nextBody, /old summary/); }); test("desktop release summary preserves human review when a candidate is rebuilt", () => { const humanReview = [ REVIEW_START, "## Release Notes", "### 中文", "人工确认标题", "#### 问题修复", "- 人工确认内容", "### English", "Reviewed headline", "#### Bug Fixes", "- Reviewed item", REVIEW_END ].join("\n"); const nextBody = buildUpdatedReleaseBody({ existingBody: `${humanReview}\n\n## What's Changed\n- keep me`, summary: validReleaseSummary({ zh: { headline: "新的机器建议", sections: [{ title: "功能变更", items: ["机器内容"] }], qaFocus: [] } }) }); assert.match(nextBody, /人工确认内容/); assert.match(nextBody, /新的机器建议/); assert.match(nextBody, /keep me/); }); test("approved summary is parsed from the human review instead of generated suggestions", () => { const body = buildUpdatedReleaseBody({ existingBody: "", summary: validReleaseSummary() }).replace("验证候选版本。", "人工筛选后的更新内容"); const approved = buildApprovedReleaseSummary({ body, generatedSummary: validReleaseSummary(), reviewedAt: "2026-08-17T01:00:00.000Z" }); assert.equal(approved.summarySource, "human-reviewed"); assert.deepEqual(approved.zh.sections[0].items, ["人工筛选后的更新内容"]); assert.equal(approved.generatedAt, "2026-08-17T01:00:00.000Z"); }); test("approved summary accepts reviewed items written as plain Markdown lines", () => { const body = [ REVIEW_START, "## Release Notes", "### 中文", "人工确认标题", "#### 功能变更", "默认开启快捷提示词管理", "支持会话分叉", "### English", "Reviewed headline", "#### Feature Changes", "Quick prompt management is enabled by default", "Conversation forking is supported", REVIEW_END ].join("\n"); const approved = buildApprovedReleaseSummary({ body, generatedSummary: validReleaseSummary() }); assert.deepEqual(approved.zh.sections[0].items, [ "默认开启快捷提示词管理", "支持会话分叉" ]); assert.deepEqual(approved.en.sections[0].items, [ "Quick prompt management is enabled by default", "Conversation forking is supported" ]); }); test("desktop release summary trims only generated notes at GitHub's body limit", () => { const oversizedNotes = [ "## What's Changed", ...Array.from( { length: 2_000 }, (_, index) => `- generated note ${index}: ${"x".repeat(80)}` ) ].join("\n"); const nextBody = buildUpdatedReleaseBody({ existingBody: oversizedNotes, summary: validReleaseSummary({ en: { headline: "A concise release summary.", sections: [{ title: "Bug Fixes", items: ["Kept releases reliable."] }], qaFocus: [] } }) }); assert.ok(nextBody.length <= GITHUB_RELEASE_BODY_MAX_LENGTH); assert.match(nextBody, /A concise release summary/); assert.match(nextBody, /generated note 0:/); assert.doesNotMatch(nextBody, /generated note 1999:/); assert.ok(nextBody.includes(RELEASE_NOTES_TRUNCATION_NOTICE)); }); test("desktop release summary validator accepts a complete staged summary", () => { const summary = validReleaseSummary(); assert.equal( validateReleaseSummary(summary, { tag: summary.tag, channel: summary.channel, targetCommit: summary.targetCommit }), summary ); }); test("desktop release summary validator accepts a human-reviewed summary", () => { assert.doesNotThrow(() => validateReleaseSummary( validReleaseSummary({ summarySource: "human-reviewed" }) ) ); }); test("desktop release summary validator rejects inconsistent or incomplete summaries", () => { assert.throws( () => validateReleaseSummary(validReleaseSummary({ prerelease: false })), /prerelease flag/ ); assert.throws( () => validateReleaseSummary(validReleaseSummary({ en: { headline: "" } })), /en.headline/ ); });