@@ -305,6 +305,111 @@ async def test_save_load_delete(service_type, artifact_service_factory):
305305 )
306306
307307
308+ @pytest .mark .asyncio
309+ @pytest .mark .parametrize (
310+ "service_type" ,
311+ [
312+ ArtifactServiceType .IN_MEMORY ,
313+ ArtifactServiceType .GCS ,
314+ ],
315+ )
316+ @pytest .mark .parametrize ("session_id" , ["user" , "user/x" , "user\\ x" ])
317+ async def test_save_artifact_rejects_reserved_user_as_session_id (
318+ service_type , session_id , artifact_service_factory
319+ ):
320+ """IN_MEMORY and GCS lay session-scoped and user-scoped artifacts out in
321+ the same flat namespace, using the literal segment "user" to mark
322+ user-scoped ones. A session actually named "user" (or starting with "user/")
323+ must be rejected rather than silently colliding with that reserved segment."""
324+ artifact_service = artifact_service_factory (service_type )
325+
326+ with pytest .raises (InputValidationError , match = "reserved value 'user'" ):
327+ await artifact_service .save_artifact (
328+ app_name = "app0" ,
329+ user_id = "user0" ,
330+ session_id = session_id ,
331+ filename = "report.txt" ,
332+ artifact = types .Part (text = "hello" ),
333+ )
334+
335+
336+ @pytest .mark .asyncio
337+ @pytest .mark .parametrize ("session_id" , ["user" , "user/x" , "user\\ x" ])
338+ async def test_file_allows_reserved_user_as_session_id (
339+ session_id ,
340+ artifact_service_factory ,
341+ ):
342+ """Unlike IN_MEMORY and GCS, FILE lays session-scoped artifacts out under
343+ their own `sessions/<id>/` subtree, distinct from the user-scoped
344+ `artifacts/` subtree, so a session literally named "user" cannot collide
345+ with it and is not rejected."""
346+ artifact_service = artifact_service_factory (ArtifactServiceType .FILE )
347+
348+ await artifact_service .save_artifact (
349+ app_name = "app0" ,
350+ user_id = "user0" ,
351+ session_id = session_id ,
352+ filename = "report.txt" ,
353+ artifact = types .Part (text = "hello" ),
354+ )
355+ loaded = await artifact_service .load_artifact (
356+ app_name = "app0" ,
357+ user_id = "user0" ,
358+ session_id = session_id ,
359+ filename = "report.txt" ,
360+ )
361+ assert loaded == types .Part (text = "hello" )
362+
363+
364+ @pytest .mark .asyncio
365+ @pytest .mark .parametrize (
366+ "service_type" ,
367+ [
368+ ArtifactServiceType .IN_MEMORY ,
369+ ArtifactServiceType .GCS ,
370+ ],
371+ )
372+ @pytest .mark .parametrize ("session_id" , ["user" , "user/x" , "user\\ x" ])
373+ async def test_read_and_delete_paths_allow_reserved_user_as_session_id (
374+ service_type , session_id , artifact_service_factory
375+ ):
376+ """Reads and deletes must remain permissive for session IDs named "user" or
377+ starting with "user/", so existing data already stored under that prefix in a
378+ live bucket or memory store remains reachable and removable."""
379+ artifact_service = artifact_service_factory (service_type )
380+
381+ assert (
382+ await artifact_service .list_artifact_keys (
383+ app_name = "app0" , user_id = "user0" , session_id = session_id
384+ )
385+ == []
386+ )
387+ assert (
388+ await artifact_service .load_artifact (
389+ app_name = "app0" ,
390+ user_id = "user0" ,
391+ session_id = session_id ,
392+ filename = "report.txt" ,
393+ )
394+ is None
395+ )
396+ assert (
397+ await artifact_service .list_versions (
398+ app_name = "app0" ,
399+ user_id = "user0" ,
400+ session_id = session_id ,
401+ filename = "report.txt" ,
402+ )
403+ == []
404+ )
405+ await artifact_service .delete_artifact (
406+ app_name = "app0" ,
407+ user_id = "user0" ,
408+ session_id = session_id ,
409+ filename = "report.txt" ,
410+ )
411+
412+
308413@pytest .mark .asyncio
309414async def test_in_memory_loads_nested_artifact_reference (
310415 artifact_service_factory ,
0 commit comments