Skip to content

Commit c9233e8

Browse files
committed
test(e2e): repair self-host scenarios and gate the suite in CI
The self-host e2e project never ran in CI, so it drifted red while the app moved on. Repair the failing scenarios (stale connect-modal selectors, a racy action-bar position read, a shared-admin connection-count assertion, a multi-tenant-only org-slug 404 step, and a cloud-shaped toolkit MCP URL), add a documented skip affordance to the scenario helper, and quarantine the two Microsoft emulator scenarios that need a canonical block-YAML Graph spec (tracked separately). Cherry-picked from origin/fix-selfhost-e2e-and-ci (PR #1239); its CI job is superseded by the cloud+selfhost matrix job already on this branch.
1 parent e663495 commit c9233e8

13 files changed

Lines changed: 242 additions & 33 deletions

apps/cloud/src/routeTree.gen.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -411,15 +411,11 @@ export const routeTree = rootRouteImport
411411
._addFileTypes<FileRouteTypes>()
412412

413413
import type { getRouter } from './router.tsx'
414-
415414
import type { startInstance } from './start.ts'
416-
417415
declare module '@tanstack/react-start' {
418416
interface Register {
419417
ssr: true
420-
421418
router: Awaited<ReturnType<typeof getRouter>>
422-
423419
config: Awaited<ReturnType<typeof startInstance.getOptions>>
424420
}
425421
}

e2e/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const coreApi = composePluginApi([] as const); // tools/integrations/connections
4040

4141
scenario(
4242
"Tools · a fresh workspace advertises the built-in tools",
43-
{}, // options: { timeout?: number }
43+
{}, // options: { timeout?: number; skip?: string (reason — registers as skipped) }
4444
Effect.gen(function* () {
4545
const target = yield* Target;
4646
const { client } = yield* Api;

e2e/scenarios/api-tools.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@ scenario(
3131
const { client } = yield* Api;
3232
const identity = yield* target.newIdentity();
3333
const api = yield* client(coreApi, identity);
34+
// The list call itself exercises the endpoint on every target (a failure
35+
// fails the test). Only isolated-identity targets (a fresh org per identity)
36+
// can additionally guarantee the list is empty. Selfhost shares one
37+
// bootstrap admin, so other scenarios' connections legitimately appear here;
38+
// asserting a global count there is exactly what e2e/AGENTS.md forbids.
3439
const connections = yield* api.connections.list({ query: {} });
35-
expect(connections.length, "no connections leak across identities").toBe(0);
40+
if (target.name === "selfhost") return;
41+
expect(connections.length, "a fresh org starts with no connections").toBe(0);
3642
}),
3743
);

e2e/scenarios/connect-handoff-session.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,9 @@ scenario(
152152
.waitFor({ timeout: 15_000 });
153153
});
154154
await step("Paste the Resend API key and connect", async () => {
155-
const credential = page.getByPlaceholder(/paste the value \/ token/i);
155+
// Affixed single-input bearer field: value input placeholder is
156+
// "token" (scoped to the dialog to stay unique).
157+
const credential = page.getByRole("dialog").getByPlaceholder("token");
156158
await credential.waitFor({ timeout: 15_000 });
157159
await credential.fill(apiKey);
158160
await page.getByRole("button", { name: "Add connection", exact: true }).click();

e2e/scenarios/connect-handoff.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,10 @@ const runScenario = (input: {
209209
});
210210

211211
await step("Paste the emulator API key", async () => {
212-
const credential = page.getByPlaceholder(/paste the value \/ token/i);
212+
// The single-input bearer method renders an affixed field ("Authorization:
213+
// Bearer " prefix) whose value input placeholder is "token". Scope to the
214+
// dialog so the match stays unique.
215+
const credential = page.getByRole("dialog").getByPlaceholder("token");
213216
await credential.waitFor({ timeout: 15_000 });
214217
await credential.fill(apiKey);
215218
});

e2e/scenarios/microsoft-emulator.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,18 @@ return { ok: result.ok, path: item.path, result: result.ok ? result.data : resul
8080

8181
scenario(
8282
"Microsoft · client credentials against the emulator mint a Graph connection and call /users",
83-
{ timeout: 180_000 },
83+
{
84+
// Blocked (pre-existing, not this PR): `microsoft.addGraph` only accepts the
85+
// canonical Graph spec in the streamable block-YAML profile — it structurally
86+
// splits the doc to avoid OOMing the 128MB Workers isolate on the real 37MB
87+
// spec (packages/plugins/microsoft/src/sdk/graph.ts), and hard-errors on
88+
// anything else. The @executor-js/emulate Microsoft emulator serves a small
89+
// custom Graph spec that isn't in that profile, so addGraph rejects it. Fix
90+
// needs the emulator to serve a block-YAML-profile Graph spec (or a
91+
// non-Workers compile path); tracked separately.
92+
skip: "microsoft.addGraph requires the canonical block-YAML Graph spec; the emulator spec is not in that profile",
93+
timeout: 180_000,
94+
},
8495
Effect.scoped(
8596
Effect.gen(function* () {
8697
const target = yield* Target;

e2e/scenarios/oauth-client-handoff.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,18 @@ const requireOAuthClientCredential = (credential: IssuedCredential) =>
310310

311311
scenario(
312312
"OAuth client · agent hands off, the human enters the secret in the browser, and the app connects",
313-
{ timeout: 240_000 },
313+
{
314+
// Blocked (pre-existing, not this PR): this scenario drives the handoff
315+
// through `microsoft.addGraph`, which only accepts the canonical Graph spec
316+
// in the streamable block-YAML profile (structural split to avoid OOMing the
317+
// 128MB Workers isolate on the 37MB doc — packages/plugins/microsoft/src/sdk/
318+
// graph.ts). The @executor-js/emulate Microsoft emulator serves a small spec
319+
// outside that profile, so addGraph hard-errors. The other two OAuth-client
320+
// scenarios in this file (createHandoff, approval-gating) do not touch Graph
321+
// and pass. Fix needs a block-YAML-profile emulator spec; tracked separately.
322+
skip: "drives microsoft.addGraph, which requires the canonical block-YAML Graph spec the emulator does not serve",
323+
timeout: 240_000,
324+
},
314325
Effect.gen(function* () {
315326
const target = yield* Target;
316327
const { client: makeApiClient } = yield* Api;

e2e/scenarios/openapi-add-integration-action-bar.test.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,15 @@ scenario(
5252
});
5353

5454
await step(
55-
"Submitting does not reflow the bar, then lands on the integration",
55+
"Submitting commits the source and lands on the created integration",
5656
async () => {
5757
// The reported ghost was the bar painting doubled when the submit
58-
// button changed width on click. With a stable-width loading button the
59-
// row must not move: Cancel stays put while the add is in flight.
60-
const cancel = page.getByRole("button", { name: "Cancel" });
61-
const before = await cancel.boundingBox();
58+
// button changed width on click. The single-node counts (above and
59+
// below) are the hard regression cover for that; the floating action
60+
// bar unmounts the instant the router navigates, so there is no
61+
// reliable in-flight frame to measure its position without racing the
62+
// teardown. Assert the submit completes and lands on the integration.
6263
await page.getByRole("button", { name: "Add integration" }).click();
63-
// The submit button marks itself data-loading synchronously on click.
64-
await page
65-
.locator('[data-slot="button"][data-loading]')
66-
.first()
67-
.waitFor({ timeout: 5_000 });
68-
const during = await cancel.boundingBox();
69-
expect(Math.round(during?.x ?? -1), "Cancel does not move when submitting").toBe(
70-
Math.round(before?.x ?? -2),
71-
);
7264
await page.waitForURL(/\/integrations\/(?!add\b)[^/?]+$/, { timeout: 30_000 });
7365
await page.getByText("Connections").first().waitFor();
7466
},

e2e/scenarios/org-slug-routing.test.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,17 @@ scenario(
4545
await page.getByText("Policies").first().waitFor();
4646
});
4747

48-
await step("An unknown org slug is a wrong address, not a redirect", async () => {
49-
await page.goto("/zz-no-such-org/policies", { waitUntil: "networkidle" });
50-
await page.getByText("Page not found").waitFor({ timeout: 30_000 });
51-
});
48+
// The "unknown slug is a 404" contract is multi-tenant only. Selfhost is
49+
// single-tenant: /account/me always returns the instance org regardless of
50+
// the URL segment, so the slug is cosmetic and an unknown one canonicalizes
51+
// onto the shell rather than 404ing. Cloud enforces the not-found; selfhost
52+
// legitimately does not.
53+
if (target.name !== "selfhost") {
54+
await step("An unknown org slug is a wrong address, not a redirect", async () => {
55+
await page.goto("/zz-no-such-org/policies", { waitUntil: "networkidle" });
56+
await page.getByText("Page not found").waitFor({ timeout: 30_000 });
57+
});
58+
}
5259

5360
await step("In-shell navigation keeps the slug prefix", async () => {
5461
await page.goto(`/${slug}`, { waitUntil: "networkidle" });

e2e/selfhost/auth-methods-ui.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,9 @@ scenario(
142142
});
143143

144144
await step("Connect through the new method", async () => {
145-
await page.getByPlaceholder("paste the value / token").fill(token);
145+
// Custom "Authorization: Bearer " method renders the affixed field,
146+
// whose value input placeholder is "token".
147+
await page.getByRole("dialog").getByPlaceholder("token").fill(token);
146148
await page.getByRole("button", { name: "Add connection" }).click();
147149
await page.getByText("Connection added").waitFor();
148150
});

0 commit comments

Comments
 (0)