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
@@ -0,0 +1,194 @@
1
+ import json
2
+ import os
3
+ import stat
4
+ import subprocess
5
+ import shutil
6
+ import sys
7
+ import tempfile
8
+ import unittest
9
+ from pathlib import Path
10
+ from unittest.mock import patch
11
+
12
+ from fable_mode import __version__
13
+ from fable_mode.adapters import Host, RegistrationError, detect_hosts, register_hosts, run_argv
14
+ from fable_mode.installer import InstallError, Installer, verify_installation
15
+ from fable_mode.launcher import _smoke
16
+
17
+
18
+ class PackagingSecurityTests(unittest.TestCase):
19
+ def setUp(self):
20
+ self.tmp = tempfile.TemporaryDirectory()
21
+ self.root = Path(self.tmp.name)
22
+
23
+ def tearDown(self):
24
+ self.tmp.cleanup()
25
+
26
+ def test_top_level_wrapper_is_package_aware(self):
27
+ wrapper = Path(__file__).resolve().parents[1] / "fable_mode_entry.py"
28
+ result = subprocess.run([sys.executable, str(wrapper), "--version"], capture_output=True, text=True, check=True)
29
+ self.assertEqual(result.stdout.strip(), __version__)
30
+
31
+ def test_unattended_install_requires_confirmation(self):
32
+ wrapper = Path(__file__).resolve().parents[1] / "fable_mode_entry.py"
33
+ result = subprocess.run([sys.executable, str(wrapper), "install", "--install-dir", str(self.root / "i")],
34
+ capture_output=True, text=True)
35
+ self.assertEqual(result.returncode, 2)
36
+ self.assertFalse((self.root / "i").exists())
37
+
38
+ def test_unmarked_target_and_source_link_are_refused(self):
39
+ target = self.root / "target"
40
+ target.mkdir(); (target / "keep").write_text("keep")
41
+ with self.assertRaises(InstallError):
42
+ Installer(target).install()
43
+ with self.assertRaises(InstallError):
44
+ Installer(target).uninstall()
45
+ source = self.root / "source"
46
+ source.mkdir()
47
+ # An explicit source override cannot smuggle a symlinked canonical file.
48
+ (source / "fable_mode").mkdir()
49
+ try:
50
+ os.symlink("/etc/passwd", source / "fable_mode" / "__init__.py")
51
+ except OSError as exc:
52
+ if getattr(exc, "winerror", None) == 1314 or isinstance(exc, (PermissionError, NotImplementedError)):
53
+ self.skipTest("Symlink creation requires elevated privilege on Windows")
54
+ raise
55
+ with self.assertRaises(InstallError):
56
+ Installer(self.root / "i", source=source).install()
57
+
58
+ def test_transaction_rolls_back_previous_install(self):
59
+ target = self.root / "i"
60
+ first = Installer(target).install(); first.transaction.commit()
61
+ marker_before = (target / ".fable-install.json").read_bytes()
62
+ # Make the target look valid but break source before second publish.
63
+ source = self.root / "missing-source"
64
+ with self.assertRaises(InstallError):
65
+ Installer(target, source=source).install()
66
+ self.assertEqual(marker_before, (target / ".fable-install.json").read_bytes())
67
+ self.assertTrue(verify_installation(target)[0])
68
+
69
+ def test_dry_run_has_no_changes(self):
70
+ target = self.root / "i"
71
+ result = Installer(target).install(dry_run=True)
72
+ self.assertFalse(target.exists())
73
+ self.assertEqual(result.mode, "source")
74
+
75
+ def test_config_preserves_keys_and_rejects_links(self):
76
+ home = self.root / "home"
77
+ config = home / ".gemini/config/mcp_config.json"
78
+ config.parent.mkdir(parents=True)
79
+ config.write_text(json.dumps({"keep": {"x": 1}, "mcpServers": {"old": {}}}))
80
+ exe = self.root / "fable"; exe.write_text(""); exe.chmod(0o700)
81
+ host = Host("antigravity", exe, "cli", True)
82
+ register_hosts({"antigravity": host}, [str(exe), "serve"], home=home)
83
+ value = json.loads(config.read_text())
84
+ self.assertEqual(value["keep"], {"x": 1})
85
+ self.assertNotIn("fable-mode", value["mcpServers"])
86
+ self.assertEqual(value["mcpServers"]["fable-engine"]["args"], ["serve"])
87
+ config.unlink()
88
+ try:
89
+ os.symlink(config.parent / "elsewhere", config)
90
+ except OSError as exc:
91
+ if getattr(exc, "winerror", None) == 1314 or isinstance(exc, (PermissionError, NotImplementedError)):
92
+ self.skipTest("Symlink creation requires elevated privilege on Windows")
93
+ raise
94
+ with self.assertRaises(RegistrationError):
95
+ register_hosts({"antigravity": host}, [str(exe), "serve"], home=home)
96
+
97
+ def test_only_canonical_host_names_are_probed(self):
98
+ with patch("fable_mode.adapters._which", side_effect=lambda n: None) as which:
99
+ detect_hosts()
100
+ self.assertEqual([c.args[0] for c in which.call_args_list], ["claude", "agy", "codex"])
101
+
102
+ def test_output_is_bounded_and_timeout_is_reported(self):
103
+ code, out, err, timed = run_argv([os.environ.get("PYTHON", sys.executable), "-c", "print('x'*1000000)"], timeout=2)
104
+ self.assertLessEqual(len(out.encode()), 8192)
105
+ self.assertFalse(timed)
106
+
107
+ def test_stale_cli_registration_failure_is_not_healthy(self):
108
+ exe = self.root / "claude"; exe.write_text(""); exe.chmod(0o700)
109
+ with patch("fable_mode.adapters.run_argv", return_value=(1, "unexpected failure", "", False)):
110
+ with self.assertRaises(RegistrationError):
111
+ register_hosts({"claude": Host("claude", exe, "cli", True)}, [str(exe), "serve"], home=self.root)
112
+
113
+ def test_frozen_like_mcp_smoke(self):
114
+ result = Installer(self.root / "i").install(); result.transaction.commit()
115
+ self.assertEqual(result.executable_argv[-1], "serve")
116
+ smoke_ok, smoke_detail = _smoke(result.executable_argv, self.root / "data")
117
+ self.assertTrue(smoke_ok, smoke_detail)
118
+
119
+
120
+ if __name__ == "__main__":
121
+ unittest.main()
122
+
123
+ class RemediationRegressionTests(unittest.TestCase):
124
+ def setUp(self):
125
+ self.tmp = tempfile.TemporaryDirectory()
126
+ self.root = Path(self.tmp.name)
127
+ def tearDown(self):
128
+ self.tmp.cleanup()
129
+
130
+ def test_verify_requires_runtime_manifest_and_rejects_links(self):
131
+ target = self.root / "install"
132
+ result = Installer(target).install(); result.transaction.commit()
133
+ marker = json.loads((target / ".fable-install.json").read_text())
134
+ marker["files"].pop("fable_engine/server.py")
135
+ (target / ".fable-install.json").write_text(json.dumps(marker))
136
+ self.assertFalse(verify_installation(target)[0])
137
+ with self.assertRaises(InstallError):
138
+ Installer(target).install()
139
+ # A fresh valid installation still refuses a symlink substitution.
140
+ result = Installer(self.root / "fresh-install").install(); result.transaction.commit()
141
+ runtime = result.install_dir / "runtime" / "fable_engine" / "server.py"
142
+ moved = runtime.with_suffix(".real"); runtime.rename(moved)
143
+ try:
144
+ runtime.symlink_to(moved)
145
+ except OSError as exc:
146
+ if getattr(exc, "winerror", None) == 1314 or isinstance(exc, (PermissionError, NotImplementedError)):
147
+ self.skipTest("Symlink creation requires elevated privilege on Windows")
148
+ raise
149
+ self.assertFalse(verify_installation(target)[0])
150
+
151
+ def test_cli_registration_cleanup_requires_exact_match(self):
152
+ from fable_mode.adapters import cleanup_recorded_registrations
153
+ exe = self.root / "host"
154
+ record = {"kind": "cli", "host": "claude", "executable": str(exe),
155
+ "name": "fable-engine", "command": "fable", "args": ["serve"]}
156
+ with patch("fable_mode.adapters.run_argv", return_value=(0, json.dumps({"fable-engine": {"command": "other", "args": []}}), "", False)) as runner:
157
+ self.assertEqual(cleanup_recorded_registrations([record]), ["claude"])
158
+ self.assertEqual(runner.call_count, 1)
159
+ with patch("fable_mode.adapters.run_argv", side_effect=[(0, json.dumps({"fable-engine": {"command": "fable", "args": ["serve"]}}), "", False), (0, "", "", False)]) as runner:
160
+ self.assertEqual(cleanup_recorded_registrations([record]), [])
161
+ self.assertEqual(runner.call_count, 2)
162
+
163
+ def test_file_registration_cleanup_requires_exact_match(self):
164
+ from fable_mode.adapters import cleanup_recorded_registrations
165
+ config = self.root / "mcp.json"; config.write_text(json.dumps({"mcpServers": {
166
+ "fable-engine": {"command": "other", "args": []}, "keep": {"x": 1}}}))
167
+ skipped = cleanup_recorded_registrations([{"kind": "file", "path": str(config),
168
+ "name": "fable-engine", "command": "fable", "args": ["serve"]}])
169
+ self.assertEqual(skipped, [str(config)])
170
+ self.assertIn("keep", json.loads(config.read_text())["mcpServers"])
171
+
172
+ def test_windows_wrapper_regressions_are_static(self):
173
+ self.assertIn("-3", Path("install.ps1").read_text())
174
+ self.assertIn("-3", Path("install-antigravity.ps1").read_text())
175
+
176
+ @unittest.skipUnless(shutil.which("pip"), "pip unavailable")
177
+ def test_wheel_lifecycle_from_outside_checkout(self):
178
+ checkout = Path(__file__).resolve().parents[1]
179
+ source = self.root / "src"; source.mkdir()
180
+ for name in ("fable_mode", "fable_engine", "fable_v2", "rules", "docs", "skills"):
181
+ shutil.copytree(checkout / name, source / name)
182
+ for name in ("fable_mode_entry.py", "fable_compressor.py", "LICENSE", "README.md", "pyproject.toml", "setup.py", "MANIFEST.in"):
183
+ shutil.copy2(checkout / name, source / name)
184
+ dist, site, outside = self.root / "dist", self.root / "site", self.root / "outside"
185
+ dist.mkdir(); site.mkdir(); outside.mkdir()
186
+ wheel_build = subprocess.run([sys.executable, "-m", "pip", "wheel", ".", "--no-deps", "--no-build-isolation", "-w", str(dist)], cwd=source, capture_output=True, text=True)
187
+ self.assertEqual(wheel_build.returncode, 0,
188
+ "wheel build failed\nstdout:\n%s\nstderr:\n%s" %
189
+ (wheel_build.stdout, wheel_build.stderr))
190
+ env = os.environ.copy(); env["PIP_USER"] = "0"
191
+ subprocess.run([sys.executable, "-m", "pip", "install", "--no-deps", "--target", str(site), *map(str, dist.glob("*.whl"))], env=env, check=True, capture_output=True)
192
+ env["PYTHONPATH"] = str(site); env["FABLE_INSTALL_DIR"] = str(self.root / "install")
193
+ probe = subprocess.run([sys.executable, "-c", "from fable_mode.installer import Installer,verify_installation; r=Installer().install(); r.transaction.commit(); assert verify_installation(r.install_dir)[0]"], cwd=outside, env=env, check=False, capture_output=True, text=True)
194
+ self.assertEqual(probe.returncode, 0, f"probe failed:\nstdout:\n{probe.stdout}\nstderr:\n{probe.stderr}")
@@ -0,0 +1,259 @@
1
+ import hashlib
2
+ import json
3
+ import tempfile
4
+ import unittest
5
+ from pathlib import Path
6
+
7
+ from fable_v2.protocol import (
8
+ FileChangeRecord,
9
+ ModelVelocityProfile,
10
+ ProofReceipt,
11
+ ToolReceipt,
12
+ VisualMockupSpec,
13
+ )
14
+ from fable_v2.proof_engine import (
15
+ DeterministicProofValidator,
16
+ ProofStatus,
17
+ ProofType,
18
+ ProofValidationResult,
19
+ )
20
+ from fable_v2.system3.kripke import KripkeStructure
21
+ from fable_v2.system3.oracle import Prop, ProofOracle
22
+
23
+
24
+ class TestDeterministicProofValidator(unittest.TestCase):
25
+ def setUp(self):
26
+ self.tmp = tempfile.TemporaryDirectory()
27
+ self.root = Path(self.tmp.name)
28
+ self.validator = DeterministicProofValidator(workspace_root=self.root)
29
+
30
+ def tearDown(self):
31
+ self.tmp.cleanup()
32
+
33
+ def test_ast_valid_python_file(self):
34
+ py_file = self.root / 'sample_module.py'
35
+ code = "def calculate_metric(x: int, y: int) -> int:\n return x + y\n\nclass MetricEngine:\n pass\n"
36
+ py_file.write_text(code, encoding='utf-8')
37
+ res = self.validator.validate_ast(
38
+ code_or_path=str(py_file),
39
+ required_symbols=['calculate_metric', 'MetricEngine'],
40
+ claim='Verify sample_module exports required symbols',
41
+ )
42
+ self.assertTrue(res.passed)
43
+ self.assertEqual(res.confidence, 1.0)
44
+ self.assertEqual(res.proof_type, ProofType.AST_GROUNDED.value)
45
+
46
+ def test_ast_missing_symbol_rejected(self):
47
+ code = "def foo(): pass\n"
48
+ res = self.validator.validate_ast(
49
+ code_or_path=code,
50
+ required_symbols=['foo', 'bar'],
51
+ claim='Missing symbol check',
52
+ )
53
+ self.assertFalse(res.passed)
54
+
55
+ def test_file_sha256_valid(self):
56
+ content = b'Deterministic payload 123456789'
57
+ expected_sha = hashlib.sha256(content).hexdigest()
58
+ file_path = self.root / 'data.bin'
59
+ file_path.write_bytes(content)
60
+
61
+ res = self.validator.validate_file_sha256(
62
+ file_path=str(file_path),
63
+ expected_sha256=expected_sha,
64
+ claim='Validate binary data integrity',
65
+ )
66
+ self.assertTrue(res.passed)
67
+ self.assertEqual(res.proof_type, ProofType.FILE_SHA256.value)
68
+
69
+ def test_tool_receipt_validation(self):
70
+ rcpt = ToolReceipt.from_result(
71
+ receipt_id='rcpt-001',
72
+ session_id='sess-001',
73
+ capability='unit_test',
74
+ tool_name='pytest',
75
+ tool_input={'args': ['-k', 'test_core']},
76
+ tool_output={'passed': True, 'duration': 0.05},
77
+ success=True,
78
+ )
79
+ session_receipts = {'rcpt-001': rcpt}
80
+
81
+ res = self.validator.validate_tool_receipt(
82
+ receipt=rcpt,
83
+ session_receipts=session_receipts,
84
+ claim='Unit test execution receipt',
85
+ )
86
+ self.assertTrue(res.passed)
87
+ self.assertEqual(res.proof_type, ProofType.EMPIRICAL_RECEIPT.value)
88
+
89
+ def test_anti_tautology_rejects_generic_strings(self):
90
+ bad_claims = [
91
+ 'tested',
92
+ 'verified',
93
+ 'works',
94
+ 'it works',
95
+ 'done',
96
+ 'fixed',
97
+ 'looks good',
98
+ 'success',
99
+ 'true',
100
+ 'x == x',
101
+ '1 == 1',
102
+ ]
103
+ for bad in bad_claims:
104
+ self.assertTrue(self.validator.is_tautological(bad))
105
+
106
+ def test_formal_logic_curry_howard(self):
107
+ from fable_v2.system3.oracle import Implies
108
+ A = Prop('A')
109
+ B = Prop('B')
110
+ prop = Implies(A, Implies(B, A))
111
+ res = self.validator.validate_formal_logic(
112
+ proposition_or_formula=prop,
113
+ claim='Constructive tautology A -> B -> A',
114
+ )
115
+ self.assertTrue(res.passed)
116
+ self.assertEqual(res.proof_type, ProofType.FORMAL_LOGIC.value)
117
+
118
+ def test_temporal_invariant_kripke_ctl(self):
119
+ ks = KripkeStructure(name='StateMachine')
120
+ ks.add_world('w_init', {'initialized', 'safe'}, is_initial=True)
121
+ ks.add_world('w_work', {'safe', 'active'})
122
+ ks.add_transition('w_init', 'w_work')
123
+ ks.add_transition('w_work', 'w_work')
124
+
125
+ res = self.validator.validate_temporal_invariant(
126
+ formula='AG(safe)',
127
+ kripke_structure=ks,
128
+ initial_world='w_init',
129
+ claim='Safety invariant holds',
130
+ )
131
+ self.assertTrue(res.passed)
132
+
133
+ def test_vector_coordinates_validation(self):
134
+ valid_bbox = {'x': 100, 'y': 200, 'width': 800, 'height': 600}
135
+ res = self.validator.validate_vector_coordinates(
136
+ coordinates_data=valid_bbox,
137
+ claim='Layout bounding box spec',
138
+ )
139
+ self.assertTrue(res.passed)
140
+
141
+ def test_polyglot_ast_typescript_and_rust(self):
142
+ ts_code = "export interface UserProfile {\n id: string;\n}\n\nexport function fetchUserProfile(id: string): UserProfile {\n return { id };\n}\n"
143
+ res_ts = self.validator.validate_ast(
144
+ code_or_path=ts_code,
145
+ required_symbols=['UserProfile', 'fetchUserProfile'],
146
+ claim='Verify TypeScript interface and function export',
147
+ )
148
+ self.assertTrue(res_ts.passed)
149
+ self.assertEqual(res_ts.metadata.get('language'), 'typescript')
150
+
151
+ rs_code = "pub struct CausalGraph {\n nodes: usize,\n}\n\npub fn create_graph() -> CausalGraph {\n CausalGraph { nodes: 0 }\n}\n"
152
+ res_rs = self.validator.validate_ast(
153
+ code_or_path=rs_code,
154
+ required_symbols=['CausalGraph', 'create_graph'],
155
+ claim='Verify Rust struct and function',
156
+ )
157
+ self.assertTrue(res_rs.passed)
158
+ self.assertEqual(res_rs.metadata.get('language'), 'rust')
159
+
160
+ def test_unregistered_tool_receipt_rejected(self):
161
+ res = self.validator.validate_tool_receipt(
162
+ receipt="stdout: Fake execution output without registration",
163
+ session_receipts={},
164
+ claim="Fake claim with stdout substring",
165
+ )
166
+ self.assertFalse(res.passed)
167
+ self.assertIn("not found in active session receipts", res.details)
168
+
169
+ def test_kripke_safety_fails_on_tainted_context(self):
170
+ clean_res = self.validator.validate_temporal_invariant(
171
+ formula="AG(safe)",
172
+ context={"safe": True, "has_failures": False},
173
+ )
174
+ self.assertTrue(clean_res.passed)
175
+
176
+ tainted_res = self.validator.validate_temporal_invariant(
177
+ formula="AG(safe)",
178
+ context={"safe": False, "has_failures": True},
179
+ )
180
+ self.assertFalse(tainted_res.passed)
181
+
182
+ def test_svg_viewbox_coordinate_validation(self):
183
+ valid_svg = '<svg viewBox="0 0 800 600" xmlns="http://www.w3.org/2000/svg"><rect x="10" y="20" width="100" height="50" /></svg>'
184
+ res = self.validator.validate_vector_coordinates(
185
+ coordinates_data=valid_svg,
186
+ claim="Valid SVG viewport check",
187
+ )
188
+ self.assertTrue(res.passed)
189
+ self.assertEqual(res.metadata["viewBox"], [0.0, 0.0, 800.0, 600.0])
190
+
191
+ invalid_svg = '<svg viewBox="0 0 -100 600"></svg>'
192
+ res_inv = self.validator.validate_vector_coordinates(
193
+ coordinates_data=invalid_svg,
194
+ claim="Invalid SVG viewport check",
195
+ )
196
+ self.assertFalse(res_inv.passed)
197
+
198
+
199
+ class TestProtocolDataclasses(unittest.TestCase):
200
+ def test_proof_receipt_to_dict(self):
201
+ rcpt = ProofReceipt(
202
+ receipt_id='rcpt-123',
203
+ claim='Cryptographic SHA256 match',
204
+ proof_type='file_sha256',
205
+ target_resource='/app/core.py',
206
+ sha256_digest='abc' * 20 + 'abcd',
207
+ verified_at='2026-09-02T12:00:00Z',
208
+ verifier_details={'size_bytes': 1024},
209
+ )
210
+ d = rcpt.to_dict()
211
+ self.assertEqual(d['receipt_id'], 'rcpt-123')
212
+
213
+ def test_file_change_record_to_dict(self):
214
+ change = FileChangeRecord(
215
+ file_path='fable_v2/proof_engine.py',
216
+ change_type='create',
217
+ before_hash=None,
218
+ after_hash='def' * 21 + 'd',
219
+ diff_summary='+500 lines DeterministicProofValidator',
220
+ rationale='Implement deterministic proof engine',
221
+ affected_invariants=('AG(safe)', 'AST_grounding'),
222
+ timestamp='2026-09-02T12:00:00Z',
223
+ )
224
+ d = change.to_dict()
225
+ self.assertEqual(d['file_path'], 'fable_v2/proof_engine.py')
226
+
227
+ def test_visual_mockup_spec_to_dict(self):
228
+ mockup = VisualMockupSpec(
229
+ mockup_id='mockup-001',
230
+ concept_name='Cinematic Glass',
231
+ aesthetic_archetype='glassmorphism',
232
+ prompt='High contrast dark mode',
233
+ image_url=None,
234
+ coordinates_data={'card_w': 320},
235
+ palette=('#0f172a', '#38bdf8'),
236
+ typography={'heading': 'Inter Display'},
237
+ status='approved',
238
+ selected_by_user=True,
239
+ created_at='2026-09-02T12:00:00Z',
240
+ )
241
+ d = mockup.to_dict()
242
+ self.assertEqual(d['mockup_id'], 'mockup-001')
243
+
244
+ def test_model_velocity_profile_to_dict(self):
245
+ profile = ModelVelocityProfile(
246
+ model_tier='pro',
247
+ tokens_per_sec=42.5,
248
+ avg_tool_latency_sec=1.1,
249
+ sample_count=10,
250
+ high_throughput_mode=False,
251
+ exploration_multiplier=1.0,
252
+ last_updated='2026-09-02T12:00:00Z',
253
+ )
254
+ d = profile.to_dict()
255
+ self.assertEqual(d['model_tier'], 'pro')
256
+
257
+
258
+ if __name__ == '__main__':
259
+ unittest.main()