test(e2e): gate beta packaged runtime (#637)

* test(e2e): gate beta mac packaged runtime

* test(e2e): separate ui automation layout

* test(e2e): move localized content coverage

* chore(release): prepare packaged 0.4.1 beta validation

* test(e2e): keep ui lane playwright-only

* fix(web): keep chat recoverable after conversation load failure

* fix(desktop): honor native mac quit
This commit is contained in:
PerishFire
2026-05-06 17:44:29 +08:00
committed by GitHub
parent 95bd7e5373
commit f1cdb2844a
49 changed files with 1174 additions and 2178 deletions
+137 -2
View File
@@ -1,4 +1,4 @@
import { readdir } from "node:fs/promises";
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
const repoRoot = path.resolve(import.meta.dirname, "..");
@@ -69,6 +69,10 @@ const residualAllowedPathPrefixes = [
"e2e/reports/html/",
"e2e/reports/playwright-html-report/",
"e2e/reports/test-results/",
"e2e/ui/.od-data/",
"e2e/ui/reports/playwright-html-report/",
"e2e/ui/reports/test-results/",
"e2e/ui/test-results/",
// Vendored upstream HyperFrames skill helper scripts.
"skills/hyperframes/scripts/",
// Vendored upstream html-ppt skill runtime assets (lewislulu/html-ppt-skill).
@@ -136,7 +140,7 @@ async function checkResidualJavaScript(): Promise<boolean> {
}
const testLayoutScopedDirectories = ["apps", "packages", "tools"];
const testLayoutSkippedDirectories = new Set([".next", "dist", "node_modules", "out"]);
const testLayoutSkippedDirectories = new Set([".next", ".od-data", "dist", "node_modules", "out", "reports", "test-results"]);
function isTestFile(fileName: string): boolean {
return /\.test\.tsx?$/.test(fileName);
@@ -205,9 +209,140 @@ async function checkTestLayout(): Promise<boolean> {
return true;
}
const e2ePackageJsonPath = path.join(repoRoot, "e2e", "package.json");
const e2eSkippedDirectories = new Set([".od-data", "node_modules", "reports", "test-results"]);
const e2eAllowedScripts = ["test", "typecheck"];
async function collectRepositoryFiles(directory: string, skippedDirectoryNames = new Set<string>()): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
if (skippedDirectoryNames.has(entry.name)) continue;
files.push(...(await collectRepositoryFiles(fullPath, skippedDirectoryNames)));
continue;
}
if (entry.isFile()) files.push(toRepositoryPath(fullPath));
}
return files;
}
async function checkE2eLayout(): Promise<boolean> {
const violations: string[] = [];
const packageJson = JSON.parse(await readFile(e2ePackageJsonPath, "utf8")) as {
scripts?: Record<string, unknown>;
};
const scriptNames = Object.keys(packageJson.scripts ?? {}).sort();
if (scriptNames.join("\0") !== e2eAllowedScripts.join("\0")) {
violations.push(
`e2e/package.json scripts must be exactly ${e2eAllowedScripts.join(", ")} (found: ${scriptNames.join(", ")})`,
);
}
const e2eRoot = path.join(repoRoot, "e2e");
for (const repositoryPath of await collectRepositoryFiles(e2eRoot, e2eSkippedDirectories)) {
if (
repositoryPath === "e2e/package.json" ||
repositoryPath === "e2e/tsconfig.json" ||
repositoryPath === "e2e/vitest.config.ts" ||
repositoryPath === "e2e/playwright.config.ts" ||
repositoryPath === "e2e/AGENTS.md"
) {
continue;
}
if (repositoryPath.startsWith("e2e/specs/")) {
if (!/\.spec\.ts$/.test(repositoryPath)) {
violations.push(`${repositoryPath} -> e2e specs must be *.spec.ts`);
}
continue;
}
if (repositoryPath.startsWith("e2e/tests/")) {
if (!/\.test\.ts$/.test(repositoryPath)) {
violations.push(`${repositoryPath} -> e2e tests must be *.test.ts`);
}
continue;
}
if (repositoryPath.startsWith("e2e/ui/")) {
const relativePath = repositoryPath.slice("e2e/ui/".length);
if (relativePath.includes("/") || !/\.test\.ts$/.test(repositoryPath)) {
violations.push(`${repositoryPath} -> e2e UI files must be flat Playwright *.test.ts files under ui/`);
}
continue;
}
if (repositoryPath.startsWith("e2e/resources/")) {
const relativePath = repositoryPath.slice("e2e/resources/".length);
if (relativePath.includes("/") || !/\.ts$/.test(repositoryPath)) {
violations.push(`${repositoryPath} -> e2e resources must be flat TypeScript files under resources/`);
}
continue;
}
if (repositoryPath.startsWith("e2e/lib/")) {
if (!/\.ts$/.test(repositoryPath)) {
violations.push(`${repositoryPath} -> e2e lib files must be TypeScript`);
}
continue;
}
if (repositoryPath.startsWith("e2e/scripts/")) {
if (repositoryPath !== "e2e/scripts/playwright.ts") {
violations.push(`${repositoryPath} -> e2e scripts currently allow only scripts/playwright.ts`);
}
continue;
}
violations.push(`${repositoryPath} -> e2e source files must live in specs/, tests/, ui/, resources/, lib/, or scripts/playwright.ts`);
}
if (violations.length > 0) {
console.error("E2E package layout violations found:");
for (const violation of violations) console.error(`- ${violation}`);
return false;
}
console.log("E2E layout check passed: Vitest, Playwright UI, resources, lib, and scripts stay in their lanes.");
return true;
}
const webTestSkippedDirectories = new Set([".od-data", "reports", "test-results"]);
async function checkWebTestLayout(): Promise<boolean> {
const violations: string[] = [];
const webTestsRoot = path.join(repoRoot, "apps", "web", "tests");
for (const repositoryPath of await collectRepositoryFiles(webTestsRoot, webTestSkippedDirectories)) {
if (repositoryPath.startsWith("apps/web/tests/vitest/") || repositoryPath.startsWith("apps/web/tests/playwright/")) {
violations.push(`${repositoryPath} -> web tests should stay lightweight under apps/web/tests/ without vitest/playwright nesting`);
continue;
}
if (/\.(spec|test)\.tsx?$/.test(repositoryPath) && !/\.test\.tsx?$/.test(repositoryPath)) {
violations.push(`${repositoryPath} -> web Vitest test files must be *.test.ts or *.test.tsx`);
}
}
if (violations.length > 0) {
console.error("Web test layout violations found:");
for (const violation of violations) console.error(`- ${violation}`);
return false;
}
console.log("Web test layout check passed: web tests stay lightweight and Vitest-only.");
return true;
}
const checks: GuardCheck[] = [
{ name: "residual JavaScript", run: checkResidualJavaScript },
{ name: "test layout", run: checkTestLayout },
{ name: "e2e layout", run: checkE2eLayout },
{ name: "web test layout", run: checkWebTestLayout },
];
const results: boolean[] = [];