Files
cfw-autumn/server/tests/testRunner/groupRunnerV2.ts
2025-10-24 10:11:16 +01:00

395 lines
9.0 KiB
TypeScript

#!/usr/bin/env bun
import dotenv from "dotenv";
import { resolve } from "path";
import { spawn } from "bun";
// Load environment variables from server/.env
dotenv.config({ path: resolve(import.meta.dir, "..", "..", ".env") });
import type { TestGroup } from "./config.js";
import { runTests } from "./runTestsV2.js";
export type TestFileProgress = {
name: string;
status: "pending" | "running" | "passed" | "failed";
duration?: number;
error?: string;
output?: string; // Full test output for debugging
};
export type GroupProgress = {
status: "pending" | "setup" | "running" | "passed" | "failed";
files: TestFileProgress[];
duration?: number;
error?: string;
};
export type ProgressCallback = (progress: GroupProgress) => void;
export type GroupResult = {
group: TestGroup;
success: boolean;
output: string;
error?: string;
duration: number;
files: TestFileProgress[];
};
/**
* Calls the platform API to delete an org by slug
*/
async function deleteOrg({ slug }: { slug: string }): Promise<void> {
const secretKey = process.env.TEST_ORG_SECRET_KEY;
if (!secretKey) {
throw new Error("TEST_ORG_SECRET_KEY not found in environment");
}
const baseUrl = process.env.BASE_URL || "http://localhost:8080";
const response = await fetch(`${baseUrl}/v1/platform/beta/organizations`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secretKey}`,
},
body: JSON.stringify({ slug }),
});
if (!response.ok) {
// If org doesn't exist (404), that's fine - we just wanted it deleted anyway
if (response.status === 404) {
return;
}
const error = await response.text();
throw new Error(
`Failed to delete org ${slug}: ${response.status} ${error}`,
);
}
}
/**
* Calls the platform API to get existing org credentials
*/
async function getExistingOrg({
slug,
}: {
slug: string;
}): Promise<{ secretKey: string; fullSlug: string } | null> {
const secretKey = process.env.TEST_ORG_SECRET_KEY;
if (!secretKey) {
throw new Error("TEST_ORG_SECRET_KEY not found in environment");
}
const baseUrl = process.env.BASE_URL || "http://localhost:8080";
const response = await fetch(`${baseUrl}/v1/platform/beta/organizations`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secretKey}`,
},
body: JSON.stringify({
org_slug: slug,
}),
});
if (!response.ok) {
if (response.status === 404) {
return null;
}
const error = await response.text();
throw new Error(
`Failed to get org ${slug}: ${response.status} ${error}`,
);
}
const data = await response.json();
if (!data.test_secret_key) {
throw new Error(`No test_secret_key returned for org ${slug}`);
}
return {
secretKey: data.test_secret_key,
fullSlug: slug,
};
}
/**
* Calls the platform API to create a new org
*/
async function createOrg({
slug,
name,
userEmail,
}: {
slug: string;
name: string;
userEmail: string;
}): Promise<{ secretKey: string; fullSlug: string }> {
const secretKey = process.env.TEST_ORG_SECRET_KEY;
if (!secretKey) {
throw new Error("TEST_ORG_SECRET_KEY not found in environment");
}
const baseUrl = process.env.BASE_URL || "http://localhost:8080";
const response = await fetch(`${baseUrl}/v1/platform/beta/organizations`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secretKey}`,
},
body: JSON.stringify({
user_email: userEmail,
name,
slug,
env: "test",
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(
`Failed to create org ${slug}: ${response.status} ${error}`,
);
}
const data = await response.json();
if (!data.test_secret_key) {
throw new Error(`No test_secret_key returned for org ${slug}`);
}
if (!data.org_slug) {
throw new Error(`No org_slug returned for org ${slug}`);
}
// Wait a moment for API key cache to propagate
await new Promise((resolve) => setTimeout(resolve, 1000));
return {
secretKey: data.test_secret_key,
fullSlug: data.org_slug,
};
}
/**
* Parse test file list from directory paths
*/
async function getTestFiles(paths: string[]): Promise<string[]> {
const { readdir } = await import("fs/promises");
const testFiles: string[] = [];
for (const path of paths) {
const resolvedPath = resolve(process.cwd(), path);
// Check if it's a specific test file
if (path.endsWith(".test.ts")) {
testFiles.push(resolvedPath);
continue;
}
// Otherwise treat it as a directory
try {
const files = await readdir(resolvedPath);
for (const file of files) {
if (file.endsWith(".test.ts")) {
testFiles.push(resolve(resolvedPath, file));
}
}
} catch (error) {
// Ignore read errors
}
}
return testFiles;
}
/**
* Extract file name from path
*/
function getFileName(filePath: string): string {
return filePath.split("/").pop() || filePath;
}
/**
* Runs tests for a single group with progress callbacks
*/
export async function runTestGroupV2({
group,
skipSetup = false,
onProgress,
}: {
group: TestGroup;
skipSetup?: boolean;
onProgress?: ProgressCallback;
}): Promise<GroupResult> {
const startTime = performance.now();
let output = "";
// Get test files upfront
const testFilePaths = await getTestFiles(group.paths);
const files: TestFileProgress[] = testFilePaths.map((path) => ({
name: getFileName(path),
status: "pending" as const,
}));
// Report initial state
onProgress?.({
status: skipSetup ? "running" : "setup",
files,
duration: 0,
});
try {
let secretKey: string;
let fullSlug: string;
if (skipSetup) {
// Try to get org from API
const existing = await getExistingOrg({ slug: group.slug });
if (!existing) {
throw new Error(
`Cannot skip setup: org ${group.slug} not found. Run with --setup flag to create it: bun t ${group.slug} --setup`,
);
}
secretKey = existing.secretKey;
fullSlug = existing.fullSlug;
} else {
// 1. Delete existing org
try {
await deleteOrg({ slug: group.slug });
} catch (error: any) {
// Ignore delete errors
}
// 2. Create new org
const orgResult = await createOrg({
slug: group.slug,
name: `Test Group: ${group.slug}`,
userEmail: "test@gmail.com",
});
secretKey = orgResult.secretKey;
fullSlug = orgResult.fullSlug;
// 3. Run setup
const serverDir = resolve(import.meta.dir, "..", "..");
const setupPath = resolve(serverDir, "tests/setupMain.ts");
const setupProc = spawn(["bun", setupPath], {
stdout: "pipe",
stderr: "pipe",
cwd: serverDir,
env: {
...process.env,
UNIT_TEST_AUTUMN_SECRET_KEY: secretKey,
TESTS_ORG: fullSlug,
},
});
// Collect setup output silently
let setupOutput = "";
const setupDecoder = new TextDecoder();
if (setupProc.stdout) {
for await (const chunk of setupProc.stdout) {
setupOutput += setupDecoder.decode(chunk);
}
}
if (setupProc.stderr) {
for await (const chunk of setupProc.stderr) {
setupOutput += setupDecoder.decode(chunk);
}
}
await setupProc.exited;
if (setupProc.exitCode !== 0) {
throw new Error(
`Setup failed for ${group.slug}: ${setupOutput.slice(0, 500)}`,
);
}
}
// 5. Run tests with real-time progress callbacks
onProgress?.({
status: "running",
files,
duration: performance.now() - startTime,
});
// Set environment for test execution
process.env.UNIT_TEST_AUTUMN_SECRET_KEY = secretKey;
process.env.TESTS_ORG = fullSlug;
// Run tests with progress callbacks
const results = await runTests(group.paths, {
maxParallel: 6,
progress: {
onTestStart: (file) => {
const fileName = getFileName(file);
const fileIndex = files.findIndex((f) => f.name === fileName);
if (fileIndex !== -1) {
files[fileIndex].status = "running";
onProgress?.({
status: "running",
files: [...files],
duration: performance.now() - startTime,
});
}
},
onTestComplete: (file, result) => {
const fileName = getFileName(file);
const fileIndex = files.findIndex((f) => f.name === fileName);
if (fileIndex !== -1) {
files[fileIndex].status = result.status;
files[fileIndex].duration = result.duration;
if (result.error) {
files[fileIndex].error = result.error;
}
if (result.output) {
files[fileIndex].output = result.output;
}
onProgress?.({
status: "running",
files: [...files],
duration: performance.now() - startTime,
});
}
},
},
});
const duration = performance.now() - startTime;
const success = results.every((r) => r.status === "passed");
onProgress?.({
status: success ? "passed" : "failed",
files,
duration,
});
return {
group,
success,
output,
duration,
files,
error: success ? undefined : "One or more tests failed",
};
} catch (error: any) {
const duration = performance.now() - startTime;
onProgress?.({
status: "failed",
files,
duration,
error: error.message,
});
return {
group,
success: false,
output,
error: error.message,
duration,
files,
};
}
}