Files
civitai__civitai/tests
Justin Maier 04b7cfa9d3 chore(bitdex): remove the app-side BitDex code (#4568)
* wip(bitdex): excise the BitDex blocks from image.service.ts + delete dead files

🔴 INCOMPLETE — DOES NOT TYPECHECK. Committed so the analysis and the block
excision survive the session, not because it is ready.

Done: 18 files deleted (both jobs + tests, src/server/bitdex/, the two internal
endpoints, bitdex-feed-serve.metrics + tests, six bitdex-* service tests), and
1,346 lines excised from image.service.ts — imports, the native filter helpers,
postFilterBitdexDocs, isPublicallyPublished, isScheduledForFuture,
BitdexCursoredPageUnavailable, fetchBitdexPrimary, mapBitdexDoc and
getImagesFromBitdexPreFilter. Each cut by brace balance from its own
declaration rather than by hand-typed line numbers.

Verified before cutting: 11 of the 15 filter helpers and both post-filter
predicates had no callers outside the removed blocks.

Not done: ~52 in-place references in image.service.ts, the bitdexMode dispatch
in getImagesFromSearch, image.controller.ts, image-search.service.ts, the client
call sites, the two job registrations, the Flipt enum entries and the telemetry
counters.

claudedocs/bitdex-app-removal-handoff-2026-09-01.md records what the removed
machinery did — the bdx: cursor codec, primary/shadow dispatch, the superset
post-filter and its two predicates, and bitdexCallsObserved — because phase 2
depends on that understanding and it existed nowhere else.

Baseline for whoever continues: typecheck was clean (0 errors, 410s) on this
worktree at origin/main before any of these edits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KYrq2ighNp2rABTAvpzhj9

* chore(bitdex): finish the app-side removal

Completes the WIP commit. BitDex is decommissioned — the engine is gone from the
cluster, both cron jobs are off, and the 8 ops write triggers are dropped from
prod — so the app code that routed to it is dead and comes out.

image.service.ts: the primary/shadow dispatch in getImagesFromSearch, the bdx:
cursor codec, fetchBitdexPrimary and its pagination loop, postFilterBitdexDocs
and its two predicates, mapBitdexDoc, getImagesFromBitdexPreFilter, the native
filter helpers and the BitdexCursoredPageUnavailable class.

image.controller.ts / image-search.service.ts: both getFliptVariant evaluations,
`useBitdex`, the `skipBitdex` carve-out, the bitdexMode pass-throughs and the
now-unreachable getAllImagesIndex arm in the REST path. Routing is unchanged for
every live request: the flag resolved to `off` for every segment, so `useBitdex`
was already false everywhere.

One semantic change, deliberate: `model3dId` resolved to `undefined` on a BitDex
doc (fall back to getByPostId) and `null` on a Meili doc (confirmed-absent). With
only Meili serving, the "not indexed" branch cannot occur and it collapses to
null.

Also: both job registrations, the three FLIPT_FEATURE_FLAGS entries, BITDEX_URL,
the reemit_*/bitdex_audit_* counters, the feed-serve metrics registration, the
bitdex-test skill, the dev-server console filter, and the admin sortAt reconcile
endpoint.

KEPT ON PURPOSE: `'bitdex-image-feed'` in FEEDBACK_AREAS. It is a stored data
label on existing feedback rows, and the zod enum is what a moderator queries
them through — removing it makes historical feedback unqueryable. The prompt that
wrote it is gone, so no new rows carry it.

Verified: typecheck 0 errors in 314s against a 0-error baseline on origin/main in
the same worktree. Lint introduces nothing — the 4 errors in touched files
reproduce identically at origin/main with only line numbers shifted. Covering
suites 81 passed (5 files), exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KYrq2ighNp2rABTAvpzhj9

* fix(bitdex): review round 1 — remove the doc from the public repo, delete dead plumbing

Findings from the five-lane review. Ranked as the lanes ranked them.

🔴 SECURITY. Removes claudedocs/bitdex-app-removal-handoff-2026-09-01.md, which I
added in the previous commit. This repo is public and permanent, and that file
named an unrotated production credential and where to find it, an internal
secret's auth posture and namespace, host filesystem paths, node identifiers, a
public DNS record, and two private repository names. Four categories from
CLAUDE.md's "do not commit these" list. Flagged independently by the intent and
safety lanes. Moved to _local/docs/plans/. Removal is not remediation — the
branch was pushed, so the credential it named must be treated as disclosed and
rotated on that basis; raised separately.

Dead plumbing the removal left behind, found by intent, perf and safety
independently:

- ImagesInfinite kept the FeedbackPrompt import, the showFeedbackPrompt prop and
  the feedSnapshot destructure after its only mount was deleted, and five call
  sites still passed a prop that did nothing.
- getFeedSources / resolveFeedSource / buildFeedSnapshot and FEED_SOURCE_NONE had
  no consumer left once that prompt went; buildFeedSnapshot still ran on every
  new data identity inside useQueryImages, the hook behind every image feed.
  Removed with their test rather than left passing against renamed labels.
- Dead imports in image.service.ts (getFliptVariant, buildFliptContext,
  withDetachedSpan), image.controller.ts and image-search.service.ts
  (FLIPT_FEATURE_FLAGS, getFliptVariant, buildFliptContext, getAllImagesIndex),
  plus the unused PostFilterStats type and useRef in image.utils.ts.
- The resolvedHubSources memo was inert: the two builders are mutually exclusive
  per request and each calls it once, so input.resolvedHub was written and never
  read. Memo and its ImageSearchInput field removed, helper kept.

Comments that had become false rather than merely stale:

- image.service.ts said Meili docs carry model3dId but "raw-SQL rows do NOT (the
  field is left undefined there -> the chip falls back to the postId lookup)".
  The raw-SQL path does carry it, selected and visibility-resolved to
  number | null. My earlier edit substituted "raw-SQL rows" into a sentence that
  was only ever true of BitDex docs.
- Added the note the safety lane asked for: the model3dId null arm is correct
  only while one backend serves that path.
- user-hub.service.ts claimed three filter builders apply the hub cap; there are
  two, and my earlier edit had left the sentence garbled.
- metric-helpers.ts pointed at an internal stats endpoint deleted in this PR.
- packages/civitai-flipt/README.md still used bitdex-image-search as its worked
  example; the identical example in its own env.ts was already fixed.

Verified: typecheck 0 errors in 276s. Covering suites 75 passed (5 files),
exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KYrq2ighNp2rABTAvpzhj9

* fix(bitdex): review round 2 — restore the routing-flag test, drop orphaned helper and stale comments

Second round of five-lane findings.

🔴 The one that mattered: I removed coverage of `features.imageIndexFeed`. Flipping
the fixture to `true` and deleting the flag-off routing test left `imageIndexFeed`
appearing in exactly one test file as a single hardcoded literal, shared by every
test — so `useIndex = !!hubId || (features.imageIndexFeed && !requiresDbPath)`
could have its flag conjunct deleted outright with nothing in the suite going red.
Found by the tests lane, which proved it structurally rather than by running.

Added the replacement test with its own ctx. Verified it can fail rather than
asserting it: mutating the controller to `!!input.hubId || !requiresDbPath` gives
`Tests 1 failed | 6 passed (7)` and

  AssertionError: expected "vi.fn()" to be called 1 times, but got 0 times

on the named test. Mutant reverted, 27 passed (2 files), exit 0.

Also from the lanes:

- `hubCreatorScope` had zero callers — its only one was inside the deleted region.
  Removed rather than left as a documented helper nothing constructs.
- `getImagesFromFeedSearch`'s hideChallenges security note claimed
  image-search.service.ts spreads `data` into "all three branches" and that "two
  branches would filter and this one wouldn't". Two branches now, one filters.
  The arithmetic in a security argument is worth keeping correct.
- `hub-feed-filter.test.ts` said the empty-intersection branch is "three separate
  `if (!capped)` lines"; two. Second copy of the sentence I fixed last round.
- Dropped the flipt mock from image.controller.feed-source.test.ts — the assertion
  that gave it meaning went with the BitDex routing, leaving a mock nothing observes.

Not changed, with reasons: the tests lane ruled my declared coverage gap a false
alarm — `withMeta:false` and both tag assertions already exist against
getImagesFromFeedSearch at index.test.ts:197-246, so replacements would have been
duplicates. `hubFilterArms` kept as a seam though it now has one consumer; its
parity comment was corrected last round.

Verified: typecheck 0 errors in 163s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KYrq2ighNp2rABTAvpzhj9

* docs(feedback): say in the code why bitdex-image-feed survives its producer

The slug has no writer since the BitDex decommission, so it reads as leftover and
the obvious cleanup is to delete it. Deleting it breaks a read path: this enum is
what getFeedbackAreaSchema validates a moderator's area query against, so removing
the slug makes every historical row filed under it unqueryable through that route.

The reason existed only in a PR body and a mail thread, which is where intent goes
to be refactored away. Putting it at the constant so the next person meets it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KYrq2ighNp2rABTAvpzhj9

* fix(bitdex): review round 3 — a comment my own fix broke, and a surviving mutant

Round-2 re-review of all five lanes at 47d883171b. Findings and remedies.

🔴 My round-1 fix broke the comment it repaired. metric-helpers.ts lost the
closing paren and the end of its sentence when the deleted endpoint name was cut
out, fusing two independent sentences into one unparseable line. Flagged by three
lanes independently. This is the class where fix rounds are more dangerous than
the diff they fix, and it happened on comment text where nothing typechecks.

🔴 A mutation of `useIndex` survives every test in the repo: dropping the
`!!input.hubId ||` arm passes 247 tests across the controller suites. That leaves
hub routing unpinned — with the index flag off a hub would route to getAllImages,
which throws rather than leaking, so the safety net holds while the routing
decision does not. Added a test; verified it kills the mutant rather than
assuming: `Tests 1 failed | 7 passed (8)`, failing test named, mutant reverted.

The first attempt at that control silently did not apply — a quoting error meant
the mutation was never written and the suite passed. A control that did not run
is indistinguishable from one that passed, so it is recorded here.

My FEEDBACK_AREAS comment named a mechanism that does not exist. It claimed
getFeedbackAreaSchema validates a moderator's query against the enum; that route
reads a Flipt flag and never touches a Feedback row, and there is no read path
over that table anywhere in the repo. Replaced with the true reason — the column
stores the label and this list is the only place the valid ones are written down
— and named the test that actually enforces the keep.

Stale comments the lanes caught, all describing deleted code as though it were
live: the ImagesInfinite JSDoc for a prop this PR removed; the `source` stamp in
image.controller.ts and its test docstring, both justified by a feed notice that
no longer exists; hubFilterArms' "both backends build their own clause syntax";
and the same claim in hub-feed-filter.test.ts.

image_post_triggers.sql is live programmability, not a record, and three of its
comments justified surviving design decisions by a BitDex sync trigger that was
dropped from prod tonight. Rewritten around the reasons that survive — the ~92M
unbackfilled rows and Meili's incremental sync window — with the rejected
alternative explicitly marked as not re-derived rather than left standing on a
dead premise.

Deferred, named rather than done: the two hub blocks in image.service.ts are now
byte-identical 15-line duplicates that were three copies with three clause
syntaxes. Collapsing them is the one reuse change worth making, but it is a
refactor on the hot feed path inside a removal PR.

Verified: typecheck 0 errors in 158s. Covering suites 95 passed (4 files), exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KYrq2ighNp2rABTAvpzhj9

* test(bitdex): drop the inert flipt and index mocks from the v1 images suite

Last of the round-2 findings. This file still mocked `~/server/flipt/client` and
seeded `getFliptVariant` twice with comments claiming the value selected a
backend — it does not: routing there is `useLegacyMethod` alone. It also mocked
`getAllImagesIndex`, which the handler graph can no longer reach, and nothing
asserted on.

The flipt mock was mine: I narrowed it to `FLIPT_FEATURE_FLAGS: {}` when removing
the BitDex enum member. A hand-listed mock standing in an empty object for a real
enum is the shape CLAUDE.md warns about — anything the graph later reads off it
yields undefined, and `getFliptBoolean`/`isFlipt` were not on the mock at all. It
passed only because `getAllImages` is mocked, one import edge from a confusing
failure. Removed rather than widened: the module still loads transitively through
`importOriginal` of image.service.ts, so no new env edge.

Test count unchanged at 33 passed, which is the point — removing a mock nothing
observed should move nothing.

Verified: typecheck 0 errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KYrq2ighNp2rABTAvpzhj9

* fix(bitdex): re-point the flipt eval-context ledger at the moved lines

CI `Unit tests (4)` was red on the head. The eval-context guard pins sites by
file:line, and this PR deletes 1,346 lines from image.service.ts, so every
ledgered line below the cut drifted. Both directions of the guard fired at once —
four sites unledgered, four ledger rows stale — which is the guard working.

Re-pointed five references, four ledger rows and one assertion:
  4136 -> 3101   feed-fetch-filter-in-post
  4277 -> 3142   feed-image-existence  (also the argc assertion at :222)
  5015 -> 3865   feed-image-existence
  6149 -> 4670   feed-image-existence
Each new line verified to be the same getFliptBoolean call as the old one on
origin/main. No ledger row added, dropped, or re-reasoned.

🔴 I first reported this failure as pre-existing, on a control that did not run:
`git stash` on an already-clean tree stashed nothing, so the "origin/main" run was
my own branch again and returned the same 3 failures. The real control — detaching
the worktree to origin/main — gives `Tests 12 passed (12)` there against
`3 failed | 9 passed (12)` here. The failure is mine. A stash-based control is
worthless unless something was actually stashed.

Verified: 12 passed (12), exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KYrq2ighNp2rABTAvpzhj9

* test(image): witness the userHubs refusal on the feed handler

Nothing tested it. Deleting both lines of the guard at image.controller.ts:283-284
left 115 tests green across all six hub-adjacent suites — `user-hub.router.gate`
covers the hub procedures, and `image.getInfinite` is not one of them.

`hubId` is a plain URL parameter, so without the refusal `/images?hubId=N` keeps
serving a hub feed after `userHubs` is turned back off. That gate had no witness.

Pre-existing, but this PR made it worse in a way worth naming: the hub-pinning
test added in 9d38e02e39 sets `userHubs: true`, so it exercises the guard's
pass-through and leaves the refusal unwitnessed — better-pinned neighbourhood,
same exposed gate.

Asserts the throw AND that neither fetch mock was called. The second half is
load-bearing: a change that refused after dispatching would satisfy a throw-only
assertion.

Verified capable of failing, naming the test rather than merely going red:
deleting both guard lines gives `Tests 1 failed | 8 passed (9)` with the failure
on `refuses a hub outright when userHubs is off, without dispatching` and nothing
else. Mutant reverted; typecheck 0 errors.

Test-only — no runtime line changed.

Found by the test lane in review round 3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KYrq2ighNp2rABTAvpzhj9

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 08:22:20 -06:00
..

Video: https://discord.com/channels/955572167662260295/1062092338698145812/1338591521401733192

# Playwright Testing

### Goal
E2E testing for the main app.
Catch potential issues when changing code, or evaluate edge cases.

### Anti-Goal
For this to be a frustrating pain-in-the-ass time vampire.
It's better to have 1 decent test than try to do 10 perfect ones and give up because it's too time consuming.

### What to Do

- Write tests for common user flows (fill a form, click a button, get a result)
- Handle what-if scenarios (errors, browsers, etc)
- A good rule of thumb is: if it's been reported as a bug, we should have a test in place for it (and things like it)

### What Not to Do

- Write "1==1" tests (dopamine hit, but pointless)
- Mandate coverage percentages (leads to annoyance and features not being done)

---

### How

Testing is intended to work on local development (docker) for consistency with users/data and easy tear down.

1) Run local services (`make init` or devcontainers)
2) Create a file in the `tests/` directory, or use an existing one. Doesn't really matter. Open to directory structure, so something like `tests/generator/gen-queue.spec.ts` would be reasonable.
3) Start writing tests.
    (a) can be done by hand if you know what you're looking to do
    (b) easier approach: `npm run test:gen -- --load-storage tests/auth/{user}.json --viewport-size 1920,1080 http://localhost:3000/{url}`
        - This allows you to create tests by interacting with the page and picking locators
    (c) we'll need better locators, especially for icons. add `data-testid=` to the places you need them (they'll be stripped from production)
    (d) use the various authed users to test different scenarios (mod, full access, muted, etc)
    (e) feel free to mock responses from any of the APIs, but in general it's best to only do this for external services
4) Run with either `npm run test` or `npm run test:ui` to do it interactively with screenshots
5) If you need to reset the db after each test, you can either:
    (a) clean up the mutations as part of the test (delete an object you just made)
    (b) `make boostrap-db` to reset the whole database back to normal
6) We'll eventually set up the github action to run this before a deploy

### Test Failures

There are 4 types of test failures:

1) A bad test (always fail)
    - these might have bad selectors or inaccurate logic
    - **solution**: fix them
2) A flaky test (sometimes fail)
    - frustrating tests which seem to pass most of the time, but not always
        - this is usually a result of race conditions, mismatched timing, or not properly awaiting events like animations
    - **solution**: narrow down which part of the test fails, and catch the flaky issue
3) Intended code change
    - we might have changed the verbiage on a button, which makes certain locators no longer work
        - this is fine, although locators should try to be as agnostic as possible
    - alternatively, we might have simply changed the business logic
    - **solution**: in either case, simply update the test itself
4) Unintended code change
    - you've changed something in the app, and a test breaks due to the introduction of a bug
        - this is the major reason we have tests
    - **solution**: leave the tests alone, they're doing their job. fix the code.