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
2 changes: 2 additions & 0 deletions __tests__/domains/config/merger/conflict-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ function createMergeResult(): MergeResult {
hooksAdded: 0,
hooksPreserved: 0,
hooksSkipped: 0,
hooksRemoved: 0,
mcpServersPreserved: 0,
mcpServersSkipped: 0,
mcpServersRemoved: 0,
conflictsDetected: [],
newlyInstalledHooks: [],
newlyInstalledServers: [],
Expand Down
150 changes: 150 additions & 0 deletions __tests__/domains/config/merger/merge-engine.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { describe, expect, it } from "bun:test";
import { mergeHooks, mergeMcp, mergeSettings } from "@/domains/config/merger/merge-engine.js";
import type { HookEntry, MergeResult, SettingsJson } from "@/domains/config/merger/types.js";

function createMergeResult(): MergeResult {
return {
merged: {},
hooksAdded: 0,
hooksPreserved: 0,
hooksSkipped: 0,
hooksRemoved: 0,
mcpServersPreserved: 0,
mcpServersSkipped: 0,
mcpServersRemoved: 0,
conflictsDetected: [],
newlyInstalledHooks: [],
newlyInstalledServers: [],
hooksByOrigin: new Map(),
};
}

describe("merge-engine deprecation removal", () => {
describe("mergeHooks removes deprecated hooks", () => {
it("should remove hook in installed but not in source", () => {
const sourceHooks: Record<string, HookEntry[]> = {
SessionStart: [{ type: "command", command: "node new-hook.js" }],
};
const destHooks: Record<string, HookEntry[]> = {
SessionStart: [
{ type: "command", command: "node new-hook.js" },
{ type: "command", command: "node deprecated-hook.js" },
],
};
const result = createMergeResult();

const merged = mergeHooks(sourceHooks, destHooks, result, {
installedSettings: {
hooks: ["node deprecated-hook.js"],
},
});

// deprecated-hook.js should be removed
expect(merged.SessionStart).toHaveLength(1);
expect(result.hooksRemoved).toBe(1);
expect(result.removedHooks).toContain("node deprecated-hook.js");
});

it("should preserve user-added hook not in installed", () => {
const sourceHooks: Record<string, HookEntry[]> = {
SessionStart: [{ type: "command", command: "node ck-hook.js" }],
};
const destHooks: Record<string, HookEntry[]> = {
SessionStart: [
{ type: "command", command: "node ck-hook.js" },
{ type: "command", command: "node user-hook.js" },
],
};
const result = createMergeResult();

const merged = mergeHooks(sourceHooks, destHooks, result, {
installedSettings: {
hooks: ["node ck-hook.js"], // user-hook.js not in installed = user added it
},
});

// user-hook.js should be preserved
expect(merged.SessionStart).toHaveLength(2);
expect(result.hooksRemoved).toBe(0);
});

it("should not remove anything with empty installedSettings", () => {
const sourceHooks: Record<string, HookEntry[]> = {
SessionStart: [{ type: "command", command: "node new-hook.js" }],
};
const destHooks: Record<string, HookEntry[]> = {
SessionStart: [{ type: "command", command: "node old-hook.js" }],
};
const result = createMergeResult();

const merged = mergeHooks(sourceHooks, destHooks, result, {
installedSettings: { hooks: [] },
});

// old-hook.js preserved (fresh install scenario)
expect(merged.SessionStart).toHaveLength(2);
expect(result.hooksRemoved).toBe(0);
});
});

describe("mergeMcp removes deprecated servers", () => {
it("should remove server in installed but not in source", () => {
const sourceMcp: SettingsJson["mcp"] = {
servers: { "new-server": { command: "npx new" } },
};
const destMcp: SettingsJson["mcp"] = {
servers: {
"new-server": { command: "npx new" },
"deprecated-server": { command: "npx old" },
},
};
const result = createMergeResult();

const merged = mergeMcp(sourceMcp, destMcp, result, {
installedSettings: {
mcpServers: ["deprecated-server"],
},
});

expect(merged?.servers).not.toHaveProperty("deprecated-server");
expect(merged?.servers).toHaveProperty("new-server");
expect(result.mcpServersRemoved).toBe(1);
expect(result.removedMcpServers).toContain("deprecated-server");
});

it("should preserve user-added server not in installed", () => {
const sourceMcp: SettingsJson["mcp"] = {
servers: { "ck-server": { command: "npx ck" } },
};
const destMcp: SettingsJson["mcp"] = {
servers: {
"ck-server": { command: "npx ck" },
"user-server": { command: "npx user" },
},
};
const result = createMergeResult();

const merged = mergeMcp(sourceMcp, destMcp, result, {
installedSettings: {
mcpServers: ["ck-server"], // user-server not in installed
},
});

expect(merged?.servers).toHaveProperty("ck-server");
expect(merged?.servers).toHaveProperty("user-server");
expect(result.mcpServersRemoved).toBe(0);
});
});

describe("mergeSettings initializes removal counters", () => {
it("should initialize hooksRemoved and mcpServersRemoved to 0", () => {
const source: SettingsJson = {};
const dest: SettingsJson = {};

const result = mergeSettings(source, dest);

expect(result.hooksRemoved).toBe(0);
expect(result.mcpServersRemoved).toBe(0);
});
});
});
216 changes: 216 additions & 0 deletions src/__tests__/services/transformers/content-transformer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/**
* Tests for content-transformer.ts
*
* Verifies that command references are properly transformed
* when --prefix flag is used.
*/

import { describe, expect, it } from "bun:test";
import { transformCommandContent } from "@/services/transformers/commands-prefix/content-transformer.js";

describe("transformCommandContent", () => {
describe("basic command transformations", () => {
it("transforms /plan: to /ck:plan:", () => {
const input = "Execute `/plan:fast` to create a plan";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe("Execute `/ck:plan:fast` to create a plan");
expect(changes).toBe(1);
});

it("transforms /fix: to /ck:fix:", () => {
const input = "Use `/fix:types` for TypeScript errors";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe("Use `/ck:fix:types` for TypeScript errors");
expect(changes).toBe(1);
});

it("transforms /code: to /ck:code:", () => {
const input = "Run `/code:auto` to implement";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe("Run `/ck:code:auto` to implement");
expect(changes).toBe(1);
});

it("transforms /review: to /ck:review:", () => {
const input = "Use `/review:codebase` for analysis";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe("Use `/ck:review:codebase` for analysis");
expect(changes).toBe(1);
});

it("transforms /cook: to /ck:cook:", () => {
const input = "Try `/cook:auto` for quick implementation";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe("Try `/ck:cook:auto` for quick implementation");
expect(changes).toBe(1);
});

it("transforms /brainstorm to /ck:brainstorm", () => {
const input = "Start with `/brainstorm` to explore options";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe("Start with `/ck:brainstorm` to explore options");
expect(changes).toBe(1);
});

it("transforms /test to /ck:test", () => {
const input = "Run `/test` to verify";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe("Run `/ck:test` to verify");
expect(changes).toBe(1);
});

it("transforms /preview to /ck:preview", () => {
const input = "Use `/preview` to see changes";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe("Use `/ck:preview` to see changes");
expect(changes).toBe(1);
});

it("transforms /kanban to /ck:kanban", () => {
const input = "Open `/kanban` dashboard";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe("Open `/ck:kanban` dashboard");
expect(changes).toBe(1);
});

it("transforms /journal to /ck:journal", () => {
const input = "Write with `/journal`";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe("Write with `/ck:journal`");
expect(changes).toBe(1);
});

it("transforms /debug to /ck:debug", () => {
const input = "Use `/debug` for more info";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe("Use `/ck:debug` for more info");
expect(changes).toBe(1);
});

it("transforms /watzup to /ck:watzup", () => {
const input = "Check `/watzup` for changes";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe("Check `/ck:watzup` for changes");
expect(changes).toBe(1);
});
});

describe("multiple transformations", () => {
it("transforms multiple commands in same content", () => {
const input = "Use `/plan:fast` then `/code:auto` then `/fix:types`";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe("Use `/ck:plan:fast` then `/ck:code:auto` then `/ck:fix:types`");
expect(changes).toBe(3);
});

it("transforms commands across multiple lines", () => {
const input = `1. Run /plan:hard
2. Execute /code:parallel
3. Verify with /review:codebase`;
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe(`1. Run /ck:plan:hard
2. Execute /ck:code:parallel
3. Verify with /ck:review:codebase`);
expect(changes).toBe(3);
});
});

describe("edge cases - should NOT transform", () => {
it("does not transform URLs containing command-like paths", () => {
const input = "Visit https://example.com/plan:something";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe(input);
expect(changes).toBe(0);
});

it("does not transform already-prefixed commands", () => {
const input = "Use `/ck:plan:fast` (already prefixed)";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe(input);
expect(changes).toBe(0);
});

it("does not transform word boundaries incorrectly", () => {
const input = "The planning process uses /plan:fast";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe("The planning process uses /ck:plan:fast");
expect(changes).toBe(1);
});

it("does not transform partial matches in middle of words", () => {
const input = "This is someplan:thing";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe(input);
expect(changes).toBe(0);
});
});

describe("context preservation", () => {
it("preserves backtick wrapping", () => {
const input = "Run ``/plan:fast`` command";
const { transformed } = transformCommandContent(input);
expect(transformed).toContain("/ck:plan:fast");
});

it("preserves markdown formatting", () => {
const input = "**Important:** Use `/fix:hard` for complex issues";
const { transformed } = transformCommandContent(input);
expect(transformed).toBe("**Important:** Use `/ck:fix:hard` for complex issues");
});

it("handles commands at start of line", () => {
const input = "/plan:fast is the command";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe("/ck:plan:fast is the command");
expect(changes).toBe(1);
});

it("handles commands at end of line", () => {
const input = "Use this command: /brainstorm";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe("Use this command: /ck:brainstorm");
expect(changes).toBe(1);
});
});

describe("real-world content examples", () => {
it("transforms markdown file content", () => {
const input = `## Workflow

- Decide to use \`/plan:fast\` or \`/plan:hard\` SlashCommands based on the complexity.
- Execute SlashCommand: \`/plan:fast <detailed-instructions-prompt>\` or \`/plan:hard <detailed-instructions-prompt>\``;

const { transformed, changes } = transformCommandContent(input);
expect(transformed).toContain("/ck:plan:fast");
expect(transformed).toContain("/ck:plan:hard");
expect(changes).toBe(4);
});

it("transforms agent definition content", () => {
const input =
"Use the **Skill tool** to invoke `/plan:fast` or `/plan:hard` SlashCommand based on complexity.";

const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe(
"Use the **Skill tool** to invoke `/ck:plan:fast` or `/ck:plan:hard` SlashCommand based on complexity.",
);
expect(changes).toBe(2);
});
});

describe("no changes needed", () => {
it("returns 0 changes for content without commands", () => {
const input = "This is regular content without any slash commands";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe(input);
expect(changes).toBe(0);
});

it("returns 0 changes for empty content", () => {
const input = "";
const { transformed, changes } = transformCommandContent(input);
expect(transformed).toBe("");
expect(changes).toBe(0);
});
});
});
Loading