11"""Unit tests for the Oracle's deterministic machinery.
22
3- No real model: the generator's two model calls (choose a path, grow the code)
4- are driven by small scripted fakes so the loop's logic — path coercion,
5- truncation-vs-error classification, the write→continue→fix progression — is
6- exercised directly and fast. Real generation is covered in test_integration.py.
3+ No real model: the model calls (name a file, grow a bite of code, summarise) are
4+ driven by small scripted fakes so the orchestration — issue shuffling,
5+ issue/source interleaving, the write→continue→fix progression, and accumulating
6+ MULTIPLE files across inspirations — is exercised directly and fast. Real
7+ generation is covered in test_integration.py.
78"""
89from __future__ import annotations
910
10- import ast
11+ import re
1112
1213from oracle import improve
1314
1415
15- # A fake model keyed on which step is asking (we control the prompt text, so
16- # this is test-side dispatch, not production parsing).
17- def oracle_fake (* , path = "src/mechanism.py" , write = "" , cont = "" , fix = "" , reason = "A bold vision" ):
16+ def model (route ):
17+ """Wrap route(messages)->str into a fake generate(), recording calls."""
1818 calls = []
1919
2020 def generate (messages , * , num_predict = None , temperature = None ):
2121 calls .append (messages )
22- u = messages [- 1 ]["content" ]
23- if "Existing Python modules" in u : return path
24- if "electrifying sentence" in u : return reason
25- if "continues from exactly where" in u : return cont
26- if "does not parse" in u : return fix
27- return write
22+ return route (messages )
2823
2924 generate .calls = calls
3025 return generate
@@ -33,6 +28,33 @@ def generate(messages, *, num_predict=None, temperature=None):
3328BIG = 10 ** 9 # effectively no deadline for the fast unit loops
3429
3530
31+ # --- issue shuffling: ALL issues surface, in a wandering order ---------------
32+ def test_collect_issue_list_includes_every_issue (monkeypatch ):
33+ fake = [{"number" : i , "title" : f"t{ i } " , "body" : "" , "labels" : []} for i in range (6 )]
34+ monkeypatch .setattr (improve , "_fetch_issues" , lambda : fake )
35+ items = improve .collect_issue_list ()
36+ assert len (items ) == 6
37+ for i in range (6 ):
38+ assert any (f"#{ i } t{ i } " in it for it in items ) # not just one issue — all of them
39+
40+
41+ def test_collect_issue_list_actually_shuffles (monkeypatch ):
42+ fake = [{"number" : i , "title" : f"t{ i } " , "body" : "" , "labels" : []} for i in range (6 )]
43+ monkeypatch .setattr (improve , "_fetch_issues" , lambda : fake )
44+ orders = set ()
45+ for seed in range (6 ):
46+ improve ._RNG .seed (seed )
47+ items = improve .collect_issue_list ()
48+ orders .add (tuple (int (re .search (r"#(\d+)" , it ).group (1 )) for it in items ))
49+ assert len (orders ) > 1 # different seeds → different orders → it really shuffles
50+
51+
52+ def test_interleave_alternates_then_trails ():
53+ assert improve ._interleave ([1 , 3 ], [2 , 4 ]) == [1 , 2 , 3 , 4 ]
54+ assert improve ._interleave ([1 ], [2 , 4 , 6 ]) == [1 , 2 , 4 , 6 ]
55+ assert improve ._interleave ([], [2 ]) == [2 ]
56+
57+
3658# --- path coercion -----------------------------------------------------------
3759def test_bare_directory_becomes_a_file (scratch ):
3860 scratch ()
@@ -45,145 +67,133 @@ def test_traversal_is_contained(scratch):
4567 assert improve .coerce_to_src ("../../etc/passwd" , "data" ).is_relative_to (improve .SRC_DIR )
4668
4769
48- def test_absolute_path_is_contained (scratch ):
49- scratch ()
50- assert improve .coerce_to_src ("/etc/shadow" , "data" ).is_relative_to (improve .SRC_DIR )
51-
52-
53- # --- content validation ------------------------------------------------------
54- def test_valid_python_passes (scratch ):
55- scratch ()
56- assert improve .content_problems (improve .SRC_DIR / "m.py" , "def f():\n return 1\n " ) == []
57-
58-
70+ # --- content validation + code extraction ------------------------------------
5971def test_broken_python_is_flagged (scratch ):
6072 scratch ()
6173 probs = improve .content_problems (improve .SRC_DIR / "m.py" , "def f():\n return +\n " )
6274 assert len (probs ) == 1 and "syntax error" in probs [0 ].lower ()
6375
6476
65- def test_markdown_is_not_syntax_checked (scratch ):
66- scratch ()
67- assert improve .content_problems (improve .SRC_DIR / "notes.md" , "def not python (((" ) == []
68-
69-
70- # --- code extraction (model wraps in fences despite instructions) ------------
71- def test_strip_code_fenced ():
77+ def test_strip_code_fenced_and_dangling ():
7278 assert improve ._strip_code ("```python\n X = 1\n ```" ) == "X = 1"
73-
74-
75- def test_strip_code_dangling_open_fence ():
7679 assert improve ._strip_code ("```python\n X = 1" ) == "X = 1"
77-
78-
79- def test_strip_code_raw ():
8080 assert improve ._strip_code ("X = 1\n " ) == "X = 1"
8181
8282
83- # --- truncation vs. logic error ---------------------------------------------
84- def test_truncation_detected_for_unclosed_bracket ():
85- code = "x = [1, 2,"
86- assert improve ._is_truncation (code , improve ._syntax_error (code ))
87-
88-
89- def test_truncation_detected_for_unterminated_paren_midfile ():
90- code = "def f(start):\n total = (start +"
91- assert improve ._is_truncation (code , improve ._syntax_error (code ))
92-
83+ def test_truncation_vs_logic_error ():
84+ assert improve ._is_truncation ("x = [1, 2," , improve ._syntax_error ("x = [1, 2," ))
85+ broke = "def f(:\n return 1"
86+ assert not improve ._is_truncation (broke , improve ._syntax_error (broke ))
9387
94- def test_logic_error_is_not_truncation ():
95- code = "def f(:\n return 1" # malformed signature, complete file
96- e = improve ._syntax_error (code )
97- assert e is not None and not improve ._is_truncation (code , e )
9888
99-
100- # --- choose the target file (a separate, constrained call) -------------------
89+ # --- choosing the target (informed by one inspiration) -----------------------
10190def test_choose_target_extracts_path (scratch ):
10291 scratch ({"src/mechanism.py" : "" })
103- t = improve .choose_target (lambda m , ** k : "I shall forge src/mechanism.py today " , "t " , "s " , "i" )
92+ t = improve .choose_target (lambda m , ** k : "src/mechanism.py" , "tree " , ( "issue " , "#1 do x" ), [] )
10493 assert t .name == "mechanism.py" and t .is_relative_to (improve .SRC_DIR )
10594
10695
107- def test_choose_target_can_create_a_new_file (scratch ):
96+ def test_choose_target_can_create_new_file (scratch ):
10897 scratch ({"src/mechanism.py" : "" })
109- t = improve .choose_target (lambda m , ** k : "src/orrery.py" , "t" , "s" , "i" )
110- assert t .name == "orrery.py" and not t .exists () # a NEW module, created later by write
111- assert t .is_relative_to (improve .SRC_DIR )
112-
113-
114- def test_choose_target_defaults_on_junk (scratch ):
115- scratch ()
116- t = improve .choose_target (lambda m , ** k : "no path here!!" , "t" , "s" , "i" )
117- assert t .suffix == ".py" and t .is_relative_to (improve .SRC_DIR )
98+ t = improve .choose_target (lambda m , ** k : "src/orrery.py" , "tree" , ("issue" , "#1" ), [])
99+ assert t .name == "orrery.py" and not t .exists () and t .is_relative_to (improve .SRC_DIR )
118100
119101
120- def test_choose_target_forces_py_extension (scratch ):
102+ def test_choose_target_forces_py_and_defaults_on_junk (scratch ):
121103 scratch ()
122- t = improve .choose_target (lambda m , ** k : "src/notes" , "t" , "s " , "i" )
123- assert t .suffix == ".py"
104+ assert improve .choose_target (lambda m , ** k : "src/notes" , "t" , ( "issue " , "x" ), []). suffix == ".py"
105+ assert improve . choose_target ( lambda m , ** k : "garbage" , "t" , ( "issue" , "x" ), []) .suffix == ".py"
124106
125107
126- # --- grow the code : write → continue (truncated) / fix (broken) - -------------
108+ # --- growing a file : write → continue (truncated) / fix (broken) -------------
127109def test_grow_file_valid_on_first_write (scratch ):
128110 scratch ()
129- target = improve .SRC_DIR / "m.py"
130- g = oracle_fake (write = "VALUE = 42\n " )
131- code , last = improve .grow_file (g , target , "t" , "s" , "i" , deadline = BIG , max_rounds = 4 )
132- assert improve .content_problems (target , code ) == []
133- assert len (g .calls ) == 1 # no repair needed
111+ g = model (lambda m : "VALUE = 42\n " )
112+ code , last = improve .grow_file (g , improve .SRC_DIR / "m.py" , "t" , ("issue" , "x" ), "" ,
113+ deadline = BIG , max_rounds = 4 )
114+ assert improve .content_problems (improve .SRC_DIR / "m.py" , code ) == [] and len (g .calls ) == 1
134115
135116
136117def test_grow_file_continues_a_truncated_draft (scratch ):
137118 scratch ()
138- target = improve .SRC_DIR / "m.py"
139- g = oracle_fake (write = "def tooth_count(start=13):\n total = (start +" ,
140- cont = " 1)\n return total\n " )
141- code , last = improve .grow_file (g , target , "t" , "s" , "i" , deadline = BIG , max_rounds = 4 )
142- assert improve .content_problems (target , code ) == []
119+
120+ def route (m ):
121+ if "continues from exactly where" in m [- 1 ]["content" ]:
122+ return " 1)\n return total\n "
123+ return "def tooth_count(start=13):\n total = (start +"
124+
125+ code , _ = improve .grow_file (model (route ), improve .SRC_DIR / "m.py" , "t" , ("issue" , "x" ), "" ,
126+ deadline = BIG , max_rounds = 4 )
127+ assert improve .content_problems (improve .SRC_DIR / "m.py" , code ) == []
143128 assert "return total" in code
144129
145130
146131def test_grow_file_fixes_a_broken_draft (scratch ):
147132 scratch ()
148- target = improve .SRC_DIR / "m.py"
149- g = oracle_fake (write = "def f(:\n return 1" , fix = "def f():\n return 1\n " )
150- code , last = improve .grow_file (g , target , "t" , "s" , "i" , deadline = BIG , max_rounds = 4 )
151- assert improve .content_problems (target , code ) == []
152- # it took the FIX path (not continue), proving the classification routed correctly
153- assert any ("does not parse" in m [- 1 ]["content" ] for m in g .calls )
154133
134+ def route (m ):
135+ return "def f():\n return 1\n " if "does not parse" in m [- 1 ]["content" ] else "def f(:\n return 1"
155136
156- # --- orchestration -----------------------------------------------------------
157- def test_generate_improvement_end_to_end ( scratch ):
158- scratch ({ "src/mechanism.py" : "" } )
159- g = oracle_fake ( path = "src/mechanism .py" , write = "VALUE = 42 \n " , reason = "Forge the back dial" )
160- reason , files , last , valid = improve . generate_improvement ( g , "t" , "s" , "i" , deadline_seconds = 999 )
161- assert valid and len ( files ) == 1
162- target , code = files [ 0 ]
163- assert target . name == "mechanism.py" and code . strip () == "VALUE = 42"
164- assert reason == "Forge the back dial"
165- written = improve . write_blocks ( files )
166- ast . parse (( improve . SRC_DIR / "mechanism.py" ). read_text ())
167- improve . write_pr_outputs ( reason , written , valid = valid )
168- assert improve . PR_TITLE_PATH . read_text (). strip ()
169- assert "Vision " in improve . PR_BODY_PATH . read_text ()
170-
171-
172- def test_generate_improvement_reports_invalid_when_unrepairable ( scratch ):
173- scratch ()
174- # always-broken write, and continue/fix also broken → never parses
175- g = oracle_fake ( write = "def f(:" , fix = "def f(:" , cont = "def f(:" )
137+ g = model ( route )
138+ code , _ = improve . grow_file ( g , improve . SRC_DIR / "m.py" , "t" , ( "issue" , "x" ), "" ,
139+ deadline = BIG , max_rounds = 4 )
140+ assert improve . content_problems ( improve . SRC_DIR / "m .py" , code ) == []
141+ assert any ( "does not parse" in m [ - 1 ][ "content" ] for m in g . calls ) # took the FIX path
142+
143+
144+ # --- orchestration: MANY files across MANY inspirations ----------------------
145+ def test_generates_multiple_files_across_inspirations ( scratch ):
146+ scratch ({ "src/mech.py" : "X = 1 \n " } )
147+
148+ def route ( m ):
149+ u = m [ - 1 ][ "content" ]
150+ if "electrifying sentence " in u :
151+ return "A vision spanning several files"
152+ if "Pick the ONE file" in u :
153+ return "src/from_issue.py" if "An open issue" in u else "src/from_source.py"
154+ return "VALUE = 1 \n " # every write/fix produces valid code
155+
156+ g = model ( route )
176157 reason , files , last , valid = improve .generate_improvement (
177- g , "t" , "s" , "i" , deadline_seconds = 999 , max_rounds = 3 )
178- assert files and valid is False # ships best effort, flagged not-valid
158+ g , "tree" , [("src/mech.py" , "X = 1\n " )], ["#7 forge a gear" , "#9 wind the dial" ],
159+ deadline_seconds = 999 , max_files = 5 )
160+ rels = {t .name for t , _ in files }
161+ assert {"from_issue.py" , "from_source.py" } <= rels # both issue- and source-driven files
162+ assert len (files ) >= 2 and valid
163+ assert reason == "A vision spanning several files"
164+
165+
166+ def test_respects_max_files (scratch ):
167+ scratch ()
168+ route = lambda m : ("one" if "electrifying" in m [- 1 ]["content" ]
169+ else f"src/f{ len (m )} .py" if "Pick the ONE file" in m [- 1 ]["content" ]
170+ else "V = 1\n " )
171+ # many inspirations, but cap at 1 file
172+ _ , files , _ , _ = improve .generate_improvement (
173+ model (route ), "t" , [], ["#1" , "#2" , "#3" , "#4" ], deadline_seconds = 999 , max_files = 1 )
174+ assert len (files ) == 1
175+
176+
177+ def test_reports_invalid_when_unrepairable (scratch ):
178+ scratch ()
179+
180+ def route (m ):
181+ u = m [- 1 ]["content" ]
182+ if "Pick the ONE file" in u : return "src/broken.py"
183+ if "electrifying sentence" in u : return "a flawed vision"
184+ return "def f(:" # never parses, in any mode
185+
186+ _ , files , _ , valid = improve .generate_improvement (
187+ model (route ), "t" , [], ["#1" ], deadline_seconds = 999 , max_files = 1 , max_rounds = 3 )
188+ assert files and valid is False # ships best effort, flagged invalid
179189
180190
181191def test_deadline_guard_makes_no_model_calls ():
182192 def boom (* a , ** k ):
183193 raise AssertionError ("model called past deadline" )
184- clock = iter ([0 , 100 ]) # now()=0 sets deadline=50; next now()=100 is past it
185- reason , files , last , valid = improve .generate_improvement (
186- boom , "t" , "s " , "i" , deadline_seconds = 50 , now = lambda : next (clock ))
194+ clock = iter ([0 , 100 ]) # now()=0 sets deadline=50; next now()=100 is past it
195+ _ , files , _ , valid = improve .generate_improvement (
196+ boom , "t" , [( "src/a.py " , "x" )], [ "#1" ] , deadline_seconds = 50 , now = lambda : next (clock ))
187197 assert files == [] and valid is False
188198
189199
@@ -195,15 +205,10 @@ def test_ollama_generate_builds_request_and_parses(monkeypatch):
195205 class FakeResp :
196206 def __enter__ (self ): return self
197207 def __exit__ (self , * a ): return False
198- def read (self ): return json .dumps ({"message" : {"content" : "the gear turns" }}).encode ()
199-
200- def fake_urlopen (req , timeout = None ):
201- captured ["url" ] = req .full_url
202- captured ["body" ] = json .loads (req .data )
203- return FakeResp ()
204-
205- monkeypatch .setattr (improve .urllib .request , "urlopen" , fake_urlopen )
206- out = improve .ollama_generate ([{"role" : "user" , "content" : "hi" }], temperature = 0.4 , num_predict = 7 )
207- assert out == "the gear turns"
208- assert captured ["url" ].endswith ("/api/chat" )
209- assert captured ["body" ]["options" ]["num_predict" ] == 7
208+ def read (self ): return json .dumps ({"message" : {"content" : "ok" }}).encode ()
209+
210+ monkeypatch .setattr (improve .urllib .request , "urlopen" ,
211+ lambda req , timeout = None : captured .update (body = json .loads (req .data ),
212+ url = req .full_url ) or FakeResp ())
213+ assert improve .ollama_generate ([{"role" : "user" , "content" : "hi" }], num_predict = 7 ) == "ok"
214+ assert captured ["url" ].endswith ("/api/chat" ) and captured ["body" ]["options" ]["num_predict" ] == 7
0 commit comments