@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
@@ -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()