← Files Biological Sequence & Alignment ViewerARCHIVED FILE

src/sequence/tracks.test.ts

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

↓ Download file

import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";

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

import {
  buildEvidenceWindow,
  parseAnnotationTrack,
  parseSam,
  parseSequenceTrack,
  parseUncompressedBam,
  parseVcf,
} from "./tracks";
import {
  DEFAULT_READ_PILEUP_FILTERS,
  readPassesPileupFilters,
} from "./read-pileup";

describe("evidence and annotation tracks", () => {
  it("parses BED's zero-based half-open coordinates into explicit one-based intervals", () => {
    expect(
      parseAnnotationTrack("chr1\t9\t20\tfeature-a\t5\t-\n", "bed"),
    ).toEqual([
      expect.objectContaining({
        end: 20,
        label: "feature-a",
        reference: "chr1",
        score: 5,
        start: 10,
        strand: "-",
      }),
    ]);
  });

  it("preserves genuine UCSC BED12 exon blocks, coding bounds, strand, score, and RGB", () => {
    // Public Biopython UCSC fixture at immutable commit
    // c9489604d1d9607602ca9199a3852c1219ed330f, Tests/Blat/bed12.bed.
    const features = parseAnnotationTrack(
      [
        "chr22\t1000\t5000\tmRNA1\t960\t+\t1200\t4900\t255,0,0\t2\t567,488,\t0,3512,",
        "chr22\t2000\t6000\tmRNA2\t900\t-\t2300\t5960\t0,255,0\t2\t433,399,\t0,3601,",
      ].join("\n"),
      "bed",
    );

    expect(features).toEqual([
      expect.objectContaining({
        attributes: { item_rgb: "255,0,0" },
        codingSegments: [
          { end: 1567, start: 1201 },
          { end: 4900, start: 4513 },
        ],
        label: "mRNA1",
        score: 960,
        segments: [
          { end: 1567, start: 1001 },
          { end: 5000, start: 4513 },
        ],
        strand: "+",
      }),
      expect.objectContaining({
        attributes: { item_rgb: "0,255,0" },
        codingSegments: [
          { end: 2433, start: 2301 },
          { end: 5960, start: 5602 },
        ],
        label: "mRNA2",
        score: 900,
        segments: [
          { end: 2433, start: 2001 },
          { end: 6000, start: 5602 },
        ],
        strand: "-",
      }),
    ]);
    expect(
      features[0]?.segments?.some(
        ({ end, start }) => start <= 3000 && end >= 3000,
      ),
    ).toBe(false);
  });

  it("rejects incomplete, overlapping, and out-of-bounds BED12 block definitions", () => {
    expect(() =>
      parseAnnotationTrack(
        "chr22\t1000\t5000\tmRNA1\t960\t+\t1200\t4900\t255,0,0\t2\t567,488,\n",
        "bed",
      ),
    ).toThrow("blockCount, blockSizes, and blockStarts");
    expect(() =>
      parseAnnotationTrack(
        "chr22\t1000\t5000\tmRNA1\t960\t+\t1200\t4900\t255,0,0\t2\t567,488,\t0,200,\n",
        "bed",
      ),
    ).toThrow("overlapping or out-of-range");
    expect(() =>
      parseAnnotationTrack(
        "chr22\t1000\t5000\tmRNA1\t960\t+\t900\t4900\t255,0,0\t2\t567,488,\t0,3512,\n",
        "bed",
      ),
    ).toThrow("coding boundaries");
  });

  it("preserves GFF3 and GTF attributes without flattening coordinates", () => {
    expect(
      parseAnnotationTrack(
        "chr1\tsource\tgene\t4\t12\t.\t+\t.\tID=g1;Name=Gene%201\n",
        "gff3",
      )[0],
    ).toMatchObject({ id: "g1", label: "Gene 1", start: 4, end: 12 });
    expect(
      parseAnnotationTrack(
        'chr2\tsource\texon\t7\t19\t.\t-\t.\tgene_id "g2"; gene_name "Beta";\n',
        "gtf",
      )[0],
    ).toMatchObject({ id: "g2", label: "Beta", strand: "-" });
  });

  it("parses VCF alleles, filters, INFO, and samples", () => {
    const variants = parseVcf(
      "##fileformat=VCFv4.3\n#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\tS1\nchr1\t11\trs1\tA\tG,T\t50\tq10\tDP=12;SOMATIC\tGT\t0/1\n",
    );
    expect(variants[0]).toMatchObject({
      alternateAlleles: ["G", "T"],
      filters: ["q10"],
      format: "GT",
      id: "rs1",
      info: { DP: "12", SOMATIC: true },
      position: 11,
      quality: 50,
      rawFilter: "q10",
      rawId: "rs1",
      rawInfo: "DP=12;SOMATIC",
      rawQuality: "50",
      sampleValues: ["0/1"],
      samples: { S1: "0/1" },
    });
  });

  it("retains complete VCF metadata, declared sample order, and unknown FILTER semantics", () => {
    const track = parseSequenceTrack({
      content: [
        "##fileformat=VCFv4.3",
        '##FORMAT=<ID=GT,Number=1,Type=String,Description="Genotype">',
        '##INFO=<ID=DP,Number=1,Type=Integer,Description="Depth">',
        "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\tHG00096\tHG00097",
        "1\t10583\trs58108140\tG\tA\t100\t.\tDP=12\tGT:DS:GL\t0|0:0.200:-0.18,-0.47,-2.42\t0|0:0.150:-0.24,-0.44,-1.16",
      ].join("\n"),
      displayName: "official-vcf-projection.vcf",
      format: "vcf",
      id: "vcf-projection",
      requestedReference: "chr1",
    });

    expect(track.vcfHeader).toEqual({
      columns: [
        "#CHROM",
        "POS",
        "ID",
        "REF",
        "ALT",
        "QUAL",
        "FILTER",
        "INFO",
        "FORMAT",
        "HG00096",
        "HG00097",
      ],
      metaLines: [
        "##fileformat=VCFv4.3",
        '##FORMAT=<ID=GT,Number=1,Type=String,Description="Genotype">',
        '##INFO=<ID=DP,Number=1,Type=Integer,Description="Depth">',
      ],
      sampleNames: ["HG00096", "HG00097"],
    });
    expect(track.variants?.[0]).toMatchObject({
      filters: [],
      format: "GT:DS:GL",
      rawFilter: ".",
      rawId: "rs58108140",
      rawInfo: "DP=12",
      rawQuality: "100",
      sampleValues: [
        "0|0:0.200:-0.18,-0.47,-2.42",
        "0|0:0.150:-0.24,-0.44,-1.16",
      ],
      samples: {
        HG00096: "0|0:0.200:-0.18,-0.47,-2.42",
        HG00097: "0|0:0.150:-0.24,-0.44,-1.16",
      },
    });
    expect(track.mapping.matchedReference).toBe("1");
  });

  it("retains all 46 headers and every exact sample genotype from the public 100-sample VCF", () => {
    // Exact first-record projection of the public HTS-specs VCF at immutable
    // commit 510c107ba89aa13113818c9e4fa3071283cb7f7b:
    // test/vcf/4.3/passed/complexfile_passed_000.vcf.
    // Complete upstream SHA-256:
    // f0618cfb67afdd6fc8ee594217824f34cb40d26b277392876984aa0a5211eadf.
    const publicFixture = readFileSync(
      "src/sequence/__fixtures__/official-vcf-4.3-first-100-sample-variant.vcf",
      "utf8",
    );
    expect(createHash("sha256").update(publicFixture).digest("hex")).toBe(
      "2841a817ca64295730ef905725a735b4a424bdcfd3e231304eeee8f44e76997b",
    );

    const track = parseSequenceTrack({
      content: publicFixture,
      displayName: "public-official-100-sample-vcf.vcf",
      format: "vcf",
      id: "official-vcf",
      requestedReference: "1",
    });
    const variant = track.variants?.[0];

    expect(track.vcfHeader?.metaLines).toHaveLength(46);
    expect(track.vcfHeader?.columns).toHaveLength(109);
    expect(track.vcfHeader?.sampleNames).toHaveLength(100);
    expect(track.vcfHeader?.sampleNames[0]).toBe("HG00096");
    expect(track.vcfHeader?.sampleNames[99]).toBe("HG00261");
    expect(variant?.format).toBe("GT:DS:GL");
    expect(variant?.sampleValues).toHaveLength(100);
    expect(Object.keys(variant?.samples ?? {})).toHaveLength(100);
    expect(variant?.samples.HG00096).toBe("0|0:0.200:-0.18,-0.47,-2.42");
    expect(variant?.samples.HG00261).toBe("0|1:1.300:-2.60,-0.46,-0.19");
  });

  it("rejects malformed VCF sample declarations before lossy materialization", () => {
    expect(() =>
      parseVcf(
        "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\tS1\tS1\nchr1\t1\t.\tA\tG\t.\t.\t.\tGT\t0/0\t0/1\n",
      ),
    ).toThrow("sample identifiers must be unique");
    expect(() =>
      parseVcf(
        "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\tS1\nchr1\t1\t.\tA\tG\t.\t.\t.\tGT\n",
      ),
    ).toThrow("header declares");
  });

  it("retains genuine CDS phases instead of converting them to implicit zero", () => {
    expect(
      parseAnnotationTrack(
        'chr22\tNCBI\tCDS\t3698\t3978\t.\t+\t2\tgene_id "AC007323.5"; transcript_id "AT1G01010";\n',
        "gtf",
      )[0],
    ).toMatchObject({
      end: 3978,
      phase: 2,
      start: 3698,
      type: "CDS",
    });
    expect(() =>
      parseAnnotationTrack(
        'chr22\tNCBI\tCDS\t3698\t3978\t.\t+\t3\tgene_id "AC007323.5";\n',
        "gtf",
      ),
    ).toThrow("invalid CDS phase");
  });

  it("parses SAM CIGAR, quality, mate, strand, and typed tags", () => {
    const reads = parseSam(
      "@SQ\tSN:chr1\tLN:100\nread1\t16\tchr1\t5\t42\t3M1D2M\t=\t20\t18\tAACGT\tIIIII\tNM:i:1\tRG:Z:sample\n",
    );
    expect(reads[0]).toMatchObject({
      cigar: "3M1D2M",
      end: 10,
      mappingQuality: 42,
      matePosition: 20,
      mateReference: "chr1",
      position: 5,
      quality: [40, 40, 40, 40, 40],
      strand: "-",
      tags: { NM: 1, RG: "sample" },
    });
  });

  it("rejects malformed SAM fields before they enter session state", () => {
    expect(() =>
      parseSam("read1\t0\tchr1\t5\t42\t3Mbad\t*\t0\t0\tACG\tIII\n"),
    ).toThrow("invalid CIGAR");
    expect(() =>
      parseSam("read1\t0\tchr1\t5\t42\t3M\t*\t0\t0\tACG\tII\n"),
    ).toThrow("quality symbols");
    expect(() =>
      parseSam("read1\t0\tchr1\t5\t256\t3M\t*\t0\t0\tACG\tIII\n"),
    ).toThrow("exceeds 255");
    expect(() =>
      parseSam(
        "read1\t0\tchr1\t5\t42\t3M\t*\t0\t0\tACG\tIII\tNM:i:not-a-number\n",
      ),
    ).toThrow("invalid numeric value");
  });

  it("decodes BGZF-decompressed BAM alignment blocks", () => {
    const reads = parseUncompressedBam(buildMinimalBam());
    expect(reads).toEqual([
      expect.objectContaining({
        cigar: "4M",
        end: 13,
        id: "read1",
        mappingQuality: 60,
        position: 10,
        quality: [30, 31, 32, 33],
        reference: "chr1",
        sequence: "ACGT",
        strand: "+",
      }),
    ]);
  });

  it("reports reference mapping diagnostics instead of silently attaching mismatched tracks", () => {
    const track = parseSequenceTrack({
      content: "chr2\t0\t10\n",
      displayName: "regions.bed",
      format: "bed",
      id: "track-1",
      requestedReference: "chr1",
    });
    expect(track.mapping).toEqual({
      matchedReference: null,
      requestedReference: "chr1",
      status: "unmatched",
      unmatchedReferences: ["chr2"],
    });
  });

  it("omits absent optional fields from strictly serializable evidence tracks", () => {
    const track = parseSequenceTrack({
      content:
        "@SQ\tSN:chr1\tLN:100\nread1\t0\tchr1\t1\t60\t5M\t*\t0\t0\tACGTA\t*\n",
      displayName: "reads.sam",
      format: "sam",
      id: "serializable-track",
      requestedReference: "chr1",
    });

    expect(track.source).toEqual({ displayName: "reads.sam" });
    expect(track.reads?.[0]).not.toHaveProperty("matePosition");
    expect(track.reads?.[0]).not.toHaveProperty("mateReference");
    expect(track.reads?.[0]).not.toHaveProperty("quality");
    expect(JSON.parse(JSON.stringify(track))).toEqual(track);
  });

  it("uses region-only loading, all-read coverage, and disclosed deterministic downsampling", () => {
    const track = parseSequenceTrack({
      content: Array.from(
        { length: 20 },
        (_, index) =>
          `read${index}\t0\tchr1\t${index + 1}\t60\t5M\t*\t0\t0\tACGTA\tIIIII`,
      ).join("\n"),
      displayName: "reads.sam",
      format: "sam",
      id: "reads",
      requestedReference: "1",
    });
    const window = buildEvidenceWindow({
      end: 20,
      maxReads: 5,
      reference: "chr1",
      start: 1,
      tracks: [track],
    });
    expect(window).toMatchObject({
      downsampled: true,
      sampledReadCount: 5,
      totalReadCount: 20,
    });
    expect(window.coverage[4]?.depth).toBe(5);
  });

  it("preserves indexed-read source downsampling in track and evidence summaries", () => {
    const track = parseSequenceTrack({
      content:
        "@SQ\tSN:chr1\tLN:100\nread1\t0\tchr1\t1\t60\t5M\t*\t0\t0\tACGTA\tIIIII\n",
      displayName: "regional.bam",
      format: "bam",
      id: "indexed-reads",
      requestedReference: "chr1",
      sourceItemCount: 25_000,
      sourceTruncated: true,
    });
    expect(track.summary).toMatchObject({
      itemCount: 25_000,
      materializedItemCount: 1,
      truncated: true,
    });
    const evidenceWindow = buildEvidenceWindow({
      end: 20,
      reference: "chr1",
      start: 1,
      tracks: [track],
    });
    expect(evidenceWindow).toMatchObject({
      coverageComplete: false,
      coverageOmittedReadCount: 0,
      coverageReadCount: 1,
      downsampled: true,
      sampledReadCount: 1,
      sourceReadCount: 25_000,
      sourceTruncated: true,
      totalReadCount: 1,
    });
    expect(evidenceWindow.coverage[4]?.depth).toBe(1);
    expect(evidenceWindow.coverage[5]?.depth).toBe(0);
  });

  it("does not infer a reference is absent from a truncated retained read sample", () => {
    const track = parseSequenceTrack({
      content:
        "@SQ\tSN:chr1\tLN:4\n@SQ\tSN:chr2\tLN:4\nr1\t0\tchr1\t1\t60\t4M\t*\t0\t0\tACGT\tIIII",
      displayName: "sampled.sam",
      format: "sam",
      id: "sampled",
      requestedReference: "chr2",
      sourceItemCount: 200_001,
      sourceTruncated: true,
    });
    const window = buildEvidenceWindow({
      start: 1,
      end: 4,
      reference: "chr2",
      tracks: [track],
    });
    expect(window).toMatchObject({
      coverageComplete: false,
      sourceReadCount: 200_001,
      sourceTruncated: true,
      sourceReferenceUncertain: true,
      totalReadCount: 0,
    });
    expect(window.coverage.map((point) => point.depth)).toEqual([0, 0, 0, 0]);
  });

  it("prefers exact reference names and treats ambiguous aliases as unknown rather than zero coverage", () => {
    const track = parseSequenceTrack({
      content:
        "alias\t0\t1\t1\t60\t2M\t*\t0\t0\tAA\tII\nexact\t0\tchr1\t1\t60\t2M\t*\t0\t0\tCC\tII",
      displayName: "aliases.sam",
      format: "sam",
      id: "aliases",
      requestedReference: "chr1",
    });
    expect(track.mapping.matchedReference).toBe("chr1");
    const exact = buildEvidenceWindow({
      start: 1,
      end: 2,
      reference: "chr1",
      tracks: [track],
    });
    expect(exact.reads.map((read) => read.id)).toEqual(["exact"]);
    expect(exact.coverageComplete).toBe(true);
    const ambiguous = buildEvidenceWindow({
      start: 1,
      end: 2,
      reference: "CHR1",
      tracks: [track],
    });
    expect(ambiguous).toMatchObject({
      coverageComplete: false,
      sourceReferenceUncertain: true,
      reads: [],
    });
    const aliasOnly = {
      ...track,
      reads: track.reads?.slice(0, 1),
      summary: { ...track.summary, references: ["1"] },
    };
    const wrongDocumentAlias = buildEvidenceWindow({
      start: 1,
      end: 2,
      reference: "chr1",
      referenceRecords: [{ sourceLabel: "1" }, { sourceLabel: "chr1" }],
      tracks: [aliasOnly],
    });
    expect(wrongDocumentAlias).toMatchObject({
      coverageComplete: false,
      sourceReferenceUncertain: true,
      reads: [],
    });
  });

  it("counts only CIGAR aligned bases, and reports unavailable CIGAR without inventing coverage", () => {
    const track = parseSequenceTrack({
      content: [
        "spliced\t0\tchr1\t1\t60\t2M1I1M1D2M2N1M1S\t*\t0\t0\tATGTCGAA\tIIIIIIII",
        "unknown\t0\tchr1\t1\t60\t*\t*\t0\t0\tACGT\tIIII",
      ].join("\n"),
      displayName: "cigar.sam",
      format: "sam",
      id: "cigar",
      requestedReference: "chr1",
    });
    const window = buildEvidenceWindow({
      start: 1,
      end: 10,
      reference: "chr1",
      tracks: [track],
    });
    expect(window.coverage.map((point) => point.depth)).toEqual([
      1, 1, 1, 0, 1, 1, 0, 0, 1, 0,
    ]);
    expect(window).toMatchObject({
      coverageComplete: false,
      coverageReadCount: 1,
      coverageOmittedReadCount: 1,
      totalReadCount: 2,
    });
  });

  it("applies filters before exact loaded-read coverage and display sampling", () => {
    const track = parseSequenceTrack({
      content: Array.from(
        { length: 20 },
        (_, index) =>
          `r${index}\t0\tchr1\t1\t${index < 10 ? 60 : 255}\t4M\t*\t0\t0\tACGT\tIIII`,
      ).join("\n"),
      displayName: "filtered.sam",
      format: "sam",
      id: "filtered",
      requestedReference: "chr1",
    });
    const filters = {
      ...DEFAULT_READ_PILEUP_FILTERS,
      minimumMappingQuality: 30,
      includeUnknownMappingQuality: false,
    };
    const window = buildEvidenceWindow({
      start: 1,
      end: 5,
      maxReads: 2,
      reference: "chr1",
      tracks: [track],
      readFilter: (read) => readPassesPileupFilters(read, filters),
    });
    expect(window).toMatchObject({
      coverageComplete: true,
      coverageReadCount: 10,
      filteredReadCount: 10,
      totalReadCount: 20,
      sampledReadCount: 2,
      downsampled: true,
    });
    expect(window.coverage.map((point) => point.depth)).toEqual([
      10, 10, 10, 10, 0,
    ]);
  });

  it("rejects malformed coordinate and oversized-window requests", () => {
    expect(() => parseAnnotationTrack("chr1\t10\t5\n", "bed")).toThrow(
      "empty or reversed",
    );
    expect(() =>
      buildEvidenceWindow({
        end: 100_001,
        reference: "chr1",
        start: 1,
        tracks: [],
      }),
    ).toThrow("100,000 bases");
  });

  it("reports partial coverage when cumulative CIGAR work exceeds the processing budget", () => {
    const track = parseSequenceTrack({
      content: `read\t0\tchr1\t1\t60\t${"1M".repeat(1_000)}\t*\t0\t0\t${"A".repeat(1_000)}\t*`,
      displayName: "complex.sam",
      format: "sam",
      id: "complex",
      requestedReference: "chr1",
    });
    const read = track.reads?.[0];
    if (read == null) throw new Error("Expected a read fixture.");
    track.reads = Array.from({ length: 1_001 }, (_, index) => ({
      ...read,
      id: `read${index}`,
    }));
    track.summary.itemCount = 1_001;
    const window = buildEvidenceWindow({
      start: 1,
      end: 2,
      reference: "chr1",
      tracks: [track],
    });
    expect(window).toMatchObject({
      coverageBudgetExceeded: true,
      coverageComplete: false,
      coverageOmittedReadCount: 1,
      coverageReadCount: 1_000,
      totalReadCount: 1_001,
    });
    expect(window.coverage).toEqual([
      { coordinate: 1, depth: 1_000 },
      { coordinate: 2, depth: 1_000 },
    ]);
  });
});

function buildMinimalBam(): Uint8Array {
  const headerText = "@HD\tVN:1.6\n@SQ\tSN:chr1\tLN:100\n";
  const referenceName = new TextEncoder().encode("chr1\0");
  const readName = new TextEncoder().encode("read1\0");
  const blockSize = 32 + readName.length + 4 + 2 + 4;
  const bytes = new Uint8Array(
    4 +
      4 +
      headerText.length +
      4 +
      4 +
      referenceName.length +
      4 +
      4 +
      blockSize,
  );
  const view = new DataView(bytes.buffer);
  let offset = 0;
  bytes.set([0x42, 0x41, 0x4d, 0x01], offset);
  offset += 4;
  view.setInt32(offset, headerText.length, true);
  offset += 4;
  bytes.set(new TextEncoder().encode(headerText), offset);
  offset += headerText.length;
  view.setInt32(offset, 1, true);
  offset += 4;
  view.setInt32(offset, referenceName.length, true);
  offset += 4;
  bytes.set(referenceName, offset);
  offset += referenceName.length;
  view.setInt32(offset, 100, true);
  offset += 4;
  view.setInt32(offset, blockSize, true);
  offset += 4;
  view.setInt32(offset, 0, true);
  view.setInt32(offset + 4, 9, true);
  view.setUint32(offset + 8, (60 << 8) | readName.length, true);
  view.setUint32(offset + 12, 1, true);
  view.setInt32(offset + 16, 4, true);
  view.setInt32(offset + 20, -1, true);
  view.setInt32(offset + 24, -1, true);
  view.setInt32(offset + 28, 0, true);
  offset += 32;
  bytes.set(readName, offset);
  offset += readName.length;
  view.setUint32(offset, (4 << 4) | 0, true);
  offset += 4;
  bytes.set([0x12, 0x48], offset);
  offset += 2;
  bytes.set([30, 31, 32, 33], offset);
  return bytes;
}

SHA-256: 195efe73fa21f95ef6f6f417fa924bfb48e4a5640e103e50e8ddbc1107281a70