@heretek-ai/epistemic-swarm 0.7.25 → 1.0.2
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/index.ts +1 -0
- package/package.json +63 -139
- package/rpc.ts +1 -0
- package/server.ts +1 -0
- package/src/agents.ts +69 -0
- package/src/continue.ts +40 -0
- package/src/hooks.ts +283 -0
- package/src/panels.tsx +220 -0
- package/src/policy.ts +221 -0
- package/src/pr.ts +37 -0
- package/src/research.ts +63 -0
- package/src/rpc-def.ts +136 -0
- package/src/rpc.ts +331 -0
- package/src/runtime.ts +112 -0
- package/src/server.ts +311 -0
- package/src/session.ts +61 -0
- package/src/tools.ts +61 -0
- package/src/tui.tsx +206 -0
- package/tui.tsx +1 -0
- package/.agents/skills/brainstorming/SKILL.md +0 -19
- package/.agents/skills/code_audit/SKILL.md +0 -19
- package/.agents/skills/darkharvest/SKILL.md +0 -19
- package/.agents/skills/epistemic_search/SKILL.md +0 -19
- package/.agents/skills/factory/SKILL.md +0 -19
- package/.agents/skills/grilling/SKILL.md +0 -19
- package/.agents/skills/oss_scout/SKILL.md +0 -19
- package/.agents/skills/research_cache/SKILL.md +0 -19
- package/.agents/skills/swarm_config/SKILL.md +0 -19
- package/.claude-plugin/marketplace.json +0 -61
- package/.claude-plugin/plugin.json +0 -76
- package/.omp/README.md +0 -39
- package/.omp/SYSTEM.md +0 -17
- package/.omp/commands/audit.md +0 -10
- package/.omp/commands/brainstorming.md +0 -12
- package/.omp/commands/darkharvest.md +0 -11
- package/.omp/commands/domainexpansion.md +0 -9
- package/.omp/commands/factory.md +0 -9
- package/.omp/commands/grill.md +0 -9
- package/.omp/commands/scout.md +0 -10
- package/.omp/commands/swarm-config.md +0 -10
- package/.omp/commands/swarm.md +0 -9
- package/.omp/hooks/post/epistemic-audit.ts +0 -16
- package/.omp/hooks/pre/epistemic-redirect.ts +0 -21
- package/.omp/prompts/brainstorming.md +0 -8
- package/.omp/prompts/swarm.md +0 -8
- package/LICENSE +0 -126
- package/MARKETPLACE.md +0 -130
- package/README.md +0 -403
- package/bin/cli.js +0 -297
- package/config/claude-settings-patch.json +0 -10
- package/config/docker-compose.infra.yml +0 -23
- package/config/domain_packs/biopharma.json +0 -23
- package/config/domain_packs/legal.json +0 -19
- package/config/domain_packs/quant.json +0 -19
- package/config/mcp-research-servers.json +0 -33
- package/config/mcp_launcher.py +0 -153
- package/config/opencode-snippet.json +0 -156
- package/config/searxng_mcp.py +0 -92
- package/extensions/pi/index.js +0 -357
- package/hooks/hooks.json +0 -45
- package/install.sh +0 -473
- package/plugins/antigravity/README.md +0 -72
- package/plugins/antigravity/agents/alpha-thesis.md +0 -36
- package/plugins/antigravity/agents/beta-antithesis.md +0 -35
- package/plugins/antigravity/agents/brainstormer.md +0 -27
- package/plugins/antigravity/agents/code-auditor.md +0 -28
- package/plugins/antigravity/agents/darkharvester.md +0 -30
- package/plugins/antigravity/agents/epistemic-auditor.md +0 -28
- package/plugins/antigravity/agents/manager.md +0 -26
- package/plugins/antigravity/agents/oss-scout.md +0 -27
- package/plugins/antigravity/agents/programmer.md +0 -25
- package/plugins/antigravity/agents/qa-a.md +0 -22
- package/plugins/antigravity/agents/qa-b.md +0 -22
- package/plugins/antigravity/antigravity_hooks.py +0 -265
- package/plugins/antigravity/commands/audit.md +0 -11
- package/plugins/antigravity/commands/brainstorm.md +0 -9
- package/plugins/antigravity/commands/brainstorming.md +0 -10
- package/plugins/antigravity/commands/darkharvest.md +0 -12
- package/plugins/antigravity/commands/domainexpansion.md +0 -12
- package/plugins/antigravity/commands/factory.md +0 -14
- package/plugins/antigravity/commands/grill.md +0 -10
- package/plugins/antigravity/commands/scout.md +0 -11
- package/plugins/antigravity/commands/swarm-config.md +0 -9
- package/plugins/antigravity/commands/swarm.md +0 -12
- package/plugins/antigravity/hook_bus_bridge.js +0 -352
- package/plugins/antigravity/hooks.json +0 -44
- package/plugins/antigravity/mcp_config.json +0 -33
- package/plugins/antigravity/mcp_launcher.py +0 -58
- package/plugins/antigravity/plugin.json +0 -13
- package/plugins/antigravity/rules/AGENTS.md +0 -127
- package/plugins/antigravity/rules/epistemic-integrity.md +0 -16
- package/plugins/antigravity/skills/brainstorming/SKILL.md +0 -19
- package/plugins/antigravity/skills/code_audit/SKILL.md +0 -19
- package/plugins/antigravity/skills/darkharvest/SKILL.md +0 -19
- package/plugins/antigravity/skills/epistemic_search/SKILL.md +0 -19
- package/plugins/antigravity/skills/factory/SKILL.md +0 -19
- package/plugins/antigravity/skills/grilling/SKILL.md +0 -19
- package/plugins/antigravity/skills/oss_scout/SKILL.md +0 -19
- package/plugins/antigravity/skills/research_cache/SKILL.md +0 -19
- package/plugins/antigravity/skills/swarm_config/SKILL.md +0 -19
- package/plugins/codex/AGENTS.md.snippet +0 -10
- package/plugins/codex/README.md +0 -37
- package/plugins/codex/config.toml.snippet +0 -28
- package/plugins/codex/openai.yaml +0 -24
- package/plugins/codex/skills/brainstorming/SKILL.md +0 -19
- package/plugins/codex/skills/code_audit/SKILL.md +0 -19
- package/plugins/codex/skills/darkharvest/SKILL.md +0 -19
- package/plugins/codex/skills/epistemic_search/SKILL.md +0 -19
- package/plugins/codex/skills/factory/SKILL.md +0 -19
- package/plugins/codex/skills/grilling/SKILL.md +0 -19
- package/plugins/codex/skills/oss_scout/SKILL.md +0 -19
- package/plugins/codex/skills/research_cache/SKILL.md +0 -19
- package/plugins/codex/skills/swarm_config/SKILL.md +0 -19
- package/plugins/darkharvest/.claude-plugin/plugin.json +0 -15
- package/plugins/darkharvest/agents/harvest-proponent.md +0 -22
- package/plugins/darkharvest/agents/harvest-redteam.md +0 -20
- package/plugins/darkharvest/evals/teardown-verdict/graders/license-line.md +0 -6
- package/plugins/darkharvest/evals/teardown-verdict/graders/skill-fired.md +0 -5
- package/plugins/darkharvest/evals/teardown-verdict/prompt.md +0 -6
- package/plugins/darkharvest/skills/darkharvest/SKILL.md +0 -70
- package/plugins/darkharvest/skills/darkharvest/scripts/harvest.py +0 -289
- package/plugins/factory/.claude-plugin/plugin.json +0 -15
- package/plugins/factory/agents/factory-manager.md +0 -22
- package/plugins/factory/agents/programmer.md +0 -16
- package/plugins/factory/agents/qa-adversarial.md +0 -17
- package/plugins/factory/agents/qa-functional.md +0 -17
- package/plugins/factory/evals/gate-halt/graders/gates-first.md +0 -6
- package/plugins/factory/evals/gate-halt/graders/skill-fired.md +0 -5
- package/plugins/factory/evals/gate-halt/prompt.md +0 -6
- package/plugins/factory/skills/factory/SKILL.md +0 -61
- package/plugins/factory/skills/factory/scripts/factory.py +0 -300
- package/plugins/gemini/GEMINI.md +0 -16
- package/plugins/gemini/README.md +0 -20
- package/plugins/gemini/commands/audit.toml +0 -6
- package/plugins/gemini/commands/brainstorming.toml +0 -10
- package/plugins/gemini/commands/darkharvest.toml +0 -8
- package/plugins/gemini/commands/domainexpansion.toml +0 -7
- package/plugins/gemini/commands/factory.toml +0 -8
- package/plugins/gemini/commands/grill.toml +0 -6
- package/plugins/gemini/commands/scout.toml +0 -7
- package/plugins/gemini/commands/swarm-config.toml +0 -7
- package/plugins/gemini/commands/swarm.toml +0 -8
- package/plugins/gemini/gemini-extension.json +0 -38
- package/plugins/gemini/hooks/hooks.json +0 -11
- package/plugins/gemini/skills/brainstorming/SKILL.md +0 -19
- package/plugins/gemini/skills/code_audit/SKILL.md +0 -19
- package/plugins/gemini/skills/darkharvest/SKILL.md +0 -19
- package/plugins/gemini/skills/epistemic_search/SKILL.md +0 -19
- package/plugins/gemini/skills/factory/SKILL.md +0 -19
- package/plugins/gemini/skills/grilling/SKILL.md +0 -19
- package/plugins/gemini/skills/oss_scout/SKILL.md +0 -19
- package/plugins/gemini/skills/research_cache/SKILL.md +0 -19
- package/plugins/gemini/skills/swarm_config/SKILL.md +0 -19
- package/plugins/opencode/analysis-bridge.js +0 -620
- package/plugins/opencode/config-io.js +0 -1651
- package/plugins/opencode/dep-health.js +0 -1039
- package/plugins/opencode/hook-bus.js +0 -496
- package/plugins/opencode/index.js +0 -2992
- package/plugins/opencode/tui.js +0 -1551
- package/plugins/opencode/weight-baseline.json +0 -16
- package/plugins/opencode/weight-signals.js +0 -769
- package/plugins/research-cache/.claude-plugin/plugin.json +0 -15
- package/plugins/research-cache/skills/research_cache/SKILL.md +0 -36
- package/plugins/research-cache/skills/research_cache/__init__.py +0 -0
- package/plugins/research-cache/skills/research_cache/hasher.py +0 -302
- package/plugins/socratic-grilling/.claude-plugin/plugin.json +0 -15
- package/plugins/socratic-grilling/skills/grilling/SKILL.md +0 -61
- package/plugins/socratic-grilling/skills/grilling/__init__.py +0 -0
- package/plugins/socratic-grilling/skills/grilling/socratic_tree.py +0 -363
- package/prompts/agent_alpha_thesis.md +0 -70
- package/prompts/agent_beta_antithesis.md +0 -78
- package/prompts/agent_brainstormer.md +0 -143
- package/prompts/agent_code_auditor.md +0 -88
- package/prompts/agent_darkharvest.md +0 -140
- package/prompts/agent_oss_scout.md +0 -78
- package/prompts/antigravity_repo_factcheck.md +0 -143
- package/prompts/base_epistemic_system.md +0 -50
- package/prompts/epistemic_auditor.md +0 -74
- package/prompts/orchestrator.md +0 -70
- package/runner/__init__.py +0 -0
- 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/antigravity_hooks.py +0 -265
- package/runner/auctioneer.py +0 -169
- package/runner/auditor_engine.py +0 -711
- package/runner/claim_store.py +0 -417
- package/runner/claim_witness.py +0 -412
- package/runner/darkharvest_claims.py +0 -358
- package/runner/errors.py +0 -51
- package/runner/living_dossiers.py +0 -395
- package/runner/mcp_protocol.py +0 -188
- package/runner/mcp_server.py +0 -1447
- package/runner/path_safety.py +0 -62
- package/runner/pcrb.py +0 -290
- package/runner/pcrb_verify.py +0 -267
- package/runner/preflight.py +0 -1011
- package/runner/refinement.py +0 -580
- package/runner/research_swarm.py +0 -1848
- package/runner/retrieval_log.py +0 -90
- package/runner/schema_validate.py +0 -193
- package/runner/schemas.py +0 -630
- package/runner/state_machine.py +0 -463
- 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_nk_hardening.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/auction_objective.json +0 -35
- package/runner/tests/fixtures/borderline_claims.json +0 -27
- package/runner/tests/fixtures/config_validation_cases.json +0 -276
- package/runner/tests/fixtures/ddg_anomaly_page.html +0 -176
- package/runner/tests/fixtures/ddg_results_page.html +0 -28
- package/runner/tests/fixtures/divergence_objectives.json +0 -31
- package/runner/tests/test_analysis_bridge.py +0 -450
- package/runner/tests/test_antigravity_plugin.py +0 -361
- package/runner/tests/test_auction_order.py +0 -173
- package/runner/tests/test_backends.py +0 -457
- package/runner/tests/test_bet1_spike.py +0 -95
- package/runner/tests/test_claim_store.py +0 -162
- package/runner/tests/test_claim_witness.py +0 -186
- package/runner/tests/test_claude_plugin.py +0 -170
- package/runner/tests/test_config_schema.py +0 -879
- package/runner/tests/test_contract_tooling.py +0 -312
- package/runner/tests/test_darkharvest_audit.py +0 -395
- package/runner/tests/test_dep_health.py +0 -595
- package/runner/tests/test_domain_packs.py +0 -217
- package/runner/tests/test_dossier_contract.py +0 -196
- package/runner/tests/test_factory.py +0 -258
- package/runner/tests/test_fleet_seam.py +0 -113
- package/runner/tests/test_frontier_concurrency.py +0 -288
- package/runner/tests/test_frontier_mirror_standalone.py +0 -156
- package/runner/tests/test_grounding.py +0 -183
- package/runner/tests/test_hardening_contract.py +0 -391
- package/runner/tests/test_hook_bus.py +0 -956
- package/runner/tests/test_living_dossiers.py +0 -204
- package/runner/tests/test_manifest_concurrency.py +0 -187
- package/runner/tests/test_mcp_server.py +0 -257
- package/runner/tests/test_nk_hardening.py +0 -1046
- package/runner/tests/test_opencode_ux.py +0 -2438
- package/runner/tests/test_opencode_v2_registrars.py +0 -2157
- package/runner/tests/test_pcrb.py +0 -240
- package/runner/tests/test_phase08_waiver_reopens.py +0 -539
- package/runner/tests/test_refinement.py +0 -255
- package/runner/tests/test_search_anomaly.py +0 -342
- package/runner/tests/test_search_provider.py +0 -1534
- package/runner/tests/test_search_provider_plugin.py +0 -401
- package/runner/tests/test_swarm.py +0 -444
- package/runner/tests/test_swarm_config.py +0 -520
- package/runner/tests/test_sweep_regressions.py +0 -453
- package/runner/tests/test_webcache.py +0 -271
- package/runner/tests/test_weight_signals.py +0 -401
- package/schemas/alpha_dossier.schema.json +0 -204
- package/schemas/audit_report.schema.json +0 -79
- package/schemas/beta_dossier.schema.json +0 -214
- package/schemas/config.schema.json +0 -311
- package/schemas/frontier.schema.json +0 -61
- package/schemas/manifest.schema.json +0 -49
- package/schemas/scope_manifest.schema.json +0 -47
- 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/scripts/auction_experiment.py +0 -188
- package/scripts/bet1_advisory_spike.py +0 -442
- package/scripts/build_adapters.py +0 -417
- package/scripts/check_docs.py +0 -66
- package/scripts/divergence_experiment.py +0 -214
- package/scripts/gen_schemas.py +0 -69
- package/skills/brainstorming/SKILL.md +0 -106
- package/skills/brainstorming/__init__.py +0 -1
- package/skills/brainstorming/scripts/brainstorm.py +0 -201
- package/skills/code_audit/SKILL.md +0 -45
- package/skills/darkharvest/SKILL.md +0 -70
- package/skills/darkharvest/scripts/harvest.py +0 -289
- package/skills/epistemic_search/SKILL.md +0 -67
- package/skills/epistemic_search/__init__.py +0 -1
- package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
- package/skills/epistemic_search/scripts/fetch.py +0 -156
- package/skills/epistemic_search/scripts/search.py +0 -551
- package/skills/epistemic_search/scripts/webcache.py +0 -339
- package/skills/factory/SKILL.md +0 -61
- package/skills/factory/scripts/factory.py +0 -300
- package/skills/grilling/SKILL.md +0 -61
- package/skills/grilling/__init__.py +0 -0
- package/skills/grilling/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/grilling/__pycache__/socratic_tree.cpython-311.pyc +0 -0
- package/skills/grilling/socratic_tree.py +0 -363
- package/skills/oss_scout/SKILL.md +0 -49
- package/skills/research_cache/SKILL.md +0 -36
- package/skills/research_cache/__init__.py +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 +0 -302
- package/skills/swarm_config/SKILL.md +0 -178
- package/skills/swarm_config/__init__.py +0 -35
- 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 +0 -1002
|
@@ -1,879 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Canonical CONFIG schema + validated single-writer persistence (phase 01).
|
|
3
|
-
|
|
4
|
-
Covers the phase-01 acceptance criteria:
|
|
5
|
-
|
|
6
|
-
* A1 — `schemas/config.schema.json` is generated from `runner/schemas.py`, the
|
|
7
|
-
default config validates, and the shared fixture corpus (rejecting enum,
|
|
8
|
-
type, range, minLength and pattern violations) yields the expected verdicts.
|
|
9
|
-
* A2 — crash-safety (a failure between temp-write and rename leaves the
|
|
10
|
-
original byte-identical) and lock contention (two concurrent writers never
|
|
11
|
-
tear the file or lose each other's keys). NOTE: the full `discover` run is
|
|
12
|
-
blocked in this sandbox by pre-existing failures/hangs in
|
|
13
|
-
`test_search_provider` (`TestRunPathFailFast`/`TestMcpRunStatus`), identical
|
|
14
|
-
on pristine HEAD; phase-owned suites are the verification bar.
|
|
15
|
-
* A3 — `expected_hash` mismatch is rejected with a structured error and no
|
|
16
|
-
write; a match (full hash or ≥8-hex prefix) proceeds.
|
|
17
|
-
* A4 — MCP `iumbtems_config` accepts every canonical key, rejects invalid
|
|
18
|
-
values before writing, and its advertised input schema matches the GENERATED
|
|
19
|
-
artifact `schemas/config.schema.json` (R2; not the deepcopy source).
|
|
20
|
-
* A5 — honest field wiring: `verify.min_fuzzy_confidence` changes verification
|
|
21
|
-
through the MCP surface; `divergence_threshold` is advisory and emitted only
|
|
22
|
-
for non-default values (R3); `output_dir` is deprecated and inert.
|
|
23
|
-
* A6 — legacy pin migration is covered in `test_swarm_config.py` (unchanged),
|
|
24
|
-
including R1's refusal to persist schema-rejected files.
|
|
25
|
-
|
|
26
|
-
Rework (retry 1) regressions: R2/R3/R5/R6/R7/R8 live here; R1/R4 also have
|
|
27
|
-
writer-level regressions in `test_swarm_config.py`.
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
import io
|
|
31
|
-
import json
|
|
32
|
-
import math
|
|
33
|
-
import os
|
|
34
|
-
import re
|
|
35
|
-
import sys
|
|
36
|
-
import tempfile
|
|
37
|
-
import unittest
|
|
38
|
-
from contextlib import redirect_stdout
|
|
39
|
-
from pathlib import Path
|
|
40
|
-
|
|
41
|
-
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
42
|
-
if str(PROJECT_ROOT) not in sys.path:
|
|
43
|
-
sys.path.insert(0, str(PROJECT_ROOT))
|
|
44
|
-
|
|
45
|
-
from runner.mcp_server import CONFIG_TOOL_CONTROL_ARGS # noqa: E402
|
|
46
|
-
from runner.schemas import CONFIG, CONFIG_DEFAULTS # noqa: E402
|
|
47
|
-
from runner.schema_validate import validate # noqa: E402
|
|
48
|
-
from skills.research_cache.hasher import SourceHasher # noqa: E402
|
|
49
|
-
from skills.swarm_config.configure import ( # noqa: E402
|
|
50
|
-
DEFAULT_CONFIG,
|
|
51
|
-
config_hash,
|
|
52
|
-
load_config,
|
|
53
|
-
validate_config,
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
FIXTURES = Path(__file__).resolve().parent / "fixtures"
|
|
57
|
-
VALIDATION_CASES = json.loads(
|
|
58
|
-
(FIXTURES / "config_validation_cases.json").read_text(encoding="utf-8")
|
|
59
|
-
)["cases"]
|
|
60
|
-
|
|
61
|
-
CONTROL_ARGS = set(CONFIG_TOOL_CONTROL_ARGS)
|
|
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
|
-
|
|
76
|
-
# One representative valid value per canonical key; the test asserts this map
|
|
77
|
-
# covers the schema exactly, so a new key cannot slip in without a test.
|
|
78
|
-
VALID_UPDATES = {
|
|
79
|
-
"search_engine": "brave",
|
|
80
|
-
"max_iterations": 3,
|
|
81
|
-
"divergence_threshold": 0.5,
|
|
82
|
-
"mode": "audit",
|
|
83
|
-
"backend": "opencode",
|
|
84
|
-
"cache_raw_markdown": False,
|
|
85
|
-
"cache_ttl_days": 14,
|
|
86
|
-
"search_timeout_s": 3.5,
|
|
87
|
-
"searxng_url": "http://localhost:8080",
|
|
88
|
-
"license_whitelist": ["MIT"],
|
|
89
|
-
"output_dir": "/tmp/renamed-workspace",
|
|
90
|
-
"allocation": "auction",
|
|
91
|
-
"domain_pack": "biopharma",
|
|
92
|
-
"verify": {"min_fuzzy_confidence": 0.9},
|
|
93
|
-
"agents": {
|
|
94
|
-
"alpha": {
|
|
95
|
-
"backend": ["claude", "-p"],
|
|
96
|
-
"model": "example/model",
|
|
97
|
-
"opencode_agent": "brainstormer",
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
"opencode_auto": False,
|
|
101
|
-
"opencode_agent": "darkharvester",
|
|
102
|
-
"mcp_servers": {"searxng": False, "firecrawl": True},
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
# One clearly invalid value per canonical key.
|
|
106
|
-
INVALID_UPDATES = {
|
|
107
|
-
"search_engine": "google",
|
|
108
|
-
"max_iterations": 9,
|
|
109
|
-
"divergence_threshold": "high",
|
|
110
|
-
"mode": "swarm",
|
|
111
|
-
"backend": "gemini",
|
|
112
|
-
"cache_raw_markdown": "yes",
|
|
113
|
-
"cache_ttl_days": -1,
|
|
114
|
-
"search_timeout_s": 0,
|
|
115
|
-
"searxng_url": 42,
|
|
116
|
-
"license_whitelist": "MIT",
|
|
117
|
-
"output_dir": 42,
|
|
118
|
-
"allocation": "lottery",
|
|
119
|
-
"domain_pack": 7,
|
|
120
|
-
"verify": {"min_fuzzy_confidence": 2.0},
|
|
121
|
-
"agents": "claude -p",
|
|
122
|
-
"opencode_auto": "yes",
|
|
123
|
-
"opencode_agent": 5,
|
|
124
|
-
"mcp_servers": "enabled",
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _write_config(base: Path, payload: dict) -> Path:
|
|
129
|
-
base.mkdir(parents=True, exist_ok=True)
|
|
130
|
-
cfg_file = base / "config.json"
|
|
131
|
-
cfg_file.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
132
|
-
return cfg_file
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
class TestCanonicalSchema(unittest.TestCase):
|
|
136
|
-
def test_generated_config_schema_in_sync(self):
|
|
137
|
-
from scripts.gen_schemas import check
|
|
138
|
-
|
|
139
|
-
self.assertEqual(check(), 0)
|
|
140
|
-
|
|
141
|
-
def test_default_config_matches_canonical_defaults(self):
|
|
142
|
-
self.assertEqual(DEFAULT_CONFIG, CONFIG_DEFAULTS)
|
|
143
|
-
self.assertEqual(validate(CONFIG_DEFAULTS, CONFIG), [])
|
|
144
|
-
self.assertEqual(validate_config(DEFAULT_CONFIG), [])
|
|
145
|
-
|
|
146
|
-
def test_every_default_is_declared_in_the_schema(self):
|
|
147
|
-
self.assertEqual(set(CONFIG_DEFAULTS), set(CONFIG["properties"]))
|
|
148
|
-
for key, value in CONFIG_DEFAULTS.items():
|
|
149
|
-
self.assertEqual(CONFIG["properties"][key].get("default"), value, key)
|
|
150
|
-
|
|
151
|
-
def test_per_key_annotations_present(self):
|
|
152
|
-
scopes = {"run", "retrieval", "dispatch", "audit", "cache", "layout"}
|
|
153
|
-
restarts = {"next-run", "none"}
|
|
154
|
-
for key, subschema in CONFIG["properties"].items():
|
|
155
|
-
self.assertIsInstance(subschema.get("x-label"), str, key)
|
|
156
|
-
self.assertIn(subschema.get("x-scope"), scopes, key)
|
|
157
|
-
self.assertIn(subschema.get("x-restart"), restarts, key)
|
|
158
|
-
self.assertIsInstance(subschema.get("description"), str, key)
|
|
159
|
-
|
|
160
|
-
def test_output_dir_is_flagged_deprecated(self):
|
|
161
|
-
self.assertTrue(CONFIG["properties"]["output_dir"].get("deprecated"))
|
|
162
|
-
self.assertEqual(CONFIG["properties"]["output_dir"]["x-restart"], "none")
|
|
163
|
-
|
|
164
|
-
def test_fixture_corpus_verdicts(self):
|
|
165
|
-
for case in VALIDATION_CASES:
|
|
166
|
-
with self.subTest(case=case["name"]):
|
|
167
|
-
problems = validate(case["config"], CONFIG)
|
|
168
|
-
self.assertEqual(
|
|
169
|
-
not problems,
|
|
170
|
-
case["valid"],
|
|
171
|
-
f"{case['name']}: {problems}",
|
|
172
|
-
)
|
|
173
|
-
if not case["valid"] and case.get("expect"):
|
|
174
|
-
self.assertTrue(
|
|
175
|
-
any(case["expect"] in p for p in problems),
|
|
176
|
-
f"{case['name']}: {case['expect']!r} not in {problems}",
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
def test_non_finite_numbers_are_rejected(self):
|
|
180
|
-
# Python's json accepts bare NaN/Infinity; neither can satisfy a bounded
|
|
181
|
-
# contract, and NaN would slip past ordinary comparisons.
|
|
182
|
-
for bad in (math.nan, math.inf, -math.inf):
|
|
183
|
-
with self.subTest(value=bad):
|
|
184
|
-
problems = validate(
|
|
185
|
-
{**CONFIG_DEFAULTS, "divergence_threshold": bad}, CONFIG
|
|
186
|
-
)
|
|
187
|
-
self.assertTrue(problems)
|
|
188
|
-
|
|
189
|
-
def test_searxng_url_and_domain_pack_are_constrained(self):
|
|
190
|
-
"""R7 choice: constrain (non-empty pack id; http(s) scheme for SearXNG)."""
|
|
191
|
-
for bad in ("localhost:8080", "ftp://host/", " "):
|
|
192
|
-
with self.subTest(value=bad):
|
|
193
|
-
self.assertTrue(validate({"searxng_url": bad}, CONFIG))
|
|
194
|
-
self.assertTrue(validate({"domain_pack": ""}, CONFIG))
|
|
195
|
-
self.assertEqual(
|
|
196
|
-
validate({"searxng_url": "https://searx.internal:8080"}, CONFIG), []
|
|
197
|
-
)
|
|
198
|
-
self.assertEqual(validate({"domain_pack": "biopharma"}, CONFIG), [])
|
|
199
|
-
self.assertEqual(
|
|
200
|
-
validate({"searxng_url": None, "domain_pack": None}, CONFIG), []
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
class TestMcpConfigSurface(unittest.TestCase):
|
|
205
|
-
"""`iumbtems_config` accepts the full canonical key set, rejects garbage."""
|
|
206
|
-
|
|
207
|
-
def _call(self, args):
|
|
208
|
-
from runner.mcp_server import _handle_config
|
|
209
|
-
|
|
210
|
-
return json.loads(_handle_config(args))
|
|
211
|
-
|
|
212
|
-
def test_advertised_schema_parity_with_generated_artifact(self):
|
|
213
|
-
"""R2: compare against the GENERATED artifact, never the deepcopy source."""
|
|
214
|
-
from runner.mcp_server import build_tools, config_tool_input_schema
|
|
215
|
-
|
|
216
|
-
artifact_path = PROJECT_ROOT / "schemas" / "config.schema.json"
|
|
217
|
-
artifact = json.loads(artifact_path.read_text(encoding="utf-8"))
|
|
218
|
-
advertised = config_tool_input_schema()
|
|
219
|
-
spec = next(t for t in build_tools() if t.name == "iumbtems_config")
|
|
220
|
-
self.assertEqual(spec.input_schema, advertised)
|
|
221
|
-
|
|
222
|
-
problems = []
|
|
223
|
-
self._compare_properties(artifact, advertised, "", problems)
|
|
224
|
-
self.assertEqual(problems, [])
|
|
225
|
-
advertised_keys = set(advertised["properties"]) - CONTROL_ARGS
|
|
226
|
-
self.assertEqual(advertised_keys, set(artifact["properties"]))
|
|
227
|
-
# The artifact is the generated mirror of the canonical module.
|
|
228
|
-
self.assertEqual(set(artifact["properties"]), set(CONFIG["properties"]))
|
|
229
|
-
# R4: both spellings are advertised and documented.
|
|
230
|
-
self.assertIn("expected_hash", advertised["properties"])
|
|
231
|
-
self.assertIn("expectedHash", advertised["properties"])
|
|
232
|
-
self.assertIn(
|
|
233
|
-
"expected_hash",
|
|
234
|
-
advertised["properties"]["expectedHash"]["description"],
|
|
235
|
-
)
|
|
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
|
-
|
|
269
|
-
def _compare_properties(self, canonical, advertised, path, problems):
|
|
270
|
-
for key, sub in (canonical.get("properties") or {}).items():
|
|
271
|
-
adv = (advertised.get("properties") or {}).get(key)
|
|
272
|
-
if adv is None:
|
|
273
|
-
problems.append(f"missing advertised key: {path}{key}")
|
|
274
|
-
continue
|
|
275
|
-
self._compare_schema_node(sub, adv, f"{path}{key}", problems)
|
|
276
|
-
|
|
277
|
-
def test_parity_comparator_is_not_vacuous(self):
|
|
278
|
-
"""R2: the comparator must bite when the advertised surface drifts."""
|
|
279
|
-
from runner.mcp_server import config_tool_input_schema
|
|
280
|
-
|
|
281
|
-
artifact = json.loads(
|
|
282
|
-
(PROJECT_ROOT / "schemas" / "config.schema.json").read_text(
|
|
283
|
-
encoding="utf-8"
|
|
284
|
-
)
|
|
285
|
-
)
|
|
286
|
-
dropped = config_tool_input_schema()
|
|
287
|
-
dropped["properties"].pop("verify")
|
|
288
|
-
problems = []
|
|
289
|
-
self._compare_properties(artifact, dropped, "", problems)
|
|
290
|
-
self.assertTrue(any("missing advertised key" in p for p in problems))
|
|
291
|
-
|
|
292
|
-
drifted = config_tool_input_schema()
|
|
293
|
-
drifted["properties"]["searxng_url"].pop("pattern")
|
|
294
|
-
problems = []
|
|
295
|
-
self._compare_properties(artifact, drifted, "", problems)
|
|
296
|
-
self.assertTrue(any("searxng_url: pattern" in p for p in problems))
|
|
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
|
-
|
|
371
|
-
def test_valid_update_map_covers_every_canonical_key(self):
|
|
372
|
-
self.assertEqual(set(VALID_UPDATES), set(CONFIG["properties"]))
|
|
373
|
-
self.assertEqual(set(INVALID_UPDATES), set(CONFIG["properties"]))
|
|
374
|
-
|
|
375
|
-
def test_every_canonical_key_is_accepted_and_persisted(self):
|
|
376
|
-
for key, value in VALID_UPDATES.items():
|
|
377
|
-
with self.subTest(key=key):
|
|
378
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
379
|
-
payload = self._call({key: value, "base_dir": tmp})
|
|
380
|
-
self.assertEqual(payload["status"], "updated", payload)
|
|
381
|
-
on_disk = load_config(tmp)
|
|
382
|
-
if key == "agents":
|
|
383
|
-
# Deep-merge: the provided role wins, defaults survive.
|
|
384
|
-
self.assertEqual(
|
|
385
|
-
on_disk["agents"]["alpha"]["model"], "example/model"
|
|
386
|
-
)
|
|
387
|
-
else:
|
|
388
|
-
self.assertEqual(on_disk[key], value, key)
|
|
389
|
-
|
|
390
|
-
def test_invalid_values_rejected_before_write(self):
|
|
391
|
-
for key, value in INVALID_UPDATES.items():
|
|
392
|
-
with self.subTest(key=key):
|
|
393
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
394
|
-
payload = self._call({key: value, "base_dir": tmp})
|
|
395
|
-
self.assertEqual(payload["status"], "error", payload)
|
|
396
|
-
self.assertEqual(payload["code"], "CONFIG_VALIDATION_FAILED")
|
|
397
|
-
self.assertFalse(payload["written"])
|
|
398
|
-
self.assertTrue(payload["errors"])
|
|
399
|
-
self.assertFalse((Path(tmp) / "config.json").exists())
|
|
400
|
-
|
|
401
|
-
def test_unknown_key_rejected_with_accepted_list(self):
|
|
402
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
403
|
-
payload = self._call({"not_a_key": 1, "base_dir": tmp})
|
|
404
|
-
self.assertEqual(payload["status"], "error")
|
|
405
|
-
self.assertEqual(payload["code"], "UNKNOWN_CONFIG_KEY")
|
|
406
|
-
self.assertFalse(payload["written"])
|
|
407
|
-
self.assertIn("not_a_key", payload["errors"][0])
|
|
408
|
-
self.assertEqual(set(payload["accepted_keys"]), set(CONFIG["properties"]))
|
|
409
|
-
self.assertFalse((Path(tmp) / "config.json").exists())
|
|
410
|
-
|
|
411
|
-
def test_legacy_aliases_still_work(self):
|
|
412
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
413
|
-
payload = self._call({"engine": "firecrawl", "depth": 4, "base_dir": tmp})
|
|
414
|
-
self.assertEqual(payload["status"], "updated")
|
|
415
|
-
on_disk = load_config(tmp)
|
|
416
|
-
self.assertEqual(on_disk["search_engine"], "firecrawl")
|
|
417
|
-
self.assertEqual(on_disk["max_iterations"], 4)
|
|
418
|
-
|
|
419
|
-
def test_show_path_is_unchanged_and_writes_nothing(self):
|
|
420
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
421
|
-
payload = self._call({"base_dir": tmp})
|
|
422
|
-
self.assertEqual(payload["status"], "current")
|
|
423
|
-
self.assertFalse(payload["migrated"])
|
|
424
|
-
self.assertEqual(payload["config"], load_config(tmp))
|
|
425
|
-
self.assertFalse((Path(tmp) / "config.json").exists())
|
|
426
|
-
shown = self._call({"base_dir": tmp, "show": True})
|
|
427
|
-
self.assertEqual(shown["status"], "current")
|
|
428
|
-
# R11: the legacy-pin case must NOT write (the old test was vacuous
|
|
429
|
-
# here). Seed a pin, snapshot the bytes, and prove the show leg
|
|
430
|
-
# leaves them byte-identical while still reporting the migration.
|
|
431
|
-
cfg_file = _write_config(
|
|
432
|
-
Path(tmp), {"agents": {"alpha": {"backend": ["claude", "-p"]}}}
|
|
433
|
-
)
|
|
434
|
-
before = cfg_file.read_bytes()
|
|
435
|
-
pinned = self._call({"base_dir": tmp})
|
|
436
|
-
self.assertEqual(pinned["status"], "current")
|
|
437
|
-
self.assertTrue(pinned["migrated"])
|
|
438
|
-
self.assertEqual(cfg_file.read_bytes(), before)
|
|
439
|
-
|
|
440
|
-
def test_write_leg_heals_legacy_pin_and_persists(self):
|
|
441
|
-
# R11 preserves phase-01 A6: a write still migrates the legacy pin on disk.
|
|
442
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
443
|
-
base = Path(tmp)
|
|
444
|
-
cfg_file = _write_config(
|
|
445
|
-
base, {"agents": {"alpha": {"backend": ["claude", "-p"]}}}
|
|
446
|
-
)
|
|
447
|
-
payload = self._call({"max_iterations": 2, "base_dir": tmp})
|
|
448
|
-
self.assertEqual(payload["status"], "updated", payload)
|
|
449
|
-
healed = json.loads(cfg_file.read_text(encoding="utf-8"))
|
|
450
|
-
self.assertIsNone(healed["agents"]["alpha"]["backend"])
|
|
451
|
-
self.assertEqual(healed["max_iterations"], 2)
|
|
452
|
-
|
|
453
|
-
def test_guarded_write_on_legacy_pin_succeeds_and_heals(self):
|
|
454
|
-
# F1/R12: the wizard always sends `expected_hash`. With no heal-before-
|
|
455
|
-
# save, the guard still matches so the write is `updated` (never a false
|
|
456
|
-
# `stale`) and the pin is migrated on disk.
|
|
457
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
458
|
-
base = Path(tmp)
|
|
459
|
-
cfg_file = _write_config(
|
|
460
|
-
base, {"agents": {"alpha": {"backend": ["claude", "-p"]}}}
|
|
461
|
-
)
|
|
462
|
-
start_hash = config_hash(tmp)
|
|
463
|
-
payload = self._call(
|
|
464
|
-
{"max_iterations": 3, "expected_hash": start_hash, "base_dir": tmp}
|
|
465
|
-
)
|
|
466
|
-
self.assertEqual(payload["status"], "updated", payload)
|
|
467
|
-
healed = json.loads(cfg_file.read_text(encoding="utf-8"))
|
|
468
|
-
self.assertIsNone(healed["agents"]["alpha"]["backend"])
|
|
469
|
-
self.assertEqual(healed["max_iterations"], 3)
|
|
470
|
-
|
|
471
|
-
def test_invalid_pin_file_show_reports_no_migration_and_write_blocked(self):
|
|
472
|
-
# R12/R11: an invalid file is never healed, and a write that would leave
|
|
473
|
-
# it invalid is rejected without touching the bytes.
|
|
474
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
475
|
-
base = Path(tmp)
|
|
476
|
-
cfg_file = _write_config(
|
|
477
|
-
base,
|
|
478
|
-
{
|
|
479
|
-
"max_iterations": 99,
|
|
480
|
-
"agents": {"alpha": {"backend": ["claude", "-p"]}},
|
|
481
|
-
},
|
|
482
|
-
)
|
|
483
|
-
before = cfg_file.read_bytes()
|
|
484
|
-
shown = self._call({"base_dir": tmp})
|
|
485
|
-
self.assertEqual(shown["status"], "current")
|
|
486
|
-
self.assertFalse(shown["migrated"])
|
|
487
|
-
self.assertEqual(cfg_file.read_bytes(), before)
|
|
488
|
-
blocked = self._call({"mode": "audit", "base_dir": tmp})
|
|
489
|
-
self.assertEqual(blocked["status"], "error")
|
|
490
|
-
self.assertEqual(blocked["code"], "CONFIG_VALIDATION_FAILED")
|
|
491
|
-
self.assertFalse(blocked["written"])
|
|
492
|
-
self.assertEqual(cfg_file.read_bytes(), before)
|
|
493
|
-
|
|
494
|
-
def test_null_clears_a_nullable_key(self):
|
|
495
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
496
|
-
self._call({"searxng_url": "http://localhost:8080", "base_dir": tmp})
|
|
497
|
-
payload = self._call({"searxng_url": None, "base_dir": tmp})
|
|
498
|
-
self.assertEqual(payload["status"], "updated")
|
|
499
|
-
self.assertIsNone(load_config(tmp)["searxng_url"])
|
|
500
|
-
|
|
501
|
-
def test_searxng_url_scheme_and_domain_pack_nonempty_enforced(self):
|
|
502
|
-
"""R7 constraints are enforced before any write at the MCP surface."""
|
|
503
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
504
|
-
payload = self._call({"searxng_url": "localhost:8080", "base_dir": tmp})
|
|
505
|
-
self.assertEqual(payload["status"], "error")
|
|
506
|
-
self.assertEqual(payload["code"], "CONFIG_VALIDATION_FAILED")
|
|
507
|
-
payload = self._call({"domain_pack": "", "base_dir": tmp})
|
|
508
|
-
self.assertEqual(payload["status"], "error")
|
|
509
|
-
self.assertEqual(payload["code"], "CONFIG_VALIDATION_FAILED")
|
|
510
|
-
self.assertFalse((Path(tmp) / "config.json").exists())
|
|
511
|
-
|
|
512
|
-
def test_stale_expected_hash_returns_snapshot_without_write(self):
|
|
513
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
514
|
-
updated = self._call({"mode": "scout", "base_dir": tmp})
|
|
515
|
-
stale = updated["hash"]
|
|
516
|
-
self.assertEqual(len(stale), 64)
|
|
517
|
-
# External writer lands a newer version.
|
|
518
|
-
_write_config(Path(tmp), {"mode": "audit"})
|
|
519
|
-
before = (Path(tmp) / "config.json").read_bytes()
|
|
520
|
-
payload = self._call(
|
|
521
|
-
{"mode": "research", "expected_hash": stale, "base_dir": tmp}
|
|
522
|
-
)
|
|
523
|
-
self.assertEqual(payload["status"], "stale")
|
|
524
|
-
self.assertFalse(payload["written"])
|
|
525
|
-
self.assertEqual(payload["expected_hash"], stale)
|
|
526
|
-
self.assertEqual(payload["current_hash"], config_hash(tmp))
|
|
527
|
-
self.assertEqual(payload["config"]["mode"], "audit")
|
|
528
|
-
# R5: no absolute workspace path in the stale payload.
|
|
529
|
-
self.assertNotIn("path", payload)
|
|
530
|
-
self.assertEqual((Path(tmp) / "config.json").read_bytes(), before)
|
|
531
|
-
|
|
532
|
-
def test_conflicting_expected_hash_spellings_are_rejected(self):
|
|
533
|
-
"""R4: never silently prefer one spelling over the other."""
|
|
534
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
535
|
-
created = self._call({"mode": "scout", "base_dir": tmp})
|
|
536
|
-
live = created["hash"]
|
|
537
|
-
before = (Path(tmp) / "config.json").read_bytes()
|
|
538
|
-
payload = self._call(
|
|
539
|
-
{
|
|
540
|
-
"mode": "research",
|
|
541
|
-
"expected_hash": live,
|
|
542
|
-
"expectedHash": "0" * 8,
|
|
543
|
-
"base_dir": tmp,
|
|
544
|
-
}
|
|
545
|
-
)
|
|
546
|
-
self.assertEqual(payload["status"], "error")
|
|
547
|
-
self.assertEqual(payload["code"], "CONFLICTING_EXPECTED_HASH")
|
|
548
|
-
self.assertFalse(payload["written"])
|
|
549
|
-
self.assertEqual((Path(tmp) / "config.json").read_bytes(), before)
|
|
550
|
-
# Equal spellings are accepted and apply the guard.
|
|
551
|
-
ok = self._call(
|
|
552
|
-
{
|
|
553
|
-
"mode": "research",
|
|
554
|
-
"expected_hash": live,
|
|
555
|
-
"expectedHash": live,
|
|
556
|
-
"base_dir": tmp,
|
|
557
|
-
}
|
|
558
|
-
)
|
|
559
|
-
self.assertEqual(ok["status"], "updated")
|
|
560
|
-
|
|
561
|
-
def test_four_hex_expected_hash_is_rejected(self):
|
|
562
|
-
"""R4: minimum guard prefix is 8 hex chars."""
|
|
563
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
564
|
-
created = self._call({"mode": "scout", "base_dir": tmp})
|
|
565
|
-
four_hex = created["hash"][:4]
|
|
566
|
-
before = (Path(tmp) / "config.json").read_bytes()
|
|
567
|
-
payload = self._call(
|
|
568
|
-
{"mode": "research", "expected_hash": four_hex, "base_dir": tmp}
|
|
569
|
-
)
|
|
570
|
-
self.assertEqual(payload["status"], "error")
|
|
571
|
-
self.assertEqual(payload["code"], "CONFIG_INVALID_EXPECTED_HASH")
|
|
572
|
-
self.assertFalse(payload["written"])
|
|
573
|
-
self.assertEqual((Path(tmp) / "config.json").read_bytes(), before)
|
|
574
|
-
|
|
575
|
-
def test_null_for_non_nullable_keys_is_rejected(self):
|
|
576
|
-
"""R8: null is an explicit error, never a masked no-op."""
|
|
577
|
-
for key in ("search_engine", "max_iterations", "agents", "verify"):
|
|
578
|
-
with self.subTest(key=key):
|
|
579
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
580
|
-
payload = self._call({key: None, "base_dir": tmp})
|
|
581
|
-
self.assertEqual(payload["status"], "error", payload)
|
|
582
|
-
self.assertEqual(payload["code"], "NULL_FOR_NON_NULLABLE_KEY")
|
|
583
|
-
self.assertFalse(payload["written"])
|
|
584
|
-
self.assertIn(key, payload["errors"][0])
|
|
585
|
-
self.assertIn("cache_ttl_days", payload["nullable_keys"])
|
|
586
|
-
self.assertNotIn(key, payload["nullable_keys"])
|
|
587
|
-
self.assertFalse((Path(tmp) / "config.json").exists())
|
|
588
|
-
|
|
589
|
-
def test_invalid_file_is_not_healed_and_blocks_writes(self):
|
|
590
|
-
"""R1 at the MCP surface: a schema-rejected file is never persisted."""
|
|
591
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
592
|
-
base = Path(tmp)
|
|
593
|
-
cfg_file = _write_config(
|
|
594
|
-
base,
|
|
595
|
-
{
|
|
596
|
-
"max_iterations": 99,
|
|
597
|
-
"agents": {"alpha": {"backend": ["claude", "-p"]}},
|
|
598
|
-
},
|
|
599
|
-
)
|
|
600
|
-
before = cfg_file.read_bytes()
|
|
601
|
-
current = self._call({"base_dir": tmp})
|
|
602
|
-
self.assertEqual(current["status"], "current")
|
|
603
|
-
self.assertFalse(current["migrated"])
|
|
604
|
-
self.assertEqual(cfg_file.read_bytes(), before)
|
|
605
|
-
|
|
606
|
-
# One valid write fixes both the invalid value and the pin.
|
|
607
|
-
fixed = self._call({"max_iterations": 2, "base_dir": tmp})
|
|
608
|
-
self.assertEqual(fixed["status"], "updated")
|
|
609
|
-
healed = json.loads(cfg_file.read_text(encoding="utf-8"))
|
|
610
|
-
self.assertEqual(healed["max_iterations"], 2)
|
|
611
|
-
self.assertIsNone(healed["agents"]["alpha"]["backend"])
|
|
612
|
-
|
|
613
|
-
def test_updated_payload_is_single_read_coherent(self):
|
|
614
|
-
"""R10: `updated` config + hash come from ONE read of the written bytes.
|
|
615
|
-
|
|
616
|
-
Regression against the pre-R10 shape (`load_config()` then
|
|
617
|
-
`config_hash()` as two independent reads): a write landing between the
|
|
618
|
-
two would pair an era-A config with an era-B hash. We inject exactly
|
|
619
|
-
that write into `config_hash`; a single-read implementation never calls
|
|
620
|
-
it, so the returned config/hash stay coherent with the bytes on disk.
|
|
621
|
-
"""
|
|
622
|
-
import hashlib
|
|
623
|
-
|
|
624
|
-
from skills.swarm_config import configure
|
|
625
|
-
|
|
626
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
627
|
-
original = configure.config_hash
|
|
628
|
-
|
|
629
|
-
def torn(base):
|
|
630
|
-
# Simulate a concurrent writer landing era-B between the old
|
|
631
|
-
# shape's two reads, immediately before the hash read.
|
|
632
|
-
cfg = Path(base) / "config.json"
|
|
633
|
-
data = json.loads(cfg.read_text(encoding="utf-8"))
|
|
634
|
-
data["mode"] = "audit"
|
|
635
|
-
cfg.write_text(json.dumps(data), encoding="utf-8")
|
|
636
|
-
return original(base)
|
|
637
|
-
|
|
638
|
-
configure.config_hash = torn
|
|
639
|
-
try:
|
|
640
|
-
payload = self._call({"mode": "scout", "base_dir": tmp})
|
|
641
|
-
finally:
|
|
642
|
-
configure.config_hash = original
|
|
643
|
-
|
|
644
|
-
self.assertEqual(payload["status"], "updated")
|
|
645
|
-
written = (Path(tmp) / "config.json").read_bytes()
|
|
646
|
-
# The hash covers the bytes the config was derived from ...
|
|
647
|
-
self.assertEqual(payload["hash"], hashlib.sha256(written).hexdigest())
|
|
648
|
-
# ... and the config is that same era (never scout + audit-hash).
|
|
649
|
-
self.assertEqual(payload["config"]["mode"], json.loads(written)["mode"])
|
|
650
|
-
self.assertEqual(payload["config"], load_config(tmp))
|
|
651
|
-
|
|
652
|
-
def test_output_dir_deprecation_is_inert(self):
|
|
653
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
654
|
-
base = Path(tmp)
|
|
655
|
-
elsewhere = base / "some-other-workspace"
|
|
656
|
-
payload = self._call({"output_dir": str(elsewhere), "base_dir": str(base)})
|
|
657
|
-
self.assertEqual(payload["status"], "updated")
|
|
658
|
-
# The value persists for backward compatibility ...
|
|
659
|
-
self.assertEqual(load_config(str(base))["output_dir"], str(elsewhere))
|
|
660
|
-
# ... but nothing moves: the workspace is still base_dir.
|
|
661
|
-
self.assertTrue((base / "config.json").exists())
|
|
662
|
-
self.assertFalse(elsewhere.exists())
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
class TestVerifyThresholdConsumer(unittest.TestCase):
|
|
666
|
-
"""`verify.min_fuzzy_confidence` is a real parameter behind a real consumer."""
|
|
667
|
-
|
|
668
|
-
@staticmethod
|
|
669
|
-
def _seed(base: Path):
|
|
670
|
-
hasher = SourceHasher(base)
|
|
671
|
-
words = [f"w{i}" for i in range(1, 21)]
|
|
672
|
-
content = " ".join(words)
|
|
673
|
-
digest = hasher.store_source(url="https://example.test/q", content=content)
|
|
674
|
-
# 9 of 10 quote words match a contiguous source window -> 0.90 overlap.
|
|
675
|
-
quote = " ".join(words[:9]) + " nomatch"
|
|
676
|
-
return digest, quote
|
|
677
|
-
|
|
678
|
-
def _verify(self, base: Path, digest: str, quote: str):
|
|
679
|
-
from runner.mcp_server import _handle_verify_quote
|
|
680
|
-
|
|
681
|
-
return json.loads(
|
|
682
|
-
_handle_verify_quote(
|
|
683
|
-
{"hash": digest, "quote": quote, "base_dir": str(base)}
|
|
684
|
-
)
|
|
685
|
-
)
|
|
686
|
-
|
|
687
|
-
def test_default_threshold_behavior_is_unchanged(self):
|
|
688
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
689
|
-
base = Path(tmp)
|
|
690
|
-
digest, quote = self._seed(base)
|
|
691
|
-
payload = self._verify(base, digest, quote)
|
|
692
|
-
# Byte-identical decision to the unconfigured hasher default.
|
|
693
|
-
expected = SourceHasher(base).verify_quote(digest, quote)
|
|
694
|
-
self.assertEqual(payload["verified"], expected[0])
|
|
695
|
-
self.assertEqual(payload["confidence"], expected[1])
|
|
696
|
-
self.assertEqual(payload["min_fuzzy_confidence"], 0.88)
|
|
697
|
-
|
|
698
|
-
def test_configured_threshold_changes_the_decision(self):
|
|
699
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
700
|
-
base = Path(tmp)
|
|
701
|
-
digest, quote = self._seed(base)
|
|
702
|
-
self.assertTrue(self._verify(base, digest, quote)["verified"])
|
|
703
|
-
|
|
704
|
-
from runner.mcp_server import _handle_config, _handle_verify_quote
|
|
705
|
-
|
|
706
|
-
json.loads(
|
|
707
|
-
_handle_config(
|
|
708
|
-
{
|
|
709
|
-
"verify": {"min_fuzzy_confidence": 0.95},
|
|
710
|
-
"base_dir": str(base),
|
|
711
|
-
}
|
|
712
|
-
)
|
|
713
|
-
)
|
|
714
|
-
strict_payload = json.loads(
|
|
715
|
-
_handle_verify_quote(
|
|
716
|
-
{"hash": digest, "quote": quote, "base_dir": str(base)}
|
|
717
|
-
)
|
|
718
|
-
)
|
|
719
|
-
self.assertFalse(strict_payload["verified"])
|
|
720
|
-
self.assertEqual(strict_payload["min_fuzzy_confidence"], 0.95)
|
|
721
|
-
|
|
722
|
-
# Back to the default: the same quote verifies again.
|
|
723
|
-
json.loads(
|
|
724
|
-
_handle_config(
|
|
725
|
-
{
|
|
726
|
-
"verify": {"min_fuzzy_confidence": 0.88},
|
|
727
|
-
"base_dir": str(base),
|
|
728
|
-
}
|
|
729
|
-
)
|
|
730
|
-
)
|
|
731
|
-
self.assertTrue(self._verify(base, digest, quote)["verified"])
|
|
732
|
-
|
|
733
|
-
def test_unreachable_threshold_can_never_fabricate_a_pass(self):
|
|
734
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
735
|
-
base = Path(tmp)
|
|
736
|
-
digest, quote = self._seed(base)
|
|
737
|
-
passed, _, _ = SourceHasher(base).verify_quote(
|
|
738
|
-
digest, quote, min_fuzzy_confidence=1.5
|
|
739
|
-
)
|
|
740
|
-
self.assertFalse(passed)
|
|
741
|
-
|
|
742
|
-
def test_verify_quote_clamps_the_threshold(self):
|
|
743
|
-
"""R6: the clamp lives in `verify_quote` (single enforcement point)."""
|
|
744
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
745
|
-
base = Path(tmp)
|
|
746
|
-
hasher = SourceHasher(base)
|
|
747
|
-
words = [f"w{i}" for i in range(1, 21)]
|
|
748
|
-
digest = hasher.store_source(
|
|
749
|
-
url="https://example.test/c", content=" ".join(words)
|
|
750
|
-
)
|
|
751
|
-
# 3 of 4 quote words match a source window -> best overlap 0.75.
|
|
752
|
-
partial = "w1 w2 w3 nomatch"
|
|
753
|
-
self.assertFalse(hasher.verify_quote(digest, partial)[0])
|
|
754
|
-
# Above 1.0 clamps down to 1.0: still no fabricated pass.
|
|
755
|
-
self.assertFalse(
|
|
756
|
-
hasher.verify_quote(digest, partial, min_fuzzy_confidence=1.5)[0]
|
|
757
|
-
)
|
|
758
|
-
# Below 0 clamps up to 0.0: a sub-default overlap then passes.
|
|
759
|
-
self.assertTrue(
|
|
760
|
-
hasher.verify_quote(digest, partial, min_fuzzy_confidence=-5)[0]
|
|
761
|
-
)
|
|
762
|
-
# Malformed values fall back to the strict default.
|
|
763
|
-
self.assertFalse(
|
|
764
|
-
hasher.verify_quote(digest, partial, min_fuzzy_confidence="high")[0]
|
|
765
|
-
)
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
class TestAuditThresholdConsumer(unittest.TestCase):
|
|
769
|
-
"""The auditor (a real swarm consumer) honors the configured threshold."""
|
|
770
|
-
|
|
771
|
-
def _engine(self, base, threshold):
|
|
772
|
-
from runner.auditor_engine import EpistemicAuditorEngine
|
|
773
|
-
|
|
774
|
-
return EpistemicAuditorEngine(
|
|
775
|
-
base_dir=base, min_fuzzy_confidence=threshold, repo_validator=None
|
|
776
|
-
)
|
|
777
|
-
|
|
778
|
-
def test_auditor_rejects_at_a_stricter_threshold(self):
|
|
779
|
-
from runner.state_machine import ResearchStateMachine
|
|
780
|
-
|
|
781
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
782
|
-
base = Path(tmp)
|
|
783
|
-
words = [f"w{i}" for i in range(1, 21)]
|
|
784
|
-
hasher = SourceHasher(base)
|
|
785
|
-
digest = hasher.store_source(
|
|
786
|
-
url="https://example.test/a", content=" ".join(words)
|
|
787
|
-
)
|
|
788
|
-
quote = " ".join(words[:9]) + " nomatch"
|
|
789
|
-
sm = ResearchStateMachine(base_dir=base)
|
|
790
|
-
sm.init_session("objective", session_id="s1")
|
|
791
|
-
sm.set_scopes([{"scope_id": "sc1", "title": "t", "objective": "o"}])
|
|
792
|
-
dossier = {
|
|
793
|
-
"scope_id": "sc1",
|
|
794
|
-
"affirmative_claims": [
|
|
795
|
-
{
|
|
796
|
-
"claim_id": "C1",
|
|
797
|
-
"tag": "VERIFIED",
|
|
798
|
-
"statement": "s",
|
|
799
|
-
"source_hash": digest,
|
|
800
|
-
"verbatim_quote": quote,
|
|
801
|
-
}
|
|
802
|
-
],
|
|
803
|
-
}
|
|
804
|
-
sm.record_agent_completion("sc1", "alpha", dossier)
|
|
805
|
-
sm.record_agent_completion("sc1", "beta", {"scope_id": "sc1"})
|
|
806
|
-
|
|
807
|
-
lenient = self._engine(base, 0.88).audit_scope("sc1")
|
|
808
|
-
self.assertEqual(lenient["summary"]["verified_passed"], 1)
|
|
809
|
-
strict = self._engine(base, 0.95).audit_scope("sc1")
|
|
810
|
-
self.assertEqual(strict["summary"]["verified_passed"], 0)
|
|
811
|
-
self.assertEqual(strict["summary"]["unverified_rejected"], 1)
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
class TestDivergenceAdvisory(unittest.TestCase):
|
|
815
|
-
"""`divergence_threshold`: emitted only when non-default, never a decision.
|
|
816
|
-
|
|
817
|
-
R3: default/absent threshold runs must stay byte-identical to before the key
|
|
818
|
-
existed — no manifest field, no synthesis bullet, no banner line.
|
|
819
|
-
"""
|
|
820
|
-
|
|
821
|
-
def _run(self, threshold):
|
|
822
|
-
from runner.research_swarm import SwarmRunner
|
|
823
|
-
|
|
824
|
-
tmp = tempfile.mkdtemp()
|
|
825
|
-
base = Path(tmp) / ".research"
|
|
826
|
-
base.mkdir(parents=True)
|
|
827
|
-
if threshold is not None:
|
|
828
|
-
_write_config(base, {"divergence_threshold": threshold})
|
|
829
|
-
runner = SwarmRunner(base_dir=base, mock_mode=True, mode="research")
|
|
830
|
-
buf = io.StringIO()
|
|
831
|
-
with redirect_stdout(buf):
|
|
832
|
-
runner.run_swarm("Evaluate hardware prover latency")
|
|
833
|
-
manifest = runner.state_machine.load_global_manifest()
|
|
834
|
-
audit = json.loads(
|
|
835
|
-
(base / "scratchpads" / "scope_01_latency" / "audit_report.json").read_text(
|
|
836
|
-
encoding="utf-8"
|
|
837
|
-
)
|
|
838
|
-
)
|
|
839
|
-
report = (base / "final_synthesis.md").read_text(encoding="utf-8")
|
|
840
|
-
return manifest, audit["summary"], report, buf.getvalue()
|
|
841
|
-
|
|
842
|
-
def test_default_threshold_emits_nothing(self):
|
|
843
|
-
for threshold in (None, 0.75):
|
|
844
|
-
with self.subTest(threshold=threshold):
|
|
845
|
-
manifest, _, report, output = self._run(threshold)
|
|
846
|
-
self.assertNotIn("divergence_threshold", manifest)
|
|
847
|
-
self.assertNotIn("Advisory Divergence Threshold", report)
|
|
848
|
-
self.assertNotIn("Divergence threshold (advisory)", output)
|
|
849
|
-
|
|
850
|
-
def test_configured_threshold_is_recorded_but_changes_no_decision(self):
|
|
851
|
-
manifest_default, summary_default, report_default, out_default = self._run(None)
|
|
852
|
-
manifest_custom, summary_custom, report_custom, out_custom = self._run(0.95)
|
|
853
|
-
|
|
854
|
-
# Recorded in the manifest, the run summary, and the banner.
|
|
855
|
-
self.assertEqual(manifest_custom["divergence_threshold"], 0.95)
|
|
856
|
-
self.assertIn("Advisory Divergence Threshold", report_custom)
|
|
857
|
-
self.assertIn("0.95", report_custom)
|
|
858
|
-
self.assertIn("Divergence threshold (advisory): 0.95", out_custom)
|
|
859
|
-
|
|
860
|
-
# No decision changed: the audit summaries are equal.
|
|
861
|
-
self.assertEqual(summary_default, summary_custom)
|
|
862
|
-
|
|
863
|
-
# Removing the (only) non-default bullet makes the reports identical
|
|
864
|
-
# (modulo wall-clock stamps).
|
|
865
|
-
stripped = "\n".join(
|
|
866
|
-
line
|
|
867
|
-
for line in report_custom.splitlines()
|
|
868
|
-
if "Advisory Divergence Threshold" not in line
|
|
869
|
-
)
|
|
870
|
-
|
|
871
|
-
def _normalized(report):
|
|
872
|
-
report = re.sub(r"`session-\d{8}-\d{6}`", "`session-<ts>`", report)
|
|
873
|
-
return re.sub(r"`\d{4}-\d{2}-\d{2}T[^`]*`", "`<ts>`", report)
|
|
874
|
-
|
|
875
|
-
self.assertEqual(_normalized(report_default), _normalized(stripped))
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
if __name__ == "__main__":
|
|
879
|
-
unittest.main()
|