192 lines · 6.3 KB · TypeScript
RawDownload
import * as NodeServices from "@effect/platform-node/NodeServices";import {  CommandId,  DEFAULT_PROVIDER_INTERACTION_MODE,  MessageId,  ProjectId,  PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,  ProviderInstanceId,  ThreadId,  type ClientOrchestrationCommand,  type UploadChatAttachment,} from "@t3tools/contracts";import { describe, expect, it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Path from "effect/Path"; import * as ServerConfig from "../config.ts";import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";import { canonicalizeClientCommandTimestamps, normalizeDispatchCommand } from "./Normalizer.ts"; const testLayer = Layer.mergeAll(  ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3-normalizer-test-" }),  WorkspacePaths.layer,).pipe(Layer.provideMerge(NodeServices.layer)); const clientCreatedAt = "2031-01-01T00:00:00.000Z";const serverReceivedAt = "2026-07-18T00:00:00.000Z";const now = "2026-01-01T00:00:00.000Z"; const turnStartCommand = (  attachments: ReadonlyArray<UploadChatAttachment>,): ClientOrchestrationCommand => ({  type: "thread.turn.start",  commandId: CommandId.make("cmd-turn-start-1"),  threadId: ThreadId.make("thread-1"),  message: {    messageId: MessageId.make("user-message-1"),    role: "user",    text: "hello normalizer",    attachments,  },  runtimeMode: "full-access",  interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,  createdAt: now,}); const asDataUrl = (mimeType: string, bytes: Buffer) =>  `data:${mimeType};base64,${bytes.toString("base64")}`; describe("canonicalizeClientCommandTimestamps", () => {  it("replaces a client command timestamp with the server receipt timestamp", () => {    const command: ClientOrchestrationCommand = {      type: "project.create",      commandId: CommandId.make("command-1"),      projectId: ProjectId.make("project-1"),      title: "Clock-safe project",      workspaceRoot: "/tmp/clock-safe-project",      createdAt: clientCreatedAt,    };     expect(canonicalizeClientCommandTimestamps(command, serverReceivedAt)).toEqual({      ...command,      createdAt: serverReceivedAt,    });  });   it("replaces both timestamps when the first turn bootstraps a thread", () => {    const command: ClientOrchestrationCommand = {      type: "thread.turn.start",      commandId: CommandId.make("command-2"),      threadId: ThreadId.make("thread-1"),      message: {        messageId: MessageId.make("message-1"),        role: "user",        text: "Start a thread",        attachments: [],      },      runtimeMode: "full-access",      interactionMode: "default",      bootstrap: {        createThread: {          projectId: ProjectId.make("project-1"),          title: "Clock-safe thread",          modelSelection: {            instanceId: ProviderInstanceId.make("codex"),            model: "gpt-5.4",          },          runtimeMode: "full-access",          interactionMode: "default",          branch: null,          worktreePath: null,          createdAt: clientCreatedAt,        },      },      createdAt: clientCreatedAt,    };     const result = canonicalizeClientCommandTimestamps(command, serverReceivedAt);     expect(result.type).toBe("thread.turn.start");    if (result.type !== "thread.turn.start") {      throw new Error("Expected a thread.turn.start command");    }    expect(result.createdAt).toBe(serverReceivedAt);    expect(result.bootstrap?.createThread?.createdAt).toBe(serverReceivedAt);  });}); describe("Normalizer", () => {  it.effect("persists file attachments and writes their bytes to the attachments dir", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const path = yield* Path.Path;      const { attachmentsDir } = yield* ServerConfig.ServerConfig;      const bytes = Buffer.from("hello world");       const normalized = yield* normalizeDispatchCommand(        turnStartCommand([          {            type: "file",            name: "notes.txt",            mimeType: "text/plain",            sizeBytes: bytes.byteLength,            dataUrl: asDataUrl("text/plain", bytes),          },        ]),      );       expect(normalized.type).toBe("thread.turn.start");      if (normalized.type !== "thread.turn.start") {        return;      }      const attachment = normalized.message.attachments[0];      expect(attachment).toMatchObject({        type: "file",        name: "notes.txt",        mimeType: "text/plain",        sizeBytes: bytes.byteLength,      });      expect(attachment?.id).toMatch(        /^thread-1-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,      );      if (!attachment) {        return;      }       const attachmentPath = path.join(attachmentsDir, `${attachment.id}.txt`);      expect(yield* fileSystem.readFileString(attachmentPath)).toBe("hello world");    }).pipe(Effect.provide(testLayer)),  );   it.effect("rejects image attachments with a non-image data URL mime type", () =>    Effect.gen(function* () {      const error = yield* normalizeDispatchCommand(        turnStartCommand([          {            type: "image",            name: "fake.png",            mimeType: "image/png",            sizeBytes: 5,            dataUrl: asDataUrl("text/plain", Buffer.from("hello")),          },        ]),      ).pipe(Effect.flip);       expect(error._tag).toBe("OrchestrationDispatchCommandError");      expect(error.message).toBe("Invalid image attachment payload for 'fake.png'.");    }).pipe(Effect.provide(testLayer)),  );   it.effect("rejects file attachments over the maximum attachment size", () =>    Effect.gen(function* () {      const bytes = Buffer.alloc(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES + 1, "a");      const error = yield* normalizeDispatchCommand(        turnStartCommand([          {            type: "file",            name: "big.txt",            mimeType: "text/plain",            sizeBytes: PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,            dataUrl: asDataUrl("text/plain", bytes),          },        ]),      ).pipe(Effect.flip);       expect(error._tag).toBe("OrchestrationDispatchCommandError");      expect(error.message).toBe("Attachment 'big.txt' is empty or too large.");    }).pipe(Effect.provide(testLayer)),  );});