| 1 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import { expect, it } from "@effect/vitest"; |
| 4 | import { |
| 5 | DEFAULT_SERVER_SETTINGS, |
| 6 | EnvironmentId, |
| 7 | ProjectId, |
| 8 | ProviderInstanceId, |
| 9 | ThreadId, |
| 10 | } from "@t3tools/contracts"; |
| 11 | import { resolveProjectSettings } from "@t3tools/shared/projectSettings"; |
| 12 | import * as Effect from "effect/Effect"; |
| 13 | import * as Layer from "effect/Layer"; |
| 14 | import * as Stream from "effect/Stream"; |
| 15 | |
| 16 | import * as ServerConfig from "../../../config.ts"; |
| 17 | import * as Preview from "../../../preview/Manager.ts"; |
| 18 | import * as ServerSettings from "../../../serverSettings.ts"; |
| 19 | import * as McpInvocationContext from "../../McpInvocationContext.ts"; |
| 20 | import * as McpToolAccess from "../../McpToolAccess.ts"; |
| 21 | import * as McpToolAccessTestkit from "../../McpToolAccess.testkit.ts"; |
| 22 | import * as PreviewAutomationBroker from "../../PreviewAutomationBroker.ts"; |
| 23 | import * as PreviewControlsHandlers from "./handlers.ts"; |
| 24 | import { PreviewControlsToolkit } from "./tools.ts"; |
| 25 | |
| 26 | it.effect.each([ |
| 27 | { name: "project opt-in", globalAccess: false, projectAccess: true }, |
| 28 | { name: "project opt-out", globalAccess: true, projectAccess: false }, |
| 29 | ])("list and close respect the credential for a $name", ({ globalAccess, projectAccess }) => |
| 30 | Effect.scoped( |
| 31 | Effect.gen(function* () { |
| 32 | const projectId = ProjectId.make("preview-controls-project"); |
| 33 | const threadId = ThreadId.make("preview-controls-thread"); |
| 34 | const settings = { |
| 35 | ...DEFAULT_SERVER_SETTINGS, |
| 36 | enableAgentBrowserAccess: globalAccess, |
| 37 | projectSettingsOverrides: { |
| 38 | [projectId]: { enableAgentBrowserAccess: projectAccess }, |
| 39 | }, |
| 40 | }; |
| 41 | const effective = resolveProjectSettings(settings, projectId).settings; |
| 42 | const scope: McpInvocationContext.McpInvocationScope = { |
| 43 | environmentId: EnvironmentId.make("preview-controls-environment"), |
| 44 | requestNamespace: "preview-controls-provider-session", |
| 45 | thread: { |
| 46 | threadId, |
| 47 | providerSessionId: "preview-controls-provider-session", |
| 48 | providerInstanceId: ProviderInstanceId.make("codex"), |
| 49 | }, |
| 50 | client: undefined, |
| 51 | capabilities: new Set(effective.enableAgentBrowserAccess ? ["preview"] : []), |
| 52 | issuedAt: 0, |
| 53 | }; |
| 54 | const manager = yield* Preview.make.pipe( |
| 55 | Effect.provide( |
| 56 | Layer.merge( |
| 57 | ServerConfig.layerTest(process.cwd(), { prefix: "t3-preview-controls-" }).pipe( |
| 58 | Layer.provide(NodeServices.layer), |
| 59 | ), |
| 60 | NodeCrypto.layer, |
| 61 | ), |
| 62 | ), |
| 63 | ); |
| 64 | const tab = yield* manager.open({ threadId, url: "http://localhost:3000" }); |
| 65 | const layerDependencies = Layer.mergeAll( |
| 66 | PreviewAutomationBroker.layer.pipe(Layer.provide(NodeServices.layer)), |
| 67 | Layer.succeed(Preview.PreviewManager, manager), |
| 68 | Layer.succeed(McpInvocationContext.McpInvocationContext, scope), |
| 69 | McpToolAccessTestkit.liveThreadsLayer, |
| 70 | Layer.mock(ServerSettings.ServerSettingsService)({ |
| 71 | getSettings: Effect.succeed(settings), |
| 72 | }), |
| 73 | ); |
| 74 | const toolkit = yield* PreviewControlsToolkit.pipe( |
| 75 | Effect.provide( |
| 76 | McpToolAccess.HandlersLayer.layer(PreviewControlsHandlers.layer).pipe( |
| 77 | Layer.provide(layerDependencies), |
| 78 | ), |
| 79 | ), |
| 80 | ); |
| 81 | const listed = yield* toolkit |
| 82 | .handle("t3_preview_list", {}) |
| 83 | .pipe(Stream.unwrap, Stream.runCollect, Effect.provide(layerDependencies)); |
| 84 | const closed = yield* toolkit |
| 85 | .handle("t3_preview_close", { tabId: tab.tabId }) |
| 86 | .pipe(Stream.unwrap, Stream.runCollect, Effect.provide(layerDependencies)); |
| 87 | if (projectAccess) { |
| 88 | expect(listed.at(-1)?.result).toMatchObject({ sessions: [tab], nextCursor: null }); |
| 89 | expect(closed.at(-1)?.result).toEqual({}); |
| 90 | expect((yield* manager.list({ threadId })).sessions).toEqual([]); |
| 91 | } else { |
| 92 | for (const result of [listed, closed]) { |
| 93 | expect(result.at(-1)?.result).toMatchObject({ |
| 94 | _tag: "PreviewAutomationUnavailableError", |
| 95 | capability: "preview", |
| 96 | threadId, |
| 97 | }); |
| 98 | } |
| 99 | expect((yield* manager.list({ threadId })).sessions).toEqual([tab]); |
| 100 | } |
| 101 | }), |
| 102 | ), |
| 103 | ); |