1212from pathlib import Path
1313from typing import Literal
1414
15+ import pytest
16+ from pydantic import ValidationError
17+
1518from agents .sandbox import Manifest
1619from agents .sandbox .session import SandboxSessionState
1720from agents .sandbox .snapshot import LocalSnapshot
@@ -27,6 +30,21 @@ class _StubSessionState(SandboxSessionState):
2730 custom_field : str
2831
2932
33+ class _PlainTypeSessionState (SandboxSessionState ):
34+ __test__ = False
35+ type : str = "plain-type"
36+
37+
38+ class _EmptyDefaultSessionState (SandboxSessionState ):
39+ __test__ = False
40+ type : Literal ["" ] = ""
41+
42+
43+ class _SimpleSessionState (SandboxSessionState ):
44+ __test__ = False
45+ type : Literal ["simple-roundtrip" ] = "simple-roundtrip"
46+
47+
3048# ---------------------------------------------------------------------------
3149# Helpers
3250# ---------------------------------------------------------------------------
@@ -93,3 +111,80 @@ def test_model_dump_preserves_snapshot_subclass_fields(self) -> None:
93111 dumped = state .model_dump ()
94112
95113 assert "base_path" in dumped ["snapshot" ]
114+
115+ def test_parse_returns_subclass_instances_as_is (self ) -> None :
116+ state = _make_session_state ()
117+
118+ assert SandboxSessionState .parse (state ) is state
119+
120+ def test_parse_upgrades_base_instance_through_registry (self ) -> None :
121+ state = _SimpleSessionState (
122+ session_id = uuid .UUID ("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" ),
123+ snapshot = LocalSnapshot (id = "snap-1" , base_path = Path ("/tmp/snapshots" )),
124+ manifest = Manifest (),
125+ )
126+ base_instance = SandboxSessionState .model_validate (state .model_dump ())
127+
128+ reconstructed = SandboxSessionState .parse (base_instance )
129+
130+ assert type (reconstructed ) is _SimpleSessionState
131+ assert reconstructed .session_id == uuid .UUID ("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" )
132+
133+ @pytest .mark .parametrize (
134+ ("payload" , "error_type" , "message" ),
135+ [
136+ ({}, ValueError , "must include a string `type`" ),
137+ ({"type" : "missing" }, ValueError , "unknown sandbox session state type `missing`" ),
138+ ("not-a-state" , TypeError , "session state payload must be" ),
139+ ],
140+ )
141+ def test_parse_rejects_invalid_payloads (
142+ self ,
143+ payload : object ,
144+ error_type : type [Exception ],
145+ message : str ,
146+ ) -> None :
147+ with pytest .raises (error_type , match = message ):
148+ SandboxSessionState .parse (payload )
149+
150+ def test_subclass_registration_skips_non_literal_or_empty_type_defaults (self ) -> None :
151+ assert "plain-type" not in SandboxSessionState ._subclass_registry
152+ assert "" not in SandboxSessionState ._subclass_registry
153+
154+ @pytest .mark .parametrize (
155+ ("raw_ports" , "expected" ),
156+ [
157+ (None , ()),
158+ (8080 , (8080 ,)),
159+ ([8080 , 9000 , 8080 ], (8080 , 9000 )),
160+ ],
161+ )
162+ def test_exposed_ports_are_normalized (
163+ self , raw_ports : object , expected : tuple [int , ...]
164+ ) -> None :
165+ state = _StubSessionState (
166+ snapshot = LocalSnapshot (id = "snap-1" , base_path = Path ("/tmp/snapshots" )),
167+ manifest = Manifest (),
168+ custom_field = "my-value" ,
169+ exposed_ports = raw_ports , # type: ignore[arg-type]
170+ )
171+
172+ assert state .exposed_ports == expected
173+
174+ @pytest .mark .parametrize (
175+ ("raw_ports" , "message" ),
176+ [
177+ ("8080" , "exposed_ports must be an iterable" ),
178+ ([8080 , "9000" ], "exposed_ports must contain integers" ),
179+ ([0 ], "exposed_ports entries must be between 1 and 65535" ),
180+ ([65536 ], "exposed_ports entries must be between 1 and 65535" ),
181+ ],
182+ )
183+ def test_exposed_ports_reject_invalid_values (self , raw_ports : object , message : str ) -> None :
184+ with pytest .raises ((TypeError , ValidationError ), match = message ):
185+ _StubSessionState (
186+ snapshot = LocalSnapshot (id = "snap-1" , base_path = Path ("/tmp/snapshots" )),
187+ manifest = Manifest (),
188+ custom_field = "my-value" ,
189+ exposed_ports = raw_ports , # type: ignore[arg-type]
190+ )
0 commit comments