// Cloud (browser): abandoning an OAuth connection must not wedge the// add-connection modal. A user picks a registered OAuth app, clicks "Connect// with OAuth" (the modal opens the provider popup and flips to a busy// "Connecting…" state), then bails by closing the popup without granting consent.// The popup-closed signal is intentionally not polled (providers' COOP headers// make `popup.closed` unreliable), so the modal can't detect the abandonment on// its own. The guarantee under test: closing the modal afterwards RESETS it, so// reopening offers a fresh attempt instead of staying stuck on "Connecting…".//// Repro for the user report: "I bailed on finishing the OAuth connection … the// Executor app can't detect [it]. But closing the modal should reset the state// so I can try again."import { randomBytes } from "node:crypto"; import { expect } from "@effect/vitest";import { Effect } from "effect";import { composePluginApi } from "@executor-js/api/server";import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";import { IntegrationSlug, OAuthClientSlug } from "@executor-js/sdk/shared";import { serveOAuthTestServer } from "@executor-js/sdk/testing"; import { scenario } from "../src/scenario";import { Api, Browser, Target } from "../src/services"; const api = composePluginApi([openApiHttpPlugin()] as const); const unique = (prefix: string) => `${prefix}_${randomBytes(4).toString("hex")}`; scenario( "Connections · closing the add-connection modal after abandoning OAuth lets you try again (not stuck on Connecting)", { timeout: 120_000 }, Effect.scoped( Effect.gen(function* () { const target = yield* Target; const browser = yield* Browser; const { client: makeClient } = yield* Api; // A real authorization server on 127.0.0.1: the modal's popup navigates to // its authorize page, which we abandon by closing the window. const oauth = yield* serveOAuthTestServer(); const identity = yield* target.newIdentity(); const client = yield* makeClient(api, identity); // An integration that declares an OAuth auth method (no DCR: it carries // explicit endpoints, no registration/discovery URL), so the modal shows // the bring-your-own app picker. const integration = IntegrationSlug.make(unique("oauthint")); yield* client.openapi.addSpec({ payload: { spec: { kind: "blob", value: JSON.stringify({ openapi: "3.0.3", info: { title: "OAuth-protected API", version: "1.0.0" }, paths: { "/me": { get: { operationId: "getMe", tags: ["default"], responses: { "200": { description: "the caller" } }, }, }, }, }), }, slug: integration, baseUrl: "http://127.0.0.1:59999", authenticationTemplate: [ { slug: "oauth", kind: "oauth2", authorizationUrl: oauth.authorizationEndpoint, tokenUrl: oauth.tokenEndpoint, scopes: ["read"], }, ], }, }); // A registered OAuth app whose endpoints match the integration's, so the // picker auto-selects it and the footer offers "Connect with OAuth". const clientSlug = OAuthClientSlug.make(unique("oauthc")); yield* client.oauth.createClient({ payload: { owner: "org", slug: clientSlug, authorizationUrl: oauth.authorizationEndpoint, tokenUrl: oauth.tokenEndpoint, grant: "authorization_code", clientId: "test-client", clientSecret: "test-secret", }, }); yield* browser.session(identity, async ({ page, step }) => { const dialog = page.getByRole("dialog"); const addConnection = page.getByRole("button", { name: "Add connection", exact: true }); const connectWithOAuth = dialog.getByRole("button", { name: "Connect with OAuth" }); const connecting = dialog.getByRole("button", { name: "Connecting…" }); await step("Open the integration and start a new connection", async () => { await page.goto(`/integrations/${integration}`, { waitUntil: "networkidle" }); await addConnection.click(); // The registered app is auto-selected, so the OAuth connect button is // present and enabled. await connectWithOAuth.waitFor({ state: "visible", timeout: 15_000 }); expect( await connectWithOAuth.isDisabled(), "the auto-selected app makes Connect with OAuth actionable", ).toBe(false); }); await step("Begin OAuth, then bail by closing the provider popup", async () => { const [popup] = await Promise.all([page.waitForEvent("popup"), connectWithOAuth.click()]); // The footer flips to the busy "Connecting…" state while the popup is // open; the flow is genuinely in flight. await connecting.waitFor({ state: "visible", timeout: 15_000 }); // Let the popup actually reach the authorize page so the OAuth session // is live, then abandon it: the user closes the window without // granting consent. await popup.waitForURL((url) => !url.href.startsWith("about:"), { timeout: 15_000 }); await popup.close(); }); await step("Close the modal", async () => { // The Close button is disabled while busy, so the user backs out with // Escape, exactly the "bail" path from the report. await page.keyboard.press("Escape"); await dialog.waitFor({ state: "hidden", timeout: 15_000 }); }); await step( "Reopen the modal: it offers a fresh attempt, not a stuck Connecting", async () => { await addConnection.click(); await dialog.waitFor({ state: "visible", timeout: 15_000 }); await page.waitForLoadState("networkidle"); // The guarantee: the reopened modal is reset. Before the fix it stays // wedged on "Connecting…" (the abandoned flow's busy state survived the // close), so this count is 1 and the test fails, reproducing the bug. expect( await connecting.count(), "the reopened modal must not be stuck in the Connecting state", ).toBe(0); // And a fresh OAuth attempt is actually offered and actionable again. await connectWithOAuth.waitFor({ state: "visible", timeout: 15_000 }); expect( await connectWithOAuth.isDisabled(), "the reopened modal lets the user start OAuth again", ).toBe(false); }, ); }); }), ),);
// Cloud (browser): abandoning an OAuth connection must not wedge the
// add-connection modal. A user picks a registered OAuth app, clicks "Connect
// with OAuth" (the modal opens the provider popup and flips to a busy
// "Connecting…" state), then bails by closing the popup without granting consent.
// The popup-closed signal is intentionally not polled (providers' COOP headers
// make `popup.closed` unreliable), so the modal can't detect the abandonment on
// its own. The guarantee under test: closing the modal afterwards RESETS it, so
// reopening offers a fresh attempt instead of staying stuck on "Connecting…".
//
// Repro for the user report: "I bailed on finishing the OAuth connection … the
// Executor app can't detect [it]. But closing the modal should reset the state
// so I can try again."
import { randomBytes } from "node:crypto";
import { expect } from "@effect/vitest";
import { Effect } from "effect";
import { composePluginApi } from "@executor-js/api/server";
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
import { IntegrationSlug, OAuthClientSlug } from "@executor-js/sdk/shared";
import { serveOAuthTestServer } from "@executor-js/sdk/testing";
import { scenario } from "../src/scenario";
import { Api, Browser, Target } from "../src/services";
const api = composePluginApi([openApiHttpPlugin()] as const);
const unique = (prefix: string) => `${prefix}_${randomBytes(4).toString("hex")}`;
scenario(
"Connections · closing the add-connection modal after abandoning OAuth lets you try again (not stuck on Connecting)",
{ timeout: 120_000 },
Effect.scoped(
Effect.gen(function* () {
const target = yield* Target;
const browser = yield* Browser;
const { client: makeClient } = yield* Api;
// A real authorization server on 127.0.0.1: the modal's popup navigates to
// its authorize page, which we abandon by closing the window.
const oauth = yield* serveOAuthTestServer();
const identity = yield* target.newIdentity();
const client = yield* makeClient(api, identity);
// An integration that declares an OAuth auth method (no DCR: it carries
// explicit endpoints, no registration/discovery URL), so the modal shows
// the bring-your-own app picker.
const integration = IntegrationSlug.make(unique("oauthint"));
yield* client.openapi.addSpec({
payload: {
spec: {
kind: "blob",
value: JSON.stringify({
openapi: "3.0.3",
info: { title: "OAuth-protected API", version: "1.0.0" },
paths: {
"/me": {
get: {
operationId: "getMe",
tags: ["default"],
responses: { "200": { description: "the caller" } },
},
},
},
}),
},
slug: integration,
baseUrl: "http://127.0.0.1:59999",
authenticationTemplate: [
{
slug: "oauth",
kind: "oauth2",
authorizationUrl: oauth.authorizationEndpoint,
tokenUrl: oauth.tokenEndpoint,
scopes: ["read"],
},
],
},
});
// A registered OAuth app whose endpoints match the integration's, so the
// picker auto-selects it and the footer offers "Connect with OAuth".
const clientSlug = OAuthClientSlug.make(unique("oauthc"));
yield* client.oauth.createClient({
payload: {
owner: "org",
slug: clientSlug,
authorizationUrl: oauth.authorizationEndpoint,
tokenUrl: oauth.tokenEndpoint,
grant: "authorization_code",
clientId: "test-client",
clientSecret: "test-secret",
},
});
yield* browser.session(identity, async ({ page, step }) => {
const dialog = page.getByRole("dialog");
const addConnection = page.getByRole("button", { name: "Add connection", exact: true });
const connectWithOAuth = dialog.getByRole("button", { name: "Connect with OAuth" });
const connecting = dialog.getByRole("button", { name: "Connecting…" });
await step("Open the integration and start a new connection", async () => {
await page.goto(`/integrations/${integration}`, { waitUntil: "networkidle" });
await addConnection.click();
// The registered app is auto-selected, so the OAuth connect button is
// present and enabled.
await connectWithOAuth.waitFor({ state: "visible", timeout: 15_000 });
expect(
await connectWithOAuth.isDisabled(),
"the auto-selected app makes Connect with OAuth actionable",
).toBe(false);
});
await step("Begin OAuth, then bail by closing the provider popup", async () => {
const [popup] = await Promise.all([page.waitForEvent("popup"), connectWithOAuth.click()]);
// The footer flips to the busy "Connecting…" state while the popup is
// open; the flow is genuinely in flight.
await connecting.waitFor({ state: "visible", timeout: 15_000 });
// Let the popup actually reach the authorize page so the OAuth session
// is live, then abandon it: the user closes the window without
// granting consent.
await popup.waitForURL((url) => !url.href.startsWith("about:"), { timeout: 15_000 });
await popup.close();
});
await step("Close the modal", async () => {
// The Close button is disabled while busy, so the user backs out with
// Escape, exactly the "bail" path from the report.
await page.keyboard.press("Escape");
await dialog.waitFor({ state: "hidden", timeout: 15_000 });
});
await step(
"Reopen the modal: it offers a fresh attempt, not a stuck Connecting",
async () => {
await addConnection.click();
await dialog.waitFor({ state: "visible", timeout: 15_000 });
await page.waitForLoadState("networkidle");
// The guarantee: the reopened modal is reset. Before the fix it stays
// wedged on "Connecting…" (the abandoned flow's busy state survived the
// close), so this count is 1 and the test fails, reproducing the bug.
expect(
await connecting.count(),
"the reopened modal must not be stuck in the Connecting state",
).toBe(0);
// And a fresh OAuth attempt is actually offered and actionable again.
await connectWithOAuth.waitFor({ state: "visible", timeout: 15_000 });
expect(
await connectWithOAuth.isDisabled(),
"the reopened modal lets the user start OAuth again",
).toBe(false);
},
);
});
}),
),
);