Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/web/__serial__/settings.test.ts

Raw
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
import { afterEach, describe, expect, it } from "vitest";
import {
	createPiSession,
	type PiSession,
} from "../../../test/fixture/create-pi-session.ts";
import {
	minimaxApiHost,
	zaiContentSize,
	zaiFreshness,
	zaiLocation,
	zaiMcpUrl,
} from "../settings.ts";
import { zaiSearch } from "../zai.ts";

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

type Case = {
	user?: unknown;
	project?: unknown;
	env?: Record<string, string>;
	untrusted?: boolean;
};

type Notification = {
	message: unknown;
	type?: "info" | "warning" | "error" | undefined;
};

describe("web settings resolution", () => {
	let session: PiSession | undefined;
	let root: string | undefined;
	let notifications: Notification[] = [];
	afterEach(async () => {
		await session?.dispose();
		session = undefined;
		notifications = [];
		if (root) rmSync(root, { recursive: true, force: true });
		root = undefined;
	});

	function recordingUI(): ExtensionUIContext {
		const noop = () => undefined;
		return {
			select: async () => undefined,
			confirm: async () => false,
			input: async () => undefined,
			editor: async () => undefined,
			custom: async () => undefined,
			notify: (message: string, type?: "info" | "warning" | "error") =>
				notifications.push({ message, type }),
			onTerminalInput: () => noop,
			setStatus: noop,
			setWorkingMessage: noop,
			setWorkingVisible: noop,
			setWorkingIndicator: noop,
		} as unknown as ExtensionUIContext;
	}

	async function start(input: Case) {
		const dir = mkdtempSync(path.join(tmpdir(), "pi-ext-web-settings-"));
		root = dir;
		if ("project" in input) {
			const at = path.join(dir, ".pi");
			mkdirSync(at, { recursive: true });
			writeFileSync(
				path.join(at, "settings.json"),
				JSON.stringify({ web: input.project }),
			);
		}
		session = await createPiSession({
			cwd: dir,
			extensionPaths: [extensionPath],
			...("user" in input ? { userSettings: { web: input.user } } : {}),
			env: {
				ZAI_API_KEY: "test-key",
				ZAI_SEARCH_FRESHNESS: undefined,
				ZAI_SEARCH_MCP_URL: undefined,
				ZAI_SEARCH_BASE_URL: undefined,
				ZAI_SEARCH_CONTENT_SIZE: undefined,
				ZAI_SEARCH_LOCATION: undefined,
				MINIMAX_API_HOST: undefined,
				...input.env,
			},
		});
		const runner = session.session.extensionRunner;
		runner.setUIContext(recordingUI());
		if (input.untrusted)
			session.session.settingsManager.setProjectTrusted(false);
		await runner.emit({ type: "session_start", reason: "startup" });
		const warnings = notifications
			.filter((call) => String(call.message).startsWith("web: Invalid"))
			.map((call) => call.message);
		const ctx = { cwd: dir, isProjectTrusted: () => !input.untrusted };
		return { ctx, warnings };
	}

	it("uses defaults without settings", async () => {
		const { ctx, warnings } = await start({});
		expect(warnings).toEqual([]);
		expect(zaiFreshness(ctx)).toBeNull();
		expect(zaiMcpUrl(ctx)).toBe(
			"https://api.z.ai/api/mcp/web_search_prime/mcp",
		);
		expect(zaiContentSize(ctx)).toBe("medium");
		expect(zaiLocation(ctx)).toBe("us");
		expect(minimaxApiHost(ctx)).toBe("https://api.minimax.io");
	});

	it("merges trusted project over user settings", async () => {
		const { ctx, warnings } = await start({
			user: {
				zai: { freshness: "week", location: "eu" },
				minimax: { apiHost: "https://user.test" },
			},
			project: { zai: { freshness: "day" } },
		});
		expect(warnings).toEqual([]);
		expect(zaiFreshness(ctx)).toBe("day");
		expect(zaiLocation(ctx)).toBe("eu");
		expect(minimaxApiHost(ctx)).toBe("https://user.test");
	});

	it("prefers env over project and user settings", async () => {
		const { ctx } = await start({
			user: { zai: { contentSize: "high" } },
			project: { zai: { contentSize: "low", mcpUrl: "https://project.test" } },
			env: {
				ZAI_SEARCH_CONTENT_SIZE: "medium",
				MINIMAX_API_HOST: "https://env.test",
			},
		});
		expect(zaiContentSize(ctx)).toBe("medium");
		expect(zaiMcpUrl(ctx)).toBe("https://project.test");
		expect(minimaxApiHost(ctx)).toBe("https://env.test");
	});

	it("orders ZAI_SEARCH_MCP_URL over ZAI_SEARCH_BASE_URL over settings", async () => {
		const both = await start({
			project: { zai: { mcpUrl: "https://project.test" } },
			env: {
				ZAI_SEARCH_MCP_URL: "https://mcp.test",
				ZAI_SEARCH_BASE_URL: "https://base.test",
			},
		});
		expect(zaiMcpUrl(both.ctx)).toBe("https://mcp.test");
		await session?.dispose();
		session = undefined;
		if (root) rmSync(root, { recursive: true, force: true });
		root = undefined;
		const base = await start({
			project: { zai: { mcpUrl: "https://project.test" } },
			env: { ZAI_SEARCH_BASE_URL: "https://base.test" },
		});
		expect(zaiMcpUrl(base.ctx)).toBe("https://base.test");
	});

	it("ignores untrusted project settings", async () => {
		const { ctx, warnings } = await start({
			user: { zai: { freshness: "week" } },
			project: { zai: { freshness: 42 } },
			untrusted: true,
		});
		expect(warnings).toEqual([]);
		expect(zaiFreshness(ctx)).toBe("week");
	});

	it.each([
		[
			"project freshness",
			{
				user: { zai: { freshness: "week" } },
				project: { zai: { freshness: "hour" } },
			},
			"web: Invalid web.zai.freshness from project settings: expected day, week, month, year, or none",
		],
		[
			"env location",
			{ user: { zai: { location: "eu" } }, env: { ZAI_SEARCH_LOCATION: " " } },
			"web: Invalid web.zai.location from environment variable ZAI_SEARCH_LOCATION: expected a non-empty string",
		],
		[
			"user api host",
			{ user: { minimax: { apiHost: 1 } } },
			"web: Invalid web.minimax.apiHost from user settings: expected a non-empty string",
		],
	] satisfies [string, Case, string][])(
		"warns on invalid %s without falling through",
		async (_name, input, warning) => {
			const { warnings } = await start(input);
			expect(warnings).toEqual([warning]);
		},
	);

	it("fails Z.AI search on an invalid setting", async () => {
		await start({ user: { zai: { contentSize: "" } } });
		await expect(zaiSearch("q", undefined)).rejects.toThrow(
			"Invalid web.zai.contentSize from user settings: expected a non-empty string",
		);
	});
});