fable-engine 1.3.1__py3-none-any.whl

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. fable_compressor.py +356 -0
  2. fable_engine/__init__.py +1 -0
  3. fable_engine/actions/__init__.py +291 -0
  4. fable_engine/actions/cas.py +182 -0
  5. fable_engine/actions/deliberation.py +523 -0
  6. fable_engine/actions/fleet.py +807 -0
  7. fable_engine/actions/lifecycle.py +298 -0
  8. fable_engine/actions/scrapers.py +116 -0
  9. fable_engine/actions/system3.py +815 -0
  10. fable_engine/browser.py +824 -0
  11. fable_engine/cas.py +974 -0
  12. fable_engine/fable_session.json +510 -0
  13. fable_engine/guards.py +283 -0
  14. fable_engine/schema.py +714 -0
  15. fable_engine/scrapers/__init__.py +32 -0
  16. fable_engine/scrapers/arxiv.py +115 -0
  17. fable_engine/scrapers/base.py +386 -0
  18. fable_engine/scrapers/github.py +129 -0
  19. fable_engine/scrapers/reddit.py +154 -0
  20. fable_engine/scrapers/web.py +120 -0
  21. fable_engine/scrapers/x.py +125 -0
  22. fable_engine/scrapers/youtube.py +132 -0
  23. fable_engine/server.py +414 -0
  24. fable_engine/session.py +1819 -0
  25. fable_engine/test_server.py +1362 -0
  26. fable_engine/updater.py +541 -0
  27. fable_engine-1.3.1.dist-info/LICENSE +22 -0
  28. fable_engine-1.3.1.dist-info/METADATA +173 -0
  29. fable_engine-1.3.1.dist-info/RECORD +104 -0
  30. fable_engine-1.3.1.dist-info/WHEEL +5 -0
  31. fable_engine-1.3.1.dist-info/entry_points.txt +5 -0
  32. fable_engine-1.3.1.dist-info/top_level.txt +6 -0
  33. fable_mode/__init__.py +3 -0
  34. fable_mode/__main__.py +4 -0
  35. fable_mode/adapters.py +1014 -0
  36. fable_mode/installer.py +553 -0
  37. fable_mode/launcher.py +437 -0
  38. fable_mode/manifest.py +142 -0
  39. fable_mode/resources.json +114 -0
  40. fable_mode/safety.py +103 -0
  41. fable_mode_entry.py +10 -0
  42. fable_v2/__init__.py +146 -0
  43. fable_v2/adapters.py +151 -0
  44. fable_v2/coder_fleet/__init__.py +100 -0
  45. fable_v2/coder_fleet/ast_tools.py +158 -0
  46. fable_v2/coder_fleet/compute.py +199 -0
  47. fable_v2/coder_fleet/design_engine.py +1316 -0
  48. fable_v2/coder_fleet/diagnostics.py +293 -0
  49. fable_v2/coder_fleet/fleet_dispatcher.py +214 -0
  50. fable_v2/coder_fleet/mock_auditor.py +306 -0
  51. fable_v2/coder_fleet/mutation.py +216 -0
  52. fable_v2/coder_fleet/property_oracle.py +260 -0
  53. fable_v2/coder_fleet/receipt_attestor.py +122 -0
  54. fable_v2/coder_fleet/red_team_swarm.py +908 -0
  55. fable_v2/coder_fleet/test_harness.py +198 -0
  56. fable_v2/coder_fleet/vector_engine.py +1287 -0
  57. fable_v2/coder_fleet/visual.py +357 -0
  58. fable_v2/coder_fleet/workspace.py +153 -0
  59. fable_v2/cortical/__init__.py +20 -0
  60. fable_v2/cortical/plasticity_engine.py +992 -0
  61. fable_v2/execution_broker.py +811 -0
  62. fable_v2/proof_engine.py +1141 -0
  63. fable_v2/protocol.py +485 -0
  64. fable_v2/runtime.py +1010 -0
  65. fable_v2/system3/__init__.py +204 -0
  66. fable_v2/system3/causal.py +558 -0
  67. fable_v2/system3/dialectical.py +577 -0
  68. fable_v2/system3/evolution.py +503 -0
  69. fable_v2/system3/executive.py +338 -0
  70. fable_v2/system3/free_energy.py +479 -0
  71. fable_v2/system3/hyperbolic.py +555 -0
  72. fable_v2/system3/induction.py +336 -0
  73. fable_v2/system3/kripke.py +548 -0
  74. fable_v2/system3/oracle.py +745 -0
  75. fable_v2/verifiers.py +72 -0
  76. tests/__init__.py +1 -0
  77. tests/test_anti_loop_circuit_breaker.py +64 -0
  78. tests/test_auto_updater.py +407 -0
  79. tests/test_coder_fleet.py +535 -0
  80. tests/test_delegation_compiler.py +54 -0
  81. tests/test_descriptor_boundaries.py +126 -0
  82. tests/test_design_engine.py +603 -0
  83. tests/test_epistemic_evidence_validator.py +66 -0
  84. tests/test_execution_broker.py +233 -0
  85. tests/test_fable_v2.py +406 -0
  86. tests/test_fleet_transitions.py +116 -0
  87. tests/test_fsm_redteam_evolution.py +406 -0
  88. tests/test_goal_rubric_and_pipeline.py +367 -0
  89. tests/test_hebbian_plasticity.py +585 -0
  90. tests/test_packaging_runtime.py +194 -0
  91. tests/test_proof_engine.py +259 -0
  92. tests/test_red_team_swarm.py +645 -0
  93. tests/test_redteam_remediation.py +169 -0
  94. tests/test_registration_transaction.py +375 -0
  95. tests/test_requested_regressions.py +467 -0
  96. tests/test_scrapers.py +370 -0
  97. tests/test_server_actions.py +93 -0
  98. tests/test_server_frontier_actions.py +269 -0
  99. tests/test_server_protocol.py +88 -0
  100. tests/test_stealth_browser.py +970 -0
  101. tests/test_system3.py +381 -0
  102. tests/test_system3_deep_integration.py +385 -0
  103. tests/test_system3_frontier.py +436 -0
  104. tests/test_vector_engine.py +608 -0
fable_v2/verifiers.py ADDED
@@ -0,0 +1,72 @@
1
+ """Composable verifier contracts for Fable V2.
2
+
3
+ Deterministic verifiers should run before model-based judges. A verifier may
4
+ be backed by tests, a compiler, a citation checker, a schema validator, or an
5
+ independent model; the runtime only accepts explicit VerificationResult data.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Callable, Iterable, Protocol
12
+
13
+ from .protocol import Candidate, VerificationResult
14
+
15
+
16
+ class Verifier(Protocol):
17
+ name: str
18
+
19
+ def verify(self, candidate: Candidate) -> VerificationResult:
20
+ ...
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class FunctionVerifier:
25
+ """Adapt a deterministic function into a registered verifier.
26
+
27
+ The function returns ``(passed, reasons, score)``. It is deliberately
28
+ dependency-free so hosts can wrap compilers, tests, schemas, or API calls.
29
+ The default boundary is explicitly ``in_process``. This API prevents a
30
+ model from forging a result, but it does not isolate arbitrary Python
31
+ application code; process-attested verifiers require a separate broker.
32
+ """
33
+
34
+ name: str
35
+ check: Callable[[Candidate], tuple[bool, Iterable[str], float | None]]
36
+ verifier_class: str = "deterministic"
37
+ independent: bool = False
38
+ # This is intentionally not named ``trusted``: in-process registration is
39
+ # an application convention, not a security boundary.
40
+ trust_boundary: str = "in_process"
41
+ evidence_ids: tuple[str, ...] = ()
42
+
43
+ def verify(self, candidate: Candidate) -> VerificationResult:
44
+ passed, reasons, score = self.check(candidate)
45
+ return VerificationResult(
46
+ verification_id=f"{self.name}:{candidate.candidate_id}",
47
+ session_id=candidate.session_id,
48
+ candidate_id=candidate.candidate_id,
49
+ verifier=self.name,
50
+ passed=bool(passed),
51
+ reasons=tuple(str(reason) for reason in reasons),
52
+ score=score,
53
+ evidence_ids=self.evidence_ids,
54
+ )
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class CompositeVerifier:
59
+ """Run a list of verifiers and require every one to pass."""
60
+
61
+ verifiers: tuple[Verifier, ...]
62
+
63
+ @property
64
+ def name(self) -> str:
65
+ return "composite[" + ",".join(v.name for v in self.verifiers) + "]"
66
+
67
+ def verify_all(self, candidate: Candidate) -> tuple[VerificationResult, ...]:
68
+ return tuple(verifier.verify(candidate) for verifier in self.verifiers)
69
+
70
+ def passed(self, candidate: Candidate) -> bool:
71
+ results = self.verify_all(candidate)
72
+ return bool(results) and all(result.passed for result in results)
tests/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Test package for fable-mode."""
@@ -0,0 +1,64 @@
1
+ import unittest
2
+ from pathlib import Path
3
+ import sys
4
+
5
+ # Ensure fable-engine directory is in sys.path
6
+ BASE_DIR = Path(__file__).resolve().parent.parent
7
+ for p in [str(BASE_DIR), str(BASE_DIR / "fable_engine"), str(Path(__file__).resolve().parent)]:
8
+ if p not in sys.path:
9
+ sys.path.insert(0, p)
10
+
11
+ from server import AntiLoopCircuitBreaker
12
+
13
+
14
+ class TestAntiLoopCircuitBreaker(unittest.TestCase):
15
+ def setUp(self):
16
+ self.breaker = AntiLoopCircuitBreaker(max_consecutive_repeats=2, window_size=6)
17
+
18
+ def test_single_failure_does_not_trip(self):
19
+ tripped, msg = self.breaker.record_and_evaluate(
20
+ "replace_file_content",
21
+ {"TargetFile": "src/main.py", "StartLine": 10},
22
+ is_error=True
23
+ )
24
+ self.assertFalse(tripped)
25
+ self.assertEqual(msg, "OK")
26
+
27
+ def test_consecutive_identical_failures_trip(self):
28
+ tool = "replace_file_content"
29
+ args = {"TargetFile": "src/main.py", "StartLine": 10}
30
+
31
+ # 1st fail
32
+ tripped, _ = self.breaker.record_and_evaluate(tool, args, is_error=True)
33
+ self.assertFalse(tripped)
34
+
35
+ # 2nd consecutive identical fail -> Trips
36
+ tripped, msg = self.breaker.record_and_evaluate(tool, args, is_error=True)
37
+ self.assertTrue(tripped)
38
+ self.assertIn("CIRCUIT_BREAKER_TRIGGERED", msg)
39
+ self.assertIn("replace_file_content", msg)
40
+
41
+ def test_identical_successful_invocations_do_not_trip(self):
42
+ tool = "view_file"
43
+ args = {"AbsolutePath": "src/main.py"}
44
+ for _ in range(5):
45
+ tripped, msg = self.breaker.record_and_evaluate(tool, args, is_error=False)
46
+ self.assertFalse(tripped)
47
+ self.assertEqual(msg, "OK")
48
+
49
+ def test_cyclical_oscillation_trips(self):
50
+ args_a = {"command": "cargo test --bin server"}
51
+ args_b = {"command": "cargo test --bin client"}
52
+
53
+ # Sequence: A -> B -> A -> B
54
+ self.breaker.record_and_evaluate("run_command", args_a, is_error=True)
55
+ self.breaker.record_and_evaluate("run_command", args_b, is_error=True)
56
+ self.breaker.record_and_evaluate("run_command", args_a, is_error=True)
57
+ tripped, msg = self.breaker.record_and_evaluate("run_command", args_b, is_error=True)
58
+
59
+ self.assertTrue(tripped)
60
+ self.assertIn("Cyclical 2-step oscillation detected", msg)
61
+
62
+
63
+ if __name__ == "__main__":
64
+ unittest.main()
@@ -0,0 +1,407 @@
1
+ """Unit and Integration Test Suite for Fable-Mode Autonomous Silent Self-Updater.
2
+
3
+ Tests:
4
+ - Initialization and host directory discovery
5
+ - Upstream check with mock git results
6
+ - Offline and timeout fail-safe behavior
7
+ - Non-blocking daemon background update trigger
8
+ - Non-destructive cortical experience preservation (antibodies, weights, custom lobes)
9
+ - Host skill and rule hot-syncing
10
+ - MCP server handler integration (check_auto_update and apply_auto_update)
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ from pathlib import Path
17
+ import shutil
18
+ import subprocess
19
+ import sys
20
+ import tempfile
21
+ import threading
22
+ import time
23
+ import unittest
24
+ from unittest.mock import MagicMock, patch
25
+
26
+ BASE_DIR = Path(__file__).resolve().parent.parent
27
+ for p in [str(BASE_DIR), str(BASE_DIR / "fable_engine"), str(Path(__file__).resolve().parent)]:
28
+ if p not in sys.path:
29
+ sys.path.insert(0, p)
30
+
31
+ from fable_engine.updater import AutoUpdater, BASELINE_LOBES
32
+ from fable_v2.cortical.plasticity_engine import CorticalLobe, HeuristicAntibody
33
+ from fable_engine.server import handle_fable_session
34
+
35
+
36
+ class TestAutoUpdaterCore(unittest.TestCase):
37
+ """Core tests for AutoUpdater initialization and discovery."""
38
+
39
+ def setUp(self) -> None:
40
+ self.temp_dir = tempfile.TemporaryDirectory()
41
+ self.root = Path(self.temp_dir.name).resolve()
42
+
43
+ def tearDown(self) -> None:
44
+ self.temp_dir.cleanup()
45
+
46
+ def test_initialization_defaults(self) -> None:
47
+ updater = AutoUpdater()
48
+ self.assertTrue(updater.repo_root.exists())
49
+ self.assertEqual(updater.remote_url, "https://github.com/REX-codebase/fable-mode.git")
50
+ self.assertIsInstance(updater.host_targets, list)
51
+
52
+ def test_explicit_repo_root_and_skills_dir(self) -> None:
53
+ fake_skills = self.root / "fake_skills"
54
+ updater = AutoUpdater(repo_root=self.root, skills_dir=fake_skills)
55
+ self.assertEqual(updater.repo_root, self.root)
56
+ self.assertEqual(updater.skills_dir, fake_skills)
57
+ self.assertIn(fake_skills, updater.host_targets)
58
+
59
+ def test_discover_host_targets_env_override(self) -> None:
60
+ env_skills = self.root / "env_skills"
61
+ with patch.dict(os.environ, {"FABLE_SKILLS_DIR": str(env_skills)}):
62
+ updater = AutoUpdater(repo_root=self.root)
63
+ self.assertIn(env_skills, updater.host_targets)
64
+
65
+
66
+ class TestAutoUpdaterChecks(unittest.TestCase):
67
+ """Tests for safe upstream checking, timeouts, and offline handling."""
68
+
69
+ def setUp(self) -> None:
70
+ self.temp_dir = tempfile.TemporaryDirectory()
71
+ self.root = Path(self.temp_dir.name).resolve()
72
+ self.updater = AutoUpdater(repo_root=self.root)
73
+
74
+ def tearDown(self) -> None:
75
+ self.temp_dir.cleanup()
76
+
77
+ def test_check_non_git_repo_fails_safe(self) -> None:
78
+ # self.root has no .git directory
79
+ result = self.updater.check_for_updates()
80
+ self.assertFalse(result["update_available"])
81
+ self.assertTrue(result["offline"])
82
+ self.assertIn("not a git repository", result["message"].lower())
83
+
84
+ def test_check_up_to_date(self) -> None:
85
+ (self.root / ".git").mkdir()
86
+ fake_sha = "a1b2c3d4e5f60718293a4b5c6d7e8f9012345678"
87
+
88
+ def fake_run(cmd, **kwargs):
89
+ if "rev-parse" in cmd:
90
+ return subprocess.CompletedProcess(cmd, 0, stdout=fake_sha, stderr="")
91
+ if "ls-remote" in cmd:
92
+ return subprocess.CompletedProcess(
93
+ cmd, 0, stdout=f"{fake_sha}\trefs/heads/main\n", stderr=""
94
+ )
95
+ return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="unknown cmd")
96
+
97
+ with patch("subprocess.run", side_effect=fake_run):
98
+ result = self.updater.check_for_updates()
99
+ self.assertFalse(result["update_available"])
100
+ self.assertFalse(result["offline"])
101
+ self.assertEqual(result["local_commit"], fake_sha)
102
+ self.assertEqual(result["remote_commit"], fake_sha)
103
+ self.assertIn("up to date", result["message"].lower())
104
+
105
+ def test_check_update_available(self) -> None:
106
+ (self.root / ".git").mkdir()
107
+ local_sha = "1111111111111111111111111111111111111111"
108
+ remote_sha = "2222222222222222222222222222222222222222"
109
+
110
+ def fake_run(cmd, **kwargs):
111
+ if "rev-parse" in cmd:
112
+ return subprocess.CompletedProcess(cmd, 0, stdout=local_sha, stderr="")
113
+ if "ls-remote" in cmd:
114
+ return subprocess.CompletedProcess(
115
+ cmd, 0, stdout=f"{remote_sha}\trefs/heads/main\n", stderr=""
116
+ )
117
+ return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="")
118
+
119
+ with patch("subprocess.run", side_effect=fake_run):
120
+ result = self.updater.check_for_updates()
121
+ self.assertTrue(result["update_available"])
122
+ self.assertFalse(result["offline"])
123
+ self.assertEqual(result["local_commit"], local_sha)
124
+ self.assertEqual(result["remote_commit"], remote_sha)
125
+ self.assertIn("update available", result["message"].lower())
126
+
127
+ def test_check_timeout_fails_safe(self) -> None:
128
+ (self.root / ".git").mkdir()
129
+
130
+ def fake_run(cmd, **kwargs):
131
+ if "rev-parse" in cmd:
132
+ return subprocess.CompletedProcess(cmd, 0, stdout="localsha", stderr="")
133
+ if "ls-remote" in cmd:
134
+ raise subprocess.TimeoutExpired(cmd, 3.0)
135
+ return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="")
136
+
137
+ with patch("subprocess.run", side_effect=fake_run):
138
+ result = self.updater.check_for_updates(timeout_seconds=3.0)
139
+ self.assertFalse(result["update_available"])
140
+ self.assertTrue(result["offline"])
141
+ self.assertIn("timed out", result["message"].lower())
142
+
143
+ def test_check_network_failure_fails_safe(self) -> None:
144
+ (self.root / ".git").mkdir()
145
+
146
+ def fake_run(cmd, **kwargs):
147
+ if "rev-parse" in cmd:
148
+ return subprocess.CompletedProcess(cmd, 0, stdout="localsha", stderr="")
149
+ if "ls-remote" in cmd:
150
+ return subprocess.CompletedProcess(cmd, 128, stdout="", stderr="Could not resolve host")
151
+ return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="")
152
+
153
+ with patch("subprocess.run", side_effect=fake_run):
154
+ result = self.updater.check_for_updates()
155
+ self.assertFalse(result["update_available"])
156
+ self.assertTrue(result["offline"])
157
+ self.assertIn("offline", result["message"].lower())
158
+
159
+
160
+ class TestAutoUpdaterBackgroundThread(unittest.TestCase):
161
+ """Tests for non-blocking daemon background update invocation."""
162
+
163
+ def test_trigger_silent_background_update_is_daemon_and_returns_instantly(self) -> None:
164
+ updater = AutoUpdater()
165
+ start = time.perf_counter()
166
+ thread = updater.trigger_silent_background_update()
167
+ duration_ms = (time.perf_counter() - start) * 1000
168
+
169
+ self.assertIsInstance(thread, threading.Thread)
170
+ self.assertTrue(thread.daemon)
171
+ # Verify 0ms startup delay (under 50ms)
172
+ self.assertLess(duration_ms, 50.0)
173
+
174
+
175
+ class TestCortexPreservationLogic(unittest.TestCase):
176
+ """Tests for non-destructive cortical experience preservation."""
177
+
178
+ def setUp(self) -> None:
179
+ self.temp_dir = tempfile.TemporaryDirectory()
180
+ self.root = Path(self.temp_dir.name).resolve()
181
+ self.updater = AutoUpdater(repo_root=self.root)
182
+ self.cortex_dir = self.root / "skills" / "fable-mode" / "cortex"
183
+ self.cortex_dir.mkdir(parents=True, exist_ok=True)
184
+
185
+ def tearDown(self) -> None:
186
+ self.temp_dir.cleanup()
187
+
188
+ def test_custom_lobe_preservation(self) -> None:
189
+ # Create a custom user-sprouted lobe
190
+ custom_lobe = CorticalLobe(
191
+ name="zig_systems",
192
+ description="Low-level memory safety and comptime metaprogramming",
193
+ activation_count=18,
194
+ synaptic_weights={"comptime": 0.95, "allocator": 0.92},
195
+ antibodies=[
196
+ HeuristicAntibody(
197
+ antibody_id="ab_zig_alloc_leak",
198
+ domain="zig_systems",
199
+ trigger_condition="Missing defer allocator.free(buf)",
200
+ lethal_anti_pattern="var buf = try alloc.alloc(u8, 1024);",
201
+ prescribed_defense="Always defer dealloc immediately after allocation.",
202
+ )
203
+ ],
204
+ specialized_heuristics=["Comptime assertions over runtime assertions."],
205
+ )
206
+ custom_path = self.cortex_dir / "zig_systems.md"
207
+ custom_lobe.save_to_disk(custom_path)
208
+
209
+ # Snapshot cortex
210
+ snapshot = self.updater._snapshot_cortex()
211
+ self.assertIn("zig_systems", snapshot["raw_text"])
212
+
213
+ # Simulate git checkout or removal of untracked custom lobe during a pull
214
+ custom_path.unlink()
215
+ self.assertFalse(custom_path.exists())
216
+
217
+ # Restore from snapshot
218
+ preserved = self.updater._merge_and_restore_cortex(snapshot)
219
+ self.assertIn("zig_systems", preserved)
220
+ self.assertTrue(custom_path.exists())
221
+
222
+ restored_lobe = CorticalLobe.load_from_disk(custom_path)
223
+ self.assertEqual(restored_lobe.name, "zig_systems")
224
+ self.assertEqual(len(restored_lobe.antibodies), 1)
225
+ self.assertEqual(restored_lobe.antibodies[0].antibody_id, "ab_zig_alloc_leak")
226
+ self.assertAlmostEqual(restored_lobe.synaptic_weights["comptime"], 0.95)
227
+
228
+ def test_baseline_lobe_antibody_and_weight_merge(self) -> None:
229
+ # 1. Local baseline lobe with locally evolved antibody and high synaptic weight
230
+ local_ab = HeuristicAntibody(
231
+ antibody_id="ab_python_local_evolved_bug",
232
+ domain="python",
233
+ trigger_condition="Local thread safety violation",
234
+ lethal_anti_pattern="shared_state.append(x)",
235
+ prescribed_defense="Use queue.Queue or threading.Lock",
236
+ verified_counterfactual="100-thread stress test passed",
237
+ )
238
+ local_lobe = CorticalLobe(
239
+ name="python",
240
+ description="Python lobe with local learning",
241
+ activation_count=25,
242
+ synaptic_weights={"asyncio_event_loop": 0.99, "local_tool": 0.85},
243
+ antibodies=[local_ab],
244
+ specialized_heuristics=["Local heuristic: prefer fast paths."],
245
+ )
246
+ python_path = self.cortex_dir / "python.md"
247
+ local_lobe.save_to_disk(python_path)
248
+
249
+ # Snapshot before pull
250
+ snapshot = self.updater._snapshot_cortex()
251
+
252
+ # 2. Simulate upstream git pull overwriting python.md with upstream default version
253
+ upstream_ab = HeuristicAntibody(
254
+ antibody_id="ab_python_upstream_new_bug",
255
+ domain="python",
256
+ trigger_condition="Upstream newly discovered flaw",
257
+ lethal_anti_pattern="bad_pattern()",
258
+ prescribed_defense="safe_pattern()",
259
+ )
260
+ upstream_lobe = CorticalLobe(
261
+ name="python",
262
+ description="Upstream Python lobe",
263
+ activation_count=10,
264
+ synaptic_weights={"asyncio_event_loop": 0.80, "upstream_tool": 0.75},
265
+ antibodies=[upstream_ab],
266
+ specialized_heuristics=["Upstream heuristic: use TaskGroup."],
267
+ )
268
+ upstream_lobe.save_to_disk(python_path)
269
+
270
+ # 3. Perform non-destructive merge and restore
271
+ preserved = self.updater._merge_and_restore_cortex(snapshot)
272
+ self.assertIn("python", preserved)
273
+
274
+ merged_lobe = CorticalLobe.load_from_disk(python_path)
275
+
276
+ # Verify BOTH antibodies exist (non-destructive union)
277
+ ab_ids = {ab.antibody_id for ab in merged_lobe.antibodies}
278
+ self.assertIn("ab_python_local_evolved_bug", ab_ids)
279
+ self.assertIn("ab_python_upstream_new_bug", ab_ids)
280
+
281
+ # Verify synaptic weights kept maximum values (did not overwrite local training)
282
+ self.assertAlmostEqual(merged_lobe.synaptic_weights["asyncio_event_loop"], 0.99)
283
+ self.assertAlmostEqual(merged_lobe.synaptic_weights["local_tool"], 0.85)
284
+ self.assertAlmostEqual(merged_lobe.synaptic_weights["upstream_tool"], 0.75)
285
+
286
+ # Verify activation count kept maximum (25)
287
+ self.assertEqual(merged_lobe.activation_count, 25)
288
+
289
+ # Verify both heuristics preserved
290
+ self.assertTrue(any("Local heuristic" in h for h in merged_lobe.specialized_heuristics))
291
+ self.assertTrue(any("Upstream heuristic" in h for h in merged_lobe.specialized_heuristics))
292
+
293
+ def test_synaptic_matrix_merge(self) -> None:
294
+ matrix_path = self.cortex_dir / "synaptic_matrix.json"
295
+ local_matrix = {
296
+ "local_node": {"tool_a": 0.95},
297
+ "shared_node": {"tool_b": 0.90},
298
+ }
299
+ matrix_path.write_text(json.dumps(local_matrix), encoding="utf-8")
300
+
301
+ snapshot = self.updater._snapshot_cortex()
302
+
303
+ # Upstream has a different matrix
304
+ upstream_matrix = {
305
+ "upstream_node": {"tool_c": 0.80},
306
+ "shared_node": {"tool_b": 0.60},
307
+ }
308
+ matrix_path.write_text(json.dumps(upstream_matrix), encoding="utf-8")
309
+
310
+ self.updater._merge_and_restore_cortex(snapshot)
311
+
312
+ merged_matrix = json.loads(matrix_path.read_text(encoding="utf-8"))
313
+ self.assertIn("local_node", merged_matrix)
314
+ self.assertIn("upstream_node", merged_matrix)
315
+ # Shared node retains max weight (0.90)
316
+ self.assertAlmostEqual(merged_matrix["shared_node"]["tool_b"], 0.90)
317
+
318
+
319
+ class TestHostSync(unittest.TestCase):
320
+ """Tests for hot-syncing skills, rules, and MCP definitions into host paths."""
321
+
322
+ def setUp(self) -> None:
323
+ self.temp_dir = tempfile.TemporaryDirectory()
324
+ self.root = Path(self.temp_dir.name).resolve()
325
+
326
+ # Setup source files
327
+ self.skill_src = self.root / "skills" / "fable-mode"
328
+ self.skill_src.mkdir(parents=True, exist_ok=True)
329
+ (self.skill_src / "SKILL.md").write_text("# Fable Skill v2", encoding="utf-8")
330
+
331
+ self.rules_src = self.root / "rules"
332
+ self.rules_src.mkdir(parents=True, exist_ok=True)
333
+ (self.rules_src / "fable-mode.md").write_text("# Fable Rules", encoding="utf-8")
334
+
335
+ self.target_dir = self.root / "target_skills" / "fable-mode"
336
+ self.updater = AutoUpdater(repo_root=self.root, skills_dir=self.target_dir)
337
+
338
+ def tearDown(self) -> None:
339
+ self.temp_dir.cleanup()
340
+
341
+ def test_sync_host_targets(self) -> None:
342
+ synced = self.updater._sync_host_targets()
343
+ self.assertIn(str(self.target_dir), synced)
344
+ self.assertTrue((self.target_dir / "SKILL.md").exists())
345
+ self.assertEqual((self.target_dir / "SKILL.md").read_text(encoding="utf-8"), "# Fable Skill v2")
346
+
347
+ def test_sync_antigravity_server_dest(self) -> None:
348
+ fake_home = self.root / "fake_home"
349
+ agy_server_dest = fake_home / ".gemini" / "antigravity" / "fable-engine"
350
+ agy_server_dest.mkdir(parents=True, exist_ok=True)
351
+
352
+ # Setup fable_engine sources
353
+ fable_engine_src = self.root / "fable_engine"
354
+ fable_engine_src.mkdir(parents=True, exist_ok=True)
355
+ (fable_engine_src / "server.py").write_text("# updated server", encoding="utf-8")
356
+ (fable_engine_src / "fable_session.json").write_text('{"tools": ["fable_session"]}', encoding="utf-8")
357
+
358
+ # Setup fable_v2 tree
359
+ fable_v2_src = self.root / "fable_v2"
360
+ fable_v2_src.mkdir(parents=True, exist_ok=True)
361
+ (fable_v2_src / "core.py").write_text("# fable_v2 core module", encoding="utf-8")
362
+ sub_dir = fable_v2_src / "sub"
363
+ sub_dir.mkdir(parents=True, exist_ok=True)
364
+ (sub_dir / "sub_module.py").write_text("# sub module", encoding="utf-8")
365
+
366
+ with patch("pathlib.Path.home", return_value=fake_home):
367
+ synced = self.updater._sync_host_targets()
368
+
369
+ self.assertIn(str(agy_server_dest / "server.py"), synced)
370
+ self.assertTrue((agy_server_dest / "server.py").exists())
371
+ self.assertEqual((agy_server_dest / "server.py").read_text(encoding="utf-8"), "# updated server")
372
+ self.assertTrue((agy_server_dest / "fable_session.json").exists())
373
+ self.assertEqual(
374
+ (agy_server_dest / "fable_session.json").read_text(encoding="utf-8"),
375
+ '{"tools": ["fable_session"]}',
376
+ )
377
+ self.assertTrue((agy_server_dest / "fable_v2" / "core.py").exists())
378
+ self.assertEqual(
379
+ (agy_server_dest / "fable_v2" / "core.py").read_text(encoding="utf-8"),
380
+ "# fable_v2 core module",
381
+ )
382
+ self.assertTrue((agy_server_dest / "fable_v2" / "sub" / "sub_module.py").exists())
383
+ self.assertEqual(
384
+ (agy_server_dest / "fable_v2" / "sub" / "sub_module.py").read_text(encoding="utf-8"),
385
+ "# sub module",
386
+ )
387
+
388
+
389
+ class TestMCPServerAutoUpdateActions(unittest.TestCase):
390
+ """Tests for handle_fable_session auto-update actions."""
391
+
392
+ def test_check_auto_update_dispatch(self) -> None:
393
+ res = handle_fable_session({"action": "check_auto_update"})
394
+ self.assertIn("Fable Autonomous Auto-Updater Status", res)
395
+ self.assertIn("Update Available", res)
396
+ self.assertIn("Local Commit", res)
397
+
398
+ @patch.object(AutoUpdater, "apply_update", return_value={"success": True, "updated": False, "message": "Up to date", "preserved_lobes": ["rust"], "synced_targets": []})
399
+ def test_apply_auto_update_dispatch(self, mock_apply) -> None:
400
+ res = handle_fable_session({"action": "apply_auto_update", "preserve_cortex": True})
401
+ self.assertIn("Fable Autonomous Auto-Updater Applied", res)
402
+ self.assertIn("Success", res)
403
+ mock_apply.assert_called_once_with(preserve_cortex=True)
404
+
405
+
406
+ if __name__ == "__main__":
407
+ unittest.main()