142 lines · 5.1 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Data, Effect, Exit } from "effect"; import { createExecutor, definePlugin } from "@executor-js/sdk";import { makeTestConfig } from "@executor-js/sdk/testing";import type { CodeExecutor, ExecuteResult } from "@executor-js/codemode-core"; import { createExecutionEngine, formatPausedExecution } from "./engine";import { FormElicitation } from "@executor-js/sdk/core"; // Regression for the hang reported as the executor-MCP "180s timeout" against// Cowork (Claude web). Cowork goes down the `executeWithPause` branch because// it doesn't advertise managed elicitation. When the dynamic worker fails// fast (e.g. user submits TS with a `:` type annotation, "Unexpected token// ':'" inside ~25ms), the failure was swallowed and the request hung until// the client gave up at 180s. The cause was `Effect.race` having// prefer-success semantics in Effect v4: the racing pause-signal Deferred// never resolves, so a fiber failure is never observed by the racer. class FakeRuntimeError extends Data.TaggedError("FakeRuntimeError")<{  readonly message: string;}> {} const failingExecutor: CodeExecutor<FakeRuntimeError> = {  execute: () => Effect.fail(new FakeRuntimeError({ message: "Unexpected token ':'" })),}; const succeedingExecutor: CodeExecutor<FakeRuntimeError> = {  execute: () => Effect.succeed({ result: "ok", logs: [] } satisfies ExecuteResult),}; const emptyPlugin = definePlugin(() => ({  id: "empty-test" as const,  storage: () => ({}),  staticIntegrations: () => [],})); const makeExecutor = () => createExecutor(makeTestConfig({ plugins: [emptyPlugin()] as const })); describe("executeWithPause failure propagation", () => {  it.effect("surfaces a fast codeExecutor failure as an Exit.Failure", () =>    Effect.gen(function* () {      const executor = yield* makeExecutor();      const engine = createExecutionEngine({        executor,        codeExecutor: failingExecutor,      });       const exit = yield* Effect.exit(engine.executeWithPause("noop"));      expect(Exit.isFailure(exit)).toBe(true);    }),  );   it.effect("does not hang when codeExecutor fails", () =>    Effect.gen(function* () {      const executor = yield* makeExecutor();      const engine = createExecutionEngine({        executor,        codeExecutor: failingExecutor,      });       // Race the executeWithPause against a short sleep. With the bug      // present this resolves to "hung" because the failure is swallowed      // by the prefer-success race against the pause Deferred.      const outcome = yield* Effect.race(        Effect.exit(engine.executeWithPause("noop")).pipe(          Effect.map((exit) => ({ kind: "settled" as const, exit })),        ),        Effect.sleep("500 millis").pipe(Effect.as({ kind: "hung" as const })),      );       expect(outcome.kind).toBe("settled");    }),  );   it.effect("control: succeedingExecutor returns completed", () =>    Effect.gen(function* () {      const executor = yield* makeExecutor();      const engine = createExecutionEngine({        executor,        codeExecutor: succeedingExecutor,      });       const result = yield* engine.executeWithPause("noop");      expect(result.status).toBe("completed");    }),  );}); describe("pausedExecutionCount", () => {  it.effect("starts at zero", () =>    Effect.gen(function* () {      const executor = yield* makeExecutor();      const engine = createExecutionEngine({        executor,        codeExecutor: succeedingExecutor,      });       expect(yield* engine.pausedExecutionCount()).toBe(0);      expect(yield* engine.hasPausedExecutions()).toBe(false);    }),  );}); describe("formatPausedExecution approval terms", () => {  const paused = (request: FormElicitation) =>    ({      id: "exec_1",      elicitationContext: { address: "tools.x.org.default.y", args: {}, request },    }) as Parameters<typeof formatPausedExecution>[0];   it("states the terms an upstream attached to the approval", () => {    // An empty schema makes this look like a plain yes/no, but the metadata    // says accepting persists for the origin — so the answer differs, and the    // caller has to be able to see it.    const result = formatPausedExecution(      paused(        FormElicitation.make({          message: "Allow Browser use to access https://example.com?",          requestedSchema: {},          meta: { persist: "always", origin: "https://example.com" },        }),      ),    );     expect(result.text).toContain("Approval terms:");    expect(result.text).toContain('"persist": "always"');    expect((result.structured["interaction"] as { readonly meta?: unknown }).meta).toEqual({      persist: "always",      origin: "https://example.com",    });  });   it("says nothing about terms when the upstream attached none", () => {    const result = formatPausedExecution(      paused(FormElicitation.make({ message: "Proceed?", requestedSchema: {} })),    );     expect(result.text).not.toContain("Approval terms:");    expect((result.structured["interaction"] as Record<string, unknown>)["meta"]).toBeUndefined();  });});