mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-16 06:51:21 +03:00
auto-claude: subtask-1-3 - Add unit tests for config loading
Added comprehensive unit tests for suppression config loading: - Valid config with all required fields - Malformed date warning (non-blocking) - Missing required field validation - Malformed JSON error handling - File not found graceful fallback - Custom path priority - Environment variable override - Missing/empty suppressions array handling All 10 tests passing. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,479 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Suppression config loading tests for openclaw-audit-watchdog.
|
||||
*
|
||||
* Tests cover:
|
||||
* - Valid config file loading and normalization
|
||||
* - Required field validation
|
||||
* - Date format validation with graceful fallback
|
||||
* - Malformed JSON error handling
|
||||
* - File not found graceful fallback
|
||||
* - Multi-path priority (custom path > env var > primary > fallback)
|
||||
*
|
||||
* Run: node skills/openclaw-audit-watchdog/test/suppression_config.test.mjs
|
||||
*/
|
||||
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { loadSuppressionConfig } from "../scripts/load_suppression_config.mjs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
let passCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
function pass(name) {
|
||||
passCount += 1;
|
||||
console.log(`✓ ${name}`);
|
||||
}
|
||||
|
||||
function fail(name, error) {
|
||||
failCount += 1;
|
||||
console.error(`✗ ${name}`);
|
||||
console.error(` ${String(error)}`);
|
||||
}
|
||||
|
||||
async function withTempFile(content) {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-"));
|
||||
const tmpFile = path.join(tmpDir, "test-config.json");
|
||||
await fs.writeFile(tmpFile, content, "utf8");
|
||||
|
||||
return {
|
||||
path: tmpFile,
|
||||
cleanup: async () => {
|
||||
try {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function withEnv(key, value, fn) {
|
||||
const oldValue = process.env[key];
|
||||
try {
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
return await fn();
|
||||
} finally {
|
||||
if (oldValue === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = oldValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: valid config with all required fields
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testValidConfig() {
|
||||
const testName = "loadSuppressionConfig: loads valid config with all required fields";
|
||||
let fixture = null;
|
||||
|
||||
try {
|
||||
const validConfig = JSON.stringify({
|
||||
suppressions: [
|
||||
{
|
||||
checkId: "SCAN-001",
|
||||
skill: "soul-guardian",
|
||||
reason: "False positive - reviewed by security team",
|
||||
suppressedAt: "2026-02-15",
|
||||
},
|
||||
{
|
||||
checkId: "SCAN-002",
|
||||
skill: "clawtributor",
|
||||
reason: "Accepted risk for legacy code",
|
||||
suppressedAt: "2026-02-14",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
fixture = await withTempFile(validConfig);
|
||||
const config = await loadSuppressionConfig(fixture.path);
|
||||
|
||||
if (
|
||||
config.source === fixture.path &&
|
||||
config.suppressions.length === 2 &&
|
||||
config.suppressions[0].checkId === "SCAN-001" &&
|
||||
config.suppressions[0].skill === "soul-guardian" &&
|
||||
config.suppressions[0].reason === "False positive - reviewed by security team" &&
|
||||
config.suppressions[0].suppressedAt === "2026-02-15" &&
|
||||
config.suppressions[1].checkId === "SCAN-002" &&
|
||||
config.suppressions[1].skill === "clawtributor"
|
||||
) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Unexpected config: ${JSON.stringify(config)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
} finally {
|
||||
if (fixture) {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: malformed date warns but doesn't fail
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testMalformedDateWarning() {
|
||||
const testName = "loadSuppressionConfig: malformed date warns but doesn't fail";
|
||||
let fixture = null;
|
||||
|
||||
try {
|
||||
const configWithBadDate = JSON.stringify({
|
||||
suppressions: [
|
||||
{
|
||||
checkId: "SCAN-003",
|
||||
skill: "soul-guardian",
|
||||
reason: "Test suppression",
|
||||
suppressedAt: "02/15/2026",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
fixture = await withTempFile(configWithBadDate);
|
||||
|
||||
// Capture stderr to check for warning
|
||||
let stderrOutput = "";
|
||||
const originalStderrWrite = process.stderr.write;
|
||||
process.stderr.write = function (chunk) {
|
||||
stderrOutput += chunk.toString();
|
||||
return true;
|
||||
};
|
||||
|
||||
try {
|
||||
const config = await loadSuppressionConfig(fixture.path);
|
||||
|
||||
if (
|
||||
config.suppressions.length === 1 &&
|
||||
config.suppressions[0].checkId === "SCAN-003" &&
|
||||
config.suppressions[0].suppressedAt === "02/15/2026" &&
|
||||
stderrOutput.includes("Warning") &&
|
||||
stderrOutput.includes("malformed date")
|
||||
) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Expected warning but got: ${stderrOutput}`);
|
||||
}
|
||||
} finally {
|
||||
process.stderr.write = originalStderrWrite;
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
} finally {
|
||||
if (fixture) {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: missing required field fails
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testMissingRequiredField() {
|
||||
const testName = "loadSuppressionConfig: missing required field fails";
|
||||
let fixture = null;
|
||||
|
||||
try {
|
||||
const configMissingReason = JSON.stringify({
|
||||
suppressions: [
|
||||
{
|
||||
checkId: "SCAN-004",
|
||||
skill: "soul-guardian",
|
||||
suppressedAt: "2026-02-15",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
fixture = await withTempFile(configMissingReason);
|
||||
|
||||
try {
|
||||
await loadSuppressionConfig(fixture.path);
|
||||
fail(testName, "Expected error for missing required field");
|
||||
} catch (err) {
|
||||
if (err.message.includes("missing required field: reason")) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Wrong error message: ${err.message}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
} finally {
|
||||
if (fixture) {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: malformed JSON fails
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testMalformedJSON() {
|
||||
const testName = "loadSuppressionConfig: malformed JSON fails";
|
||||
let fixture = null;
|
||||
|
||||
try {
|
||||
const invalidJSON = "{ suppressions: [ { not valid json } ] }";
|
||||
|
||||
fixture = await withTempFile(invalidJSON);
|
||||
|
||||
try {
|
||||
await loadSuppressionConfig(fixture.path);
|
||||
fail(testName, "Expected error for malformed JSON");
|
||||
} catch (err) {
|
||||
if (err.message.includes("Malformed JSON")) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Wrong error message: ${err.message}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
} finally {
|
||||
if (fixture) {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: file not found returns empty suppressions
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testFileNotFoundGracefulFallback() {
|
||||
const testName = "loadSuppressionConfig: file not found returns empty suppressions";
|
||||
|
||||
try {
|
||||
await withEnv("OPENCLAW_AUDIT_CONFIG", undefined, async () => {
|
||||
const nonExistentPath1 = path.join(os.homedir(), ".openclaw", "non-existent-12345.json");
|
||||
const nonExistentPath2 = ".clawsec/non-existent-12345.json";
|
||||
|
||||
// Ensure neither path exists
|
||||
try {
|
||||
await fs.access(nonExistentPath1);
|
||||
fail(testName, "Test precondition failed: primary path should not exist");
|
||||
return;
|
||||
} catch (err) {
|
||||
// Expected - file should not exist
|
||||
}
|
||||
|
||||
const config = await loadSuppressionConfig();
|
||||
|
||||
if (config.source === "none" && Array.isArray(config.suppressions) && config.suppressions.length === 0) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Expected empty suppressions but got: ${JSON.stringify(config)}`);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: custom path has highest priority
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testCustomPathPriority() {
|
||||
const testName = "loadSuppressionConfig: custom path has highest priority";
|
||||
let fixture = null;
|
||||
|
||||
try {
|
||||
const customConfig = JSON.stringify({
|
||||
suppressions: [
|
||||
{
|
||||
checkId: "CUSTOM-001",
|
||||
skill: "custom-skill",
|
||||
reason: "Custom path config",
|
||||
suppressedAt: "2026-02-15",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
fixture = await withTempFile(customConfig);
|
||||
const config = await loadSuppressionConfig(fixture.path);
|
||||
|
||||
if (
|
||||
config.source === fixture.path &&
|
||||
config.suppressions.length === 1 &&
|
||||
config.suppressions[0].checkId === "CUSTOM-001"
|
||||
) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Unexpected config: ${JSON.stringify(config)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
} finally {
|
||||
if (fixture) {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: environment variable override
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testEnvironmentVariableOverride() {
|
||||
const testName = "loadSuppressionConfig: environment variable overrides default paths";
|
||||
let fixture = null;
|
||||
|
||||
try {
|
||||
const envConfig = JSON.stringify({
|
||||
suppressions: [
|
||||
{
|
||||
checkId: "ENV-001",
|
||||
skill: "env-skill",
|
||||
reason: "Environment variable config",
|
||||
suppressedAt: "2026-02-15",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
fixture = await withTempFile(envConfig);
|
||||
|
||||
await withEnv("OPENCLAW_AUDIT_CONFIG", fixture.path, async () => {
|
||||
const config = await loadSuppressionConfig();
|
||||
|
||||
if (
|
||||
config.source === fixture.path &&
|
||||
config.suppressions.length === 1 &&
|
||||
config.suppressions[0].checkId === "ENV-001"
|
||||
) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Unexpected config: ${JSON.stringify(config)}`);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
} finally {
|
||||
if (fixture) {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: missing suppressions array fails
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testMissingSuppressions() {
|
||||
const testName = "loadSuppressionConfig: missing suppressions array fails";
|
||||
let fixture = null;
|
||||
|
||||
try {
|
||||
const configWithoutSuppressions = JSON.stringify({
|
||||
note: "This config is missing the suppressions array",
|
||||
});
|
||||
|
||||
fixture = await withTempFile(configWithoutSuppressions);
|
||||
|
||||
try {
|
||||
await loadSuppressionConfig(fixture.path);
|
||||
fail(testName, "Expected error for missing suppressions array");
|
||||
} catch (err) {
|
||||
if (err.message.includes("missing 'suppressions' array")) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Wrong error message: ${err.message}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
} finally {
|
||||
if (fixture) {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: empty suppressions array is valid
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testEmptySuppressions() {
|
||||
const testName = "loadSuppressionConfig: empty suppressions array is valid";
|
||||
let fixture = null;
|
||||
|
||||
try {
|
||||
const emptyConfig = JSON.stringify({
|
||||
suppressions: [],
|
||||
});
|
||||
|
||||
fixture = await withTempFile(emptyConfig);
|
||||
const config = await loadSuppressionConfig(fixture.path);
|
||||
|
||||
if (config.source === fixture.path && config.suppressions.length === 0) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Unexpected config: ${JSON.stringify(config)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
} finally {
|
||||
if (fixture) {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: custom path not found throws error
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testCustomPathNotFoundFails() {
|
||||
const testName = "loadSuppressionConfig: custom path not found throws error";
|
||||
|
||||
try {
|
||||
const nonExistentPath = path.join(os.tmpdir(), "absolutely-does-not-exist-12345.json");
|
||||
|
||||
try {
|
||||
await loadSuppressionConfig(nonExistentPath);
|
||||
fail(testName, "Expected error for custom path not found");
|
||||
} catch (err) {
|
||||
if (err.message.includes("Custom config file not found")) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Wrong error message: ${err.message}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Main test runner
|
||||
// -----------------------------------------------------------------------------
|
||||
async function runTests() {
|
||||
console.log("=== OpenClaw Audit Watchdog - Suppression Config Tests ===\n");
|
||||
|
||||
await testValidConfig();
|
||||
await testMalformedDateWarning();
|
||||
await testMissingRequiredField();
|
||||
await testMalformedJSON();
|
||||
await testFileNotFoundGracefulFallback();
|
||||
await testCustomPathPriority();
|
||||
await testEnvironmentVariableOverride();
|
||||
await testMissingSuppressions();
|
||||
await testEmptySuppressions();
|
||||
await testCustomPathNotFoundFails();
|
||||
|
||||
console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`);
|
||||
|
||||
if (failCount > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
runTests().catch((error) => {
|
||||
console.error("Test runner failed:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user