import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { chmod, mkdir, mkdtemp, readFile, utimes, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import test from "node:test"; import { pathToFileURL } from "node:url"; import { buildTuttiAppRelease, validateCLIManifest, validateManifest } from "./build-tutti-app-release.mjs"; import { buildTuttiAppCatalog } from "./build-tutti-app-catalog.mjs"; import { buildTuttiAppVersions } from "../../packages/workspace/app-release-tools/bin/build-tutti-app-versions.mjs"; import { verifyTuttiAppReleaseArtifacts } from "../../packages/workspace/app-release-tools/bin/verify-tutti-app-release-artifacts.mjs"; const reusableWorkflowPath = new URL( "../../.github/workflows/publish-tutti-app-release.yml", import.meta.url ); const catalogWorkflowPath = new URL( "../../.github/workflows/publish-tutti-app-catalog.yml", import.meta.url ); const stagingCatalogWorkflowPath = new URL( "../../.github/workflows/publish-tutti-app-catalog-staging.yml", import.meta.url ); const publishCatalogToolPath = new URL( "../../packages/workspace/app-release-tools/bin/publish-tutti-app-catalog.mjs", import.meta.url ); const releaseReferencePath = new URL( "../../services/tuttid/service/workspace/agent_workspace_app_reference/references/github-actions-release.md", import.meta.url ); test("buildTuttiAppRelease writes immutable release and latest metadata", async () => { const packageDir = await createPackageForTest("vibe-design"); const outputDir = await mkdtemp(path.join(tmpdir(), "tutti-release-")); const result = await buildTuttiAppRelease({ appId: "vibe-design", packageDir, outputDir, baseUrl: "https://cdn.example.test/tutti-apps/", version: "0.1.0+abc123", gitSha: "abc123", publishedAt: "2026-06-04T00:00:00Z" }); assert.equal(result.release.appId, "vibe-design"); assert.equal(result.release.version, "0.1.0+abc123"); assert.equal(result.release.manifest.version, "0.1.0+abc123"); assert.deepEqual(result.release.manifest.authors, [ { name: "Tutti Developer", avatarUrl: "https://github.com/tutti-os.png", url: "https://github.com/tutti-os" } ]); assert.deepEqual(result.release.manifest.source, { type: "github", url: "https://github.com/tutti-os/vibe-design" }); assert.equal( result.release.artifactUrl, "https://cdn.example.test/tutti-apps/apps/vibe-design/0.1.0%2Babc123/vibe-design-0.1.0%2Babc123.zip" ); assert.match(result.release.artifactSha256, /^[a-f0-9]{64}$/); assert.ok(result.release.artifactSizeBytes > 0); assert.equal( result.release.iconUrl, "https://cdn.example.test/tutti-apps/apps/vibe-design/0.1.0%2Babc123/icon.svg" ); const latest = JSON.parse(await readFile(result.latestJsonPath, "utf8")); assert.deepEqual(latest, result.release); const manifest = JSON.parse( await readFile(path.join(packageDir, "tutti.app.json"), "utf8") ); assert.equal(manifest.version, "0.1.0+abc123"); }); test("buildTuttiAppRelease includes package manifest localizations", async () => { const packageDir = await createPackageForTest("localized-app", { localizations: [ { locale: "zh-CN", metadata: { name: "本地化应用", description: "本地化描述", tags: ["本地化", "工作区", "工作区"] } } ] }); const outputDir = await mkdtemp(path.join(tmpdir(), "tutti-release-")); const result = await buildTuttiAppRelease({ appId: "localized-app", packageDir, outputDir, baseUrl: "https://cdn.example.test/tutti-apps/", version: "0.1.0+abc123", gitSha: "abc123", publishedAt: "2026-06-04T00:00:00Z" }); assert.deepEqual(result.release.localizations, [ { locale: "zh-CN", name: "本地化应用", description: "本地化描述", tags: ["本地化", "工作区"] } ]); }); test("buildTuttiAppRelease creates reproducible archives", async () => { const packageDir = await createPackageForTest("reproducible-app"); const firstOutputDir = await mkdtemp( path.join(tmpdir(), "tutti-release-first-") ); const secondOutputDir = await mkdtemp( path.join(tmpdir(), "tutti-release-second-") ); const options = { appId: "reproducible-app", packageDir, baseUrl: "https://cdn.example.test/tutti-apps", version: "1.2.3", gitSha: "abc123", publishedAt: "2026-06-04T00:00:00Z" }; const first = await buildTuttiAppRelease({ ...options, outputDir: firstOutputDir }); const changedTimestamp = new Date("2030-01-02T03:04:06.000Z"); await utimes( path.join(packageDir, "AGENTS.md"), changedTimestamp, changedTimestamp ); const second = await buildTuttiAppRelease({ ...options, outputDir: secondOutputDir }); assert.equal(first.release.artifactSha256, second.release.artifactSha256); assert.deepEqual( await readFile(first.artifactPath), await readFile(second.artifactPath) ); }); test("buildTuttiAppCatalog merges release files into remote catalog", async () => { const alpha = await releaseFileForTest("alpha-app"); const beta = await releaseFileForTest("beta-app", "0.1.0", { localizations: [ { locale: "zh-CN", name: "Beta 应用", description: "Beta 描述", tags: ["Beta"] } ] }); const outputDir = await mkdtemp(path.join(tmpdir(), "tutti-catalog-")); const outputPath = path.join(outputDir, "catalog.json"); const result = await buildTuttiAppCatalog({ releaseFiles: [beta, alpha], outputPath }); assert.equal(result.catalog.schemaVersion, "tutti.app.catalog.v1"); assert.deepEqual( result.catalog.apps.map((app) => app.manifest.appId), ["alpha-app", "beta-app"] ); assert.equal(result.catalog.apps[0].distribution.kind, "remote"); assert.equal( result.catalog.apps[0].distribution.iconUrl, "https://cdn.example.test/apps/alpha-app/icon.svg" ); assert.deepEqual(result.catalog.apps[1].localizations, [ { locale: "zh-CN", name: "Beta 应用", description: "Beta 描述", tags: ["Beta"] } ]); const written = JSON.parse(await readFile(outputPath, "utf8")); assert.deepEqual(written, result.catalog); }); test("buildTuttiAppCatalog merges release files into an existing catalog", async () => { const alpha = await releaseFileForTest("alpha-app", "0.2.0"); const existingCatalog = await catalogFileForTest([ catalogAppForTest("alpha-app", "0.1.0"), catalogAppForTest("gamma-app", "0.1.0") ]); const outputDir = await mkdtemp(path.join(tmpdir(), "tutti-catalog-")); const outputPath = path.join(outputDir, "catalog.json"); const result = await buildTuttiAppCatalog({ existingCatalogPath: existingCatalog, releaseFiles: [alpha], outputPath }); assert.deepEqual( result.catalog.apps.map((app) => app.manifest.appId), ["alpha-app", "gamma-app"] ); assert.equal(result.catalog.apps[0].manifest.version, "0.2.0"); assert.equal(result.catalog.apps[1].manifest.version, "0.1.0"); }); test("buildTuttiAppCatalog can refresh an existing catalog without release files", async () => { const existingCatalog = await catalogFileForTest([ catalogAppForTest("gamma-app", "0.1.0"), catalogAppForTest("alpha-app", "0.1.0") ]); const outputDir = await mkdtemp(path.join(tmpdir(), "tutti-catalog-")); const outputPath = path.join(outputDir, "catalog.json"); const result = await buildTuttiAppCatalog({ existingCatalogPath: existingCatalog, releaseFiles: [], outputPath }); assert.deepEqual( result.catalog.apps.map((app) => app.manifest.appId), ["alpha-app", "gamma-app"] ); }); test("buildTuttiAppCatalog rejects duplicate app ids", async () => { const first = await releaseFileForTest("duplicate-app"); const second = await releaseFileForTest("duplicate-app"); await assert.rejects( () => buildTuttiAppCatalog({ releaseFiles: [first, second], outputPath: path.join(tmpdir(), "unused-catalog.json") }), /duplicate release appId duplicate-app/ ); }); test("buildTuttiAppVersions preserves history and is idempotent", async () => { const first = await releaseFileForTest("versioned-app", "0.1.0"); const second = await releaseFileForTest("versioned-app", "0.2.0", { publishedAt: "2026-06-05T00:00:00Z" }); const tempDir = await mkdtemp(path.join(tmpdir(), "tutti-versions-")); const firstOutput = path.join(tempDir, "first.json"); const secondOutput = path.join(tempDir, "second.json"); await buildTuttiAppVersions({ releaseFiles: [first], minTuttiVersion: "0.0.0", outputPath: firstOutput }); const result = await buildTuttiAppVersions({ existingVersionsPath: firstOutput, releaseFiles: [second], minTuttiVersion: "0.12.0", outputPath: secondOutput }); assert.deepEqual( result.versions.versions.map((record) => [ record.release.version, record.minTuttiVersion, record.status ]), [ ["0.1.0", "0.0.0", "active"], ["0.2.0", "0.12.0", "active"] ] ); const idempotentOutput = path.join(tempDir, "idempotent.json"); const idempotentResult = await buildTuttiAppVersions({ existingVersionsPath: secondOutput, releaseFiles: [second], minTuttiVersion: "0.12.0", outputPath: idempotentOutput }); assert.deepEqual(idempotentResult.versions, result.versions); assert.equal( await readFile(idempotentOutput, "utf8"), await readFile(secondOutput, "utf8") ); }); test("buildTuttiAppVersions rejects changed compatibility for an immutable version", async () => { const release = await releaseFileForTest("immutable-app", "1.0.0"); const tempDir = await mkdtemp(path.join(tmpdir(), "tutti-versions-")); const existing = path.join(tempDir, "existing.json"); await buildTuttiAppVersions({ releaseFiles: [release], minTuttiVersion: "0.0.0", outputPath: existing }); await assert.rejects( () => buildTuttiAppVersions({ existingVersionsPath: existing, releaseFiles: [release], minTuttiVersion: "0.12.0", outputPath: path.join(tempDir, "changed.json") }), /different compatibility or release metadata/ ); }); test("capability-gated releases derive immutable metadata from the app manifest", async () => { const fallback = await releaseFileForTest("capability-app", "1.0.0"); const capable = await releaseFileForTest("capability-app", "2.0.0", { hostCompatibility: { requiredTuttiCapabilities: ["managed-model-cli-v1"] } }); const tempDir = await mkdtemp(path.join(tmpdir(), "tutti-capability-")); const baseline = path.join(tempDir, "baseline.json"); const versions = path.join(tempDir, "versions.json"); await buildTuttiAppVersions({ releaseFiles: [fallback], minTuttiVersion: "0.0.0", outputPath: baseline }); const result = await buildTuttiAppVersions({ existingVersionsPath: baseline, releaseFiles: [capable], outputPath: versions }); assert.deepEqual(result.versions.versions[1].requiredTuttiCapabilities, [ "managed-model-cli-v1" ]); assert.equal(result.versions.versions[1].minTuttiVersion, undefined); const catalog = await buildTuttiAppCatalog({ versionsFiles: [versions], outputPath: path.join(tempDir, "catalog.json") }); assert.equal(catalog.catalog.apps[0].manifest.version, "1.0.0"); assert.equal( catalog.catalog.compatibility.apps["capability-app"][0].app.manifest .version, "1.0.0" ); assert.deepEqual( catalog.catalog.compatibility.capabilityApps["capability-app"].map( (entry) => [entry.requiredTuttiCapabilities, entry.app.manifest.version] ), [[["managed-model-cli-v1"], "2.0.0"]] ); }); test("capability-gated releases reject an additional version selector", async () => { const release = await releaseFileForTest("capability-app", "2.0.0", { hostCompatibility: { requiredTuttiCapabilities: ["managed-model-cli-v1"] } }); await assert.rejects( () => buildTuttiAppVersions({ releaseFiles: [release], minTuttiVersion: "0.12.0", outputPath: path.join(tmpdir(), "capability-selector.json") }), /cannot declare both minTuttiVersion and requiredTuttiCapabilities/ ); }); test("capability declarations are canonical and immutable per release version", async () => { const invalidManifest = manifestForTest("capability-app"); invalidManifest.hostCompatibility = { requiredTuttiCapabilities: ["managed-model-cli-v1", "managed-model-cli-v1"] }; assert.throws( () => validateManifest(invalidManifest), /must be sorted and unique/ ); const original = await releaseFileForTest("capability-app", "2.0.0", { hostCompatibility: { requiredTuttiCapabilities: ["managed-model-cli-v1"] } }); const changed = await releaseFileForTest("capability-app", "2.0.0", { hostCompatibility: { requiredTuttiCapabilities: ["future-host-capability-v1"] } }); const tempDir = await mkdtemp(path.join(tmpdir(), "tutti-capability-")); const existing = path.join(tempDir, "versions.json"); await buildTuttiAppVersions({ releaseFiles: [original], outputPath: existing }); await assert.rejects( () => buildTuttiAppVersions({ existingVersionsPath: existing, releaseFiles: [changed], outputPath: path.join(tempDir, "changed.json") }), /different compatibility or release metadata/ ); }); test("buildTuttiAppCatalog emits legacy-safe apps and a compact compatibility frontier", async () => { const releases = await Promise.all([ releaseFileForTest("frontier-app", "1.0.0"), releaseFileForTest("frontier-app", "1.1.0", { publishedAt: "2026-06-05T00:00:00Z" }), releaseFileForTest("frontier-app", "1.2.0", { publishedAt: "2026-06-06T00:00:00Z" }) ]); const tempDir = await mkdtemp(path.join(tmpdir(), "tutti-frontier-")); const baseline = path.join(tempDir, "baseline.json"); const compatible = path.join(tempDir, "compatible.json"); const versions = path.join(tempDir, "versions.json"); await buildTuttiAppVersions({ releaseFiles: releases.slice(0, 2), minTuttiVersion: "0.0.0", outputPath: baseline }); await buildTuttiAppVersions({ existingVersionsPath: baseline, releaseFiles: [releases[2]], minTuttiVersion: "0.12.0", outputPath: compatible }); await buildTuttiAppVersions({ existingVersionsPath: compatible, setStatusVersion: "1.0.0", status: "withdrawn", outputPath: versions }); const result = await buildTuttiAppCatalog({ versionsFiles: [versions], outputPath: path.join(tempDir, "catalog.json") }); assert.equal(result.catalog.apps[0].manifest.version, "1.1.0"); assert.deepEqual( result.catalog.compatibility.apps["frontier-app"].map((entry) => [ entry.minTuttiVersion, entry.app.manifest.version ]), [ ["0.0.0", "1.1.0"], ["0.12.0", "1.2.0"] ] ); }); test("verifyTuttiAppReleaseArtifacts validates release artifact hash and size", async () => { const tempDir = await mkdtemp(path.join(tmpdir(), "tutti-release-verify-")); const artifactPath = path.join(tempDir, "app.zip"); const artifactBytes = Buffer.from("release artifact"); await writeFile(artifactPath, artifactBytes); const releasePath = await releaseFileForTest("verified-app", "0.1.0", { artifactUrl: pathToFileURL(artifactPath).href, artifactSha256: createHash("sha256").update(artifactBytes).digest("hex"), artifactSizeBytes: artifactBytes.length }); const result = await verifyTuttiAppReleaseArtifacts({ releaseFiles: [releasePath] }); assert.equal(result.checkedArtifactCount, 1); }); test("verifyTuttiAppReleaseArtifacts rejects catalog artifact sha mismatches", async () => { const tempDir = await mkdtemp(path.join(tmpdir(), "tutti-catalog-verify-")); const artifactPath = path.join(tempDir, "app.zip"); const artifactBytes = Buffer.from("release artifact"); await writeFile(artifactPath, artifactBytes); const releasePath = await releaseFileForTest("verified-app", "0.1.0", { artifactUrl: pathToFileURL(artifactPath).href, artifactSha256: createHash("sha256").update(artifactBytes).digest("hex"), artifactSizeBytes: artifactBytes.length }); const catalogPath = await catalogFileForTest([ catalogAppForTest("verified-app", "0.1.0", { artifactUrl: pathToFileURL(artifactPath).href, artifactSha256: "b".repeat(64) }) ]); await assert.rejects( () => verifyTuttiAppReleaseArtifacts({ catalogFile: catalogPath, releaseFiles: [releasePath] }), /artifactSha256 must match latest release metadata/ ); }); test("validateManifest rejects packages without manifest icon assets", () => { assert.throws( () => validateManifest({ schemaVersion: "tutti.app.manifest.v1", appId: "bad-app", version: "0.1.0", name: "Bad App", description: "Bad app", runtime: { bootstrap: "bootstrap.sh", healthcheckPath: "/" } }), /icon is required/ ); }); test("validateManifest accepts managed runtime manifests without launch metadata", () => { assert.doesNotThrow(() => validateManifest(manifestForTest("managed-app"))); }); test("validateManifest accepts references list endpoints", () => { const manifest = manifestForTest("references-app"); manifest.references = { listEndpoint: "/references/list" }; assert.doesNotThrow(() => validateManifest(manifest)); }); test("validateManifest accepts references search endpoints", () => { const manifest = manifestForTest("references-app"); manifest.references = { listEndpoint: "/references/list", searchEndpoint: "/references/search" }; assert.doesNotThrow(() => validateManifest(manifest)); }); test("validateManifest rejects unsupported references fields", () => { const manifest = manifestForTest("references-app"); manifest.references = { listEndpoint: "/references/list", unknownEndpoint: "/references/unknown" }; assert.throws( () => validateManifest(manifest), /references\.unknownEndpoint is unsupported/ ); }); test("validateManifest rejects invalid references list endpoints", () => { for (const listEndpoint of [ "references/list", "//example.test/references", "https://example.test/references", "/references?query=1", "/references#section" ]) { const manifest = manifestForTest("references-app"); manifest.references = { listEndpoint }; assert.throws( () => validateManifest(manifest), /references\.listEndpoint must be a relative URL path/ ); } }); test("validateManifest rejects invalid references search endpoints", () => { for (const searchEndpoint of [ "references/search", "//example.test/references", "https://example.test/references", "/references?query=1", "/references#section" ]) { const manifest = manifestForTest("references-app"); manifest.references = { listEndpoint: "/references/list", searchEndpoint }; assert.throws( () => validateManifest(manifest), /references\.searchEndpoint must be a relative URL path/ ); } }); test("validateCLIManifest accepts the app CLI HTTP bridge contract", () => { assert.doesNotThrow(() => validateCLIManifest(cliManifestForTest(), "tutti.cli.json") ); }); test("validateCLIManifest accepts wait execution without app-owned timeout input", () => { const manifest = cliManifestForTest(); manifest.commands[0].path = ["runs", "wait"]; manifest.commands[0].execution = { mode: "wait" }; assert.doesNotThrow(() => validateCLIManifest(manifest, "tutti.cli.json")); }); test("validateCLIManifest rejects app-owned timeout input on wait execution", () => { const manifest = cliManifestForTest(); manifest.commands[0].execution = { mode: "wait" }; manifest.commands[0].inputSchema.properties["timeout-ms"] = { type: "integer" }; assert.throws( () => validateCLIManifest(manifest, "tutti.cli.json"), /reserves --timeout-ms for the total CLI wait timeout/ ); }); test("validateCLIManifest accepts typed input enum values", () => { const manifest = cliManifestForTest(); manifest.commands[0].inputSchema.properties.mode = { type: "string", description: "Run mode", enum: ["fast", "safe"], default: "safe" }; manifest.commands[0].inputSchema.properties.priority = { type: "integer", enum: [1, 2], default: 1 }; manifest.commands[0].inputSchema.properties.enabled = { type: "boolean", enum: [true], default: true }; assert.doesNotThrow(() => validateCLIManifest(manifest, "tutti.cli.json")); }); test("validateCLIManifest rejects enum values that do not match property type", () => { const manifest = cliManifestForTest(); manifest.commands[0].inputSchema.properties.mode = { type: "string", enum: ["fast", 1] }; assert.throws( () => validateCLIManifest(manifest, "tutti.cli.json"), /inputSchema\.properties\.mode\.enum\[1\] must be string/ ); }); test("validateCLIManifest rejects defaults outside declared enum values", () => { const manifest = cliManifestForTest(); manifest.commands[0].inputSchema.properties.mode = { type: "string", enum: ["fast", "safe"], default: "slow" }; assert.throws( () => validateCLIManifest(manifest, "tutti.cli.json"), /inputSchema\.properties\.mode\.default must be one of the declared enum values/ ); }); test("validateCLIManifest rejects invalid command visibility", () => { const manifest = cliManifestForTest(); manifest.commands[0].visibility = "private"; assert.throws( () => validateCLIManifest(manifest, "tutti.cli.json"), /visibility must be public or integration/ ); }); test("buildTuttiAppRelease validates declared CLI manifests", async () => { const packageDir = await createPackageForTest("cli-app"); const manifest = manifestForTest("cli-app"); manifest.cli = { manifest: "tutti.cli.json" }; await writeFile( path.join(packageDir, "tutti.app.json"), `${JSON.stringify(manifest, null, 2)}\n` ); await writeFile( path.join(packageDir, "tutti.cli.json"), `${JSON.stringify({ ...cliManifestForTest(), commands: [] }, null, 2)}\n` ); const outputDir = path.join( await mkdtemp(path.join(tmpdir(), "tutti-release-")), "out" ); await assert.rejects( () => buildTuttiAppRelease({ appId: "cli-app", packageDir, outputDir, baseUrl: "https://cdn.example.test/tutti-apps/" }), /commands must be a non-empty array/ ); }); test("buildTuttiAppRelease rejects unsafe release path segments", async () => { const packageDir = await createPackageForTest("vibe-design"); const outputDir = await mkdtemp(path.join(tmpdir(), "tutti-release-")); await assert.rejects( () => buildTuttiAppRelease({ appId: "vibe-design", packageDir, outputDir, baseUrl: "https://cdn.example.test/tutti-apps/", version: "0.1.0/abc123" }), /version must use only/ ); }); test("Tutti app release workflow is reusable by external app repositories", async () => { const workflow = await readFile(reusableWorkflowPath, "utf8"); assert.match(workflow, /workflow_call:/); assert.match(workflow, /release_tag_prefix:/); assert.match(workflow, /release_bump:/); assert.match(workflow, /min_tutti_version:/); assert.match(workflow, /create_release_tag:/); assert.match(workflow, /publish_catalog:/); assert.match(workflow, /catalog_only:/); assert.match(workflow, /prepare_only:/); assert.match(workflow, /candidate_run_id:/); assert.match(workflow, /expected_version:/); assert.match(workflow, /expected_artifact_sha256:/); assert.match(workflow, /expected_git_sha:/); assert.match(workflow, /catalog_cloudfront_distribution_id:/); assert.match(workflow, /Validate release inputs/); assert.match( workflow, /package_command is required unless catalog_only is true/ ); assert.match( workflow, /release_assets_base_url is required unless catalog_only is true/ ); assert.match(workflow, /min_tutti_version must be valid SemVer/); assert.doesNotMatch( workflow, /min_tutti_version is required unless catalog_only is true/ ); assert.match(workflow, /concurrency:/); assert.match(workflow, /tutti-app-catalog-\{0\}-\{1\}/); assert.match(workflow, /tutti-app-release-\{0\}-\{1\}/); assert.match(workflow, /cancel-in-progress: false/); assert.match(workflow, /\[skip release\]/); assert.match(workflow, /release_tools_package:/); assert.match(workflow, /default:\s+"@tutti-os\/app-release-tools@latest"/); assert.doesNotMatch(workflow, /auto_bump_version:/); assert.doesNotMatch(workflow, /version_bump:/); assert.doesNotMatch(workflow, /version_manifest_path:/); assert.doesNotMatch(workflow, /bump-tutti-app-version/); assert.doesNotMatch(workflow, /release_version:/); assert.doesNotMatch(workflow, /INPUT_RELEASE_VERSION/); assert.doesNotMatch(workflow, /Commit app version bump/); assert.doesNotMatch(workflow, /git push origin "HEAD:\$\{GITHUB_REF_NAME\}"/); assert.doesNotMatch(workflow, /REF_TYPE: \$\{\{ github\.ref_type \}\}/); assert.doesNotMatch(workflow, /github\.ref_type/); assert.match(workflow, /`\$\{process\.env\.APP_ID\}-v`/); assert.match(workflow, /git", \["fetch", "--tags", "--force"\]/); assert.match(workflow, /parseStableVersion/); assert.match(workflow, /latestVersionSeed/); assert.match(workflow, /nextVersionFromSources/); assert.match(workflow, /manifest\.version/); assert.match(workflow, /Package manifest version must be stable semver/); assert.match( workflow, /Resolved release version seed \$\{latest\.version\} from \$\{latest\.source\}/ ); assert.match(workflow, /release_bump requires create_release_tag/); assert.match(workflow, /\[ "\$\{PREPARE_ONLY\}" != "true" \]/); assert.match(workflow, /create_release_tag requires release_bump/); assert.match( workflow, /const seed = latestVersionSeed\(tagPrefix, manifest\.version, false\)/ ); assert.match( workflow, /releaseVersion = `\$\{seed\.version\}\+\$\{gitSha\.slice\(0, 12\)\}`/ ); assert.match(workflow, /name: Create release tag/); assert.match(workflow, /git tag -a "\$\{RELEASE_TAG_NAME\}"/); assert.match( workflow, /git push origin "refs\/tags\/\$\{RELEASE_TAG_NAME\}"/ ); assert.match( workflow, /pnpm --package "\$\{RELEASE_TOOLS_PACKAGE\}" dlx build-tutti-app-release/ ); assert.doesNotMatch(workflow, /Checkout Tutti release tools/); assert.match( workflow, /aws s3 sync "tutti-app-release\/apps\/\$\{APP_ID\}\/\$\{RELEASE_VERSION\}\/"/ ); assert.doesNotMatch( workflow, /aws s3 cp "tutti-app-release\/apps\/\$\{APP_ID\}\/latest\.json"/ ); assert.match(workflow, /aws s3api head-object/); assert.match(workflow, /matching immutable metadata/); assert.match(workflow, /different immutable metadata/); assert.match(workflow, /Repairing mutable latest\/catalog state/); assert.match(workflow, /const comparedKeys = \[/); assert.match(workflow, /Verify published app release artifact/); assert.match(workflow, /verify-tutti-app-release-artifacts/); assert.match( workflow, /--release-file "tutti-app-release\/apps\/\$\{APP_ID\}\/latest\.json"/ ); assert.match(workflow, /Publish app release metadata/); assert.match(workflow, /publish-tutti-app-metadata/); assert.match(workflow, /name: Upload production release candidate/); assert.match(workflow, /uses: actions\/upload-artifact@v4/); assert.match(workflow, /uses: actions\/download-artifact@v5/); assert.match(workflow, /run-id: \$\{\{ inputs\.candidate_run_id \}\}/); assert.match(workflow, /Approved release candidate mismatch/); assert.match( workflow, /candidate\.artifactSha256 !== expectedArtifactSha256/ ); assert.match(workflow, /candidate\.gitSha !== expectedGitSha/); assert.match( workflow, /catalog_only and prepare_only are mutually exclusive/ ); assert.match( workflow, /if: \$\{\{ inputs\.prepare_only \}\}[\s\S]*path: tutti-app-release/ ); assert.match( workflow, /name: Configure AWS credentials\n\s+if: \$\{\{ !inputs\.prepare_only \}\}/ ); assert.match( workflow, /name: Upload app release to S3\n\s+if: \$\{\{ !inputs\.catalog_only && !inputs\.prepare_only \}\}/ ); assert.match( workflow, /name: Publish app release metadata\n\s+if: \$\{\{ !inputs\.prepare_only \}\}/ ); assert.match( workflow, /name: Verify published app metadata\n\s+if: \$\{\{ !inputs\.prepare_only \}\}/ ); assert.match( workflow, /name: Create release tag\n\s+if: \$\{\{ !inputs\.catalog_only && !inputs\.prepare_only && inputs\.create_release_tag \}\}/ ); assert.match( workflow, /name: Invalidate app catalog\n\s+if: \$\{\{ !inputs\.prepare_only && \(inputs\.publish_catalog \|\| inputs\.catalog_only\)/ ); const verifyCandidateIndex = workflow.indexOf( "name: Verify local app release candidate" ); const uploadCandidateIndex = workflow.indexOf( "name: Upload production release candidate" ); const configureAwsIndex = workflow.indexOf("name: Configure AWS credentials"); const identityCheckIndex = workflow.indexOf( "Approved release candidate mismatch" ); assert.notEqual(verifyCandidateIndex, -1); assert.notEqual(uploadCandidateIndex, -1); assert.notEqual(configureAwsIndex, -1); assert.notEqual(identityCheckIndex, -1); assert.ok( verifyCandidateIndex < uploadCandidateIndex, "candidate verification must precede candidate artifact upload" ); assert.ok( verifyCandidateIndex < configureAwsIndex, "candidate verification must precede AWS credentials and cloud mutation" ); assert.ok( identityCheckIndex < configureAwsIndex, "approved candidate identity checks must precede AWS credentials" ); const uploadToS3Index = workflow.indexOf("name: Upload app release to S3"); const verifyPublishedIndex = workflow.indexOf( "name: Verify published app release artifact" ); assert.notEqual(uploadToS3Index, -1); assert.notEqual(verifyPublishedIndex, -1); assert.ok( uploadToS3Index < verifyPublishedIndex, "remote artifact verification must run after the immutable upload" ); assert.match(workflow, /CATALOG_ONLY:/); assert.match(workflow, /--min-tutti-version "\$\{MIN_TUTTI_VERSION\}"/); assert.doesNotMatch(workflow, /required_tutti_capabilities:/); assert.match(workflow, /--versions-file tutti-app-metadata\/versions\.json/); assert.doesNotMatch(workflow, /aws s3 cp tutti-app-catalog\/catalog\.json/); assert.match(workflow, /Invalidate app catalog/); assert.match(workflow, /cloudfront create-invalidation/); }); test("production candidate verification hashes the local archive before cloud access", async () => { const workflow = await readFile(reusableWorkflowPath, "utf8"); const script = extractWorkflowNodeScript( workflow, "Verify local app release candidate" ); const root = await mkdtemp(path.join(tmpdir(), "tutti-local-candidate-")); const appId = "candidate-test"; const version = "1.2.3"; const releaseDir = path.join( root, "tutti-app-release", "apps", appId, version ); const latestDir = path.dirname(releaseDir); const artifactName = `${appId}-${version}.zip`; const artifact = Buffer.from("approved candidate archive", "utf8"); const release = { appId, version, artifactUrl: `https://example.invalid/tutti-app-releases/apps/${appId}/${version}/${artifactName}`, artifactSha256: createHash("sha256").update(artifact).digest("hex"), artifactSizeBytes: artifact.length }; await mkdir(releaseDir, { recursive: true }); await writeFile(path.join(releaseDir, artifactName), artifact); await writeFile( path.join(releaseDir, "release.json"), `${JSON.stringify(release)}\n` ); await writeFile( path.join(latestDir, "latest.json"), `${JSON.stringify(release)}\n` ); const verified = runWorkflowNodeScript(script, root, { appId, version }); assert.equal(verified.status, 0, verified.stderr); assert.match(verified.stdout, /Verified local release candidate/); await writeFile(path.join(releaseDir, artifactName), "tampered archive"); const rejected = runWorkflowNodeScript(script, root, { appId, version }); assert.notEqual(rejected.status, 0); assert.match(rejected.stderr, /local release artifact sha256 mismatch/); }); function extractWorkflowNodeScript(workflow, stepName) { const normalized = workflow.replaceAll("\r\n", "\n"); const stepIndex = normalized.indexOf(`name: ${stepName}`); assert.notEqual(stepIndex, -1, `missing workflow step ${stepName}`); const heredocIndex = normalized.indexOf("node <<'NODE'\n", stepIndex); assert.notEqual(heredocIndex, -1, `missing Node heredoc for ${stepName}`); const scriptStart = heredocIndex + "node <<'NODE'\n".length; const scriptEnd = normalized.indexOf("\n NODE", scriptStart); assert.notEqual(scriptEnd, -1, `unterminated Node heredoc for ${stepName}`); return normalized .slice(scriptStart, scriptEnd) .split("\n") .map((line) => line.replace(/^ /u, "")) .join("\n"); } function runWorkflowNodeScript(script, cwd, { appId, version }) { return spawnSync(process.execPath, ["-"], { cwd, encoding: "utf8", env: { ...process.env, APP_ID: appId, RELEASE_VERSION: version }, input: script }); } test("Tutti app catalog workflow aggregates version indexes", async () => { const workflow = await readFile(catalogWorkflowPath, "utf8"); assert.match(workflow, /workflow_dispatch:/); assert.match(workflow, /catalog_mode:/); assert.match(workflow, /default:\s*merge/); assert.doesNotMatch(workflow, /app_ids_preset/); assert.doesNotMatch(workflow, /APP_IDS_PRESET/); assert.match(workflow, /aws s3api head-object/); assert.match(workflow, /Refusing to publish a partial merge catalog/); assertCatalogWorkflowRefreshesExistingAppLatestMetadata(workflow); assert.match(workflow, /publish-tutti-app-catalog/); assert.match(workflow, /app-ids\.txt/); assert.match(workflow, /Verify app catalog artifacts/); assertAwsValidationBeforeConfigure(workflow, [ "AWS_REGION_VALUE", "AWS_ROLE_ARN_VALUE", "S3_BUCKET_VALUE" ]); assert.match(workflow, /verify-tutti-app-release-artifacts/); assert.match(workflow, /--catalog-file tutti-app-catalog\/catalog\.json/); assert.doesNotMatch(workflow, /aws s3 cp tutti-app-catalog\/catalog\.json/); assert.match(workflow, /cloudfront create-invalidation/); }); test("publishTuttiAppCatalog verifies before uploading catalog metadata", async () => { const source = await readFile(publishCatalogToolPath, "utf8"); const verifyIndex = source.indexOf("await verifyTuttiAppReleaseArtifacts({"); const uploadIndex = source.indexOf("putObject({"); assert.notEqual(verifyIndex, -1); assert.notEqual(uploadIndex, -1); assert.ok( verifyIndex < uploadIndex, "catalog verification must precede upload" ); }); test("Tutti app staging catalog workflow uses an isolated prefix", async () => { const workflow = await readFile(stagingCatalogWorkflowPath, "utf8"); assert.match(workflow, /workflow_dispatch:/); assert.match(workflow, /TUTTI_APP_RELEASES_STAGING_S3_PREFIX/); assert.match(workflow, /tutti-app-releases-staging/); assert.match(workflow, /catalog_mode:/); assert.doesNotMatch(workflow, /app_ids_preset/); assert.doesNotMatch(workflow, /APP_IDS_PRESET/); assert.match(workflow, /aws s3api head-object/); assert.match(workflow, /Refusing to publish a partial merge catalog/); assertCatalogWorkflowRefreshesExistingAppLatestMetadata(workflow); assert.match(workflow, /publish-tutti-app-catalog/); assert.match(workflow, /app-ids\.txt/); assert.match(workflow, /Verify app catalog artifacts/); assertAwsValidationBeforeConfigure(workflow, [ "AWS_REGION_VALUE", "AWS_ROLE_ARN_VALUE", "S3_BUCKET_VALUE" ]); assert.match(workflow, /verify-tutti-app-release-artifacts/); assert.match(workflow, /--catalog-file tutti-app-catalog\/catalog\.json/); }); test("Tutti app staging release template requires an explicit staging asset base URL", async () => { const reference = await readFile(releaseReferencePath, "utf8"); assert.match(reference, /TUTTI_APP_RELEASES_STAGING_BASE_URL/); assert.match(reference, /push:\n\s+branches:\n\s+- main/); assert.ok( reference.includes( "release_assets_base_url: ${{ vars.TUTTI_APP_RELEASES_STAGING_BASE_URL }}" ) ); assert.ok( reference.includes( "publish_catalog: ${{ github.event_name == 'push' || inputs.publish_catalog }}" ) ); assert.ok( reference.includes( "catalog_only: ${{ github.event_name == 'workflow_dispatch' && inputs.catalog_only }}" ) ); assert.doesNotMatch( reference, /TUTTI_APP_RELEASES_STAGING_BASE_URL \|\| vars\.TUTTI_APP_RELEASES_BASE_URL/ ); assert.match( reference, /private repositories that cannot read organization variables/ ); }); function assertCatalogWorkflowRefreshesExistingAppLatestMetadata(workflow) { assert.match( workflow, /\[ -z "\$\{app_ids_value\}" \] && \[ "\$\{CATALOG_MODE\}" = "merge" \]/ ); assert.match(workflow, /existing-catalog\.json/); assert.match(workflow, /manifest\??\.appId/); assert.match(workflow, /compatibility\?\.capabilityApps/); assert.match(workflow, /app_ids_value="\$\{existing_app_ids\}"/); } function assertAwsValidationBeforeConfigure(workflow, names) { const validationIndex = workflow.indexOf("name: Validate AWS configuration"); const configureIndex = workflow.indexOf("name: Configure AWS credentials"); assert.notEqual(validationIndex, -1, "workflow should validate AWS config"); assert.notEqual(configureIndex, -1, "workflow should configure AWS"); assert.ok( validationIndex < configureIndex, "AWS validation must run before credentials configuration" ); for (const name of names) { assert.match(workflow, new RegExp(`for name in[\\s\\S]*${name}`)); } assert.match(workflow, /Missing required AWS configuration/); } async function releaseFileForTest(appId, version = "0.1.0", overrides = {}) { const tempDir = await mkdtemp(path.join(tmpdir(), "tutti-release-file-")); const release = { schemaVersion: "tutti.app.release.v1", appId, version, name: appId, description: `${appId} description`, manifest: { ...manifestForTest(appId, version), ...(overrides.hostCompatibility ? { hostCompatibility: overrides.hostCompatibility } : {}) }, ...(overrides.localizations ? { localizations: overrides.localizations } : {}), artifactUrl: overrides.artifactUrl ?? `https://cdn.example.test/apps/${appId}/${appId}.zip`, artifactSha256: overrides.artifactSha256 ?? "a".repeat(64), artifactSizeBytes: overrides.artifactSizeBytes ?? 123, iconUrl: `https://cdn.example.test/apps/${appId}/icon.svg`, publishedAt: overrides.publishedAt ?? "2026-06-04T00:00:00Z", gitSha: "abc123" }; const releasePath = path.join(tempDir, "release.json"); await writeFile(releasePath, `${JSON.stringify(release, null, 2)}\n`); return releasePath; } async function catalogFileForTest(apps) { const tempDir = await mkdtemp(path.join(tmpdir(), "tutti-catalog-file-")); const catalogPath = path.join(tempDir, "catalog.json"); await writeFile( catalogPath, `${JSON.stringify( { schemaVersion: "tutti.app.catalog.v1", apps }, null, 2 )}\n` ); return catalogPath; } function catalogAppForTest(appId, version, overrides = {}) { return { manifest: manifestForTest(appId, version), distribution: { kind: "remote", artifactUrl: overrides.artifactUrl ?? `https://cdn.example.test/apps/${appId}/${appId}.zip`, artifactSha256: overrides.artifactSha256 ?? "b".repeat(64), iconUrl: `https://cdn.example.test/apps/${appId}/icon.svg` } }; } async function createPackageForTest(appId, options = {}) { const packageDir = await mkdtemp(path.join(tmpdir(), "tutti-app-package-")); await mkdir(path.join(packageDir, "web"), { recursive: true }); const manifest = manifestForTest(appId); if (options.localizations?.length > 0) { manifest.localizationInfo = { defaultLocale: "en", additionalLocales: options.localizations.map((localization) => ({ locale: localization.locale, file: `locales/${localization.locale}/manifest.json` })) }; } await writeFile( path.join(packageDir, "tutti.app.json"), `${JSON.stringify(manifest, null, 2)}\n` ); for (const localization of options.localizations ?? []) { const localeDir = path.join(packageDir, "locales", localization.locale); await mkdir(localeDir, { recursive: true }); await writeFile( path.join(localeDir, "manifest.json"), `${JSON.stringify(localization.metadata, null, 2)}\n` ); } await writeFile(path.join(packageDir, "AGENTS.md"), "App instructions\n"); await writeFile(path.join(packageDir, "icon.svg"), `<${"svg"}>\n`); await writeFile(path.join(packageDir, "web", "index.html"), "
\n"); const bootstrapPath = path.join(packageDir, "bootstrap.sh"); await writeFile(bootstrapPath, "#!/usr/bin/env bash\nexit 0\n"); await chmod(bootstrapPath, 0o755); return packageDir; } function manifestForTest(appId, version = "0.1.0") { return { schemaVersion: "tutti.app.manifest.v1", appId, version, name: appId, description: `${appId} description`, icon: { type: "asset", src: "icon.svg" }, runtime: { bootstrap: "bootstrap.sh", healthcheckPath: "/" }, authors: [ { name: "Tutti Developer", avatarUrl: "https://github.com/tutti-os.png", url: "https://github.com/tutti-os" } ], source: { type: "github", url: `https://github.com/tutti-os/${appId}` } }; } function cliManifestForTest() { return { schemaVersion: "tutti.app.cli.v1", scope: "automation", commands: [ { path: ["run"], summary: "Run automation", inputSchema: { type: "object", properties: { name: { type: "string" }, "dry-run": { type: "boolean" } }, required: ["name"] }, output: { defaultMode: "json", json: true }, handler: { kind: "http", method: "POST", path: "/tutti/cli/run", timeoutMs: 30000 } } ] }; }