Files
tutti/tools/scripts/build-tutti-app-release.test.mjs
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

1289 lines
42 KiB
JavaScript

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"}></${"svg"}>\n`);
await writeFile(path.join(packageDir, "web", "index.html"), "<div></div>\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
}
}
]
};
}