|
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 7m19s
CI / Build container images (pull_request) Successful in 1m23s
CI / Auth e2e pack (pull_request) Successful in 8m55s
CI / Import/export fidelity gate (pull_request) Successful in 57s
CD / Build and push images (push) Successful in 14s
CD / Deploy to Test (push) Successful in 16s
CD / Smoke tests against Test (push) Successful in 1m28s
CD / Promote to Int (push) Successful in 13s
CI / Lint, typecheck, test (push) Successful in 6m52s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 9m6s
CI / Import/export fidelity gate (push) Successful in 58s
Deleting a pond already had a strict prompt — typing the pond name, stricter than a confirm dialog. That was never the gap. The gap is that the person who deletes it loses access the moment they do: the pond leaves their view, only a Site Admin can bring it back, and the export is no longer reachable for them. So the archive is offered INSIDE the deletion flow, before the button. What it contains, and why it is not the existing export: - Every page the requester may read, as Markdown, as before. - **Every attachment of the pond**, not only the embedded ones. An attachment nobody put on a page would otherwise vanish unnoticed — which is the whole reason this issue exists. - `manifest.json`: pond settings (EFFECTIVE, defaults filled in — a preservation format must not require its reader to know Dorfteich's defaults), labels, the page hierarchy and sort keys, comments, and attachment metadata including the #199 hash so a reader can verify bytes. It extends the #210 manifest rather than adding a second descriptor, and carries an explicit `formatVersion`. - `README.txt`, because the manifest is for machines: whoever unpacks a folder of Markdown a year from now must not believe they hold a one-click restore. Decisions worth naming: - **"Complete" describes the RESULT, not the route.** A pond admin who may read every page gets `complete: true`; only an archive that actually leaves pages out is incomplete. The Site-Admin route skips the read filter (an archive taken before an irreversible purge must not depend on which ponds the operator happens to be a member of) — those are two different questions and the first version of this conflated them. - **The omission is named before the download**, with its number, in the UI and in the manifest. An archive silently missing content is worse than no archive, because it ends the search. - **Not downloading stays allowed.** A pond of test pages should not require one, and the server cannot tell whether a file arrived anyway — so the finality is stated in text instead of enforced. - **A plain link, not fetch-into-a-blob.** The api streams the ZIP; buffering a whole pond in the tab to draw a progress bar would trade memory for cosmetics. The browser reports progress and completion; what it cannot say — that the archive is being BUILT — is announced in a live region. - Read trail unchanged in kind (ADR 0023): one `export` event per classified page before any classified byte enters the stream. Attachments never travel without their page, so the same events cover them. - New audit action `pond.archived` (catalogue v1.7) with page and attachment counts, omitted pages, and completeness. Format documented in `docs/architecture/pond-archive-format.md`, including what is deliberately NOT in it (history, permissions, trash). Verified by hand, not only asserted: a real pond's archive downloaded and unpacked — README, manifest, three page files, the media file; the manifest's effective settings, per-page classification, the VS-NfD frontmatter and marking preserved in the classified page's Markdown, and the attachment's sha256 present. Plus six api tests (including that an unembedded attachment travels and that a Site Admin gets a complete archive without membership) and the a11y pack 11/11 in both schemes, which now also scans the pond settings screen. Not done, because there is nothing to attach it to: the Site Admin's purge dialog (#193) exists only as an api endpoint — there is no pond-trash UI in the web app. The api half is here and tested, so it becomes a link when that screen is built. |
||
|---|---|---|
| .. | ||
| a11y.spec.ts | ||
| access-rules.spec.ts | ||
| admin-quotas.spec.ts | ||
| admin-users.spec.ts | ||
| attachments.spec.ts | ||
| auth.spec.ts | ||
| backlinks.spec.ts | ||
| collab-permissions.spec.ts | ||
| collab.spec.ts | ||
| comments.spec.ts | ||
| content.spec.ts | ||
| create-missing-page.spec.ts | ||
| editor.spec.ts | ||
| export.spec.ts | ||
| favorites.spec.ts | ||
| fonts.spec.ts | ||
| graph.spec.ts | ||
| helpers.ts | ||
| image.spec.ts | ||
| import-vault.spec.ts | ||
| import.spec.ts | ||
| labels.spec.ts | ||
| legal.spec.ts | ||
| link.spec.ts | ||
| markdown.spec.ts | ||
| members.spec.ts | ||
| mermaid.spec.ts | ||
| offline.spec.ts | ||
| page-tools.spec.ts | ||
| page-tree.spec.ts | ||
| permission-matrix.spec.ts | ||
| plugin-admin.spec.ts | ||
| plugin-blocks.spec.ts | ||
| plugin-fixtures.ts | ||
| plugins.spec.ts | ||
| pond-theme.spec.ts | ||
| public.spec.ts | ||
| README.md | ||
| reorder.spec.ts | ||
| search.spec.ts | ||
| section-styles.spec.ts | ||
| settings-nav.spec.ts | ||
| setup.spec.ts | ||
| sidebar.spec.ts | ||
| smoke.spec.ts | ||
| social.spec.ts | ||
| system.spec.ts | ||
| tasks.spec.ts | ||
| theme-accent.spec.ts | ||
| theme.spec.ts | ||
| trash.spec.ts | ||
| vs-nfd-marking.spec.ts | ||
| wikilink.spec.ts | ||
End-to-end tests
Playwright suites, most local-only — three run in CI/CD:
| Suite | Target | Where it runs |
|---|---|---|
smoke.spec.ts |
any deployed stage | CD pipeline against https://test.dorfteich.cloud after every deploy |
auth.spec.ts |
full local stack with Mailpit | CI job auth-e2e on every PR/push; locally against the dev stack |
content.spec.ts |
full local stack | same CI job auth-e2e (a second step), right after the auth pack |
| everything else | full local stack | locally only — editor/sidebar/image/link/markdown/trash.spec.ts |
content.spec.ts is the M2 content regression pack (issue #32): page
lifecycle, editor basics, image paste, Markdown round-trip, and trash —
enough to catch a regression across the whole content model without
re-running every edge case the feature-specific packs above already cover.
Its Markdown round-trip test is a real regression pin, not just a smoke
check: it compares the seeded "Every Element" fixture page's exported
Markdown byte-for-byte against the checked-in content-page.md (see
"Content fixtures" below) — any schema/serializer change that alters how a
node round-trips fails it, once the seed has re-run against the changed
code (build → migrate → seed → test, exactly CI's order).
Running locally
# 1. Stack: database + Mailpit, api (3001), web dev server (5173)
docker compose -f deploy/compose/docker-compose.yml -f deploy/compose/compose.dev.yml up -d db mailpit
DATABASE_URL=postgresql://dorfteich:dorfteich@localhost:5434/dorfteich pnpm --filter @dorfteich/api db:seed
DATABASE_URL=postgresql://dorfteich:dorfteich@localhost:5434/dorfteich PORT=3001 pnpm --filter @dorfteich/api start:dev &
pnpm --filter @dorfteich/web dev &
# 2. Tests
E2E_BASE_URL=http://localhost:5173 E2E_MAILPIT_URL=http://localhost:8025 pnpm --filter @dorfteich/web e2e
auth.spec.ts skips itself when E2E_MAILPIT_URL is unset, so the CD
smoke run never trips over it.
vs-nfd-marking.spec.ts (issues #244/#245) has three parts selected by
E2E_VS_NFD_MODE (marked, hidden, unset = off); each needs an api
started with the matching VS_NFD_MODE. CI runs the marked half against
the main e2e stack and the hidden half against a second api (port 3006)
on the same database; the off assertions run in local default stacks.
Fixture matrix
Seeded by pnpm --filter @dorfteich/api db:seed (idempotent — re-running
never duplicates). Shared password: fixture passwort 123. Fixtures exist
only on dev machines and disposable CI/Test databases.
| Username | State | Purpose |
|---|---|---|
fixture-admin |
active, Site Admin | admin UI/permissions cases |
fixture-user |
active | regular journeys, settings, sessions |
fixture-editor |
active | second regular account for the collab-permissions pack (reader/editor of another's pond) |
fixture-viewer |
active | signed-in non-member for authenticated/public access-rule cases (issue #55) |
fixture-outsider |
active | the "foreign user" of the permission matrix — member of nothing (issue #60) |
fixture-pending |
e-mail not verified | unverified-login cases |
Permission matrix (permission-matrix.spec.ts, issue #60)
The cross-feature permission hardening pack pins the security-relevant subject × surface combinations against regressions. It is API-level (the UI adds nothing over the resolved status code) and enforces the 404-vs-403 policy: an unauthorized read is 404 (existence hidden), an unauthorized write on something readable is 403.
- Subjects: site admin (
fixture-admin), pond admin / owner (fixture-user), editor (fixture-editor), the same editor label-restricted by asecret-label deny, reader (fixture-viewer), public (anonymous), and the foreign user (fixture-outsider, a member of nothing). - Surfaces: page read, edit (collab-token
rw/ro), sidebar list, search, versions (history = write), media, and the public HTML endpoint. - Extending it: a new permission-touching feature adds a surface here (one
expectrow per subject) rather than a bespoke test, so the matrix stays the one place the policy is pinned. A weakened guard is caught here — verified by temporarily loosening a route decorator and watching the pack go red.
Plugin sandbox (plugins.spec.ts, issue #73)
The sandbox security pack drives the Site-Admin plugin preview page
(/admin/plugins/:id/preview) — the exact sandbox runtime pages embed — with
three fixture plugins built in plugin-fixtures.ts and installed through the
admin API:
- well-behaved: answers
renderand resizes its own frame via the declareduicapability; - malicious (the permanent security regression asset, ADR 0008): probes the
parent DOM, cookies,
localStorage, same-origin and externalfetch, and an undeclared capability — every probe must reportblocked. ALEAKEDverdict is a sandbox escape and fails the build; - hung: never answers, so the host's 5 s deadline must collapse it to the failure placeholder while the surrounding page stays responsive.
Runs in the auth-e2e CI job (needs the api's writable PLUGINS_DIR, satisfied
by the default ./data/plugins).
Attachments (attachments.spec.ts, issue #61)
Non-image attachments: a page's attachments section uploads an allowlisted
file, lists it, and inserts it into the document as a download link (verified
to serve with Content-Disposition: attachment + nosniff, never inline); a
disallowed extension is rejected with the localized allowlist error; the Pond
Admin file manager reports storage usage and flags an orphan (a pond-level
upload with no embedding page). The SVG sanitize/reject policy is covered at
the api level in files.e2e.db.test.ts.
Import (import.spec.ts, issue #64)
The sidebar "import document" action: pick a file, upload, watch progress, open
the new page. .md imports directly (the response is already succeeded);
.docx/.odt poll a conversion job. Cases: a .docx corpus fixture (#63)
opens the converted page, a .md opens directly, an unsupported .txt shows
the localized error and creates no page, and two concurrent .md imports both
complete. The .docx case self-skips unless E2E_PANDOC is set — CI's e2e
stack has no reachable pandoc sidecar (jobs are container-networked; same reason
the api's real-pandoc fixtures test skips in CI, #63), so it runs locally / on a
stage. Run it locally with a sidecar reachable at the api's PANDOC_URL:
docker run -d -p 3030:3030 pandoc/core:3.6 server # api PANDOC_URL → this
E2E_PANDOC=1 E2E_BASE_URL=http://localhost:5990 \
pnpm --filter @dorfteich/web exec playwright test e2e/import.spec.ts
Export (export.spec.ts, issue #65)
The pond-settings "Download pond as ZIP" link (a Markdown ZIP of the readable
pages) and the page-menu office export. The ZIP download needs no sidecar; the
.docx export runs a conversion job and, like the import .docx case,
self-skips unless E2E_PANDOC is set. The permission-filtered ZIP contents,
the docx pandoc output, and the 500-page streaming path are covered at the api
level in export.service.db.test.ts (+ export-markdown.test.ts).
Content fixtures
db:seed also creates a shared pond content-fixtures (owned by
fixture-user) with two pages, for the content regression pack and manual
QA:
- Every Element (
every-element) — every editor schema node and mark (issue #24: headings 1–4, all list types, table, blockquote, code block, horizontal rule, hard break, and all five marks). Loaded from the checked-inapps/api/prisma/fixtures/content-page.yjs, a Yjs snapshot generated from the human-readablecontent-page.mdnext to it —content-page.mdis the thing to read or edit; the.yjsfile is a build artifact of it, not source. - Fixture Image (
fixture-image) — one real, servable uploaded image (the placeholderfileIdinside the Markdown fixture above is not a real attachment; this page's image is). - Classified Note (
classified-note) — a VS-NfD-marked page (issue #206, ADR 0022) so the a11y pack can assert the classification banner (top and bottom, both themes).
Print marking check (print.css, issue #207)
The VS-NfD marking must appear at the top AND bottom of every printed
sheet (ADR 0022). Mechanism: the page content sits in a real
<table class="print-frame"> whose thead/tfoot carry the banners —
the one construct Chromium and Gecko both repeat per page (@page margin
boxes are unimplemented; position: fixed places unreliably). The check,
repeatable against a running local stack (seeded, signed in as
fixture-user):
- Classify a multi-page document:
UPDATE pages SET classification='VS_NFD' WHERE slug='every-element'; - Chromium: print
/p/content-fixtures/every-elementto PDF (browser print dialog, or Playwrightpage.pdf({ preferCSSPageSize: true })after logging in). - Gecko (Firefox): open the same page (or grant it a public reader
and use
/public/content-fixtures/every-element), Cmd+P → save as PDF. Headless equivalent: geckodriver + WebDriverPOST /session/<id>/print. - Every page of both PDFs must show
VS – NUR FÜR DEN DIENSTGEBRAUCHonce at the top and once at the bottom, with no navigation chrome and no overlap with content (quick text check:pypdf→page.extract_text().count(...) == 2per page).
Last verified 2026-07-31: Chromium 140 (Playwright) and Firefox 153, 2 markings on every page of a 2-page PDF each.
Regenerating after editing content-page.md:
pnpm --filter @dorfteich/api fixtures:regenerate
This is deterministic — re-running without editing the Markdown produces a
byte-identical .yjs file (the script pins the Yjs document's clientID,
which is otherwise randomized per Y.Doc instance) — and it refuses to
write a snapshot that isn't a fixed point of the Markdown round-trip
(docToMarkdown(markdownToDoc(x)) === x), so a stale fixture can't get
checked in silently.
Conventions
- New feature packs get their own
<feature>.spec.tsnext to these and extend the fixture matrix here (permission matrix arrives with M5, issue #60). - Use
contextForUser()fromhelpers.tsfor signed-in tests — it logs in through the api and hands you a browser context with the session cookie, no UI login repetition. - Flaky tests are defects (ADR 0014): fix or quarantine immediately.