repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/vcs-status/__tests__/contract.test.ts
Rawimport {
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);
});
});