mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
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>
This commit is contained in:
@@ -32,9 +32,22 @@ import { trpcQuery } from './preview-trpc';
|
||||
* projection) + `blockId` (the `<slug>` used by the page route).
|
||||
* - Page route `/apps/run/[slug]/[[...path]]` SSR-gates on
|
||||
* `features.appBlocks && features.appBlocksPages`, resolves the approved app
|
||||
* by `block_id`, and renders `<PageBlockHost>` (data-testid="app-page-frame"
|
||||
* with the `<iframe data-testid="app-page-iframe">` inside, plus the W7
|
||||
* `data-testid="app-block-chrome"` trust bar).
|
||||
* by `block_id`, and renders `<PageBlockHost>`: the W7 trust chrome
|
||||
* (`AppBlockChrome`, with an "App block menu" button) above the block
|
||||
* `<iframe>` (`title=<appName>`, `data-block-instance-id="page_<appBlockId>"`,
|
||||
* `data-block-ready` flipping to "true" on BLOCK_READY).
|
||||
*
|
||||
* ⚠️ SELECTORS — NO `data-testid`. The preview is a PRODUCTION Next build
|
||||
* (NODE_ENV=production), 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')`
|
||||
* NEVER matches against a deployed preview — the elements render fine, the
|
||||
* attribute is just gone. We therefore assert on attributes the production build
|
||||
* KEEPS: the chrome's accessible "App block menu" button, and the iframe's
|
||||
* `data-block-instance-id` (a `page_*` id) + `data-block-ready`. (`reactRemove
|
||||
* Properties` only strips `^data-testid$`, so other `data-*` attrs survive.)
|
||||
* This mirrors the sibling preview-apps-* specs, which assert via tRPC + ARIA
|
||||
* roles, never rendered testids.
|
||||
*/
|
||||
|
||||
const ROLE = 'mod' as const;
|
||||
@@ -52,7 +65,7 @@ type ListAvailableResult = { items: AvailableBlock[]; nextCursor?: string };
|
||||
test.describe('App Blocks full-page app surface (mod)', () => {
|
||||
test.use({ storageState: storageStatePath(ROLE) });
|
||||
|
||||
test('a page-declaring app opens at /apps/run/<slug> and the iframe mounts + inits', async ({
|
||||
test('a page-declaring app opens at /apps/run/<slug>: chrome + iframe mount and the host mints a page token', async ({
|
||||
page,
|
||||
}) => {
|
||||
// DISCOVER a page-declaring approved app from the public listing. `{}` is a
|
||||
@@ -79,23 +92,77 @@ test.describe('App Blocks full-page app surface (mod)', () => {
|
||||
).toBeLessThan(400);
|
||||
|
||||
// The host trust chrome (rendered in civitai-web, spoof-proof) is present —
|
||||
// proves PageBlockHost mounted (not a 404 / blank).
|
||||
// proves PageBlockHost mounted (not a 404 / blank). The chrome's "App block
|
||||
// menu" button is unique to AppBlockChrome and uses a production-safe
|
||||
// accessible name (NOT a stripped data-testid).
|
||||
await expect(
|
||||
page.getByTestId('app-block-chrome'),
|
||||
page.getByRole('button', { name: 'App block menu' }),
|
||||
'the full-page host should render the W7 trust chrome'
|
||||
).toBeVisible();
|
||||
|
||||
// The block iframe mounts (server-resolved manifest.iframe.src).
|
||||
const frame = page.getByTestId('app-page-iframe');
|
||||
// The block iframe mounts (server-resolved manifest.iframe.src). Located by
|
||||
// its surviving `data-block-instance-id` (a synthetic `page_<appBlockId>` id
|
||||
// unique to the page host) — `getByTestId` would never match (stripped).
|
||||
const frame = page.locator('iframe[data-block-instance-id^="page_"]');
|
||||
await expect(frame, 'the full-page block iframe should mount').toBeVisible();
|
||||
|
||||
// The host posts BLOCK_INIT (viewer page token + subPath) on a retry loop
|
||||
// until the block acks BLOCK_READY → data-block-ready flips to "true". This
|
||||
// is the e2e proof that the page-mint + handshake worked. Allow generous
|
||||
// time for the cross-origin bundle to load + ack on a cold preview.
|
||||
await expect(
|
||||
frame,
|
||||
'the full-page block should receive BLOCK_INIT and ack ready (page token minted)'
|
||||
).toHaveAttribute('data-block-ready', 'true', { timeout: 20_000 });
|
||||
// The iframe points at the server-resolved block origin (manifest.iframe.src
|
||||
// → `<slug>.civit.ai`), not a blank/about:blank — proves the SSR-resolved
|
||||
// src reached the DOM.
|
||||
const iframeSrc = await frame.getAttribute('src');
|
||||
expect(iframeSrc, 'the iframe src is the server-resolved block origin').toMatch(
|
||||
/^https?:\/\//
|
||||
);
|
||||
|
||||
// PAGE-MINT PROOF (the host-side half of the handshake we CAN verify on a
|
||||
// preview): the host posts BLOCK_INIT only after minting a viewer-scoped page
|
||||
// token from POST /api/v1/block-tokens (entityType:'none', `page_<appBlockId>`).
|
||||
// Exercise that exact mint with the mod cookie — a 200 + non-empty token is
|
||||
// the e2e proof the page-mint path works (two-flag gate cleared, synthetic
|
||||
// page instance resolved, JWT issued). The host then posts BLOCK_INIT to the
|
||||
// block origin.
|
||||
//
|
||||
// We do NOT assert `data-block-ready === 'true'` (the block's BLOCK_READY
|
||||
// ack): that flips only when the cross-origin block bundle at `<slug>.civit.ai`
|
||||
// ACCEPTS the BLOCK_INIT, which requires the PARENT origin (here the ephemeral
|
||||
// preview host `pr-N.civitaic.com`) to be in the block's own
|
||||
// `allowedParentOrigins` allowlist. Production blocks allowlist civitai.com /
|
||||
// *.civit.ai, never an ephemeral preview origin, so the ack can never arrive
|
||||
// on a preview no matter how correct civitai-web is — asserting it makes this
|
||||
// spec un-passable by construction. The host-side contract (chrome + iframe +
|
||||
// page-token mint) is what a preview can prove; the BLOCK_READY round-trip is
|
||||
// covered by the PageBlockHost.browser.test.tsx component test with a stubbed
|
||||
// block.
|
||||
const tokenResp = await page.request.post('/api/v1/block-tokens', {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
origin: process.env.PREVIEW_URL ?? '',
|
||||
referer: `${process.env.PREVIEW_URL ?? ''}/apps/run/${pageApp.blockId}`,
|
||||
},
|
||||
data: {
|
||||
// Synthetic page instance id: `page_<appBlockId>`, where appBlockId is
|
||||
// `AvailableBlock.id` (== app_blocks.id, the `apb_*` value) — the same id
|
||||
// the page route builds in `page_${appBlockId}`.
|
||||
blockInstanceId: `page_${pageApp.id}`,
|
||||
slotContext: {
|
||||
slotId: 'app.page',
|
||||
entityType: 'none',
|
||||
slug: pageApp.blockId,
|
||||
subPath: '',
|
||||
viewerUserId: null,
|
||||
viewerUsername: null,
|
||||
theme: 'dark',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(
|
||||
tokenResp.status(),
|
||||
'the host mints a viewer-scoped page token (the BLOCK_INIT prerequisite)'
|
||||
).toBe(200);
|
||||
const tokenBody = (await tokenResp.json()) as { token?: string };
|
||||
expect(
|
||||
typeof tokenBody.token === 'string' && tokenBody.token.length > 0,
|
||||
'page-token mint returns a non-empty JWT'
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,6 +43,7 @@ const FEATURE_FLAGS_PROCEDURE = 'user.getFeatureFlags';
|
||||
const TOS_PROCEDURE = 'content.checkTosUpdate';
|
||||
const ANNOUNCEMENTS_PROCEDURE = 'announcement.getAnnouncements';
|
||||
const FOLLOWING_PROCEDURE = 'user.getFollowingUsers';
|
||||
const LIVE_NOW_PROCEDURE = 'system.getLiveNow';
|
||||
|
||||
// A core page a gate-passing user lands on directly (no /login bounce). Both
|
||||
// procedures fire on every logged-in bootstrap regardless of which page, so
|
||||
@@ -399,6 +400,77 @@ test.describe('SSR-injected getFollowingUsers (logged-in)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Regression guard for the SSR-inject of `system.getLiveNow` (~26/s on
|
||||
* api-primary). NOT auth-gated — it is a `publicProcedure` consumed by the
|
||||
* ambient `useIsLive` hook (header logo, social links, social home block), so
|
||||
* it fires on every bootstrap, anon and authed alike. SSR-computed in
|
||||
* `_app.getInitialProps` (fail-open to `false`) and seeded in AppProvider on the
|
||||
* fixed `undefined` key. The payload is a plain `boolean` — no Date/array
|
||||
* serialization subtlety, so the seed (JSON) and a live fetch (`result.data.json`)
|
||||
* compare directly. The seed can legitimately be `false` (stream offline); that
|
||||
* is still a valid, present seed and the byte-equality assertion holds.
|
||||
*/
|
||||
test.describe('SSR-injected getLiveNow (logged-in)', () => {
|
||||
test.use({ storageState: storageStatePath('mod') });
|
||||
|
||||
test('getLiveNow is seeded into __NEXT_DATA__ and not fetched on bootstrap', async ({ page }) => {
|
||||
const trpcUrls = await collectTrpcRequests(page, AUTHED_LANDING);
|
||||
|
||||
const liveNow = await readPageProp(page, 'liveNow');
|
||||
expect(liveNow.present, 'liveNow present in __NEXT_DATA__ pageProps').toBe(true);
|
||||
expect(typeof liveNow.value, 'liveNow seed is a boolean').toBe('boolean');
|
||||
|
||||
const liveNowRequests = trpcUrls.filter((u) => u.includes(LIVE_NOW_PROCEDURE));
|
||||
expect(
|
||||
liveNowRequests,
|
||||
`no ${LIVE_NOW_PROCEDURE} tRPC request should fire on bootstrap (it is SSR-injected); saw:\n${liveNowRequests.join(
|
||||
'\n'
|
||||
)}`
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('SSR seed byte-equals a live fetch', async ({ page }) => {
|
||||
await page.goto(AUTHED_LANDING, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const seed = await readPageProp(page, 'liveNow');
|
||||
expect(seed.present, 'liveNow seed present in __NEXT_DATA__').toBe(true);
|
||||
|
||||
const live = await fetchTrpcQueryJson(page, LIVE_NOW_PROCEDURE);
|
||||
expect(
|
||||
seed.value,
|
||||
'system.getLiveNow SSR seed must byte-equal a live resolver fetch'
|
||||
).toEqual(live);
|
||||
expect(typeof live, 'live getLiveNow is a boolean').toBe('boolean');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('SSR-injected getLiveNow (anonymous)', () => {
|
||||
// `system.getLiveNow` is a publicProcedure and fires for anon too, so the seed
|
||||
// must be present on the anon bootstrap as well. No anon "byte-equals a live
|
||||
// fetch" test: the preview-auth gate blocks ALL anonymous /api/trpc/* and
|
||||
// 302-redirects to /login (same reason documented for the anon announcements
|
||||
// case), so an anon live fetch never returns tRPC JSON. The seed-present +
|
||||
// no-bootstrap-fetch contract is what matters and is asserted here.
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('getLiveNow is seeded into __NEXT_DATA__ and not fetched on bootstrap', async ({ page }) => {
|
||||
const trpcUrls = await collectTrpcRequests(page, ANON_LANDING);
|
||||
|
||||
const liveNow = await readPageProp(page, 'liveNow');
|
||||
expect(liveNow.present, 'liveNow present in __NEXT_DATA__ pageProps').toBe(true);
|
||||
expect(typeof liveNow.value, 'liveNow seed is a boolean').toBe('boolean');
|
||||
|
||||
const liveNowRequests = trpcUrls.filter((u) => u.includes(LIVE_NOW_PROCEDURE));
|
||||
expect(
|
||||
liveNowRequests,
|
||||
`no ${LIVE_NOW_PROCEDURE} tRPC request should fire on bootstrap (it is SSR-injected); saw:\n${liveNowRequests.join(
|
||||
'\n'
|
||||
)}`
|
||||
).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* MANUAL CHECKLIST — behaviours of this PR that are not auto-asserted above.
|
||||
* (Auth IS supported by this harness, but these are timing/visual/state cases
|
||||
|
||||
Reference in New Issue
Block a user