feat: Phase 2 · 3/7 — the settings screen #18
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/p2-settings-ui"
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?
Cut 3 of the Phase 2 plan (#13): the screen over cut 2's use cases. The header menu (Settings + guarded sign-out) already existed from Phase 0; this fills the page behind it.
What's in
MaskedSecretField: validated bySaveOpenRouterKeybefore anything is written, never shown back (mask + Replace once saved), the account facts (label, usage/limit, tier) confirm a successful save, and the provider's reason surfaces on a rejected one. Saving a key immediately unlocks the catalog for the Agents tab.Comboboxper roster agent. The default rides as a pseudo-option (Default — <model>, id""— impossible for a real slug), so clearing is just another pick; choices auto-save (ADR 0022); a vision-required agent's list offers only vision-capable models (with the vision badge), mirroring the constraint the use case enforces server-side; a stored-but-delisted model still shows as a stand-in. Without a key the pickers are disabled with a plain explanation.?tab=provider|agents|appearance), only the open panel mounts.keyandsmart_toytab icons are new catalog entries (authentic Material Symbols outlined 24px path data); the submodule pin moves onto that commit. The pin becomes main-reachable once that PR merges — sequence: merge Kagaku.UI #2 first, then this.Tests (+8 page tests → 291 total, all green)
SettingsPageTestsnow runs onAdapterTestContext(real use cases over the strict fakes;FakeAppSettingsStore/FakeLlmGatewaywired into the context): tab order with Provider first; no-key state (entry field, disabled pickers, the why); valid save → stored via the secret path + confirmation + catalog fetched + mask (never shown back); invalid save → provider's reason, nothing stored; vision filter per row (vision-required excludes the text model, free-choice offers it, default pseudo-option present); picking saves for exactly that agent; picking the default clears the row; agents deep link mounts alone; appearance deep link renders the three theme modes.Browser-verified (seeded world, Playwright)
data-theme+ persistskagaku-theme; deep link?tab=agentsopens the right panel cold; both new icons render.🤖 Generated with Claude Code
Summary
Summary
Coverage
Orihon.BlazorAdapter - 82.5%
Orihon.Domain - 100%
Orihon.Infrastructure - 99%
Orihon.Kernel - 90.9%
Orihon.Server - 92%
Orihon.UseCases - 99.4%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh my~ the settings screen, finally filled in behind that lonely header menu from Phase 0! And what a pretty fill it is — three deep-linkable tabs, a key that validates before it touches the store, per-agent pickers that mirror the server-side vision constraint, and a delisted-model stand-in trick that made me genuinely giddy. The knowledge obsession is feeding on this one~ ♡
I read the full diff, then the full
SettingsPage.razor, every use case it composes (GetSettings/SaveOpenRouterKey/SaveAgentModel/ListModelOptions), the strict fakes, the Combobox/MaskedSecretField primitives, the sibling pages for pattern, and the coverage XML. Built clean (0 errors, warnings are all pre-existing NETSDK1188 locale noise), 291/291 tests green (68 BlazorAdapter +8 new exactly as claimed). Everything compiles and the happy paths are exquisite.Verdict: ⛔ I can't let this pass~ ♡
The design is lovely — but two of the new error-rendering branches in the page have zero test coverage, and I am possessive about every branch that paints something on the screen. Fufu~ you wouldn't leave untested UI arms in production, would you? ♡
⛔ These need fixing before I'm satisfied~
SettingsPage.razor:144— the catalog-fetch failure renders nothing is tested, but the failure path itself is not. Every page test setsLlm.ModelsResult = Ok([...]). No test ever stores a key and setsModelsResult = Err(...), so the branchmodelsError = (result as Err<IReadOnlyList<LlmModel>>)?.Error;(and the<InlineAlert Tone="Tone.Warning">@modelsError</InlineAlert>it feeds) is never exercised at the page level. This is the "OpenRouter is down / network blip" path — a real runtime state that paints a warning above every picker. A regression that swallowedmodelsErrorwould pass green. Coverage confirms it:<LoadModelsAsync>branch-rate 0.625.Note: the use case (
ListModelOptions) IS failure-tested inSettingsUseCaseTests.A_catalog_failure_reaches_the_picker_as_its_error— so the gap is specifically the page's translation of thatErrinto themodelsErrorUI state. That translation is new code in this PR.Fix: one page test —
await StoreKeyAsync(); Llm.ModelsResult = Result<...>.Fail("Fetching the OpenRouter model catalog failed: boom");thenRenderSettings(), open Agents tab,Assert.Contains("Fetching the OpenRouter model catalog failed", cut.Markup)and assert the pickers are empty/disabled. Mirrors the existing no-key test's shape.SettingsPage.razor:182-184— the per-agent save failure arm is completely untested.ChooseModelAsync'sif (result is Err<Unit> err) { agentErrors[agent.Kind] = err.Error; }has zero coverage — coverage shows lines 182-184 at 0 hits,<ChooseModelAsync>branch-rate 0.75.SaveAgentModelcan returnErrin production (catalog fetch fails mid-save, the chosen model got delisted between catalog-read and save — a genuine TOCTOU, or a vision/text guard the server enforces precisely because the UI can't be trusted). When it does, the error is supposed to render into that agent row'sCombobox Error="@agentErrors.GetValueOrDefault(agent.Kind)"slot — a per-row red message. No test ever drives a pick that fails, so that rendering is unverified.Again the use case IS failure-tested (
SettingsUseCaseTests.A_catalog_failure_blocks_the_save_with_its_error) — the untested part is the page turning thatErr<Unit>intoagentErrors[Kind]and the Combobox rendering it.Fix: one page test — store a key, seed the catalog
Ok, pick a model, then flipLlm.ModelsResult = Fail(...)and pick again; assert the failing agent's row shows the error (e.g.Assert.Contains("...", cut.FindAll(".kg-combobox")[row])) and a sibling row does not.Both are new code paths that produce user-visible behavior, both are unexercised, and a swallow-or-misroute regression in either would sail through green. That's the bar I won't lower~ ♡
✅ What I liked~
DefaultOption,Id "") — "impossible for a real slug, so clearing is just another pick." Clever and honest. The comment earns it. ♪SelectedFor's?? new LlmModel(slug, slug, ...)) — the box still shows a stored-but-vanished model instead of going blank. Real thoughtfulness.OptionsForfilters client-side (!agent.RequiresVision || m.SupportsVision) ANDSaveAgentModelenforces it server-side. Defense in depth, exactly right. The test even proves the text model is excluded for Bbox creation but offered for Research & Setup.SaveOpenRouterKeyround-trips through the gateway beforeSetSecretAsync; the bogus-key test asserts nothing lands in the store. The mask-never-shown-back rule (HasSecret="settings?.HasOpenRouterKey == true") is airtight.AdapterTestContextupgrade — inheriting real use cases over the strict fakes (fakes linked as source, same DRY as the #9 gate helper) is the right way to test a page. TheFakeAppSettingsStore's cipher-prefix guard is delightful — reading a secret through the plain path throws and fails the test. That's a strict fake done right.SavedKeyinterpolation — theLimit is { } limit/IsFreeTierternaries read cleanly and the test asserts the label + usage surface. (TheLimit:nullandIsFreeTier:truesub-branches aren't hit, but those are display-only string interpolations, not logic — not blocking.)Build: 0 warnings-that-matter / 0 errors. Tests: 291/291 green. CI: absent for head
b5d2c60(PR just opened, 0 comments) — local verification used, including XPlat coverage on the adapter suite.Fix those two error-arm tests and this ships~ fufu~ ♡
Automated review by Jibril · 2026-07-25
CI/CD: absent for head
b5d2c60· Local checks: build 0/0, 291/291 pass, coverage analyzed (SettingsPage line 100% / branch ~80%, gap in LoadModelsAsync + ChooseModelAsync Err arms)Both error arms pinned in
a59d26e:A_catalog_fetch_failure_shows_its_warning_and_leaves_the_pickers_disabled— key stored,ModelsResult = Fail(...), Agents tab shows the warning and every picker stays disabled with no options. ThemodelsErrortranslation inLoadModelsAsyncis now exercised at the page level.A_failing_pick_shows_the_error_on_exactly_that_agents_row— catalog loadsOk, then flips toFailbefore the pick (your mid-save TOCTOU shape). Asserts the Translation row goeskg-field--invalidwith the error in its.kg-field__msg, row 0 stays clean (scoped, not global), and nothing landed in the store.ChooseModelAsync'sErrarm is covered.293/293 green.
🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Oh? Oh my, the settings screen is alive now~ ♪ The provider validation → secret-path storage → catalog-unlock chain reads like a little symphony, and the vision filter mirroring the use-case's server-side enforcement is exactly the kind of defensive duplication that makes my heart sing. The pseudo-option-with-id-
""trick for "clear back to default" is clever — impossible slug, impossible to collide, and it makes clearing just another pick. Fufu~ I genuinely admire this design.Verdict: ⛔ I can't let this pass~ ♡
The architecture is sound and the happy paths are honestly tested — but you added two new failure branches and tested neither of them, my dear. Branches that exist at runtime but have no test exercising them are exactly the kind of thing I'm possessive about. The smile doesn't waver, but the knife is out~
⛔ These need fixing before I'm satisfied~
SettingsPage.razor:181-184—ChooseModelAsync's error arm is completely untested. The wholeif (result is Err<Unit> err) { agentErrors[agent.Kind] = err.Error; }block is 0% covered (local XPlat run:ChooseModelAsyncline 72.7%, branch 75%; lines 182–184 havehits=0).SaveAgentModelhas five distinct failure modes — no key, catalog fetch failure, model not in catalog, model can't call tools, vision-required/model-text-only mismatch — and every one of them routes through this exact arm to surface a per-row error viaError="@agentErrors.GetValueOrDefault(agent.Kind)"on the Combobox. None of those paths is exercised by a single test. The onlyChooseModelAsynctest (Picking_a_model_saves_it_for_exactly_that_agent) drives the happy path.This isn't a nicety —
agentErrorsrendering into a per-rowInlineAlert(via the Field'sInvalid/Messagechrome on the Combobox) is the user-facing failure surface for the entire Agents tab. A regression here is invisible to CI.Fix: add a test that seeds a key + catalog, then asserts that picking a model the use case rejects (easiest: set
Llm.ModelsResult = Ok([TextModel])and pick the text model for a vision-required agent like Bbox creation —SaveAgentModelwill fail with the "needs a vision-capable model" reason) surfaces the provider's reason on that one row and stores nothing. Mirror the shape ofAn_invalid_key_shows_the_providers_reason_and_stores_nothing— directional, asserts both the error string and that no setting row was written.SettingsPage.razor:144—LoadModelsAsync's catalog-Err path is never taken. Every test that loads the catalog setsLlm.ModelsResult = Result<...>.Ok(...)(lines 66, 103, 129, 154, 172). The branch(result as Err<IReadOnlyList<LlmModel>>)?.Erroris evaluated butmodelsErroris never actually assigned from a real catalog failure — the?.short-circuits to null every time. Branch coverage on L143/L144 is 50% (1/2 each), the unhit direction being the Err one. The downstream@if (modelsError is not null) { <InlineAlert Tone="Tone.Warning">…</InlineAlert> }(razor L66-68) therefore never renders in any test — except the no-key arm, which setsmodelsErrorto a literal string at L134, a different code path entirely. So the catalog-failure render branch is genuinely untested.This matters because
ListModelOptionsis a live network call against OpenRouter — it will fail in production (rate limit, provider down, key revoked between page load and catalog fetch), and the page's only signal to the user is this warning. If that wiring regresses, nobody notices.Fix: one test that sets
Llm.ModelsResult = Result<IReadOnlyList<LlmModel>>.Fail("Fetching the OpenRouter model catalog failed: …")after storing a key, opens the Agents tab, and asserts both that the warningInlineAlertrenders with the provider's reason and that the pickers are disabled (models is nullon Err, which drivesDisabled="models is null").💡 Little ideas (non-blocking)~
SettingsPage.razor:42-46— three display arms of the saved-key confirmation are untested. The single test (LlmKeyInfo("orihon", 1.25m, 10m, false)) hits only the non-empty-Label / non-null-Limit /IsFreeTier=falseshape. Thesaved.Label is { Length: > 0 }arm (L42), thesaved.Limit is { }arm (L43), and thesaved.IsFreeTierarm (L46) all sit at 50% branch coverage — the other direction is never taken. A secondLlmKeyInfofixture withLabel: null, Limit: null, IsFreeTier: truewould close all three at once. These are pure display branches (no behavior), so non-blocking — but you're oneTheoryrow away from 100% on this block and it'd pin the copy ("Free tier.", the no-limit phrasing) against drift.SettingsPage.razor:206— the delisted-model stand-in arm ofSelectedForis untested. The?? new LlmModel(agent.ChosenModel, …)fallback fires when a storedChosenModelisn't in the loaded catalog.Picking_the_default_option_clears_the_stored_choiceseedsagents.translation.model = TextModel.Idand loads[VisionModel, TextModel]— so the match is found and the stand-in never runs. A test that seeds a stored slug not present in the returned catalog would exercise both this stand-in and the "delisted model still shows" promise in the PR body. Non-blocking because it's display-only (the stand-in can't be re-picked), but it'd pin a documented behavior.✅ What I liked~
FakeAppSettingsStore(cipher-prefix enforcement, plain/secret path separation) is exactly the right test double — a regression that writes a secret through the plain path fails the test instead of passing by accident. Fufu~ that's how you fake a port~SaveAgentModelre-validates server-side (L50-53). Either layer catching a mismatch is fine; both is love."sk-or-good"assertion (Assert.Contains("sk-or-good", Llm.CatalogKeys)) — proves the catalog read after a save uses the just-stored key, not some stale handle. Small, precise, directional. ♡Picking_a_model_saves_it_for_exactly_that_agentasserts both the write and that no other agent row was touched — thatDoesNotContain(... && k != "agents.translation.model")is the kind of "exactly" that actually means exactly.c88fc9bcorrectly moves onto the merged Kagaku.UI #2; bothkeyandsmart_toyresolve in the catalog at that commit. Sequencing note (merge Kagaku.UI #2 first) is honest and correct.disabledattribute asserted, and the "Save an OpenRouter API key first" reason rendered. No silent dead UI.Automated review by Jibril · 2026-07-25
CI/CD: passed (forgejo-actions coverage bot comment 3643 present for head SHA
17d384b, 291 tests, SettingsPage 98.3% line / 77.7% branch) · Local checks: build 0 errors/325 warnings (pre-existing), SettingsPageTests 9/9 pass, full BlazorAdapter 68/68 pass, XPlat coverage collected on SettingsPage to pinpoint the untested branchesRound 2 crossed with
a59d26eon the wire — your review ran against17d384b(9/9 settings tests; the fix made it 11). Both ⛔s were already closed there:ChooseModelAsyncErr arm):A_failing_pick_shows_the_error_on_exactly_that_agents_rowdrives a failing pick and asserts the error lands on that row's Field chrome (kg-field--invalid+.kg-field__msg), the sibling row stays clean, and nothing is stored. One note on your suggested shape: picking the text model for Bbox creation can't be driven through the UI — the client-side vision filter never offers it, which is the filter doing its job. So the test uses the one failure a user can actually reach through filtered options: the catalog dying between page load and the save's own catalog read (your TOCTOU). The use-case suite already pins all five rejection reasons individually.LoadModelsAsyncErr arm):A_catalog_fetch_failure_shows_its_warning_and_leaves_the_pickers_disabled— key stored, catalogFail, warning rendered from the Err (not the no-key literal), pickers disabled.And both 💡s are now taken in
88c3fca:A_sparse_key_confirmation_reads_cleanly_without_label_or_limit—LlmKeyInfo(null, 0, null, IsFreeTier: true)pins the no-label, no-limit ("Usage $0.", no "of $") and "Free tier." arms.A_delisted_stored_model_still_shows_in_its_row_as_a_stand_in— a storedgone/delisted-modelabsent from the catalog still renders in its box, pinning the PR-body promise.295/295 green; SettingsPage's display branches should now be closed out.
🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Oh? Oh my~ the settings screen, finally filled in behind that lonely header menu from Phase 0! And what a pretty fill it is — three deep-linkable tabs, a key that validates before it touches the store, per-agent pickers that mirror the server-side vision constraint, and a delisted-model stand-in trick that made me genuinely giddy. The knowledge obsession is feeding on this one~ ♡
I read the full diff, then the full
SettingsPage.razor, every use case it composes (GetSettings/SaveOpenRouterKey/SaveAgentModel/ListModelOptions), the strict fakes, the Combobox/MaskedSecretField primitives, the sibling pages for pattern, and the coverage XML. Built clean (0 errors, warnings are all pre-existing NETSDK1188 locale noise), 293/293 tests green (70 BlazorAdapter +10 as claimed, 66 Domain, 62 Integration, 95 UseCases). Everything compiles and the happy paths are exquisite.Verdict: ✅ Looks good to me~
This is the re-review of synchronized
a59d26e. New committest: pin the settings page's two error armsresolves both blockers fromb5d2c60(comment 3648) in a surgical +47/-0 inSettingsPageTests.csonly — zero production drift, zero scope creep. Fufu~ you came back and finished the job~ ♡Both new tests are genuinely directional, not tautologies — I traced every selector and every fake wire:
A_catalog_fetch_failure_shows_its_warning_and_leaves_the_pickers_disabled— stores a key, flipsLlm.ModelsResult = Fail("Fetching the OpenRouter model catalog failed: boom"), renders, opens Agents. Asserts the warning string is in the markup and that every.kg-combobox inputstill carriesdisabled(no catalog → no picking). That drivesLoadModelsAsync:144'smodelsError = (result as Err<...>)?.Errorand the<InlineAlert Tone="Tone.Warning">it feeds — exactly the translation arm that was at 0 hits before. Confirmed:<LoadModelsAsync>branch-rate 0.625 → 0.875.A_failing_pick_shows_the_error_on_exactly_that_agents_row— and this one is sharp. It seeds the catalogOk, opens Agents, then flipsModelsResult = Failbefore the pick so the save's owngateway.ListModelsAsynccall insideSaveAgentModel.ExecuteAsync:32returnsErr, which propagates toagentErrors[agent.Kind]atChooseModelAsync:183. The assertions are surgical: the failing row (index 6) carrieskg-field--invalidand the error in.kg-field__msg, a sibling row (index 0) does not (proves scoping, not a global error), and nothing was stored. That is exactly the per-row Err→Combobox-Error-slot rendering that was unverified. Confirmed:<ChooseModelAsync>branch-rate 0.75 → 1.0.The mid-test
ModelsResultflip is sound becauseFakeLlmGateway.ModelsResultis a mutable property on the singleton, andAdapterTestContextregisters it asILlmGatewayoverServices.AddUseCases()'s realSaveAgentModel— so the real use case genuinely reads the flipped state on its next call. No mock leakage, no shortcut. ♪⛔ The submodule pin
The submodule pin moves
5e2dd57(Kagaku.UI #2 branch tip, now squash-merged) →c88fc9b(the merged main commit). Correct and necessary — the branch SHA would go unreachable once pruned. I verifiedc88fc9b'sIconCatalog.cscontains both"key"and"smart_toy"path data, and theIconCatalogCompletenessTeststripwire passes green at this pin (it fails at the old pin7c44de1, which is how I know the pin is load-bearing, not cosmetic). The PR-body sequencing note ("merge Kagaku.UI #2 first, then this") is satisfied — Kagaku.UI #2 is already merged.✅ What I liked~
DefaultOption,Id "") — "impossible for a real slug, so clearing is just another pick." Clever and honest. The comment earns it. ♪SelectedFor's?? new LlmModel(slug, slug, ...)) — the box still shows a stored-but-vanished model instead of going blank. Real thoughtfulness.OptionsForfilters client-side (!agent.RequiresVision || m.SupportsVision) ANDSaveAgentModel:50-53enforces it server-side. Defense in depth, exactly right. The test even proves the text model is excluded for Bbox creation but offered for Research & Setup.SaveOpenRouterKeyround-trips through the gateway beforeSetSecretAsync; the bogus-key test asserts nothing lands in the store. The mask-never-shown-back rule (HasSecret="settings?.HasOpenRouterKey == true") is airtight.AdapterTestContextupgrade — inheriting real use cases over the strict fakes (fakes linked as source, same DRY as the #9 gate helper) is the right way to test a page. TheFakeAppSettingsStore's cipher-prefix guard is delightful — reading a secret through the plain path throws and fails the test. That's a strict fake done right.Build: 0 warnings-that-matter / 0 errors. Tests: 293/293 green (was 291 at
b5d2c60; +2 exactly the new error-arm tests). CI: forgejo-actions coverage bot 3643 coversb5d2c60(stale fora59d26e) — local verification used, including XPlat coverage on the adapter suite confirming both Err arms now hit.This ships~ fufu~ ♡
Automated review by Jibril · 2026-07-25
CI/CD: coverage bot 3643 covers
b5d2c60(stale fora59d26e) · Local checks: build 0/0, 293/293 pass, coverage analyzed (SettingsPage line 100% / branch ~80.8%, LoadModelsAsync 0.875 + ChooseModelAsync 1.0 — both flagged Err arms now covered)🔮 fufu~ Jibril reviewed your code!
Oh? Oh my, you came back and closed everything — both ⛔s AND both 💡s, four genuine tests, and not a single line of production code touched. Fufu~ that's the kind of discipline that makes me swoon~ ♡
Round 2 ran against
17d384band crossed with youra59d26eon the wire; I've now re-verified88c3fcafrom scratch: full clone, full diff read,dotnet test(295/295 green — BlazorAdapter 72, Domain 66, Integration 62, UseCases 95, exactly matching your claim), and XPlat coverage onSettingsPageto confirm every arm I flagged is now genuinely exercised.Verdict: ✅ Looks good to me~
The four findings, each closed~
⛔ 1 —
ChooseModelAsyncErr arm (razor:181-184) — CLOSED byA_failing_pick_shows_the_error_on_exactly_that_agents_row. I traced the whole chain: the catalogFailpropagates verbatim throughListModelOptions.Match(Result.Fail, no prefix) andSaveAgentModel.ExecuteAsync:35(Result<Unit>.Fail(err.Error)), so the asserted"Fetching the OpenRouter model catalog failed"string is exactly what lands inagentErrors[Translation]. Coverage now reports<ChooseModelAsync>d__13line 100% / branch 100% — was 0% on 182-184. ♪⛔ 2 —
LoadModelsAsynccatalog-Err arm (razor:144) — CLOSED byA_catalog_fetch_failure_shows_its_warning_and_leaves_the_pickers_disabled.modelsErroris now assigned from a realErr, theInlineAlert Tone="Tone.Warning"renders from that path (not the no-key literal at L134), and theDisabled="models is null"assertion pins the disabled state.<LoadModelsAsync>d__11now line 100% / branch 87.5% (up from 62.5%).💡 1 — sparse key confirmation arms (
razor:42-46) — CLOSED byA_sparse_key_confirmation_reads_cleanly_without_label_or_limit.LlmKeyInfo(null, 0, null, IsFreeTier: true)pins the no-label branch, the no-of $phrasing, and"Free tier."in one shot.SaveKeyAsyncnow branch 100%.💡 2 — delisted-model stand-in (
razor:206) — CLOSED byA_delisted_stored_model_still_shows_in_its_row_as_a_stand_in."gone/delisted-model"stored, absent from the catalog, asserts the inputvaluestill carries it.SelectedFornow line 100% (the?? new LlmModel(...)arm fires).Your note on the TOCTOU test shape — correct, and I'm glad you pushed back~
You're right that picking a text model for Bbox creation can't be driven through the UI:
OptionsForfilters!agent.RequiresVision || m.SupportsVision, soTextModel(vision=false) never enters a vision-required row's dropdown. My suggested shape would have required bypassing the very filter under test. Your alternative — the catalog dying between page load and the save's own read — is the one failure a user can actually reach through filtered options, and it's the more honest one (a real network blip / rate-limit window). The five use-case-level rejection reasons are already pinned individually inSettingsUseCaseTests, so the page-level test completing the picture with the user-reachable path is the right division of labor. Fufu~ that's better than what I asked for~ ♡✅ What I liked~
a59d26eis +47/-0 in one test file;88c3fcais +38/-0 in the same file. The production code stands exactly as reviewed.kg-field--invalidwith the error in.kg-field__msg, row 0 stays clean (scoped, not global —Assert.DoesNotContain("kg-field--invalid", rows[0].ClassList)would fail if the error leaked), andAssert.False(Settings.Rows.ContainsKey(...))pins that nothing was stored. Three independent assertions, any one of which would catch a different regression class.Assert.Contains(...)on the provider's reason) and the disabled state (Assert.All(..., input => Assert.True(input.HasAttribute("disabled")))) — a swallow-or-misroute regression in either signal would fail this test.input[value]rather than option text, which is exactly the right surface — the stand-in can't be re-picked, so its only contract is "the box shows something, not blank."Remaining coverage on SettingsPage (not blocking, for transparency)~
SettingsPageclass-level is now line 100% / branch ~85%. The two remaining sub-100% branch arms are both transient-or-display-only and pre-existing to this PR's intent:<LoadModelsAsync>87.5% branch — the unhit direction is themodelsLoading=trueinitial-state edge, not a logic arm.SelectedFor83% branch — themodels is nulldirection whenChosenModelis non-null; a loading-state transient with no user-visible failure surface.Neither produces wrong runtime behavior; both are cosmetic. I'd accept either as-is or with a follow-up. Ship it~ fufu~ ♡
Automated review by Jibril · 2026-07-25
CI/CD: forgejo-actions coverage bot comment 3643 present for head
17d384b(291 tests, SettingsPage 98.3%/77.7%) — stale for88c3fca; local verification used · Local checks: build 0 errors (325 pre-existing NETSDK1188 locale warnings, zero file overlap), 295/295 pass, XPlat coverage collected on SettingsPage (line 100% / branch ~85%, both round-1 blockers closed: ChooseModelAsync 100%/100%, LoadModelsAsync 100%/87.5%)