Ctrl K
ComunidadGuías…

tests/speakRoutes.test.ts

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

Archivo original ↗Preguntar a KiBot
// Read aloud's routes (server/speak.ts) through the Hono app, over a
// throwaway vault, with a FAKE engine standing in for the Python child: the
// language is detected and reported, the engine is chosen per language, a
// second ask is a cache hit, nothing installed is a 409 that names what to
// install, and a visitor is heard only with "Readers may listen" on, only on
// a published note, and only for words that are on that page.

import assert from "node:assert/strict";
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, utimesSync, writeFileSync } from "node:fs";
import path from "node:path";
import { after, before, describe, it } from "node:test";
import { Hono } from "hono";
import { initAuth } from "../server/auth.ts";
import { initIndexer } from "../server/indexer.ts";
import { initSite } from "../server/site.ts";
import { initVault, type VaultError } from "../server/vault.ts";
import { api } from "../server/api.ts";
import { getSettings, patchSettings } from "../server/settings.ts";
import { fetchVoice, setFakeEngine, toneWav, type SpeakJob } from "../server/speakEngine.ts";
import { foldForMatch } from "../server/speak.ts";
import { makeDir, makeVault, note, removeVault } from "./helpers/vault.ts";
import type { SpeakStatus } from "../shared/speech.ts";

const VAULT: Record<string, string> = {
  "Carnet.md": note({ lang: "fr" }, "# Carnet\n\nLa grenouille saute dans le jardin.\n"),
  "Public.md": note({ publish: "true" }, "# Public\n\nThe **quick** brown fox reads the [[Paper|evening paper]].\n"),
  "Private.md": note({ publish: "false" }, "# Private\n\nA secret sentence.\n"),
};

const data = makeDir();
const root = makeVault(VAULT);
const app = new Hono();
app.route("/api", api);
const asked: SpeakJob[] = [];
const speak = (body: unknown) =>
  app.request("/api/speak", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });

before(async () => {
  initAuth({}); // local mode: the owner at their own machine
  initSite({ ASTROLABE_DATA: data });
  initVault(root);
  await initIndexer();
  setFakeEngine(async (job) => {
    asked.push(job);
    return { audio: toneWav(job.text), mime: "audio/wav", ms: 1 };
  }, ["light", "natural"]);
});

after(() => {
  setFakeEngine(null);
  initAuth({});
  removeVault(root);
  removeVault(data);
});

describe("POST /api/speak (the owner)", () => {
  it("speaks, reports the language and the engine, and closes a bare word", async () => {
    asked.length = 0;
    const res = await speak({ text: "図書館" });
    assert.equal(res.status, 200);
    assert.equal(res.headers.get("content-type"), "audio/wav");
    assert.equal(res.headers.get("x-speak-lang"), "ja");
    assert.equal(res.headers.get("x-speak-engine"), "natural");
    assert.equal(res.headers.get("x-speak-cache"), "miss");
    const bytes = new Uint8Array(await res.arrayBuffer());
    assert.equal(String.fromCharCode(...bytes.subarray(0, 4)), "RIFF");
    assert.equal(asked[0].text, "図書館。");
    assert.equal(asked[0].voice, "jf_alpha");
  });

  it("the same words again are a cache hit, and the engine is not asked", async () => {
    asked.length = 0;
    const res = await speak({ text: "図書館" });
    assert.equal(res.headers.get("x-speak-cache"), "hit");
    assert.equal(asked.length, 0);
  });

  it("Arabic goes to Piper whatever the choice; French to the choice", async () => {
    patchSettings({ speak: { engine: "natural" } });
    try {
      const ar = await speak({ text: "المكتبة العامة" });
      assert.equal(ar.headers.get("x-speak-engine"), "light");
      const fr = await speak({ text: "Les enfants ont appris leurs leçons en une heure." });
      assert.equal(fr.headers.get("x-speak-lang"), "fr");
      assert.equal(fr.headers.get("x-speak-engine"), "natural");
    } finally {
      patchSettings({ speak: null });
    }
  });

  it("a lone word takes the note's own lang, then the sentence around it", async () => {
    const byNote = await speak({ text: "jardin", path: "Carnet.md" });
    assert.equal(byNote.headers.get("x-speak-lang"), "fr");
    const byContext = await speak({ text: "grenouille", context: "La grenouille saute dans le jardin de la maison." });
    assert.equal(byContext.headers.get("x-speak-lang"), "fr");
    const bare = await speak({ text: "window" });
    assert.equal(bare.headers.get("x-speak-lang"), "en");
  });

  it("an explicit lang wins, and the owner's voice and rate reach the engine", async () => {
    patchSettings({ speak: { engine: "natural", rate: 0.8, voices: { en: "am_michael" } } });
    try {
      asked.length = 0;
      const res = await speak({ text: "Bonjour", lang: "en" });
      assert.equal(res.headers.get("x-speak-lang"), "en");
      assert.equal(asked[0].voice, "am_michael");
      assert.equal(asked[0].speed, 0.8);
    } finally {
      patchSettings({ speak: null });
    }
  });

  it("refuses empty and oversized text", async () => {
    assert.equal((await speak({ text: "  " })).status, 400);
    assert.equal((await speak({ text: "a".repeat(1001) })).status, 413);
  });

  it("nothing installed that speaks the language is a 409 naming what to install", async () => {
    setFakeEngine(async (job) => ({ audio: toneWav(job.text), mime: "audio/wav", ms: 1 }), ["light"]);
    try {
      const res = await speak({ text: "ありがとう" });
      assert.equal(res.status, 409);
      const body = (await res.json()) as { code: string; lang: string; needs: string };
      assert.deepEqual([body.code, body.lang, body.needs], ["speakNotInstalled", "ja", "natural"]);
    } finally {
      setFakeEngine(async (job) => {
        asked.push(job);
        return { audio: toneWav(job.text), mime: "audio/wav", ms: 1 };
      }, ["light", "natural"]);
    }
  });

  it("an engine failure is a 502 with its code, and the next request is served", async () => {
    setFakeEngine(async () => {
      throw Object.assign(new Error("boom"), { code: "engine" });
    }, ["light", "natural"]);
    const res = await speak({ text: "A fresh failing sentence." });
    assert.equal(res.status, 502);
    assert.equal(((await res.json()) as { code: string }).code, "engine");
    setFakeEngine(async (job) => ({ audio: toneWav(job.text), mime: "audio/wav", ms: 1 }), ["light", "natural"]);
    assert.equal((await speak({ text: "A fresh failing sentence." })).status, 200);
  });
});

describe("a voice that downloads on choice (Light's Pierre), and a voice of the other engine", () => {
  const pierre = "fr_FR-upmc-medium#pierre";
  const post = (url: string, body: unknown) =>
    app.request(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
  const status = async (): Promise<SpeakStatus> => (await (await app.request("/api/speak/status")).json()) as SpeakStatus;

  before(() => {
    setFakeEngine(async (job) => {
      asked.push(job);
      return { audio: toneWav(job.text), mime: "audio/wav", ms: 1 };
    }, ["light", "natural"]);
  });

  it("is listed as not here yet, with its size, and Install did not fetch it", async () => {
    const s = await status();
    assert.deepEqual(Object.keys(s.voices).sort(), ["fr_FR-upmc-medium#jessica", pierre]);
    assert.equal(s.voices[pierre].ready, false);
    assert.equal(s.voices[pierre].bytes, 76_733_615);
    assert.equal(s.fetch.phase, "idle");
  });

  it("chosen but not downloaded: the language's first voice speaks, and the download starts", async () => {
    patchSettings({ speak: { engine: "light", voices: { fr: pierre } } });
    try {
      asked.length = 0;
      const res = await speak({ text: "Bonjour, je suis Pierre et je lis le français." });
      assert.equal(res.headers.get("x-speak-voice"), "fr_FR-siwis-medium");
      assert.equal(asked[0].voice, "fr_FR-siwis-medium");
      // The route asked for the saved choice's model; join that download.
      assert.equal((await status()).fetch.phase, "downloading");
      await fetchVoice(pierre);
      const s = await status();
      assert.equal(s.voices[pierre].ready, true);
      assert.equal(s.fetch.voice, pierre);
      assert.equal(s.fetch.phase, "done");
      // Now Pierre speaks: the model file, and the male speaker's number.
      asked.length = 0;
      const again = await speak({ text: "Bonjour, je suis Pierre et je lis encore." });
      assert.equal(decodeURIComponent(again.headers.get("x-speak-voice") ?? ""), pierre);
      assert.equal(asked[0].voice, "fr_FR-upmc-medium");
      assert.equal(asked[0].speaker, 1);
    } finally {
      patchSettings({ speak: null });
    }
  });

  it("POST /api/speak/voice downloads the voice the Settings picker just chose", async () => {
    setFakeEngine(async (job) => {
      asked.push(job);
      return { audio: toneWav(job.text), mime: "audio/wav", ms: 1 };
    }, ["light", "natural"]); // a fresh fake: nothing downloaded
    assert.equal((await status()).voices["fr_FR-upmc-medium#jessica"].ready, false);
    const res = await post("/api/speak/voice", { voice: "fr_FR-upmc-medium#jessica" });
    assert.equal(res.status, 202);
    const during = await status();
    assert.deepEqual([during.fetch.voice, during.fetch.phase], ["fr_FR-upmc-medium#jessica", "downloading"]);
    await fetchVoice("fr_FR-upmc-medium#jessica");
    assert.equal((await status()).voices["fr_FR-upmc-medium#jessica"].ready, true);
    // Refused: a voice that comes with Install, a Kokoro voice, nonsense.
    assert.equal((await post("/api/speak/voice", { voice: "fr_FR-siwis-medium" })).status, 400);
    assert.equal((await post("/api/speak/voice", { voice: "am_michael" })).status, 400);
    assert.equal((await post("/api/speak/voice", { voice: "../../etc" })).status, 400);
  });

  it("with Light not installed, asking for a Light voice is a 409 naming Light", async () => {
    setFakeEngine(async (job) => ({ audio: toneWav(job.text), mime: "audio/wav", ms: 1 }), ["natural"]);
    try {
      const res = await post("/api/speak/voice", { voice: pierre });
      assert.equal(res.status, 409);
      assert.equal(((await res.json()) as { needs: string }).needs, "light");
    } finally {
      setFakeEngine(async (job) => {
        asked.push(job);
        return { audio: toneWav(job.text), mime: "audio/wav", ms: 1 };
      }, ["light", "natural"]);
    }
  });

  it("the settings take a Light voice for French under Natural, and Light speaks it", async () => {
    await post("/api/speak/voice", { voice: pierre });
    await fetchVoice(pierre);
    patchSettings({ speak: { engine: "natural", voices: { fr: pierre } } });
    try {
      assert.equal(getSettings().speak?.voices?.fr, pierre);
      asked.length = 0;
      const res = await speak({ text: "Je finis mon travail à dix-huit heures." });
      assert.equal(res.headers.get("x-speak-lang"), "fr");
      assert.equal(res.headers.get("x-speak-engine"), "light");
      assert.deepEqual([asked[0].engine, asked[0].voice, asked[0].speaker], ["light", "fr_FR-upmc-medium", 1]);
      // Japanese under the same settings is still Natural's.
      assert.equal((await speak({ text: "図書館へ行きます" })).headers.get("x-speak-engine"), "natural");
    } finally {
      patchSettings({ speak: null });
    }
  });

  it("a French lesson's heading, sent with its passage's language, is French", async () => {
    const plain = await speak({ text: "Décrire son quotidien." });
    assert.equal(plain.headers.get("x-speak-lang"), "en", "alone it has no French word — the bug");
    const withPassage = await speak({ text: "Décrire son quotidien.", passage: "fr" });
    assert.equal(withPassage.headers.get("x-speak-lang"), "fr");
  });
});

describe("GET /api/speak/status", () => {
  it("tells the owner what is installed and where the venv lives", async () => {
    const res = await app.request("/api/speak/status");
    const body = (await res.json()) as { engines: { light: { installed: boolean } }; venv: string; settings: { engine: string } };
    assert.equal(body.engines.light.installed, true);
    assert.match(body.venv, /tts[\\/]venv$/);
    assert.equal(body.settings.engine, "light");
  });
});

// ── Your own voices and the external speaker ───────────────────────────────

const voicesDir = makeDir();
const piperJson = (code: string, extra: Record<string, unknown> = {}) =>
  JSON.stringify({ audio: { sample_rate: 22050 }, phoneme_id_map: { _: [0] }, language: { code }, num_speakers: 1, speaker_id_map: {}, ...extra });
const upmc = path.join(voicesDir, "fr", "fr_FR", "upmc", "medium", "fr_FR-upmc-medium.onnx");
const davefx = path.join(voicesDir, "es_ES-davefx-medium.onnx");
const fakeSpeaker = path.join(voicesDir, "..", `speaker-${path.basename(voicesDir)}.sh`);

describe("your own voices (the voices folder)", () => {
  before(() => {
    mkdirSync(path.dirname(upmc), { recursive: true });
    writeFileSync(upmc, "onnx");
    writeFileSync(`${upmc}.json`, piperJson("fr_FR", { num_speakers: 2, speaker_id_map: { jessica: 0, pierre: 1 } }));
    writeFileSync(davefx, "onnx");
    writeFileSync(`${davefx}.json`, piperJson("es_ES"));
    writeFileSync(path.join(voicesDir, "en_US-ryan-high.onnx"), "onnx"); // no config: skipped
    setFakeEngine(async (job) => {
      asked.push(job);
      return { audio: toneWav(job.text), mime: "audio/wav", ms: 1 };
    }, ["light", "natural"]);
  });
  after(() => {
    patchSettings({ speak: null });
    removeVault(voicesDir);
    rmSync(fakeSpeaker, { force: true });
  });

  it("refuses a folder that is relative, inside the vault, or not there — with a code the panel can say", () => {
    const refused = (dir: string): string => {
      try {
        patchSettings({ speak: { voicesDir: dir } });
      } catch (err) {
        return (err as VaultError).code ?? "";
      }
      return "accepted";
    };
    assert.equal(refused("piper-voices"), "speakDirRelative");
    assert.equal(refused(root), "speakDirInVault");
    assert.equal(refused(path.join(root, "voices")), "speakDirInVault");
    assert.equal(refused(path.join(voicesDir, "nowhere")), "speakDirMissing");
    assert.equal(refused(davefx), "speakDirNotDir");
    assert.equal(getSettings().speak, undefined, "a refused patch wrote nothing");
  });

  it("keeps the folder on this machine: in speak-local.json, never in settings.json", () => {
    const saved = patchSettings({ speak: { voicesDir } });
    assert.equal(saved.effective.speak.voicesDir, voicesDir);
    assert.doesNotMatch(readFileSync(path.join(data, "settings.json"), "utf8").toString(), /voicesDir/);
    assert.equal(JSON.parse(readFileSync(path.join(data, "speak-local.json"), "utf8")).voicesDir, voicesDir);
  });

  it("lists what the scan found per language, and what it skipped", async () => {
    const res = await app.request("/api/speak/voices/rescan", { method: "POST" });
    assert.equal(res.status, 200);
    const status = (await res.json()) as SpeakStatus;
    assert.equal(status.own.dir, voicesDir);
    assert.deepEqual(
      status.own.voices.map((v) => [v.lang, v.name]),
      [
        ["es", "Davefx"],
        ["fr", "Upmc · Jessica"],
        ["fr", "Upmc · Pierre"],
      ],
    );
    assert.deepEqual(status.own.skipped, [{ file: "en_US-ryan-high.onnx", reason: "noJson" }]);
    assert.equal(JSON.stringify(status).includes(upmc), false, "the status names voices, not the paths the worker loads");
  });

  it("a found voice picked for a language speaks it, loaded by path, with its speaker", async () => {
    patchSettings({ speak: { voices: { fr: "own:fr/fr_FR/upmc/medium/fr_FR-upmc-medium.onnx#pierre" } } });
    asked.length = 0;
    const res = await speak({ text: "Le petit prince regarde les étoiles.", lang: "fr" });
    assert.equal(res.status, 200);
    assert.equal(res.headers.get("x-speak-engine"), "light");
    assert.equal(decodeURIComponent(res.headers.get("x-speak-voice") ?? ""), "own:fr/fr_FR/upmc/medium/fr_FR-upmc-medium.onnx#pierre");
    assert.equal(asked[0].model, upmc);
    assert.equal(asked[0].config, `${upmc}.json`);
    assert.equal(asked[0].speaker, 1);
    // The same sentence again is a hit; the file replaced is spoken afresh.
    assert.equal((await speak({ text: "Le petit prince regarde les étoiles.", lang: "fr" })).headers.get("x-speak-cache"), "hit");
    const later = new Date(Date.now() + 60_000);
    utimesSync(upmc, later, later);
    assert.equal((await speak({ text: "Le petit prince regarde les étoiles.", lang: "fr" })).headers.get("x-speak-cache"), "miss");
  });

  it("a language only a found voice speaks is spoken on a Light-only machine", async () => {
    setFakeEngine(async (job) => {
      asked.push(job);
      return { audio: toneWav(job.text), mime: "audio/wav", ms: 1 };
    }, ["light"]);
    try {
      asked.length = 0;
      const res = await speak({ text: "¿Dónde está la biblioteca?", lang: "es" });
      assert.equal(res.status, 200);
      assert.equal(asked[0].model, davefx);
      assert.equal(asked[0].engine, "light");
    } finally {
      setFakeEngine(async (job) => {
        asked.push(job);
        return { audio: toneWav(job.text), mime: "audio/wav", ms: 1 };
      }, ["light", "natural"]);
    }
  });

  it("with no engine installed, the 409 names Light — the runtime the found voices need", async () => {
    setFakeEngine(async (job) => ({ audio: toneWav(job.text), mime: "audio/wav", ms: 1 }), []);
    try {
      const res = await speak({ text: "¿Qué hora es?", lang: "es" });
      assert.equal(res.status, 409);
      assert.equal(((await res.json()) as { needs: string }).needs, "light");
    } finally {
      setFakeEngine(async (job) => {
        asked.push(job);
        return { audio: toneWav(job.text), mime: "audio/wav", ms: 1 };
      }, ["light", "natural"]);
    }
  });

  it("while the operator has not allowed it (no SPEAK_EXTERNAL=on), an external speaker cannot be saved", async () => {
    delete process.env.SPEAK_EXTERNAL;
    let code = "";
    try {
      patchSettings({ speak: { external: { command: `"${fakeSpeaker}" {lang} {out}`, langs: ["fr"] } } });
    } catch (err) {
      code = (err as VaultError).code ?? "";
    }
    assert.equal(code, "speakExternalOff");
    const status = (await (await app.request("/api/speak/status")).json()) as SpeakStatus;
    assert.equal(status.externalAllowed, false);
    assert.equal(status.settings.external, null, "nothing was saved");
    // Clearing is always allowed.
    patchSettings({ speak: { external: null } });
  });

  it("the external speaker speaks the languages it was given, and says when it failed", async () => {
    process.env.SPEAK_EXTERNAL = "on";
    writeFileSync(fakeSpeaker, `#!/bin/sh\ncat > /dev/null\n[ "$1" = "fr" ] || exit 7\nprintf 'OggS-fake' > "$2"\n`);
    chmodSync(fakeSpeaker, 0o755);
    patchSettings({ speak: { external: { command: `"${fakeSpeaker}" {lang} {out}`, langs: ["fr", "ar"] } } });
    assert.equal(((await (await app.request("/api/speak/status")).json()) as SpeakStatus).externalAllowed, true);
    assert.doesNotMatch(readFileSync(path.join(data, "settings.json"), "utf8"), /external/);
    asked.length = 0;
    const fr = await speak({ text: "Une phrase pour le programme.", lang: "fr" });
    assert.equal(fr.status, 200);
    assert.equal(fr.headers.get("x-speak-engine"), "external");
    assert.equal(fr.headers.get("content-type"), "audio/ogg");
    assert.equal(asked.length, 0, "the app's engine was not asked");
    // English was not given to it.
    assert.equal((await speak({ text: "A sentence for the app.", lang: "en" })).headers.get("x-speak-engine"), "light");
    // Arabic was, and the program refuses it: a 502 the player names.
    const ar = await speak({ text: "جملة للبرنامج", lang: "ar" });
    assert.equal(ar.status, 502);
    assert.equal(((await ar.json()) as { code: string }).code, "speakExternal");
  });

  it("a command saved while allowed is never run once the operator turns the switch off", async () => {
    const ran = path.join(data, "ran-while-off.txt");
    writeFileSync(fakeSpeaker, `#!/bin/sh\ntouch "${ran}"\nprintf 'OggS-fake' > "$2"\n`);
    chmodSync(fakeSpeaker, 0o755);
    process.env.SPEAK_EXTERNAL = "on";
    patchSettings({ speak: { external: { command: `"${fakeSpeaker}" {lang} {out}`, langs: ["fr"] } } });
    process.env.SPEAK_EXTERNAL = "off";
    try {
      const fr = await speak({ text: "Une phrase que le programme ne lira pas.", lang: "fr" });
      assert.equal(fr.status, 200);
      assert.equal(fr.headers.get("x-speak-engine"), "light", "the app's own voice answered");
      assert.equal(existsSync(ran), false, "the program never ran");
    } finally {
      process.env.SPEAK_EXTERNAL = "on";
    }
  });


  it("refuses a command with no {out}, or with quotes that do not close", () => {
    assert.throws(() => patchSettings({ speak: { external: { command: "piper --model x.onnx", langs: ["fr"] } } }), /\{out\}/);
    assert.throws(() => patchSettings({ speak: { external: { command: 'piper "{out}', langs: ["fr"] } } }), /quotes/);
  });
});

describe("POST /api/speak (a visitor)", () => {
  before(() => {
    initAuth({
      ADMIN_PASSWORD_HASH: "$argon2id$v=19$m=65536,t=3,p=4$c2FsdHNhbHQ$aGFzaGhhc2hoYXNoaGFzaA",
      SESSION_SECRET: "relsecret0123456789abcdef0123456789",
    });
  });
  after(() => initAuth({}));

  it("is a 404 until the owner turns on Readers may listen", async () => {
    assert.equal((await speak({ text: "quick brown fox", path: "Public.md" })).status, 404);
    const status = (await (await app.request("/api/speak/status")).json()) as Record<string, unknown>;
    assert.deepEqual(status, { public: false });
  });

  it("with it on: the words of a published page, and nothing else", async () => {
    initAuth({});
    patchSettings({ speak: { public: true } });
    initAuth({
      ADMIN_PASSWORD_HASH: "$argon2id$v=19$m=65536,t=3,p=4$c2FsdHNhbHQ$aGFzaGhhc2hoYXNoaGFzaA",
      SESSION_SECRET: "relsecret0123456789abcdef0123456789",
    });
    try {
      const status = (await (await app.request("/api/speak/status")).json()) as Record<string, unknown>;
      assert.deepEqual(status, { public: true });
      // The rendered page's words: the link's label, no markup.
      assert.equal((await speak({ text: "The quick brown fox reads the evening paper.", path: "Public.md" })).status, 200);
      assert.equal((await speak({ text: "Say something the page never said.", path: "Public.md" })).status, 403);
      assert.equal((await speak({ text: "A secret sentence.", path: "Private.md" })).status, 404);
      assert.equal((await speak({ text: "quick brown fox" })).status, 404);
    } finally {
      initAuth({});
      patchSettings({ speak: null });
    }
  });

  it("never runs the owner's external speaker: a visitor hears the app's voices", async () => {
    initAuth({});
    process.env.SPEAK_EXTERNAL = "on"; // allowed, so what refuses it here is the visitor rule
    const program = path.join(data, "never-run.sh");
    const ran = path.join(data, "never-run.txt");
    writeFileSync(program, `#!/bin/sh\ntouch "${ran}"\nprintf 'OggS' > "$1"\n`);
    chmodSync(program, 0o755);
    patchSettings({ speak: { public: true, external: { command: `"${program}" {out}`, langs: ["en"] } } });
    initAuth({
      ADMIN_PASSWORD_HASH: "$argon2id$v=19$m=65536,t=3,p=4$c2FsdHNhbHQ$aGFzaGhhc2hoYXNoaGFzaA",
      SESSION_SECRET: "relsecret0123456789abcdef0123456789",
    });
    try {
      const res = await speak({ text: "The quick brown fox reads the evening paper.", path: "Public.md" });
      assert.equal(res.status, 200);
      assert.equal(res.headers.get("x-speak-engine"), "light");
      assert.equal(existsSync(ran), false, "the program never ran");
    } finally {
      initAuth({});
      patchSettings({ speak: null });
      delete process.env.SPEAK_EXTERNAL;
    }
  });

  it("folds text the way the page check does", () => {
    assert.equal(foldForMatch("The “quick”, brown — FOX!"), "thequickbrownfox");
    assert.equal(foldForMatch("الْمَكْتَبَةُ"), foldForMatch("الْمَكْتَبَةُ"));
  });
});