Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions __tests__/config-core.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
"use strict";

jest.mock("fs")
jest.mock("os", () => ({
homedir: jest.fn(() => "/home/user"),
hostname: jest.fn(() => "host"),
type: jest.fn(() => "Linux"),
release: jest.fn(() => "5.0"),
arch: jest.fn(() => "x64"),
userInfo: jest.fn(() => ({ username: "user" })),
}))
jest.mock("../cli/plugins-store", () => ({
getEffectivePluginCommands: jest.fn(() => []),
}))

const { normalizeMcpServerEntry } = require("../cli/config-core")

describe("normalizeMcpServerEntry", () => {
describe("name validation", () => {
test("returns null for null name", () => {
expect(normalizeMcpServerEntry(null, { url: "http://x" })).toBeNull()
})

test("returns null for empty string name", () => {
expect(normalizeMcpServerEntry("", { url: "http://x" })).toBeNull()
})

test("returns null for numeric name", () => {
expect(normalizeMcpServerEntry(42, { url: "http://x" })).toBeNull()
})

test("returns null for undefined name", () => {
expect(normalizeMcpServerEntry(undefined, { url: "http://x" })).toBeNull()
})
})

describe("entry validation", () => {
test("returns null for null entry", () => {
expect(normalizeMcpServerEntry("srv", null)).toBeNull()
})

test("returns null for array entry", () => {
expect(normalizeMcpServerEntry("srv", ["http://x"])).toBeNull()
})

test("returns null for non-object entry", () => {
expect(normalizeMcpServerEntry("srv", "http://x")).toBeNull()
})

test("returns null for undefined entry", () => {
expect(normalizeMcpServerEntry("srv", undefined)).toBeNull()
})
})

describe("url and command fields", () => {
test("includes url when it is a string", () => {
const out = normalizeMcpServerEntry("srv", { url: "http://example.com" })
expect(out).toEqual({ name: "srv", url: "http://example.com" })
})

test("omits url when it is not a string", () => {
const out = normalizeMcpServerEntry("srv", { url: 1234 })
expect(out).not.toHaveProperty("url")
})

test("includes command when it is a string", () => {
const out = normalizeMcpServerEntry("srv", { command: "node" })
expect(out).toEqual({ name: "srv", command: "node" })
})

test("omits command when it is not a string", () => {
const out = normalizeMcpServerEntry("srv", { command: true })
expect(out).not.toHaveProperty("command")
})
})

describe("args and commandArgs filtering", () => {
test("includes only string elements from args", () => {
const out = normalizeMcpServerEntry("srv", { args: ["a", 2, null, "b"] })
expect(out.args).toEqual(["a", "b"])
})

test("omits args entirely when not an array", () => {
const out = normalizeMcpServerEntry("srv", { args: "bad" })
expect(out).not.toHaveProperty("args")
})

test("includes only string elements from commandArgs", () => {
const out = normalizeMcpServerEntry("srv", { commandArgs: ["x", 0, "y"] })
expect(out.commandArgs).toEqual(["x", "y"])
})

test("omits commandArgs when not an array", () => {
const out = normalizeMcpServerEntry("srv", { commandArgs: {} })
expect(out).not.toHaveProperty("commandArgs")
})
})

describe("headers filtering", () => {
test("keeps string-to-string header entries", () => {
const out = normalizeMcpServerEntry("srv", { headers: { Auth: "Bearer x", Bad: 123 } })
expect(out.headers).toEqual({ Auth: "Bearer x" })
})

test("omits headers when they are an array", () => {
const out = normalizeMcpServerEntry("srv", { headers: ["bad"] })
expect(out).not.toHaveProperty("headers")
})

test("omits headers when not an object", () => {
const out = normalizeMcpServerEntry("srv", { headers: "bad" })
expect(out).not.toHaveProperty("headers")
})
})

describe("env filtering", () => {
test("keeps string-to-string env entries", () => {
const out = normalizeMcpServerEntry("srv", { env: { TOKEN: "abc", NUM: 1 } })
expect(out.env).toEqual({ TOKEN: "abc" })
})

test("omits env when it is an array", () => {
const out = normalizeMcpServerEntry("srv", { env: ["bad"] })
expect(out).not.toHaveProperty("env")
})
})

describe("timeout_ms", () => {
test("includes timeout_ms for positive number", () => {
const out = normalizeMcpServerEntry("srv", { timeout_ms: 5000 })
expect(out.timeout_ms).toBe(5000)
})

test("omits timeout_ms when zero", () => {
const out = normalizeMcpServerEntry("srv", { timeout_ms: 0 })
expect(out).not.toHaveProperty("timeout_ms")
})

test("omits timeout_ms when negative", () => {
const out = normalizeMcpServerEntry("srv", { timeout_ms: -1 })
expect(out).not.toHaveProperty("timeout_ms")
})

test("omits timeout_ms when it is a string", () => {
const out = normalizeMcpServerEntry("srv", { timeout_ms: "5000" })
expect(out).not.toHaveProperty("timeout_ms")
})
})

describe("stateful flag", () => {
test("includes stateful when exactly true", () => {
const out = normalizeMcpServerEntry("srv", { stateful: true })
expect(out.stateful).toBe(true)
})

test("omits stateful when it is the string 'true'", () => {
const out = normalizeMcpServerEntry("srv", { stateful: "true" })
expect(out).not.toHaveProperty("stateful")
})

test("omits stateful when false", () => {
const out = normalizeMcpServerEntry("srv", { stateful: false })
expect(out).not.toHaveProperty("stateful")
})
})

describe("combined entry", () => {
test("normalizes a full valid entry correctly", () => {
const out = normalizeMcpServerEntry("my-server", {
url: "http://mcp.example.com",
command: "node",
args: ["server.js", 0],
headers: { Authorization: "Bearer tok", "X-Num": 1 },
env: { API_KEY: "key" },
timeout_ms: 3000,
stateful: true,
})
expect(out).toEqual({
name: "my-server",
url: "http://mcp.example.com",
command: "node",
args: ["server.js"],
headers: { Authorization: "Bearer tok" },
env: { API_KEY: "key" },
timeout_ms: 3000,
stateful: true,
})
})
})
})
107 changes: 107 additions & 0 deletions __tests__/plugins-store.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ jest.mock("os", () => ({
const {
SUPERCLI_PLUGINS_DIR,
SUPERCLI_LOCAL_LOCK_FILE,
SUPERCLI_SERVER_LOCK_FILE,
LEGACY_PLUGINS_FILE,
readPluginsLock,
writePluginsLock,
readServerPluginsLock,
writeServerPluginsLock,
listInstalledPlugins,
listServerInstalledPlugins,
getPlugin,
getInstalledPluginCommands,
getServerPluginCommands,
getEffectivePluginCommands,
} = require("../cli/plugins-store")
const path = require("path")
Expand Down Expand Up @@ -147,4 +153,105 @@ describe("plugins-store", () => {
expect(commands).toEqual([{ id: "local" }, { id: "server" }])
})
})

describe("readServerPluginsLock (readLockFile edge cases)", () => {
test("returns empty lock if server lock file does not exist", () => {
fs.existsSync.mockReturnValue(false)
const lock = readServerPluginsLock()
expect(lock).toEqual({ version: 1, installed: {} })
})

test("returns parsed server lock if file exists", () => {
fs.existsSync.mockReturnValue(true)
const mockLock = { version: 1, installed: { s1: { name: "s1" } } }
fs.readFileSync.mockReturnValue(JSON.stringify(mockLock))
const lock = readServerPluginsLock()
expect(lock).toEqual(mockLock)
})

test("returns empty lock if server lock file contains invalid JSON", () => {
fs.existsSync.mockReturnValue(true)
fs.readFileSync.mockReturnValue("{broken")
const lock = readServerPluginsLock()
expect(lock).toEqual({ version: 1, installed: {} })
})

test("resets installed to {} when installed is null", () => {
fs.existsSync.mockReturnValue(true)
fs.readFileSync.mockReturnValue(JSON.stringify({ version: 2, installed: null }))
const lock = readServerPluginsLock()
expect(lock.installed).toEqual({})
})

test("resets installed to {} when installed is a string", () => {
fs.existsSync.mockReturnValue(true)
fs.readFileSync.mockReturnValue(JSON.stringify({ version: 2, installed: "bad" }))
const lock = readServerPluginsLock()
expect(lock.installed).toEqual({})
})
})

describe("writeServerPluginsLock", () => {
test("writes server lock file and ensures plugins dir exists", () => {
fs.existsSync.mockReturnValue(false)
const lock = { version: 1, installed: { s1: { name: "s1" } } }
const result = writeServerPluginsLock(lock)
expect(fs.mkdirSync).toHaveBeenCalledWith(SUPERCLI_PLUGINS_DIR, { recursive: true })
expect(fs.writeFileSync).toHaveBeenCalledWith(
SUPERCLI_SERVER_LOCK_FILE,
expect.stringContaining('"name": "s1"')
)
expect(result).toEqual(lock)
})
})

describe("listServerInstalledPlugins", () => {
test("returns array of server-installed plugins", () => {
fs.existsSync.mockReturnValue(true)
const s1 = { name: "s1" }
fs.readFileSync.mockReturnValue(JSON.stringify({ installed: { s1 } }))
expect(listServerInstalledPlugins()).toEqual([s1])
})

test("returns empty array when server lock has no plugins", () => {
fs.existsSync.mockReturnValue(false)
expect(listServerInstalledPlugins()).toEqual([])
})
})

describe("getPlugin", () => {
test("returns plugin by name when it exists", () => {
fs.existsSync.mockReturnValue(true)
const p1 = { name: "p1", version: "1.0.0" }
fs.readFileSync.mockReturnValue(JSON.stringify({ installed: { p1 } }))
expect(getPlugin("p1")).toEqual(p1)
})

test("returns null when plugin does not exist", () => {
fs.existsSync.mockReturnValue(true)
fs.readFileSync.mockReturnValue(JSON.stringify({ installed: {} }))
expect(getPlugin("missing")).toBeNull()
})

test("returns null when lock file is absent", () => {
fs.existsSync.mockReturnValue(false)
expect(getPlugin("any")).toBeNull()
})
})

describe("getServerPluginCommands", () => {
test("returns flattened commands from server plugins", () => {
fs.existsSync.mockReturnValue(true)
const c1 = { id: "sc1" }
fs.readFileSync.mockReturnValue(JSON.stringify({
installed: { s1: { name: "s1", commands: [c1] } }
}))
expect(getServerPluginCommands()).toEqual([c1])
})

test("returns empty array when server lock is empty", () => {
fs.existsSync.mockReturnValue(false)
expect(getServerPluginCommands()).toEqual([])
})
})
})
Loading