Brings the working codebase (Next.js app, auth system, Stripe billing, Docker/deploy config, tests, docs) into version control on top of the placeholder initial commit, and adds account self-deletion (Danger Zone in Settings, password + typed-email confirmation, cascading DB cleanup, Stripe cancellation) per GDPR right-to-erasure. Excludes local build caches, node_modules, and internal agent scratch files; .gitignore hardened to keep those out going forward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
652 lines
20 KiB
TypeScript
652 lines
20 KiB
TypeScript
/**
|
|
* Zenith Silver E2E Test Runner & Assertion Library
|
|
* High-performance, zero-dependency async test framework for Receipt Scanner to Excel
|
|
*/
|
|
|
|
// ANSI Color Codes
|
|
const RESET = "\x1b[0m";
|
|
const BOLD = "\x1b[1m";
|
|
const DIM = "\x1b[2m";
|
|
const RED = "\x1b[31m";
|
|
const GREEN = "\x1b[32m";
|
|
const YELLOW = "\x1b[33m";
|
|
const BLUE = "\x1b[34m";
|
|
const MAGENTA = "\x1b[35m";
|
|
const CYAN = "\x1b[36m";
|
|
const WHITE = "\x1b[37m";
|
|
const BG_RED = "\x1b[41m";
|
|
const BG_GREEN = "\x1b[42m";
|
|
|
|
export type TestFn = () => void | Promise<void>;
|
|
export type HookFn = () => void | Promise<void>;
|
|
|
|
export interface TestCase {
|
|
name: string;
|
|
fn: TestFn;
|
|
durationMs?: number;
|
|
error?: Error;
|
|
passed?: boolean;
|
|
}
|
|
|
|
export interface TestSuite {
|
|
name: string;
|
|
tests: TestCase[];
|
|
beforeAllHooks: HookFn[];
|
|
afterAllHooks: HookFn[];
|
|
beforeEachHooks: HookFn[];
|
|
afterEachHooks: HookFn[];
|
|
parent?: TestSuite;
|
|
children: TestSuite[];
|
|
}
|
|
|
|
class TestRegistry {
|
|
suites: TestSuite[] = [];
|
|
currentSuite: TestSuite | null = null;
|
|
|
|
createSuite(name: string, parent?: TestSuite): TestSuite {
|
|
return {
|
|
name,
|
|
tests: [],
|
|
beforeAllHooks: [],
|
|
afterAllHooks: [],
|
|
beforeEachHooks: [],
|
|
afterEachHooks: [],
|
|
parent,
|
|
children: [],
|
|
};
|
|
}
|
|
}
|
|
|
|
const registry = new TestRegistry();
|
|
|
|
export function describe(name: string, fn: () => void) {
|
|
const suite = registry.createSuite(name, registry.currentSuite || undefined);
|
|
if (registry.currentSuite) {
|
|
registry.currentSuite.children.push(suite);
|
|
} else {
|
|
registry.suites.push(suite);
|
|
}
|
|
|
|
const previousSuite = registry.currentSuite;
|
|
registry.currentSuite = suite;
|
|
try {
|
|
fn();
|
|
} finally {
|
|
registry.currentSuite = previousSuite;
|
|
}
|
|
}
|
|
|
|
export function test(name: string, fn: TestFn) {
|
|
if (!registry.currentSuite) {
|
|
const rootSuite = registry.createSuite("Root Suite");
|
|
registry.suites.push(rootSuite);
|
|
registry.currentSuite = rootSuite;
|
|
}
|
|
registry.currentSuite.tests.push({ name, fn });
|
|
}
|
|
|
|
export const it = test;
|
|
|
|
export function beforeAll(fn: HookFn) {
|
|
if (registry.currentSuite) {
|
|
registry.currentSuite.beforeAllHooks.push(fn);
|
|
}
|
|
}
|
|
|
|
export function afterAll(fn: HookFn) {
|
|
if (registry.currentSuite) {
|
|
registry.currentSuite.afterAllHooks.push(fn);
|
|
}
|
|
}
|
|
|
|
export function beforeEach(fn: HookFn) {
|
|
if (registry.currentSuite) {
|
|
registry.currentSuite.beforeEachHooks.push(fn);
|
|
}
|
|
}
|
|
|
|
export function afterEach(fn: HookFn) {
|
|
if (registry.currentSuite) {
|
|
registry.currentSuite.afterEachHooks.push(fn);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// ASSERTION LIBRARY (expect)
|
|
// ============================================================================
|
|
|
|
export class AssertionError extends Error {
|
|
constructor(message: string, public actual?: any, public expected?: any) {
|
|
super(message);
|
|
this.name = "AssertionError";
|
|
}
|
|
}
|
|
|
|
function deepEqual(a: any, b: any): boolean {
|
|
if (a === b) return true;
|
|
if (a == null || b == null) return false;
|
|
if (typeof a !== "object" || typeof b !== "object") return false;
|
|
|
|
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
if (Array.isArray(a)) {
|
|
if (a.length !== b.length) return false;
|
|
for (let i = 0; i < a.length; i++) {
|
|
if (!deepEqual(a[i], b[i])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (a instanceof Date && b instanceof Date) {
|
|
return a.getTime() === b.getTime();
|
|
}
|
|
|
|
if (a instanceof RegExp && b instanceof RegExp) {
|
|
return a.toString() === b.toString();
|
|
}
|
|
|
|
const keysA = Object.keys(a);
|
|
const keysB = Object.keys(b);
|
|
if (keysA.length !== keysB.length) return false;
|
|
|
|
for (const key of keysA) {
|
|
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
|
if (!deepEqual(a[key], b[key])) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export interface Matchers<T> {
|
|
toBe(expected: any): void;
|
|
toEqual(expected: any): void;
|
|
toStrictEqual(expected: any): void;
|
|
toBeCloseTo(expected: number, deltaOrDigits?: number): void;
|
|
toBeGreaterThan(expected: number): void;
|
|
toBeGreaterThanOrEqual(expected: number): void;
|
|
toBeLessThan(expected: number): void;
|
|
toBeLessThanOrEqual(expected: number): void;
|
|
toBeTruthy(): void;
|
|
toBeFalsy(): void;
|
|
toBeNull(): void;
|
|
toBeUndefined(): void;
|
|
toBeDefined(): void;
|
|
toContain(expected: any): void;
|
|
toHaveLength(expected: number): void;
|
|
toMatch(regex: RegExp | string): void;
|
|
toBeInstanceOf(expected: any): void;
|
|
toThrow(expectedError?: string | RegExp | Function): void;
|
|
not: Matchers<T>;
|
|
}
|
|
|
|
export function expect<T = any>(actual: T): Matchers<T> {
|
|
const createMatcher = (isNot: boolean): Matchers<T> => {
|
|
return {
|
|
toBe(expected: any) {
|
|
const pass = Object.is(actual, expected);
|
|
if (isNot ? pass : !pass) {
|
|
throw new AssertionError(
|
|
`Expected ${JSON.stringify(actual)} ${isNot ? "NOT to be" : "to be"} ${JSON.stringify(expected)}`,
|
|
actual,
|
|
expected
|
|
);
|
|
}
|
|
},
|
|
|
|
toEqual(expected: any) {
|
|
const pass = deepEqual(actual, expected);
|
|
if (isNot ? pass : !pass) {
|
|
throw new AssertionError(
|
|
`Expected ${JSON.stringify(actual)} ${isNot ? "NOT to equal" : "to equal"} ${JSON.stringify(expected)}`,
|
|
actual,
|
|
expected
|
|
);
|
|
}
|
|
},
|
|
|
|
toStrictEqual(expected: any) {
|
|
const pass = deepEqual(actual, expected);
|
|
if (isNot ? pass : !pass) {
|
|
throw new AssertionError(
|
|
`Expected ${JSON.stringify(actual)} ${isNot ? "NOT to strictly equal" : "to strictly equal"} ${JSON.stringify(expected)}`,
|
|
actual,
|
|
expected
|
|
);
|
|
}
|
|
},
|
|
|
|
toBeCloseTo(expected: number, deltaOrDigits: number = 2) {
|
|
if (typeof actual !== "number") {
|
|
throw new AssertionError(`Actual value ${actual} is not a number`);
|
|
}
|
|
// If deltaOrDigits <= 0.1, treat as delta, otherwise decimal digits
|
|
const delta = deltaOrDigits < 1 ? deltaOrDigits : Math.pow(10, -deltaOrDigits) / 2;
|
|
const diff = Math.abs(actual - expected);
|
|
const pass = diff <= delta;
|
|
if (isNot ? pass : !pass) {
|
|
throw new AssertionError(
|
|
`Expected ${actual} ${isNot ? "NOT to be close to" : "to be close to"} ${expected} (diff: ${diff.toFixed(4)}, max allowed: ${delta})`,
|
|
actual,
|
|
expected
|
|
);
|
|
}
|
|
},
|
|
|
|
toBeGreaterThan(expected: number) {
|
|
const pass = (actual as any) > expected;
|
|
if (isNot ? pass : !pass) {
|
|
throw new AssertionError(
|
|
`Expected ${actual} ${isNot ? "NOT to be >" : "to be >"} ${expected}`,
|
|
actual,
|
|
expected
|
|
);
|
|
}
|
|
},
|
|
|
|
toBeGreaterThanOrEqual(expected: number) {
|
|
const pass = (actual as any) >= expected;
|
|
if (isNot ? pass : !pass) {
|
|
throw new AssertionError(
|
|
`Expected ${actual} ${isNot ? "NOT to be >=" : "to be >="} ${expected}`,
|
|
actual,
|
|
expected
|
|
);
|
|
}
|
|
},
|
|
|
|
toBeLessThan(expected: number) {
|
|
const pass = (actual as any) < expected;
|
|
if (isNot ? pass : !pass) {
|
|
throw new AssertionError(
|
|
`Expected ${actual} ${isNot ? "NOT to be <" : "to be <"} ${expected}`,
|
|
actual,
|
|
expected
|
|
);
|
|
}
|
|
},
|
|
|
|
toBeLessThanOrEqual(expected: number) {
|
|
const pass = (actual as any) <= expected;
|
|
if (isNot ? pass : !pass) {
|
|
throw new AssertionError(
|
|
`Expected ${actual} ${isNot ? "NOT to be <=" : "to be <="} ${expected}`,
|
|
actual,
|
|
expected
|
|
);
|
|
}
|
|
},
|
|
|
|
toBeTruthy() {
|
|
const pass = Boolean(actual);
|
|
if (isNot ? pass : !pass) {
|
|
throw new AssertionError(
|
|
`Expected ${JSON.stringify(actual)} ${isNot ? "to be falsy" : "to be truthy"}`,
|
|
actual,
|
|
!isNot
|
|
);
|
|
}
|
|
},
|
|
|
|
toBeFalsy() {
|
|
const pass = !Boolean(actual);
|
|
if (isNot ? pass : !pass) {
|
|
throw new AssertionError(
|
|
`Expected ${JSON.stringify(actual)} ${isNot ? "to be truthy" : "to be falsy"}`,
|
|
actual,
|
|
isNot
|
|
);
|
|
}
|
|
},
|
|
|
|
toBeNull() {
|
|
const pass = actual === null;
|
|
if (isNot ? pass : !pass) {
|
|
throw new AssertionError(
|
|
`Expected ${JSON.stringify(actual)} ${isNot ? "NOT to be null" : "to be null"}`,
|
|
actual,
|
|
null
|
|
);
|
|
}
|
|
},
|
|
|
|
toBeUndefined() {
|
|
const pass = actual === undefined;
|
|
if (isNot ? pass : !pass) {
|
|
throw new AssertionError(
|
|
`Expected ${actual} ${isNot ? "NOT to be undefined" : "to be undefined"}`,
|
|
actual,
|
|
undefined
|
|
);
|
|
}
|
|
},
|
|
|
|
toBeDefined() {
|
|
const pass = actual !== undefined;
|
|
if (isNot ? pass : !pass) {
|
|
throw new AssertionError(
|
|
`Expected value ${isNot ? "to be undefined" : "to be defined"}`,
|
|
actual,
|
|
"defined"
|
|
);
|
|
}
|
|
},
|
|
|
|
toContain(expected: any) {
|
|
let pass = false;
|
|
if (typeof actual === "string") {
|
|
pass = actual.includes(String(expected));
|
|
} else if (Array.isArray(actual)) {
|
|
pass = actual.some((item) => deepEqual(item, expected));
|
|
} else if (actual instanceof Set || actual instanceof Map) {
|
|
pass = (actual as any).has(expected);
|
|
}
|
|
if (isNot ? pass : !pass) {
|
|
throw new AssertionError(
|
|
`Expected container ${isNot ? "NOT to contain" : "to contain"} ${JSON.stringify(expected)}`,
|
|
actual,
|
|
expected
|
|
);
|
|
}
|
|
},
|
|
|
|
toHaveLength(expected: number) {
|
|
const len = (actual as any)?.length ?? (actual as any)?.size;
|
|
const pass = len === expected;
|
|
if (isNot ? pass : !pass) {
|
|
throw new AssertionError(
|
|
`Expected length ${isNot ? "NOT to be" : "to be"} ${expected}, but received ${len}`,
|
|
len,
|
|
expected
|
|
);
|
|
}
|
|
},
|
|
|
|
toMatch(regex: RegExp | string) {
|
|
const str = String(actual);
|
|
const re = typeof regex === "string" ? new RegExp(regex) : regex;
|
|
const pass = re.test(str);
|
|
if (isNot ? pass : !pass) {
|
|
throw new AssertionError(
|
|
`Expected string "${str}" ${isNot ? "NOT to match" : "to match"} pattern ${re}`,
|
|
actual,
|
|
regex
|
|
);
|
|
}
|
|
},
|
|
|
|
toBeInstanceOf(expected: any) {
|
|
const pass = actual instanceof expected;
|
|
if (isNot ? pass : !pass) {
|
|
throw new AssertionError(
|
|
`Expected object ${isNot ? "NOT to be instance of" : "to be instance of"} ${expected?.name || expected}`,
|
|
actual,
|
|
expected
|
|
);
|
|
}
|
|
},
|
|
|
|
toThrow(expectedError?: string | RegExp | Function) {
|
|
if (typeof actual !== "function") {
|
|
throw new AssertionError(`Actual target is not a function: ${typeof actual}`);
|
|
}
|
|
let threw = false;
|
|
let caughtError: any = null;
|
|
try {
|
|
(actual as any)();
|
|
} catch (err) {
|
|
threw = true;
|
|
caughtError = err;
|
|
}
|
|
|
|
if (isNot) {
|
|
if (threw) {
|
|
throw new AssertionError(
|
|
`Expected function NOT to throw, but it threw: ${caughtError?.message || caughtError}`
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!threw) {
|
|
throw new AssertionError(`Expected function to throw an error, but it returned cleanly`);
|
|
}
|
|
|
|
if (expectedError) {
|
|
const msg = caughtError?.message || String(caughtError);
|
|
if (typeof expectedError === "string" && !msg.includes(expectedError)) {
|
|
throw new AssertionError(
|
|
`Expected thrown error message to contain "${expectedError}", received: "${msg}"`
|
|
);
|
|
} else if (expectedError instanceof RegExp && !expectedError.test(msg)) {
|
|
throw new AssertionError(
|
|
`Expected thrown error message to match ${expectedError}, received: "${msg}"`
|
|
);
|
|
} else if (typeof expectedError === "function" && !(caughtError instanceof expectedError)) {
|
|
throw new AssertionError(
|
|
`Expected thrown error to be instance of ${expectedError.name}, received: ${caughtError?.name}`
|
|
);
|
|
}
|
|
}
|
|
},
|
|
|
|
get not() {
|
|
return createMatcher(!isNot);
|
|
},
|
|
};
|
|
};
|
|
|
|
return createMatcher(false);
|
|
}
|
|
|
|
// ============================================================================
|
|
// SUITE EXECUTION ENGINE
|
|
// ============================================================================
|
|
|
|
export interface RunResults {
|
|
totalSuites: number;
|
|
totalTests: number;
|
|
passedCount: number;
|
|
failedCount: number;
|
|
durationMs: number;
|
|
failures: { suiteName: string; testName: string; error: Error }[];
|
|
}
|
|
|
|
async function runSuite(
|
|
suite: TestSuite,
|
|
results: RunResults,
|
|
indent = ""
|
|
): Promise<void> {
|
|
results.totalSuites++;
|
|
console.log(`\n${indent}${BOLD}${CYAN}▸ ${suite.name}${RESET}`);
|
|
|
|
// Run beforeAll hooks
|
|
for (const hook of suite.beforeAllHooks) {
|
|
try {
|
|
await hook();
|
|
} catch (err: any) {
|
|
console.error(`${indent} ${RED}✖ [beforeAll Hook Failed]${RESET}`, err.message);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// Run suite tests
|
|
for (const testCase of suite.tests) {
|
|
results.totalTests++;
|
|
|
|
// Run beforeEach hooks
|
|
for (const hook of suite.beforeEachHooks) {
|
|
await hook();
|
|
}
|
|
|
|
const startTime = performance.now();
|
|
try {
|
|
await testCase.fn();
|
|
testCase.durationMs = performance.now() - startTime;
|
|
testCase.passed = true;
|
|
results.passedCount++;
|
|
|
|
const timeStr = testCase.durationMs > 100
|
|
? `${YELLOW}(${testCase.durationMs.toFixed(1)}ms)${RESET}`
|
|
: `${DIM}(${testCase.durationMs.toFixed(1)}ms)${RESET}`;
|
|
|
|
console.log(`${indent} ${GREEN}✓${RESET} ${testCase.name} ${timeStr}`);
|
|
} catch (err: any) {
|
|
testCase.durationMs = performance.now() - startTime;
|
|
testCase.passed = false;
|
|
testCase.error = err;
|
|
results.failedCount++;
|
|
results.failures.push({
|
|
suiteName: suite.name,
|
|
testName: testCase.name,
|
|
error: err,
|
|
});
|
|
|
|
console.log(`${indent} ${RED}✖ ${testCase.name}${RESET} ${RED}(${testCase.durationMs.toFixed(1)}ms)${RESET}`);
|
|
console.log(`${indent} ${RED}${err.name}: ${err.message}${RESET}`);
|
|
if (err.stack) {
|
|
const stackLines = err.stack.split("\n").slice(1, 4).map((l: string) => `${indent} ${DIM}${l.trim()}${RESET}`);
|
|
console.log(stackLines.join("\n"));
|
|
}
|
|
}
|
|
|
|
// Run afterEach hooks
|
|
for (const hook of suite.afterEachHooks) {
|
|
await hook();
|
|
}
|
|
}
|
|
|
|
// Run nested suites
|
|
for (const childSuite of suite.children) {
|
|
await runSuite(childSuite, results, indent + " ");
|
|
}
|
|
|
|
// Run afterAll hooks
|
|
for (const hook of suite.afterAllHooks) {
|
|
try {
|
|
await hook();
|
|
} catch (err: any) {
|
|
console.error(`${indent} ${RED}✖ [afterAll Hook Failed]${RESET}`, err.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function runAllTests(filterPattern?: string): Promise<boolean> {
|
|
const globalStartTime = performance.now();
|
|
|
|
console.log(`\n${BOLD}${WHITE}================================================================================${RESET}`);
|
|
console.log(`${BOLD}${CYAN} ZENITH SILVER RECEIPT SCANNER — END-TO-END VERIFICATION SUITE ${RESET}`);
|
|
console.log(`${BOLD}${WHITE}================================================================================${RESET}`);
|
|
console.log(`${DIM}Runner: Native Async TypeScript • Date: ${new Date().toISOString()}${RESET}\n`);
|
|
|
|
const results: RunResults = {
|
|
totalSuites: 0,
|
|
totalTests: 0,
|
|
passedCount: 0,
|
|
failedCount: 0,
|
|
durationMs: 0,
|
|
failures: [],
|
|
};
|
|
|
|
const suitesToRun = filterPattern
|
|
? registry.suites.filter((s) => s.name.toLowerCase().includes(filterPattern.toLowerCase()))
|
|
: registry.suites;
|
|
|
|
for (const suite of suitesToRun) {
|
|
await runSuite(suite, results);
|
|
}
|
|
|
|
results.durationMs = performance.now() - globalStartTime;
|
|
|
|
console.log(`\n${BOLD}${WHITE}================================================================================${RESET}`);
|
|
console.log(`${BOLD}TEST RUN SUMMARY${RESET}`);
|
|
console.log(`${BOLD}${WHITE}================================================================================${RESET}`);
|
|
console.log(`Total Suites : ${results.totalSuites}`);
|
|
console.log(`Total Tests : ${results.totalTests}`);
|
|
console.log(`Passed Tests : ${GREEN}${BOLD}${results.passedCount} ✓${RESET}`);
|
|
console.log(`Failed Tests : ${results.failedCount > 0 ? `${RED}${BOLD}${results.failedCount} ✖${RESET}` : `${GREEN}0${RESET}`}`);
|
|
console.log(`Total Time : ${(results.durationMs / 1000).toFixed(2)}s (${results.durationMs.toFixed(1)} ms)`);
|
|
|
|
if (results.failures.length > 0) {
|
|
console.log(`\n${BOLD}${RED}FAILED TESTS SUMMARY (${results.failures.length}):${RESET}`);
|
|
results.failures.forEach((f, idx) => {
|
|
console.log(`\n ${RED}${idx + 1}) [${f.suiteName}] ${f.testName}${RESET}`);
|
|
console.log(` ${RED}${f.error.name}: ${f.error.message}${RESET}`);
|
|
});
|
|
}
|
|
|
|
const allPassed = results.failedCount === 0 && results.totalTests > 0;
|
|
|
|
if (allPassed) {
|
|
console.log(`\n${BG_GREEN}${BOLD}${WHITE} ALL ${results.totalTests} TESTS PASSED CLEANLY (100% VERIFIED) ${RESET}\n`);
|
|
} else {
|
|
console.log(`\n${BG_RED}${BOLD}${WHITE} TEST SUITE FAILED WITH ${results.failedCount} ERRORS ${RESET}\n`);
|
|
}
|
|
|
|
return allPassed;
|
|
}
|
|
|
|
// Automatically execute if run directly
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
const filter = args[0];
|
|
|
|
// Import test suites dynamically if this is the entry point
|
|
try {
|
|
await import("./tier1_features.test");
|
|
await import("./tier2_boundaries.test");
|
|
await import("./tier3_interactions.test");
|
|
await import("./tier4_workloads.test");
|
|
await import("../../src/components/dashboard/__tests__/batchUpload.test");
|
|
await import("./m1_adversarial.test");
|
|
await import("./challenger2_stress.test");
|
|
await import("../../src/components/dashboard/__tests__/inspectorModal.test");
|
|
await import("./m2_adversarial.test");
|
|
await import("../../src/components/dashboard/__tests__/liveTable.test");
|
|
await import("./m3_adversarial.test");
|
|
await import("./challenger_m3_stress");
|
|
await import("./m3_challenger_deep_stress.test");
|
|
await import("../../src/components/dashboard/__tests__/responsiveShell.test");
|
|
await import("./m4_adversarial.test");
|
|
await import("./challenger_m4_stress");
|
|
await import("./challenger_m4_2_stress");
|
|
await import("./challenger_m4_1_deep_stress");
|
|
await import("./extraction_quality.test");
|
|
await import("./export_localization.test");
|
|
await import("./export_pdf.test");
|
|
await import("./challenger_excel_adversarial.test");
|
|
await import("./auth_security.test");
|
|
await import("./security_headers.test");
|
|
await import("./csrf_tokens.test");
|
|
await import("./subdomain_routing.test");
|
|
await import("./seo_slugs.test");
|
|
await import("./user_enumeration.test");
|
|
await import("./upload_whitelist.test");
|
|
await import("./sprint_a_scanner.test");
|
|
await import("./sprint_b_speed.test");
|
|
await import("./sprint_c_image.test");
|
|
await import("./sprint_e.test");
|
|
await import("./server_pricing.test");
|
|
await import("./webhook_verification.test");
|
|
} catch (err) {
|
|
console.error("Failed to load test suite modules:", err);
|
|
process.exit(1);
|
|
}
|
|
|
|
const success = await runAllTests(filter);
|
|
if (!success) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Check if current file is the main entry point
|
|
if (typeof require !== "undefined" && require.main === module) {
|
|
main().catch((err) => {
|
|
console.error("Fatal runner crash:", err);
|
|
process.exit(1);
|
|
});
|
|
} else if (typeof process !== "undefined" && process.argv && process.argv[1]?.includes("runner.ts")) {
|
|
main().catch((err) => {
|
|
console.error("Fatal runner crash:", err);
|
|
process.exit(1);
|
|
});
|
|
}
|