Merge pull request #2148 from heygen-com/07-10-test_repo_enforce_workspace_contracts

test(repo): enforce workspace contracts
This commit is contained in:
James Russo
2026-07-12 01:51:56 -07:00
committed by GitHub
13 changed files with 420 additions and 86 deletions
+1 -76
View File
@@ -419,82 +419,7 @@ jobs:
# Load the studio in headless Chrome with API mocking to trigger
# the full splash→main transition (catches hooks-after-early-return bugs)
cd packages/producer
node --input-type=module <<'SMOKE_EOF'
import puppeteer from "puppeteer";
const browser = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
const errors = [];
page.on("pageerror", (err) => errors.push(err.message));
page.on("console", (msg) => {
if (msg.type() === "error") errors.push(msg.text());
});
// Mock the project API so the studio transitions past the splash screen.
// Without this, useServerConnection stays in "waiting" and the full React
// tree (with all hooks) never renders — missing hooks-order violations.
const COMP_HTML = '<div data-composition-id="root" data-width="1920" data-height="1080" data-duration="1" data-start="0"><div class="clip" data-start="0" data-duration="1">Test</div></div>';
await page.setRequestInterception(true);
page.on("request", (req) => {
const url = req.url();
if (url.includes("/api/projects") && !url.includes("/files") && !url.includes("/preview") && !url.includes("/gsap")) {
req.respond({
status: 200,
contentType: "application/json",
body: JSON.stringify({ projects: [{ id: "smoke-test" }] }),
});
} else if (url.includes("/api/") && url.includes("/files")) {
req.respond({
status: 200,
contentType: "application/json",
body: JSON.stringify({ files: [{ path: "index.html", type: "file" }] }),
});
} else if (url.includes("/api/") && url.includes("/preview")) {
req.respond({ status: 200, contentType: "text/html", body: COMP_HTML });
} else if (url.includes("/api/")) {
req.respond({ status: 200, contentType: "application/json", body: JSON.stringify({}) });
} else {
req.continue();
}
});
await page.goto("http://localhost:5199/#project=smoke-test", {
waitUntil: "networkidle0",
timeout: 30000,
});
// Wait for React to render past splash into the full studio UI
await new Promise((r) => setTimeout(r, 3000));
// Check for React error boundary (catches hooks violations, render crashes)
const errorBoundary = await page.evaluate(() => {
const text = document.body.innerText;
if (text.includes("Something went wrong")) return text;
return null;
});
if (errorBoundary) {
errors.push("React error boundary triggered: " + errorBoundary);
}
await browser.close();
// Filter expected noise from mock endpoints
const fatal = errors.filter(
(e) =>
!e.includes("favicon") &&
!e.includes("ERR_CONNECTION_REFUSED") &&
!e.includes("Failed to fetch") &&
!e.includes("is not iterable") &&
!e.includes("Cannot read properties of undefined") &&
!e.includes("Cannot read properties of null"),
);
if (fatal.length > 0) {
console.error("FAIL: studio had runtime errors:");
for (const e of fatal) console.error(" •", e);
process.exit(1);
}
console.log("PASS: studio loaded and transitioned without runtime errors");
SMOKE_EOF
node scripts/studio-runtime-smoke.mjs http://localhost:5199/#project=smoke-test
kill $SERVER_PID 2>/dev/null || true
+3
View File
@@ -285,7 +285,10 @@
"gsap": "^3.15.0",
},
"devDependencies": {
"@types/node": "^25.0.10",
"typescript": "^5.0.0",
"vite": "^6.4.2",
"vitest": "^3.2.4",
},
},
"packages/shader-transitions": {
+4 -3
View File
@@ -11,7 +11,7 @@
"type": "module",
"scripts": {
"dev": "bun run studio",
"build": "bun run --filter '@hyperframes/{parsers,lint,studio-server}' build && bun run --filter @hyperframes/core build && bun run --filter '@hyperframes/{core,engine,producer,player,studio,shader-transitions,aws-lambda,gcp-cloud-run,sdk}' build && bun run --filter @hyperframes/cli build",
"build": "bun run --filter '@hyperframes/{parsers,lint,studio-server}' build && bun run --filter @hyperframes/core build && bun run --filter '@hyperframes/{core,engine,producer,player,studio,shader-transitions,aws-lambda,gcp-cloud-run,sdk}' build && bun run --filter @hyperframes/cli build && bun run --filter @hyperframes/sdk-playground build",
"build:producer": "bun run --filter @hyperframes/producer build",
"studio": "bun run --filter @hyperframes/studio dev",
"build:hyperframes-runtime": "bun run --filter @hyperframes/core build:hyperframes-runtime",
@@ -24,10 +24,11 @@
"changelog:weekly": "tsx scripts/changelog-weekly.ts",
"sync-schemas": "tsx scripts/sync-schemas.ts",
"sync-schemas:check": "tsx scripts/sync-schemas.ts --check",
"lint": "bun run check:tracked-artifacts && oxlint . && tsx scripts/lint-skills.ts",
"lint": "bun run check:tracked-artifacts && bun run check:workspace-contracts && oxlint . && tsx scripts/lint-skills.ts",
"lint:skills": "tsx scripts/lint-skills.ts",
"lint:fix": "oxlint --fix .",
"check:tracked-artifacts": "node scripts/check-tracked-artifacts.mjs",
"check:workspace-contracts": "node scripts/check-workspace-contracts.mjs",
"format": "oxfmt .",
"test": "bun run test:unit",
"test:unit": "bun run --filter '*' test",
@@ -40,7 +41,7 @@
"player:perf": "bun run --filter @hyperframes/player perf",
"format:check": "oxfmt --check .",
"knip": "knip",
"test:scripts": "node --import tsx --test scripts/check-tracked-artifacts.test.mjs scripts/validate-release-channel.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/verify-packed-manifests.test.mjs",
"test:scripts": "node --import tsx --test scripts/check-tracked-artifacts.test.mjs scripts/check-workspace-contracts.test.mjs scripts/validate-release-channel.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs",
"test:skills": "node --test 'skills/**/*.test.mjs'",
"generate:previews": "tsx scripts/generate-template-previews.ts",
"generate:catalog-previews": "tsx scripts/generate-catalog-previews.ts",
+7 -2
View File
@@ -5,7 +5,9 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
"build": "vite build",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
"@hyperframes/core": "workspace:*",
@@ -13,6 +15,9 @@
"gsap": "^3.15.0"
},
"devDependencies": {
"vite": "^6.4.2"
"@types/node": "^25.0.10",
"typescript": "^5.0.0",
"vite": "^6.4.2",
"vitest": "^3.2.4"
}
}
@@ -0,0 +1,49 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { createFileAdapter } from "./fileAdapter";
afterEach(() => {
vi.unstubAllGlobals();
});
describe("createFileAdapter", () => {
it("loads the initial composition through the public SDK adapter contract", async () => {
const fetchMock = vi
.fn()
.mockImplementation(() => Promise.resolve(new Response("<main>demo</main>")));
vi.stubGlobal("fetch", fetchMock);
const { adapter, initialHtml } = await createFileAdapter();
expect(initialHtml).toBe("<main>demo</main>");
expect(await adapter.read("ignored.html")).toBe("<main>demo</main>");
expect(fetchMock).toHaveBeenCalledWith("/api/composition");
});
it("reports failed writes through persist:error without rejecting", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(new Response("", { status: 404 }))
.mockResolvedValueOnce(new Response("failed", { status: 500 }));
vi.stubGlobal("fetch", fetchMock);
const { adapter } = await createFileAdapter();
const errors: string[] = [];
adapter.on("persist:error", (event) => errors.push(event.error.message));
await expect(adapter.write("ignored.html", "<main>updated</main>")).resolves.toBeUndefined();
expect(errors).toEqual(["Error: write failed: 500"]);
});
it("maps persisted versions to the SDK version shape", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(new Response("", { status: 404 }))
.mockResolvedValueOnce(Response.json([{ key: "version-2", timestamp: 42 }], { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
const { adapter } = await createFileAdapter();
await expect(adapter.listVersions("ignored.html")).resolves.toEqual([
{ key: "version-2", content: "", timestamp: 42 },
]);
});
});
+1 -2
View File
@@ -1,5 +1,4 @@
import type { PersistAdapter, PersistVersionEntry } from "@hyperframes/sdk/adapters/types";
import type { PersistErrorEvent } from "@hyperframes/sdk";
import type { PersistAdapter, PersistErrorEvent, PersistVersionEntry } from "@hyperframes/sdk";
const API = "/api/composition";
+2 -2
View File
@@ -256,7 +256,7 @@ const TRACK_COLORS = ["#3b82f6", "#8b5cf6", "#f59e0b", "#10b981", "#f87171", "#0
function selectorToHfId(selector: string): string | null {
const m = /\[data-hf-id=['"]([^'"]+)['"]\]/.exec(selector);
if (m) return m[1];
if (m) return m[1] ?? null;
if (/^#/.test(selector.trim())) return selector.trim().slice(1);
return null;
}
@@ -347,7 +347,7 @@ function buildTrackRow(
index: number,
dur: number,
): HTMLDivElement {
const color = TRACK_COLORS[index % TRACK_COLORS.length];
const color = TRACK_COLORS[index % TRACK_COLORS.length] ?? "#3b82f6";
const row = document.createElement("div");
row.className = "tl-row";
const labelEl = document.createElement("div");
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["node", "vite/client"],
"strict": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"isolatedModules": true
},
"include": ["src", "vite.config.ts"]
}
+1 -1
View File
@@ -4,7 +4,7 @@ import type { Plugin } from "vite";
import type { Connect } from "vite";
import type { ServerResponse } from "node:http";
import { createFsAdapter } from "@hyperframes/sdk/adapters/fs";
import type { PersistAdapter } from "@hyperframes/sdk/adapters/types";
import type { PersistAdapter } from "@hyperframes/sdk";
const COMP_ROOT = path.resolve(import.meta.dirname);
const COMP_PATH = "composition.html";
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env node
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = join(import.meta.dirname, "..");
export const REQUIRED_WORKSPACE_SCRIPTS = ["build", "typecheck", "test"];
function hasExecutableScript(pkg, script) {
return typeof pkg.scripts?.[script] === "string" && pkg.scripts[script].trim().length > 0;
}
function hasSpecificReason(reason) {
return typeof reason === "string" ? reason.trim().length >= 20 : false;
}
function hasDocumentedOptOut(pkg, script) {
const optOut = pkg.hyperframesWorkspaceContract?.[script];
return optOut?.optOut === true && hasSpecificReason(optOut.reason);
}
export function listWorkspaceContractIssues(workspace, pkg) {
return REQUIRED_WORKSPACE_SCRIPTS.flatMap((script) => {
if (hasExecutableScript(pkg, script) || hasDocumentedOptOut(pkg, script)) return [];
return [
`${workspace}: missing executable \`${script}\` script or ` +
`hyperframesWorkspaceContract.${script} opt-out with a specific reason`,
];
});
}
export function listWorkspacePackages(root = ROOT) {
const packagesDir = join(root, "packages");
return readdirSync(packagesDir)
.sort()
.filter((name) => existsSync(join(packagesDir, name, "package.json")))
.map((name) => {
const workspace = `packages/${name}`;
const pkg = JSON.parse(readFileSync(join(root, workspace, "package.json"), "utf8"));
return { workspace, pkg };
});
}
export function checkWorkspaceContracts(root = ROOT) {
return listWorkspacePackages(root).flatMap(({ workspace, pkg }) =>
listWorkspaceContractIssues(workspace, pkg),
);
}
function main() {
const issues = checkWorkspaceContracts();
if (issues.length > 0) {
console.error("Workspace contract violations:");
issues.forEach((issue) => console.error(`- ${issue}`));
process.exitCode = 1;
return;
}
console.log("Workspace contracts verified: build, typecheck, and test are explicit.");
}
if (process.argv[1] === fileURLToPath(import.meta.url)) main();
@@ -0,0 +1,40 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
listWorkspaceContractIssues,
REQUIRED_WORKSPACE_SCRIPTS,
} from "./check-workspace-contracts.mjs";
describe("workspace contract checker", () => {
it("accepts workspaces with all executable lifecycle scripts", () => {
const scripts = Object.fromEntries(REQUIRED_WORKSPACE_SCRIPTS.map((name) => [name, "echo ok"]));
assert.deepEqual(listWorkspaceContractIssues("packages/example", { scripts }), []);
});
it("reports every missing or empty lifecycle script", () => {
assert.deepEqual(
listWorkspaceContractIssues("packages/example", { scripts: { build: " " } }),
REQUIRED_WORKSPACE_SCRIPTS.map(
(script) =>
`packages/example: missing executable \`${script}\` script or ` +
`hyperframesWorkspaceContract.${script} opt-out with a specific reason`,
),
);
});
it("accepts an explicit opt-out only when it has a specific reason", () => {
const pkg = {
scripts: { build: "build", typecheck: "typecheck" },
hyperframesWorkspaceContract: {
test: {
optOut: true,
reason: "Generated fixture validated by the owning package integration suite.",
},
},
};
assert.deepEqual(listWorkspaceContractIssues("packages/example", pkg), []);
pkg.hyperframesWorkspaceContract.test.reason = "no tests";
assert.equal(listWorkspaceContractIssues("packages/example", pkg).length, 1);
});
});
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env node
import { createRequire } from "node:module";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = join(import.meta.dirname, "..");
const PROJECT_ID = "smoke-test";
export const SMOKE_COMPOSITION_HTML =
'<!doctype html><html><body><div data-composition-id="root" data-width="1920" ' +
'data-height="1080" data-duration="1" data-start="0"><div class="clip" ' +
'data-hf-id="title" data-start="0" data-duration="1">Test</div></div></body></html>';
function json(body, status = 200) {
return { status, contentType: "application/json", body: JSON.stringify(body) };
}
function text(body, contentType = "text/plain") {
return { status: 200, contentType, body };
}
const PROJECT_PATH = `/api/projects/${PROJECT_ID}`;
const GET_RESPONSES = new Map([
[
"/api/projects",
json({ projects: [{ id: PROJECT_ID, dir: "/tmp/smoke-test", title: "Smoke test" }] }),
],
[
PROJECT_PATH,
json({
id: PROJECT_ID,
dir: "/tmp/smoke-test",
title: "Smoke test",
files: ["index.html"],
compositions: ["index.html"],
}),
],
[`${PROJECT_PATH}/preview`, text(SMOKE_COMPOSITION_HTML, "text/html")],
[`${PROJECT_PATH}/renders`, json({ renders: [] })],
[`${PROJECT_PATH}/lint`, json({ findings: [] })],
[
`${PROJECT_PATH}/storyboard`,
json({
exists: false,
path: "STORYBOARD.md",
globals: { extra: {} },
frames: [],
warnings: [],
script: { exists: false, path: "SCRIPT.md", content: "" },
}),
],
[`${PROJECT_PATH}/selection`, json({ selection: null, updatedAt: null })],
["/api/registry/blocks", json([])],
["/api/fonts", json({ fonts: [] })],
["/api/fonts/google", json({ fonts: [] })],
["/api/assets/global", json({ assets: [] })],
]);
const MUTATION_RESPONSES = new Map([
[`${PROJECT_PATH}/selection`, json({ ok: true, selection: null, updatedAt: null })],
]);
function projectFileResponse(pathname) {
if (!pathname.startsWith(`${PROJECT_PATH}/files/`)) return undefined;
const filename = decodeURIComponent(pathname.split("/files/")[1] ?? "");
return json({
filename,
content: filename === "index.html" ? SMOKE_COMPOSITION_HTML : "",
});
}
function projectPreviewResponse(pathname) {
if (!pathname.startsWith(`${PROJECT_PATH}/preview/`)) return undefined;
return pathname.endsWith("/.media/manifest.jsonl")
? text("", "application/x-ndjson")
: text(SMOKE_COMPOSITION_HTML, "text/html");
}
function gsapAnimationsResponse(pathname) {
if (!pathname.startsWith(`${PROJECT_PATH}/gsap-animations/`)) return undefined;
return json({ animations: [], timelineVar: "tl", preamble: "", postamble: "" });
}
function getStudioSmokeResponse(pathname) {
return (
GET_RESPONSES.get(pathname) ??
projectFileResponse(pathname) ??
projectPreviewResponse(pathname) ??
gsapAnimationsResponse(pathname)
);
}
function studioSmokeApiPathResponse(method, pathname) {
return method === "GET"
? (getStudioSmokeResponse(pathname) ?? null)
: (MUTATION_RESPONSES.get(pathname) ?? null);
}
export function studioSmokeApiResponse(method, requestUrl) {
const { pathname } = new URL(requestUrl);
return pathname.startsWith("/api/") ? studioSmokeApiPathResponse(method, pathname) : undefined;
}
export function isExpectedStudioSmokeError(message) {
return message.includes("favicon.ico");
}
async function loadPuppeteer() {
const requireFromProducer = createRequire(join(ROOT, "packages", "producer", "package.json"));
return requireFromProducer("puppeteer");
}
export async function runStudioRuntimeSmoke(targetUrl) {
const puppeteer = await loadPuppeteer();
const browser = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
const errors = [];
const unmockedApiRequests = [];
page.on("pageerror", (error) => errors.push(error.message));
page.on("console", (message) => {
if (message.type() === "error") errors.push(message.text());
});
await page.setRequestInterception(true);
page.on("request", (request) => {
const response = studioSmokeApiResponse(request.method(), request.url());
if (response === undefined) {
void request.continue();
return;
}
if (response === null) {
unmockedApiRequests.push(`${request.method()} ${new URL(request.url()).pathname}`);
void request.respond(json({ error: "unmocked smoke endpoint" }, 501));
return;
}
void request.respond(response);
});
try {
await page.goto(targetUrl, { waitUntil: "networkidle0", timeout: 30_000 });
await new Promise((resolve) => setTimeout(resolve, 3_000));
const errorBoundary = await page.evaluate(() => {
const textContent = document.body.innerText;
return textContent.includes("Something went wrong") ? textContent : null;
});
if (errorBoundary) errors.push(`React error boundary triggered: ${errorBoundary}`);
} finally {
await browser.close();
}
const fatal = errors.filter((error) => !isExpectedStudioSmokeError(error));
const failures = [
...new Set(unmockedApiRequests.map((request) => `unmocked API request: ${request}`)),
...fatal,
];
if (failures.length > 0) {
throw new Error(
`Studio runtime smoke failed:\n${failures.map((error) => `- ${error}`).join("\n")}`,
);
}
}
async function main() {
const targetUrl = process.argv[2] ?? "http://localhost:5199/#project=smoke-test";
await runStudioRuntimeSmoke(targetUrl);
console.log("PASS: studio loaded with schema-valid API fixtures and no runtime errors");
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main().catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
}
+58
View File
@@ -0,0 +1,58 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
isExpectedStudioSmokeError,
SMOKE_COMPOSITION_HTML,
studioSmokeApiResponse,
} from "./studio-runtime-smoke.mjs";
function bodyOf(method, path) {
const response = studioSmokeApiResponse(method, `http://localhost:5199${path}`);
assert.ok(response && response !== null);
return JSON.parse(response.body);
}
describe("Studio runtime smoke fixtures", () => {
it("matches the collection and project-detail API schemas", () => {
assert.deepEqual(bodyOf("GET", "/api/projects"), {
projects: [{ id: "smoke-test", dir: "/tmp/smoke-test", title: "Smoke test" }],
});
assert.deepEqual(bodyOf("GET", "/api/projects/smoke-test"), {
id: "smoke-test",
dir: "/tmp/smoke-test",
title: "Smoke test",
files: ["index.html"],
compositions: ["index.html"],
});
});
it("returns file content rather than the unrelated file-tree shape", () => {
assert.deepEqual(bodyOf("GET", "/api/projects/smoke-test/files/index.html"), {
filename: "index.html",
content: SMOKE_COMPOSITION_HTML,
});
});
it("returns iterable collections for eager Studio queries", () => {
assert.deepEqual(bodyOf("GET", "/api/projects/smoke-test/renders"), { renders: [] });
assert.deepEqual(bodyOf("GET", "/api/projects/smoke-test/gsap-animations/index.html"), {
animations: [],
timelineVar: "tl",
preamble: "",
postamble: "",
});
assert.deepEqual(bodyOf("GET", "/api/projects/smoke-test/lint"), { findings: [] });
});
it("marks unknown API requests as fixture failures", () => {
assert.equal(studioSmokeApiResponse("GET", "http://localhost:5199/api/new-endpoint"), null);
assert.equal(studioSmokeApiResponse("GET", "http://localhost:5199/src/main.tsx"), undefined);
});
it("does not suppress generic JavaScript or network failures", () => {
assert.equal(isExpectedStudioSmokeError("Cannot read properties of undefined"), false);
assert.equal(isExpectedStudioSmokeError("value is not iterable"), false);
assert.equal(isExpectedStudioSmokeError("Failed to fetch"), false);
assert.equal(isExpectedStudioSmokeError("GET /favicon.ico 404"), true);
});
});