@heretek-ai/epistemic-swarm 0.7.21 → 0.7.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +1 -1
  3. package/bin/cli.js +3 -2
  4. package/extensions/pi/index.js +3 -2
  5. package/package.json +5 -1
  6. package/plugins/antigravity/plugin.json +1 -1
  7. package/plugins/factory/skills/factory/SKILL.md +1 -1
  8. package/plugins/opencode/analysis-bridge.js +566 -0
  9. package/plugins/opencode/config-io.js +598 -31
  10. package/plugins/opencode/dep-health.js +1039 -0
  11. package/plugins/opencode/hook-bus.js +434 -0
  12. package/plugins/opencode/index.js +252 -19
  13. package/plugins/opencode/tui.js +230 -17
  14. package/plugins/opencode/weight-baseline.json +16 -0
  15. package/plugins/opencode/weight-signals.js +769 -0
  16. package/plugins/research-cache/skills/research_cache/hasher.py +33 -8
  17. package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
  18. package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
  19. package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
  20. package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
  21. package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
  22. package/runner/__pycache__/darkharvest_claims.cpython-311.pyc +0 -0
  23. package/runner/__pycache__/errors.cpython-311.pyc +0 -0
  24. package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
  25. package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
  26. package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
  27. package/runner/__pycache__/path_safety.cpython-311.pyc +0 -0
  28. package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
  29. package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
  30. package/runner/__pycache__/preflight.cpython-311.pyc +0 -0
  31. package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
  32. package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
  33. package/runner/__pycache__/retrieval_log.cpython-311.pyc +0 -0
  34. package/runner/__pycache__/schema_validate.cpython-311.pyc +0 -0
  35. package/runner/__pycache__/schemas.cpython-311.pyc +0 -0
  36. package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
  37. package/runner/mcp_server.py +249 -81
  38. package/runner/refinement.py +5 -0
  39. package/runner/schema_validate.py +63 -1
  40. package/runner/schemas.py +9 -4
  41. package/runner/tests/__pycache__/test_analysis_bridge.cpython-311.pyc +0 -0
  42. package/runner/tests/__pycache__/test_antigravity_plugin.cpython-311.pyc +0 -0
  43. package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
  44. package/runner/tests/__pycache__/test_backends.cpython-311.pyc +0 -0
  45. package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
  46. package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
  47. package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
  48. package/runner/tests/__pycache__/test_claude_plugin.cpython-311.pyc +0 -0
  49. package/runner/tests/__pycache__/test_config_schema.cpython-311.pyc +0 -0
  50. package/runner/tests/__pycache__/test_contract_tooling.cpython-311.pyc +0 -0
  51. package/runner/tests/__pycache__/test_darkharvest_audit.cpython-311.pyc +0 -0
  52. package/runner/tests/__pycache__/test_dep_health.cpython-311.pyc +0 -0
  53. package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
  54. package/runner/tests/__pycache__/test_dossier_contract.cpython-311.pyc +0 -0
  55. package/runner/tests/__pycache__/test_factory.cpython-311.pyc +0 -0
  56. package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
  57. package/runner/tests/__pycache__/test_frontier_concurrency.cpython-311.pyc +0 -0
  58. package/runner/tests/__pycache__/test_frontier_mirror_standalone.cpython-311.pyc +0 -0
  59. package/runner/tests/__pycache__/test_grounding.cpython-311.pyc +0 -0
  60. package/runner/tests/__pycache__/test_hardening_contract.cpython-311.pyc +0 -0
  61. package/runner/tests/__pycache__/test_hook_bus.cpython-311.pyc +0 -0
  62. package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
  63. package/runner/tests/__pycache__/test_manifest_concurrency.cpython-311.pyc +0 -0
  64. package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
  65. package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
  66. package/runner/tests/__pycache__/test_opencode_v2_registrars.cpython-311.pyc +0 -0
  67. package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
  68. package/runner/tests/__pycache__/test_phase08_waiver_reopens.cpython-311.pyc +0 -0
  69. package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
  70. package/runner/tests/__pycache__/test_search_anomaly.cpython-311.pyc +0 -0
  71. package/runner/tests/__pycache__/test_search_provider.cpython-311.pyc +0 -0
  72. package/runner/tests/__pycache__/test_search_provider_plugin.cpython-311.pyc +0 -0
  73. package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
  74. package/runner/tests/__pycache__/test_swarm_config.cpython-311.pyc +0 -0
  75. package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
  76. package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
  77. package/runner/tests/__pycache__/test_weight_signals.cpython-311.pyc +0 -0
  78. package/runner/tests/fixtures/config_validation_cases.json +60 -0
  79. package/runner/tests/test_analysis_bridge.py +356 -0
  80. package/runner/tests/test_config_schema.py +119 -15
  81. package/runner/tests/test_dep_health.py +484 -0
  82. package/runner/tests/test_hardening_contract.py +391 -0
  83. package/runner/tests/test_hook_bus.py +802 -0
  84. package/runner/tests/test_opencode_ux.py +232 -0
  85. package/runner/tests/test_opencode_v2_registrars.py +548 -16
  86. package/runner/tests/test_phase08_waiver_reopens.py +539 -0
  87. package/runner/tests/test_weight_signals.py +361 -0
  88. package/schemas/config.schema.json +4 -3
  89. package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
  90. package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
  91. package/scripts/__pycache__/check_docs.cpython-311.pyc +0 -0
  92. package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
  93. package/scripts/__pycache__/gen_schemas.cpython-311.pyc +0 -0
  94. package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
  95. package/skills/factory/SKILL.md +1 -1
  96. package/skills/grilling/__pycache__/__init__.cpython-311.pyc +0 -0
  97. package/skills/grilling/__pycache__/socratic_tree.cpython-311.pyc +0 -0
  98. package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
  99. package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
  100. package/skills/research_cache/hasher.py +33 -8
  101. package/skills/swarm_config/SKILL.md +6 -1
  102. package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
  103. package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
  104. package/skills/swarm_config/configure.py +254 -17
@@ -60,6 +60,19 @@ VALIDATION_CASES = json.loads(
60
60
 
61
61
  CONTROL_ARGS = set(CONFIG_TOOL_CONTROL_ARGS)
62
62
 
63
+ # H1: every keyword the parity comparator deep-compares at each schema node.
64
+ # `items` / `additionalProperties` recurse as schema nodes; `default` compares
65
+ # by value (deep: dict/list defaults compare element-wise via !=).
66
+ COMPARE_KEYWORDS = (
67
+ "type",
68
+ "enum",
69
+ "minimum",
70
+ "maximum",
71
+ "minLength",
72
+ "pattern",
73
+ "default",
74
+ )
75
+
63
76
  # One representative valid value per canonical key; the test asserts this map
64
77
  # covers the schema exactly, so a new key cannot slip in without a test.
65
78
  VALID_UPDATES = {
@@ -221,27 +234,45 @@ class TestMcpConfigSurface(unittest.TestCase):
221
234
  advertised["properties"]["expectedHash"]["description"],
222
235
  )
223
236
 
237
+ def _compare_schema_node(self, sub, advertised_node, path, problems):
238
+ """Deep-compare one schema node, recursing into subschemas.
239
+
240
+ H1: besides the scalar keywords, `properties` recurses per key,
241
+ `items` recurses as `{path}[]`, and `additionalProperties` recurses
242
+ as `{path}.*`. A canonical subschema missing on the advertised side
243
+ is a drift report, never a silent skip.
244
+ """
245
+ for keyword in COMPARE_KEYWORDS:
246
+ if sub.get(keyword) != advertised_node.get(keyword):
247
+ problems.append(
248
+ f"{path}: {keyword} {sub.get(keyword)!r} != "
249
+ f"{advertised_node.get(keyword)!r}"
250
+ )
251
+ if isinstance(sub.get("properties"), dict):
252
+ if not isinstance(advertised_node.get("properties"), dict):
253
+ problems.append(f"{path}: properties schema missing on advertised side")
254
+ else:
255
+ self._compare_properties(sub, advertised_node, f"{path}.", problems)
256
+ for keyword, suffix in (("items", "[]"), ("additionalProperties", ".*")):
257
+ sub_node = sub.get(keyword)
258
+ if isinstance(sub_node, dict):
259
+ adv_node = advertised_node.get(keyword)
260
+ if not isinstance(adv_node, dict):
261
+ problems.append(
262
+ f"{path}{suffix}: {keyword} schema missing on advertised side"
263
+ )
264
+ else:
265
+ self._compare_schema_node(
266
+ sub_node, adv_node, f"{path}{suffix}", problems
267
+ )
268
+
224
269
  def _compare_properties(self, canonical, advertised, path, problems):
225
270
  for key, sub in (canonical.get("properties") or {}).items():
226
271
  adv = (advertised.get("properties") or {}).get(key)
227
272
  if adv is None:
228
273
  problems.append(f"missing advertised key: {path}{key}")
229
274
  continue
230
- for keyword in (
231
- "type",
232
- "enum",
233
- "minimum",
234
- "maximum",
235
- "minLength",
236
- "pattern",
237
- ):
238
- if sub.get(keyword) != adv.get(keyword):
239
- problems.append(
240
- f"{path}{key}: {keyword} {sub.get(keyword)!r} != "
241
- f"{adv.get(keyword)!r}"
242
- )
243
- if isinstance(sub.get("properties"), dict):
244
- self._compare_properties(sub, adv, f"{path}{key}.", problems)
275
+ self._compare_schema_node(sub, adv, f"{path}{key}", problems)
245
276
 
246
277
  def test_parity_comparator_is_not_vacuous(self):
247
278
  """R2: the comparator must bite when the advertised surface drifts."""
@@ -264,6 +295,79 @@ class TestMcpConfigSurface(unittest.TestCase):
264
295
  self._compare_properties(artifact, drifted, "", problems)
265
296
  self.assertTrue(any("searxng_url: pattern" in p for p in problems))
266
297
 
298
+ def test_parity_comparator_catches_items_additionalProperties_default_drift(self):
299
+ """H1: each newly walked keyword bites when the advertised side drifts.
300
+
301
+ Every sub-case mutates a FRESH advertised schema exactly once, so a
302
+ green sub-case proves its own keyword is compared (never vacuous via
303
+ a shared broken fixture).
304
+ """
305
+ from runner.mcp_server import config_tool_input_schema
306
+
307
+ artifact = json.loads(
308
+ (PROJECT_ROOT / "schemas" / "config.schema.json").read_text(
309
+ encoding="utf-8"
310
+ )
311
+ )
312
+
313
+ def complained_of(mutator, needle):
314
+ advertised = config_tool_input_schema()
315
+ mutator(advertised["properties"])
316
+ problems = []
317
+ self._compare_properties(artifact, advertised, "", problems)
318
+ self.assertTrue(
319
+ any(needle in p for p in problems),
320
+ f"{needle!r} not in {problems}",
321
+ )
322
+
323
+ # `items` drift: top-level (license_whitelist) and nested
324
+ # (agents.alpha.backend); then a dropped `items` subschema.
325
+ complained_of(
326
+ lambda props: props["license_whitelist"].__setitem__(
327
+ "items", {"type": "integer"}
328
+ ),
329
+ "license_whitelist[]: type",
330
+ )
331
+ complained_of(
332
+ lambda props: props["agents"]["properties"]["alpha"]["properties"][
333
+ "backend"
334
+ ].__setitem__("items", {"type": "integer"}),
335
+ "agents.alpha.backend[]: type",
336
+ )
337
+ complained_of(
338
+ lambda props: props["license_whitelist"].pop("items"),
339
+ "license_whitelist[]: items schema missing",
340
+ )
341
+
342
+ # `additionalProperties` drift (mcp_servers) and a dropped subschema.
343
+ complained_of(
344
+ lambda props: props["mcp_servers"].__setitem__(
345
+ "additionalProperties", {"type": "string"}
346
+ ),
347
+ "mcp_servers.*: type",
348
+ )
349
+ complained_of(
350
+ lambda props: props["mcp_servers"].pop("additionalProperties"),
351
+ "mcp_servers.*: additionalProperties schema missing",
352
+ )
353
+
354
+ # `default` drift: top-level (mode) and nested
355
+ # (verify.min_fuzzy_confidence); then a dropped `default`.
356
+ complained_of(
357
+ lambda props: props["mode"].__setitem__("default", "scout"),
358
+ "mode: default",
359
+ )
360
+ complained_of(
361
+ lambda props: props["verify"]["properties"][
362
+ "min_fuzzy_confidence"
363
+ ].__setitem__("default", 0.5),
364
+ "verify.min_fuzzy_confidence: default",
365
+ )
366
+ complained_of(
367
+ lambda props: props["mode"].pop("default"),
368
+ "mode: default",
369
+ )
370
+
267
371
  def test_valid_update_map_covers_every_canonical_key(self):
268
372
  self.assertEqual(set(VALID_UPDATES), set(CONFIG["properties"]))
269
373
  self.assertEqual(set(INVALID_UPDATES), set(CONFIG["properties"]))
@@ -0,0 +1,484 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for Phase 03-dep-health-gate: programmer-time dep warnings + QA gate.
3
+
4
+ Mock-host only (no live TUI, no real npm/OSV): every runner is injected,
5
+ every budget is tens of milliseconds, at most zero MCP spawns. Pins:
6
+ never-blocks (critical-CVE fixture still allows), offline-unavailable-recorded,
7
+ absent-pass, missing-report-fail, unrecorded-findings-fail, annotation key
8
+ completeness (eleven always-present keys), OSV timeout bound, the repo
9
+ baseline shape (npm scanned, pip absent, 2x :latest advisories, actions
10
+ clean), and the setup adopt/dispose wiring in plugins/opencode/index.js.
11
+
12
+ Phase evidence cited in-code per brief:
13
+ [VERIFIED: sha256:7690b878a654ee4dee2b649b60740bc95d4b746cf786051887153b6f656f2e5e]
14
+ [VERIFIED: sha256:1fd0c2b8b0d37d553f1f841cea4e4b60d92cdf873257172a726d482954dddf8f]
15
+ [VERIFIED: sha256:7b62111192e07c73547a125eeab7ff98f2d1305a04149f1ab51c66a41a3e3f95]
16
+ [VERIFIED: sha256:c381a646894ce66669432e847f0afd647ea5ff00ac64a01cf91f325f5515e601]
17
+ [VERIFIED: sha256:ae16788a0a85caed3983dbc70455eaa05857827c837a5f5999242ef32b0a16a7]
18
+ """
19
+
20
+ import json
21
+ import subprocess
22
+ import sys
23
+ import unittest
24
+ from pathlib import Path
25
+
26
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
27
+ if str(PROJECT_ROOT) not in sys.path:
28
+ sys.path.insert(0, str(PROJECT_ROOT))
29
+
30
+
31
+ def run_node(code):
32
+ return subprocess.run(
33
+ ["node", "--input-type=module", "-e", code],
34
+ capture_output=True,
35
+ text=True,
36
+ cwd=str(PROJECT_ROOT),
37
+ )
38
+
39
+
40
+ def last_json_object(stdout):
41
+ lines = [
42
+ line.strip()
43
+ for line in stdout.strip().split("\n")
44
+ if line.strip().startswith("{")
45
+ ]
46
+ return json.loads(lines[-1])
47
+
48
+
49
+ DEP_IMPORT = (
50
+ 'import { createHookBus } from "./plugins/opencode/hook-bus.js";\n'
51
+ 'import { checkDeps, depHealthCoverage, createDepHealthHandler, registerDepHealth, toDepFinding } from "./plugins/opencode/dep-health.js";\n'
52
+ )
53
+
54
+ MOCK_CLEAN_RUNNERS = """
55
+ const runOutdated = async () => ({status: "ok", json: {}});
56
+ const runAudit = async () => ({status: "ok", json: {advisories: {}}});
57
+ const queryOsv = async () => ({status: "ok", vulns: []});
58
+ """
59
+
60
+ MOCK_CRITICAL_RUNNERS = """
61
+ const runOutdated = async () => ({status: "ok", json: {
62
+ "left-pad": {current: "1.0.0", wanted: "1.3.0", latest: "1.3.0"},
63
+ }});
64
+ const runAudit = async () => ({status: "ok", json: {vulnerabilities: {
65
+ "left-pad": {severity: "critical", range: "1.0.0", fixAvailable: "1.3.0",
66
+ via: [{title: "CVE-2099-0001 prototype pollution", url: "https://example.invalid/cve"}]},
67
+ }}});
68
+ const queryOsv = async () => ({status: "ok", vulns: [
69
+ {id: "GHSA-xxxx-yyyy", summary: "critical RCE",
70
+ affected: [{ranges: [{events: [{introduced: "0"}, {fixed: "2.0.0"}]}]}]},
71
+ ]});
72
+ """
73
+
74
+ MOCK_OFFLINE_RUNNERS = """
75
+ const runOutdated = async () => { throw new Error("ENOTFOUND registry.npmjs.org"); };
76
+ const runAudit = async () => ({status: "error", message: "EAI_AGAIN"});
77
+ const queryOsv = async () => ({status: "error", message: "fetch failed"});
78
+ """
79
+
80
+ SETUP_DEP = """
81
+ const bus = createHookBus();
82
+ const notices = [];
83
+ const notify = (s) => { notices.push(s); };
84
+ const reg = await registerDepHealth(null, { bus, runOutdated, runAudit, queryOsv, notify, osvTimeoutMs: 200 });
85
+ """
86
+
87
+
88
+ class TestNeverBlocks(unittest.TestCase):
89
+ def test_critical_fixture_still_allows(self):
90
+ res = run_node(
91
+ DEP_IMPORT
92
+ + MOCK_CRITICAL_RUNNERS
93
+ + SETUP_DEP
94
+ + """
95
+ const manifests = { packageJson: JSON.stringify({dependencies: {"left-pad": "1.0.0"}}) };
96
+ const direct = await checkDeps({ manifests, runOutdated, runAudit, queryOsv, osvTimeoutMs: 200, tool: "edit", eventId: "e-crit" });
97
+ const flat = JSON.stringify(direct);
98
+ const r = await bus.emit("pre-tool-use", {tool: "edit", eventId: "e-crit"});
99
+ const audit = bus.getAuditLog();
100
+ const findingAudits = audit.filter((a) => a.tool === "edit" && a.eventId === "e-crit");
101
+ console.log(JSON.stringify({
102
+ allowed: direct.allowed,
103
+ hasDeny: flat.includes("deny"),
104
+ npmStatus: direct.ecosystems.npm.status,
105
+ severities: direct.ecosystems.npm.findings.map((f) => f.severity),
106
+ emitAllowed: r.allowed,
107
+ notices: notices.length,
108
+ notice: notices[0] && notices[0].message,
109
+ findingAudits: findingAudits.length,
110
+ }));
111
+ """
112
+ )
113
+ self.assertEqual(res.returncode, 0, f"never-blocks test failed: {res.stderr}")
114
+ data = last_json_object(res.stdout)
115
+ self.assertTrue(data["allowed"], "critical CVE must still allow (all-advisory)")
116
+ self.assertFalse(data["hasDeny"], "no deny verdict anywhere in the result")
117
+ self.assertEqual(data["npmStatus"], "scanned")
118
+ self.assertIn(
119
+ "critical", data["severities"], "critical severity must be recorded"
120
+ )
121
+ self.assertTrue(
122
+ data["emitAllowed"], "critical findings must not block the caller"
123
+ )
124
+ self.assertEqual(data["notices"], 1, "exactly one toast summary per scan")
125
+ self.assertIn("finding", data["notice"])
126
+ # Toast summarizes counts only: no package names, no CVE text.
127
+ self.assertNotIn("left-pad", data["notice"])
128
+ self.assertNotIn("CVE-2099-0001", data["notice"])
129
+ self.assertGreaterEqual(
130
+ data["findingAudits"], 2, "findings must be audit-logged on the bus"
131
+ )
132
+
133
+
134
+ class TestOfflineUnavailable(unittest.TestCase):
135
+ def test_offline_records_unavailable_and_allows(self):
136
+ res = run_node(
137
+ DEP_IMPORT
138
+ + MOCK_OFFLINE_RUNNERS
139
+ + SETUP_DEP
140
+ + """
141
+ const manifests = { packageJson: JSON.stringify({dependencies: {"left-pad": "1.0.0"}}) };
142
+ let threw = false;
143
+ let direct = null;
144
+ try {
145
+ direct = await checkDeps({ manifests, runOutdated, runAudit, queryOsv, osvTimeoutMs: 200 });
146
+ } catch { threw = true; }
147
+ let r = null;
148
+ try {
149
+ r = await bus.emit("pre-tool-use", {tool: "edit", eventId: "e-off"});
150
+ } catch { threw = true; }
151
+ console.log(JSON.stringify({
152
+ threw, allowed: direct && direct.allowed,
153
+ npmStatus: direct && direct.ecosystems.npm.status,
154
+ recorded: direct && direct.ecosystems.npm.findings.length,
155
+ sources: direct && direct.ecosystems.npm.findings.map((f) => f.source),
156
+ emitAllowed: r && r.allowed,
157
+ }));
158
+ """
159
+ )
160
+ self.assertEqual(res.returncode, 0, f"offline test failed: {res.stderr}")
161
+ data = last_json_object(res.stdout)
162
+ self.assertFalse(data["threw"], "offline run must never throw")
163
+ self.assertTrue(data["allowed"], "offline run must fail open")
164
+ self.assertEqual(data["npmStatus"], "unavailable")
165
+ self.assertGreaterEqual(data["recorded"], 1, "unavailability must be RECORDED")
166
+ self.assertIn("unavailable", data["sources"])
167
+ self.assertTrue(data["emitAllowed"])
168
+ # The recorded outage passes the mechanical gate.
169
+ res2 = run_node(
170
+ DEP_IMPORT
171
+ + """
172
+ const cov = depHealthCoverage({
173
+ npm: {status: "unavailable", findings: [{ecosystem: "npm", source: "unavailable"}]},
174
+ pip: {status: "absent", findings: []},
175
+ containers: {status: "absent", findings: []},
176
+ actions: {status: "absent", findings: []},
177
+ });
178
+ console.log(JSON.stringify({pass: cov.pass, fails: cov.gaps.filter((g) => g.fail)}));
179
+ """
180
+ )
181
+ self.assertEqual(res2.returncode, 0, f"gate test failed: {res2.stderr}")
182
+ data2 = last_json_object(res2.stdout)
183
+ self.assertTrue(data2["pass"], "unavailable(recorded) must pass the gate")
184
+ self.assertEqual(data2["fails"], [])
185
+
186
+
187
+ class TestCoverageMatrix(unittest.TestCase):
188
+ def test_matrix(self):
189
+ res = run_node(
190
+ DEP_IMPORT
191
+ + """
192
+ const scanned = depHealthCoverage({
193
+ npm: {status: "scanned", findings: []},
194
+ pip: {status: "absent", findings: []},
195
+ containers: {status: "scanned", findings: [{ecosystem: "containers"}]},
196
+ actions: {status: "scanned", findings: []},
197
+ });
198
+ const absentNote = depHealthCoverage({
199
+ npm: {status: "scanned", findings: []},
200
+ pip: {status: "absent", findings: []},
201
+ containers: {status: "absent", findings: []},
202
+ actions: {status: "absent", findings: []},
203
+ });
204
+ const missing = depHealthCoverage({npm: {status: "scanned", findings: []}});
205
+ const unrecorded = depHealthCoverage({
206
+ npm: {status: "scanned"},
207
+ pip: {status: "absent", findings: []},
208
+ containers: {status: "absent", findings: []},
209
+ actions: {status: "absent", findings: []},
210
+ });
211
+ const unrecordedFlag = depHealthCoverage({
212
+ npm: {status: "scanned", findings: [], recorded: false},
213
+ pip: {status: "absent", findings: []},
214
+ containers: {status: "absent", findings: []},
215
+ actions: {status: "absent", findings: []},
216
+ });
217
+ const emptyUnavailable = depHealthCoverage({
218
+ npm: {status: "unavailable", findings: []},
219
+ pip: {status: "absent", findings: []},
220
+ containers: {status: "absent", findings: []},
221
+ actions: {status: "absent", findings: []},
222
+ });
223
+ console.log(JSON.stringify({
224
+ scannedPass: scanned.pass, scannedGaps: scanned.gaps,
225
+ absentPass: absentNote.pass,
226
+ absentNotes: absentNote.gaps.filter((g) => !g.fail).length,
227
+ missingPass: missing.pass,
228
+ missingFails: missing.gaps.filter((g) => g.fail).map((g) => g.ecosystem),
229
+ unrecordedPass: unrecorded.pass,
230
+ flagPass: unrecordedFlag.pass,
231
+ emptyUnavailablePass: emptyUnavailable.pass,
232
+ }));
233
+ """
234
+ )
235
+ self.assertEqual(res.returncode, 0, f"matrix test failed: {res.stderr}")
236
+ data = last_json_object(res.stdout)
237
+ self.assertTrue(data["scannedPass"], "scanned/clean must pass")
238
+ self.assertEqual(
239
+ [g for g in data["scannedGaps"] if g["fail"]],
240
+ [],
241
+ "scanned/clean must carry no failing gaps",
242
+ )
243
+ self.assertTrue(data["absentPass"], "absent must pass-with-note")
244
+ self.assertGreaterEqual(data["absentNotes"], 1, "absent must leave a note")
245
+ self.assertFalse(data["missingPass"], "missing report must fail")
246
+ self.assertEqual(
247
+ set(data["missingFails"]),
248
+ {"pip", "containers", "actions"},
249
+ "every missing ecosystem must be named",
250
+ )
251
+ self.assertFalse(data["unrecordedPass"], "unrecorded findings must fail")
252
+ self.assertFalse(data["flagPass"], "recorded:false must fail")
253
+ self.assertFalse(
254
+ data["emptyUnavailablePass"],
255
+ "unavailable with zero entries is unrecorded and must fail",
256
+ )
257
+
258
+
259
+ class TestAnnotationKeys(unittest.TestCase):
260
+ def test_every_finding_key_always_present(self):
261
+ res = run_node(
262
+ DEP_IMPORT
263
+ + """
264
+ const bare = toDepFinding({});
265
+ const full = toDepFinding({tool: "edit", eventId: "e-k", ecosystem: "pip",
266
+ package: "requests", installed: "2.0", wanted: "2.1", latest: "2.2",
267
+ severity: "HIGH", advisory: "CVE-x", fixedIn: "2.1", source: "osv.dev"});
268
+ const direct = await checkDeps({
269
+ manifests: {packageJson: JSON.stringify({dependencies: {a: "1.0.0"}})},
270
+ runOutdated: async () => ({status: "ok", json: {}}),
271
+ runAudit: async () => ({status: "ok", json: {}}),
272
+ queryOsv: async () => ({status: "error", message: "down"}),
273
+ osvTimeoutMs: 200,
274
+ });
275
+ console.log(JSON.stringify({
276
+ bareKeys: Object.keys(bare).sort(),
277
+ fullSeverity: full.severity,
278
+ npmKeys: direct.ecosystems.npm.findings.length
279
+ ? Object.keys(direct.ecosystems.npm.findings[0]).sort() : [],
280
+ }));
281
+ """
282
+ )
283
+ self.assertEqual(res.returncode, 0, f"keys test failed: {res.stderr}")
284
+ data = last_json_object(res.stdout)
285
+ required = {
286
+ "tool",
287
+ "eventId",
288
+ "ecosystem",
289
+ "package",
290
+ "installed",
291
+ "wanted",
292
+ "latest",
293
+ "severity",
294
+ "advisory",
295
+ "fixedIn",
296
+ "source",
297
+ }
298
+ self.assertTrue(
299
+ required.issubset(set(data["bareKeys"])),
300
+ f"bare finding missing keys: {required - set(data['bareKeys'])}",
301
+ )
302
+ self.assertEqual(
303
+ data["fullSeverity"], "high", "severity normalises to lowercase"
304
+ )
305
+ # Findings from a real scan path carry the same eleven keys.
306
+ if data["npmKeys"]:
307
+ self.assertTrue(required.issubset(set(data["npmKeys"])))
308
+
309
+
310
+ class TestOsvTimeoutBound(unittest.TestCase):
311
+ def test_hung_feed_resolves_inside_budget(self):
312
+ res = run_node(
313
+ DEP_IMPORT
314
+ + """
315
+ const runOutdated = async () => ({status: "ok", json: {}});
316
+ const runAudit = async () => ({status: "ok", json: {}});
317
+ const queryOsv = () => new Promise(() => {});
318
+ const t0 = Date.now();
319
+ const direct = await checkDeps({
320
+ manifests: {packageJson: JSON.stringify({dependencies: {a: "1.0.0"}})},
321
+ runOutdated, runAudit, queryOsv, osvTimeoutMs: 60,
322
+ });
323
+ const elapsed = Date.now() - t0;
324
+ console.log(JSON.stringify({
325
+ allowed: direct.allowed, elapsed,
326
+ npmStatus: direct.ecosystems.npm.status,
327
+ }));
328
+ """
329
+ )
330
+ self.assertEqual(res.returncode, 0, f"osv-timeout test failed: {res.stderr}")
331
+ data = last_json_object(res.stdout)
332
+ self.assertTrue(data["allowed"])
333
+ self.assertLess(
334
+ data["elapsed"],
335
+ 2000,
336
+ "hung OSV feed must resolve near the slow-tier budget",
337
+ )
338
+ self.assertEqual(
339
+ data["npmStatus"],
340
+ "scanned",
341
+ "local signals decide npm; a hung feed must not flip scanned",
342
+ )
343
+
344
+
345
+ class TestRepoBaseline(unittest.TestCase):
346
+ def test_baseline_shape(self):
347
+ res = run_node(
348
+ DEP_IMPORT
349
+ + MOCK_CLEAN_RUNNERS
350
+ + """
351
+ const direct = await checkDeps({
352
+ root: ".", runOutdated, runAudit, queryOsv, osvTimeoutMs: 200,
353
+ });
354
+ const containers = direct.ecosystems.containers;
355
+ const actions = direct.ecosystems.actions;
356
+ const pkgs = containers.findings.map((f) => f.package || "");
357
+ console.log(JSON.stringify({
358
+ allowed: direct.allowed,
359
+ npm: direct.ecosystems.npm.status,
360
+ pip: direct.ecosystems.pip.status,
361
+ containers: containers.status,
362
+ containerCount: containers.findings.length,
363
+ hasSearxng: pkgs.some((p) => p.includes("searxng")),
364
+ hasFirecrawl: pkgs.some((p) => p.includes("firecrawl")),
365
+ containerSources: [...new Set(containers.findings.map((f) => f.source))],
366
+ actions: actions.status,
367
+ actionCount: actions.findings.length,
368
+ keys: containers.findings.length ? Object.keys(containers.findings[0]).sort() : [],
369
+ }));
370
+ """
371
+ )
372
+ self.assertEqual(res.returncode, 0, f"baseline test failed: {res.stderr}")
373
+ data = last_json_object(res.stdout)
374
+ self.assertTrue(data["allowed"])
375
+ self.assertEqual(data["npm"], "scanned", "repo has package.json: npm scanned")
376
+ self.assertEqual(data["pip"], "absent", "repo has no pip manifests: pip absent")
377
+ self.assertEqual(data["containers"], "scanned")
378
+ self.assertGreaterEqual(
379
+ data["containerCount"], 2, "the 2x :latest advisories (searxng + firecrawl)"
380
+ )
381
+ self.assertTrue(data["hasSearxng"], "searxng :latest advisory recorded")
382
+ self.assertTrue(data["hasFirecrawl"], "firecrawl :latest advisory recorded")
383
+ self.assertTrue(
384
+ data["actions"] == "scanned" and data["actionCount"] == 0,
385
+ "majors-pinned workflows scan clean",
386
+ )
387
+ required = {
388
+ "tool",
389
+ "eventId",
390
+ "ecosystem",
391
+ "package",
392
+ "installed",
393
+ "wanted",
394
+ "latest",
395
+ "severity",
396
+ "advisory",
397
+ "fixedIn",
398
+ "source",
399
+ }
400
+ self.assertTrue(required.issubset(set(data["keys"])))
401
+
402
+
403
+ class TestParsers(unittest.TestCase):
404
+ def test_actions_unpinned_flagged(self):
405
+ res = run_node(
406
+ 'import { parseActionPins, parseContainerImages, isFloatingImage, parsePipDeps } from "./plugins/opencode/dep-health.js";\n'
407
+ + """
408
+ const pins = parseActionPins("steps:\\n - uses: actions/checkout@v4\\n - uses: foo/bar@main\\n - uses: baz/qux\\n");
409
+ const imgs = parseContainerImages("services:\\n a:\\n image: searxng/searxng:latest\\n b:\\n image: redis:7-alpine\\n");
410
+ const pip = parsePipDeps("requests>=2.0\\n# comment\\nflask==3.0; python_version>'3.8'\\n");
411
+ console.log(JSON.stringify({
412
+ pinned: pins.map((p) => p.pinned),
413
+ floating: imgs.map(isFloatingImage),
414
+ pipNames: pip.map((d) => d.name),
415
+ }));
416
+ """
417
+ )
418
+ self.assertEqual(res.returncode, 0, f"parser test failed: {res.stderr}")
419
+ data = last_json_object(res.stdout)
420
+ self.assertEqual(
421
+ data["pinned"],
422
+ [True, False, False],
423
+ "majors pinned; branch/missing refs flagged",
424
+ )
425
+ self.assertEqual(data["floating"], [True, False])
426
+ self.assertEqual(data["pipNames"], ["requests", "flask"])
427
+
428
+
429
+ class TestSetupAdoptsDepHealth(unittest.TestCase):
430
+ def test_setup_adopts_and_disposes_dep_health(self):
431
+ res = run_node(
432
+ """
433
+ import plugin, { hookBus } from "./plugins/opencode/index.js";
434
+ const before = {
435
+ pre: hookBus.handlerCount("pre-tool-use"),
436
+ commit: hookBus.handlerCount("pre-commit"),
437
+ };
438
+ const host = {
439
+ options: {},
440
+ command: {
441
+ list: async () => ({data: []}),
442
+ transform: async (fn) => { fn({add: () => {}}); return {dispose: async () => {}}; }
443
+ },
444
+ tool: {
445
+ transform: async (fn) => { fn({add: () => {}}); return {dispose: async () => {}}; }
446
+ },
447
+ session: { prompt: async () => ({}) }
448
+ };
449
+ const cleanup = await plugin.setup(host);
450
+ const after = {
451
+ pre: hookBus.handlerCount("pre-tool-use"),
452
+ commit: hookBus.handlerCount("pre-commit"),
453
+ };
454
+ const attached = !!host.iumbtemsDepHealth;
455
+ await cleanup();
456
+ const closed = {
457
+ pre: hookBus.handlerCount("pre-tool-use"),
458
+ commit: hookBus.handlerCount("pre-commit"),
459
+ };
460
+ console.log(JSON.stringify({before, after, attached, closed}));
461
+ """
462
+ )
463
+ self.assertEqual(res.returncode, 0, f"adopt test failed: {res.stderr}")
464
+ data = last_json_object(res.stdout)
465
+ self.assertTrue(data["attached"], "dep-health must be adopted on setup")
466
+ self.assertGreater(
467
+ data["after"]["pre"],
468
+ data["before"]["pre"],
469
+ "pre-tool-use dep-health handler subscribed",
470
+ )
471
+ self.assertGreater(
472
+ data["after"]["commit"],
473
+ data["before"]["commit"],
474
+ "pre-commit dep-health handler subscribed",
475
+ )
476
+ self.assertEqual(
477
+ data["closed"],
478
+ data["before"],
479
+ "cleanup must dispose dep-health subscriptions",
480
+ )
481
+
482
+
483
+ if __name__ == "__main__":
484
+ unittest.main()