From 22025e30fdc677e46e5ff6605821c905c44929dc Mon Sep 17 00:00:00 2001 From: Adron Hall Date: Thu, 24 Sep 2026 02:53:52 -0700 Subject: [PATCH] test(contract): classify the last 21 endpoints and cover the new reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final pass over the contract suite now that all 47 parity PRs are on main. The inventory test had been failing since the merge pass began, because it re-derives the endpoint set from the client on every run and the client kept growing. That is the feature working; this makes it green. Three fixes: 1. Two more files had to leave the portable subset. The glob introduced in #185 picks up new Services/ files automatically, which is what makes it safe — and it correctly failed on two that are NOT portable: AiAvailabilityService.cs -> CommunityToolkit.Mvvm GitHubListIndex.cs -> uses AppServices and AppLog Both now excluded, with the reason recorded next to the others. 2. The scanner could not resolve one more call shape: ReadTagsAsync, a private read helper whose name does not start with "Get". GetHelper now matches (Get|Read)[A-Za-z]*Async. Extending the convention rather than renaming production code to suit the scanner — both prefixes unambiguously denote a GET, and HttpMethod still wins when present. 3. Classified the remaining 21 endpoints: blog, list folders, list/document invites, tags, link metadata, weather/location, and the GitHub-backed list refresh. Six read, fifteen skip. The skips are where the judgement is. Every one of them would touch something real: POST /api/blog/subscribe and the two invite POSTs send actual emails to actual addresses; PUT /api/folders/{id} moves a folder and the server does NOT reject a cycle, so a careless move corrupts the tree; DELETE /api/folders/{id} cascades to subfolders; /api/weather and /api/location need coordinates and would assert against a real user's stored profile location; and the invite GETs are owner-only against a list the suite does not own. Added the six read probes the inventory then demanded. Two are deliberately narrow: link-metadata unfurls example.com so it asserts the unfurler answers rather than that some third-party site is up, and tags/autocomplete queries a single letter because an empty q is a 400 and anything longer assumes content. GET api/documents/{documentId} is worth having: the tree payload omits `content` with no opt-in, so this is the only bulk source of a document body and the editor breaks silently if it stops carrying one. Verified: live run: 95/95 passed (was 71 before the merge pass) gated off: 4 passed, 91 skipped, exit 0 app: Debug and Release both clean tray: builds clean Sync.Core: 38/38 Co-Authored-By: Claude Opus 5 --- .../EndpointInventoryTests.cs | 10 +++- .../InterlinedList.Contract.Tests.csproj | 6 ++- .../ReadContractTests.cs | 53 +++++++++++++++++++ .../endpoints.manifest.tsv | 25 +++++++++ 4 files changed, 91 insertions(+), 3 deletions(-) diff --git a/InterlinedList.Contract.Tests/EndpointInventoryTests.cs b/InterlinedList.Contract.Tests/EndpointInventoryTests.cs index 9758a3f..399cad0 100644 --- a/InterlinedList.Contract.Tests/EndpointInventoryTests.cs +++ b/InterlinedList.Contract.Tests/EndpointInventoryTests.cs @@ -26,7 +26,15 @@ public sealed class EndpointInventoryTests private static readonly Regex PathLiteral = new("\"\\$?(api/[^\"]*)\"", RegexOptions.Compiled); private static readonly Regex HttpVerb = new(@"HttpMethod\.(Get|Post|Put|Patch|Delete)", RegexOptions.Compiled); private static readonly Regex Interpolation = new(@"\{Uri\.EscapeDataString\(([A-Za-z0-9_]+)\)\}", RegexOptions.Compiled); - private static readonly Regex GetHelper = new(@"\bGet[A-Za-z]*Async\s*[<(]", RegexOptions.Compiled); + /// + /// A read helper, by name. Get…Async covers the shared plumbing + /// (GetJsonAsync, GetElementAsync, GetStringAsync, + /// GetUserArrayAsync) and most domain helpers; Read…Async + /// covers the ones that read more naturally that way, like + /// ReadTagsAsync. Both unambiguously denote a GET. + /// + private static readonly Regex GetHelper = + new(@"\b(Get|Read)[A-Za-z]*Async\s*[<(]", RegexOptions.Compiled); /// /// A helper whose NAME carries its verb — Get…Async, Put…Async, diff --git a/InterlinedList.Contract.Tests/InterlinedList.Contract.Tests.csproj b/InterlinedList.Contract.Tests/InterlinedList.Contract.Tests.csproj index 4173454..f26252b 100644 --- a/InterlinedList.Contract.Tests/InterlinedList.Contract.Tests.csproj +++ b/InterlinedList.Contract.Tests/InterlinedList.Contract.Tests.csproj @@ -81,8 +81,10 @@ AppLog.cs -> System.Diagnostics, Windows Event Log CredentialStore.cs -> System.Security.Cryptography, DPAPI SessionService.cs -> CommunityToolkit.Mvvm - AppServices.cs -> transitively needs SessionService + AiAvailabilityService.cs -> CommunityToolkit.Mvvm + AppServices.cs -> transitively needs the two above ProfileLocationProvider.cs-> transitively needs SessionService + GitHubListIndex.cs -> uses AppServices and AppLog If you add a file needing WPF, DPAPI, the Event Log or the MVVM toolkit — or one that merely references such a file — exclude it here too. And prefer not to: portability is what lets this suite run on @@ -90,7 +92,7 @@ stayed portable. --> + Exclude="..\InterlinedList\Services\AppLog.cs;..\InterlinedList\Services\CredentialStore.cs;..\InterlinedList\Services\SessionService.cs;..\InterlinedList\Services\AiAvailabilityService.cs;..\InterlinedList\Services\AppServices.cs;..\InterlinedList\Services\ProfileLocationProvider.cs;..\InterlinedList\Services\GitHubListIndex.cs" />