Ctrl K
ComunidadGuías…

tests/pocketGit.test.ts

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

Archivo original ↗Preguntar a KiBot
// THE REPOSITORY ON THE PHONE (mobile/src/pocket/git.ts), end to end, offline.
//
// isomorphic-git over the in-memory filesystem the pocket server's tests use,
// talking smart HTTP to a bare repository in a temporary directory through
// `git http-backend` run as a child process (tests/helpers/gitBackend.ts): no
// socket, no network, no repository but the one each test makes. The
// "laptop" is a working clone of the same bare repository, driven with the
// git CLI, so the remote moves the way a second machine moves it.
//
// What is pinned: a clone is ONE shallow branch with `.trash/` excluded and
// the base recorded; a commit stages writes and deletes and refuses nothing
// to commit; a push lands on the remote and moves the base; a pull tells the
// three cases apart — unchanged, fast-forward, diverged — and a divergence
// sets the phone's version down beside the remote's rather than merging
// prose; a note's past and an old blob are read from the phone's commits.

import assert from "node:assert/strict";
import { afterEach, describe, it } from "node:test";
import { PocketRepo } from "../mobile/src/pocket/git.ts";
import { makeMemoryFs, type MemoryFs } from "./helpers/memoryFs.ts";
import { makeRemote, readLaptop, type Remote } from "./helpers/gitBackend.ts";

const DIR = "/pocket/vault";
const SEED = {
  "Welcome.md": "# Welcome\n\nThe first note.\n",
  "Ideas/Astrolabe.md": "# Astrolabe\n\nAn instrument.\n",
  "Ideas/Old.md": "# Old\n\nSoon gone.\n",
};

let remote: Remote | null = null;
afterEach(() => {
  remote?.cleanup();
  remote = null;
});

async function setup(): Promise<{ remote: Remote; fs: MemoryFs; repo: PocketRepo; base: { value: string | null } }> {
  remote = makeRemote(SEED);
  const fs = makeMemoryFs();
  const base = { value: null as string | null };
  const repo = new PocketRepo({
    fs,
    http: remote.http,
    dir: DIR,
    url: remote.url,
    branch: "main",
    headers: { Authorization: "Basic dGVzdA==" },
    author: { name: "Phone", email: "[email protected]" },
    base: {
      async read() {
        return base.value;
      },
      async write(sha) {
        base.value = sha;
      },
    },
  });
  return { remote, fs, repo, base };
}

const read = async (fs: MemoryFs, file: string): Promise<string | null> => {
  try {
    return String(await fs.promises.readFile(`${DIR}/${file}`, { encoding: "utf8" }));
  } catch {
    return null;
  }
};
const write = (fs: MemoryFs, file: string, content: string): Promise<void> => fs.promises.writeFile(`${DIR}/${file}`, content, { encoding: "utf8" });

describe("clone", () => {
  it("is one shallow branch, with the base recorded and .trash/ excluded", async () => {
    const { remote, fs, repo, base } = await setup();
    const head = await repo.clone();
    assert.equal(head, remote.remoteHead());
    assert.equal(base.value, head);
    assert.equal(await read(fs, "Welcome.md"), SEED["Welcome.md"]);
    assert.equal(await read(fs, "Ideas/Astrolabe.md"), SEED["Ideas/Astrolabe.md"]);
    // depth 1: the first commit is on the remote and not on the phone
    const past = await repo.history("Welcome.md", 10);
    assert.deepEqual(past.map((r) => r.subject), ["The notes"]);
    assert.equal(String(await fs.promises.readFile(`${DIR}/.git/shallow`, { encoding: "utf8" })).trim(), head);
    assert.match(String(await fs.promises.readFile(`${DIR}/.git/info/exclude`, { encoding: "utf8" })), /^\.trash\/$/m);
    // the token rode in a header, and the URL carries no credentials
    assert.ok(remote.requests.length > 0);
    assert.ok(remote.requests.every((r) => r.auth === "Basic dGVzdA==" && !r.url.includes("@")));
    const config = String(await fs.promises.readFile(`${DIR}/.git/config`, { encoding: "utf8" }));
    assert.ok(!config.includes("dGVzdA=="), "the token is never written into .git/config");
    assert.equal(await repo.ahead(), 0);
  });
});

describe("commit", () => {
  it("stages a write and a delete, counts itself ahead, and commits nothing when there is nothing", async () => {
    const { fs, repo } = await setup();
    await repo.clone();
    await write(fs, "Welcome.md", "# Welcome\n\nWritten on the phone.\n");
    await fs.promises.unlink(`${DIR}/Ideas/Old.md`);
    const sha = await repo.commit("Astrolabe pocket: Welcome", ["Welcome.md", "Ideas/Old.md", "Ideas/Old.md"]);
    assert.match(sha ?? "", /^[0-9a-f]{40}$/);
    assert.equal(await repo.ahead(), 1);
    const [entry] = await repo.history("Welcome.md", 1);
    assert.equal(entry.sha, sha);
    assert.equal(entry.subject, "Astrolabe pocket: Welcome");
    assert.equal(await repo.commit("nothing", ["Never-existed.md"]), null, "an untracked, absent path is nothing to commit");
    await fs.promises.mkdir(`${DIR}/.trash`);
    await write(fs, ".trash/Welcome.md", "undo copy");
    assert.equal(await repo.commit("the drawer", [".trash/Welcome.md"]), null, ".trash/ is never committed");
  });
});

describe("push", () => {
  it("lands the phone's commit on the remote and moves the base", async () => {
    const { remote, fs, repo, base } = await setup();
    await repo.clone();
    await write(fs, "Phone note.md", "# From the phone\n");
    const sha = await repo.commit("Astrolabe pocket: Phone note", ["Phone note.md"]);
    assert.equal(await repo.push(), true);
    assert.equal(remote.remoteHead(), sha);
    assert.equal(remote.remoteFile("Phone note.md"), "# From the phone\n");
    assert.equal(base.value, sha);
    assert.equal(await repo.ahead(), 0);
  });
});

describe("pull", () => {
  it("is unchanged when the remote has not moved", async () => {
    const { repo } = await setup();
    const head = await repo.clone();
    const result = await repo.pull();
    assert.equal(result.kind, "unchanged");
    assert.equal(result.remoteSha, head);
    assert.deepEqual([result.changed, result.removed, result.conflicts], [[], [], []]);
  });

  it("fast-forwards what the laptop wrote, and names what changed and what went", async () => {
    const { remote, fs, repo, base } = await setup();
    await repo.clone();
    const sha = remote.laptopCommit({ "Ideas/Astrolabe.md": "# Astrolabe\n\nEdited on the laptop.\n", "New.md": "# New\n", "Ideas/Old.md": null }, "From the laptop");
    const result = await repo.pull();
    assert.equal(result.kind, "fast-forward");
    assert.equal(result.remoteSha, sha);
    assert.deepEqual(result.changed.sort(), ["Ideas/Astrolabe.md", "New.md"]);
    assert.deepEqual(result.removed, ["Ideas/Old.md"]);
    assert.equal(await read(fs, "Ideas/Astrolabe.md"), "# Astrolabe\n\nEdited on the laptop.\n");
    assert.equal(await read(fs, "Ideas/Old.md"), null);
    assert.equal(base.value, sha);
    assert.equal(await repo.head(), sha);
  });

  it("sets the phone's version down beside the laptop's when both changed one note, and keeps a note only the phone touched", async () => {
    const { remote, fs, repo } = await setup();
    await repo.clone();
    await write(fs, "Welcome.md", "# Welcome\n\nThe phone's sentence.\n");
    await write(fs, "Ideas/Astrolabe.md", "# Astrolabe\n\nOnly the phone touched this.\n");
    await repo.commit("Astrolabe pocket: two notes", ["Welcome.md", "Ideas/Astrolabe.md"]);
    const laptopSha = remote.laptopCommit({ "Welcome.md": "# Welcome\n\nThe laptop's sentence.\n" }, "From the laptop");

    const result = await repo.pull();
    assert.equal(result.kind, "diverged");
    assert.equal(result.remoteSha, laptopSha);
    assert.equal(result.conflicts.length, 1);
    const [pair] = result.conflicts;
    assert.equal(pair.path, "Welcome.md");
    assert.match(pair.phonePath, /^Welcome \(phone\)\.md$/);
    assert.equal(await read(fs, "Welcome.md"), "# Welcome\n\nThe laptop's sentence.\n", "the remote's version keeps the name");
    assert.equal(await read(fs, pair.phonePath), "# Welcome\n\nThe phone's sentence.\n", "the phone's is beside it, whole");
    assert.equal(await read(fs, "Ideas/Astrolabe.md"), "# Astrolabe\n\nOnly the phone touched this.\n");
    // both sentences reach the laptop on the next push
    assert.equal(await repo.push(), true);
    remote.laptopPull();
    assert.equal(readLaptop(remote, "Welcome.md"), "# Welcome\n\nThe laptop's sentence.\n");
    assert.equal(readLaptop(remote, pair.phonePath), "# Welcome\n\nThe phone's sentence.\n");
    assert.equal(readLaptop(remote, "Ideas/Astrolabe.md"), "# Astrolabe\n\nOnly the phone touched this.\n");
  });

  it("a push refused because the remote moved says so, and the pull after it carries both", async () => {
    const { remote, fs, repo } = await setup();
    await repo.clone();
    await write(fs, "Phone.md", "# Phone\n");
    await repo.commit("Astrolabe pocket: Phone", ["Phone.md"]);
    remote.laptopCommit({ "Laptop.md": "# Laptop\n" }, "From the laptop");
    await assert.rejects(() => repo.push(), /fast.?forward|rejected|non-fast/i, "the words session.ts reads as \"the remote moved\"");
    const result = await repo.pull();
    assert.equal(result.kind, "diverged");
    assert.equal(result.conflicts.length, 0, "different notes are not a conflict");
    assert.equal(await repo.push(), true);
    assert.equal(remote.remoteFile("Phone.md"), "# Phone\n");
    assert.equal(remote.remoteFile("Laptop.md"), "# Laptop\n");
  });
});

describe("a note's past", () => {
  it("lists the phone's commits for one file, newest first, and reads an old version back", async () => {
    const { fs, repo } = await setup();
    const cloned = await repo.clone();
    await write(fs, "Welcome.md", "# Welcome\n\nSecond.\n");
    const second = await repo.commit("Astrolabe pocket: Welcome", ["Welcome.md"]);
    const past = await repo.history("Welcome.md", 10);
    assert.deepEqual(past.map((r) => r.sha), [second, cloned]);
    assert.equal(past[0].short, second!.slice(0, 7));
    assert.equal(past[0].subject, "Astrolabe pocket: Welcome");
    assert.equal(await repo.blobAt("Welcome.md", cloned), SEED["Welcome.md"]);
    assert.equal(await repo.blobAt("Nope.md", cloned), null);
    assert.deepEqual(await repo.history("Nope.md", 10), []);
  });
});