Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
300 lines
9.4 KiB
JavaScript
300 lines
9.4 KiB
JavaScript
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/
|
|
);
|
|
});
|