diff --git a/mcp/packages/plugin/src/ExecuteCodeTaskConsole.test.ts b/mcp/packages/plugin/src/ExecuteCodeTaskConsole.test.ts new file mode 100644 index 0000000000..40295aee4a --- /dev/null +++ b/mcp/packages/plugin/src/ExecuteCodeTaskConsole.test.ts @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { ExecuteCodeTaskConsole } from "./task-handlers/ExecuteCodeTaskHandler.ts"; + +test("formats plain objects and primitives", () => { + const console = new ExecuteCodeTaskConsole(); + console.log({ a: 1 }, null, "text", 42, undefined); + assert.equal(console.getLog(), '[LOG] {\n "a": 1\n} null text 42 undefined\n'); +}); + +test("keeps the name and message of a logged error", () => { + const console = new ExecuteCodeTaskConsole(); + console.error("failed", new TypeError("shape is undefined")); + assert.equal(console.getLog(), "[ERROR] failed TypeError: shape is undefined\n"); +}); + +test("does not throw for values JSON cannot serialize", () => { + const console = new ExecuteCodeTaskConsole(); + const circular: { self?: unknown } = {}; + circular.self = circular; + console.log(circular, { big: 1n }); + assert.equal(console.getLog(), "[LOG] [object Object] [object Object]\n"); +}); diff --git a/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts b/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts index 47004e5cfe..b2fff14f05 100644 --- a/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts +++ b/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts @@ -8,7 +8,7 @@ import { PenpotUtils } from "../PenpotUtils.ts"; * Provides the same interface as the native console object but appends * all output to an internal log string that can be retrieved. */ -class ExecuteCodeTaskConsole { +export class ExecuteCodeTaskConsole { /** * Accumulated log output from all console method calls. */ @@ -30,15 +30,32 @@ class ExecuteCodeTaskConsole { return this.logOutput; } + /** + * Formats one argument of a captured console call. + * Errors have no enumerable fields, and values such as circular objects + * cannot be serialized to JSON, so fall back to their string form. + */ + private formatLogArgument(arg: unknown): string { + if (arg instanceof Error) { + return String(arg); + } + if (typeof arg === "object") { + try { + return JSON.stringify(arg, null, 2); + } catch { + return String(arg); + } + } + return String(arg); + } + /** * Appends a formatted message to the log output. * @param level - Log level prefix (e.g., "LOG", "WARN", "ERROR") * @param args - Arguments to log, will be stringified and joined */ private appendToLog(level: string, ...args: any[]): void { - const message = args - .map((arg) => (typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg))) - .join(" "); + const message = args.map((arg) => this.formatLogArgument(arg)).join(" "); this.logOutput += `[${level}] ${message}\n`; }