import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { readConfig } from "../src/config.js";
describe("readConfig — string[] field sanitization", () => {
let dir: string;
let path: string;
let warnSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "reasonix-readconfig-"));
path = join(dir, "config.json");
warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
});
afterEach(() => {
warnSpy.mockRestore();
rmSync(dir, { recursive: true, force: true });
});
it("drops object / null / number items from mcp[] and warns once", () => {
writeFileSync(
path,
JSON.stringify({
mcp: [
"fs=npx -y @modelcontextprotocol/server-filesystem /tmp",
{ name: "github", command: "npx", args: ["-y"] },
null,
42,
"local=https://127.0.0.1:9000/sse",
],
}),
);
const cfg = readConfig(path);
expect(cfg.mcp).toEqual([
"fs=npx -y @modelcontextprotocol/server-filesystem /tmp",
"local=https://127.0.0.1:9000/sse",
]);
expect(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy.mock.calls[0][0]).toMatch(/field "mcp" had 3 non-string item\(s\)/);
});
it("drops mcp field entirely when it's not an array", () => {
writeFileSync(path, JSON.stringify({ mcp: "fs=npx -y pkg" }));
const cfg = readConfig(path);
expect(cfg.mcp).toBeUndefined();
expect(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy.mock.calls[0][0]).toMatch(/field "mcp" is not an array/);
});
it("sanitizes mcpDisabled[] the same way", () => {
writeFileSync(path, JSON.stringify({ mcpDisabled: ["fs", { x: 1 }, "remote"] }));
const cfg = readConfig(path);
expect(cfg.mcpDisabled).toEqual(["fs", "remote"]);
});
it("sanitizes recentWorkspaces[]", () => {
writeFileSync(path, JSON.stringify({ recentWorkspaces: ["/a", 7, "/b"] }));
const cfg = readConfig(path);
expect(cfg.recentWorkspaces).toEqual(["/a", "/b"]);
});
it("sanitizes nested skills.paths[]", () => {
writeFileSync(path, JSON.stringify({ skills: { paths: ["/x", null, "/y", { p: "z" }] } }));
const cfg = readConfig(path);
expect(cfg.skills?.paths).toEqual(["/x", "/y"]);
});
it("leaves all-string arrays untouched and does not warn", () => {
writeFileSync(
path,
JSON.stringify({
mcp: ["a=b", "c=d"],
mcpDisabled: ["a"],
recentWorkspaces: ["/a"],
skills: { paths: ["/x"] },
}),
);
const cfg = readConfig(path);
expect(cfg.mcp).toEqual(["a=b", "c=d"]);
expect(cfg.mcpDisabled).toEqual(["a"]);
expect(cfg.recentWorkspaces).toEqual(["/a"]);
expect(cfg.skills?.paths).toEqual(["/x"]);
expect(warnSpy).not.toHaveBeenCalled();
});
it("returns empty config when top-level is not an object", () => {
writeFileSync(path, JSON.stringify(["not", "an", "object"]));
expect(readConfig(path)).toEqual({});
});
});