@heretek-ai/epistemic-swarm 0.7.21 → 0.7.22
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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/bin/cli.js +3 -2
- package/extensions/pi/index.js +3 -2
- package/package.json +1 -1
- package/plugins/antigravity/plugin.json +1 -1
- package/plugins/factory/skills/factory/SKILL.md +1 -1
- package/plugins/opencode/config-io.js +598 -31
- package/plugins/opencode/index.js +56 -15
- package/plugins/opencode/tui.js +115 -11
- package/plugins/research-cache/skills/research_cache/hasher.py +33 -8
- package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
- package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
- package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
- package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
- package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
- package/runner/__pycache__/darkharvest_claims.cpython-311.pyc +0 -0
- package/runner/__pycache__/errors.cpython-311.pyc +0 -0
- package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
- package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
- package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
- package/runner/__pycache__/path_safety.cpython-311.pyc +0 -0
- package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
- package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
- package/runner/__pycache__/preflight.cpython-311.pyc +0 -0
- package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
- package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
- package/runner/__pycache__/retrieval_log.cpython-311.pyc +0 -0
- package/runner/__pycache__/schema_validate.cpython-311.pyc +0 -0
- package/runner/__pycache__/schemas.cpython-311.pyc +0 -0
- package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
- package/runner/mcp_server.py +249 -81
- package/runner/refinement.py +5 -0
- package/runner/schema_validate.py +63 -1
- package/runner/schemas.py +9 -4
- package/runner/tests/__pycache__/test_antigravity_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_backends.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claude_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_config_schema.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_contract_tooling.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_darkharvest_audit.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_dossier_contract.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_factory.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_frontier_concurrency.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_frontier_mirror_standalone.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_grounding.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_hardening_contract.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_manifest_concurrency.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_opencode_v2_registrars.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_phase08_waiver_reopens.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_anomaly.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_provider.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_provider_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm_config.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
- package/runner/tests/fixtures/config_validation_cases.json +60 -0
- package/runner/tests/test_config_schema.py +119 -15
- package/runner/tests/test_hardening_contract.py +391 -0
- package/runner/tests/test_opencode_ux.py +232 -0
- package/runner/tests/test_opencode_v2_registrars.py +548 -16
- package/runner/tests/test_phase08_waiver_reopens.py +539 -0
- package/schemas/config.schema.json +4 -3
- package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
- package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
- package/scripts/__pycache__/check_docs.cpython-311.pyc +0 -0
- package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
- package/scripts/__pycache__/gen_schemas.cpython-311.pyc +0 -0
- package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
- package/skills/factory/SKILL.md +1 -1
- package/skills/grilling/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/grilling/__pycache__/socratic_tree.cpython-311.pyc +0 -0
- package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
- package/skills/research_cache/hasher.py +33 -8
- package/skills/swarm_config/SKILL.md +6 -1
- package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
- package/skills/swarm_config/configure.py +254 -17
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -134,6 +134,37 @@
|
|
|
134
134
|
"expect": "search_timeout_s",
|
|
135
135
|
"config": {"search_timeout_s": 0}
|
|
136
136
|
},
|
|
137
|
+
{
|
|
138
|
+
"name": "float_integral_divergence_above_maximum",
|
|
139
|
+
"valid": false,
|
|
140
|
+
"expect": "maximum",
|
|
141
|
+
"config": {"divergence_threshold": 2.0}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"name": "float_integral_search_timeout_below_minimum",
|
|
145
|
+
"valid": false,
|
|
146
|
+
"expect": "minimum",
|
|
147
|
+
"config": {"search_timeout_s": 0.0}
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"name": "float_integral_min_fuzzy_above_maximum",
|
|
151
|
+
"valid": false,
|
|
152
|
+
"expect": "min_fuzzy_confidence",
|
|
153
|
+
"config": {"verify": {"min_fuzzy_confidence": 2.0}}
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"name": "float_fractional_cache_ttl_type_error",
|
|
157
|
+
"valid": false,
|
|
158
|
+
"expect": "got float",
|
|
159
|
+
"config": {"cache_ttl_days": -1.5}
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"name": "type_integral_float_in_integer_field",
|
|
163
|
+
"valid": false,
|
|
164
|
+
"expect": "got float",
|
|
165
|
+
"lexeme_sensitive": true,
|
|
166
|
+
"config": {"cache_ttl_days": 2.0}
|
|
167
|
+
},
|
|
137
168
|
{
|
|
138
169
|
"name": "type_search_engine_integer",
|
|
139
170
|
"valid": false,
|
|
@@ -211,6 +242,35 @@
|
|
|
211
242
|
"valid": false,
|
|
212
243
|
"expect": "pattern",
|
|
213
244
|
"config": {"searxng_url": "localhost:8080"}
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"name": "vert_searxng_url_scheme_without_host",
|
|
248
|
+
"valid": false,
|
|
249
|
+
"expect": "pattern",
|
|
250
|
+
"config": {"searxng_url": "http://"}
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"name": "vert_searxng_url_scheme_without_host_slash",
|
|
254
|
+
"valid": false,
|
|
255
|
+
"expect": "pattern",
|
|
256
|
+
"config": {"searxng_url": "https:///path-only"}
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"name": "vert_searxng_url_host_with_port_and_path",
|
|
260
|
+
"valid": true,
|
|
261
|
+
"config": {"searxng_url": "https://searx.local:8888/search"}
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"name": "vert_domain_pack_whitespace_only",
|
|
265
|
+
"valid": false,
|
|
266
|
+
"expect": "pattern",
|
|
267
|
+
"config": {"domain_pack": " "}
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"name": "vert_domain_pack_padded",
|
|
271
|
+
"valid": false,
|
|
272
|
+
"expect": "pattern",
|
|
273
|
+
"config": {"domain_pack": " biopharma "}
|
|
214
274
|
}
|
|
215
275
|
]
|
|
216
276
|
}
|
|
@@ -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
|
-
|
|
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,391 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Phase 05 (hardening-contract, H2-H6) acceptance tests: C1-C5.
|
|
3
|
+
|
|
4
|
+
Each class maps to one acceptance criterion in
|
|
5
|
+
`.roadmap/05-hardening-contract/GOAL.md`. Default-absent behavior is pinned
|
|
6
|
+
byte-identical throughout (absent keys, None guards, and clean configs behave
|
|
7
|
+
exactly as before).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import tempfile
|
|
15
|
+
import unittest
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _write_config(base: Path, payload: dict) -> Path:
|
|
22
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
cfg_file = base / "config.json"
|
|
24
|
+
cfg_file.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
25
|
+
return cfg_file
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _handle_config(args):
|
|
29
|
+
from runner.mcp_server import _handle_config as h
|
|
30
|
+
|
|
31
|
+
return json.loads(h(args))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestC1ExpectedHashNormalization(unittest.TestCase):
|
|
35
|
+
"""H2: normalized conflict comparison, 8-hex minimum, show+hash decision."""
|
|
36
|
+
|
|
37
|
+
def test_case_and_whitespace_only_difference_is_one_guard(self):
|
|
38
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
39
|
+
base = Path(tmp)
|
|
40
|
+
_write_config(base, {"mode": "research"})
|
|
41
|
+
live = hashlib.sha256((base / "config.json").read_bytes()).hexdigest()
|
|
42
|
+
payload = _handle_config(
|
|
43
|
+
{
|
|
44
|
+
"mode": "audit",
|
|
45
|
+
"base_dir": str(base),
|
|
46
|
+
"expected_hash": " " + live.upper() + " ",
|
|
47
|
+
"expectedHash": live,
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
self.assertEqual(payload["status"], "updated", payload)
|
|
51
|
+
self.assertTrue(payload["written_to"])
|
|
52
|
+
|
|
53
|
+
def test_genuinely_different_spellings_still_conflict(self):
|
|
54
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
55
|
+
base = Path(tmp)
|
|
56
|
+
cfg_file = _write_config(base, {"mode": "research"})
|
|
57
|
+
before = cfg_file.read_bytes()
|
|
58
|
+
payload = _handle_config(
|
|
59
|
+
{
|
|
60
|
+
"mode": "audit",
|
|
61
|
+
"base_dir": str(base),
|
|
62
|
+
"expected_hash": "a" * 64,
|
|
63
|
+
"expectedHash": "b" * 64,
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
self.assertEqual(payload["code"], "CONFLICTING_EXPECTED_HASH")
|
|
67
|
+
self.assertFalse(payload["written"])
|
|
68
|
+
self.assertEqual(cfg_file.read_bytes(), before)
|
|
69
|
+
|
|
70
|
+
def test_malformed_but_equal_spellings_surface_hash_error(self):
|
|
71
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
72
|
+
base = Path(tmp)
|
|
73
|
+
cfg_file = _write_config(base, {"mode": "research"})
|
|
74
|
+
before = cfg_file.read_bytes()
|
|
75
|
+
payload = _handle_config(
|
|
76
|
+
{
|
|
77
|
+
"mode": "audit",
|
|
78
|
+
"base_dir": str(base),
|
|
79
|
+
"expected_hash": "zzzzzzzz",
|
|
80
|
+
"expectedHash": "ZZZZZZZZ",
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
self.assertEqual(payload["code"], "CONFIG_INVALID_EXPECTED_HASH")
|
|
84
|
+
self.assertFalse(payload["written"])
|
|
85
|
+
self.assertEqual(cfg_file.read_bytes(), before)
|
|
86
|
+
|
|
87
|
+
def test_short_guard_rejected_through_mcp(self):
|
|
88
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
89
|
+
base = Path(tmp)
|
|
90
|
+
_write_config(base, {"mode": "research"})
|
|
91
|
+
payload = _handle_config(
|
|
92
|
+
{"mode": "audit", "base_dir": str(base), "expected_hash": "abcd"}
|
|
93
|
+
)
|
|
94
|
+
self.assertEqual(payload["code"], "CONFIG_INVALID_EXPECTED_HASH")
|
|
95
|
+
self.assertFalse(payload["written"])
|
|
96
|
+
|
|
97
|
+
def test_show_wins_over_updates_and_ignores_the_guard(self):
|
|
98
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
99
|
+
base = Path(tmp)
|
|
100
|
+
cfg_file = _write_config(base, {"mode": "research"})
|
|
101
|
+
before = cfg_file.read_bytes()
|
|
102
|
+
payload = _handle_config(
|
|
103
|
+
{
|
|
104
|
+
"show": True,
|
|
105
|
+
"mode": "audit",
|
|
106
|
+
"base_dir": str(base),
|
|
107
|
+
"expected_hash": "deadbeef00", # bogus: must NOT go stale
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
self.assertEqual(payload["status"], "current", payload)
|
|
111
|
+
self.assertEqual(payload["config"]["mode"], "research")
|
|
112
|
+
self.assertEqual(payload["ignored_updates"], ["mode"])
|
|
113
|
+
self.assertEqual(cfg_file.read_bytes(), before)
|
|
114
|
+
|
|
115
|
+
def test_show_without_updates_is_unchanged(self):
|
|
116
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
117
|
+
base = Path(tmp)
|
|
118
|
+
_write_config(base, {"mode": "research"})
|
|
119
|
+
payload = _handle_config({"show": True, "base_dir": str(base)})
|
|
120
|
+
self.assertEqual(payload["status"], "current")
|
|
121
|
+
self.assertNotIn("ignored_updates", payload)
|
|
122
|
+
|
|
123
|
+
def test_js_resolve_expected_hash_normalizes(self):
|
|
124
|
+
"""config-io.js mirrors the normalized conflict rule (Phase-05 H2)."""
|
|
125
|
+
res = subprocess.run(
|
|
126
|
+
[
|
|
127
|
+
"node",
|
|
128
|
+
"--input-type=module",
|
|
129
|
+
"-e",
|
|
130
|
+
"""
|
|
131
|
+
import { resolveExpectedHash } from "./plugins/opencode/config-io.js";
|
|
132
|
+
const out = [];
|
|
133
|
+
out.push(resolveExpectedHash({ expected_hash: "ABCDEF12", expectedHash: "abcdef12" }));
|
|
134
|
+
let conflict = null;
|
|
135
|
+
try {
|
|
136
|
+
resolveExpectedHash({ expected_hash: "a".repeat(8), expectedHash: "b".repeat(8) });
|
|
137
|
+
} catch (e) { conflict = e.code; }
|
|
138
|
+
let malformed = null;
|
|
139
|
+
try {
|
|
140
|
+
resolveExpectedHash({ expected_hash: "zzzzzzzz", expectedHash: "ZZZZZZZZ" });
|
|
141
|
+
} catch (e) { malformed = e.code; }
|
|
142
|
+
console.log(JSON.stringify({ same: out[0], conflict, malformed }));
|
|
143
|
+
""",
|
|
144
|
+
],
|
|
145
|
+
capture_output=True,
|
|
146
|
+
text=True,
|
|
147
|
+
cwd=str(PROJECT_ROOT),
|
|
148
|
+
)
|
|
149
|
+
self.assertEqual(res.returncode, 0, res.stderr)
|
|
150
|
+
data = json.loads(res.stdout.strip().splitlines()[-1])
|
|
151
|
+
self.assertEqual(data["same"], "abcdef12")
|
|
152
|
+
self.assertEqual(data["conflict"], "CONFLICTING_EXPECTED_HASH")
|
|
153
|
+
self.assertEqual(data["malformed"], "CONFIG_INVALID_EXPECTED_HASH")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TestC2HostAndPackValidation(unittest.TestCase):
|
|
157
|
+
"""H3: searxng host presence + domain_pack trim/reject."""
|
|
158
|
+
|
|
159
|
+
def test_scheme_without_host_rejected(self):
|
|
160
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
161
|
+
base = Path(tmp)
|
|
162
|
+
cfg_file = _write_config(base, {"mode": "research"})
|
|
163
|
+
before = cfg_file.read_bytes()
|
|
164
|
+
for bad in ("http://", "https://", "https:///path-only", "http:// "):
|
|
165
|
+
with self.subTest(bad=bad):
|
|
166
|
+
payload = _handle_config(
|
|
167
|
+
{"searxng_url": bad, "base_dir": str(base)}
|
|
168
|
+
)
|
|
169
|
+
self.assertEqual(payload["code"], "CONFIG_VALIDATION_FAILED")
|
|
170
|
+
self.assertFalse(payload["written"])
|
|
171
|
+
self.assertEqual(cfg_file.read_bytes(), before)
|
|
172
|
+
|
|
173
|
+
def test_host_with_port_and_path_accepted(self):
|
|
174
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
175
|
+
base = Path(tmp)
|
|
176
|
+
payload = _handle_config(
|
|
177
|
+
{
|
|
178
|
+
"searxng_url": "https://searx.local:8888/search",
|
|
179
|
+
"base_dir": str(base),
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
self.assertEqual(payload["status"], "updated", payload)
|
|
183
|
+
|
|
184
|
+
def test_null_searxng_url_still_means_untouched(self):
|
|
185
|
+
"""Default-absent pin: explicit null clears, absent leaves unchanged."""
|
|
186
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
187
|
+
base = Path(tmp)
|
|
188
|
+
payload = _handle_config({"searxng_url": None, "base_dir": str(base)})
|
|
189
|
+
self.assertEqual(payload["status"], "updated", payload)
|
|
190
|
+
self.assertIsNone(payload["config"]["searxng_url"])
|
|
191
|
+
|
|
192
|
+
def test_blank_and_padded_pack_rejected_at_config_surface(self):
|
|
193
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
194
|
+
base = Path(tmp)
|
|
195
|
+
cfg_file = _write_config(base, {"mode": "research"})
|
|
196
|
+
before = cfg_file.read_bytes()
|
|
197
|
+
for bad in ("", " ", " biopharma", "biopharma ", " biopharma "):
|
|
198
|
+
with self.subTest(bad=bad):
|
|
199
|
+
payload = _handle_config(
|
|
200
|
+
{"domain_pack": bad, "base_dir": str(base)}
|
|
201
|
+
)
|
|
202
|
+
self.assertEqual(
|
|
203
|
+
payload["code"], "CONFIG_VALIDATION_FAILED", payload
|
|
204
|
+
)
|
|
205
|
+
self.assertFalse(payload["written"])
|
|
206
|
+
self.assertEqual(cfg_file.read_bytes(), before)
|
|
207
|
+
|
|
208
|
+
def test_set_domain_pack_strips_and_activates(self):
|
|
209
|
+
from runner.mcp_server import _handle_set_domain_pack
|
|
210
|
+
|
|
211
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
212
|
+
base = Path(tmp)
|
|
213
|
+
payload = json.loads(
|
|
214
|
+
_handle_set_domain_pack(
|
|
215
|
+
{"pack": " biopharma ", "base_dir": str(base)}
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
self.assertEqual(payload["status"], "activated")
|
|
219
|
+
self.assertEqual(payload["domain_pack"], "biopharma")
|
|
220
|
+
data = json.loads((base / "config.json").read_text(encoding="utf-8"))
|
|
221
|
+
self.assertEqual(data["domain_pack"], "biopharma")
|
|
222
|
+
|
|
223
|
+
def test_set_domain_pack_rejects_blank(self):
|
|
224
|
+
from runner.mcp_server import _handle_set_domain_pack
|
|
225
|
+
|
|
226
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
227
|
+
with self.assertRaises(ValueError):
|
|
228
|
+
_handle_set_domain_pack({"pack": " ", "base_dir": str(tmp)})
|
|
229
|
+
|
|
230
|
+
def test_load_domain_pack_strips(self):
|
|
231
|
+
from runner.refinement import load_domain_pack
|
|
232
|
+
|
|
233
|
+
self.assertEqual(
|
|
234
|
+
load_domain_pack(" biopharma ").accept_threshold,
|
|
235
|
+
load_domain_pack("biopharma").accept_threshold,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class TestC3NullCodeUnification(unittest.TestCase):
|
|
240
|
+
"""H4: any caller-supplied null where null is banned -> one code."""
|
|
241
|
+
|
|
242
|
+
def test_nested_null_reports_null_code_with_path(self):
|
|
243
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
244
|
+
base = Path(tmp)
|
|
245
|
+
cfg_file = _write_config(base, {"mode": "research"})
|
|
246
|
+
before = cfg_file.read_bytes()
|
|
247
|
+
payload = _handle_config(
|
|
248
|
+
{
|
|
249
|
+
"verify": {"min_fuzzy_confidence": None},
|
|
250
|
+
"base_dir": str(base),
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
self.assertEqual(payload["code"], "NULL_FOR_NON_NULLABLE_KEY")
|
|
254
|
+
self.assertFalse(payload["written"])
|
|
255
|
+
self.assertTrue(
|
|
256
|
+
any("verify.min_fuzzy_confidence" in e for e in payload["errors"]),
|
|
257
|
+
payload,
|
|
258
|
+
)
|
|
259
|
+
self.assertEqual(cfg_file.read_bytes(), before)
|
|
260
|
+
|
|
261
|
+
def test_top_level_null_still_reports_null_code(self):
|
|
262
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
263
|
+
base = Path(tmp)
|
|
264
|
+
payload = _handle_config({"mode": None, "base_dir": str(base)})
|
|
265
|
+
self.assertEqual(payload["code"], "NULL_FOR_NON_NULLABLE_KEY")
|
|
266
|
+
self.assertFalse(payload["written"])
|
|
267
|
+
|
|
268
|
+
def test_nullable_nested_null_still_clears(self):
|
|
269
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
270
|
+
base = Path(tmp)
|
|
271
|
+
payload = _handle_config(
|
|
272
|
+
{"agents": {"alpha": {"backend": None}}, "base_dir": str(base)}
|
|
273
|
+
)
|
|
274
|
+
self.assertEqual(payload["status"], "updated", payload)
|
|
275
|
+
|
|
276
|
+
def test_map_value_null_reports_null_code_with_path(self):
|
|
277
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
278
|
+
base = Path(tmp)
|
|
279
|
+
payload = _handle_config(
|
|
280
|
+
{"mcp_servers": {"searxng": None}, "base_dir": str(base)}
|
|
281
|
+
)
|
|
282
|
+
self.assertEqual(payload["code"], "NULL_FOR_NON_NULLABLE_KEY")
|
|
283
|
+
self.assertTrue(
|
|
284
|
+
any("mcp_servers.searxng" in e for e in payload["errors"]), payload
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def test_non_null_violation_keeps_validation_code(self):
|
|
288
|
+
"""The null code must not swallow ordinary schema violations."""
|
|
289
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
290
|
+
base = Path(tmp)
|
|
291
|
+
payload = _handle_config(
|
|
292
|
+
{"verify": {"min_fuzzy_confidence": 2.0}, "base_dir": str(base)}
|
|
293
|
+
)
|
|
294
|
+
self.assertEqual(payload["code"], "CONFIG_VALIDATION_FAILED")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class TestC4SingleCoercionContract(unittest.TestCase):
|
|
298
|
+
"""H5: hasher owns coercion; MCP forwards raw and echoes effective."""
|
|
299
|
+
|
|
300
|
+
def test_coerce_helper_is_the_single_rule(self):
|
|
301
|
+
from skills.research_cache.hasher import coerce_min_fuzzy_confidence
|
|
302
|
+
|
|
303
|
+
self.assertEqual(coerce_min_fuzzy_confidence("0.95"), 0.95)
|
|
304
|
+
self.assertEqual(coerce_min_fuzzy_confidence(0.95), 0.95)
|
|
305
|
+
self.assertEqual(coerce_min_fuzzy_confidence("high"), 0.88)
|
|
306
|
+
self.assertEqual(coerce_min_fuzzy_confidence(None), 0.88)
|
|
307
|
+
self.assertEqual(coerce_min_fuzzy_confidence(1.5), 1.0)
|
|
308
|
+
self.assertEqual(coerce_min_fuzzy_confidence(-5), 0.0)
|
|
309
|
+
|
|
310
|
+
def test_string_config_forwards_to_hasher_and_echoes_effective(self):
|
|
311
|
+
from runner.mcp_server import _handle_verify_quote
|
|
312
|
+
from skills.research_cache.hasher import SourceHasher
|
|
313
|
+
|
|
314
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
315
|
+
base = Path(tmp)
|
|
316
|
+
hasher = SourceHasher(base)
|
|
317
|
+
words = [f"w{i}" for i in range(1, 21)]
|
|
318
|
+
digest = hasher.store_source(
|
|
319
|
+
url="https://example.test/s", content=" ".join(words)
|
|
320
|
+
)
|
|
321
|
+
quote = " ".join(words[:9]) + " nomatch"
|
|
322
|
+
# Hand-edited string threshold (schema-strict saves can never
|
|
323
|
+
# produce this; the coercion contract covers it anyway).
|
|
324
|
+
_write_config(base, {"verify": {"min_fuzzy_confidence": "0.95"}})
|
|
325
|
+
payload = json.loads(
|
|
326
|
+
_handle_verify_quote(
|
|
327
|
+
{"hash": digest, "quote": quote, "base_dir": str(base)}
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
expected = hasher.verify_quote(digest, quote, min_fuzzy_confidence="0.95")
|
|
331
|
+
self.assertEqual(payload["verified"], expected[0])
|
|
332
|
+
self.assertEqual(payload["min_fuzzy_confidence"], 0.95)
|
|
333
|
+
|
|
334
|
+
def test_malformed_config_falls_back_and_echoes_default(self):
|
|
335
|
+
from runner.mcp_server import _handle_verify_quote
|
|
336
|
+
from skills.research_cache.hasher import SourceHasher
|
|
337
|
+
|
|
338
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
339
|
+
base = Path(tmp)
|
|
340
|
+
hasher = SourceHasher(base)
|
|
341
|
+
digest = hasher.store_source(
|
|
342
|
+
url="https://example.test/m", content="exact words here today"
|
|
343
|
+
)
|
|
344
|
+
_write_config(base, {"verify": {"min_fuzzy_confidence": "high"}})
|
|
345
|
+
payload = json.loads(
|
|
346
|
+
_handle_verify_quote(
|
|
347
|
+
{
|
|
348
|
+
"hash": digest,
|
|
349
|
+
"quote": "exact words here today",
|
|
350
|
+
"base_dir": str(base),
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
self.assertTrue(payload["verified"])
|
|
355
|
+
self.assertEqual(payload["min_fuzzy_confidence"], 0.88)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class TestC5WrittenToAbsolute(unittest.TestCase):
|
|
359
|
+
"""H6: absolute `written_to` on both updated legs."""
|
|
360
|
+
|
|
361
|
+
def test_write_leg_reports_absolute_written_to(self):
|
|
362
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
363
|
+
base = Path(tmp)
|
|
364
|
+
payload = _handle_config({"mode": "audit", "base_dir": str(base)})
|
|
365
|
+
self.assertEqual(payload["status"], "updated")
|
|
366
|
+
self.assertTrue(os.path.isabs(payload["written_to"]), payload)
|
|
367
|
+
self.assertTrue(Path(payload["written_to"]).is_file())
|
|
368
|
+
self.assertEqual(
|
|
369
|
+
payload["hash"],
|
|
370
|
+
hashlib.sha256(Path(payload["written_to"]).read_bytes()).hexdigest(),
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def test_defensive_leg_reports_absolute_written_to(self):
|
|
374
|
+
from runner.mcp_server import _config_updated_payload
|
|
375
|
+
from skills.swarm_config.configure import DEFAULT_CONFIG
|
|
376
|
+
|
|
377
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
378
|
+
missing = Path(tmp) / "no-such-dir" / "config.json"
|
|
379
|
+
payload = _config_updated_payload(
|
|
380
|
+
missing, {"mode": "audit"}, DEFAULT_CONFIG
|
|
381
|
+
)
|
|
382
|
+
self.assertEqual(payload["status"], "updated")
|
|
383
|
+
self.assertIsNone(payload["hash"])
|
|
384
|
+
self.assertTrue(os.path.isabs(payload["written_to"]), payload)
|
|
385
|
+
self.assertEqual(
|
|
386
|
+
payload["written_to"], str(Path(os.path.realpath(missing)))
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
if __name__ == "__main__":
|
|
391
|
+
unittest.main()
|