Commit Graph

2349 Commits

Author SHA1 Message Date
Marcel
f8dad85020 test(pdf-renderer): document libLoader rejection leaves pdfjsReady false
Regression-protection test: init() propagates the loader rejection
before pdfjsReady is set, so the renderer stays in a safe unready state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:57:28 +02:00
Marcel
5cd330de74 docs(pdf-viewer): comment untrack invariant on renderer init
Without untrack, a reactive libLoader prop reference change would
reinitialise the whole renderer and lose all loaded state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:57:28 +02:00
Marcel
06b158bf54 refactor(pdf-viewer): export LibLoader type and update callers
Exporting LibLoader gives the type a stable, named identity.
PdfViewer.svelte and PdfViewer.svelte.spec.ts now import it directly
instead of using Parameters<typeof createPdfRenderer>[0].

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:57:28 +02:00
Marcel
3594204214 ci(coverage): simplify coverage step and pin shell to bash
- removes unreachable `; exit ${PIPESTATUS[0]}` — already covered by pipefail (Tobias)
- adds explicit `shell: bash` to both new steps for clarity (Tobias)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:57:28 +02:00
Marcel
073b6cb45d refactor(test): move PdfViewer import to top and annotate partial fake cast
- import PdfViewer left mid-file from vi.mock hoisting — no longer needed (Sara/Felix)
- adds one-line comment explaining as unknown as cast is an intentional partial fake (Felix)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:57:28 +02:00
Marcel
a7e0a66355 docs(adr): 012 — browser-mode test mocking strategy
Documents why vi.mock(module, factory) races with birpc teardown for
dynamically-imported modules, the libLoader injection pattern used to fix
#535, and the residual exceptions ($app/*, $env/*) that are safe to keep
as vi.mock because they are resolved statically before any test runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:57:28 +02:00
Marcel
538adb43a9 ci(guard): fail unit-tests job if [birpc] rpc is closed appears in coverage run
Captures npm run test:coverage output with tee and adds an always-run step
that greps for the teardown-race fingerprint. Any future regression where a
vi.mock factory races with birpc teardown will now surface as an explicit CI
failure rather than a silent exit-1 after all tests report green (#535).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:57:28 +02:00
Marcel
115476453a fix(pdf-viewer): replace vi.mock(pdfjs-dist) with injected libLoader prop
Removes both vi.mock('pdfjs-dist', factory) and
vi.mock('pdfjs-dist/build/pdf.worker.min.mjs?url', factory) from
PdfViewer.svelte.spec.ts — the ManualMockedModule registrations that were
racing with vitest-browser-playwright's birpc teardown channel.

PdfViewer.svelte now accepts an optional libLoader prop (typed as
Parameters<typeof createPdfRenderer>[0]) that is passed untracked to
createPdfRenderer(). Tests supply a vi.fn() fake loader directly as a prop;
production code uses the default loader that imports the real pdfjs-dist.
The birpc route handler for pdfjs-dist is never registered, so no teardown
race is possible. Fixes #535.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:57:28 +02:00
Marcel
817ec44439 test(pdf-renderer): inject libLoader into createPdfRenderer to eliminate vi.mock factories
Adds an optional LibLoader parameter (defaults to the real pdfjs-dist dynamic
imports) and a failing test that verified the loader is called during init().
This is the first step toward removing ManualMockedModule registrations that
race with vitest-browser-playwright's birpc teardown (#535).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:57:28 +02:00
51e2d50dd0 Merge pull request 'fix(ci): replace iproute2 ip with /proc/net/route for gateway detection' (#544) from fix/nightly-caddy-reload into main
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / fail2ban Regex (push) Has been cancelled
CI / Compose Bucket Idempotency (push) Has been cancelled
2026-05-12 09:57:02 +02:00
Marcel
9c26c00eee fix(ci): replace iproute2 ip with /proc/net/route for gateway detection
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / fail2ban Regex (push) Has been cancelled
CI / Compose Bucket Idempotency (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / OCR Service Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / fail2ban Regex (pull_request) Has been cancelled
CI / Compose Bucket Idempotency (pull_request) Has been cancelled
`ip route` (iproute2) is not installed in the Gitea runner container,
causing the smoke test step to exit 127. /proc/net/route is a kernel
virtual file that is always present on Linux; awk decodes the
little-endian hex gateway field to dotted-decimal without any external
binary dependency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:50:56 +02:00
Marcel
6d16be4669 fix(ci): quote \$RESOLVE in all curl calls
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 1m51s
CI / OCR Service Tests (pull_request) Successful in 18s
CI / Backend Unit Tests (pull_request) Successful in 4m1s
CI / fail2ban Regex (pull_request) Successful in 38s
CI / Compose Bucket Idempotency (pull_request) Failing after 11s
CI / Unit & Component Tests (push) Failing after 1m51s
CI / OCR Service Tests (push) Successful in 18s
CI / Backend Unit Tests (push) Successful in 4m10s
CI / fail2ban Regex (push) Successful in 38s
CI / Compose Bucket Idempotency (push) Failing after 10s
Unquoted variable expansion is safe here since the value contains
no spaces or glob characters, but quoting is the correct default
and keeps the script consistent with surrounding style.

Addresses review suggestion by Felix Brandt and Tobias Wendt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:26:35 +02:00
Marcel
f1032865f3 fix(ci): guard against empty HOST_IP in smoke test
If `ip route show default` returns no output the old code passed
an empty string to curl --resolve, producing a confusing error 6
("couldn't resolve host") with no indication that gateway detection
had failed.  The new guard exits immediately with a clear message.

Addresses review concern raised by Tobias Wendt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:26:35 +02:00
Marcel
3056311c24 fix(ci): resolve smoke test host via bridge gateway, not 127.0.0.1
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 1m50s
CI / OCR Service Tests (pull_request) Successful in 17s
CI / Backend Unit Tests (pull_request) Successful in 4m8s
CI / fail2ban Regex (pull_request) Successful in 38s
CI / Compose Bucket Idempotency (pull_request) Failing after 10s
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / fail2ban Regex (push) Has been cancelled
CI / Unit & Component Tests (push) Has started running
CI / Compose Bucket Idempotency (push) Has been cancelled
Job containers run in bridge network mode (runner-config.yaml). Inside
a bridge-networked container 127.0.0.1 is the container's own loopback;
Caddy on the host is unreachable there, causing an immediate ECONNREFUSED.

Use the Docker bridge gateway IP instead — the host's docker0 interface
where Caddy (bound on 0.0.0.0:443) is reachable from the container.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:10:17 +02:00
Marcel
e9caa3a1f7 chore(renovate): require manual review for privileged CI image digest bumps
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 1m46s
CI / OCR Service Tests (pull_request) Successful in 16s
CI / Backend Unit Tests (pull_request) Successful in 4m8s
CI / fail2ban Regex (pull_request) Successful in 38s
CI / Compose Bucket Idempotency (pull_request) Failing after 11s
CI / OCR Service Tests (push) Successful in 16s
CI / Unit & Component Tests (push) Failing after 1m52s
CI / Backend Unit Tests (push) Successful in 4m11s
CI / fail2ban Regex (push) Successful in 39s
CI / Compose Bucket Idempotency (push) Failing after 10s
Adds a packageRule matching .gitea/workflows/** digest updates with
automerge: false. Digest bumps for images running --privileged --pid=host
have root-equivalent host access and must not be auto-merged.

Addresses Nora's review concern on #537.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 07:42:28 +02:00
Marcel
58922bee53 docs(ci): add Troubleshooting section for Reload Caddy failures
Covers the three failure modes Sara flagged: Caddy stopped (explicit
systemctl error), symlink missing/mis-pointed (silent reload, stale
smoke test), and Docker socket / nsenter unavailable (container error).
Each failure mode includes symptoms and recovery steps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 07:42:28 +02:00
Marcel
bbdf1c3e67 docs(adr): ADR-012 — nsenter via privileged container for host service management in CI
Captures the architectural decision, alternatives considered (sudo
systemctl, Caddy admin API, SSH), and consequences (symlink contract,
Renovate review requirement, step duplication tracked in #539).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 07:42:28 +02:00
Marcel
8536b2ebbd docs(deploy): note Caddyfile symlink is a CI dependency
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 07:42:28 +02:00
Marcel
4bb988824f docs(ci): update nsenter example to Alpine, document alternatives considered
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 07:42:28 +02:00
Marcel
544b96bc9e fix(ci): pin Reload Caddy to alpine:3.21 digest, add reload-vs-restart rationale
- Switch ubuntu:22.04 (floating, ~70 MB) to alpine:3.21 pinned by sha256
  digest (~5 MB); util-linux installed at run time via apk add
- Add explicit comment explaining why `reload` not `restart`: SIGHUP
  re-reads config in-process without dropping TLS connections

Addresses Tobias + Nora blocker from PR review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 07:42:28 +02:00
Marcel
fe2cdaae83 docs(ci): document DooD runner architecture and nsenter pattern
Replace the stale generic runner provisioning docs with an accurate
description of the actual two-container setup on the Hetzner VPS.
Document the nsenter pattern for running host-level commands (systemctl)
from containerised CI steps, and the Caddyfile symlink contract that the
reload step depends on.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 07:42:28 +02:00
Marcel
d29169eb39 fix(ci): add Caddy reload step to release workflow
Same gap as nightly.yml: production deploys also need Caddy to reload
the updated Caddyfile before the smoke test validates the public surface.
Uses the same nsenter pattern introduced in the previous commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 07:42:28 +02:00
Marcel
d750d5cee2 fix(ci): reload Caddy via nsenter, not sudo systemctl
`sudo systemctl reload caddy` does not work from inside a DooD job
container: `systemctl` is absent from Ubuntu container images and
container processes cannot reach the host systemd without entering its
namespaces. Replace with `docker run --privileged --pid=host ubuntu:22.04
nsenter -t 1 -m -u -n -p -i -- /bin/systemctl reload caddy`, which uses
the already-mounted Docker socket to spin up a privileged sibling
container that enters the host PID namespace via nsenter. Tested live on
the Hetzner VPS. No sudoers entry required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 07:42:28 +02:00
Marcel
90f52eae41 ci(nightly): reload Caddy before smoke test
Adds a `sudo systemctl reload caddy` step between the docker compose
deploy and the smoke test. This ensures any committed Caddyfile changes
are applied before the public surface is verified.

Previously the workflow had no mechanism to push Caddyfile changes to
the running host daemon. A Caddyfile edit would land in the repo but
Caddy would keep serving the previous config, causing the smoke test to
catch a stale header or still-proxied /actuator route rather than the
intended current config.

This step also surfaces the root cause of today's port-443 failure
explicitly: if Caddy is not running, the step fails with a clear service
error rather than a misleading "Failed to connect to port 443" from curl.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 07:42:28 +02:00
Marcel
dacc7d6ff8 test(admin): convert .not.toThrow into form-stays-mounted assertion (admin/groups/new)
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m4s
CI / OCR Service Tests (push) Successful in 17s
CI / Backend Unit Tests (push) Successful in 4m5s
CI / fail2ban Regex (push) Successful in 39s
CI / Compose Bucket Idempotency (push) Failing after 10s
nightly / deploy-staging (push) Failing after 1m25s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
e9d7b6568c test(admin): convert .not.toThrow into merge-success-banner-absent assertion (admin/tags/[id])
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
b67ac17eef test(admin): convert .not.toThrow into form-stays-mounted assertion (admin/users/new)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
6ba89da829 test(geschichten): convert .not.toThrow into person-filter chip rendering assertion
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
de55a4e7ab test(persons): convert .not.toThrow self-skip test into Self-letter rendering assertion
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
56930fb586 test(briefwechsel): convert 3 .not.toThrow to localStorage / container assertions
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
fec2b2ccbd test(routes): convert 3 .not.toThrow in home page to main/h1 assertions
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
d4ae74d9a5 test(admin): replace 1 setTimeout sleep in invites page with vi.waitFor
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
d754e23922 test(tags): replace 1 setTimeout sleep in TagTreeNode with vi.waitFor
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
6da686ccea test(briefwechsel): replace 1 setTimeout sleep in CorrespondenzPersonBar with vi.waitFor
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
df75a0b5f3 test(documents): replace 1 setTimeout sleep in bulk-edit page with vi.waitFor
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
eb666b2eb3 test(transcription): replace 3 setTimeout sleeps in TranscriptionEditView with vi.waitFor
Also replaces a vacuous expect(true).toBe(true) with a real behavioral
assertion that both block texts remain rendered after rerender.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
b4c249c489 test(documents): replace 2 setTimeout sleeps in [id]/edit page with vi.waitFor
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
0e9d88eed4 test(document): replace 2 setTimeout sleeps in WhoWhenSection with vi.waitFor
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
dccd000d66 test(routes): drop 2 setTimeout sleeps in AppNav (auto-wait via expect.element)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
1035527278 test(person): replace 7 setTimeout sleeps in AddRelationshipForm with vi.waitFor
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
910f890c75 test(ocr): replace 8 setTimeout sleeps in OcrProgress with vi.waitFor
waitForSource() helper polls for the EventSource constructor effect
to register the mock; assertion blocks use vi.waitFor on the progress
bar / heading / button changes after each SSE event dispatch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
f044e8f499 test(register): replace 8 setTimeout sleeps with vi.waitFor on reactive state changes
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
ebfa20dde5 test(admin): rewrite admin/system page test with vi.waitFor
Replaces 15 setTimeout sleeps with vi.waitFor on the actual signal
(fetch URL recorded, banner appears, status text rendered) and
switches the default fetch mock from mockResolvedValue to
mockImplementation so each call yields a fresh Response — no more
"body stream already read" unhandled rejections.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
6c7d696d56 test(discussion): rewrite MentionEditor test with vi.waitFor
Replaces 16 setTimeout(350ms / 30ms / 50ms) sleeps with vi.waitFor on
the actual signal — popup listbox appearance/disappearance, option
aria-selected state — so the test no longer races the 200ms internal
debounce against the real clock under CI load.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
e70511a8f8 test(admin): rewrite EntityNav flyout tests with behavioral assertions
Replaces the vacuous expect(true).toBe(true) sleep test with a real
flyout-open assertion (role=dialog appears after trigger click) and
turns the Escape-keydown smoke test into a full open→Escape→closed
behavioral test. Routes the Escape event through document (matches
the svelte:document binding) instead of window.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
a483c1020f test(ocr): convert useOcrJob polling tests to fake timers
Replaces 2 setTimeout-based wait() helpers with vi.useFakeTimers() +
vi.advanceTimersByTimeAsync() so the polling-loop tests no longer
race against the real clock under CI load — they instead deterministically
advance the setInterval by the exact poll interval and let microtasks
flush. Also converts the destroy() .not.toThrow smoke into a direct
expect(job.destroy()).toBeUndefined() check.

Per Sara: polling-loop tests are the legitimate case for fake timers
(time progression matters) — exactly the pattern she requested.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
29672c066b test(document): rewrite FileSwitcherStrip test with behavioral assertions
Replaces 3 setTimeout sleeps with vi.waitFor on document.activeElement
during keyboard nav, and converts 2 .not.toThrow smoke tests on the
prev/next buttons into no-op assertions: with a single file in the
strip the active chip stays selected and onSelect is not invoked.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
ca6342363a test(person): rewrite PersonTypeahead test with behavioral assertions
Replaces 3 setTimeout sleeps with vi.waitFor on listbox / aria-expanded
state and converts 2 .not.toThrow smoke tests + 1 vacuous expect(true)
into assertions about the input remaining usable after fetch errors
and Escape on a closed dropdown being a no-op.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
f3915c4878 test(discussion): rewrite CommentThread test with behavioral assertions
Replaces 8 setTimeout sleeps with vi.waitFor on the actual signal
(textarea value, fetch URL recorded, onCountChange call) and converts
3 .not.toThrow smoke tests into behavioural assertions:

- "no onCountChange wired" → asserts initial comment text still renders
- "network error during reload" → asserts empty-hint state is shown
- "non-OK reload" → asserts empty-hint state is shown

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
251891fbed test(routes): rewrite DropZone test with behavioral assertions
Replaces 5 setTimeout sleeps with vi.waitFor on the actual class
transition, and converts 6 .not.toThrow smoke tests into assertions
that the validation guard surfaces the expected error message (or
absence thereof). Tightens the dragging-state regex to bg-accent-bg
so it cannot match the idle hover:border-primary substring.

Runtime: faster + deterministic.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00