Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions apps/cloud/executor.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ interface CloudPluginDeps {
readonly workosVaultClient?: WorkOSVaultClient;
readonly activeToolkitSlug?: string;
/** Mirrors `HostConfig.allowLocalNetwork` (`ALLOW_LOCAL_NETWORK`): lets
* `microsoft.addGraph` point at a loopback emulator instead of the pinned
* Microsoft Graph URLs. Off by default; production leaves it unset. */
* custom Microsoft workload adds point at a loopback emulator instead of
* the pinned Microsoft Graph URLs. Off by default; production leaves it
* unset. */
readonly allowLocalNetwork?: boolean;
}

Expand Down
4 changes: 2 additions & 2 deletions apps/host-cloudflare/src/worker.e2e.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ describe("cloudflare host e2e (workerd/miniflare)", () => {
expect(integration?.slug).toBe(slug);
}, 60_000);

it("registers the Microsoft Graph API route in the runtime plugin set", async () => {
const res = await worker.fetch("/api/microsoft/graph", {
it("registers the Microsoft workloads API route in the runtime plugin set", async () => {
const res = await worker.fetch("/api/microsoft/workloads", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({}),
Expand Down
4 changes: 2 additions & 2 deletions apps/host-selfhost/executor.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import { resolveSecretKey } from "./src/config";
interface SelfHostPluginDeps {
readonly activeToolkitSlug?: string;
/** Mirrors `HostConfig.allowLocalNetwork` (EXECUTOR_ALLOW_LOCAL_NETWORK):
* lets `microsoft.addGraph` point at a loopback emulator instead of the
* pinned Microsoft Graph URLs. Off by default. */
* lets custom Microsoft workload adds point at a loopback emulator instead
* of the pinned Microsoft Graph URLs. Off by default. */
readonly allowLocalNetwork?: boolean;
}

Expand Down
76 changes: 46 additions & 30 deletions e2e/scenarios/google-per-service-add-ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
// not one bundled "google" source. This drives the whole browser path:
//
// 1. Open /integrations/add/google (the Google-owned add flow).
// 2. Clear the featured defaults, then check exactly Calendar + Gmail + Drive.
// 2. Clear the featured defaults, check Calendar + Gmail + Drive, and add a
// custom Tasks Discovery URL.
// 3. Submit via `google-add-submit`. The flow fetches each preset's real
// Google Discovery document (www.googleapis.com) and registers one
// integration per product.
// 4. The result panel shows three `add-result-row-*` rows, each data-state
// integration per product, plus one custom integration.
// 4. The result panel shows four `add-result-row-*` rows, each data-state
// "added", each with an Open link.
// 5. Follow one Open link: the integration detail page shows the per-service
// name ("Google Calendar"), proving the fan-out kept preset identities.
// 6. Back on the integrations list, all three separate integrations exist
// 6. Back on the integrations list, all four separate integrations exist
// (asserted by their distinct slugs in the list UI).
// 7. Re-open the add flow, check Calendar again, submit: its row now reports
// data-state "skipped" (the integration already exists), proving the
Expand All @@ -33,7 +34,7 @@ import { Effect } from "effect";

import { scenario } from "../src/scenario";
import { Browser, Target } from "../src/services";
import { setPresetChecked } from "./support/picker";
import { clearCheckedPresets, setPresetChecked } from "./support/picker";

scenario(
"Google · the per-service picker fans out to separate integrations and skips existing ones",
Expand All @@ -42,6 +43,7 @@ scenario(
const target = yield* Target;
const browser = yield* Browser;
const identity = yield* target.newIdentity();
const customDiscoveryUrl = "https://www.googleapis.com/discovery/v1/apis/tasks/v1/rest";

yield* browser.session(identity, async ({ page, step }) => {
await step("Open the Google add flow", async () => {
Expand All @@ -52,28 +54,34 @@ scenario(
await page.getByTestId("preset-checkbox-google-calendar").waitFor();
});

await step("Clear the defaults, then check exactly Calendar + Gmail + Drive", async () => {
// Clear every featured default, then select only the three under test.
const featuredDefaults = [
"google-calendar",
"google-gmail",
"google-sheets",
"google-drive",
"google-docs",
];
for (const presetId of featuredDefaults) {
await setPresetChecked(page, presetId, false);
}
await step(
"Clear defaults, check Calendar + Gmail + Drive, and add a custom URL",
async () => {
// Clear every featured default, then select only the three preset
// products under test.
await clearCheckedPresets(page);
for (const presetId of ["google-calendar", "google-gmail", "google-drive"]) {
await setPresetChecked(page, presetId, true);
}
// Everything else is unchecked: this is a scoped three-product selection.
expect(await page.getByTestId("preset-checkbox-google-sheets").isChecked()).toBe(false);
expect(await page.getByTestId("preset-checkbox-google-docs").isChecked()).toBe(false);

for (const presetId of ["google-calendar", "google-gmail", "google-drive"]) {
await setPresetChecked(page, presetId, true);
}
// Everything else is unchecked: this is a scoped three-product selection.
expect(await page.getByTestId("preset-checkbox-google-sheets").isChecked()).toBe(false);
expect(await page.getByTestId("preset-checkbox-google-docs").isChecked()).toBe(false);
});
await page
.getByPlaceholder(
"https://www.googleapis.com/discovery/v1/apis/<service>/<version>/rest",
)
.fill(customDiscoveryUrl);
await page
.getByPlaceholder(
"https://www.googleapis.com/discovery/v1/apis/<service>/<version>/rest",
)
.press("Enter");
await page.getByText(customDiscoveryUrl).waitFor();
},
);

await step("Submit the fan-out and see three added integrations", async () => {
await step("Submit the fan-out and see four added integrations", async () => {
await page.getByTestId("google-add-submit").click();
// The result panel appears once every product is registered. Discovery
// fetch + spec compile per product; allow generous time.
Expand All @@ -90,6 +98,15 @@ scenario(
// Each added row links out to its own integration page.
await row.getByRole("link", { name: "Open" }).waitFor();
}

const customRow = page.getByTestId("add-result-row-google_tasks");
await customRow.waitFor({ timeout: 120_000 });
expect(
await customRow.getAttribute("data-state"),
"the custom Discovery URL is added through the services endpoint",
).toBe("added");
await customRow.getByText("Google Tasks").waitFor();
await customRow.getByRole("link", { name: "Open" }).waitFor();
});

await step("Open one product: its integration page shows the per-service name", async () => {
Expand All @@ -108,27 +125,26 @@ scenario(
.waitFor({ timeout: 20_000 });
});

await step("The integrations list has three separate Google integrations", async () => {
await step("The integrations list has four separate Google integrations", async () => {
await page.goto("/integrations", { waitUntil: "networkidle" });
// Each fanned-out integration is its own list entry, keyed by its slug
// (the list renders the slug as each entry's description).
for (const slug of ["google_calendar", "google_gmail", "google_drive"]) {
for (const slug of ["google_calendar", "google_gmail", "google_drive", "google_tasks"]) {
await page
.getByRole("main")
.getByText(slug, { exact: true })
.first()
.waitFor({ timeout: 20_000 });
}
await page.getByRole("main").getByText("Google Tasks").first().waitFor({ timeout: 20_000 });
});

await step("Re-adding an existing product reports it as skipped", async () => {
await page.goto("/integrations/add/google", { waitUntil: "domcontentloaded" });
await page.getByText("Customize your Google connection").waitFor();

// Clear the featured defaults so only Calendar (already added) is checked.
for (const presetId of ["google-gmail", "google-sheets", "google-drive", "google-docs"]) {
await setPresetChecked(page, presetId, false);
}
await clearCheckedPresets(page);
await setPresetChecked(page, "google-calendar", true);

await page.getByTestId("google-add-submit").click();
Expand Down
33 changes: 18 additions & 15 deletions e2e/scenarios/microsoft-emulator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,13 @@ return { ok: result.ok, path: item.path, result: result.ok ? result.data : resul
scenario(
"Microsoft · client credentials against the emulator mint a Graph connection and call /users",
{
// Blocked (pre-existing, not this PR): `microsoft.addGraph` only accepts the
// canonical Graph spec in the streamable block-YAML profile — it structurally
// splits the doc to avoid OOMing the 128MB Workers isolate on the real 37MB
// spec (packages/plugins/microsoft/src/sdk/graph.ts), and hard-errors on
// anything else. The @executor-js/emulate Microsoft emulator serves a small
// custom Graph spec that isn't in that profile, so addGraph rejects it. Fix
// needs the emulator to serve a block-YAML-profile Graph spec (or a
// non-Workers compile path); tracked separately.
skip: "microsoft.addGraph requires the canonical block-YAML Graph spec; the emulator spec is not in that profile",
// Blocked (pre-existing, not this PR): Microsoft workload adds require the
// canonical Graph spec in the streamable block-YAML profile. The emulator
// serves a small custom Graph spec that is outside that profile, so the
// workload add rejects it. Fix needs the emulator to serve a
// block-YAML-profile Graph spec (or a non-Workers compile path); tracked
// separately.
skip: "Microsoft workload add requires the canonical block-YAML Graph spec; the emulator spec is not in that profile",
timeout: 180_000,
},
Effect.scoped(
Expand All @@ -110,14 +108,19 @@ scenario(

yield* Effect.ensuring(
Effect.gen(function* () {
yield* client.microsoft.addGraph({
yield* client.microsoft.addWorkloads({
payload: {
presetIds: ["users"],
customScopes: [],
slug: integration,
name: "Microsoft Graph Emulator",
baseUrl: emulator.url,
specUrl: emulator.openapiUrl,
workloads: [
{
custom: {
customScopes: ["User.Read.All"],
slug: integration,
name: "Microsoft Graph Emulator",
specUrl: emulator.openapiUrl,
},
},
],
},
});

Expand Down
Loading
Loading