Skip to content
Merged
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
47 changes: 47 additions & 0 deletions packages/provider-cursor/src/server/CursorSdk.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { assert, describe, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { vi } from "vite-plus/test";

import * as CursorSdk from "./CursorSdk.ts";

vi.mock("./sdk.ts", () => ({ Agent: {}, createAgentPlatform: {}, SqliteLocalAgentStore: {} }));

// Shaped like the SDK's minified class: the constructor is not named after the error.
class o extends Error {
override name = "WriteIterableClosedError";
}

// Calls the guard directly, with Vitest's own listener detached so the guard does not defer to it.
const rejectThroughGuard = (reason: unknown) =>
Effect.gen(function* () {
const others = process.listeners("unhandledRejection");
yield* Layer.build(CursorSdk.layer);
const guard = process.listeners("unhandledRejection").find((l) => !others.includes(l));
assert.isDefined(guard);
using exit = vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
using _log = vi.spyOn(console, "error").mockImplementation(() => {});
for (const listener of others) process.off("unhandledRejection", listener);
try {
guard(reason, Promise.resolve());
} finally {
for (const listener of others) process.on("unhandledRejection", listener);
}
return exit.mock.calls;
}).pipe(Effect.scoped);

describe("CursorSdk unhandled rejection guard", () => {
it.effect("keeps the server running when the SDK writes to a closed stream", () =>
Effect.gen(function* () {
assert.deepStrictEqual(yield* rejectThroughGuard(new o("WritableIterable is closed")), []);
}),
);

it.effect("still exits on other rejections", () =>
Effect.gen(function* () {
assert.deepStrictEqual(yield* rejectThroughGuard(new Error("WritableIterable is closed")), [
[1],
]);
}),
);
});
13 changes: 13 additions & 0 deletions packages/provider-cursor/src/server/CursorSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,24 @@ function isCursorShellSpawnFailure(reason: unknown): boolean {
);
}

/**
* After a local exec fails, the SDK writes a final frame to the exec's stream from
* a promise nothing awaits. If that stream has already closed, the write rejects.
* The SDK's class is minified, so only its `name` identifies it.
*/
function isCursorClosedStreamWrite(reason: unknown): boolean {
return reason instanceof Error && reason.name === "WriteIterableClosedError";
}

function onUnhandledRejection(reason: unknown): void {
if (isCursorShellSpawnFailure(reason)) {
console.error("Cursor shell spawn failed. The server will keep running.", reason);
return;
}
if (isCursorClosedStreamWrite(reason)) {
console.error("Cursor SDK wrote to a closed exec stream. The server will keep running.");
return;
}
if (process.listenerCount("unhandledRejection") > 1) {
return;
}
Expand Down
Loading