import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } 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 { findIdeaProjectRoot, INTELLIJ_URL_SETTING, normalizeIntellijUrl, } from "../index.ts"; import { resolveSetting } from "../src/pi-ext-settings.ts"; const extensionPath = fileURLToPath(new URL("../index.ts", import.meta.url)); let root: string | undefined; let session: PiSession | undefined; function notify(): ReturnType { const runner = session!.session.extensionRunner; const ui = { ...runner.getUIContext() }; const spy = vi.spyOn(ui, "notify"); runner.setUIContext(ui, "print"); return spy; } function project(): string { root = mkdtempSync(join(tmpdir(), "pi-ext-intellij-settings-")); mkdirSync(join(root, ".idea")); return root; } afterEach(async () => { await session?.dispose(); session = undefined; if (root) rmSync(root, { recursive: true, force: true }); root = undefined; }); describe("intellij settings and detection", () => { it("accepts only credential-free loopback HTTP URLs", () => { expect(normalizeIntellijUrl("http://127.0.0.1:63342/mcp")).toBe( "http://127.0.0.1:63342/mcp", ); for (const value of [ "https://localhost/mcp", "http://example.com/mcp", "http://user:secret@localhost/mcp", "not a url", ]) expect(() => normalizeIntellijUrl(value)).toThrow(); }); it("uses trusted project setting and ignores untrusted project setting", async () => { const cwd = project(); mkdirSync(join(cwd, ".pi")); writeFileSync( join(cwd, ".pi", "settings.json"), JSON.stringify({ intellij: { url: "http://127.0.0.1:63342/project" } }), ); session = await createPiSession({ cwd, userSettings: { intellij: { url: "http://127.0.0.1:63342/user" } }, extensionPaths: [extensionPath], }); const pi = { getFlag: () => undefined }; expect( resolveSetting( pi, { cwd, isProjectTrusted: () => true }, INTELLIJ_URL_SETTING, ), ).toMatchObject({ value: "http://127.0.0.1:63342/project", source: "project", }); expect( resolveSetting( pi, { cwd, isProjectTrusted: () => false }, INTELLIJ_URL_SETTING, ), ).toMatchObject({ value: "http://127.0.0.1:63342/user", source: "user" }); }); it("warns only for IntelliJ projects with missing or invalid URL", async () => { const cwd = project(); session = await createPiSession({ cwd, extensionPaths: [extensionPath] }); const missing = notify(); await vi.waitFor(() => expect(missing).toHaveBeenCalledWith( expect.stringContaining("Set intellij.url"), "warning", ), ); await session.dispose(); session = undefined; rmSync(cwd, { recursive: true, force: true }); root = undefined; const invalid = project(); session = await createPiSession({ cwd: invalid, userSettings: { intellij: { url: "http://user:secret@localhost/mcp" } }, extensionPaths: [extensionPath], }); const bad = notify(); await vi.waitFor(() => expect(bad).toHaveBeenCalledWith( expect.stringContaining("must not contain credentials"), "warning", ), ); }); it("finds nearest parent root and has no activation without .idea", async () => { const cwd = mkdtempSync(join(tmpdir(), "pi-ext-intellij-no-project-")); root = cwd; mkdirSync(join(cwd, "a", "b"), { recursive: true }); expect(await findIdeaProjectRoot(join(cwd, "a", "b"))).toBeUndefined(); session = await createPiSession({ cwd, userSettings: { intellij: { url: "http://127.0.0.1:63342/mcp" } }, extensionPaths: [extensionPath], }); await new Promise((resolve) => setTimeout(resolve, 20)); expect(session.errors).toEqual([]); }); });