Files
civitai__civitai/tests/preview-image-feed.spec.ts
Zachary Lowden ed0b0b04e3 docs(retool): hold the moderator id table privately, genericise internal refs (#4476)
This repository is public. raw/README.md already documented a sanitisation rule for
the Retool exports, but that pass matched on the SHAPE of a value, so content that
looks like ordinary prose went straight through.

Staff identity. moderator-id-mapping.md paired moderation team members' real names
with their Civitai user ids and usernames, and user-lookup-v2.json gated features on
current_user.fullName === a real name, so the authorization model itself was written
in names. The table now lives in the private infra repo; the public doc keeps the
coverage figures and the backfill method and points there. Export literals became
__MODERATOR_A__..__MODERATOR_C__, underscore-delimited because bare MODERATOR_A is a
strict prefix of the live MODERATOR_APP_URL env var.

One staff member had all three legs of a pseudonym-to-identity linkage present in
this repo -- pseudonym, userId in the retained moderator id set, and real name in a
planning doc. That one was reconstructible from repo content alone; the display name
is dropped. Other staff mentions complete no linkage and stand.

End-user identifiers. bulk-ban carried four real banned users' IP addresses and five
real account ids. Now RFC5737 addresses and <accountId>.

Internal service names. Comments, .env.examples and test fixtures named real
in-cluster services and two node hostnames. Every namespace token is now clear.
Deliberately unchanged: five executed fallbacks, verify-runner as an application
identifier, stub-oidc as a local e2e test double, and cnpg-database in immutable
Prisma migrations.

Four adversarial audit rounds. The redactions verified clean every round; the prose
describing them needed four public corrections, including one that misattributed
which moderator was exposed and one that misdiagnosed why a survivor survived -- it
was a term-list defect, not the line wrap I claimed, so the guard I added protected
against the wrong mechanism. Corrections are on the PR.

These files were already committed and pushed, so this narrows further exposure
rather than undoing it. Nothing here is a credential. The history decision -- leave
it, treat the content as disclosed -- is recorded on the tracking ticket.

Verified: 168 tests across four suites (run with --project; without it vitest
silently runs three files and still reports success), 11/11 raw exports parse, byte
deltas match the replacements arithmetically.
2026-08-28 19:06:11 -05:00

82 lines
4.2 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { storageStatePath } from './preview-fixtures';
import { trpcQuery } from './preview-trpc';
import { retryFlaky } from './preview-retry';
/**
* Image-feed content smoke: the real /images browse feed actually renders content.
*
* preview-feed.spec.ts covers the models feed (model.getAll, DB-backed) but
* deliberately skipped images. This adds the images half against the ACTUAL feed:
*
* - A broad image.getInfinite (no entity filter) routes server-side through
* getAllImagesIndex — the MEILISEARCH index path (image.controller.ts:300-320),
* not the DB. The index `metrics_images_v1` (~114M docs) lives on the in-cluster
* feeds-meilisearch proxy, and previews reach it via
* METRICS_SEARCH_HOST=http://feeds-proxy.internal.svc.cluster.local
* (same host prod uses; verified both feeds backends populated 2026-06-11). So
* the broad feed IS available to previews and this asserts the real /images
* surface end to end (meili query + DB hydration).
*
* Why NOT a modelId-scoped DB query instead: a bare-modelId image.getInfinite
* forces the DB path (getAllImages), but that scan over the 115M-row dev clone
* takes >60s per model and blew the test timeout — the index path is both the real
* feed AND the fast/reliable one (~3s). (An earlier 1-core preview pod
* intermittently returned 0 from the broad feed — suspected server-side abort under
* CPU throttle; on the 2-core pod, post the CPU bump, it returns content in ~3s.)
*
* Runs as `tester` (free member that PASSES the preview gate). image.getInfinite is
* a heavyProcedure, reachable for a gate-passing user. page.request carries the
* storageState auth cookie; preview-trpc stamps Origin/Referer for the CSRF gate —
* same authed-tRPC pattern as the sibling preview-feed / preview-engagement specs.
*
* Verified tRPC shape: image.getInfinite (server/routers/image.router.ts:126),
* input getInfiniteImagesSchema; `{ limit }` valid (all fields defaulted), returns
* { items: [{ id }], nextCursor }.
*
* Tolerant on COUNT, strict on STRUCTURE: assert ">= 1 image with a numeric id" —
* the structural intent is "the /images feed produced a real, non-empty page", not
* any specific image. An empty feed is the regression this guards. NOTE: this path
* depends on the shared feeds-meilisearch being healthy; a feeds incident could
* make it (report-only) flake — that's the accepted cost of covering the real feed
* rather than a synthetic DB query.
*
* Only runs under playwright.preview.config.ts (needs PREVIEW_URL + minted states).
*/
const ROLE = 'tester' as const;
const FEED_LIMIT = 5;
test.describe('images browse feed renders real content (tester)', () => {
test.use({ storageState: storageStatePath(ROLE) });
test('image.getInfinite (the /images feed via meili) returns a non-empty page', async ({
page,
}) => {
// Warm the request context against the preview origin (auth cookie + a real
// navigated origin). domcontentloaded only: NEVER networkidle — the app's
// background traffic never idles, so a networkidle nav hangs to timeout.
await page.goto('/', { waitUntil: 'domcontentloaded' });
// image.getInfinite is meili-backed via the shared in-cluster feeds-proxy, which
// intermittently returns HTTP 408 ("Image search is temporarily overloaded —
// please retry") under concurrent preview-build load. The app's own error tells
// us to retry; Playwright's whole-test retries fire within seconds (all <5s) so
// they don't outlast the overload. retryFlaky spaces the retries with backoff to
// ride it out — honest: a sustained overload still fails after the attempts.
const data = await retryFlaky('image.getInfinite feed', () =>
trpcQuery<{ items: Array<{ id: number }> }>(page.request, 'image.getInfinite', {
limit: FEED_LIMIT,
})
);
const items = data?.items ?? [];
expect(Array.isArray(items), 'image.getInfinite returns an items array').toBe(true);
expect(
items.length,
'the /images feed (meili-backed) should render >= 1 image'
).toBeGreaterThan(0);
expect(typeof items[0]?.id, 'a feed image should carry a numeric id').toBe('number');
});
});