1import { assert, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as Exit from "effect/Exit";
4import * as Schema from "effect/Schema";
5
6import {
7 ChatAttachment,
8 isProviderSendTurnSupportedImageMimeType,
9 PROVIDER_SEND_TURN_MAX_FILE_BYTES,
10 SnapShotAccessibility,
11} from "./chatAttachment.ts";
12
13const decodeAttachment = Schema.decodeUnknownEffect(ChatAttachment);
14const decodeSnapShotAccessibility = Schema.decodeUnknownEffect(SnapShotAccessibility);
15
16// Attachments ride on persisted events and thread streams with no client
17// version negotiation. A type this build does not know must decode instead of
18// failing the whole message.
19it.effect("tolerates attachment types from newer builds", () =>
20 Effect.gen(function* () {
21 const attachment = yield* decodeAttachment({
22 type: "somethingnew",
23 id: "thread-1-00000000-0000-4000-8000-000000000003-glb",
24 name: "scene.glb",
25 mimeType: "model/gltf-binary",
26 sizeBytes: 12,
27 });
28 assert.strictEqual(attachment.type, "somethingnew");
29 }),
30);
31
32// The tolerant member must not catch malformed known attachments: a file over
33// the size cap or an image with a bad mime has to fail its own schema, not
34// slide through the open one with those constraints unchecked.
35it.effect("rejects malformed known attachment types instead of tolerating them", () =>
36 Effect.gen(function* () {
37 const base = {
38 id: "thread-1-00000000-0000-4000-8000-000000000003-pdf",
39 name: "report.pdf",
40 mimeType: "application/pdf",
41 };
42 const oversizedFile = yield* Effect.exit(
43 decodeAttachment({ ...base, type: "file", sizeBytes: PROVIDER_SEND_TURN_MAX_FILE_BYTES + 1 }),
44 );
45 assert.strictEqual(Exit.isFailure(oversizedFile), true);
46 const badMimeImage = yield* Effect.exit(
47 decodeAttachment({ ...base, type: "image", mimeType: "application/pdf", sizeBytes: 12 }),
48 );
49 assert.strictEqual(Exit.isFailure(badMimeImage), true);
50 }),
51);
52
53it.effect("rejects accessibility trees above the serialized payload limit", () =>
54 Effect.gen(function* () {
55 const result = yield* Effect.exit(
56 decodeSnapShotAccessibility({
57 format: "element-tree",
58 coordinateSpace: "captured-image",
59 imageSize: { width: 800, height: 600 },
60 truncated: false,
61 root: {
62 role: "window",
63 bounds: { x: 0, y: 0, width: 800, height: 600 },
64 children: Array.from({ length: 10 }, () => ({
65 role: "text",
66 value: "x".repeat(8_000),
67 bounds: null,
68 children: [],
69 })),
70 },
71 }),
72 );
73
74 assert.strictEqual(Exit.isFailure(result), true);
75 }),
76);
77
78it("isProviderSendTurnSupportedImageMimeType accepts raster formats and rejects svg", () => {
79 assert.strictEqual(isProviderSendTurnSupportedImageMimeType("image/png"), true);
80 assert.strictEqual(isProviderSendTurnSupportedImageMimeType("IMAGE/JPEG"), true);
81 assert.strictEqual(isProviderSendTurnSupportedImageMimeType("image/svg+xml"), false);
82});