diff --git a/Jellyfin.Plugin.JRay.Tests/Jellyfin.Plugin.JRay.Tests.csproj b/Jellyfin.Plugin.JRay.Tests/Jellyfin.Plugin.JRay.Tests.csproj
index 9b8a9bd..dbffd1c 100644
--- a/Jellyfin.Plugin.JRay.Tests/Jellyfin.Plugin.JRay.Tests.csproj
+++ b/Jellyfin.Plugin.JRay.Tests/Jellyfin.Plugin.JRay.Tests.csproj
@@ -20,6 +20,18 @@
and on CI without pinning either to a runtime that is not the plugin's.
-->
LatestMajor
+
+ true
diff --git a/docs/requirements.md b/docs/requirements.md
index 65bf9f1..2c117cf 100644
--- a/docs/requirements.md
+++ b/docs/requirements.md
@@ -20,21 +20,22 @@ Tag code with `// TRACES: JR-012 | SR-002`.
| ID | Asserts | Covers | Status |
|---|---|---|---|
-| UT-001 | Marked tag **and its trailing newline** removed — no blank line accumulates per upgrade cycle | JR-022 | Written |
-| UT-002 | Marked tag with no trailing newline removed | JR-022 | Written |
-| UT-003 | Document without the marker left byte-identical | JR-022 | Written |
-| UT-004 | Removal is idempotent — startup runs it on every boot forever after | JR-022 | Written |
-| UT-005 | **Another plugin's injection left intact** — it is their file too | JR-022 | Written |
-| UT-006 | An unmarked look-alike script tag is left alone — JRay did not write it | JR-022 | Written |
-| UT-007 | No matching rule resolves to `null` | JR-016 | Written |
-| UT-008 | Item rule beats Series rule | JR-016 | Written |
-| UT-009 | **Prioritised series inside an ignored genre — series wins** | JR-016 | Written |
-| UT-010 | Genre matching is case-insensitive | JR-016 | Written |
-| UT-011 | A Series rule valued `Guid.Empty` does not swallow every movie | JR-016 | Written |
+| UT-001 | Marked tag **and its trailing newline** removed — no blank line accumulates per upgrade cycle | JR-022 | **Passing** |
+| UT-002 | Marked tag with no trailing newline removed | JR-022 | **Passing** |
+| UT-003 | Document without the marker left byte-identical | JR-022 | **Passing** |
+| UT-004 | Removal is idempotent — startup runs it on every boot forever after | JR-022 | **Passing** |
+| UT-005 | **Another plugin's injection left intact** — it is their file too | JR-022 | **Passing** |
+| UT-006 | An unmarked look-alike script tag is left alone — JRay did not write it | JR-022 | **Passing** |
+| UT-007 | No matching rule resolves to `null` | JR-016 | **Passing** |
+| UT-008 | Item rule beats Series rule | JR-016 | **Passing** |
+| UT-009 | **Prioritised series inside an ignored genre — series wins** | JR-016 | **Passing** |
+| UT-010 | Genre matching is case-insensitive | JR-016 | **Passing** |
+| UT-011 | A Series rule valued `Guid.Empty` does not swallow every movie | JR-016 | **Passing** |
-`Written` rather than `Passing`: see the execution blocker below. A test whose
-result nobody has seen is not evidence, and recording it as passing would be the
-same failure as counting a GPU-only test as covered.
+All 11 execute and pass. The suite was also checked to **fail** on a mutation —
+removing the newline-stripping from `RemoveInjection` fails UT-001 and nothing
+else — because a suite that has only ever passed is not evidence that it tests
+anything.
`JR` is flat rather than split by theme. The plugin is one deployable with one
audience, and the thematic grouping lives in the section headings below, where it
@@ -82,7 +83,7 @@ coordinated `schema_version` bumps (SR-003).
| ID | Requirement | Traces to | Priority | Status |
|---|---|---|---|---|
| JR-015 | `Tasks/Pending` serves a random sample of items with no truth data, so pollers spread across the backlog without server-side task state | PR-003 | High | Done |
-| JR-016 | Prioritise/ignore rules scoped `Genre` / `Series` / `Item`; **most specific wins**; scope+value is the unique key | PR-003 | Medium | Done |
+| JR-016 | Prioritise/ignore rules scoped `Genre` / `Series` / `Item`; **most specific wins**; scope+value is the unique key | PR-003 | Medium | **Done** (UT-007…011) |
| JR-017 | Rules steer **work discovery only** — never the overlay or the read endpoints | PR-003 | Medium | Done |
| JR-018 | Coverage report by media type and genre; ignored items leave the percent-done denominator rather than dragging it down | PR-003 | Medium | Done |
| JR-019 | Picker endpoints (genres, series, item search) populate the rule editor | PR-003 | Low | Done |
@@ -93,7 +94,7 @@ coordinated `schema_version` bumps (SR-003).
|---|---|---|---|---|
| JR-020 | Pause overlay: injected client script queries `jray?t=` and renders the scene's cast | **PR-001** | High | Done |
| JR-021 | **jRay never injects into `index.html` on disk.** File Transformation is a hard dependency; there is no on-disk fallback. The only permitted write is JR-022's removal | PR-004 | High | **Done** |
-| JR-022 | Migration: remove any on-disk patch left by an earlier jRay, identified by the `` marker | PR-004 | High | In Progress |
+| JR-022 | Migration: remove any on-disk patch left by an earlier jRay, identified by the `` marker | PR-004 | High | **Done** (UT-001…006) |
| JR-023 | Absent the dependency, disable **only** the overlay and say so in the log and the config page; never bundle the assembly | PR-004 | Medium | In Progress |
| JR-024 | Actor names and all server-supplied strings render as **text, never markup** | SR-004 | High | Done |
@@ -187,31 +188,29 @@ thing when read across repos; reusing T3 for a live tier here would make a
cross-repo reader count live-only requirements as covered.
`Jellyfin.Plugin.JRay.Tests` (xUnit, in the solution) carries the T1 tier. It
-builds clean alongside the plugin.
+builds clean alongside the plugin and all 11 tests pass.
-### Execution blocker — the suite does not run on this machine
+### Running the suite on a box without the web runtime
-The plugin framework-references **`Microsoft.AspNetCore.App`** through
-`Jellyfin.Controller`, so the test host demands that shared framework even for
-tests that touch only pure logic. This box has `Microsoft.NETCore.App` 8.0.29
-and 10.0.10 and **no ASP.NET Core runtime at all**, so `dotnet test` aborts
-before a single test executes:
+`dotnet test` from the repo root. Two properties on the test project make that
+work anywhere, and both are load-bearing rather than incidental:
-```
-Framework: 'Microsoft.AspNetCore.App', version '9.0.0' — No frameworks were found.
-```
+- **`RollForward=LatestMajor`.** The plugin targets `net9.0` to match Jellyfin's
+ ABI, but a machine that can *build* it need not have the 9.0 runtime. Rolling
+ the test host forward keeps the suite runnable without pinning developers to a
+ runtime the plugin does not otherwise need.
+- **`DisableTransitiveFrameworkReferences=true`.** The plugin
+ framework-references `Microsoft.AspNetCore.App` through `Jellyfin.Controller`,
+ and that flows into anything referencing it — so the test host would demand a
+ web runtime even for tests that touch no web type. .NET resolves assemblies
+ lazily, so pure-logic types load fine without it.
-`LatestMajor` on the test project solves the *other*
-half — the plugin targets `net9.0` to match Jellyfin's ABI, and no .NET 9
-runtime is installed either — but roll-forward cannot conjure a framework of
-which no version exists.
-
-**Fix:** install the ASP.NET Core runtime (`aspnet-runtime` on Arch). It is
-needed on the CI host for the same reason, so this belongs in the CI image
-rather than in a developer's setup notes.
-
-Until then every `UT` above is **Written, not Passing**, and no requirement
-should be promoted to `Done` on the strength of them.
+**This is a T1-tier decision, not a workaround to unwind.** These tests exercise
+string and rule logic; requiring an ASP.NET Core runtime to run them would be
+incidental coupling. **T2 — controllers and authorisation — genuinely needs that
+runtime**, and those tests belong in a second project that keeps the reference.
+Note that no ASP.NET Core 9 exists in the Arch repositories (8 and 10 only), so
+that project will lean on `RollForward` too.
### Per-requirement verification plan