From c9a66d5c99b6a4ed9d9867a3c36d445573be7587 Mon Sep 17 00:00:00 2001 From: davida-ps Date: Fri, 27 Feb 2026 09:20:36 +0200 Subject: [PATCH] Extract Shared Test Harness Module from 9 Test Files (#85) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 --------- Co-authored-by: Claude Sonnet 4.5 --- .gitignore | 9 + eslint.config.js | 2 +- package-lock.json | 373 ++++++------------ package.json | 2 +- .../test/advisory_application_scope.test.mjs | 21 +- .../test/advisory_suppression.test.mjs | 51 +-- .../test/feed_verification.test.mjs | 93 ++--- .../test/fuzz_properties.test.mjs | 13 +- .../test/guarded_install.test.mjs | 41 +- skills/clawsec-suite/test/lib/.gitkeep | 0 .../clawsec-suite/test/lib/test_harness.mjs | 182 +++++++++ .../test/path_resolution.test.mjs | 39 +- .../test/skill_catalog_discovery.test.mjs | 22 +- .../test/render_report_suppression.test.mjs | 39 +- .../test/suppression_config.test.mjs | 65 +-- 15 files changed, 407 insertions(+), 545 deletions(-) create mode 100644 skills/clawsec-suite/test/lib/.gitkeep create mode 100644 skills/clawsec-suite/test/lib/test_harness.mjs diff --git a/.gitignore b/.gitignore index bb8f2b7..28ff202 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,12 @@ __pycache__/ *.sln *.sw? clawsec-signing-private.pem + +# Auto Claude generated files +.auto-claude/ +.auto-claude-security.json +.auto-claude-status +.claude_settings.json +.worktrees/ +.security-key +logs/security/ diff --git a/eslint.config.js b/eslint.config.js index ea2ee62..a467363 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -113,6 +113,6 @@ export default [ } }, { - ignores: ['dist/', 'node_modules/', '*.config.js', 'public/'] + ignores: ['dist/', 'node_modules/', '*.config.js', 'public/', '.venv/'] } ]; diff --git a/package-lock.json b/package-lock.json index dae8d7e..9402ca2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -796,19 +796,19 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", "dev": true, "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" }, "engines": { @@ -1380,17 +1380,16 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz", - "integrity": "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.55.0", - "@typescript-eslint/type-utils": "8.55.0", - "@typescript-eslint/utils": "8.55.0", - "@typescript-eslint/visitor-keys": "8.55.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -1403,69 +1402,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.55.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz", - "integrity": "sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/typescript-estree": "8.55.0", - "@typescript-eslint/utils": "8.55.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.55.0.tgz", - "integrity": "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.55.0", - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/typescript-estree": "8.55.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", - "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "engines": { @@ -1480,145 +1431,14 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", - "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.0", - "@typescript-eslint/types": "^8.56.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", - "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", - "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", - "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", - "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.56.0", - "@typescript-eslint/tsconfig-utils": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", - "debug": "^4.4.3", - "minimatch": "^9.0.5", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", - "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.56.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", - "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@typescript-eslint/project-service": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.55.0.tgz", - "integrity": "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.55.0", - "@typescript-eslint/types": "^8.55.0", + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "engines": { @@ -1633,14 +1453,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz", - "integrity": "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/visitor-keys": "8.55.0" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1651,11 +1470,10 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.0.tgz", - "integrity": "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1667,12 +1485,35 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.55.0.tgz", - "integrity": "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1682,18 +1523,17 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz", - "integrity": "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.55.0", - "@typescript-eslint/tsconfig-utils": "8.55.0", - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/visitor-keys": "8.55.0", + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" @@ -1709,15 +1549,37 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz", - "integrity": "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==", + "node_modules/@typescript-eslint/utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.55.0", - "eslint-visitor-keys": "^4.2.1" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1728,13 +1590,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, - "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2612,9 +2473,9 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", @@ -2623,7 +2484,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -2755,9 +2616,9 @@ } }, "node_modules/eslint/node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4651,16 +4512,15 @@ ] }, "node_modules/minimatch": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz", - "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -5321,8 +5181,9 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" }, "node_modules/semver": { - "version": "7.7.3", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "bin": { "semver": "bin/semver.js" diff --git a/package.json b/package.json index 8f06c9e..d74f155 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,6 @@ "ajv": "6.14.0", "balanced-match": "4.0.3", "brace-expansion": "5.0.2", - "minimatch": "10.2.1" + "minimatch": "10.2.4" } } diff --git a/skills/clawsec-suite/test/advisory_application_scope.test.mjs b/skills/clawsec-suite/test/advisory_application_scope.test.mjs index f648769..d3c7fd3 100644 --- a/skills/clawsec-suite/test/advisory_application_scope.test.mjs +++ b/skills/clawsec-suite/test/advisory_application_scope.test.mjs @@ -11,25 +11,12 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; +import { pass, fail, report, exitWithResults } from "./lib/test_harness.mjs"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const LIB_PATH = path.resolve(__dirname, "..", "hooks", "clawsec-advisory-guardian", "lib"); const { advisoryAppliesToOpenclaw } = await import(`${LIB_PATH}/advisory_scope.mjs`); -let passCount = 0; -let failCount = 0; - -function pass(name) { - passCount += 1; - console.log(`\u2713 ${name}`); -} - -function fail(name, error) { - failCount += 1; - console.error(`\u2717 ${name}`); - console.error(` ${String(error)}`); -} - function testFindMatchesFiltersByApplicationScope() { const testName = "advisoryAppliesToOpenclaw: openclaw + legacy advisories are considered"; @@ -89,10 +76,8 @@ function runTests() { testFindMatchesAcceptsApplicationArray(); testInvalidApplicationValueFallsBackCompat(); - console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`); - if (failCount > 0) { - process.exit(1); - } + report(); + exitWithResults(); } runTests(); diff --git a/skills/clawsec-suite/test/advisory_suppression.test.mjs b/skills/clawsec-suite/test/advisory_suppression.test.mjs index 8cf5f2b..b69505a 100644 --- a/skills/clawsec-suite/test/advisory_suppression.test.mjs +++ b/skills/clawsec-suite/test/advisory_suppression.test.mjs @@ -15,9 +15,9 @@ */ import fs from "node:fs/promises"; -import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { pass, fail, report, exitWithResults, 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"); @@ -27,29 +27,6 @@ const { isAdvisorySuppressed, loadAdvisorySuppression } = await import( ); let tempDir; -let passCount = 0; -let failCount = 0; - -function pass(name) { - passCount++; - console.log(`\u2713 ${name}`); -} - -function fail(name, error) { - failCount++; - console.error(`\u2717 ${name}`); - console.error(` ${String(error)}`); -} - -async function setupTestDir() { - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "advisory-suppression-test-")); -} - -async function cleanupTestDir() { - if (tempDir) { - await fs.rm(tempDir, { recursive: true, force: true }); - } -} function makeMatch(advisoryId, skillName, version = "1.0.0") { return { @@ -190,7 +167,7 @@ async function testMissingAdvisoryId() { async function testLoadWithAdvisorySentinel() { const testName = "loadAdvisorySuppression: loads config with advisory sentinel"; try { - const configFile = path.join(tempDir, "advisory-config.json"); + const configFile = path.join(tempDir.path, "advisory-config.json"); await fs.writeFile(configFile, JSON.stringify({ enabledFor: ["advisory"], suppressions: [{ @@ -215,7 +192,7 @@ async function testLoadWithAdvisorySentinel() { async function testLoadWithMissingSentinel() { const testName = "loadAdvisorySuppression: missing sentinel returns empty config"; try { - const configFile = path.join(tempDir, "no-sentinel.json"); + const configFile = path.join(tempDir.path, "no-sentinel.json"); await fs.writeFile(configFile, JSON.stringify({ suppressions: [{ checkId: "CVE-2026-25593", @@ -239,7 +216,7 @@ async function testLoadWithMissingSentinel() { async function testLoadWithAuditOnlySentinel() { const testName = "loadAdvisorySuppression: audit-only sentinel returns empty for advisory"; try { - const configFile = path.join(tempDir, "audit-only.json"); + const configFile = path.join(tempDir.path, "audit-only.json"); await fs.writeFile(configFile, JSON.stringify({ enabledFor: ["audit"], suppressions: [{ @@ -264,7 +241,7 @@ async function testLoadWithAuditOnlySentinel() { async function testLoadWithBothSentinels() { const testName = "loadAdvisorySuppression: both audit+advisory sentinels activates advisory"; try { - const configFile = path.join(tempDir, "both-sentinel.json"); + const configFile = path.join(tempDir.path, "both-sentinel.json"); await fs.writeFile(configFile, JSON.stringify({ enabledFor: ["audit", "advisory"], suppressions: [{ @@ -289,7 +266,7 @@ async function testLoadWithBothSentinels() { async function testLoadNonexistentExplicitPath() { const testName = "loadAdvisorySuppression: explicit nonexistent path throws"; try { - await loadAdvisorySuppression(path.join(tempDir, "does-not-exist.json")); + await loadAdvisorySuppression(path.join(tempDir.path, "does-not-exist.json")); fail(testName, "Expected error for nonexistent explicit path"); } catch (error) { if (String(error).includes("not found")) { @@ -328,7 +305,7 @@ async function testLoadNoConfigReturnsEmpty() { async function testEnvPathHomeExpansion() { const testName = "loadAdvisorySuppression: OPENCLAW_AUDIT_CONFIG expands $HOME"; try { - const configFile = path.join(tempDir, "env-home.json"); + const configFile = path.join(tempDir.path, "env-home.json"); await fs.writeFile(configFile, JSON.stringify({ enabledFor: ["advisory"], suppressions: [{ @@ -341,7 +318,7 @@ async function testEnvPathHomeExpansion() { const savedConfig = process.env.OPENCLAW_AUDIT_CONFIG; const savedHome = process.env.HOME; - process.env.HOME = tempDir; + process.env.HOME = tempDir.path; process.env.OPENCLAW_AUDIT_CONFIG = "$HOME/env-home.json"; try { const config = await loadAdvisorySuppression(); @@ -390,7 +367,7 @@ async function testEscapedHomeTokenRejected() { async function runAllTests() { console.log("=== Advisory Suppression Tests ===\n"); - await setupTestDir(); + tempDir = await createTempDir(); try { // isAdvisorySuppressed tests @@ -412,15 +389,11 @@ async function runAllTests() { await testEnvPathHomeExpansion(); await testEscapedHomeTokenRejected(); } finally { - await cleanupTestDir(); + await tempDir.cleanup(); } - console.log(""); - console.log(`=== Results: ${passCount} passed, ${failCount} failed ===`); - - if (failCount > 0) { - process.exit(1); - } + report(); + exitWithResults(); } runAllTests().catch((err) => { diff --git a/skills/clawsec-suite/test/feed_verification.test.mjs b/skills/clawsec-suite/test/feed_verification.test.mjs index 854aebb..f9db7a6 100644 --- a/skills/clawsec-suite/test/feed_verification.test.mjs +++ b/skills/clawsec-suite/test/feed_verification.test.mjs @@ -14,9 +14,17 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; -import os from "node:os"; 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"); @@ -26,33 +34,7 @@ const { verifySignedPayload, loadLocalFeed, isValidFeedPayload } = await import( `${LIB_PATH}/feed.mjs` ); -let tempDir; -let passCount = 0; -let failCount = 0; - -function pass(name) { - passCount++; - console.log(`✓ ${name}`); -} - -function fail(name, error) { - failCount++; - console.error(`✗ ${name}`); - console.error(` ${String(error)}`); -} - -function generateEd25519KeyPair() { - const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519"); - const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }); - const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }); - return { publicKeyPem, privateKeyPem }; -} - -function signPayload(data, privateKeyPem) { - const privateKey = crypto.createPrivateKey(privateKeyPem); - const signature = crypto.sign(null, Buffer.from(data, "utf8"), privateKey); - return signature.toString("base64"); -} +let tempDirCleanup; function createValidFeed() { return JSON.stringify( @@ -88,16 +70,6 @@ function createChecksumManifest(files) { ); } -async function setupTestDir() { - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawsec-test-")); -} - -async function cleanupTestDir() { - if (tempDir) { - await fs.rm(tempDir, { recursive: true, force: true }); - } -} - // ----------------------------------------------------------------------------- // Test: verifySignedPayload - valid signature // ----------------------------------------------------------------------------- @@ -252,11 +224,11 @@ async function testLoadLocalFeed_ValidSignedFeed() { const checksumSignature = signPayload(checksumManifest, privateKeyPem); // Write files - const feedPath = path.join(tempDir, "feed.json"); - const sigPath = path.join(tempDir, "feed.json.sig"); - const checksumPath = path.join(tempDir, "checksums.json"); - const checksumSigPath = path.join(tempDir, "checksums.json.sig"); - const keyPath = path.join(tempDir, "feed-signing-public.pem"); + 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"); @@ -293,7 +265,7 @@ async function testLoadLocalFeed_AdvisoriesPrefixedChecksumKeys() { const feedContent = createValidFeed(); const feedSignature = signPayload(feedContent, privateKeyPem); - const advisoriesDir = path.join(tempDir, "advisories"); + const advisoriesDir = path.join(globalThis.__testTempDir, "advisories"); await fs.mkdir(advisoriesDir, { recursive: true }); const checksumManifest = createChecksumManifest({ @@ -347,8 +319,8 @@ async function testLoadLocalFeed_TamperedFeedFails() { // Tamper with feed after signing const tamperedFeed = feedContent.replace("TEST-001", "TAMPERED-001"); - const feedPath = path.join(tempDir, "tampered-feed.json"); - const sigPath = path.join(tempDir, "tampered-feed.json.sig"); + 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"); @@ -383,8 +355,8 @@ async function testLoadLocalFeed_MissingSignatureFails() { const { publicKeyPem } = generateEd25519KeyPair(); const feedContent = createValidFeed(); - const feedPath = path.join(tempDir, "nosig-feed.json"); - const sigPath = path.join(tempDir, "nosig-feed.json.sig"); + 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 @@ -418,7 +390,7 @@ async function testLoadLocalFeed_AllowUnsignedBypasses() { try { const feedContent = createValidFeed(); - const feedPath = path.join(tempDir, "unsigned-feed.json"); + const feedPath = path.join(globalThis.__testTempDir, "unsigned-feed.json"); await fs.writeFile(feedPath, feedContent); const feed = await loadLocalFeed(feedPath, { @@ -462,10 +434,10 @@ async function testLoadLocalFeed_ChecksumMismatchFails() { ); const checksumSignature = signPayload(badChecksumManifest, privateKeyPem); - const feedPath = path.join(tempDir, "badcs-feed.json"); - const sigPath = path.join(tempDir, "badcs-feed.json.sig"); - const checksumPath = path.join(tempDir, "badcs-checksums.json"); - const checksumSigPath = path.join(tempDir, "badcs-checksums.json.sig"); + 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"); @@ -580,7 +552,11 @@ async function testIsValidFeedPayload_AdvisoryMissingId() { async function runTests() { console.log("=== ClawSec Feed Verification Tests ===\n"); - await setupTestDir(); + 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 @@ -604,14 +580,11 @@ async function runTests() { await testIsValidFeedPayload_MissingVersion(); await testIsValidFeedPayload_AdvisoryMissingId(); } finally { - await cleanupTestDir(); + await tempDirCleanup(); } - console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`); - - if (failCount > 0) { - process.exit(1); - } + report(); + exitWithResults(); } runTests().catch((error) => { diff --git a/skills/clawsec-suite/test/fuzz_properties.test.mjs b/skills/clawsec-suite/test/fuzz_properties.test.mjs index 1e19e9d..577b530 100644 --- a/skills/clawsec-suite/test/fuzz_properties.test.mjs +++ b/skills/clawsec-suite/test/fuzz_properties.test.mjs @@ -6,14 +6,17 @@ * Run: node skills/clawsec-suite/test/fuzz_properties.test.mjs */ +import { pass, fail, report, exitWithResults } from "./lib/test_harness.mjs"; import { runFuzzProperties } from "./fuzz_properties.js"; +console.log("=== ClawSec Fast-Check Fuzz Properties ===\n"); + try { - console.log("=== ClawSec Fast-Check Fuzz Properties ===\n"); runFuzzProperties(); - console.log("=== Results: all fuzz properties passed ==="); + pass("Property-based fuzz tests"); } catch (error) { - console.error("Fuzz property test failed:"); - console.error(error); - process.exit(1); + fail("Property-based fuzz tests", error); } + +report(); +exitWithResults(); diff --git a/skills/clawsec-suite/test/guarded_install.test.mjs b/skills/clawsec-suite/test/guarded_install.test.mjs index c7bfeb6..d82842b 100644 --- a/skills/clawsec-suite/test/guarded_install.test.mjs +++ b/skills/clawsec-suite/test/guarded_install.test.mjs @@ -18,37 +18,19 @@ import os from "node:os"; import path from "node:path"; import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; +import { + pass, + fail, + report, + exitWithResults, + generateEd25519KeyPair, + signPayload, +} from "./lib/test_harness.mjs"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const SCRIPT_PATH = path.resolve(__dirname, "..", "scripts", "guarded_skill_install.mjs"); let tempDir; -let passCount = 0; -let failCount = 0; - -function pass(name) { - passCount++; - console.log(`✓ ${name}`); -} - -function fail(name, error) { - failCount++; - console.error(`✗ ${name}`); - console.error(` ${String(error)}`); -} - -function generateEd25519KeyPair() { - const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519"); - const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }); - const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }); - return { publicKeyPem, privateKeyPem }; -} - -function signPayload(data, privateKeyPem) { - const privateKey = crypto.createPrivateKey(privateKeyPem); - const signature = crypto.sign(null, Buffer.from(data, "utf8"), privateKey); - return signature.toString("base64"); -} function createFeed(advisories) { return JSON.stringify( @@ -416,11 +398,8 @@ async function runTests() { await cleanupTestDir(); } - console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`); - - if (failCount > 0) { - process.exit(1); - } + report(); + exitWithResults(); } runTests().catch((error) => { diff --git a/skills/clawsec-suite/test/lib/.gitkeep b/skills/clawsec-suite/test/lib/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skills/clawsec-suite/test/lib/test_harness.mjs b/skills/clawsec-suite/test/lib/test_harness.mjs new file mode 100644 index 0000000..ba75c1c --- /dev/null +++ b/skills/clawsec-suite/test/lib/test_harness.mjs @@ -0,0 +1,182 @@ +/** + * Shared test harness for clawsec-suite tests. + * Provides consistent test reporting and runner utilities. + */ + +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +let passCount = 0; +let failCount = 0; + +/** + * Records a passing test. + * @param {string} name - Test name + */ +export function pass(name) { + passCount++; + console.log(`✓ ${name}`); +} + +/** + * Records a failing test. + * @param {string} name - Test name + * @param {Error|string} error - Error details + */ +export function fail(name, error) { + failCount++; + console.error(`✗ ${name}`); + console.error(` ${String(error)}`); +} + +/** + * Gets current test statistics. + * @returns {{passCount: number, failCount: number}} + */ +export function getStats() { + return { passCount, failCount }; +} + +/** + * Reports final test results to console. + */ +export function report() { + console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`); +} + +/** + * Exits with appropriate code based on test results. + * Exit code 0 for success, 1 for failures. + */ +export function exitWithResults() { + if (failCount > 0) { + process.exit(1); + } +} + +/** + * Creates an isolated test runner with its own pass/fail counters. + * Useful for running independent test suites within the same process. + * @returns {{pass: Function, fail: Function, getStats: Function, report: Function, exitWithResults: Function}} + */ +export function createTestRunner() { + let localPassCount = 0; + let localFailCount = 0; + + return { + /** + * Records a passing test. + * @param {string} name - Test name + */ + pass(name) { + localPassCount++; + console.log(`✓ ${name}`); + }, + + /** + * Records a failing test. + * @param {string} name - Test name + * @param {Error|string} error - Error details + */ + fail(name, error) { + localFailCount++; + console.error(`✗ ${name}`); + console.error(` ${String(error)}`); + }, + + /** + * Gets current test statistics. + * @returns {{passCount: number, failCount: number}} + */ + getStats() { + return { passCount: localPassCount, failCount: localFailCount }; + }, + + /** + * Reports final test results to console. + */ + report() { + console.log(`\n=== Results: ${localPassCount} passed, ${localFailCount} failed ===`); + }, + + /** + * Exits with appropriate code based on test results. + * Exit code 0 for success, 1 for failures. + */ + exitWithResults() { + if (localFailCount > 0) { + process.exit(1); + } + }, + }; +} + +/** + * Generates an Ed25519 keypair for test use. + * @returns {{publicKeyPem: string, privateKeyPem: string}} + */ +export function generateEd25519KeyPair() { + const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519"); + const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }); + const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }); + return { publicKeyPem, privateKeyPem }; +} + +/** + * Signs a payload with an Ed25519 private key. + * @param {string} data - Data to sign + * @param {string} privateKeyPem - PEM-encoded private key + * @returns {string} Base64-encoded signature + */ +export function signPayload(data, privateKeyPem) { + const privateKey = crypto.createPrivateKey(privateKeyPem); + const signature = crypto.sign(null, Buffer.from(data, "utf8"), privateKey); + return signature.toString("base64"); +} + +/** + * Creates a temporary directory for test use. + * @returns {Promise<{path: string, cleanup: Function}>} Object with temp dir path and cleanup function + */ +export async function createTempDir() { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawsec-test-")); + + return { + path: tmpDir, + cleanup: async () => { + try { + await fs.rm(tmpDir, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors + } + }, + }; +} + +/** + * Temporarily sets an environment variable for the duration of a function. + * Restores the original value (or deletes the variable) after the function completes. + * @param {string} key - Environment variable name + * @param {string|undefined} value - Value to set (undefined to delete) + * @param {Function} fn - Function to execute with the modified environment + * @returns {Promise<*>} Result of the function + */ +export 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; + } + } +} diff --git a/skills/clawsec-suite/test/path_resolution.test.mjs b/skills/clawsec-suite/test/path_resolution.test.mjs index 00e1d66..bff16d7 100644 --- a/skills/clawsec-suite/test/path_resolution.test.mjs +++ b/skills/clawsec-suite/test/path_resolution.test.mjs @@ -8,43 +8,12 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; +import { pass, fail, report, exitWithResults, withEnv } from "./lib/test_harness.mjs"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const LIB_PATH = path.resolve(__dirname, "..", "hooks", "clawsec-advisory-guardian", "lib"); const { resolveUserPath, resolveConfiguredPath } = await import(`${LIB_PATH}/utils.mjs`); -let passCount = 0; -let failCount = 0; - -function pass(name) { - passCount += 1; - console.log(`\u2713 ${name}`); -} - -function fail(name, error) { - failCount += 1; - console.error(`\u2717 ${name}`); - console.error(` ${String(error)}`); -} - -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; - } - } -} - async function testTildeExpansion() { const testName = "resolveUserPath: expands leading tilde"; await withEnv("HOME", "/tmp/clawsec-home", async () => { @@ -157,10 +126,8 @@ async function runTests() { await testConfiguredPathFallbackOnInvalidExplicit(); await testConfiguredPathUsesValidExplicit(); - console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`); - if (failCount > 0) { - process.exit(1); - } + report(); + exitWithResults(); } runTests().catch((error) => { diff --git a/skills/clawsec-suite/test/skill_catalog_discovery.test.mjs b/skills/clawsec-suite/test/skill_catalog_discovery.test.mjs index 65a783c..b9fbaf5 100644 --- a/skills/clawsec-suite/test/skill_catalog_discovery.test.mjs +++ b/skills/clawsec-suite/test/skill_catalog_discovery.test.mjs @@ -15,24 +15,11 @@ 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"); -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)}`); -} - function runCatalogScript(args, env = {}) { return new Promise((resolve) => { const proc = spawn("node", [SCRIPT_PATH, ...args], { @@ -237,11 +224,8 @@ async function runTests() { await testInvalidRemotePayloadFallsBack(); await testUnreachableRemoteFallsBack(); - console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`); - - if (failCount > 0) { - process.exit(1); - } + report(); + exitWithResults(); } runTests().catch((error) => { diff --git a/skills/openclaw-audit-watchdog/test/render_report_suppression.test.mjs b/skills/openclaw-audit-watchdog/test/render_report_suppression.test.mjs index 0afba67..7bc3aab 100755 --- a/skills/openclaw-audit-watchdog/test/render_report_suppression.test.mjs +++ b/skills/openclaw-audit-watchdog/test/render_report_suppression.test.mjs @@ -16,39 +16,16 @@ */ import fs from "node:fs/promises"; -import os from "node:os"; import path from "node:path"; import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; +import { pass, fail, report, exitWithResults, createTempDir } from "../../clawsec-suite/test/lib/test_harness.mjs"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const SCRIPT_PATH = path.resolve(__dirname, "..", "scripts", "render_report.mjs"); const NODE_BIN = process.execPath; let tempDir; -let passCount = 0; -let failCount = 0; - -function pass(name) { - passCount++; - console.log(`✓ ${name}`); -} - -function fail(name, error) { - failCount++; - console.error(`✗ ${name}`); - console.error(` ${String(error)}`); -} - -async function setupTestDir() { - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "render-report-test-")); -} - -async function cleanupTestDir() { - if (tempDir) { - await fs.rm(tempDir, { recursive: true, force: true }); - } -} function createAuditJson(findings) { return JSON.stringify({ @@ -730,7 +707,8 @@ async function testConfigWithoutEnableFlagDoesNotSuppress() { // Main test runner // ----------------------------------------------------------------------------- async function runAllTests() { - await setupTestDir(); + const tmpDir = await createTempDir(); + tempDir = tmpDir.path; try { await testSuppressedFindingsDisplayed(); @@ -745,16 +723,11 @@ async function runAllTests() { await testEmptySuppressions(); await testConfigWithoutEnableFlagDoesNotSuppress(); } finally { - await cleanupTestDir(); + await tmpDir.cleanup(); } - console.log(""); - console.log(`Passed: ${passCount}`); - console.log(`Failed: ${failCount}`); - - if (failCount > 0) { - process.exit(1); - } + report(); + exitWithResults(); } runAllTests().catch((err) => { diff --git a/skills/openclaw-audit-watchdog/test/suppression_config.test.mjs b/skills/openclaw-audit-watchdog/test/suppression_config.test.mjs index edc087f..be602fb 100755 --- a/skills/openclaw-audit-watchdog/test/suppression_config.test.mjs +++ b/skills/openclaw-audit-watchdog/test/suppression_config.test.mjs @@ -19,57 +19,33 @@ import fs from "node:fs/promises"; import path from "node:path"; import os from "node:os"; +import { + pass, + fail, + report, + exitWithResults, + createTempDir, + withEnv, +} from "../../clawsec-suite/test/lib/test_harness.mjs"; import { loadSuppressionConfig } from "../scripts/load_suppression_config.mjs"; -let passCount = 0; -let failCount = 0; - -function pass(name) { - passCount += 1; - console.log(`\u2713 ${name}`); -} - -function fail(name, error) { - failCount += 1; - console.error(`\u2717 ${name}`); - console.error(` ${String(error)}`); -} - +/** + * Creates a temporary file with the given content. + * Wrapper around createTempDir for test config file creation. + * @param {string} content - File content + * @returns {Promise<{path: string, cleanup: Function}>} + */ async function withTempFile(content) { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-")); - const tmpFile = path.join(tmpDir, "test-config.json"); + const tmpDir = await createTempDir(); + const tmpFile = path.join(tmpDir.path, "test-config.json"); await fs.writeFile(tmpFile, content, "utf8"); return { path: tmpFile, - cleanup: async () => { - try { - await fs.rm(tmpDir, { recursive: true, force: true }); - } catch { - // Ignore cleanup errors - } - }, + cleanup: tmpDir.cleanup, }; } -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; - } - } -} - /** Suppress stderr output during a function call (avoids noisy warnings in test output). */ async function silenceStderr(fn) { const original = process.stderr.write; @@ -748,11 +724,8 @@ async function runTests() { await testMissingSentinel(); await testWrongSentinel(); - console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`); - - if (failCount > 0) { - process.exit(1); - } + report(); + exitWithResults(); } runTests().catch((error) => {