@@ -64,6 +64,33 @@ def _journal(effect_id: str) -> dict[str, object]:
6464 }
6565
6666
67+ @pytest .mark .parametrize ("occupied_with_directory" , [True , False ])
68+ def test_read_info_distinguishes_directory_from_unreadable_runtime_metadata (
69+ tmp_path : Path ,
70+ monkeypatch ,
71+ occupied_with_directory : bool ,
72+ ) -> None :
73+ info_path = tmp_path / "runtime-info"
74+ if occupied_with_directory :
75+ info_path .mkdir ()
76+ else :
77+ info_path .write_text ("{}" , encoding = "utf-8" )
78+
79+ original_read_text = Path .read_text
80+
81+ def deny_read (path : Path , * args , ** kwargs ):
82+ if path == info_path :
83+ raise PermissionError ("synthetic denied read" )
84+ return original_read_text (path , * args , ** kwargs )
85+
86+ monkeypatch .setattr (Path , "read_text" , deny_read )
87+ if occupied_with_directory :
88+ assert effect_runtime ._read_info (info_path , fingerprint = "fixture" ) is None
89+ else :
90+ with pytest .raises (effect_runtime .EffectRuntimeHostPermissionError ):
91+ effect_runtime ._read_info (info_path , fingerprint = "fixture" )
92+
93+
6794def _raw_runtime_response (
6895 info : dict [str , object ],
6996 payload : bytes ,
@@ -931,19 +958,32 @@ def terminate(self) -> None:
931958
932959
933960@pytest .mark .parametrize (
934- ("failure" , "expected_code" ),
961+ ("failure" , "simulate_permission_denied" , " expected_code" ),
935962 [
936963 # Windows rejects replacement of an occupied directory with EPERM/EACCES;
937964 # preserve that platform's shared permission diagnostic rather than
938965 # requiring the Unix EISDIR classification.
939- ("directory" , "io_permission_denied" if os .name == "nt" else "io_is_directory" ),
940- ("live_lock" , "mutation_lock_timeout" ),
966+ (
967+ "directory" ,
968+ False ,
969+ "io_permission_denied" if os .name == "nt" else "io_is_directory" ,
970+ ),
971+ # Exercise the Windows Python open-directory error on every platform
972+ # through the production runtime request path, retaining the running
973+ # platform's native filesystem diagnostic from the managed server.
974+ (
975+ "directory" ,
976+ True ,
977+ "io_permission_denied" if os .name == "nt" else "io_is_directory" ,
978+ ),
979+ ("live_lock" , False , "mutation_lock_timeout" ),
941980 ],
942981)
943982def test_locator_publication_failure_surfaces_safe_typed_startup_diagnostic (
944983 tmp_path : Path ,
945984 monkeypatch ,
946985 failure : str ,
986+ simulate_permission_denied : bool ,
947987 expected_code : str ,
948988) -> None :
949989 marker = "private-locator-fixture"
@@ -958,6 +998,16 @@ def test_locator_publication_failure_surfaces_safe_typed_startup_diagnostic(
958998 else :
959999 lock_path .write_text (json .dumps (lock_owner ), encoding = "utf-8" )
9601000
1001+ if simulate_permission_denied :
1002+ original_read_text = Path .read_text
1003+
1004+ def deny_directory_read (path : Path , * args , ** kwargs ):
1005+ if path == info_path :
1006+ raise PermissionError ("synthetic denied directory open" )
1007+ return original_read_text (path , * args , ** kwargs )
1008+
1009+ monkeypatch .setattr (Path , "read_text" , deny_directory_read )
1010+
9611011 captures : list [bytes ] = []
9621012 read_stderr = effect_runtime ._read_startup_stderr
9631013
0 commit comments