299 lines · 10.0 KB · TypeScript
RawDownload
import { expect, it } from "@effect/vitest";import {  EnvironmentId,  PreviewAutomationClientDisconnectedError,  PreviewAutomationInvalidSelectorError,  PreviewAutomationMalformedResponseError,  PreviewAutomationNoFocusedOwnerError,  ProviderInstanceId,  ThreadId,  type PreviewAutomationOwner,} from "@t3tools/contracts";import * as Effect from "effect/Effect";import * as Fiber from "effect/Fiber";import * as Stream from "effect/Stream"; import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts"; const scope = {  environmentId: EnvironmentId.make("environment-1"),  threadId: ThreadId.make("thread-1"),  providerSessionId: "provider-session-1",  providerInstanceId: ProviderInstanceId.make("codex"),  capabilities: new Set(["preview"] as const),  issuedAt: 1,  expiresAt: 2,}; const makeOwner = (overrides: Partial<PreviewAutomationOwner> = {}): PreviewAutomationOwner => ({  clientId: "client-1",  environmentId: scope.environmentId,  threadId: scope.threadId,  tabId: null,  visible: false,  supportsAutomation: true,  focusedAt: "2026-06-11T00:00:00.000Z",  ...overrides,}); it.effect("atomically registers a connected owner and correlates its response", () =>  Effect.scoped(    Effect.gen(function* () {      const broker = yield* PreviewAutomationBroker.make;      const requests = yield* broker.connect(makeOwner());      yield* Stream.runForEach(requests, (request) =>        broker.respond({          requestId: request.requestId,          ok: true,          result: { available: true },        }),      ).pipe(Effect.forkScoped);      yield* Effect.yieldNow;       const result = yield* broker.invoke<{ available: boolean }>({        scope,        operation: "open",        input: {},      });       expect(result).toEqual({ available: true });    }),  ),); it.effect("preserves bounded request and remote selector diagnostics", () => {  const locator = "role=button[name='request-secret']";  const remoteMessage = "Unexpected token near remote-secret.";  const remoteError = {    _tag: "PreviewAutomationInvalidSelectorError",    message: remoteMessage,    detail: { selector: "role=button[name='remote-secret']" },  } as const;   return Effect.scoped(    Effect.gen(function* () {      const broker = yield* PreviewAutomationBroker.make;      const requests = yield* broker.connect(makeOwner({ tabId: "tab-1" }));      yield* Stream.runForEach(requests, (request) =>        broker.respond({          requestId: request.requestId,          ok: false,          error: remoteError,        }),      ).pipe(Effect.forkScoped);      yield* Effect.yieldNow;       const error = yield* broker        .invoke<void>({          scope,          operation: "click",          input: { locator },          timeoutMs: 1_234,        })        .pipe(Effect.flip);       expect(error).toBeInstanceOf(PreviewAutomationInvalidSelectorError);      expect(error).toMatchObject({        operation: "click",        environmentId: scope.environmentId,        threadId: scope.threadId,        providerSessionId: scope.providerSessionId,        providerInstanceId: scope.providerInstanceId,        clientId: "client-1",        requestId: "preview-0",        tabId: "tab-1",        timeoutMs: 1_234,        selectorKind: "locator",        selectorLength: locator.length,        remoteTag: "PreviewAutomationInvalidSelectorError",        remoteMessageLength: remoteMessage.length,        remoteDetailKind: "object",      });      expect(error.message).toBe(        `Preview automation click received an invalid locator (${locator.length} characters).`,      );      expect(error.message).not.toContain("secret");      expect(error.cause).toBe(remoteError);      expect("selector" in error).toBe(false);      expect("remoteMessage" in error).toBe(false);      expect("remoteDetail" in error).toBe(false);    }),  );}); it.effect("distinguishes malformed remote failures", () =>  Effect.scoped(    Effect.gen(function* () {      const broker = yield* PreviewAutomationBroker.make;      const requests = yield* broker.connect(makeOwner());      yield* Stream.runForEach(requests, (request) =>        broker.respond({ requestId: request.requestId, ok: false }),      ).pipe(Effect.forkScoped);      yield* Effect.yieldNow;       const error = yield* broker        .invoke<void>({ scope, operation: "status", input: {}, timeoutMs: 2_000 })        .pipe(Effect.flip);       expect(error).toBeInstanceOf(PreviewAutomationMalformedResponseError);      expect(error).toMatchObject({        operation: "status",        environmentId: scope.environmentId,        threadId: scope.threadId,        providerSessionId: scope.providerSessionId,        providerInstanceId: scope.providerInstanceId,        clientId: "client-1",        requestId: "preview-0",        timeoutMs: 2_000,      });    }),  ),); it.effect("rejects calls when no focused owner exists", () =>  Effect.gen(function* () {    const broker = yield* PreviewAutomationBroker.make;    const error = yield* broker      .invoke<void>({ scope, operation: "status", input: {} })      .pipe(Effect.flip);    expect(error).toBeInstanceOf(PreviewAutomationNoFocusedOwnerError);    expect(error).toMatchObject({      operation: "status",      environmentId: scope.environmentId,      threadId: scope.threadId,      providerSessionId: scope.providerSessionId,      providerInstanceId: scope.providerInstanceId,    });  }),); it.effect("routes interactive commands to a hidden durable browser host", () =>  Effect.scoped(    Effect.gen(function* () {      const broker = yield* PreviewAutomationBroker.make;      const requests = yield* broker.connect(        makeOwner({ clientId: "client-hidden", tabId: "tab-hidden" }),      );      yield* Stream.runForEach(requests, (request) =>        broker.respond({ requestId: request.requestId, ok: true }),      ).pipe(Effect.forkScoped);      yield* Effect.yieldNow;       yield* broker.invoke<void>({ scope, operation: "click", input: { x: 10, y: 10 } });    }),  ),); it.effect("lets the browser host resolve an active tab that has not been reported yet", () =>  Effect.scoped(    Effect.gen(function* () {      const broker = yield* PreviewAutomationBroker.make;      const requests = yield* broker.connect(makeOwner({ tabId: null }));      let routedTabId: string | undefined;      yield* Stream.runForEach(requests, (request) => {        routedTabId = request.tabId;        return broker.respond({ requestId: request.requestId, ok: true });      }).pipe(Effect.forkScoped);      yield* Effect.yieldNow;       yield* broker.invoke<void>({ scope, operation: "click", input: { x: 10, y: 10 } });       expect(routedTabId).toBeUndefined();    }),  ),); it.effect("preserves current owner metadata when its request stream reconnects", () =>  Effect.scoped(    Effect.gen(function* () {      const broker = yield* PreviewAutomationBroker.make;      const firstRequests = yield* broker.connect(makeOwner());      yield* Stream.runDrain(firstRequests).pipe(Effect.forkScoped);      yield* broker.reportOwner(makeOwner({ tabId: "tab-current", visible: true }));       const reconnectedRequests = yield* broker.connect(makeOwner());      let routedTabId: string | undefined;      yield* Stream.runForEach(reconnectedRequests, (request) => {        routedTabId = request.tabId;        return broker.respond({ requestId: request.requestId, ok: true });      }).pipe(Effect.forkScoped);      yield* Effect.yieldNow;       yield* broker.invoke<void>({ scope, operation: "click", input: { x: 10, y: 10 } });       expect(routedTabId).toBe("tab-current");    }),  ),); it.effect("ignores stale owner cleanup after the client moves to another thread", () =>  Effect.scoped(    Effect.gen(function* () {      const broker = yield* PreviewAutomationBroker.make;      const requests = yield* broker.connect(makeOwner());      yield* Stream.runForEach(requests, (request) =>        broker.respond({ requestId: request.requestId, ok: true }),      ).pipe(Effect.forkScoped);      yield* Effect.yieldNow;       yield* broker.clearOwner({        clientId: "client-1",        environmentId: scope.environmentId,        threadId: ThreadId.make("thread-stale"),      });       yield* broker.invoke<void>({ scope, operation: "status", input: {} });    }),  ),); it.effect("fails requests assigned to a browser stream when that stream reconnects", () =>  Effect.scoped(    Effect.gen(function* () {      const broker = yield* PreviewAutomationBroker.make;      const _requests = yield* broker.connect(makeOwner());      const pending = yield* broker        .invoke<void>({ scope, operation: "status", input: {} })        .pipe(Effect.flip, Effect.forkScoped);      yield* Effect.yieldNow;       const _replacementRequests = yield* broker.connect(makeOwner());       const error = yield* Fiber.join(pending);      expect(error).toBeInstanceOf(PreviewAutomationClientDisconnectedError);      expect(error).toMatchObject({        operation: "status",        environmentId: scope.environmentId,        threadId: scope.threadId,        providerSessionId: scope.providerSessionId,        providerInstanceId: scope.providerInstanceId,        clientId: "client-1",        requestId: "preview-0",        timeoutMs: 15_000,      });    }),  ),); it.effect("falls back to an older connected owner when a newer report is not connected", () =>  Effect.scoped(    Effect.gen(function* () {      const broker = yield* PreviewAutomationBroker.make;      const requests = yield* broker.connect(makeOwner({ clientId: "client-connected" }));      yield* Stream.runForEach(requests, (request) =>        broker.respond({ requestId: request.requestId, ok: true, result: "connected" }),      ).pipe(Effect.forkScoped);      yield* Effect.yieldNow;      yield* broker.reportOwner(        makeOwner({          clientId: "client-report-only",          focusedAt: "2026-06-11T00:00:01.000Z",        }),      );       const result = yield* broker.invoke<string>({ scope, operation: "status", input: {} });       expect(result).toBe("connected");    }),  ),);