@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,8 +1,10 @@
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 { createPreviewTool } from '../../src/tools/preview.js';
5
6
  import { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';
7
+ const engineLock = new Mutex();
6
8
  function makeDeps() {
7
9
  const fs = new InMemoryWorkspaceFS();
8
10
  const store = new RevisionStore(fs);
@@ -31,7 +33,7 @@ describe('preview tool', () => {
31
33
  const slot = await session.acquire(null);
32
34
  session.commit(slot, 'rev_warm');
33
35
  fakeEngine._scene = 'WARM';
34
- const tool = createPreviewTool({ store, session });
36
+ const tool = createPreviewTool({ store, session, engineLock });
35
37
  const result = await tool.call({ blockId: 42 });
36
38
  expect(result.isError).toBeFalsy();
37
39
  expect(result.content[0].type).toBe('image');
@@ -41,7 +43,7 @@ describe('preview tool', () => {
41
43
  });
42
44
  it('returns isError when no revision supplied AND no warm slot exists', async () => {
43
45
  const { store, session } = makeDeps();
44
- const tool = createPreviewTool({ store, session });
46
+ const tool = createPreviewTool({ store, session, engineLock });
45
47
  const result = await tool.call({ blockId: 42 });
46
48
  expect(result.isError).toBe(true);
47
49
  expect(result.content[0].text).toMatch(/no.*engine.*state/i);
@@ -53,7 +55,7 @@ describe('preview tool', () => {
53
55
  parent: null,
54
56
  title: 'T'
55
57
  });
56
- const tool = createPreviewTool({ store, session });
58
+ const tool = createPreviewTool({ store, session, engineLock });
57
59
  // adapter.loadFromString is set as a plain async fn (not a vi.fn) above, so wrap it
58
60
  const loadSpy = vi.spyOn(adapter, 'loadFromString');
59
61
  await tool.call({ blockId: 42, revision: root.id });
@@ -63,7 +65,7 @@ describe('preview tool', () => {
63
65
  const { store, session, fakeEngine } = makeDeps();
64
66
  const slot = await session.acquire(null);
65
67
  session.commit(slot, 'rev_warm');
66
- const tool = createPreviewTool({ store, session });
68
+ const tool = createPreviewTool({ store, session, engineLock });
67
69
  await tool.call({ blockId: 42, width: 9999, height: 9999 });
68
70
  const call = fakeEngine.block.export.mock
69
71
  .calls[0];
@@ -1 +1 @@
1
- {"version":3,"file":"preview.test.js","sourceRoot":"","sources":["../../../../test/tools/preview.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 { createPreviewTool } from '../../src/tools/preview.js';\nimport { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';\n\ninterface FakeEngine {\n _scene: string;\n block: { export: ReturnType<typeof vi.fn> };\n}\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 (_id: number, _mime: string) =>\n new Blob([new Uint8Array([1, 2, 3, 4])])\n )\n }\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, adapter };\n}\n\ndescribe('preview tool', () => {\n it('renders the active engine state when no revision is supplied', async () => {\n const { store, session, fakeEngine } = makeDeps();\n const slot = await session.acquire(null);\n session.commit(slot, 'rev_warm');\n fakeEngine._scene = 'WARM';\n const tool = createPreviewTool({ store, session });\n const result = await tool.call({ blockId: 42 });\n expect(result.isError).toBeFalsy();\n expect((result.content[0] as { type: string }).type).toBe('image');\n expect(fakeEngine.block.export).toHaveBeenCalledWith(\n 42,\n 'image/webp',\n expect.any(Object)\n );\n // Engine state is preserved (NOT reset) when no revision is supplied.\n expect(fakeEngine._scene).toBe('WARM');\n });\n\n it('returns isError when no revision supplied AND no warm slot exists', async () => {\n const { store, session } = makeDeps();\n const tool = createPreviewTool({ store, session });\n const result = await tool.call({ blockId: 42 });\n expect(result.isError).toBe(true);\n expect((result.content[0] as { text: string }).text).toMatch(\n /no.*engine.*state/i\n );\n });\n\n it('loads a revision into the engine when `revision` is supplied', async () => {\n const { store, session, adapter } = makeDeps();\n const root = await store.write({\n scene: 'SCENE_FOR_PREVIEW',\n parent: null,\n title: 'T'\n });\n const tool = createPreviewTool({ store, session });\n // adapter.loadFromString is set as a plain async fn (not a vi.fn) above, so wrap it\n const loadSpy = vi.spyOn(adapter, 'loadFromString');\n await tool.call({ blockId: 42, revision: root.id });\n expect(loadSpy).toHaveBeenCalledWith(\n expect.anything(),\n 'SCENE_FOR_PREVIEW'\n );\n });\n\n it('clamps width/height to 1800 px', async () => {\n const { store, session, fakeEngine } = makeDeps();\n const slot = await session.acquire(null);\n session.commit(slot, 'rev_warm');\n const tool = createPreviewTool({ store, session });\n await tool.call({ blockId: 42, width: 9999, height: 9999 });\n const call = (fakeEngine.block.export as ReturnType<typeof vi.fn>).mock\n .calls[0];\n expect(call[2]).toMatchObject({ targetWidth: 1800, targetHeight: 1800 });\n });\n});\n"]}
1
+ {"version":3,"file":"preview.test.js","sourceRoot":"","sources":["../../../../test/tools/preview.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 { createPreviewTool } from '../../src/tools/preview.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}\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 (_id: number, _mime: string) =>\n new Blob([new Uint8Array([1, 2, 3, 4])])\n )\n }\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, adapter };\n}\n\ndescribe('preview tool', () => {\n it('renders the active engine state when no revision is supplied', async () => {\n const { store, session, fakeEngine } = makeDeps();\n const slot = await session.acquire(null);\n session.commit(slot, 'rev_warm');\n fakeEngine._scene = 'WARM';\n const tool = createPreviewTool({ store, session, engineLock });\n const result = await tool.call({ blockId: 42 });\n expect(result.isError).toBeFalsy();\n expect((result.content[0] as { type: string }).type).toBe('image');\n expect(fakeEngine.block.export).toHaveBeenCalledWith(\n 42,\n 'image/webp',\n expect.any(Object)\n );\n // Engine state is preserved (NOT reset) when no revision is supplied.\n expect(fakeEngine._scene).toBe('WARM');\n });\n\n it('returns isError when no revision supplied AND no warm slot exists', async () => {\n const { store, session } = makeDeps();\n const tool = createPreviewTool({ store, session, engineLock });\n const result = await tool.call({ blockId: 42 });\n expect(result.isError).toBe(true);\n expect((result.content[0] as { text: string }).text).toMatch(\n /no.*engine.*state/i\n );\n });\n\n it('loads a revision into the engine when `revision` is supplied', async () => {\n const { store, session, adapter } = makeDeps();\n const root = await store.write({\n scene: 'SCENE_FOR_PREVIEW',\n parent: null,\n title: 'T'\n });\n const tool = createPreviewTool({ store, session, engineLock });\n // adapter.loadFromString is set as a plain async fn (not a vi.fn) above, so wrap it\n const loadSpy = vi.spyOn(adapter, 'loadFromString');\n await tool.call({ blockId: 42, revision: root.id });\n expect(loadSpy).toHaveBeenCalledWith(\n expect.anything(),\n 'SCENE_FOR_PREVIEW'\n );\n });\n\n it('clamps width/height to 1800 px', async () => {\n const { store, session, fakeEngine } = makeDeps();\n const slot = await session.acquire(null);\n session.commit(slot, 'rev_warm');\n const tool = createPreviewTool({ store, session, engineLock });\n await tool.call({ blockId: 42, width: 9999, height: 9999 });\n const call = (fakeEngine.block.export as ReturnType<typeof vi.fn>).mock\n .calls[0];\n expect(call[2]).toMatchObject({ targetWidth: 1800, targetHeight: 1800 });\n });\n});\n"]}
@@ -10,8 +10,7 @@ describe('rename tool', () => {
10
10
  const tool = createRenameTool({ store });
11
11
  const result = await tool.call({ revision: r2.id, title: 'New' });
12
12
  expect(result.content[0].text).toContain('ok');
13
- const root = await store.read(r1.id);
14
- expect(root.meta.title).toBe('New');
13
+ expect(await store.currentTitle(r1.id)).toBe('New');
15
14
  });
16
15
  });
17
16
  //# sourceMappingURL=rename.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rename.test.js","sourceRoot":"","sources":["../../../../test/tools/rename.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAEvE,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;QAClE,MAAM,KAAK,GAAG,IAAI,aAAa,CAAC,IAAI,mBAAmB,EAAE,CAAC,CAAC;QAC3D,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QACzE,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5D,MAAM,IAAI,GAAG,gBAAgB,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACzC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAClE,MAAM,CAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAsB,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACrE,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QACrC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, expect, it } from 'vitest';\nimport { RevisionStore } from '../../src/revisions/store.js';\nimport { createRenameTool } from '../../src/tools/rename.js';\nimport { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';\n\ndescribe('rename tool', () => {\n it('renames the root reachable from the given revision', async () => {\n const store = new RevisionStore(new InMemoryWorkspaceFS());\n const r1 = await store.write({ scene: 'a', parent: null, title: 'Old' });\n const r2 = await store.write({ scene: 'b', parent: r1.id });\n const tool = createRenameTool({ store });\n const result = await tool.call({ revision: r2.id, title: 'New' });\n expect((result.content[0] as { text: string }).text).toContain('ok');\n const root = await store.read(r1.id);\n expect(root.meta.title).toBe('New');\n });\n});\n"]}
1
+ {"version":3,"file":"rename.test.js","sourceRoot":"","sources":["../../../../test/tools/rename.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAEvE,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;QAClE,MAAM,KAAK,GAAG,IAAI,aAAa,CAAC,IAAI,mBAAmB,EAAE,CAAC,CAAC;QAC3D,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QACzE,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5D,MAAM,IAAI,GAAG,gBAAgB,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACzC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAClE,MAAM,CAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAsB,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACrE,MAAM,CAAC,MAAM,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, expect, it } from 'vitest';\nimport { RevisionStore } from '../../src/revisions/store.js';\nimport { createRenameTool } from '../../src/tools/rename.js';\nimport { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';\n\ndescribe('rename tool', () => {\n it('renames the root reachable from the given revision', async () => {\n const store = new RevisionStore(new InMemoryWorkspaceFS());\n const r1 = await store.write({ scene: 'a', parent: null, title: 'Old' });\n const r2 = await store.write({ scene: 'b', parent: r1.id });\n const tool = createRenameTool({ store });\n const result = await tool.call({ revision: r2.id, title: 'New' });\n expect((result.content[0] as { text: string }).text).toContain('ok');\n expect(await store.currentTitle(r1.id)).toBe('New');\n });\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=save.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"save.test.d.ts","sourceRoot":"","sources":["../../../../test/tools/save.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,137 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { Mutex } from '../../src/engine/mutex.js';
3
+ import { EngineSession } from '../../src/engine/session.js';
4
+ import { RevisionStore } from '../../src/revisions/store.js';
5
+ import { createSaveTool } from '../../src/tools/save.js';
6
+ import { LocalViewerUrlBuilder } from '../../src/viewer/url-builder.js';
7
+ import { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';
8
+ const engineLock = new Mutex();
9
+ const urlBuilder = new LocalViewerUrlBuilder({ base: 'http://127.0.0.1:9000' });
10
+ function makeDeps() {
11
+ const fs = new InMemoryWorkspaceFS();
12
+ const store = new RevisionStore(fs);
13
+ const fakeEngine = {
14
+ _scene: '',
15
+ _resourcesLoaded: false,
16
+ scene: {
17
+ getPages: vi.fn(() => [10]),
18
+ // The archive bundles the asset marker ONLY if resources were loaded
19
+ // first — mirrors saveToArchive's "assets accessible when called" contract.
20
+ saveToArchive: vi.fn(async () => {
21
+ const body = fakeEngine._resourcesLoaded ? 'ZIP+ASSET' : 'ZIP';
22
+ return new Blob([new TextEncoder().encode(body)]);
23
+ })
24
+ },
25
+ block: {
26
+ forceLoadResources: vi.fn(async () => {
27
+ fakeEngine._resourcesLoaded = true;
28
+ })
29
+ }
30
+ };
31
+ const adapter = {
32
+ create: () => fakeEngine,
33
+ reset: async (e) => {
34
+ e._scene = 'EMPTY';
35
+ },
36
+ loadFromString: async (e, s) => {
37
+ e._scene = s;
38
+ },
39
+ saveToString: async (e) => e._scene
40
+ };
41
+ const session = new EngineSession({ size: 1, adapter });
42
+ return { fs, store, session, fakeEngine };
43
+ }
44
+ describe('save tool', () => {
45
+ it('writes archive.zip and returns uri + httpUrl, no path', async () => {
46
+ const { fs, store, session } = makeDeps();
47
+ const root = await store.write({
48
+ scene: 'SCENE_ABC',
49
+ parent: null,
50
+ title: 'T'
51
+ });
52
+ const tool = createSaveTool({ fs, store, session, engineLock, urlBuilder });
53
+ const result = await tool.call({ revision: root.id });
54
+ const body = JSON.parse(result.content[0].text);
55
+ const shard = root.id.slice(0, 2);
56
+ const rest = root.id.slice(2);
57
+ expect(body.uri).toBe(`workspace://exports/${shard}/${rest}/archive.zip`);
58
+ expect(body.httpUrl).toBe(`http://127.0.0.1:9000/r/exports/${shard}/${rest}/archive.zip`);
59
+ expect(body.path).toBeUndefined();
60
+ expect(body.format).toBe('archive');
61
+ expect(body.revision).toBe(root.id);
62
+ expect(await fs.exists(`exports/${shard}/${rest}/archive.zip`)).toBe(true);
63
+ });
64
+ it('respects local `outPath` escape hatch — returns path, not httpUrl', async () => {
65
+ const { fs, store, session } = makeDeps();
66
+ const root = await store.write({ scene: 'S', parent: null, title: 'T' });
67
+ const { mkdtemp, rm } = await import('node:fs/promises');
68
+ const { tmpdir } = await import('node:os');
69
+ const { join } = await import('node:path');
70
+ const dir = await mkdtemp(join(tmpdir(), 'save-out-'));
71
+ const outPath = join(dir, 'design.zip');
72
+ const tool = createSaveTool({
73
+ fs,
74
+ store,
75
+ session,
76
+ engineLock,
77
+ urlBuilder,
78
+ allowOutPath: true
79
+ });
80
+ const result = await tool.call({ revision: root.id, outPath });
81
+ const body = JSON.parse(result.content[0].text);
82
+ expect(body.path).toBe(outPath);
83
+ expect(body.httpUrl).toBeUndefined();
84
+ await rm(dir, { recursive: true });
85
+ });
86
+ it('rejects `outPath` when not allowed (Hosted/sandbox)', async () => {
87
+ const { fs, store, session } = makeDeps();
88
+ const root = await store.write({ scene: 'S', parent: null, title: 'T' });
89
+ const tool = createSaveTool({
90
+ fs,
91
+ store,
92
+ session,
93
+ engineLock,
94
+ urlBuilder,
95
+ allowOutPath: false
96
+ });
97
+ const result = await tool.call({
98
+ revision: root.id,
99
+ outPath: '/tmp/x.zip'
100
+ });
101
+ expect(result.isError).toBe(true);
102
+ expect(result.content[0].text).toMatch(/outPath.*not.*supported/i);
103
+ });
104
+ it('omits `outPath` from the schema when not allowed (Hosted), advertises it when allowed (Local)', () => {
105
+ const { fs, store, session } = makeDeps();
106
+ const base = { fs, store, session, engineLock, urlBuilder };
107
+ const hosted = createSaveTool({ ...base, allowOutPath: false });
108
+ const local = createSaveTool({ ...base, allowOutPath: true });
109
+ const props = (s) => s.properties;
110
+ expect(props(hosted.inputSchema)).not.toHaveProperty('outPath');
111
+ expect(props(local.inputSchema)).toHaveProperty('outPath');
112
+ });
113
+ it('errors when revision is missing', async () => {
114
+ const { fs, store, session } = makeDeps();
115
+ const tool = createSaveTool({ fs, store, session, engineLock, urlBuilder });
116
+ const result = await tool.call({});
117
+ expect(result.isError).toBe(true);
118
+ expect(result.content[0].text).toMatch(/revision/i);
119
+ });
120
+ it('force-loads resources before archiving so assets are bundled', async () => {
121
+ const { fs, store, session, fakeEngine } = makeDeps();
122
+ const root = await store.write({ scene: 'S', parent: null, title: 'T' });
123
+ const tool = createSaveTool({ fs, store, session, engineLock, urlBuilder });
124
+ await tool.call({ revision: root.id });
125
+ // forceLoadResources must run with the scene's pages...
126
+ expect(fakeEngine.block.forceLoadResources).toHaveBeenCalledWith([10]);
127
+ // ...and BEFORE saveToArchive.
128
+ const callOrder = (m) => m.mock.invocationCallOrder[0];
129
+ expect(callOrder(fakeEngine.block.forceLoadResources)).toBeLessThan(callOrder(fakeEngine.scene.saveToArchive));
130
+ // ...so the produced archive actually contains the asset bytes.
131
+ const shard = root.id.slice(0, 2);
132
+ const rest = root.id.slice(2);
133
+ const zip = await fs.readFile(`exports/${shard}/${rest}/archive.zip`, 'utf8');
134
+ expect(zip).toContain('ASSET');
135
+ });
136
+ });
137
+ //# sourceMappingURL=save.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"save.test.js","sourceRoot":"","sources":["../../../../test/tools/save.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 { createSaveTool } from '../../src/tools/save.js';\nimport { LocalViewerUrlBuilder } from '../../src/viewer/url-builder.js';\nimport { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';\n\nconst engineLock = new Mutex();\nconst urlBuilder = new LocalViewerUrlBuilder({ base: 'http://127.0.0.1:9000' });\n\ninterface FakeEngine {\n _scene: string;\n _resourcesLoaded: boolean;\n scene: {\n getPages: ReturnType<typeof vi.fn>;\n saveToArchive: ReturnType<typeof vi.fn>;\n };\n block: { forceLoadResources: ReturnType<typeof vi.fn> };\n}\n\nfunction makeDeps() {\n const fs = new InMemoryWorkspaceFS();\n const store = new RevisionStore(fs);\n const fakeEngine: FakeEngine = {\n _scene: '',\n _resourcesLoaded: false,\n scene: {\n getPages: vi.fn(() => [10]),\n // The archive bundles the asset marker ONLY if resources were loaded\n // first — mirrors saveToArchive's \"assets accessible when called\" contract.\n saveToArchive: vi.fn(async () => {\n const body = fakeEngine._resourcesLoaded ? 'ZIP+ASSET' : 'ZIP';\n return new Blob([new TextEncoder().encode(body)]);\n })\n },\n block: {\n forceLoadResources: vi.fn(async () => {\n fakeEngine._resourcesLoaded = true;\n })\n }\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('save tool', () => {\n it('writes archive.zip 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 = createSaveTool({ fs, store, session, engineLock, urlBuilder });\n const result = await tool.call({ revision: root.id });\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 format: string;\n revision: 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}/archive.zip`);\n expect(body.httpUrl).toBe(\n `http://127.0.0.1:9000/r/exports/${shard}/${rest}/archive.zip`\n );\n expect(body.path).toBeUndefined();\n expect(body.format).toBe('archive');\n expect(body.revision).toBe(root.id);\n expect(await fs.exists(`exports/${shard}/${rest}/archive.zip`)).toBe(true);\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({ scene: 'S', parent: null, title: 'T' });\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(), 'save-out-'));\n const outPath = join(dir, 'design.zip');\n const tool = createSaveTool({\n fs,\n store,\n session,\n engineLock,\n urlBuilder,\n allowOutPath: true\n });\n const result = await tool.call({ revision: root.id, outPath });\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 = createSaveTool({\n fs,\n store,\n session,\n engineLock,\n urlBuilder,\n allowOutPath: false\n });\n const result = await tool.call({\n revision: root.id,\n outPath: '/tmp/x.zip'\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 = createSaveTool({ ...base, allowOutPath: false });\n const local = createSaveTool({ ...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('errors when revision is missing', async () => {\n const { fs, store, session } = makeDeps();\n const tool = createSaveTool({ fs, store, session, engineLock, urlBuilder });\n const result = await tool.call({});\n expect(result.isError).toBe(true);\n expect((result.content[0] as { text: string }).text).toMatch(/revision/i);\n });\n\n it('force-loads resources before archiving so assets are bundled', async () => {\n const { fs, store, session, fakeEngine } = makeDeps();\n const root = await store.write({ scene: 'S', parent: null, title: 'T' });\n const tool = createSaveTool({ fs, store, session, engineLock, urlBuilder });\n await tool.call({ revision: root.id });\n\n // forceLoadResources must run with the scene's pages...\n expect(fakeEngine.block.forceLoadResources).toHaveBeenCalledWith([10]);\n // ...and BEFORE saveToArchive.\n const callOrder = (m: ReturnType<typeof vi.fn>): number =>\n m.mock.invocationCallOrder[0];\n expect(callOrder(fakeEngine.block.forceLoadResources)).toBeLessThan(\n callOrder(fakeEngine.scene.saveToArchive)\n );\n\n // ...so the produced archive actually contains the asset bytes.\n const shard = root.id.slice(0, 2);\n const rest = root.id.slice(2);\n const zip = await fs.readFile(\n `exports/${shard}/${rest}/archive.zip`,\n 'utf8'\n );\n expect(zip).toContain('ASSET');\n });\n});\n"]}
@@ -13,6 +13,18 @@ async function makeApp() {
13
13
  app.use(createViewerRouter({ store, broadcaster, fs }));
14
14
  return { app, fs, store, broadcaster };
15
15
  }
16
+ // The HTTP transport mounts the router under "/viewer"; the stdio listener
17
+ // mounts it at root. The SPA must address its assets/API relative to the live
18
+ // mount path, injected at serve time. This mounts under a prefix to lock that
19
+ // in.
20
+ async function makeMountedApp(mount) {
21
+ const fs = new InMemoryWorkspaceFS();
22
+ const store = new RevisionStore(fs);
23
+ const broadcaster = new SseBroadcaster();
24
+ const app = express();
25
+ app.use(mount, createViewerRouter({ store, broadcaster, fs }));
26
+ return { app, fs, store, broadcaster };
27
+ }
16
28
  describe('viewer router', () => {
17
29
  it('GET / serves the studio HTML shell', async () => {
18
30
  const { app } = await makeApp();
@@ -73,6 +85,14 @@ describe('viewer router', () => {
73
85
  title: 'T'
74
86
  });
75
87
  });
88
+ it('GET /snapshot/<rev> reflects a renamed title (live, not creation-time)', async () => {
89
+ const { app, store } = await makeApp();
90
+ const r = await store.write({ scene: 'S', parent: null, title: 'Before' });
91
+ await store.renameRoot(r.id, 'After');
92
+ const res = await request(app).get(`/snapshot/${r.id}`);
93
+ expect(res.status).toBe(200);
94
+ expect(res.body.title).toBe('After');
95
+ });
76
96
  it('GET /snapshot/<rev>/thumbnail returns the per-revision webp if present', async () => {
77
97
  const { app, store, fs } = await makeApp();
78
98
  const r = await store.write({ scene: 'S', parent: null, title: 'T' });
@@ -151,6 +171,42 @@ describe('viewer router', () => {
151
171
  expect(res.status).toBe(200);
152
172
  expect(res.body.resourceBase).toBe('/r/');
153
173
  });
174
+ it('serves a root-relative SPA base when mounted at root', async () => {
175
+ const { app, store } = await makeApp();
176
+ const r = await store.write({ scene: 'S', parent: null, title: 'T' });
177
+ const res = await request(app).get(`/d/${r.id}`);
178
+ expect(res.text).toContain("window.__VIEWER_BASE__ = '';");
179
+ expect(res.text).toContain('src="/static/viewer.js"');
180
+ });
181
+ it('injects the mount prefix into the SPA base when mounted at /viewer', async () => {
182
+ const { app, store } = await makeMountedApp('/viewer');
183
+ const r = await store.write({ scene: 'S', parent: null, title: 'T' });
184
+ const res = await request(app).get(`/viewer/d/${r.id}`);
185
+ expect(res.status).toBe(200);
186
+ expect(res.text).toContain("window.__VIEWER_BASE__ = '/viewer';");
187
+ expect(res.text).toContain('src="/viewer/static/viewer.js"');
188
+ // No leftover unprefixed asset path that would 404 under the mount.
189
+ expect(res.text).not.toContain('src="/static/viewer.js"');
190
+ });
191
+ it('studio shell carries the mount prefix too when mounted at /viewer', async () => {
192
+ const { app } = await makeMountedApp('/viewer');
193
+ const res = await request(app).get('/viewer/');
194
+ expect(res.status).toBe(200);
195
+ expect(res.text).toContain("window.__VIEWER_BASE__ = '/viewer';");
196
+ expect(res.text).toContain('src="/viewer/static/studio.js"');
197
+ });
198
+ it('resourceBase is mount-relative when mounted at /viewer', async () => {
199
+ const { app } = await makeMountedApp('/viewer');
200
+ const res = await request(app).get('/viewer/api/config');
201
+ expect(res.status).toBe(200);
202
+ expect(res.body.resourceBase).toBe('/viewer/r/');
203
+ });
204
+ it('serves the SPA JS under the mount prefix', async () => {
205
+ const { app } = await makeMountedApp('/viewer');
206
+ const res = await request(app).get('/viewer/static/viewer.js');
207
+ expect(res.status).toBe(200);
208
+ expect(res.headers['content-type']).toMatch(/javascript/);
209
+ });
154
210
  it('GET /r/<asset> is reachable through the viewer router', async () => {
155
211
  const { app, fs } = await makeApp();
156
212
  await fs.writeFile('assets/abc.png', new Uint8Array([1, 2, 3]));
@@ -1 +1 @@
1
- {"version":3,"file":"router.test.js","sourceRoot":"","sources":["../../../../test/viewer/router.test.ts"],"names":[],"mappings":"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","sourcesContent":["import express from 'express';\nimport request from 'supertest';\nimport { describe, expect, it } from 'vitest';\nimport { RevisionStore } from '../../src/revisions/store.js';\nimport { SseBroadcaster } from '../../src/viewer/broadcaster.js';\nimport { createViewerRouter } from '../../src/viewer/router.js';\nimport { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';\n\nasync function makeApp() {\n const fs = new InMemoryWorkspaceFS();\n const store = new RevisionStore(fs);\n const broadcaster = new SseBroadcaster();\n const app = express();\n app.use(createViewerRouter({ store, broadcaster, fs }));\n return { app, fs, store, broadcaster };\n}\n\ndescribe('viewer router', () => {\n it('GET / serves the studio HTML shell', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/');\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/html/);\n expect(res.text).toContain('IMG.LY CODESIGN MCP Workspace');\n });\n\n it('GET /d/<root> serves the viewer HTML shell', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const res = await request(app).get(`/d/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/html/);\n expect(res.text).toContain('codesign viewer');\n });\n\n it('GET /d/<root>/<rev> serves the viewer HTML shell', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const res = await request(app).get(`/d/${r.id}/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/html/);\n expect(res.text).toContain('codesign viewer');\n });\n\n it('GET /s/<rev> is removed — returns 404', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const res = await request(app).get(`/s/${r.id}`);\n expect(res.status).toBe(404);\n });\n\n it('GET /static/viewer.js serves the SPA JS', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/static/viewer.js');\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/javascript/);\n expect(res.text).toContain('CreativeEditorSDK');\n expect(res.text).toContain('setURIResolver');\n });\n\n it('GET /static/revision-panel.js serves the panel module', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/static/revision-panel.js');\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/javascript/);\n expect(res.text).toContain('createRevisionPanel');\n });\n\n it('GET /snapshot/<rev> returns scene + meta + rootRevision', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({\n scene: 'SCENE_BYTES',\n parent: null,\n title: 'T'\n });\n const res = await request(app).get(`/snapshot/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.body).toMatchObject({\n scene: 'SCENE_BYTES',\n rootRevision: r.id,\n title: 'T'\n });\n });\n\n it('GET /snapshot/<rev>/thumbnail returns the per-revision webp if present', async () => {\n const { app, store, fs } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const shard = r.id.slice(0, 2);\n const rest = r.id.slice(2);\n const bytes = new Uint8Array([1, 2, 3, 4]);\n await fs.writeFile(`exports/${shard}/${rest}/preview.webp`, bytes);\n const res = await request(app).get(`/snapshot/${r.id}/thumbnail`);\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toBe('image/webp');\n });\n\n it('GET /api/designs returns the root list', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T1' });\n const res = await request(app).get('/api/designs');\n expect(res.status).toBe(200);\n expect(res.body).toEqual(\n expect.arrayContaining([\n expect.objectContaining({ rootRevision: r.id, title: 'T1' })\n ])\n );\n });\n\n it(\"GET /api/design/<root> returns that design's record\", async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T1' });\n const res = await request(app).get(`/api/design/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.body).toMatchObject({\n rootRevision: r.id,\n title: 'T1',\n latestLeaf: r.id\n });\n });\n\n it('GET /api/design/<unknown> returns 404', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/api/design/deadbeefdead');\n expect(res.status).toBe(404);\n expect(res.body).toEqual({ error: 'unknown design' });\n });\n\n it(\"GET /api/design/<root>/revisions returns the design's revision tree\", async () => {\n const { app, store } = await makeApp();\n const r1 = await store.write({ scene: 'a', parent: null, title: 'T' });\n const r2 = await store.write({ scene: 'b', parent: r1.id, note: 'second' });\n const fork = await store.write({\n scene: 'b2',\n parent: r1.id,\n note: 'fork'\n });\n const res = await request(app).get(`/api/design/${r1.id}/revisions`);\n expect(res.status).toBe(200);\n expect(res.body.map((n: { id: string }) => n.id).sort()).toEqual(\n [r1.id, r2.id, fork.id].sort()\n );\n expect(res.body).toEqual(\n expect.arrayContaining([\n expect.objectContaining({ id: r2.id, parent: r1.id, note: 'second' })\n ])\n );\n });\n\n it('GET /api/design/<unknown>/revisions returns 404', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/api/design/deadbeefdead/revisions');\n expect(res.status).toBe(404);\n expect(res.body).toEqual({ error: 'unknown design' });\n });\n\n it('a /api/design/<root>/revisions error is sanitised to a generic 500', async () => {\n const { app, store, fs } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n await fs.writeFile(\n `objects/${r.id.slice(0, 2)}/${r.id.slice(2)}.json`,\n 'not json'\n );\n const res = await request(app).get(`/api/design/${r.id}/revisions`);\n expect(res.status).toBe(500);\n expect(res.text).toBe('internal error');\n });\n\n it('GET /snapshot/<unknown> returns 404', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/snapshot/deadbeefdead');\n expect(res.status).toBe(404);\n });\n\n it('GET /api/config includes the /r/ resourceBase', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/api/config');\n expect(res.status).toBe(200);\n expect(res.body.resourceBase).toBe('/r/');\n });\n\n it('GET /r/<asset> is reachable through the viewer router', async () => {\n const { app, fs } = await makeApp();\n await fs.writeFile('assets/abc.png', new Uint8Array([1, 2, 3]));\n const res = await request(app).get('/r/assets/abc.png');\n expect(res.status).toBe(200);\n });\n\n it('a route error is sanitised to a generic 500 (no leak)', async () => {\n const { app, store, fs } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n // Corrupt the revision meta so store.read throws a non-ENOENT parse error.\n await fs.writeFile(\n `objects/${r.id.slice(0, 2)}/${r.id.slice(2)}.json`,\n 'not json'\n );\n const res = await request(app).get(`/snapshot/${r.id}`);\n expect(res.status).toBe(500);\n expect(res.text).toBe('internal error');\n expect(res.text).not.toContain('not json');\n });\n});\n"]}
1
+ {"version":3,"file":"router.test.js","sourceRoot":"","sources":["../../../../test/viewer/router.test.ts"],"names":[],"mappings":"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","sourcesContent":["import express from 'express';\nimport request from 'supertest';\nimport { describe, expect, it } from 'vitest';\nimport { RevisionStore } from '../../src/revisions/store.js';\nimport { SseBroadcaster } from '../../src/viewer/broadcaster.js';\nimport { createViewerRouter } from '../../src/viewer/router.js';\nimport { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';\n\nasync function makeApp() {\n const fs = new InMemoryWorkspaceFS();\n const store = new RevisionStore(fs);\n const broadcaster = new SseBroadcaster();\n const app = express();\n app.use(createViewerRouter({ store, broadcaster, fs }));\n return { app, fs, store, broadcaster };\n}\n\n// The HTTP transport mounts the router under \"/viewer\"; the stdio listener\n// mounts it at root. The SPA must address its assets/API relative to the live\n// mount path, injected at serve time. This mounts under a prefix to lock that\n// in.\nasync function makeMountedApp(mount: string) {\n const fs = new InMemoryWorkspaceFS();\n const store = new RevisionStore(fs);\n const broadcaster = new SseBroadcaster();\n const app = express();\n app.use(mount, createViewerRouter({ store, broadcaster, fs }));\n return { app, fs, store, broadcaster };\n}\n\ndescribe('viewer router', () => {\n it('GET / serves the studio HTML shell', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/');\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/html/);\n expect(res.text).toContain('IMG.LY CODESIGN MCP Workspace');\n });\n\n it('GET /d/<root> serves the viewer HTML shell', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const res = await request(app).get(`/d/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/html/);\n expect(res.text).toContain('codesign viewer');\n });\n\n it('GET /d/<root>/<rev> serves the viewer HTML shell', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const res = await request(app).get(`/d/${r.id}/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/html/);\n expect(res.text).toContain('codesign viewer');\n });\n\n it('GET /s/<rev> is removed — returns 404', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const res = await request(app).get(`/s/${r.id}`);\n expect(res.status).toBe(404);\n });\n\n it('GET /static/viewer.js serves the SPA JS', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/static/viewer.js');\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/javascript/);\n expect(res.text).toContain('CreativeEditorSDK');\n expect(res.text).toContain('setURIResolver');\n });\n\n it('GET /static/revision-panel.js serves the panel module', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/static/revision-panel.js');\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/javascript/);\n expect(res.text).toContain('createRevisionPanel');\n });\n\n it('GET /snapshot/<rev> returns scene + meta + rootRevision', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({\n scene: 'SCENE_BYTES',\n parent: null,\n title: 'T'\n });\n const res = await request(app).get(`/snapshot/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.body).toMatchObject({\n scene: 'SCENE_BYTES',\n rootRevision: r.id,\n title: 'T'\n });\n });\n\n it('GET /snapshot/<rev> reflects a renamed title (live, not creation-time)', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'Before' });\n await store.renameRoot(r.id, 'After');\n const res = await request(app).get(`/snapshot/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.body.title).toBe('After');\n });\n\n it('GET /snapshot/<rev>/thumbnail returns the per-revision webp if present', async () => {\n const { app, store, fs } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const shard = r.id.slice(0, 2);\n const rest = r.id.slice(2);\n const bytes = new Uint8Array([1, 2, 3, 4]);\n await fs.writeFile(`exports/${shard}/${rest}/preview.webp`, bytes);\n const res = await request(app).get(`/snapshot/${r.id}/thumbnail`);\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toBe('image/webp');\n });\n\n it('GET /api/designs returns the root list', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T1' });\n const res = await request(app).get('/api/designs');\n expect(res.status).toBe(200);\n expect(res.body).toEqual(\n expect.arrayContaining([\n expect.objectContaining({ rootRevision: r.id, title: 'T1' })\n ])\n );\n });\n\n it(\"GET /api/design/<root> returns that design's record\", async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T1' });\n const res = await request(app).get(`/api/design/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.body).toMatchObject({\n rootRevision: r.id,\n title: 'T1',\n latestLeaf: r.id\n });\n });\n\n it('GET /api/design/<unknown> returns 404', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/api/design/deadbeefdead');\n expect(res.status).toBe(404);\n expect(res.body).toEqual({ error: 'unknown design' });\n });\n\n it(\"GET /api/design/<root>/revisions returns the design's revision tree\", async () => {\n const { app, store } = await makeApp();\n const r1 = await store.write({ scene: 'a', parent: null, title: 'T' });\n const r2 = await store.write({ scene: 'b', parent: r1.id, note: 'second' });\n const fork = await store.write({\n scene: 'b2',\n parent: r1.id,\n note: 'fork'\n });\n const res = await request(app).get(`/api/design/${r1.id}/revisions`);\n expect(res.status).toBe(200);\n expect(res.body.map((n: { id: string }) => n.id).sort()).toEqual(\n [r1.id, r2.id, fork.id].sort()\n );\n expect(res.body).toEqual(\n expect.arrayContaining([\n expect.objectContaining({ id: r2.id, parent: r1.id, note: 'second' })\n ])\n );\n });\n\n it('GET /api/design/<unknown>/revisions returns 404', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/api/design/deadbeefdead/revisions');\n expect(res.status).toBe(404);\n expect(res.body).toEqual({ error: 'unknown design' });\n });\n\n it('a /api/design/<root>/revisions error is sanitised to a generic 500', async () => {\n const { app, store, fs } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n await fs.writeFile(\n `objects/${r.id.slice(0, 2)}/${r.id.slice(2)}.json`,\n 'not json'\n );\n const res = await request(app).get(`/api/design/${r.id}/revisions`);\n expect(res.status).toBe(500);\n expect(res.text).toBe('internal error');\n });\n\n it('GET /snapshot/<unknown> returns 404', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/snapshot/deadbeefdead');\n expect(res.status).toBe(404);\n });\n\n it('GET /api/config includes the /r/ resourceBase', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/api/config');\n expect(res.status).toBe(200);\n expect(res.body.resourceBase).toBe('/r/');\n });\n\n it('serves a root-relative SPA base when mounted at root', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const res = await request(app).get(`/d/${r.id}`);\n expect(res.text).toContain(\"window.__VIEWER_BASE__ = '';\");\n expect(res.text).toContain('src=\"/static/viewer.js\"');\n });\n\n it('injects the mount prefix into the SPA base when mounted at /viewer', async () => {\n const { app, store } = await makeMountedApp('/viewer');\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const res = await request(app).get(`/viewer/d/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.text).toContain(\"window.__VIEWER_BASE__ = '/viewer';\");\n expect(res.text).toContain('src=\"/viewer/static/viewer.js\"');\n // No leftover unprefixed asset path that would 404 under the mount.\n expect(res.text).not.toContain('src=\"/static/viewer.js\"');\n });\n\n it('studio shell carries the mount prefix too when mounted at /viewer', async () => {\n const { app } = await makeMountedApp('/viewer');\n const res = await request(app).get('/viewer/');\n expect(res.status).toBe(200);\n expect(res.text).toContain(\"window.__VIEWER_BASE__ = '/viewer';\");\n expect(res.text).toContain('src=\"/viewer/static/studio.js\"');\n });\n\n it('resourceBase is mount-relative when mounted at /viewer', async () => {\n const { app } = await makeMountedApp('/viewer');\n const res = await request(app).get('/viewer/api/config');\n expect(res.status).toBe(200);\n expect(res.body.resourceBase).toBe('/viewer/r/');\n });\n\n it('serves the SPA JS under the mount prefix', async () => {\n const { app } = await makeMountedApp('/viewer');\n const res = await request(app).get('/viewer/static/viewer.js');\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/javascript/);\n });\n\n it('GET /r/<asset> is reachable through the viewer router', async () => {\n const { app, fs } = await makeApp();\n await fs.writeFile('assets/abc.png', new Uint8Array([1, 2, 3]));\n const res = await request(app).get('/r/assets/abc.png');\n expect(res.status).toBe(200);\n });\n\n it('a route error is sanitised to a generic 500 (no leak)', async () => {\n const { app, store, fs } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n // Corrupt the revision meta so store.read throws a non-ENOENT parse error.\n await fs.writeFile(\n `objects/${r.id.slice(0, 2)}/${r.id.slice(2)}.json`,\n 'not json'\n );\n const res = await request(app).get(`/snapshot/${r.id}`);\n expect(res.status).toBe(500);\n expect(res.text).toBe('internal error');\n expect(res.text).not.toContain('not json');\n });\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=session-mount.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-mount.test.d.ts","sourceRoot":"","sources":["../../../../test/viewer/session-mount.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,52 @@
1
+ import express from 'express';
2
+ import request from 'supertest';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { RevisionStore } from '../../src/revisions/store.js';
5
+ import { SseBroadcaster } from '../../src/viewer/broadcaster.js';
6
+ import { mountSessionViewer } from '../../src/viewer/session-mount.js';
7
+ import { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';
8
+ import { ScopedWorkspaceFS } from '../../src/workspace/scoped.js';
9
+ function makeApp() {
10
+ const rawFs = new InMemoryWorkspaceFS();
11
+ const app = express();
12
+ mountSessionViewer(app, {
13
+ rawFs,
14
+ broadcaster: new SseBroadcaster(),
15
+ cesdkLicense: 'lic-123'
16
+ });
17
+ return { app, rawFs };
18
+ }
19
+ describe('session viewer mount (/v/:sid)', () => {
20
+ it('serves the studio shell with the per-session base injected', async () => {
21
+ const { app } = makeApp();
22
+ const res = await request(app).get('/v/sid-1/');
23
+ expect(res.status).toBe(200);
24
+ expect(res.headers['content-type']).toMatch(/html/);
25
+ expect(res.text).toContain('/v/sid-1');
26
+ });
27
+ it('scopes data to the sid: a design in s/<sid> is visible only there', async () => {
28
+ const { app, rawFs } = makeApp();
29
+ const store = new RevisionStore(new ScopedWorkspaceFS(rawFs, 's/sid-1'));
30
+ const r = await store.write({ scene: 'S', parent: null, title: 'T' });
31
+ const mine = await request(app).get('/v/sid-1/api/designs');
32
+ expect(mine.status).toBe(200);
33
+ expect(mine.body.map((d) => d.rootRevision)).toContain(r.id);
34
+ const theirs = await request(app).get('/v/sid-2/api/designs');
35
+ expect(theirs.status).toBe(200);
36
+ expect(theirs.body).toEqual([]);
37
+ });
38
+ it('serves the license via /api/config', async () => {
39
+ const { app } = makeApp();
40
+ const res = await request(app).get('/v/sid-1/api/config');
41
+ expect(res.status).toBe(200);
42
+ expect(res.body.license).toBe('lic-123');
43
+ expect(res.body.resourceBase).toBe('/v/sid-1/r/');
44
+ });
45
+ it('rejects a traversal sid with 404', async () => {
46
+ const { app } = makeApp();
47
+ // %2E%2E decodes to ".." — sanitizeWorkspaceId must reject it.
48
+ const res = await request(app).get('/v/%2E%2E/api/designs');
49
+ expect(res.status).toBe(404);
50
+ });
51
+ });
52
+ //# sourceMappingURL=session-mount.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-mount.test.js","sourceRoot":"","sources":["../../../../test/viewer/session-mount.test.ts"],"names":[],"mappings":"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","sourcesContent":["import express from 'express';\nimport request from 'supertest';\nimport { describe, expect, it } from 'vitest';\nimport { RevisionStore } from '../../src/revisions/store.js';\nimport { SseBroadcaster } from '../../src/viewer/broadcaster.js';\nimport { mountSessionViewer } from '../../src/viewer/session-mount.js';\nimport { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';\nimport { ScopedWorkspaceFS } from '../../src/workspace/scoped.js';\n\nfunction makeApp() {\n const rawFs = new InMemoryWorkspaceFS();\n const app = express();\n mountSessionViewer(app, {\n rawFs,\n broadcaster: new SseBroadcaster(),\n cesdkLicense: 'lic-123'\n });\n return { app, rawFs };\n}\n\ndescribe('session viewer mount (/v/:sid)', () => {\n it('serves the studio shell with the per-session base injected', async () => {\n const { app } = makeApp();\n const res = await request(app).get('/v/sid-1/');\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/html/);\n expect(res.text).toContain('/v/sid-1');\n });\n\n it('scopes data to the sid: a design in s/<sid> is visible only there', async () => {\n const { app, rawFs } = makeApp();\n const store = new RevisionStore(new ScopedWorkspaceFS(rawFs, 's/sid-1'));\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n\n const mine = await request(app).get('/v/sid-1/api/designs');\n expect(mine.status).toBe(200);\n expect(\n mine.body.map((d: { rootRevision: string }) => d.rootRevision)\n ).toContain(r.id);\n\n const theirs = await request(app).get('/v/sid-2/api/designs');\n expect(theirs.status).toBe(200);\n expect(theirs.body).toEqual([]);\n });\n\n it('serves the license via /api/config', async () => {\n const { app } = makeApp();\n const res = await request(app).get('/v/sid-1/api/config');\n expect(res.status).toBe(200);\n expect(res.body.license).toBe('lic-123');\n expect(res.body.resourceBase).toBe('/v/sid-1/r/');\n });\n\n it('rejects a traversal sid with 404', async () => {\n const { app } = makeApp();\n // %2E%2E decodes to \"..\" — sanitizeWorkspaceId must reject it.\n const res = await request(app).get('/v/%2E%2E/api/designs');\n expect(res.status).toBe(404);\n });\n});\n"]}