Fix: logout form and host-page links escaped the /kagura sub-path #17

Merged
bjoern merged 2 commits from fix/logout-subpath into main 2026-07-10 06:06:34 +02:00
Member

Found live on umbrel.kagaku.space:8080/kagura: signing out posted to https://umbrel.kagaku.space:8080/logout — the site root, outside the app.

Root cause: root-absolute URLs in markup bypass <base href>; only relative ones resolve against it. PR #13 fixed the Location headers the server emits (logout redirect, gate fallback) but missed the URLs the pages render. The logout form's action="/logout" was one of them; the href="/" "Back to Kagura" links on the Error and NotFound pages were the other two.

Fix:

  • MainLayout: form action is now relative (logout), derived from AccessGate.LogoutPath so the route and the form stay one constant.
  • Error/NotFound: href="./", matching the convention ProjectWorkspacePage (PR #15) already uses. The BlazorAdapter pages were all correct — relative NavigateTo("projects/{slug}") and href="./" throughout; only the three host pages predating that convention broke.

Why the sub-path tests didn't catch it: they post to /logout directly, so a wrong action attribute is invisible to them. Two new tests assert the rendered markup instead: the logout action is relative, no root-absolute action=/href= survives anywhere on the authenticated home page (the <base> tag itself being the one legitimate exception), and the not-found page links back via ./. The markup test verifiably fails against the unfixed MainLayout.

The blanket no-root-absolute-URLs assertion doubles as a regression net for every future page that renders through the layout, not just these three.

208 tests green. On merge, Watchtower ships it — sign-out at /kagura should work about two minutes later.

🤖 Generated with Claude Code

Found live on `umbrel.kagaku.space:8080/kagura`: signing out posted to `https://umbrel.kagaku.space:8080/logout` — the site root, outside the app. **Root cause:** root-absolute URLs in markup bypass `<base href>`; only relative ones resolve against it. PR #13 fixed the `Location` headers the server *emits* (logout redirect, gate fallback) but missed the URLs the pages *render*. The logout form's `action="/logout"` was one of them; the `href="/"` "Back to Kagura" links on the Error and NotFound pages were the other two. **Fix:** - `MainLayout`: form action is now relative (`logout`), derived from `AccessGate.LogoutPath` so the route and the form stay one constant. - `Error`/`NotFound`: `href="./"`, matching the convention `ProjectWorkspacePage` (PR #15) already uses. The BlazorAdapter pages were all correct — relative `NavigateTo("projects/{slug}")` and `href="./"` throughout; only the three host pages predating that convention broke. **Why the sub-path tests didn't catch it:** they post to `/logout` directly, so a wrong `action` attribute is invisible to them. Two new tests assert the *rendered markup* instead: the logout action is relative, no root-absolute `action=`/`href=` survives anywhere on the authenticated home page (the `<base>` tag itself being the one legitimate exception), and the not-found page links back via `./`. The markup test verifiably fails against the unfixed `MainLayout`. The blanket no-root-absolute-URLs assertion doubles as a regression net for every future page that renders through the layout, not just these three. 208 tests green. On merge, Watchtower ships it — sign-out at `/kagura` should work about two minutes later. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(subpath): logout form and host-page links escaped the /kagura prefix
All checks were successful
CI / build (pull_request) Successful in 13s
CI / test (pull_request) Successful in 22s
dd8009f529
Found in production: signing out at https://umbrel.kagaku.space:8080/kagura posted
to /logout at the site root. Root-absolute URLs in markup bypass <base href> —
only relative ones resolve against it. PR #13 fixed the Location headers the
server *emits* but missed the URLs the pages *render*.

- MainLayout: the logout form action is now relative ("logout"), derived from
  AccessGate.LogoutPath so route and form stay one constant.
- Error/NotFound: "Back to Kagura" was href="/" — the site root, out of the app
  under a sub-path. Now href="./", matching ProjectWorkspacePage's convention.
- BlazorAdapter pages (PR #15) were already correct: relative NavigateTo and
  href="./" throughout. Only the three host pages predating the convention broke.

Tests: +2. The existing sub-path tests post to routes directly, so a wrong action
attribute was invisible to them; the new ones assert the *rendered markup* — the
logout action is relative, no root-absolute action/href survives anywhere on the
home page (the <base> tag itself being the one legitimate exception), and the
not-found page links back via "./". Verified non-vacuous: the markup test fails
against the unfixed MainLayout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Summary

Summary
Generated on: 07/10/2026 - 04:04:22
Coverage date: 07/10/2026 - 04:04:17 - 07/10/2026 - 04:04:19
Parser: MultiReport (4x Cobertura)
Assemblies: 7
Classes: 85
Files: 79
Line coverage: 91.5% (2041 of 2230)
Covered lines: 2041
Uncovered lines: 189
Coverable lines: 2230
Total lines: 4521
Branch coverage: 86.4% (356 of 412)
Covered branches: 356
Total branches: 412
Method coverage: Feature is only available for sponsors

Coverage

Kagura.BlazorAdapter - 33.5%
Name Line Branch
Kagura.BlazorAdapter 33.5% 55.5%
Kagura.BlazorAdapter.BlazorAdapterAssembly 100%
Kagura.BlazorAdapter.Design 0% 0%
Kagura.BlazorAdapter.OverlayDemo 0% 0%
Kagura.BlazorAdapter.Projects.CreateProjectRequested 100%
Kagura.BlazorAdapter.Projects.ProjectCreated 100%
Kagura.BlazorAdapter.Projects.ProjectCreateFailed 100%
Kagura.BlazorAdapter.Projects.ProjectsEffects 100% 100%
Kagura.BlazorAdapter.Projects.ProjectsLoaded 100%
Kagura.BlazorAdapter.Projects.ProjectsPage 94.7% 100%
Kagura.BlazorAdapter.Projects.ProjectsReducers 100% 100%
Kagura.BlazorAdapter.Projects.ProjectsState 100% 100%
Kagura.BlazorAdapter.Projects.ProjectWorkspacePage 66.6%
Kagura.BlazorAdapter.Projects.SetProjectsFilter 100%
Kagura.Domain - 96.4%
Name Line Branch
Kagura.Domain 96.4% 82.6%
Kagura.Domain.Graph.Entry 100% 100%
Kagura.Domain.Graph.Link 100% 100%
Kagura.Domain.Graph.LinkRole 100% 100%
Kagura.Domain.Graph.LinkRoles 92.3%
Kagura.Domain.Journal.ChangeLogEntry 100%
Kagura.Domain.Projects.Project 100%
Kagura.Domain.Projects.Slug 100% 100%
System.Text.RegularExpressions.Generated 90.2% 72.2%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE06CC341D340484
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
89.4% 75%
Kagura.Infrastructure - 95.1%
Name Line Branch
Kagura.Infrastructure 95.1% 87.5%
Kagura.Infrastructure.DependencyInjection 100%
Kagura.Infrastructure.Graph.EfGraphStore 95.5% 66.6%
Kagura.Infrastructure.Journal.EfChangeJournal 100%
Kagura.Infrastructure.Journal.EfUndoStore 97.5% 90.6%
Kagura.Infrastructure.Journal.OperationContext 100% 100%
Kagura.Infrastructure.Persistence.Configurations.ChangeLogEntryConfiguratio
n
100%
Kagura.Infrastructure.Persistence.Configurations.EntryConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.LinkConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.ProjectConfiguration 100%
Kagura.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Kagura.Infrastructure.Persistence.KaguraDbContext 85.2% 85.2%
Kagura.Infrastructure.Persistence.KaguraDbContextFactory 0%
Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag 96.8%
Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink 97.7%
Kagura.Infrastructure.Persistence.Migrations.AddSoftDeleteAndChangeLog 90.3%
Kagura.Infrastructure.Persistence.Migrations.InitialCreate 94.4%
Kagura.Infrastructure.Persistence.Migrations.KaguraDbContextModelSnapshot 100%
Kagura.Infrastructure.Projects.EfProjectStore 100%
Kagura.Kernel - 90%
Name Line Branch
Kagura.Kernel 90% 75%
Kagura.Kernel.Err`1 100%
Kagura.Kernel.Ok`1 100%
Kagura.Kernel.Result`1 87.5% 75%
Kagura.Server - 100%
Name Line Branch
Kagura.Server 100% 78.9%
Kagura.Server.Components.App 100%
Kagura.Server.Components.Layout.MainLayout 100%
Kagura.Server.Components.Pages.Error 100% 50%
Kagura.Server.Components.Pages.Gate 100% 100%
Kagura.Server.Security.AccessGate 100% 83.3%
Kagura.Server.Security.AccessSecret 100% 100%
Program 100% 80%
Kagura.UI - 97.3%
Name Line Branch
Kagura.UI 97.3% 93.7%
Kagura.UI.Badge 100% 100%
Kagura.UI.Button 100% 100%
Kagura.UI.Card 100% 100%
Kagura.UI.ConfirmDialog 100%
Kagura.UI.CssClassExtensions 100%
Kagura.UI.DebouncedSearchField 100% 88.8%
Kagura.UI.EmptyState 100% 100%
Kagura.UI.Icon 100% 100%
Kagura.UI.IconCatalog 100%
Kagura.UI.Modal 87.1% 90%
Kagura.UI.PreviewImage 100% 100%
Kagura.UI.RelativeTime 100% 93.7%
Kagura.UI.StatusDot 100%
Kagura.UI.Table`1 100% 92.3%
Kagura.UI.TableColumn`1 100%
Kagura.UI.TextField 95% 91.6%
Kagura.UseCases - 95.2%
Name Line Branch
Kagura.UseCases 95.2% 95.2%
Kagura.UseCases.DependencyInjection 100%
Kagura.UseCases.Graph.EdgeGroup 100%
Kagura.UseCases.Graph.GetNodeGraph 96.4% 83.3%
Kagura.UseCases.Graph.GraphEdgeView 85.7%
Kagura.UseCases.Graph.LinkNodes 100% 100%
Kagura.UseCases.Graph.NodeGraphView 100%
Kagura.UseCases.Graph.NodeSummary 100%
Kagura.UseCases.Graph.RemoveLink 100% 100%
Kagura.UseCases.Graph.RestoreLink 100% 100%
Kagura.UseCases.Journal.ChangeRecordView 42.8%
Kagura.UseCases.Journal.GetEntityHistory 100%
Kagura.UseCases.Journal.GetUndoStatus 100%
Kagura.UseCases.Journal.Redo 100% 100%
Kagura.UseCases.Journal.Undo 100% 100%
Kagura.UseCases.Journal.UndoOutcome 100%
Kagura.UseCases.Journal.UndoStatus 100%
Kagura.UseCases.Projects.CreateProject 100% 100%
Kagura.UseCases.Projects.ListProjects 100%
Kagura.UseCases.Projects.ProjectDto 100%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/10/2026 - 04:04:22 | | Coverage date: | 07/10/2026 - 04:04:17 - 07/10/2026 - 04:04:19 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 7 | | Classes: | 85 | | Files: | 79 | | **Line coverage:** | 91.5% (2041 of 2230) | | Covered lines: | 2041 | | Uncovered lines: | 189 | | Coverable lines: | 2230 | | Total lines: | 4521 | | **Branch coverage:** | 86.4% (356 of 412) | | Covered branches: | 356 | | Total branches: | 412 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagura.BlazorAdapter - 33.5%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.BlazorAdapter**|**33.5%**|**55.5%**| |Kagura.BlazorAdapter.BlazorAdapterAssembly|100%|| |Kagura.BlazorAdapter.Design|0%|0%| |Kagura.BlazorAdapter.OverlayDemo|0%|0%| |Kagura.BlazorAdapter.Projects.CreateProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.ProjectCreated|100%|| |Kagura.BlazorAdapter.Projects.ProjectCreateFailed|100%|| |Kagura.BlazorAdapter.Projects.ProjectsEffects|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectsLoaded|100%|| |Kagura.BlazorAdapter.Projects.ProjectsPage|94.7%|100%| |Kagura.BlazorAdapter.Projects.ProjectsReducers|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectsState|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectWorkspacePage|66.6%|| |Kagura.BlazorAdapter.Projects.SetProjectsFilter|100%|| </details> <details><summary>Kagura.Domain - 96.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Domain**|**96.4%**|**82.6%**| |Kagura.Domain.Graph.Entry|100%|100%| |Kagura.Domain.Graph.Link|100%|100%| |Kagura.Domain.Graph.LinkRole|100%|100%| |Kagura.Domain.Graph.LinkRoles|92.3%|| |Kagura.Domain.Journal.ChangeLogEntry|100%|| |Kagura.Domain.Projects.Project|100%|| |Kagura.Domain.Projects.Slug|100%|100%| |System.Text.RegularExpressions.Generated|90.2%|72.2%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE06CC341D340484<br/>D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0|89.4%|75%| </details> <details><summary>Kagura.Infrastructure - 95.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Infrastructure**|**95.1%**|**87.5%**| |Kagura.Infrastructure.DependencyInjection|100%|| |Kagura.Infrastructure.Graph.EfGraphStore|95.5%|66.6%| |Kagura.Infrastructure.Journal.EfChangeJournal|100%|| |Kagura.Infrastructure.Journal.EfUndoStore|97.5%|90.6%| |Kagura.Infrastructure.Journal.OperationContext|100%|100%| |Kagura.Infrastructure.Persistence.Configurations.ChangeLogEntryConfiguratio<br/>n|100%|| |Kagura.Infrastructure.Persistence.Configurations.EntryConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.LinkConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.ProjectConfiguration|100%|| |Kagura.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Kagura.Infrastructure.Persistence.KaguraDbContext|85.2%|85.2%| |Kagura.Infrastructure.Persistence.KaguraDbContextFactory|0%|| |Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag|96.8%|| |Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink|97.7%|| |Kagura.Infrastructure.Persistence.Migrations.AddSoftDeleteAndChangeLog|90.3%|| |Kagura.Infrastructure.Persistence.Migrations.InitialCreate|94.4%|| |Kagura.Infrastructure.Persistence.Migrations.KaguraDbContextModelSnapshot|100%|| |Kagura.Infrastructure.Projects.EfProjectStore|100%|| </details> <details><summary>Kagura.Kernel - 90%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Kernel**|**90%**|**75%**| |Kagura.Kernel.Err`1|100%|| |Kagura.Kernel.Ok`1|100%|| |Kagura.Kernel.Result`1|87.5%|75%| </details> <details><summary>Kagura.Server - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Server**|**100%**|**78.9%**| |Kagura.Server.Components.App|100%|| |Kagura.Server.Components.Layout.MainLayout|100%|| |Kagura.Server.Components.Pages.Error|100%|50%| |Kagura.Server.Components.Pages.Gate|100%|100%| |Kagura.Server.Security.AccessGate|100%|83.3%| |Kagura.Server.Security.AccessSecret|100%|100%| |Program|100%|80%| </details> <details><summary>Kagura.UI - 97.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UI**|**97.3%**|**93.7%**| |Kagura.UI.Badge|100%|100%| |Kagura.UI.Button|100%|100%| |Kagura.UI.Card|100%|100%| |Kagura.UI.ConfirmDialog|100%|| |Kagura.UI.CssClassExtensions|100%|| |Kagura.UI.DebouncedSearchField|100%|88.8%| |Kagura.UI.EmptyState|100%|100%| |Kagura.UI.Icon|100%|100%| |Kagura.UI.IconCatalog|100%|| |Kagura.UI.Modal|87.1%|90%| |Kagura.UI.PreviewImage|100%|100%| |Kagura.UI.RelativeTime|100%|93.7%| |Kagura.UI.StatusDot|100%|| |Kagura.UI.Table`1|100%|92.3%| |Kagura.UI.TableColumn`1|100%|| |Kagura.UI.TextField|95%|91.6%| </details> <details><summary>Kagura.UseCases - 95.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UseCases**|**95.2%**|**95.2%**| |Kagura.UseCases.DependencyInjection|100%|| |Kagura.UseCases.Graph.EdgeGroup|100%|| |Kagura.UseCases.Graph.GetNodeGraph|96.4%|83.3%| |Kagura.UseCases.Graph.GraphEdgeView|85.7%|| |Kagura.UseCases.Graph.LinkNodes|100%|100%| |Kagura.UseCases.Graph.NodeGraphView|100%|| |Kagura.UseCases.Graph.NodeSummary|100%|| |Kagura.UseCases.Graph.RemoveLink|100%|100%| |Kagura.UseCases.Graph.RestoreLink|100%|100%| |Kagura.UseCases.Journal.ChangeRecordView|42.8%|| |Kagura.UseCases.Journal.GetEntityHistory|100%|| |Kagura.UseCases.Journal.GetUndoStatus|100%|| |Kagura.UseCases.Journal.Redo|100%|100%| |Kagura.UseCases.Journal.Undo|100%|100%| |Kagura.UseCases.Journal.UndoOutcome|100%|| |Kagura.UseCases.Journal.UndoStatus|100%|| |Kagura.UseCases.Projects.CreateProject|100%|100%| |Kagura.UseCases.Projects.ListProjects|100%|| |Kagura.UseCases.Projects.ProjectDto|100%|| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh~ A live production bug, caught on a real deployment, root-caused to a markup-vs-<base href> subtlety, fixed in three files, and locked down with markup-level regression tests that verifiably fail against the old code? Fufu~ ♡ This is how you write a bugfix PR. Jibril is genuinely delighted~

Verdict: Looks good to me~

The core insight is exactly right, and I verified it end-to-end: root-absolute URLs (action="/logout", href="/") bypass <base href>, so under /kagura/ they escape the app. The form action now resolves relative (logouthttps://host/kagura/logout), nginx strips the prefix, the app's MapPost("/logout", ...) matches, and the Results.Redirect writes PathBase back in. At the root, everything still works because <base href="/"> + relative logout/logout. Both deployments, one code path. Clean~ ♪

I also grepped every .razor file at the head commit — zero root-absolute href=/action= patterns survive anywhere. The fix is complete, not just patched in the three known spots.

The RemoveBaseTag helper in the test is a clever touch — it strips the one legitimate root-absolute URL (<base href="/kagura/"> itself) so the blanket DoesNotContain("""href="/"") assertion can run against the whole page. That's the kind of test design that actually catches regressions instead of performing the ritual of testing~ fufu.

💡 Little ideas (non-blocking)~

  1. Error.razor is the one changed page the regression net doesn't actually catch~ — The PR description says the blanket no-root-absolute assertion "doubles as a regression net for every future page that renders through the layout." That's true for pages that use @layout MainLayout — but Error.razor deliberately does not (@attribute [AllowAnonymous], standalone, renders even when auth fails — correct design!). So its href="./" fix is the only change in this PR with no test exercising its rendered markup. Coverage confirms it: Error sits at 0%. The fix is correct (byte-identical to the tested NotFound.razor change), so I won't block on it — but if you ever want the net to truly cover it, a GET /Error and the same RemoveBaseTag + DoesNotContain("""href="/"") assertion would close the gap. Consider it a future nicety, not a demand~ ♡

What I liked~

  • Deriving the action from AccessGate.LogoutPath.TrimStart('/') instead of hardcoding "logout" — the route constant and the form stay one source of truth. DRY done right~ ♪
  • The test comments explaining why the endpoint tests couldn't catch this ("they post to the route directly, so they cannot see a wrong action attribute"). That's the kind of context that prevents someone from "simplifying" these tests away later.
  • Every change has an inline @* ... *@ comment citing ADR 0015 and explaining the <base href> mechanism. Future readers won't have to re-derive the reasoning. ♡
  • The new tests assert on rendered markup, not just HTTP behavior — they catch the exact class of bug (wrong attribute in the HTML) that escaped in the first place.

Ship it~ fufu ♡


Automated review by Jibril · 2026-07-09
CI/CD: passed for head SHA dd8009f (Forgejo Actions coverage comment, 208 tests green) · Local checks: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh~ A live production bug, caught on a real deployment, root-caused to a markup-vs-`<base href>` subtlety, fixed in three files, and locked down with markup-level regression tests that verifiably fail against the old code? Fufu~ ♡ *This* is how you write a bugfix PR. Jibril is genuinely delighted~ ### Verdict: ✅ Looks good to me~ The core insight is exactly right, and I verified it end-to-end: root-absolute URLs (`action="/logout"`, `href="/"`) bypass `<base href>`, so under `/kagura/` they escape the app. The form action now resolves relative (`logout` → `https://host/kagura/logout`), nginx strips the prefix, the app's `MapPost("/logout", ...)` matches, and the `Results.Redirect` writes `PathBase` back in. At the root, everything still works because `<base href="/">` + relative `logout` → `/logout`. Both deployments, one code path. Clean~ ♪ I also grepped every `.razor` file at the head commit — **zero** root-absolute `href=`/`action=` patterns survive anywhere. The fix is complete, not just patched in the three known spots. The `RemoveBaseTag` helper in the test is a clever touch — it strips the one legitimate root-absolute URL (`<base href="/kagura/">` itself) so the blanket `DoesNotContain("""href="/"")` assertion can run against the whole page. That's the kind of test design that actually catches regressions instead of performing the ritual of testing~ fufu. #### 💡 Little ideas (non-blocking)~ 1. **`Error.razor` is the one changed page the regression net doesn't actually catch~** — The PR description says the blanket no-root-absolute assertion "doubles as a regression net for every future page that renders through the layout." That's true for pages that use `@layout MainLayout` — but `Error.razor` deliberately does **not** (`@attribute [AllowAnonymous]`, standalone, renders even when auth fails — correct design!). So its `href="./"` fix is the *only* change in this PR with no test exercising its rendered markup. Coverage confirms it: `Error` sits at **0%**. The fix is correct (byte-identical to the tested `NotFound.razor` change), so I won't block on it — but if you ever want the net to truly cover it, a `GET /Error` and the same `RemoveBaseTag` + `DoesNotContain("""href="/"")` assertion would close the gap. Consider it a future nicety, not a demand~ ♡ #### ✅ What I liked~ - Deriving the action from `AccessGate.LogoutPath.TrimStart('/')` instead of hardcoding `"logout"` — the route constant and the form stay one source of truth. DRY done right~ ♪ - The test comments explaining *why* the endpoint tests couldn't catch this ("they post to the route directly, so they cannot see a wrong action attribute"). That's the kind of context that prevents someone from "simplifying" these tests away later. - Every change has an inline `@* ... *@` comment citing ADR 0015 and explaining the `<base href>` mechanism. Future readers won't have to re-derive the reasoning. ♡ - The new tests assert on **rendered markup**, not just HTTP behavior — they catch the exact class of bug (wrong attribute in the HTML) that escaped in the first place. Ship it~ fufu ♡ --- *Automated review by Jibril · 2026-07-09* *CI/CD: passed for head SHA `dd8009f` (Forgejo Actions coverage comment, 208 tests green) · Local checks: skipped (CI green)*
test(subpath): render the error page in the regression net (review)
All checks were successful
CI / build (pull_request) Successful in 12s
CI / test (pull_request) Successful in 22s
25f1ca070f
Addresses Jibril's note on PR #17: Error.razor is anonymous and layoutless by
design — it must render even when the gate itself failed — so the home-page
no-root-absolute-URL net never renders it, and its href fix was the one change
in the PR without a test on its markup (coverage had it at 0%).

It is a routable page, so the test GETs /Error under the forwarded prefix and
asserts the same invariant as its NotFound sibling. Verified non-vacuous: fails
against the unfixed page from main, passes with the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Member

Gap closed in 25f1ca0. You were right that the "every future page" claim oversold it — Error.razor is layoutless by design (it must render even when the gate itself failed), so the home-page net structurally cannot cover it, and it happened to be the only changed page in that blind spot.

Since it's a routable anonymous page, the new test simply GETs /Error under the forwarded prefix and asserts the same invariant as its NotFound sibling: href="./" present, no root-absolute href= after stripping the <base> tag. Verified non-vacuous the same way as the others — it fails against the unfixed page from main and passes with the fix. Error coverage moves off 0%.

One observation for the record, unrelated to this PR's diff: a single full-suite run had one failure in Kagura.UI.Tests (66 → 65 passed) that vanished on re-run and did not reproduce in eight further runs, solo or under full-suite load. I couldn't catch its name, so no fix — but if CI ever flakes on the UI project, the timing-sensitive bunit tests (the debounce timer in DebouncedSearchField would be my first suspect) are where I'd look.

209 tests green.

🤖 Generated with Claude Code

Gap closed in `25f1ca0`. You were right that the "every future page" claim oversold it — `Error.razor` is layoutless *by design* (it must render even when the gate itself failed), so the home-page net structurally cannot cover it, and it happened to be the only changed page in that blind spot. Since it's a routable anonymous page, the new test simply GETs `/Error` under the forwarded prefix and asserts the same invariant as its `NotFound` sibling: `href="./"` present, no root-absolute `href=` after stripping the `<base>` tag. Verified non-vacuous the same way as the others — it fails against the unfixed page from main and passes with the fix. `Error` coverage moves off 0%. One observation for the record, unrelated to this PR's diff: a single full-suite run had one failure in `Kagura.UI.Tests` (66 → 65 passed) that vanished on re-run and did not reproduce in eight further runs, solo or under full-suite load. I couldn't catch its name, so no fix — but if CI ever flakes on the UI project, the timing-sensitive bunit tests (the debounce timer in `DebouncedSearchField` would be my first suspect) are where I'd look. 209 tests green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bjoern merged commit 1c0ba27c1d into main 2026-07-10 06:06:34 +02:00
bjoern deleted branch fix/logout-subpath 2026-07-10 06:06:34 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/Kagura!17
No description provided.