From 1839de5b1d3ea3f668ae6d031f79ebb0590258cc Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 06:58:33 +0200 Subject: [PATCH] Restore provider readiness for mobile plugins (#6760) * fix(client): restore provider readiness on mobile React Native uses an AbortSignal without throwIfAborted. Check aborted with the existing API disposal error so readiness works while preserving cleanup and cancellation. Co-authored-by: GeorgeR <897457+GeorgeR@users.noreply.github.com> * test(plugins): run mobile readiness regression in CI --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: GeorgeR <897457+GeorgeR@users.noreply.github.com> --- packages/client/src/index.ts | 2 +- .../plugins/plugin-paseo-api.e2e.test.ts | 57 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 5bccc94009..058ec158ef 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -1051,7 +1051,7 @@ async function waitForProvidersReady( try { await observation.ready; - signal.throwIfAborted(); + if (signal.aborted) throw new Error("Paseo API is disposed"); return await new Promise((resolve, reject) => { let settled = false; let requestId: string | null = null; diff --git a/packages/server/src/server/plugins/plugin-paseo-api.e2e.test.ts b/packages/server/src/server/plugins/plugin-paseo-api.e2e.test.ts index d0ad016cec..5da355d5df 100644 --- a/packages/server/src/server/plugins/plugin-paseo-api.e2e.test.ts +++ b/packages/server/src/server/plugins/plugin-paseo-api.e2e.test.ts @@ -1,3 +1,7 @@ +import { createRequire } from "node:module"; +import { runInNewContext } from "node:vm"; +import { fileURLToPath } from "node:url"; +import { build } from "esbuild"; import { EventEmitter, once } from "node:events"; import { resolveDaemonVersion } from "../daemon-version.js"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; @@ -15,6 +19,59 @@ afterEach(async () => { await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); }); +test.each(["React Native", "native"])( + "provider readiness works with %s AbortController", + async (runtime) => { + const require = createRequire(import.meta.url); + const reactNativeRequire = createRequire(require.resolve("react-native/package.json")); + const { AbortController: ReactNativeAbortController } = reactNativeRequire( + "abort-controller/dist/abort-controller", + ); + const bundle = await build({ + entryPoints: [fileURLToPath(new URL("../../../../client/src/index.ts", import.meta.url))], + bundle: true, + write: false, + format: "iife", + globalName: "paseoSdk", + platform: "browser", + }); + // A separate runtime starts with React Native's real abort implementation. + // Node's globals and the daemon's runtime remain unchanged. + const sdk: typeof import("@getpaseo/client") = runInNewContext( + `${bundle.outputFiles[0]!.text}\npaseoSdk`, + { + AbortController: runtime === "React Native" ? ReactNativeAbortController : AbortController, + setTimeout, + clearTimeout, + queueMicrotask, + TextEncoder, + TextDecoder, + }, + ); + const daemon = await createTestPaseoDaemon({ mcpEnabled: false }); + const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); + const api = sdk.createPaseoApi(client); + try { + await client.connect(); + const snapshot = await api.providers.waitForReady({ cwd: daemon.staticDir }); + expect(snapshot.entries.length).toBeGreaterThan(0); + expect(snapshot.entries.every((entry) => entry.status !== "loading")).toBe(true); + const waiting = api.providers.waitForReady({ cwd: daemon.staticDir }); + const canceled = expect(waiting).rejects.toThrow(/disposed|released/); + await api.dispose(); + await canceled; + const { diagnostic } = await client.collectDiagnostics(); + expect(diagnostic).toContain("Source registrations: 0"); + expect(diagnostic).toContain("Producer listeners: 0"); + expect(() => api.providers.waitForReady()).toThrow("disposed"); + } finally { + await api.dispose(); + await client.close(); + await daemon.close(); + } + }, +); + test("plugin handlers create workspaces and agents through their Paseo API", async () => { const pluginDirectory = await mkdtemp(path.join(tmpdir(), "paseo-api-plugin-")); const workspaceDirectory = await mkdtemp(path.join(tmpdir(), "paseo-api-workspace-"));