348 lines
12 KiB
TypeScript
348 lines
12 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
import { mkdtempSync, mkdirSync, rmSync, readFileSync, existsSync } from "node:fs";
|
|
import { tmpdir, platform } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
// Connect adapters for Qwen Code, Antigravity, and Kiro. Each writes
|
|
// the canonical MCP block (npx @agentmemory/mcp + env defaults) into
|
|
// the agent's documented config path.
|
|
|
|
function freshHome(): string {
|
|
return mkdtempSync(join(tmpdir(), "am-connect-"));
|
|
}
|
|
|
|
describe("connect: Qwen Code", () => {
|
|
let home: string;
|
|
const ORIG = process.env["HOME"];
|
|
beforeEach(() => {
|
|
home = freshHome();
|
|
vi.resetModules();
|
|
process.env["HOME"] = home;
|
|
});
|
|
afterEach(() => {
|
|
process.env["HOME"] = ORIG;
|
|
rmSync(home, { recursive: true, force: true });
|
|
});
|
|
|
|
it("does not detect when ~/.qwen/ is absent", async () => {
|
|
const { adapter } = await import("../src/cli/connect/qwen.js");
|
|
expect(adapter.detect()).toBe(false);
|
|
});
|
|
|
|
it("writes mcpServers.agentmemory to ~/.qwen/settings.json", async () => {
|
|
mkdirSync(join(home, ".qwen"), { recursive: true });
|
|
const { adapter } = await import("../src/cli/connect/qwen.js");
|
|
expect(adapter.detect()).toBe(true);
|
|
const result = await adapter.install({ dryRun: false, force: false });
|
|
expect(result.kind).toBe("installed");
|
|
const cfg = JSON.parse(
|
|
readFileSync(join(home, ".qwen", "settings.json"), "utf-8"),
|
|
);
|
|
expect(cfg.mcpServers.agentmemory.command).toBe("npx");
|
|
expect(cfg.mcpServers.agentmemory.args).toContain("@agentmemory/mcp");
|
|
expect(cfg.mcpServers.agentmemory.env.AGENTMEMORY_URL).toMatch(
|
|
/\$\{AGENTMEMORY_URL:-/,
|
|
);
|
|
expect(cfg.mcpServers.agentmemory.env.AGENTMEMORY_TOOLS).toMatch(
|
|
/\$\{AGENTMEMORY_TOOLS:-all\}/,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("connect: Antigravity", () => {
|
|
let home: string;
|
|
const ORIG = process.env["HOME"];
|
|
beforeEach(() => {
|
|
home = freshHome();
|
|
vi.resetModules();
|
|
process.env["HOME"] = home;
|
|
});
|
|
afterEach(() => {
|
|
process.env["HOME"] = ORIG;
|
|
rmSync(home, { recursive: true, force: true });
|
|
});
|
|
|
|
it("writes mcpServers.agentmemory to the platform-specific config path", async () => {
|
|
const isMac = platform() === "darwin";
|
|
const userDir = isMac
|
|
? join(home, "Library", "Application Support", "Antigravity", "User")
|
|
: join(home, ".config", "Antigravity", "User");
|
|
mkdirSync(userDir, { recursive: true });
|
|
const { adapter } = await import("../src/cli/connect/antigravity.js");
|
|
expect(adapter.detect()).toBe(true);
|
|
const result = await adapter.install({ dryRun: false, force: false });
|
|
expect(result.kind).toBe("installed");
|
|
const cfg = JSON.parse(
|
|
readFileSync(join(userDir, "mcp_config.json"), "utf-8"),
|
|
);
|
|
expect(cfg.mcpServers.agentmemory.command).toBe("npx");
|
|
expect(cfg.mcpServers.agentmemory.env.AGENTMEMORY_URL).toMatch(
|
|
/\$\{AGENTMEMORY_URL:-/,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("connect: Kiro", () => {
|
|
let home: string;
|
|
const ORIG = process.env["HOME"];
|
|
beforeEach(() => {
|
|
home = freshHome();
|
|
vi.resetModules();
|
|
process.env["HOME"] = home;
|
|
});
|
|
afterEach(() => {
|
|
process.env["HOME"] = ORIG;
|
|
rmSync(home, { recursive: true, force: true });
|
|
});
|
|
|
|
it("does not detect when ~/.kiro/ is absent", async () => {
|
|
const { adapter } = await import("../src/cli/connect/kiro.js");
|
|
expect(adapter.detect()).toBe(false);
|
|
});
|
|
|
|
it("writes mcpServers.agentmemory to ~/.kiro/settings/mcp.json", async () => {
|
|
mkdirSync(join(home, ".kiro"), { recursive: true });
|
|
const { adapter } = await import("../src/cli/connect/kiro.js");
|
|
expect(adapter.detect()).toBe(true);
|
|
const result = await adapter.install({ dryRun: false, force: false });
|
|
expect(result.kind).toBe("installed");
|
|
const cfgPath = join(home, ".kiro", "settings", "mcp.json");
|
|
expect(existsSync(cfgPath)).toBe(true);
|
|
const cfg = JSON.parse(readFileSync(cfgPath, "utf-8"));
|
|
expect(cfg.mcpServers.agentmemory.command).toBe("npx");
|
|
expect(cfg.mcpServers.agentmemory.args).toContain("@agentmemory/mcp");
|
|
});
|
|
});
|
|
|
|
describe("connect: Warp", () => {
|
|
let home: string;
|
|
const ORIG = process.env["HOME"];
|
|
beforeEach(() => {
|
|
home = freshHome();
|
|
vi.resetModules();
|
|
process.env["HOME"] = home;
|
|
});
|
|
afterEach(() => {
|
|
process.env["HOME"] = ORIG;
|
|
rmSync(home, { recursive: true, force: true });
|
|
});
|
|
|
|
it("does not detect when ~/.warp/ is absent", async () => {
|
|
const { adapter } = await import("../src/cli/connect/warp.js");
|
|
expect(adapter.detect()).toBe(false);
|
|
});
|
|
|
|
it("writes mcpServers.agentmemory to ~/.warp/.mcp.json", async () => {
|
|
mkdirSync(join(home, ".warp"), { recursive: true });
|
|
const { adapter } = await import("../src/cli/connect/warp.js");
|
|
expect(adapter.detect()).toBe(true);
|
|
const result = await adapter.install({ dryRun: false, force: false });
|
|
expect(result.kind).toBe("installed");
|
|
const cfgPath = join(home, ".warp", ".mcp.json");
|
|
expect(existsSync(cfgPath)).toBe(true);
|
|
const cfg = JSON.parse(readFileSync(cfgPath, "utf-8"));
|
|
expect(cfg.mcpServers.agentmemory.command).toBe("npx");
|
|
expect(cfg.mcpServers.agentmemory.args).toContain("@agentmemory/mcp");
|
|
expect(cfg.mcpServers.agentmemory.env.AGENTMEMORY_URL).toMatch(
|
|
/\$\{AGENTMEMORY_URL:-/,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("connect: Cline", () => {
|
|
let home: string;
|
|
const ORIG = process.env["HOME"];
|
|
beforeEach(() => {
|
|
home = freshHome();
|
|
vi.resetModules();
|
|
process.env["HOME"] = home;
|
|
});
|
|
afterEach(() => {
|
|
process.env["HOME"] = ORIG;
|
|
rmSync(home, { recursive: true, force: true });
|
|
});
|
|
|
|
it("does not detect when ~/.cline/ is absent", async () => {
|
|
const { adapter } = await import("../src/cli/connect/cline.js");
|
|
expect(adapter.detect()).toBe(false);
|
|
});
|
|
|
|
it("writes mcpServers.agentmemory to ~/.cline/mcp.json", async () => {
|
|
mkdirSync(join(home, ".cline"), { recursive: true });
|
|
const { adapter } = await import("../src/cli/connect/cline.js");
|
|
expect(adapter.detect()).toBe(true);
|
|
const result = await adapter.install({ dryRun: false, force: false });
|
|
expect(result.kind).toBe("installed");
|
|
const cfg = JSON.parse(
|
|
readFileSync(join(home, ".cline", "mcp.json"), "utf-8"),
|
|
);
|
|
expect(cfg.mcpServers.agentmemory.command).toBe("npx");
|
|
expect(cfg.mcpServers.agentmemory.args).toContain("@agentmemory/mcp");
|
|
});
|
|
});
|
|
|
|
describe("connect: Droid (Factory.ai)", () => {
|
|
let home: string;
|
|
const ORIG = process.env["HOME"];
|
|
beforeEach(() => {
|
|
home = freshHome();
|
|
vi.resetModules();
|
|
process.env["HOME"] = home;
|
|
});
|
|
afterEach(() => {
|
|
process.env["HOME"] = ORIG;
|
|
rmSync(home, { recursive: true, force: true });
|
|
});
|
|
|
|
it("does not detect when ~/.factory/ is absent", async () => {
|
|
const { adapter } = await import("../src/cli/connect/droid.js");
|
|
expect(adapter.detect()).toBe(false);
|
|
});
|
|
|
|
it("writes mcpServers.agentmemory to ~/.factory/mcp.json with type:stdio", async () => {
|
|
mkdirSync(join(home, ".factory"), { recursive: true });
|
|
const { adapter } = await import("../src/cli/connect/droid.js");
|
|
expect(adapter.detect()).toBe(true);
|
|
const result = await adapter.install({ dryRun: false, force: false });
|
|
expect(result.kind).toBe("installed");
|
|
const cfg = JSON.parse(
|
|
readFileSync(join(home, ".factory", "mcp.json"), "utf-8"),
|
|
);
|
|
expect(cfg.mcpServers.agentmemory.command).toBe("npx");
|
|
expect(cfg.mcpServers.agentmemory.args).toContain("@agentmemory/mcp");
|
|
// Droid requires `type` per its documented schema
|
|
expect(cfg.mcpServers.agentmemory.type).toBe("stdio");
|
|
});
|
|
});
|
|
|
|
describe("connect: Zed", () => {
|
|
let home: string;
|
|
const ORIG = process.env["HOME"];
|
|
beforeEach(() => {
|
|
home = freshHome();
|
|
vi.resetModules();
|
|
process.env["HOME"] = home;
|
|
});
|
|
afterEach(() => {
|
|
process.env["HOME"] = ORIG;
|
|
rmSync(home, { recursive: true, force: true });
|
|
});
|
|
|
|
it("does not detect when ~/.config/zed/ is absent", async () => {
|
|
const { adapter } = await import("../src/cli/connect/zed.js");
|
|
expect(adapter.detect()).toBe(false);
|
|
});
|
|
|
|
it("writes context_servers.agentmemory to ~/.config/zed/settings.json", async () => {
|
|
mkdirSync(join(home, ".config", "zed"), { recursive: true });
|
|
const { adapter } = await import("../src/cli/connect/zed.js");
|
|
expect(adapter.detect()).toBe(true);
|
|
const result = await adapter.install({ dryRun: false, force: false });
|
|
expect(result.kind).toBe("installed");
|
|
const cfg = JSON.parse(
|
|
readFileSync(join(home, ".config", "zed", "settings.json"), "utf-8"),
|
|
);
|
|
expect(cfg.context_servers.agentmemory.command).toBe("npx");
|
|
expect(cfg.context_servers.agentmemory.args).toContain("@agentmemory/mcp");
|
|
expect(cfg.mcpServers).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("connect: Continue.dev", () => {
|
|
let home: string;
|
|
const ORIG = process.env["HOME"];
|
|
beforeEach(() => {
|
|
home = freshHome();
|
|
vi.resetModules();
|
|
process.env["HOME"] = home;
|
|
});
|
|
afterEach(() => {
|
|
process.env["HOME"] = ORIG;
|
|
rmSync(home, { recursive: true, force: true });
|
|
});
|
|
|
|
it("does not detect when ~/.continue/ is absent", async () => {
|
|
const { adapter } = await import("../src/cli/connect/continue.js");
|
|
expect(adapter.detect()).toBe(false);
|
|
});
|
|
|
|
it("creates config.yaml from scratch when neither yaml nor json exists", async () => {
|
|
mkdirSync(join(home, ".continue"), { recursive: true });
|
|
const { adapter } = await import("../src/cli/connect/continue.js");
|
|
expect(adapter.detect()).toBe(true);
|
|
const result = await adapter.install({ dryRun: false, force: false });
|
|
expect(result.kind).toBe("installed");
|
|
const yamlPath = join(home, ".continue", "config.yaml");
|
|
expect(existsSync(yamlPath)).toBe(true);
|
|
expect(existsSync(join(home, ".continue", "config.json"))).toBe(false);
|
|
const yaml = readFileSync(yamlPath, "utf-8");
|
|
expect(yaml).toContain("mcpServers:");
|
|
expect(yaml).toContain("name: agentmemory");
|
|
expect(yaml).toContain("@agentmemory/mcp");
|
|
expect(yaml).toContain("AGENTMEMORY_URL");
|
|
});
|
|
|
|
it("modifies existing legacy config.json", async () => {
|
|
mkdirSync(join(home, ".continue"), { recursive: true });
|
|
const { writeFileSync } = await import("node:fs");
|
|
writeFileSync(
|
|
join(home, ".continue", "config.json"),
|
|
JSON.stringify({ models: [], mcpServers: [] }),
|
|
);
|
|
const { adapter } = await import("../src/cli/connect/continue.js");
|
|
const result = await adapter.install({ dryRun: false, force: false });
|
|
expect(result.kind).toBe("installed");
|
|
const cfg = JSON.parse(
|
|
readFileSync(join(home, ".continue", "config.json"), "utf-8"),
|
|
);
|
|
expect(Array.isArray(cfg.mcpServers)).toBe(true);
|
|
const entry = cfg.mcpServers.find(
|
|
(s: { name: string }) => s.name === "agentmemory",
|
|
);
|
|
expect(entry.command).toBe("npx");
|
|
expect(entry.args).toContain("@agentmemory/mcp");
|
|
});
|
|
|
|
it("returns stub when config.yaml already exists (refuses silent yaml mutation)", async () => {
|
|
mkdirSync(join(home, ".continue"), { recursive: true });
|
|
const { writeFileSync } = await import("node:fs");
|
|
writeFileSync(
|
|
join(home, ".continue", "config.yaml"),
|
|
"models: []\nmcpServers:\n - name: existing\n command: noop\n",
|
|
);
|
|
const { adapter } = await import("../src/cli/connect/continue.js");
|
|
const result = await adapter.install({ dryRun: false, force: false });
|
|
expect(result.kind).toBe("stub");
|
|
// user's yaml must be untouched
|
|
const yaml = readFileSync(
|
|
join(home, ".continue", "config.yaml"),
|
|
"utf-8",
|
|
);
|
|
expect(yaml).toContain("existing");
|
|
expect(yaml).not.toContain("agentmemory");
|
|
});
|
|
});
|
|
|
|
describe("connect: all eight new agents registered in ADAPTERS", () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
});
|
|
|
|
it("knownAgents includes qwen, antigravity, kiro, warp, cline, continue, zed, droid", async () => {
|
|
const { knownAgents } = await import("../src/cli/connect/index.js");
|
|
const agents = knownAgents();
|
|
for (const name of [
|
|
"qwen",
|
|
"antigravity",
|
|
"kiro",
|
|
"warp",
|
|
"cline",
|
|
"continue",
|
|
"zed",
|
|
"droid",
|
|
]) {
|
|
expect(agents).toContain(name);
|
|
}
|
|
});
|
|
});
|