Files
clawsec/skills/clawsec-suite/test/skill_catalog_discovery.test.mjs
T
davida-ps c9a66d5c99 Extract Shared Test Harness Module from 9 Test Files (#85)
* 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>
2026-02-27 09:20:36 +02:00

235 lines
7.0 KiB
JavaScript

#!/usr/bin/env node
/**
* Dynamic skill catalog discovery tests for clawsec-suite.
*
* Tests cover:
* - Remote index fetch and normalization
* - Enrichment with suite-local metadata (non-breaking compatibility)
* - Fallback behavior when remote index is invalid/unavailable
*
* Run: node skills/clawsec-suite/test/skill_catalog_discovery.test.mjs
*/
import http from "node:http";
import path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { pass, fail, report, exitWithResults } from "./lib/test_harness.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SCRIPT_PATH = path.resolve(__dirname, "..", "scripts", "discover_skill_catalog.mjs");
function runCatalogScript(args, env = {}) {
return new Promise((resolve) => {
const proc = spawn("node", [SCRIPT_PATH, ...args], {
env: { ...process.env, ...env },
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
proc.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
proc.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
proc.on("close", (code) => {
resolve({ code, stdout, stderr });
});
});
}
function withServer(handler) {
return new Promise((resolve, reject) => {
const server = http.createServer(handler);
server.listen(0, "127.0.0.1", () => {
const addr = server.address();
if (!addr || typeof addr === "string") {
reject(new Error("Failed to bind test server"));
return;
}
resolve({
url: `http://127.0.0.1:${addr.port}`,
close: () =>
new Promise((done) => {
server.close(() => done());
}),
});
});
server.on("error", reject);
});
}
// -----------------------------------------------------------------------------
// Test: remote index is used when valid
// -----------------------------------------------------------------------------
async function testRemoteCatalogSuccess() {
const testName = "discover_skill_catalog: uses remote index when valid";
let fixture = null;
try {
fixture = await withServer((req, res) => {
if (req.url !== "/index.json") {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "not found" }));
return;
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
version: "1.0.0",
updated: "2026-02-16T08:20:00Z",
skills: [
{
id: "soul-guardian",
name: "soul-guardian",
version: "9.9.9",
description: "Remote skill metadata",
emoji: "👻",
category: "security",
tag: "soul-guardian-v9.9.9",
},
{
id: "clawtributor",
name: "clawtributor",
version: "1.2.3",
description: "Remote clawtributor metadata",
emoji: "🤝",
category: "security",
tag: "clawtributor-v1.2.3",
},
],
}),
);
});
const result = await runCatalogScript(["--json"], {
CLAWSEC_SKILLS_INDEX_URL: `${fixture.url}/index.json`,
CLAWSEC_SKILLS_INDEX_TIMEOUT_MS: "2000",
});
if (result.code !== 0) {
fail(testName, `Expected exit 0, got ${result.code}: ${result.stderr}`);
return;
}
const payload = JSON.parse(result.stdout);
const clawtributor = payload.skills.find((entry) => entry.id === "clawtributor");
const soulGuardian = payload.skills.find((entry) => entry.id === "soul-guardian");
if (
payload.source === "remote" &&
payload.updated === "2026-02-16T08:20:00Z" &&
soulGuardian?.version === "9.9.9" &&
clawtributor?.requires_explicit_consent === true
) {
pass(testName);
} else {
fail(testName, `Unexpected payload: ${result.stdout}`);
}
} catch (error) {
fail(testName, error);
} finally {
if (fixture) {
await fixture.close();
}
}
}
// -----------------------------------------------------------------------------
// Test: invalid remote payload falls back to suite-local catalog
// -----------------------------------------------------------------------------
async function testInvalidRemotePayloadFallsBack() {
const testName = "discover_skill_catalog: invalid remote payload falls back";
let fixture = null;
try {
fixture = await withServer((_req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ version: "1.0.0", note: "missing skills" }));
});
const result = await runCatalogScript(["--json"], {
CLAWSEC_SKILLS_INDEX_URL: `${fixture.url}/index.json`,
CLAWSEC_SKILLS_INDEX_TIMEOUT_MS: "2000",
});
if (result.code !== 0) {
fail(testName, `Expected exit 0, got ${result.code}: ${result.stderr}`);
return;
}
const payload = JSON.parse(result.stdout);
const hasSoulGuardian = Array.isArray(payload.skills)
? payload.skills.some((entry) => entry.id === "soul-guardian")
: false;
if (payload.source === "fallback" && hasSoulGuardian && String(payload.warning).includes("skills array")) {
pass(testName);
} else {
fail(testName, `Unexpected payload: ${result.stdout}`);
}
} catch (error) {
fail(testName, error);
} finally {
if (fixture) {
await fixture.close();
}
}
}
// -----------------------------------------------------------------------------
// Test: unreachable remote index falls back to suite-local catalog
// -----------------------------------------------------------------------------
async function testUnreachableRemoteFallsBack() {
const testName = "discover_skill_catalog: unreachable remote index falls back";
try {
const result = await runCatalogScript(["--json"], {
CLAWSEC_SKILLS_INDEX_URL: "http://127.0.0.1:9/index.json",
CLAWSEC_SKILLS_INDEX_TIMEOUT_MS: "250",
});
if (result.code !== 0) {
fail(testName, `Expected exit 0, got ${result.code}: ${result.stderr}`);
return;
}
const payload = JSON.parse(result.stdout);
if (payload.source === "fallback" && Array.isArray(payload.skills) && payload.skills.length > 0) {
pass(testName);
} else {
fail(testName, `Unexpected payload: ${result.stdout}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Main test runner
// -----------------------------------------------------------------------------
async function runTests() {
console.log("=== ClawSec Skill Catalog Discovery Tests ===\n");
await testRemoteCatalogSuccess();
await testInvalidRemotePayloadFallsBack();
await testUnreachableRemoteFallsBack();
report();
exitWithResults();
}
runTests().catch((error) => {
console.error("Test runner failed:", error);
process.exit(1);
});