1import { describe, expect, it } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { ThreadId } from "@t3tools/contracts";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Layer from "effect/Layer";
7import * as Path from "effect/Path";
8
9import {
10 createPendingAttachmentId,
11 parseThreadSegmentFromAttachmentId,
12} from "../../../attachmentStore.ts";
13import * as ServerConfig from "../../../config.ts";
14import { claimPreviewRecording, normalizePreviewOpenInput } from "./handlers.ts";
15
16describe("normalizePreviewOpenInput", () => {
17 it("leaves an unstated visibility for the client preference to decide", () => {
18 // Filling `open` in here would outrank `browserAutoShowFloatingPreview`,
19 // which is desktop-local and cannot be read from the server.
20 expect(normalizePreviewOpenInput({})).toEqual({ reuseExistingTab: true });
21 });
22
23 it("preserves an explicit background-only opt-out", () => {
24 expect(normalizePreviewOpenInput({ open: false })).toEqual({
25 open: false,
26 reuseExistingTab: true,
27 show: false,
28 });
29 });
30
31 it("supports show as a legacy alias while preferring open", () => {
32 expect(normalizePreviewOpenInput({ show: false })).toEqual({
33 open: false,
34 reuseExistingTab: true,
35 show: false,
36 });
37 expect(normalizePreviewOpenInput({ open: true, show: false })).toEqual({
38 open: true,
39 reuseExistingTab: true,
40 show: true,
41 });
42 });
43});
44
45describe("claimPreviewRecording", () => {
46 it.effect("overlapping and repeated claims return the same retained recording", () =>
47 Effect.gen(function* () {
48 const config = yield* ServerConfig.ServerConfig;
49 const fileSystem = yield* FileSystem.FileSystem;
50 const path = yield* Path.Path;
51 const uploadedAttachmentId = createPendingAttachmentId(".webm");
52 const pendingPath = path.join(config.attachmentsDir, `${uploadedAttachmentId}.webm`);
53 yield* fileSystem.makeDirectory(config.attachmentsDir, { recursive: true });
54 yield* fileSystem.writeFileString(pendingPath, "video!");
55 const response = {
56 id: "desktop-recording",
57 tabId: "tab-1",
58 path: "/desktop/recording.webm",
59 mimeType: "video/webm",
60 sizeBytes: 6,
61 createdAt: "2026-09-07T00:00:00.000Z",
62 uploadedAttachmentId,
63 };
64 const claim = claimPreviewRecording(ThreadId.make("thread-1"), response);
65 const [first, second] = yield* Effect.all([claim, claim], { concurrency: "unbounded" });
66 expect(first).toEqual(second);
67 expect(yield* claim).toEqual(first);
68 expect(yield* fileSystem.readFileString(first.path)).toBe("video!");
69 expect(yield* fileSystem.exists(pendingPath)).toBe(false);
70 const wrongThread = yield* claimPreviewRecording(ThreadId.make("thread-2"), response).pipe(
71 Effect.result,
72 );
73 expect(wrongThread._tag).toBe("Failure");
74 const wrongPath = yield* claimPreviewRecording(ThreadId.make("thread-1"), {
75 ...response,
76 uploadedAttachmentId: `../${uploadedAttachmentId}`,
77 }).pipe(Effect.result);
78 expect(wrongPath._tag).toBe("Failure");
79 }).pipe(
80 Effect.provide(
81 ServerConfig.layerTest(process.cwd(), { prefix: "t3-preview-recording-" }).pipe(
82 Layer.provideMerge(NodeServices.layer),
83 ),
84 ),
85 ),
86 );
87
88 it.effect.each([6, 5])(
89 "claims only a complete uploaded recording (reported bytes: %s)",
90 (sizeBytes) =>
91 Effect.gen(function* () {
92 const config = yield* ServerConfig.ServerConfig;
93 const fileSystem = yield* FileSystem.FileSystem;
94 const path = yield* Path.Path;
95 const uploadedAttachmentId = createPendingAttachmentId(".webm");
96 const pendingPath = path.join(config.attachmentsDir, `${uploadedAttachmentId}.webm`);
97 yield* fileSystem.makeDirectory(config.attachmentsDir, { recursive: true });
98 yield* fileSystem.writeFileString(pendingPath, "video!");
99 const response = {
100 id: "desktop-recording",
101 tabId: "tab-1",
102 path: "/desktop/recording.webm",
103 mimeType: "video/webm",
104 sizeBytes,
105 createdAt: "2026-09-07T00:00:00.000Z",
106 uploadedAttachmentId,
107 };
108 const result = yield* claimPreviewRecording(ThreadId.make("thread-1"), response).pipe(
109 Effect.result,
110 );
111 if (sizeBytes === 6) {
112 expect(result._tag).toBe("Success");
113 if (result._tag !== "Success") return;
114 expect(result.success.path).not.toBe(response.path);
115 expect(parseThreadSegmentFromAttachmentId(result.success.id)).toBe("thread-1");
116 expect(yield* fileSystem.readFileString(result.success.path)).toBe("video!");
117 expect(yield* fileSystem.exists(pendingPath)).toBe(false);
118 } else {
119 expect(result._tag).toBe("Failure");
120 if (result._tag !== "Failure") return;
121 expect(result.failure._tag).toBe("PreviewAutomationRecordingTransferError");
122 expect(yield* fileSystem.exists(pendingPath)).toBe(true);
123 }
124 }).pipe(
125 Effect.provide(
126 ServerConfig.layerTest(process.cwd(), { prefix: "t3-preview-recording-" }).pipe(
127 Layer.provideMerge(NodeServices.layer),
128 ),
129 ),
130 ),
131 );
132
133 it.effect("reports an older desktop without returning its inaccessible path", () =>
134 Effect.gen(function* () {
135 const result = yield* claimPreviewRecording(ThreadId.make("thread-1"), {
136 id: "desktop-recording",
137 tabId: "tab-1",
138 path: "/desktop/recording.webm",
139 mimeType: "video/webm",
140 sizeBytes: 6,
141 createdAt: "2026-09-07T00:00:00.000Z",
142 }).pipe(Effect.result);
143 expect(result._tag).toBe("Failure");
144 if (result._tag !== "Failure") return;
145 expect(result.failure._tag).toBe("PreviewAutomationRecordingDesktopUpdateRequiredError");
146 expect(result.failure.message).toContain("Update the desktop app");
147 }).pipe(
148 Effect.provide(
149 ServerConfig.layerTest(process.cwd(), { prefix: "t3-preview-recording-" }).pipe(
150 Layer.provideMerge(NodeServices.layer),
151 ),
152 ),
153 ),
154 );
155});