mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
main
77 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6471abff78 |
feat(ingestion): scan images with imageScanning behind a Flipt flag
ClickUp 868m5wp5r. Image and video ingestion can submit the single
orchestrator `imageScanning` step instead of `wdTagging` + `mediaRating`,
gated by the Flipt flag `image-ingestion-image-scanning` (per image id,
off by default). Flag off, the submitted workflow is unchanged.
- `/api/webhooks/image-scan-result` stays the only callback. It fetches
the workflow and routes on its step types, so both shapes can be in
flight across a flag flip.
- Stages both pipelines share move unchanged to `image-scan-pipeline.ts`.
The legacy service keeps its own parsing and flow.
- New `image-scanning-result.service.ts` reads the imageScanning output
directly (images and video frames), keeps only general tags, and
records csam without acting on it, as legacy does.
- The new pipeline logs to Axiom as `image-scanning-result` /
`image-scanning-ingestion`, submits under
`image_scan_submitted_total{lane="imageScanning"}`, and writes scanner
audit rows as version '2'.
- Remove the non-orchestrator scanner path: the webhook's legacy body
handling, the `IMAGE_SCANNER_NEW` Redis toggle, `ingestImageBulk`,
`image.ingestArticleImages`, `/api/webhooks/reingest-images`,
`/api/internal/add-missing-phash`, `/api/mod/scan-images`, and the
`IMAGE_SCANNING_ENDPOINT` / `IMAGE_SCANNING_MODEL` env vars.
- Bump `@civitai/orchestration-client` to 0.2.0-beta.106 for the
imageScanning types.
Keep the flag off until a deploy has fully rolled out: pods on the
previous build cannot read imageScanning callbacks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
730a6b14e2 |
feat(apps): one nav instead of two — a collapsible left rail for /apps/* (#4813)
Replaces the second horizontal nav bar on /apps/* with a collapsible left rail, following the AccountLayout section-registry pattern and the CollectionsLayout rail mechanics. 260px open (default), 56px collapsed, localStorage plus a cookie mirror for the SSR seed, one global state across all 12 routes so the chrome alignment invariant holds, Drawer below 1300px. Also ships a repo-wide ESLint rule (no-ssr-divergent-media-query) banning the four media-query hooks with no server answer, and removes ~742 lines: a hand-rolled AST walk whose hazard was already covered by the SSR render test, and the localStorage half of the rail store. No store column ladder change ships. The re-tune was reverted: a rail-state-aware rung is not expressible as one width-keyed ladder, and a flat one made viewers with no rail on screen pay a column. The density cost is now confined to viewers who opened the rail, in the 1600-1920 band. Verified: step-level green across 13 check-runs and 7 commit statuses on the merged tree; unscoped unit project 1,796/1,800 files passing, with the single failure established pre-existing by a control run at origin/main. Not verified: the page has never been loaded in a browser at any viewport. See the accepted-exposure comment on the PR. |
||
|
|
cdf6fc8cc1 |
feat(apps): surface Build apps as an /apps/* subnav tab, collapse the dropdown to one entry (#4668)
* feat(apps): surface Build apps as an /apps/* subnav tab, collapse the dropdown to one entry
The user-menu dropdown carried two adjacent App-Blocks rows — "Build apps" ->
/apps/get-started and "Apps" -> /apps. A moderator holds both flags, so they saw
two near-identical rows for one product.
Now there is ONE row. Its href is /apps with store access and /apps/get-started
without: a get-started-only viewer cannot load /apps at all (its
getServerSideProps runs resolveAppsPageAccess, which returns notFound), so
pointing them there would be a menu entry into a 404. "Build apps" is instead a
tab in the shared /apps/* subnav, ahead of Marketplace.
The subnav's whole-bar gate widens from hasAppsStoreAccess(features) to
hasAppsStoreAccess(features) || features.appBlocksGetStarted. This is required,
not optional: on the old gate the container returns null for exactly the cohort
the new tab exists for, so the tab would be invisible to them on the one page
they can load. Safe on the first paint for the same reason already written out
for appBlocksAuthor — appBlocksGetStarted is SSR-seeded into pageProps.flags,
frozen by useState in FeatureFlagsProvider, and not toggleable, so the server and
first client renders compute the same boolean.
Visibility side effect, intended: "Build apps" is unconditional, so the always-on
tab set goes 1 -> 2 and the "< 2 tabs" collapse no longer hides the bar for a
non-author with no installs. Those viewers now get the subnav on all 13 /apps/*
routes. The collapse branch is kept but is no longer reachable through the
container, and the tests that used to cover it say so rather than pretending.
The app-block chrome menu deliberately does NOT mirror the new row: it opens over
a RUNNING app, and it has no feature-flag plumbing, so mirroring a kill-switched
page would keep offering it after the switch. That exclusion is now asserted as a
set in chromeNavAlignsWithSubNav.test.ts rather than left as silence.
Known and unchanged by this commit: the Marketplace tab is unconditional, so a
viewer admitted by the get-started term alone sees a tab that /apps answers with
notFound. Not reachable today (the flag is staged mod-only and a moderator holds
the store flags), but the trigger is a Flipt toggle, not a deploy. Gating
Marketplace is not the fix — it drops that viewer to one tab and the collapse
hides the whole bar again. Recorded on both the subnav entry and get-started.tsx.
* fix(apps): gate the Build apps tab on appBlocksGetStarted, not on nothing
The tab was `visible: () => true` while the whole-bar gate is an OR
(`hasAppsStoreAccess(features) || features.appBlocksGetStarted`). A viewer with
store access but WITHOUT the get-started flag therefore passed the gate and was
offered a "Build apps" tab whose page answers 404 -- `resolveGetStartedAccess`
returns `{ notFound: true }` and the client body renders `<NotFound/>`.
Two consequences, both real:
1. A non-mod `app-dev-testers` member holds `appBlocks` (so `hasAppsStoreAccess`)
but not `appBlocksGetStarted`, so they were offered a 404. Before this branch
they had no route to that page at all -- the dropdown row was correctly gated
on `appsNav.getStarted`.
2. Flipping `app-blocks-get-started` OFF in Flipt -- the flag's stated purpose as
a kill switch -- no longer removed the nav entry, because every mod holds
`appBlocks` and the tab was unconditional. Section 4 of `IframeHost.tsx` cites
exactly that hazard as the reason to EXCLUDE the route from the app-block
chrome nav; the subnav was doing the thing that argument forbids.
The fix follows the pattern already established for `isAuthor`:
`AppsNavContext` gains `canGetStarted`, the container derives it from
`features.appBlocksGetStarted`, and the tab reads `visible: (_s, c) =>
c.canGetStarted`. The flag is verified NOT `toggleable` (absent from
`computeUserFeatureFlagsOverlay`; defined at feature-flags.service.ts:561), so it
is SSR-frozen and safe outside the `useIsClient` deferral -- the same argument
the branch already makes for the bar gate.
`canGetStarted` is deliberately NOT session-scoped, unlike `isAuthor`:
`resolveGetStartedAccess` reads the flag and consults no user, so folding it into
the `currentUser` branch would hide the tab from a logged-out viewer the page
would serve, and (Marketplace being their only other tab) the `< 2` collapse
would then hide the whole bar from them.
Resulting behaviour, each pinned by a test:
- store access only -> Marketplace alone, `< 2` collapse, no bar
= exactly main's behaviour, no new regression
- appBlocksGetStarted only -> Build apps + Marketplace, bar renders
- both -> both tabs
- logged out, no flag -> no bar; logged out WITH the flag -> Build apps
Because the collapse is reachable again, the test blocks the previous round had
to relabel as "pinning the view, not a cohort" are relabelled back to what they
are: coverage of a live cohort. Mutation-checked -- deleting `links.length < 2`
turns 6 of them red.
Also in this commit:
* chromeNavAlignsWithSubNav.test.ts: the exclusion ledger's parse controls sat on
the live counts (`toBe(4)` / `>= 8`), so they STOLE the ledger's failure.
Adding a chrome entry failed on `expect(inChrome.size, 'parsed no items out of
the chrome platform nav').toBe(4)` -- a message that states the opposite of
what happened -- and the `toEqual` ledger never ran, leaving its SHRINK
direction unproven. All three count controls are now `>= 1` and the ledgers own
their sets. Re-mutated both ways: GROW and SHRINK now each die on the ledger's
own assertion and message.
* The two `/apps/*/edit.tsx` notes and the `get-started.tsx` referent they point
at contradicted each other after this branch. Repaired against the code: the
empty-band case is closed for `/apps/get-started` and still open for the other
pages, and the tab enumeration now includes the get-started capability.
* AppsPageLayout.geometry.browser.test.tsx: dropped the computed-but-unasserted
`tabWidth`, put every element lookup behind a `required()` helper that fails
with the actual diagnosis instead of a bare TypeError, and rewrote the stale
header note from the post-fix code.
* preview-apps-marketplace.spec.ts documented "first tab is Marketplace
(AppsSubNav.tsx:55)" -- both halves wrong. Now names the row it keys on and
says why order is irrelevant to the assertion.
* hooks.tsx: recorded the label/destination decision for the get-started-only
cohort ("Apps" + plug glyph, navigating to onboarding) rather than leaving it
undocumented, with the trigger to re-decide it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzWdpMmKwGdvb2K3eSua8h
* docs(apps): the hydration note now covers all three SSR-frozen inputs
It said "Both inputs" and enumerated only `appBlocksAuthor` and
`currentUser.isModerator`; the context object now derives a third,
`canGetStarted`, from `features.appBlocksGetStarted`. Same argument, same
evidence (not `toggleable`, so the client overlay cannot move it) — the comment
just did not say so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzWdpMmKwGdvb2K3eSua8h
* test(apps): own parseAllChromeLinks' route set with a ledger; correct two decision records
Round 3. Round 2 relaxed three count controls in
chromeNavAlignsWithSubNav.test.ts from exact counts to
toBeGreaterThanOrEqual(1). Two of those were right — their sets are still
owned by a toEqual elsewhere (the exact 4-item platform nav; the exact
excluded set). The third was not: nothing owned parseAllChromeLinks' set,
and its comment claimed "same reasoning as the two above", which was false.
The gap was live, not theoretical. Rules (a) and (b) in that test are
per-link, so a new chrome item pointing at a route SUB_NAV_LINKS already
carries, wearing that row's own glyph, satisfies both. Measured on the
pre-fix tree: adding a "Build apps" ChromeSurfaceItem for /apps/get-started
to the ⋮ overflow — outside the platform-nav slice the other ledgers
enumerate — passed all 8 tests. That is exactly the hazard the DELIBERATE
SUBSET note in IframeHost.tsx argues disqualifies the route: this surface
has no feature-flag plumbing, so it would keep advertising a page that
answers notFound once appBlocksGetStarted goes down.
Fix: add (d), a toEqual ledger over the whole literal-href set, sorted (so a
reorder cannot report a route change that did not happen) and keeping
duplicates (/apps/installed legitimately appears twice). Placed after
(a)/(b)/(c) so the more specific rules keep their own messages. toBe(5) is
deliberately NOT restored — it misdirected, telling a maintainer who added a
legitimate destination that the scanner had broken.
Mutation matrix (unit tier, isolated to one hunk each):
overflow-grow (add /apps/get-started to the ⋮ overflow)
pre-fix : 8 passed — SURVIVED
post-fix: 1 failed | 7 passed — dies on (d)'s own message,
"the set of routes the app-block chrome links to has changed…:
expected [ '/apps', '/apps/get-started', …(4) ] to deeply
equal [ '/apps', '/apps/installed', …(3) ]"
(a) and (b) pass, so (d) is the only thing that catches it.
platform-shrink-review (delete the Review item)
(d) fires with its own message (…(2) vs …(3)). Three other tests also
fire, which is correct — a platform-nav item is owned by them too.
overflow-shrink (delete "Manage apps")
dies on (c), which precedes (d) in the same test. Recorded honestly:
every EXISTING link is already owned by some assertion for SHRINK, so
(d)'s unique contribution is GROW outside the platform-nav slice.
Re-confirmed, no regression, both pre-existing ledgers still die on their
own toEqual:
exclusion ledger GROW (new SUB_NAV_LINKS row) → 1 failed | 7 passed
exclusion ledger SHRINK (add get-started to the platform nav) → fires
expected-glyphs GROW (same mutant) → fires
expected-glyphs SHRINK (delete Review) → fires
Also corrected two decision records that claimed more than the repo can
establish:
- The comment above the relaxed floor no longer says "same reasoning as the
two above". It now says this floor has NO sibling toEqual to inherit its
set from, and points at (d).
- hooks.tsx: the re-decide trigger was narrower than the branch's
reachability. The branch is `!marketplace && getStarted`, so the store
flags NARROWING (app-blocks-enabled / app-listings turned off in Flipt
while get-started stays on) reaches it just as widening get-started does —
every moderator would then see a row labelled "Apps" with
IconPlugConnected navigating to developer onboarding. Both directions are
now named. The stated ground was also a live-Flipt claim presented as
derived from `availability`; since getFeatureFlags returns Flipt's answer
before evaluating roles, `availability` is only the Flipt-down fallback, so
the cohort's emptiness is observable only in live Flipt. Said so. All four
App-Blocks flags verified Flipt-backed (feature-flags.service.ts:510, 520,
561, 571). Comment-only; no behaviour change.
Verification (branch worktree, instruments validated before each zero):
- vitest --project unit <this file> → 8 passed; whole AppLayout+guard → 32 passed
- vitest --project component (unfiltered) → 217 files, 2403 tests, all passed
- node scripts/typecheck.mjs → OK — 0 type errors in 81s. Positive control:
the identical command minutes earlier reported 2848 errors off a Prisma
client generated 2025-12-08; `prisma generate` under the flake's engine
paths takes it to 0.
- eslint on both files → 0. Positive control: src/utils/zod-helpers.ts
appended reports 6 problems. Array-quoted, file count read back as 2.
- prettier --check → clean. Negative control: a misformatted scratch file
reports "Code style issues found".
- Merged tree vs current origin/main (
|
||
|
|
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 |
||
|
|
9b04487b32 |
fix(tests): unbreak main — the redis-key guard and the reaction.toggle contract (#4544)
* test(preview): pin reaction.toggle to its restored awaited contract `preview / smoke-tests` has been red on main since #4516. The spec asserted `reaction.toggle` resolves to null; it now resolves to "created". The code is right and the assertion was stale. Timeline: |
||
|
|
965cd09ab4 |
fix(tours): stop guided tours dying silently, and make the failures measurable (#4478)
A missing target, a rejected step hook, or a handle to a Joyride that had already
been replaced all ended a tour the same way: silently, and persisted as completed.
Each now degrades and records why.
- Missing targets and rejected hooks no longer read as a click on Next, and skip in
the direction of travel so `Back` works behind a dead step.
- `gen:buzz` had pointed at `BuzzTransactionButton` since the generator stopped
rendering one (
|
||
|
|
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. |
||
|
|
6e61e1821c |
test(preview): fail a stranded /moderator probe in the local suite, not only in a job nobody reads
#3573 migrated the moderator surfaces to the standalone app and four preview specs kept probing paths that now 302 off-origin. `preview / smoke-tests` went red and STAYED red on every PR based after it, and because that job is report-only nothing stopped: three PRs merged through it in the four hours before someone looked, and an agent nearly attributed a genuine `main` breakage to their own PR because it arrived inside an already-red set. #4179 fixed the four failures. This is the recurrence guard. It scans the preview specs for `/moderator/*` literals and fails when one has migrated or resolves to no page, so the next migration reddens the machine of whoever performs it — `test:unit:run` is on the before-committing list — as one named test carrying the path and the fix, rather than as browser assertions in a job whose red is ambient. Be precise about what that buys, because the obvious reading is wrong: it does NOT make the fact blocking. The `unit` job is `continue-on-error: true` and `main` has no required status checks, so this is report-only too. What changes is where and how the failure appears. The three specs that assert the redirect on purpose carry an inline `@migrated-route-probe` marker, checked in both directions — a path that comes BACK to this app strands the assertion the same way. A marked line may hold only one probe, or the marker would excuse the others silently. Two properties the scan needs and did not get for free: the positive control asserts the ENFORCED partition rather than the total, because `it.each([])` registers zero tests and exits 0 (measured), so marking every line would empty the guard with nothing to show for it; and the capture stops before `?`, since `/moderator/reports?status=Pending` is the natural shape of a queue probe and the exact route the incident was about. Documentation this turned up as stale: the convention-guard list named four of seven, `test:lint- rules` is invoked by no workflow (those guards run because they match the `unit` project), the root test-command list omitted the packages and apps suites entirely, and the SvelteKit standard had no testing section at all despite all three apps having one. The `unit` job's flip-to-blocking note now carries what was measured today rather than leaving it to be rediscovered: 8 of 39 recent runs had a red `Unit tests` STEP across 8 distinct branches, which reads as flake but was trunk-red from a ledger test failing on every PR (fixed by #4191); 5 of 5 green after it. Plus the two traps — the run-level `conclusion` says success while the step under it failed, and this workflow is `pull_request`-only, so a trunk-red test shows up as every PR reddening at once. Refs 868kubuz6 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9c373dfb4e |
docs(tests): the dropped report coverage is LOST, not relocated — a do-not-restore comment had a false reason (#4183)
Comment-only. Corrects a claim #4179 put into the merged code, where it is more dangerous than it was in a PR body. `tests/preview-moderation.spec.ts` asserted in two places that the dropped report-actioning coverage "belongs to `apps/moderator`'s own suite". There is no such suite: apps/moderator *.test.ts / *.spec.ts files : 0 positive control — packages/ : 81 The coverage is LOST, NOT RELOCATED, and the loss originates in #3573's deletion of `report.getAll` / `report.setStatus` — not in the change that stopped asserting them. 🔴 Why this warranted a PR rather than a note on the issue: the second instance is attached to a DO-NOT-RESTORE instruction. That is the shape where a false rationale actively misleads — a maintainer who does the right thing and checks the stated reason before obeying finds no suite, reasonably concludes the comment is stale, and may try to restore legs calling procedures that no longer exist in this app. The load-bearing half of that comment is correct and kept verbatim: the procedures were deleted, so asserting them from a preview of this app is not possible. Only the clause about where the coverage went was wrong. The do-not-restore reason is now "the procedures do not exist, so it cannot be made to work", which holds independently, plus an explicit note that the old rationale was refuted and why the distinction matters at that specific site. Verified comment-only by stripping comments from base and head and diffing the result, not by a line-prefix grep — which cannot tell a block comment from code. prettier clean. No behaviour, assertions or test names touched. Refs: #4182, #4179, #4171, #3573 |
||
|
|
3cf6c7e879 |
fix(tests): unbreak preview smoke — moderator routes migrated, and two router procedures deleted (#4179)
`preview / smoke-tests` had been red on every PR based after #3573, which migrated the moderator surfaces to the standalone app. Three PRs were merged through the red gate in the four hours before this fix — the failure mode a permanently-red shared gate always produces. Four failures, from TWO causes, not one: * Two routing assertions still pointed at `/moderator/reports` and `/moderator/images`, which now 302 to the standalone app. Repointed to `/moderator/rewards` and `/moderator/suspicious-audit-matches`. * `mod can self-seed a post, report it, and action the report` is NOT a routing problem: #3573 also deleted `report.getAll` and `report.setStatus` from the main app's router. Keeps `post.create` -> `report.create` (both still main-app `guardedProcedure`) and drops the actioning legs. Route screening targeted the property that defeated three earlier candidates — an anchor that renders UNCONDITIONALLY. Both routes were checked on `origin/main` for: file present; not matched by the real longest-prefix `migratedRouteKey` logic (run with `reports`->`reports` and `images`->`images` as positive controls in the same pass); `requireModerator: true`; no feature-flag gate; and no counterpart in `apps/moderator/src/routes/`. Anchors are `<Meta title>` plus an unconditional `<Title order={1}>` read via `getByRole('heading')` — structural rather than a text search, and above the `isLoading ? … : empty ? … : rows` ternary, so they hold on a full or empty prod-clone DB and even if the page's tRPC query errors. `preview-auth-guard.spec.ts` keeps `MODERATOR_PATH` and asserts the 3xx with `maxRedirects: 0`, checking the `Location` is neither of the guard's two bounces and IS the migrated hop — the idiom that file already uses for the external `/login` hub. Also fixes `tests/preview-auth.setup.ts`, which warmed `/moderator/reports` and `/moderator/images` with the mod cookie. Those now 302 to `MODERATOR_APP_URL`, which DEFAULTS TO PRODUCTION — so CI was warming the production moderator app and compiling nothing on the preview. ⚠️ The dropped report-actioning coverage is LOST, NOT RELOCATED. `apps/moderator` has zero test files (positive control: 81 under `packages/`), so there is no suite for it to move to. The loss originates in #3573's deletion of the procedures, not here — leaving the assertions would keep the gate red for a reason no main-app change can fix. Tracked as a follow-up. Verified on the real gate rather than inferred: `preview / smoke-tests` 66 passed / 0 flaky / 0 failed on pipeline `pr-preview-4179-s64z2`, against the broken baseline of 61 passed / 4 failed. Test-only — 3 files under `tests/`, no `src/` change. Closes #4171 |
||
|
|
018f9a0e7f |
fix(og): unblock the SVG loader Next 16.3.0 leaves blocked, so /api/og stops 500ing (#3777)
Next 16.3.0's image optimizer applies a process-global libvips loader allowlist that omits SVG. next/og rasterizes satori's SVG through the same sharp instance, so every ImageResponse threw 'Input buffer contains unsupported image format' once the optimizer initialized — making /api/og return 500 on 100% of requests. Carries upstream vercel/next.js#96681 verbatim as a version-pinned pnpm patch (both the CJS and ESM image-optimizer copies), plus a CI guard that fails the build if the patch stops applying. Verified on the standalone preview build: /api/og returns a valid PNG on both the FallbackCard and OgCard paths after the optimizer is warmed to a cache miss. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
08f81f0bac |
test(preview): fix whatIfFromGraph smoke test against tRPC request batching (#3541)
* test(preview): fix whatIfFromGraph smoke test against tRPC request batching The `whatIfFromGraph fires on /generate` preview smoke test has failed 100% of the time on every PR. It is NOT a cold pod, a DB artifact, or a product defect: driving a live preview shows the request fires ~1.3s after navigation and returns HTTP 200 with `cost.total = 8`. Two test-side defects, both introduced by `trpcBatching` (#2946) ramping on: 1. URL predicate. `httpBatchStreamLink` coalesces concurrent queries into one request whose path is the COMMA-JOINED procedure list, and whatIf is last: /api/trpc/content.get,challenge.getInfinite,...,orchestrator.whatIfFromGraph That does not contain `/api/trpc/orchestrator.whatIfFromGraph`, so `waitForResponse` never matched and timed out on a request that succeeded. Now matched by procedure name within the path list (batched or not). 2. Body shape + capture. The client sends `trpc-accept: application/jsonl`, so the response is newline-delimited chunks — `response.json()` throws, and the payload sits at a different depth than the old walk expected. Parse raw text, handling all three wire shapes, and locate the payload structurally (numeric `cost.total` + boolean `ready`, the whatIf contract) so a sibling procedure in the batch cannot produce a false pass. The app also reads the full stream and then abandons the idle reader, which Chromium reports as `net::ERR_ABORTED` and evicts — making a later `response.text()` fail with "No data found for resource" on 4 of 8 loads. The body is now buffered by a route handler, removing that race. Verified against the live preview for PR #3535: red at origin/main (45s `waitForResponse` timeout, the exact CI signature), green after, 12/12 runs (11/12 without the internal retry), ~2s to the assertion. Mutation-checked: disabling the jsonl branch fails with this guard's own error ("cost.total parsed from whatIfFromGraph response" -> null). The 45s budget is unchanged and the test still guards the pre-spend cost quote. * fix(test): narrow capturedBody honestly instead of casting through null CI Typecheck failed: tests/preview-generation.spec.ts(177,24): error TS2352: Conversion of type 'null' to type 'string' may be a mistake `capturedBody` is declared `string | null`, but it is assigned from inside the `page.route` closure, which TS control-flow analysis cannot see. After the per-attempt reset to null, TS narrows it to `null` at the return, making `as string` an illegal null->string conversion. Replaced the `expect(...).not.toBeNull()` + cast with an explicit null check. That narrows the type honestly, removes the cast entirely, and fails with a message distinguishing the two halves: the response was observed, but the route handler did not capture its body. Verified: tsc error count 22 at origin/main and 22 here in the same environment (delta 0), with zero errors mentioning this spec. The 22 are pre-existing local artifacts (no prisma generate on NixOS, missing submodule). Why this was not caught before pushing: the fix was verified by RUNNING the Playwright spec 12 times, and Playwright transpiles without full type checking. `tests/` is in the tsconfig include set, so CI checks it — a green Playwright run is not a typecheck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: audit <alexhurwitz.dev@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c263ce0d42 |
feat(apps): retire the legacy /apps/[appBlockId] detail page — redirect to the store detail (#3493)
* feat(apps): retire the legacy /apps/[appBlockId] detail page (redirect to the store detail)
The per-app route `/apps/<appBlockId>` was a second, diverging detail surface for
the same app. It rendered `by <app name>` where the store correctly renders the
owner's username, and its raw bridge-less `<iframe>` "Live preview" painted a
permanent light-theme panel on a dark page. Its whole action set is already
covered by `/apps/store-preview/<slug>`.
`getServerSideProps` now redirects it there:
- approved listing found -> 302 to `/apps/store-preview/<slug>`
- no approved listing (pending / rejected / never-approved app) -> site-standard
404, NOT a bounce to `/apps`. Listings are minted at approval, so those apps
have no store target; sending their owner to a store that by construction does
not list them is a silent dead end.
The store-visibility flag gate stays FIRST, before the route param is read and
before any DB access, so the retirement is not an existence oracle for a viewer
the flag does not grant.
The branch is extracted as a pure, I/O-free `resolveLegacyAppRedirect` module so
both decided outcomes are asserted in the node-env unit project (13 tests),
including slug encoding / open-redirect containment.
Redirect only: the page body and the three inbound callsites are deliberately
untouched so a stale bookmark or external link resolves through the hop. Sibling
routes (`/edit`, `/edit-manifest`, `/listing`, `/revenue`) are unaffected.
* test(apps): cover the SSR half of the retirement + retarget the e2e spec
Follow-up to the adversarial review of this PR, which found that the
security-relevant half of the change was the untested half.
- Extract the whole SSR decision into `resolveLegacyAppRoute`, taking the
listing lookup as an argument. The gate-before-query ordering is the real
invariant here and was invisible to a test of the string-building function
alone; with the lookup injected, a test asserts it is never even CALLED for a
viewer without store visibility. Verified by mutation: moving the gate after
the lookup fails 2 tests.
- Pin the approved-only filter via `approvedListingSlugQuery`. Dropping
`status: 'approved'` was a one-word edit that inverted the decided behaviour
while every existing test still passed. Verified by mutation: 1 test fails.
- Cover route-param handling (missing / blank / non-string) and the
either-flag OR-fallback. 13 -> 21 tests.
- Retarget `tests/preview-apps-marketplace.spec.ts`: its last leg asserted a
heading on the retired page, which no longer renders — it would have tested
the store detail by accident and raced that page's client-side query. It now
asserts the retirement itself (lands under /apps/store-preview/), and names a
404 as a real data signal rather than flake.
- Correct the callsite count in the docs: there are FOUR inbound links, not
three (the editor's own "Back" was missed). Record the two accepted
consequences of leaving them alone, including that the store detail's
info-mode CTA for a model-slot app now hops back to itself (no live app is
in that state — every approved on-site listing declares a page).
- Narrow the existence-oracle claim in the module docstring: gate-first
protects viewers WITHOUT store visibility; for those with it, 302-vs-404 is
a new HTTP-level signal the old page did not emit.
* docs(apps): name the Install gap, drop assertions that cannot fail
Second independent adversarial pass over this PR. Its conditional blocker
resolves to a NIT against live data (0 approved on-site listings are page-less
— every one declares a page, so none takes the `info` branch), but it surfaced
a claim of mine that was simply wrong and several assertions with no failure
mode.
- Correct the "the store detail covers the whole action set" claim. It does
not: `AppListingDetailBody` has NO install/manage affordance, so a
model-slot app would have nowhere to install from. Vacuous today, but it is
the real gap to close before a model-slot app is approved — and the reason
the follow-up must RETARGET the info-mode CTA, not just delete the route.
- Note the sibling `legacyEditRedirect` in listingEditNav.ts, which does the
same job for the owner-edit routes, and why the stricter non-string handling
here is deliberate rather than an oversight.
- Note on /apps that the documented one-line rollback to the AppBlock grid is
now partial: AppBlockCard's links point at the retired route.
- Replace `destination.startsWith(STORE_PREVIEW_PATH_PREFIX)` — a tautology,
since the destination is built from that same constant — with the property
that can actually fail: the slug never introduces a second path segment.
That is the real open-redirect containment assertion.
- Drop a vestigial `detailName.length > 0` from the e2e leg and assert instead
that the viewer did not remain on the legacy route.
- Record all five verified break-it mutations in the test file header.
* style(apps): drop an unrelated reformat from the retired page
A prettier --write run collapsed a JSX block that this PR does not otherwise
touch. Restore main's formatting so the diff stays confined to the change.
* docs+test(apps): name the THIRD unreplicated gate, correct the orphan claim, kill four unfailable assertions
Audit follow-up on the legacy-route retirement. Production logic unchanged —
this is a disclosure correction plus four test defects.
Disclosure:
- The gate-parity note named TWO unreplicated destination gates; there are
THREE. `getListingDetail` also applies a store-scope KIND gate
(`scope === 'public-external' && row.kind !== 'offsite'`). Because this
route's param is an `AppBlock.id` and every listing carrying an
`appBlockId` is `kind='onsite'` (20/20 in live data), that gate covers
100% of this route's resolvable set under `public-external` — where the
deploy and maturity gates are 0-instance. Unreachable today only because
the page gate keys on the same Flipt flag as the scope's first axis; a
widen of both breaks the alignment. The remediation is restated as all
three and is explicitly NOT implemented here — it needs the resolved store
scope and red-capability threaded into an SSR resolver that takes neither.
- "Nothing is orphaned" was FALSE. The legacy 5-star AppBlockReview write
form's only other host is AppDetailsModal ← AppBlockCard ← MarketplaceBody
/ RecentlyOpenedApps, and MarketplaceBody has had no importer in app code
since /apps swapped to AppListingsMarketplaceBody. After this redirect the
surface has no reachable entry point. Harmless today (`app_block_reviews`
is 0 rows in production) but the follow-up must decide its fate explicitly.
- "Four callsites" is four FILES / five link sites — AppBlockCard links twice.
Also named: the hop drops the query string (harmless in-product; external
tracking params are lost).
Tests (21 -> 24), each verified by a killing mutation:
- Passing the UNTRIMMED appBlockId to the lookup while still guarding the
trimmed value left the suite fully green — every fixture id was
whitespace-free. A ' apb_x ' granted-viewer case now catches it.
- Two `expect(...includes('/')).toBe(false)` sub-assertions each followed a
literal-equality assertion over the same string in the same `it`, so they
could never be the assertion that failed. Removed; the containment property
now has its own `it` over separator-bearing inputs no literal constrains.
- The traversal claim was overstated: encodeURIComponent('..') === '..', so a
dot-only slug resolves to /apps/, not under the prefix. Claim narrowed and
the dot-only case pinned (unreachable given SLUG_REGEX).
- The retargeted e2e leg asserted `landedOn !== '/apps/<id>'` after asserting
it matched /^\/apps\/store-preview\/.+/ — unfailable. Replaced by a
one-path-segment assertion and a redirect-chain assertion that pins the
retirement to the HTTP layer.
|
||
|
|
0015996cd9 |
fix(tests): repair stale approve-gate assertion after the icon+cover floor (#3392) (#3468)
The `preview / smoke-tests` check has been red on every open PR for days on a STALE test assertion, not a real regression. #3392 ( |
||
|
|
76706c367d |
fix(tests): repair component-suite build death (sharp) + smoke external-listing OAuth payloads (#3317)
Two independent baseline breaks that fail on every PR preview.
COMPONENT (build death): two page-importing browser tests
(review-detail-page / review-queue-nav) drag the full server router graph
(server-side-helpers -> routers/index -> creator-shop.router ->
creator-shop.service -> `import sharp from 'sharp'`) into the browser
bundle. Next strips the server-only getServerSideProps graph from real
client builds; Vitest's browser build does not, so esbuild's optimizeDeps
scan follows the import into sharp and dies bundling its native
`require('../build/Release/sharp-*.node')` -- killing the WHOLE component
suite before any test runs. (The tests already `vi.mock`
server-side-helpers, but that is a runtime interception and can't stop the
build-time static scan.) Fix at the build level: alias `sharp` to a trivial
stub for the `component` project only (test/stubs/sharp.ts); the `unit`
project keeps real sharp. Verified: the sharp `.node` esbuild error is gone
and the affected tests build + execute.
Also unmasked by the build fix: review-detail-page.browser.test.tsx was
stale vs a page refactor -- the page now renders the review body via
`ReviewDetailView` (which owns the real trpc-backed `ReviewActionBar`),
not the old `OnsiteReviewModalBody` the test stubbed, so 2 tests threw
`trpc.useUtils is not a function`. Re-point the body stub to
`ReviewDetailView` (matches the test's stated intent -- assert shell
wiring, not re-run the action bar's own covered behaviour). 5/5 pass.
SMOKE (400 on submitExternalListing): the three preview-apps-external-*
specs predate #3227, which MERGED OAuth-connect into the single external-app
submit flow ("every external app IS an OAuth app"). `connectClientId` +
`requestedScopes` + `scopeJustifications` are now unconditionally required
for ALL external listings BY DESIGN (well-documented, well-reasoned in the
schema) -- this is intended, NOT a regression, so the source is left
untouched and the stale smoke payloads are updated. Each submit-bearing spec
now creates a throwaway owned OAuth client (`oauthClient.create`), passes its
id as `connectClientId` with an empty scope disclosure (`requestedScopes: 0`
+ `scopeJustifications: {}` -- 0 is a subset of any client ceiling; no scopes
=> no justifications), and deletes the client on cleanup (self-cleaning like
the draft + slug). Verified the new payload against the real zod schema
(passes; the old payload still fails); the end-to-end preview run
(connectClientId ownership is a service/DB check) is CI-to-confirm.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
5e8ec4f70b |
W13 P3b PR4 — off-site claimListing (mod-arbitrated ownership reassignment) [DARK] (#2970)
* W13 P3b PR4 — off-site claimListing (mod-arbitrated ownership reassignment) [DARK]
The FINAL P3b piece, split out of PR3. A moderator-only, arbitrated ownership
transfer for an approved OR delisted off-site AppListing: reassigns
`AppListing.userId` to a mod-verified target user, fully audited, no self-service.
Service (`offsite-moderation.service.ts`), mirroring the delist/relist/purge
tx + guard + audit pattern EXACTLY:
- kind guard: offsite-only (on-site is 1:1 with an owned AppBlock) → generic NOT_FOUND
- status guard: allow claim only on {approved, removed}; draft/pending/rejected →
NOT_TRANSITIONABLE, zero events
- target-user validation on the PRIMARY inside the tx → friendly INVALID_TARGET_USER
(BAD_REQUEST) instead of a raw FK 23503 leaking as INTERNAL
- pre-state (before.userId + slug) snapshotted from the in-tx PRIMARY read (mirrors
PR3 purge) so replica lag can't stamp a stale owner
- status-guarded updateMany (status IN approved|removed); 0-count → NOT_TRANSITIONABLE,
rollback before the audit event (zero events on a guarded/raced claim)
- ONE AppListingModerationEvent(action='claim', actor=reviewer, before/after userId, slug)
- `AppListingPublishRequest.submittedByUserId` left INTACT (locked decision — the
historical submission record is preserved; claim reassigns the owner only)
Router: `claimListing` = moderatorProcedure + inner isModerator recheck + mapOffsiteError.
NO protectedProcedure self-claim endpoint (mod-only is the whole trust boundary).
Schema: `claimListingSchema` (appListingId, positive-int targetUserId, bounded reason).
No schema/migration change — the `claim` action already exists in the merged CHECK.
UI (dark, mods only): a Claim action on the Reports-tab action set + a modal with a
numeric target-user id + reason. View-model (`appListingModerationView`) offers claim
on approved AND removed rows.
Tests (all pass with symlinked deps): +claim service coverage (happy-path
approved/removed, status/kind/target-user guards, TOCTOU, submittedByUserId untouched,
in-tx-primary snapshot, audit correctness, reason floor); router authz (moderator-only,
tester/anon forbidden, INVALID_TARGET_USER→BAD_REQUEST, single claim proc / no self-claim);
view-model claim action-set; e2e claim guard rejections + tester-forbidden (happy path is
unit-covered — a claimable state isn't constructible in preview).
Closes P3b. Activation gate unchanged: all three P3b migrations before `release`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(w13-p3b): thread+resolve reportId on claimListing (mirror delist); render owner transfer in history
Post-audit coherence fixes for W13 P3b PR4 (claimListing):
- claimListing now accepts an optional reportId (schema, same shape/bounds as
delist), links it on the AppListingModerationEvent, and resolves that report in
the SAME tx — listing-scoped + status-guarded, so a mismatched/already-closed
reportId is a silent no-op (the claim still succeeds). Mirrors delistListing
exactly. In the impersonation flow (report -> delist -> claim -> ban) the claim
is the substantive resolution, so it now closes the triggering report instead of
leaving it pending. No migration: reportId column already exists (PR1).
- UI: the Claim modal (always initiated from a report row) passes report.id.
- History modal: renders the claim event's before/after owner transfer
("owner: X -> Y"), guarded to the {userId}-shaped claim payload so it never
mis-reads the {status}-shaped delist/relist/purge/report-* events.
- Tests: +2 (matching reportId resolves+links; cross-listing reportId scoped
no-op) and pin the no-report case (event reportId=null, report untouched).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
6a8580b282 |
W13 P3b PR3 — off-site mod takedown loop: delist/relist/purge/resolve/dismiss [DARK] (#2969)
* W13 P3b PR3 — off-site mod takedown loop (delist/relist/purge/resolve/dismiss) [DARK] The moderator takedown surface for approved off-site AppListings, dark behind `app-blocks-enabled` (mods only). Builds on PR1 (data model + manual migrations) and PR2 (report backend). `claimListing` is deliberately OUT — it lands in a separate PR4. Service (offsite-moderation.service.ts) — 5 mod-only procs, each writes EXACTLY one AppListingModerationEvent in the SAME tx as its mutation: - delistListing: approved→removed (status+kind-guarded updateMany; drops out of the approved-only store read path for free) + optional in-tx resolve of a linked reportId. - relistListing: removed→approved (reversibility). - purgeListing: hard delete — writes the audit event FIRST, THEN deletes the row so the event survives via the ON DELETE SET NULL FK + the denormalized slug snapshot (screenshots/reports cascade). The self-clean primitive. - resolveReport / dismissReport: pending→resolved/dismissed + audit event. - listModerationEvents: per-listing history read (keyset, PII-safe projection). A guarded 0-count rolls the whole tx back BEFORE the audit write (zero events on a guarded/failed mutation). Reuses the PR2 OffsiteModerationError + mapOffsiteError generic-message discipline (no infra leak). Router — delist/relist/purge/resolveReport/dismissReport/listModerationEvents as moderatorProcedure + the inner isModerator recheck; reviewer bound to ctx.user.id. UI — a "Reports" tab on /apps/review (OffsiteReportsQueue): the report queue with per-row Delist/Relist/Purge/Resolve/Dismiss actions (Purge behind a destructive confirm) + a per-listing moderation-history modal. The report-row→action-set + status/action→chip logic is extracted into a pure, unit-tested view-model (appListingModerationView.ts). Tests (58 new, all passing with symlinked deps): - service: delist/relist/purge/resolve/dismiss state transitions + status/kind guards + zero-event-on-guard + purge event-before-delete ordering + audit-event correctness; listModerationEvents keyset + PII-safe projection. - router authz matrix: every mod action FORBIDDEN for a tester, mapOffsiteError typed→code no leak, claimListing absent (PR4). - pure view-model tests + a migration-agreement test (action CHECK ⟺ code tuple). - e2e (preview-apps-external-delist.spec.ts, tRPC-driven, self-cleaning): purge hard-delete end-to-end + report/delist guards on a draft + the mod-only authz matrix. (The approve-success→store→delist round-trip is covered in unit tests, not the preview: the image scanner is unreachable in preview so an off-site listing can't reach `approved` there — purge still self-cleans everything.) 🔴 The AppListing.status CHECK ALTER (adds 'removed') is MANUAL-APPLY per rule #8 — apply to the dev clone before this preview runs and to prod nvme0 before release. Migrations shipped in PR1; the migration-agreement unit tests catch code/DDL drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * W13 P3b PR3 — post-audit fixes: scope delist report-resolve, purge in-tx snapshot + kind guard, framing Post-audit cheap correctness/integrity items (all still DARK): - 🟡 Scope delist's in-tx report-resolve to the delisted listing: add `appListingId` to the report `updateMany` WHERE so a mismatched `reportId` (a pending report for a DIFFERENT listing) matches 0 rows instead of closing an unrelated report. Silent no-op (the delist is the primary action). - 🟡 Purge: read the pre-delete status/slug/kind INSIDE the tx from the PRIMARY (`tx.appListing.findUnique`), not the replica classify, so `before.status` can't be stamped stale under replica lag. Replica classify kept as a fail-fast + info-leak-parity gate. Event-before-delete ordering preserved. - 🟢 Purge deleteMany carries an inline `kind:'offsite'` guard (defense-in-depth symmetry with delist/relist on the destructive op). - 🟢 Framing: soften the "events survive" comments to forensic/row-level only (not retrievable via `listModerationEvents` once the FK is SetNull'd); correct the dark-posture comment to "UI-dark; server gate is moderatorProcedure + isModerator" (enforceAppBlocksFlag would be inert — mods bypass the flag). Tests: +3 in offsite-moderation.service.mod-actions (cross-listing reportId no-op, purge snapshot-from-primary vs stale replica, vanished-between-classify); purge deleteMany kind-guard assertion. PR3 mod-action unit set 58→61 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ab1c907f77 |
feat(app-blocks): W13 P3a PR-b — off-site approve/reject + assertListingAssetsComplete wiring (dark) (#2954)
* feat(app-blocks): W13 P3a — off-site approve/reject + assertListingAssetsComplete wiring (dark)
PR-b of the App Store off-site (external-link) listing flow. Extends
offsite-listing.service (PR-a, on main) with the moderator approve/reject
state machine and ACTIVATES the dark P1 mandatory-asset gate at approve.
Everything stays dark behind app-blocks-author / moderatorProcedure — no UI.
- approveExternalRequest (moderatorProcedure): loads the request + its draft
AppListing, asserts pending, then enforces two gates BEFORE any mutation:
(1) assertListingAssetsComplete(listing) — THE P3 activation: approve FAILS
BAD_REQUEST { missing } unless icon + cover + >=1 screenshot (a screenshot
whose Image was deleted / imageId null does NOT count, mirroring
getListingAssets); (2) re-validates the STORED externalUrl (defense-in-depth,
a non-https stored value blocks approve). Then, in ONE transaction, flips the
request pending->approved (status-guarded TOCTOU), flips the listing
draft->approved (status-guarded), sets reviewedBy*/reviewedAt/approvalNotes,
and supersedes any sibling pending request for the slug (parity w/ the on-site
publish-request approve).
- rejectExternalRequest (moderatorProcedure): requires rejectionReason >=10;
flips the request pending->rejected + reviewedBy*, and DELETES the draft
AppListing (status-guarded deleteMany({id, status:'draft'}) — releases the
slug, can never remove an approved listing).
- Router: wire approveExternalRequest / rejectExternalRequest on the appListings
router as moderatorProcedure; new approveExternalRequestSchema /
rejectExternalRequestSchema (mirror the on-site approve/reject shapes). Service
failures map to BAD_REQUEST with the message (mirrors blocks.approve/reject).
Locked decisions honored:
- Reject deletes the draft (releases the slug); approve flips draft->approved so
the approved-only read path surfaces it in the store.
- v1 ALLOWS mod self-approve (reviewer == submitter) — trusted, enables
single-mod dogfood + the approve e2e. A reviewer!=submitter restriction is
DEFERRED to GA/P3b (noted in-code). Self-approve is NOT blocked.
- No schema/DDL change, no migration; no ...input spread (update data built
explicitly).
Tests: offsite-listing.schema (+8 approve/reject shape), offsite-listing.service
(+17 approve/reject: happy-path, gate-blocked per missing asset + all-present
pass, non-pending, stored-URL re-validation, supersede, TOCTOU, mod self-approve
allowed), router authz matrix (+7: both procs moderatorProcedure, non-mod
FORBIDDEN, gate error -> BAD_REQUEST) — 92 in the three files, all green.
New e2e preview-apps-external-approve.spec.ts (reject path + approve-gate path,
mod, self-cleaning; approve-SUCCESS->store deferred to PR-c since there is no
delete-approved path yet — noted in the spec header). Runs in Tekton
pr-smoke-test, not locally.
PR-c (UI: submit form + kind-aware review queue) and PR-d (#2821 retirement)
follow as separate PRs off main.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(app-blocks): W13 P3a post-audit — primary-gate-in-tx, error mapping, atomic reject
Post-audit hardening of the off-site listing approve/reject flow (PR #2954):
1. Gate on the PRIMARY inside the approve tx. assertListingAssetsComplete
previously read iconId/coverId + the screenshot count via dbRead (replica),
but the sibling asset mutators write to dbWrite — so under replica lag + a
concurrent owner asset-edit the gate could pass on stale-complete state. The
authoritative gate now re-reads iconId/coverId + the imageId-bearing
screenshot count via the tx client (primary), row-consistent with the status
flip; the cheap replica pre-tx check is retained as a fail-fast.
2. Proper error-code mapping in the router approve/reject catch. A new
mapOffsiteError passes shaped TRPCErrors through, maps typed
OffsiteRequestError codes (NOT_FOUND->NOT_FOUND, NOT_OWNED->FORBIDDEN,
NOT_PENDING->BAD_REQUEST), and turns any unexpected infra/Prisma throw into
INTERNAL_SERVER_ERROR with a generic message (raw error kept only on cause) —
replacing the blanket BAD_REQUEST + raw-message that mis-coded typed failures
and leaked infra messages to moderators.
3. Atomic reject. The request flip + draft delete are now wrapped in one
$transaction (parity with approve) so a crash between them can't orphan a
hidden draft listing squatting the slug.
Tests: dbRead/dbWrite split into DISTINCT mocks so the gate's primary reads are
asserted (incl. a replica-lag divergence test where the replica reads complete
but the primary is incomplete -> approve BLOCKED); added typed->proper-code and
untyped->INTERNAL_SERVER_ERROR (no message leak) router tests; added a
reject-atomicity test. 147 related unit tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(app-blocks/offsite): TS2352 in mapOffsiteError — cast Error via {code?:unknown}
Error -> {code:string} is an invalid direct cast (missing required prop);
the line-124 guard already narrows via {code?:unknown} + typeof===string,
so read it the same way. Would fail the Tekton typecheck otherwise.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7c8814d2ca |
feat(app-blocks): W13 P3a — off-site listing submission backend (dark) (#2950)
* feat(app-blocks): W13 P3a off-site listing submission backend (dark) Adds the native external-link off-site app submission flow behind the `app-blocks-author` flag (mods + app-dev-testers). Design B1 (locked): submit creates, in one transaction, a DRAFT AppListing(kind='offsite', status='draft') + a pending AppListingPublishRequest(kind='offsite', appListingId=<draft id>), so the author can reuse the P1 asset CRUD to attach icon/cover/screenshots before a mod approves. The read path hides non-approved rows, so a draft never surfaces in the store. - New offsite-listing.schema.ts: submitExternalListingSchema (name/externalUrl/slug/tagline?/description?/category?/contentRating default 'g'/changelog?), reusing validateExternalUrl + assertNoOnPlatformSurface from external-app.schema (https-only, external vs on-platform mutual exclusivity). - New offsite-listing.service.ts: submitExternalListing (owner-bound, slug-collision pre-check + P2002-race branch, cross-kind block-id check), withdrawExternalRequest (IDOR + TOCTOU status-guarded updateMany, terminal: deletes the draft listing to release the slug), listMySubmissions + read-only mod queue lists (pending/approved/rejected). - Wire procs on the appListings router: submit/withdraw/listMySubmissions as appDeveloperProcedure; the queue lists as moderatorProcedure. - Widen the P1 asset-CRUD flag gate mod->author (enforceAppBlocksAuthorFlag via isAppBlocksAuthorEnabled); the service-layer owner check still bounds each caller to their own listing. backfillAssets stays moderatorProcedure. - Comment-only Prisma update on AppListingPublishRequest.appListingId (B1 sets it at submit) — no schema/DDL change, no migration. Tests: offsite-listing.schema (18), offsite-listing.service (17), router authz matrix (22) — all green. e2e spec authored (submit -> mod queue -> withdraw, self-cleaning; runs in Tekton pr-smoke-test, not locally). Dark: no UI. PR-b (approve/reject + assertListingAssetsComplete), PR-c (UI), and PR-d (#2821 retirement) follow as separate PRs off main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * harden(app-blocks/offsite): rate-limit + pending cap + block_id primary re-check Fold the pre-deploy hardening items from the PR #2950 audit into the P3a off-site submission backend (dark behind app-blocks-author; reachable by non-mod dev-testers via tRPC once deployed): - submitExternalListing: add rateLimit (10/hour) middleware, mirroring the public read procs' rateLimit idiom — throttles draft-spam / slug-squat. - Per-user OUTSTANDING pending-submission cap (MAX_PENDING_OFFSITE_SUBMISSIONS = 10) in the service — bounds standing orphan-draft accrual (drafts have no TTL, only clear on withdraw/reject); at/over cap -> TOO_MANY_REQUESTS. - Cross-kind AppBlock.block_id collision: re-check from the PRIMARY (dbWrite) inside the create tx to close the replica-lag window the constraint-less pre-check leaves open (AppListing.slug is P2002-backstopped; block_id is not). - Re-assert author-declared contentRating against OFFSITE_CONTENT_RATINGS in the service (defense-in-depth, matching the URL/surface/category re-checks; keeps the 'g' default). - offsite-listing.schema: z.ZodIssueCode.custom -> 'custom' (Zod v4 idiom). Tests: +5 service cases (pending cap at/under, block_id primary-recheck path, contentRating re-assert). 61 offsite unit tests green (schema+service+router). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(w13-p3a): re-trigger Tekton preview build * chore(ci): re-trigger preview build (Tekton transient issue resolved) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(app-blocks/offsite): run P3a submit e2e as mod, not the synthetic tester The preview 'tester' fixture (id 2000000002) is in the preview-ACCESS allowlist but NOT the app-blocks-author cohort, so submitExternalListing (appDeveloperProcedure) 403s it by design. Mods are authors via the app-blocks-author mod floor, so run the whole submit->queue->withdraw leg as mod, matching every sibling apps smoke spec (publish/install/marketplace/page). The author-gate rejection is covered by the unit router-authz tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b78a72256a |
Merge pull request #2849 from civitai/feat/seo-improvements
Feat/seo improvements |
||
|
|
3b1902f184 |
fix(lighthouse): make cookie-mint dependency-free (drop jose, build JWE with node:crypto) (#2836)
The PR-preview Lighthouse cookie-mint kept flaking with "Cannot find module 'jose'/'uuid'" -> "cookie mint failed" -> Lighthouse silently skipped, whenever the shared-workspace pnpm install was incomplete. The script ran in the constrained lhci-client image (Node 18, non-root) where a pnpm self-heal can't even run (corepack enable needs /usr/local/bin write). Root-cause fix: remove the last external dependency. The cookie is a next-auth v4 `dir`/A256GCM session JWE — build it directly with Node's built-in node:crypto (createCipheriv aes-256-gcm + the existing HKDF key + randomUUID). The mint now needs zero installed packages, so it can never fail on an incomplete workspace. Verified three ways: - jose.jwtDecrypt round-trips the hand-rolled JWE (so @civitai/auth's decodeLegacySessionCookie accepts it); - runs with NO node_modules under the real patrickhulce/lhci-client:0.15.1 image (Node 18.20.8): exit 0, lighthouserc.runtime.json written; - end-to-end on a live preview: the minted cookie yields a valid authenticated ci-smoke-gold session (/api/auth/session returns the gold user). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
05839641c6 |
chore(layout): remove MatureContentMigrationAlert
Drop the green-domain 'mature content moved to civitai.red' banner: the component file, its AppLayout usage, and the stale test-checklist reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9f148cffd5 |
fix(lighthouse): drop uuid dep from mint script (use crypto.randomUUID) (#2810)
The PR-preview Lighthouse cookie-mint flaked with "Cannot find module 'uuid'" when the shared-workspace pnpm install was incomplete (seen on pr-preview-2806), skipping Lighthouse with a misleading "mint failed". uuid was only used for a random session id — node:crypto.randomUUID() (already importing node:crypto) is a built-in drop-in, removing one external dependency the script can fail to resolve. jose remains the sole external dep (and the pipeline now self-heals it). Verified under the real patrickhulce/lhci-client:0.15.1 image (Node 18.20.8) with only jose available (no uuid): exit 0, lighthouserc.runtime.json written. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
850e7583f0 |
fix(lighthouse): polyfill globalThis.crypto for jose v6 on Node 18 (#2802)
Follow-up to #2797. With jose loaded via dynamic import the next failure surfaced in the Tekton lighthouse task (lhci-client image, Node 18.20.8): mint-cookie failed: crypto is not defined jose v6 uses Web Crypto via the global `crypto`, which Node 18 does not expose as a global (it became global in Node 19+). The mint still fails -> no authed cookie -> Lighthouse skipped on every preview. Polyfill globalThis.crypto from node:crypto.webcrypto when absent (no-op on Node 19+). Verified by running the script in the actual patrickhulce/lhci-client:0.15.1 image (Node 18.20.8): exit 0, lighthouserc.runtime.json written with the extraHeaders cookie. (#2797 was verified on Node 20 locally, which already has the crypto global — that's why this slipped through; tested under the image's real runtime this time.) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
182d58bde3 |
fix(lighthouse): load jose via dynamic import (jose v6 is ESM-only) (#2797)
The PR-preview Lighthouse CI cookie-mint script (tests/lighthouse-mint-cookie.cjs)
required('jose') at the top of a .cjs file. jose has been ESM-only since v6
(package now pins ^6.0.11), so require() throws ERR_REQUIRE_ESM:
Error [ERR_REQUIRE_ESM]: require() of ES Module .../jose/dist/webapi/index.js
... not supported.
The mint fails -> no authed cookie -> lighthouse collect is skipped -> the
lhci-bot comment falls through to "⚠️ no Lighthouse results produced" on every
recent PR. Move the only consumer (EncryptJWT) to a dynamic import() inside the
async main(), which is supported from a CommonJS module. Verified locally: the
script now writes lighthouserc.runtime.json with the extraHeaders cookie.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
0cc030120f |
fix(auth): preview-only legacy-session fallback so preview smoke can authenticate (#2786)
* fix(auth): preview-only legacy-session fallback so preview smoke can authenticate The first-party OAuth cutover (#2712 + follow-ups) moved user resolution off the app DB onto getSessionUserById, which reads the shared session cache then the centralized hub (auth.civitai.com) — both backed by the PRODUCTION identity store. PR previews run against the dev DB clone, where the ci-smoke-* smoke users are seeded (datapacket-talos seed-smoke-test-users CronJob) but the hub has no row for them. So getLegacySession decoded the minted legacy cookie fine, extracted the userId, then getSessionUserById returned null (hub 404) → null session → the _app route guard 307'd every authenticated request to /login → auth.civitai.com → back → ERR_TOO_MANY_REDIRECTS. Result: ALL ~53 authenticated preview smoke tests failed (observed on pr-2781..2784 previews; #2773's smoke was green before the cutover). Production is unaffected — real users resolve via the hub normally. Fix: in getLegacySession, when getSessionUserById misses AND IS_PREVIEW, fall back to the rich `user` embedded in the minted legacy cookie — exactly what the pre-cutover gate did (read token.user straight from the cookie, no DB hit). Gated on IS_PREVIEW so production NEVER trusts the embedded user (it must resolve via the hub); zero production blast radius. Updated the preview-auth.setup.ts header to document the new mechanism. This restores pre-cutover preview behaviour as a stopgap; a longer-term option is to teach the hub/session-client a preview identity source, but that's the migration owner's call. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(preview): repoint anonymous SSR-inject landing /login -> /preview-restricted The anonymous SSR-injection smoke tests (preview-bootstrap, preview-ssr-inject) used ANON_LANDING='/login' — pre-cutover the in-app /login PAGE rendered through _app, so __NEXT_DATA__ carried the SSR-injected browsingSettingsAddons / announcements / getLiveNow. The first-party-OAuth cutover (remove in-app login UI) made /login a server-side redirect to the hub: it no longer renders, so the anon landing seeded nothing → the tests failed (and, with AUTH_JWT_ISSUER unset on the preview, looped). /preview-restricted is the preview gate's OTHER allow-listed exception (resolveAuthGuard: `path !== '/preview-restricted'`) and is a plain page (no custom getServerSideProps) that renders via _app for anyone — anonymous included. Repoint ANON_LANDING there so the same _app SSR-inject path is exercised, keeping the coverage instead of dropping it. (Companion to PR #2786's preview-only legacy-session fallback for the AUTHENTICATED loop, and the datapacket-talos preview ConfigMap getting AUTH_JWT_ISSUER so /login forwards to the hub instead of degrading to / on regular previews.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cd3b824ea9 |
Merge main into monorepo-bootstrap (11 commits)
Same trivial extracted-area pattern: - @civitai/redis: added main's new REDIS_SYS_KEYS.BLOCKS.WITHDRAW_RATE_LIMIT. - src/pages/api/v1/blocks/withdraw.ts (new main file): next-auth -> ~/types/session. Everything else auto-merged: blocks withdraw/dev-token-local-manifest, video controls overhaul, gen OTel spans, civitai-link .red fix. No schema change. Verified: typecheck 0 errors; @civitai/redis 87 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
0de1e5a524 |
test(app-blocks): fix 3 post-merge test failures (consent race, missing mock, renamed selector) (#2773)
Three report-only suites went red on main after today's app-blocks merges. All
three are test issues — none is a product bug; verified each against source.
1) component PageBlockHost.browser.test.tsx — "REQUEST_CONSENT opens the consent
dialog". driveToReady() only guarantees the DOM `data-block-ready` attribute,
but the host's message-gate state can lag it a tick, so the single fire-once
REQUEST_CONSENT could land while the gate still reads "not ready", get dropped
(cf. the "before BLOCK_READY is dropped" test), and never retry → dialog never
opens (a 10s CI-contention flake; yesterday's vi.waitFor timeout bump only
delayed the failure). Fix: re-post REQUEST_CONSENT INSIDE the waitFor, mirroring
driveToReady's BLOCK_READY retry. waitFor exits on first success so exactly one
post opens exactly one dialog. Verified 23/23.
2) unit appBlockReview.reward.test.ts — the ~/server/prom/client mock omitted
`clickhouseFailSoftCounter`, which base.reward.ts increments on the ClickHouse
fail-soft path → `.inc` on undefined rejected, failing the "insert throw does
NOT propagate" test. Added the missing counter to the mock (sibling
base.reward.failsoft.test.ts already mocks it). Verified 7/7.
3) smoke preview-apps-page.spec.ts — asserted a `getByRole('button', { name:
'App block menu' })` that #2767 renamed to `aria-label="App menu"` in
IframeHost.tsx; the component test was updated, the smoke spec was not. Updated
the selector (+ the two doc-comment references). The button renders fine — pure
test/UI desync. (Can't run the smoke spec locally; matched against
IframeHost.tsx:274 + AppBlockChrome.browser.test.tsx which asserts 'App menu'.)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
8b8b01d32b |
Merge main into monorepo-bootstrap (26 commits)
Single conflict in the extracted prom area: - @civitai/telemetry: added main's new clickhouseFailSoftCounter (ClickHouse transient transport-error fail-soft). App prom shim re-exports it via `export *` to the app call sites (not a redis-bridge counter). Everything else auto-merged: @civitai/client beta.74->75, clickhouse/meili transient-error 503 reclassification, Krea 2 LoRA training, app-blocks UI. No schema change, no new next-auth files. Verified: typecheck 0 errors (app + packages). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7ebad80173 |
test(ci): fix failing unit + component + smoke suites on main (#2762)
* test(ci): fix failing unit + smoke suites (flaky perf budgets, cold-import timeouts, stale heading)
Three CI suites were red on recent PRs. Root-caused and fixed each
(no skips, no disabled tests, no loosened correctness assertions).
Unit suite (`pnpm test:unit:run`):
- ReDoS perf guards (audit-redos / audit-cjk-redos / audit-gate-perf)
asserted absolute wall-clock budgets (`expect(ms).toBeLessThan(100)`)
on a single regex call over a long input. That measures CPU speed, not
code: the same (provably LINEAR) code ran ~15ms on a fast runner and
~300-560ms on a loaded one, flaking PASS->FAIL purely on hardware. The
real invariant is asymptotic — the `{0,200}` gap bound + zero-width word
boundaries made the audit O(n), not the pre-fix O(n^2)/exponential
(11-84s of synchronous CPU = user-triggerable DoS). Replaced the
hardware-coupled budgets with `expectSubQuadraticScaling` (new shared
`redos-perf-helpers.ts`): a large-N absolute ceiling with an
order-of-magnitude margin over linear but far below the known quadratic
cost, plus a small->large scaling-ratio check (skipped below the timer
noise floor). Hardware-independent, and STRICTER against a reintroduced
ReDoS than the old budgets. All correctness assertions kept verbatim.
(Verified linear empirically: includesMinor 8.6/16/19/47/96ms at
N=2k/4k/8k/16k/32k.)
- Several tests cold-`await import(...)` a large Next API-page / service
module graph (mocked I/O, but a real ~9-16s TS transform). Under a
saturated worker pool that legitimate transform raced for CPU and
overran the 10s global timeout, cascading the next test into a
half-loaded-module assertion. Hoisted the heavy import into `beforeAll`
(paid once, off the per-test budget) for model.service +
/api/v1/models, gave the single-test catalog-cors-wiring an explicit
timeout, and raised the global unit `testTimeout` 10s->60s (these are
mocked-I/O tests; nothing should legitimately approach a minute).
Smoke suite (`tests/preview-apps-marketplace.spec.ts:90`):
- Asserted a `getByRole('heading', { name: 'Civitai App Blocks' })` that
the app-blocks nav refactor (#2749/#2758) DELIBERATELY removed —
`AppsPageLayout` now omits the title on the marketplace surface. Updated
the stale assertion to the always-on `AppsSubNav` "Marketplace" tab
(`getByRole('tab', { name: 'Marketplace' })`), which uniquely identifies
the rendered apps surface (proves the appBlocks SSR gate cleared, not a
404). Selector validated against AppsSubNav.tsx and the passing
AppsSubNav component test's real DOM (`<a href role="tab">`).
Component suite (`pnpm test:component`): green on this tree (26 files /
264 tests) against the current source incl. the nav refactor — no
component test references the removed heading; no change needed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(component): fix CliSubmitCta copy tests failing in CI headless Chromium
The component suite failed on recent PRs (preview / component-tests) on
src/components/Apps/CliSubmitCta.browser.test.tsx — the two copy tests that
assert the "Copied" affordance renders after activating the copy button.
Root cause: Mantine's `useClipboard` calls `navigator.clipboard.writeText`.
In CI's headless Chromium the page is an insecure context with no clipboard
permission, so the real `writeText` rejects — `copied` never flips and "Copied"
never renders. The tests passed locally only because a desktop Chromium grants
the permission. Reproduced the exact CI failure by forcing `writeText` to reject
(same "Cannot find element: getByText('Copied')" error).
Fix: stub a resolving `navigator.clipboard` in the shared browser-mode setup
(test/component-setup.tsx) so copy behaviour is deterministic and matches a real
secure-context browser, independent of the CI Chromium's permission state. The
tests assert the "Copied" UI state, not OS clipboard contents. Verified: copy
tests now pass (8/8, 751ms vs the prior 2x15s timeouts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(slow-log): drain emit tail deterministically (fix CI flake on loaded box)
CI unit suite failed on 2 tests: trpc-slow-log.test.ts:66 and
audit-slow-log.test.ts:74 — both `expect(logToAxiom).toHaveBeenCalledTimes(1)`
got 0. They passed locally but fail on the saturated CI box.
Root cause: emitSlowLog in both modules is a fire-and-forget async tail that
`await import('~/server/logging/client')` (audit also awaits node:crypto) before
calling logToAxiom. The tests drained it with a FIXED ~25ms wall-clock flush
(5x setTimeout(5ms)). On a loaded CI runner (this run: transform 467s / import
1189s) the dynamic import resolves slower than 25ms, so logToAxiom hasn't been
called when the assertion runs → 0 calls. Pure timeout-vs-load race.
Fix: track the in-flight emit promise in a module-level Set and expose a
`__flushPendingEmitsForTest()` hook (consistent with the existing __reset* /
__rateGate* test hooks). The tests' `flush()` now awaits the actual emit instead
of a fixed timeout — deterministic regardless of CI load. Production behaviour is
unchanged: still fire-and-forget; the Set entry is removed on settle (no
retention). Verified: 29/29 pass, trpc file in 24ms.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(redos): stop the scaling-ratio guard flaking on a contended CI box
The unit suite still had 1 failure on CI: audit-redos.test.ts:182 —
`includesMinor "young"+run: cost scaled 11.99x when input grew 4x` (ratio
ceiling 8x). The op is genuinely linear (9.3ms→111.7ms) and the PRIMARY absolute
guard passed (111ms ≪ 1500ms); only the SECONDARY scaling-ratio check tripped.
Root cause: at factor=4 the ratio ceiling (8x) sits between linear (4x) and
quadratic (16x) with too little margin, and the baseline (9.3ms) was barely above
the 8ms noise floor. On a shared/contended CI core, contention adds variable
absolute ms to each short measurement independently, so a ~10ms baseline against a
contention-inflated large reads as a false 12x — pure measurement noise, not a
ReDoS.
Fix (redos-perf-helpers.ts):
- Raise NOISE_FLOOR_MS 8→40 so the ratio is skipped when the baseline is too
small to be stable. The absolute LARGE_N (1500ms) + HANG (5s) ceilings carry
correctness in that regime (the documented design).
- Widen the ratio ceiling: quadraticGuard 2→3.5 (ceiling 8→14), giving linear
work 3.5x slack over its expected 4x while still tripping a true O(n²) (≥16x).
The absolute large-N guard remains the load-bearing ReDoS check (linear ~100ms,
removed O(n²) ~2800ms+); the ratio is now a secondary clearly-egregious backstop.
Verified: all 3 ReDoS suites green (27/27).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(unit): kill remaining CI-contention flakes (redos absolute ceiling, cold-import hook timeouts)
The unit suite on the preview CI box (this run: transform 467s / import 1189s —
extreme worker-pool contention) surfaced two more load-sensitive failures:
1) audit-redos.test.ts — the ratio fix worked (ratio read ~4x = linear), but the
PRIMARY absolute guard then tripped: genuinely-linear work measured 1900ms,
over the 1500ms LARGE_N_LINEAR_CEILING. A single absolute wall-clock ceiling
can't be both tight-enough-to-catch-a-mild-quadratic and loose-enough-to-never-
flake on this box. Fix (redos-perf-helpers.ts): split into two regimes —
- reliable-ratio regime (both medians ≥ 40ms noise floor, i.e. a slow/contended
box): the SHAPE ratio is authoritative (hardware-independent) + only the
generous 5s HANG ceiling as an absolute backstop.
- fast-box regime (baseline below the floor, ratio noise-dominated): the tight
1500ms ceiling, which has enormous margin on a fast box.
Both regimes catch a reintroduced O(n²); neither false-trips on linear.
2) file-download-lookup.test.ts — `Test timed out in 30000ms`. The describe had a
`{ timeout: 30000 }` override (below the 60s global) and each test did a lazy
`await import('../file.service')` — a heavy cold module graph that exceeds 30s
under contention. Fix: hoist the import into beforeAll (paid once, 60s hook
timeout), drop the tight override, tests use the warm reference.
3) vitest.config.mts — add `hookTimeout: 60000` to the unit project. testTimeout
was already 60s but hookTimeout defaulted to 10s, so any beforeAll/beforeEach
cold-import (file-download-lookup, listForModel.behavior, ...) would flake the
HOOK instead of the test. One-line global hedge for the whole class.
Verified locally: redos suites + file-download-lookup green (31/31).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(component): raise vi.waitFor default timeout to absorb CI contention
PageBlockHost.browser.test.tsx flaked on the preview CI box: "after BLOCK_READY,
REQUEST_CONSENT opens the consent dialog" — `expected [] to have length 1`. The
test already wraps the assertion in vi.waitFor (correct), but vi.waitFor defaults
to a 1000ms timeout, and on the saturated box (browser tests share the host with
the image build) the postMessage→consent-dialog round-trip exceeded 1s, so the
waitFor expired → empty dialog set → PASS→FAIL on load, not code.
The browser suite has ~80 vi.waitFor sites and NONE pass an explicit timeout, so
any of them awaiting an async round-trip (postMessage, tRPC settle, zustand
update) is a latent 1000ms-vs-contention flake. Rather than edit every call site,
raise the DEFAULT globally in the shared browser setup (test/component-setup.tsx):
wrap vi.waitFor so a call that omits `timeout` gets 10s; calls that pass their own
timeout (e.g. the 10s/15s terminal-path tests) are untouched. One root-cause fix
for the whole class. Verified: PageBlockHost + CliSubmitCta green (20/20).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
3d5934b9b6 |
Merge main into monorepo-bootstrap (231 commits)
Reconcile main with the package-extraction branch. 8 conflicts resolved by keeping the @civitai/* package shims and porting main's NEW logic into the packages (not the app shims): - prisma enums/models: re-export shims; regenerated from merged schema (Model3D*) - package.json/pnpm-lock: union scripts; @civitai/client beta.71->73, +three - @civitai/axiom: logToAxiom stderr-before-guards reordering (test relocated here) - @civitai/telemetry: redisSelfHealReconnect + redisMetricWriteFailSoft counters - clickhouse tracker.ts: Tracker.view() context fields + new blockRender() - @civitai/redis: ported main's cluster self-heal + packed-compression subsystem (cluster-selfheal/inflight/deadline-hits/packed-compression moved into the package, deadline.ts + 8 REDIS_CLUSTER_SELFHEAL_* env vars wired, metric bridge) - search services: dropped next-auth import -> ~/types/session SessionUser Verified: typecheck 0 errors (app+packages); redis(53)/axiom(3)/app redis+logging(78) tests pass; two independent review agents confirmed no lost main functionality and a faithful (9/9 invariants) redis-resilience port. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
15416aa0d3 |
fix(test): assert production-safe selectors in preview-apps-page smoke spec (#2706)
The W10 full-page App Blocks smoke spec failed on every preview at
`getByTestId('app-block-chrome')` ("element(s) not found"). Root cause is NOT a
product bug — the page renders correctly (verified against a live preview: the
AppBlockChrome trust bar shows the app icon + "Gen Matrix" name + "App block
menu" button, and the block iframe mounts pointing at <slug>.civit.ai with a
successfully-minted page token).
The spec was authored assuming `data-testid` survives into the preview, but a
preview is a PRODUCTION Next build and next.config.mjs strips every data-testid
in production (`compiler.reactRemoveProperties: { properties: ['^data-testid$'] }`).
So `getByTestId('app-page-iframe' | 'app-block-chrome' | 'app-page-frame')` can
never match a deployed preview. This spec was the ONLY preview smoke spec using
getByTestId; the siblings assert via tRPC + ARIA roles precisely because of this.
Rewrite to assert on attributes the production build KEEPS:
- trust chrome → getByRole('button', { name: 'App block menu' })
- iframe mount → iframe[data-block-instance-id^="page_"] (non-testid data-*
attrs are not stripped) + its src is the resolved block origin
- page-mint → exercise POST /api/v1/block-tokens (entityType:'none',
page_<appBlockId>) and assert 200 + non-empty JWT — the host-side
prerequisite for BLOCK_INIT, fully provable on a preview.
Drop the `data-block-ready === 'true'` (BLOCK_READY ack) assertion: it flips only
when the cross-origin block bundle at <slug>.civit.ai accepts BLOCK_INIT, which
requires the ephemeral preview origin (pr-N.civitaic.com) to be in the block's
own allowedParentOrigins allowlist. Production blocks allowlist civitai.com /
*.civit.ai, never a preview origin, so that ack can never arrive on a preview no
matter how correct civitai-web is — asserting it made the spec un-passable by
construction. The BLOCK_READY round-trip stays covered by
PageBlockHost.browser.test.tsx (stubbed block).
Verified: passes against the live pr-2700 preview (chrome + iframe + 200 token).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
b751ebcd3e |
revert(api): drop the /api/user/settings notif+chat-unread fan-out (SSR 9s-timeout regression) (#2699)
* Revert "perf(api): SSR-seed chat.getUnreadCount to cut ~19 req/s off api-primary (#2691)" This reverts commit |
||
|
|
86eab5d1d2 |
perf(api): SSR-inject user.checkNotifications to cut ~21 req/s off api-primary (#2690)
* perf(api): SSR-inject user.checkNotifications to cut ~21 req/s off api-primary SSR-seed the header notification-bell unread count so the ambient `user.checkNotifications` client query never fires on bootstrap, removing ~21 req/s of full non-batched tRPC middleware + context-build + superjson cycles from `civitai-dp-prod-api-primary`. This is Tier-1 #6 in the datapacket-talos analysis `claudedocs/api-primary-trpc-cost-analysis-2026-06-21.md`; mirrors the just-merged #2683 (system.getLiveNow) and #2471 (following/getSettings) pattern. The cost is the per-request fixed middleware cycle x21/s (the CPU lever), not the already-redis-cached resolver. Cutting the bootstrap call removes one full fixed-cost cycle per logged-in page load. How (the #2471 server-endpoint pattern, so NO server-only module leaks into the client bundle): - Extract the resolver's reduce into a shared `getUserNotificationCounts` (notification.service.ts) so BOTH `user.checkNotifications` and the SSR-seed path produce a byte-identical `{ all, <category>: count }` object. The controller now calls it directly (public tRPC type unchanged). - Compute the count in the `/api/user/settings` self-fetch `_app` already makes (alongside the existing `following` seed) — authed-only, `.catch(() => undefined)` so a redis/DB blip degrades to no-seed (client self-heals) and can never reject the Promise.all and drop the critical settings/session payload. - Thread `notificationCounts` through `_app` pageProps -> AppProvider, which seeds `trpc.user.checkNotifications.useQuery(undefined, { initialData, enabled: !!notificationCounts, staleTime: Infinity })` on the fixed `undefined` key. Live-count freshness preserved: the seed REPLACES the one-shot bootstrap fetch only. The consumer hook already uses `staleTime: Infinity` (no time-poll today either); the count stays live via the `NotificationNew` SignalR push + the mark-read optimistic `setData`, both of which apply on top of the cache (seed or fetched) exactly as before — not made stickier than today. Byte-equality: payload is a plain object of numbers (no Date/array/undefined field), so the JSON seed and the live `result.data.json` compare directly with no superjson undefined->null divergence; an absent category is absent on both sides. The shared reduce is the single source of truth backing both. Tests: extends `tests/preview-ssr-inject.spec.ts` with an authed describe block asserting (a) the seed is present in `__NEXT_DATA__.props.pageProps`, (b) no `user.checkNotifications` request fires on bootstrap, and (c) the seed deep-equals a live authed fetch (fetched from within the page, not page.request). Authed-only (protectedProcedure → never fires anon, like getFollowingUsers). These run under playwright.preview.config.ts (need a live preview). Verification: - tsc: zero type errors in any of the changed lines (the repo's pre-existing repo-wide implicit-any / Prisma.sql errors are untouched and unrelated). - Full `next build` (bundle-leak check): INCONCLUSIVE locally — Turbopack rejects the worktree's symlinked node_modules ("points out of the filesystem root") and the --webpack fallback OOMs the Node heap (same blockers #2683 hit). No module-resolution/leak error surfaced before those infra failures. The two new imports into client-bundled files are `import type` only (erased at compile); the runtime value flows through the existing `/api/user/settings` self-fetch, so the client-bundle surface is structurally identical to the in-production `following` seed (#2471). Preview-build CI is the gate. Behavior-preserving; dark-safe (cache seed only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(notifications): type checkNotifications setData updaters to UserNotificationCounts The SSR-seed PR narrowed the `user.checkNotifications` resolver to return the shared `getUserNotificationCounts` (explicitly `UserNotificationCounts = Record<Lowercase<NotificationCategory> | 'all', number>`). That made the query's `setData` `Updater` data type strictly `UserNotificationCounts | undefined`, so the two optimistic updaters' `Record<string, number>` returns no longer type-checked (TS2345 at the old 111/210). Keep the `Record<string, number>` working buffer (needed for the dynamic string-key indexing the body does) and return it typed as `UserNotificationCounts` — the buffer is `...old` plus a guaranteed `all`, only mutated numerically, so it satisfies the shape. No runtime behavior change. notifications.utils.ts is now free of TS2345 (only the pre-existing TS7006 implicit-any baseline in the untouched announcements helper remains). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b0f2845f0f |
perf(api): SSR-inject system.getLiveNow to cut ~26 req/s off api-primary (#2683)
`system.getLiveNow` is a global boolean (a single `redis.get(LIVE_NOW)`, identical for every user) fired ambiently on every bootstrap via the `useIsLive` hook (header logo, social links, social home block). At ~26 req/s on api-primary it pays the full non-batched tRPC middleware + context-build + superjson cycle for a boolean — pure per-request fixed CPU cost, the diffuse lever behind the recurring 504 waves (see datapacket-talos claudedocs/api-primary-trpc-cost-analysis-2026-06-21.md, Tier-1 #1). Mirror the proven #2464 SSR-inject pattern: compute the value in `_app.getInitialProps` (from the SAME already-imported `system-cache` module that already calls `getBrowsingSettingAddons` — no new client-bundle surface), thread it through pageProps, and seed it as `initialData` on the fixed `undefined` query key in AppProvider (alongside the existing getSettings / getFollowingUsers seeds). The ambient `useIsLive` query then reads a primed cache and never fires on bootstrap; its own 5-minute refetchInterval keeps it current once a consumer mounts. Fail-open to `false` (not-live) in getInitialProps: getLiveNow has no internal try/catch, and an uncaught redis throw on the SSR path would 500 every render. Extends tests/preview-ssr-inject.spec.ts with authed + anon describe blocks asserting (a) the seed is present in __NEXT_DATA__, (b) no system.getLiveNow request fires on bootstrap, and (c) the seed byte-equals a live authed fetch. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
330d9b2ec8 |
Merge origin/main into monorepo-bootstrap
Reconciles 116 commits of main into the package-extracted + next-auth-free branch. Notable conflict resolutions (full detail below): PACKAGE PORTS (main evolved inline; ported into the @civitai/* packages): - @civitai/redis: ported main's sysRedis HA migration + socket hardening that postdated our extraction — Sentinel client branch, per-kind socketTimeout, commandsQueueMaxLength + disableOfflineQueue (system), pingInterval clamp, cluster-command deadline (#2611), the 8 new env vars + sentinel superRefine, per-command instrumentation + attachSysSentinelListeners, and a UNION of the REDIS_KEYS/SYS_KEYS additions (TENSOR_METADATA, RATING_REVIEW_RATE_LIMIT, SUBMIT_RATE_LIMIT). atomic.ts (hSetWithTTL) merged cleanly (self-contained). Added package tests (env/deadline/sentinel, 13). - @civitai/clickhouse: ported main's Tracker additions into app-side tracker.ts — provenance + setProvenance, the provenance threading into 9 content events, and article()/articleRatingReview()/articleRatingReviewResolved(). - @civitai/telemetry: ported 8 prom counters main added (block spend/subscription attribution, cache fail-open, redis inflight/duration, sentinel topology/errors) + the registerSysredisCounter helper; the prom shim publishes the __civitaiRedisMetrics globalThis bridge the redis package reads. - @civitai/db-schema: generated models/enums/kysely regenerated from the merged prisma schema. TOOK OURS (architecture is authoritative): get-server-auth-session.ts (main's only additions were next-auth-specific workarounds, now obsolete), the redis/ prom/clickhouse/prisma app shims. DELETIONS KEPT: token-refresh.ts, token-tracking.ts (+ orphaned test) — the next-auth strip; their only importers were the already-deleted next-auth files. NEXT-AUTH FOLLOW-UPS on newly-merged main files: redirected two type imports (blocks/submit-version, model-files/tensor-metadata) from 'next-auth' to '~/types/session'; rewired AppBlocks PageBlockHost REQUEST_SIGN_IN from the deleted in-page LoginModal to the hub-driven openLoginPopup (+ updated its browser test). OTHER: tos.md/tos.green.md kept main's lastmod; event-engine-common submodule advanced to main's commit (strictly ahead). Verified: full typecheck clean; @civitai/redis (13) + @civitai/auth (124) + ban-session-revocation regression (7) green; four independent review subagents confirmed the ports faithful. (Two pre-existing vitest failures remain — workspace @civitai/* subpaths aren't resolvable in the root vitest config — and component browser tests can't launch here due to a Playwright build skew; neither is a merge regression.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4b17fc88ad |
test(auth): tier-3 e2e specs for the cutover + unblock the hub harness
Runtime (Playwright) complements to the unit layer. Both target DEPLOYED environments (PREVIEW_URL / HUB_URL); validated locally via discovery + types. - tests/preview-auth-guard.spec.ts (main-app preview harness): the _app moderator guard bounces a gate-passing non-mod (tester/gold) off /moderator, admits a mod (control), and /login forwards to the hub threading returnUrl. Covers the tier-1 _app guard + /login cutover at runtime. - apps/auth/e2e/hub-login.spec.ts (hub harness): GET /login/<provider> 302s to the upstream authorize URL with code flow + scope + PKCE (S256) + state + /callback redirect_uri. Runtime complement to the buildAuthorizeUrl unit test. Fix: hub-auth.setup.ts imported sessionCookieName from @civitai/auth, whose package main is raw TS with no "type":"module" — Playwright's ESM loader can't extract a named export from it, and as a project dependency that import broke the ENTIRE hub suite (hub-smoke included). Derive the cookie name locally instead (mirrors preview-auth.setup.ts), so the suite can list + run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
c142c37782 |
refactor(auth): move auth guards to the Node layer; drop next-auth dependency
The edge middleware decoded the session via next-auth getToken, which only resolves legacy cookies — not the thin hub civ-token (no embedded user; edge can't hit Redis/DB). Move the session-based guards to the Node layer and remove next-auth entirely. - /moderator + /testing page guards and the preview-deploy gate now run in _app getInitialProps with the resolved session (preview Flipt check extracted to server/auth/preview-access.ts). Catches direct/SSR loads; non-mods can't client-nav there and the tRPC procedures are the real data gate. - Strip getToken from middleware/index.ts; drop user/useSession from the Middleware type; route-guards keeps only the sessionless /api/testing gate. - Delete preview-auth.middleware + next-auth.d.ts; repoint middleware.trpc's ExtendedUser -> first-party SessionUser (a superset). Drop next-auth + @next-auth/prisma-adapter — next-auth is fully removed. - Preview e2e minters mint the legacy JWE via jose (mirrors @civitai/auth's legacy-cookie decoder) instead of next-auth/jwt. Needs a QA pass before release (auth-critical, not runtime-tested). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6d918e4a96 |
feat(app-blocks): full-page apps (W10) on a slot-registry foundation — DARK (#2606)
* feat(app-blocks): full-page apps (W10) on a slot-registry foundation — DARK
Builds the durable W10 full-page App Blocks surface on a clean slot-registry
foundation, fully dark behind two flags so merging changes nothing
user-visible. Combines PR1 (slot registry) + PR2 (entity-aware mint) + PR4
(page route) from the W10 plan; PR3 (install/resolver polymorphism + migration)
is deferred — a page is STATELESS (no install row, no migration).
Foundation (PR1, behavior-preserving):
- New src/shared/constants/slot-registry.ts: SlotDef + SLOT_REGISTRY holding
today's 3 model slots + one new entity=none `app.page` slot. KNOWN_SLOT_IDS
re-exported from the registry under the same name (model-only enum), so the
reuse sites in blocks.router.ts are byte-identical. Marketplace slot filter
and client unions derive from the registry. Regression-locked: KNOWN_SLOT_IDS
.options deep-equals the historical model-slot tuple.
Entity-aware mint + binding (PR2, security-sensitive, inert):
- block-tokens mint generalized to an entity discriminator. MODEL path is
byte-identical (ctx = { modelId, slotId }, no entityType added). PAGE path
(entity=none): resolves a synthetic page_<appBlockId> from the approved
AppBlock, mints a viewer-scoped token with ctx = { slotId, entityType:'none' }
(no modelId — can never satisfy a model-bound check), gated on the new
appBlocksPages flag, page-slot-only, page_ instance-id-only.
- HARD RULE: a page mint rejects ai:write:budgeted / buzz:read:self /
social:tip:self (belt over the manifest + approved-scope intersection).
- BlockSlot remount key generalized to ${slotId}:${entityType}:${entityId}
(slotRemountKey) — preserves the exact model remount-on-nav behavior (H-4).
W10 page route + host (PR4, dark):
- New src/pages/apps/run/[slug]/[[...path]].tsx — SSR gates on
appBlocks && appBlocksPages FIRST (fail-closed notFound), resolves the
approved page app by slug (== block_id), renders a full-bleed PageBlockHost
under the W7 trust chrome with subPath deep-linking (NAVIGATE + ROUTE_CHANGED,
shallow-routed, traversal-guarded).
- New PageBlockHost.tsx — separate from the model IframeHost (kept untouched) so
the model path is not destabilized; reuses usePostMessage / IframeInitController
/ AppBlockChrome / intersectSandbox.
Manifest + discovery:
- Manifest validator: validates page:{path,title,icon?} AND closes the
pre-existing gap by validating targets[].slotId ∈ KNOWN_SLOT_IDS.
- Public manifest projection surfaces a hasPage boolean (no internals); "Open
app" affordance on /apps cards + the detail page, flag-gated.
Flag: new dark appBlocksPages (fliptKey app-blocks-pages-enabled,
availability ['mod']). Both flags off → fully inert.
Tests: slot-registry regression lock; entity-agnostic remount key preserves
model behavior; manifest validator accepts page + rejects unknown slotId; page
mint carries no money scopes, is flag-gated + approved-only + stateless (no
subscription row); model token claims byte-identical for generate-from-model.
Plus a preview e2e spec (tests/preview-apps-page.spec.ts) driving a mod opening
/apps/run/<slug> and asserting the iframe mounts + receives BLOCK_INIT — runs on
the PR preview (needs the flag relaxed there). Full existing AppBlocks corpus
stays green (220 unit tests pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: clarify block_id is globally unique (audit nit)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(app-blocks): W10 page-host audit fixes — authoritative trust tier + real granted scopes + terminal fallback (DARK)
Addresses the should-fix audit findings on the W10 full-page-apps substrate
(PR #2606). Feature stays DARK (no flag changes); model render path untouched.
#2 (security): resolvePageBlockBySlug now sources the iframe sandbox's trust
tier from the authoritative, mod-controlled `AppBlock.trustTier` COLUMN, not
the publisher-self-declared `manifest.trustTier` (which reintroduced the C1
trust-tier self-escalation class for the page sandbox). Mirrors the model
render path's column-is-authoritative treatment. Column value wins (e.g.
column='unverified' over manifest='internal' → restrictive sandbox).
#3/#6 (functional): PageBlockHost stops hardcoding `scopes: []` in BLOCK_INIT /
TOKEN_REFRESH / REQUEST_TOKEN. The page route now threads the page manifest's
declared scopes + the mint's missingScopes/needsConsent/error into the host,
which advertises the REAL granted set (declared − missing) the JWT carries
(apps:storage:*). A hard mint error surfaces an `error` terminal state instead
of hanging at no_token.
#4 (UX): terminal states (timeout/fatal/no_token/error) render a BlockFallback
message inside the page frame instead of a blank viewport (full-page surface
collapsing to null is just an empty screen). Anti-spoof posture preserved —
the message is host chrome, not the block.
#5 (partial): page-visibility SUSPEND/RESUME listener with cleanup (mirrors
IframeHost). REQUEST_SIGN_IN deliberately left for the flag-widen.
#7 (nit): sandbox extracted independently of `iframe.src` being a string.
Pure logic extracted to pageBlockHostLogic.ts (grantedPageScopes,
pageFallbackReason) for node-vitest coverage, mirroring the hostRenderDecision
pattern. New tests: block-registry.resolve-page (trust-tier column authority,
sandbox/#7, scope surfacing) + pageBlockHostLogic (granted scopes ≠ [], terminal
→ fallback). Behavior-preserving corpus (oldBehaviorProof, slotRemountKey,
slot-registry, page-mint, block-token, block-manifest-validator,
block-scope.middleware, iframeInitController) all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
9f71df785f |
fix(tos): trigger ToS re-accept on content hash, not lastmod date
The ToS modal re-prompted on every `lastmod` frontmatter bump even when the terms body was unchanged (an unrelated PR bumped the date and forced a global re-accept), and conversely could MISS a real body change if `lastmod` wasn't bumped. Switch the trigger to a sha256 of the ToS body (frontmatter excluded), so it fires iff the terms text actually changes. - content.service: hash the gray-matter body in getStaticContent; replace the per-user checkTosUpdate resolver with static per-domain getTosMeta (hash + rollout baseline + settings field keys). - Remove the content.checkTosUpdate tRPC route. The decision is now computed client-side in useToSUpdateModal from the SSR-seeded user.getSettings against the static tosMeta delivered via pageProps -- one fewer per-bootstrap query. - Pure-hash check: (storedHash ?? baselineHash) !== currentHash. Users with no stored hash default to the hardcoded rollout baseline, so existing users are credited with the rollout text WITHOUT a data backfill and are only re-prompted once the body changes. First accept records a real per-domain hash. - Persist the accepted hash on accept (TosModal) and at onboarding (TOS/RedTOS). Date fields are kept as a write-only acceptance-timestamp audit trail but no longer drive the trigger. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
99a82aa8ca |
feat(app-blocks): git-push authoring on-ramp + self-service Forgejo credentials (Phase 3) (#2587)
* feat(app-blocks): approve push-originated requests from Forgejo (Phase 3 core) The load-bearing core of the git-push authoring on-ramp. When a developer git-pushes to civitai-apps/<slug>, the webhook parks a pending publish request with EMPTY bundle pointers (bundleKey=''), but approveRequest called fetchBundleBuffer(bundleKey) unconditionally → it crashed on approve, so a push could be parked but never approved/deployed. approveRequest's bundle source is now source-agnostic: bundleKey → MinIO ZIP (unchanged); else forgejoCommitSha → reconstruct the bundle from the Forgejo repo at the pushed sha; else throw. Everything downstream (platform-owned filter, committed-manifest rewrite, screenshots, commit, sha stamp, build trigger, Phase-2 deploy-state) is unchanged. New `reconstructBundleFromForgejo(slug, ref)` builds a deterministic ZIP from listRepoTreeAtRef + getBlobContent; backfillPublishRequest refactored onto it. Added `listRepoTreeAtRef(slug, ref, org)` (resolves a commit sha via the git trees endpoint; listRepoTree delegates to it, no caller-signature change). No change to the no-trust-on-push deploy gate — pushes still never deploy without mod approval. Tests: push-path approve (reconstructs from Forgejo, no S3 GET, commits, stamps sha, triggers build, deploy_state=building) + ZIP-path regression + reconstruct-helper determinism. Blocks suite 319/319 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app-blocks): per-developer Forgejo identity + getMyAppRepo (Phase 3 self-service) Lazily provisions each civitai user a scoped, restricted Forgejo identity the first time they request git access to their app, and exposes the clone URL + push credential to the app owner. - Schema: new app_dev_forgejo_identity table (1:1 with userId) storing the Forgejo username + AES-256-GCM-encrypted PAT (keyed on NEXTAUTH_SECRET). Additive migration (manual apply, prod + dev clone). - forgejo.service: createForgejoUser (POST /admin/users, restricted+private, idempotent), getForgejoUser, mintForgejoUserToken (HTTP-Basic as the user — gitea requires the user's own creds; scope write:repository), deleteForgejoUser. - dev-git-access.service: ensureForgejoIdentity — read-or-provision, made CONCURRENCY-SAFE via a DB CLAIM (insert a placeholder row; the userId PK lets one caller own provisioning while the rest wait for the token) — pooler-safe (no advisory locks) and non-destructive. Claim is rolled back on failure. Fixes a race in the first cut where two concurrent first-provisions could purge the user mid-mint and persist a dead token. - blocks.router: getMyAppRepo (protectedProcedure + flag, OWNER-gated) → provisions identity + addCollaborator(write) on this app's repo + returns the authed clone URL + push instructions. Not-approved apps return a "first version is ZIP-only" shape. Isolation: restricted Forgejo user, write only on its own civitai-apps/<slug> repo(s); a push parks a pending review request and CANNOT deploy without mod approval (no-trust-on-push gate unchanged). Tests: forgejo APIs, the claim-first provisioning incl. owner-wait + rollback + orphan edge, owner gate. Blocks suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app-blocks): "Author via git" UI + getMyAppRepo e2e (Phase 3) Developer-facing surface for the git-push on-ramp + its preview e2e. - AuthorViaGit.tsx: an "Author via git" panel under each approved submission the user owns on /apps/my-submissions. Mount-on-expand — getMyAppRepo is only called when the user clicks (it provisions a Forgejo identity as a side effect, so it must be user-initiated, never on page load; query is staleTime/gcTime 0). Clone URL is credential-MASKED by default (maskCloneUrlCredential, unit-tested) with a reveal toggle; copy buttons copy the real value. Note: "first version is ZIP; new versions via git push; pushes go to mod review, never auto-deploy." Non-owner FORBIDDEN / not-approved are shown as muted states, never a crash. - preview-apps-git-access.spec.ts: e2e (mod fixture) asserting the SECURITY- critical owner-gate live — getMyAppRepo on a non-owned app → FORBIDDEN (throws before any Forgejo user/collaborator is created), unknown id → NOT_FOUND. The full provisioning happy-path (real Forgejo user + git push → park) is unit- covered + a manual preview check (shared-Forgejo state, no safe teardown), mirroring the publish spec's approve exclusion. git-access unit 5/5; e2e discovered under preview-smoke. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app-blocks): audit follow-ups — wedge recovery, token masking, ban gate (Phase 3) Audit fixes for PR #2587. (The load-bearing approved-sha integrity / no-trust-on- push property was verified SOUND — sha pinned end-to-end, content-addressed.) HIGH — permanent provisioning wedge: a claim row is a standalone insert, so a hard owner crash (pod kill) between the claim and the token write left an empty- token row with no recovery → that user was locked out of git access forever. ensureForgejoIdentity now treats an empty-token claim older than 60s as ABANDONED and atomically reclaims it (optimistic-concurrency guarded on createdAt), then provisions it — no permanent wedge. +test. MEDIUM — the "Steps" block rendered the token-bearing clone URL in cleartext regardless of the reveal toggle (the masker was anchored to a bare URL and didn't touch the embedded URL in the instructions string). maskCloneUrlCredential is now a global mask that handles an embedded credential in any text; the Steps snippet is masked under the same reveal toggle. +test. MEDIUM — getMyAppRepo now refuses to issue a push credential to a banned account (ctx.user.bannedAt → FORBIDDEN). Full revoke-on-ban remains a follow-up. Deferred (documented as follow-ups): orphan-recreate re-grant across the dev's other repos; getMyAppRepo query→mutation; >1000-file tree pagination; Forgejo- user cleanup on GDPR delete. MUST verify before launch: Forgejo restricted-user + private-repo isolation (a dev PAT can't clone other apps / the starter / review org) — config-dependent, not verifiable from the app code. Blocks + git-access suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app-blocks): reclaim guard must be range-based (timestamptz µs vs JS-Date ms) The stale-claim reclaim guarded on exact createdAt equality, but Postgres timestamptz(6) stores microseconds while Prisma reads a millisecond-precision JS Date — the equality would systematically miss, so abandoned claims would never be reclaimed (the wedge fix was inert). Switched to a range guard (createdAt < now-60s), which also serializes concurrent reclaimers via the row lock. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app-blocks): conditional token-fill/rollback + ban-gate test (re-audit) Second-audit follow-ups (verdict was settled/mergeable; these close the last narrow edge + a test nit). LOW edge: a single owner that stalled >60s between createForgejoUser and the DB write could, after a waiter reclaimed + filled the row, OVERWRITE the winner's good token with its own (now minted against a deleted Forgejo user) → a dead token persisted forever. The fill is now a CONDITIONAL updateMany guarded on the still-empty token (count 0 ⇒ we were superseded ⇒ return the winner's stored token, never persist a dead one). The failure rollback is likewise deleteMany guarded on the empty token, so it can't nuke a reclaimer's filled row. NIT: added a getMyAppRepo test asserting a banned owner → FORBIDDEN with nothing provisioned. dev-git-access + getMyAppRepo suites green (15/15 affected). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
58840582f6 |
feat(app-blocks): server-own iframe.src + drop Dockerfile/nginx requirement (#2581)
* feat(app-blocks): server-own iframe.src + drop Dockerfile/nginx requirement Two developer-friction reductions for the App Blocks publish flow (Phase 1 of the HF-Spaces-inspired DX arc). Both are deterministic, no schema change. iframe.src is now PLATFORM-OWNED, not developer-authored. The only valid value is the canonical per-app subdomain root (https://<slug>.<APPS_DOMAIN>/), and every gate already demanded exactly that — so a developer had to hand-type a subdomain that doesn't exist until their app is approved, and a wrong value rejected them AFTER a multi-MiB upload. New pure helper `manifest-normalize.ts#stampCanonicalIframeSrc` derives + stamps it at all three ingestion points (submitVersion, approveRequest, the git-push webhook), mirroring how trustTier is already server-owned. approveRequest also rewrites the committed block.manifest.json so the build-source repo stays byte-consistent with app_blocks.manifest and the webhook re-validates the canonical value. Other iframe fields (minHeight, sandbox) stay developer-authored. Dockerfile/nginx.conf are no longer expected in the bundle: the build pipeline injects its own platform-owned recipe and already strips tenant copies at approve (isPlatformOwnedPath), and nothing required them (only block.manifest.json is mandatory). Updates the submit-page copy + the feature doc; the stripping behavior is unchanged. Tests: new manifest-normalize unit test; the submit-time iframe.src reject tests become stamp/normalize tests; the two approve-time H-4 tests now trigger via a sandbox token disallowed for the unverified tier (a webhook-rejectable shape that survives canonical-src stamping) so H-4 coverage is preserved; happy-path asserts the committed + stored manifest carry the canonical src. Full App Blocks suite green locally (423/423, node-only unit project). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(app-blocks): preview-e2e smoke for the publish-request submit leg Adds tests/preview-apps-publish.spec.ts to the preview-e2e suite, closing the coverage gap the existing apps specs (install/marketplace) left: they deliberately skip submit/approve. Runs as the provisioned `mod` fixture (ci-smoke-mod), self-seeding + self-cleaning against the shared dev DB. Asserts the deterministic DX change end-to-end on a live preview: a bundle whose manifest OMITS iframe.src and contains NO Dockerfile is ACCEPTED by POST /api/blocks/submit-version, and the stored manifest (read back via blocks.listPendingRequests) carries the server-stamped canonical https://<slug>.<APPS_DOMAIN>/. On origin/main the same bundle 400s with "manifest.iframe.src must be a string". SAFE + self-cleaning: per-preview slug (no cross-preview pending-unique collision), pre/post withdrawPublishRequest, and submit only touches the review org + MinIO + one dev-DB row — NO Tekton build, NO civitai-apps Deployment, NO CF DNS. approve→build→render is intentionally NOT e2e'd (real shared-infra + ~5min flaky build); its stamping is covered by the unit suite (orchestration + git-push.gate + manifest-normalize). Compiles + is discovered by playwright --list under the preview-smoke project; not yet run against a live preview (the run needs the preview NEXTAUTH_SECRET). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(app-blocks): commit faithful manifest (audit M1) — only stamp iframe.src Audit follow-up to PR #2581. The approve-time committed block.manifest.json was re-serialized from the stored JSONB (request.manifest), which (a) normalizes key order (Postgres jsonb) so the dev's build repo showed a confusing reorder, and (b) baked the server-resolved `trustTier` into the tenant-visible repo. Neither is a security/runtime issue (the runtime reads app_blocks.manifest, and the git-push webhook no-ops on the approved sha BEFORE validating), but it's an avoidable wart on the developer's repo. Now we commit the developer's ORIGINAL block.manifest.json bytes with ONLY the platform-owned iframe.src corrected — preserving their field order and not injecting server-resolved fields. Falls back to the stored manifest if the bundle's manifest is missing/unparseable. Also documents that the webhook's iframe.src exact-match is now defense-in-depth (the in-memory stamp makes it always match for an object manifest). Unit suite unchanged + green (orchestration happy-path still asserts the committed + stored manifest carry the canonical iframe.src). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d3852d93c5 |
test(blocks): preview-e2e coverage for App Blocks marketplace + install/consent (F-C) (#2578)
Adds the first App Blocks journeys to the Playwright preview-e2e suite (report-only
smoke-test task), as the `mod` fixture — the only preview role with
features.appBlocks (Flipt mod-segment) + rate-limit exempt:
- preview-apps-marketplace.spec.ts: GET /apps renders the marketplace for an
appBlocks-enabled mod (not the non-mod 404) → blocks.listAvailable discovers an
appBlockId from .items → blocks.getAppDetail shape (name/scopes/slots) →
GET /apps/[id] renders the block-name heading. Public read path end-to-end.
- preview-apps-install.spec.ts: getInstallConfig → upsertSubscription
(viewer_personal) → listMySubscriptions read-back → grantScopes (consent, from
the manifest∩approved ceiling) → listMyScopeGrants corroboration → cleanup
deleteSubscription in finally (self-cleaning on the shared dev clone).
Discover IDs at runtime (never hardcode); test.skip (annotated) if the weekly dev
clone has zero approved blocks. grantScopes().granted is the authoritative,
race-immune consent check; the listMyScopeGrants surface read-back is SOFT
(assert-if-present / annotate-if-absent) because the shared mod subscription it
derives from can be deleted by a concurrent preview's cleanup.
Generate/Buzz-spend is intentionally NOT e2e'd (GA-gated assertViewerIsModerator +
Buzz is an external service — same rationale as the deferred generation-submit);
consent grant is the furthest deterministically-coverable pre-spend step.
Found via reading source (not assumed): listAvailable returns {items:[]} not a
bare array; subscription id is a string ULID; listMyScopeGrants is an aggregated
per-app surface (manifest scopes), not the raw grant ledger; no revokeScopes proc.
Adversarial-audited (verbs/inputs/return-shapes/gates/headings/typecheck verified).
|
||
|
|
16989f63a4 |
ci(lighthouse): measure the model detail page (highest-traffic entrypoint) (#2543)
Add /models/4201 (a stable foundational base model) to the Lighthouse CI route set so the highest-traffic landing page is measured on every preview (was only /, /models listing, /generate). A spot-check found the model page is the worst a11y page (button-name x45, image-alt x5, ...) + CLS 0.271, previously a blind spot. Report-only; clean authed/no-ads baseline. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cca249e517 |
ci(lighthouse): report-only Lighthouse CI on PR previews (Tekton) (#2516)
* ci(lighthouse): report-only Lighthouse CI on PR previews (Tekton) Adds the civitai-side config for a new report-only `lighthouse` task in the datapacket-talos pr-preview pipeline. On each preview build it runs Lighthouse (5 runs, desktop, median-aggregated) against the deployed preview at pr-<N>.civitaic.com and posts a report-only PR comment with perf/a11y scores + LCP/CLS/TBT/INP and a public report link. - lighthouserc.json: 3 routes (/ /models /generate), numberOfRuns 5, desktop preset, --no-sandbox, temporary-public-storage upload. All assertions are `warn` (report-only) — promotion path documented (deterministic CLS/byte budgets to `error` first, noisy timing metrics last, after a soak). - tests/lighthouse-mint-cookie.cjs: mints the gate-passing ci-smoke-gold NextAuth session cookie (same next-auth/jwt encode + NEXTAUTH_SECRET as tests/preview-auth.setup.ts) and injects it into collect.settings.extraHeaders -> lighthouserc.runtime.json, so headless Chrome clears the preview-auth middleware /login gate instead of measuring the login page. Report-only first; structured to flip to gating later. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: retrigger preview for lighthouse upload fix * ci: retrigger preview for lighthouse manifest fix --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0d59c11eb7 |
test(preview): trim the search-readiness gate to a single warm-up (#2507)
The #2495 readiness gate polled the image-search path 12x6s (~72s) until 2xx — but it logged 'not ready' while the specs passed anyway (it was never the load-bearing fix). retryFlaky on each search-dependent spec (whatIf, /moderator/images, image-feed) is what actually rides out a transient 408/5xx. Replace the blocking poll with a single fire-and-forget warm-up GET, removing up to ~72s of wasted setup time per run. |
||
|
|
fc63831d2c |
test(preview): ride out image-search 408 overload in the image-feed spec (#2503)
image.getInfinite (meili-backed via the shared feeds-proxy) intermittently returns HTTP 408 'Image search is temporarily overloaded — please retry' under concurrent preview-build load — the same flaky search path the #2495 resilience fix addressed for whatIf + /moderator/images, but the image-feed spec was missed. It hard-failed an unrelated PR (#2501). Wrap the getInfinite query in retryFlaky (spaced backoff) so a transient overload is ridden out; honest — a sustained overload still fails. The app's own 408 message explicitly says to retry. |
||
|
|
10a65819ce |
test(preview): cover posting an image via direct upload (real B2 presigned PUT) (#2494)
Drives the 3-leg direct-upload handshake request-side: POST /api/v1/image-upload (server mints key + presigned B2 PUT url; preview has real S3_IMAGE_B2_* creds) -> PUT the bytes to Backblaze B2 via the presigned url (the load-bearing integration a service test can't cover) -> post.createWithImages(publish:true) attaches the row by the B2 key and publishes -> read back via post.getEdit (publishedAt + images.length). Asserts the upload+attach+publish DB path; NOT public-feed visibility (ingestion stays Pending in preview, by design). Tiny 1x1 PNG fixture. Role: tester. Report-only. |
||
|
|
e33c22d81d |
test(preview): cover uploading + posting a model (model.upsert -> version -> publish) (#2497)
Covers the model authoring + publish contract via tRPC: model.upsert (draft) -> modelVersion.upsert (baseModel SD 1.5) -> model.getById read-back -> model.publish -> assert status Published -> best-effort delete cleanup. Fileless: publishModelById has no file/scan precondition (scan gate is UI-only), and the binary B2 upload is already covered by preview-post-upload.spec.ts. No rate limit on these procedures (unlike article.upsert), so role tester. Mirrors preview-article.spec.ts. Report-only. |
||
|
|
dcad7b70f1 |
test(preview): cover the remix flow (#2492)
* test(preview): cover the remix flow (generation.getGenerationData resolves an image into a remix graph) Remix = client-side pre-fill of the generator from an image's generation meta (no submit, no Buzz, no GPU). Asserts the load-bearing, preview-seedable half: the server resolves a real meta-bearing image into a remix graph keyed to that image (remixOfId === source id, non-empty params, resources array). Discovers a remixable image at runtime via /api/v1/images?withMeta (report-only test.skip if the dev DB has none). Sibling of preview-generation.spec.ts; gold role. * test(preview): remix discovery must avoid Meilisearch (DB path via modelId) First preview run hard-failed: /api/v1/images?sort=Most Reactions routes through the search index (getAllImagesIndex) and the preview pod can't reach Meilisearch (MeiliSearchCommunicationError) -> 500. Switch discovery to the DB path: pick models via /api/v1/models (DB-backed, proven in preview by preview-resource-review), then pull each model's gallery images with modelId set -> useLegacyMethod -> getAllImages (DB), bypassing Meili. Also: a 5xx on the generic read endpoints now skips (preview cold/unhealthy, not a remix bug) instead of hard-failing, while a getGenerationData that 5xxs on every candidate still surfaces (real endpoint regression). |