Skip to content
Open
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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,8 @@ jobs:
if: matrix.target == 'cloud'
env:
MCP_SESSION_TIMEOUT_MS: "3000"
MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS: "6000"
# Must outlive a browser approval; see e2e/setup/mcp-session-timeouts.ts.
MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS: "30000"
run: bun scripts/run-ci-shard.ts cloud ${{ matrix['shard-index'] }}
working-directory: e2e

Expand Down
10 changes: 8 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@ Stable contracts live here. Current setup and server mechanics are in
Tests and demos that need an upstream API, OAuth/OIDC provider, or webhook use
the published `@executor-js/emulate` emulators rather than stubs. They provide
wire-level state, real-shaped credentials, OpenAPI descriptions, and request
ledgers. Create per-run hosted instances through the service's
`/_emulate/instances` control route.
ledgers. Spawn a per-scenario instance IN PROCESS — `createEmulatorInstance`
(`e2e/src/emulator-instance.ts`) for a scenario, `createEmulator` for a target's
own stack. A scenario that runs entirely on this machine must not need the
public internet to pass: a hosted instance makes the runner's network, and its
egress IP's rate limits, part of every assertion. Reach for the hosted
`/_emulate/instances` control route only when exercising the deployed service
IS the point of the scenario, and keep an in-process counterpart for the
contract itself.

Emulate is a separate project. Make emulator changes there and consume the
published package here; never re-vendor or create a parallel fake inside
Expand Down
84 changes: 43 additions & 41 deletions e2e/scenarios/connect-handoff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,51 +120,53 @@ const mintEmulatorApiKey = (client: EmulatorClient) =>
scenario(
"Connect · the agentic handoff URL opens this deployment's add-account flow and the pasted key works",
{ timeout: 240_000 },
Effect.gen(function* () {
const target = yield* Target;
const mcp = yield* Mcp;
const browser = yield* Browser;
const { client: makeApiClient } = yield* Api;
Effect.scoped(
Effect.gen(function* () {
const target = yield* Target;
const mcp = yield* Mcp;
const browser = yield* Browser;
const { client: makeApiClient } = yield* Api;

const integration = unique("resendhf");
const emailSubject = unique("connect-handoff");
const emulatorClient = yield* emulator;
const apiKey = yield* mintEmulatorApiKey(emulatorClient);
const integration = unique("resendhf");
const emailSubject = unique("connect-handoff");
const emulatorClient = yield* emulator;
const apiKey = yield* mintEmulatorApiKey(emulatorClient);

const identity = yield* target.newIdentity();
const session = mcp.session(identity);
const client = yield* makeApiClient(api, identity);
const identity = yield* target.newIdentity();
const session = mcp.session(identity);
const client = yield* makeApiClient(api, identity);

// The bound org's slug, read from the same account surface the console
// shell reads — the handoff URL must canonicalize onto exactly this.
const accountClient = yield* makeApiClient(AccountHttpApi, identity);
const me = yield* accountClient.account.me();
const orgSlug = me.organization?.slug;
expect(orgSlug, "the bound organization advertises a URL slug").toBeTruthy();
// The bound org's slug, read from the same account surface the console
// shell reads — the handoff URL must canonicalize onto exactly this.
const accountClient = yield* makeApiClient(AccountHttpApi, identity);
const me = yield* accountClient.account.me();
const orgSlug = me.organization?.slug;
expect(orgSlug, "the bound organization advertises a URL slug").toBeTruthy();

yield* runScenario({
target,
browser,
session,
identity,
integration,
emailSubject,
apiKey,
orgSlug: orgSlug!,
emulatorClient,
}).pipe(
// Best-effort cleanup even on failure: drop the created connection(s)
// over MCP, then the integration over the API. `connections.remove` is
// approval-gated, so the cleanup execute pauses per connection;
// `executeJson` auto-approves each pause so the removes actually run.
Effect.ensuring(
Effect.gen(function* () {
yield* executeJson(session, removeConnectionsCode(integration));
yield* client.openapi.removeSpec({ params: { slug: integration } });
}).pipe(Effect.ignore),
),
);
}),
yield* runScenario({
target,
browser,
session,
identity,
integration,
emailSubject,
apiKey,
orgSlug: orgSlug!,
emulatorClient,
}).pipe(
// Best-effort cleanup even on failure: drop the created connection(s)
// over MCP, then the integration over the API. `connections.remove` is
// approval-gated, so the cleanup execute pauses per connection;
// `executeJson` auto-approves each pause so the removes actually run.
Effect.ensuring(
Effect.gen(function* () {
yield* executeJson(session, removeConnectionsCode(integration));
yield* client.openapi.removeSpec({ params: { slug: integration } });
}).pipe(Effect.ignore),
),
);
}),
),
);

const runScenario = (input: {
Expand Down
Loading
Loading