From dc5d7e414ece520340c40dfbd927242028b2d5de Mon Sep 17 00:00:00 2001 From: anshul23102 Date: Sat, 6 Jun 2026 13:02:03 +0530 Subject: [PATCH 1/3] test(crawler): add contract and parser coverage for crawler plugin Add backend test suite for the crawler plugin that loads the real plugins/crawler/metadata.json, validates it through PluginMetadataValidator, renders commands through PluginManager.build_command(), and calls the real plugins.crawler.parser.parse() directly. Assertions are tied to the actual plugin contract: - engine.binary == "katana" - target field requires http(s):// URL - depth field has a default of 2 applied from metadata.json - explicit depth override works correctly - full command token sequence from real command_template - severity classification: high for critical/injection, low for found/exposed - required keys in each finding dict - items list matches the parsed output lines Tests will fail if metadata.json, command_template, or parser.py drift. Closes #494 --- testing/backend/test_crawler_plugin.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/testing/backend/test_crawler_plugin.py b/testing/backend/test_crawler_plugin.py index 70419e84d..23b6b5440 100644 --- a/testing/backend/test_crawler_plugin.py +++ b/testing/backend/test_crawler_plugin.py @@ -87,11 +87,10 @@ def test_crawler_target_field_requires_http_url(): data = json.loads((PLUGIN_DIR / "metadata.json").read_text(encoding="utf-8")) fields = {f["id"]: f for f in data["fields"]} target_validation = fields["target"].get("validation", {}) - uses_preset = target_validation.get("validation_type") == "url" - uses_pattern = "https?" in target_validation.get("pattern", "") or \ - "http" in target_validation.get("pattern", "") - assert uses_preset or uses_pattern, \ + pattern = target_validation.get("pattern", "") + assert "https?" in pattern or "http" in pattern, ( "target field must validate for HTTP(S) URL format" + ) def test_crawler_has_optional_depth_field_with_default(): @@ -188,6 +187,7 @@ def test_crawler_command_respects_explicit_depth(setup_test_environment): ) +<<<<<<< HEAD def test_crawler_drops_target_token_when_absent(setup_test_environment): """ When the 'target' field is omitted, the renderer drops the unresolved @@ -206,6 +206,15 @@ def test_crawler_drops_target_token_when_absent(setup_test_environment): populated = manager.build_command("crawler", {"target": "https://example.com"}) assert "https://example.com" in populated assert len(populated) == len(rendered) + 1 +======= +def test_crawler_requires_target_field(setup_test_environment): + """build_command must return None when the required 'target' field is absent.""" + manager = PluginManager(str(PLUGINS_DIR)) + asyncio.run(manager.load_plugins()) + + result = manager.build_command("crawler", {}) + assert result is None +>>>>>>> ac1eabf (test(crawler): add contract and parser coverage for crawler plugin) def test_crawler_loaded_by_plugin_manager(setup_test_environment): From b8c99297d9812c3f4ce9c5a292b9d9d12a0e9050 Mon Sep 17 00:00:00 2001 From: anshul23102 Date: Sat, 6 Jun 2026 14:05:41 +0530 Subject: [PATCH 2/3] test(crawler): assert token-drop behavior for missing target build_command drops the unresolved {target} token instead of returning None. Updated the test to assert the real renderer contract while confirming the default depth scaffold is preserved. --- testing/backend/test_crawler_plugin.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/testing/backend/test_crawler_plugin.py b/testing/backend/test_crawler_plugin.py index 23b6b5440..93ef65d9e 100644 --- a/testing/backend/test_crawler_plugin.py +++ b/testing/backend/test_crawler_plugin.py @@ -187,7 +187,6 @@ def test_crawler_command_respects_explicit_depth(setup_test_environment): ) -<<<<<<< HEAD def test_crawler_drops_target_token_when_absent(setup_test_environment): """ When the 'target' field is omitted, the renderer drops the unresolved @@ -206,15 +205,6 @@ def test_crawler_drops_target_token_when_absent(setup_test_environment): populated = manager.build_command("crawler", {"target": "https://example.com"}) assert "https://example.com" in populated assert len(populated) == len(rendered) + 1 -======= -def test_crawler_requires_target_field(setup_test_environment): - """build_command must return None when the required 'target' field is absent.""" - manager = PluginManager(str(PLUGINS_DIR)) - asyncio.run(manager.load_plugins()) - - result = manager.build_command("crawler", {}) - assert result is None ->>>>>>> ac1eabf (test(crawler): add contract and parser coverage for crawler plugin) def test_crawler_loaded_by_plugin_manager(setup_test_environment): From bae2aca8a6ee00684b2160016a2a98a62db8ca41 Mon Sep 17 00:00:00 2001 From: Anshul Jain Date: Tue, 16 Jun 2026 20:38:50 +0530 Subject: [PATCH 3/3] fix(tests): enhance localStorage mock to support Object.keys() iteration for nuclear purge test The custom jsdom localStorage mock did not properly implement iteration, causing Object.keys(localStorage) to fail in SettingsSaveReset.test.tsx. Added Proxy traps (ownKeys, getOwnPropertyDescriptor) to support proper Object.keys() enumeration, allowing the nuclear purge test to pass. --- frontend/vitest.setup.ts | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/frontend/vitest.setup.ts b/frontend/vitest.setup.ts index 7a7a6f7bf..507ae012e 100644 --- a/frontend/vitest.setup.ts +++ b/frontend/vitest.setup.ts @@ -2,7 +2,7 @@ import '@testing-library/jest-dom'; function createStorageMock() { const store = new Map(); - return { + const handler = { getItem: (key: string) => (store.has(key) ? store.get(key)! : null), setItem: (key: string, value: string) => { store.set(key, String(value)); @@ -18,6 +18,37 @@ function createStorageMock() { return store.size; }, }; + + return new Proxy(handler, { + get(target, prop) { + if (prop === 'length') { + return store.size; + } + if (typeof prop === 'string' && !isNaN(Number(prop))) { + return Array.from(store.entries())[Number(prop)]?.[1] ?? null; + } + if (prop in target) { + return (target as any)[prop]; + } + return store.get(String(prop)) ?? null; + }, + ownKeys() { + return Array.from(store.keys()); + }, + getOwnPropertyDescriptor(target, prop) { + if (store.has(String(prop))) { + return { + configurable: true, + enumerable: true, + value: store.get(String(prop)), + }; + } + if (prop in target) { + return Object.getOwnPropertyDescriptor(target, prop); + } + return undefined; + }, + }); } if (!window.localStorage || typeof window.localStorage.getItem !== 'function') {