@imgly/codesign-mcp 0.1.2 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/docs/api/index.d.ts +457 -27
- package/dist/assets/inline.generated.js +1 -1
- package/dist/assets/inline.generated.js.map +1 -1
- package/dist/cli.js +14 -0
- package/dist/cli.js.map +1 -1
- package/dist/test/test/tools/edit.test.js +11 -11
- package/dist/test/test/tools/edit.test.js.map +1 -1
- package/dist/test/tsconfig.test.tsbuildinfo +1 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +6 -13
- package/dist/tools/edit.js.map +1 -1
- package/dist/viewer/spa-assets.generated.js +1 -1
- package/dist/viewer/spa-assets.generated.js.map +1 -1
- package/package.json +7 -4
package/dist/cli.js
CHANGED
|
@@ -30,7 +30,21 @@ Common env:
|
|
|
30
30
|
IMGLY_DEMO_ASSETS_BASE_URL manifest root for the demo asset sources
|
|
31
31
|
(default: IMG.LY CDN).
|
|
32
32
|
`;
|
|
33
|
+
// The CE.SDK headless engine seeds its randomness from a global `crypto`
|
|
34
|
+
// (`crypto.getRandomValues`), which Node only exposes by default on v20+.
|
|
35
|
+
// On older runtimes — or sandboxes that strip globals — the engine dies deep
|
|
36
|
+
// inside its WASM runtime with a cryptic `ReferenceError: crypto is not
|
|
37
|
+
// defined`, often taking the whole process (and the MCP connection) down with
|
|
38
|
+
// it. Feature-detect the capability up front and fail with a clear message.
|
|
39
|
+
function assertSupportedRuntime() {
|
|
40
|
+
if (typeof globalThis.crypto?.getRandomValues !== 'function') {
|
|
41
|
+
process.stderr.write(`codesign-mcp: unsupported Node.js runtime (${process.version}). ` +
|
|
42
|
+
`Node.js >= 20 is required.\n`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
33
46
|
async function main() {
|
|
47
|
+
assertSupportedRuntime();
|
|
34
48
|
const sub = process.argv[2];
|
|
35
49
|
switch (sub) {
|
|
36
50
|
case 'stdio': {
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,6CAA6C;AAC7C,EAAE;AACF,2EAA2E;AAC3E,wEAAwE;AACxE,8CAA8C;AAC9C,EAAE;AACF,0EAA0E;AAC1E,4EAA4E;AAC5E,EAAE;AACF,yEAAyE;AACzE,yEAAyE;AAEzE,MAAM,KAAK,GAAG;;;;;;;;;;;;;;;;;;;CAmBb,CAAC;AAEF,KAAK,UAAU,IAAI;IACjB,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5B,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAC;YACpD,MAAM,QAAQ,EAAE,CAAC;YACjB,OAAO;QACT,CAAC;QACD,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAC;YAClD,MAAM,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YACrC,OAAO;QACT,CAAC;QACD,KAAK,IAAI,CAAC;QACV,KAAK,QAAQ;YACX,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAC5B,OAAO;QACT,KAAK,SAAS;YACZ,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,sCAAsC,KAAK,EAAE,CAAC,CAAC;YACpE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAChB,OAAO;QACT;YACE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,qCAAqC,GAAG,OAAO,KAAK,EAAE,CACvD,CAAC;YACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,yBAAyB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAC7F,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\n// codesign-mcp — single bin, two transports.\n//\n// codesign-mcp stdio MCP server over stdin/stdout. Spawned by hosts\n// like Claude Desktop / Claude Code via their\n// `command` config.\n//\n// codesign-mcp http MCP server over Streamable HTTP. Long-running\n// listener; hosts connect via their `url` config.\n//\n// Lazy imports per subcommand: stdio mode never loads express; http mode\n// never loads StdioServerTransport. Smaller crash surface, faster start.\n\nconst USAGE = `Usage: codesign-mcp <stdio|http> [flags]\n\nSubcommands:\n stdio MCP server over stdin/stdout. Spawned by hosts\n like Claude Desktop / Claude Code via their\n \\`command\\` config.\n\n http [--port <n>] MCP server over Streamable HTTP. Long-running\n [--host <ip>] listener; hosts connect via their \\`url\\` config.\n [--max-sessions <n>] Defaults: --port 3030 --host 127.0.0.1\n [--idle-timeout-ms <n>] --max-sessions 10 --idle-timeout-ms 1800000\n\nCommon env:\n CESDK_LICENSE required for both modes (CE.SDK headless\n engine license).\n IMGLY_DEFAULT_ASSETS_BASE_URL manifest root for the default asset sources\n (self-hosted deployments; default: IMG.LY CDN).\n IMGLY_DEMO_ASSETS_BASE_URL manifest root for the demo asset sources\n (default: IMG.LY CDN).\n`;\n\nasync function main(): Promise<void> {\n const sub = process.argv[2];\n switch (sub) {\n case 'stdio': {\n const { runStdio } = await import('./cli/stdio.js');\n await runStdio();\n return;\n }\n case 'http': {\n const { runHttp } = await import('./cli/http.js');\n await runHttp(process.argv.slice(3));\n return;\n }\n case '-h':\n case '--help':\n process.stdout.write(USAGE);\n return;\n case undefined:\n process.stderr.write(`codesign-mcp: missing subcommand.\\n${USAGE}`);\n process.exit(2);\n return;\n default:\n process.stderr.write(\n `codesign-mcp: unknown subcommand \"${sub}\".\\n${USAGE}`\n );\n process.exit(2);\n }\n}\n\nmain().catch((err) => {\n process.stderr.write(\n `codesign-mcp: fatal — ${err instanceof Error ? (err.stack ?? err.message) : String(err)}\\n`\n );\n process.exit(1);\n});\n"]}
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,6CAA6C;AAC7C,EAAE;AACF,2EAA2E;AAC3E,wEAAwE;AACxE,8CAA8C;AAC9C,EAAE;AACF,0EAA0E;AAC1E,4EAA4E;AAC5E,EAAE;AACF,yEAAyE;AACzE,yEAAyE;AAEzE,MAAM,KAAK,GAAG;;;;;;;;;;;;;;;;;;;CAmBb,CAAC;AAEF,yEAAyE;AACzE,0EAA0E;AAC1E,6EAA6E;AAC7E,wEAAwE;AACxE,8EAA8E;AAC9E,4EAA4E;AAC5E,SAAS,sBAAsB;IAC7B,IAAI,OAAO,UAAU,CAAC,MAAM,EAAE,eAAe,KAAK,UAAU,EAAE,CAAC;QAC7D,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,8CAA8C,OAAO,CAAC,OAAO,KAAK;YAChE,8BAA8B,CACjC,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,sBAAsB,EAAE,CAAC;IACzB,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5B,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAC;YACpD,MAAM,QAAQ,EAAE,CAAC;YACjB,OAAO;QACT,CAAC;QACD,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAC;YAClD,MAAM,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YACrC,OAAO;QACT,CAAC;QACD,KAAK,IAAI,CAAC;QACV,KAAK,QAAQ;YACX,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAC5B,OAAO;QACT,KAAK,SAAS;YACZ,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,sCAAsC,KAAK,EAAE,CAAC,CAAC;YACpE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAChB,OAAO;QACT;YACE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,qCAAqC,GAAG,OAAO,KAAK,EAAE,CACvD,CAAC;YACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,yBAAyB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAC7F,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\n// codesign-mcp — single bin, two transports.\n//\n// codesign-mcp stdio MCP server over stdin/stdout. Spawned by hosts\n// like Claude Desktop / Claude Code via their\n// `command` config.\n//\n// codesign-mcp http MCP server over Streamable HTTP. Long-running\n// listener; hosts connect via their `url` config.\n//\n// Lazy imports per subcommand: stdio mode never loads express; http mode\n// never loads StdioServerTransport. Smaller crash surface, faster start.\n\nconst USAGE = `Usage: codesign-mcp <stdio|http> [flags]\n\nSubcommands:\n stdio MCP server over stdin/stdout. Spawned by hosts\n like Claude Desktop / Claude Code via their\n \\`command\\` config.\n\n http [--port <n>] MCP server over Streamable HTTP. Long-running\n [--host <ip>] listener; hosts connect via their \\`url\\` config.\n [--max-sessions <n>] Defaults: --port 3030 --host 127.0.0.1\n [--idle-timeout-ms <n>] --max-sessions 10 --idle-timeout-ms 1800000\n\nCommon env:\n CESDK_LICENSE required for both modes (CE.SDK headless\n engine license).\n IMGLY_DEFAULT_ASSETS_BASE_URL manifest root for the default asset sources\n (self-hosted deployments; default: IMG.LY CDN).\n IMGLY_DEMO_ASSETS_BASE_URL manifest root for the demo asset sources\n (default: IMG.LY CDN).\n`;\n\n// The CE.SDK headless engine seeds its randomness from a global `crypto`\n// (`crypto.getRandomValues`), which Node only exposes by default on v20+.\n// On older runtimes — or sandboxes that strip globals — the engine dies deep\n// inside its WASM runtime with a cryptic `ReferenceError: crypto is not\n// defined`, often taking the whole process (and the MCP connection) down with\n// it. Feature-detect the capability up front and fail with a clear message.\nfunction assertSupportedRuntime(): void {\n if (typeof globalThis.crypto?.getRandomValues !== 'function') {\n process.stderr.write(\n `codesign-mcp: unsupported Node.js runtime (${process.version}). ` +\n `Node.js >= 20 is required.\\n`\n );\n process.exit(1);\n }\n}\n\nasync function main(): Promise<void> {\n assertSupportedRuntime();\n const sub = process.argv[2];\n switch (sub) {\n case 'stdio': {\n const { runStdio } = await import('./cli/stdio.js');\n await runStdio();\n return;\n }\n case 'http': {\n const { runHttp } = await import('./cli/http.js');\n await runHttp(process.argv.slice(3));\n return;\n }\n case '-h':\n case '--help':\n process.stdout.write(USAGE);\n return;\n case undefined:\n process.stderr.write(`codesign-mcp: missing subcommand.\\n${USAGE}`);\n process.exit(2);\n return;\n default:\n process.stderr.write(\n `codesign-mcp: unknown subcommand \"${sub}\".\\n${USAGE}`\n );\n process.exit(2);\n }\n}\n\nmain().catch((err) => {\n process.stderr.write(\n `codesign-mcp: fatal — ${err instanceof Error ? (err.stack ?? err.message) : String(err)}\\n`\n );\n process.exit(1);\n});\n"]}
|
|
@@ -138,22 +138,22 @@ describe('edit tool — safety + validation', () => {
|
|
|
138
138
|
expect(result.isError).toBe(true);
|
|
139
139
|
expect(result.content[0].text).toMatch(/code.*required/i);
|
|
140
140
|
});
|
|
141
|
-
it('
|
|
142
|
-
// LLMs
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
const { store, session } = makeDeps();
|
|
141
|
+
it('coerces the literal string "null" to a real null parent (new design root)', async () => {
|
|
142
|
+
// Some LLMs / MCP clients emit the four-character string "null" instead of
|
|
143
|
+
// the bare JSON null literal — a quoting slip, not a real revision id. The
|
|
144
|
+
// tool coerces "null" → null rather than rejecting, so the slip never
|
|
145
|
+
// blocks creating a new design root. (Stochastic, model-dependent.)
|
|
146
|
+
const { store, session, adapter } = makeDeps();
|
|
148
147
|
const tool = createEditTool({ store, session, engineLock });
|
|
149
148
|
const result = await tool.call({
|
|
150
149
|
parent: 'null',
|
|
151
150
|
code: "engine._scene = 'X'"
|
|
152
151
|
});
|
|
153
|
-
expect(result.isError).
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
expect(
|
|
152
|
+
expect(result.isError).toBeFalsy();
|
|
153
|
+
expect(adapter.reset).toHaveBeenCalledOnce(); // treated as parent=null
|
|
154
|
+
const envelope = JSON.parse(result.content[0].text);
|
|
155
|
+
expect(envelope.parent).toBeNull();
|
|
156
|
+
expect(envelope.revision).toMatch(/^[0-9a-f]{12}$/);
|
|
157
157
|
});
|
|
158
158
|
});
|
|
159
159
|
describe('edit tool — per-leaf thumbnail', () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit.test.js","sourceRoot":"","sources":["../../../../test/tools/edit.test.ts"],"names":[],"mappings":"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","sourcesContent":["// packages/codesign-mcp/test/tools/edit.test.ts\nimport { describe, expect, it, vi } from 'vitest';\nimport { Mutex } from '../../src/engine/mutex.js';\nimport { type EngineAdapter, EngineSession } from '../../src/engine/session.js';\nimport { RevisionStore } from '../../src/revisions/store.js';\nimport { createEditTool } from '../../src/tools/edit.js';\nimport { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';\n\nconst engineLock = new Mutex();\n\nfunction makeDeps() {\n const fs = new InMemoryWorkspaceFS();\n const store = new RevisionStore(fs);\n const fakeEngine = { _scene: '' };\n const adapter: EngineAdapter = {\n create: () => fakeEngine,\n reset: vi.fn(async () => {\n fakeEngine._scene = 'EMPTY';\n }),\n loadFromString: vi.fn(async (e, s) => {\n (e as typeof fakeEngine)._scene = s;\n }),\n saveToString: vi.fn(async (e) => (e as typeof fakeEngine)._scene)\n };\n const session = new EngineSession({ size: 1, adapter });\n const onLeafWritten = vi.fn();\n return { fs, store, session, adapter, onLeafWritten, fakeEngine };\n}\n\ndescribe('edit tool — happy path', () => {\n it('parent=null resets engine, writes a root revision, returns {revision, parent}', async () => {\n const { store, session, adapter, onLeafWritten } = makeDeps();\n const tool = createEditTool({ store, session, engineLock, onLeafWritten });\n // The user `code` is what mutates the engine — it runs AFTER reset/loadFromString\n // resets the slot. So we set _scene from inside the code string.\n const result = await tool.call({\n parent: null,\n code: \"engine._scene = 'ENGINE_AFTER_USER_CODE'\",\n title: 'Wedding card',\n note: 'blank canvas'\n });\n expect(adapter.reset).toHaveBeenCalledOnce();\n expect(result.isError).toBeFalsy();\n const part = result.content[0] as { type: 'text'; text: string };\n const envelope = JSON.parse(part.text) as {\n revision: string;\n parent: string | null;\n };\n expect(envelope.parent).toBeNull();\n expect(envelope.revision).toMatch(/^[0-9a-f]{12}$/);\n expect(onLeafWritten).toHaveBeenCalledWith({\n revision: envelope.revision,\n rootRevision: envelope.revision\n });\n\n const persisted = await store.read(envelope.revision);\n expect(persisted.meta.title).toBe('Wedding card');\n expect(persisted.scene).toBe('ENGINE_AFTER_USER_CODE');\n // The code that ran is persisted alongside the revision for `inspect`.\n expect(await store.readCode(envelope.revision)).toBe(\n \"engine._scene = 'ENGINE_AFTER_USER_CODE'\"\n );\n });\n\n it('parent=existing revision is a linear edit (no reload on warm slot)', async () => {\n const { store, session, adapter, onLeafWritten } = makeDeps();\n const tool = createEditTool({ store, session, engineLock, onLeafWritten });\n const first = await tool.call({\n parent: null,\n code: \"engine._scene = 'SCENE_V1'\",\n title: 'T'\n });\n const firstRev = JSON.parse(\n (first.content[0] as { text: string }).text\n ).revision;\n\n (adapter.loadFromString as ReturnType<typeof vi.fn>).mockClear();\n const second = await tool.call({\n parent: firstRev,\n code: \"engine._scene = 'SCENE_V2'\",\n note: 'tweak'\n });\n expect(adapter.loadFromString).not.toHaveBeenCalled(); // engine already at firstRev\n const secondRev = JSON.parse(\n (second.content[0] as { text: string }).text\n ).revision;\n const persisted = await store.read(secondRev);\n expect(persisted.meta.parent).toBe(firstRev);\n expect(persisted.scene).toBe('SCENE_V2');\n });\n\n it('parent=older revision reloads engine and creates a fork', async () => {\n const { store, session, adapter, onLeafWritten } = makeDeps();\n const tool = createEditTool({ store, session, engineLock, onLeafWritten });\n const r1 = await tool.call({\n parent: null,\n code: \"engine._scene = 'SCENE_V1'\",\n title: 'T'\n });\n const rev1 = JSON.parse((r1.content[0] as { text: string }).text).revision;\n const _r2 = await tool.call({\n parent: rev1,\n code: \"engine._scene = 'SCENE_V2'\"\n });\n // now rewind: parent = rev1\n (adapter.loadFromString as ReturnType<typeof vi.fn>).mockClear();\n const r3 = await tool.call({\n parent: rev1,\n code: \"engine._scene = 'SCENE_V3_FORK'\"\n });\n expect(adapter.loadFromString).toHaveBeenCalledOnce(); // reloaded SCENE_V1\n const rev3 = JSON.parse((r3.content[0] as { text: string }).text).revision;\n const persisted = await store.read(rev3);\n expect(persisted.meta.parent).toBe(rev1);\n expect(persisted.scene).toBe('SCENE_V3_FORK');\n });\n});\n\ndescribe('edit tool — safety + validation', () => {\n it('marks slot dirty when code throws; next edit pays one reload', async () => {\n const { store, session, adapter, onLeafWritten } = makeDeps();\n const tool = createEditTool({ store, session, engineLock, onLeafWritten });\n const r1 = await tool.call({\n parent: null,\n code: \"engine._scene = 'SCENE_V1'\",\n title: 'T'\n });\n const rev1 = JSON.parse((r1.content[0] as { text: string }).text).revision;\n\n const bad = await tool.call({\n parent: rev1,\n code: \"throw new Error('boom')\"\n });\n expect(bad.isError).toBe(true);\n\n // Populate the session with the scene so we can re-acquire\n const rev = await store.read(rev1);\n await session.populate(rev1, rev.scene);\n\n const slot = await session.acquire(rev1);\n // After dirty + re-acquire, loadFromString must have been called.\n expect(adapter.loadFromString).toHaveBeenCalled();\n expect(slot.currentRevision).toBe(rev1);\n });\n\n it('returns isError when parent property is missing entirely', async () => {\n const { store, session } = makeDeps();\n const tool = createEditTool({ store, session, engineLock });\n const result = await tool.call({\n code: '// no parent'\n } as unknown as Record<string, unknown>);\n expect(result.isError).toBe(true);\n expect((result.content[0] as { text: string }).text).toMatch(\n /parent.*required/i\n );\n });\n\n it('returns isError when code is missing', async () => {\n const { store, session } = makeDeps();\n const tool = createEditTool({ store, session, engineLock });\n const result = await tool.call({ parent: null } as unknown as Record<\n string,\n unknown\n >);\n expect(result.isError).toBe(true);\n expect((result.content[0] as { text: string }).text).toMatch(\n /code.*required/i\n );\n });\n\n it('returns a teaching error when parent arrives as the literal string \"null\"', async () => {\n // LLMs sometimes serialize JSON null as the four-character string \"null\"\n // when a tool description mentions `null` in backticks. Catch it\n // explicitly so the agent learns the right shape on the first try,\n // instead of seeing \"unknown revision: null\" from the store and going\n // fs-diving to figure out what went wrong.\n const { store, session } = makeDeps();\n const tool = createEditTool({ store, session, engineLock });\n const result = await tool.call({\n parent: 'null',\n code: \"engine._scene = 'X'\"\n });\n expect(result.isError).toBe(true);\n const text = (result.content[0] as { text: string }).text;\n expect(text).toMatch(/string \"null\"/);\n expect(text).toMatch(/JSON null/);\n });\n});\n\ndescribe('edit tool — per-leaf thumbnail', () => {\n it('calls renderThumbnail with the new revision id and persists the bytes', async () => {\n const fs = new InMemoryWorkspaceFS();\n const store = new RevisionStore(fs);\n const fakeEngine = { _scene: '' };\n const adapter: EngineAdapter = {\n create: () => fakeEngine,\n reset: vi.fn(async () => {}),\n loadFromString: vi.fn(async () => {}),\n saveToString: vi.fn(async () => fakeEngine._scene)\n };\n const session = new EngineSession({ size: 1, adapter });\n const renderThumbnail = vi.fn(\n async () => new Uint8Array([0x52, 0x49, 0x46, 0x46])\n ); // \"RIFF\" — webp header\n const tool = createEditTool({\n store,\n session,\n engineLock,\n renderThumbnail,\n fs\n });\n const r = await tool.call({\n parent: null,\n code: \"engine._scene = 'SCENE_V1'\",\n title: 'T'\n });\n const rev = JSON.parse((r.content[0] as { text: string }).text).revision;\n expect(renderThumbnail).toHaveBeenCalledWith(\n expect.objectContaining({ revision: rev })\n );\n const shard = rev.slice(0, 2);\n const rest = rev.slice(2);\n expect(await fs.exists(`exports/${shard}/${rest}/preview.webp`)).toBe(true);\n });\n\n it('does not fail the edit when renderThumbnail throws', async () => {\n const fs = new InMemoryWorkspaceFS();\n const store = new RevisionStore(fs);\n const fakeEngine = { _scene: '' };\n const adapter: EngineAdapter = {\n create: () => fakeEngine,\n reset: vi.fn(async () => {}),\n loadFromString: vi.fn(async () => {}),\n saveToString: vi.fn(async () => fakeEngine._scene)\n };\n const session = new EngineSession({ size: 1, adapter });\n const renderThumbnail = vi.fn(async () => {\n throw new Error('thumbnail render boom');\n });\n const tool = createEditTool({\n store,\n session,\n engineLock,\n renderThumbnail,\n fs\n });\n const r = await tool.call({\n parent: null,\n code: \"engine._scene = 'S'\",\n title: 'T'\n });\n expect(r.isError).toBeFalsy();\n });\n});\n\ndescribe('edit tool — concurrency', () => {\n it('two sessions sharing one engine do not corrupt each other under the lock', async () => {\n const fs = new InMemoryWorkspaceFS();\n const store = new RevisionStore(fs);\n // ONE shared engine — exactly the production wiring, where\n // ensureHeadlessEngine is a process singleton handed to every session.\n const sharedEngine = { _scene: '' };\n const makeAdapter = (): EngineAdapter => ({\n create: () => sharedEngine,\n reset: async () => {\n sharedEngine._scene = '';\n },\n loadFromString: async (e, s) => {\n (e as typeof sharedEngine)._scene = s;\n },\n saveToString: async (e) => (e as typeof sharedEngine)._scene\n });\n // Each MCP session owns its EngineSession (per-session slot state) but they\n // share the engine. ONE lock is shared across sessions — the invariant the\n // bootstrap upholds by creating the Mutex once, outside createSession.\n const lock = new Mutex();\n const sessionA = new EngineSession({ size: 1, adapter: makeAdapter() });\n const sessionB = new EngineSession({ size: 1, adapter: makeAdapter() });\n const toolA = createEditTool({\n store,\n session: sessionA,\n engineLock: lock\n });\n const toolB = createEditTool({\n store,\n session: sessionB,\n engineLock: lock\n });\n\n // Each edit stamps the shared engine, then awaits — the exact window where,\n // WITHOUT the lock, the other session would overwrite the engine before\n // saveToString reads it back (so revA would persist SCENE_B). The shared\n // lock serializes the two, so each revision keeps its own scene.\n const code = (mark: string) =>\n `engine._scene = '${mark}'; await new Promise((r) => setTimeout(r, 10));`;\n const [resA, resB] = await Promise.all([\n toolA.call({ parent: null, code: code('SCENE_A'), title: 'A' }),\n toolB.call({ parent: null, code: code('SCENE_B'), title: 'B' })\n ]);\n const revA = (\n JSON.parse((resA.content[0] as { text: string }).text) as {\n revision: string;\n }\n ).revision;\n const revB = (\n JSON.parse((resB.content[0] as { text: string }).text) as {\n revision: string;\n }\n ).revision;\n expect((await store.read(revA)).scene).toBe('SCENE_A');\n expect((await store.read(revB)).scene).toBe('SCENE_B');\n });\n});\n"]}
|
|
1
|
+
{"version":3,"file":"edit.test.js","sourceRoot":"","sources":["../../../../test/tools/edit.test.ts"],"names":[],"mappings":"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","sourcesContent":["// packages/codesign-mcp/test/tools/edit.test.ts\nimport { describe, expect, it, vi } from 'vitest';\nimport { Mutex } from '../../src/engine/mutex.js';\nimport { type EngineAdapter, EngineSession } from '../../src/engine/session.js';\nimport { RevisionStore } from '../../src/revisions/store.js';\nimport { createEditTool } from '../../src/tools/edit.js';\nimport { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';\n\nconst engineLock = new Mutex();\n\nfunction makeDeps() {\n const fs = new InMemoryWorkspaceFS();\n const store = new RevisionStore(fs);\n const fakeEngine = { _scene: '' };\n const adapter: EngineAdapter = {\n create: () => fakeEngine,\n reset: vi.fn(async () => {\n fakeEngine._scene = 'EMPTY';\n }),\n loadFromString: vi.fn(async (e, s) => {\n (e as typeof fakeEngine)._scene = s;\n }),\n saveToString: vi.fn(async (e) => (e as typeof fakeEngine)._scene)\n };\n const session = new EngineSession({ size: 1, adapter });\n const onLeafWritten = vi.fn();\n return { fs, store, session, adapter, onLeafWritten, fakeEngine };\n}\n\ndescribe('edit tool — happy path', () => {\n it('parent=null resets engine, writes a root revision, returns {revision, parent}', async () => {\n const { store, session, adapter, onLeafWritten } = makeDeps();\n const tool = createEditTool({ store, session, engineLock, onLeafWritten });\n // The user `code` is what mutates the engine — it runs AFTER reset/loadFromString\n // resets the slot. So we set _scene from inside the code string.\n const result = await tool.call({\n parent: null,\n code: \"engine._scene = 'ENGINE_AFTER_USER_CODE'\",\n title: 'Wedding card',\n note: 'blank canvas'\n });\n expect(adapter.reset).toHaveBeenCalledOnce();\n expect(result.isError).toBeFalsy();\n const part = result.content[0] as { type: 'text'; text: string };\n const envelope = JSON.parse(part.text) as {\n revision: string;\n parent: string | null;\n };\n expect(envelope.parent).toBeNull();\n expect(envelope.revision).toMatch(/^[0-9a-f]{12}$/);\n expect(onLeafWritten).toHaveBeenCalledWith({\n revision: envelope.revision,\n rootRevision: envelope.revision\n });\n\n const persisted = await store.read(envelope.revision);\n expect(persisted.meta.title).toBe('Wedding card');\n expect(persisted.scene).toBe('ENGINE_AFTER_USER_CODE');\n // The code that ran is persisted alongside the revision for `inspect`.\n expect(await store.readCode(envelope.revision)).toBe(\n \"engine._scene = 'ENGINE_AFTER_USER_CODE'\"\n );\n });\n\n it('parent=existing revision is a linear edit (no reload on warm slot)', async () => {\n const { store, session, adapter, onLeafWritten } = makeDeps();\n const tool = createEditTool({ store, session, engineLock, onLeafWritten });\n const first = await tool.call({\n parent: null,\n code: \"engine._scene = 'SCENE_V1'\",\n title: 'T'\n });\n const firstRev = JSON.parse(\n (first.content[0] as { text: string }).text\n ).revision;\n\n (adapter.loadFromString as ReturnType<typeof vi.fn>).mockClear();\n const second = await tool.call({\n parent: firstRev,\n code: \"engine._scene = 'SCENE_V2'\",\n note: 'tweak'\n });\n expect(adapter.loadFromString).not.toHaveBeenCalled(); // engine already at firstRev\n const secondRev = JSON.parse(\n (second.content[0] as { text: string }).text\n ).revision;\n const persisted = await store.read(secondRev);\n expect(persisted.meta.parent).toBe(firstRev);\n expect(persisted.scene).toBe('SCENE_V2');\n });\n\n it('parent=older revision reloads engine and creates a fork', async () => {\n const { store, session, adapter, onLeafWritten } = makeDeps();\n const tool = createEditTool({ store, session, engineLock, onLeafWritten });\n const r1 = await tool.call({\n parent: null,\n code: \"engine._scene = 'SCENE_V1'\",\n title: 'T'\n });\n const rev1 = JSON.parse((r1.content[0] as { text: string }).text).revision;\n const _r2 = await tool.call({\n parent: rev1,\n code: \"engine._scene = 'SCENE_V2'\"\n });\n // now rewind: parent = rev1\n (adapter.loadFromString as ReturnType<typeof vi.fn>).mockClear();\n const r3 = await tool.call({\n parent: rev1,\n code: \"engine._scene = 'SCENE_V3_FORK'\"\n });\n expect(adapter.loadFromString).toHaveBeenCalledOnce(); // reloaded SCENE_V1\n const rev3 = JSON.parse((r3.content[0] as { text: string }).text).revision;\n const persisted = await store.read(rev3);\n expect(persisted.meta.parent).toBe(rev1);\n expect(persisted.scene).toBe('SCENE_V3_FORK');\n });\n});\n\ndescribe('edit tool — safety + validation', () => {\n it('marks slot dirty when code throws; next edit pays one reload', async () => {\n const { store, session, adapter, onLeafWritten } = makeDeps();\n const tool = createEditTool({ store, session, engineLock, onLeafWritten });\n const r1 = await tool.call({\n parent: null,\n code: \"engine._scene = 'SCENE_V1'\",\n title: 'T'\n });\n const rev1 = JSON.parse((r1.content[0] as { text: string }).text).revision;\n\n const bad = await tool.call({\n parent: rev1,\n code: \"throw new Error('boom')\"\n });\n expect(bad.isError).toBe(true);\n\n // Populate the session with the scene so we can re-acquire\n const rev = await store.read(rev1);\n await session.populate(rev1, rev.scene);\n\n const slot = await session.acquire(rev1);\n // After dirty + re-acquire, loadFromString must have been called.\n expect(adapter.loadFromString).toHaveBeenCalled();\n expect(slot.currentRevision).toBe(rev1);\n });\n\n it('returns isError when parent property is missing entirely', async () => {\n const { store, session } = makeDeps();\n const tool = createEditTool({ store, session, engineLock });\n const result = await tool.call({\n code: '// no parent'\n } as unknown as Record<string, unknown>);\n expect(result.isError).toBe(true);\n expect((result.content[0] as { text: string }).text).toMatch(\n /parent.*required/i\n );\n });\n\n it('returns isError when code is missing', async () => {\n const { store, session } = makeDeps();\n const tool = createEditTool({ store, session, engineLock });\n const result = await tool.call({ parent: null } as unknown as Record<\n string,\n unknown\n >);\n expect(result.isError).toBe(true);\n expect((result.content[0] as { text: string }).text).toMatch(\n /code.*required/i\n );\n });\n\n it('coerces the literal string \"null\" to a real null parent (new design root)', async () => {\n // Some LLMs / MCP clients emit the four-character string \"null\" instead of\n // the bare JSON null literal — a quoting slip, not a real revision id. The\n // tool coerces \"null\" → null rather than rejecting, so the slip never\n // blocks creating a new design root. (Stochastic, model-dependent.)\n const { store, session, adapter } = makeDeps();\n const tool = createEditTool({ store, session, engineLock });\n const result = await tool.call({\n parent: 'null',\n code: \"engine._scene = 'X'\"\n });\n expect(result.isError).toBeFalsy();\n expect(adapter.reset).toHaveBeenCalledOnce(); // treated as parent=null\n const envelope = JSON.parse(\n (result.content[0] as { text: string }).text\n ) as { revision: string; parent: string | null };\n expect(envelope.parent).toBeNull();\n expect(envelope.revision).toMatch(/^[0-9a-f]{12}$/);\n });\n});\n\ndescribe('edit tool — per-leaf thumbnail', () => {\n it('calls renderThumbnail with the new revision id and persists the bytes', async () => {\n const fs = new InMemoryWorkspaceFS();\n const store = new RevisionStore(fs);\n const fakeEngine = { _scene: '' };\n const adapter: EngineAdapter = {\n create: () => fakeEngine,\n reset: vi.fn(async () => {}),\n loadFromString: vi.fn(async () => {}),\n saveToString: vi.fn(async () => fakeEngine._scene)\n };\n const session = new EngineSession({ size: 1, adapter });\n const renderThumbnail = vi.fn(\n async () => new Uint8Array([0x52, 0x49, 0x46, 0x46])\n ); // \"RIFF\" — webp header\n const tool = createEditTool({\n store,\n session,\n engineLock,\n renderThumbnail,\n fs\n });\n const r = await tool.call({\n parent: null,\n code: \"engine._scene = 'SCENE_V1'\",\n title: 'T'\n });\n const rev = JSON.parse((r.content[0] as { text: string }).text).revision;\n expect(renderThumbnail).toHaveBeenCalledWith(\n expect.objectContaining({ revision: rev })\n );\n const shard = rev.slice(0, 2);\n const rest = rev.slice(2);\n expect(await fs.exists(`exports/${shard}/${rest}/preview.webp`)).toBe(true);\n });\n\n it('does not fail the edit when renderThumbnail throws', async () => {\n const fs = new InMemoryWorkspaceFS();\n const store = new RevisionStore(fs);\n const fakeEngine = { _scene: '' };\n const adapter: EngineAdapter = {\n create: () => fakeEngine,\n reset: vi.fn(async () => {}),\n loadFromString: vi.fn(async () => {}),\n saveToString: vi.fn(async () => fakeEngine._scene)\n };\n const session = new EngineSession({ size: 1, adapter });\n const renderThumbnail = vi.fn(async () => {\n throw new Error('thumbnail render boom');\n });\n const tool = createEditTool({\n store,\n session,\n engineLock,\n renderThumbnail,\n fs\n });\n const r = await tool.call({\n parent: null,\n code: \"engine._scene = 'S'\",\n title: 'T'\n });\n expect(r.isError).toBeFalsy();\n });\n});\n\ndescribe('edit tool — concurrency', () => {\n it('two sessions sharing one engine do not corrupt each other under the lock', async () => {\n const fs = new InMemoryWorkspaceFS();\n const store = new RevisionStore(fs);\n // ONE shared engine — exactly the production wiring, where\n // ensureHeadlessEngine is a process singleton handed to every session.\n const sharedEngine = { _scene: '' };\n const makeAdapter = (): EngineAdapter => ({\n create: () => sharedEngine,\n reset: async () => {\n sharedEngine._scene = '';\n },\n loadFromString: async (e, s) => {\n (e as typeof sharedEngine)._scene = s;\n },\n saveToString: async (e) => (e as typeof sharedEngine)._scene\n });\n // Each MCP session owns its EngineSession (per-session slot state) but they\n // share the engine. ONE lock is shared across sessions — the invariant the\n // bootstrap upholds by creating the Mutex once, outside createSession.\n const lock = new Mutex();\n const sessionA = new EngineSession({ size: 1, adapter: makeAdapter() });\n const sessionB = new EngineSession({ size: 1, adapter: makeAdapter() });\n const toolA = createEditTool({\n store,\n session: sessionA,\n engineLock: lock\n });\n const toolB = createEditTool({\n store,\n session: sessionB,\n engineLock: lock\n });\n\n // Each edit stamps the shared engine, then awaits — the exact window where,\n // WITHOUT the lock, the other session would overwrite the engine before\n // saveToString reads it back (so revA would persist SCENE_B). The shared\n // lock serializes the two, so each revision keeps its own scene.\n const code = (mark: string) =>\n `engine._scene = '${mark}'; await new Promise((r) => setTimeout(r, 10));`;\n const [resA, resB] = await Promise.all([\n toolA.call({ parent: null, code: code('SCENE_A'), title: 'A' }),\n toolB.call({ parent: null, code: code('SCENE_B'), title: 'B' })\n ]);\n const revA = (\n JSON.parse((resA.content[0] as { text: string }).text) as {\n revision: string;\n }\n ).revision;\n const revB = (\n JSON.parse((resB.content[0] as { text: string }).text) as {\n revision: string;\n }\n ).revision;\n expect((await store.read(revA)).scene).toBe('SCENE_A');\n expect((await store.read(revB)).scene).toBe('SCENE_B');\n });\n});\n"]}
|