561 lines · 21.8 KB · TypeScript
RawDownload
// @effect-diagnostics nodeBuiltinImport:offimport * 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 {  ClientOrchestrationCommand,  CommandId,  ApprovalRequestId,  MessageId,  ThreadId,} from "@t3tools/contracts";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Schema from "effect/Schema"; 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";const isClientCommand = Schema.is(ClientOrchestrationCommand); 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);      const claimedPngPath = NodePath.join(config.attachmentsDir, `${attachmentId}.png`);      expect(NodeFS.existsSync(claimedPngPath)).toBe(true);      // A copy, not a hard link: editing the delivered file must not mutate      // the retryable pending upload.      expect(NodeFS.statSync(claimedPngPath).ino).not.toBe(NodeFS.statSync(pendingPath).ino);      expect(NodeFS.readFileSync(claimedPngPath)).toEqual(bytes);    }).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("claims uploaded documents without changing their original extension", () =>    Effect.gen(function* () {      const config = yield* ServerConfig.ServerConfig;      const pendingId = `pending-${attachmentUuid}-pdf`;      const pendingPath = NodePath.join(config.attachmentsDir, `${pendingId}.pdf`);      NodeFS.writeFileSync(pendingPath, Buffer.from("report"));       const imageCommand = turnStartCommand({ attachments: [] });      if (imageCommand.type !== "thread.turn.start") {        throw new Error("Expected a thread.turn.start command.");      }      const normalized = yield* normalizeDispatchCommand({        ...imageCommand,        message: {          ...imageCommand.message,          attachments: [            {              type: "file",              id: pendingId,              name: "report.pdf",              mimeType: "application/pdf",              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.type).toBe("file");      expect(attachment.id).toMatch(/^thread-1-.*-pdf$/);      const claimedPath = NodePath.join(config.attachmentsDir, `${attachment.id}.pdf`);      expect(NodeFS.readFileSync(claimedPath)).toEqual(Buffer.from("report"));      expect(NodeFS.statSync(claimedPath).ino).not.toBe(NodeFS.statSync(pendingPath).ino);    }).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("attachment type");    }).pipe(Effect.provide(testLayer)),  );}); describe("question attachments", () => {  it.effect("enforces the total response limit and claims duplicate filenames independently", () =>    Effect.gen(function* () {      const config = yield* ServerConfig.ServerConfig;      const id = `pending-${attachmentUuid}-txt`;      NodeFS.writeFileSync(NodePath.join(config.attachmentsDir, `${id}.txt`), "report");      const attachment = {        type: "file" as const,        id,        name: 'notes "final" ü.txt',        mimeType: "text/plain",        sizeBytes: 6,      };      const command: ClientOrchestrationCommand = {        type: "thread.user-input.respond",        commandId: CommandId.make("answer-cap"),        threadId: ThreadId.make("thread-1"),        requestId: ApprovalRequestId.make("request-cap"),        answers: { first: "", second: "" },        createdAt: "2026-08-01T00:00:00.000Z",        attachmentsByQuestionId: {          first: Array.from({ length: 4 }, () => attachment),          second: Array.from({ length: 5 }, () => attachment),        },      };      const failure = yield* normalizeDispatchCommand(command).pipe(Effect.flip);      expect(failure.message).toContain("up to 8");      expect(NodeFS.readdirSync(config.attachmentsDir)).toEqual([`${id}.txt`]);      const accepted = {        ...command,        attachmentsByQuestionId: {          ...command.attachmentsByQuestionId,          second: Array.from({ length: 4 }, () => attachment),        },      };      const normalized = yield* normalizeDispatchCommand(accepted);      if (normalized.type !== "thread.user-input.respond") throw new Error("Wrong command");      const attachments = Object.values(normalized.attachmentsByQuestionId!).flat();      expect(attachments).toHaveLength(8);      expect(new Set(attachments.map((item) => item.id)).size).toBe(8);      for (const item of attachments) {        expect(item.name).toBe(attachment.name);        expect(          NodeFS.readFileSync(NodePath.join(config.attachmentsDir, `${item.id}.txt`), "utf8"),        ).toBe("report");      }      yield* cleanupFailedUploadedAttachments(accepted, normalized);      expect(NodeFS.readdirSync(config.attachmentsDir)).toEqual([`${id}.txt`]);    }).pipe(Effect.provide(testLayer)),  );  it("requires uploaded metadata for question images, including pasted images", () => {    expect(      isClientCommand({        type: "thread.user-input.respond",        commandId: "answer",        threadId: "thread-1",        requestId: "request",        answers: { q: "" },        createdAt: "2026-08-01T00:00:00.000Z",        attachmentsByQuestionId: {          q: [            {              type: "image",              name: "image.png",              mimeType: "image/png",              sizeBytes: 6,              dataUrl: "data:image/png;base64,cGl4ZWxz",            },          ],        },      }),    ).toBe(false);  });   it.effect("preserves a __proto__ question key and cleans up its claimed files", () =>    Effect.gen(function* () {      const config = yield* ServerConfig.ServerConfig;      const id = `pending-${attachmentUuid}`;      NodeFS.writeFileSync(NodePath.join(config.attachmentsDir, `${id}.png`), "pixels");      const command: ClientOrchestrationCommand = {        type: "thread.user-input.respond",        commandId: CommandId.make("answer"),        threadId: ThreadId.make("thread-1"),        requestId: ApprovalRequestId.make("request"),        answers: { ["__proto__"]: "" },        createdAt: "2026-08-01T00:00:00.000Z",        attachmentsByQuestionId: {          ["__proto__"]: [            { type: "image", id, name: "image.png", mimeType: "image/png", sizeBytes: 6 },          ],        },      };      const normalized = yield* normalizeDispatchCommand(command);      if (normalized.type !== "thread.user-input.respond") throw new Error("Wrong command");      expect(Object.keys(normalized.attachmentsByQuestionId!)).toEqual(["__proto__"]);      const attachment = normalized.attachmentsByQuestionId!["__proto__"]![0]!;      const claimedPath = NodePath.join(config.attachmentsDir, `${attachment.id}.png`);      expect(NodeFS.existsSync(claimedPath)).toBe(true);      yield* cleanupFailedUploadedAttachments(command, normalized);      expect(NodeFS.existsSync(claimedPath)).toBe(false);    }).pipe(Effect.provide(testLayer)),  );  it.effect(    "claims images and files by question, preserves answers, and cleans up failed dispatches",    () =>      Effect.gen(function* () {        const config = yield* ServerConfig.ServerConfig;        const imageId = `pending-${attachmentUuid}`;        const fileId = `pending-${attachmentUuid}-txt`;        NodeFS.writeFileSync(NodePath.join(config.attachmentsDir, `${imageId}.png`), "pixels");        NodeFS.writeFileSync(NodePath.join(config.attachmentsDir, `${fileId}.txt`), "report");        const command: ClientOrchestrationCommand = {          type: "thread.user-input.respond",          commandId: CommandId.make("answer"),          threadId: ThreadId.make("thread-1"),          requestId: ApprovalRequestId.make("request"),          answers: { q1: ["Selected option"], q2: "" },          createdAt: "2026-08-01T00:00:00.000Z",          attachmentsByQuestionId: {            q1: [              {                type: "image",                id: imageId,                name: "image.png",                mimeType: "image/png",                sizeBytes: 6,              },            ],            q2: [              {                type: "file",                id: fileId,                name: "report.txt",                mimeType: "text/plain",                sizeBytes: 6,              },

Lines 1–500 of 561.