Conversation
tests/test_schema_offline.py calls every public method of the resources, resource_attributes, resource_relations, resource_roles, roles, condition_sets and condition_set_rules APIs through both clients, each closed after the call. It checks the request each sends (method, path, query string, headers and JSON body), the type the response parses into, the Permit*Error an API error response raises, and that with proxy_facts_via_pdp on the requests still go to the API. The 27 schema operations these tests are the first to send are covered now, so their `untested` entries leave the API coverage allowlist (PER-16177). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add offline tests for the 22 facts operations the API coverage report listed as untested: the bulk user, tenant and resource instance operations, single and bulk relationship tuple and role assignment writes, resource instance CRUD, tenants.list_tenant_users() and the user invite methods. Each method is called on the async and the blocking client, with proxy_facts_via_pdp off (the API) and on (the PDP's /facts route, or the API for the user invite methods). The tests check the method, path, query, Authorization, Content-Type and wait-for-sync headers and JSON body, the parsed return type, and that the API's error response raises the matching PermitApiError. Their untested entries leave the allowlist (PER-16177). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add offline tests for every public method of permit.api.projects and permit.api.environments, which covers the 11 project and environment operations the API coverage report listed as untested, the stats one included. Each method is called on the async and the blocking client with an API key of the narrowest level it accepts. The tests check the method, path, query, headers and JSON body, the parsed return type, that the API's error response raises the matching PermitApiError, and that a key one level narrower is refused before anything is sent. Their untested entries leave the allowlist (PER-16177). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_sync_client_multithreading submitted work to a thread pool and never read the futures, so an exception in a thread was lost and the test passed. Its users had random keys out of 1000 and were deleted only when every step before the delete passed. Each thread now creates a user with a unique key, registers its deletion before creating it, reads it back, deletes it and checks it is gone. The main thread reads every thread's result with a time limit, so a thread that raises fails the test. Many threads sharing one client, whose calls all run on its one background loop, are tested as well as a client per thread. The threads are daemons and each client is closed with the same time limit, so a call that never returns fails the test instead of hanging the session (PER-16177). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The offline tests of permit.api.resource_actions and action_groups checked each request's method, path, query and body and the parsed result. They now also check its Authorization and Content-Type headers, check that a 404 with the API's error body raises PermitNotFoundError for every method, and close each client they create (PER-16177). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pin the request permit.elements.login_as() sends, from the async and the blocking client, for keys and for UUIDs: its path, query, headers and JSON body. Check what it returns, the API's ticket with the redirect URL in content, and that a 404 with the API's error body raises PermitNotFoundError (PER-16177). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both units deleted their own untested entries from the API coverage allowlist; the merged file drops all 60 of them and nothing else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API coverage section explains the untested status but not where the wire test that retires one lives. Name the offline modules that hold the wire tests, what they check, and that a new method gets its wire test in the change that adds it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
create_read_delete kept its teardown after deleting the user and checking it was gone, so every passing run sent a second DELETE per user (21 across the three tests), each answered 404 and logged as a tolerated cleanup error. Drop the teardown once the user is confirmed gone; until then it still deletes the user whatever happens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
run_in_threads read the futures one at a time, each with its own time limit, so a thread that hung hid the error a later thread raised: the test failed with a bare TimeoutError. Wait for all the threads together under one limit, then raise the first error any call raised, or a TimeoutError that says how many threads did not return. Waiting for every thread also lets each one clean up before the clients close. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API answers a get or a list of environments with EnvironmentReadWithEmailConfig, which requires email_configuration. The wire tests' mock answers for environments.list, get, get_by_key and get_by_id left it out. Add it to those four, and leave the create, update and copy answers, whose schema is EnvironmentRead, as they are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The schema, facts operations, projects and environments, resource actions and elements wire tests each defined their own invoke, sent_headers, HEADERS, JSON_HEADERS, ApiError and error_details, and two error_details had already drifted to different arguments and bodies. Define them once in tests/utils.py, with the NOT_FOUND, DUPLICATE and FORBIDDEN errors, and import them in those five modules. error_details now returns the API's ErrorDetails shape everywhere. The resource action cases name their methods under permit.api, as the other modules do, so they can use the shared invoke. The resource action and login_as tests now also check that no wait-for-sync header is sent. The collected test ids are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_elements_login_as_sends_canonical_uuid_strings and test_elements_login_as_passes_string_ids_through checked only the JSON body of an async login_as call with UUID ids and with string ids, on a client they never closed. test_login_as_request_and_response checks the same bodies on both clients, with the path, headers and return value. Remove the two, and say next to its ids that a UUID is sent in its canonical hyphenated form, not UUID.hex. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The paragraph on where a method's wire test goes said most of the modules fail test_every_public_method_has_a_case, but the facts operations module holds only the user invite methods to its guard. Name the modules that have that test and what the facts operations module checks instead, mention the shared helpers in tests/utils.py, and say "CI runs the report" so the next paragraph no longer reads as if CI ran a wire test in two places. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dependency Security AuditScanned: pyproject.toml dependencies + dev group, resolved at Python 3.10 (the current resolution, and the lowest versions the published specs permit under each pydantic major) ✅ No known vulnerabilities found. Both the resolved dependency set and the lowest versions the published specs permit are clean at HIGH and CRITICAL. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # tests/test_offline_regressions.py
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zeevmoney
added this pull request to stack #145
October 2, 2026 18:24
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linear issues
untested, a threaded sync-client e2e test that reads every thread's result, and the surfaces the ticket listed as having no tests.Why
The API coverage report listed 60 GA control-plane operations as
untested: an SDK method sends each one, but no offline test did. The threaded sync-client test submitted work to aThreadPoolExecutorand never read the futures, so an exception in a thread was lost and the test passed.What changed
Wire tests (offline, pytest-httpserver). Each case calls the method on the async client (
async with Permit) and the blocking client (with permit.sync.Permit), and closes the client after the call. It checks the exact method, path, query,Authorization,Content-Type, the wait-for-sync headers and the JSON body. It also checks the parsed return type and value, and that the API's error body raises the matchingPermit*Errorwith its status and details.tests/test_schema_offline.py: all 52 public methods ofresources,resource_attributes,resource_relations,resource_roles,roles,condition_setsandcondition_set_rules(91 cases). It tests 404, 409, 400 and 422 errors. Withproxy_facts_via_pdpon, it checks that every request still goes to the API.tests/test_facts_operations_offline.py: bulk users, tenants and resource instances; single and bulk relationship tuple and role assignment writes; resource instance CRUD;tenants.list_tenant_users; and the 5 user invite methods. Each case runs withproxy_facts_via_pdpoff (request goes to the API) and on (request goes to the PDP's/factsroute, served by a second server), and checks that nothing reaches the other server.tests/test_projects_environments_offline.py: all 17 public methods ofprojectsandenvironments, each called with the narrowest API key level it accepts. A further test checks that a key one level narrower raisesPermitContextErrorbefore anything is sent.tests/test_elements_offline.py:elements.login_aswith keys and with UUIDs, its return value includingcontent, and a 404.tests/test_fix_resource_actions.py: adds header checks and a 404 test for each of the 14 resource action and action group methods, and closes the clients it creates.tests/utils.py: holds the helpers these modules share:invoke,sent_headers,HEADERS,JSON_HEADERS,ApiError,error_details(the API'sErrorDetailsshape), and theNOT_FOUND,DUPLICATEandFORBIDDENerrors..github/scripts/api_coverage_allowlist.json: the 60untestedentries are removed. The report fails on a stale entry, so this shows each operation is now covered.tests/test_offline_regressions.py: the two asynclogin_asbody tests are removed;test_elements_offline.pycovers the same bodies on both clients.CONTRIBUTING.md: says where a method's wire test goes, which helpers it uses, and whichtest_every_public_*_has_a_casetest each module has.Threaded sync-client test (
tests/test_sync_client.py, e2e).test_sync_client_multithreadingis replaced by two tests:test_threads_sharing_one_sync_client: 10 threads on one client and its background loop.test_threads_each_with_a_sync_client_of_their_own: 10 clients, one per thread.test_sync_clientstays. Each thread usesunique_key(), registers the user's deletion before creating it, then creates the user, reads it back, deletes it and checks it is gone. The cleanup is dropped once the user is confirmed gone, so a passing run sends one DELETE per user. The main thread waits for all the threads, 300 s at most in all. It then raises the first error a thread raised; if none raised but a thread has not returned, it raises aTimeoutErrorthat says how many did not. Each client is closed under the same limit.Surfaces PER-16177 listed as having no tests. All four already had offline tests:
resource_actionsandresource_action_groups:tests/test_fix_resource_actions.py, 14 methods on both clients. This PR adds header and error checks.tests/test_fix_deprecated_facade.py, 21 methods on both clients. Unchanged.elements.login_as: the facade test covered it; the newtests/test_elements_offline.pychecks the full request and response.No wire test found an SDK bug, so there are no xfails and no
untestedentries kept back.Behaviour changes
None. No file under
permit/changes. Test ids change:tests/test_sync_client.py::test_sync_client_multithreadingis replaced bytest_threads_sharing_one_sync_clientandtest_threads_each_with_a_sync_client_of_their_own, and the twotest_elements_login_as_*tests intests/test_offline_regressions.pyare removed.How it was tested
login_as, and 40 in the resource actions module.API Coveragejob runs it: exit 0. Control-plane GA has 213 operations: 121 covered (61 at the base), 70 excluded, 22 deferred, 0 untested, 0 missing. Each of the 60 operations is covered by tests on both clients.permit/confirmed clean, and each one failed the new tests. They covered wrong paths and methods, a dropped body or query parameter, a wrong return type, routing to the PDP, a dropped or extra header, an access-level change, and 404/409 error mapping. An edit makinglogin_assendUUID.hexfailed both clients' UUID cases.1 of 2 threads did not return within 2s.uv lock --check, pre-commit (all hooks), mypy on both lanes,tests/test_typing_surface.pyon both lanes, the CI script tests (223 passed),pytest --collect-only -m e2e(45 collected), actionlint and zizmor: all pass.Owner actions before merge
tests/test_sync_client.pyagainst the real backend; they have only been run against a local stub.proxy_facts_via_pdpon and a facts sync timeout set should sendX-Wait-TimeoutandX-Timeout-Policyon requests that go to the API (schema, condition set rules, user invites). The API spec does not list these headers there. The tests pin the current behaviour, astests/test_facts_sync_offline.pyalready does. If it is not intended, file a ticket.tests/utils.pyhelpers, and to move their identicalpdp_serverfixtures intotests/conftest.py.🤖 Generated with Claude Code