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
tests/test_scrapers.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for Fable Engine Zero-Cost Research Scrapers, SSRF protection,
|
|
3
|
+
alternate numeric IP canonicalization, redirect validation, IP pinning,
|
|
4
|
+
clamped retry delays, content boundaries, error handling, and MCP Action Handlers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import unittest
|
|
9
|
+
import urllib.error
|
|
10
|
+
from unittest.mock import MagicMock, patch
|
|
11
|
+
|
|
12
|
+
from fable_engine.actions import handle_fable_session
|
|
13
|
+
from fable_engine.scrapers import (
|
|
14
|
+
ArxivScraper,
|
|
15
|
+
GitHubScraper,
|
|
16
|
+
RedditScraper,
|
|
17
|
+
ResearchResult,
|
|
18
|
+
WebScraper,
|
|
19
|
+
XScraper,
|
|
20
|
+
YouTubeScraper,
|
|
21
|
+
fetch_url,
|
|
22
|
+
scrape_arxiv,
|
|
23
|
+
scrape_github,
|
|
24
|
+
scrape_reddit,
|
|
25
|
+
scrape_web,
|
|
26
|
+
scrape_x,
|
|
27
|
+
scrape_youtube,
|
|
28
|
+
)
|
|
29
|
+
from fable_engine.scrapers.base import PinnedHTTPSConnection, parse_canonical_ip, validate_safe_url
|
|
30
|
+
from fable_engine.session import get_or_load_session
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestSSRFProtectionAndRateLimiting(unittest.TestCase):
|
|
34
|
+
|
|
35
|
+
def test_alternate_numeric_ip_canonicalization(self):
|
|
36
|
+
self.assertEqual(parse_canonical_ip("127.0.0.1"), "127.0.0.1")
|
|
37
|
+
self.assertEqual(parse_canonical_ip("2130706433"), "127.0.0.1")
|
|
38
|
+
self.assertEqual(parse_canonical_ip("127.1"), "127.0.0.1")
|
|
39
|
+
self.assertEqual(parse_canonical_ip("0x7f000001"), "127.0.0.1")
|
|
40
|
+
self.assertEqual(parse_canonical_ip("0177.0.0.1"), "127.0.0.1")
|
|
41
|
+
self.assertIsNone(parse_canonical_ip("arxiv.org"))
|
|
42
|
+
|
|
43
|
+
def test_ssrf_validation_blocks_unsafe_hosts_and_alternate_ips(self):
|
|
44
|
+
unsafe_urls = [
|
|
45
|
+
"http://localhost:8080/admin",
|
|
46
|
+
"http://127.0.0.1/secret",
|
|
47
|
+
"http://127.0.0.255/",
|
|
48
|
+
"http://2130706433/secret",
|
|
49
|
+
"http://127.1/admin",
|
|
50
|
+
"http://0x7f000001/status",
|
|
51
|
+
"http://0177.0.0.1/keys",
|
|
52
|
+
"http://10.0.0.1/internal",
|
|
53
|
+
"http://172.16.0.1/status",
|
|
54
|
+
"http://192.168.1.1/router",
|
|
55
|
+
"http://169.254.169.254/latest/meta-data/",
|
|
56
|
+
"file:///etc/passwd",
|
|
57
|
+
"gopher://localhost:70",
|
|
58
|
+
"ftp://ftp.local/data",
|
|
59
|
+
]
|
|
60
|
+
for url in unsafe_urls:
|
|
61
|
+
safe, reason, resolved_ip = validate_safe_url(url)
|
|
62
|
+
self.assertFalse(safe, f"Expected '{url}' to be blocked by SSRF validation, but it passed.")
|
|
63
|
+
|
|
64
|
+
def test_ssrf_validation_allows_public_urls(self):
|
|
65
|
+
safe_urls = [
|
|
66
|
+
"https://arxiv.org/abs/2401.12345",
|
|
67
|
+
"https://api.github.com/repos/REX-codebase/fable-mode",
|
|
68
|
+
"https://www.reddit.com/r/Python/hot.json",
|
|
69
|
+
]
|
|
70
|
+
for url in safe_urls:
|
|
71
|
+
safe, reason, resolved_ip = validate_safe_url(url)
|
|
72
|
+
self.assertTrue(safe, f"Expected public URL '{url}' to pass SSRF validation, failed: {reason}")
|
|
73
|
+
self.assertTrue(resolved_ip is not None)
|
|
74
|
+
|
|
75
|
+
def test_fetch_url_raises_value_error_on_ssrf_target(self):
|
|
76
|
+
with self.assertRaises(ValueError) as ctx:
|
|
77
|
+
fetch_url("http://127.0.0.1:8000/keys")
|
|
78
|
+
self.assertIn("SSRF validation failed", str(ctx.exception))
|
|
79
|
+
|
|
80
|
+
@patch("fable_engine.scrapers.base.validate_safe_url")
|
|
81
|
+
def test_ssrf_redirect_to_private_ip_is_blocked(self, mock_validate):
|
|
82
|
+
mock_validate.side_effect = [
|
|
83
|
+
(True, "ok", "93.184.216.34"), # example.com (public)
|
|
84
|
+
(False, "SSRF blocked: target IP '127.0.0.1' is private, loopback, or reserved.", None)
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
with patch("fable_engine.scrapers.base.PinnedHTTPSConnection") as mock_conn_cls:
|
|
88
|
+
mock_conn = MagicMock()
|
|
89
|
+
mock_resp_redirect = MagicMock()
|
|
90
|
+
mock_resp_redirect.status = 302
|
|
91
|
+
mock_resp_redirect.getheader.side_effect = lambda h: "http://127.0.0.1/secret" if h == "Location" else None
|
|
92
|
+
mock_conn.getresponse.return_value = mock_resp_redirect
|
|
93
|
+
mock_conn_cls.return_value = mock_conn
|
|
94
|
+
|
|
95
|
+
with self.assertRaises(ValueError) as ctx:
|
|
96
|
+
fetch_url("https://example.com/redirect-me")
|
|
97
|
+
self.assertIn("SSRF validation failed", str(ctx.exception))
|
|
98
|
+
self.assertIn("127.0.0.1", str(ctx.exception))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TestResearchResultAndContentBoundaries(unittest.TestCase):
|
|
102
|
+
|
|
103
|
+
def test_untrusted_content_boundary_formatting(self):
|
|
104
|
+
res = ResearchResult(
|
|
105
|
+
ok=True,
|
|
106
|
+
source_type="web",
|
|
107
|
+
canonical_url="https://example.com",
|
|
108
|
+
title="Sample Page",
|
|
109
|
+
content="Sample webpage text."
|
|
110
|
+
)
|
|
111
|
+
self.assertEqual(res.metadata.get("trust_level"), "untrusted_external_content")
|
|
112
|
+
md = res.to_markdown()
|
|
113
|
+
self.assertIn("[BEGIN UNTRUSTED EXTERNAL RESEARCH CONTENT]", md)
|
|
114
|
+
self.assertIn("Sample webpage text.", md)
|
|
115
|
+
self.assertIn("[END UNTRUSTED EXTERNAL RESEARCH CONTENT]", md)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TestResearchScrapers(unittest.TestCase):
|
|
119
|
+
|
|
120
|
+
@patch("fable_engine.scrapers.web.fetch_url")
|
|
121
|
+
def test_scrape_web_url_success(self, mock_fetch):
|
|
122
|
+
mock_fetch.return_value = """
|
|
123
|
+
<html>
|
|
124
|
+
<head><title>Test Page</title></head>
|
|
125
|
+
<body>
|
|
126
|
+
<h1>Hello World</h1>
|
|
127
|
+
<p>This is a test paragraph with a <a href="https://example.com/link">link</a>.</p>
|
|
128
|
+
</body>
|
|
129
|
+
</html>
|
|
130
|
+
"""
|
|
131
|
+
res = scrape_web("https://example.com/page")
|
|
132
|
+
self.assertTrue(res.ok)
|
|
133
|
+
self.assertEqual(res.source_type, "web")
|
|
134
|
+
self.assertEqual(res.canonical_url, "https://example.com/page")
|
|
135
|
+
self.assertIn("Test Page", res.title)
|
|
136
|
+
md = res.to_markdown()
|
|
137
|
+
self.assertIn("Hello World", md)
|
|
138
|
+
self.assertIn("[link](https://example.com/link)", md)
|
|
139
|
+
|
|
140
|
+
@patch("fable_engine.scrapers.web.fetch_url")
|
|
141
|
+
def test_scrape_web_http_error_handling(self, mock_fetch):
|
|
142
|
+
mock_fetch.side_effect = urllib.error.HTTPError("https://example.com/forbidden", 403, "Forbidden", {}, None)
|
|
143
|
+
res = scrape_web("https://example.com/forbidden")
|
|
144
|
+
self.assertFalse(res.ok)
|
|
145
|
+
self.assertIn("403", res.error)
|
|
146
|
+
md = res.to_markdown()
|
|
147
|
+
self.assertIn("Research Retrieval Failed", md)
|
|
148
|
+
|
|
149
|
+
@patch("fable_engine.scrapers.youtube.fetch_url")
|
|
150
|
+
def test_scrape_youtube_video_success(self, mock_fetch):
|
|
151
|
+
def side_effect(url, *args, **kwargs):
|
|
152
|
+
if "watch?v=" in url:
|
|
153
|
+
return """
|
|
154
|
+
<html>
|
|
155
|
+
<meta property="og:title" content="Test AI Video">
|
|
156
|
+
<meta property="og:description" content="A great video about AI.">
|
|
157
|
+
<link itemprop="name" content="TechChannel">
|
|
158
|
+
"captionTracks":[{"baseUrl":"https://youtube.com/api/timedtext"}]
|
|
159
|
+
</html>
|
|
160
|
+
"""
|
|
161
|
+
elif "timedtext" in url:
|
|
162
|
+
return """<?xml version="1.0" encoding="utf-8" ?>
|
|
163
|
+
<transcript>
|
|
164
|
+
<text start="0" dur="2">Welcome to the video.</text>
|
|
165
|
+
<text start="2" dur="3">Today we discuss AI agents.</text>
|
|
166
|
+
</transcript>
|
|
167
|
+
"""
|
|
168
|
+
return ""
|
|
169
|
+
|
|
170
|
+
mock_fetch.side_effect = side_effect
|
|
171
|
+
res = scrape_youtube("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
|
172
|
+
self.assertTrue(res.ok)
|
|
173
|
+
self.assertEqual(res.source_type, "youtube")
|
|
174
|
+
self.assertEqual(res.canonical_url, "https://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
|
175
|
+
self.assertIn("Test AI Video", res.title)
|
|
176
|
+
self.assertIn("Welcome to the video. Today we discuss AI agents.", res.content)
|
|
177
|
+
|
|
178
|
+
@patch("fable_engine.scrapers.youtube.fetch_url")
|
|
179
|
+
def test_scrape_youtube_missing_transcript_fallback(self, mock_fetch):
|
|
180
|
+
mock_fetch.return_value = """
|
|
181
|
+
<html>
|
|
182
|
+
<meta property="og:title" content="No Captions Video">
|
|
183
|
+
<link itemprop="name" content="TechChannel">
|
|
184
|
+
</html>
|
|
185
|
+
"""
|
|
186
|
+
res = scrape_youtube("dQw4w9WgXcQ")
|
|
187
|
+
self.assertTrue(res.ok)
|
|
188
|
+
self.assertIn("Best-effort automated transcript track status", res.content)
|
|
189
|
+
|
|
190
|
+
@patch("fable_engine.scrapers.reddit.fetch_url")
|
|
191
|
+
def test_scrape_reddit_thread_success(self, mock_fetch):
|
|
192
|
+
mock_response = [
|
|
193
|
+
{
|
|
194
|
+
"data": {
|
|
195
|
+
"children": [
|
|
196
|
+
{
|
|
197
|
+
"data": {
|
|
198
|
+
"title": "Fable Engine Released",
|
|
199
|
+
"subreddit": "Python",
|
|
200
|
+
"author": "rex_dev",
|
|
201
|
+
"score": 150,
|
|
202
|
+
"selftext": "Check out the new cognitive runtime.",
|
|
203
|
+
"permalink": "/r/Python/comments/123/fable/"
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"data": {
|
|
211
|
+
"children": [
|
|
212
|
+
{
|
|
213
|
+
"data": {
|
|
214
|
+
"author": "user1",
|
|
215
|
+
"body": "Looks amazing!",
|
|
216
|
+
"score": 25
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
mock_fetch.return_value = json.dumps(mock_response)
|
|
224
|
+
res = scrape_reddit("https://www.reddit.com/r/Python/comments/123/fable/")
|
|
225
|
+
self.assertTrue(res.ok)
|
|
226
|
+
self.assertEqual(res.source_type, "reddit")
|
|
227
|
+
self.assertIn("Fable Engine Released", res.title)
|
|
228
|
+
self.assertIn("Check out the new cognitive runtime.", res.content)
|
|
229
|
+
self.assertIn("u/user1 (25 points)", res.content)
|
|
230
|
+
|
|
231
|
+
@patch("fable_engine.scrapers.reddit.fetch_url")
|
|
232
|
+
def test_scrape_reddit_empty_structure_handling(self, mock_fetch):
|
|
233
|
+
mock_fetch.return_value = json.dumps([])
|
|
234
|
+
res = scrape_reddit("r/empty_sub")
|
|
235
|
+
self.assertFalse(res.ok)
|
|
236
|
+
self.assertTrue(res.error is not None)
|
|
237
|
+
|
|
238
|
+
@patch("fable_engine.scrapers.x.fetch_url")
|
|
239
|
+
def test_scrape_x_tweet_success(self, mock_fetch):
|
|
240
|
+
mock_tweet = {
|
|
241
|
+
"text": "Fable Mode is now 100% free with research scrapers!",
|
|
242
|
+
"user": {
|
|
243
|
+
"name": "Fable Engine",
|
|
244
|
+
"screen_name": "fable_mode"
|
|
245
|
+
},
|
|
246
|
+
"created_at": "Mon Mar 03 12:00:00 +0000 2025",
|
|
247
|
+
"favorite_count": 42,
|
|
248
|
+
"retweet_count": 10
|
|
249
|
+
}
|
|
250
|
+
mock_fetch.return_value = json.dumps(mock_tweet)
|
|
251
|
+
res = scrape_x("https://x.com/fable_mode/status/1234567890")
|
|
252
|
+
self.assertTrue(res.ok)
|
|
253
|
+
self.assertEqual(res.source_type, "x")
|
|
254
|
+
self.assertIn("Fable Engine", res.title)
|
|
255
|
+
self.assertIn("100% free", res.content)
|
|
256
|
+
|
|
257
|
+
@patch("fable_engine.scrapers.github.fetch_url")
|
|
258
|
+
def test_scrape_github_repo_success(self, mock_fetch):
|
|
259
|
+
def side_effect(url, *args, **kwargs):
|
|
260
|
+
if "api.github.com/repos" in url:
|
|
261
|
+
return json.dumps({
|
|
262
|
+
"full_name": "REX-codebase/fable-mode",
|
|
263
|
+
"description": "Control plane for AI agents",
|
|
264
|
+
"stargazers_count": 100,
|
|
265
|
+
"forks_count": 12,
|
|
266
|
+
"language": "Python",
|
|
267
|
+
"default_branch": "main",
|
|
268
|
+
"html_url": "https://github.com/REX-codebase/fable-mode"
|
|
269
|
+
})
|
|
270
|
+
elif "raw.githubusercontent.com" in url:
|
|
271
|
+
return "# Fable Mode\nStructured Deliberation for Agents."
|
|
272
|
+
return ""
|
|
273
|
+
|
|
274
|
+
mock_fetch.side_effect = side_effect
|
|
275
|
+
res = scrape_github("REX-codebase/fable-mode")
|
|
276
|
+
self.assertTrue(res.ok)
|
|
277
|
+
self.assertEqual(res.source_type, "github")
|
|
278
|
+
self.assertIn("REX-codebase/fable-mode", res.title)
|
|
279
|
+
self.assertIn("Control plane for AI agents", res.content)
|
|
280
|
+
|
|
281
|
+
@patch("fable_engine.scrapers.arxiv.fetch_url")
|
|
282
|
+
def test_scrape_arxiv_paper_success(self, mock_fetch):
|
|
283
|
+
mock_xml = """<?xml version="1.0" encoding="UTF-8"?>
|
|
284
|
+
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
285
|
+
<entry>
|
|
286
|
+
<id>http://arxiv.org/abs/2401.12345v1</id>
|
|
287
|
+
<published>2024-01-20T10:00:00Z</published>
|
|
288
|
+
<title>Cognitive Deliberation in AI Agents</title>
|
|
289
|
+
<summary>We propose Fable Mode for structured deliberation.</summary>
|
|
290
|
+
<author><name>Alice Smith</name></author>
|
|
291
|
+
<author><name>Bob Jones</name></author>
|
|
292
|
+
</entry>
|
|
293
|
+
</feed>
|
|
294
|
+
"""
|
|
295
|
+
mock_fetch.return_value = mock_xml
|
|
296
|
+
res = scrape_arxiv("2401.12345")
|
|
297
|
+
self.assertTrue(res.ok)
|
|
298
|
+
self.assertEqual(res.source_type, "arxiv")
|
|
299
|
+
self.assertIn("Cognitive Deliberation in AI Agents", res.content)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class TestScraperActionsAndEpistemicLog(unittest.TestCase):
|
|
303
|
+
|
|
304
|
+
@patch("fable_engine.actions.scrapers.scrape_arxiv")
|
|
305
|
+
def test_mcp_action_dispatch(self, mock_scrape):
|
|
306
|
+
mock_scrape.return_value = ResearchResult(
|
|
307
|
+
ok=True,
|
|
308
|
+
source_type="arxiv",
|
|
309
|
+
canonical_url="https://arxiv.org/abs/2401.12345",
|
|
310
|
+
title="AI Deliberation",
|
|
311
|
+
content="Paper abstract."
|
|
312
|
+
)
|
|
313
|
+
resp = handle_fable_session({
|
|
314
|
+
"action": "scrape_arxiv",
|
|
315
|
+
"target": "2401.12345"
|
|
316
|
+
})
|
|
317
|
+
self.assertIn("AI Deliberation", resp)
|
|
318
|
+
mock_scrape.assert_called_once_with("2401.12345")
|
|
319
|
+
|
|
320
|
+
@patch("fable_engine.actions.scrapers.scrape_github")
|
|
321
|
+
def test_auto_log_epistemic_hypothesis_success_only(self, mock_scrape):
|
|
322
|
+
session_name = "test_scraper_session"
|
|
323
|
+
|
|
324
|
+
handle_fable_session({
|
|
325
|
+
"action": "create_session",
|
|
326
|
+
"session_name": session_name,
|
|
327
|
+
"objective": "Test scraping auto-log as HYPOTHESIS",
|
|
328
|
+
"time_budget_minutes": 2.0
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
# 1. Test failed result (ok=False) -> MUST NOT log hypothesis
|
|
332
|
+
mock_scrape.return_value = ResearchResult(
|
|
333
|
+
ok=False,
|
|
334
|
+
source_type="github",
|
|
335
|
+
canonical_url="https://github.com/invalid/repo",
|
|
336
|
+
error="404 Not Found"
|
|
337
|
+
)
|
|
338
|
+
handle_fable_session({
|
|
339
|
+
"action": "scrape_github",
|
|
340
|
+
"session_name": session_name,
|
|
341
|
+
"target": "invalid/repo",
|
|
342
|
+
"auto_log_epistemic": True
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
session = get_or_load_session(session_name)
|
|
346
|
+
self.assertEqual(len(session.epistemic_ledger), 0)
|
|
347
|
+
|
|
348
|
+
# 2. Test successful result (ok=True) -> MUST log hypothesis
|
|
349
|
+
mock_scrape.return_value = ResearchResult(
|
|
350
|
+
ok=True,
|
|
351
|
+
source_type="github",
|
|
352
|
+
canonical_url="https://github.com/REX-codebase/fable-mode",
|
|
353
|
+
title="REX-codebase/fable-mode",
|
|
354
|
+
content="# GitHub Repo\nName: fable-mode"
|
|
355
|
+
)
|
|
356
|
+
handle_fable_session({
|
|
357
|
+
"action": "scrape_github",
|
|
358
|
+
"session_name": session_name,
|
|
359
|
+
"target": "REX-codebase/fable-mode",
|
|
360
|
+
"auto_log_epistemic": True
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
session = get_or_load_session(session_name)
|
|
364
|
+
hypothesis_items = [item for item in session.epistemic_ledger if item["tag"] == "HYPOTHESIS"]
|
|
365
|
+
self.assertEqual(len(hypothesis_items), 1)
|
|
366
|
+
self.assertIn("REX-codebase/fable-mode", hypothesis_items[0]["claim"])
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
if __name__ == "__main__":
|
|
370
|
+
unittest.main()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
8
|
+
for p in [str(BASE_DIR), str(BASE_DIR / "fable_engine"), str(Path(__file__).resolve().parent)]:
|
|
9
|
+
if p not in sys.path:
|
|
10
|
+
sys.path.insert(0, p)
|
|
11
|
+
|
|
12
|
+
from server import handle_fable_session, ACTIVE_SESSIONS, SESSIONS_DIR, FableSession
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestServerActions(unittest.TestCase):
|
|
16
|
+
def setUp(self):
|
|
17
|
+
self.session_name = f"test_session_{int(time.time() * 1000)}"
|
|
18
|
+
|
|
19
|
+
def tearDown(self):
|
|
20
|
+
if self.session_name in ACTIVE_SESSIONS:
|
|
21
|
+
del ACTIVE_SESSIONS[self.session_name]
|
|
22
|
+
session_file = SESSIONS_DIR / f"{self.session_name}.json"
|
|
23
|
+
if session_file.exists():
|
|
24
|
+
try:
|
|
25
|
+
session_file.unlink()
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
def test_session_lifecycle_with_silent_deliberation(self):
|
|
30
|
+
# 1. Create Session
|
|
31
|
+
res = handle_fable_session({
|
|
32
|
+
"action": "create_session",
|
|
33
|
+
"session_name": self.session_name,
|
|
34
|
+
"objective": "Verify frontier uplift protocol",
|
|
35
|
+
"time_budget_minutes": 30.0
|
|
36
|
+
})
|
|
37
|
+
self.assertIn("Fable Cognitive Session Initialized", res)
|
|
38
|
+
self.assertIn("SILENT-DELIBERATION ACTIVE", res)
|
|
39
|
+
|
|
40
|
+
session = ACTIVE_SESSIONS[self.session_name]
|
|
41
|
+
tel = session.get_telemetry()
|
|
42
|
+
self.assertTrue(tel["silent_deliberation_active"])
|
|
43
|
+
self.assertTrue(tel["execution_locked"])
|
|
44
|
+
|
|
45
|
+
# 2. Log Proven Item with valid command evidence
|
|
46
|
+
res = handle_fable_session({
|
|
47
|
+
"action": "log_epistemic_item",
|
|
48
|
+
"session_name": self.session_name,
|
|
49
|
+
"tag": "PROVEN",
|
|
50
|
+
"claim": "Python 3 installed",
|
|
51
|
+
"evidence": "python --version stdout: Python 3.12"
|
|
52
|
+
})
|
|
53
|
+
self.assertIn("Epistemic Item Logged", res)
|
|
54
|
+
self.assertIn("SILENT-DELIBERATION ACTIVE", res)
|
|
55
|
+
|
|
56
|
+
# 3. Log Proven Item with invalid file evidence -> Expect error
|
|
57
|
+
res = handle_fable_session({
|
|
58
|
+
"action": "log_epistemic_item",
|
|
59
|
+
"session_name": self.session_name,
|
|
60
|
+
"tag": "PROVEN",
|
|
61
|
+
"claim": "Bogus claim",
|
|
62
|
+
"evidence": "non_existent_file_xyz_123.py:L10"
|
|
63
|
+
})
|
|
64
|
+
self.assertIn("Error:", res)
|
|
65
|
+
self.assertIn("Epistemic Evidence Validation Failed", res)
|
|
66
|
+
|
|
67
|
+
# 4. Compile Delegation Contract
|
|
68
|
+
valid_contract = """
|
|
69
|
+
### SUBAGENT DELEGATION CONTRACT
|
|
70
|
+
- TargetFile: src/core/engine.py
|
|
71
|
+
- InterfaceContract: def process_event(event: Event) -> bool
|
|
72
|
+
- StrictConstraints: Lock-free, zero heap allocation in loop
|
|
73
|
+
- VerificationCommand: pytest tests/test_engine.py
|
|
74
|
+
"""
|
|
75
|
+
res = handle_fable_session({
|
|
76
|
+
"action": "compile_delegation_contract",
|
|
77
|
+
"session_name": self.session_name,
|
|
78
|
+
"subagent_prompt": valid_contract
|
|
79
|
+
})
|
|
80
|
+
self.assertIn("Subagent Delegation Contract Compiled Successfully", res)
|
|
81
|
+
self.assertIn("READY_FOR_SUBAGENT_DISPATCH", res)
|
|
82
|
+
|
|
83
|
+
# 5. Compile Invalid Contract
|
|
84
|
+
res = handle_fable_session({
|
|
85
|
+
"action": "compile_delegation_contract",
|
|
86
|
+
"session_name": self.session_name,
|
|
87
|
+
"subagent_prompt": "Please write the code"
|
|
88
|
+
})
|
|
89
|
+
self.assertIn("Subagent Delegation Contract Compilation Failed", res)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
unittest.main()
|