From 6f7250ee948421ff4fa90869afe1032967aa8348 Mon Sep 17 00:00:00 2001 From: Molecule AI SDK-Dev Date: Wed, 13 May 2026 09:12:03 +0000 Subject: [PATCH 1/2] fix(mcp): replace fake RemoteAgentClient.register_from_env() with correct API handleGetRemoteAgentSetupCommand generated a Python one-liner that called RemoteAgentClient.register_from_env(), which does not exist in the SDK. Replace it with the correct pattern: c = RemoteAgentClient(workspace_id='...', platform_url='...') if c.load_token() is None: c.register() This matches the actual API surface in molecule_agent/client.py and the pattern used by python -m molecule_agent connect. Tests: 4 new cases covering command shape, localhost warning, platform_url_override, and non-external runtime error. Co-Authored-By: Claude Opus 4.7 --- src/tools/remote_agents.ts | 5 +- tests/__tests__/api.test.ts | 96 +++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/tools/remote_agents.ts b/src/tools/remote_agents.ts index 69fa8b0..39bb28d 100644 --- a/src/tools/remote_agents.ts +++ b/src/tools/remote_agents.ts @@ -90,13 +90,14 @@ export async function handleGetRemoteAgentSetupCommand(params: { `WORKSPACE_ID=${w.id} \\`, `PLATFORM_URL=${targetUrl} \\`, `python3 -c "from molecule_agent import RemoteAgentClient; \\`, - ` c = RemoteAgentClient.register_from_env(); \\`, + ` c = RemoteAgentClient(workspace_id='${w.id}', platform_url='${targetUrl}'); \\`, + ` if c.load_token() is None: c.register(); \\`, ` c.pull_secrets(); \\`, ` c.run_heartbeat_loop()"`, ``, `# For a richer demo (logging, graceful shutdown) see`, `# examples/remote-agent/run.py in the molecule-sdk-python checkout.`, - `# The agent will register, mint its bearer token (cached at`, + `# The agent will register (mint + cache bearer token at`, `# ~/.molecule/${w.id}/.auth_token), pull secrets, then heartbeat.`, ].join("\n"); return toMcpResult({ diff --git a/tests/__tests__/api.test.ts b/tests/__tests__/api.test.ts index a1c8085..bfde1aa 100644 --- a/tests/__tests__/api.test.ts +++ b/tests/__tests__/api.test.ts @@ -290,3 +290,99 @@ describe("platformGet", () => { }); }); }); + +// --------------------------------------------------------------------------- +// remote_agents — handleGetRemoteAgentSetupCommand +// --------------------------------------------------------------------------- + +// remote_agents.ts reads PLATFORM_URL at module-load time from process.env. +// We use jest.isolateModules so each test gets a fresh module context with +// the right env var set before the module is loaded. +const originalEnv = process.env.MOLECULE_API_URL; + +describe("handleGetRemoteAgentSetupCommand", () => { + beforeEach(() => { + jest.resetModules(); + process.env.MOLECULE_API_URL = "http://localhost:8080"; + }); + + afterEach(() => { + if (originalEnv === undefined) { + delete process.env.MOLECULE_API_URL; + } else { + process.env.MOLECULE_API_URL = originalEnv; + } + }); + + async function loadHandlerAndMock(workspace: Record) { + let handler!: typeof import("../../src/tools/remote_agents").handleGetRemoteAgentSetupCommand; + let mockGet!: jest.Mock; + await new Promise((resolve) => { + jest.isolateModules(() => { + mockGet = jest.fn().mockResolvedValue(workspace); + jest.mock("../../src/api", () => ({ + ...jest.requireActual("../../src/api"), + platformGet: mockGet, + })); + const mod = require("../../src/tools/remote_agents"); + handler = mod.handleGetRemoteAgentSetupCommand; + resolve(); + }); + }); + return { handler, mockGet }; + } + + it("generates valid Python command with constructor + register pattern", async () => { + const { handler } = await loadHandlerAndMock({ + id: "ws-abc123", + name: "my-agent", + runtime: "external", + }); + const result = await handler({ workspace_id: "ws-abc123" }); + const parsed = JSON.parse((result.content[0] as { text: string }).text); + expect(parsed.workspace_id).toBe("ws-abc123"); + expect(parsed.workspace_name).toBe("my-agent"); + expect(parsed.setup_command).toContain("RemoteAgentClient(workspace_id='ws-abc123'"); + expect(parsed.setup_command).not.toContain("register_from_env"); + expect(parsed.setup_command).toContain("register()"); + }); + + it("warns when PLATFORM_URL is localhost and no override is given", async () => { + const { handler } = await loadHandlerAndMock({ + id: "ws-abc123", + name: "my-agent", + runtime: "external", + }); + const result = await handler({ workspace_id: "ws-abc123" }); + const parsed = JSON.parse((result.content[0] as { text: string }).text); + expect(parsed.warnings).toBeDefined(); + expect(parsed.warnings![0]).toContain("localhost"); + }); + + it("uses platform_url_override when provided", async () => { + const { handler } = await loadHandlerAndMock({ + id: "ws-abc123", + name: "my-agent", + runtime: "external", + }); + const result = await handler({ + workspace_id: "ws-abc123", + platform_url_override: "https://platform.example.com", + }); + const parsed = JSON.parse((result.content[0] as { text: string }).text); + expect(parsed.setup_command).toContain("platform_url='https://platform.example.com'"); + expect(parsed.warnings).toBeUndefined(); + }); + + it("returns error when workspace is not runtime=external", async () => { + const { handler } = await loadHandlerAndMock({ + id: "ws-abc123", + name: "my-agent", + runtime: "docker", + }); + const result = await handler({ workspace_id: "ws-abc123" }); + const parsed = JSON.parse((result.content[0] as { text: string }).text); + expect(parsed.error).toContain("not external"); + expect(parsed.setup_command).toBeUndefined(); + }); +}); -- 2.52.0 From 36ed15fef203a041102dd84595097ed73f0698a1 Mon Sep 17 00:00:00 2001 From: Molecule AI SDK-Dev Date: Wed, 13 May 2026 09:31:00 +0000 Subject: [PATCH 2/2] test(remote_agents): add unit tests for handleGetRemoteAgentSetupCommand Tests cover: - Setup command contains correct RemoteAgentClient API (constructor + load_token) - localhost warning when PLATFORM_URL is localhost and no override given - platform_url_override suppresses the localhost warning - Non-external runtime returns descriptive error - Workspace not found returns error All 5 tests pass on the fix/remote-agent-setup-command branch. Co-Authored-By: Claude Opus 4.7 --- tests/__tests__/remote_agents.test.ts | 164 ++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 tests/__tests__/remote_agents.test.ts diff --git a/tests/__tests__/remote_agents.test.ts b/tests/__tests__/remote_agents.test.ts new file mode 100644 index 0000000..e30e49e --- /dev/null +++ b/tests/__tests__/remote_agents.test.ts @@ -0,0 +1,164 @@ +/** + * Unit tests for src/tools/remote_agents.ts + * + * Tests handleGetRemoteAgentSetupCommand which generates a Python bootstrap + * command for remote agents. Key edge cases: + * - localhost warning when PLATFORM_URL is localhost and no override given + * - platform_url_override bypasses localhost warning + * - non-external runtime returns error + * - workspace not found returns error + */ + +import { toMcpResult } from "../../src/api"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** Factory so each fetch call gets a fresh Response (bodies can only be read once). */ +function makeFetchResponse(body: unknown, init: ResponseInit = {}): Response { + const text = typeof body === "string" ? body : JSON.stringify(body); + return new Response(text, { + status: init.status ?? 200, + statusText: init.statusText, + headers: init.headers as HeadersInit, + }); +} + +type RemoteAgentsHandler = { + handleGetRemoteAgentSetupCommand: ( + params: { workspace_id: string; platform_url_override?: string } + ) => Promise>; +}; + +/** + * Dynamically import the remote_agents module with a mocked platformGet. + * Must be called inside jest.isolateModules() with MOLECULE_API_URL set. + */ +async function loadHandlerWithMock( + mockPlatformGet: jest.Mock, +): Promise { + let handler!: RemoteAgentsHandler; + await new Promise((resolve) => { + jest.isolateModules(() => { + jest.mock("../../src/api", () => ({ + ...jest.requireActual("../../src/api"), + platformGet: mockPlatformGet, + })); + const mod = require("../../src/tools/remote_agents") as RemoteAgentsHandler; + handler = mod; + resolve(); + }); + }); + return handler; +} + +// --------------------------------------------------------------------------- +// handleGetRemoteAgentSetupCommand tests +// --------------------------------------------------------------------------- + +describe("handleGetRemoteAgentSetupCommand", () => { + beforeEach(() => { + jest.resetModules(); + }); + + it("returns a setup command with correct RemoteAgentClient API call", async () => { + const mockGet = jest.fn().mockResolvedValue({ + id: "ws-abc123", + name: "test-agent", + runtime: "external", + }); + + const handler = await loadHandlerWithMock(mockGet); + const result = await handler.handleGetRemoteAgentSetupCommand({ + workspace_id: "ws-abc123", + }); + + expect(result.content[0].text).toContain("ws-abc123"); + expect(result.content[0].text).toContain("molecule_agent import RemoteAgentClient"); + // Must use constructor + load_token pattern, NOT the non-existent register_from_env() + expect(result.content[0].text).not.toContain("register_from_env()"); + expect(result.content[0].text).toContain("load_token()"); + expect(result.content[0].text).toContain("pull_secrets()"); + expect(result.content[0].text).toContain("run_heartbeat_loop()"); + }); + + it("returns a localhost warning when PLATFORM_URL is localhost and no override given", async () => { + // Set localhost as the platform URL before loading the module + process.env.MOLECULE_API_URL = "http://localhost:8080"; + + const mockGet = jest.fn().mockResolvedValue({ + id: "ws-abc123", + name: "test-agent", + runtime: "external", + }); + + const handler = await loadHandlerWithMock(mockGet); + const result = await handler.handleGetRemoteAgentSetupCommand({ + workspace_id: "ws-abc123", + }); + + const parsed = JSON.parse(result.content[0].text); + expect(parsed.warnings).toBeDefined(); + expect(parsed.warnings[0]).toContain("localhost"); + expect(parsed.warnings[0]).toContain("platform_url_override"); + + delete process.env.MOLECULE_API_URL; + }); + + it("platform_url_override bypasses the localhost warning", async () => { + // Even with localhost as the base URL, passing an override suppresses the warning + process.env.MOLECULE_API_URL = "http://localhost:8080"; + + const mockGet = jest.fn().mockResolvedValue({ + id: "ws-abc123", + name: "test-agent", + runtime: "external", + }); + + const handler = await loadHandlerWithMock(mockGet); + const result = await handler.handleGetRemoteAgentSetupCommand({ + workspace_id: "ws-abc123", + platform_url_override: "https://platform.example.com", + }); + + const parsed = JSON.parse(result.content[0].text); + expect(parsed.warnings).toBeUndefined(); + expect(parsed.platform_url).toBe("https://platform.example.com"); + + delete process.env.MOLECULE_API_URL; + }); + + it("returns error when workspace runtime is not 'external'", async () => { + const mockGet = jest.fn().mockResolvedValue({ + id: "ws-abc123", + name: "docker-agent", + runtime: "docker", + }); + + const handler = await loadHandlerWithMock(mockGet); + const result = await handler.handleGetRemoteAgentSetupCommand({ + workspace_id: "ws-abc123", + }); + + const parsed = JSON.parse(result.content[0].text); + expect(parsed.error).toContain("not external"); + expect(parsed.error).toContain("runtime='external'"); + expect(parsed.actual_runtime).toBe("docker"); + }); + + it("returns error when workspace is not found", async () => { + const mockGet = jest.fn().mockResolvedValue({ + error: "not found", + detail: "workspace ws-missing does not exist", + }); + + const handler = await loadHandlerWithMock(mockGet); + const result = await handler.handleGetRemoteAgentSetupCommand({ + workspace_id: "ws-missing", + }); + + const parsed = JSON.parse(result.content[0].text); + expect(parsed.error).toBeDefined(); + }); +}); -- 2.52.0