Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0842746
fix(e2e): increase sendSignInCode email polling timeout from ~20s to 60s
devin-ai-integration[bot] May 19, 2026
e786c28
fix(e2e): add ClickHouse retry logic and use fastSignUp in session-re…
devin-ai-integration[bot] May 19, 2026
ee7a20c
fix(e2e): increase ClickHouse polling window and use fastSignUp in to…
devin-ai-integration[bot] May 19, 2026
aef17f8
fix(e2e): wait for all expected country data before snapshot in inter…
devin-ai-integration[bot] May 19, 2026
551b360
fix(e2e): use fastSignUp in analytics-events cross-project isolation …
devin-ai-integration[bot] May 19, 2026
ad6502e
fix(e2e): skip email wait in sessions cross-user isolation test
devin-ai-integration[bot] May 19, 2026
54810df
fix(e2e): replace fixed 12s wait with polling loop in delivery-info s…
devin-ai-integration[bot] May 19, 2026
37e9b47
fix(e2e): skip email wait in unsubscribe-link transactional test
devin-ai-integration[bot] May 19, 2026
6f4a935
fix(backend): increase test timeout from 20s to 60s for backend unit …
devin-ai-integration[bot] May 19, 2026
c28ad24
fix(e2e): parallelize email waits in items test and increase timeout …
devin-ai-integration[bot] May 19, 2026
10c320c
Add instrumentation to email pipeline for diagnosing slow emails
devin-ai-integration[bot] May 20, 2026
33927b7
Merge branch 'dev' into devin/1779233168-fix-flaky-tests
N2D4 May 20, 2026
1b61d60
Remove email pipeline instrumentation code
devin-ai-integration[bot] May 20, 2026
01b15c2
Update E2E test snapshots for is_development_environment field
devin-ai-integration[bot] May 20, 2026
da54168
Fix config.tsx test: spy on correct function for dev environment check
devin-ai-integration[bot] May 20, 2026
8527cab
Fix lint: remove unnecessary conditional on always-truthy props.reset
devin-ai-integration[bot] May 20, 2026
db6cdc1
Fix remaining snapshot and message mismatches after dev merge
devin-ai-integration[bot] May 20, 2026
5a4e972
Merge branch 'dev' into devin/1779233168-fix-flaky-tests
N2D4 May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions apps/backend/src/lib/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1091,7 +1091,12 @@ import.meta.vitest?.test('setEnvironmentConfigOverride blocks writes for develop

const developmentEnvironment = await import("./development-environment");

const isDevelopmentEnvironmentProjectSpy = vi.spyOn(developmentEnvironment, "isDevelopmentEnvironmentProject").mockResolvedValue(true);
// Spy on getEnvironmentConfigWriteBlockReason directly, because spying on
// isDevelopmentEnvironmentProject does not intercept intra-module calls
// (the function is called directly within the same module, not through
// the module namespace export).
const spy = vi.spyOn(developmentEnvironment, "getEnvironmentConfigWriteBlockReason")
.mockResolvedValue(DEVELOPMENT_ENVIRONMENT_ENV_CONFIG_BLOCKED_MESSAGE);

try {
await expect(setEnvironmentConfigOverride({
Expand All @@ -1100,7 +1105,7 @@ import.meta.vitest?.test('setEnvironmentConfigOverride blocks writes for develop
environmentConfigOverride: {},
})).rejects.toThrow(DEVELOPMENT_ENVIRONMENT_ENV_CONFIG_BLOCKED_MESSAGE);
} finally {
isDevelopmentEnvironmentProjectSpy.mockRestore();
spy.mockRestore();
}
});

Expand Down
2 changes: 0 additions & 2 deletions apps/backend/src/lib/email-queue-step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -734,7 +734,6 @@ async function processSingleEmail(context: TenancyProcessingContext, row: EmailO
return;
}
}

const result = getEnvBoolean("STACK_EMAIL_BRANCHING_DISABLE_QUEUE_SENDING")
? Result.error({ errorType: "email-sending-disabled", canRetry: false, message: "Email sending is disabled", rawError: new Error("Email sending is disabled") })
: await lowLevelSendEmailDirectWithoutRetries({
Expand All @@ -745,7 +744,6 @@ async function processSingleEmail(context: TenancyProcessingContext, row: EmailO
html: row.renderedHtml ?? undefined,
text: row.renderedText ?? undefined,
});

if (result.status === "error") {
const newAttemptCount = row.sendRetries + 1;
const isAttemptsExhausted = result.error.canRetry && newAttemptCount >= MAX_SEND_ATTEMPTS;
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default mergeConfig(
sharedConfig,
defineConfig({
test: {
testTimeout: 20000,
testTimeout: 60000,
env: {
...loadEnv('', process.cwd(), ''),
...loadEnv('development', process.cwd(), ''),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1630,9 +1630,9 @@ function Draggable(props: {
<div className="text-red-500 text-sm p-2 bg-red-500/10 font-mono whitespace-pre-wrap">
A runtime error occured while rendering this widget.<br />
<br />
{props.reset && <button className="text-blue-500 hover:underline" onClick={() => {
props.reset!();
}}>Reload widget</button>}<br />
<button className="text-blue-500 hover:underline" onClick={() => {
props.reset();
}}>Reload widget</button><br />
<br />
{errorToNiceString(props.error)}
</div>
Expand Down
12 changes: 7 additions & 5 deletions apps/e2e/tests/backend/backend-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,20 +483,22 @@ export namespace Auth {
"headers": Headers { <some fields may have been hidden> },
}
`);
for (let i = 0; true; i++) {
const deadlineMs = 60_000;
const intervalMs = 500;
const deadline = performance.now() + deadlineMs;
while (true) {
const messages = await mailbox.fetchMessages();
const containsSubstring = messages.some(message => message.subject.includes("Sign in to") && message.body?.html.includes(response.body.nonce));
if (containsSubstring) {
break;
}
await wait(100 + i * 20);
if (i >= 40) {
throw new StackAssertionError(`Sign-in code message not found after ${i} attempts`, {
if (performance.now() >= deadline) {
throw new StackAssertionError(`Sign-in code message not found within ${deadlineMs}ms`, {
response,
messages: messages.map(m => ({ ...m, body: m.body && omit(m.body, ["html"]) })),
outboxEmails: await getOutboxEmails(),
});
}
await wait(intervalMs);
}
return {
sendSignInCodeResponse: response,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,11 @@ it("stores $token-refresh data in snake_case without row identity fields", async
it("cannot read events from other projects", async ({ expect }) => {
await Project.createAndSwitch({ config: { magic_link_enabled: true } });
const projectAKeys = backendContext.value.projectKeys;
await Auth.Otp.signIn();
await Auth.fastSignUp();

// Switch to another project and generate its own event
await Project.createAndSwitch({ config: { magic_link_enabled: true } });
const { userId: projectBUserId } = await Auth.Otp.signIn();
const { userId: projectBUserId } = await Auth.fastSignUp();
const projectBResponse = await fetchEventsWithRetry({
userId: projectBUserId,
eventType: "$token-refresh",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,12 +263,12 @@ it("cannot delete current session as client", async ({ expect }) => {
});

it("cannot read another user's sessions as client", async ({ expect }) => {
// Create first user and sign up
const user1 = await Auth.Password.signUpWithEmail();
// Create first user and sign up (skip email wait — not needed for this test)
const user1 = await Auth.Password.signUpWithEmail({ noWaitForEmail: true });

// Create second user and sign up
backendContext.set({ userAuth: null, mailbox: createMailbox() }); // Clear first user's auth
const user2 = await Auth.Password.signUpWithEmail();
const user2 = await Auth.Password.signUpWithEmail({ noWaitForEmail: true });

// Try to read user1's sessions while authenticated as user2
const listResponse = await niceBackendFetch("/api/v1/auth/sessions", {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ describe("with valid credentials", () => {
});

it("should track sent emails", async ({ expect }) => {
await Auth.Otp.signIn();
await Auth.fastSignUp();
await Project.createAndSwitch({
display_name: "Test Sent Stats Project",
config: {
Expand Down Expand Up @@ -167,50 +167,32 @@ describe("with valid credentials", () => {
}
`);

// wait for the email to be processed
await wait(12_000);

const response = await niceBackendFetch("/api/v1/emails/delivery-info", {
method: "GET",
accessType: "server",
});

expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"capacity": {
"boost_expires_at": null,
"boost_multiplier": 1,
"is_boost_active": false,
"penalty_factor": 1,
"rate_per_second": 27.777779320987655,
},
"stats": {
"day": {
"bounced": 0,
"marked_as_spam": 0,
"sent": 1,
},
"hour": {
"bounced": 0,
"marked_as_spam": 0,
"sent": 1,
},
"month": {
"bounced": 0,
"marked_as_spam": 0,
"sent": 1,
},
"week": {
"bounced": 0,
"marked_as_spam": 0,
"sent": 1,
},
},
},
"headers": Headers { <some fields may have been hidden> },
// Poll until email stats are updated instead of a fixed wait
let response;
for (let i = 0; i < 30; i++) {
await wait(1_000);
response = await niceBackendFetch("/api/v1/emails/delivery-info", {
method: "GET",
accessType: "server",
});
if (response.status === 200 && response.body?.stats?.hour?.sent >= 1) {
break;
}
`);
}
Comment on lines +170 to +181
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Polling loop waits before first check and gives no timeout error

The loop unconditionally waits 1 s before ever issuing the first request, wasting time even when the email is already processed. More importantly, if the 30-iteration budget (30 s) is exhausted without the condition being met, the loop exits silently; the subsequent expect(stats.hour.sent).toBe(1) then produces a confusing assertion failure rather than a clear timeout message. waitForMetricsMatch in the same test suite throws a descriptive error on timeout — a similar pattern here would make failures easier to diagnose.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/e2e/tests/backend/endpoints/api/v1/emails/delivery-info.test.ts
Line: 170-181

Comment:
**Polling loop waits before first check and gives no timeout error**

The loop unconditionally waits 1 s before ever issuing the first request, wasting time even when the email is already processed. More importantly, if the 30-iteration budget (30 s) is exhausted without the condition being met, the loop exits silently; the subsequent `expect(stats.hour.sent).toBe(1)` then produces a confusing assertion failure rather than a clear timeout message. `waitForMetricsMatch` in the same test suite throws a descriptive error on timeout — a similar pattern here would make failures easier to diagnose.

How can I resolve this? If you propose a fix, please make it concise.


expect(response!.status).toBe(200);
const { stats, capacity } = response!.body;
expect(stats.hour.sent).toBe(1);
expect(stats.day.sent).toBe(1);
expect(stats.week.sent).toBe(1);
expect(stats.month.sent).toBe(1);
expect(stats.hour.bounced).toBe(0);
expect(stats.hour.marked_as_spam).toBe(0);
expect(capacity.is_boost_active).toBe(false);
expect(capacity.boost_expires_at).toBe(null);
expect(capacity.boost_multiplier).toBe(1);
expect(capacity.penalty_factor).toBe(1);
expect(capacity.rate_per_second).toBeGreaterThan(27);
expect(capacity.rate_per_second).toBeLessThan(28);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ it("should be able to provision a new project if client details are correct", as
"description": "Project created by an external integration",
"display_name": "Test project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ it("lists oauth providers", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ it("get project details", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down Expand Up @@ -98,6 +99,7 @@ it("creates and updates the basic project information of a project", async ({ ex
"description": "Updated description",
"display_name": "Updated Project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": true,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down Expand Up @@ -172,6 +174,7 @@ it("creates and updates the email config of a project", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ it("should be able to provision a new project if neon client details are correct
"description": "Created with Neon",
"display_name": "Test project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,12 @@ it("should return metrics data with users", async ({ expect }) => {
backendContext.set({ mailbox: mailboxes[2], ipData: { country: "CH", ipAddress: "127.0.0.1", city: "Zurich", region: "ZH", latitude: 47.3769, longitude: 8.5417, tzIdentifier: "Europe/Zurich" } });
await Auth.Otp.signIn();

const response = await waitForMetricsToIncludeUsersByCountry({ countryCode: "CH", expectedCount: 1 });
const response = await waitForMetricsMatch(
false,
(r) =>
r.body?.users_by_country?.["CH"] === 1 &&
(r.body?.active_users_by_country?.["AQ"]?.length ?? 0) >= 2,
);
expect(response).toMatchSnapshot(`metrics_result_with_users`);

await ensureAnonymousUsersAreStillExcluded(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { it } from "../../../../../helpers";
import { backendContext, niceBackendFetch } from "../../../../backend-helpers";

const isLocalEmulator = process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR === "true";
const blockedMessage = "cannot be changed in the local emulator";
const blockedMessage = "cannot be changed in a development environment";
const localEmulatorProjectEndpoint = "/api/v1/internal/local-emulator/project";

async function switchToLocalEmulatorProject() {
Expand Down
12 changes: 12 additions & 0 deletions apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ it("creates a new project", async ({ expect }) => {
"description": "",
"display_name": "Test Project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down Expand Up @@ -163,6 +164,7 @@ it("creates a new project with different configurations", async ({ expect }) =>
"description": "Test description",
"display_name": "Test Project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": true,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down Expand Up @@ -222,6 +224,7 @@ it("creates a new project with different configurations", async ({ expect }) =>
"description": "",
"display_name": "Test Project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down Expand Up @@ -272,6 +275,7 @@ it("creates a new project with different configurations", async ({ expect }) =>
"description": "",
"display_name": "Test Project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down Expand Up @@ -336,6 +340,7 @@ it("creates a new project with different configurations", async ({ expect }) =>
"description": "",
"display_name": "Test Project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down Expand Up @@ -402,6 +407,7 @@ it("creates a new project with different configurations", async ({ expect }) =>
"description": "",
"display_name": "Test Project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down Expand Up @@ -451,6 +457,7 @@ it("lists the current projects after creating a new project", async ({ expect })
"description": "",
"display_name": "New Project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down Expand Up @@ -506,6 +513,7 @@ it("verifies email_theme update persists", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down Expand Up @@ -550,6 +558,7 @@ it("verifies email_theme update persists", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down Expand Up @@ -604,6 +613,7 @@ it("updates trusted domains without modifying allow_localhost", async ({ expect
"description": "",
"display_name": "New Project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down Expand Up @@ -661,6 +671,7 @@ it("updates trusted domains without modifying allow_localhost", async ({ expect
"description": "",
"display_name": "New Project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down Expand Up @@ -735,6 +746,7 @@ it("lets user update logo_url to a valid image", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "<stripped UUID>",
"is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,7 @@ async function createLiveModeSubscriptionWithRenewal(): Promise<{
};
}

it("refunds a renewal invoice (invoice_id path) without money or revoke — sourceTxnId is sub-renewal", async () => {
it("refunds a renewal invoice (invoice_id path) without money or revoke — sourceTxnId is sub-renewal", { timeout: 120_000 }, async () => {
const { subscriptionId, renewalInvoiceId } = await createLiveModeSubscriptionWithRenewal();

// Use amount_usd=0 and end_action='at-period-end' to exercise the
Expand Down Expand Up @@ -964,7 +964,7 @@ it("refunds a renewal invoice (invoice_id path) without money or revoke — sour
expect(refundRow).toBeDefined();
});

it("rejects end_action='now' when invoice_id targets a renewal invoice", async () => {
it("rejects end_action='now' when invoice_id targets a renewal invoice", { timeout: 120_000 }, async () => {
// The product grant lives on the sub-start txn, not on renewals — so a
// revocation entry referencing a renewal would point at a non-existent
// entry. Force admin to end-immediately against the start invoice (or the
Expand All @@ -987,7 +987,7 @@ it("rejects end_action='now' when invoice_id targets a renewal invoice", async (
expect(refundRes.body.error).toMatch(/Cannot end product access immediately when refunding a renewal invoice/);
});

it("rejects refund with invoice_id that does not belong to the subscription", async () => {
it("rejects refund with invoice_id that does not belong to the subscription", { timeout: 120_000 }, async () => {
const { subscriptionId } = await createLiveModeSubscriptionWithRenewal();
const unrelatedInvoiceId = randomUUID();

Expand Down
Loading
Loading