1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { expect, it } from "@effect/vitest";
4import {
5 DEFAULT_SERVER_SETTINGS,
6 EnvironmentId,
7 ProjectId,
8 ProviderInstanceId,
9 ThreadId,
10} from "@t3tools/contracts";
11import { resolveProjectSettings } from "@t3tools/shared/projectSettings";
12import * as Effect from "effect/Effect";
13import * as Layer from "effect/Layer";
14import * as Stream from "effect/Stream";
15
16import * as ServerConfig from "../../../config.ts";
17import * as Preview from "../../../preview/Manager.ts";
18import * as ServerSettings from "../../../serverSettings.ts";
19import * as McpInvocationContext from "../../McpInvocationContext.ts";
20import * as McpToolAccess from "../../McpToolAccess.ts";
21import * as McpToolAccessTestkit from "../../McpToolAccess.testkit.ts";
22import * as PreviewAutomationBroker from "../../PreviewAutomationBroker.ts";
23import * as PreviewControlsHandlers from "./handlers.ts";
24import { PreviewControlsToolkit } from "./tools.ts";
25
26it.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);