← Files Biological Sequence & Alignment ViewerARCHIVED FILE

src/workbench-persistence.integration.test.ts

8.16 KB · Sep 30, 2026 · 23:01 UTC

↓ Download file

import { randomUUID } from "node:crypto";
import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";

import type { App } from "@modelcontextprotocol/ext-apps";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { afterEach, describe, expect, it } from "vitest";

import { createSequenceViewerServer } from "./server";
import {
  SEQUENCE_VIEWER_COMPLETE_COMMAND_TOOL_NAME,
  SEQUENCE_VIEWER_REGISTER_SESSION_TOOL_NAME,
  SEQUENCE_VIEWER_WAIT_FOR_COMMAND_TOOL_NAME,
  queuedSequenceViewerCommandSchema,
} from "./viewer-commands";
import {
  SEQUENCE_VIEWER_EXPORT_TOOL_NAME,
  SEQUENCE_VIEWER_SAVE_SESSION_TOOL_NAME,
} from "./viewer-operations";
import { persistSequenceCommandResult } from "./views/workbench-persistence";

const temporaryDirectories: Array<string> = [];

afterEach(async () => {
  await Promise.all(
    temporaryDirectories
      .splice(0)
      .map((directory) => rm(directory, { force: true, recursive: true })),
  );
});

describe("real app/server workbench persistence protocol", () => {
  it("persists multi-megabyte artifacts and near-limit sessions before compact completion", async () => {
    const stateDirectory = await temporaryDirectory();
    const client = new Client(
      { name: "sequence-persistence-integration", version: "0.1.0" },
      { capabilities: {} },
    );
    const server = createSequenceViewerServer({ stateDirectory });
    const [clientTransport, serverTransport] =
      InMemoryTransport.createLinkedPair();
    await Promise.all([
      client.connect(clientTransport),
      server.connect(serverTransport),
    ]);
    const app = {
      callServerTool: async (request: Parameters<App["callServerTool"]>[0]) =>
        await client.callTool(request),
    } as unknown as Pick<App, "callServerTool">;
    try {
      const registration = await client.callTool({
        arguments: {},
        name: SEQUENCE_VIEWER_REGISTER_SESSION_TOOL_NAME,
      });
      const sessionId = (
        registration.structuredContent as { sessionId: string }
      ).sessionId;

      const exportPromise = client.callTool({
        arguments: { format: "fasta", scope: "all", sessionId },
        name: SEQUENCE_VIEWER_EXPORT_TOOL_NAME,
      });
      const exportCommand = await waitForCommand(client, sessionId, 0);
      const exportContent = `>${"A".repeat(3 * 1_024 * 1_024)}`;
      const exportCompletion = await persistSequenceCommandResult(app, {
        command: exportCommand,
        result: {
          applied: true,
          message: "Prepared large.fasta for download.",
          state: {
            artifact: {
              content: exportContent,
              format: "fasta",
              mediaType: "text/x-fasta",
              name: "large.fasta",
            },
            provenance: {
              engine: "sequence-viewer-export-v1",
              parameters: { scope: "all" },
              sourceRevision: 1,
            },
          },
        },
        sessionId,
      });
      expect(JSON.stringify(exportCompletion)).not.toContain(
        exportContent.slice(-10_000),
      );
      await client.callTool({
        arguments: {
          applied: exportCompletion.applied,
          commandId: exportCommand.commandId,
          message: exportCompletion.message,
          sessionId,
          state: exportCompletion.state,
        },
        name: SEQUENCE_VIEWER_COMPLETE_COMMAND_TOOL_NAME,
      });
      const exported = await exportPromise;
      expect(exported.structuredContent).toMatchObject({
        result: {
          resourceUri: expect.stringMatching(/^viewer-artifact:/u),
          sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
          size: Buffer.byteLength(exportContent),
        },
      });

      const savePromise = client.callTool({
        arguments: { name: "large.session.json", sessionId },
        name: SEQUENCE_VIEWER_SAVE_SESSION_TOOL_NAME,
      });
      const saveCommand = await waitForCommand(
        client,
        sessionId,
        exportCommand.revision,
      );
      const session = validSession(440 * 1_024);
      const saveCompletion = await persistSequenceCommandResult(app, {
        command: saveCommand,
        result: {
          applied: true,
          message: "Serialized the current workbench session.",
          state: { session },
        },
        sessionId,
      });
      expect(JSON.stringify(saveCompletion)).not.toContain(
        session.slice(-10_000),
      );
      await client.callTool({
        arguments: {
          applied: saveCompletion.applied,
          commandId: saveCommand.commandId,
          message: saveCompletion.message,
          sessionId,
          state: saveCompletion.state,
        },
        name: SEQUENCE_VIEWER_COMPLETE_COMMAND_TOOL_NAME,
      });
      await expect(savePromise).resolves.toMatchObject({
        structuredContent: {
          result: {
            name: "large.session.json",
            savedSessionId: expect.any(String),
            sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
            size: Buffer.byteLength(session),
          },
        },
      });
    } finally {
      await client.close();
      await server.close();
    }
  });

  it("rejects persistence that is not bound to the pending command", async () => {
    const stateDirectory = await temporaryDirectory();
    const client = new Client(
      { name: "sequence-persistence-binding", version: "0.1.0" },
      { capabilities: {} },
    );
    const server = createSequenceViewerServer({ stateDirectory });
    const [clientTransport, serverTransport] =
      InMemoryTransport.createLinkedPair();
    await Promise.all([
      client.connect(clientTransport),
      server.connect(serverTransport),
    ]);
    try {
      await expect(
        client.callTool({
          arguments: {
            byteLength: 1,
            callerId: randomUUID(),
            commandId: randomUUID(),
            dataBase64: "QQ==",
            format: "fasta",
            kind: "artifact",
            mediaType: "text/x-fasta",
            name: "unauthorized.fasta",
            sessionId: randomUUID(),
            sha256:
              "559aead08264d5795d3909718cdd05abd49572e84fe55590eef31a88a08fdffd",
            uploadId: randomUUID(),
          },
          name: "sequence.persist_workbench_payload",
        }),
      ).resolves.toMatchObject({
        content: [{ text: expect.stringMatching(/viewer session|pending/u) }],
        isError: true,
      });
    } finally {
      await client.close();
      await server.close();
    }
  });
});

async function waitForCommand(
  client: Client,
  sessionId: string,
  afterRevision: number,
) {
  const response = await client.callTool({
    arguments: { afterRevision, sessionId, timeoutMs: 1_000 },
    name: SEQUENCE_VIEWER_WAIT_FOR_COMMAND_TOOL_NAME,
  });
  return queuedSequenceViewerCommandSchema.parse(
    (response.structuredContent as { command: unknown }).command,
  );
}

function validSession(artifactBytes: number): string {
  return JSON.stringify({
    artifacts: [
      {
        content: `>${"C".repeat(artifactBytes - 1)}`,
        createdAt: 1,
        format: "fasta",
        id: randomUUID(),
        mediaType: "text/x-fasta",
        name: "derived.fasta",
        provenance: {
          engine: "test",
          parameters: {},
          sourceRevision: 0,
        },
      },
    ],
    createdAt: 1,
    dirty: false,
    jobs: [],
    revision: 0,
    schemaVersion: 1,
    source: { fileName: "demo.fasta", format: "fasta" },
    tracks: [],
    view: {
      mode: "sequence",
      sequence: {
        geneticCodeId: 1,
        layout: "linear",
        orientation: "forward",
        paletteId: "neutral",
        selectedFeatureId: null,
        selectedRecordId: "record-1",
        selection: null,
        showFeatures: true,
        showQuality: true,
        showTranslation: true,
        synchronizedViews: true,
        viewport: null,
        wrapWidth: 60,
      },
    },
  });
}

async function temporaryDirectory(): Promise<string> {
  const directory = await mkdtemp(
    path.join(os.tmpdir(), "sequence-persistence-integration-"),
  );
  temporaryDirectories.push(directory);
  return directory;
}

SHA-256: 60221d2607d3e6bafcd8985eb6e57f2fd875a3dfa5259fee0a34d27d922d6276