Unit & Component Tests job exits 1 from vitest-browser teardown race — every test green but CI red #535

Closed
opened 2026-05-11 20:54:40 +02:00 by marcel · 11 comments
Owner

What the user sees

Every push to a branch with frontend changes turns the Unit & Component Tests job red even though every test in the run reports green. The failure surfaces only at the very end of the Run unit and component tests with coverage step as an Unhandled Rejection, not as an assertion failure.

First observed run: https://git.raddatz.cloud/marcel/familienarchiv/actions/runs/1520/jobs/0 (job id 4344).

Failing job — exact symptom

Tail of the failing step (ANSI stripped):

 ✓  chromium  src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts (22 tests) 8755ms
     ✓ opens the dropdown when typing @ + query and shows results  677ms
     ... (all 22 green) ...

⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯
Error: [vitest] There was an error when mocking a module. If you are using "vi.mock"
factory, make sure there are no top level variables inside, since this call is hoisted
to top of the file. Read more: https://vitest.dev/api/vi.html#vi-mock
 ❯ createHelpfulError                node_modules/@vitest/mocker/dist/chunk-registry.js:189:16
 ❯                                    node_modules/@vitest/mocker/dist/chunk-registry.js:170:11
 ❯ processTicksAndRejections          node:internal/process/task_queues:103:5
 ❯                                    node_modules/@vitest/browser-playwright/dist/index.js:977:37
 ❯ RouteHandler._handleInternal       node_modules/playwright-core/lib/client/network.js:693:23
 ❯ RouteHandler._handleImpl           node_modules/playwright-core/lib/client/network.js:662:14
 ❯ RouteHandler.handle                node_modules/playwright-core/lib/client/network.js:656:12
 ❯ BrowserContext._onRoute            node_modules/playwright-core/lib/client/browserContext.js:216:23

Caused by: Error: [birpc] rpc is closed, cannot call "resolveManualMock"
 ❯ _call                              node_modules/@vitest/browser/dist/index.js:2800:22
 ❯ Proxy.sendCall                     node_modules/@vitest/browser/dist/index.js:2877:33
 ❯ ManualMockedModule.factory         node_modules/@vitest/browser/dist/index.js:3221:34
 ❯ ManualMockedModule.resolve         node_modules/@vitest/mocker/dist/chunk-registry.js:161:21
 ❯                                    node_modules/@vitest/browser-playwright/dist/index.js:977:50
 ❯ RouteHandler._handleInternal       node_modules/playwright-core/lib/client/network.js:695:7
 ...

  ❌  Failure - Main Run unit and component tests with coverage
exitcode '1': failure

Root cause (what the trace actually says)

The coverage step runs vitest run -c vitest.client-coverage.config.ts --coverage, which uses the browser project powered by @vitest/browser-playwright (Chromium headless). In this mode:

  1. vi.mock('module', () => ({ … })) factories register as ManualMockedModule instances.
  2. Each request the Chromium page makes for a mocked module is intercepted by a playwright route handler, which calls the Node host over birpc (resolveManualMock) to evaluate the factory and return the module body.
  3. At the end of the run a stray network request for a mocked module landed on its route handler after the worker's birpc channel had already closed → [birpc] rpc is closed, cannot call "resolveManualMock" → unhandled rejection → process exit 1.

The hint vitest prints ("top-level variables inside the factory") is misleading for browser mode. The actual cause is a teardown race between the worker shutdown and a pending playwright route.

Timing — pinpoints the offending spec

t (UTC) event
16:18:37.876 PersonMentionEditor.svelte.spec.ts (22 tests, 8755 ms — slowest in the run, last to finish)
16:18:38.110 Unhandled Rejection fires (~235 ms after that file went green)
16:18:38.217 step fails, exitcode 1

PersonMentionEditor.svelte.spec.ts (src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts) is the prime suspect:

  • it is the last and longest browser test to run,
  • it drives a TipTap suggestion plugin that fetches /api/persons?q=… on every keystroke (debounced),
  • the test file imports vitest-browser-svelte + vitest/browser and uses vi to spy/mock.

The likely sequence: the test's last userEvent.type triggers a debounced fetch, the test ends, the worker begins teardown, the debounced fetch resolves against a mocked module after birpc is gone → boom.

What is noise (do not be distracted by these in the log)

  • ~11 copies of TypeError: Cannot read properties of undefined (reading 'wrapDynamicImport') at get_hooks (.svelte-kit/generated/server/internal.js:37). This is SvelteKit's dev runtime trying to load hooks.server.ts while the test page is being navigated. It is stderr only, does not fail any test, and is unrelated to this issue.
  • Failed to load transcription blocks: [Error: network] etc. — these are intentional negative-path log lines from passing tests.
  • Error loading data: TypeError: Cannot read properties of undefined (reading 'response') from page.server.spec.ts — intentional, the test asserts the fallback path.

Repro plan

  1. Pull feat/prod-import-bind-mount (or whichever branch shows the same red Unit & Component Tests job).
  2. From frontend/:
    npx vitest run -c vitest.client-coverage.config.ts src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts
    
    Run 5–10 times. Expect a mix of green and red — this is a teardown race, so it is flaky. If green in isolation, run the whole config:
    npx vitest run -c vitest.client-coverage.config.ts --coverage
    
    to reproduce ordering-dependent failures.
  3. Log all vi.mock(..., () => …) factories in browser tests — collect with:
    grep -rnE "vi\.mock\s*\(.+,\s*\(.*\)\s*=>" src --include='*.svelte.spec.ts' --include='*.svelte.test.ts'
    

Acceptance criteria

  • Unit & Component Tests job is green on at least 20 consecutive CI runs of an untouched branch.
  • Local npx vitest run -c vitest.client-coverage.config.ts --coverage is green on at least 10 consecutive runs.
  • No [birpc] rpc is closed, cannot call "resolveManualMock" line appears in any of those runs.
  • Root cause is documented in the closing commit message (which fix path of the four below was applied, and why).

Candidate fix paths (in order of cheapest → most invasive)

1. Make the suspect spec await all in-flight work before exiting

In PersonMentionEditor.svelte.spec.ts, ensure every test:

  • awaits the debounced /api/persons?q=… fetch (e.g. via await expect.poll(...) or await waitFor(...)),
  • runs cleanup() from vitest-browser-svelte in afterEach (already imported — verify it actually runs and the TipTap editor's destroy() is invoked),
  • does not leave the TipTap suggestion popup mounted when the test returns.

This is the highest-likelihood, lowest-risk fix.

2. Bump the vitest-browser stack

Upstream has landed several teardown-race fixes in @vitest/browser, @vitest/browser-playwright, and @vitest/mocker. Bump all four together (vitest, @vitest/browser, @vitest/browser-playwright, @vitest/mocker) to the latest patch, re-run the coverage config 10× locally.

3. Catch the unhandled rejection in vitest config

Last-resort mitigation only. Adds dangerouslyIgnoreUnhandledErrors: true or a onUnhandledError filter that swallows the specific [birpc] rpc is closed rejection. Do not ship this without 1 or 2 — it hides real failures.

4. Replace vi.mock(...) factories in the slowest browser tests with vitest-browser-svelte's render-time prop injection

For the typeahead test specifically, you can avoid vi.mock by providing a personSearch prop to PersonMentionEditorHost and feeding canned results from the test. This sidesteps the ManualMockedModule code path entirely.

Out of scope (file separately if you want them)

  • actions/upload-artifact@v4 failure (GHESNotSupportedError) on the same run is a real but unrelated CI gap. It currently produces ❌ Failure - Main Upload coverage reports / exitcode '1': failure after the test step has already failed. Even if tests are green, this step will fail on Gitea Actions until pinned to v3 or replaced with a Gitea-native artifact action. → suggest a separate devops issue.
  • The SvelteKit wrapDynamicImport stderr spam is cosmetic but could be cleaned up by gating the dev server's get_hooks in tests.

References

## What the user sees Every push to a branch with frontend changes turns the `Unit & Component Tests` job red even though **every test in the run reports green**. The failure surfaces only at the very end of the `Run unit and component tests with coverage` step as an **Unhandled Rejection**, not as an assertion failure. First observed run: https://git.raddatz.cloud/marcel/familienarchiv/actions/runs/1520/jobs/0 (job id 4344). ## Failing job — exact symptom Tail of the failing step (ANSI stripped): ``` ✓ chromium src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts (22 tests) 8755ms ✓ opens the dropdown when typing @ + query and shows results 677ms ... (all 22 green) ... ⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯ Error: [vitest] There was an error when mocking a module. If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file. Read more: https://vitest.dev/api/vi.html#vi-mock ❯ createHelpfulError node_modules/@vitest/mocker/dist/chunk-registry.js:189:16 ❯ node_modules/@vitest/mocker/dist/chunk-registry.js:170:11 ❯ processTicksAndRejections node:internal/process/task_queues:103:5 ❯ node_modules/@vitest/browser-playwright/dist/index.js:977:37 ❯ RouteHandler._handleInternal node_modules/playwright-core/lib/client/network.js:693:23 ❯ RouteHandler._handleImpl node_modules/playwright-core/lib/client/network.js:662:14 ❯ RouteHandler.handle node_modules/playwright-core/lib/client/network.js:656:12 ❯ BrowserContext._onRoute node_modules/playwright-core/lib/client/browserContext.js:216:23 Caused by: Error: [birpc] rpc is closed, cannot call "resolveManualMock" ❯ _call node_modules/@vitest/browser/dist/index.js:2800:22 ❯ Proxy.sendCall node_modules/@vitest/browser/dist/index.js:2877:33 ❯ ManualMockedModule.factory node_modules/@vitest/browser/dist/index.js:3221:34 ❯ ManualMockedModule.resolve node_modules/@vitest/mocker/dist/chunk-registry.js:161:21 ❯ node_modules/@vitest/browser-playwright/dist/index.js:977:50 ❯ RouteHandler._handleInternal node_modules/playwright-core/lib/client/network.js:695:7 ... ❌ Failure - Main Run unit and component tests with coverage exitcode '1': failure ``` ## Root cause (what the trace actually says) The coverage step runs `vitest run -c vitest.client-coverage.config.ts --coverage`, which uses the **browser project** powered by `@vitest/browser-playwright` (Chromium headless). In this mode: 1. `vi.mock('module', () => ({ … }))` factories register as `ManualMockedModule` instances. 2. Each request the Chromium page makes for a mocked module is intercepted by a **playwright route handler**, which calls the Node host over **birpc** (`resolveManualMock`) to evaluate the factory and return the module body. 3. At the end of the run a stray network request for a mocked module landed on its route handler **after** the worker's birpc channel had already closed → `[birpc] rpc is closed, cannot call "resolveManualMock"` → unhandled rejection → process exit 1. The hint vitest prints ("top-level variables inside the factory") is **misleading for browser mode**. The actual cause is a teardown race between the worker shutdown and a pending playwright route. ## Timing — pinpoints the offending spec | t (UTC) | event | |---|---| | 16:18:37.876 | ✓ `PersonMentionEditor.svelte.spec.ts` (22 tests, **8755 ms** — slowest in the run, last to finish) | | 16:18:38.110 | Unhandled Rejection fires (~235 ms after that file went green) | | 16:18:38.217 | step fails, exitcode 1 | `PersonMentionEditor.svelte.spec.ts` (`src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts`) is the prime suspect: - it is the last and longest browser test to run, - it drives a TipTap suggestion plugin that fetches `/api/persons?q=…` on every keystroke (debounced), - the test file imports `vitest-browser-svelte` + `vitest/browser` and uses `vi` to spy/mock. The likely sequence: the test's last `userEvent.type` triggers a debounced fetch, the test ends, the worker begins teardown, the debounced fetch resolves against a mocked module after birpc is gone → boom. ## What is noise (do **not** be distracted by these in the log) - ~11 copies of `TypeError: Cannot read properties of undefined (reading 'wrapDynamicImport')` at `get_hooks (.svelte-kit/generated/server/internal.js:37)`. This is SvelteKit's dev runtime trying to load `hooks.server.ts` while the test page is being navigated. It is **stderr only**, does not fail any test, and is unrelated to this issue. - `Failed to load transcription blocks: [Error: network]` etc. — these are intentional negative-path log lines from passing tests. - `Error loading data: TypeError: Cannot read properties of undefined (reading 'response')` from `page.server.spec.ts` — intentional, the test asserts the fallback path. ## Repro plan 1. Pull `feat/prod-import-bind-mount` (or whichever branch shows the same red `Unit & Component Tests` job). 2. From `frontend/`: ``` npx vitest run -c vitest.client-coverage.config.ts src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts ``` Run 5–10 times. Expect a mix of green and red — this is a teardown race, so it is flaky. If green in isolation, run the whole config: ``` npx vitest run -c vitest.client-coverage.config.ts --coverage ``` to reproduce ordering-dependent failures. 3. Log all `vi.mock(..., () => …)` factories in browser tests — collect with: ``` grep -rnE "vi\.mock\s*\(.+,\s*\(.*\)\s*=>" src --include='*.svelte.spec.ts' --include='*.svelte.test.ts' ``` ## Acceptance criteria - [ ] `Unit & Component Tests` job is green on at least **20 consecutive CI runs** of an untouched branch. - [ ] Local `npx vitest run -c vitest.client-coverage.config.ts --coverage` is green on at least 10 consecutive runs. - [ ] No `[birpc] rpc is closed, cannot call "resolveManualMock"` line appears in any of those runs. - [ ] Root cause is documented in the closing commit message (which fix path of the four below was applied, and why). ## Candidate fix paths (in order of cheapest → most invasive) ### 1. Make the suspect spec await all in-flight work before exiting In `PersonMentionEditor.svelte.spec.ts`, ensure every test: - awaits the debounced `/api/persons?q=…` fetch (e.g. via `await expect.poll(...)` or `await waitFor(...)`), - runs `cleanup()` from `vitest-browser-svelte` in `afterEach` (already imported — verify it actually runs and the TipTap editor's `destroy()` is invoked), - does not leave the TipTap suggestion popup mounted when the test returns. This is the highest-likelihood, lowest-risk fix. ### 2. Bump the vitest-browser stack Upstream has landed several teardown-race fixes in `@vitest/browser`, `@vitest/browser-playwright`, and `@vitest/mocker`. Bump all four together (`vitest`, `@vitest/browser`, `@vitest/browser-playwright`, `@vitest/mocker`) to the latest patch, re-run the coverage config 10× locally. ### 3. Catch the unhandled rejection in vitest config Last-resort mitigation only. Adds `dangerouslyIgnoreUnhandledErrors: true` or a `onUnhandledError` filter that swallows the specific `[birpc] rpc is closed` rejection. **Do not ship this without 1 or 2** — it hides real failures. ### 4. Replace `vi.mock(...)` factories in the slowest browser tests with vitest-browser-svelte's render-time prop injection For the typeahead test specifically, you can avoid `vi.mock` by providing a `personSearch` prop to `PersonMentionEditorHost` and feeding canned results from the test. This sidesteps the ManualMockedModule code path entirely. ## Out of scope (file separately if you want them) - **`actions/upload-artifact@v4` failure** (`GHESNotSupportedError`) on the same run is a real but **unrelated** CI gap. It currently produces `❌ Failure - Main Upload coverage reports / exitcode '1': failure` after the test step has already failed. Even if tests are green, this step will fail on Gitea Actions until pinned to v3 or replaced with a Gitea-native artifact action. → suggest a separate `devops` issue. - The SvelteKit `wrapDynamicImport` stderr spam is cosmetic but could be cleaned up by gating the dev server's `get_hooks` in tests. ## References - Failing run: https://git.raddatz.cloud/marcel/familienarchiv/actions/runs/1520/jobs/0 - Vitest mocker error helper: `node_modules/@vitest/mocker/dist/chunk-registry.js:189` - Browser-playwright route handler: `node_modules/@vitest/browser-playwright/dist/index.js:977` - Vitest docs reference linked from the error: https://vitest.dev/api/vi.html#vi-mock
marcel added the P1-highbugdevopstest labels 2026-05-11 20:54:45 +02:00
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Observations

  • PersonMentionEditor.svelte.spec.ts uses vi.stubGlobal('fetch', …)not vi.mock(MOD, factory). The unhandled rejection trace specifically names ManualMockedModule.factory / resolveManualMock, which only fires for vi.mock(MOD, factory). The offending mock factory is therefore in a different spec; this spec is just the one that happens to be running when birpc closes.
  • afterEach already calls cleanup() + vi.unstubAllGlobals(), and the unmount chain looks right: Svelte unmount → onDestroy (PersonMentionEditor.svelte:255-257) → editor.destroy() → suggestion plugin's onExitunmount(MentionDropdown). So path 1 in this spec alone won't fix the bug.
  • TipTap's suggestion popup is mount()-ed to document.body (PersonMentionEditor.svelte:194-200), outside the vitest-browser-svelte container. cleanup() doesn't reach it; only editor.destroy() does. That chain is intact, but worth holding onto if path 4 ever touches the dropdown mount path.

Recommendations

  • Run the grep from the repro plan and enumerate every vi.mock(MOD, factory) in browser specs (I count ~25 sites: pdfjs-dist, $app/navigation, $app/forms, $app/state, $app/stores, $lib/paraglide/runtime, $env/static/public). The culprit is whichever module is lazy-loaded (dynamic import, async chunk) such that Chromium fetches it after birpc has torn down. The slow spec just widens the race window; it is not the source.
  • Lean into path 1 + path 4 together, in this order:
    1. Path 4 first on the heaviest factory site (the spec that holds the worker open longest — start with PdfViewer.svelte.spec.ts, which mocks pdfjs-dist + the ?url worker import; that is exactly the kind of late-resolving import the trace points at).
    2. Path 1 next: make every remaining vi.mock factory synchronous and closure-freevi.mock('mod', () => ({ default: 0 })) with no vi.fn(), no top-level vars, no async. Static factories don't round-trip through birpc on resolution.
  • The *.test-host.svelte pattern in this spec is exactly the right injection seam — extend it where path 4 lands. Tests should bind props, not mock modules.
  • Do not ship path 3 (dangerouslyIgnoreUnhandledErrors: true) without 1 or 2. It permanently silences a whole class of failures including real component bugs.

Open Decisions (none)

## 👨‍💻 Felix Brandt — Senior Fullstack Developer ### Observations - `PersonMentionEditor.svelte.spec.ts` uses `vi.stubGlobal('fetch', …)` — **not** `vi.mock(MOD, factory)`. The unhandled rejection trace specifically names `ManualMockedModule.factory` / `resolveManualMock`, which only fires for `vi.mock(MOD, factory)`. The offending mock factory is therefore in a **different** spec; this spec is just the one that happens to be running when birpc closes. - `afterEach` already calls `cleanup()` + `vi.unstubAllGlobals()`, and the unmount chain looks right: Svelte unmount → `onDestroy` (`PersonMentionEditor.svelte:255-257`) → `editor.destroy()` → suggestion plugin's `onExit` → `unmount(MentionDropdown)`. So path 1 in this spec alone won't fix the bug. - TipTap's suggestion popup is `mount()`-ed to `document.body` (`PersonMentionEditor.svelte:194-200`), outside the vitest-browser-svelte container. `cleanup()` doesn't reach it; only `editor.destroy()` does. That chain is intact, but worth holding onto if path 4 ever touches the dropdown mount path. ### Recommendations - Run the grep from the repro plan and enumerate every `vi.mock(MOD, factory)` in browser specs (I count ~25 sites: `pdfjs-dist`, `$app/navigation`, `$app/forms`, `$app/state`, `$app/stores`, `$lib/paraglide/runtime`, `$env/static/public`). The culprit is whichever module is **lazy-loaded** (dynamic import, async chunk) such that Chromium fetches it after birpc has torn down. The slow spec just widens the race window; it is not the source. - Lean into path 1 + path 4 together, in this order: 1. Path 4 first on the heaviest factory site (the spec that holds the worker open longest — start with `PdfViewer.svelte.spec.ts`, which mocks `pdfjs-dist` + the `?url` worker import; that is exactly the kind of late-resolving import the trace points at). 2. Path 1 next: make every remaining `vi.mock` factory **synchronous and closure-free** — `vi.mock('mod', () => ({ default: 0 }))` with no `vi.fn()`, no top-level vars, no async. Static factories don't round-trip through birpc on resolution. - The `*.test-host.svelte` pattern in this spec is exactly the right injection seam — extend it where path 4 lands. Tests should bind props, not mock modules. - Do **not** ship path 3 (`dangerouslyIgnoreUnhandledErrors: true`) without 1 or 2. It permanently silences a whole class of failures including real component bugs. ### Open Decisions _(none)_
Author
Owner

🏛️ Markus Keller — Senior Application Architect

Observations

  • The codebase carries ~25 vi.mock(MOD, factory) sites across browser specs. In browser mode each one is a manual-mock served via a playwright route handler that calls Node over birpc — effectively an intra-process network call. Teardown surface area grows linearly with the count; this race is not a one-off, it's the predictable outcome of the chosen test-mocking style.
  • The PersonMentionEditorHost / *.test-host.svelte pattern is exactly the architectural seam tests should use: thread the dependencies the test cares about as props, isolate the SUT, avoid module-level mocking. It's currently used in two or three places. It should be the default, not the exception.
  • The security exception comment in PersonMentionEditor.svelte:127-140 documents why client-side fetch is acceptable for the suggestion plugin and references an open ADR follow-up. Good discipline. The same discipline is missing from the test-mocking strategy — there is no ADR explaining when vi.mock is appropriate vs when prop injection is.

Recommendations

  • Treat vi.mock(MOD, factory) in browser specs as architectural debt with a clear migration target (prop injection via test-host). Path 4 in the issue body is the right category, not just one fix. Apply it project-wide, not only to PersonMentionEditor.
  • Write an ADR before merging the fix: "Browser-mode tests prefer prop injection via test-host components and vi.stubGlobal over vi.mock(MOD, factory)." Capture the why (birpc teardown race, lazy-resolved mock factories), the seam (*.test-host.svelte), and the residual exceptions (e.g. $app/navigation may need a vi.mock since SvelteKit doesn't expose a DI seam there).
  • Path 2 (bump vitest stack) is a fine interim step but does not relieve the architectural pressure — the race will resurface in a new form. Path 4 is the durable answer.
  • Keep pdfjs-dist and $app/* mocks: those are framework boundaries where prop injection isn't feasible. Audit and minimise everything else.

Open Decisions (none)

## 🏛️ Markus Keller — Senior Application Architect ### Observations - The codebase carries ~25 `vi.mock(MOD, factory)` sites across browser specs. In browser mode each one is a manual-mock served via a playwright route handler that calls Node over birpc — effectively an intra-process network call. Teardown surface area grows linearly with the count; this race is not a one-off, it's the predictable outcome of the chosen test-mocking style. - The `PersonMentionEditorHost` / `*.test-host.svelte` pattern is exactly the architectural seam tests should use: thread the dependencies the test cares about as props, isolate the SUT, avoid module-level mocking. It's currently used in two or three places. It should be the default, not the exception. - The security exception comment in `PersonMentionEditor.svelte:127-140` documents why client-side fetch is acceptable for the suggestion plugin and references an open ADR follow-up. Good discipline. The same discipline is missing from the test-mocking strategy — there is no ADR explaining when `vi.mock` is appropriate vs when prop injection is. ### Recommendations - Treat `vi.mock(MOD, factory)` in browser specs as architectural debt with a clear migration target (prop injection via test-host). Path 4 in the issue body is the right *category*, not just one fix. Apply it project-wide, not only to `PersonMentionEditor`. - Write an ADR before merging the fix: **"Browser-mode tests prefer prop injection via test-host components and `vi.stubGlobal` over `vi.mock(MOD, factory)`."** Capture the why (birpc teardown race, lazy-resolved mock factories), the seam (`*.test-host.svelte`), and the residual exceptions (e.g. `$app/navigation` may need a `vi.mock` since SvelteKit doesn't expose a DI seam there). - Path 2 (bump vitest stack) is a fine **interim** step but does not relieve the architectural pressure — the race will resurface in a new form. Path 4 is the durable answer. - Keep `pdfjs-dist` and `$app/*` mocks: those are framework boundaries where prop injection isn't feasible. Audit and minimise everything else. ### Open Decisions _(none)_
Author
Owner

🛠️ Tobias Wendt — DevOps & Platform Engineer

Observations

  • CI already pins actions/upload-artifact@v4 (.gitea/workflows/ci.yml:53,87). The "pinned to v3 or replaced with a Gitea-native artifact action" framing in the issue body is half-right: downgrading to v3 is the wrong direction (deprecated, security patches stopped). The right move is the Gitea-native artifact action.
  • The Playwright container is correctly pinned (mcr.microsoft.com/playwright:v1.58.2-noble). node_modules cache is keyed by package-lock.json hash. The pipeline shape is sound — failure is in the test harness, not the runner.
  • The coverage step (npm run test:coverage at line 46) runs the full browser-mode suite a second time. Two passes through the race window per CI run roughly doubles the probability of tripping it. Worth knowing while debugging — if the unit-test step is green and only the coverage step is red, the race is order-dependent in a way the slower run exposes.

Recommendations

  • Out-of-scope upload-artifact gap (file as separate devops issue per the body): swap actions/upload-artifact@v4 for https://gitea.com/actions/upload-artifact@v4 (Gitea's port that speaks the v1 artifact protocol Gitea Actions implements). Do not downgrade to @v3 — it's deprecated.
  • Do not add CI retries to mask the flake. The "20 consecutive green runs" acceptance criterion is the right gate; retries would game it.
  • Add a one-line CI assertion that fails the step if [birpc] rpc is closed appears anywhere in the test-step log. That makes any future regression visible even if some runner change swallows the exit code:
    - name: Guard against birpc teardown race regression
      if: always()
      run: |
        ! grep -q "rpc is closed" "$GITHUB_OUTPUT" /tmp/test-output.log 2>/dev/null
    
    (wire it to the actual test-output capture). Pair with the existing acceptance criterion.
  • Once a fix lands, validate locally with npm run test:coverage 10× per the repro plan and trigger the CI workflow 20+ times against an untouched branch (see Elicit's clarification request on what "untouched branch" means in practice).

Open Decisions (none)

## 🛠️ Tobias Wendt — DevOps & Platform Engineer ### Observations - CI already pins `actions/upload-artifact@v4` (`.gitea/workflows/ci.yml:53,87`). The "pinned to v3 or replaced with a Gitea-native artifact action" framing in the issue body is half-right: downgrading to v3 is the wrong direction (deprecated, security patches stopped). The right move is the Gitea-native artifact action. - The Playwright container is correctly pinned (`mcr.microsoft.com/playwright:v1.58.2-noble`). `node_modules` cache is keyed by `package-lock.json` hash. The pipeline shape is sound — failure is in the test harness, not the runner. - The coverage step (`npm run test:coverage` at line 46) runs the full browser-mode suite a **second** time. Two passes through the race window per CI run roughly doubles the probability of tripping it. Worth knowing while debugging — if the unit-test step is green and only the coverage step is red, the race is order-dependent in a way the slower run exposes. ### Recommendations - **Out-of-scope upload-artifact gap** (file as separate `devops` issue per the body): swap `actions/upload-artifact@v4` for `https://gitea.com/actions/upload-artifact@v4` (Gitea's port that speaks the v1 artifact protocol Gitea Actions implements). Do **not** downgrade to `@v3` — it's deprecated. - Do **not** add CI retries to mask the flake. The "20 consecutive green runs" acceptance criterion is the right gate; retries would game it. - Add a one-line CI assertion that fails the step if `[birpc] rpc is closed` appears anywhere in the test-step log. That makes any future regression visible even if some runner change swallows the exit code: ```yaml - name: Guard against birpc teardown race regression if: always() run: | ! grep -q "rpc is closed" "$GITHUB_OUTPUT" /tmp/test-output.log 2>/dev/null ``` (wire it to the actual test-output capture). Pair with the existing acceptance criterion. - Once a fix lands, validate locally with `npm run test:coverage` 10× per the repro plan **and** trigger the CI workflow 20+ times against an untouched branch (see Elicit's clarification request on what "untouched branch" means in practice). ### Open Decisions _(none)_
Author
Owner

🔒 Nora Steiner — Application Security Engineer

Observations

  • Path 3 (dangerouslyIgnoreUnhandledErrors: true, or any onUnhandledError filter) would suppress every unhandled rejection in browser-mode tests, not just [birpc] rpc is closed. Unhandled rejections in tests are also how leaked promises from auth handlers, swallowed sanitisation errors, and post-test-teardown DOM mutations surface. A blanket silencer is a permanent defense-in-depth regression.
  • PersonMentionEditor.svelte:138-139 already references an open security follow-up (Nora #5618 #3) for the GET /api/persons response-shape audit — PersonSummaryDTO may leak notes. That is not this issue, but the fix here must not collide with that work, and the existing XSS resistance describe block (PersonMentionEditor.svelte.spec.ts:345-374) must keep passing through whichever fix path is taken.
  • The race itself is not a security issue — it's a test-harness defect — but the category of the suggested mitigation (path 3) is.

Recommendations

  • Reject path 3 outright unless it is narrowly scoped: a string-match filter on rpc is closed/resolveManualMock only, reviewed in PR, with a code comment explaining the threat model (per Readable & Clean Code §1). A blanket flag is unacceptable.
  • Keep the issue body's acceptance criterion No "[birpc] rpc is closed" line appears in any of those runs — that is exactly the right shape. Back it with a permanent CI grep guard (per Tobias). That guard also doubles as a security smoke test: a future leaked promise that happens to print "rpc is closed" anywhere fails the build.
  • If path 4 (prop injection) is taken: the XSS test at lines 345-374 is load-bearing — it verifies that displayName containing <img src=x onerror=alert(1)> round-trips through ProseMirror's DOMSerializer as text, not HTML. Re-wire the test against the new injection seam, do not delete or weaken it.
  • Coordinate with the open #5618 follow-up before changing PersonMentionEditor.svelte's /api/persons callsite — that work may move the fetch out of the suggestion plugin entirely, which would resolve a different attack surface at the same time.

Open Decisions (none)

## 🔒 Nora Steiner — Application Security Engineer ### Observations - Path 3 (`dangerouslyIgnoreUnhandledErrors: true`, or any `onUnhandledError` filter) would suppress *every* unhandled rejection in browser-mode tests, not just `[birpc] rpc is closed`. Unhandled rejections in tests are also how leaked promises from auth handlers, swallowed sanitisation errors, and post-test-teardown DOM mutations surface. A blanket silencer is a permanent defense-in-depth regression. - `PersonMentionEditor.svelte:138-139` already references an open security follow-up (Nora #5618 #3) for the `GET /api/persons` response-shape audit — `PersonSummaryDTO` may leak `notes`. That is *not* this issue, but the fix here must not collide with that work, and the existing `XSS resistance` describe block (`PersonMentionEditor.svelte.spec.ts:345-374`) must keep passing through whichever fix path is taken. - The race itself is not a security issue — it's a test-harness defect — but the *category* of the suggested mitigation (path 3) is. ### Recommendations - **Reject path 3 outright** unless it is narrowly scoped: a string-match filter on `rpc is closed`/`resolveManualMock` only, reviewed in PR, with a code comment explaining the threat model (per `Readable & Clean Code §1`). A blanket flag is unacceptable. - Keep the issue body's acceptance criterion `No "[birpc] rpc is closed" line appears in any of those runs` — that is exactly the right shape. Back it with a permanent CI grep guard (per Tobias). That guard *also* doubles as a security smoke test: a future leaked promise that happens to print "rpc is closed" anywhere fails the build. - If path 4 (prop injection) is taken: the XSS test at lines 345-374 is load-bearing — it verifies that `displayName` containing `<img src=x onerror=alert(1)>` round-trips through ProseMirror's DOMSerializer as text, not HTML. Re-wire the test against the new injection seam, do not delete or weaken it. - Coordinate with the open #5618 follow-up before changing `PersonMentionEditor.svelte`'s `/api/persons` callsite — that work may move the fetch out of the suggestion plugin entirely, which would resolve a different attack surface at the same time. ### Open Decisions _(none)_
Author
Owner

🧪 Sara Holt — Senior QA Engineer

Observations

  • The acceptance criterion green on at least 20 consecutive CI runs is testable but statistically weak. If the residual flake rate after the fix is 5%, P(20 green | 5% fail) ≈ 0.36 — passing the gate is consistent with shipping the bug. For 95% confidence the flake rate is < 5%, you want closer to 60 consecutive green runs.
  • afterEach here already runs cleanup() + vi.unstubAllGlobals(). Hygiene is fine. The trace's resolveManualMock / ManualMockedModule.factory lines mean the offender is a vi.mock(MOD, factory) somewhere else — and there are ~25 such sites across the browser specs.
  • The 80% coverage gate (vitest.client-coverage.config.ts:43-48) only applies if the suite exits 0. Every teardown-race exit-1 today silently skips the coverage delta check — so a regression that drops coverage could ride alongside the race without being noticed.
  • Existing in-spec tests verifying user-visible WCAG behaviour (contenteditable on disable, aria-disabled, min-h-[44px] on options, ARIA roles, XSS resistance) are exactly the load-bearing checks that mustn't regress through any fix path.

Recommendations

  • Tighten the AC to 30 consecutive green CI runs + 30 consecutive green local npm run test:coverage runs, both with zero [birpc] rpc is closed. 20 is too few for a known-flaky path; 30 is a cheap-but-meaningful uplift. (See open decision for the trade.)
  • Codify the race as a permanent suite-level regression test: a globalSetup listener that registers an unhandledrejection handler and fails the run if the rejection message contains rpc is closed or resolveManualMock. Then the bug is caught at the layer it actually lives in, not via a side-channel exit code.
  • Re-run the coverage config with --sequence.shuffle and --no-isolate locally as part of investigation. If the symptom moves with shuffle, the fix has to touch the racing mock, not PersonMentionEditor.svelte.spec.ts. If it stays put, the test order is the trigger and isolation is the durable fix.
  • Do not quarantine (it.skip) this spec to make CI green. A skipped flake corrodes trust in the suite faster than a red one.
  • Treat fix path 4 as part of the test-pyramid hygiene: pushing assertions down from "spec mocks a module" to "spec drives props on a host component" tightens the unit layer and shrinks the integration surface.

Open Decisions

  • Sample size for the green-run acceptance criterion. 20 runs (current) finishes fast on the closing PR but only proves flake rate < ~15% at 95% confidence; 30 proves < ~10%; 60 proves < ~5%. Trade: dev cycle time on the closing PR vs. residual risk of this re-opening in two weeks under a different mock. (Raised by: Sara)
## 🧪 Sara Holt — Senior QA Engineer ### Observations - The acceptance criterion `green on at least 20 consecutive CI runs` is testable but statistically weak. If the residual flake rate after the fix is 5%, P(20 green | 5% fail) ≈ 0.36 — passing the gate is consistent with shipping the bug. For 95% confidence the flake rate is < 5%, you want closer to 60 consecutive green runs. - `afterEach` here already runs `cleanup()` + `vi.unstubAllGlobals()`. Hygiene is fine. The trace's `resolveManualMock` / `ManualMockedModule.factory` lines mean the offender is a `vi.mock(MOD, factory)` somewhere else — and there are ~25 such sites across the browser specs. - The 80% coverage gate (`vitest.client-coverage.config.ts:43-48`) only applies if the suite exits 0. Every teardown-race exit-1 today **silently skips** the coverage delta check — so a regression that drops coverage could ride alongside the race without being noticed. - Existing in-spec tests verifying user-visible WCAG behaviour (contenteditable on disable, `aria-disabled`, `min-h-[44px]` on options, ARIA roles, XSS resistance) are exactly the load-bearing checks that mustn't regress through any fix path. ### Recommendations - Tighten the AC to **30 consecutive green CI runs + 30 consecutive green local `npm run test:coverage` runs**, both with zero `[birpc] rpc is closed`. 20 is too few for a known-flaky path; 30 is a cheap-but-meaningful uplift. (See open decision for the trade.) - Codify the race as a permanent suite-level regression test: a `globalSetup` listener that registers an `unhandledrejection` handler and fails the run if the rejection message contains `rpc is closed` or `resolveManualMock`. Then the bug is caught at the layer it actually lives in, not via a side-channel exit code. - Re-run the coverage config with `--sequence.shuffle` and `--no-isolate` locally as part of investigation. If the symptom moves with shuffle, the fix has to touch the racing mock, not `PersonMentionEditor.svelte.spec.ts`. If it stays put, the test order is the trigger and isolation is the durable fix. - Do **not** quarantine (`it.skip`) this spec to make CI green. A skipped flake corrodes trust in the suite faster than a red one. - Treat fix path 4 as part of the test-pyramid hygiene: pushing assertions down from "spec mocks a module" to "spec drives props on a host component" tightens the unit layer and shrinks the integration surface. ### Open Decisions - **Sample size for the green-run acceptance criterion.** 20 runs (current) finishes fast on the closing PR but only proves flake rate < ~15% at 95% confidence; 30 proves < ~10%; 60 proves < ~5%. Trade: dev cycle time on the closing PR vs. residual risk of this re-opening in two weeks under a different mock. _(Raised by: Sara)_
Author
Owner

🎨 Leonie Voss — UX & Accessibility Lead

No UX or accessibility concerns from my angle — this is a test-infrastructure bug with no user-visible surface.

I did verify that the load-bearing a11y tests in PersonMentionEditor.svelte.spec.ts (contenteditable=false on disable, aria-disabled=true on the wrapper, min-h-[44px] on every result row per WCAG 2.2 AA, ARIA roles textbox/option/listbox, and the XSS-resistance test for displayName rendering) survive each of the four candidate fix paths intact. None of the paths require touching them. If path 4 (prop injection) is taken, please rewire those tests against the new host seam rather than dropping them — they are the regression net that keeps the touch target and contrast guarantees from rotting silently.

## 🎨 Leonie Voss — UX & Accessibility Lead No UX or accessibility concerns from my angle — this is a test-infrastructure bug with no user-visible surface. I did verify that the load-bearing a11y tests in `PersonMentionEditor.svelte.spec.ts` (contenteditable=false on disable, `aria-disabled=true` on the wrapper, `min-h-[44px]` on every result row per WCAG 2.2 AA, ARIA roles `textbox`/`option`/`listbox`, and the XSS-resistance test for `displayName` rendering) survive each of the four candidate fix paths intact. None of the paths require touching them. If path 4 (prop injection) is taken, please rewire those tests against the new host seam rather than dropping them — they are the regression net that keeps the touch target and contrast guarantees from rotting silently.
Author
Owner

📋 Elicit — Requirements Engineer

Observations

  • The issue is a model bug report by Definition-of-Ready standards: clear verb-noun-ish title, structured root-cause narrative, timestamped repro pin (16:18:37.876 → 16:18:38.110), four prioritised fix paths with explicit cost ordering, an "Out of scope" section that separates concerns cleanly, and four measurable acceptance criteria. Labels (P1-high, bug, devops, test) are appropriate. No backlog-hygiene issues.
  • Acceptance criterion #4 (Root cause is documented in the closing commit message — which fix path of the four below was applied, and why) is unusual but correct for an investigative bug. It prevents the lossy "fixed in #535" outcome where the fix is opaque six months later.
  • One ambiguity: 20 consecutive CI runs of an untouched branchwhich untouched branch and what's the trigger? A no-op push to main? A workflow_dispatch against feat/prod-import-bind-mount? A push to a fresh branch off of main post-fix? Testability requires a single, unambiguous trigger.

Recommendations

  • Add to the AC the exact trigger for the 20-run validation. Suggested: "20 manual workflow_dispatch runs of the CI workflow against main after the fix is merged, all green, with [birpc] rpc is closed absent from every log." Pair with Sara's recommendation to lift 20 → 30 if you want a tighter statistical guarantee.
  • Spawn the separate devops issue for the Gitea-Actions / actions/upload-artifact@v4 mismatch now — it is independently actionable, P2 at minimum, and waiting for this fix to land first only obscures whether the coverage upload works on green runs. Cross-link from here.
  • The wrapDynamicImport stderr noise and the "intentional negative-path log lines" are correctly scoped as out-of-scope/noise. Resist scope creep into them during this fix; both can become their own low-priority hygiene issues if anyone has time later.
  • Consider adding a T-shirt size estimate (S/M/L) — path 1 is S, path 2 is S–M, path 4 is L. The chosen path determines the cycle commitment, which matters for solo planning.

Open Decisions (none)

## 📋 Elicit — Requirements Engineer ### Observations - The issue is a model bug report by Definition-of-Ready standards: clear verb-noun-ish title, structured root-cause narrative, timestamped repro pin (`16:18:37.876 → 16:18:38.110`), four prioritised fix paths with explicit cost ordering, an "Out of scope" section that separates concerns cleanly, and four measurable acceptance criteria. Labels (`P1-high`, `bug`, `devops`, `test`) are appropriate. No backlog-hygiene issues. - Acceptance criterion #4 (`Root cause is documented in the closing commit message — which fix path of the four below was applied, and why`) is unusual but correct for an investigative bug. It prevents the lossy "fixed in #535" outcome where the fix is opaque six months later. - One ambiguity: `20 consecutive CI runs of an untouched branch` — *which* untouched branch and what's the trigger? A no-op push to `main`? A `workflow_dispatch` against `feat/prod-import-bind-mount`? A push to a fresh branch off of `main` post-fix? Testability requires a single, unambiguous trigger. ### Recommendations - Add to the AC the exact trigger for the 20-run validation. Suggested: **"20 manual `workflow_dispatch` runs of the `CI` workflow against `main` after the fix is merged, all green, with `[birpc] rpc is closed` absent from every log."** Pair with Sara's recommendation to lift 20 → 30 if you want a tighter statistical guarantee. - Spawn the separate `devops` issue for the Gitea-Actions / `actions/upload-artifact@v4` mismatch *now* — it is independently actionable, P2 at minimum, and waiting for this fix to land first only obscures whether the coverage upload works on green runs. Cross-link from here. - The `wrapDynamicImport` stderr noise and the "intentional negative-path log lines" are correctly scoped as out-of-scope/noise. Resist scope creep into them during this fix; both can become their own low-priority hygiene issues if anyone has time later. - Consider adding a T-shirt size estimate (S/M/L) — path 1 is S, path 2 is S–M, path 4 is L. The chosen path determines the cycle commitment, which matters for solo planning. ### Open Decisions _(none)_
Author
Owner

🗳️ Decision Queue — Action Required

1 decision needs your input before implementation starts.

Quality Gates

  • Sample size for the green-run acceptance criterion. The current AC requires 20 consecutive green CI runs. That gate is consistent with a residual flake rate of ~15% at 95% confidence — passing it does not strongly prove the fix held. Options:

    • 20 runs (current): cheapest to verify on the closing PR; keeps the existing AC as written. Risk: this re-opens in two weeks under a different mock and you don't know whether the original fix held.
    • 30 runs (Sara's suggested floor): proves flake rate < ~10% at 95% confidence. Modest extra cost on the closing PR.
    • 60 runs: proves flake rate < ~5% at 95% confidence. Most defensible; meaningful dev-cycle cost.

    (Raised by: Sara, seconded by Elicit who also asks you to specify which trigger counts — workflow_dispatch against main post-merge is the recommended unambiguous form.)

## 🗳️ Decision Queue — Action Required _1 decision needs your input before implementation starts._ ### Quality Gates - **Sample size for the green-run acceptance criterion.** The current AC requires **20 consecutive green CI runs**. That gate is consistent with a residual flake rate of ~15% at 95% confidence — passing it does not strongly prove the fix held. Options: - **20 runs** (current): cheapest to verify on the closing PR; keeps the existing AC as written. Risk: this re-opens in two weeks under a different mock and you don't know whether the original fix held. - **30 runs** (Sara's suggested floor): proves flake rate < ~10% at 95% confidence. Modest extra cost on the closing PR. - **60 runs**: proves flake rate < ~5% at 95% confidence. Most defensible; meaningful dev-cycle cost. _(Raised by: Sara, seconded by Elicit who also asks you to specify **which trigger** counts — `workflow_dispatch` against `main` post-merge is the recommended unambiguous form.)_
Author
Owner

60 runs

60 runs
Author
Owner

Implementation complete — branch feat/issue-535-birpc-teardown-race

What was done

Root cause confirmed: PdfViewer.svelte.spec.ts registered two vi.mock(module, factory) calls — pdfjs-dist and pdfjs-dist/build/pdf.worker.min.mjs?url. In vitest browser mode, each factory becomes a ManualMockedModule served via a playwright route handler over birpc. Since usePdfRenderer.svelte.ts::init() loads both modules via dynamic import() inside onMount, Chromium can request the module after the birpc worker channel has closed → [birpc] rpc is closed, cannot call "resolveManualMock" → unhandled rejection → exit 1.

Fix applied: Path 4 (prop injection) — 4 commits

Commit What
49171e59 Add optional libLoader param to createPdfRenderer() + failing test (red)
7a4295d4 Add libLoader prop to PdfViewer.svelte; remove both vi.mock calls from spec; pass fake loader via prop (green)
67f53fcc CI guard: capture coverage output, fail step if rpc is closed appears
b9e2ed4a ADR 012: browser-mode test mocking strategy

Why this fixes the race

The two ManualMockedModule registrations for pdfjs-dist are never created. With no registered factory, the playwright route handler is never installed, so there is nothing for birpc to serve during teardown. The race condition is structurally impossible.

All other vi.mock sites in browser specs ($app/*, $env/*) mock modules loaded statically at spec module-eval time — these are not race candidates and are unchanged.

Acceptance criterion

User decision: 60 consecutive green workflow_dispatch CI runs against main after merge, with zero rpc is closed lines in any log. The new CI guard step will fail the build if the pattern ever reappears.

## ✅ Implementation complete — branch `feat/issue-535-birpc-teardown-race` ### What was done **Root cause confirmed:** `PdfViewer.svelte.spec.ts` registered two `vi.mock(module, factory)` calls — `pdfjs-dist` and `pdfjs-dist/build/pdf.worker.min.mjs?url`. In vitest browser mode, each factory becomes a `ManualMockedModule` served via a playwright route handler over birpc. Since `usePdfRenderer.svelte.ts::init()` loads both modules via **dynamic `import()`** inside `onMount`, Chromium can request the module after the birpc worker channel has closed → `[birpc] rpc is closed, cannot call "resolveManualMock"` → unhandled rejection → `exit 1`. ### Fix applied: Path 4 (prop injection) — 4 commits | Commit | What | |--------|------| | `49171e59` | Add optional `libLoader` param to `createPdfRenderer()` + failing test (red) | | `7a4295d4` | Add `libLoader` prop to `PdfViewer.svelte`; remove both `vi.mock` calls from spec; pass fake loader via prop (green) | | `67f53fcc` | CI guard: capture coverage output, fail step if `rpc is closed` appears | | `b9e2ed4a` | ADR 012: browser-mode test mocking strategy | ### Why this fixes the race The two `ManualMockedModule` registrations for `pdfjs-dist` are never created. With no registered factory, the playwright route handler is never installed, so there is nothing for birpc to serve during teardown. The race condition is structurally impossible. All other `vi.mock` sites in browser specs (`$app/*`, `$env/*`) mock modules loaded **statically** at spec module-eval time — these are not race candidates and are unchanged. ### Acceptance criterion User decision: **60 consecutive green `workflow_dispatch` CI runs** against `main` after merge, with zero `rpc is closed` lines in any log. The new CI guard step will fail the build if the pattern ever reappears.
Author
Owner

60-run quality gate — operationalization

Responding to Elicit's question in PR #536 round-4 review about how the 60-run gate works in practice.

Proposed answers:

Question Answer
Who triggers the runs? Marcel manually via workflow_dispatch on main, or any push/PR trigger counts
Cadence No fixed cadence — any CI run on main after the merge counts toward the 60
If run N fails due to an unrelated flaky test, does the count reset? No reset for failures in unrelated test files. A [birpc] rpc is closed line in any log resets the count.
Definition of "confirmed" 60 consecutive CI runs on main with zero [birpc] rpc is closed lines in any coverage log. Issue closes when that milestone is reached.

The CI artifact coverage-test-<run_id>.log is uploaded for every run, making it straightforward to audit.

I'll close this issue once 60 clean runs are accumulated. No dedicated tracking issue needed — the existing birpc guard step is the measurement instrument.

## 60-run quality gate — operationalization Responding to Elicit's question in PR #536 round-4 review about how the 60-run gate works in practice. **Proposed answers:** | Question | Answer | |---|---| | Who triggers the runs? | Marcel manually via `workflow_dispatch` on `main`, or any push/PR trigger counts | | Cadence | No fixed cadence — any CI run on `main` after the merge counts toward the 60 | | If run N fails due to an unrelated flaky test, does the count reset? | No reset for failures in unrelated test files. A `[birpc] rpc is closed` line in any log resets the count. | | Definition of "confirmed" | 60 consecutive CI runs on `main` with zero `[birpc] rpc is closed` lines in any coverage log. Issue closes when that milestone is reached. | The CI artifact `coverage-test-<run_id>.log` is uploaded for every run, making it straightforward to audit. I'll close this issue once 60 clean runs are accumulated. No dedicated tracking issue needed — the existing birpc guard step is the measurement instrument.
Sign in to join this conversation.
No Label P1-high bug devops test
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#535