Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/horst/__tests__/contract.test.ts

Raw
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import os, { 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 {
	getHostSection,
	type HostSnapshot,
	renderHostSection,
} from "../host.ts";
import {
	EMPTY_HORST_SETTINGS,
	parseHorst,
	resolveHorstTools,
} from "../settings.ts";

const horstPath = fileURLToPath(new URL("../index.ts", import.meta.url));

describe("parseHorst", () => {
	it("keeps well-formed tool deltas and lowercases keys", () => {
		const settings = parseHorst({
			preferredShell: " nu ",
			os: { WIN32: { tools: ["+powershell", "-bash"] } },
			hosts: { WinBox: { tools: ["-write"] } },
		});
		expect(settings).toEqual({
			os: { win32: ["+powershell", "-bash"] },
			hosts: { winbox: ["-write"] },
			preferredShell: "nu",
		});
		expect(parseHorst({})).toEqual(EMPTY_HORST_SETTINGS);
		expect(parseHorst({ os: { lnx: { tools: [] } } })).toEqual({
			os: { lnx: [] },
			hosts: {},
		});
	});

	it.each([
		[undefined, "expected an object"],
		[[], "expected an object"],
		[{ preferredShell: "  " }, "preferredShell must be a non-empty string"],
		[{ preferredShell: 42 }, "preferredShell must be a non-empty string"],
		[{ hosts: "nope" }, "hosts must be an object"],
		[{ os: { lnx: { flat: {} } } }, "os.lnx.tools must be an array"],
		[
			{ os: { lnx: { tools: { add: ["grep"], remove: ["bash"] } } } },
			'use ["+name", "-name"] instead of { add, remove }',
		],
		[{ hosts: { a: { tools: "-read" } } }, "hosts.a.tools must be an array"],
		...[42, "read", "+", "-", "+read*", "+ read", "-read "].map(
			(tool): [unknown, string] => [
				{ os: { lnx: { tools: [tool] } } },
				"os.lnx.tools must contain only +name/-name modifiers",
			],
		),
	] satisfies [unknown, string][])("rejects %j", (raw, message) => {
		expect(() => parseHorst(raw)).toThrow(message);
	});
});

describe("resolveHorstTools", () => {
	it.each([
		["linux", "lnx"],
		["darwin", "mac"],
		["win32", "win32"],
	])("maps %s to %s", (platform, key) => {
		expect(
			resolveHorstTools({ os: { [key]: ["+grep"] }, hosts: {} }, platform, "x"),
		).toEqual(["+grep"]);
	});

	it("matches hostnames case-insensitively and appends host modifiers after os", () => {
		const settings = {
			os: { win32: ["+powershell", "-bash"] },
			hosts: { winbox: ["-powershell", "+bash", "-write"] },
		};
		expect(resolveHorstTools(settings, "win32", "WinBox")).toEqual([
			"+powershell",
			"-bash",
			"-powershell",
			"+bash",
			"-write",
		]);
	});

	it("returns undefined without a match", () => {
		expect(
			resolveHorstTools(EMPTY_HORST_SETTINGS, "linux", "x"),
		).toBeUndefined();
	});
});

describe("renderHostSection", () => {
	it("renders host facts without a misleading environment shell or editor", () => {
		const host: HostSnapshot = {
			hostname: "winbox",
			osLabel: "Fedora Linux 42",
			kernel: "6.14.0",
			arch: "x64",
			cpuModel: "AMD Ryzen 9",
			cpuCount: 24,
			memGiB: 32,
		};
		const section = renderHostSection(host);
		expect(section).toContain("hostname: winbox");
		expect(section).toContain("os: Fedora Linux 42");
		expect(section).toContain("cpu: AMD Ryzen 9 × 24");
		expect(section).toContain("mem: 32 GiB");
		expect(section).not.toContain("Stable host environment facts:");
		expect(section).not.toContain("shell:");
		expect(section).not.toContain("editor:");
		expect(
			renderHostSection({ ...host, loginShell: "/bin/bash" }, "nu"),
		).toContain("login shell: /bin/bash\npreferred shell: nu");
	});

	it("reads account login shell independently of inherited SHELL", async () => {
		vi.resetModules();
		const userInfo = vi.spyOn(os, "userInfo").mockReturnValue({
			...os.userInfo(),
			shell: "/bin/zsh",
		});
		try {
			const { getHostSection: freshSection } = await import("../host.ts");
			expect(await freshSection()).toContain("login shell: /bin/zsh");
			expect(await freshSection()).not.toContain("preferred shell:");
		} finally {
			userInfo.mockRestore();
		}
	});

	it("omits unavailable account shell", async () => {
		for (const shell of [null, "", "throws"] as const) {
			vi.resetModules();
			const userInfo = vi.spyOn(os, "userInfo");
			if (shell === "throws")
				userInfo.mockImplementation(() => {
					throw new Error("unavailable");
				});
			else userInfo.mockReturnValue({ ...os.userInfo(), shell });
			try {
				const { getHostSection: freshSection } = await import("../host.ts");
				expect(await freshSection()).not.toContain("login shell:");
			} finally {
				userInfo.mockRestore();
			}
		}
	});
});

describe("horst owned fixture", () => {
	let session: PiSession | undefined;

	afterEach(async () => {
		await session?.dispose();
		session = undefined;
	});

	it("loads in real Pi extension runtime", async () => {
		session = await createPiSession({ extensionPaths: [horstPath] });
		expect(session.extensions.map((extension) => extension.path)).toContain(
			horstPath,
		);
	});

	it("applies the host tool delta from project settings", async () => {
		const root = mkdtempSync(path.join(tmpdir(), "horst-delta-"));
		try {
			mkdirSync(path.join(root, ".pi"), { recursive: true });
			writeFileSync(
				path.join(root, ".pi", "settings.json"),
				JSON.stringify({
					horst: {
						hosts: {
							[os.hostname().toLowerCase()]: {
								tools: ["+grep", "-grep", "+find", "+find", "-write"],
							},
						},
					},
				}),
			);
			session = await createPiSession({
				cwd: root,
				extensionPaths: [horstPath],
			});
			await session.session.extensionRunner.emit({
				type: "session_start",
				reason: "startup",
			});
			const active = session.session.getActiveToolNames();
			expect(active).not.toContain("grep");
			expect(active.filter((name) => name === "find")).toHaveLength(1);
			expect(active).not.toContain("write");
		} finally {
			await session?.dispose();
			session = undefined;
			rmSync(root, { recursive: true, force: true });
		}
	});

	it("injects the stable host section each turn", async () => {
		session = await createPiSession({ extensionPaths: [horstPath] });
		const runner = session.session.extensionRunner;
		const first = await runner.emitBeforeAgentStart(
			{ messageType: "user", prompt: "prompt" },
			{
				cwd: session.cwd,
			},
		);
		const second = await runner.emitBeforeAgentStart(
			{ messageType: "user", prompt: "prompt" },
			{
				cwd: session.cwd,
			},
		);
		const { horst } = first.systemPromptOptions.sections;
		expect(horst).toBe(second.systemPromptOptions.sections["horst"]);
		expect(horst).toContain(`hostname: ${os.hostname()}`);
		await expect(getHostSection()).resolves.toBe(horst);
	});

	it("injects project shell preference even without a tool delta", async () => {
		const root = mkdtempSync(path.join(tmpdir(), "horst-shell-"));
		try {
			mkdirSync(path.join(root, ".pi"), { recursive: true });
			writeFileSync(
				path.join(root, ".pi", "settings.json"),
				JSON.stringify({ horst: { preferredShell: " fish " } }),
			);
			session = await createPiSession({
				cwd: root,
				extensionPaths: [horstPath],
			});
			const runner = session.session.extensionRunner;
			await runner.emit({ type: "session_start", reason: "startup" });
			const first = await runner.emitBeforeAgentStart(
				{ messageType: "user", prompt: "prompt" },
				{
					cwd: session.cwd,
				},
			);
			const second = await runner.emitBeforeAgentStart(
				{ messageType: "user", prompt: "prompt" },
				{
					cwd: session.cwd,
				},
			);
			expect(first.systemPromptOptions.sections["horst"]).toContain(
				"preferred shell: fish",
			);
			expect(second.systemPromptOptions.sections["horst"]).toBe(
				first.systemPromptOptions.sections["horst"],
			);
		} finally {
			await session?.dispose();
			session = undefined;
			rmSync(root, { recursive: true, force: true });
		}
	});
});