@@ -165,6 +165,105 @@ def test_list_conversion(self):
165165 self .assertEqual (list (qr ), recs )
166166
167167
168+ class TestQueryResultListLikeContract (unittest .TestCase ):
169+ """Contract tests: QueryResult must be substitutable for ``list[Record]``
170+ in the patterns shown in the class docstring, examples, and skill docs.
171+
172+ These tests exist to catch the gap from PR #175 where ``__getitem__`` was
173+ missing despite docs and examples treating ``QueryResult`` as list-like.
174+ They drive the contract from the *caller's* perspective rather than
175+ introspecting which dunders are implemented, so removing any single dunder
176+ breaks at least one assertion here with a clear signal.
177+ """
178+
179+ def _records (self , n = 3 ):
180+ return [Record (id = f"id-{ i } " , table = "account" , data = {"name" : f"R{ i } " }) for i in range (n )]
181+
182+ def test_contract_index_then_field_access (self ):
183+ """Pattern from examples/advanced/fetchxml.py: ``row = result[0]; row.get(...)``."""
184+ qr = QueryResult (self ._records (3 ))
185+ row = qr [0 ]
186+ self .assertEqual (row .get ("name" ), "R0" )
187+
188+ def test_contract_single_loop_field_access (self ):
189+ """Pattern from examples/basic/installation_example.py: ``for r in result: r["name"]``."""
190+ qr = QueryResult (self ._records (3 ))
191+ names = [r ["name" ] for r in qr ]
192+ self .assertEqual (names , ["R0" , "R1" , "R2" ])
193+
194+ def test_contract_first_with_none_guard (self ):
195+ """Pattern recommended by Copilot review: ``result.first()`` with None-check."""
196+ empty = QueryResult ([])
197+ nonempty = QueryResult (self ._records (2 ))
198+ self .assertIsNone (empty .first ())
199+ first = nonempty .first ()
200+ self .assertIsNotNone (first )
201+ self .assertEqual (first .get ("name" ), "R0" ) # type: ignore[union-attr]
202+
203+ def test_contract_truthy_guard (self ):
204+ """Pattern: ``if result: ...`` to skip empty results before indexing."""
205+ if QueryResult (self ._records (1 )):
206+ ok = True
207+ else :
208+ ok = False
209+ self .assertTrue (ok )
210+ self .assertFalse (bool (QueryResult ([])))
211+
212+ def test_contract_len_for_size_check (self ):
213+ """Pattern: ``f"{len(result)} rows"`` in log/print statements."""
214+ self .assertEqual (len (QueryResult (self ._records (7 ))), 7 )
215+ self .assertEqual (len (QueryResult ([])), 0 )
216+
217+ def test_contract_slice_returns_list_like (self ):
218+ """Slicing must yield something that supports iteration and len()."""
219+ qr = QueryResult (self ._records (5 ))
220+ page = qr [1 :4 ]
221+ self .assertEqual (len (page ), 3 )
222+ self .assertEqual ([r .get ("name" ) for r in page ], ["R1" , "R2" , "R3" ])
223+
224+ def test_contract_negative_index (self ):
225+ """``result[-1]`` for "last record" is a common Python idiom."""
226+ qr = QueryResult (self ._records (3 ))
227+ self .assertEqual (qr [- 1 ].get ("name" ), "R2" )
228+
229+ def test_contract_list_conversion_round_trip (self ):
230+ """``list(result)`` must yield the same records iteration yields."""
231+ recs = self ._records (4 )
232+ qr = QueryResult (recs )
233+ self .assertEqual (list (qr ), recs )
234+ self .assertEqual (list (qr ), [r for r in qr ])
235+
236+ def test_contract_iteration_does_not_consume (self ):
237+ """Multiple ``for`` loops over the same result must all see records.
238+
239+ Guards against an accidental refactor to a single-shot iterator.
240+ """
241+ qr = QueryResult (self ._records (3 ))
242+ first_pass = list (qr )
243+ second_pass = list (qr )
244+ self .assertEqual (first_pass , second_pass )
245+ self .assertEqual (len (first_pass ), 3 )
246+
247+ def test_contract_nested_loop_iterates_records_not_fields (self ):
248+ """Regression guard for the bug in installation_example.py (PR #175 review #3).
249+
250+ ``for page in result: for r in page: ...`` would iterate Record keys
251+ if QueryResult were a flat iterable of Records (each Record is also
252+ iterable over its keys). This test makes it explicit that the outer
253+ loop yields Records, not pages — so callers know to use a single loop.
254+ """
255+ qr = QueryResult (self ._records (2 ))
256+ outer = list (qr )
257+ self .assertTrue (all (isinstance (r , Record ) for r in outer ))
258+
259+ def test_contract_records_attribute_is_underlying_list (self ):
260+ """``result.records`` is the documented escape hatch for list-only APIs."""
261+ recs = self ._records (3 )
262+ qr = QueryResult (recs )
263+ self .assertIsInstance (qr .records , list )
264+ self .assertIs (qr .records , recs )
265+
266+
168267class TestExecuteReturnsQueryResult (unittest .TestCase ):
169268 """execute() flat mode returns QueryResult."""
170269
0 commit comments