Repository navigation
Test the ten least-covered classes in Core and AspNetCore (O02) - #328
Conversation
A coverage run (coverlet, the whole suite) put Core and AspNetCore at 84.4% of lines. These tests cover the ten least-covered classes that are more than platform branches or property getters: MachinePolicyLoader, AnswerCapture, SemanticRouterBuilder, KnowledgeGraphMemorySource, TuningApi (the effect of a learned change), ProtocolErrors, StallGuard, OpenAIRequestBuilder, QuotaResetParser and ConfigurationIssues. Together: 85.5%, 100 more tests. Two wrong field paths found in ConfigurationIssues are left out here and fixed in a separate pull request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The profile path came from the first quoted text in the validator's message, which is '@', not the name. And Type.GetType("System.Uri") returns null because Uri is not in CoreLib, so a bad BaseAddress got the generic hint.
siebe41
left a comment
There was a problem hiding this comment.
Needs changes, small
Both real problems are in one new test, A_change_an_hour_old_lists_its_effect… in TuningTests. Both were confirmed by changing the production code in a scratch worktree and running the filtered tests.
Medium: the TuningApi "effect" test only works because two clocks disagree (tests/SemanticRouter.Tests/AspNetCore/TuningTests.cs:253)
- The test sets the injected
ManualTimeProviderto wall-clock minus 3 h, soRouterTunerstamps the changes 3 h ago. - The change only looks an hour old because
TuningApi.Statusignores that provider and readsDateTimeOffset.UtcNow(src/SemanticRouter.AspNetCore/Dashboard/TuningApi.cs:97)./judge/agreementin the same file (:28) already usesRequestServices.GetService<TimeProvider>() ?? TimeProvider.System. - When
Statustakes itsnowfrom the injectedTimeProvider, which is the consistent fix, the test fails witheffect= null (InvalidOperationException … element has type 'Null'), although the behaviour under test is unchanged. So the test pins a clock inconsistency, not the effect calculation. - Fix: make
Statusread the injectedTimeProvider(one line in each of the two/tuningMapGetlambdas). Then, in the test, run the tuner at the manual clock's time andtime.Advance(TimeSpan.FromHours(3))before calling/router/api/tuning. Ifsrc/has to stay untouched in this PR, add a comment at:253saying the test depends onStatususing the wall clock.
Medium: the "and those it rerouted" part of the route assertion is never exercised (TuningTests.cs:277, :289)
- For a route change,
Measurecounts decisions on the change's route or withLearned == change.Id(TuningApi.cs:128). - The only decision with
learned: 2is itself on route"reasoning", so the|| e.Learned == change.Idbranch adds nothing. With that branch deleted, all 12TuningTestsstill pass. - The assertion message at
:289says this case is covered. - Fix: let the learned decision land elsewhere, as a raised
reasoningthreshold would:Judged(Decision(TimeSpan.FromHours(1), "smalltalk", learned: 2));. Both expected tuples stay as they are,(3, 2, 1, 3, 2, 1)and(3, 1, 0, 1, 1, 1), and deleting the branch makes the route assertion fail. Optionally add an unjudged after-change"reasoning"decision, so both halves of the OR are counted.
Low: one StallGuard test can fail under load, despite what the PR body says (tests/SemanticRouter.Tests/AspNetCore/StallGuardTests.cs:14, :45-57)
- The body says these tests use only lower bounds, so "load can make them slower but not fail them". That doesn't hold for
Time_the_client_takes_between_reads_does_not_count. - Every read arms a 100 ms
CancelAfter. Only thefinallyinStallGuard.ReadAsyncresets it, once the inner read returns. - If the test thread is held up for more than 100 ms in that window,
Stalledbecomes true and the test fails. A GC pause or thread-pool starvation on a loaded CI runner can do that. - Fix: give this test its own larger limit (say 500 ms to 1 s, with the pause above it), or correct the body.
nit: MachinePolicyTests.cs:113-114. The JsonObject indexer also returns null for a key that isn't there, so root["Binary"].Should().BeNull() would still pass if FromValues dropped the key. If "present with a null value" is the contract, also assert root.ContainsKey("Binary").
Checked
- The diff is tests only: 10 files under
tests/, nothing insrc/. Per CONTRIBUTING, no CHANGELOG line is needed. - Proved by mutation that these assertions catch the behaviour they name:
- Changing
AnswerCapture.Keep'sbytes.Length > roomto>=failsA_reply_of_exactly_the_limit_is_not_truncated. - Removing StallGuard's
CancelAfter(Timeout.InfiniteTimeSpan)reset failsTime_the_client_takes_between_reads_does_not_count.
- Changing
- From the production code:
- The array-overload StallGuard test would fail if
GuardedStreamdidn't override that overload. - The ProtocolErrors "client is gone" test would fail without the
IOExceptioncatch, sinceGoneStreamoverrides the overload the production code actually calls. - The QuotaResetParser edge cases fail for the right reasons. Feb 30 and Sep 31 hit the
DaysInMonthcheck, the pastOct 8, 9amdoesn't trigger the year bump, and the +02:00 case pins the<=boundary.
- The array-overload StallGuard test would fail if
- Cross-platform:
- The
FileShare.Nonelocked-file test passes on the ubuntu-latest and macos-latest CI runs. - All file I/O goes through
Directory.CreateTempSubdirectory/TempFolderand is cleaned up. - The relative paths in
SemanticRouterBuilderTestsare only resolved, never written. - The default
UseSecretFile()store only callsGetFullPathand creates its protector lazily, so nothing reaches the user's key ring.
- The
- Nothing uses the network, a port or a vendor CLI, and no test changes the current directory.
- The body's "never tested before" claims hold on the base: there were no
AnswerCapturetests and no assertion oneffect. - The coverage numbers in the body were not re-measured.
CI is green on all checks. Nothing pushed.
… its test covers a rerouted decision; the between-reads StallGuard test has a limit load can't trip
…O02-config-issue-paths
|
Review addressed in 96e557d (merged
The body now says that Verified: |
…paths Tie an '@' profile name and a non-text address to the right configuration field (O02)
What changed
Tests, plus one consistency fix in
src/from the review:TuningApi's/tuningand/tuning/changesmeasure a change's age and effect on the router'sTimeProvider(the clockRouterTunerstamps changes with, and the one/judge/agreementalready used) instead ofDateTimeOffset.UtcNow. A coverage run (dotnet test --collect:"XPlat Code Coverage", the whole suite, on Windows) putSemanticRouter.CoreandSemanticRouter.AspNetCoreat 84.4% of lines (20,880 / 24,729). I ranked the hand-written classes by line coverage. Nested and compiler-generated types were folded into their outer type, and theSystem.Text.RegularExpressions.Generatedand*JsonContextsource-generated code was left out. From that list I took the ten least-covered classes that aren't just platform branches or property getters, and added focused tests:MachinePolicyLoadernull, text that only looks like JSON), a missing macOS property list is no policy, and a policy file that exists but can't be read stops the router with an error naming the fileAnswerCaptureLimitbytes are kept,Truncated(never set by any test before), and disposing leaves the response stream openSemanticRouterBuilderUse*/Add*registers what it says over the default: embedding provider by type and by factory, OpenAI-compatible embeddings, evaluation set, model-family and client-turn files (made absolute), the secret file next to the configuration file, custom stores, connectors, memory sources and models; and each refuses a missing argumentKnowledgeGraphMemorySourcememory.jsonlbeforememory.json, and the errors for a missing file or a folder without a graphTuningApiProtocolErrorsStallGuardReadAsyncoverloads); time the client takes between reads doesn't count; a cancelled request isn't a stall; headers and the whole body come through; synchronous reads pass through unguarded; the duration textOpenAIRequestBuildertool_choicefrom OpenAI (none,required, named) and from Anthropic (none,any,tool);top_p,stop,user;json_objectandjson_schema(strict, default name and schema); a tool schema that doesn't parse is written as any objectQuotaResetParser(Core)(UTC)/(GMT)/unknown zones, a zero relative reset, andRetry-AfterHTTP dates in the past and futureConfigurationIssuesOverall: 85.5% (21,155 / 24,729), 1,562 tests (100 new). I also gave
TuningTests.StartTunedAsyncan optionalTimeProvider, so a test can make changes in the past.Left out of the ten, and why:
CliScratch(36.8%) andOwnerOnly(73.7%): the missed lines are the Unix-only paths. Their tests exist (CliScratchTestsand others) and return early on Windows, so a Windows run can't see them. Linux and macOS CI run them.TuningOptions(70.5%): the missed lines are property getters.ServiceHealth(75.5%): what's missed reads the machine's network interfaces and process start time, with no seam for a test.Why
Overflow task O02 of the usage sprint: find the least-covered non-trivial classes in Core and AspNetCore (protocol translation, routing decisions, budgets, secrets) and add focused tests before 1.0. Most of these classes were tested only end to end, so their error and edge paths (stalls, aborted streams, error envelopes, reset-time edge cases, builder registrations) never ran.
How it was verified
dotnet build -c Release: 0 warnings, 0 errors. Two analyzer findings on the new tests were fixed; one is suppressed where the test calls the arrayWriteAsyncoverload on purpose (CA1835).dotnet test -c Release --filter "…ConfigurationIssuesTests|…QuotaResetParserTests|…AnswerCaptureTests|…StallGuardTests|…ProtocolErrorsTests|…OpenAIRequestBuilderTests|…KnowledgeGraphMemorySourceTests|…MachinePolicyTests|…SemanticRouterBuilderTests|…TuningTests": 136 passed.ChatProtocolTests.A_large_tool_result_is_searched_in_bounded_timeand two socket-bound host tests (LifecycleTests…restarts_the_router…,ServiceTests…address_in_use). They time out when the other sprint lanes load this PC, they test code this PR doesn't touch, and all 3 pass when run alone.dotnet format --verify-no-changes: clean.dotnet pack.Linux and macOS (this PC is Windows):
MachinePolicyTests.A_policy_file_that_cannot_be_read…holds the file open withFileShare.None. On Unix, .NET turns that into an exclusive advisoryflock, andFile.ReadAllText's shared lock is refused with anIOException: the same failure as on Windows, which is what the loader turns intoInvalidDataException. If a CI file system ignoresflock(NFS, orDOTNET_SYSTEM_IO_DISABLEFILELOCKINGset), this test is the one to look at.StallGuardTestsuses real timers. The stall tests need a 100 ms limit to fire, which load can only delay. The between-reads test arms a limit on each read that is reset only when the read returns, so a thread held up past the limit would trip it: it uses its own 1 s limit (for a read whose data is already waiting) and a 1.5 s client pause that must not count.Path.GetFullPath/Path.Combineonly. The time zone tests use a custom+02:00zone and a zone id that exists on no OS.Not done
ConfigurationIssuesthat these tests found. Each is fixed, with its regression test, in a separate PR stacked on this one (Tie an '@' profile name and a non-text address to the right configuration field (O02) #329), as the task asks:@is tied toProfiles.@instead of the profile: the validator's message quotes'@'before the name, and the first quoted text was taken."BaseAddress": 5) says "is not valid here" instead of the address hint:Type.GetType("System.Uri")returns null, becauseUriisn't in CoreLib.MachinePolicyLoader's Windows registry path (HKLM) and macOSplutilpath stay untested. The first needs a machine-wide registry key, which the sprint rules don't allow on this PC; the second needs macOS.src/change is not user-visible (outside tests, the router'sTimeProvideris the system clock).Refs: usage-sprint overflow task O02 (no issue).
Latest integration: merged current main and updated the remaining test calls to its new EvaluationCase, ConfigurationIssue and classifier dependency APIs. The focused coverage/API tests pass (60/60), and changed-file formatting is clean.
Full-suite follow-up: all 1,593 tests passed on the integrated branch (
OPENAI_API_KEYunset for test isolation).