Ctrl K
ComunidadGuías…

tests/pocketParity.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// POCKET PARITY — the phone and the server give the same answers about one vault.
//
// mobile/src/pocket/index.ts is a second index: it answers /api/* inside the
// phone's WebView for a vault cloned from GitHub. Every question it answers,
// the server answers too, and the reader sees the same vault on both — so
// where the two disagree, one of them is lying about the vault. The audit
// behind this file found five such disagreements (the banner read `cover:`
// and `image:` on the phone, aliases were neither deduplicated nor refused a
// date, a `...`-closed frontmatter was a block on one side only, the tree
// sorted by a different collation, a `.tex` note's aliases and tags were
// invisible to the phone). They were fixed by moving each rule into shared/;
// this file holds the two indexes to one fixture vault, question by question.
//
// The fixture is built to hit the seams: every alias spelling (flow, block,
// scalar-with-commas, the singular key, a duplicate, a date), a banner beside
// a tracker's `cover:`, a `...` closer, an Arabic note written pointed and
// linked plain, a `.tex` note with a comment-block frontmatter, two notes
// sharing a basename, and links that arrive by path, by name, by alias and
// with an anchor.

import assert from "node:assert/strict";
import { statSync } from "node:fs";
import path from "node:path";
import { after, before, describe, it } from "node:test";
import {
  aliasEntries,
  backlinks as serverBacklinks,
  initIndexer,
  publishedBanner,
  resolveLink as serverResolve,
  search as serverSearch,
  tags as serverTags,
} from "../server/indexer.ts";
import { initSite } from "../server/site.ts";
import { buildTree, initVault } from "../server/vault.ts";
import { PocketIndex } from "../mobile/src/pocket/index.ts";
import type { TreeNode } from "../shared/types.ts";
import { makeDir, makeVault, removeVault } from "./helpers/vault.ts";

const FILES: Record<string, string> = {
  "Welcome.md": [
    "---",
    "tags: [start, guide]",
    "publish: true",
    "banner: https://example.com/welcome.png",
    "---",
    "# Welcome",
    "",
    "Start at [[Ideas/Astrolabe]], then [[ML]], then [[Deep Learning#Layers]].",
    "A path link: [[Notes/Machine Learning]] and a plain one [[القراءة]].",
    "#inline-tag in the body.",
  ].join("\n"),
  "Ideas/Astrolabe.md": [
    "---",
    "aliases:",
    "  - The Instrument",
    "  - the instrument",
    "tags:",
    "  - guide",
    "---",
    "## On the instrument",
    "",
    "An astrolabe measures the sky. Back to [[Welcome]].",
  ].join("\n"),
  "Notes/Machine Learning.md": "---\naliases: [ML, machine-learning]\npublish: true\ncover: covers/ml.png\n---\n# Machine Learning\n\nSee [[The Instrument]].\n",
  "Notes/Deep Learning.md": "---\naliases: DL, deep learning\nimage: https://example.com/not-a-banner.png\npublish: true\n---\n# Deep Learning\n\n## Layers\n\nStacked. Links [[ML]].\n",
  "Notes/Singular.md": "---\nalias: Older\n---\n# Singular\n\n[[Welcome]] again.\n",
  "Notes/Dated.md": "---\naliases: 2024-01-01\n---\n# Dated\n",
  // Closed with YAML's own end-of-document marker.
  "Notes/Dots.md": "---\ntags: [dots]\naliases: [Ellipsis]\n...\n# Dots\n\nThe block above closes with three dots. [[Welcome]]\n",
  // Pointed on disk, typed plain in the link above.
  "القراءة.md": "# القراءة\n\nالكِتَابَةُ بالعربيّة. رابط إلى [[Welcome]].\n",
  // Two notes with one basename: the shorter path wins, on both.
  "Index.md": "# Index\n\n[[Welcome]]\n",
  "deep/down/Index.md": "# Index, deeper\n",
  // A LaTeX note: its frontmatter is a comment block, and a `#` is not a tag.
  "papers/Fourier.tex": [
    "%---",
    "% aliases: [Heat, heat equation]",
    "% tags: [maths]",
    "%---%",
    "\\documentclass{article}",
    "\\begin{document}",
    "On heat, \\#1 in the series. % [[Welcome]]",
    "\\end{document}",
  ].join("\n"),
  "covers/ml.png": "\u0089PNG not really",
  "Media/diagram.PNG": "\u0089PNG not really",
  "Media/alpha.pdf": "%PDF-1.4",
  "Media/Zeta.pdf": "%PDF-1.4",
};

const data = makeDir();
const root = makeVault(FILES);
const pocket = new PocketIndex();

before(async () => {
  initSite({ ASTROLABE_DATA: data });
  initVault(root);
  await initIndexer();
  for (const [rel, content] of Object.entries(FILES)) {
    const { mtimeMs, size } = statSync(path.join(root, rel));
    if (/\.(md|tex)$/.test(rel)) pocket.put(rel, content, mtimeMs);
    else pocket.putAsset(rel, size, mtimeMs);
  }
});

after(() => {
  removeVault(root);
  removeVault(data);
});

const NOTES = Object.keys(FILES).filter((p) => /\.(md|tex)$/.test(p));

describe("pocket parity: resolve", () => {
  const NAMES = [
    "Welcome", "welcome", "Welcome.md", "Ideas/Astrolabe", "./Ideas/Astrolabe", "Astrolabe",
    "The Instrument", "ML", "machine-learning", "DL", "deep learning", "Older", "Ellipsis",
    "2024-01-01", "القراءة", "Index", "deep/down/Index", "Fourier", "Heat", "heat equation",
    "Deep Learning#Layers", "ML|shown", "Missing note",
  ];
  for (const name of NAMES) {
    it(`[[${name}]]`, () => {
      assert.equal(pocket.resolve(name), serverResolve(name, false, null));
    });
  }
});

describe("pocket parity: aliases", () => {
  it("the alias table is the same, entry for entry", () => {
    const key = (e: { alias: string; path: string }) => `${e.path} ${e.alias}`;
    const server = aliasEntries(false, null).map(key).sort();
    const phone = pocket.aliasEntries().map(key).sort();
    assert.deepEqual(phone, server);
    // …and the rules it encodes: a duplicate collapses, a date is not a name.
    assert.ok(!server.some((k) => k.endsWith(" the instrument")));
    assert.ok(!server.some((k) => k.startsWith("Notes/Dated.md")));
    assert.ok(server.includes("papers/Fourier.tex Heat"));
  });
});

describe("pocket parity: tags", () => {
  it("the tag counts are the same", () => {
    assert.deepEqual(pocket.tags(), serverTags(false, null));
    assert.ok(!pocket.tags().some((t) => t.tag === "1"), "a LaTeX `\\#1` is not a tag");
    assert.ok(pocket.tags().some((t) => t.tag === "dots"), "a `...`-closed block is read");
  });
});

describe("pocket parity: banner", () => {
  for (const rel of ["Welcome.md", "Notes/Machine Learning.md", "Notes/Deep Learning.md"]) {
    it(rel, () => {
      assert.equal(pocket.notes.get(rel)?.banner ?? null, publishedBanner(rel));
    });
  }
});

describe("pocket parity: backlinks", () => {
  for (const target of NOTES) {
    it(target, () => {
      const shape = (b: { path: string; line: number }) => `${b.path}:${b.line}`;
      assert.deepEqual(
        pocket.backlinks(target).map(shape).sort(),
        serverBacklinks(target, false, null).map(shape).sort(),
      );
    });
  }
  it("the fixture exercises them (Welcome has five)", () => {
    assert.equal(serverBacklinks("Welcome.md", false, null).length, 5);
  });
});

describe("pocket parity: search", () => {
  const QUERIES = ["astrolabe", "welcome", "machine learning", "ML", "Heat", "index", "Layers", "الكتابة", "الكِتَابَةُ", "tag:guide", "path:notes deep", "-tag:guide sky", "sky"];
  for (const q of QUERIES) {
    it(JSON.stringify(q), () => {
      const server = serverSearch(q, false, null).map((h) => h.path);
      const phone = pocket.search(q).map((h) => h.path);
      assert.deepEqual(new Set(phone), new Set(server), "the same notes");
      assert.equal(phone[0], server[0], "the same first hit");
    });
  }
});

describe("pocket parity: the tree's order", () => {
  it("folders, notes, attachments; alphabetised the same way", async () => {
    const flat = (node: TreeNode): string[] => [node.path, ...(node.children ?? []).flatMap(flat)];
    const server = flat(await buildTree()).slice(1);
    const phone = flat(pocket.tree()).slice(1);
    assert.deepEqual(phone, server);
  });
});