Files
RenewCMS/features/image_variants_cache.qa.md

14 KiB

QA procedure: image_variants_cache

Two parts: Go for the API and tracking, Vitest for the Cache screen. No live server, no real SQLite file, no real SMTP, no time.Sleep (use an injected clock and manual flushes).

Go (package test, black-box)

New files: test/image_variants_cache_http_test.go, test/image_variants_cache_tracking_test.go, test/image_variants_cache_purge_test.go, test/image_variants_cache_audit_test.go. Extend test/image_variants_support_test.go. Fake type names must stay unique in the package.

Setup shared by every case

  1. validator.InitValidator() once. jwtauth.New("HS256", []byte("test-secret"), nil) stands in for JWT_SECRET.
  2. Reuse mockVariantRepo, mockVariantFileRepo, mockVariantStorage, countingRenderer, mockImageRepo, the user fake, the API-key fake and the audit fakes of test/activity_support_test.go. Add to mockVariantFileRepo: last_requested_at per row, the aggregated totals, unused totals (cutoff, enabled variants only), top-images query (limit 10, bytes desc, id asc), batched listing for purge (batch size recorded), the batch update of requested times, with a call counter per method. Add fakes for the usage repository (mockVariantUsageRepo: day rows, batch writes counted, switchable failure) and a controllable clock.
  3. Users: owner 1, admin 2, verified plain user 3, unverified user 4. Real signed jwt cookies with user_id and token_version; a stale cookie; a valid API key.
  4. Router: a fresh chi.NewRouter() mirroring NewImageVariantRouter in api/router.go (jwtauth.Verifier, AuditContext, RequireSignedIn, Authenticator, RequireCurrentToken, RequireVerifiedUser, RequireOwnerOrAdmin) with Get("/cache") and Post("/cache/purge") beside the existing variant routes, mounted at /v1/image-variants. Mount the real public handler staticimages.NewHandler(...) at /static/uploadedImages over the same storage, variant use cases and the real tracker.
  5. Seed the Default fixture of the feature (six variants, the per-variant bytes, 312 unused files with the 29/31/30 day boundary rows, the ten top images, 37 rejections over the window). Images are generated in the test with image/png, image/jpeg, image/draw.

A. Guards

  1. Table of the feature: each method, path and credential gives the status and exact body constant; assert no file, storage, usage or audit fake was touched. API key gets 401. Owner and admin get 200.

B. Statistics

  1. Default fixture: decode the body and assert every field and value, by_variant order and content, top_images order, url. Sum invariants. Zero-file variant listed; disabled variant in totals but not in unused; orphan index rows counted but absent from top_images; tie at the tenth place; empty cache raw body "top_images":[]; no variant raw "by_variant":[].
  2. unused boundaries: 29 days (in use), exactly the cutoff (in use), 31 days (unused), created_at fallback, disabled variant excluded.
  3. Aggregation: the file fake saw one totals call, one unused call and one top-images call, regardless of the number of images. A pending-tracking fixture asserts one batch write before the read, and that the 3 pending rejections are in rejected_requests. A failing totals call gives 500 Internal server error. GET records no audit event and opens no blob.

C. Tracking

  1. Hits: for each row of the "what counts" table drive the real public handler and assert the pending key set and zero usage writes. 1000 hits on one file: zero writes, then one flush gives one update with the last time. 40 files: one batch of 40. Nothing pending: repository not called. 500 distinct keys trigger a flush without the tick (use the injected trigger, not a timer). Failed batch then recovery: nothing lost, nothing counted twice, latest time wins. A failing repository never fails or slows the public response. The stored time never moves back. A late update for a purged file creates no row.
  2. Rejections: for each row of the table drive the public handler and assert the counter and the status. 1000 rejections: zero writes, one batch adds 1000. Window: clock at 2026-10-08 23:59 UTC, rows of 2026-10-02 counted, 2026-10-01 not. Day boundary at 23:59:59 and 00:00:00 UTC gives two rows. The flush prunes rows older than 7 days. Failed flush keeps the pending counts.
  3. Race (run with -race): 50 goroutines record hits and rejections while another flushes; totals written equal totals recorded. Shutdown: closing the container flushes once.

D. Purge

  1. One case per scope with the numbers of the feature (all 1284 and 222298112, unused 312 and 42991616, variant hero 244 and 88080384, image 1 6 and 6082765): assert the body, each deleted blob name, each deleted row, and that originals and definitions are unchanged. Disabled variant: scope all and scope variant include it; scope unused excludes it. A following public GET renders again.
  2. Empty cache returns zeros. A blob missing from storage still removes its row and counts as purged.
  3. Refusals table: unknown scope, case change, empty body, missing or empty variant, missing, zero and negative image_id, unknown variant (404 Variant not found), unknown image (404 Image not found), invalid JSON (400 apperr.BodyErrorMessage). Assert no storage or row change and no audit event. Extra target fields for all and unused are ignored.
  4. Partial failure: storage fails on 644 rows; 200 with purged_files 640, failed_files 644, failing rows kept; retry after recovery purges the rest. Listing failure gives 500. Batch size at most 500 over 5000 rows (assert the largest batch the fake returned).

E. Audit

  1. One case per scope row: action, target (none for all and unused, image_variant for variant, image for image), outcome, params including older_than_days 30 for unused. Partial failure is a success carrying failed_files. All-failed is a failed event with the generic reason. The no-op rows record nothing. A failing audit repository leaves the response and files unchanged. GET /v1/activity?category=media lists the four events. Extend the failureReasons mapping test for the two new actions.

F. Mutation guard

  1. task mutate scope is ./internal/infrastructure/useCases, scoped with MUTATE_FILES to the new use case files and MUTATE_RUN to the new test names. Each new branch needs a killing assertion: the unused cutoff comparison (below, at and above), the enabled filter of unused, the purged_files > 0 guard, the success versus failed outcome (purged > 0 versus failed > 0), the top-10 limit and its id tie-break, the window start (7 days) and the never-moving-back time guard. Bare-bool conditions produce no mutant, so cover both branches anyway.

G. Layering and hygiene

  1. Guard test (go list -deps or grep): the domain imports only the stdlib and domain packages; the new use cases and the tracker import no persistence, mailer, models, gorm, net/http, chi or api; the handler imports no persistence, mailer, models or gorm. Check migrate.go lists ImageVariantRejectionDay. CGO_ENABLED=0 go build -tags nofrontend ./... succeeds. No comment in new or edited files. docs/api.md lists the two routes, the shapes, the scopes and the errors, and docs/architecture.md or the variants doc describes the tracker and its 30 second flush.

Automated (Vitest + Vue Test Utils, task test:web)

Mock useApi's request per case (GET /v1/image-variants/cache, POST /v1/image-variants/cache/purge); no backend. Mount CachePage inside DashboardShell with a resolved verified session (id 1, username "marta", role "owner") and a test router at /settings/media/cache. Mock matchMedia for the 640 px breakpoint; run layout cases at desktop and below-640. Use fake timers for the 150 ms skeleton delay. No test freezes the system date (this screen shows no date).

Default fixture = the Background of the feature, with the six variants, the ten top images and the numbers above.

  1. Access, Examples-driven: no role, role "owner", role "admin", 403 on the list. Forbidden copy and "Back to Home"; no request when the role is missing.
  2. Header and tabs: title, description, the two buttons with icons, the two tabs; "Variants" routes to /settings/media/variants; the Variants page's "Cache" tab now routes here (update the existing VariantsPage spec that asserted aria-disabled); breadcrumb and mobile title.
  3. Tiles: the four rows of the table; two columns on mobile, four on desktop.
  4. By variant: order, stats text, the Disabled badge, bar width percentages (100, 69, 25, 25, 23, 11), the colour classes for enabled and disabled, 1 file singular, 0 width when the largest is 0.
  5. Top 10: ten rows, numbering, thumbnails, 6 variants, 1 variant, sizes; mobile 60 px rows with the size after the count and an icon-only 44 px Purge with aria-label and tooltip.
  6. Loading: nothing at 149 ms; at 150 ms aria-busy, label "Loading cache", skeleton counts (5 and 6; Top 10 hidden on mobile); a response at 100 ms never draws skeletons.
  7. Empty: tile subs "Nothing cached", empty block copy, both purge buttons disabled, no lists. Purge unused disabled when unused is 0.
  8. Error, Examples-driven: 500 and network error, alert copy, no close button, Try again re-sends and renders.
  9. Purge all dialog: copy, focus on the field, confirm disabled; the typed-word table (pur, Purge, purge, purged); Cancel and Escape send nothing and return focus; confirm sends {"scope": "all"} once.
  10. Purge unused dialog: copy with 312 and 41 MB, "Purge 312 files", sends {"scope": "unused"}.
  11. Purging: banner copy "Purging 1,284 files", aria-live="polite", dimmed content, all purge controls disabled, tabs active.
  12. Done: toasts "Purged 1,284 files, freed 212 MB" and "Purged 312 files, freed 41 MB"; one refetch; the empty state after purge all.
  13. Failed: toast "Purge didn't finish", "640 of 1,284 files were removed. Try again.", one refetch, Try again re-sends the same body without the typed word.
  14. Errors, Examples-driven: 400, 403, 500, network, 401 (redirect then toast), each with the copy of the table; banner removed and content restored.
  15. Row purge: sends {"scope": "image", "image_id": 1} with no dialog; button loader; other rows enabled; toast "Purged 6 files (5.8 MB) for north-wall.jpg"; 404 toast and refetch; partial failure "4 of 6 files were removed. Try again.".
  16. Activity sentences (extend activitySentence.spec.ts and ActivityVariants.spec.ts): the seven rows of the sentence table, the partial detail line, and SENTENCE_ACTIONS including the three actions.
  17. French: setLocale('fr'); a catalogue test that every key used on the page and in the new sentences exists in messages.ts; the page renders without missing keys.
  18. Vocabulary and accessibility: no rendered text contains "thumbnail preset", "version" or "rendition"; tabs, labels, focus return.

Visual (task visual:check -- image-variants, desktop and mobile, light and dark)

NOT RUN: the user forbade running task visual:check for this slice, so every item below is reported NOT RUN.

Reference: docs/design/screens/image-variants.md section 5 and the cache frame in docs/design/source/VariantsFrame.dc.html (lines 250-278, with AppFrame.dc.html for the shell). Add each state to web/visual/states.ts with the board's data. The gate: every non-skipped capture within 10 %, both viewports, both themes. None of these states depends on the date or time, so none is skipped.

Board sample data the mocks must reuse: variants thumb 412 files 21 MB, card 318 and 58 MB, hero 244 and 84 MB, square 140 and 19 MB, og 96 and 9 MB, print 74 and 21 MB (disabled); totals 1,284 files and 212 MB; 6 variants, 5 enabled and 1 disabled; 37 rejected requests; unused 312 files and 41 MB; the Top 10 of the frame (north-wall.jpg 6 variants 5.8 MB, lime-plaster.jpg 6 and 4.9 MB, kitchen-after.webp 5 and 4.4 MB, kitchen-before.webp 5 and 3.9 MB, floorboards.jpg 5 and 3.6 MB, IMG_4471.jpg 4 and 3.1 MB, stone-wall-detail.jpg 4 and 2.8 MB, bathroom-tiles.png 4 and 2.6 MB, roof-lantern.jpg 3 and 2.3 MB, chimney-stack.jpg 3 and 2.1 MB); session name "Marta Alves". The mock returns the By variant rows in the board's order (hero, card, then the ties).

  • 5a Loaded
  • 5b Loading
  • 5c Empty cache
  • 5d Purging (the frame draws "640 of 1,284 · 106 MB freed" with a 50 % bar; the draft shows an indeterminate bar and no progress line, so this state is the one at risk against the budget; see open question 1)
  • 5e Purge done (empty cache; the toast)
  • 5f Purge failed (toast "Purge didn't finish")
  • 5g Error loading
  • 5h Purge all: confirm (typed "pur", button disabled)
  • 5i Purge all: confirm, typed (typed "purge")
  • 5j Purge unused: confirm
  • Layout from the frame, not components.md: tiles 4 columns and 16 px gap on desktop, 2 columns and 8 px on mobile; tile padding 16 px (12 px mobile), value 28 px (22 px mobile); two columns 460 px / rest; bars 8 px high; Top 10 rows 40 px (60 px mobile) with the Purge button 26 px high (44 px square icon-only on mobile).
  • Not drawn, no visual state: the error toasts for 400, 403, 500 and 404, the row-purge toast, the row loader, the disabled Purge unused with a non-empty cache; Vitest only.
  • Not covered by screenshots: keyboard and focus order (Tab reaches the buttons, the tabs, each Purge; the dialog traps focus and Escape returns it), hover and active states, motion (dialog, skeleton pulse stopped under reduced motion).

Manual verification

  • Real flush loop: task dev:api over a scratch SQLite file. Request /static/uploadedImages/<storedName>?v=thumb repeatedly, wait 30 s, then open the Cache page and check "Rejected requests" after a few ?v=unknown requests. Check in the file that last_requested_at is set once per file and the day row holds the count.
  • Graceful shutdown: record hits, stop the server with Ctrl+C within 30 s, restart and check the counts survived.
  • Real purge: generate real variants, then purge all, unused (age a row in the file), one image, and check the files disappear from UPLOAD_DIR and reappear on request.
  • S3 driver (STORAGE_DRIVER=s3, MinIO): purge all and by image.
  • Large cache: 20,000 index rows; check the Cache page loads in a reasonable time and the purge keeps memory flat.
  • Other databases: run the stats and purge against MariaDB and PostgreSQL if available (portable SQL).
  • Activity screen: read the four new events against section 8 of the design.
  • French: switch the language and read every state, the dialogs and the toasts.