mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
c9a66d5c99
* refactor: extract shared test harness module from 9 test files Extract duplicated test utilities into a reusable test_harness.mjs module to eliminate ~200-250 lines of boilerplate code across test files. Changes: - Create skills/clawsec-suite/test/lib/test_harness.mjs with: - Test reporting: pass(), fail(), report(), exitWithResults() - Crypto utilities: generateEd25519KeyPair(), signPayload() - Temp directory: createTempDir() with cleanup - Environment helpers: withEnv() for isolated env vars - Test runner factory: createTestRunner() for isolated counters - Refactor 9 test files to use shared harness: - feed_verification.test.mjs - guarded_install.test.mjs - skill_catalog_discovery.test.mjs - advisory_suppression.test.mjs - advisory_application_scope.test.mjs - path_resolution.test.mjs - fuzz_properties.test.mjs - suppression_config.test.mjs - render_report_suppression.test.mjs Benefits: - Single source of truth for test utilities - Consistent test reporting across all files - Easier to add new test files - Reduced maintenance burden Verification: - All 80 tests pass (15+8+3+15+4+6+1+17+11) - Zero ESLint warnings - No behavior changes - only code deduplication - Cross-skill module sharing works (openclaw-audit-watchdog → clawsec-suite) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: update minimatch override to 10.2.4 to resolve ReDoS vulnerabilities Bump minimatch from 10.2.1 to 10.2.4 in overrides to fix 10 high-severity ReDoS vulnerabilities (GHSA-7r86-cg39-jmmj, GHSA-23c5-xmqv-rm74). Also add .venv/ to ESLint ignores to prevent linting Python venv files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
594 lines
19 KiB
JavaScript
594 lines
19 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Feed verification tests for clawsec-suite.
|
|
*
|
|
* Tests cover:
|
|
* - Signature verification success/failure/tampered cases
|
|
* - Checksum manifest verification success/failure/tampered cases
|
|
* - Fail-closed behavior when signatures are missing/invalid
|
|
* - Temporary compatibility flag behavior
|
|
*
|
|
* Run: node skills/clawsec-suite/test/feed_verification.test.mjs
|
|
*/
|
|
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import {
|
|
pass,
|
|
fail,
|
|
report,
|
|
exitWithResults,
|
|
generateEd25519KeyPair,
|
|
signPayload,
|
|
createTempDir,
|
|
} from "./lib/test_harness.mjs";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const LIB_PATH = path.resolve(__dirname, "..", "hooks", "clawsec-advisory-guardian", "lib");
|
|
|
|
// Dynamic import to ensure we test the actual module
|
|
const { verifySignedPayload, loadLocalFeed, isValidFeedPayload } = await import(
|
|
`${LIB_PATH}/feed.mjs`
|
|
);
|
|
|
|
let tempDirCleanup;
|
|
|
|
function createValidFeed() {
|
|
return JSON.stringify(
|
|
{
|
|
version: "1.0.0",
|
|
updated: "2026-02-08T12:00:00Z",
|
|
advisories: [
|
|
{
|
|
id: "TEST-001",
|
|
severity: "high",
|
|
affected: ["test-skill@1.0.0"],
|
|
},
|
|
],
|
|
},
|
|
null,
|
|
2,
|
|
);
|
|
}
|
|
|
|
function createChecksumManifest(files) {
|
|
const checksums = {};
|
|
for (const [name, content] of Object.entries(files)) {
|
|
checksums[name] = crypto.createHash("sha256").update(content).digest("hex");
|
|
}
|
|
return JSON.stringify(
|
|
{
|
|
schema_version: "1.0",
|
|
algorithm: "sha256",
|
|
files: checksums,
|
|
},
|
|
null,
|
|
2,
|
|
);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: verifySignedPayload - valid signature
|
|
// -----------------------------------------------------------------------------
|
|
async function testVerifySignedPayload_ValidSignature() {
|
|
const testName = "verifySignedPayload: valid signature passes";
|
|
try {
|
|
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
|
|
const payload = "test payload content";
|
|
const signature = signPayload(payload, privateKeyPem);
|
|
|
|
const result = verifySignedPayload(payload, signature, publicKeyPem);
|
|
|
|
if (result === true) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Expected true, got false");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: verifySignedPayload - invalid signature
|
|
// -----------------------------------------------------------------------------
|
|
async function testVerifySignedPayload_InvalidSignature() {
|
|
const testName = "verifySignedPayload: invalid signature fails";
|
|
try {
|
|
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
|
|
const payload = "test payload content";
|
|
const signature = signPayload(payload, privateKeyPem);
|
|
|
|
// Tamper with payload
|
|
const tamperedPayload = "TAMPERED payload content";
|
|
const result = verifySignedPayload(tamperedPayload, signature, publicKeyPem);
|
|
|
|
if (result === false) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Expected false for tampered payload, got true");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: verifySignedPayload - wrong key
|
|
// -----------------------------------------------------------------------------
|
|
async function testVerifySignedPayload_WrongKey() {
|
|
const testName = "verifySignedPayload: wrong key fails";
|
|
try {
|
|
const keyPair1 = generateEd25519KeyPair();
|
|
const keyPair2 = generateEd25519KeyPair();
|
|
const payload = "test payload content";
|
|
const signature = signPayload(payload, keyPair1.privateKeyPem);
|
|
|
|
// Verify with different public key
|
|
const result = verifySignedPayload(payload, signature, keyPair2.publicKeyPem);
|
|
|
|
if (result === false) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Expected false for wrong key, got true");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: verifySignedPayload - malformed signature
|
|
// -----------------------------------------------------------------------------
|
|
async function testVerifySignedPayload_MalformedSignature() {
|
|
const testName = "verifySignedPayload: malformed signature fails";
|
|
try {
|
|
const { publicKeyPem } = generateEd25519KeyPair();
|
|
const payload = "test payload content";
|
|
|
|
const result = verifySignedPayload(payload, "not-valid-base64!!!", publicKeyPem);
|
|
|
|
if (result === false) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Expected false for malformed signature, got true");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: verifySignedPayload - empty signature
|
|
// -----------------------------------------------------------------------------
|
|
async function testVerifySignedPayload_EmptySignature() {
|
|
const testName = "verifySignedPayload: empty signature fails";
|
|
try {
|
|
const { publicKeyPem } = generateEd25519KeyPair();
|
|
const payload = "test payload content";
|
|
|
|
const result = verifySignedPayload(payload, "", publicKeyPem);
|
|
|
|
if (result === false) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Expected false for empty signature, got true");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: verifySignedPayload - JSON-wrapped signature format
|
|
// -----------------------------------------------------------------------------
|
|
async function testVerifySignedPayload_JsonWrappedSignature() {
|
|
const testName = "verifySignedPayload: JSON-wrapped signature passes";
|
|
try {
|
|
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
|
|
const payload = "test payload content";
|
|
const signatureBase64 = signPayload(payload, privateKeyPem);
|
|
const jsonWrapped = JSON.stringify({ signature: signatureBase64 });
|
|
|
|
const result = verifySignedPayload(payload, jsonWrapped, publicKeyPem);
|
|
|
|
if (result === true) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Expected true for JSON-wrapped signature, got false");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: loadLocalFeed - valid signed feed
|
|
// -----------------------------------------------------------------------------
|
|
async function testLoadLocalFeed_ValidSignedFeed() {
|
|
const testName = "loadLocalFeed: valid signed feed loads successfully";
|
|
try {
|
|
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
|
|
const feedContent = createValidFeed();
|
|
const feedSignature = signPayload(feedContent, privateKeyPem);
|
|
|
|
// Create checksum manifest
|
|
const checksumManifest = createChecksumManifest({
|
|
"feed.json": feedContent,
|
|
"feed.json.sig": feedSignature + "\n",
|
|
"feed-signing-public.pem": publicKeyPem,
|
|
});
|
|
const checksumSignature = signPayload(checksumManifest, privateKeyPem);
|
|
|
|
// Write files
|
|
const feedPath = path.join(globalThis.__testTempDir, "feed.json");
|
|
const sigPath = path.join(globalThis.__testTempDir, "feed.json.sig");
|
|
const checksumPath = path.join(globalThis.__testTempDir, "checksums.json");
|
|
const checksumSigPath = path.join(globalThis.__testTempDir, "checksums.json.sig");
|
|
const keyPath = path.join(globalThis.__testTempDir, "feed-signing-public.pem");
|
|
|
|
await fs.writeFile(feedPath, feedContent);
|
|
await fs.writeFile(sigPath, feedSignature + "\n");
|
|
await fs.writeFile(checksumPath, checksumManifest);
|
|
await fs.writeFile(checksumSigPath, checksumSignature + "\n");
|
|
await fs.writeFile(keyPath, publicKeyPem);
|
|
|
|
const feed = await loadLocalFeed(feedPath, {
|
|
signaturePath: sigPath,
|
|
checksumsPath: checksumPath,
|
|
checksumsSignaturePath: checksumSigPath,
|
|
publicKeyPem,
|
|
verifyChecksumManifest: true,
|
|
checksumPublicKeyEntry: "feed-signing-public.pem",
|
|
});
|
|
|
|
if (feed && feed.version === "1.0.0" && feed.advisories.length === 1) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Feed did not load with expected content");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: loadLocalFeed - supports advisories/* checksum keys
|
|
// -----------------------------------------------------------------------------
|
|
async function testLoadLocalFeed_AdvisoriesPrefixedChecksumKeys() {
|
|
const testName = "loadLocalFeed: advisories/* checksum keys are accepted";
|
|
try {
|
|
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
|
|
const feedContent = createValidFeed();
|
|
const feedSignature = signPayload(feedContent, privateKeyPem);
|
|
|
|
const advisoriesDir = path.join(globalThis.__testTempDir, "advisories");
|
|
await fs.mkdir(advisoriesDir, { recursive: true });
|
|
|
|
const checksumManifest = createChecksumManifest({
|
|
"advisories/feed.json": feedContent,
|
|
"advisories/feed.json.sig": feedSignature + "\n",
|
|
"advisories/feed-signing-public.pem": publicKeyPem,
|
|
});
|
|
const checksumSignature = signPayload(checksumManifest, privateKeyPem);
|
|
|
|
const feedPath = path.join(advisoriesDir, "feed.json");
|
|
const sigPath = path.join(advisoriesDir, "feed.json.sig");
|
|
const checksumPath = path.join(advisoriesDir, "checksums.json");
|
|
const checksumSigPath = path.join(advisoriesDir, "checksums.json.sig");
|
|
const keyPath = path.join(advisoriesDir, "feed-signing-public.pem");
|
|
|
|
await fs.writeFile(feedPath, feedContent);
|
|
await fs.writeFile(sigPath, feedSignature + "\n");
|
|
await fs.writeFile(checksumPath, checksumManifest);
|
|
await fs.writeFile(checksumSigPath, checksumSignature + "\n");
|
|
await fs.writeFile(keyPath, publicKeyPem);
|
|
|
|
const feed = await loadLocalFeed(feedPath, {
|
|
signaturePath: sigPath,
|
|
checksumsPath: checksumPath,
|
|
checksumsSignaturePath: checksumSigPath,
|
|
publicKeyPem,
|
|
verifyChecksumManifest: true,
|
|
checksumPublicKeyEntry: path.basename(keyPath),
|
|
});
|
|
|
|
if (feed && feed.version === "1.0.0" && feed.advisories.length === 1) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Feed did not load with advisories/* checksum keys");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: loadLocalFeed - tampered feed fails (fail-closed)
|
|
// -----------------------------------------------------------------------------
|
|
async function testLoadLocalFeed_TamperedFeedFails() {
|
|
const testName = "loadLocalFeed: tampered feed fails (fail-closed)";
|
|
try {
|
|
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
|
|
const feedContent = createValidFeed();
|
|
const feedSignature = signPayload(feedContent, privateKeyPem);
|
|
|
|
// Tamper with feed after signing
|
|
const tamperedFeed = feedContent.replace("TEST-001", "TAMPERED-001");
|
|
|
|
const feedPath = path.join(globalThis.__testTempDir, "tampered-feed.json");
|
|
const sigPath = path.join(globalThis.__testTempDir, "tampered-feed.json.sig");
|
|
|
|
await fs.writeFile(feedPath, tamperedFeed);
|
|
await fs.writeFile(sigPath, feedSignature + "\n");
|
|
|
|
let didFail = false;
|
|
try {
|
|
await loadLocalFeed(feedPath, {
|
|
signaturePath: sigPath,
|
|
publicKeyPem,
|
|
verifyChecksumManifest: false,
|
|
});
|
|
} catch {
|
|
didFail = true;
|
|
}
|
|
|
|
if (didFail) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Expected failure for tampered feed, but it loaded");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: loadLocalFeed - missing signature fails (fail-closed)
|
|
// -----------------------------------------------------------------------------
|
|
async function testLoadLocalFeed_MissingSignatureFails() {
|
|
const testName = "loadLocalFeed: missing signature fails (fail-closed)";
|
|
try {
|
|
const { publicKeyPem } = generateEd25519KeyPair();
|
|
const feedContent = createValidFeed();
|
|
|
|
const feedPath = path.join(globalThis.__testTempDir, "nosig-feed.json");
|
|
const sigPath = path.join(globalThis.__testTempDir, "nosig-feed.json.sig");
|
|
|
|
await fs.writeFile(feedPath, feedContent);
|
|
// Don't write signature file
|
|
|
|
let didFail = false;
|
|
try {
|
|
await loadLocalFeed(feedPath, {
|
|
signaturePath: sigPath,
|
|
publicKeyPem,
|
|
verifyChecksumManifest: false,
|
|
});
|
|
} catch {
|
|
didFail = true;
|
|
}
|
|
|
|
if (didFail) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Expected failure for missing signature, but it loaded");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: loadLocalFeed - allowUnsigned bypasses verification
|
|
// -----------------------------------------------------------------------------
|
|
async function testLoadLocalFeed_AllowUnsignedBypasses() {
|
|
const testName = "loadLocalFeed: allowUnsigned=true bypasses verification";
|
|
try {
|
|
const feedContent = createValidFeed();
|
|
|
|
const feedPath = path.join(globalThis.__testTempDir, "unsigned-feed.json");
|
|
await fs.writeFile(feedPath, feedContent);
|
|
|
|
const feed = await loadLocalFeed(feedPath, {
|
|
allowUnsigned: true,
|
|
verifyChecksumManifest: false,
|
|
});
|
|
|
|
if (feed && feed.version === "1.0.0") {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Feed did not load with allowUnsigned=true");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: loadLocalFeed - checksum mismatch fails
|
|
// -----------------------------------------------------------------------------
|
|
async function testLoadLocalFeed_ChecksumMismatchFails() {
|
|
const testName = "loadLocalFeed: checksum mismatch fails";
|
|
try {
|
|
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
|
|
const feedContent = createValidFeed();
|
|
const feedSignature = signPayload(feedContent, privateKeyPem);
|
|
|
|
// Create checksum manifest with WRONG hash
|
|
const badChecksumManifest = JSON.stringify(
|
|
{
|
|
schema_version: "1.0",
|
|
algorithm: "sha256",
|
|
files: {
|
|
"feed.json": "0".repeat(64), // Wrong hash
|
|
"feed.json.sig":
|
|
crypto.createHash("sha256").update(feedSignature + "\n").digest("hex"),
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
);
|
|
const checksumSignature = signPayload(badChecksumManifest, privateKeyPem);
|
|
|
|
const feedPath = path.join(globalThis.__testTempDir, "badcs-feed.json");
|
|
const sigPath = path.join(globalThis.__testTempDir, "badcs-feed.json.sig");
|
|
const checksumPath = path.join(globalThis.__testTempDir, "badcs-checksums.json");
|
|
const checksumSigPath = path.join(globalThis.__testTempDir, "badcs-checksums.json.sig");
|
|
|
|
await fs.writeFile(feedPath, feedContent);
|
|
await fs.writeFile(sigPath, feedSignature + "\n");
|
|
await fs.writeFile(checksumPath, badChecksumManifest);
|
|
await fs.writeFile(checksumSigPath, checksumSignature + "\n");
|
|
|
|
let didFail = false;
|
|
try {
|
|
await loadLocalFeed(feedPath, {
|
|
signaturePath: sigPath,
|
|
checksumsPath: checksumPath,
|
|
checksumsSignaturePath: checksumSigPath,
|
|
publicKeyPem,
|
|
verifyChecksumManifest: true,
|
|
});
|
|
} catch {
|
|
didFail = true;
|
|
}
|
|
|
|
if (didFail) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Expected failure for checksum mismatch, but it loaded");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: isValidFeedPayload - valid feed
|
|
// -----------------------------------------------------------------------------
|
|
async function testIsValidFeedPayload_Valid() {
|
|
const testName = "isValidFeedPayload: valid feed passes";
|
|
try {
|
|
const feed = {
|
|
version: "1.0.0",
|
|
advisories: [
|
|
{
|
|
id: "TEST-001",
|
|
severity: "high",
|
|
affected: ["test-skill@1.0.0"],
|
|
},
|
|
],
|
|
};
|
|
|
|
if (isValidFeedPayload(feed)) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Expected valid feed to pass validation");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: isValidFeedPayload - missing version fails
|
|
// -----------------------------------------------------------------------------
|
|
async function testIsValidFeedPayload_MissingVersion() {
|
|
const testName = "isValidFeedPayload: missing version fails";
|
|
try {
|
|
const feed = {
|
|
advisories: [
|
|
{
|
|
id: "TEST-001",
|
|
severity: "high",
|
|
affected: [],
|
|
},
|
|
],
|
|
};
|
|
|
|
if (!isValidFeedPayload(feed)) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Expected feed without version to fail validation");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: isValidFeedPayload - advisory missing id fails
|
|
// -----------------------------------------------------------------------------
|
|
async function testIsValidFeedPayload_AdvisoryMissingId() {
|
|
const testName = "isValidFeedPayload: advisory missing id fails";
|
|
try {
|
|
const feed = {
|
|
version: "1.0.0",
|
|
advisories: [
|
|
{
|
|
severity: "high",
|
|
affected: [],
|
|
},
|
|
],
|
|
};
|
|
|
|
if (!isValidFeedPayload(feed)) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Expected advisory without id to fail validation");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Main test runner
|
|
// -----------------------------------------------------------------------------
|
|
async function runTests() {
|
|
console.log("=== ClawSec Feed Verification Tests ===\n");
|
|
|
|
const tempDir = await createTempDir();
|
|
tempDirCleanup = tempDir.cleanup;
|
|
|
|
// Store temp dir path in module scope for tests to access
|
|
globalThis.__testTempDir = tempDir.path;
|
|
|
|
try {
|
|
// Signature verification tests
|
|
await testVerifySignedPayload_ValidSignature();
|
|
await testVerifySignedPayload_InvalidSignature();
|
|
await testVerifySignedPayload_WrongKey();
|
|
await testVerifySignedPayload_MalformedSignature();
|
|
await testVerifySignedPayload_EmptySignature();
|
|
await testVerifySignedPayload_JsonWrappedSignature();
|
|
|
|
// Local feed loading tests
|
|
await testLoadLocalFeed_ValidSignedFeed();
|
|
await testLoadLocalFeed_AdvisoriesPrefixedChecksumKeys();
|
|
await testLoadLocalFeed_TamperedFeedFails();
|
|
await testLoadLocalFeed_MissingSignatureFails();
|
|
await testLoadLocalFeed_AllowUnsignedBypasses();
|
|
await testLoadLocalFeed_ChecksumMismatchFails();
|
|
|
|
// Feed payload validation tests
|
|
await testIsValidFeedPayload_Valid();
|
|
await testIsValidFeedPayload_MissingVersion();
|
|
await testIsValidFeedPayload_AdvisoryMissingId();
|
|
} finally {
|
|
await tempDirCleanup();
|
|
}
|
|
|
|
report();
|
|
exitWithResults();
|
|
}
|
|
|
|
runTests().catch((error) => {
|
|
console.error("Test runner failed:", error);
|
|
process.exit(1);
|
|
});
|