@imgly/codesign-mcp 0.1.0 → 0.1.2

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.
Files changed (128) hide show
  1. package/assets/docs/handbook/SKILL.md +6 -0
  2. package/dist/assets/inline.generated.js +1 -1
  3. package/dist/assets/inline.generated.js.map +1 -1
  4. package/dist/cli/http.d.ts.map +1 -1
  5. package/dist/cli/http.js +4 -0
  6. package/dist/cli/http.js.map +1 -1
  7. package/dist/cli/stdio.d.ts.map +1 -1
  8. package/dist/cli/stdio.js +55 -18
  9. package/dist/cli/stdio.js.map +1 -1
  10. package/dist/engine/cesdk-adapter.d.ts +7 -1
  11. package/dist/engine/cesdk-adapter.d.ts.map +1 -1
  12. package/dist/engine/cesdk-adapter.js +7 -1
  13. package/dist/engine/cesdk-adapter.js.map +1 -1
  14. package/dist/engine/unsplash-source.d.ts +8 -0
  15. package/dist/engine/unsplash-source.d.ts.map +1 -0
  16. package/dist/engine/unsplash-source.js +77 -0
  17. package/dist/engine/unsplash-source.js.map +1 -0
  18. package/dist/http/server.d.ts.map +1 -1
  19. package/dist/http/server.js +0 -1
  20. package/dist/http/server.js.map +1 -1
  21. package/dist/server.d.ts +0 -2
  22. package/dist/server.d.ts.map +1 -1
  23. package/dist/server.js.map +1 -1
  24. package/dist/test/test/cli/redirect-console.test.d.ts +2 -0
  25. package/dist/test/test/cli/redirect-console.test.d.ts.map +1 -0
  26. package/dist/test/test/cli/redirect-console.test.js +47 -0
  27. package/dist/test/test/cli/redirect-console.test.js.map +1 -0
  28. package/dist/test/test/cli/resolve-license.test.d.ts +2 -0
  29. package/dist/test/test/cli/resolve-license.test.d.ts.map +1 -0
  30. package/dist/test/test/cli/resolve-license.test.js +20 -0
  31. package/dist/test/test/cli/resolve-license.test.js.map +1 -0
  32. package/dist/test/test/cli/stdio-license.test.d.ts +2 -0
  33. package/dist/test/test/cli/stdio-license.test.d.ts.map +1 -0
  34. package/dist/test/test/cli/stdio-license.test.js +19 -0
  35. package/dist/test/test/cli/stdio-license.test.js.map +1 -0
  36. package/dist/test/test/config.test.d.ts +2 -0
  37. package/dist/test/test/config.test.d.ts.map +1 -0
  38. package/dist/test/test/config.test.js +91 -0
  39. package/dist/test/test/config.test.js.map +1 -0
  40. package/dist/test/test/engine/asset-sources.test.d.ts +2 -0
  41. package/dist/test/test/engine/asset-sources.test.d.ts.map +1 -0
  42. package/dist/test/test/engine/asset-sources.test.js +100 -0
  43. package/dist/test/test/engine/asset-sources.test.js.map +1 -0
  44. package/dist/test/test/engine/mutex.test.d.ts +2 -0
  45. package/dist/test/test/engine/mutex.test.d.ts.map +1 -0
  46. package/dist/test/test/engine/mutex.test.js +63 -0
  47. package/dist/test/test/engine/mutex.test.js.map +1 -0
  48. package/dist/test/test/engine/session.test.js +15 -0
  49. package/dist/test/test/engine/session.test.js.map +1 -1
  50. package/dist/test/test/engine/unsplash-source.test.d.ts +2 -0
  51. package/dist/test/test/engine/unsplash-source.test.d.ts.map +1 -0
  52. package/dist/test/test/engine/unsplash-source.test.js +129 -0
  53. package/dist/test/test/engine/unsplash-source.test.js.map +1 -0
  54. package/dist/test/test/handbook-contract.test.js +8 -4
  55. package/dist/test/test/handbook-contract.test.js.map +1 -1
  56. package/dist/test/test/http/server.test.js +129 -0
  57. package/dist/test/test/http/server.test.js.map +1 -1
  58. package/dist/test/test/http/session-id-header.test.d.ts +2 -0
  59. package/dist/test/test/http/session-id-header.test.d.ts.map +1 -0
  60. package/dist/test/test/http/session-id-header.test.js +76 -0
  61. package/dist/test/test/http/session-id-header.test.js.map +1 -0
  62. package/dist/test/test/http/session-manager.test.js +14 -0
  63. package/dist/test/test/http/session-manager.test.js.map +1 -1
  64. package/dist/test/test/http/workspace-auth.test.d.ts +2 -0
  65. package/dist/test/test/http/workspace-auth.test.d.ts.map +1 -0
  66. package/dist/test/test/http/workspace-auth.test.js +41 -0
  67. package/dist/test/test/http/workspace-auth.test.js.map +1 -0
  68. package/dist/test/test/metrics/mount.test.d.ts +2 -0
  69. package/dist/test/test/metrics/mount.test.d.ts.map +1 -0
  70. package/dist/test/test/metrics/mount.test.js +59 -0
  71. package/dist/test/test/metrics/mount.test.js.map +1 -0
  72. package/dist/test/test/metrics/recorder.test.d.ts +2 -0
  73. package/dist/test/test/metrics/recorder.test.d.ts.map +1 -0
  74. package/dist/test/test/metrics/recorder.test.js +118 -0
  75. package/dist/test/test/metrics/recorder.test.js.map +1 -0
  76. package/dist/test/test/revisions/store.test.js +81 -11
  77. package/dist/test/test/revisions/store.test.js.map +1 -1
  78. package/dist/test/test/server.metrics.test.d.ts +2 -0
  79. package/dist/test/test/server.metrics.test.d.ts.map +1 -0
  80. package/dist/test/test/server.metrics.test.js +87 -0
  81. package/dist/test/test/server.metrics.test.js.map +1 -0
  82. package/dist/test/test/server.test.js +33 -4
  83. package/dist/test/test/server.test.js.map +1 -1
  84. package/dist/test/test/tools/asset-search.test.d.ts +2 -0
  85. package/dist/test/test/tools/asset-search.test.d.ts.map +1 -0
  86. package/dist/test/test/tools/asset-search.test.js +121 -0
  87. package/dist/test/test/tools/asset-search.test.js.map +1 -0
  88. package/dist/test/test/tools/edit.test.js +71 -9
  89. package/dist/test/test/tools/edit.test.js.map +1 -1
  90. package/dist/test/test/tools/export.test.js +60 -30
  91. package/dist/test/test/tools/export.test.js.map +1 -1
  92. package/dist/test/test/tools/index.test.d.ts +2 -0
  93. package/dist/test/test/tools/index.test.d.ts.map +1 -0
  94. package/dist/test/test/tools/index.test.js +55 -0
  95. package/dist/test/test/tools/index.test.js.map +1 -0
  96. package/dist/test/test/tools/metrics.test.d.ts +2 -0
  97. package/dist/test/test/tools/metrics.test.d.ts.map +1 -0
  98. package/dist/test/test/tools/metrics.test.js +29 -0
  99. package/dist/test/test/tools/metrics.test.js.map +1 -0
  100. package/dist/test/test/tools/preview.test.js +6 -4
  101. package/dist/test/test/tools/preview.test.js.map +1 -1
  102. package/dist/test/test/tools/rename.test.js +1 -2
  103. package/dist/test/test/tools/rename.test.js.map +1 -1
  104. package/dist/test/test/tools/save.test.d.ts +2 -0
  105. package/dist/test/test/tools/save.test.d.ts.map +1 -0
  106. package/dist/test/test/tools/save.test.js +137 -0
  107. package/dist/test/test/tools/save.test.js.map +1 -0
  108. package/dist/test/test/viewer/router.test.js +56 -0
  109. package/dist/test/test/viewer/router.test.js.map +1 -1
  110. package/dist/test/test/viewer/session-mount.test.d.ts +2 -0
  111. package/dist/test/test/viewer/session-mount.test.d.ts.map +1 -0
  112. package/dist/test/test/viewer/session-mount.test.js +52 -0
  113. package/dist/test/test/viewer/session-mount.test.js.map +1 -0
  114. package/dist/test/test/viewer/url-builder.test.js +76 -9
  115. package/dist/test/test/viewer/url-builder.test.js.map +1 -1
  116. package/dist/test/test/workspace/s3.test.d.ts +2 -0
  117. package/dist/test/test/workspace/s3.test.d.ts.map +1 -0
  118. package/dist/test/test/workspace/s3.test.js +153 -0
  119. package/dist/test/test/workspace/s3.test.js.map +1 -0
  120. package/dist/test/test/workspace/scoped.test.d.ts +2 -0
  121. package/dist/test/test/workspace/scoped.test.d.ts.map +1 -0
  122. package/dist/test/test/workspace/scoped.test.js +59 -0
  123. package/dist/test/test/workspace/scoped.test.js.map +1 -0
  124. package/dist/test/tsconfig.test.tsbuildinfo +1 -1
  125. package/dist/tools/index.d.ts.map +1 -1
  126. package/dist/tools/index.js +14 -4
  127. package/dist/tools/index.js.map +1 -1
  128. package/package.json +1 -1
@@ -1,9 +1,11 @@
1
1
  // packages/codesign-mcp/test/tools/edit.test.ts
2
2
  import { describe, expect, it, vi } from 'vitest';
3
+ import { Mutex } from '../../src/engine/mutex.js';
3
4
  import { EngineSession } from '../../src/engine/session.js';
4
5
  import { RevisionStore } from '../../src/revisions/store.js';
5
6
  import { createEditTool } from '../../src/tools/edit.js';
6
7
  import { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';
8
+ const engineLock = new Mutex();
7
9
  function makeDeps() {
8
10
  const fs = new InMemoryWorkspaceFS();
9
11
  const store = new RevisionStore(fs);
@@ -25,7 +27,7 @@ function makeDeps() {
25
27
  describe('edit tool — happy path', () => {
26
28
  it('parent=null resets engine, writes a root revision, returns {revision, parent}', async () => {
27
29
  const { store, session, adapter, onLeafWritten } = makeDeps();
28
- const tool = createEditTool({ store, session, onLeafWritten });
30
+ const tool = createEditTool({ store, session, engineLock, onLeafWritten });
29
31
  // The user `code` is what mutates the engine — it runs AFTER reset/loadFromString
30
32
  // resets the slot. So we set _scene from inside the code string.
31
33
  const result = await tool.call({
@@ -52,7 +54,7 @@ describe('edit tool — happy path', () => {
52
54
  });
53
55
  it('parent=existing revision is a linear edit (no reload on warm slot)', async () => {
54
56
  const { store, session, adapter, onLeafWritten } = makeDeps();
55
- const tool = createEditTool({ store, session, onLeafWritten });
57
+ const tool = createEditTool({ store, session, engineLock, onLeafWritten });
56
58
  const first = await tool.call({
57
59
  parent: null,
58
60
  code: "engine._scene = 'SCENE_V1'",
@@ -73,7 +75,7 @@ describe('edit tool — happy path', () => {
73
75
  });
74
76
  it('parent=older revision reloads engine and creates a fork', async () => {
75
77
  const { store, session, adapter, onLeafWritten } = makeDeps();
76
- const tool = createEditTool({ store, session, onLeafWritten });
78
+ const tool = createEditTool({ store, session, engineLock, onLeafWritten });
77
79
  const r1 = await tool.call({
78
80
  parent: null,
79
81
  code: "engine._scene = 'SCENE_V1'",
@@ -100,7 +102,7 @@ describe('edit tool — happy path', () => {
100
102
  describe('edit tool — safety + validation', () => {
101
103
  it('marks slot dirty when code throws; next edit pays one reload', async () => {
102
104
  const { store, session, adapter, onLeafWritten } = makeDeps();
103
- const tool = createEditTool({ store, session, onLeafWritten });
105
+ const tool = createEditTool({ store, session, engineLock, onLeafWritten });
104
106
  const r1 = await tool.call({
105
107
  parent: null,
106
108
  code: "engine._scene = 'SCENE_V1'",
@@ -122,7 +124,7 @@ describe('edit tool — safety + validation', () => {
122
124
  });
123
125
  it('returns isError when parent property is missing entirely', async () => {
124
126
  const { store, session } = makeDeps();
125
- const tool = createEditTool({ store, session });
127
+ const tool = createEditTool({ store, session, engineLock });
126
128
  const result = await tool.call({
127
129
  code: '// no parent'
128
130
  });
@@ -131,7 +133,7 @@ describe('edit tool — safety + validation', () => {
131
133
  });
132
134
  it('returns isError when code is missing', async () => {
133
135
  const { store, session } = makeDeps();
134
- const tool = createEditTool({ store, session });
136
+ const tool = createEditTool({ store, session, engineLock });
135
137
  const result = await tool.call({ parent: null });
136
138
  expect(result.isError).toBe(true);
137
139
  expect(result.content[0].text).toMatch(/code.*required/i);
@@ -143,7 +145,7 @@ describe('edit tool — safety + validation', () => {
143
145
  // instead of seeing "unknown revision: null" from the store and going
144
146
  // fs-diving to figure out what went wrong.
145
147
  const { store, session } = makeDeps();
146
- const tool = createEditTool({ store, session });
148
+ const tool = createEditTool({ store, session, engineLock });
147
149
  const result = await tool.call({
148
150
  parent: 'null',
149
151
  code: "engine._scene = 'X'"
@@ -167,7 +169,13 @@ describe('edit tool — per-leaf thumbnail', () => {
167
169
  };
168
170
  const session = new EngineSession({ size: 1, adapter });
169
171
  const renderThumbnail = vi.fn(async () => new Uint8Array([0x52, 0x49, 0x46, 0x46])); // "RIFF" — webp header
170
- const tool = createEditTool({ store, session, renderThumbnail, fs });
172
+ const tool = createEditTool({
173
+ store,
174
+ session,
175
+ engineLock,
176
+ renderThumbnail,
177
+ fs
178
+ });
171
179
  const r = await tool.call({
172
180
  parent: null,
173
181
  code: "engine._scene = 'SCENE_V1'",
@@ -193,7 +201,13 @@ describe('edit tool — per-leaf thumbnail', () => {
193
201
  const renderThumbnail = vi.fn(async () => {
194
202
  throw new Error('thumbnail render boom');
195
203
  });
196
- const tool = createEditTool({ store, session, renderThumbnail, fs });
204
+ const tool = createEditTool({
205
+ store,
206
+ session,
207
+ engineLock,
208
+ renderThumbnail,
209
+ fs
210
+ });
197
211
  const r = await tool.call({
198
212
  parent: null,
199
213
  code: "engine._scene = 'S'",
@@ -202,4 +216,52 @@ describe('edit tool — per-leaf thumbnail', () => {
202
216
  expect(r.isError).toBeFalsy();
203
217
  });
204
218
  });
219
+ describe('edit tool — concurrency', () => {
220
+ it('two sessions sharing one engine do not corrupt each other under the lock', async () => {
221
+ const fs = new InMemoryWorkspaceFS();
222
+ const store = new RevisionStore(fs);
223
+ // ONE shared engine — exactly the production wiring, where
224
+ // ensureHeadlessEngine is a process singleton handed to every session.
225
+ const sharedEngine = { _scene: '' };
226
+ const makeAdapter = () => ({
227
+ create: () => sharedEngine,
228
+ reset: async () => {
229
+ sharedEngine._scene = '';
230
+ },
231
+ loadFromString: async (e, s) => {
232
+ e._scene = s;
233
+ },
234
+ saveToString: async (e) => e._scene
235
+ });
236
+ // Each MCP session owns its EngineSession (per-session slot state) but they
237
+ // share the engine. ONE lock is shared across sessions — the invariant the
238
+ // bootstrap upholds by creating the Mutex once, outside createSession.
239
+ const lock = new Mutex();
240
+ const sessionA = new EngineSession({ size: 1, adapter: makeAdapter() });
241
+ const sessionB = new EngineSession({ size: 1, adapter: makeAdapter() });
242
+ const toolA = createEditTool({
243
+ store,
244
+ session: sessionA,
245
+ engineLock: lock
246
+ });
247
+ const toolB = createEditTool({
248
+ store,
249
+ session: sessionB,
250
+ engineLock: lock
251
+ });
252
+ // Each edit stamps the shared engine, then awaits — the exact window where,
253
+ // WITHOUT the lock, the other session would overwrite the engine before
254
+ // saveToString reads it back (so revA would persist SCENE_B). The shared
255
+ // lock serializes the two, so each revision keeps its own scene.
256
+ const code = (mark) => `engine._scene = '${mark}'; await new Promise((r) => setTimeout(r, 10));`;
257
+ const [resA, resB] = await Promise.all([
258
+ toolA.call({ parent: null, code: code('SCENE_A'), title: 'A' }),
259
+ toolB.call({ parent: null, code: code('SCENE_B'), title: 'B' })
260
+ ]);
261
+ const revA = JSON.parse(resA.content[0].text).revision;
262
+ const revB = JSON.parse(resB.content[0].text).revision;
263
+ expect((await store.read(revA)).scene).toBe('SCENE_A');
264
+ expect((await store.read(revB)).scene).toBe('SCENE_B');
265
+ });
266
+ });
205
267
  //# sourceMappingURL=edit.test.js.map
@@ -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 { 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\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, 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, 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, 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, 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 });\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 });\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 });\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({ store, session, renderThumbnail, fs });\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({ store, session, renderThumbnail, fs });\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"]}
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,9 +1,11 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
+ import { Mutex } from '../../src/engine/mutex.js';
2
3
  import { EngineSession } from '../../src/engine/session.js';
3
4
  import { RevisionStore } from '../../src/revisions/store.js';
4
5
  import { createExportTool } from '../../src/tools/export.js';
5
6
  import { LocalViewerUrlBuilder } from '../../src/viewer/url-builder.js';
6
7
  import { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';
8
+ const engineLock = new Mutex();
7
9
  const urlBuilder = new LocalViewerUrlBuilder({ base: 'http://127.0.0.1:9000' });
8
10
  function makeDeps() {
9
11
  const fs = new InMemoryWorkspaceFS();
@@ -29,32 +31,16 @@ function makeDeps() {
29
31
  return { fs, store, session, fakeEngine };
30
32
  }
31
33
  describe('export tool', () => {
32
- it('export target=scene writes the bytes and returns uri + httpUrl, no path', async () => {
33
- const { fs, store, session } = makeDeps();
34
- const root = await store.write({
35
- scene: 'SCENE_ABC',
36
- parent: null,
37
- title: 'T'
38
- });
39
- const tool = createExportTool({ fs, store, session, urlBuilder });
40
- const result = await tool.call({
41
- target: 'scene',
42
- format: 'scene',
43
- revision: root.id
44
- });
45
- const body = JSON.parse(result.content[0].text);
46
- const shard = root.id.slice(0, 2);
47
- const rest = root.id.slice(2);
48
- expect(body.uri).toBe(`workspace://exports/${shard}/${rest}/scene.scene`);
49
- expect(body.httpUrl).toBe(`http://127.0.0.1:9000/r/exports/${shard}/${rest}/scene.scene`);
50
- expect(body.path).toBeUndefined();
51
- expect(await fs.readFile(`exports/${shard}/${rest}/scene.scene`, 'utf8')).toBe('SCENE_ABC');
52
- expect(body.bytes).toBe(9);
53
- });
54
34
  it('export target=image renders the block and writes the file deterministically', async () => {
55
35
  const { fs, store, session } = makeDeps();
56
36
  const root = await store.write({ scene: 'S', parent: null, title: 'T' });
57
- const tool = createExportTool({ fs, store, session, urlBuilder });
37
+ const tool = createExportTool({
38
+ fs,
39
+ store,
40
+ session,
41
+ engineLock,
42
+ urlBuilder
43
+ });
58
44
  const result = await tool.call({
59
45
  target: 'image',
60
46
  format: 'png',
@@ -70,7 +56,13 @@ describe('export tool', () => {
70
56
  it("export target=pdf renders via block.export('application/pdf')", async () => {
71
57
  const { fs, store, session, fakeEngine } = makeDeps();
72
58
  const root = await store.write({ scene: 'S', parent: null, title: 'T' });
73
- const tool = createExportTool({ fs, store, session, urlBuilder });
59
+ const tool = createExportTool({
60
+ fs,
61
+ store,
62
+ session,
63
+ engineLock,
64
+ urlBuilder
65
+ });
74
66
  await tool.call({
75
67
  target: 'pdf',
76
68
  format: 'pdf',
@@ -82,7 +74,13 @@ describe('export tool', () => {
82
74
  it('returns isError when target=video (deferred to Phase 4)', async () => {
83
75
  const { fs, store, session } = makeDeps();
84
76
  const root = await store.write({ scene: 'S', parent: null, title: 'T' });
85
- const tool = createExportTool({ fs, store, session, urlBuilder });
77
+ const tool = createExportTool({
78
+ fs,
79
+ store,
80
+ session,
81
+ engineLock,
82
+ urlBuilder
83
+ });
86
84
  const result = await tool.call({
87
85
  target: 'video',
88
86
  format: 'mp4',
@@ -102,18 +100,20 @@ describe('export tool', () => {
102
100
  const { tmpdir } = await import('node:os');
103
101
  const { join } = await import('node:path');
104
102
  const dir = await mkdtemp(join(tmpdir(), 'export-out-'));
105
- const outPath = join(dir, 'x.scene');
103
+ const outPath = join(dir, 'x.png');
106
104
  const tool = createExportTool({
107
105
  fs,
108
106
  store,
109
107
  session,
108
+ engineLock,
110
109
  urlBuilder,
111
110
  allowOutPath: true
112
111
  });
113
112
  const result = await tool.call({
114
- target: 'scene',
115
- format: 'scene',
113
+ target: 'image',
114
+ format: 'png',
116
115
  revision: root.id,
116
+ blockId: 5,
117
117
  outPath
118
118
  });
119
119
  const body = JSON.parse(result.content[0].text);
@@ -128,17 +128,47 @@ describe('export tool', () => {
128
128
  fs,
129
129
  store,
130
130
  session,
131
+ engineLock,
131
132
  urlBuilder,
132
133
  allowOutPath: false
133
134
  });
135
+ const result = await tool.call({
136
+ target: 'image',
137
+ format: 'png',
138
+ revision: root.id,
139
+ blockId: 5,
140
+ outPath: '/tmp/x'
141
+ });
142
+ expect(result.isError).toBe(true);
143
+ expect(result.content[0].text).toMatch(/outPath.*not.*supported/i);
144
+ });
145
+ it('omits `outPath` from the schema when not allowed (Hosted), advertises it when allowed (Local)', () => {
146
+ const { fs, store, session } = makeDeps();
147
+ const base = { fs, store, session, engineLock, urlBuilder };
148
+ const hosted = createExportTool({ ...base, allowOutPath: false });
149
+ const local = createExportTool({ ...base, allowOutPath: true });
150
+ const props = (s) => s.properties;
151
+ expect(props(hosted.inputSchema)).not.toHaveProperty('outPath');
152
+ expect(props(local.inputSchema)).toHaveProperty('outPath');
153
+ });
154
+ it('rejects target=scene (moved to the save tool)', async () => {
155
+ const { fs, store, session } = makeDeps();
156
+ const root = await store.write({ scene: 'S', parent: null, title: 'T' });
157
+ const tool = createExportTool({
158
+ fs,
159
+ store,
160
+ session,
161
+ engineLock,
162
+ urlBuilder
163
+ });
134
164
  const result = await tool.call({
135
165
  target: 'scene',
136
166
  format: 'scene',
137
167
  revision: root.id,
138
- outPath: '/tmp/x'
168
+ blockId: 5
139
169
  });
140
170
  expect(result.isError).toBe(true);
141
- expect(result.content[0].text).toMatch(/outPath.*not.*allowed/i);
171
+ expect(result.content[0].text).toMatch(/invalid format/i);
142
172
  });
143
173
  });
144
174
  //# sourceMappingURL=export.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"export.test.js","sourceRoot":"","sources":["../../../../test/tools/export.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, expect, it, vi } from 'vitest';\nimport { type EngineAdapter, EngineSession } from '../../src/engine/session.js';\nimport { RevisionStore } from '../../src/revisions/store.js';\nimport { createExportTool } from '../../src/tools/export.js';\nimport { LocalViewerUrlBuilder } from '../../src/viewer/url-builder.js';\nimport { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';\n\ninterface FakeEngine {\n _scene: string;\n block: { export: ReturnType<typeof vi.fn> };\n scene: { saveToString: ReturnType<typeof vi.fn> };\n}\n\nconst urlBuilder = new LocalViewerUrlBuilder({ base: 'http://127.0.0.1:9000' });\n\nfunction makeDeps() {\n const fs = new InMemoryWorkspaceFS();\n const store = new RevisionStore(fs);\n const fakeEngine: FakeEngine = {\n _scene: '',\n block: {\n export: vi.fn(\n async () => new Blob([new Uint8Array([0xff, 0xd8, 0xff, 0xe0])])\n )\n },\n scene: { saveToString: vi.fn(async () => '<scene-serialized/>') }\n };\n const adapter: EngineAdapter<FakeEngine> = {\n create: () => fakeEngine,\n reset: async (e) => {\n e._scene = 'EMPTY';\n },\n loadFromString: async (e, s) => {\n e._scene = s;\n },\n saveToString: async (e) => e._scene\n };\n const session = new EngineSession<FakeEngine>({ size: 1, adapter });\n return { fs, store, session, fakeEngine };\n}\n\ndescribe('export tool', () => {\n it('export target=scene writes the bytes and returns uri + httpUrl, no path', async () => {\n const { fs, store, session } = makeDeps();\n const root = await store.write({\n scene: 'SCENE_ABC',\n parent: null,\n title: 'T'\n });\n const tool = createExportTool({ fs, store, session, urlBuilder });\n const result = await tool.call({\n target: 'scene',\n format: 'scene',\n revision: root.id\n });\n const body = JSON.parse((result.content[0] as { text: string }).text) as {\n uri: string;\n httpUrl?: string;\n path?: string;\n bytes: number;\n };\n const shard = root.id.slice(0, 2);\n const rest = root.id.slice(2);\n expect(body.uri).toBe(`workspace://exports/${shard}/${rest}/scene.scene`);\n expect(body.httpUrl).toBe(\n `http://127.0.0.1:9000/r/exports/${shard}/${rest}/scene.scene`\n );\n expect(body.path).toBeUndefined();\n expect(\n await fs.readFile(`exports/${shard}/${rest}/scene.scene`, 'utf8')\n ).toBe('SCENE_ABC');\n expect(body.bytes).toBe(9);\n });\n\n it('export target=image renders the block and writes the file deterministically', async () => {\n const { fs, store, session } = makeDeps();\n const root = await store.write({ scene: 'S', parent: null, title: 'T' });\n const tool = createExportTool({ fs, store, session, urlBuilder });\n const result = await tool.call({\n target: 'image',\n format: 'png',\n revision: root.id,\n blockId: 5\n });\n const body = JSON.parse((result.content[0] as { text: string }).text) as {\n uri: string;\n };\n const shard = root.id.slice(0, 2);\n const rest = root.id.slice(2);\n expect(body.uri).toBe(`workspace://exports/${shard}/${rest}/image.png`);\n expect(await fs.exists(`exports/${shard}/${rest}/image.png`)).toBe(true);\n });\n\n it(\"export target=pdf renders via block.export('application/pdf')\", async () => {\n const { fs, store, session, fakeEngine } = makeDeps();\n const root = await store.write({ scene: 'S', parent: null, title: 'T' });\n const tool = createExportTool({ fs, store, session, urlBuilder });\n await tool.call({\n target: 'pdf',\n format: 'pdf',\n revision: root.id,\n blockId: 5\n });\n expect(fakeEngine.block.export).toHaveBeenCalledWith(\n 5,\n 'application/pdf',\n expect.any(Object)\n );\n });\n\n it('returns isError when target=video (deferred to Phase 4)', async () => {\n const { fs, store, session } = makeDeps();\n const root = await store.write({ scene: 'S', parent: null, title: 'T' });\n const tool = createExportTool({ fs, store, session, urlBuilder });\n const result = await tool.call({\n target: 'video',\n format: 'mp4',\n revision: root.id\n });\n expect(result.isError).toBe(true);\n expect((result.content[0] as { text: string }).text).toMatch(/phase 4/i);\n });\n\n it('respects local `outPath` escape hatch — returns path, not httpUrl', async () => {\n const { fs, store, session } = makeDeps();\n const root = await store.write({\n scene: 'SCENE_X',\n parent: null,\n title: 'T'\n });\n const { mkdtemp, rm } = await import('node:fs/promises');\n const { tmpdir } = await import('node:os');\n const { join } = await import('node:path');\n const dir = await mkdtemp(join(tmpdir(), 'export-out-'));\n const outPath = join(dir, 'x.scene');\n const tool = createExportTool({\n fs,\n store,\n session,\n urlBuilder,\n allowOutPath: true\n });\n const result = await tool.call({\n target: 'scene',\n format: 'scene',\n revision: root.id,\n outPath\n });\n const body = JSON.parse((result.content[0] as { text: string }).text) as {\n path?: string;\n httpUrl?: string;\n };\n expect(body.path).toBe(outPath);\n expect(body.httpUrl).toBeUndefined();\n await rm(dir, { recursive: true });\n });\n\n it('rejects `outPath` when not allowed (Hosted/sandbox)', async () => {\n const { fs, store, session } = makeDeps();\n const root = await store.write({ scene: 'S', parent: null, title: 'T' });\n const tool = createExportTool({\n fs,\n store,\n session,\n urlBuilder,\n allowOutPath: false\n });\n const result = await tool.call({\n target: 'scene',\n format: 'scene',\n revision: root.id,\n outPath: '/tmp/x'\n });\n expect(result.isError).toBe(true);\n expect((result.content[0] as { text: string }).text).toMatch(\n /outPath.*not.*allowed/i\n );\n });\n});\n"]}
1
+ {"version":3,"file":"export.test.js","sourceRoot":"","sources":["../../../../test/tools/export.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { 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 { createExportTool } from '../../src/tools/export.js';\nimport { LocalViewerUrlBuilder } from '../../src/viewer/url-builder.js';\nimport { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';\n\nconst engineLock = new Mutex();\n\ninterface FakeEngine {\n _scene: string;\n block: { export: ReturnType<typeof vi.fn> };\n scene: { saveToString: ReturnType<typeof vi.fn> };\n}\n\nconst urlBuilder = new LocalViewerUrlBuilder({ base: 'http://127.0.0.1:9000' });\n\nfunction makeDeps() {\n const fs = new InMemoryWorkspaceFS();\n const store = new RevisionStore(fs);\n const fakeEngine: FakeEngine = {\n _scene: '',\n block: {\n export: vi.fn(\n async () => new Blob([new Uint8Array([0xff, 0xd8, 0xff, 0xe0])])\n )\n },\n scene: { saveToString: vi.fn(async () => '<scene-serialized/>') }\n };\n const adapter: EngineAdapter<FakeEngine> = {\n create: () => fakeEngine,\n reset: async (e) => {\n e._scene = 'EMPTY';\n },\n loadFromString: async (e, s) => {\n e._scene = s;\n },\n saveToString: async (e) => e._scene\n };\n const session = new EngineSession<FakeEngine>({ size: 1, adapter });\n return { fs, store, session, fakeEngine };\n}\n\ndescribe('export tool', () => {\n it('export target=image renders the block and writes the file deterministically', async () => {\n const { fs, store, session } = makeDeps();\n const root = await store.write({ scene: 'S', parent: null, title: 'T' });\n const tool = createExportTool({\n fs,\n store,\n session,\n engineLock,\n urlBuilder\n });\n const result = await tool.call({\n target: 'image',\n format: 'png',\n revision: root.id,\n blockId: 5\n });\n const body = JSON.parse((result.content[0] as { text: string }).text) as {\n uri: string;\n };\n const shard = root.id.slice(0, 2);\n const rest = root.id.slice(2);\n expect(body.uri).toBe(`workspace://exports/${shard}/${rest}/image.png`);\n expect(await fs.exists(`exports/${shard}/${rest}/image.png`)).toBe(true);\n });\n\n it(\"export target=pdf renders via block.export('application/pdf')\", async () => {\n const { fs, store, session, fakeEngine } = makeDeps();\n const root = await store.write({ scene: 'S', parent: null, title: 'T' });\n const tool = createExportTool({\n fs,\n store,\n session,\n engineLock,\n urlBuilder\n });\n await tool.call({\n target: 'pdf',\n format: 'pdf',\n revision: root.id,\n blockId: 5\n });\n expect(fakeEngine.block.export).toHaveBeenCalledWith(\n 5,\n 'application/pdf',\n expect.any(Object)\n );\n });\n\n it('returns isError when target=video (deferred to Phase 4)', async () => {\n const { fs, store, session } = makeDeps();\n const root = await store.write({ scene: 'S', parent: null, title: 'T' });\n const tool = createExportTool({\n fs,\n store,\n session,\n engineLock,\n urlBuilder\n });\n const result = await tool.call({\n target: 'video',\n format: 'mp4',\n revision: root.id\n });\n expect(result.isError).toBe(true);\n expect((result.content[0] as { text: string }).text).toMatch(/phase 4/i);\n });\n\n it('respects local `outPath` escape hatch — returns path, not httpUrl', async () => {\n const { fs, store, session } = makeDeps();\n const root = await store.write({\n scene: 'SCENE_X',\n parent: null,\n title: 'T'\n });\n const { mkdtemp, rm } = await import('node:fs/promises');\n const { tmpdir } = await import('node:os');\n const { join } = await import('node:path');\n const dir = await mkdtemp(join(tmpdir(), 'export-out-'));\n const outPath = join(dir, 'x.png');\n const tool = createExportTool({\n fs,\n store,\n session,\n engineLock,\n urlBuilder,\n allowOutPath: true\n });\n const result = await tool.call({\n target: 'image',\n format: 'png',\n revision: root.id,\n blockId: 5,\n outPath\n });\n const body = JSON.parse((result.content[0] as { text: string }).text) as {\n path?: string;\n httpUrl?: string;\n };\n expect(body.path).toBe(outPath);\n expect(body.httpUrl).toBeUndefined();\n await rm(dir, { recursive: true });\n });\n\n it('rejects `outPath` when not allowed (Hosted/sandbox)', async () => {\n const { fs, store, session } = makeDeps();\n const root = await store.write({ scene: 'S', parent: null, title: 'T' });\n const tool = createExportTool({\n fs,\n store,\n session,\n engineLock,\n urlBuilder,\n allowOutPath: false\n });\n const result = await tool.call({\n target: 'image',\n format: 'png',\n revision: root.id,\n blockId: 5,\n outPath: '/tmp/x'\n });\n expect(result.isError).toBe(true);\n expect((result.content[0] as { text: string }).text).toMatch(\n /outPath.*not.*supported/i\n );\n });\n\n it('omits `outPath` from the schema when not allowed (Hosted), advertises it when allowed (Local)', () => {\n const { fs, store, session } = makeDeps();\n const base = { fs, store, session, engineLock, urlBuilder };\n const hosted = createExportTool({ ...base, allowOutPath: false });\n const local = createExportTool({ ...base, allowOutPath: true });\n const props = (s: unknown) =>\n (s as { properties: Record<string, unknown> }).properties;\n expect(props(hosted.inputSchema)).not.toHaveProperty('outPath');\n expect(props(local.inputSchema)).toHaveProperty('outPath');\n });\n\n it('rejects target=scene (moved to the save tool)', async () => {\n const { fs, store, session } = makeDeps();\n const root = await store.write({ scene: 'S', parent: null, title: 'T' });\n const tool = createExportTool({\n fs,\n store,\n session,\n engineLock,\n urlBuilder\n });\n const result = await tool.call({\n target: 'scene',\n format: 'scene',\n revision: root.id,\n blockId: 5\n });\n expect(result.isError).toBe(true);\n expect((result.content[0] as { text: string }).text).toMatch(\n /invalid format/i\n );\n });\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../../../test/tools/index.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildDesignToolRegistrations } from '../../src/tools/index.js';
3
+ // The tool factories only close over their deps (they touch them on `call`,
4
+ // not at construction), so stub primitives are enough to assert which tools
5
+ // get registered.
6
+ const stub = {};
7
+ function gateway(available) {
8
+ return {
9
+ available,
10
+ generateImage: async () => ({
11
+ bytes: new Uint8Array(),
12
+ mimeType: 'image/png'
13
+ })
14
+ };
15
+ }
16
+ function toolNames(available) {
17
+ return buildDesignToolRegistrations({
18
+ fs: stub,
19
+ store: stub,
20
+ session: stub,
21
+ engineLock: stub,
22
+ gateway: gateway(available),
23
+ urlBuilder: stub,
24
+ allowOutPath: true
25
+ }).map((t) => t.name);
26
+ }
27
+ // asset_generate is hidden for the beta via the ASSET_GENERATE_ENABLED
28
+ // kill-switch in src/tools/index.ts, so it is omitted regardless of gateway
29
+ // availability. When the feature ships and the flag flips back to true, the
30
+ // "available" case should assert it IS registered again.
31
+ describe('buildDesignToolRegistrations — asset_generate gating (beta: hidden)', () => {
32
+ const alwaysOn = [
33
+ 'edit',
34
+ 'preview',
35
+ 'export',
36
+ 'save',
37
+ 'list',
38
+ 'history',
39
+ 'inspect',
40
+ 'rename',
41
+ 'open_editor',
42
+ 'asset_search'
43
+ ];
44
+ it('omits asset_generate when the gateway is unavailable (no API_KEY)', () => {
45
+ const names = toolNames(false);
46
+ expect(names).not.toContain('asset_generate');
47
+ expect(names).toEqual(alwaysOn);
48
+ });
49
+ it('stays hidden even when the gateway is available (beta kill-switch)', () => {
50
+ const names = toolNames(true);
51
+ expect(names).not.toContain('asset_generate');
52
+ expect(names).toEqual(alwaysOn);
53
+ });
54
+ });
55
+ //# sourceMappingURL=index.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../../../test/tools/index.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAC;AAExE,4EAA4E;AAC5E,4EAA4E;AAC5E,kBAAkB;AAClB,MAAM,IAAI,GAAG,EAAW,CAAC;AAEzB,SAAS,OAAO,CAAC,SAAkB;IACjC,OAAO;QACL,SAAS;QACT,aAAa,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;YAC1B,KAAK,EAAE,IAAI,UAAU,EAAE;YACvB,QAAQ,EAAE,WAAW;SACtB,CAAC;KACH,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,SAAkB;IACnC,OAAO,4BAA4B,CAAC;QAClC,EAAE,EAAE,IAAI;QACR,KAAK,EAAE,IAAI;QACX,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,IAAI;QAChB,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC;QAC3B,UAAU,EAAE,IAAI;QAChB,YAAY,EAAE,IAAI;KACnB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,uEAAuE;AACvE,4EAA4E;AAC5E,4EAA4E;AAC5E,yDAAyD;AACzD,QAAQ,CAAC,qEAAqE,EAAE,GAAG,EAAE;IACnF,MAAM,QAAQ,GAAG;QACf,MAAM;QACN,SAAS;QACT,QAAQ;QACR,MAAM;QACN,MAAM;QACN,SAAS;QACT,SAAS;QACT,QAAQ;QACR,aAAa;QACb,cAAc;KACf,CAAC;IAEF,EAAE,CAAC,mEAAmE,EAAE,GAAG,EAAE;QAC3E,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;QAC/B,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;QAC9C,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oEAAoE,EAAE,GAAG,EAAE;QAC5E,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;QAC9C,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, expect, it } from 'vitest';\nimport type { GatewayClient } from '../../src/gateway/types.js';\nimport { buildDesignToolRegistrations } from '../../src/tools/index.js';\n\n// The tool factories only close over their deps (they touch them on `call`,\n// not at construction), so stub primitives are enough to assert which tools\n// get registered.\nconst stub = {} as never;\n\nfunction gateway(available: boolean): GatewayClient {\n return {\n available,\n generateImage: async () => ({\n bytes: new Uint8Array(),\n mimeType: 'image/png'\n })\n };\n}\n\nfunction toolNames(available: boolean): string[] {\n return buildDesignToolRegistrations({\n fs: stub,\n store: stub,\n session: stub,\n engineLock: stub,\n gateway: gateway(available),\n urlBuilder: stub,\n allowOutPath: true\n }).map((t) => t.name);\n}\n\n// asset_generate is hidden for the beta via the ASSET_GENERATE_ENABLED\n// kill-switch in src/tools/index.ts, so it is omitted regardless of gateway\n// availability. When the feature ships and the flag flips back to true, the\n// \"available\" case should assert it IS registered again.\ndescribe('buildDesignToolRegistrations — asset_generate gating (beta: hidden)', () => {\n const alwaysOn = [\n 'edit',\n 'preview',\n 'export',\n 'save',\n 'list',\n 'history',\n 'inspect',\n 'rename',\n 'open_editor',\n 'asset_search'\n ];\n\n it('omits asset_generate when the gateway is unavailable (no API_KEY)', () => {\n const names = toolNames(false);\n expect(names).not.toContain('asset_generate');\n expect(names).toEqual(alwaysOn);\n });\n\n it('stays hidden even when the gateway is available (beta kill-switch)', () => {\n const names = toolNames(true);\n expect(names).not.toContain('asset_generate');\n expect(names).toEqual(alwaysOn);\n });\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=metrics.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.test.d.ts","sourceRoot":"","sources":["../../../../test/tools/metrics.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { MetricsRecorder } from '../../src/metrics/recorder.js';
3
+ import { createMetricsTool } from '../../src/tools/metrics.js';
4
+ describe('metrics tool', () => {
5
+ it('returns the recorder snapshot plus live session facts', async () => {
6
+ const recorder = new MetricsRecorder();
7
+ recorder.record({ tool: 'edit', durationMs: 120, ok: true });
8
+ recorder.record({ tool: 'edit', durationMs: 80, ok: false });
9
+ const tool = createMetricsTool({
10
+ recorder,
11
+ activeSessions: () => 3,
12
+ maxSessions: 5
13
+ });
14
+ expect(tool.name).toBe('metrics');
15
+ const result = await tool.call({});
16
+ const first = result.content[0];
17
+ if (first.type !== 'text')
18
+ throw new Error('expected text content');
19
+ const body = JSON.parse(first.text);
20
+ const edit = body.tools.find((t) => t.tool === 'edit');
21
+ expect(edit?.count).toBe(2);
22
+ expect(edit?.errorCount).toBe(1);
23
+ expect(body.recent).toHaveLength(2);
24
+ expect(body.activeSessions).toBe(3);
25
+ expect(body.maxSessions).toBe(5);
26
+ expect(body.uptimeMs).toBeGreaterThan(0);
27
+ });
28
+ });
29
+ //# sourceMappingURL=metrics.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.test.js","sourceRoot":"","sources":["../../../../test/tools/metrics.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAE/D,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;QACrE,MAAM,QAAQ,GAAG,IAAI,eAAe,EAAE,CAAC;QACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7D,MAAM,IAAI,GAAG,iBAAiB,CAAC;YAC7B,QAAQ;YACR,cAAc,EAAE,GAAG,EAAE,CAAC,CAAC;YACvB,WAAW,EAAE,CAAC;SACf,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAElC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACnC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAChC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;QACpE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAMjC,CAAC;QACF,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;QACvD,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC5B,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACpC,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACpC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, expect, it } from 'vitest';\nimport { MetricsRecorder } from '../../src/metrics/recorder.js';\nimport { createMetricsTool } from '../../src/tools/metrics.js';\n\ndescribe('metrics tool', () => {\n it('returns the recorder snapshot plus live session facts', async () => {\n const recorder = new MetricsRecorder();\n recorder.record({ tool: 'edit', durationMs: 120, ok: true });\n recorder.record({ tool: 'edit', durationMs: 80, ok: false });\n const tool = createMetricsTool({\n recorder,\n activeSessions: () => 3,\n maxSessions: 5\n });\n expect(tool.name).toBe('metrics');\n\n const result = await tool.call({});\n const first = result.content[0];\n if (first.type !== 'text') throw new Error('expected text content');\n const body = JSON.parse(first.text) as {\n tools: Array<{ tool: string; count: number; errorCount: number }>;\n recent: unknown[];\n activeSessions: number;\n maxSessions: number;\n uptimeMs: number;\n };\n const edit = body.tools.find((t) => t.tool === 'edit');\n expect(edit?.count).toBe(2);\n expect(edit?.errorCount).toBe(1);\n expect(body.recent).toHaveLength(2);\n expect(body.activeSessions).toBe(3);\n expect(body.maxSessions).toBe(5);\n expect(body.uptimeMs).toBeGreaterThan(0);\n });\n});\n"]}