test: query-parameter drift check, table-driven endpoint tests, shared test kit #186
No reviewers
Labels
No labels
breaking-change
bug
documentation
duplicate
enhancement
good first issue
help wanted
performance
question
wontfix
No milestone
No project
No assignees
4 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/ATProto.NET!186
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "debloat-e1-endpoint-tests"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #180 (pass E, part 1: Lexicon client tests).
What this does
Pass E's L2 (table-driven endpoint tests + query-parameter drift check) and L6
(test kit consolidation), scoped to
tests/ATProtoNet.Tests/{Ozone,Chat/Bsky, Lexicon/App/Bsky,Lexicon/Com/AtProto,Lexicon/Site}. Nosrc/changes: thedrift check found no real bugs (see below). Test-only, no CHANGELOG entry.
L2(a): query-parameter drift check
Lexicon/Upstream/SdkSource.Parameters.cswalks the source the same way theexisting NSID/method scan does: it finds every
var parameters = new XrpcParams()...;(orfilter.ToParams(...)...;) statement, the nearestfollowing XRPC call that uses it, and the literal keys passed to
.Add/.AddAllin between — including keys added inside a conditional block orchained after a
ToParams()helper.LexiconDriftTests.ParamCalls_ KeysAreUpstreamParametersthen checks every key against the upstreamLexicon's
parametersfor that NSID, with aQueryParamAllowlist(currentlyempty) for deliberate exceptions, checked for staleness like the other
allow-lists.
Coverage: 143 call sites, 477 keys, 0 failures. Verified the check actually
catches drift by temporarily renaming a key in
GraphClient.GetFollowersAsync(
sort→sortXX) and confirming the test failed withapp.bsky.graph. getFollowers:sortXX: not an upstream parameter, then reverting.Findings: no real bugs. Every existing query-parameter key already
matches its upstream Lexicon; the allow-list is empty.
L2(b): table-driven theory for simple endpoints
Lexicon/EndpointRequestTests.csis a[Theory]over 53 rows, each aself-contained scenario: script a response, call the endpoint with every
argument, assert the exact HTTP method/NSID/query-or-body (via new
HttpStub. AssertGet/AssertPost, proxy header included where the original testchecked one), and a light check that the response deserializes. It spans
chat.bsky.{actor,notification,group,convo,moderation},app.bsky.{labeler, bookmark,notification,unspecced,embed,graph,draft,ageassurance},com.atproto.{admin,temp},tools.ozone.{queue,report,moderation, communication,team,set,server,signature,setting,safelink}andsite. standard.*'s delete wrappers.Removed test files (nothing left to keep once their tests moved to the table):
Chat/Bsky/Notification/ChatNotificationClientTests.cs(2 tests: getPreferences, putPreferences)Lexicon/App/Bsky/Labeler/LabelerClientTests.cs(2 tests: getServices with/withoutdetailed)Removed test methods, by category (covered by the table row of the same NSID unless noted):
chat.bsky.{actor.deleteAccount, group.withdrawJoinRequest, group.rejectJoinRequest, group.updateJoinRequestsRead, convo.updateAllRead, moderation.updateActorAccess},app.bsky.{notification.updateSeen, graph.muteActor, draft.updateDraft, draft.deleteDraft, bookmark.createBookmark, bookmark.deleteBookmark, notification.unregisterPush},com.atproto.temp.{requestPhoneVerification, revokeAccountCredentials},tools.ozone.{queue.unassignModerator, report.unassignModerator, report.refreshStats, setting.removeOptions, safelink.removeRule},site.standard.*'s 4 delete wrappers (publication/document/subscription/recommend).chat.bsky.actor.getStatus,app.bsky.{labeler.getServices, ageassurance.begin, notification.listActivitySubscriptions, notification.putActivitySubscription, unspecced.getPostThreadV2's defaults-only case, embed.getEmbedExternalView's nothing-resolved case},com.atproto.{admin.searchAccounts, admin.updateAccountSigningKey, temp.checkSignupQueue, temp.dereferenceScope},tools.ozone.{queue.routeReports, queue.deleteQueue, moderation.getEvent, moderation.getRepo, moderation.queryEvents's plain form, moderation.getAccountTimeline, moderation.getReporterStats, communication.*, team.*, set.*, server.getConfig, signature.findRelatedAccounts}.OzoneClientTests.FindRelatedAccounts_QueriesByDidandTypedOzoneClientTests. FindRelatedAccountsAsync_LimitThenCursor_SendsBothcovered the same endpoint from two files (did-only, thenlimit+cursor); one row now covers did+limit+cursor.
What was kept, and why (per-row response checks would either lose real
coverage or duplicate a test that already exists for the right reason):
OzoneModerationToolingTests(repo/record/subject/event variants),OzoneClientTests.QueryStatusesAsync_ReadsSubjectStatuses(ConvoSubject/AccountHosting),ComAtProtoGapTests'sCheckHandleAvailabilityAsync_*(3 tests, the Available/Unavailable/Unknown result variants),TypedBskyClientTests,TypedOzoneClientTests,TypedRepoClientTests(typed-identifier round-trips are these files' whole point),BookmarkClientTests,UnspeccedClientTests,EmbedClientTests,GraphSurfaceTests(thread/embed/bookmark itemvariants).
Enumerate*Asynccursor-following test (Chat, Bsky, Ozone).IdentityClientTests.ResolveIdentityAsync_HandleNotFound_*,TypedOzoneClientTests. GetRepoAsync_InvalidDidInTheResponse_*,TypedRepoClientTests.Response_WithAnInvalidIdentifier_*,ComAtProtoGapTests.ImportRepoAsync_Rejected_*.ComAtProtoGapTests's 3ImportRepoAsync_*stream tests(content-length, stream position, chunked non-seekable),
TypedBskyClientTests.PostAsync_ WritesACanonicalCreatedAt(timestamp canonicalization),VideoUploadTests.UploadVideoAsync_ BacksOffExponentially(its ownXrpcClient— see below).OzoneClientTests. QueryStatusesAsync_Filter_*("querySubjects never existed"),TypedBskyClientTests. ListNotificationsAsync_*("seenAt is deliberately not a parameter"),ConvoClientTests. ProxyHeader_DoesNotAffectOtherXrpcCallsandChatModerationClientTests.Calls_WithoutAClientWideProxy_*(proxy-absence, not proxy-presence),
FeedSurfaceTests.SearchPostsV2Async_SendsEveryFilterUnderItsLexiconName(asserts the full upstream parameter set, bidirectionally — kept as-is rather than reduced to the table's
lighter check),
WireFidelityTests(untouched, per its own docstring: regression tests for #123).L6: test kit
Ozone/OzoneTestClient.cs(hand-rolledRecordingHandler+SentRequest) deleted; its 4 consumers(
OzoneQueueClientTests,OzoneReportClientTests,OzoneToolsClientTests,OzoneModerationToolingTests)moved to
XrpcTestClient.HttpStub+HttpClient+XrpcClienttrios named in the issue — ChatActor/Convo/Group/Moderation/Notification, Labeler, VideoUpload, StandardSite — moved onto
XrpcTestClient,reached through
AtProtoClient's ownChat/Bsky/Siteproperties instead of constructing the Lexiconclient directly. Exception:
VideoUploadTests.UploadVideoAsync_BacksOffExponentiallykeeps its inlineXrpcClient—AtProtoClient's internal test constructor accepts aTimeProviderbut only wires it toSessionManager, not to theXrpcClient's own retry delay, so routing this one test through it would silentlydrop the injected clock. Flagged as a follow-up task (see below); out of scope for
src/here.AtProtoClients named in the issue (TypedBskyClientTests,ComAtProtoGapTests) moved ontoXrpcTestClient. Found and fixed the same pattern in 2 more files in scope not named in the issue(
IdentityClientTests,TypedRepoClientTests) — same anti-pattern, same fix, no test-body changes since everytest in those two checks distinct behavior.
HttpStubgainedLast,AssertCall/AssertGet/AssertPost(method/NSID/query-or-body/proxy in one call,JSON body compared structurally) and a static
Json(object)helper, replacing 12 per-file copies of the samehelpers across
ChatModerationClientTests,GroupClientTests,OzoneClientTestsand others.XrpcTestClientforwards all of them, plus
Fallback(both overloads) andClearRequests.TestSupport/TestIds.ModDidreplaces thedid:plc:ewvi7nxzyoun6zhxrhs64oizliteral 9 files in scoperedeclared as their own
private const string ModDid/DidText(44 across the whole suite;BskyFixtures. AliceDid— used well beyond this PR's scope — now aliases the same constant instead of repeating the literal).Each file's local const still exists (so none of their many interpolated fixture strings needed touching),
just pointed at the shared one.
Verification
python3 <scratchpad>/loc.py origin/main HEAD(buckettests/ATProtoNet.Tests, my only touched area):code 48007 → 47830 (−177), xmldoc 1476 → 1543 (+67, the new theory/kit doc comments), comment 1842 → 1899
(+57, the "covered by EndpointRequestTests" pointers left in place of removed tests), files 291 → 291
(3 deleted, 3 created).
AspireManifestTests, unrelated). Net count is unchanged — 53 new theory rows against roughly that manyindividual
[Fact]s removed — while coverage strengthened: the drift check now checks query-parameter namesfor 143 call sites SDK-wide, which the removed per-endpoint tests only checked one at a time.
dotnet build -c Release -p:EnableSourceControlManagerQueries=false→ 0 warnings, 0 errors.dotnet test tests/ATProtoNet.Tests/ --configuration Release -p:EnableSourceControlManagerQueries=false,run twice: identical both times (0 failed, 5000 passed, 5 skipped).
Deferred
reason (union/typed-identifier round-trip, pagination, error mapping, bespoke encoding, or an explicit
regression comment) — see "What was kept" above. A further pass could still fold some of the "plain lookup
with an exhaustive multi-field response check" tests into lighter table rows, but I judged the marginal
line savings weren't worth the risk of quietly dropping a real assertion during transcription.
ConvoModelsTests,CachingLexiconResolverTests,LexiconResolverTests,LexiconClientTests(uses its ownScriptedHandler, not theHttpStubfamily) and the remaining Lexicon/App/Bsky "Surface" test files(Feed/Graph) were reviewed but left untouched — they're model/resolver machinery or already tight,
union-heavy files, not per-endpoint duplication.
HttpMessageHandler/IDidResolver/TimeProviderfakes, identifier-contract testde-duplication,
XrpcClientProxyTeststheories) are explicitly a later part of Pass E per the brief; theIdentity,Http,Streaming,ServerandSpacestest folders were not touched.src/out of scope here):AtProtoClient's internal 5-arg test constructor doesn't wireits
TimeProviderparameter to theXrpcClient's retry delay, only to session refresh — a background taskwas spawned for this.
🤖 Generated with Claude Code
🤖 Reviewing this PR — the review will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the review runs.
⏳ Held on the OpenCode Go window — the 5-hour window is at 0% (the bot holds at 95%).
This continues automatically after the window resets (2026-09-27T22:24:20.012Z) — nothing to re-trigger.
@claude /review, please. This is part 1 of pass E of #180. It touches tests only. Please check three things:
Lexicon/Upstream/SdkSource.Parameters.cs,LexiconDriftTests.ParamCalls_KeysAreUpstreamParameters). Does it really find every.Add/.AddAllkey per call site? Could it pass vacuously, for example if a regex misses a call shape?EndpointRequestTeststheory. Does each row assert at least what the removed tests asserted? The description lists them by category.HttpStubhelpers andTestIds.🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
✅ Verdict: approve — no blocking issues found.
This is a large, mechanical test-only refactor (table-driven
EndpointRequestTests, a new query-parameter drift check, and consolidation ontoXrpcTestClient/HttpStub), and it's well-executed. I checked the parts of the diff that weren't shown in full (SdkSource.Parameters.cs,HttpStub.cs,XrpcTestClient.cs,TestIds.cs) directly against the PR branch, and they're consistent with the description: the new drift-check scanner is an honestly-documented heuristic (nearest-declaration-to-nearest-call regex walk, not a real parser), which is an acceptable trade-off for test infrastructure since a false negative there just means reduced coverage, not a false test failure. Spot-checked a sample of the moved rows inEndpointRequestTests.csagainst the tests they replaced (argument order, expected query/body strings, proxy headers) and they match the original assertions faithfully — including the trickier ones like the mergedFindRelatedAccountsAsyncdid+limit+cursor row and theModDid/TestIdsconstant consolidation. No functional or security issues found.One small piece of leftover dead code noted below; not blocking.
⏱ 1m 43s · 11 turns · tokens: 102.9k in / 10.5k out (+1223.3k cache-read)
📜 Full session transcript
@ -173,2 +147,3 @@private HttpStub.RecordedRequest AssertGet(string nsid, string query) => _fixture.AssertGet(nsid, query, OzoneProxy);public void Dispose() => _httpClient.Dispose();private HttpStub.RecordedRequest AssertPost(string nsid) => _fixture.AssertPost(nsid, proxy: OzoneProxy);private HttpStub.RecordedRequest AssertPost(string nsid) => _fixture.AssertPost(nsid, proxy: OzoneProxy);is now unused — its only caller,UpdateActorAccessAsync_PostsTheActorAccessAndRef, was removed and moved toEndpointRequestTests. Every remaining test in this file usesAssertGetonly. Worth deleting; doesn't appear to trigger a build warning here so it's not blocking.🔧 Let Claude fix this — tick the box and claude-bot picks up the review above, on this PR branch. Nothing runs until you do.
Thanks. The unused
AssertPosthelper inChatModerationClientTests.csgets deleted in the next pass-E PR, which carries on consolidating these test files. I'm merging this one on the approval.