fix(converters): preserve user config when writing MCP servers (#479)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trevin Chow
2026-04-01 11:46:57 -07:00
committed by GitHub
parent c56c7667df
commit c65a698d93
8 changed files with 862 additions and 71 deletions

View File

@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
import { promises as fs } from "fs"
import path from "path"
import os from "os"
import { writeCodexBundle } from "../src/targets/codex"
import { mergeCodexConfig, renderCodexConfig, writeCodexBundle } from "../src/targets/codex"
import type { CodexBundle } from "../src/types/codex"
async function exists(filePath: string): Promise<boolean> {
@@ -44,6 +44,8 @@ describe("writeCodexBundle", () => {
expect(await exists(configPath)).toBe(true)
const config = await fs.readFile(configPath, "utf8")
expect(config).toContain("# BEGIN Compound Engineering plugin MCP -- do not edit this block")
expect(config).toContain("# END Compound Engineering plugin MCP")
expect(config).toContain("[mcp_servers.local]")
expect(config).toContain("command = \"echo\"")
expect(config).toContain("args = [\"hello\"]")
@@ -74,12 +76,12 @@ describe("writeCodexBundle", () => {
expect(await exists(path.join(codexRoot, "skills", "skill-one", "SKILL.md"))).toBe(true)
})
test("backs up existing config.toml before overwriting", async () => {
test("preserves existing user config when writing MCP servers", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-backup-"))
const codexRoot = path.join(tempRoot, ".codex")
const configPath = path.join(codexRoot, "config.toml")
// Create existing config
// Create existing config with user settings
await fs.mkdir(codexRoot, { recursive: true })
const originalContent = "# My original config\n[custom]\nkey = \"value\"\n"
await fs.writeFile(configPath, originalContent)
@@ -93,11 +95,17 @@ describe("writeCodexBundle", () => {
await writeCodexBundle(codexRoot, bundle)
// New config should be written
const newConfig = await fs.readFile(configPath, "utf8")
// Plugin MCP servers should be present in a managed block
expect(newConfig).toContain("[mcp_servers.test]")
expect(newConfig).toContain("# BEGIN Compound Engineering plugin MCP -- do not edit this block")
expect(newConfig).toContain("# END Compound Engineering plugin MCP")
// User's original config should be preserved
expect(newConfig).toContain("# My original config")
expect(newConfig).toContain("[custom]")
expect(newConfig).toContain('key = "value"')
// Backup should exist with original content
// Backup should still exist with original content
const files = await fs.readdir(codexRoot)
const backupFileName = files.find((f) => f.startsWith("config.toml.bak."))
expect(backupFileName).toBeDefined()
@@ -106,6 +114,120 @@ describe("writeCodexBundle", () => {
expect(backupContent).toBe(originalContent)
})
test("is idempotent — running twice does not duplicate managed block", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-idempotent-"))
const codexRoot = path.join(tempRoot, ".codex")
const configPath = path.join(codexRoot, "config.toml")
await fs.mkdir(codexRoot, { recursive: true })
await fs.writeFile(configPath, "[user]\nmodel = \"gpt-4.1\"\n")
const bundle: CodexBundle = {
prompts: [],
skillDirs: [],
generatedSkills: [],
mcpServers: { test: { command: "echo" } },
}
await writeCodexBundle(codexRoot, bundle)
await writeCodexBundle(codexRoot, bundle)
const config = await fs.readFile(configPath, "utf8")
expect(config.match(/# BEGIN Compound Engineering plugin MCP/g)?.length).toBe(1)
expect(config.match(/# END Compound Engineering plugin MCP/g)?.length).toBe(1)
expect(config).toContain("[user]")
})
test("migrates old managed block markers to new ones", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-migrate-"))
const codexRoot = path.join(tempRoot, ".codex")
const configPath = path.join(codexRoot, "config.toml")
await fs.mkdir(codexRoot, { recursive: true })
await fs.writeFile(configPath, [
"[user]",
'model = "gpt-4.1"',
"",
"# BEGIN compound-plugin Claude Code MCP",
"[mcp_servers.old]",
'command = "old"',
"# END compound-plugin Claude Code MCP",
].join("\n"))
const bundle: CodexBundle = {
prompts: [],
skillDirs: [],
generatedSkills: [],
mcpServers: { fresh: { command: "new" } },
}
await writeCodexBundle(codexRoot, bundle)
const config = await fs.readFile(configPath, "utf8")
expect(config).not.toContain("# BEGIN compound-plugin Claude Code MCP")
expect(config).toContain("# BEGIN Compound Engineering plugin MCP")
expect(config).not.toContain("[mcp_servers.old]")
expect(config).toContain("[mcp_servers.fresh]")
expect(config).toContain("[user]")
})
test("migrates unmarked legacy format (# Generated by compound-plugin)", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-unmarked-"))
const codexRoot = path.join(tempRoot, ".codex")
const configPath = path.join(codexRoot, "config.toml")
// Simulate old writer output: entire file was just the generated config
await fs.mkdir(codexRoot, { recursive: true })
await fs.writeFile(configPath, [
"# Generated by compound-plugin",
"",
"[mcp_servers.old]",
'command = "old"',
"",
].join("\n"))
const bundle: CodexBundle = {
prompts: [],
skillDirs: [],
generatedSkills: [],
mcpServers: { fresh: { command: "new" } },
}
await writeCodexBundle(codexRoot, bundle)
const config = await fs.readFile(configPath, "utf8")
expect(config).not.toContain("# Generated by compound-plugin")
expect(config).not.toContain("[mcp_servers.old]")
expect(config).toContain("# BEGIN Compound Engineering plugin MCP")
expect(config).toContain("[mcp_servers.fresh]")
// Should have exactly one BEGIN marker (no duplication)
expect(config.match(/# BEGIN Compound Engineering plugin MCP/g)?.length).toBe(1)
})
test("strips stale managed block when plugin has no MCP servers", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-stale-"))
const codexRoot = path.join(tempRoot, ".codex")
const configPath = path.join(codexRoot, "config.toml")
await fs.mkdir(codexRoot, { recursive: true })
await fs.writeFile(configPath, [
"[user]",
'model = "gpt-4.1"',
"",
"# BEGIN Compound Engineering plugin MCP -- do not edit this block",
"[mcp_servers.stale]",
'command = "should-be-removed"',
"# END Compound Engineering plugin MCP",
].join("\n"))
await writeCodexBundle(codexRoot, { prompts: [], skillDirs: [], generatedSkills: [] })
const config = await fs.readFile(configPath, "utf8")
expect(config).not.toContain("mcp_servers.stale")
expect(config).not.toContain("# BEGIN Compound Engineering")
expect(config).toContain("[user]")
})
test("transforms copied SKILL.md files using Codex invocation targets", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-skill-transform-"))
const sourceSkillDir = path.join(tempRoot, "source-skill")
@@ -265,3 +387,142 @@ Workflow handoff:
expect(installedSkill).not.toContain("https://prompts:www.proofeditor.ai")
})
})
describe("renderCodexConfig", () => {
test("skips servers with neither command nor url", () => {
const result = renderCodexConfig({ broken: {} })
expect(result).toBeNull()
})
test("skips malformed servers but keeps valid ones", () => {
const result = renderCodexConfig({
valid: { command: "echo" },
broken: {},
alsoValid: { url: "https://example.com/mcp" },
})
expect(result).not.toBeNull()
expect(result).toContain("[mcp_servers.valid]")
expect(result).toContain("[mcp_servers.alsoValid]")
expect(result).not.toContain("[mcp_servers.broken]")
})
test("returns null for empty or undefined input", () => {
expect(renderCodexConfig(undefined)).toBeNull()
expect(renderCodexConfig({})).toBeNull()
})
})
describe("mergeCodexConfig", () => {
test("returns managed block when no existing content", () => {
const result = mergeCodexConfig("", "[mcp_servers.test]\ncommand = \"echo\"")
expect(result).toContain("# BEGIN Compound Engineering plugin MCP")
expect(result).toContain("[mcp_servers.test]")
expect(result).toContain("# END Compound Engineering plugin MCP")
})
test("preserves user content and replaces managed block", () => {
const existing = [
"[user]",
'model = "gpt-4.1"',
"",
"# BEGIN Compound Engineering plugin MCP -- do not edit this block",
"[mcp_servers.old]",
'command = "old"',
"# END Compound Engineering plugin MCP",
"",
"[after]",
'key = "value"',
].join("\n")
const result = mergeCodexConfig(existing, "[mcp_servers.new]\ncommand = \"new\"")!
expect(result).toContain("[user]")
expect(result).toContain("[after]")
expect(result).not.toContain("[mcp_servers.old]")
expect(result).toContain("[mcp_servers.new]")
})
test("strips previous-generation markers", () => {
const existing = [
"[user]",
'model = "gpt-4.1"',
"",
"# BEGIN compound-plugin Claude Code MCP",
"[mcp_servers.old]",
'command = "old"',
"# END compound-plugin Claude Code MCP",
].join("\n")
const result = mergeCodexConfig(existing, "[mcp_servers.new]\ncommand = \"new\"")!
expect(result).not.toContain("# BEGIN compound-plugin Claude Code MCP")
expect(result).not.toContain("[mcp_servers.old]")
expect(result).toContain("# BEGIN Compound Engineering plugin MCP")
expect(result).toContain("[mcp_servers.new]")
})
test("returns cleaned content (no block) when mcpToml is null", () => {
const existing = [
"[user]",
'model = "gpt-4.1"',
"",
"# BEGIN Compound Engineering plugin MCP -- do not edit this block",
"[mcp_servers.stale]",
'command = "stale"',
"# END Compound Engineering plugin MCP",
].join("\n")
const result = mergeCodexConfig(existing, null)!
expect(result).toContain("[user]")
expect(result).not.toContain("mcp_servers.stale")
expect(result).not.toContain("# BEGIN")
})
test("strips unmarked legacy format (# Generated by compound-plugin)", () => {
const existing = [
"# Generated by compound-plugin",
"",
"[mcp_servers.old]",
'command = "old"',
"",
].join("\n")
const result = mergeCodexConfig(existing, "[mcp_servers.new]\ncommand = \"new\"")!
expect(result).not.toContain("# Generated by compound-plugin")
expect(result).not.toContain("[mcp_servers.old]")
expect(result).toContain("# BEGIN Compound Engineering plugin MCP")
expect(result).toContain("[mcp_servers.new]")
})
test("preserves user config before unmarked legacy format", () => {
const existing = [
"[user]",
'model = "gpt-4.1"',
"",
"# Generated by compound-plugin",
"",
"[mcp_servers.old]",
'command = "old"',
].join("\n")
const result = mergeCodexConfig(existing, "[mcp_servers.new]\ncommand = \"new\"")!
expect(result).toContain("[user]")
expect(result).not.toContain("# Generated by compound-plugin")
expect(result).not.toContain("[mcp_servers.old]")
expect(result).toContain("[mcp_servers.new]")
})
test("returns null when no existing content and no mcpToml", () => {
expect(mergeCodexConfig("", null)).toBeNull()
})
test("returns empty string when file was only a managed block and mcpToml is null", () => {
const existing = [
"# BEGIN Compound Engineering plugin MCP -- do not edit this block",
"[mcp_servers.stale]",
'command = "stale"',
"# END Compound Engineering plugin MCP",
].join("\n")
const result = mergeCodexConfig(existing, null)
expect(result).toBe("")
})
})

View File

@@ -203,6 +203,174 @@ Run these research agents:
expect(installedSkill).not.toContain("Task compound-engineering:")
})
test("removes stale plugin MCP servers on re-install", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "copilot-converge-"))
const githubRoot = path.join(tempRoot, ".github")
const bundle1: CopilotBundle = {
agents: [],
generatedSkills: [],
skillDirs: [],
mcpConfig: { old: { type: "local", command: "old-server", tools: ["*"] } },
}
const bundle2: CopilotBundle = {
agents: [],
generatedSkills: [],
skillDirs: [],
mcpConfig: { fresh: { type: "local", command: "new-server", tools: ["*"] } },
}
await writeCopilotBundle(tempRoot, bundle1)
await writeCopilotBundle(tempRoot, bundle2)
const result = JSON.parse(await fs.readFile(path.join(githubRoot, "copilot-mcp-config.json"), "utf8"))
expect(result.mcpServers.fresh).toBeDefined()
expect(result.mcpServers.old).toBeUndefined()
})
test("cleans up all plugin MCP servers when bundle has none", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "copilot-zero-"))
const githubRoot = path.join(tempRoot, ".github")
const bundle1: CopilotBundle = {
agents: [],
generatedSkills: [],
skillDirs: [],
mcpConfig: { old: { type: "local", command: "old-server", tools: ["*"] } },
}
const bundle2: CopilotBundle = {
agents: [],
generatedSkills: [],
skillDirs: [],
// No mcpConfig
}
await writeCopilotBundle(tempRoot, bundle1)
await writeCopilotBundle(tempRoot, bundle2)
const result = JSON.parse(await fs.readFile(path.join(githubRoot, "copilot-mcp-config.json"), "utf8"))
expect(result.mcpServers.old).toBeUndefined()
expect(result._compound_managed_mcp).toEqual([])
})
test("does not prune untracked user config when plugin has zero MCP servers", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "copilot-untracked-"))
const githubRoot = path.join(tempRoot, ".github")
await fs.mkdir(githubRoot, { recursive: true })
// Pre-existing user config with no tracking key (never had the plugin before)
await fs.writeFile(
path.join(githubRoot, "copilot-mcp-config.json"),
JSON.stringify({
mcpServers: { "user-tool": { type: "local", command: "my-tool", tools: ["*"] } },
}),
)
// Plugin installs with zero MCP servers
await writeCopilotBundle(githubRoot, {
agents: [],
generatedSkills: [],
skillDirs: [],
})
const result = JSON.parse(await fs.readFile(path.join(githubRoot, "copilot-mcp-config.json"), "utf8"))
expect(result.mcpServers["user-tool"]).toBeDefined()
expect(result._compound_managed_mcp).toEqual([])
})
test("preserves user servers across zero-MCP-then-MCP round trip", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "copilot-roundtrip-"))
const githubRoot = path.join(tempRoot, ".github")
const mcpPath = path.join(githubRoot, "copilot-mcp-config.json")
// 1. Install with plugin MCP
await writeCopilotBundle(tempRoot, {
agents: [], generatedSkills: [], skillDirs: [],
mcpConfig: { plugin: { type: "local", command: "plugin-server", tools: ["*"] } },
})
// 2. User adds their own server
const afterInstall = JSON.parse(await fs.readFile(mcpPath, "utf8"))
afterInstall.mcpServers["user-tool"] = { type: "local", command: "my-tool", tools: ["*"] }
await fs.writeFile(mcpPath, JSON.stringify(afterInstall))
// 3. Install with zero plugin MCP
await writeCopilotBundle(tempRoot, {
agents: [], generatedSkills: [], skillDirs: [],
})
// 4. Install with plugin MCP again
await writeCopilotBundle(tempRoot, {
agents: [], generatedSkills: [], skillDirs: [],
mcpConfig: { new_plugin: { type: "local", command: "new-plugin", tools: ["*"] } },
})
const result = JSON.parse(await fs.readFile(mcpPath, "utf8"))
expect(result.mcpServers["user-tool"]).toBeDefined()
expect(result.mcpServers.new_plugin).toBeDefined()
expect(result.mcpServers.plugin).toBeUndefined()
})
test("preserves user-added MCP servers across re-installs", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "copilot-user-mcp-"))
const githubRoot = path.join(tempRoot, ".github")
await fs.mkdir(githubRoot, { recursive: true })
// User has their own MCP server alongside plugin-managed ones (tracking key present)
await fs.writeFile(
path.join(githubRoot, "copilot-mcp-config.json"),
JSON.stringify({
mcpServers: { "user-tool": { type: "local", command: "my-tool", tools: ["*"] } },
_compound_managed_mcp: [],
}),
)
const bundle: CopilotBundle = {
agents: [],
generatedSkills: [],
skillDirs: [],
mcpConfig: { plugin: { type: "local", command: "plugin-server", tools: ["*"] } },
}
await writeCopilotBundle(githubRoot, bundle)
const result = JSON.parse(await fs.readFile(path.join(githubRoot, "copilot-mcp-config.json"), "utf8"))
expect(result.mcpServers["user-tool"]).toBeDefined()
expect(result.mcpServers.plugin).toBeDefined()
})
test("prunes stale servers from legacy config without tracking key", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "copilot-legacy-"))
const githubRoot = path.join(tempRoot, ".github")
await fs.mkdir(githubRoot, { recursive: true })
// Simulate old writer output: has mcpServers but no _compound_managed_mcp
await fs.writeFile(
path.join(githubRoot, "copilot-mcp-config.json"),
JSON.stringify({
mcpServers: {
old: { type: "local", command: "old-server", tools: ["*"] },
renamed: { type: "local", command: "renamed-server", tools: ["*"] },
},
}),
)
const bundle: CopilotBundle = {
agents: [],
generatedSkills: [],
skillDirs: [],
mcpConfig: { fresh: { type: "local", command: "new-server", tools: ["*"] } },
}
await writeCopilotBundle(githubRoot, bundle)
const result = JSON.parse(await fs.readFile(path.join(githubRoot, "copilot-mcp-config.json"), "utf8"))
expect(result.mcpServers.fresh).toBeDefined()
expect(result.mcpServers.old).toBeUndefined()
expect(result.mcpServers.renamed).toBeUndefined()
expect(result._compound_managed_mcp).toEqual(["fresh"])
})
test("creates skill directories with SKILL.md", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "copilot-genskill-"))
const bundle: CopilotBundle = {

182
tests/qwen-writer.test.ts Normal file
View File

@@ -0,0 +1,182 @@
import { describe, expect, test } from "bun:test"
import { promises as fs } from "fs"
import os from "os"
import path from "path"
import { writeQwenBundle } from "../src/targets/qwen"
import type { QwenBundle } from "../src/types/qwen"
function makeBundle(mcpServers?: Record<string, { command: string }>): QwenBundle {
return {
config: {
name: "test-plugin",
version: "1.0.0",
commands: "commands",
skills: "skills",
agents: "agents",
mcpServers,
},
agents: [],
commandFiles: [],
skillDirs: [],
}
}
describe("writeQwenBundle", () => {
test("removes stale plugin MCP servers on re-install", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qwen-converge-"))
await writeQwenBundle(tempRoot, makeBundle({ old: { command: "old-server" } }))
await writeQwenBundle(tempRoot, makeBundle({ fresh: { command: "new-server" } }))
const result = JSON.parse(await fs.readFile(path.join(tempRoot, "qwen-extension.json"), "utf8"))
expect(result.mcpServers.fresh).toBeDefined()
expect(result.mcpServers.old).toBeUndefined()
})
test("preserves user-added MCP servers across re-installs", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qwen-user-mcp-"))
// User has their own MCP server alongside plugin-managed ones (tracking key present)
await fs.writeFile(
path.join(tempRoot, "qwen-extension.json"),
JSON.stringify({
name: "user-project",
mcpServers: { "user-tool": { command: "my-tool" } },
_compound_managed_mcp: [],
}),
)
await writeQwenBundle(tempRoot, makeBundle({ plugin: { command: "plugin-server" } }))
const result = JSON.parse(await fs.readFile(path.join(tempRoot, "qwen-extension.json"), "utf8"))
expect(result.mcpServers["user-tool"]).toBeDefined()
expect(result.mcpServers.plugin).toBeDefined()
})
test("preserves unknown top-level keys from existing config", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qwen-preserve-"))
await fs.writeFile(
path.join(tempRoot, "qwen-extension.json"),
JSON.stringify({ name: "user-project", customField: "should-survive" }),
)
await writeQwenBundle(tempRoot, makeBundle({ plugin: { command: "p" } }))
const result = JSON.parse(await fs.readFile(path.join(tempRoot, "qwen-extension.json"), "utf8"))
expect(result.customField).toBe("should-survive")
// Tracking key should be written so future installs can prune stale plugin keys
expect(result._compound_managed_keys).toBeInstanceOf(Array)
expect(result._compound_managed_keys).not.toContain("customField")
})
test("prunes stale servers from legacy config without tracking key", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qwen-legacy-"))
// Simulate old writer output: has mcpServers but no _compound_managed_mcp
await fs.writeFile(
path.join(tempRoot, "qwen-extension.json"),
JSON.stringify({
name: "old-project",
mcpServers: { old: { command: "old-server" }, renamed: { command: "renamed-server" } },
}),
)
await writeQwenBundle(tempRoot, makeBundle({ fresh: { command: "new-server" } }))
const result = JSON.parse(await fs.readFile(path.join(tempRoot, "qwen-extension.json"), "utf8"))
expect(result.mcpServers.fresh).toBeDefined()
expect(result.mcpServers.old).toBeUndefined()
expect(result.mcpServers.renamed).toBeUndefined()
expect(result._compound_managed_mcp).toEqual(["fresh"])
})
test("does not prune untracked user config when plugin has zero MCP servers", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qwen-untracked-"))
// Pre-existing user config with no tracking key (never had the plugin before)
await fs.writeFile(
path.join(tempRoot, "qwen-extension.json"),
JSON.stringify({
name: "user-project",
mcpServers: { "user-tool": { command: "my-tool" } },
}),
)
// Plugin installs with zero MCP servers
await writeQwenBundle(tempRoot, makeBundle())
const result = JSON.parse(await fs.readFile(path.join(tempRoot, "qwen-extension.json"), "utf8"))
expect(result.mcpServers["user-tool"]).toBeDefined()
expect(result._compound_managed_mcp).toEqual([])
})
test("cleans up all plugin MCP servers when bundle has none", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qwen-zero-"))
await writeQwenBundle(tempRoot, makeBundle({ old: { command: "old-server" } }))
await writeQwenBundle(tempRoot, makeBundle())
const result = JSON.parse(await fs.readFile(path.join(tempRoot, "qwen-extension.json"), "utf8"))
expect(result.mcpServers).toBeUndefined()
expect(result._compound_managed_mcp).toEqual([])
})
test("preserves user servers across zero-MCP-then-MCP round trip", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qwen-roundtrip-"))
// 1. Install with plugin MCP
await writeQwenBundle(tempRoot, makeBundle({ plugin: { command: "plugin-server" } }))
// 2. User adds their own server (with tracking key present)
const configPath = path.join(tempRoot, "qwen-extension.json")
const afterInstall = JSON.parse(await fs.readFile(configPath, "utf8"))
afterInstall.mcpServers["user-tool"] = { command: "my-tool" }
await fs.writeFile(configPath, JSON.stringify(afterInstall))
// 3. Install with zero plugin MCP
await writeQwenBundle(tempRoot, makeBundle())
// 4. Install with plugin MCP again
await writeQwenBundle(tempRoot, makeBundle({ new_plugin: { command: "new-plugin" } }))
const result = JSON.parse(await fs.readFile(configPath, "utf8"))
expect(result.mcpServers["user-tool"]).toBeDefined()
expect(result.mcpServers.new_plugin).toBeDefined()
expect(result.mcpServers.plugin).toBeUndefined()
})
test("prunes stale top-level plugin keys when incoming config drops them", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qwen-stale-keys-"))
// First install with settings
const bundleWithSettings: QwenBundle = {
config: {
name: "test-plugin",
version: "1.0.0",
commands: "commands",
skills: "skills",
agents: "agents",
settings: [{ name: "api-key", description: "API key", envVar: "API_KEY", sensitive: true }],
},
agents: [],
commandFiles: [],
skillDirs: [],
}
await writeQwenBundle(tempRoot, bundleWithSettings)
// User adds their own top-level key
const configPath = path.join(tempRoot, "qwen-extension.json")
const afterInstall = JSON.parse(await fs.readFile(configPath, "utf8"))
afterInstall.userCustom = "should-survive"
await fs.writeFile(configPath, JSON.stringify(afterInstall))
// Second install without settings
await writeQwenBundle(tempRoot, makeBundle())
const result = JSON.parse(await fs.readFile(configPath, "utf8"))
expect(result.settings).toBeUndefined()
expect(result.userCustom).toBe("should-survive")
expect(result.name).toBe("test-plugin")
})
})

View File

@@ -56,9 +56,36 @@ describe("syncToCodex", () => {
expect(content).toContain("[mcp_servers.remote]")
expect(content).toContain("url = \"https://example.com/mcp\"")
expect(content).toContain("http_headers")
expect(content.match(/# BEGIN compound-plugin Claude Code MCP/g)?.length).toBe(1)
// Old markers should be replaced with new ones
expect(content).not.toContain("# BEGIN compound-plugin Claude Code MCP")
expect(content.match(/# BEGIN Compound Engineering plugin MCP/g)?.length).toBe(1)
const perms = (await fs.stat(configPath)).mode & 0o777
expect(perms).toBe(0o600)
})
test("cleans up stale managed block when syncing with zero MCP servers", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-codex-zero-"))
const fixtureSkillDir = path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one")
const configPath = path.join(tempRoot, "config.toml")
// First sync with MCP servers
const configWithServers: ClaudeHomeConfig = {
skills: [{ name: "skill-one", sourceDir: fixtureSkillDir, skillPath: path.join(fixtureSkillDir, "SKILL.md") }],
mcpServers: { old: { command: "old-server" } },
}
await syncToCodex(configWithServers, tempRoot)
expect(await fs.readFile(configPath, "utf8")).toContain("[mcp_servers.old]")
// Second sync with zero MCP servers
const configEmpty: ClaudeHomeConfig = {
skills: [{ name: "skill-one", sourceDir: fixtureSkillDir, skillPath: path.join(fixtureSkillDir, "SKILL.md") }],
mcpServers: {},
}
await syncToCodex(configEmpty, tempRoot)
const content = await fs.readFile(configPath, "utf8")
expect(content).not.toContain("[mcp_servers.old]")
expect(content).not.toContain("# BEGIN")
})
})