@@ -336,3 +336,141 @@ def test_exec_request_args_defaults_none(self):
336336 lang = "py" ,
337337 )
338338 assert request .args is None
339+
340+
341+ class TestUploadedFileStateRestoration :
342+ """Tests for uploaded file state restoration behavior.
343+
344+ Uploaded files should share the same behavior as generated files:
345+ - After first use in execution, they get a state_hash
346+ - On subsequent use with restore_state=true, that state is restored
347+ """
348+
349+ def test_uploaded_file_no_initial_state_hash (self ):
350+ """Test that uploaded file has no state_hash initially."""
351+ file_info = FileInfo (
352+ file_id = "uploaded-file-123" ,
353+ filename = "data.csv" ,
354+ size = 1024 ,
355+ content_type = "text/csv" ,
356+ created_at = datetime .now (timezone .utc ),
357+ path = "/data.csv" ,
358+ # No state_hash, execution_id, or last_used_at
359+ )
360+ assert file_info .state_hash is None
361+ assert file_info .execution_id is None
362+ assert file_info .last_used_at is None
363+
364+ def test_uploaded_file_gets_state_hash_after_use (self ):
365+ """Test that uploaded file gets state_hash after being used in execution."""
366+ now = datetime .now (timezone .utc )
367+
368+ # Simulate file before use
369+ file_before = FileInfo (
370+ file_id = "uploaded-file-123" ,
371+ filename = "data.csv" ,
372+ size = 1024 ,
373+ content_type = "text/csv" ,
374+ created_at = now ,
375+ path = "/data.csv" ,
376+ )
377+ assert file_before .state_hash is None
378+
379+ # Simulate file after use (update_file_state_hash was called)
380+ file_after = FileInfo (
381+ file_id = "uploaded-file-123" ,
382+ filename = "data.csv" ,
383+ size = 1024 ,
384+ content_type = "text/csv" ,
385+ created_at = now ,
386+ path = "/data.csv" ,
387+ state_hash = "abc123def456" ,
388+ execution_id = "exec-789" ,
389+ last_used_at = now ,
390+ )
391+ assert file_after .state_hash == "abc123def456"
392+ assert file_after .execution_id == "exec-789"
393+ assert file_after .last_used_at == now
394+
395+ @pytest .mark .asyncio
396+ async def test_update_file_state_hash_works_for_uploaded_files (self ):
397+ """Test that update_file_state_hash works on uploaded files."""
398+ from src .services .file import FileService
399+ from unittest .mock import AsyncMock , MagicMock
400+
401+ mock_redis = AsyncMock ()
402+ mock_redis .hset = AsyncMock ()
403+
404+ mock_minio = MagicMock ()
405+
406+ service = FileService .__new__ (FileService )
407+ service .redis_client = mock_redis
408+ service .minio_client = mock_minio
409+ service .bucket_name = "test-bucket"
410+
411+ # Call update_file_state_hash (simulating what happens after execution)
412+ result = await service .update_file_state_hash (
413+ session_id = "session-123" ,
414+ file_id = "uploaded-file-456" , # This is an uploaded file
415+ state_hash = "statehash789" ,
416+ execution_id = "exec-abc" ,
417+ )
418+
419+ assert result is True
420+ mock_redis .hset .assert_called_once ()
421+
422+ # Verify the updates include all state fields
423+ call_args = mock_redis .hset .call_args
424+ mapping = call_args [1 ]["mapping" ]
425+ assert mapping ["state_hash" ] == "statehash789"
426+ assert mapping ["execution_id" ] == "exec-abc"
427+ assert "last_used_at" in mapping
428+
429+ def test_restore_state_flag_works_with_state_hash (self ):
430+ """Test that RequestFile with restore_state=True works when file has state_hash."""
431+ from src .models .exec import RequestFile
432+
433+ # Uploaded file reference with restore_state flag
434+ file_ref = RequestFile (
435+ id = "uploaded-file-123" ,
436+ session_id = "session-456" ,
437+ name = "data.csv" ,
438+ restore_state = True , # Request state restoration
439+ )
440+ assert file_ref .restore_state is True
441+
442+ def test_restore_state_requires_state_hash_to_be_set (self ):
443+ """Test that state restoration requires file to have state_hash.
444+
445+ This documents expected behavior: if an uploaded file hasn't been used
446+ yet (no state_hash), restore_state=True is effectively ignored until
447+ the file is used in an execution.
448+ """
449+ # File with no state_hash (never used in execution)
450+ file_info_no_state = FileInfo (
451+ file_id = "uploaded-file-123" ,
452+ filename = "data.csv" ,
453+ size = 1024 ,
454+ content_type = "text/csv" ,
455+ created_at = datetime .now (timezone .utc ),
456+ path = "/data.csv" ,
457+ )
458+
459+ # The mount logic checks: file_info.state_hash is truthy
460+ # For uploaded files that haven't been used, this will be None/False
461+ can_restore = bool (file_info_no_state .state_hash )
462+ assert can_restore is False
463+
464+ # After first use, file has state_hash
465+ file_info_with_state = FileInfo (
466+ file_id = "uploaded-file-123" ,
467+ filename = "data.csv" ,
468+ size = 1024 ,
469+ content_type = "text/csv" ,
470+ created_at = datetime .now (timezone .utc ),
471+ path = "/data.csv" ,
472+ state_hash = "abc123def456" ,
473+ )
474+
475+ can_restore_now = bool (file_info_with_state .state_hash )
476+ assert can_restore_now is True
0 commit comments