Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/vcs-status/__tests__/contract.test.ts

Raw
import {
	chmodSync,
	mkdirSync,
	mkdtempSync,
	readdirSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
	createPiSession,
	type PiSession,
} from "../../../test/fixture/create-pi-session.ts";
import { writeWindowsExecutable } from "../../../test/fixture/windows-executable.ts";
import type { BranchChangeBridge } from "./branch-change-emitter.ts";

const BRIDGE_KEY = "__vcsStatusBranchChangeBridge";
let session: PiSession | undefined;
let root: string | undefined;

afterEach(async () => {
	(globalThis as Record<string, unknown>)[BRIDGE_KEY] = undefined;
	await session?.dispose();
	session = undefined;
	if (root) rmSync(root, { recursive: true, force: true });
	root = undefined;
});

function vcsPath(): string {
	return fileURLToPath(new URL("../index.ts", import.meta.url));
}

function emitterPath(): string {
	return fileURLToPath(new URL("./branch-change-emitter.ts", import.meta.url));
}

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

describe("vcs-status contract", () => {
	it("loads through the owned fixture", async () => {
		const extensionPath = vcsPath();
		session = await createPiSession({ extensionPaths: [extensionPath] });

		expect(session.errors).toEqual([]);
		expect(session.extensions).toHaveLength(1);
		expect(session.extensions[0]!.path).toBe(vcsPath());
	});

	it("writes debug lifecycle records in its sandbox when enabled", async () => {
		session = await createPiSession({
			extensionPaths: [vcsPath()],
			env: { PI_VCS_STATUS_DEBUG: "1" },
		});
		const extension = session.extensions[0]!;
		const context = session.session.extensionRunner.createCommandContext();
		for (const handler of extension.handlers.get("session_start") ?? [])
			await handler({ type: "session_start" }, context);
		for (const handler of extension.handlers.get("session_shutdown") ?? [])
			await handler({ type: "session_shutdown" }, context);

		const directory = path.join(
			session.cwd,
			".test-state",
			"pi-ext",
			"debug",
			"vcs-status",
		);
		const records = readFileSync(
			path.join(directory, readdirSync(directory)[0]!),
			"utf8",
		)
			.trim()
			.split("\n")
			.map((line) => JSON.parse(line));
		expect(records.map((record) => record.event)).toEqual(
			expect.arrayContaining(["session.start", "session.shutdown"]),
		);
	});

	it("refreshes on footer:branch-change with the latest context", async () => {
		root = mkdtempSync(path.join(tmpdir(), "pi-ext-vcs-status-branch-"));
		mkdirSync(path.join(root, ".git"));
		const headPath = path.join(root, ".git/HEAD");
		writeFileSync(headPath, "ref: refs/heads/main\n");
		const bin = path.join(root, "bin");
		mkdirSync(bin);
		if (process.platform === "win32")
			writeWindowsExecutable(bin, "git", "# branch.head main\n");
		else {
			writeFileSync(
				path.join(bin, "git"),
				"#!/bin/sh\nprintf '# branch.head main\\n'\n",
			);
			chmodSync(path.join(bin, "git"), 0o755);
		}

		const bridge: BranchChangeBridge = {};
		(globalThis as Record<string, unknown>)[BRIDGE_KEY] = bridge;
		session = await createPiSession({
			cwd: root,
			extensionPaths: [vcsPath(), emitterPath()],
			env: { PATH: `${bin}${path.delimiter}${process.env["PATH"]}` },
		});
		expect(session.errors).toEqual([]);
		expect(bridge.emit).toBeTypeOf("function");

		const vcsLabels: string[] = [];
		const runner = session.session.extensionRunner;
		runner.setUIContext(
			{
				setStatus: (key: string, text?: string) => {
					if (key === "vcs" && text) vcsLabels.push(text);
				},
				notify: () => {},
			} as never,
			"print",
		);

		// Before session start there is no context to refresh with.
		bridge.emit?.({ branch: "main" });
		await sleep(50);
		expect(vcsLabels).toEqual([]);

		await runner.emit({ type: "session_start", reason: "startup" });
		await vi.waitFor(() => expect(vcsLabels.at(-1)).toContain("main"), {
			timeout: 5000,
		});

		writeFileSync(headPath, "ref: refs/heads/feature\n");
		const before = vcsLabels.length;
		bridge.emit?.({ branch: "feature" });
		await vi.waitFor(
			() => {
				expect(vcsLabels.length).toBeGreaterThan(before);
				expect(vcsLabels.at(-1)).toContain("feature");
			},
			{ timeout: 5000 },
		);

		// After shutdown the event is ignored.
		await runner.emit({ type: "session_shutdown", reason: "quit" });
		const afterShutdown = vcsLabels.length;
		bridge.emit?.({ branch: "main" });
		await sleep(50);
		expect(vcsLabels.length).toBe(afterShutdown);
	});
});