From 1713657087c789faf10b050a35ea9a55b424ad8a Mon Sep 17 00:00:00 2001 From: Ahalya Radhakrishnan Date: Tue, 1 Sep 2026 13:21:06 +0530 Subject: [PATCH 1/7] feat(spec): add updatedAt support for orderBy and projection in query specs --- nisystemlink/clients/spec/models/__init__.py | 1 + .../clients/spec/models/_query_specs.py | 2 + tests/integration/spec/test_spec.py | 52 +++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/nisystemlink/clients/spec/models/__init__.py b/nisystemlink/clients/spec/models/__init__.py index b30499ec..7656ced6 100644 --- a/nisystemlink/clients/spec/models/__init__.py +++ b/nisystemlink/clients/spec/models/__init__.py @@ -16,6 +16,7 @@ from ._query_specs import ( QuerySpecificationsRequest, PagedSpecifications, + SpecificationOrderBy, SpecificationProjection, ) from ._specification import ( diff --git a/nisystemlink/clients/spec/models/_query_specs.py b/nisystemlink/clients/spec/models/_query_specs.py index 1d827b99..024739fa 100644 --- a/nisystemlink/clients/spec/models/_query_specs.py +++ b/nisystemlink/clients/spec/models/_query_specs.py @@ -32,6 +32,7 @@ class SpecificationProjection(str, Enum): WORKSPACE = "WORKSPACE" CREATED_AT = "CREATED_AT" CREATED_BY = "CREATED_BY" + UPDATED_AT = "UPDATED_AT" class SpecificationOrderBy(Enum): @@ -39,6 +40,7 @@ class SpecificationOrderBy(Enum): ID = "ID" SPEC_ID = "SPEC_ID" + UPDATED_AT = "UPDATED_AT" class QuerySpecificationsRequest(JsonModel): diff --git a/tests/integration/spec/test_spec.py b/tests/integration/spec/test_spec.py index a1c0d470..0ef8987f 100644 --- a/tests/integration/spec/test_spec.py +++ b/tests/integration/spec/test_spec.py @@ -16,6 +16,7 @@ NumericConditionValue, QuerySpecificationsRequest, SpecificationLimit, + SpecificationOrderBy, SpecificationProjection, SpecificationType, StringConditionValue, @@ -405,3 +406,54 @@ def test__without_condition_type_projection__query_specs__condition_type_field_i assert "condition_name" in spec_columns assert "condition_unit" in spec_columns assert "condition_type" not in spec_columns + + def test__query_specs_order_by_updated_at__returns_in_ascending_order( + self, client: SpecClient, create_specs, create_specs_for_query, product + ): + request = QuerySpecificationsRequest( + product_ids=[product], + order_by=SpecificationOrderBy.UPDATED_AT, + order_by_descending=False, + ) + + response = client.query_specs(request) + + assert response.specs + assert len(response.specs) == 3 + updated_ats = [spec.updated_at for spec in response.specs if spec.updated_at] + assert updated_ats == sorted(updated_ats) + + def test__query_specs_order_by_updated_at_descending__returns_in_descending_order( + self, client: SpecClient, create_specs, create_specs_for_query, product + ): + request = QuerySpecificationsRequest( + product_ids=[product], + order_by=SpecificationOrderBy.UPDATED_AT, + order_by_descending=True, + ) + + response = client.query_specs(request) + + assert response.specs + assert len(response.specs) == 3 + updated_ats = [spec.updated_at for spec in response.specs if spec.updated_at] + assert updated_ats == sorted(updated_ats, reverse=True) + + def test__query_specs_with_updated_at_projection__returns_updated_at_field( + self, client: SpecClient, create_specs, create_specs_for_query, product + ): + request = QuerySpecificationsRequest( + product_ids=[product], + projection=[SpecificationProjection.UPDATED_AT], + ) + + response = client.query_specs(request) + specs = [vars(spec) for spec in response.specs or []] + non_none_fields = { + key for spec in specs for key, val in spec.items() if val is not None + } + + assert response.specs + assert len(response.specs) == 3 + assert "updated_at" in non_none_fields + assert len(non_none_fields) == 1 From 0e79db3bd6485ac953b8202a1a5635c6ec3431b4 Mon Sep 17 00:00:00 2001 From: Ahalya Radhakrishnan Date: Tue, 1 Sep 2026 15:52:27 +0530 Subject: [PATCH 2/7] test(spec): strengthen updated_at order_by tests to assert deterministic ordering --- tests/integration/spec/test_spec.py | 87 +++++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 10 deletions(-) diff --git a/tests/integration/spec/test_spec.py b/tests/integration/spec/test_spec.py index 0ef8987f..ed5002f3 100644 --- a/tests/integration/spec/test_spec.py +++ b/tests/integration/spec/test_spec.py @@ -408,36 +408,103 @@ def test__without_condition_type_projection__query_specs__condition_type_field_i assert "condition_type" not in spec_columns def test__query_specs_order_by_updated_at__returns_in_ascending_order( - self, client: SpecClient, create_specs, create_specs_for_query, product + self, client: SpecClient, create_specs, product ): + # Create two specs, then update the first so it has a later updated_at than the second. + spec_1_id = uuid.uuid1().hex + spec_2_id = uuid.uuid1().hex + response = create_specs( + CreateSpecificationsRequest( + specs=[ + CreateSpecificationsRequestObject( + product_id=product, + spec_id=spec_1_id, + type=SpecificationType.FUNCTIONAL, + ), + CreateSpecificationsRequestObject( + product_id=product, + spec_id=spec_2_id, + type=SpecificationType.FUNCTIONAL, + ), + ] + ) + ) + spec_1 = next(s for s in response.created_specs if s.spec_id == spec_1_id) + client.update_specs( + UpdateSpecificationsRequest( + specs=[ + UpdateSpecificationsRequestObject( + id=spec_1.id, + product_id=spec_1.product_id, + spec_id=spec_1.spec_id, + type=SpecificationType.PARAMETRIC, + version=spec_1.version, + workspace=spec_1.workspace, + ) + ] + ) + ) + request = QuerySpecificationsRequest( product_ids=[product], order_by=SpecificationOrderBy.UPDATED_AT, order_by_descending=False, ) - response = client.query_specs(request) assert response.specs - assert len(response.specs) == 3 - updated_ats = [spec.updated_at for spec in response.specs if spec.updated_at] - assert updated_ats == sorted(updated_ats) + # spec_2 was not updated, so it has an earlier updated_at — must come first + assert response.specs[0].spec_id == spec_2_id + assert response.specs[1].spec_id == spec_1_id def test__query_specs_order_by_updated_at_descending__returns_in_descending_order( - self, client: SpecClient, create_specs, create_specs_for_query, product + self, client: SpecClient, create_specs, product ): + spec_1_id = uuid.uuid1().hex + spec_2_id = uuid.uuid1().hex + response = create_specs( + CreateSpecificationsRequest( + specs=[ + CreateSpecificationsRequestObject( + product_id=product, + spec_id=spec_1_id, + type=SpecificationType.FUNCTIONAL, + ), + CreateSpecificationsRequestObject( + product_id=product, + spec_id=spec_2_id, + type=SpecificationType.FUNCTIONAL, + ), + ] + ) + ) + spec_1 = next(s for s in response.created_specs if s.spec_id == spec_1_id) + client.update_specs( + UpdateSpecificationsRequest( + specs=[ + UpdateSpecificationsRequestObject( + id=spec_1.id, + product_id=spec_1.product_id, + spec_id=spec_1.spec_id, + type=SpecificationType.PARAMETRIC, + version=spec_1.version, + workspace=spec_1.workspace, + ) + ] + ) + ) + request = QuerySpecificationsRequest( product_ids=[product], order_by=SpecificationOrderBy.UPDATED_AT, order_by_descending=True, ) - response = client.query_specs(request) assert response.specs - assert len(response.specs) == 3 - updated_ats = [spec.updated_at for spec in response.specs if spec.updated_at] - assert updated_ats == sorted(updated_ats, reverse=True) + # spec_1 was updated last, so it has a later updated_at — must come first + assert response.specs[0].spec_id == spec_1_id + assert response.specs[1].spec_id == spec_2_id def test__query_specs_with_updated_at_projection__returns_updated_at_field( self, client: SpecClient, create_specs, create_specs_for_query, product From 95eb2a33635c22d6c0670d75aaa2bccdd39bbd6d Mon Sep 17 00:00:00 2001 From: Ahalya Radhakrishnan Date: Tue, 1 Sep 2026 15:54:06 +0530 Subject: [PATCH 3/7] test(spec): add updated_at to existing projection test instead of separate test --- tests/integration/spec/test_spec.py | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/tests/integration/spec/test_spec.py b/tests/integration/spec/test_spec.py index ed5002f3..270d6fdc 100644 --- a/tests/integration/spec/test_spec.py +++ b/tests/integration/spec/test_spec.py @@ -319,7 +319,11 @@ def test__query_spec_projection_columns__columns_returned( ): request = QuerySpecificationsRequest( product_ids=[product], - projection=[SpecificationProjection.SPEC_ID, SpecificationProjection.NAME], + projection=[ + SpecificationProjection.SPEC_ID, + SpecificationProjection.NAME, + SpecificationProjection.UPDATED_AT, + ], ) response = client.query_specs(request) @@ -330,9 +334,10 @@ def test__query_spec_projection_columns__columns_returned( assert response.specs assert len(response.specs) == 3 - assert len(spec_columns) == 2 + assert len(spec_columns) == 3 assert "spec_id" in spec_columns assert "name" in spec_columns + assert "updated_at" in spec_columns def test__query_specs__returns_condition_value_type_correctly( self, client: SpecClient, create_specs, create_specs_for_query, product @@ -506,21 +511,4 @@ def test__query_specs_order_by_updated_at_descending__returns_in_descending_orde assert response.specs[0].spec_id == spec_1_id assert response.specs[1].spec_id == spec_2_id - def test__query_specs_with_updated_at_projection__returns_updated_at_field( - self, client: SpecClient, create_specs, create_specs_for_query, product - ): - request = QuerySpecificationsRequest( - product_ids=[product], - projection=[SpecificationProjection.UPDATED_AT], - ) - - response = client.query_specs(request) - specs = [vars(spec) for spec in response.specs or []] - non_none_fields = { - key for spec in specs for key, val in spec.items() if val is not None - } - assert response.specs - assert len(response.specs) == 3 - assert "updated_at" in non_none_fields - assert len(non_none_fields) == 1 From a84452e52226e609358301c5d7d7adbe58bc431b Mon Sep 17 00:00:00 2001 From: Ahalya Radhakrishnan Date: Tue, 1 Sep 2026 15:56:24 +0530 Subject: [PATCH 4/7] test(spec): use descriptive variable name in order_by tests --- tests/integration/spec/test_spec.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/spec/test_spec.py b/tests/integration/spec/test_spec.py index 270d6fdc..d68662b2 100644 --- a/tests/integration/spec/test_spec.py +++ b/tests/integration/spec/test_spec.py @@ -434,7 +434,7 @@ def test__query_specs_order_by_updated_at__returns_in_ascending_order( ] ) ) - spec_1 = next(s for s in response.created_specs if s.spec_id == spec_1_id) + spec_1 = next(spec for spec in response.created_specs if spec.spec_id == spec_1_id) client.update_specs( UpdateSpecificationsRequest( specs=[ @@ -483,7 +483,7 @@ def test__query_specs_order_by_updated_at_descending__returns_in_descending_orde ] ) ) - spec_1 = next(s for s in response.created_specs if s.spec_id == spec_1_id) + spec_1 = next(spec for spec in response.created_specs if spec.spec_id == spec_1_id) client.update_specs( UpdateSpecificationsRequest( specs=[ From f1cca10cc04909acbefe813b8f7c67d87f5ff59f Mon Sep 17 00:00:00 2001 From: Ahalya Radhakrishnan Date: Tue, 1 Sep 2026 16:04:56 +0530 Subject: [PATCH 5/7] style(spec): apply black formatting to integration tests --- tests/integration/spec/test_spec.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/integration/spec/test_spec.py b/tests/integration/spec/test_spec.py index d68662b2..cc756ce1 100644 --- a/tests/integration/spec/test_spec.py +++ b/tests/integration/spec/test_spec.py @@ -434,7 +434,9 @@ def test__query_specs_order_by_updated_at__returns_in_ascending_order( ] ) ) - spec_1 = next(spec for spec in response.created_specs if spec.spec_id == spec_1_id) + spec_1 = next( + spec for spec in response.created_specs if spec.spec_id == spec_1_id + ) client.update_specs( UpdateSpecificationsRequest( specs=[ @@ -483,7 +485,9 @@ def test__query_specs_order_by_updated_at_descending__returns_in_descending_orde ] ) ) - spec_1 = next(spec for spec in response.created_specs if spec.spec_id == spec_1_id) + spec_1 = next( + spec for spec in response.created_specs if spec.spec_id == spec_1_id + ) client.update_specs( UpdateSpecificationsRequest( specs=[ @@ -510,5 +514,3 @@ def test__query_specs_order_by_updated_at_descending__returns_in_descending_orde # spec_1 was updated last, so it has a later updated_at — must come first assert response.specs[0].spec_id == spec_1_id assert response.specs[1].spec_id == spec_2_id - - From 2adefa062d2337b434622fbee1b58b9f6c995d1b Mon Sep 17 00:00:00 2001 From: Ahalya Radhakrishnan Date: Tue, 1 Sep 2026 16:25:55 +0530 Subject: [PATCH 6/7] test(spec): assert update_specs succeeds before asserting sort order --- tests/integration/spec/test_spec.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/integration/spec/test_spec.py b/tests/integration/spec/test_spec.py index cc756ce1..11915674 100644 --- a/tests/integration/spec/test_spec.py +++ b/tests/integration/spec/test_spec.py @@ -437,7 +437,7 @@ def test__query_specs_order_by_updated_at__returns_in_ascending_order( spec_1 = next( spec for spec in response.created_specs if spec.spec_id == spec_1_id ) - client.update_specs( + update_response = client.update_specs( UpdateSpecificationsRequest( specs=[ UpdateSpecificationsRequestObject( @@ -451,6 +451,9 @@ def test__query_specs_order_by_updated_at__returns_in_ascending_order( ] ) ) + assert update_response + assert update_response.updated_specs + assert len(update_response.updated_specs) == 1 request = QuerySpecificationsRequest( product_ids=[product], @@ -488,7 +491,7 @@ def test__query_specs_order_by_updated_at_descending__returns_in_descending_orde spec_1 = next( spec for spec in response.created_specs if spec.spec_id == spec_1_id ) - client.update_specs( + update_response = client.update_specs( UpdateSpecificationsRequest( specs=[ UpdateSpecificationsRequestObject( @@ -502,6 +505,9 @@ def test__query_specs_order_by_updated_at_descending__returns_in_descending_orde ] ) ) + assert update_response + assert update_response.updated_specs + assert len(update_response.updated_specs) == 1 request = QuerySpecificationsRequest( product_ids=[product], From 4d14e1efec7869aee5abd7110a901df1d4228c81 Mon Sep 17 00:00:00 2001 From: Ahalya Radhakrishnan Date: Tue, 1 Sep 2026 18:26:01 +0530 Subject: [PATCH 7/7] test(spec): parametrize updated_at order_by tests to remove duplication --- tests/integration/spec/test_spec.py | 79 +++++++---------------------- 1 file changed, 19 insertions(+), 60 deletions(-) diff --git a/tests/integration/spec/test_spec.py b/tests/integration/spec/test_spec.py index 11915674..d9cff32d 100644 --- a/tests/integration/spec/test_spec.py +++ b/tests/integration/spec/test_spec.py @@ -412,8 +412,21 @@ def test__without_condition_type_projection__query_specs__condition_type_field_i assert "condition_unit" in spec_columns assert "condition_type" not in spec_columns - def test__query_specs_order_by_updated_at__returns_in_ascending_order( - self, client: SpecClient, create_specs, product + @pytest.mark.parametrize( + "order_by_descending, expected_first, expected_second", + [ + (False, "spec_2_id", "spec_1_id"), + (True, "spec_1_id", "spec_2_id"), + ], + ) + def test__query_specs_order_by_updated_at__returns_in_expected_order( + self, + client: SpecClient, + create_specs, + product, + order_by_descending: bool, + expected_first: str, + expected_second: str, ): # Create two specs, then update the first so it has a later updated_at than the second. spec_1_id = uuid.uuid1().hex @@ -458,65 +471,11 @@ def test__query_specs_order_by_updated_at__returns_in_ascending_order( request = QuerySpecificationsRequest( product_ids=[product], order_by=SpecificationOrderBy.UPDATED_AT, - order_by_descending=False, - ) - response = client.query_specs(request) - - assert response.specs - # spec_2 was not updated, so it has an earlier updated_at — must come first - assert response.specs[0].spec_id == spec_2_id - assert response.specs[1].spec_id == spec_1_id - - def test__query_specs_order_by_updated_at_descending__returns_in_descending_order( - self, client: SpecClient, create_specs, product - ): - spec_1_id = uuid.uuid1().hex - spec_2_id = uuid.uuid1().hex - response = create_specs( - CreateSpecificationsRequest( - specs=[ - CreateSpecificationsRequestObject( - product_id=product, - spec_id=spec_1_id, - type=SpecificationType.FUNCTIONAL, - ), - CreateSpecificationsRequestObject( - product_id=product, - spec_id=spec_2_id, - type=SpecificationType.FUNCTIONAL, - ), - ] - ) - ) - spec_1 = next( - spec for spec in response.created_specs if spec.spec_id == spec_1_id - ) - update_response = client.update_specs( - UpdateSpecificationsRequest( - specs=[ - UpdateSpecificationsRequestObject( - id=spec_1.id, - product_id=spec_1.product_id, - spec_id=spec_1.spec_id, - type=SpecificationType.PARAMETRIC, - version=spec_1.version, - workspace=spec_1.workspace, - ) - ] - ) - ) - assert update_response - assert update_response.updated_specs - assert len(update_response.updated_specs) == 1 - - request = QuerySpecificationsRequest( - product_ids=[product], - order_by=SpecificationOrderBy.UPDATED_AT, - order_by_descending=True, + order_by_descending=order_by_descending, ) response = client.query_specs(request) + ids = {"spec_1_id": spec_1_id, "spec_2_id": spec_2_id} assert response.specs - # spec_1 was updated last, so it has a later updated_at — must come first - assert response.specs[0].spec_id == spec_1_id - assert response.specs[1].spec_id == spec_2_id + assert response.specs[0].spec_id == ids[expected_first] + assert response.specs[1].spec_id == ids[expected_second]