Files
tutti/tools/scripts/desktop-release-config.test.mjs
agent-radar 2ed3415fb7
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
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

1624 lines
60 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import { access, readFile } from "node:fs/promises";
import { inflateSync } from "node:zlib";
const desktopPackagePath = new URL(
"../../apps/desktop/package.json",
import.meta.url
);
const workspaceRootPackagePath = new URL("../../package.json", import.meta.url);
const workflowPath = new URL(
"../../.github/workflows/desktop-release.yml",
import.meta.url
);
const promoteWorkflowPath = new URL(
"../../.github/workflows/desktop-release-promote.yml",
import.meta.url
);
const changelogRepairWorkflowPath = new URL(
"../../.github/workflows/desktop-release-changelog-repair.yml",
import.meta.url
);
const buildScriptPath = new URL(
"../../tools/scripts/build-desktop-package.sh",
import.meta.url
);
const windowsAlphaWorkflowPath = new URL(
"../../.github/workflows/windows-desktop-alpha.yml",
import.meta.url
);
const storeWorkflowPath = new URL(
"../../.github/workflows/desktop-store-submit.yml",
import.meta.url
);
const managedPosixShellVendorScriptPath = new URL(
"../../apps/desktop/scripts/vendor-managed-posix-shell.mjs",
import.meta.url
);
const managedPosixShellLockPath = new URL(
"../../config/tutti.managed-posix-shell.lock.json",
import.meta.url
);
const mutagenVendorScriptPath = new URL(
"../../apps/desktop/scripts/vendor-mutagen.mjs",
import.meta.url
);
const mutagenLockPath = new URL(
"../../config/tutti.mutagen.lock.json",
import.meta.url
);
const managedUVVendorScriptPath = new URL(
"../../apps/desktop/scripts/vendor-managed-uv.mjs",
import.meta.url
);
const rtkVendorScriptPath = new URL(
"../../apps/desktop/scripts/vendor-rtk.mjs",
import.meta.url
);
const tuttidManagerPath = new URL(
"../../apps/desktop/src/main/daemon/tuttidManager.ts",
import.meta.url
);
const claudeSidecarVendorScriptPath = new URL(
"../../apps/desktop/scripts/vendor-claude-sdk-sidecar.mjs",
import.meta.url
);
const electronViteConfigPath = new URL(
"../../apps/desktop/electron.vite.config.ts",
import.meta.url
);
const browserNodeGuestPreloadPath = new URL(
"../../apps/desktop/src/preload/entries/browserNodeGuest.ts",
import.meta.url
);
const loopbackPreviewProxyPath = new URL(
"../../packages/browser/workbench-node/src/electron-main/loopbackPreviewProxy.ts",
import.meta.url
);
const desktopBuildIconPath = new URL(
"../../apps/desktop/build/icon.png",
import.meta.url
);
const desktopStoreManifestPath = new URL(
"../../apps/desktop/build/appxmanifest.xml",
import.meta.url
);
const desktopStoreAssetDimensions = new Map([
["StoreLogo.png", [50, 50]],
["Square44x44Logo.png", [44, 44]],
["Square150x150Logo.png", [150, 150]],
["Wide310x150Logo.png", [310, 150]]
]);
function readPngDimensions(buffer) {
assert.equal(buffer.subarray(1, 4).toString("ascii"), "PNG");
assert.equal(buffer.subarray(12, 16).toString("ascii"), "IHDR");
return [buffer.readUInt32BE(16), buffer.readUInt32BE(20)];
}
function readPngAlphaRange(buffer) {
assert.equal(buffer.readUInt8(24), 8, "Store assets must use 8-bit PNG data");
assert.equal(buffer.readUInt8(25), 6, "Store assets must use RGBA PNG data");
assert.equal(
buffer.readUInt8(28),
0,
"Interlaced Store assets are unsupported"
);
const [width, height] = readPngDimensions(buffer);
const chunks = [];
let offset = 8;
while (offset < buffer.length) {
const chunkLength = buffer.readUInt32BE(offset);
const chunkType = buffer.subarray(offset + 4, offset + 8).toString("ascii");
if (chunkType === "IDAT") {
chunks.push(buffer.subarray(offset + 8, offset + 8 + chunkLength));
}
offset += chunkLength + 12;
if (chunkType === "IEND") break;
}
const decoded = inflateSync(Buffer.concat(chunks));
const bytesPerPixel = 4;
const rowLength = width * bytesPerPixel;
let decodedOffset = 0;
let previous = Buffer.alloc(rowLength);
let minAlpha = 255;
let maxAlpha = 0;
for (let y = 0; y < height; y += 1) {
const filter = decoded[decodedOffset];
decodedOffset += 1;
const current = Buffer.alloc(rowLength);
for (let x = 0; x < rowLength; x += 1) {
const encoded = decoded[decodedOffset];
decodedOffset += 1;
const left = x >= bytesPerPixel ? current[x - bytesPerPixel] : 0;
const above = previous[x];
const upperLeft = x >= bytesPerPixel ? previous[x - bytesPerPixel] : 0;
let predictor = 0;
if (filter === 1) predictor = left;
else if (filter === 2) predictor = above;
else if (filter === 3) predictor = Math.floor((left + above) / 2);
else if (filter === 4) {
const estimate = left + above - upperLeft;
const leftDistance = Math.abs(estimate - left);
const aboveDistance = Math.abs(estimate - above);
const upperLeftDistance = Math.abs(estimate - upperLeft);
predictor =
leftDistance <= aboveDistance && leftDistance <= upperLeftDistance
? left
: aboveDistance <= upperLeftDistance
? above
: upperLeft;
} else {
assert.equal(filter, 0, `Unsupported PNG row filter ${filter}`);
}
current[x] = (encoded + predictor) & 0xff;
}
for (let x = 3; x < rowLength; x += bytesPerPixel) {
minAlpha = Math.min(minAlpha, current[x]);
maxAlpha = Math.max(maxAlpha, current[x]);
}
previous = current;
}
return [minAlpha, maxAlpha];
}
test("desktop package includes runtime outputs without repository source", async () => {
const packageJson = JSON.parse(await readFile(desktopPackagePath, "utf8"));
assert.deepEqual(packageJson.build.files, ["out/**", "package.json"]);
});
test("desktop release workflow uses the published desktop package name", async () => {
const packageJson = JSON.parse(await readFile(desktopPackagePath, "utf8"));
const workflow = await readFile(workflowPath, "utf8");
const packageName = packageJson.name;
assert.equal(typeof packageName, "string");
assert.match(packageName, /^@tutti-os\/desktop$/);
const filterMatches = [
...workflow.matchAll(
/pnpm --filter (\S+) build:(?:mac:unsigned|mac:signed|win|linux)/g
)
];
assert.ok(
filterMatches.length > 0,
"desktop release workflow should invoke package-scoped build commands"
);
for (const [, filterName] of filterMatches) {
assert.equal(
filterName,
packageName,
`desktop release workflow filter should stay aligned with ${packageName}`
);
}
});
test("desktop release AWS CLI bootstrap avoids apt mirrors and bounds downloads", async () => {
const workflows = [
["release", await readFile(workflowPath, "utf8")],
["promotion", await readFile(promoteWorkflowPath, "utf8")]
];
for (const [name, workflow] of workflows) {
const installStep = workflow.match(
/ - name: Install AWS CLI\n[\s\S]*?(?=\n - name:)/
)?.[0];
assert.ok(installStep, `${name} workflow must install AWS CLI`);
assert.doesNotMatch(
installStep,
/sudo apt-get (?:update|install)/,
`${name} workflow must not depend on an apt mirror to install AWS CLI`
);
assert.match(
installStep,
/- name: Install AWS CLI\n\s+timeout-minutes: 5/,
`${name} workflow must bound the complete AWS CLI bootstrap step`
);
assert.match(installStep, /--retry 3/);
assert.match(installStep, /--retry-all-errors/);
assert.match(installStep, /--connect-timeout 10/);
assert.match(installStep, /--max-time 180/);
assert.match(installStep, /python3 -m zipfile -e/);
}
});
test("desktop release submits only stable builds to an isolated Store workflow", async () => {
const workflow = await readFile(workflowPath, "utf8");
const storeWorkflow = await readFile(storeWorkflowPath, "utf8");
assert.match(workflow, /submit-store:/);
assert.match(
workflow,
/needs\.resolve\.outputs\.release_channel == 'stable'/
);
assert.match(
workflow,
/vars\.TUTTI_WINDOWS_STORE_SUBMISSION_ENABLED == 'true'/
);
assert.doesNotMatch(workflow, /publication_mode|draft_only/);
assert.match(workflow, /needs:\s*\[resolve, promote\]/);
assert.match(
workflow,
/uses:\s+\.\/\.github\/workflows\/desktop-store-submit\.yml/
);
assert.match(workflow, /store_environment:\s+microsoft-store-production/);
assert.doesNotMatch(
workflow,
/needs:\s*\[resolve, build-macos, build-windows, submit-store\]/
);
assert.match(storeWorkflow, /workflow_call:/);
assert.match(storeWorkflow, /workflow_dispatch:/);
const stableTagPattern = storeWorkflow.match(/-notmatch '([^']+)'/)?.[1];
assert.ok(stableTagPattern, "Store workflow should validate its stable tag");
const stableTagRegex = new RegExp(stableTagPattern);
assert.equal(stableTagRegex.exec("v1.2.3")?.groups?.version, "1.2.3");
assert.equal(stableTagRegex.exec("v0.0.0")?.groups?.version, "0.0.0");
assert.equal(stableTagRegex.test("v1.2.3-rc.1"), false);
assert.match(
storeWorkflow,
/uses:\s+microsoft\/microsoft-store-apppublisher@v1\.1/
);
assert.match(storeWorkflow, /msstore reconfigure/);
assert.match(storeWorkflow, /msstore publish/);
assert.match(storeWorkflow, /ChangeExtension\(\$appxPath, '\.msix'\)/);
assert.doesNotMatch(storeWorkflow, /msstore submission poll/);
assert.match(storeWorkflow, /TUTTI_STORE_IDENTITY_NAME/);
assert.match(storeWorkflow, /TUTTI_STORE_PUBLISHER/);
assert.match(
storeWorkflow,
/'TUTTI_STORE_APPLICATION_ID',[\s\S]*?'TUTTI_STORE_DISPLAY_NAME'/
);
assert.match(storeWorkflow, /Store package display name mismatch/);
assert.match(storeWorkflow, /Store application id mismatch/);
assert.match(storeWorkflow, /Store executable mismatch/);
assert.match(storeWorkflow, /Store entry point mismatch/);
assert.match(storeWorkflow, /Installed application display name mismatch/);
assert.match(
storeWorkflow,
/application tile background must be transparent/
);
assert.match(storeWorkflow, /application must appear in the Start menu/);
assert.match(storeWorkflow, /does not declare the Tutti desktop shortcut/);
assert.match(storeWorkflow, /Store desktop shortcut path mismatch/);
assert.match(storeWorkflow, /Store desktop shortcut icon mismatch/);
assert.match(storeWorkflow, /Store package did not use branded asset/);
for (const assetName of desktopStoreAssetDimensions.keys()) {
assert.match(storeWorkflow, new RegExp(assetName.replaceAll(".", "\\.")));
}
assert.match(storeWorkflow, /@Name='tutti'/);
assert.match(storeWorkflow, /@Name='runFullTrust'/);
assert.match(storeWorkflow, /Get-FileHash .* -Algorithm SHA256/);
});
test("desktop Store packaging reuses the Windows payload and emits AppX only", async () => {
const packageJson = JSON.parse(await readFile(desktopPackagePath, "utf8"));
const buildScript = await readFile(buildScriptPath, "utf8");
const storeManifest = await readFile(desktopStoreManifestPath, "utf8");
assert.equal(
packageJson.description,
"Where people and agents build in tune."
);
assert.equal(
packageJson.scripts["build:win:store"],
"bash ../../tools/scripts/build-desktop-package.sh win-store"
);
assert.equal(packageJson.build.appx.electronUpdaterAware, false);
assert.equal(packageJson.build.appx.minVersion, "10.0.17763.0");
assert.equal(packageJson.build.appx.maxVersionTested, "10.0.26100.0");
assert.equal(
packageJson.build.appx.customManifestPath,
"build/appxmanifest.xml"
);
assert.deepEqual(packageJson.build.appx.capabilities, ["runFullTrust"]);
assert.match(
storeManifest,
/<Properties>[\s\S]*?<DisplayName>\$\{displayName\}<\/DisplayName>/
);
assert.match(storeManifest, /<uap:VisualElements[\s\S]*?DisplayName="Tutti"/);
assert.match(storeManifest, /BackgroundColor="transparent"/);
assert.match(storeManifest, /AppListEntry="default"/);
assert.match(
storeManifest,
/xmlns:desktop7="http:\/\/schemas\.microsoft\.com\/appx\/manifest\/desktop\/windows10\/7"/
);
assert.match(storeManifest, /IgnorableNamespaces="desktop7"/);
assert.match(
storeManifest,
/<desktop7:Extension Category="windows\.shortcut">[\s\S]*?File="\$\(Desktop\)\\Tutti\.lnk"[\s\S]*?Icon="app\\Tutti\.exe"/
);
assert.doesNotMatch(storeManifest, /\$\{extensions\}/);
assert.deepEqual(packageJson.build.win.protocols, [
{
name: "Tutti login callback",
schemes: ["tutti"]
}
]);
assert.match(buildScript, /win\|win-unpacked\|win-store/);
assert.match(buildScript, /electron-builder --win appx --x64/);
assert.match(buildScript, /TUTTI_STORE_IDENTITY_NAME/);
assert.match(buildScript, /TUTTI_STORE_PUBLISHER/);
assert.match(
buildScript,
/win-store\)\s*\n\s*run_timed_phase "electron_builder_win_store" run_electron_builder_win_store/
);
});
test("desktop Store packaging provides branded assets for every manifest tile", async () => {
for (const [fileName, expectedDimensions] of desktopStoreAssetDimensions) {
const assetPath = new URL(
`../../apps/desktop/build/appx/${fileName}`,
import.meta.url
);
const asset = await readFile(assetPath);
assert.deepEqual(
readPngDimensions(asset),
expectedDimensions,
`${fileName} should use the dimensions expected by electron-builder`
);
assert.deepEqual(
readPngAlphaRange(asset),
[0, 255],
`${fileName} should preserve fully transparent and fully opaque pixels`
);
}
});
test("desktop release workflow publishes rc tags as prereleases and keeps stable tags as latest", async () => {
const workflow = await readFile(workflowPath, "utf8");
assert.match(workflow, /push:\s*\n\s*tags:\s*\n\s*-\s*"v\*"/);
assert.doesNotMatch(workflow, /-\s*"tutti-desktop-v\*"/);
assert.match(workflow, /default:\s*patch_rc_release/);
assert.match(
workflow,
/release_mode:[\s\S]*?options:\s*\r?\n\s*-\s*unsigned_dry_run\r?\n\s*-\s*patch_beta_release\r?\n\s*-\s*patch_rc_release\r?\n\s*-\s*patch_release\r?\n\s*-\s*minor_release\r?\n\s*-\s*major_release\r?\n\s*-\s*explicit_version_release/
);
assert.match(
workflow,
/prerelease:\s+\${{\s*needs\.resolve\.outputs\.release_prerelease\s*==\s*'true'\s*}}/
);
assert.match(
workflow,
/make_latest:\s+\${{\s*needs\.resolve\.outputs\.release_make_latest\s*==\s*'true'\s*}}/
);
assert.match(
workflow,
/release_channel:\s+\${{\s*steps\.release\.outputs\.release_channel\s*}}/
);
assert.match(workflow, /patch_beta_release\)\s*\n\s*strategy=patch_beta/);
});
test("desktop release workflow gates manual rc and stable modes by release branch", async () => {
const workflow = await readFile(workflowPath, "utf8");
assert.match(workflow, /name:\s+Validate release dispatch branch/);
assert.match(
workflow,
/RELEASE_EVENT_NAME:\s+\${{\s*github\.event_name\s*}}/
);
assert.match(workflow, /RELEASE_REF_NAME:\s+\${{\s*github\.ref_name\s*}}/);
assert.match(workflow, /RELEASE_REF_TYPE:\s+\${{\s*github\.ref_type\s*}}/);
assert.match(
workflow,
/if \[\[ "\$\{RELEASE_EVENT_NAME\}" != "workflow_dispatch" \]\]; then/
);
assert.match(
workflow,
/patch_rc_release\|patch_release\|minor_release\|major_release\)/
);
assert.match(workflow, /patch_release\|minor_release\|major_release\)/);
assert.match(workflow, /main\|release\/\*/);
assert.match(
workflow,
/git ls-remote --heads origin "refs\/heads\/release\/\*"/
);
assert.match(
workflow,
/Stable desktop release modes must run from a release\/\* branch while release branches exist\./
);
});
test("desktop promotion requires the managed app runtime release first", async () => {
const promoteWorkflow = await readFile(promoteWorkflowPath, "utf8");
const checkoutIndex = promoteWorkflow.indexOf(
"name: Checkout release target"
);
const runtimeGateIndex = promoteWorkflow.indexOf(
"name: Verify managed app runtime is published"
);
const downloadIndex = promoteWorkflow.indexOf(
"name: Download staged GitHub release assets"
);
assert.ok(checkoutIndex >= 0, "promotion should checkout the release target");
assert.ok(
runtimeGateIndex > checkoutIndex,
"runtime gate should read the lock from the release target"
);
assert.ok(
downloadIndex > runtimeGateIndex,
"runtime gate should pass before promotion begins"
);
assert.match(
promoteWorkflow,
/node tools\/scripts\/verify-tutti-app-runtime-release\.mjs/
);
});
test("desktop release checks the managed app runtime before reserving a tag or building", async () => {
const workflow = await readFile(workflowPath, "utf8");
const runtimeGateIndex = workflow.indexOf(
"name: Verify managed app runtime is published before release build"
);
const releaseTagIndex = workflow.indexOf("name: Resolve release tag");
const buildIndex = workflow.indexOf("build-macos:");
assert.ok(
runtimeGateIndex >= 0,
"release should include an early runtime gate"
);
assert.ok(
runtimeGateIndex < releaseTagIndex,
"runtime gate should pass before reserving a release tag"
);
assert.ok(
runtimeGateIndex < buildIndex,
"runtime gate should pass before desktop builds start"
);
assert.match(workflow, /if: steps\.mode\.outputs\.dry_run != 'true'/);
assert.match(
workflow,
/node tools\/scripts\/verify-tutti-app-runtime-release\.mjs/
);
});
test("desktop release workflow schedules a daily Beijing 4:16am rc release", async () => {
const workflow = await readFile(workflowPath, "utf8");
assert.match(workflow, /schedule:\s*\n\s*-\s*cron:\s*"16 20 \* \* \*"/);
assert.doesNotMatch(workflow, /timezone:\s*"Asia\/Shanghai"/);
assert.match(
workflow,
/RELEASE_EVENT_NAME:\s+\${{\s*github\.event_name\s*}}/
);
assert.match(
workflow,
/if \[\[ "\$\{RELEASE_EVENT_NAME\}" == "schedule" \]\]; then\s*\n\s*strategy=patch_rc/
);
});
test("desktop release workflow keeps less common rc bumps behind explicit version input", async () => {
const workflow = await readFile(workflowPath, "utf8");
assert.match(workflow, /minor_rc_release\)/);
assert.match(workflow, /major_rc_release\)/);
assert.doesNotMatch(workflow, /tag_name:\s*\n/);
});
test("desktop release workflow defers stable tags but still reserves prerelease tags", async () => {
const workflow = await readFile(workflowPath, "utf8");
assert.match(workflow, /^concurrency:\s*$/m);
assert.match(workflow, /cancel-in-progress:\s+false/);
assert.match(workflow, /apps\/desktop\/scripts\/reserve-release-tag\.mjs/);
assert.match(workflow, /release_candidate=true/);
assert.match(workflow, /release_channel.*stable/);
assert.match(workflow, /reserve_args=.*--strategy explicit_tag/);
assert.match(workflow, /release_candidate != 'true'/);
});
test("desktop release workflow passes tsh-aligned Feishu card context", async () => {
const workflow = await readFile(workflowPath, "utf8");
assert.match(
workflow,
/FEISHU_WEBHOOK_URL:\s+\${{\s*secrets\.FEISHU_RELEASE_WEBHOOK_URL\s*}}/
);
assert.match(workflow, /RELEASE_ACTOR:\s+\${{\s*github\.actor\s*}}/);
assert.match(
workflow,
/RELEASE_BRANCH:\s+\${{\s*github\.ref_type == 'branch' && github\.ref_name \|\| ''\s*}}/
);
assert.match(
workflow,
/TUTTI_DESKTOP_RELEASE_ASSETS_BASE_URL:\s+\${{\s*vars\.TUTTI_DESKTOP_RELEASE_ASSETS_BASE_URL\s*}}/
);
assert.match(
workflow,
/release_url:\s*\${{\s*needs\.resolve\.outputs\.release_candidate\s*==\s*'true'[\s\S]*steps\.resolve-candidate-release-url\.outputs\.release_url/
);
assert.match(
workflow,
/RELEASE_ID:\s+\${{\s*steps\.stage-candidate-release\.outputs\.release_id\s*}}/
);
assert.match(
workflow,
/gh api "repos\/\${GITHUB_REPOSITORY}\/releases\/\${RELEASE_ID}" --jq \.html_url/
);
assert.ok(
workflow.indexOf("name: Resolve current stable GitHub draft URL") >
workflow.indexOf(
"name: Update release notes with summary and direct downloads"
),
"the candidate draft URL should be refreshed after release notes are updated"
);
assert.match(
workflow,
/id:\s+stage-release\s*\n\s*name:\s+Stage GitHub release assets/
);
assert.match(
workflow,
/RELEASE_URL:\s+\${{\s*needs\.stage\.outputs\.release_url\s*}}/
);
assert.doesNotMatch(workflow, /RELEASE_ASSET_DIRECTORY:\s+release-assets/);
});
test("desktop release workflow scopes generated notes to the previous release tag", async () => {
const workflow = await readFile(workflowPath, "utf8");
const previousTagIndex = workflow.indexOf(
"name: Resolve previous GitHub release tag"
);
const stageIndex = workflow.indexOf("name: Stage GitHub release assets");
assert.notEqual(
previousTagIndex,
-1,
"the previous release tag should be resolved"
);
assert.ok(
previousTagIndex < stageIndex,
"the previous release tag should be resolved first"
);
assert.match(
workflow,
/node apps\/desktop\/scripts\/resolve-previous-release-tag\.mjs/
);
assert.match(workflow, /generate_release_notes:\s+true/);
assert.match(
workflow,
/previous_tag:\s+\${{\s*steps\.previous-release-tag\.outputs\.tag\s*}}/
);
});
test("desktop release workflow defaults Feishu notifications on outside manual dispatch", async () => {
const workflow = await readFile(workflowPath, "utf8");
assert.doesNotMatch(
workflow,
/notify_feishu=\${{\s*inputs\.notify_feishu\s*!=\s*false\s*}}/
);
assert.match(
workflow,
/notify_feishu=\${{\s*github\.event_name\s*!=\s*'workflow_dispatch'\s*\|\|\s*inputs\.notify_feishu\s*!=\s*false\s*}}/
);
});
test("desktop release post-stage jobs tolerate skipped optional dependencies", async () => {
const workflow = await readFile(workflowPath, "utf8");
const promoteJob = workflow.match(
/promote:[\s\S]*?(?=\n\s{2}[a-z][a-z0-9_-]+:\n|$)/
)?.[0];
const notifyJob = workflow.match(
/notify-candidate-feishu:[\s\S]*?(?=\n\s{2}[a-z][a-z0-9_-]+:\n|$)/
)?.[0];
assert.ok(promoteJob, "promote job should exist");
assert.ok(notifyJob, "candidate notify job should exist");
assert.match(promoteJob, /if:\s+\${{\s*always\(\)\s*&&/);
assert.match(promoteJob, /needs\.stage\.result\s*==\s*'success'/);
assert.match(notifyJob, /if:\s+\${{\s*always\(\)\s*&&/);
assert.match(notifyJob, /needs\.stage\.result\s*==\s*'success'/);
});
test("desktop release workflow does not redownload release assets for Feishu", async () => {
const workflow = await readFile(workflowPath, "utf8");
const notifyJobMatch = workflow.match(
/notify-candidate-feishu:[\s\S]*?(?=\n\s{2}[a-z][a-z0-9_-]+:\n|$)/
);
assert.ok(notifyJobMatch, "candidate notify job should exist");
const notifyJob = notifyJobMatch[0];
const checkoutIndex = notifyJob.indexOf("name: Checkout notification script");
const setupNodeIndex = notifyJob.indexOf("name: Setup Node.js");
const summaryIndex = notifyJob.indexOf("name: Download release summary");
const sendIndex = notifyJob.indexOf("name: Send candidate release card");
assert.notEqual(checkoutIndex, -1, "notify job should checkout the script");
assert.notEqual(setupNodeIndex, -1, "notify job should setup Node.js");
assert.notEqual(summaryIndex, -1, "notify job should download the summary");
assert.notEqual(sendIndex, -1, "notify job should send the release card");
assert.equal(notifyJob.indexOf("name: Download built artifacts"), -1);
assert.equal(notifyJob.indexOf("RELEASE_ASSET_DIRECTORY"), -1);
assert.ok(checkoutIndex < summaryIndex);
assert.ok(setupNodeIndex < summaryIndex);
assert.ok(summaryIndex < sendIndex);
});
test("desktop promotion workflow does not redownload release assets for Feishu", async () => {
const promoteWorkflow = await readFile(promoteWorkflowPath, "utf8");
const notifyJobMatch = promoteWorkflow.match(
/notify-feishu:[\s\S]*?(?=\n\s{2}[a-z][a-z0-9_-]+:\n|$)/
);
assert.ok(notifyJobMatch, "promotion notify job should exist");
assert.doesNotMatch(
notifyJobMatch[0],
/Download promoted release assets|RELEASE_ASSET_DIRECTORY/
);
assert.match(notifyJobMatch[0], /name:\s+Download release summary/);
assert.match(notifyJobMatch[0], /name:\s+Send release card/);
});
test("desktop promotion notification tolerates skipped optional approval", async () => {
const promoteWorkflow = await readFile(promoteWorkflowPath, "utf8");
const notifyJob = promoteWorkflow.match(
/notify-feishu:[\s\S]*?(?=\n\s{2}[a-z][a-z0-9_-]+:\n|$)/
)?.[0];
assert.ok(notifyJob, "promotion notify job should exist");
assert.match(notifyJob, /if:\s+\${{\s*always\(\)\s*&&/);
assert.match(notifyJob, /needs\.promote\.result\s*==\s*'success'/);
});
test("desktop promotion sends the canonical tag URL instead of a transient draft URL", async () => {
const promoteWorkflow = await readFile(promoteWorkflowPath, "utf8");
assert.match(
promoteWorkflow,
/release_url:\s+\${{\s*github\.server_url\s*}}\/\${{\s*github\.repository\s*}}\/releases\/tag\/\${{\s*needs\.resolve\.outputs\.release_tag\s*}}/
);
assert.doesNotMatch(
promoteWorkflow,
/release_url="\$\(jq -r \.html_url release\.json\)"/
);
assert.doesNotMatch(
promoteWorkflow,
/release_url:\s+\${{\s*needs\.resolve\.outputs\.release_url\s*}}/
);
assert.match(
promoteWorkflow,
/RELEASE_URL:\s+\${{\s*needs\.promote\.outputs\.release_url\s*}}/
);
});
test("desktop release workflow can mirror release assets to S3 and upsert direct download links", async () => {
const workflow = await readFile(workflowPath, "utf8");
const promoteWorkflow = await readFile(promoteWorkflowPath, "utf8");
assert.match(
workflow,
/permissions:\s*\n\s*contents:\s*write\s*\n\s*id-token:\s*write/
);
assert.match(workflow, /Upload immutable draft assets to AWS S3/);
assert.match(workflow, /aws-actions\/configure-aws-credentials@v4/);
assert.match(
workflow,
/TUTTI_DESKTOP_RELEASE_ASSETS_BASE_URL=https:\/\/\${TUTTI_DESKTOP_RELEASE_ASSETS_S3_BUCKET}\.s3-accelerate\.amazonaws\.com\/\${TUTTI_DESKTOP_RELEASE_ASSETS_S3_PREFIX%\/}/
);
assert.match(
`${workflow}\n${promoteWorkflow}`,
/apps\/desktop\/scripts\/upsert-release-download-links\.mjs/
);
assert.match(promoteWorkflow, /Build public release pointer/);
assert.match(
promoteWorkflow,
/apps\/desktop\/scripts\/build-release-latest\.mjs/
);
assert.match(
promoteWorkflow,
/aws s3 cp release-latest\.json "\$\{s3_root\}\/latest\.json"/
);
});
test("desktop release workflow only publishes root latest metadata for stable releases", async () => {
const workflow = await readFile(workflowPath, "utf8");
const promoteWorkflow = await readFile(promoteWorkflowPath, "utf8");
assert.doesNotMatch(workflow, /publication_mode|draft_only/);
assert.match(
workflow,
/needs\.stage\.result == 'success' && needs\.resolve\.outputs\.release_candidate != 'true'/
);
assert.doesNotMatch(workflow, /channels\/rc\/latest\.json/);
assert.doesNotMatch(workflow, /aws s3 cp release-latest\.json/);
assert.match(
promoteWorkflow,
/if \[\[ "\$\{TUTTI_DESKTOP_RELEASE_CHANNEL\}" == "stable" \]\]; then[\s\S]*"\$\{s3_root\}\/latest\.json"/
);
assert.match(
promoteWorkflow,
/channels\/preview\/latest\.json[\s\S]*channels\/rc\/latest\.json/
);
assert.match(promoteWorkflow, /channels\/beta\/latest\.json/);
});
test("desktop release workflow publishes immutable updater files before channel pointers", async () => {
const workflow = await readFile(workflowPath, "utf8");
const promoteWorkflow = await readFile(promoteWorkflowPath, "utf8");
const draftAssetsIndex = workflow.indexOf(
"name: Upload immutable draft assets to AWS S3"
);
const promoteAssetsIndex = promoteWorkflow.indexOf(
"name: Upload immutable release assets to AWS S3"
);
const pointerBuildIndex = promoteWorkflow.indexOf(
"name: Build public release pointer"
);
const pointerUploadIndex = promoteWorkflow.indexOf(
"name: Publish release pointer to AWS S3"
);
assert.ok(draftAssetsIndex >= 0, "draft assets should upload immutably");
assert.ok(promoteAssetsIndex >= 0, "promotion should repair missing assets");
assert.ok(pointerBuildIndex > promoteAssetsIndex);
assert.ok(pointerUploadIndex > pointerBuildIndex);
assert.match(promoteWorkflow, /channels\/rc\/latest\.json/);
assert.match(promoteWorkflow, /--cache-control "public, max-age=60"/);
});
test("desktop package uses the CloudFront generic updater provider", async () => {
const packageJson = JSON.parse(await readFile(desktopPackagePath, "utf8"));
assert.deepEqual(packageJson.build.publish, [
{
provider: "generic",
url: "https://d1x7gb6wqsqmnm.cloudfront.net/tutti-desktop-release-assets"
}
]);
});
test("desktop release workflow generates summaries and stable changelog metadata", async () => {
const workflow = await readFile(workflowPath, "utf8");
const promoteWorkflow = await readFile(promoteWorkflowPath, "utf8");
const releaseWorkflows = `${workflow}\n${promoteWorkflow}`;
assert.match(releaseWorkflows, /Generate desktop release summary/);
assert.match(
releaseWorkflows,
/apps\/desktop\/scripts\/generate-release-summary\.mjs/
);
assert.match(releaseWorkflows, /secrets\.AGNES_API_KEY/);
assert.match(releaseWorkflows, /Upload desktop release summary artifact/);
assert.match(releaseWorkflows, /Update release notes with summary/);
assert.match(
releaseWorkflows,
/apps\/desktop\/scripts\/upsert-release-summary\.mjs/
);
assert.match(promoteWorkflow, /Update stable changelog metadata/);
assert.match(
promoteWorkflow,
/apps\/desktop\/scripts\/upsert-release-changelog\.mjs/
);
assert.match(
promoteWorkflow,
/grep -Eq "\(404\|NoSuchKey\|Not Found\)" changelog-download\.err/
);
assert.match(
promoteWorkflow,
/"schemaVersion":"tutti\.desktop\.changelog\.v1"/
);
assert.match(promoteWorkflow, /"\$\{s3_root\}\/changelog\.json"/);
assert.match(releaseWorkflows, /Download release summary/);
assert.match(
releaseWorkflows,
/RELEASE_SUMMARY_PATH:\s+release-summary\/release-summary\.json/
);
});
test("desktop changelog repair restores one published stable summary without moving release pointers", async () => {
const repairWorkflow = await readFile(changelogRepairWorkflowPath, "utf8");
assert.match(repairWorkflow, /workflow_dispatch:/);
assert.match(repairWorkflow, /release_tag:/);
assert.match(
repairWorkflow,
/permissions:\s*\n\s*contents:\s*read\s*\n\s*id-token:\s*write/
);
assert.match(repairWorkflow, /group:\s*desktop-release-promotion/);
assert.match(
repairWorkflow,
/\^v\(0\|\[1-9\]\[0-9\]\*\)\\\.\(0\|\[1-9\]\[0-9\]\*\)\\\.\(0\|\[1-9\]\[0-9\]\*\)\$/
);
assert.match(repairWorkflow, /isDraft/);
assert.match(repairWorkflow, /isPrerelease/);
assert.match(repairWorkflow, /--pattern release-summary\.json/);
assert.match(repairWorkflow, /--pattern SHA256SUMS\.txt/);
assert.match(repairWorkflow, /sha256sum --check --strict/);
assert.match(
repairWorkflow,
/apps\/desktop\/scripts\/validate-release-summary\.mjs/
);
assert.match(repairWorkflow, /gh release view .* --json body --jq \.body/);
assert.match(
repairWorkflow,
/apps\/desktop\/scripts\/extract-approved-release-summary\.mjs/
);
assert.match(
repairWorkflow,
/apps\/desktop\/scripts\/upsert-release-changelog\.mjs/
);
assert.match(
repairWorkflow,
/existing-changelog\.json[\s\\]*approved-release-summary\.json[\s\\]*changelog\.json/
);
assert.match(repairWorkflow, /Repair removed existing changelog entry/);
assert.match(repairWorkflow, /cmp changelog\.json published-changelog\.json/);
assert.doesNotMatch(repairWorkflow, /latest\.json/);
assert.doesNotMatch(repairWorkflow, /gh release edit/);
assert.doesNotMatch(repairWorkflow, /aws s3 sync/);
});
test("desktop promotion consumes the checksummed summary staged with the draft", async () => {
const workflow = await readFile(workflowPath, "utf8");
const promoteWorkflow = await readFile(promoteWorkflowPath, "utf8");
const summaryIndex = workflow.indexOf(
"name: Generate desktop release summary"
);
const validationIndex = workflow.indexOf(
"name: Validate desktop release summary"
);
const checksumIndex = workflow.indexOf("name: Generate checksums");
const draftIndex = workflow.indexOf("name: Stage GitHub release assets");
assert.notEqual(summaryIndex, -1);
assert.notEqual(validationIndex, -1);
assert.notEqual(checksumIndex, -1);
assert.notEqual(draftIndex, -1);
assert.ok(summaryIndex < validationIndex);
assert.ok(validationIndex < checksumIndex);
assert.ok(checksumIndex < draftIndex);
assert.match(workflow, /--output release-assets\/release-summary\.json/);
assert.match(
workflow,
/validate-release-summary\.mjs release-assets\/release-summary\.json/
);
assert.match(promoteWorkflow, /release-assets\/release-summary\.json/);
assert.doesNotMatch(
promoteWorkflow,
/Generate desktop release summary|secrets\.AGNES_API_KEY/
);
});
test("desktop release workflow keeps prereleases as drafts and reserves the public list for stable", async () => {
const workflow = await readFile(workflowPath, "utf8");
const promoteWorkflow = await readFile(promoteWorkflowPath, "utf8");
const stageJobMatch = workflow.match(
/stage:[\s\S]*?(?=\n\s{2}[a-z][a-z0-9_-]+:\n|$)/
);
assert.ok(stageJobMatch, "stage job should exist");
const stageJob = stageJobMatch[0];
const stageIndex = stageJob.indexOf("name: Stage GitHub release assets");
const s3Index = stageJob.indexOf(
"name: Upload immutable draft assets to AWS S3"
);
const publishIndex = promoteWorkflow.indexOf(
"name: Publish stable GitHub release"
);
const archiveIndex = promoteWorkflow.indexOf(
"name: Archive public GitHub prereleases"
);
const stableAliasIndex = promoteWorkflow.indexOf(
"name: Refresh stable release alias"
);
assert.notEqual(stageIndex, -1, "release assets should be staged");
assert.notEqual(s3Index, -1, "draft assets should be uploaded to AWS");
assert.notEqual(
publishIndex,
-1,
"stable release should be published explicitly"
);
assert.notEqual(
archiveIndex,
-1,
"legacy public prereleases should be archived"
);
assert.notEqual(stableAliasIndex, -1, "stable release alias should refresh");
assert.match(stageJob, /draft:\s*true/);
assert.doesNotMatch(stageJob, /latest\.json/);
assert.doesNotMatch(stageJob, /--draft=false/);
assert.match(
workflow,
/uses:\s+\.\/\.github\/workflows\/desktop-release-promote\.yml/
);
assert.doesNotMatch(workflow, /publication_mode|draft_only/);
assert.match(
promoteWorkflow,
/if:\s*\$\{\{\s*needs\.resolve\.outputs\.release_channel\s*==\s*'stable'\s*\}\}/
);
assert.match(
promoteWorkflow,
/gh release edit "\$\{TUTTI_DESKTOP_RELEASE_TAG\}" --draft=false --latest/
);
assert.match(
promoteWorkflow,
/gh api "repos\/\$\{GITHUB_REPOSITORY\}\/releases\?per_page=100" --paginate/
);
assert.match(
promoteWorkflow,
/select\(\.prerelease and \(\.draft \| not\)\) \| \.id/
);
assert.match(
promoteWorkflow,
/gh api --method PATCH "repos\/\$\{GITHUB_REPOSITORY\}\/releases\/\$\{release_id\}"[\s\\]*-F draft=true/
);
assert.ok(stageIndex < s3Index);
assert.ok(publishIndex < archiveIndex);
assert.ok(archiveIndex < stableAliasIndex);
});
test("desktop promotion validates draft identity, checksums, and channel ordering", async () => {
const promoteWorkflow = await readFile(promoteWorkflowPath, "utf8");
assert.match(promoteWorkflow, /workflow_call:/);
assert.match(promoteWorkflow, /workflow_dispatch:/);
assert.match(promoteWorkflow, /group:\s+desktop-release-promotion/);
assert.match(
promoteWorkflow,
/\.tag_name == \\"\$\{release_tag\}\\" or \.tag_name == \\"\$\{release_candidate_tag\}\\"/
);
assert.match(
promoteWorkflow,
/git fetch --force origin "refs\/tags\/\$\{release_tag\}:refs\/tags\/\$\{release_tag\}"/
);
assert.match(promoteWorkflow, /sha256sum --check SHA256SUMS\.txt/);
assert.match(promoteWorkflow, /name:\s+Prevent release channel rollback/);
assert.match(promoteWorkflow, /Refusing to move/);
assert.match(promoteWorkflow, /name:\s+Verify public release pointer/);
});
test("stable candidates require environment approval and bind the reviewed notes", async () => {
const workflow = await readFile(workflowPath, "utf8");
const promoteWorkflow = await readFile(promoteWorkflowPath, "utf8");
assert.match(workflow, /build-release-candidate-manifest\.mjs/);
assert.match(workflow, /upload-github-release-assets\.mjs/);
assert.doesNotMatch(workflow, /gh release upload/);
assert.match(workflow, /release_candidate_tag="candidate-\$\{release_tag\}"/);
assert.match(workflow, /releases\/assets\/\$\{asset_id\}/);
assert.match(
workflow,
/candidates\/\$\{TUTTI_DESKTOP_RELEASE_CANDIDATE_ID\}/
);
assert.match(workflow, /PROMOTION_URL:/);
assert.match(promoteWorkflow, /environment:\s+desktop-stable-release/);
assert.match(promoteWorkflow, /extract-approved-release-summary\.mjs/);
assert.match(promoteWorkflow, /verify-release-candidate\.mjs/);
assert.match(
promoteWorkflow,
/RELEASE_TAG="\$\{release_tag\}" RELEASE_TARGET="\$\{draft_target_sha\}"/
);
assert.match(
promoteWorkflow,
/Release notes or candidate assets changed after approval/
);
assert.match(promoteWorkflow, /Create stable release tag after approval/);
assert.match(promoteWorkflow, /Protect immutable stable asset path/);
assert.match(promoteWorkflow, /\.promotion-candidate\.json/);
});
test("stable promotion can resume after the GitHub release becomes public", async () => {
const promoteWorkflow = await readFile(promoteWorkflowPath, "utf8");
const publishIndex = promoteWorkflow.indexOf(
"name: Publish stable GitHub release"
);
const verifyPointerIndex = promoteWorkflow.indexOf(
"name: Verify public release pointer"
);
const cleanupIndex = promoteWorkflow.indexOf(
"name: Remove internal candidate manifest from published release"
);
assert.match(
promoteWorkflow,
/\(\.draft \| not\) and \.tag_name == \\"\$\{release_tag\}\\" and \(\.prerelease \| not\)/
);
assert.match(
promoteWorkflow,
/release_already_published=true[\s\S]*release_already_published=\$\{release_already_published\}/
);
assert.match(
promoteWorkflow,
/Attach approved draft to the stable tag[\s\S]*release_already_published != 'true'/
);
assert.ok(publishIndex >= 0, "stable publish step should exist");
assert.ok(
verifyPointerIndex > publishIndex,
"public pointer verification should remain retryable after publication"
);
assert.ok(
cleanupIndex > verifyPointerIndex,
"candidate recovery metadata should be removed only after all fallible promotion checks"
);
});
test("desktop release workflow refreshes the stable alias without taking Latest", async () => {
const promoteWorkflow = await readFile(promoteWorkflowPath, "utf8");
const stableAliasStep = promoteWorkflow.match(
/- name: Refresh stable release alias[\s\S]*?(?=\n\s{6}- name:|\n\s{2}[a-z][a-z0-9_-]+:\n|$)/
)?.[0];
assert.ok(stableAliasStep, "stable alias step should exist");
assert.match(stableAliasStep, /TUTTI_DESKTOP_RELEASE_CHANNEL/);
assert.match(stableAliasStep, /gh release list/);
assert.match(stableAliasStep, /--exclude-drafts/);
assert.match(stableAliasStep, /--exclude-pre-releases/);
assert.match(stableAliasStep, /\.tagName != "stable"/);
assert.match(
stableAliasStep,
/select\(\.tagName != "stable" and \(\.tagName \| test\("\^v\[0-9\]\+\\\\\.\[0-9\]\+\\\\\.\[0-9\]\+\$"\)\)\)\]/
);
assert.match(
stableAliasStep,
/apps\/desktop\/scripts\/build-stable-release-alias-body\.mjs/
);
assert.match(
stableAliasStep,
/stable_tree="\$\(git rev-parse "\$\{stable_sha\}\^\{tree\}"\)"/
);
assert.match(stableAliasStep, /GIT_AUTHOR_NAME="github-actions\[bot\]"/);
assert.match(
stableAliasStep,
/GIT_AUTHOR_EMAIL="41898282\+github-actions\[bot\]@users\.noreply\.github\.com"/
);
assert.match(stableAliasStep, /GIT_AUTHOR_DATE="\$\{stable_alias_time\}"/);
assert.match(stableAliasStep, /GIT_COMMITTER_NAME="github-actions\[bot\]"/);
assert.match(
stableAliasStep,
/GIT_COMMITTER_EMAIL="41898282\+github-actions\[bot\]@users\.noreply\.github\.com"/
);
assert.match(
stableAliasStep,
/Signed-off-by: github-actions\[bot\] <41898282\+github-actions\[bot\]@users\.noreply\.github\.com>/
);
assert.match(
stableAliasStep,
/git commit-tree "\$\{stable_tree\}" -p "\$\{stable_sha\}"/
);
assert.match(
stableAliasStep,
/stable_alias_tree="\$\(git rev-parse "\$\{stable_alias_sha\}\^\{tree\}"\)"/
);
assert.match(
stableAliasStep,
/stable_alias_parent="\$\(git rev-parse "\$\{stable_alias_sha\}\^"\)"/
);
assert.match(stableAliasStep, /Stable alias tree mismatch:/);
assert.match(stableAliasStep, /Stable alias parent mismatch:/);
assert.match(stableAliasStep, /git tag -f stable "\$\{stable_alias_sha\}"/);
assert.doesNotMatch(stableAliasStep, /git tag -f stable "\$\{stable_sha\}"/);
assert.match(stableAliasStep, /git push origin refs\/tags\/stable --force/);
assert.match(stableAliasStep, /gh release delete stable --yes/);
assert.doesNotMatch(stableAliasStep, /--cleanup-tag/);
assert.doesNotMatch(stableAliasStep, /git push origin :refs\/tags\/stable/);
assert.doesNotMatch(stableAliasStep, /git tag -a/);
assert.match(stableAliasStep, /gh release create stable/);
assert.match(stableAliasStep, /--verify-tag/);
assert.match(stableAliasStep, /--title "Stable \(Recommended\)"/);
assert.doesNotMatch(stableAliasStep, /--target "\$\{stable_sha\}"/);
assert.match(stableAliasStep, /--latest=false/);
assert.match(stableAliasStep, /gh release edit "\$\{stable_tag\}" --latest/);
const aliasCommitIndex = stableAliasStep.indexOf("git commit-tree");
const tagPushIndex = stableAliasStep.indexOf(
"git push origin refs/tags/stable --force"
);
const releaseDeleteIndex = stableAliasStep.indexOf(
"gh release delete stable --yes"
);
const releaseCreateIndex = stableAliasStep.indexOf(
"gh release create stable"
);
assert.ok(aliasCommitIndex < tagPushIndex);
assert.ok(tagPushIndex < releaseDeleteIndex);
assert.ok(releaseDeleteIndex < releaseCreateIndex);
});
test("desktop release signs Windows before staging release assets", async () => {
const workflow = await readFile(workflowPath, "utf8");
const stageJobMatch = workflow.match(
/stage:[\s\S]*?(?=\n\s{2}[a-z][a-z0-9_-]+:\n|$)/
);
const notifyJobMatch = workflow.match(
/notify-candidate-feishu:[\s\S]*?(?=\n\s{2}[a-z][a-z0-9_-]+:\n|$)/
);
assert.ok(stageJobMatch, "stage job should exist");
assert.ok(notifyJobMatch, "candidate notify job should exist");
assert.doesNotMatch(workflow, /include_windows/);
assert.match(workflow, /\r?\n\s{2}build-windows:\r?\n/);
assert.match(workflow, /\r?\n\s{2}sign-windows:\r?\n/);
assert.match(workflow, /\r?\n\s{2}verify-signed-windows:\r?\n/);
assert.doesNotMatch(workflow, /\r?\n\s{2}build-linux:\r?\n/);
assert.match(
workflow,
/build-windows:[\s\S]*?CSC_IDENTITY_AUTO_DISCOVERY:\s+"false"[\s\S]*?build:win/
);
assert.match(
stageJobMatch[0],
/needs:\s+\[resolve, build-macos, build-windows, sign-windows, verify-signed-windows\]/
);
assert.match(stageJobMatch[0], /always\(\)/);
assert.match(
stageJobMatch[0],
/needs\.build-windows\.result\s*==\s*'success'/
);
assert.match(
stageJobMatch[0],
/needs\.sign-windows\.result\s*==\s*'success'/
);
assert.match(
stageJobMatch[0],
/needs\.verify-signed-windows\.result\s*==\s*'success'/
);
assert.doesNotMatch(
stageJobMatch[0],
/needs\.build-windows\.result\s*==\s*'skipped'/
);
assert.match(
stageJobMatch[0],
/pattern:\s+tutti-desktop-release-assets-macos-\*/
);
assert.match(
stageJobMatch[0],
/name:\s+tutti-desktop-release-assets-windows/
);
assert.match(stageJobMatch[0], /name:\s+Add Windows release artifacts/);
assert.match(
stageJobMatch[0],
/validate-windows-release-artifacts\.mjs release-assets/
);
assert.match(workflow, /group:\s+certum-simplysign-signing/);
assert.match(
workflow,
/CERTUM_USER_ID:\s+\$\{\{ secrets\.CERTUM_USER_ID \}\}/
);
assert.match(
workflow,
/CERTUM_OTP_URI:\s+\$\{\{ secrets\.CERTUM_OTP_URI \}\}/
);
assert.match(
workflow,
/CERTUM_CERT_FINGERPRINT:\s+\$\{\{ vars\.CERTUM_CERT_FINGERPRINT \}\}/
);
assert.match(
workflow,
/TSH_WINDOWS_EXPECTED_PUBLISHER:\s+\$\{\{ vars\.TSH_WINDOWS_EXPECTED_PUBLISHER \}\}/
);
assert.match(workflow, /build:win:unpacked/);
assert.match(workflow, /build:win:prepackaged/);
assert.match(workflow, /verify-certum-windows-package\.ps1/);
assert.match(
stageJobMatch[0],
/upsert-release-download-links\.mjs[\s\S]*?release-assets[\s\S]*?updated-release-body\.md/
);
assert.match(
stageJobMatch[0],
/export RELEASE_TAG="\$\{TUTTI_DESKTOP_RELEASE_TAG\}"/
);
assert.match(stageJobMatch[0], /merge-multiple:\s+false/);
assert.doesNotMatch(
notifyJobMatch[0],
/pattern:\s+tutti-desktop-release-assets-macos/
);
});
test("desktop promotion verifies Windows artifacts in the release and S3 mirror", async () => {
const promoteWorkflow = await readFile(promoteWorkflowPath, "utf8");
assert.match(
promoteWorkflow,
/Verify staged release assets[\s\S]*?\*-win-x64\.exe[\s\S]*?\*-win-x64\.exe\.blockmap[\s\S]*?windows_metadata/
);
assert.match(
promoteWorkflow,
/name:\s+Validate Windows updater metadata[\s\S]*?validate-windows-release-artifacts\.mjs/
);
assert.match(
promoteWorkflow,
/name:\s+Verify mirrored Windows release assets/
);
assert.match(promoteWorkflow, /aws s3api head-object/);
assert.match(promoteWorkflow, /--query ContentLength/);
assert.match(promoteWorkflow, /Mirrored asset size mismatch/);
assert.match(promoteWorkflow, /curl --fail --silent --show-error --location/);
assert.match(promoteWorkflow, /Mirrored asset checksum mismatch/);
});
test("desktop release workflow materializes macOS signing certificate before packaging", async () => {
const workflow = await readFile(workflowPath, "utf8");
assert.match(workflow, /name:\s+Prepare macOS signing certificate/);
assert.match(
workflow,
/MACOS_CSC_LINK:\s+\${{\s*secrets\.MACOS_CSC_LINK\s*}}/
);
assert.match(
workflow,
/certificate_path="\$\{RUNNER_TEMP\}\/macos-codesign-certificate\.p12"/
);
assert.match(
workflow,
/echo "CSC_LINK=\$\{certificate_path\}" >> "\$\{GITHUB_ENV\}"/
);
assert.doesNotMatch(
workflow,
/Build release artifacts[\s\S]*?CSC_LINK:\s+\${{\s*secrets\.MACOS_CSC_LINK\s*}}[\s\S]*?pnpm --filter @tutti-os\/desktop build:mac:signed/
);
});
test("desktop release macOS builds reserve enough Node heap for the renderer bundle", async () => {
const workflow = await readFile(workflowPath, "utf8");
assert.match(
workflow,
/Build release artifacts[\s\S]*?NODE_OPTIONS:\s+--max-old-space-size=4096[\s\S]*?pnpm --filter @tutti-os\/desktop build:mac:signed/
);
});
test("desktop macOS packaging builds architecture-specific and universal artifacts", async () => {
const buildScript = await readFile(buildScriptPath, "utf8");
const claudeSidecarVendorScript = await readFile(
claudeSidecarVendorScriptPath,
"utf8"
);
const packageJson = JSON.parse(await readFile(desktopPackagePath, "utf8"));
assert.match(packageJson.build.artifactName, /\$\{arch\}/);
assert.match(buildScript, /GOOS=darwin\s+GOARCH=arm64\s+go build/);
assert.match(buildScript, /GOOS=darwin\s+GOARCH=amd64\s+go build/);
assert.match(buildScript, /lipo\s+-create/);
assert.match(
buildScript,
/lipo\s+"\$\{output_path\}"\s+-verify_arch\s+arm64\s+x86_64\s+\|\|\s+\{/
);
assert.match(buildScript, /MAC_ARCH="\$\{TUTTI_DESKTOP_MAC_ARCH:-all\}"/);
assert.match(buildScript, /MAC_ARCH_ARGS=\(--x64 --arm64 --universal\)/);
assert.match(buildScript, /MAC_ARCH_ARGS=\(--x64\)/);
assert.match(buildScript, /MAC_ARCH_ARGS=\(--arm64\)/);
assert.match(buildScript, /MAC_ARCH_ARGS=\(--universal\)/);
assert.match(
buildScript,
/electron-builder --mac "\$\{MAC_ARCH_ARGS\[@\]\}"/
);
// The native claude binaries are provisioned at runtime by tuttid
// (services/tuttid/service/agentstatus/claude_binary.go); the vendored
// sidecar bundle must stay JS-only so every architecture ships identical
// resources and no Mach-O merging exemption is needed.
assert.doesNotMatch(buildScript, /--include-darwin-native-packages/);
assert.match(claudeSidecarVendorScript, /--omit=optional/);
assert.doesNotMatch(claudeSidecarVendorScript, /"pack"/);
assert.equal(
packageJson.build.mac.x64ArchFiles,
undefined,
"the sidecar bundle carries no native binaries, so no x64ArchFiles exemption may exist"
);
});
test("desktop release workflow opts JavaScript actions into Node 24", async () => {
const workflow = await readFile(workflowPath, "utf8");
assert.match(workflow, /FORCE_JAVASCRIPT_ACTIONS_TO_NODE24:\s*true/);
assert.doesNotMatch(workflow, /actions\/checkout@v4/);
assert.doesNotMatch(workflow, /actions\/setup-node@v4/);
assert.doesNotMatch(workflow, /actions\/setup-go@v5/);
assert.doesNotMatch(workflow, /actions\/upload-artifact@v4/);
assert.doesNotMatch(workflow, /pnpm\/action-setup@v4/);
});
test("desktop package declares the workspace package manager for electron-builder", async () => {
const packageJson = JSON.parse(await readFile(desktopPackagePath, "utf8"));
assert.equal(packageJson.packageManager, "pnpm@10.11.0");
});
test("desktop release pins the supported Electron platform contract", async () => {
const packageJson = JSON.parse(await readFile(desktopPackagePath, "utf8"));
const workflow = await readFile(workflowPath, "utf8");
assert.equal(packageJson.devDependencies.electron, "^43.2.0");
assert.equal(packageJson.build.mac.minimumSystemVersion, "12.0.0");
assert.match(workflow, /name:\s+Install Electron runtime/);
assert.match(
workflow,
/run:\s+pnpm --filter @tutti-os\/desktop exec install-electron/
);
});
test("desktop package verifies channel-specific prerelease updater metadata", async () => {
const packageJson = JSON.parse(await readFile(desktopPackagePath, "utf8"));
const workflow = await readFile(workflowPath, "utf8");
assert.equal(packageJson.build?.generateUpdatesFilesForAllChannels, true);
assert.match(workflow, /Verify prerelease updater metadata/);
assert.match(
workflow,
/needs\.resolve\.outputs\.release_channel\s*!=\s*'stable'/
);
assert.match(
workflow,
/updater_metadata="apps\/desktop\/dist\/\$\{TUTTI_DESKTOP_RELEASE_CHANNEL\}-mac\.yml"/
);
assert.match(workflow, /matrix:\s*\n\s*arch:\s*\[x64, arm64, universal\]/);
assert.match(
workflow,
/name:\s+Merge macOS release artifacts and updater metadata/
);
assert.match(
workflow,
/node apps\/desktop\/scripts\/merge-macos-release-artifacts\.mjs/
);
assert.doesNotMatch(workflow, /cp apps\/desktop\/dist\/latest-mac\.yml/);
});
test("desktop package uses a distinct product identity from tsh desktop", async () => {
const packageJson = JSON.parse(await readFile(desktopPackagePath, "utf8"));
assert.equal(packageJson.productName, "Tutti");
assert.equal(packageJson.build.productName, "Tutti");
assert.equal(packageJson.build.executableName, "Tutti");
});
test("desktop package ships ws as a runtime dependency for packaged main-process imports", async () => {
const packageJson = JSON.parse(await readFile(desktopPackagePath, "utf8"));
assert.equal(
packageJson.dependencies.ws,
"^8.21.0",
"packaged desktop apps need ws in production dependencies because the main process requires it at runtime"
);
assert.equal(
packageJson.devDependencies.ws,
undefined,
"ws should not live only in devDependencies or packaged apps will miss it"
);
});
test("desktop release docs describe same-architecture macOS updater preference", async () => {
const releaseDocsPath = new URL(
"../../docs/conventions/desktop-release.md",
import.meta.url
);
const releaseDocs = await readFile(releaseDocsPath, "utf8");
assert.match(
releaseDocs,
/macOS auto-update metadata must keep x64, arm64, and universal zip entries/
);
assert.match(
releaseDocs,
/electron-updater should download the same-architecture zip first/
);
});
test("desktop electron-vite config bundles ws into the packaged main process", async () => {
const electronViteConfig = await readFile(electronViteConfigPath, "utf8");
assert.match(
electronViteConfig,
/exclude:\s*\[[\s\S]*"ws"[\s\S]*\]/,
"ws must stay excluded from runtime externalization so packaged apps bundle it into the main-process output"
);
});
test("desktop electron-vite config disables bundled ws optional native dependencies", async () => {
const electronViteConfig = await readFile(electronViteConfigPath, "utf8");
assert.match(
electronViteConfig,
/const\s+bundledWsDefines\s*=\s*\{[\s\S]*process\.env\.WS_NO_BUFFER_UTIL[\s\S]*process\.env\.WS_NO_UTF_8_VALIDATE[\s\S]*\}/,
"bundled ws optional native dependency defines must stay grouped together"
);
assert.match(
electronViteConfig,
/main:\s*\{[\s\S]*define:\s*bundledWsDefines[\s\S]*plugins:\s*\[externalizeRuntimeDeps\]/,
"bundled ws must not emit a startup-time bufferutil resolution stub"
);
assert.match(
electronViteConfig,
/preload:\s*\{[\s\S]*define:\s*bundledWsDefines[\s\S]*plugins:\s*\[[\s\S]*externalizeRuntimeDeps[\s\S]*\]/,
"bundled ws must not emit a startup-time utf-8-validate resolution stub"
);
});
test("browser node guest preload stays self-contained for sandboxed webviews", async () => {
const preloadEntry = await readFile(browserNodeGuestPreloadPath, "utf8");
assert.doesNotMatch(
preloadEntry,
/shared\/contracts\/ipc/,
"sandboxed webview guest preload must not import shared IPC contracts because Rollup may emit a required chunk"
);
assert.match(preloadEntry, /browser:guestOpenUrl/);
assert.match(preloadEntry, /browser:guestDiagnostic/);
});
test("browser workbench loopback proxy uses static ws imports for bundleable desktop runtime code", async () => {
const loopbackPreviewProxy = await readFile(loopbackPreviewProxyPath, "utf8");
assert.doesNotMatch(
loopbackPreviewProxy,
/createRequire\(import\.meta\.url\)/,
"desktop runtime code should avoid createRequire for ws so ESM main-process imports stay analyzable"
);
assert.doesNotMatch(
loopbackPreviewProxy,
/require\("ws"\)/,
'desktop runtime code should avoid require("ws") so electron-vite can bundle the dependency into the packaged main process'
);
assert.match(
loopbackPreviewProxy,
/import WebSocket(?:,\s*\{\s*WebSocketServer\s*\})? from "ws"|import WebSocket,\s*\{\s*WebSocketServer\s*\} from "ws"/,
"desktop runtime code should statically import ws entry points"
);
});
test("workspace root declares package workspaces for electron-builder fallback discovery", async () => {
const packageJson = JSON.parse(
await readFile(workspaceRootPackagePath, "utf8")
);
assert.deepEqual(packageJson.workspaces, [
"apps/*",
"packages/*/*",
"services/tuttid/builtin-apps/tutti-onboarding",
"tools/fixtures/*"
]);
});
test("desktop packaging provides an application icon resource", async () => {
await access(desktopBuildIconPath);
});
test("desktop windows packaging anchors electron-builder workspace detection to the repo root", async () => {
const packageJson = JSON.parse(await readFile(desktopPackagePath, "utf8"));
const buildScript = await readFile(buildScriptPath, "utf8");
assert.match(buildScript, /npm_package_json="\$\{ROOT_DIR\}\/package\.json"/);
assert.match(buildScript, /INIT_CWD="\$\{ROOT_DIR\}"/);
assert.equal(
packageJson.scripts["build:win:prepared"],
"bash ../../tools/scripts/build-desktop-package.sh win --prepared-builtin-apps"
);
assert.match(buildScript, /--prepared-builtin-apps/);
assert.match(buildScript, /BUILTIN_APPS_PREPARED/);
});
test("desktop Windows package and daemon agree on the managed POSIX shell resource", async () => {
const packageJson = JSON.parse(await readFile(desktopPackagePath, "utf8"));
const buildScript = await readFile(buildScriptPath, "utf8");
const alphaWorkflow = await readFile(windowsAlphaWorkflowPath, "utf8");
const tuttidManager = await readFile(tuttidManagerPath, "utf8");
const lock = JSON.parse(await readFile(managedPosixShellLockPath, "utf8"));
await access(managedPosixShellVendorScriptPath);
assert.deepEqual(packageJson.build.win.extraResources, [
{
from: "build/managed-posix-shell",
to: "bin/managed-posix-shell",
filter: ["**/*"]
},
{
from: "build/mutagen",
to: "bin/mutagen",
filter: ["**/*"]
}
]);
assert.match(buildScript, /vendor-managed-posix-shell\.mjs/);
assert.match(alphaWorkflow, /managed-posix-shell/);
assert.match(alphaWorkflow, /runtime\.json/);
assert.match(alphaWorkflow, /shellMetadata\.executable/);
assert.match(tuttidManager, /"managed-posix-shell"/);
assert.match(tuttidManager, /TUTTI_MANAGED_POSIX_SHELL/);
assert.match(tuttidManager, /runtime\.json/);
assert.doesNotMatch(tuttidManager, /bash\.exe/);
assert.equal(lock.schemaVersion, "tutti.managed-posix-shell-lock.v1");
assert.equal(lock.platforms["windows-amd64"].executable, "usr/bin/bash.exe");
assert.doesNotMatch(alphaWorkflow, /\n\s+push:/);
});
test("desktop Windows package and daemon agree on the bundled Mutagen resource", async () => {
const packageJson = JSON.parse(await readFile(desktopPackagePath, "utf8"));
const buildScript = await readFile(buildScriptPath, "utf8");
const alphaWorkflow = await readFile(windowsAlphaWorkflowPath, "utf8");
const tuttidManager = await readFile(tuttidManagerPath, "utf8");
const lock = JSON.parse(await readFile(mutagenLockPath, "utf8"));
await access(mutagenVendorScriptPath);
assert.deepEqual(packageJson.build.win.extraResources[1], {
from: "build/mutagen",
to: "bin/mutagen",
filter: ["**/*"]
});
assert.match(buildScript, /vendor-mutagen\.mjs/);
assert.match(alphaWorkflow, /bin\/mutagen/);
assert.match(alphaWorkflow, /mutagenMetadata\.executable/);
assert.match(tuttidManager, /"mutagen"/);
assert.match(tuttidManager, /TUTTI_MUTAGEN_BIN/);
assert.match(tuttidManager, /tutti\.mutagen\.v1/);
assert.equal(lock.schemaVersion, "tutti.mutagen-lock.v1");
assert.equal(lock.platforms["windows-amd64"].executable, "mutagen.exe");
});
test("desktop packages and daemon agree on the bundled uv archive root", async () => {
const packageJson = JSON.parse(await readFile(desktopPackagePath, "utf8"));
const defaults = JSON.parse(
await readFile(
new URL("../../config/tutti.defaults.json", import.meta.url),
"utf8"
)
);
const buildScript = await readFile(buildScriptPath, "utf8");
const tuttidManager = await readFile(tuttidManagerPath, "utf8");
await access(managedUVVendorScriptPath);
assert.deepEqual(
packageJson.build.extraResources.find(
(entry) => entry.to === "bin/managed-uv"
),
{
from: "build/managed-uv",
to: "bin/managed-uv",
filter: ["**/*"]
}
);
assert.match(buildScript, /vendor-managed-uv\.mjs/);
assert.match(buildScript, /windows-amd64/);
assert.match(
buildScript,
/upstream macOS uv archives contain the uv and uvx executables/
);
assert.match(buildScript, /rm -rf "\$\{APP_DIR\}\/build\/managed-uv"/);
assert.match(buildScript, /mkdir -p "\$\{APP_DIR\}\/build\/managed-uv"/);
assert.match(tuttidManager, /TUTTI_BUNDLED_UV_ROOT/);
assert.equal(defaults.agentRuntimeTools.uv.version, "0.11.31");
assert.ok(defaults.agentRuntimeTools.uv.artifacts.length >= 5);
});
test("desktop packages and daemon agree on the bundled RTK executable", async () => {
const packageJson = JSON.parse(await readFile(desktopPackagePath, "utf8"));
const lock = JSON.parse(
await readFile(
new URL("../../config/tutti.app-runtime.lock.json", import.meta.url),
"utf8"
)
);
const buildScript = await readFile(buildScriptPath, "utf8");
const tuttidManager = await readFile(tuttidManagerPath, "utf8");
await access(rtkVendorScriptPath);
assert.deepEqual(packageJson.build.extraResources.at(-1), {
from: "build/rtk",
to: "bin/rtk",
filter: ["**/*"]
});
assert.ok(
packageJson.build.mac.binaries.includes("Contents/Resources/bin/rtk/rtk")
);
assert.match(buildScript, /vendor-rtk\.mjs/);
assert.match(buildScript, /darwin-arm64 darwin-amd64/);
assert.match(await readFile(rtkVendorScriptPath, "utf8"), /Expand-Archive/);
assert.match(tuttidManager, /TUTTI_BUNDLED_RTK_PATH/);
assert.equal(lock.rtk.version, "0.45.0");
assert.deepEqual(Object.keys(lock.rtk.artifacts).sort(), [
"darwin-amd64",
"darwin-arm64",
"linux-amd64",
"windows-amd64"
]);
});