← Files Biological Sequence & Alignment ViewerARCHIVED FILE

src/workspace-session-manager.test.ts

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

↓ Download file

import { createHash, randomUUID } from "node:crypto";
import {
  link,
  mkdir,
  mkdtemp,
  readFile,
  rm,
  writeFile,
} from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";

import { afterEach, describe, expect, it } from "vitest";

import type { RootsRequestExtra } from "./chat-file-resource";
import type { SequenceWorkbenchPayloadDeclaration } from "./workbench-persistence-protocol";
import { SequenceWorkspaceExportPublisher } from "./workspace-export-publisher";
import {
  createSequenceWorkspaceSessionManifest,
  SequenceWorkspaceSessionManager,
} from "./workspace-session-manager";
import { sequenceWorkspaceSessionManifestSchema } from "./workspace-session-protocol";

const temporaryDirectories: Array<string> = [];

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

describe("SequenceWorkspaceSessionManager", () => {
  it.each(["sequence", "alignment"] as const)(
    "creates, discovers, and revalidates a %s project",
    async (mode) => {
      const fixture = await createFixture();
      const payload = validSession(mode);
      await publishManifest(fixture, payload, `${mode}.sequence-viewer.session.json`);

      const listed = await fixture.manager.list(
        { sessionId: fixture.sessionId },
        fixture.extra,
      );
      expect(listed).toMatchObject({
        candidates: [
          {
            mode,
            sourceStatus: "verification-required",
            workspacePath: `data/${mode}.sequence-viewer.session.json`,
          },
        ],
        omittedCandidates: 0,
      });

      const resolved = await fixture.manager.resolve(
        listed.candidates[0]!.candidateId,
        fixture.sessionId,
        undefined,
        fixture.extra,
      );
      expect(resolved).toMatchObject({ mode, payload });
    },
  );

  it("omits malformed, incompatible, digest-tampered, linked, and non-adjacent manifests", async () => {
    const fixture = await createFixture();
    const validPath = await publishManifest(
      fixture,
      validSession("sequence"),
      "valid.sequence-viewer.session.json",
    );
    const valid = JSON.parse(await readFile(validPath, "utf8")) as Record<
      string,
      unknown
    >;
    await writeFile(
      path.join(fixture.sourceDirectory, "malformed.sequence-viewer.session.json"),
      "{",
    );
    await writeFile(
      path.join(fixture.sourceDirectory, "incompatible.sequence-viewer.session.json"),
      JSON.stringify({ ...valid, schemaVersion: 2 }),
    );
    await writeFile(
      path.join(fixture.sourceDirectory, "tampered.sequence-viewer.session.json"),
      JSON.stringify({ ...valid, payload: `${String(valid.payload)} ` }),
    );
    await link(
      validPath,
      path.join(fixture.sourceDirectory, "linked.sequence-viewer.session.json"),
    );
    await mkdir(path.join(fixture.sourceDirectory, "nested"));
    await writeFile(
      path.join(
        fixture.sourceDirectory,
        "nested",
        "hidden.sequence-viewer.session.json",
      ),
      JSON.stringify(valid),
    );

    const listed = await fixture.manager.list(
      { sessionId: fixture.sessionId },
      fixture.extra,
    );
    // Hardlinking the valid manifest also raises its link count, so both names
    // are omitted instead of exposing an aliasing race.
    expect(listed.candidates).toEqual([]);
    expect(listed.omittedCandidates).toBe(3);
  });

  it("reports optional dependency drift but rejects required dependency drift", async () => {
    const fixture = await createFixture();
    const trackPath = path.join(fixture.sourceDirectory, "features.bed");
    const track = "chr1\t0\t4\tfeature\n";
    await writeFile(trackPath, track);
    const manifestPath = await publishManifest(
      fixture,
      validSession("sequence", {
        trackHash: sha256(track),
        trackWorkspacePath: "data/features.bed",
      }),
      "tracked.sequence-viewer.session.json",
    );
    await rm(trackPath);

    let listed = await fixture.manager.list(
      { sessionId: fixture.sessionId },
      fixture.extra,
    );
    expect(listed.candidates[0]?.dependencies).toEqual([
      expect.objectContaining({ required: false, status: "missing" }),
    ]);
    await expect(
      fixture.manager.resolve(
        listed.candidates[0]!.candidateId,
        fixture.sessionId,
        undefined,
        fixture.extra,
      ),
    ).resolves.toMatchObject({
      dependencies: [expect.objectContaining({ status: "missing" })],
    });

    const manifest = sequenceWorkspaceSessionManifestSchema.parse(
      JSON.parse(await readFile(manifestPath, "utf8")),
    );
    manifest.dependencies[0]!.required = true;
    await writeFile(manifestPath, `${JSON.stringify(manifest)}\n`);
    listed = await fixture.manager.list(
      { sessionId: fixture.sessionId },
      fixture.extra,
    );
    await expect(
      fixture.manager.resolve(
        listed.candidates[0]!.candidateId,
        fixture.sessionId,
        undefined,
        fixture.extra,
      ),
    ).rejects.toThrow("Required workspace dependency");
  });

  it("rejects expired, cross-viewer, changed-manifest, changed-source, and cancelled restores", async () => {
    let now = 1_750_000_000_000;
    const fixture = await createFixture({ now: () => now });
    const manifestPath = await publishManifest(
      fixture,
      validSession("sequence"),
      "durable.sequence-viewer.session.json",
    );
    let listed = await fixture.manager.list(
      { sessionId: fixture.sessionId },
      fixture.extra,
    );
    const candidateId = listed.candidates[0]!.candidateId;
    await expect(
      fixture.manager.resolve(candidateId, randomUUID(), undefined, fixture.extra),
    ).rejects.toThrow("another viewer");

    const cancelled = new AbortController();
    cancelled.abort(new DOMException("cancelled", "AbortError"));
    await expect(
      fixture.manager.resolve(
        candidateId,
        fixture.sessionId,
        cancelled.signal,
        fixture.extra,
      ),
    ).rejects.toMatchObject({ name: "AbortError" });

    await writeFile(manifestPath, await readFile(manifestPath));
    await expect(
      fixture.manager.resolve(
        candidateId,
        fixture.sessionId,
        undefined,
        fixture.extra,
      ),
    ).rejects.toThrow(/changed/u);

    listed = await fixture.manager.list(
      { sessionId: fixture.sessionId },
      fixture.extra,
    );
    await writeFile(fixture.sourcePath, ">source\nTGCA\n");
    await expect(
      fixture.manager.resolve(
        listed.candidates[0]!.candidateId,
        fixture.sessionId,
        undefined,
        fixture.extra,
      ),
    ).rejects.toThrow();

    now += 20 * 60 * 1_000;
    await expect(
      fixture.manager.resolve(candidateId, fixture.sessionId, undefined, fixture.extra),
    ).rejects.toThrow("expired");
  });
});

async function createFixture({ now }: { now?: () => number } = {}) {
  const root = await mkdtemp(path.join(os.tmpdir(), "sequence-session-test-"));
  temporaryDirectories.push(root);
  const sourceDirectory = path.join(root, "data");
  await mkdir(sourceDirectory);
  const sourcePath = path.join(sourceDirectory, "source.fasta");
  await writeFile(sourcePath, ">source\nACGT\n");
  const extra = {
    async sendRequest() {
      return { roots: [{ uri: pathToFileURL(root).href }] };
    },
  } as unknown as RootsRequestExtra;
  const sessionId = randomUUID();
  const publisher = new SequenceWorkspaceExportPublisher({ now });
  await publisher.bindSession(sessionId, sourcePath, extra);
  return {
    extra,
    manager: new SequenceWorkspaceSessionManager(publisher, { now }),
    publisher,
    root,
    sessionId,
    sourceDirectory,
    sourcePath,
  };
}

async function publishManifest(
  fixture: Awaited<ReturnType<typeof createFixture>>,
  payload: string,
  name: string,
): Promise<string> {
  const declaration: SequenceWorkbenchPayloadDeclaration = {
    byteLength: Buffer.byteLength(payload),
    callerId: randomUUID(),
    commandId: randomUUID(),
    destination: {
      base: "opened-source",
      kind: "workspace",
      relativePath: name,
    },
    kind: "session",
    name,
    sessionId: fixture.sessionId,
    sha256: sha256(payload),
    uploadId: randomUUID(),
  };
  const plan = await fixture.publisher.prepare(declaration, undefined, fixture.extra);
  if (plan == null) throw new Error("Expected a workspace plan.");
  const prepared = await createSequenceWorkspaceSessionManifest(payload, plan);
  await fixture.publisher.publish(
    plan,
    prepared.bytes,
    prepared.sha256,
    new AbortController().signal,
    fixture.extra,
  );
  return plan.outputPath;
}

function validSession(
  mode: "alignment" | "sequence",
  dependency?: { trackHash: string; trackWorkspacePath: string },
): string {
  return JSON.stringify({
    artifacts: [],
    createdAt: 1,
    dirty: false,
    jobs: [],
    revision: 0,
    schemaVersion: 1,
    source: {
      fileName: "source.fasta",
      format: mode === "sequence" ? "fasta" : "aligned-fasta",
      stateKey: "source-state",
    },
    tracks:
      dependency == null
        ? []
        : [
            {
              features: [],
              format: "bed",
              id: "track-1",
              kind: "annotations",
              mapping: {
                matchedReference: null,
                requestedReference: null,
                status: "unresolved",
                unmatchedReferences: [],
              },
              name: "features.bed",
              source: {
                contentHash: dependency.trackHash,
                displayName: "features.bed",
                workspacePath: dependency.trackWorkspacePath,
              },
              summary: { itemCount: 0, references: [], truncated: false },
            },
          ],
    view:
      mode === "sequence"
        ? { mode, sequence: sequenceView() }
        : { alignment: alignmentView(), mode },
  });
}

function sequenceView() {
  return {
    geneticCodeId: 1,
    layout: "linear",
    orientation: "forward",
    paletteId: "neutral",
    selectedFeatureId: null,
    selectedRecordId: "source",
    selection: null,
    showFeatures: true,
    showQuality: true,
    showTranslation: true,
    synchronizedViews: true,
    viewport: null,
    wrapWidth: 60,
  };
}

function alignmentView() {
  return {
    analysisScope: "all-unhidden-rows",
    cellWidth: 24,
    colorMode: "identity",
    referenceMode: "none",
    residuePalette: null,
    rowFilter: "",
    searchScope: "currently-displayed-rows",
    selectedColumns: null,
    selectedRows: [],
    showAnnotationTracks: true,
    showIdenticalAsDots: false,
    showRnaStructureOverlays: true,
  };
}

function sha256(value: string): string {
  return createHash("sha256").update(value).digest("hex");
}

SHA-256: 0d93a07f553611d3b3cf794ef94cb26cbf14bb6b27d167b1fdd5a746fed93cf1