Advertisement

← vibesdk

space/test/git-objects.test.ts

5.6 KB Download file
/**
 * Tests for the Artifacts base-source plumbing in git-objects.ts:
 *
 * - `createArtifactsBaseSource.loadSnapshot()` must reconcile an unborn local
 *   branch onto the fetched head. Without this, the first commit after a
 *   SpaceDO cold start is a ROOT commit, and the mirrored force-push replaces
 *   the remote branch — destroying all earlier history (restore points).
 * - A failed fetch must THROW (retryable), not return null (cached forever).
 * - `stageWorkdir()` must stage the tree (incl. deletions) while never
 *   committing reserved bookkeeping paths (`.afs`).
 */
import { describe, it, expect } from "vitest"
import * as git from "isomorphic-git"
import { InMemoryFs, type FileSystem } from "@cloudflare/shell"
import { createGit } from "@cloudflare/shell/git"
import {
  createArtifactsBaseSource,
  createGitFs,
  resolveHead,
  stageWorkdir,
  walkTreeFiles,
} from "../src/space/git-objects"

const AUTHOR = { name: "Test", email: "test@vibesdk.local" }
const isReserved = (p: string) => p === "/.afs" || p.startsWith("/.afs/") || p === "/.git" || p.startsWith("/.git/")

/** Commit all current workdir files on `main`; returns the commit oid. */
async function commitAll(fs: FileSystem, message: string): Promise<string> {
  const g = createGit(fs)
  await g.add({ filepath: "." })
  const { oid } = await g.commit({ message, author: AUTHOR })
  return oid
}

/**
 * Simulate the cold-start state: the fetched objects + remote-tracking ref are
 * present in `.git`, but the local branch is unborn (fresh `git init`).
 */
async function simulateColdStart(fs: FileSystem, head: string): Promise<void> {
  const gitFs = createGitFs(fs)
  await git.writeRef({ fs: gitFs, dir: "/", ref: "refs/remotes/artifacts/main", value: head, force: true })
  await git.deleteRef({ fs: gitFs, dir: "/", ref: "refs/heads/main" })
}

function baseSource(fs: FileSystem, opts?: { hasRemote?: boolean; fetchOk?: boolean }) {
  return createArtifactsBaseSource({
    overlay: fs,
    branch: "main",
    hasRemote: async () => opts?.hasRemote ?? true,
    fetchBranch: async () => opts?.fetchOk ?? true,
  })
}

describe("createArtifactsBaseSource.loadSnapshot", () => {
  it("reconciles an unborn local branch onto the fetched head", async () => {
    const fs = new InMemoryFs()
    const g = createGit(fs)
    await g.init({ defaultBranch: "main" })
    await fs.writeFile("/app.ts", "v1")
    const c1 = await commitAll(fs, "first")

    await simulateColdStart(fs, c1)
    expect(await resolveHead(fs, "refs/heads/main")).toBeNull()

    const snapshot = await baseSource(fs).loadSnapshot()
    expect(snapshot?.head).toBe(c1)
    // The fix: the local branch now points at the fetched head…
    expect(await resolveHead(fs, "refs/heads/main")).toBe(c1)

    // …so the next commit is a DESCENDANT of the remote head, not a root.
    await fs.writeFile("/app.ts", "v2")
    const c2 = await commitAll(fs, "second")
    const log = await g.log({ depth: 2 })
    expect(log[0].oid).toBe(c2)
    expect(log[0].parent).toContain(c1)
  })

  it("never clobbers an existing local branch", async () => {
    const fs = new InMemoryFs()
    const g = createGit(fs)
    await g.init({ defaultBranch: "main" })
    await fs.writeFile("/app.ts", "v1")
    const c1 = await commitAll(fs, "first")

    await simulateColdStart(fs, c1)
    await baseSource(fs).loadSnapshot()

    await fs.writeFile("/app.ts", "v2")
    const c2 = await commitAll(fs, "second")

    // A later re-load (e.g. retry after a transient failure) must not reset
    // the local branch back to the older remote head.
    await baseSource(fs).loadSnapshot()
    expect(await resolveHead(fs, "refs/heads/main")).toBe(c2)
  })

  it("returns null without fetching when no remote repo is known", async () => {
    const fs = new InMemoryFs()
    await createGit(fs).init({ defaultBranch: "main" })
    let fetches = 0
    const source = createArtifactsBaseSource({
      overlay: fs,
      branch: "main",
      hasRemote: async () => false,
      fetchBranch: async () => {
        fetches++
        return true
      },
    })
    expect(await source.loadSnapshot()).toBeNull()
    expect(fetches).toBe(0)
  })

  it("throws on a failed fetch so the caller can retry later", async () => {
    const fs = new InMemoryFs()
    await createGit(fs).init({ defaultBranch: "main" })
    await expect(baseSource(fs, { fetchOk: false }).loadSnapshot()).rejects.toThrow(/fetch failed/i)
  })
})

describe("stageWorkdir", () => {
  it("stages new files but never reserved bookkeeping paths", async () => {
    const fs = new InMemoryFs()
    const g = createGit(fs)
    await g.init({ defaultBranch: "main" })
    await fs.mkdir("/.afs", { recursive: true })
    await fs.writeFile("/.afs/state.json", "{}")
    await fs.writeFile("/a.ts", "a")
    await fs.writeFile("/dir/b.ts", "b")

    await stageWorkdir(fs, isReserved)
    const oid = (await g.commit({ message: "c", author: AUTHOR })).oid

    const files = await walkTreeFiles(fs, oid)
    expect([...files.keys()].sort()).toEqual(["/a.ts", "/dir/b.ts"])
  })

  it("stages deletions of tracked files", async () => {
    const fs = new InMemoryFs()
    const g = createGit(fs)
    await g.init({ defaultBranch: "main" })
    await fs.writeFile("/keep.ts", "k")
    await fs.writeFile("/gone.ts", "g")
    await commitAll(fs, "first")

    await fs.rm("/gone.ts", { force: true })
    await stageWorkdir(fs, isReserved)
    const oid = (await g.commit({ message: "second", author: AUTHOR })).oid

    const files = await walkTreeFiles(fs, oid)
    expect([...files.keys()]).toEqual(["/keep.ts"])
  })
})