// @effect-diagnostics nodeBuiltinImport:off import * as NodeFS from "node:fs"; import * as NodePath from "node:path"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { describe, expect, it } from "@effect/vitest"; import { type ClientOrchestrationCommand, CommandId, MessageId, ThreadId, } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as ServerConfig from "../config.ts"; import * as WorkspacePaths from "../workspace/WorkspacePaths.ts"; import { cleanupFailedUploadedAttachments, normalizeDispatchCommand } from "./Normalizer.ts"; const testLayer = Layer.mergeAll( WorkspacePaths.layer, ServerConfig.layerTest(process.cwd(), { prefix: "t3-normalizer-attachments-" }), ).pipe(Layer.provideMerge(NodeServices.layer)); const attachmentUuid = "00000000-0000-4000-8000-0000000000aa"; function turnStartCommand(input: { readonly threadId?: string; readonly attachments: ReadonlyArray< | { readonly id: string; readonly sizeBytes: number } | { readonly dataUrl: string; readonly sizeBytes: number } >; }): ClientOrchestrationCommand { return { type: "thread.turn.start", commandId: CommandId.make("command-1"), threadId: ThreadId.make(input.threadId ?? "thread-1"), message: { messageId: MessageId.make("message-1"), role: "user", text: "look at this", attachments: input.attachments.map((attachment) => ({ type: "image" as const, name: "screenshot.png", mimeType: "image/png", ...attachment, })), }, runtimeMode: "full-access", interactionMode: "default", createdAt: "2026-08-01T00:00:00.000Z", }; } describe("normalizeDispatchCommand attachments", () => { it.effect("preserves inline image attachments from existing mobile clients", () => Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; const normalized = yield* normalizeDispatchCommand( turnStartCommand({ attachments: [{ dataUrl: "data:image/png;base64,cGl4ZWxz", sizeBytes: 6 }], }), ); if (normalized.type !== "thread.turn.start") { throw new Error("Expected a thread.turn.start command."); } const attachment = normalized.message.attachments[0]!; expect(attachment.id.startsWith("thread-1-")).toBe(true); expect( NodeFS.readFileSync(NodePath.join(config.attachmentsDir, `${attachment.id}.png`)), ).toEqual(Buffer.from("pixels")); }).pipe(Effect.provide(testLayer)), ); it.effect("claims uploaded attachments while retaining a retryable pending copy", () => Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; const bytes = Buffer.from("pixels"); const pendingPath = NodePath.join(config.attachmentsDir, `pending-${attachmentUuid}.png`); NodeFS.writeFileSync(pendingPath, bytes); const normalized = yield* normalizeDispatchCommand( turnStartCommand({ attachments: [{ id: `pending-${attachmentUuid}`, sizeBytes: bytes.byteLength }], }), ); if (normalized.type !== "thread.turn.start") { throw new Error("Expected a thread.turn.start command."); } const attachmentId = normalized.message.attachments[0]!.id; expect(attachmentId.startsWith("thread-1-")).toBe(true); expect(attachmentId).not.toBe(`thread-1-${attachmentUuid}`); expect(NodeFS.existsSync(pendingPath)).toBe(true); expect(NodeFS.existsSync(NodePath.join(config.attachmentsDir, `${attachmentId}.png`))).toBe( true, ); }).pipe(Effect.provide(testLayer)), ); it.effect("normalizes inline and uploaded attachments in the same turn", () => Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; NodeFS.writeFileSync( NodePath.join(config.attachmentsDir, `pending-${attachmentUuid}.png`), Buffer.from("pixels"), ); const normalized = yield* normalizeDispatchCommand( turnStartCommand({ attachments: [ { dataUrl: "data:image/png;base64,cGl4ZWxz", sizeBytes: 6 }, { id: `pending-${attachmentUuid}`, sizeBytes: 6 }, ], }), ); if (normalized.type !== "thread.turn.start") { throw new Error("Expected a thread.turn.start command."); } expect(normalized.message.attachments).toHaveLength(2); expect(normalized.message.attachments[1]?.id.startsWith("thread-1-")).toBe(true); }).pipe(Effect.provide(testLayer)), ); it.effect("retries a failed bootstrap with a fresh thread id", () => Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; const bytes = Buffer.from("pixels"); NodeFS.writeFileSync( NodePath.join(config.attachmentsDir, `pending-${attachmentUuid}.png`), bytes, ); const first = yield* normalizeDispatchCommand( turnStartCommand({ attachments: [{ id: `pending-${attachmentUuid}`, sizeBytes: bytes.byteLength }], }), ); if (first.type !== "thread.turn.start") { throw new Error("Expected a thread.turn.start command."); } NodeFS.rmSync( NodePath.join(config.attachmentsDir, `${first.message.attachments[0]!.id}.png`), ); const retried = yield* normalizeDispatchCommand( turnStartCommand({ threadId: "thread-retry", attachments: [{ id: `pending-${attachmentUuid}`, sizeBytes: bytes.byteLength }], }), ); if (retried.type !== "thread.turn.start") { throw new Error("Expected a thread.turn.start command."); } expect(retried.message.attachments[0]?.id.startsWith("thread-retry-")).toBe(true); }).pipe(Effect.provide(testLayer)), ); it.effect("removes failed attachment claims without deleting their pending uploads", () => Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; const pendingPath = NodePath.join(config.attachmentsDir, `pending-${attachmentUuid}.png`); NodeFS.writeFileSync(pendingPath, Buffer.from("pixels")); const command = turnStartCommand({ attachments: [ { dataUrl: "data:image/png;base64,cGl4ZWxz", sizeBytes: 6 }, { id: `pending-${attachmentUuid}`, sizeBytes: 6 }, ], }); const normalized = yield* normalizeDispatchCommand(command); if (normalized.type !== "thread.turn.start") { throw new Error("Expected a thread.turn.start command."); } const inlinePath = NodePath.join( config.attachmentsDir, `${normalized.message.attachments[0]!.id}.png`, ); const claimedPath = NodePath.join( config.attachmentsDir, `${normalized.message.attachments[1]!.id}.png`, ); yield* cleanupFailedUploadedAttachments(command, normalized); expect(NodeFS.existsSync(pendingPath)).toBe(true); expect(NodeFS.existsSync(claimedPath)).toBe(false); expect(NodeFS.existsSync(inlinePath)).toBe(true); }).pipe(Effect.provide(testLayer)), ); it.effect("removes a failed claimed copy after its pending original was removed", () => Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; const pendingPath = NodePath.join(config.attachmentsDir, `pending-${attachmentUuid}.png`); NodeFS.writeFileSync(pendingPath, Buffer.from("pixels")); const command = turnStartCommand({ attachments: [{ id: `pending-${attachmentUuid}`, sizeBytes: 6 }], }); const normalized = yield* normalizeDispatchCommand(command); if (normalized.type !== "thread.turn.start") { throw new Error("Expected a thread.turn.start command."); } const claimedPath = NodePath.join( config.attachmentsDir, `${normalized.message.attachments[0]!.id}.png`, ); NodeFS.rmSync(pendingPath); yield* cleanupFailedUploadedAttachments(command, normalized); expect(NodeFS.existsSync(claimedPath)).toBe(false); }).pipe(Effect.provide(testLayer)), ); it.effect("keeps concurrent claims independent when one dispatch fails", () => Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; const pendingPath = NodePath.join(config.attachmentsDir, `pending-${attachmentUuid}.png`); NodeFS.writeFileSync(pendingPath, Buffer.from("pixels")); const command = turnStartCommand({ attachments: [{ id: `pending-${attachmentUuid}`, sizeBytes: 6 }], }); const [failed, succeeded] = yield* Effect.all( [normalizeDispatchCommand(command), normalizeDispatchCommand(command)], { concurrency: 2 }, ); if (failed.type !== "thread.turn.start" || succeeded.type !== "thread.turn.start") { throw new Error("Expected thread.turn.start commands."); } const failedPath = NodePath.join( config.attachmentsDir, `${failed.message.attachments[0]!.id}.png`, ); const succeededPath = NodePath.join( config.attachmentsDir, `${succeeded.message.attachments[0]!.id}.png`, ); expect(failedPath).not.toBe(succeededPath); yield* cleanupFailedUploadedAttachments(command, failed); expect(NodeFS.existsSync(pendingPath)).toBe(true); expect(NodeFS.existsSync(failedPath)).toBe(false); expect(NodeFS.existsSync(succeededPath)).toBe(true); }).pipe(Effect.provide(testLayer)), ); it.effect("removes earlier claimed copies when a later attachment cannot be normalized", () => Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; const pendingId = `pending-${attachmentUuid}`; const pendingPath = NodePath.join(config.attachmentsDir, `${pendingId}.png`); NodeFS.writeFileSync(pendingPath, Buffer.from("pixels")); const failure = yield* normalizeDispatchCommand( turnStartCommand({ attachments: [ { id: pendingId, sizeBytes: 6 }, { id: "pending-00000000-0000-4000-8000-0000000000ff", sizeBytes: 6, }, ], }), ).pipe(Effect.flip); expect(failure.message).toContain("not found"); expect(NodeFS.readdirSync(config.attachmentsDir)).toEqual([`${pendingId}.png`]); }).pipe(Effect.provide(testLayer)), ); it.effect("rejects uploaded attachments with the wrong size or thread", () => Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; NodeFS.writeFileSync( NodePath.join(config.attachmentsDir, `pending-${attachmentUuid}.png`), Buffer.from("pixels"), ); const wrongSize = yield* normalizeDispatchCommand( turnStartCommand({ attachments: [{ id: `pending-${attachmentUuid}`, sizeBytes: 999 }], }), ).pipe(Effect.flip); expect(wrongSize.message).toContain("size"); const wrongThread = yield* normalizeDispatchCommand( turnStartCommand({ attachments: [{ id: `another-thread-${attachmentUuid}`, sizeBytes: 6 }], }), ).pipe(Effect.flip); expect(wrongThread.message).toContain("pending upload"); const mismatchedTypeCommand = turnStartCommand({ attachments: [{ id: `pending-${attachmentUuid}`, sizeBytes: 6 }], }); if (mismatchedTypeCommand.type !== "thread.turn.start") { throw new Error("Expected a thread.turn.start command."); } const mismatchedType = yield* normalizeDispatchCommand({ ...mismatchedTypeCommand, message: { ...mismatchedTypeCommand.message, attachments: mismatchedTypeCommand.message.attachments.map((attachment) => ({ ...attachment, mimeType: "image/jpeg", })), }, }).pipe(Effect.flip); expect(mismatchedType.message).toContain("image type"); }).pipe(Effect.provide(testLayer)), ); });