@@ -118,12 +118,39 @@ def test_delete_unknown_file_404(self, client: TestClient) -> None:
118118 res = client .delete (f"/conversations/{ conversation_id } /files/file_nope" , headers = headers )
119119 assert res .status_code == 404
120120
121+ def test_upload_over_limit_413_and_leaves_no_partial (
122+ self , client : TestClient , monkeypatch : pytest .MonkeyPatch
123+ ) -> None :
124+ """The size cap is enforced while streaming, and the partial file
125+ is removed -- a half-written upload must not linger in the
126+ agent's cwd where it could be mistaken for real input."""
127+ from app .controllers import files as files_controller
128+
129+ monkeypatch .setattr (files_controller , "MAX_UPLOAD_BYTES" , 8 )
130+ headers , conversation_id = _setup (client )
131+ res = client .post (
132+ f"/conversations/{ conversation_id } /files" ,
133+ headers = headers ,
134+ files = {"file" : ("big.xlsx" , b"x" * 64 , "application/octet-stream" )},
135+ )
136+ assert res .status_code == 413 , res .text
137+ workspace = Path (get_settings ().workspace_root ) / conversation_id
138+ assert list (workspace .iterdir ()) == []
139+ detail = client .get (f"/conversations/{ conversation_id } " , headers = headers ).json ()
140+ assert detail ["files" ] == []
141+
142+ def test_artifact_empty_name_is_not_a_listing (self , client : TestClient ) -> None :
143+ """A trailing slash must not be read as "download the workspace"."""
144+ headers , conversation_id = _setup (client )
145+ res = client .get (f"/conversations/{ conversation_id } /artifacts/" , headers = headers )
146+ assert res .status_code in (307 , 400 , 404 ), res .text
147+
121148 def test_run_prompt_includes_uploaded_files (self , client : TestClient ) -> None :
122149 from app .controllers import manager as manager_mod
123150 from app .controllers .runner import ExecResult , Runner
124151
125152 class CapturingRunner (Runner ):
126- captured : list [str ] = []
153+ captured : list [tuple [ str , str ] ] = []
127154
128155 def create (self , user_id : str , conversation_id : str ) -> str :
129156 return "sbx_files"
@@ -132,7 +159,7 @@ def destroy(self, sandbox_id: str) -> None:
132159 pass
133160
134161 def exec_run (self , sandbox_id , prompt , run_id , on_line = None , timeout = None ):
135- CapturingRunner .captured .append (prompt )
162+ CapturingRunner .captured .append (( prompt , run_id ) )
136163 return ExecResult (
137164 exit_code = 0 ,
138165 stdout = '{"type": "result", "answer": "ok", "errors": []}\n ' ,
@@ -165,7 +192,19 @@ def reap_idle(self, ttl_seconds: float) -> list[str]:
165192 continue
166193 break
167194 assert CapturingRunner .captured , "run never executed"
168- assert "sales.xlsx" in CapturingRunner .captured [0 ]
195+ harness_prompt , run_id = CapturingRunner .captured [0 ]
196+ assert "sales.xlsx" in harness_prompt
197+ assert "Save any result files" in harness_prompt
198+ # the Run row keeps the user's verbatim prompt, not the
199+ # augmented harness prompt (the UI echoes it)
200+ from app .models import Run
201+ from app .models .db import get_session_factory
202+
203+ db = get_session_factory ()()
204+ try :
205+ assert db .get (Run , run_id ).prompt == "summarize"
206+ finally :
207+ db .close ()
169208 finally :
170209 monkeypatched .undo ()
171210
0 commit comments