1044 lines · 35.0 KB · TypeScript
RawDownload
import * as NodeServices from "@effect/platform-node/NodeServices";import { expect, it } from "@effect/vitest";import {  EnvironmentId,  PreviewAutomationClientDisconnectedError,  PreviewAutomationInvalidSelectorError,  PreviewAutomationMalformedResponseError,  PreviewAutomationNoAvailableHostError,  PreviewAutomationTargetNotEditableError,  PreviewTabId,  ProviderInstanceId,  ThreadId,  type PreviewAutomationHost,  type PreviewAutomationRequest,  type PreviewAutomationStreamEvent,} from "@t3tools/contracts";import * as Effect from "effect/Effect";import * as Deferred from "effect/Deferred";import * as Fiber from "effect/Fiber";import * as Result from "effect/Result";import * as Stream from "effect/Stream"; import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts"; const makeBroker = PreviewAutomationBroker.make.pipe(Effect.provide(NodeServices.layer)); 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,}; const makeHost = (overrides: Partial<PreviewAutomationHost> = {}): PreviewAutomationHost => ({  clientId: "client-1",  environmentId: scope.environmentId,  ...overrides,}); type RoutedRequest = PreviewAutomationRequest & {  readonly connectionId: PreviewAutomationStreamEvent["connectionId"];}; const requestsFrom = (  events: Stream.Stream<PreviewAutomationStreamEvent>,  onConnected: (connectionId: PreviewAutomationStreamEvent["connectionId"]) => void = () => {},): Stream.Stream<RoutedRequest> =>  events.pipe(    Stream.filterMap((event) => {      if (event.type === "connected") {        onConnected(event.connectionId);        return Result.failVoid;      }      return Result.succeed({ ...event.request, connectionId: event.connectionId });    }),  ); it.effect("atomically registers a connected host and correlates its response", () =>  Effect.scoped(    Effect.gen(function* () {      const broker = yield* makeBroker;      const requests = requestsFrom(yield* broker.connect(makeHost()));      yield* Stream.runForEach(requests, (request) =>        broker.respond({          clientId: "client-1",          connectionId: request.connectionId,          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("targets multiple tabs explicitly while retaining a default tab", () =>  Effect.scoped(    Effect.gen(function* () {      const broker = yield* makeBroker;      const appTabId = PreviewTabId.make("tab-web-app");      const simulatorTabId = PreviewTabId.make("tab-ios-simulator");      const openedTabIds = [appTabId, simulatorTabId];      let openIndex = 0;      const routedRequests: RoutedRequest[] = [];      const requests = requestsFrom(yield* broker.connect(makeHost()));      yield* Stream.runForEach(requests, (request) => {        routedRequests.push(request);        return broker.respond({          clientId: "client-1",          connectionId: request.connectionId,          requestId: request.requestId,          ok: true,          result:            request.operation === "open"              ? { available: true, tabId: openedTabIds[openIndex++] }              : { url: "http://localhost:3200" },        });      }).pipe(Effect.forkScoped);      yield* Effect.yieldNow;       yield* broker.invoke({ scope, operation: "open", input: { reuseExistingTab: false } });      yield* broker.invoke({ scope, operation: "open", input: { reuseExistingTab: false } });      yield* broker.invoke({ scope, operation: "snapshot", input: {} });      yield* broker.invoke({ scope, operation: "snapshot", input: {}, tabId: appTabId });      yield* broker.invoke({ scope, operation: "snapshot", input: {} });       expect(routedRequests).toHaveLength(5);      expect(routedRequests[0]?.tabId).toBeUndefined();      expect(routedRequests[1]?.tabId).toBe(appTabId);      expect(routedRequests[2]?.tabId).toBe(simulatorTabId);      expect(routedRequests[2]?.tabIdExplicit).toBe(false);      expect(routedRequests[3]?.tabId).toBe(appTabId);      expect(routedRequests[3]?.tabIdExplicit).toBe(true);      expect(routedRequests[4]?.tabId).toBe(appTabId);    }),  ),); it.effect("does not let an older response replace a newer explicit tab target", () =>  Effect.scoped(    Effect.gen(function* () {      const broker = yield* makeBroker;      const olderTabId = PreviewTabId.make("tab-older-request");      const newerTabId = PreviewTabId.make("tab-newer-request");      const releaseOlderResponse = yield* Deferred.make<void>();      const routedRequests: RoutedRequest[] = [];      const requests = requestsFrom(yield* broker.connect(makeHost()));      yield* Stream.runForEach(requests, (request) => {        routedRequests.push(request);        const response = Effect.gen(function* () {          if (request.tabId === olderTabId) {            yield* Deferred.await(releaseOlderResponse);          }          yield* broker.respond({            clientId: "client-1",            connectionId: request.connectionId,            requestId: request.requestId,            ok: true,            result: { url: "http://localhost:3200" },          });          if (request.tabId === newerTabId) {            yield* Deferred.succeed(releaseOlderResponse, undefined);          }        });        return response.pipe(Effect.forkScoped, Effect.asVoid);      }).pipe(Effect.forkScoped);      yield* Effect.yieldNow;       const older = yield* broker        .invoke({ scope, operation: "snapshot", input: {}, tabId: olderTabId })        .pipe(Effect.forkScoped);      yield* Effect.yieldNow;      const newer = yield* broker        .invoke({ scope, operation: "snapshot", input: {}, tabId: newerTabId })        .pipe(Effect.forkScoped);      yield* Fiber.join(newer);      yield* Fiber.join(older);      yield* broker.invoke({ scope, operation: "snapshot", input: {} });       expect(routedRequests.at(-1)?.tabId).toBe(newerTabId);    }),  ),); it.effect("tracks the tab returned by a targeted recording stop", () =>  Effect.scoped(    Effect.gen(function* () {      const broker = yield* makeBroker;      const browsingTabId = PreviewTabId.make("tab-session-b");      const recordingTabId = PreviewTabId.make("tab-session-a-recording");      const routedRequests: RoutedRequest[] = [];      const requests = requestsFrom(yield* broker.connect(makeHost()));      yield* Stream.runForEach(requests, (request) => {        routedRequests.push(request);        return broker.respond({          clientId: "client-1",          connectionId: request.connectionId,          requestId: request.requestId,          ok: true,          result:            request.operation === "open"              ? { available: true, tabId: browsingTabId }              : request.operation === "recordingStop"                ? { id: "recording-1", tabId: recordingTabId }                : { url: "http://localhost:3200" },        });      }).pipe(Effect.forkScoped);      yield* Effect.yieldNow;       yield* broker.invoke({ scope, operation: "open", input: {} });      yield* broker.invoke({ scope, operation: "recordingStop", input: {} });      yield* broker.invoke({ scope, operation: "snapshot", input: {} });       expect(routedRequests.at(-1)?.tabId).toBe(recordingTabId);    }),  ),); it.effect("does not let a no-tab response suppress an earlier tab decision", () =>  Effect.scoped(    Effect.gen(function* () {      const broker = yield* makeBroker;      const initialTabId = PreviewTabId.make("tab-initial");      const openedTabId = PreviewTabId.make("tab-opened-late");      const releaseOpenResponse = yield* Deferred.make<void>();      const routedRequests: RoutedRequest[] = [];      const requests = requestsFrom(yield* broker.connect(makeHost()));      yield* Stream.runForEach(requests, (request) => {        routedRequests.push(request);        const marker =          typeof request.input === "object" && request.input !== null && "marker" in request.input            ? request.input.marker            : undefined;        const response = Effect.gen(function* () {          if (marker === "older") {            yield* Deferred.await(releaseOpenResponse);          }          yield* broker.respond({            clientId: "client-1",            connectionId: request.connectionId,            requestId: request.requestId,            ok: true,            result:              request.operation === "open"                ? { available: true, tabId: marker === "older" ? openedTabId : initialTabId }                : { url: "http://localhost:3200" },          });          if (marker === "newer") {            yield* Deferred.succeed(releaseOpenResponse, undefined);          }        });        return response.pipe(Effect.forkScoped, Effect.asVoid);      }).pipe(Effect.forkScoped);      yield* Effect.yieldNow;       yield* broker.invoke({ scope, operation: "open", input: {} });      const older = yield* broker        .invoke({          scope,          operation: "open",          input: { marker: "older", reuseExistingTab: false },        })        .pipe(Effect.forkScoped);      yield* Effect.yieldNow;      const newer = yield* broker        .invoke({ scope, operation: "snapshot", input: { marker: "newer" } })        .pipe(Effect.forkScoped);      yield* Fiber.join(newer);      yield* Fiber.join(older);      yield* broker.invoke({ scope, operation: "snapshot", input: {} });       expect(routedRequests.at(-1)?.tabId).toBe(openedTabId);    }),  ),); it.effect("announces a live replacement stream before delivering requests", () =>  Effect.scoped(    Effect.gen(function* () {      const broker = yield* makeBroker;      const events = yield* broker.connect(makeHost());      const receivedTypes: PreviewAutomationStreamEvent["type"][] = [];      const consumer = yield* events.pipe(        Stream.take(2),        Stream.runForEach((event) => {          receivedTypes.push(event.type);          return event.type === "connected"            ? Effect.void            : broker.respond({                clientId: "client-1",                connectionId: event.connectionId,                requestId: event.request.requestId,                ok: true,                result: "ready",              });        }),        Effect.forkScoped,      );      yield* Effect.yieldNow;       const result = yield* broker.invoke<string>({ scope, operation: "status", input: {} });      yield* Fiber.join(consumer);       expect(receivedTypes).toEqual(["connected", "request"]);      expect(result).toBe("ready");    }),  ),); 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* makeBroker;      const requests = requestsFrom(yield* broker.connect(makeHost()));      yield* Stream.runForEach(requests, (request) =>        broker.respond({          clientId: "client-1",          connectionId: request.connectionId,          requestId: request.requestId,          ok: false,          error: remoteError,        }),      ).pipe(Effect.forkScoped);      yield* Effect.yieldNow;       const error = yield* broker        .invoke<void>({          scope,          operation: "click",          input: { locator },          tabId: PreviewTabId.make("tab-1"),          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("classifies a remote non-editable target without collapsing it to execution", () => {  const remoteError = {    _tag: "PreviewAutomationTargetNotEditableError",    message: "remote target details",    detail: { selectorKind: "focused-element" },  } as const;   return Effect.scoped(    Effect.gen(function* () {      const broker = yield* makeBroker;      const requests = requestsFrom(yield* broker.connect(makeHost()));      yield* Stream.runForEach(requests, (request) =>        broker.respond({          clientId: "client-1",          connectionId: request.connectionId,          requestId: request.requestId,          ok: false,          error: remoteError,        }),      ).pipe(Effect.forkScoped);      yield* Effect.yieldNow;       const error = yield* broker        .invoke<void>({          scope,          operation: "type",          input: { text: "hello" },          tabId: PreviewTabId.make("tab-1"),        })        .pipe(Effect.flip);       expect(error).toBeInstanceOf(PreviewAutomationTargetNotEditableError);      expect(error).toMatchObject({        operation: "type",        tabId: "tab-1",        selectorKind: "focused-element",        remoteTag: "PreviewAutomationTargetNotEditableError",      });      expect(error.message).toBe("Preview automation type requires an editable focused element.");    }),  );}); it.effect("distinguishes malformed remote failures", () =>  Effect.scoped(    Effect.gen(function* () {      const broker = yield* makeBroker;      const requests = requestsFrom(yield* broker.connect(makeHost()));      yield* Stream.runForEach(requests, (request) =>        broker.respond({          clientId: "client-1",          connectionId: request.connectionId,          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 connected host exists", () =>  Effect.gen(function* () {    const broker = yield* makeBroker;    const error = yield* broker      .invoke<void>({ scope, operation: "status", input: {} })      .pipe(Effect.flip);     expect(error).toBeInstanceOf(PreviewAutomationNoAvailableHostError);    expect(error).toMatchObject({      operation: "status",      environmentId: scope.environmentId,      threadId: scope.threadId,      providerSessionId: scope.providerSessionId,      providerInstanceId: scope.providerInstanceId,    });  }),); it.effect("does not create host state from focus updates without a live stream", () =>  Effect.gen(function* () {    const broker = yield* makeBroker;    yield* broker.focusHost({      clientId: "client-1",      environmentId: scope.environmentId,      connectionId: "connection-missing",      focused: true,    });     const error = yield* broker      .invoke<void>({ scope, operation: "status", input: {} })      .pipe(Effect.flip);    expect(error).toBeInstanceOf(PreviewAutomationNoAvailableHostError);  }),); it.effect("removes host availability when the authoritative request stream disconnects", () =>  Effect.scoped(    Effect.gen(function* () {      const broker = yield* makeBroker;      const requests = requestsFrom(yield* broker.connect(makeHost()));      const beforeAcquisition = yield* broker        .invoke<void>({ scope, operation: "status", input: {} })        .pipe(Effect.flip);      expect(beforeAcquisition).toBeInstanceOf(PreviewAutomationNoAvailableHostError);       const consumer = yield* Stream.runDrain(requests).pipe(Effect.forkScoped);      yield* Effect.yieldNow;      yield* Fiber.interrupt(consumer);       const error = yield* broker        .invoke<void>({ scope, operation: "status", input: {} })        .pipe(Effect.flip);      expect(error).toBeInstanceOf(PreviewAutomationNoAvailableHostError);    }),  ),); it.effect("routes requests for background threads through an environment-level host", () =>  Effect.scoped(    Effect.gen(function* () {

Lines 1–500 of 1044.