@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.
- 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 +5 -1
- package/plugins/antigravity/plugin.json +1 -1
- package/plugins/factory/skills/factory/SKILL.md +1 -1
- package/plugins/opencode/analysis-bridge.js +566 -0
- package/plugins/opencode/config-io.js +598 -31
- package/plugins/opencode/dep-health.js +1039 -0
- package/plugins/opencode/hook-bus.js +434 -0
- package/plugins/opencode/index.js +252 -19
- package/plugins/opencode/tui.js +230 -17
- package/plugins/opencode/weight-baseline.json +16 -0
- package/plugins/opencode/weight-signals.js +769 -0
- 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_analysis_bridge.cpython-311.pyc +0 -0
- 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_dep_health.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_hook_bus.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/__pycache__/test_weight_signals.cpython-311.pyc +0 -0
- package/runner/tests/fixtures/config_validation_cases.json +60 -0
- package/runner/tests/test_analysis_bridge.py +356 -0
- package/runner/tests/test_config_schema.py +119 -15
- package/runner/tests/test_dep_health.py +484 -0
- package/runner/tests/test_hardening_contract.py +391 -0
- package/runner/tests/test_hook_bus.py +802 -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/runner/tests/test_weight_signals.py +361 -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
|
@@ -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()
|