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, /[\s\S]*?\$\{displayName\}<\/DisplayName>/ ); assert.match(storeManifest, /[\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" ]); });