From 18dc96a46000e3d10b04a9499eb0e5a94b65b590 Mon Sep 17 00:00:00 2001 From: Louis Mandelstam Date: Mon, 21 Sep 2026 11:33:14 +0200 Subject: [PATCH] test: record how TargetProcess answers a child's TeamIteration clear A parent UserStory and a child Task in the same team and the same sprint, with the child's explicit null sent through clear_team_iteration. TargetProcess unsets the field: the verifying re-read carries "TeamIteration": null - the key present and null, so the "a field the re-read does not carry counts as cleared" lenience is not what passed it - and the method returns the re-read. Recorded because no cassette covered it and the unit suite drives a mock transport, so it can only assert what the client does with a discarded null, never whether TargetProcess discards one. The cascade the method guards against did not reproduce, in either of the two configurations a live run reached. A child created with no Team came back with no TeamIteration at all, so there was nothing to cascade onto; the child recorded here was given the sprint explicitly, so the value it cleared was one TargetProcess had stored for it. The case left untested is a child holding a team assignment but never given a sprint of its own, where the value shown against it could be the parent's rather than its own. TeamIterationCascadeError and the prose describing it are deliberately untouched. This records an observation; whether it narrows the documented behaviour is not a question a fixture should settle on its own. Co-Authored-By: Claude Opus 5 (1M context) --- ...ands_on_a_child_of_a_scheduled_parent.yaml | 891 ++++++++++++++++++ tests/integration/test_live_readwrite.py | 119 +++ 2 files changed, 1010 insertions(+) create mode 100644 tests/integration/cassettes/test_live_readwrite/test_clear_team_iteration_lands_on_a_child_of_a_scheduled_parent.yaml diff --git a/tests/integration/cassettes/test_live_readwrite/test_clear_team_iteration_lands_on_a_child_of_a_scheduled_parent.yaml b/tests/integration/cassettes/test_live_readwrite/test_clear_team_iteration_lands_on_a_child_of_a_scheduled_parent.yaml new file mode 100644 index 0000000..9d7f888 --- /dev/null +++ b/tests/integration/cassettes/test_live_readwrite/test_clear_team_iteration_lands_on_a_child_of_a_scheduled_parent.yaml @@ -0,0 +1,891 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - example.tpondemand.com + user-agent: + - python-httpx/0.28.1 + x-request-id: + - b1f11ddde8754f6390315deb82675e18 + method: GET + uri: https://example.tpondemand.com/api/v1/TeamProject?access_token=REDACTED&format=json&take=25&where=Project.Id+eq+49938 + response: + body: + string: '{"Items": [{"ResourceType": "TeamProject", "Id": 501, "StartDate": + "/Date(1789644135000+0200)/", "EndDate": null, "IsProjectAccessed": true, + "IsFullProjectAccess": false, "Team": {"ResourceType": "Team", "Id": 75876, + "Name": "Sanitised Name", "EmojiIcon": null}, "Project": {"ResourceType": + "Project", "Id": 49938, "Name": "Sanitised Name"}}]}' + headers: + cache-control: + - no-store, must-revalidate, proxy-revalidate, no-cache + connection: + - keep-alive + content-length: + - '343' + content-type: + - application/json + date: + - Mon, 21 Sep 2026 09:26:03 GMT + expires: + - Sun, 20 Sep 2026 09:26:03 GMT + server: + - nginx + strict-transport-security: + - max-age=31536000; includeSubDomains + traceparent: + - 00-37ba37ce3fb30594b3948692b1db88c8-d6649b11c39a517c-01 + transfer-encoding: + - chunked + vary: + - Accept-Encoding + via: + - 1.1 kong/3.9.1 + x-api-v1-without-rest: + - 'true' + x-content-type-options: + - nosniff + x-correlationid: + - 09045d8c-f6ac-421e-a63d-ba580a057e5b + x-frame-options: + - SAMEORIGIN + x-kong-proxy-latency: + - '0' + x-kong-request-id: + - ddc3a6788bf83b084566581fe1052250 + x-kong-upstream-latency: + - '17' + x-loaddbreader: + - '1' + x-loadresourcedbreaderdata: + - '0' + x-permitted-cross-domain-policies: + - none + x-sql-stopwatch: + - '9' + x-stopwatch: + - '5' + x-tp-handled-by: + - tp-next + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - example.tpondemand.com + user-agent: + - python-httpx/0.28.1 + x-request-id: + - c400bf44d9994392ae83c106c6e7378a + method: GET + uri: https://example.tpondemand.com/api/v1/TeamIteration?access_token=REDACTED&format=json&take=25&where=Team.Id+eq+75876 + response: + body: + string: '{"Items": [{"ResourceType": "TeamIteration", "Id": 75930, "Name": "Sanitised + Name", "Description": null, "StartDate": "/Date(1790632800000+0200)/", "EndDate": + "/Date(1791842399000+0200)/", "CreateDate": "/Date(1789933054000+0200)/", + "ModifyDate": "/Date(1789933054000+0200)/", "LastCommentDate": null, "Tags": + "", "NumericPriority": 718.0, "EntityVersion": 49796361, "EntityType": {"ResourceType": + "EntityType", "Id": 24, "Name": "Sanitised Name"}, "LastEditor": {"ResourceType": + "GeneralUser", "Id": 1, "FirstName": "Sanitised FirstName", "LastName": "Sanitised + LastName", "Login": "Sanitised Login", "FullName": "Sanitised FullName"}, + "Owner": {"ResourceType": "GeneralUser", "Id": 1, "FirstName": "Sanitised + FirstName", "LastName": "Sanitised LastName", "Login": "Sanitised Login", + "FullName": "Sanitised FullName"}, "Creator": {"ResourceType": "GeneralUser", + "Id": 1, "FirstName": "Sanitised FirstName", "LastName": "Sanitised LastName", + "Login": "Sanitised Login", "FullName": "Sanitised FullName"}, "LastCommentedUser": + null, "Project": null, "LinkedTestPlan": null, "Milestone": null, "Effort": + 0.0, "EffortCompleted": 0.0, "EffortToDo": 0.0, "IsCurrent": false, "Capacity": + 0.0, "Velocity": 0.0, "Duration": 14, "Progress": 0.0, "Units": "h", "CanBeFinished": + false, "Team": {"ResourceType": "Team", "Id": 75876, "Name": "Sanitised Name", + "EmojiIcon": null}, "Release": null, "CustomFields": []}, {"ResourceType": + "TeamIteration", "Id": 75929, "Name": "Sanitised Name", "Description": null, + "StartDate": "/Date(1789423200000+0200)/", "EndDate": "/Date(1790632799000+0200)/", + "CreateDate": "/Date(1789933054000+0200)/", "ModifyDate": "/Date(1789933054000+0200)/", + "LastCommentDate": null, "Tags": "", "NumericPriority": 717.0, "EntityVersion": + 49796360, "EntityType": {"ResourceType": "EntityType", "Id": 24, "Name": "Sanitised + Name"}, "LastEditor": {"ResourceType": "GeneralUser", "Id": 1, "FirstName": + "Sanitised FirstName", "LastName": "Sanitised LastName", "Login": "Sanitised + Login", "FullName": "Sanitised FullName"}, "Owner": {"ResourceType": "GeneralUser", + "Id": 1, "FirstName": "Sanitised FirstName", "LastName": "Sanitised LastName", + "Login": "Sanitised Login", "FullName": "Sanitised FullName"}, "Creator": + {"ResourceType": "GeneralUser", "Id": 1, "FirstName": "Sanitised FirstName", + "LastName": "Sanitised LastName", "Login": "Sanitised Login", "FullName": + "Sanitised FullName"}, "LastCommentedUser": null, "Project": null, "LinkedTestPlan": + null, "Milestone": null, "Effort": 0.0, "EffortCompleted": 0.0, "EffortToDo": + 0.0, "IsCurrent": true, "Capacity": 0.0, "Velocity": 0.0, "Duration": 14, + "Progress": 0.0, "Units": "h", "CanBeFinished": true, "Team": {"ResourceType": + "Team", "Id": 75876, "Name": "Sanitised Name", "EmojiIcon": null}, "Release": + null, "CustomFields": []}, {"ResourceType": "TeamIteration", "Id": 75928, + "Name": "Sanitised Name", "Description": null, "StartDate": "/Date(1788213600000+0200)/", + "EndDate": "/Date(1789423199000+0200)/", "CreateDate": "/Date(1789933052000+0200)/", + "ModifyDate": "/Date(1789933052000+0200)/", "LastCommentDate": null, "Tags": + "", "NumericPriority": 715.0, "EntityVersion": 49796358, "EntityType": {"ResourceType": + "EntityType", "Id": 24, "Name": "Sanitised Name"}, "LastEditor": {"ResourceType": + "GeneralUser", "Id": 1, "FirstName": "Sanitised FirstName", "LastName": "Sanitised + LastName", "Login": "Sanitised Login", "FullName": "Sanitised FullName"}, + "Owner": {"ResourceType": "GeneralUser", "Id": 1, "FirstName": "Sanitised + FirstName", "LastName": "Sanitised LastName", "Login": "Sanitised Login", + "FullName": "Sanitised FullName"}, "Creator": {"ResourceType": "GeneralUser", + "Id": 1, "FirstName": "Sanitised FirstName", "LastName": "Sanitised LastName", + "Login": "Sanitised Login", "FullName": "Sanitised FullName"}, "LastCommentedUser": + null, "Project": null, "LinkedTestPlan": null, "Milestone": null, "Effort": + 0.0, "EffortCompleted": 0.0, "EffortToDo": 0.0, "IsCurrent": false, "Capacity": + 0.0, "Velocity": 0.0, "Duration": 14, "Progress": 0.0, "Units": "h", "CanBeFinished": + true, "Team": {"ResourceType": "Team", "Id": 75876, "Name": "Sanitised Name", + "EmojiIcon": null}, "Release": null, "CustomFields": []}]}' + headers: + cache-control: + - no-store, must-revalidate, proxy-revalidate, no-cache + connection: + - keep-alive + content-length: + - '4190' + content-type: + - application/json + date: + - Mon, 21 Sep 2026 09:26:04 GMT + expires: + - Sun, 20 Sep 2026 09:26:04 GMT + server: + - nginx + strict-transport-security: + - max-age=31536000; includeSubDomains + traceparent: + - 00-77fcf880aae66b72c6e2f9b12843fe26-5c96270079608612-01 + transfer-encoding: + - chunked + vary: + - Accept-Encoding + via: + - 1.1 kong/3.9.1 + x-api-v1-without-rest: + - 'true' + x-content-type-options: + - nosniff + x-correlationid: + - 38316837-1316-4c63-8dec-b9cda92b5433 + x-frame-options: + - SAMEORIGIN + x-kong-proxy-latency: + - '1' + x-kong-request-id: + - c613860009a6b46153b57b144bc0772d + x-kong-upstream-latency: + - '70' + x-loaddbreader: + - '7' + x-loadresourcedbreaderdata: + - '0' + x-permitted-cross-domain-policies: + - none + x-sql-stopwatch: + - '30' + x-stopwatch: + - '61' + x-tp-handled-by: + - tp-next + status: + code: 200 + message: OK +- request: + body: '{"Name":"Synthetic recording cascade parent story","Description":"Synthetic + record created by an integration recording run.","Project":{"Id":49938},"Team":{"Id":75876},"TeamIteration":{"Id":75928}}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '197' + content-type: + - application/json + host: + - example.tpondemand.com + user-agent: + - python-httpx/0.28.1 + x-request-id: + - 1c754e9190f546999915cfa8e8ecb88b + method: POST + uri: https://example.tpondemand.com/api/v1/UserStory?access_token=REDACTED&format=json + response: + body: + string: '{"ResourceType": "UserStory", "Id": 75944, "Name": "Sanitised Name", + "Description": "Sanitised Description", "StartDate": null, "EndDate": null, + "CreateDate": "/Date(1789982764000+0200)/", "ModifyDate": "/Date(1789982764000+0200)/", + "LastCommentDate": null, "Tags": "", "NumericPriority": 64040.32281790183, + "EntityVersion": 49860156, "EntityType": {"ResourceType": "EntityType", "Id": + 4, "Name": "Sanitised Name", "IsUnitInHourOnly": false}, "LastEditor": {"ResourceType": + "GeneralUser", "Id": 1, "FirstName": "Sanitised FirstName", "LastName": "Sanitised + LastName", "Login": "Sanitised Login", "FullName": "Sanitised FullName"}, + "Owner": {"ResourceType": "GeneralUser", "Id": 1, "FirstName": "Sanitised + FirstName", "LastName": "Sanitised LastName", "Login": "Sanitised Login", + "FullName": "Sanitised FullName"}, "Creator": {"ResourceType": "GeneralUser", + "Id": 1, "FirstName": "Sanitised FirstName", "LastName": "Sanitised LastName", + "Login": "Sanitised Login", "FullName": "Sanitised FullName"}, "LastCommentedUser": + null, "Project": {"ResourceType": "Project", "Id": 49938, "Name": "Sanitised + Name", "Process": {"ResourceType": "Process", "Id": 2}}, "LinkedTestPlan": + null, "Milestone": null, "Effort": 0.0, "EffortCompleted": 0.0, "EffortToDo": + 0.0, "Progress": 0.0, "TimeSpent": 0.0, "TimeRemain": 0.0, "LastStateChangeDate": + "/Date(1789982764000+0200)/", "PlannedStartDate": null, "PlannedEndDate": + null, "Units": "h", "Release": null, "Iteration": null, "TeamIteration": {"ResourceType": + "TeamIteration", "Id": 75928, "Name": "Sanitised Name"}, "Team": {"ResourceType": + "Team", "Id": 75876, "Name": "Sanitised Name", "EmojiIcon": null}, "Priority": + {"ResourceType": "Priority", "Id": 5, "Name": "Sanitised Name", "Importance": + 5}, "EntityState": {"ResourceType": "EntityState", "Id": 78, "Name": "Sanitised + Name", "NumericPriority": 78.0}, "ResponsibleTeam": {"ResourceType": "TeamAssignment", + "Id": 82543}, "InitialEstimate": 0.0, "Feature": null, "Build": null, "CustomFields": + []}' + headers: + cache-control: + - no-store, must-revalidate, proxy-revalidate, no-cache + connection: + - keep-alive + content-length: + - '1991' + content-type: + - application/json + date: + - Mon, 21 Sep 2026 09:26:04 GMT + expires: + - Sun, 20 Sep 2026 09:26:04 GMT + server: + - nginx + strict-transport-security: + - max-age=31536000; includeSubDomains + traceparent: + - 00-a8e0a305ed6c698bcc50048f4eafd762-159a612938d9c519-01 + via: + - 1.1 kong/3.9.1 + x-accountlockwaittime: + - '2' + x-api-v1-without-rest: + - 'true' + x-content-type-options: + - nosniff + x-correlationid: + - 795b7777-ba27-4f1c-ace1-0a6527536766 + x-diagnostic-tags: + - create-resource,get-resource + x-frame-options: + - SAMEORIGIN + x-kong-proxy-latency: + - '1' + x-kong-request-id: + - b6d7b1668f6f191cfa4d8768dc7a373d + x-kong-upstream-latency: + - '260' + x-loaddbreader: + - 1;3 + x-loadresourcedbreaderdata: + - 0;0 + x-permitted-cross-domain-policies: + - none + x-sql-stopwatch: + - 1;160 + x-stopwatch: + - '253' + x-tp-handled-by: + - tp-next + status: + code: 201 + message: Created +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - example.tpondemand.com + user-agent: + - python-httpx/0.28.1 + x-request-id: + - be51a61e52b84e7f8e5174b8b106ee42 + method: GET + uri: https://example.tpondemand.com/api/v1/UserStory/75944?access_token=REDACTED&format=json&include=%5BTeamIteration%5D + response: + body: + string: '{"ResourceType": "UserStory", "Id": 75944, "TeamIteration": {"ResourceType": + "TeamIteration", "Id": 75928, "Name": "Sanitised Name"}}' + headers: + cache-control: + - no-store, must-revalidate, proxy-revalidate, no-cache + connection: + - keep-alive + content-length: + - '133' + content-type: + - application/json + date: + - Mon, 21 Sep 2026 09:26:04 GMT + expires: + - Sun, 20 Sep 2026 09:26:04 GMT + server: + - nginx + strict-transport-security: + - max-age=31536000; includeSubDomains + traceparent: + - 00-e320cf295568413e61c53d1c6944e138-327efb04025fa309-01 + transfer-encoding: + - chunked + vary: + - Accept-Encoding + via: + - 1.1 kong/3.9.1 + x-api-v1-without-rest: + - 'true' + x-content-type-options: + - nosniff + x-correlationid: + - cb6d2796-3f1a-444e-9c93-fd321c893625 + x-diagnostic-tags: + - get-resource + x-frame-options: + - SAMEORIGIN + x-kong-proxy-latency: + - '1' + x-kong-request-id: + - 5a7c91755e225d4d5ef1fe4743902514 + x-kong-upstream-latency: + - '7' + x-loaddbreader: + - '0' + x-loadresourcedbreaderdata: + - '0' + x-permitted-cross-domain-policies: + - none + x-sql-stopwatch: + - '3' + x-stopwatch: + - '1' + x-tp-handled-by: + - tp-next + status: + code: 200 + message: OK +- request: + body: '{"Name":"Synthetic recording cascade child task","UserStory":{"Id":75944},"Team":{"Id":75876},"TeamIteration":{"Id":75928}}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '123' + content-type: + - application/json + host: + - example.tpondemand.com + user-agent: + - python-httpx/0.28.1 + x-request-id: + - 7bc404f30b7042d4abd8e4fbdca1aaed + method: POST + uri: https://example.tpondemand.com/api/v1/Task?access_token=REDACTED&format=json + response: + body: + string: '{"ResourceType": "Task", "Id": 75945, "Name": "Sanitised Name", "Description": + null, "StartDate": null, "EndDate": null, "CreateDate": "/Date(1789982764000+0200)/", + "ModifyDate": "/Date(1789982764000+0200)/", "LastCommentDate": null, "Tags": + "", "NumericPriority": 64072.34297931078, "EntityVersion": 49860159, "EntityType": + {"ResourceType": "EntityType", "Id": 5, "Name": "Sanitised Name", "IsUnitInHourOnly": + true}, "LastEditor": {"ResourceType": "GeneralUser", "Id": 1, "FirstName": + "Sanitised FirstName", "LastName": "Sanitised LastName", "Login": "Sanitised + Login", "FullName": "Sanitised FullName"}, "Owner": {"ResourceType": "GeneralUser", + "Id": 1, "FirstName": "Sanitised FirstName", "LastName": "Sanitised LastName", + "Login": "Sanitised Login", "FullName": "Sanitised FullName"}, "Creator": + {"ResourceType": "GeneralUser", "Id": 1, "FirstName": "Sanitised FirstName", + "LastName": "Sanitised LastName", "Login": "Sanitised Login", "FullName": + "Sanitised FullName"}, "LastCommentedUser": null, "Project": {"ResourceType": + "Project", "Id": 49938, "Name": "Sanitised Name", "Process": {"ResourceType": + "Process", "Id": 2}}, "LinkedTestPlan": null, "Milestone": null, "Effort": + 0.0, "EffortCompleted": 0.0, "EffortToDo": 0.0, "Progress": 0.0, "TimeSpent": + 0.0, "TimeRemain": 0.0, "LastStateChangeDate": "/Date(1789982764000+0200)/", + "PlannedStartDate": null, "PlannedEndDate": null, "Units": "h", "Release": + null, "Iteration": null, "TeamIteration": {"ResourceType": "TeamIteration", + "Id": 75928, "Name": "Sanitised Name"}, "Team": {"ResourceType": "Team", "Id": + 75876, "Name": "Sanitised Name", "EmojiIcon": null}, "Priority": {"ResourceType": + "Priority", "Id": 11, "Name": "Sanitised Name", "Importance": 1}, "EntityState": + {"ResourceType": "EntityState", "Id": 88, "Name": "Sanitised Name", "NumericPriority": + 88.0}, "ResponsibleTeam": {"ResourceType": "TeamAssignment", "Id": 82544}, + "UserStory": {"ResourceType": "UserStory", "Id": 75944, "Name": "Sanitised + Name"}, "CustomFields": []}' + headers: + cache-control: + - no-store, must-revalidate, proxy-revalidate, no-cache + connection: + - keep-alive + content-length: + - '1994' + content-type: + - application/json + date: + - Mon, 21 Sep 2026 09:26:05 GMT + expires: + - Sun, 20 Sep 2026 09:26:04 GMT + server: + - nginx + strict-transport-security: + - max-age=31536000; includeSubDomains + traceparent: + - 00-f8656ddbad3ef1ec241fc9916eb0d99f-0017957fdb73a1c8-01 + via: + - 1.1 kong/3.9.1 + x-accountlockwaittime: + - '1' + x-api-v1-without-rest: + - 'true' + x-content-type-options: + - nosniff + x-correlationid: + - f08b19de-dcea-4a15-a2ae-f7854f508ce5 + x-diagnostic-tags: + - create-resource,get-resource + x-frame-options: + - SAMEORIGIN + x-kong-proxy-latency: + - '0' + x-kong-request-id: + - 65ddad46ae1a50679f8bc01300098c60 + x-kong-upstream-latency: + - '155' + x-loaddbreader: + - '2' + x-loadresourcedbreaderdata: + - '0' + x-permitted-cross-domain-policies: + - none + x-sql-stopwatch: + - '62' + x-stopwatch: + - '149' + x-tp-handled-by: + - tp-next + status: + code: 201 + message: Created +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - example.tpondemand.com + user-agent: + - python-httpx/0.28.1 + x-request-id: + - 182e58d3fe0f46a8a6c6e8f1f2249d3a + method: GET + uri: https://example.tpondemand.com/api/v1/Task/75945?access_token=REDACTED&format=json&include=%5BTeamIteration%5D + response: + body: + string: '{"ResourceType": "Task", "Id": 75945, "TeamIteration": {"ResourceType": + "TeamIteration", "Id": 75928, "Name": "Sanitised Name"}}' + headers: + cache-control: + - no-store, must-revalidate, proxy-revalidate, no-cache + connection: + - keep-alive + content-length: + - '128' + content-type: + - application/json + date: + - Mon, 21 Sep 2026 09:26:05 GMT + expires: + - Sun, 20 Sep 2026 09:26:05 GMT + server: + - nginx + strict-transport-security: + - max-age=31536000; includeSubDomains + traceparent: + - 00-2160fe3526bd3d65101585d877b33a7e-2488ea6268329034-01 + transfer-encoding: + - chunked + vary: + - Accept-Encoding + via: + - 1.1 kong/3.9.1 + x-api-v1-without-rest: + - 'true' + x-content-type-options: + - nosniff + x-correlationid: + - 83fda5cb-5951-4d27-9eea-06e451c8310e + x-diagnostic-tags: + - get-resource + x-frame-options: + - SAMEORIGIN + x-kong-proxy-latency: + - '0' + x-kong-request-id: + - cd599300bb9d8d3068a0672819cce119 + x-kong-upstream-latency: + - '15' + x-loaddbreader: + - '1' + x-loadresourcedbreaderdata: + - '0' + x-permitted-cross-domain-policies: + - none + x-sql-stopwatch: + - 1;10 + x-stopwatch: + - '1' + x-tp-handled-by: + - tp-next + status: + code: 200 + message: OK +- request: + body: '{"TeamIteration":null}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '22' + content-type: + - application/json + host: + - example.tpondemand.com + user-agent: + - python-httpx/0.28.1 + x-request-id: + - 214d51efdc3b473689ee142cfbfbff4d + method: POST + uri: https://example.tpondemand.com/api/v1/Task/75945?access_token=REDACTED&format=json + response: + body: + string: '{"ResourceType": "Task", "Id": 75945, "Name": "Sanitised Name", "Description": + null, "StartDate": null, "EndDate": null, "CreateDate": "/Date(1789982764000+0200)/", + "ModifyDate": "/Date(1789982765000+0200)/", "LastCommentDate": null, "Tags": + "", "NumericPriority": 64072.34297931078, "EntityVersion": 49860160, "EntityType": + {"ResourceType": "EntityType", "Id": 5, "Name": "Sanitised Name", "IsUnitInHourOnly": + true}, "LastEditor": {"ResourceType": "GeneralUser", "Id": 1, "FirstName": + "Sanitised FirstName", "LastName": "Sanitised LastName", "Login": "Sanitised + Login", "FullName": "Sanitised FullName"}, "Owner": {"ResourceType": "GeneralUser", + "Id": 1, "FirstName": "Sanitised FirstName", "LastName": "Sanitised LastName", + "Login": "Sanitised Login", "FullName": "Sanitised FullName"}, "Creator": + {"ResourceType": "GeneralUser", "Id": 1, "FirstName": "Sanitised FirstName", + "LastName": "Sanitised LastName", "Login": "Sanitised Login", "FullName": + "Sanitised FullName"}, "LastCommentedUser": null, "Project": {"ResourceType": + "Project", "Id": 49938, "Name": "Sanitised Name", "Process": {"ResourceType": + "Process", "Id": 2}}, "LinkedTestPlan": null, "Milestone": null, "Effort": + 0.0, "EffortCompleted": 0.0, "EffortToDo": 0.0, "Progress": 0.0, "TimeSpent": + 0.0, "TimeRemain": 0.0, "LastStateChangeDate": "/Date(1789982764000+0200)/", + "PlannedStartDate": null, "PlannedEndDate": null, "Units": "h", "Release": + null, "Iteration": null, "TeamIteration": null, "Team": {"ResourceType": "Team", + "Id": 75876, "Name": "Sanitised Name", "EmojiIcon": null}, "Priority": {"ResourceType": + "Priority", "Id": 11, "Name": "Sanitised Name", "Importance": 1}, "EntityState": + {"ResourceType": "EntityState", "Id": 88, "Name": "Sanitised Name", "NumericPriority": + 88.0}, "ResponsibleTeam": {"ResourceType": "TeamAssignment", "Id": 82544}, + "UserStory": {"ResourceType": "UserStory", "Id": 75944, "Name": "Sanitised + Name"}, "CustomFields": []}' + headers: + cache-control: + - no-store, must-revalidate, proxy-revalidate, no-cache + connection: + - keep-alive + content-length: + - '1926' + content-type: + - application/json + date: + - Mon, 21 Sep 2026 09:26:05 GMT + expires: + - Sun, 20 Sep 2026 09:26:05 GMT + server: + - nginx + strict-transport-security: + - max-age=31536000; includeSubDomains + traceparent: + - 00-11fae370b0476906859731b28fef4913-6bc4058481f6c34a-01 + transfer-encoding: + - chunked + vary: + - Accept-Encoding + via: + - 1.1 kong/3.9.1 + x-api-v1-without-rest: + - 'true' + x-content-type-options: + - nosniff + x-correlationid: + - aa9f5c0d-2218-4914-82fd-b3143845e3a1 + x-diagnostic-tags: + - update-resource,get-resource + x-frame-options: + - SAMEORIGIN + x-kong-proxy-latency: + - '1' + x-kong-request-id: + - a9d9bd72bbf7856f6db2162ca4de9542 + x-kong-upstream-latency: + - '124' + x-loaddbreader: + - '2' + x-loadresourcedbreaderdata: + - '0' + x-permitted-cross-domain-policies: + - none + x-sql-stopwatch: + - 2;40 + x-stopwatch: + - '119' + x-tp-handled-by: + - tp-next + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - example.tpondemand.com + user-agent: + - python-httpx/0.28.1 + x-request-id: + - 0323fd8d84134397aaeee82578d20197 + method: GET + uri: https://example.tpondemand.com/api/v1/Task/75945?access_token=REDACTED&format=json&include=%5BTeamIteration%5D + response: + body: + string: '{"ResourceType": "Task", "Id": 75945, "TeamIteration": null}' + headers: + cache-control: + - no-store, must-revalidate, proxy-revalidate, no-cache + connection: + - keep-alive + content-length: + - '60' + content-type: + - application/json + date: + - Mon, 21 Sep 2026 09:26:05 GMT + expires: + - Sun, 20 Sep 2026 09:26:05 GMT + server: + - nginx + strict-transport-security: + - max-age=31536000; includeSubDomains + traceparent: + - 00-f2cbb677f1ddba82c2d00defc89e852c-fe67715224b46c49-01 + transfer-encoding: + - chunked + vary: + - Accept-Encoding + via: + - 1.1 kong/3.9.1 + x-api-v1-without-rest: + - 'true' + x-content-type-options: + - nosniff + x-correlationid: + - 9036399f-bb81-4058-b81d-374f06338ce2 + x-diagnostic-tags: + - get-resource + x-frame-options: + - SAMEORIGIN + x-kong-proxy-latency: + - '0' + x-kong-request-id: + - 28393677c252ff3b79af5f78240ae24d + x-kong-upstream-latency: + - '10' + x-loaddbreader: + - '1' + x-loadresourcedbreaderdata: + - '0' + x-permitted-cross-domain-policies: + - none + x-sql-stopwatch: + - '6' + x-stopwatch: + - '4' + x-tp-handled-by: + - tp-next + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - example.tpondemand.com + user-agent: + - python-httpx/0.28.1 + x-request-id: + - 9b3e8a3c939c4b2bbc4774000fccb309 + method: DELETE + uri: https://example.tpondemand.com/api/v1/Task/75945?access_token=REDACTED&format=json + response: + body: + string: '{"ResourceType": "Task", "Id": 75945}' + headers: + cache-control: + - no-store, must-revalidate, proxy-revalidate, no-cache + connection: + - keep-alive + content-length: + - '37' + content-type: + - application/json + date: + - Mon, 21 Sep 2026 09:26:06 GMT + expires: + - Sun, 20 Sep 2026 09:26:05 GMT + server: + - nginx + strict-transport-security: + - max-age=31536000; includeSubDomains + traceparent: + - 00-d630ed67ef628bf2204a38c59cf01e02-c724699aca52a9c9-01 + transfer-encoding: + - chunked + vary: + - Accept-Encoding + via: + - 1.1 kong/3.9.1 + x-api-v1-without-rest: + - 'true' + x-content-type-options: + - nosniff + x-correlationid: + - 1bab180e-a165-4c66-b396-555ebfa30fc7 + x-diagnostic-tags: + - delete-resource + x-frame-options: + - SAMEORIGIN + x-kong-proxy-latency: + - '0' + x-kong-request-id: + - f9eccf2e73edfa705328d9409688fe4c + x-kong-upstream-latency: + - '125' + x-permitted-cross-domain-policies: + - none + x-sql-stopwatch: + - '90' + x-stopwatch: + - '117' + x-tp-handled-by: + - tp-next + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - example.tpondemand.com + user-agent: + - python-httpx/0.28.1 + x-request-id: + - 6a8594fba33343cf9bd27ea063bc0989 + method: DELETE + uri: https://example.tpondemand.com/api/v1/UserStory/75944?access_token=REDACTED&format=json + response: + body: + string: '{"ResourceType": "UserStory", "Id": 75944}' + headers: + cache-control: + - no-store, must-revalidate, proxy-revalidate, no-cache + connection: + - keep-alive + content-length: + - '42' + content-type: + - application/json + date: + - Mon, 21 Sep 2026 09:26:06 GMT + expires: + - Sun, 20 Sep 2026 09:26:06 GMT + server: + - nginx + strict-transport-security: + - max-age=31536000; includeSubDomains + traceparent: + - 00-441b58fbbefbdb12f872d51d627706a7-5704c41101c26f2c-01 + transfer-encoding: + - chunked + vary: + - Accept-Encoding + via: + - 1.1 kong/3.9.1 + x-api-v1-without-rest: + - 'true' + x-content-type-options: + - nosniff + x-correlationid: + - 5b8dc584-7b2c-4a42-8e54-f502d1bd925c + x-diagnostic-tags: + - delete-resource + x-frame-options: + - SAMEORIGIN + x-kong-proxy-latency: + - '1' + x-kong-request-id: + - 3e65f170f02c4043aec6fd0fc5616e3b + x-kong-upstream-latency: + - '149' + x-permitted-cross-domain-policies: + - none + x-sql-stopwatch: + - '111' + x-stopwatch: + - '140' + x-tp-handled-by: + - tp-next + status: + code: 200 + message: OK +version: 1 diff --git a/tests/integration/test_live_readwrite.py b/tests/integration/test_live_readwrite.py index 2aa5c72..1be8577 100644 --- a/tests/integration/test_live_readwrite.py +++ b/tests/integration/test_live_readwrite.py @@ -408,3 +408,122 @@ async def test_custom_field_set_and_clear_round_trip(live_credentials) -> None: with pytest.raises(NotFoundError): await client.user_stories.get(story.id) + + +@pytest.mark.asyncio +async def test_clear_team_iteration_lands_on_a_child_of_a_scheduled_parent( + live_credentials, +) -> None: + """``clear_team_iteration`` on a child whose parent holds a ``TeamIteration``. + + Recorded to settle what TargetProcess does with a child's explicit + ``null`` while its parent is scheduled, which no cassette covered and no + unit test can answer: the unit suite drives a mock transport, so it + asserts what the client does with a discarded ``null`` rather than whether + TargetProcess discards one. + + On this instance it does not. The child is a Task under a UserStory, both + in the same team and the same sprint; the clear is answered with the field + genuinely unset, and the verifying re-read carries ``"TeamIteration": + null`` - the key *present* and null, so the "a field the re-read does not + carry counts as cleared" lenience is not what passed it. So + ``clear_team_iteration`` returns the re-read rather than raising, and this + test asserts that. + + What this does **not** establish is that the cascade never happens. Two + configurations were tried live and neither produced it: a child created + with no ``Team`` came back with no ``TeamIteration`` at all (nothing to + cascade onto), and the child recorded here was given the sprint + explicitly, so the value it cleared was one TargetProcess had *stored* for + it. The case left untested is a child that holds a team assignment but was + never given a sprint of its own - where a value shown against it could be + the parent's rather than its own, and a ``null`` would have nothing to + unset. ``TeamIterationCascadeError`` and the prose describing it are + deliberately untouched by this test; it records an observation, and + whether that observation narrows the documented behaviour is not a + question a fixture should answer on its own. + + The run also never re-read the *parent* after the child's clear, so the + cassette shows the parent scheduled before the clear and not after. + Nothing in the run writes to the parent in between. + + Two pieces of instance state are read, never written, as everywhere else + on this path. The team comes out of the sandbox project's own + ``TeamProject`` row rather than being named, so the test binds to whatever + is linked at record time; the sprint is that team's lowest-Id + ``TeamIteration``. Both are structural selections, because a cassette + replays every ``Name`` as a placeholder and a lookup by name would find + nothing (``test_live_readwrite_surfaces.py`` states the same rule). + + Re-recording needs a team linked to the sandbox project for the duration + of the run, exactly as ``test_team_assignment_create_and_delete`` does: + TargetProcess will not put a card in a team's sprint unless that team is + assigned to the card's project. Every other run replays offline and needs + no link. + """ + domain, token = live_credentials + async with TargetProcessClient(domain=domain, token=token, mode=ClientMode.READWRITE) as client: + links = [ + link + async for link in client.entities.list( + "TeamProject", where=f"Project.Id eq {_SANDBOX_PROJECT_ID}" + ) + ] + assert links, ( + "the sandbox project has no TeamProject row - link a team to it for the " + "duration of a re-recording run (see this test's docstring)" + ) + # TeamProject is not one of the typed resources, so its references + # arrive as raw wire dicts in ``model_extra`` under their PascalCase + # names rather than as parsed model fields. + team_id = min(links, key=lambda link: link.id).Team["Id"] + + sprints = [ + sprint async for sprint in client.team_iterations.list(where=f"Team.Id eq {team_id}") + ] + assert sprints, f"team {team_id} owns no TeamIteration to schedule the parent into" + sprint = min(sprints, key=lambda sprint: sprint.id) + + parent = await client.user_stories.create( + Name=f"{_NAME_PREFIX} cascade parent story", + Description=_CREATED_DESCRIPTION, + Project={"Id": _SANDBOX_PROJECT_ID}, + Team={"Id": team_id}, + TeamIteration={"Id": sprint.id}, + ) + try: + # Asserted before the child is made: a TeamIteration TP declined + # to store would leave the parent unscheduled, and a clear under an + # unscheduled parent is a different scenario entirely - so this + # fails here rather than as a puzzling pass three calls later. + scheduled = await client.user_stories.get(parent.id, include=["TeamIteration"]) + assert scheduled.team_iteration is not None + assert scheduled.team_iteration.id == sprint.id + + # The child carries the same team and the same sprint as its + # parent. The ``Team`` is not optional: TargetProcess holds a + # card's sprint through a team assignment, so a child created + # without one comes back with no ``TeamIteration`` to clear. + child = await client.tasks.create( + Name=f"{_NAME_PREFIX} cascade child task", + UserStory={"Id": parent.id}, + Team={"Id": team_id}, + TeamIteration={"Id": sprint.id}, + ) + try: + before = await client.tasks.get(child.id, include=["TeamIteration"]) + assert before.team_iteration is not None + assert before.team_iteration.id == sprint.id + + cleared = await client.tasks.clear_team_iteration(child.id) + + # The narrowed re-read, not TP's echo of the write: the echo + # carries EntityVersion and a re-read narrowed to one field + # does not, which is how the two are told apart. + assert cleared.id == child.id + assert cleared.team_iteration is None + assert cleared.entity_version is None + finally: + await client.tasks.delete(child.id) + finally: + await client.user_stories.delete(parent.id)