import { execFile } from "node:child_process"; import { existsSync } from "node:fs"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { promisify } from "node:util"; import { aroundEach, expect, it, vi } from "vitest"; import { cleanupNative, git, nativeOperation, nativeTest, openFixture, testSignal, withFakeGh, } from "./fixture.js"; const exec = promisify(execFile); aroundEach(nativeTest); it("isolates fixture repositories from inherited hook Git variables", async () => { const sentinel = await openFixture(); const gitDir = path.join(sentinel.root, ".git"); const files = ["config", "HEAD", "index"]; const before = await Promise.all( files.map((file) => readFile(path.join(gitDir, file))), ); const refs = await git(sentinel.root, "show-ref"); const bare = await mkdtemp(path.join(tmpdir(), "strata-bare-")); cleanupNative(() => rm(bare, { recursive: true, force: true })); try { for (const [key, value] of Object.entries({ GIT_DIR: gitDir, GIT_COMMON_DIR: gitDir, GIT_WORK_TREE: sentinel.root, GIT_INDEX_FILE: path.join(gitDir, "index"), GIT_OBJECT_DIRECTORY: path.join(gitDir, "objects"), GIT_PREFIX: "nested/", GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: "core.bare", GIT_CONFIG_VALUE_0: "true", })) vi.stubEnv(key, value); await git(bare, "init", "--quiet", "--bare"); expect(existsSync(path.join(bare, "config"))).toBe(true); const isolated = await openFixture(); expect( path.normalize( await git(isolated.root, "rev-parse", "--absolute-git-dir"), ), ).toBe(path.join(isolated.root, ".git")); await git(isolated.root, "add", "."); await git(isolated.root, "commit", "--quiet", "-m", "isolated change"); expect(await git(isolated.root, "log", "-1", "--format=%s")).toBe( "isolated change", ); } finally { vi.unstubAllEnvs(); } for (const [index, file] of files.entries()) expect(await readFile(path.join(gitDir, file))).toEqual(before[index]); expect(await git(sentinel.root, "show-ref")).toBe(refs); }); it.for(["failure", "abort"])( "drains %s cleanup before the next fake command", async (mode, context) => { const before = { PATH: process.env.PATH, NODE_OPTIONS: process.env.NODE_OPTIONS, STRATA_FAKE_GH_PRELOAD: process.env.STRATA_FAKE_GH_PRELOAD, }; const controller = new AbortController(); const reason = new Error("controlled cleanup"); let fixture!: Awaited>; let preload = ""; let finished = 0; let result!: Promise; const scope = nativeOperation(() => nativeTest( async () => { fixture = await openFixture(); const started = Promise.withResolvers(); const release = Promise.withResolvers(); result = withFakeGh('process.stdout.write("first");', async () => { preload = process.env.STRATA_FAKE_GH_PRELOAD ?? ""; try { // The preload must not intercept either Node or the required Node Git wrapper. expect( ( await exec( process.execPath, ["-e", 'process.stdout.write("helper")'], { signal: testSignal() }, ) ).stdout, ).toBe("helper"); expect(await git(fixture.root, "--version")).toMatch( /^git version /, ); started.resolve(); await release.promise; testSignal().throwIfAborted(); throw reason; } finally { finished++; started.resolve(); } }).catch((error: unknown) => error); await started.promise; if (mode === "abort") controller.abort(reason); release.resolve(); if (mode === "failure") throw await result; // Like Vitest's timeout: runTest returns while its callback is still unwinding. }, { signal: AbortSignal.any([context.signal, controller.signal]) }, ), ); if (mode === "failure") await expect(scope).rejects.toBe(reason); else await scope; expect(await result).toBe(reason); expect(finished).toBe(1); expect(existsSync(preload)).toBe(false); expect(existsSync(fixture.root)).toBe(false); for (const [key, value] of Object.entries(before)) expect(process.env[key]).toBe(value); await withFakeGh('process.stdout.write("later");', async () => { const activePreload = process.env.STRATA_FAKE_GH_PRELOAD; await fixture.close(); expect( (await exec("gh", ["pr", "view"], { signal: context.signal })).stdout, ).toBe("later"); expect(process.env.STRATA_FAKE_GH_PRELOAD).toBe(activePreload); expect(finished).toBe(1); }); for (const [key, value] of Object.entries(before)) expect(process.env[key]).toBe(value); }, );