test(e2e): follow-up gaps from legibility pre-flight (#402) #431

Open
opened 2026-05-05 19:11:57 +02:00 by marcel · 8 comments
Owner

Context

PR #430 (legibility pre-flight — make tests trustworthy) fixed the most critical E2E accuracy issues. A number of smaller gaps were intentionally deferred to keep the PR focused. This issue tracks everything that was left behind.


Deferred items

1. J10 — Full notification bell flow not tested

The current bell test only verifies open/close (Escape). It does not cover:

  • Unread count badge appears when notifications exist
  • Clicking a notification marks it as read
  • Badge disappears / count decrements after mark-as-read
  • Clicking a notification navigates to the target document

Action: Extend notification-deep-link.spec.ts bell describe with these three steps.


2. J6 — Sender filter is untested

The J6 search test covers text + tag AND combination. The sender (?sender=<uuid>) filter has no E2E coverage.

Action: Add a test that creates a document with a seeded sender via API, then verifies that searching with ?sender={personId} returns that document and filters out others.


3. Person accumulation — no DELETE /api/persons endpoint

The J5 test seeds two persons on every run (using a timestamp-based lastName to avoid collisions). Those persons are never cleaned up because PersonController has no DELETE /{id} endpoint.

Action (two options — pick one):

  • (a) Add DELETE /api/persons/{id} endpoint (admin-only) and wire up afterAll in J5.
  • (b) Accept accumulation for now; add a dev-only admin tool to purge E2E test persons by name prefix.

4. afterAll cleanup missing in older spec sections

Several older describe blocks (written before the pre-flight PR) seed documents via API but have no afterAll cleanup. Notable examples:

  • documents.spec.ts — "Document creation" and "PDF viewer" describe blocks
  • admin.spec.ts — The backfill hash test creates a document but does not delete it

Action: Audit all beforeAll blocks that call POST /api/documents and add matching afterAll deletions.


5. E2E suite not running in CI

The CI pipeline (infra/gitea/workflows/ci.yml) stops at unit/component tests. The Playwright suite requires the full Docker Compose stack and currently must be run manually.

Action: Add a Playwright job to the Gitea CI pipeline that:

  1. Starts docker-compose up -d
  2. Waits for backend health (/actuator/health)
  3. Runs npm run test:e2e
  4. Uploads test-results/ as an artifact on failure

6. Negative registration tests

The J1 registration flow is covered (happy path). Negative cases are not:

  • Invite code that has already been used
  • Invite code that has expired
  • Mismatched passwords

Action: Add negative test cases to auth.spec.ts registration describe.


7. Mobile viewport — read path

Issue #402 noted that the read path is critical on phones (320 px). The deep-link test runs at two viewports but most other tests run at desktop only.

Action: Identify 3–5 read-path tests (document detail, person detail, search results) and run them at { width: 320, height: 700 } as well.


Acceptance criteria

  • J10 bell test covers open, badge, mark-as-read, and navigate steps
  • J6 sender filter test added
  • Person cleanup strategy decided and implemented (option a or b above)
  • All beforeAll API-seeded blocks have matching afterAll teardown
  • E2E suite runs in CI on every push to main
  • Negative registration tests added
  • 3–5 read-path tests run at mobile 320 px viewport
## Context PR #430 (legibility pre-flight — make tests trustworthy) fixed the most critical E2E accuracy issues. A number of smaller gaps were intentionally deferred to keep the PR focused. This issue tracks everything that was left behind. --- ## Deferred items ### 1. J10 — Full notification bell flow not tested The current bell test only verifies open/close (Escape). It does **not** cover: - Unread count badge appears when notifications exist - Clicking a notification marks it as read - Badge disappears / count decrements after mark-as-read - Clicking a notification navigates to the target document **Action:** Extend `notification-deep-link.spec.ts` bell describe with these three steps. --- ### 2. J6 — Sender filter is untested The J6 search test covers text + tag AND combination. The sender (`?sender=<uuid>`) filter has no E2E coverage. **Action:** Add a test that creates a document with a seeded sender via API, then verifies that searching with `?sender={personId}` returns that document and filters out others. --- ### 3. Person accumulation — no `DELETE /api/persons` endpoint The J5 test seeds two persons on every run (using a timestamp-based `lastName` to avoid collisions). Those persons are never cleaned up because `PersonController` has no `DELETE /{id}` endpoint. **Action (two options — pick one):** - (a) Add `DELETE /api/persons/{id}` endpoint (admin-only) and wire up `afterAll` in J5. - (b) Accept accumulation for now; add a dev-only admin tool to purge E2E test persons by name prefix. --- ### 4. afterAll cleanup missing in older spec sections Several older describe blocks (written before the pre-flight PR) seed documents via API but have no `afterAll` cleanup. Notable examples: - `documents.spec.ts` — "Document creation" and "PDF viewer" describe blocks - `admin.spec.ts` — The backfill hash test creates a document but does not delete it **Action:** Audit all `beforeAll` blocks that call `POST /api/documents` and add matching `afterAll` deletions. --- ### 5. E2E suite not running in CI The CI pipeline (`infra/gitea/workflows/ci.yml`) stops at unit/component tests. The Playwright suite requires the full Docker Compose stack and currently must be run manually. **Action:** Add a Playwright job to the Gitea CI pipeline that: 1. Starts `docker-compose up -d` 2. Waits for backend health (`/actuator/health`) 3. Runs `npm run test:e2e` 4. Uploads `test-results/` as an artifact on failure --- ### 6. Negative registration tests The J1 registration flow is covered (happy path). Negative cases are not: - Invite code that has already been used - Invite code that has expired - Mismatched passwords **Action:** Add negative test cases to `auth.spec.ts` registration describe. --- ### 7. Mobile viewport — read path Issue #402 noted that the read path is critical on phones (320 px). The deep-link test runs at two viewports but most other tests run at desktop only. **Action:** Identify 3–5 read-path tests (document detail, person detail, search results) and run them at `{ width: 320, height: 700 }` as well. --- ## Acceptance criteria - [ ] J10 bell test covers open, badge, mark-as-read, and navigate steps - [ ] J6 sender filter test added - [ ] Person cleanup strategy decided and implemented (option a or b above) - [ ] All `beforeAll` API-seeded blocks have matching `afterAll` teardown - [ ] E2E suite runs in CI on every push to `main` - [ ] Negative registration tests added - [ ] 3–5 read-path tests run at mobile 320 px viewport
marcel added the P2-mediumlegibilitytest labels 2026-05-05 19:12:01 +02:00
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Observations

  • Bell describe block (J10) already exists in notification-deep-link.spec.ts with beforeAll/afterAll cleanup in place — that scaffolding is reusable. The current test only asserts open + Escape close. The three missing assertions (unread badge, mark-as-read, navigate to target document) are well-scoped additions inside the existing describe block.
  • Notification backend: GET /api/notifications/unread-count exists and returns { count: N }. NotificationController also has mark-as-read endpoints. E2E can hit these via request fixtures to assert state transitions without depending on polling timing.
  • J6 sender filter: DocumentController accepts ?senderId=<uuid> (not ?sender=<uuid> as the issue says — confirm the exact query param name before writing the test). The test needs a seeded document with a known sender; POST /api/persons + POST /api/documents (with senderId) in beforeAll is the pattern used by J5 in persons.spec.ts.
  • Person accumulation (item 3): PersonController has no DELETE /{id} endpoint. J5 in persons.spec.ts already creates two persons with no cleanup — the issue correctly identifies this as accumulation debt.
  • Item 4 (afterAll audit): documents.spec.ts "Document creation" and "New document" describe blocks have no afterAll. The admin.spec.ts backfill hash test also seeds without cleanup. This is confirmed — the issue's list is accurate.
  • Negative registration tests (item 6): auth.spec.ts covers happy-path registration. The current Registration via invite code describe has no negative cases. The mismatched-password case can be tested entirely in a freshCtx with no backend state — it's a cheap add.
  • Mobile viewport (item 7): notification-deep-link.spec.ts already runs at { width: 320, height: 700 } as a positive example to follow. The other specs (documents, persons, search) run desktop-only.

Recommendations

  • J10 bell: Add assertions directly to the existing 'bell opens dropdown, shows notifications list' test OR add two new it() cases inside the same describe — whichever keeps them under 25 lines each. Check unread count via request.get('/api/notifications/unread-count') before and after clicking a notification. Use page.locator('[data-unread-count]') (or equivalent ARIA badge) for the badge visibility assertion.
  • J6 sender filter: Seed person + document in beforeAll of a new describe inside documents.spec.ts. Navigate to /?senderId={personId} (verify exact param from the backend) and assert only the seeded document appears. Use afterAll to delete both the document and the person.
  • Person DELETE (option a): Add DELETE /api/persons/{id} with @RequirePermission(Permission.WRITE_ALL) — it's a one-method addition to PersonController and removes the only cleanup blocker. Option (b) defers tech debt to an indefinite admin tool that adds UI complexity without solving the underlying E2E problem. Option (a) is the clear winner.
  • Item 4 audit: Systematically add afterAll blocks to every describe that calls POST /api/documents without a matching delete. This is mechanical work — scan for request.post('/api/documents') in all spec files.
  • Negative registration tests: Add three test cases inside the existing Registration via invite code describe: (1) used invite code returns an error, (2) expired code returns an error, (3) mismatched passwords stays on /register with an error message. Each should run in freshCtx with storageState: undefined.
  • Mobile viewport: Extract a helper runAtMobile(page) that sets viewport to { width: 320, height: 700 } and use it in a parametrized loop — same pattern used in notification-deep-link.spec.ts. Apply to: document detail, person detail, document search results, home page, and briefwechsel.
## 👨‍💻 Felix Brandt — Senior Fullstack Developer ### Observations - **Bell describe block (J10)** already exists in `notification-deep-link.spec.ts` with `beforeAll`/`afterAll` cleanup in place — that scaffolding is reusable. The current test only asserts open + Escape close. The three missing assertions (unread badge, mark-as-read, navigate to target document) are well-scoped additions inside the existing describe block. - **Notification backend**: `GET /api/notifications/unread-count` exists and returns `{ count: N }`. `NotificationController` also has mark-as-read endpoints. E2E can hit these via `request` fixtures to assert state transitions without depending on polling timing. - **J6 sender filter**: `DocumentController` accepts `?senderId=<uuid>` (not `?sender=<uuid>` as the issue says — confirm the exact query param name before writing the test). The test needs a seeded document with a known sender; `POST /api/persons` + `POST /api/documents` (with `senderId`) in `beforeAll` is the pattern used by J5 in `persons.spec.ts`. - **Person accumulation (item 3)**: `PersonController` has no `DELETE /{id}` endpoint. J5 in `persons.spec.ts` already creates two persons with no cleanup — the issue correctly identifies this as accumulation debt. - **Item 4 (afterAll audit)**: `documents.spec.ts` "Document creation" and "New document" describe blocks have no `afterAll`. The `admin.spec.ts` backfill hash test also seeds without cleanup. This is confirmed — the issue's list is accurate. - **Negative registration tests (item 6)**: `auth.spec.ts` covers happy-path registration. The current `Registration via invite code` describe has no negative cases. The mismatched-password case can be tested entirely in a `freshCtx` with no backend state — it's a cheap add. - **Mobile viewport (item 7)**: `notification-deep-link.spec.ts` already runs at `{ width: 320, height: 700 }` as a positive example to follow. The other specs (documents, persons, search) run desktop-only. ### Recommendations - **J10 bell**: Add assertions directly to the existing `'bell opens dropdown, shows notifications list'` test OR add two new `it()` cases inside the same describe — whichever keeps them under 25 lines each. Check unread count via `request.get('/api/notifications/unread-count')` before and after clicking a notification. Use `page.locator('[data-unread-count]')` (or equivalent ARIA badge) for the badge visibility assertion. - **J6 sender filter**: Seed person + document in `beforeAll` of a new describe inside `documents.spec.ts`. Navigate to `/?senderId={personId}` (verify exact param from the backend) and assert only the seeded document appears. Use `afterAll` to delete both the document and the person. - **Person DELETE (option a)**: Add `DELETE /api/persons/{id}` with `@RequirePermission(Permission.WRITE_ALL)` — it's a one-method addition to `PersonController` and removes the only cleanup blocker. Option (b) defers tech debt to an indefinite admin tool that adds UI complexity without solving the underlying E2E problem. Option (a) is the clear winner. - **Item 4 audit**: Systematically add `afterAll` blocks to every describe that calls `POST /api/documents` without a matching delete. This is mechanical work — scan for `request.post('/api/documents')` in all spec files. - **Negative registration tests**: Add three test cases inside the existing `Registration via invite code` describe: (1) used invite code returns an error, (2) expired code returns an error, (3) mismatched passwords stays on `/register` with an error message. Each should run in `freshCtx` with `storageState: undefined`. - **Mobile viewport**: Extract a helper `runAtMobile(page)` that sets viewport to `{ width: 320, height: 700 }` and use it in a parametrized loop — same pattern used in `notification-deep-link.spec.ts`. Apply to: document detail, person detail, document search results, home page, and `briefwechsel`.
Author
Owner

🏗️ Markus Keller — Application Architect

Observations

  • DELETE /api/persons/{id} gap is a domain boundary hole, not just a test convenience problem. The Person domain exposes create, read, update, and merge — but no delete. Any feature that seeds data (E2E tests, future admin tooling, data quality workflows) hits this ceiling. The gap is structural, not cosmetic.
  • CI pipeline gap (item 5): The pipeline at .gitea/workflows/ci.yml runs three jobs: unit-tests, ocr-tests, and backend-unit-tests. No Playwright job exists. The E2E suite comments in frontend/e2e/CLAUDE.md explicitly document this: "E2E tests are not currently run in CI." This is a known architectural hole in the delivery pipeline.
  • Docker Compose overlay pattern: The issue proposes docker-compose up -d in CI, but the existing pipeline never starts the full Compose stack. There is no docker-compose.ci.yml overlay in the repo. A CI job that spins up the full stack must account for startup ordering (backend needs DB + MinIO healthy before accepting requests), port exposure, and ephemeral data volumes.
  • Person accumulation is a symptom of the broader absence of a test-data lifecycle strategy. The codebase uses timestamp-based naming (Date.now().toString(36)) as a collision-avoidance hack. This works but produces unbounded database growth per test run — not a problem today, but a maintenance liability as the E2E suite grows.
  • Cross-domain coupling in J6: The sender filter test requires creating both a Person and a Document and linking them. The test will call POST /api/persons and POST /api/documents — two domain boundaries. This is expected and correct E2E behavior, but the afterAll must delete both in reverse dependency order (document first, then person).

Recommendations

  • Add DELETE /api/persons/{id} as a first-class endpoint (admin-scoped: @RequirePermission(Permission.ADMIN) rather than WRITE_ALL, since deleting a historical person has irreversible archival consequences). Update PersonController and PersonService. Write the failing unit test first.
  • Design a docker-compose.ci.yml overlay before writing the Playwright CI job. The overlay should: use named (not bind-mounted) volumes, not expose postgres port externally, set POSTGRES_PASSWORD and MINIO_ROOT_PASSWORD from Gitea secrets. The existing docker-compose.yml uses bind mounts for postgres data — that needs an override for CI.
  • Add a wait-for-health step in the CI Playwright job (poll http://localhost:8080/actuator/health) with a timeout of 120 seconds before running any tests. Use a simple until loop, not sleep. The backend boots faster than OCR startup — no need to wait for OCR in E2E.
  • Formalize a test-data prefix convention: All E2E-created records should share a prefix like E2E- in their name/title. A periodic admin purge of records matching this prefix is then trivially safe. This doesn't replace afterAll cleanup but adds a safety net for interrupted test runs.

Open Decisions

  • Permission scope for DELETE /api/persons/{id}: WRITE_ALL (standard writer) vs. ADMIN (admin-only). Deleting a historical person cascades to document sender/receiver links and is irreversible — this feels closer to ADMIN scope. But WRITE_ALL is what all other mutation endpoints use. Which principal should be able to delete a person?
## 🏗️ Markus Keller — Application Architect ### Observations - **`DELETE /api/persons/{id}` gap is a domain boundary hole**, not just a test convenience problem. The Person domain exposes create, read, update, and merge — but no delete. Any feature that seeds data (E2E tests, future admin tooling, data quality workflows) hits this ceiling. The gap is structural, not cosmetic. - **CI pipeline gap (item 5)**: The pipeline at `.gitea/workflows/ci.yml` runs three jobs: `unit-tests`, `ocr-tests`, and `backend-unit-tests`. No Playwright job exists. The E2E suite comments in `frontend/e2e/CLAUDE.md` explicitly document this: "E2E tests are not currently run in CI." This is a known architectural hole in the delivery pipeline. - **Docker Compose overlay pattern**: The issue proposes `docker-compose up -d` in CI, but the existing pipeline never starts the full Compose stack. There is no `docker-compose.ci.yml` overlay in the repo. A CI job that spins up the full stack must account for startup ordering (backend needs DB + MinIO healthy before accepting requests), port exposure, and ephemeral data volumes. - **Person accumulation is a symptom** of the broader absence of a test-data lifecycle strategy. The codebase uses timestamp-based naming (`Date.now().toString(36)`) as a collision-avoidance hack. This works but produces unbounded database growth per test run — not a problem today, but a maintenance liability as the E2E suite grows. - **Cross-domain coupling in J6**: The sender filter test requires creating both a Person and a Document and linking them. The test will call `POST /api/persons` and `POST /api/documents` — two domain boundaries. This is expected and correct E2E behavior, but the `afterAll` must delete both in reverse dependency order (document first, then person). ### Recommendations - **Add `DELETE /api/persons/{id}` as a first-class endpoint** (admin-scoped: `@RequirePermission(Permission.ADMIN)` rather than `WRITE_ALL`, since deleting a historical person has irreversible archival consequences). Update `PersonController` and `PersonService`. Write the failing unit test first. - **Design a `docker-compose.ci.yml` overlay** before writing the Playwright CI job. The overlay should: use named (not bind-mounted) volumes, not expose postgres port externally, set `POSTGRES_PASSWORD` and `MINIO_ROOT_PASSWORD` from Gitea secrets. The existing `docker-compose.yml` uses bind mounts for postgres data — that needs an override for CI. - **Add a `wait-for-health` step** in the CI Playwright job (poll `http://localhost:8080/actuator/health`) with a timeout of 120 seconds before running any tests. Use a simple `until` loop, not `sleep`. The backend boots faster than OCR startup — no need to wait for OCR in E2E. - **Formalize a test-data prefix convention**: All E2E-created records should share a prefix like `E2E-` in their name/title. A periodic admin purge of records matching this prefix is then trivially safe. This doesn't replace `afterAll` cleanup but adds a safety net for interrupted test runs. ### Open Decisions - **Permission scope for `DELETE /api/persons/{id}`**: `WRITE_ALL` (standard writer) vs. `ADMIN` (admin-only). Deleting a historical person cascades to document sender/receiver links and is irreversible — this feels closer to `ADMIN` scope. But `WRITE_ALL` is what all other mutation endpoints use. Which principal should be able to delete a person?
Author
Owner

🛡️ Nora "NullX" Steiner — Security Engineer

Observations

  • DELETE /api/persons/{id} — cascade risk: If this endpoint is added (option a), it will cascade to document sender/receiver relationships. A WRITE_ALL user who can delete persons could silently sever document–person links for the entire family archive. This is data destruction, not just record deletion. The permission level and cascade behavior need explicit design.
  • E2E hardcoded admin credentials: auth.setup.ts logs in with test credentials. The CI workflow must source these from Gitea secrets, not hardcoded values in workflow YAML. Looking at devops.md's persona notes: ${{ secrets.E2E_ADMIN_PASSWORD }} is the required pattern. If someone hardcodes admin123 in the CI job YAML, that secret is committed to git history.
  • Negative registration tests (item 6): Testing that an expired or already-used invite code is rejected is a security requirement, not just a coverage gap. A regression here means any user can register with a stale code. This should be framed as a security regression test, not a nice-to-have coverage addition.
  • Invite code leakage in screenshots: auth.spec.ts takes a screenshot at test-results/e2e/admin-invite-created.png while the invite table is visible. If test-results/ is uploaded as a CI artifact on every run (not just failure), invite codes could be visible in artifact storage. This is low risk in a self-hosted private Gitea, but worth noting.
  • J10 bell mark-as-read test: The NotificationController has mark-as-read endpoints — the test should verify that after clicking a notification, the unread count drops to zero via GET /api/notifications/unread-count. This proves the server-side state changed, not just that the UI updated optimistically.

Recommendations

  • Scope DELETE /api/persons/{id} to Permission.ADMIN — not WRITE_ALL. Document the cascade behavior in a comment on the endpoint. Add a soft-delete flag (deletedAt) rather than hard delete so document–person links survive as orphaned references rather than null pointers. This is an archival system: irreversible data deletion needs a higher permission bar.
  • E2E credentials must come from Gitea secrets: When writing the CI Playwright job, use ${{ secrets.E2E_ADMIN_PASSWORD }} and inject it via an environment variable, not a hardcoded value. Add a comment in the workflow YAML explaining this is a test-only account with no production data.
  • Treat negative registration tests as security regression tests: Title them explicitly (e.g., 'rejects already-used invite code — prevents replay attack') so their security intent is visible in CI logs.
  • Limit artifact upload scope: In the CI Playwright job, upload test-results/ artifacts only if: failure(), not if: always(). The issue's spec suggests "on failure" — stick to that. Reduces artifact storage and avoids exposing any seeded test data.
  • Verify mark-as-read server-side in the J10 bell test: After clicking a notification in the bell test, assert via request.get('/api/notifications/unread-count') that the count decremented, not just that the badge disappeared from the DOM.
## 🛡️ Nora "NullX" Steiner — Security Engineer ### Observations - **`DELETE /api/persons/{id}` — cascade risk**: If this endpoint is added (option a), it will cascade to document sender/receiver relationships. A `WRITE_ALL` user who can delete persons could silently sever document–person links for the entire family archive. This is data destruction, not just record deletion. The permission level and cascade behavior need explicit design. - **E2E hardcoded admin credentials**: `auth.setup.ts` logs in with test credentials. The CI workflow must source these from Gitea secrets, not hardcoded values in workflow YAML. Looking at `devops.md`'s persona notes: `${{ secrets.E2E_ADMIN_PASSWORD }}` is the required pattern. If someone hardcodes `admin123` in the CI job YAML, that secret is committed to git history. - **Negative registration tests (item 6)**: Testing that an expired or already-used invite code is rejected is a security requirement, not just a coverage gap. A regression here means any user can register with a stale code. This should be framed as a security regression test, not a nice-to-have coverage addition. - **Invite code leakage in screenshots**: `auth.spec.ts` takes a screenshot at `test-results/e2e/admin-invite-created.png` while the invite table is visible. If `test-results/` is uploaded as a CI artifact on every run (not just failure), invite codes could be visible in artifact storage. This is low risk in a self-hosted private Gitea, but worth noting. - **J10 bell mark-as-read test**: The `NotificationController` has mark-as-read endpoints — the test should verify that after clicking a notification, the unread count drops to zero via `GET /api/notifications/unread-count`. This proves the server-side state changed, not just that the UI updated optimistically. ### Recommendations - **Scope `DELETE /api/persons/{id}` to `Permission.ADMIN`** — not `WRITE_ALL`. Document the cascade behavior in a comment on the endpoint. Add a soft-delete flag (`deletedAt`) rather than hard delete so document–person links survive as orphaned references rather than null pointers. This is an archival system: irreversible data deletion needs a higher permission bar. - **E2E credentials must come from Gitea secrets**: When writing the CI Playwright job, use `${{ secrets.E2E_ADMIN_PASSWORD }}` and inject it via an environment variable, not a hardcoded value. Add a comment in the workflow YAML explaining this is a test-only account with no production data. - **Treat negative registration tests as security regression tests**: Title them explicitly (e.g., `'rejects already-used invite code — prevents replay attack'`) so their security intent is visible in CI logs. - **Limit artifact upload scope**: In the CI Playwright job, upload `test-results/` artifacts only `if: failure()`, not `if: always()`. The issue's spec suggests "on failure" — stick to that. Reduces artifact storage and avoids exposing any seeded test data. - **Verify mark-as-read server-side in the J10 bell test**: After clicking a notification in the bell test, assert via `request.get('/api/notifications/unread-count')` that the count decremented, not just that the badge disappeared from the DOM.
Author
Owner

🧪 Sara Holt — QA Engineer & Test Strategist

Observations

  • J10 bell test is incomplete, not missing: The existing describe in notification-deep-link.spec.ts seeds a document + comment and has proper beforeAll/afterAll. The scaffolding for a richer bell test is already there — only the assertions are missing. This is an extension task, not a rewrite.
  • No afterAll in "Document creation" and "PDF viewer" in documents.spec.ts: Confirmed. These describe blocks call POST /api/documents in setup steps but have no matching delete. Every test run leaks documents. For a small archive this accumulates quickly.
  • J5 person accumulation: The Person creation describe in persons.spec.ts creates E2E Testperson on every run with no afterAll. Combined with the J5 relationship test creating two more persons per run, this produces 3+ persons per full E2E run. Without a DELETE endpoint, there is no mechanical fix possible today.
  • Sender filter test (J6): The issue says ?sender=<uuid> but DocumentController accepts ?senderId=<uuid>. The test will fail to filter anything if the wrong param name is used. Verify with curl '/?senderId={id}' against a running backend before writing the test.
  • Negative registration tests: The happy-path test uses a freshCtx correctly. The negative tests for expired/used codes need the same pattern — fresh context with no admin session. The "mismatched passwords" case is entirely client-side validation (HTML5 or SvelteKit form action) — test by filling passwordConfirm with a different value and asserting the page stays on /register.
  • Mobile viewport (item 7): The issue says "3–5 read-path tests" but doesn't name them. Recommend: home/search list, document detail, person detail, briefwechsel (conversation), and history/Chronik. These are the 5 flows cited as critical in issue #402.
  • E2E suite time: The current suite with workers: 1 runs sequentially. Adding 7 deferred items will extend runtime. The target from Sara's pyramid model is <8 minutes. Before adding more tests, measure current runtime and confirm headroom.

Recommendations

  • Bell (J10): Add three steps to the existing 'bell opens dropdown' test OR split into three focused it() cases. Name them clearly: 'bell badge shows unread count when notifications exist', 'clicking a notification marks it as read and decrements badge', 'clicking a notification navigates to the target document'. One assertion per test.
  • Prevent param name bugs in J6: Add a comment // Backend param: ?senderId (not ?sender) at the top of the test. Seed a person with a unique last name (timestamp), create a document with that person as sender, search with ?senderId={id}, and assert the specific document title appears. Seed a second unrelated document and assert it does NOT appear.
  • Person DELETE — choose option (a): Without afterAll cleanup, the E2E suite's test data grows unbounded. Option (b) (a dev admin tool) is manual and fragile. Add DELETE /api/persons/{id} now and add afterAll to J5 and the person creation describe. This is the only durable solution.
  • afterAll audit: Use grep -rn "request.post('/api/documents')" frontend/e2e/ to find all seeding calls, then check each describe for a matching request.delete. Add the delete in afterAll for every describe that's missing it. Do this as a single atomic commit so the audit is complete.
  • Mobile viewport parametrize: Follow the exact pattern in notification-deep-link.spec.ts — a for (const viewport of [...]) loop around the five read-path tests. Use 320×700 only (the issue specifies 320px).
  • Measure CI time before adding tests: Run time npm run test:e2e locally and note the current duration. Add a comment in playwright.config.ts with the baseline so future additions can be checked against the <8 min target.
## 🧪 Sara Holt — QA Engineer & Test Strategist ### Observations - **J10 bell test is incomplete, not missing**: The existing describe in `notification-deep-link.spec.ts` seeds a document + comment and has proper `beforeAll`/`afterAll`. The scaffolding for a richer bell test is already there — only the assertions are missing. This is an extension task, not a rewrite. - **No `afterAll` in "Document creation" and "PDF viewer" in `documents.spec.ts`**: Confirmed. These describe blocks call `POST /api/documents` in setup steps but have no matching delete. Every test run leaks documents. For a small archive this accumulates quickly. - **J5 person accumulation**: The `Person creation` describe in `persons.spec.ts` creates `E2E Testperson` on every run with no `afterAll`. Combined with the J5 relationship test creating two more persons per run, this produces 3+ persons per full E2E run. Without a DELETE endpoint, there is no mechanical fix possible today. - **Sender filter test (J6)**: The issue says `?sender=<uuid>` but `DocumentController` accepts `?senderId=<uuid>`. The test will fail to filter anything if the wrong param name is used. Verify with `curl '/?senderId={id}'` against a running backend before writing the test. - **Negative registration tests**: The happy-path test uses a `freshCtx` correctly. The negative tests for expired/used codes need the same pattern — fresh context with no admin session. The "mismatched passwords" case is entirely client-side validation (HTML5 or SvelteKit form action) — test by filling `passwordConfirm` with a different value and asserting the page stays on `/register`. - **Mobile viewport (item 7)**: The issue says "3–5 read-path tests" but doesn't name them. Recommend: home/search list, document detail, person detail, briefwechsel (conversation), and history/Chronik. These are the 5 flows cited as critical in issue #402. - **E2E suite time**: The current suite with `workers: 1` runs sequentially. Adding 7 deferred items will extend runtime. The target from Sara's pyramid model is <8 minutes. Before adding more tests, measure current runtime and confirm headroom. ### Recommendations - **Bell (J10)**: Add three steps to the existing `'bell opens dropdown'` test OR split into three focused `it()` cases. Name them clearly: `'bell badge shows unread count when notifications exist'`, `'clicking a notification marks it as read and decrements badge'`, `'clicking a notification navigates to the target document'`. One assertion per test. - **Prevent param name bugs in J6**: Add a comment `// Backend param: ?senderId (not ?sender)` at the top of the test. Seed a person with a unique last name (timestamp), create a document with that person as sender, search with `?senderId={id}`, and assert the specific document title appears. Seed a second unrelated document and assert it does NOT appear. - **Person DELETE — choose option (a)**: Without `afterAll` cleanup, the E2E suite's test data grows unbounded. Option (b) (a dev admin tool) is manual and fragile. Add `DELETE /api/persons/{id}` now and add `afterAll` to J5 and the person creation describe. This is the only durable solution. - **`afterAll` audit**: Use `grep -rn "request.post('/api/documents')" frontend/e2e/` to find all seeding calls, then check each describe for a matching `request.delete`. Add the delete in `afterAll` for every describe that's missing it. Do this as a single atomic commit so the audit is complete. - **Mobile viewport parametrize**: Follow the exact pattern in `notification-deep-link.spec.ts` — a `for (const viewport of [...])` loop around the five read-path tests. Use 320×700 only (the issue specifies 320px). - **Measure CI time before adding tests**: Run `time npm run test:e2e` locally and note the current duration. Add a comment in `playwright.config.ts` with the baseline so future additions can be checked against the <8 min target.
Author
Owner

⚙️ Tobias Wendt — DevOps & Platform Engineer

Observations

  • No docker-compose.ci.yml overlay exists: The existing pipeline never starts the full stack. Adding a Playwright job that calls docker-compose up -d against the development docker-compose.yml will bring in bind mounts for PostgreSQL data (./data/postgres:/var/lib/postgresql/data) — which is not safe for CI. Those paths don't exist on a fresh runner.
  • Current CI pipeline is clean: Three jobs, actions/cache@v4 for both Maven and node_modules, actions/upload-artifact@v4 (not the deprecated v3). The build hygiene is good. The Playwright job can follow the same structure.
  • Health check polling: The issue says "Waits for backend health (/actuator/health)". The backend only exposes /actuator/health — good. But the Playwright webServer config in playwright.config.ts already handles frontend dev server startup via npm run dev -- --port 3000. In CI with a real Docker Compose stack, the E2E tests should point at the built frontend (port 3000 from the SvelteKit Node adapter), not the dev server.
  • Runner context: The CI runs on the NAS Gitea runner (Docker 24.x, DOCKER_API_VERSION: "1.43" already set in backend job). The Playwright job needs the same DOCKER_API_VERSION env var if it uses Testcontainers — but for the E2E job, it's spawning Compose services directly, not via Testcontainers. Confirm the NAS runner has Docker Compose v2 (docker compose not docker-compose).
  • Artifact size: Playwright stores screenshots, videos, and traces. The issue correctly specifies upload only on failure. With screenshot: 'on' in playwright.config.ts, screenshots are always captured — that means even passing tests produce screenshot files. Consider screenshot: 'only-on-failure' in the CI profile to reduce artifact size.
  • Sequential tests (workers: 1): The E2E suite runs sequentially by design (shared auth state). This is unavoidable unless auth is refactored to per-test isolation. In CI this means the Playwright job runtime is the sum of all tests, not parallelized. Plan accordingly.

Recommendations

  • Create docker-compose.ci.yml as a proper overlay before writing the CI job. It should override: volumes (named instead of bind mounts), remove ports exposure for postgres and minio (use expose instead), and set credentials via env vars sourced from Gitea secrets. Base: docker-compose up -d db minio backend frontend.
  • CI Playwright job structure (concrete):
    e2e-tests:
      name: E2E Tests (Playwright)
      runs-on: ubuntu-latest
      needs: [unit-tests, backend-unit-tests]
      container:
        image: mcr.microsoft.com/playwright:v1.58.2-noble
      env:
        DOCKER_API_VERSION: "1.43"
        E2E_BASE_URL: http://localhost:3000
      steps:
        - uses: actions/checkout@v4
        - name: Start stack
          run: docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d
        - name: Wait for backend health
          run: until curl -sf http://localhost:8080/actuator/health; do sleep 3; done
          timeout-minutes: 2
        - name: Cache node_modules
          uses: actions/cache@v4
          with:
            path: frontend/node_modules
            key: node-modules-${{ hashFiles('frontend/package-lock.json') }}
        - name: Install deps
          if: steps.node-modules-cache.outputs.cache-hit != 'true'
          run: npm ci
          working-directory: frontend
        - name: Compile i18n
          run: npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide
          working-directory: frontend
        - name: Run E2E tests
          run: npm run test:e2e
          working-directory: frontend
          env:
            E2E_ADMIN_PASSWORD: ${{ secrets.E2E_ADMIN_PASSWORD }}
        - name: Upload artifacts on failure
          if: failure()
          uses: actions/upload-artifact@v4
          with:
            name: e2e-test-results
            path: frontend/test-results/
    
  • Switch screenshot to 'only-on-failure' in playwright.config.ts for the CI profile (or add a separate playwright.config.ci.ts). Keep 'on' for local runs where screenshots aid debugging. This halves artifact size on green runs.
  • Use docker compose (v2 syntax), not docker-compose (v1). Verify the NAS runner has Compose v2 (docker compose version) before writing the workflow. If v1 only, use the hyphen form — but plan to migrate.
  • Add a 2-minute hard timeout on the health-wait step, not just the loop. A stuck backend would otherwise block the runner indefinitely.
## ⚙️ Tobias Wendt — DevOps & Platform Engineer ### Observations - **No `docker-compose.ci.yml` overlay exists**: The existing pipeline never starts the full stack. Adding a Playwright job that calls `docker-compose up -d` against the development `docker-compose.yml` will bring in bind mounts for PostgreSQL data (`./data/postgres:/var/lib/postgresql/data`) — which is not safe for CI. Those paths don't exist on a fresh runner. - **Current CI pipeline is clean**: Three jobs, `actions/cache@v4` for both Maven and node_modules, `actions/upload-artifact@v4` (not the deprecated v3). The build hygiene is good. The Playwright job can follow the same structure. - **Health check polling**: The issue says "Waits for backend health (`/actuator/health`)". The backend only exposes `/actuator/health` — good. But the Playwright `webServer` config in `playwright.config.ts` already handles frontend dev server startup via `npm run dev -- --port 3000`. In CI with a real Docker Compose stack, the E2E tests should point at the built frontend (port 3000 from the SvelteKit Node adapter), not the dev server. - **Runner context**: The CI runs on the NAS Gitea runner (Docker 24.x, `DOCKER_API_VERSION: "1.43"` already set in backend job). The Playwright job needs the same `DOCKER_API_VERSION` env var if it uses Testcontainers — but for the E2E job, it's spawning Compose services directly, not via Testcontainers. Confirm the NAS runner has Docker Compose v2 (`docker compose` not `docker-compose`). - **Artifact size**: Playwright stores screenshots, videos, and traces. The issue correctly specifies upload only on failure. With `screenshot: 'on'` in `playwright.config.ts`, screenshots are always captured — that means even passing tests produce screenshot files. Consider `screenshot: 'only-on-failure'` in the CI profile to reduce artifact size. - **Sequential tests (`workers: 1`)**: The E2E suite runs sequentially by design (shared auth state). This is unavoidable unless auth is refactored to per-test isolation. In CI this means the Playwright job runtime is the sum of all tests, not parallelized. Plan accordingly. ### Recommendations - **Create `docker-compose.ci.yml`** as a proper overlay before writing the CI job. It should override: `volumes` (named instead of bind mounts), remove `ports` exposure for postgres and minio (use `expose` instead), and set credentials via env vars sourced from Gitea secrets. Base: `docker-compose up -d db minio backend frontend`. - **CI Playwright job structure** (concrete): ```yaml e2e-tests: name: E2E Tests (Playwright) runs-on: ubuntu-latest needs: [unit-tests, backend-unit-tests] container: image: mcr.microsoft.com/playwright:v1.58.2-noble env: DOCKER_API_VERSION: "1.43" E2E_BASE_URL: http://localhost:3000 steps: - uses: actions/checkout@v4 - name: Start stack run: docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d - name: Wait for backend health run: until curl -sf http://localhost:8080/actuator/health; do sleep 3; done timeout-minutes: 2 - name: Cache node_modules uses: actions/cache@v4 with: path: frontend/node_modules key: node-modules-${{ hashFiles('frontend/package-lock.json') }} - name: Install deps if: steps.node-modules-cache.outputs.cache-hit != 'true' run: npm ci working-directory: frontend - name: Compile i18n run: npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide working-directory: frontend - name: Run E2E tests run: npm run test:e2e working-directory: frontend env: E2E_ADMIN_PASSWORD: ${{ secrets.E2E_ADMIN_PASSWORD }} - name: Upload artifacts on failure if: failure() uses: actions/upload-artifact@v4 with: name: e2e-test-results path: frontend/test-results/ ``` - **Switch `screenshot` to `'only-on-failure'`** in `playwright.config.ts` for the CI profile (or add a separate `playwright.config.ci.ts`). Keep `'on'` for local runs where screenshots aid debugging. This halves artifact size on green runs. - **Use `docker compose` (v2 syntax)**, not `docker-compose` (v1). Verify the NAS runner has Compose v2 (`docker compose version`) before writing the workflow. If v1 only, use the hyphen form — but plan to migrate. - **Add a 2-minute hard timeout on the health-wait step**, not just the loop. A stuck backend would otherwise block the runner indefinitely.
Author
Owner

📋 Elicit — Requirements Engineer

Observations

  • Issue quality is high for a deferred-items tracker: Each item has a clear action, a named location in the codebase, and testable acceptance criteria. The issue is definition-of-ready compliant for most items.
  • Item 3 (person accumulation) has two options but the acceptance criterion doesn't specify which: "Person cleanup strategy decided and implemented (option a or b above)" is the criterion — but the criterion is satisfied by either option, making it untestable until the decision is made. This is a hidden dependency on a design decision, not a directly implementable task.
  • Item 5 (CI pipeline) has implicit NFRs that aren't stated: "E2E suite runs in CI on every push to main" implies: (a) the runner must support Docker Compose, (b) a Gitea secret must exist for E2E credentials, (c) a CI overlay Compose file must exist. None of these preconditions appear in the acceptance criteria.
  • Item 7 (mobile viewport) is scoped loosely: "3–5 read-path tests" is a range, not a specification. Which 3? Which 5? Without naming them, done-ness is subjective.
  • Item 6 (negative registration) doesn't define "expired" semantically: How long does an invite live before it expires? Is expiry time-based or use-based? The test needs a way to create an expired or used invite — this requires either backend API support for setting expiry or a test that uses an already-consumed code from a prior test step.

Recommendations

  • Split item 3 into two sub-tasks: (3a) Decision: admin chooses option a or b. (3b) Implementation: implement the chosen option. Add a separate acceptance criterion for each. The current single criterion blocks testability.
  • Expand item 5's preconditions into explicit sub-tasks: (5a) Create docker-compose.ci.yml overlay, (5b) Add E2E_ADMIN_PASSWORD Gitea secret, (5c) Verify NAS runner supports Docker Compose v2, (5d) Add Playwright CI job to ci.yml. The current single acceptance criterion ("E2E suite runs in CI on every push to main") hides four independent prerequisites.
  • Name the specific 5 read-path tests for item 7: Home/search list, document detail, person detail, Chronik/activity feed, Briefwechsel conversation timeline. These match the "critical read path" described in issue #402. Replace "3–5 tests" with a named list in the acceptance criteria.
  • Clarify invite expiry behavior before writing negative tests for item 6: Check the backend (InviteController or equivalent) for how expiry is implemented, then write acceptance criteria that can be tested deterministically. If expiry requires time manipulation, consider using an invite code from a prior test run (mark-as-used) for the replay test, and skip the expiry test if the backend doesn't expose a way to create pre-expired codes.
  • Add a CI time budget to item 5: State the target runtime (<8 minutes, matching Sara's pyramid model) in the acceptance criteria. Otherwise "runs in CI" is satisfied even if the job takes 45 minutes.
## 📋 Elicit — Requirements Engineer ### Observations - **Issue quality is high for a deferred-items tracker**: Each item has a clear action, a named location in the codebase, and testable acceptance criteria. The issue is definition-of-ready compliant for most items. - **Item 3 (person accumulation) has two options but the acceptance criterion doesn't specify which**: "Person cleanup strategy decided and implemented (option a or b above)" is the criterion — but the criterion is satisfied by either option, making it untestable until the decision is made. This is a hidden dependency on a design decision, not a directly implementable task. - **Item 5 (CI pipeline) has implicit NFRs that aren't stated**: "E2E suite runs in CI on every push to main" implies: (a) the runner must support Docker Compose, (b) a Gitea secret must exist for E2E credentials, (c) a CI overlay Compose file must exist. None of these preconditions appear in the acceptance criteria. - **Item 7 (mobile viewport) is scoped loosely**: "3–5 read-path tests" is a range, not a specification. Which 3? Which 5? Without naming them, done-ness is subjective. - **Item 6 (negative registration) doesn't define "expired" semantically**: How long does an invite live before it expires? Is expiry time-based or use-based? The test needs a way to create an expired or used invite — this requires either backend API support for setting expiry or a test that uses an already-consumed code from a prior test step. ### Recommendations - **Split item 3 into two sub-tasks**: (3a) Decision: admin chooses option a or b. (3b) Implementation: implement the chosen option. Add a separate acceptance criterion for each. The current single criterion blocks testability. - **Expand item 5's preconditions into explicit sub-tasks**: (5a) Create `docker-compose.ci.yml` overlay, (5b) Add `E2E_ADMIN_PASSWORD` Gitea secret, (5c) Verify NAS runner supports Docker Compose v2, (5d) Add Playwright CI job to `ci.yml`. The current single acceptance criterion ("E2E suite runs in CI on every push to main") hides four independent prerequisites. - **Name the specific 5 read-path tests for item 7**: Home/search list, document detail, person detail, Chronik/activity feed, Briefwechsel conversation timeline. These match the "critical read path" described in issue #402. Replace "3–5 tests" with a named list in the acceptance criteria. - **Clarify invite expiry behavior before writing negative tests for item 6**: Check the backend (`InviteController` or equivalent) for how expiry is implemented, then write acceptance criteria that can be tested deterministically. If expiry requires time manipulation, consider using an invite code from a prior test run (mark-as-used) for the replay test, and skip the expiry test if the backend doesn't expose a way to create pre-expired codes. - **Add a CI time budget to item 5**: State the target runtime (<8 minutes, matching Sara's pyramid model) in the acceptance criteria. Otherwise "runs in CI" is satisfied even if the job takes 45 minutes.
Author
Owner

🎨 Leonie Voss — UI/UX & Accessibility

Observations

  • Mobile viewport item (7) references issue #402's critical read path: The project memory confirms transcribers (60+) use laptop/tablet and readers (younger) use phones. The 320px mobile target for read-path tests directly serves the phone-native reader audience. This is the right priority.
  • Bell test gap (J10) has accessibility implications: The notification badge (unread count) must be announced by screen readers as a live region. If aria-live="polite" is missing from the badge, the count update is invisible to assistive technology. The bell test is a good place to verify this — but a Playwright/axe assertion is needed, not just a DOM check.
  • Deep-link spec already runs axe on the document detail page: The existing notification-deep-link.spec.ts includes an AxeBuilder check. The new bell test assertions should include an axe scan of the bell dropdown open state — dropdown menus are a common accessibility failure point (focus trapping, role=menu vs role=dialog).
  • Mobile read-path tests at 320px: Documents detail and person detail pages are the most content-dense read-path pages. At 320px, long document titles, person name headers, and metadata rows are the highest overflow/truncation risk. These are also the pages used by the senior audience (60+) on mobile.
  • Touch target check at 320px: The issue doesn't mention touch target verification at mobile viewports. The notification bell button, document list item links, and back navigation all need to be ≥44px tall at 320px. An axe scan at mobile viewport will catch some of these, but explicit height assertions on the bell button and key nav elements would be stronger.

Recommendations

  • Include AxeBuilder scan in the J10 bell dropdown test: After opening the dropdown, run await new AxeBuilder({ page }).analyze() and assert violations.toHaveLength(0). This catches role, aria-label, and focus-management violations in the dropdown in one assertion.
  • Check aria-live on the unread badge: After the mark-as-read step in the bell test, assert page.locator('[aria-live]').filter(...) includes the badge region, or check that the badge element has aria-live="polite". If it doesn't, that's a separate accessibility bug to file.
  • Mobile 320px test for document detail: Use page.setViewportSize({ width: 320, height: 700 }), navigate to a document detail page, and run checkA11y(page) in addition to visual assertions. A screenshot at this size is low effort and will catch overflow issues.
  • Add a touch-target assertion for the bell button: In the bell test, check const box = await bell.first().boundingBox(); expect(box.height).toBeGreaterThanOrEqual(44) — this is the WCAG 2.2 minimum. Do the same for primary navigation links in the mobile header.
  • Sequence the mobile tests last: Mobile tests can make some desktop-optimized selectors harder to hit. Running them last in each describe keeps the main test path clean and avoids confusion if a mobile layout hides a desktop-only element.
## 🎨 Leonie Voss — UI/UX & Accessibility ### Observations - **Mobile viewport item (7) references issue #402's critical read path**: The project memory confirms transcribers (60+) use laptop/tablet and readers (younger) use phones. The 320px mobile target for read-path tests directly serves the phone-native reader audience. This is the right priority. - **Bell test gap (J10) has accessibility implications**: The notification badge (unread count) must be announced by screen readers as a live region. If `aria-live="polite"` is missing from the badge, the count update is invisible to assistive technology. The bell test is a good place to verify this — but a Playwright/axe assertion is needed, not just a DOM check. - **Deep-link spec already runs axe on the document detail page**: The existing `notification-deep-link.spec.ts` includes an `AxeBuilder` check. The new bell test assertions should include an axe scan of the bell dropdown open state — dropdown menus are a common accessibility failure point (focus trapping, role=menu vs role=dialog). - **Mobile read-path tests at 320px**: Documents detail and person detail pages are the most content-dense read-path pages. At 320px, long document titles, person name headers, and metadata rows are the highest overflow/truncation risk. These are also the pages used by the senior audience (60+) on mobile. - **Touch target check at 320px**: The issue doesn't mention touch target verification at mobile viewports. The notification bell button, document list item links, and back navigation all need to be ≥44px tall at 320px. An axe scan at mobile viewport will catch some of these, but explicit height assertions on the bell button and key nav elements would be stronger. ### Recommendations - **Include `AxeBuilder` scan in the J10 bell dropdown test**: After opening the dropdown, run `await new AxeBuilder({ page }).analyze()` and assert `violations.toHaveLength(0)`. This catches `role`, `aria-label`, and focus-management violations in the dropdown in one assertion. - **Check `aria-live` on the unread badge**: After the mark-as-read step in the bell test, assert `page.locator('[aria-live]').filter(...)` includes the badge region, or check that the badge element has `aria-live="polite"`. If it doesn't, that's a separate accessibility bug to file. - **Mobile 320px test for document detail**: Use `page.setViewportSize({ width: 320, height: 700 })`, navigate to a document detail page, and run `checkA11y(page)` in addition to visual assertions. A screenshot at this size is low effort and will catch overflow issues. - **Add a touch-target assertion for the bell button**: In the bell test, check `const box = await bell.first().boundingBox(); expect(box.height).toBeGreaterThanOrEqual(44)` — this is the WCAG 2.2 minimum. Do the same for primary navigation links in the mobile header. - **Sequence the mobile tests last**: Mobile tests can make some desktop-optimized selectors harder to hit. Running them last in each describe keeps the main test path clean and avoids confusion if a mobile layout hides a desktop-only element.
Author
Owner

🗳️ Decision Queue — Action Required

1 decision needs your input before implementation starts.

Architecture / Security

  • Permission scope for DELETE /api/persons/{id} — should the delete endpoint require WRITE_ALL (same as all other mutations, simpler, consistent) or ADMIN (higher bar, reflects irreversibility of deleting a historical person that cascades to document–sender/receiver links)? Nora also recommends considering a soft-delete (deletedAt flag) rather than hard delete, so document links survive as traceable orphans rather than silent nulls. Three options:

    • (a) WRITE_ALL + hard delete — simple, consistent, dangerous for archival data
    • (b) ADMIN + hard delete — higher permission bar, consistent with "destructive = admin-only" semantics
    • (c) ADMIN + soft delete — safest for an archival system, adds a deleted_at column and Flyway migration

    (Raised by: Markus, Nora)

## 🗳️ Decision Queue — Action Required _1 decision needs your input before implementation starts._ ### Architecture / Security - **Permission scope for `DELETE /api/persons/{id}`** — should the delete endpoint require `WRITE_ALL` (same as all other mutations, simpler, consistent) or `ADMIN` (higher bar, reflects irreversibility of deleting a historical person that cascades to document–sender/receiver links)? Nora also recommends considering a soft-delete (`deletedAt` flag) rather than hard delete, so document links survive as traceable orphans rather than silent nulls. Three options: - (a) `WRITE_ALL` + hard delete — simple, consistent, dangerous for archival data - (b) `ADMIN` + hard delete — higher permission bar, consistent with "destructive = admin-only" semantics - (c) `ADMIN` + soft delete — safest for an archival system, adds a `deleted_at` column and Flyway migration _(Raised by: Markus, Nora)_
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#431