From e4cfba7ab185c3f3d50d81c4e6b73453136ccaba Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 18:23:41 +0000 Subject: [PATCH 1/4] fix: fix failing e2e tests on dev branch - projects.test.ts: add missing allow_localhost field to inline snapshot - cross-domain-auth.test.ts: use proper cookie mock that handles js-cookie's Cookies.set - cross-domain-auth.test.ts: use cross-origin afterCallbackRedirectUrl to trigger cross-domain flow Co-Authored-By: Konstantin Wohlwend --- .../backend/endpoints/api/v1/projects.test.ts | 1 + apps/e2e/tests/js/cross-domain-auth.test.ts | 23 ++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts index 2054e1b75e..a2f3d377b2 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts @@ -39,6 +39,7 @@ it("gets current project (internal)", async ({ expect }) => { "status": 200, "body": { "config": { + "allow_localhost": true, "allow_team_api_keys": false, "allow_user_api_keys": false, "client_team_creation_enabled": true, diff --git a/apps/e2e/tests/js/cross-domain-auth.test.ts b/apps/e2e/tests/js/cross-domain-auth.test.ts index 40410c468d..37b75c4637 100644 --- a/apps/e2e/tests/js/cross-domain-auth.test.ts +++ b/apps/e2e/tests/js/cross-domain-auth.test.ts @@ -2,6 +2,23 @@ import { StackClientApp } from "@stackframe/js"; import { afterEach, vi } from "vitest"; import { it, localRedirectUrl } from "../helpers"; +function createMockDocument(): Document { + const cookieJar = new Map(); + return { + get cookie() { + return [...cookieJar.entries()].map(([k, v]) => `${k}=${v}`).join('; '); + }, + set cookie(str: string) { + const [nameValue] = str.split(';'); + const eqIndex = nameValue.indexOf('='); + if (eqIndex >= 0) { + cookieJar.set(nameValue.slice(0, eqIndex).trim(), nameValue.slice(eqIndex + 1).trim()); + } + }, + createElement: () => ({}), + } as any; +} + const withHostedDomainSuffix = async (callback: () => Promise) => { const oldHostedHandlerDomainSuffix = process.env.NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX; const oldHostedHandlerUrlTemplate = process.env.NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE; @@ -178,7 +195,7 @@ it("only treats hosted OAuth callback URLs as Stack callbacks when the matching const previousWindow = globalThis.window; const previousDocument = globalThis.document; - globalThis.document = { cookie: "", createElement: () => ({}) } as any; + globalThis.document = createMockDocument(); globalThis.window = { location: { href: `${localRedirectUrl}/callback-page?code=oauth-code&state=oauth-state`, @@ -221,7 +238,7 @@ it("does not await pending auth resolutions when post-callback redirect mints a redirectBackUrl.searchParams.set("stack_cross_domain_auth", "1"); redirectBackUrl.searchParams.set("stack_cross_domain_state", "state"); redirectBackUrl.searchParams.set("stack_cross_domain_code_challenge", "challenge"); - redirectBackUrl.searchParams.set("stack_cross_domain_after_callback_redirect_url", `${localRedirectUrl}/after`); + redirectBackUrl.searchParams.set("stack_cross_domain_after_callback_redirect_url", `https://${projectId}.example-stack-hosted.test/after`); currentUrl.searchParams.set("after_auth_return_to", redirectBackUrl.toString()); const previousWindow = globalThis.window; @@ -230,7 +247,7 @@ it("does not await pending auth resolutions when post-callback redirect mints a .spyOn(clientApp as any, "_createCrossDomainAuthRedirectUrl") .mockResolvedValue(`https://${projectId}.example-stack-hosted.test/handler/final`); - globalThis.document = { cookie: "", createElement: () => ({}) } as any; + globalThis.document = createMockDocument(); globalThis.window = { location: { href: currentUrl.toString(), From 8cbe5dbe7b0eb14c55ee6c1602f0de42600b7665 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 20:39:45 +0000 Subject: [PATCH 2/4] refactor: improve document mock with expiry support and replace all plain-object mocks - Add cookie expiry/deletion handling to createMockDocument() - Replace all remaining plain-object document mocks with createMockDocument() for consistency Co-Authored-By: Konstantin Wohlwend --- apps/e2e/tests/js/cross-domain-auth.test.ts | 43 +++++++++++++-------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/apps/e2e/tests/js/cross-domain-auth.test.ts b/apps/e2e/tests/js/cross-domain-auth.test.ts index 37b75c4637..f93169b06b 100644 --- a/apps/e2e/tests/js/cross-domain-auth.test.ts +++ b/apps/e2e/tests/js/cross-domain-auth.test.ts @@ -9,10 +9,21 @@ function createMockDocument(): Document { return [...cookieJar.entries()].map(([k, v]) => `${k}=${v}`).join('; '); }, set cookie(str: string) { - const [nameValue] = str.split(';'); + const parts = str.split(';'); + const [nameValue] = parts; const eqIndex = nameValue.indexOf('='); if (eqIndex >= 0) { - cookieJar.set(nameValue.slice(0, eqIndex).trim(), nameValue.slice(eqIndex + 1).trim()); + const name = nameValue.slice(0, eqIndex).trim(); + const isExpired = parts.some(p => { + const trimmed = p.trim().toLowerCase(); + if (!trimmed.startsWith('expires=')) return false; + return new Date(trimmed.slice('expires='.length)) <= new Date(); + }); + if (isExpired) { + cookieJar.delete(name); + } else { + cookieJar.set(name, nameValue.slice(eqIndex + 1).trim()); + } } }, createElement: () => ({}), @@ -65,7 +76,7 @@ it("adds secure cross-domain handoff parameters when redirecting to hosted sign- const previousDocument = globalThis.document; let redirectedUrl = ""; - globalThis.document = { cookie: "", createElement: () => ({}) } as any; + globalThis.document = createMockDocument(); globalThis.window = { location: { href: `${localRedirectUrl}/private-page?foo=bar`, @@ -107,7 +118,7 @@ it("returns static app.urls.signIn for hosted flows", async ({ expect }) => { const previousWindow = globalThis.window; const previousDocument = globalThis.document; - globalThis.document = { cookie: "", createElement: () => ({}) } as any; + globalThis.document = createMockDocument(); globalThis.window = { location: { href: currentHref, @@ -138,7 +149,7 @@ it("returns static app.urls.signOut for hosted flows", async ({ expect }) => { const previousWindow = globalThis.window; const previousDocument = globalThis.document; - globalThis.document = { cookie: "", createElement: () => ({}) } as any; + globalThis.document = createMockDocument(); globalThis.window = { location: { href: currentHref, @@ -173,7 +184,7 @@ it("strips stale OAuth callback params from hosted current-page redirect URIs", currentUrl.searchParams.set("message", "Known message"); currentUrl.searchParams.set("details", "{}"); - globalThis.document = { cookie: "", createElement: () => ({}) } as any; + globalThis.document = createMockDocument(); globalThis.window = { location: { href: currentUrl.toString(), @@ -284,7 +295,7 @@ it("does not await pending auth resolutions when post-callback redirect adds nes const previousWindow = globalThis.window; const previousDocument = globalThis.document; - globalThis.document = { cookie: "", createElement: () => ({}) } as any; + globalThis.document = createMockDocument(); globalThis.window = { location: { href: `${localRedirectUrl}/callback-page`, @@ -339,7 +350,7 @@ it("keeps cross-domain handoff working when top-level params are dropped before .spyOn(clientApp as any, "_createCrossDomainAuthRedirectUrl") .mockResolvedValue(crossDomainAuthorizeRedirect); - globalThis.document = { cookie: "", createElement: () => ({}) } as any; + globalThis.document = createMockDocument(); globalThis.window = { location: { href: hostedAfterSignInCallbackUrl.toString(), @@ -395,7 +406,7 @@ it("keeps cross-domain handoff working when after_auth_return_to is rewritten to .spyOn(clientApp as any, "_createCrossDomainAuthRedirectUrl") .mockResolvedValue(crossDomainAuthorizeRedirect); - globalThis.document = { cookie: "", createElement: () => ({}) } as any; + globalThis.document = createMockDocument(); globalThis.window = { location: { href: hostedAfterSignInCallbackUrl.toString(), @@ -436,7 +447,7 @@ it("adds nested cross-domain auth params when redirecting signed-in users to hos const previousWindow = globalThis.window; const previousDocument = globalThis.document; let redirectedUrl = ""; - globalThis.document = { cookie: "", createElement: () => ({}) } as any; + globalThis.document = createMockDocument(); globalThis.window = { location: { href: currentHref, @@ -474,7 +485,7 @@ it("adds nested cross-domain auth params for other cross-domain handler redirect const previousWindow = globalThis.window; const previousDocument = globalThis.document; let redirectedUrl = ""; - globalThis.document = { cookie: "", createElement: () => ({}) } as any; + globalThis.document = createMockDocument(); globalThis.window = { location: { href: currentHref, @@ -515,7 +526,7 @@ it("starts nested cross-domain auth from the target domain", async ({ expect }) codeChallenge: "nested-code-challenge", }); - globalThis.document = { cookie: "", createElement: () => ({}) } as any; + globalThis.document = createMockDocument(); globalThis.window = { location: { href: currentHref, @@ -567,7 +578,7 @@ it("continues nested cross-domain auth on the source domain", async ({ expect }) .mockResolvedValue(crossDomainRedirect); vi.spyOn(clientApp as any, "_getCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(sourceRefreshTokenId); - globalThis.document = { cookie: "", createElement: () => ({}) } as any; + globalThis.document = createMockDocument(); globalThis.window = { location: { href: currentUrl.toString(), @@ -611,7 +622,7 @@ it("rejects nested cross-domain auth when the source redirect URI is untrusted", const createCrossDomainAuthRedirectUrlSpy = vi.spyOn(clientApp as any, "_createCrossDomainAuthRedirectUrl"); vi.spyOn(clientApp as any, "_isTrusted").mockResolvedValue(false); - globalThis.document = { cookie: "", createElement: () => ({}) } as any; + globalThis.document = createMockDocument(); globalThis.window = { location: { href: currentUrl.toString(), @@ -640,7 +651,7 @@ it("rejects nested cross-domain auth when the callback URL is untrusted", async vi.spyOn(clientApp as any, "_getCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(null); vi.spyOn(clientApp as any, "_isTrusted").mockResolvedValue(false); - globalThis.document = { cookie: "", createElement: () => ({}) } as any; + globalThis.document = createMockDocument(); globalThis.window = { location: { href: currentHref, @@ -671,7 +682,7 @@ it("rejects nested cross-domain auth when the source session does not match", as const createCrossDomainAuthRedirectUrlSpy = vi.spyOn(clientApp as any, "_createCrossDomainAuthRedirectUrl"); vi.spyOn(clientApp as any, "_getCurrentRefreshTokenIdIfSignedIn").mockResolvedValue("different-source-session"); - globalThis.document = { cookie: "", createElement: () => ({}) } as any; + globalThis.document = createMockDocument(); globalThis.window = { location: { href: currentUrl.toString(), From b056d92131b276d394dbc204b2b71b09f3025cfe Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 21:11:22 +0000 Subject: [PATCH 3/4] fix: fix setup-tests race condition and QEMU build dependency resolution - Add Prisma client directory check to wait-for-dev-package-imports.ts probe so codegen-docs waits for prisma generate to complete - Fix QEMU build pnpm install filter to include @stackframe/dashboard dep tree (turbo task graph requires it for stack-cli build) Co-Authored-By: Konstantin Wohlwend --- .github/workflows/qemu-emulator-build.yaml | 10 ++++++---- scripts/wait-for-dev-package-imports.ts | 13 +++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/qemu-emulator-build.yaml b/.github/workflows/qemu-emulator-build.yaml index c88242d73f..e72f561d57 100644 --- a/.github/workflows/qemu-emulator-build.yaml +++ b/.github/workflows/qemu-emulator-build.yaml @@ -148,10 +148,12 @@ jobs: - name: Build stack-cli (for emulator CLI) if: matrix.arch == 'amd64' run: | - pnpm install --frozen-lockfile --filter '@stackframe/stack-cli...' - # Turbo's trailing `...` filter builds stack-cli AND its workspace - # deps (@stackframe/js, @stackframe/stack-shared, etc.) — stack-cli - # imports them at runtime from their dist/ outputs. + # Turbo's task graph for stack-cli#build includes + # @stackframe/dashboard#build:rde-standalone, which transitively + # depends on @stackframe/stack#build (via dashboard → stack). + # The pnpm filter must cover the dashboard dep tree too so that + # devDependencies like tailwindcss are installed for the build. + pnpm install --frozen-lockfile --filter '@stackframe/stack-cli...' --filter '@stackframe/dashboard...' pnpm exec turbo run build --filter='@stackframe/stack-cli...' - name: Start emulator and verify diff --git a/scripts/wait-for-dev-package-imports.ts b/scripts/wait-for-dev-package-imports.ts index c9791b6c29..d9208afc51 100644 --- a/scripts/wait-for-dev-package-imports.ts +++ b/scripts/wait-for-dev-package-imports.ts @@ -26,6 +26,12 @@ import { setTimeout as sleep } from "timers/promises"; // This probe waits only for the package imports that the backend-side generator // needs. It does not hide real runtime errors: we retry missing-module failures // while package builds warm up, and fail immediately for other import failures. +// +// In addition to workspace packages, the probe checks that the generated Prisma +// client exists. When `turbo run dev` starts the backend, `codegen-prisma:watch` +// (`prisma generate --watch`) performs an initial generation that briefly removes +// and recreates `src/generated/prisma/`. If `codegen-docs` runs during that +// window it fails with ERR_MODULE_NOT_FOUND for `@/generated/prisma/client`. const repoRoot = path.resolve(__dirname, ".."); const backendDir = path.join(repoRoot, "apps/backend"); const timeoutMs = 60_000; @@ -35,6 +41,13 @@ const probeScript = ` (async () => { await import('@stackframe/stack'); await import('@stackframe/stack-shared/dist/utils/env'); + const { existsSync, readdirSync } = await import('node:fs'); + const { join } = await import('node:path'); + const generatedDir = join(process.cwd(), 'src', 'generated', 'prisma'); + if (!existsSync(generatedDir) || readdirSync(generatedDir).length === 0) { + const err = new Error('ERR_MODULE_NOT_FOUND: Generated Prisma client not yet available at ' + generatedDir); + throw err; + } })().then( () => undefined, (error) => { From 05ccfee789b675eb0da7d8f1349f79289fa7ff97 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 21:19:13 +0000 Subject: [PATCH 4/4] fix: also add dashboard filter to smoke test job pnpm install Co-Authored-By: Konstantin Wohlwend --- .github/workflows/qemu-emulator-build.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/qemu-emulator-build.yaml b/.github/workflows/qemu-emulator-build.yaml index e72f561d57..1989b6269f 100644 --- a/.github/workflows/qemu-emulator-build.yaml +++ b/.github/workflows/qemu-emulator-build.yaml @@ -269,10 +269,8 @@ jobs: - name: Install stack-cli deps + build run: | - pnpm install --frozen-lockfile --filter '@stackframe/stack-cli...' - # Turbo's trailing `...` filter builds stack-cli AND its workspace - # deps (@stackframe/js, @stackframe/stack-shared, etc.) — stack-cli - # imports them at runtime from their dist/ outputs. + # See "Build stack-cli" step comment for why dashboard filter is needed + pnpm install --frozen-lockfile --filter '@stackframe/stack-cli...' --filter '@stackframe/dashboard...' pnpm exec turbo run build --filter='@stackframe/stack-cli...' - name: Download built image