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.
- fable_compressor.py +356 -0
- fable_engine/__init__.py +1 -0
- fable_engine/actions/__init__.py +291 -0
- fable_engine/actions/cas.py +182 -0
- fable_engine/actions/deliberation.py +523 -0
- fable_engine/actions/fleet.py +807 -0
- fable_engine/actions/lifecycle.py +298 -0
- fable_engine/actions/scrapers.py +116 -0
- fable_engine/actions/system3.py +815 -0
- fable_engine/browser.py +824 -0
- fable_engine/cas.py +974 -0
- fable_engine/fable_session.json +510 -0
- fable_engine/guards.py +283 -0
- fable_engine/schema.py +714 -0
- fable_engine/scrapers/__init__.py +32 -0
- fable_engine/scrapers/arxiv.py +115 -0
- fable_engine/scrapers/base.py +386 -0
- fable_engine/scrapers/github.py +129 -0
- fable_engine/scrapers/reddit.py +154 -0
- fable_engine/scrapers/web.py +120 -0
- fable_engine/scrapers/x.py +125 -0
- fable_engine/scrapers/youtube.py +132 -0
- fable_engine/server.py +414 -0
- fable_engine/session.py +1819 -0
- fable_engine/test_server.py +1362 -0
- fable_engine/updater.py +541 -0
- fable_engine-1.3.1.dist-info/LICENSE +22 -0
- fable_engine-1.3.1.dist-info/METADATA +173 -0
- fable_engine-1.3.1.dist-info/RECORD +104 -0
- fable_engine-1.3.1.dist-info/WHEEL +5 -0
- fable_engine-1.3.1.dist-info/entry_points.txt +5 -0
- fable_engine-1.3.1.dist-info/top_level.txt +6 -0
- fable_mode/__init__.py +3 -0
- fable_mode/__main__.py +4 -0
- fable_mode/adapters.py +1014 -0
- fable_mode/installer.py +553 -0
- fable_mode/launcher.py +437 -0
- fable_mode/manifest.py +142 -0
- fable_mode/resources.json +114 -0
- fable_mode/safety.py +103 -0
- fable_mode_entry.py +10 -0
- fable_v2/__init__.py +146 -0
- fable_v2/adapters.py +151 -0
- fable_v2/coder_fleet/__init__.py +100 -0
- fable_v2/coder_fleet/ast_tools.py +158 -0
- fable_v2/coder_fleet/compute.py +199 -0
- fable_v2/coder_fleet/design_engine.py +1316 -0
- fable_v2/coder_fleet/diagnostics.py +293 -0
- fable_v2/coder_fleet/fleet_dispatcher.py +214 -0
- fable_v2/coder_fleet/mock_auditor.py +306 -0
- fable_v2/coder_fleet/mutation.py +216 -0
- fable_v2/coder_fleet/property_oracle.py +260 -0
- fable_v2/coder_fleet/receipt_attestor.py +122 -0
- fable_v2/coder_fleet/red_team_swarm.py +908 -0
- fable_v2/coder_fleet/test_harness.py +198 -0
- fable_v2/coder_fleet/vector_engine.py +1287 -0
- fable_v2/coder_fleet/visual.py +357 -0
- fable_v2/coder_fleet/workspace.py +153 -0
- fable_v2/cortical/__init__.py +20 -0
- fable_v2/cortical/plasticity_engine.py +992 -0
- fable_v2/execution_broker.py +811 -0
- fable_v2/proof_engine.py +1141 -0
- fable_v2/protocol.py +485 -0
- fable_v2/runtime.py +1010 -0
- fable_v2/system3/__init__.py +204 -0
- fable_v2/system3/causal.py +558 -0
- fable_v2/system3/dialectical.py +577 -0
- fable_v2/system3/evolution.py +503 -0
- fable_v2/system3/executive.py +338 -0
- fable_v2/system3/free_energy.py +479 -0
- fable_v2/system3/hyperbolic.py +555 -0
- fable_v2/system3/induction.py +336 -0
- fable_v2/system3/kripke.py +548 -0
- fable_v2/system3/oracle.py +745 -0
- fable_v2/verifiers.py +72 -0
- tests/__init__.py +1 -0
- tests/test_anti_loop_circuit_breaker.py +64 -0
- tests/test_auto_updater.py +407 -0
- tests/test_coder_fleet.py +535 -0
- tests/test_delegation_compiler.py +54 -0
- tests/test_descriptor_boundaries.py +126 -0
- tests/test_design_engine.py +603 -0
- tests/test_epistemic_evidence_validator.py +66 -0
- tests/test_execution_broker.py +233 -0
- tests/test_fable_v2.py +406 -0
- tests/test_fleet_transitions.py +116 -0
- tests/test_fsm_redteam_evolution.py +406 -0
- tests/test_goal_rubric_and_pipeline.py +367 -0
- tests/test_hebbian_plasticity.py +585 -0
- tests/test_packaging_runtime.py +194 -0
- tests/test_proof_engine.py +259 -0
- tests/test_red_team_swarm.py +645 -0
- tests/test_redteam_remediation.py +169 -0
- tests/test_registration_transaction.py +375 -0
- tests/test_requested_regressions.py +467 -0
- tests/test_scrapers.py +370 -0
- tests/test_server_actions.py +93 -0
- tests/test_server_frontier_actions.py +269 -0
- tests/test_server_protocol.py +88 -0
- tests/test_stealth_browser.py +970 -0
- tests/test_system3.py +381 -0
- tests/test_system3_deep_integration.py +385 -0
- tests/test_system3_frontier.py +436 -0
- 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()
|