import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "@effect/vitest"; import { Effect } from "effect"; import { makeGitArtifactStore } from "./git-artifact-store"; import type { ArtifactStoreError } from "../seams/artifact-store"; const run = (effect: Effect.Effect): Promise => Effect.runPromise(effect); // --------------------------------------------------------------------------- // Finding 6 regression (git compare-and-swap): two commits built from the SAME // parent cannot both advance the branch. The ref update is a CAS against the // expected old value, so the second raced writer gets a typed conflict instead // of silently clobbering the first. This is deterministic (no timing race): we // force both to share a parent by committing them against a store whose head we // pin. // --------------------------------------------------------------------------- describe("git ArtifactStore ref CAS (Fix 6)", () => { it("two concurrent commits from one parent: one wins, the other typed-conflicts", async () => { const root = mkdtempSync(join(tmpdir(), "apps-cas-")); const storeA = makeGitArtifactStore({ root }); const scopeA = await run(storeA.forScope("s")); // Seed a parent so racers commit ON TOP of the same head. await run(scopeA.commit(new Map([["tools/base.ts", "// base"]]), "base")); // Many SEPARATE store instances over the SAME repo dir. The repo already // exists (seeded above), so opening each scope is a re-`init --bare` (a no-op // on an existing repo). Open them SEQUENTIALLY so the idempotent init doesn't // itself race, then race only the COMMITS (the ref-CAS is what's under test). const N = 12; const openedScopes: { commit: typeof scopeA.commit; log: typeof scopeA.log }[] = []; for (let i = 0; i < N; i++) { const store = makeGitArtifactStore({ root }); // eslint-disable-next-line no-await-in-loop openedScopes.push(await run(store.forScope("s"))); } const scopeB = openedScopes[0]; // Fire many concurrent commits, ALL built against the seeded base parent. const results = await Promise.allSettled( openedScopes.map((sc, i) => run(sc.commit(new Map([[`tools/c${i}.ts`, `// ${i}`]]), `c${i}`)), ), ); const fulfilled = results.filter((r) => r.status === "fulfilled").length; const rejected = results.filter((r) => r.status === "rejected"); // Any failure MUST be a typed conflict (never a silent clobber). for (const r of rejected) { const err = (r as PromiseRejectedResult).reason as ArtifactStoreError; expect(String(JSON.stringify(err))).toMatch(/conflict/i); } // No lost commits: the git log length equals base(1) + every commit that // reported success. Without the CAS, a raced writer clobbers the ref and an // earlier successful commit vanishes from history, making the log SHORTER // than base + fulfilled. The CAS guarantees every reported success is a real, // reachable, non-clobbering advance. const logEntries = await run(scopeB.log(1000)); expect(logEntries.length).toBe(1 + fulfilled); }, 60_000); });