@imgly/codesign-mcp 0.1.1 → 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 (115) hide show
  1. package/dist/cli/stdio.d.ts.map +1 -1
  2. package/dist/cli/stdio.js +54 -21
  3. package/dist/cli/stdio.js.map +1 -1
  4. package/dist/engine/cesdk-adapter.d.ts +7 -1
  5. package/dist/engine/cesdk-adapter.d.ts.map +1 -1
  6. package/dist/engine/cesdk-adapter.js +7 -1
  7. package/dist/engine/cesdk-adapter.js.map +1 -1
  8. package/dist/http/server.d.ts.map +1 -1
  9. package/dist/http/server.js +0 -1
  10. package/dist/http/server.js.map +1 -1
  11. package/dist/server.d.ts +0 -2
  12. package/dist/server.d.ts.map +1 -1
  13. package/dist/server.js.map +1 -1
  14. package/dist/test/test/cli/redirect-console.test.d.ts +2 -0
  15. package/dist/test/test/cli/redirect-console.test.d.ts.map +1 -0
  16. package/dist/test/test/cli/redirect-console.test.js +47 -0
  17. package/dist/test/test/cli/redirect-console.test.js.map +1 -0
  18. package/dist/test/test/cli/resolve-license.test.d.ts +2 -0
  19. package/dist/test/test/cli/resolve-license.test.d.ts.map +1 -0
  20. package/dist/test/test/cli/resolve-license.test.js +20 -0
  21. package/dist/test/test/cli/resolve-license.test.js.map +1 -0
  22. package/dist/test/test/cli/stdio-license.test.d.ts +2 -0
  23. package/dist/test/test/cli/stdio-license.test.d.ts.map +1 -0
  24. package/dist/test/test/cli/stdio-license.test.js +19 -0
  25. package/dist/test/test/cli/stdio-license.test.js.map +1 -0
  26. package/dist/test/test/config.test.d.ts +2 -0
  27. package/dist/test/test/config.test.d.ts.map +1 -0
  28. package/dist/test/test/config.test.js +91 -0
  29. package/dist/test/test/config.test.js.map +1 -0
  30. package/dist/test/test/engine/asset-sources.test.d.ts +2 -0
  31. package/dist/test/test/engine/asset-sources.test.d.ts.map +1 -0
  32. package/dist/test/test/engine/asset-sources.test.js +100 -0
  33. package/dist/test/test/engine/asset-sources.test.js.map +1 -0
  34. package/dist/test/test/engine/mutex.test.d.ts +2 -0
  35. package/dist/test/test/engine/mutex.test.d.ts.map +1 -0
  36. package/dist/test/test/engine/mutex.test.js +63 -0
  37. package/dist/test/test/engine/mutex.test.js.map +1 -0
  38. package/dist/test/test/engine/session.test.js +15 -0
  39. package/dist/test/test/engine/session.test.js.map +1 -1
  40. package/dist/test/test/engine/unsplash-source.test.d.ts +2 -0
  41. package/dist/test/test/engine/unsplash-source.test.d.ts.map +1 -0
  42. package/dist/test/test/engine/unsplash-source.test.js +129 -0
  43. package/dist/test/test/engine/unsplash-source.test.js.map +1 -0
  44. package/dist/test/test/handbook-contract.test.js +8 -4
  45. package/dist/test/test/handbook-contract.test.js.map +1 -1
  46. package/dist/test/test/http/server.test.js +129 -0
  47. package/dist/test/test/http/server.test.js.map +1 -1
  48. package/dist/test/test/http/session-id-header.test.d.ts +2 -0
  49. package/dist/test/test/http/session-id-header.test.d.ts.map +1 -0
  50. package/dist/test/test/http/session-id-header.test.js +76 -0
  51. package/dist/test/test/http/session-id-header.test.js.map +1 -0
  52. package/dist/test/test/http/session-manager.test.js +14 -0
  53. package/dist/test/test/http/session-manager.test.js.map +1 -1
  54. package/dist/test/test/http/workspace-auth.test.d.ts +2 -0
  55. package/dist/test/test/http/workspace-auth.test.d.ts.map +1 -0
  56. package/dist/test/test/http/workspace-auth.test.js +41 -0
  57. package/dist/test/test/http/workspace-auth.test.js.map +1 -0
  58. package/dist/test/test/metrics/mount.test.d.ts +2 -0
  59. package/dist/test/test/metrics/mount.test.d.ts.map +1 -0
  60. package/dist/test/test/metrics/mount.test.js +59 -0
  61. package/dist/test/test/metrics/mount.test.js.map +1 -0
  62. package/dist/test/test/metrics/recorder.test.d.ts +2 -0
  63. package/dist/test/test/metrics/recorder.test.d.ts.map +1 -0
  64. package/dist/test/test/metrics/recorder.test.js +118 -0
  65. package/dist/test/test/metrics/recorder.test.js.map +1 -0
  66. package/dist/test/test/revisions/store.test.js +81 -11
  67. package/dist/test/test/revisions/store.test.js.map +1 -1
  68. package/dist/test/test/server.metrics.test.d.ts +2 -0
  69. package/dist/test/test/server.metrics.test.d.ts.map +1 -0
  70. package/dist/test/test/server.metrics.test.js +87 -0
  71. package/dist/test/test/server.metrics.test.js.map +1 -0
  72. package/dist/test/test/server.test.js +33 -4
  73. package/dist/test/test/server.test.js.map +1 -1
  74. package/dist/test/test/tools/asset-search.test.d.ts +2 -0
  75. package/dist/test/test/tools/asset-search.test.d.ts.map +1 -0
  76. package/dist/test/test/tools/asset-search.test.js +121 -0
  77. package/dist/test/test/tools/asset-search.test.js.map +1 -0
  78. package/dist/test/test/tools/edit.test.js +71 -9
  79. package/dist/test/test/tools/edit.test.js.map +1 -1
  80. package/dist/test/test/tools/export.test.js +60 -30
  81. package/dist/test/test/tools/export.test.js.map +1 -1
  82. package/dist/test/test/tools/index.test.d.ts +2 -0
  83. package/dist/test/test/tools/index.test.d.ts.map +1 -0
  84. package/dist/test/test/tools/index.test.js +55 -0
  85. package/dist/test/test/tools/index.test.js.map +1 -0
  86. package/dist/test/test/tools/metrics.test.d.ts +2 -0
  87. package/dist/test/test/tools/metrics.test.d.ts.map +1 -0
  88. package/dist/test/test/tools/metrics.test.js +29 -0
  89. package/dist/test/test/tools/metrics.test.js.map +1 -0
  90. package/dist/test/test/tools/preview.test.js +6 -4
  91. package/dist/test/test/tools/preview.test.js.map +1 -1
  92. package/dist/test/test/tools/rename.test.js +1 -2
  93. package/dist/test/test/tools/rename.test.js.map +1 -1
  94. package/dist/test/test/tools/save.test.d.ts +2 -0
  95. package/dist/test/test/tools/save.test.d.ts.map +1 -0
  96. package/dist/test/test/tools/save.test.js +137 -0
  97. package/dist/test/test/tools/save.test.js.map +1 -0
  98. package/dist/test/test/viewer/router.test.js +56 -0
  99. package/dist/test/test/viewer/router.test.js.map +1 -1
  100. package/dist/test/test/viewer/session-mount.test.d.ts +2 -0
  101. package/dist/test/test/viewer/session-mount.test.d.ts.map +1 -0
  102. package/dist/test/test/viewer/session-mount.test.js +52 -0
  103. package/dist/test/test/viewer/session-mount.test.js.map +1 -0
  104. package/dist/test/test/viewer/url-builder.test.js +76 -9
  105. package/dist/test/test/viewer/url-builder.test.js.map +1 -1
  106. package/dist/test/test/workspace/s3.test.d.ts +2 -0
  107. package/dist/test/test/workspace/s3.test.d.ts.map +1 -0
  108. package/dist/test/test/workspace/s3.test.js +153 -0
  109. package/dist/test/test/workspace/s3.test.js.map +1 -0
  110. package/dist/test/test/workspace/scoped.test.d.ts +2 -0
  111. package/dist/test/test/workspace/scoped.test.d.ts.map +1 -0
  112. package/dist/test/test/workspace/scoped.test.js +59 -0
  113. package/dist/test/test/workspace/scoped.test.js.map +1 -0
  114. package/dist/test/tsconfig.test.tsbuildinfo +1 -1
  115. package/package.json +1 -1
@@ -0,0 +1,91 @@
1
+ import { mkdtempSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { loadConfig, resolveSecret } from '../src/config.js';
6
+ const BASE = { IMGLY_CESDK_LICENSE: 'lic-123' };
7
+ describe('resolveSecret', () => {
8
+ it('returns the plain env var when no _FILE is set', () => {
9
+ expect(resolveSecret('X', { X: 'v' })).toBe('v');
10
+ });
11
+ it('prefers the _FILE contents (trimmed) over the env var', () => {
12
+ const dir = mkdtempSync(join(tmpdir(), 'cfg-'));
13
+ const f = join(dir, 'secret');
14
+ writeFileSync(f, 'file-value\n');
15
+ expect(resolveSecret('X', { X: 'env-value', X_FILE: f })).toBe('file-value');
16
+ });
17
+ it('returns undefined when neither is set', () => {
18
+ expect(resolveSecret('X', {})).toBeUndefined();
19
+ });
20
+ });
21
+ describe('loadConfig', () => {
22
+ it('applies defaults (header mode, local backend, port 8080)', () => {
23
+ const cfg = loadConfig(BASE);
24
+ expect(cfg.cesdkLicense).toBe('lic-123');
25
+ expect(cfg.auth.mode).toBe('header');
26
+ expect(cfg.auth.header).toBe('X-Workspace-Id');
27
+ expect(cfg.workspace.backend).toBe('local');
28
+ expect(cfg.workspace.dir).toBe('/data');
29
+ expect(cfg.port).toBe(8080);
30
+ expect(cfg.maxEngines).toBe(8);
31
+ expect(cfg.buildId).toBe('dev');
32
+ });
33
+ it('reads buildId from IMGLY_BUILD_ID (the deploy-injected git SHA)', () => {
34
+ expect(loadConfig({ ...BASE, IMGLY_BUILD_ID: 'abc1234' }).buildId).toBe('abc1234');
35
+ });
36
+ it('throws when the license is missing', () => {
37
+ expect(() => loadConfig({})).toThrow(/IMGLY_CESDK_LICENSE/);
38
+ });
39
+ it('single mode requires IMGLY_WORKSPACE_ID', () => {
40
+ expect(() => loadConfig({ ...BASE, IMGLY_WORKSPACE_MODE: 'single' })).toThrow(/IMGLY_WORKSPACE_ID/);
41
+ const cfg = loadConfig({
42
+ ...BASE,
43
+ IMGLY_WORKSPACE_MODE: 'single',
44
+ IMGLY_WORKSPACE_ID: 'acme'
45
+ });
46
+ expect(cfg.auth.fixedWorkspaceId).toBe('acme');
47
+ });
48
+ it('s3 backend requires a bucket and reads region/endpoint', () => {
49
+ expect(() => loadConfig({ ...BASE, IMGLY_WORKSPACE_BACKEND: 's3' })).toThrow(/IMGLY_WORKSPACE_S3_BUCKET/);
50
+ const cfg = loadConfig({
51
+ ...BASE,
52
+ IMGLY_WORKSPACE_BACKEND: 's3',
53
+ IMGLY_WORKSPACE_S3_BUCKET: 'designs',
54
+ IMGLY_WORKSPACE_S3_ENDPOINT: 'https://acct.r2.cloudflarestorage.com'
55
+ });
56
+ expect(cfg.workspace.backend).toBe('s3');
57
+ expect(cfg.workspace.s3?.bucket).toBe('designs');
58
+ expect(cfg.workspace.s3?.region).toBe('auto');
59
+ expect(cfg.workspace.s3?.endpoint).toBe('https://acct.r2.cloudflarestorage.com');
60
+ });
61
+ it('rejects an unknown mode', () => {
62
+ expect(() => loadConfig({ ...BASE, IMGLY_WORKSPACE_MODE: 'bogus' })).toThrow(/IMGLY_WORKSPACE_MODE/);
63
+ });
64
+ it('leaves the gateway disabled when no key is set', () => {
65
+ expect(loadConfig(BASE).gateway.apiKey).toBeUndefined();
66
+ });
67
+ it('metricsEnabled defaults to false when unset', () => {
68
+ expect(loadConfig(BASE).metricsEnabled).toBe(false);
69
+ });
70
+ it('metricsEnabled is true for "true" or "1"', () => {
71
+ expect(loadConfig({ ...BASE, IMGLY_METRICS_ENABLED: 'true' }).metricsEnabled).toBe(true);
72
+ expect(loadConfig({ ...BASE, IMGLY_METRICS_ENABLED: '1' }).metricsEnabled).toBe(true);
73
+ });
74
+ it('metricsEnabled is false for any other explicit value', () => {
75
+ expect(loadConfig({ ...BASE, IMGLY_METRICS_ENABLED: 'false' }).metricsEnabled).toBe(false);
76
+ expect(loadConfig({ ...BASE, IMGLY_METRICS_ENABLED: 'yes' }).metricsEnabled).toBe(false);
77
+ });
78
+ });
79
+ describe('IMGLY_PUBLIC_BASE_URL', () => {
80
+ it('is absent by default', () => {
81
+ expect(loadConfig(BASE).publicBaseUrl).toBeUndefined();
82
+ });
83
+ it('is loaded with trailing slashes stripped', () => {
84
+ const cfg = loadConfig({
85
+ ...BASE,
86
+ IMGLY_PUBLIC_BASE_URL: 'https://codesign.example.com/'
87
+ });
88
+ expect(cfg.publicBaseUrl).toBe('https://codesign.example.com');
89
+ });
90
+ });
91
+ //# sourceMappingURL=config.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../../test/config.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdtempSync, writeFileSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport { describe, expect, it } from 'vitest';\nimport { loadConfig, resolveSecret } from '../src/config.js';\n\nconst BASE = { IMGLY_CESDK_LICENSE: 'lic-123' };\n\ndescribe('resolveSecret', () => {\n it('returns the plain env var when no _FILE is set', () => {\n expect(resolveSecret('X', { X: 'v' })).toBe('v');\n });\n\n it('prefers the _FILE contents (trimmed) over the env var', () => {\n const dir = mkdtempSync(join(tmpdir(), 'cfg-'));\n const f = join(dir, 'secret');\n writeFileSync(f, 'file-value\\n');\n expect(resolveSecret('X', { X: 'env-value', X_FILE: f })).toBe(\n 'file-value'\n );\n });\n\n it('returns undefined when neither is set', () => {\n expect(resolveSecret('X', {})).toBeUndefined();\n });\n});\n\ndescribe('loadConfig', () => {\n it('applies defaults (header mode, local backend, port 8080)', () => {\n const cfg = loadConfig(BASE);\n expect(cfg.cesdkLicense).toBe('lic-123');\n expect(cfg.auth.mode).toBe('header');\n expect(cfg.auth.header).toBe('X-Workspace-Id');\n expect(cfg.workspace.backend).toBe('local');\n expect(cfg.workspace.dir).toBe('/data');\n expect(cfg.port).toBe(8080);\n expect(cfg.maxEngines).toBe(8);\n expect(cfg.buildId).toBe('dev');\n });\n\n it('reads buildId from IMGLY_BUILD_ID (the deploy-injected git SHA)', () => {\n expect(loadConfig({ ...BASE, IMGLY_BUILD_ID: 'abc1234' }).buildId).toBe(\n 'abc1234'\n );\n });\n\n it('throws when the license is missing', () => {\n expect(() => loadConfig({})).toThrow(/IMGLY_CESDK_LICENSE/);\n });\n\n it('single mode requires IMGLY_WORKSPACE_ID', () => {\n expect(() =>\n loadConfig({ ...BASE, IMGLY_WORKSPACE_MODE: 'single' })\n ).toThrow(/IMGLY_WORKSPACE_ID/);\n const cfg = loadConfig({\n ...BASE,\n IMGLY_WORKSPACE_MODE: 'single',\n IMGLY_WORKSPACE_ID: 'acme'\n });\n expect(cfg.auth.fixedWorkspaceId).toBe('acme');\n });\n\n it('s3 backend requires a bucket and reads region/endpoint', () => {\n expect(() =>\n loadConfig({ ...BASE, IMGLY_WORKSPACE_BACKEND: 's3' })\n ).toThrow(/IMGLY_WORKSPACE_S3_BUCKET/);\n const cfg = loadConfig({\n ...BASE,\n IMGLY_WORKSPACE_BACKEND: 's3',\n IMGLY_WORKSPACE_S3_BUCKET: 'designs',\n IMGLY_WORKSPACE_S3_ENDPOINT: 'https://acct.r2.cloudflarestorage.com'\n });\n expect(cfg.workspace.backend).toBe('s3');\n expect(cfg.workspace.s3?.bucket).toBe('designs');\n expect(cfg.workspace.s3?.region).toBe('auto');\n expect(cfg.workspace.s3?.endpoint).toBe(\n 'https://acct.r2.cloudflarestorage.com'\n );\n });\n\n it('rejects an unknown mode', () => {\n expect(() =>\n loadConfig({ ...BASE, IMGLY_WORKSPACE_MODE: 'bogus' })\n ).toThrow(/IMGLY_WORKSPACE_MODE/);\n });\n\n it('leaves the gateway disabled when no key is set', () => {\n expect(loadConfig(BASE).gateway.apiKey).toBeUndefined();\n });\n\n it('metricsEnabled defaults to false when unset', () => {\n expect(loadConfig(BASE).metricsEnabled).toBe(false);\n });\n\n it('metricsEnabled is true for \"true\" or \"1\"', () => {\n expect(\n loadConfig({ ...BASE, IMGLY_METRICS_ENABLED: 'true' }).metricsEnabled\n ).toBe(true);\n expect(\n loadConfig({ ...BASE, IMGLY_METRICS_ENABLED: '1' }).metricsEnabled\n ).toBe(true);\n });\n\n it('metricsEnabled is false for any other explicit value', () => {\n expect(\n loadConfig({ ...BASE, IMGLY_METRICS_ENABLED: 'false' }).metricsEnabled\n ).toBe(false);\n expect(\n loadConfig({ ...BASE, IMGLY_METRICS_ENABLED: 'yes' }).metricsEnabled\n ).toBe(false);\n });\n});\n\ndescribe('IMGLY_PUBLIC_BASE_URL', () => {\n it('is absent by default', () => {\n expect(loadConfig(BASE).publicBaseUrl).toBeUndefined();\n });\n\n it('is loaded with trailing slashes stripped', () => {\n const cfg = loadConfig({\n ...BASE,\n IMGLY_PUBLIC_BASE_URL: 'https://codesign.example.com/'\n });\n expect(cfg.publicBaseUrl).toBe('https://codesign.example.com');\n });\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=asset-sources.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-sources.test.d.ts","sourceRoot":"","sources":["../../../../test/engine/asset-sources.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,100 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { ensureEngineAssetSources, resetEngineAssetSourcesForTest } from '../../src/engine/asset-sources.js';
3
+ function makeEngine(existingSources) {
4
+ return {
5
+ asset: { findAllSources: vi.fn(() => existingSources) },
6
+ addDefaultAssetSources: vi.fn(async () => { }),
7
+ addDemoAssetSources: vi.fn(async () => { })
8
+ };
9
+ }
10
+ describe('ensureEngineAssetSources', () => {
11
+ beforeEach(() => {
12
+ resetEngineAssetSourcesForTest();
13
+ });
14
+ it('registers default + demo sources on a fresh engine', async () => {
15
+ const engine = makeEngine([]);
16
+ await ensureEngineAssetSources(engine);
17
+ expect(engine.addDefaultAssetSources).toHaveBeenCalledOnce();
18
+ expect(engine.addDefaultAssetSources).toHaveBeenCalledWith({
19
+ baseURL: 'https://cdn.img.ly/assets/v3',
20
+ excludeAssetSourceIds: ['ly.img.text']
21
+ });
22
+ expect(engine.addDemoAssetSources).toHaveBeenCalledOnce();
23
+ expect(engine.addDemoAssetSources).toHaveBeenCalledWith({
24
+ baseURL: 'https://cdn.img.ly/assets/demo/v3'
25
+ });
26
+ });
27
+ it('honours self-hosted manifest roots from the environment', async () => {
28
+ vi.stubEnv('IMGLY_DEFAULT_ASSETS_BASE_URL', 'https://assets.example.com/v3');
29
+ vi.stubEnv('IMGLY_DEMO_ASSETS_BASE_URL', 'https://assets.example.com/demo/v3');
30
+ try {
31
+ const engine = makeEngine([]);
32
+ await ensureEngineAssetSources(engine);
33
+ expect(engine.addDefaultAssetSources).toHaveBeenCalledWith({
34
+ baseURL: 'https://assets.example.com/v3',
35
+ excludeAssetSourceIds: ['ly.img.text']
36
+ });
37
+ expect(engine.addDemoAssetSources).toHaveBeenCalledWith({
38
+ baseURL: 'https://assets.example.com/demo/v3'
39
+ });
40
+ }
41
+ finally {
42
+ vi.unstubAllEnvs();
43
+ }
44
+ });
45
+ it('skips registration when the demo image source is already present', async () => {
46
+ const engine = makeEngine(['ly.img.image']);
47
+ await ensureEngineAssetSources(engine);
48
+ expect(engine.addDefaultAssetSources).not.toHaveBeenCalled();
49
+ expect(engine.addDemoAssetSources).not.toHaveBeenCalled();
50
+ });
51
+ it('swallows CDN failures (best-effort) without throwing', async () => {
52
+ const stderrSpy = vi
53
+ .spyOn(process.stderr, 'write')
54
+ .mockImplementation(() => true);
55
+ const engine = makeEngine([]);
56
+ engine.addDefaultAssetSources.mockRejectedValueOnce(new Error('offline'));
57
+ await expect(ensureEngineAssetSources(engine)).resolves.toBeUndefined();
58
+ expect(stderrSpy.mock.calls[0][0]).toContain('asset_search will see fewer sources');
59
+ stderrSpy.mockRestore();
60
+ });
61
+ it('concurrent calls share one in-flight registration', async () => {
62
+ const engine = makeEngine([]);
63
+ const p1 = ensureEngineAssetSources(engine);
64
+ const p2 = ensureEngineAssetSources(engine);
65
+ await Promise.all([p1, p2]);
66
+ expect(engine.addDefaultAssetSources).toHaveBeenCalledOnce();
67
+ });
68
+ it('total failure (sentinel absent) clears the guard so the next call retries', async () => {
69
+ const stderrSpy = vi
70
+ .spyOn(process.stderr, 'write')
71
+ .mockImplementation(() => true);
72
+ const engine = makeEngine([]);
73
+ engine.addDefaultAssetSources.mockRejectedValueOnce(new Error('offline'));
74
+ await ensureEngineAssetSources(engine);
75
+ await ensureEngineAssetSources(engine);
76
+ expect(engine.addDefaultAssetSources).toHaveBeenCalledTimes(2);
77
+ stderrSpy.mockRestore();
78
+ });
79
+ it('keeps the guard when demo sources land despite default-source failure', async () => {
80
+ const stderrSpy = vi
81
+ .spyOn(process.stderr, 'write')
82
+ .mockImplementation(() => true);
83
+ const sources = [];
84
+ const engine = {
85
+ asset: { findAllSources: vi.fn(() => [...sources]) },
86
+ addDefaultAssetSources: vi.fn(async () => {
87
+ throw new Error('ENOENT');
88
+ }),
89
+ addDemoAssetSources: vi.fn(async () => {
90
+ sources.push('ly.img.image');
91
+ })
92
+ };
93
+ await ensureEngineAssetSources(engine);
94
+ await ensureEngineAssetSources(engine);
95
+ expect(engine.addDefaultAssetSources).toHaveBeenCalledOnce();
96
+ expect(engine.addDemoAssetSources).toHaveBeenCalledOnce();
97
+ stderrSpy.mockRestore();
98
+ });
99
+ });
100
+ //# sourceMappingURL=asset-sources.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-sources.test.js","sourceRoot":"","sources":["../../../../test/engine/asset-sources.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { beforeEach, describe, expect, it, vi } from 'vitest';\nimport {\n ensureEngineAssetSources,\n resetEngineAssetSourcesForTest\n} from '../../src/engine/asset-sources.js';\n\nfunction makeEngine(existingSources: string[]) {\n return {\n asset: { findAllSources: vi.fn(() => existingSources) },\n addDefaultAssetSources: vi.fn(async () => {}),\n addDemoAssetSources: vi.fn(async () => {})\n };\n}\n\ndescribe('ensureEngineAssetSources', () => {\n beforeEach(() => {\n resetEngineAssetSourcesForTest();\n });\n\n it('registers default + demo sources on a fresh engine', async () => {\n const engine = makeEngine([]);\n await ensureEngineAssetSources(engine);\n expect(engine.addDefaultAssetSources).toHaveBeenCalledOnce();\n expect(engine.addDefaultAssetSources).toHaveBeenCalledWith({\n baseURL: 'https://cdn.img.ly/assets/v3',\n excludeAssetSourceIds: ['ly.img.text']\n });\n expect(engine.addDemoAssetSources).toHaveBeenCalledOnce();\n expect(engine.addDemoAssetSources).toHaveBeenCalledWith({\n baseURL: 'https://cdn.img.ly/assets/demo/v3'\n });\n });\n\n it('honours self-hosted manifest roots from the environment', async () => {\n vi.stubEnv(\n 'IMGLY_DEFAULT_ASSETS_BASE_URL',\n 'https://assets.example.com/v3'\n );\n vi.stubEnv(\n 'IMGLY_DEMO_ASSETS_BASE_URL',\n 'https://assets.example.com/demo/v3'\n );\n try {\n const engine = makeEngine([]);\n await ensureEngineAssetSources(engine);\n expect(engine.addDefaultAssetSources).toHaveBeenCalledWith({\n baseURL: 'https://assets.example.com/v3',\n excludeAssetSourceIds: ['ly.img.text']\n });\n expect(engine.addDemoAssetSources).toHaveBeenCalledWith({\n baseURL: 'https://assets.example.com/demo/v3'\n });\n } finally {\n vi.unstubAllEnvs();\n }\n });\n\n it('skips registration when the demo image source is already present', async () => {\n const engine = makeEngine(['ly.img.image']);\n await ensureEngineAssetSources(engine);\n expect(engine.addDefaultAssetSources).not.toHaveBeenCalled();\n expect(engine.addDemoAssetSources).not.toHaveBeenCalled();\n });\n\n it('swallows CDN failures (best-effort) without throwing', async () => {\n const stderrSpy = vi\n .spyOn(process.stderr, 'write')\n .mockImplementation(() => true);\n const engine = makeEngine([]);\n engine.addDefaultAssetSources.mockRejectedValueOnce(new Error('offline'));\n await expect(ensureEngineAssetSources(engine)).resolves.toBeUndefined();\n expect(stderrSpy.mock.calls[0][0]).toContain(\n 'asset_search will see fewer sources'\n );\n stderrSpy.mockRestore();\n });\n\n it('concurrent calls share one in-flight registration', async () => {\n const engine = makeEngine([]);\n const p1 = ensureEngineAssetSources(engine);\n const p2 = ensureEngineAssetSources(engine);\n await Promise.all([p1, p2]);\n expect(engine.addDefaultAssetSources).toHaveBeenCalledOnce();\n });\n\n it('total failure (sentinel absent) clears the guard so the next call retries', async () => {\n const stderrSpy = vi\n .spyOn(process.stderr, 'write')\n .mockImplementation(() => true);\n const engine = makeEngine([]);\n engine.addDefaultAssetSources.mockRejectedValueOnce(new Error('offline'));\n await ensureEngineAssetSources(engine);\n await ensureEngineAssetSources(engine);\n expect(engine.addDefaultAssetSources).toHaveBeenCalledTimes(2);\n stderrSpy.mockRestore();\n });\n\n it('keeps the guard when demo sources land despite default-source failure', async () => {\n const stderrSpy = vi\n .spyOn(process.stderr, 'write')\n .mockImplementation(() => true);\n const sources: string[] = [];\n const engine = {\n asset: { findAllSources: vi.fn(() => [...sources]) },\n addDefaultAssetSources: vi.fn(async () => {\n throw new Error('ENOENT');\n }),\n addDemoAssetSources: vi.fn(async () => {\n sources.push('ly.img.image');\n })\n };\n await ensureEngineAssetSources(engine);\n await ensureEngineAssetSources(engine);\n expect(engine.addDefaultAssetSources).toHaveBeenCalledOnce();\n expect(engine.addDemoAssetSources).toHaveBeenCalledOnce();\n stderrSpy.mockRestore();\n });\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mutex.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mutex.test.d.ts","sourceRoot":"","sources":["../../../../test/engine/mutex.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Mutex } from '../../src/engine/mutex.js';
3
+ const tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
4
+ describe('Mutex', () => {
5
+ it('runs sections one at a time — never two in their critical region at once', async () => {
6
+ const mutex = new Mutex();
7
+ let active = 0;
8
+ let maxActive = 0;
9
+ const section = async () => {
10
+ active++;
11
+ maxActive = Math.max(maxActive, active);
12
+ await tick(5); // hold the section open across an await
13
+ active--;
14
+ };
15
+ await Promise.all([
16
+ mutex.runExclusive(section),
17
+ mutex.runExclusive(section),
18
+ mutex.runExclusive(section),
19
+ mutex.runExclusive(section)
20
+ ]);
21
+ expect(maxActive).toBe(1);
22
+ expect(active).toBe(0);
23
+ });
24
+ it('preserves FIFO order', async () => {
25
+ const mutex = new Mutex();
26
+ const order = [];
27
+ await Promise.all([0, 1, 2, 3].map((i) => mutex.runExclusive(async () => {
28
+ await tick(1);
29
+ order.push(i);
30
+ })));
31
+ expect(order).toEqual([0, 1, 2, 3]);
32
+ });
33
+ it('returns the section result to its caller', async () => {
34
+ const mutex = new Mutex();
35
+ await expect(mutex.runExclusive(async () => 42)).resolves.toBe(42);
36
+ });
37
+ it('a throwing section rejects its caller but does not wedge the queue', async () => {
38
+ const mutex = new Mutex();
39
+ const boom = mutex.runExclusive(async () => {
40
+ throw new Error('boom');
41
+ });
42
+ await expect(boom).rejects.toThrow('boom');
43
+ // The next section must still run after a rejection.
44
+ await expect(mutex.runExclusive(async () => 'ok')).resolves.toBe('ok');
45
+ });
46
+ it('stays mutually exclusive under heavy interleaving (multiple yields per section)', async () => {
47
+ const mutex = new Mutex();
48
+ let active = 0;
49
+ let maxActive = 0;
50
+ const section = async () => {
51
+ active++;
52
+ maxActive = Math.max(maxActive, active);
53
+ await tick(0);
54
+ await Promise.resolve();
55
+ await tick(0);
56
+ active--;
57
+ };
58
+ await Promise.all(Array.from({ length: 12 }, () => mutex.runExclusive(section)));
59
+ expect(maxActive).toBe(1);
60
+ expect(active).toBe(0);
61
+ });
62
+ });
63
+ //# sourceMappingURL=mutex.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mutex.test.js","sourceRoot":"","sources":["../../../../test/engine/mutex.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, expect, it } from 'vitest';\nimport { Mutex } from '../../src/engine/mutex.js';\n\nconst tick = (ms = 0): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\ndescribe('Mutex', () => {\n it('runs sections one at a time — never two in their critical region at once', async () => {\n const mutex = new Mutex();\n let active = 0;\n let maxActive = 0;\n const section = async () => {\n active++;\n maxActive = Math.max(maxActive, active);\n await tick(5); // hold the section open across an await\n active--;\n };\n await Promise.all([\n mutex.runExclusive(section),\n mutex.runExclusive(section),\n mutex.runExclusive(section),\n mutex.runExclusive(section)\n ]);\n expect(maxActive).toBe(1);\n expect(active).toBe(0);\n });\n\n it('preserves FIFO order', async () => {\n const mutex = new Mutex();\n const order: number[] = [];\n await Promise.all(\n [0, 1, 2, 3].map((i) =>\n mutex.runExclusive(async () => {\n await tick(1);\n order.push(i);\n })\n )\n );\n expect(order).toEqual([0, 1, 2, 3]);\n });\n\n it('returns the section result to its caller', async () => {\n const mutex = new Mutex();\n await expect(mutex.runExclusive(async () => 42)).resolves.toBe(42);\n });\n\n it('a throwing section rejects its caller but does not wedge the queue', async () => {\n const mutex = new Mutex();\n const boom = mutex.runExclusive(async () => {\n throw new Error('boom');\n });\n await expect(boom).rejects.toThrow('boom');\n // The next section must still run after a rejection.\n await expect(mutex.runExclusive(async () => 'ok')).resolves.toBe('ok');\n });\n\n it('stays mutually exclusive under heavy interleaving (multiple yields per section)', async () => {\n const mutex = new Mutex();\n let active = 0;\n let maxActive = 0;\n const section = async () => {\n active++;\n maxActive = Math.max(maxActive, active);\n await tick(0);\n await Promise.resolve();\n await tick(0);\n active--;\n };\n await Promise.all(\n Array.from({ length: 12 }, () => mutex.runExclusive(section))\n );\n expect(maxActive).toBe(1);\n expect(active).toBe(0);\n });\n});\n"]}
@@ -61,6 +61,21 @@ describe('EngineSession (single slot)', () => {
61
61
  expect(slot2.currentRevision).toBe('rev_abc');
62
62
  });
63
63
  });
64
+ describe('EngineSession cold-instance reconstruction (spec §8.6)', () => {
65
+ it('reconstructs a cold engine by loading a staged revision scene', async () => {
66
+ // Regression guard: a cold session (no warm slot) must load the staged
67
+ // scene via loadFromString before returning the slot. This documents the
68
+ // edit-tool contract: populate(parent, scene) → acquire(parent) always
69
+ // calls loadFromString, never assumes a warm engine.
70
+ const { adapter } = makeFakeEngine();
71
+ const session = new EngineSession({ size: 1, adapter });
72
+ // Cold: no slots exist yet, so no warm engine.
73
+ await session.populate('rev-1', '<scene-for-rev-1/>');
74
+ const slot = await session.acquire('rev-1');
75
+ expect(adapter.loadFromString).toHaveBeenCalledWith(slot.engine, '<scene-for-rev-1/>');
76
+ expect(slot.currentRevision).toBe('rev-1');
77
+ });
78
+ });
64
79
  describe('EngineSession (multi-slot sticky routing)', () => {
65
80
  function makeAdapterWithCounter() {
66
81
  let id = 0;
@@ -1 +1 @@
1
- {"version":3,"file":"session.test.js","sourceRoot":"","sources":["../../../../test/engine/session.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';\n\nfunction makeFakeEngine(scene = 'INITIAL'): {\n engine: { _scene: string };\n adapter: EngineAdapter;\n} {\n const engine = { _scene: scene };\n const adapter: EngineAdapter = {\n create: () => engine,\n reset: vi.fn(async () => {\n engine._scene = 'RESET';\n }),\n loadFromString: vi.fn(async (eng, s) => {\n (eng as typeof engine)._scene = s;\n }),\n saveToString: vi.fn(async (eng) => (eng as typeof engine)._scene)\n };\n return { engine, adapter };\n}\n\ndescribe('EngineSession (single slot)', () => {\n it('acquire(parent=null) resets the engine and sets currentRevision=null', async () => {\n const { adapter } = makeFakeEngine();\n const session = new EngineSession({ size: 1, adapter });\n const slot = await session.acquire(null);\n expect(adapter.reset).toHaveBeenCalledOnce();\n expect(slot.currentRevision).toBeNull();\n });\n\n it('acquire(parent=X) loads scene X when slot currentRevision is different', async () => {\n const { adapter } = makeFakeEngine();\n const session = new EngineSession({ size: 1, adapter });\n await session.populate('rev_abc', 'SCENE_ABC'); // pre-stage the loadable scene\n const slot = await session.acquire('rev_abc');\n expect(adapter.loadFromString).toHaveBeenCalledWith(\n slot.engine,\n 'SCENE_ABC'\n );\n expect(slot.currentRevision).toBe('rev_abc');\n });\n\n it('acquire(parent=X) is a no-op when slot.currentRevision === X (linear edit)', async () => {\n const { adapter } = makeFakeEngine();\n const session = new EngineSession({ size: 1, adapter });\n await session.populate('rev_abc', 'SCENE_ABC');\n await session.acquire('rev_abc');\n (adapter.loadFromString as ReturnType<typeof vi.fn>).mockClear();\n const slot2 = await session.acquire('rev_abc');\n expect(adapter.loadFromString).not.toHaveBeenCalled();\n expect(slot2.currentRevision).toBe('rev_abc');\n });\n\n it('commit(slot, newRev) updates currentRevision (linear edit succeeded)', async () => {\n const { adapter } = makeFakeEngine();\n const session = new EngineSession({ size: 1, adapter });\n const slot = await session.acquire(null);\n session.commit(slot, 'rev_new');\n expect(slot.currentRevision).toBe('rev_new');\n });\n\n it('markDirty(slot) sets currentRevision=null (failed edit forces reload next time)', async () => {\n const { adapter } = makeFakeEngine();\n const session = new EngineSession({ size: 1, adapter });\n await session.populate('rev_abc', 'SCENE_ABC');\n const slot = await session.acquire('rev_abc');\n session.markDirty(slot);\n expect(slot.currentRevision).toBeNull();\n // next acquire even of the SAME parent must reload\n await session.populate('rev_abc', 'SCENE_ABC');\n const slot2 = await session.acquire('rev_abc');\n expect(adapter.loadFromString).toHaveBeenCalledTimes(2);\n expect(slot2.currentRevision).toBe('rev_abc');\n });\n});\n\ndescribe('EngineSession (multi-slot sticky routing)', () => {\n function makeAdapterWithCounter(): {\n adapter: EngineAdapter;\n createCount: () => number;\n } {\n let id = 0;\n const counters = { creates: 0 };\n const adapter: EngineAdapter = {\n create: () => {\n counters.creates++;\n return { _id: id++, _scene: 'INITIAL' };\n },\n reset: async (eng) => {\n (eng as { _scene: string })._scene = 'RESET';\n },\n loadFromString: async (eng, s) => {\n (eng as { _scene: string })._scene = s;\n },\n saveToString: async (eng) => (eng as { _scene: string })._scene\n };\n return { adapter, createCount: () => counters.creates };\n }\n\n it('returns warm slot whose currentRevision matches the requested parent', async () => {\n const { adapter, createCount } = makeAdapterWithCounter();\n const session = new EngineSession({ size: 2, adapter });\n await session.populate('rev_A', 'SCENE_A');\n const slotA = await session.acquire('rev_A');\n session.commit(slotA, 'rev_A2');\n await session.populate('rev_B', 'SCENE_B');\n const slotB = await session.acquire('rev_B');\n session.commit(slotB, 'rev_B2');\n\n expect(createCount()).toBe(2);\n // Sticky: re-acquire rev_A2 → reuse slotA, no new create, no reload.\n const reuse = await session.acquire('rev_A2');\n expect(reuse.engine).toBe(slotA.engine);\n expect(createCount()).toBe(2);\n });\n\n it('evicts a dirty slot before a warm slot when over cap', async () => {\n const { adapter } = makeAdapterWithCounter();\n const session = new EngineSession({ size: 1, adapter });\n const slot1 = await session.acquire(null);\n session.commit(slot1, 'rev_X');\n session.markDirty(slot1);\n await session.populate('rev_Y', 'SCENE_Y');\n const slot2 = await session.acquire('rev_Y');\n expect(slot2).toBe(slot1); // reused\n expect(slot2.currentRevision).toBe('rev_Y');\n });\n});\n\ndescribe('EngineSession.currentSlot', () => {\n it('returns undefined when no slot is warm', () => {\n const { adapter } = (function makeFakeEngine() {\n const engine = { _scene: 'INITIAL' };\n const adapter: EngineAdapter = {\n create: () => engine,\n reset: vi.fn(async () => {\n engine._scene = 'RESET';\n }),\n loadFromString: vi.fn(async () => {}),\n saveToString: vi.fn(async () => engine._scene)\n };\n return { adapter };\n })();\n const session = new EngineSession({ size: 1, adapter });\n expect(session.currentSlot()).toBeUndefined();\n });\n\n it('returns the slot once it has been committed to a revision', async () => {\n const engine = { _scene: 'INITIAL' };\n const adapter: EngineAdapter = {\n create: () => engine,\n reset: vi.fn(async () => {\n engine._scene = 'RESET';\n }),\n loadFromString: vi.fn(async () => {}),\n saveToString: vi.fn(async () => engine._scene)\n };\n const session = new EngineSession({ size: 1, adapter });\n const slot = await session.acquire(null);\n expect(session.currentSlot()).toBeUndefined(); // currentRevision is null after acquire(null)\n session.commit(slot, 'rev_X');\n expect(session.currentSlot()).toBe(slot);\n });\n});\n"]}
1
+ {"version":3,"file":"session.test.js","sourceRoot":"","sources":["../../../../test/engine/session.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';\n\nfunction makeFakeEngine(scene = 'INITIAL'): {\n engine: { _scene: string };\n adapter: EngineAdapter;\n} {\n const engine = { _scene: scene };\n const adapter: EngineAdapter = {\n create: () => engine,\n reset: vi.fn(async () => {\n engine._scene = 'RESET';\n }),\n loadFromString: vi.fn(async (eng, s) => {\n (eng as typeof engine)._scene = s;\n }),\n saveToString: vi.fn(async (eng) => (eng as typeof engine)._scene)\n };\n return { engine, adapter };\n}\n\ndescribe('EngineSession (single slot)', () => {\n it('acquire(parent=null) resets the engine and sets currentRevision=null', async () => {\n const { adapter } = makeFakeEngine();\n const session = new EngineSession({ size: 1, adapter });\n const slot = await session.acquire(null);\n expect(adapter.reset).toHaveBeenCalledOnce();\n expect(slot.currentRevision).toBeNull();\n });\n\n it('acquire(parent=X) loads scene X when slot currentRevision is different', async () => {\n const { adapter } = makeFakeEngine();\n const session = new EngineSession({ size: 1, adapter });\n await session.populate('rev_abc', 'SCENE_ABC'); // pre-stage the loadable scene\n const slot = await session.acquire('rev_abc');\n expect(adapter.loadFromString).toHaveBeenCalledWith(\n slot.engine,\n 'SCENE_ABC'\n );\n expect(slot.currentRevision).toBe('rev_abc');\n });\n\n it('acquire(parent=X) is a no-op when slot.currentRevision === X (linear edit)', async () => {\n const { adapter } = makeFakeEngine();\n const session = new EngineSession({ size: 1, adapter });\n await session.populate('rev_abc', 'SCENE_ABC');\n await session.acquire('rev_abc');\n (adapter.loadFromString as ReturnType<typeof vi.fn>).mockClear();\n const slot2 = await session.acquire('rev_abc');\n expect(adapter.loadFromString).not.toHaveBeenCalled();\n expect(slot2.currentRevision).toBe('rev_abc');\n });\n\n it('commit(slot, newRev) updates currentRevision (linear edit succeeded)', async () => {\n const { adapter } = makeFakeEngine();\n const session = new EngineSession({ size: 1, adapter });\n const slot = await session.acquire(null);\n session.commit(slot, 'rev_new');\n expect(slot.currentRevision).toBe('rev_new');\n });\n\n it('markDirty(slot) sets currentRevision=null (failed edit forces reload next time)', async () => {\n const { adapter } = makeFakeEngine();\n const session = new EngineSession({ size: 1, adapter });\n await session.populate('rev_abc', 'SCENE_ABC');\n const slot = await session.acquire('rev_abc');\n session.markDirty(slot);\n expect(slot.currentRevision).toBeNull();\n // next acquire even of the SAME parent must reload\n await session.populate('rev_abc', 'SCENE_ABC');\n const slot2 = await session.acquire('rev_abc');\n expect(adapter.loadFromString).toHaveBeenCalledTimes(2);\n expect(slot2.currentRevision).toBe('rev_abc');\n });\n});\n\ndescribe('EngineSession cold-instance reconstruction (spec §8.6)', () => {\n it('reconstructs a cold engine by loading a staged revision scene', async () => {\n // Regression guard: a cold session (no warm slot) must load the staged\n // scene via loadFromString before returning the slot. This documents the\n // edit-tool contract: populate(parent, scene) → acquire(parent) always\n // calls loadFromString, never assumes a warm engine.\n const { adapter } = makeFakeEngine();\n const session = new EngineSession({ size: 1, adapter });\n // Cold: no slots exist yet, so no warm engine.\n await session.populate('rev-1', '<scene-for-rev-1/>');\n const slot = await session.acquire('rev-1');\n expect(adapter.loadFromString).toHaveBeenCalledWith(\n slot.engine,\n '<scene-for-rev-1/>'\n );\n expect(slot.currentRevision).toBe('rev-1');\n });\n});\n\ndescribe('EngineSession (multi-slot sticky routing)', () => {\n function makeAdapterWithCounter(): {\n adapter: EngineAdapter;\n createCount: () => number;\n } {\n let id = 0;\n const counters = { creates: 0 };\n const adapter: EngineAdapter = {\n create: () => {\n counters.creates++;\n return { _id: id++, _scene: 'INITIAL' };\n },\n reset: async (eng) => {\n (eng as { _scene: string })._scene = 'RESET';\n },\n loadFromString: async (eng, s) => {\n (eng as { _scene: string })._scene = s;\n },\n saveToString: async (eng) => (eng as { _scene: string })._scene\n };\n return { adapter, createCount: () => counters.creates };\n }\n\n it('returns warm slot whose currentRevision matches the requested parent', async () => {\n const { adapter, createCount } = makeAdapterWithCounter();\n const session = new EngineSession({ size: 2, adapter });\n await session.populate('rev_A', 'SCENE_A');\n const slotA = await session.acquire('rev_A');\n session.commit(slotA, 'rev_A2');\n await session.populate('rev_B', 'SCENE_B');\n const slotB = await session.acquire('rev_B');\n session.commit(slotB, 'rev_B2');\n\n expect(createCount()).toBe(2);\n // Sticky: re-acquire rev_A2 → reuse slotA, no new create, no reload.\n const reuse = await session.acquire('rev_A2');\n expect(reuse.engine).toBe(slotA.engine);\n expect(createCount()).toBe(2);\n });\n\n it('evicts a dirty slot before a warm slot when over cap', async () => {\n const { adapter } = makeAdapterWithCounter();\n const session = new EngineSession({ size: 1, adapter });\n const slot1 = await session.acquire(null);\n session.commit(slot1, 'rev_X');\n session.markDirty(slot1);\n await session.populate('rev_Y', 'SCENE_Y');\n const slot2 = await session.acquire('rev_Y');\n expect(slot2).toBe(slot1); // reused\n expect(slot2.currentRevision).toBe('rev_Y');\n });\n});\n\ndescribe('EngineSession.currentSlot', () => {\n it('returns undefined when no slot is warm', () => {\n const { adapter } = (function makeFakeEngine() {\n const engine = { _scene: 'INITIAL' };\n const adapter: EngineAdapter = {\n create: () => engine,\n reset: vi.fn(async () => {\n engine._scene = 'RESET';\n }),\n loadFromString: vi.fn(async () => {}),\n saveToString: vi.fn(async () => engine._scene)\n };\n return { adapter };\n })();\n const session = new EngineSession({ size: 1, adapter });\n expect(session.currentSlot()).toBeUndefined();\n });\n\n it('returns the slot once it has been committed to a revision', async () => {\n const engine = { _scene: 'INITIAL' };\n const adapter: EngineAdapter = {\n create: () => engine,\n reset: vi.fn(async () => {\n engine._scene = 'RESET';\n }),\n loadFromString: vi.fn(async () => {}),\n saveToString: vi.fn(async () => engine._scene)\n };\n const session = new EngineSession({ size: 1, adapter });\n const slot = await session.acquire(null);\n expect(session.currentSlot()).toBeUndefined(); // currentRevision is null after acquire(null)\n session.commit(slot, 'rev_X');\n expect(session.currentSlot()).toBe(slot);\n });\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=unsplash-source.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unsplash-source.test.d.ts","sourceRoot":"","sources":["../../../../test/engine/unsplash-source.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,129 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { registerUnsplashSource } from '../../src/engine/unsplash-source.js';
3
+ // Minimal engine double: registerUnsplashSource only touches engine.asset.
4
+ function makeEngine(existingSources = []) {
5
+ const added = [];
6
+ const engine = {
7
+ asset: {
8
+ findAllSources: vi.fn(() => [
9
+ ...existingSources,
10
+ ...added.map((s) => s.id)
11
+ ]),
12
+ addSource: vi.fn((source) => added.push(source)),
13
+ defaultApplyAsset: vi.fn(async () => 1)
14
+ }
15
+ };
16
+ // registerUnsplashSource is typed against CreativeEngine; the double only
17
+ // implements the slice it uses.
18
+ return {
19
+ engine: engine,
20
+ added
21
+ };
22
+ }
23
+ function mockFetch(payload, ok = true, status = 200) {
24
+ const fetchMock = vi.fn(async (_url) => ({
25
+ ok,
26
+ status,
27
+ json: async () => payload
28
+ }));
29
+ vi.stubGlobal('fetch', fetchMock);
30
+ return fetchMock;
31
+ }
32
+ const PHOTO = {
33
+ id: 'abc',
34
+ description: 'A cat',
35
+ width: 800,
36
+ height: 600,
37
+ urls: { regular: 'https://img/regular.jpg', thumb: 'https://img/thumb.jpg' },
38
+ user: { name: 'Jane', links: { html: 'https://unsplash.com/@jane' } },
39
+ tags: [{ title: 'cat' }, { title: 'pet' }]
40
+ };
41
+ function findAssetsOf(added) {
42
+ return added[0].findAssets;
43
+ }
44
+ afterEach(() => {
45
+ vi.unstubAllGlobals();
46
+ });
47
+ describe('registerUnsplashSource', () => {
48
+ it('registers the unsplash source on a fresh engine', () => {
49
+ const { engine, added } = makeEngine();
50
+ registerUnsplashSource(engine);
51
+ expect(added).toHaveLength(1);
52
+ expect(added[0].id).toBe('unsplash');
53
+ });
54
+ it('is idempotent — skips when the source is already present', () => {
55
+ const { engine, added } = makeEngine(['unsplash']);
56
+ registerUnsplashSource(engine);
57
+ expect(added).toHaveLength(0);
58
+ });
59
+ it('searches via /search/photos when a query is set and maps the result', async () => {
60
+ const fetchMock = mockFetch({
61
+ results: [PHOTO],
62
+ total: 50,
63
+ total_pages: 5
64
+ });
65
+ const { engine, added } = makeEngine();
66
+ registerUnsplashSource(engine);
67
+ const query = {
68
+ query: 'cats',
69
+ page: 0,
70
+ perPage: 10,
71
+ locale: 'en'
72
+ };
73
+ const res = (await findAssetsOf(added)(query));
74
+ const url = fetchMock.mock.calls[0][0];
75
+ expect(url).toContain('/search/photos');
76
+ expect(url).toContain('query=cats');
77
+ expect(url).toContain('page=1'); // 0-based query → 1-based Unsplash
78
+ expect(url).toContain('per_page=10');
79
+ expect(res.assets).toHaveLength(1);
80
+ expect(res.assets[0]).toMatchObject({
81
+ id: 'abc',
82
+ label: 'A cat',
83
+ tags: ['cat', 'pet'],
84
+ meta: {
85
+ uri: 'https://img/regular.jpg',
86
+ thumbUri: 'https://img/thumb.jpg'
87
+ },
88
+ credits: { name: 'Jane', url: 'https://unsplash.com/@jane' }
89
+ });
90
+ expect(res.nextPage).toBe(1); // page 0 of 5 → next is 1
91
+ });
92
+ it('browses popular photos via /photos when no query is set', async () => {
93
+ const fetchMock = mockFetch([PHOTO, PHOTO]);
94
+ const { engine, added } = makeEngine();
95
+ registerUnsplashSource(engine);
96
+ const query = {
97
+ page: 0,
98
+ perPage: 2,
99
+ locale: 'en'
100
+ };
101
+ const res = (await findAssetsOf(added)(query));
102
+ const url = fetchMock.mock.calls[0][0];
103
+ expect(url).toContain('/photos?order_by=popular');
104
+ expect(res.assets).toHaveLength(2);
105
+ expect(res.nextPage).toBe(1); // full page → there may be more
106
+ });
107
+ it('stops paginating when a partial page comes back', async () => {
108
+ mockFetch([PHOTO]); // 1 < perPage(2)
109
+ const { engine, added } = makeEngine();
110
+ registerUnsplashSource(engine);
111
+ const res = (await findAssetsOf(added)({
112
+ page: 0,
113
+ perPage: 2,
114
+ locale: 'en'
115
+ }));
116
+ expect(res.nextPage).toBeUndefined();
117
+ });
118
+ it('throws when the proxy returns a non-OK status', async () => {
119
+ mockFetch({}, false, 502);
120
+ const { engine, added } = makeEngine();
121
+ registerUnsplashSource(engine);
122
+ await expect(findAssetsOf(added)({
123
+ page: 0,
124
+ perPage: 5,
125
+ locale: 'en'
126
+ })).rejects.toThrow('unsplash proxy: HTTP 502');
127
+ });
128
+ });
129
+ //# sourceMappingURL=unsplash-source.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unsplash-source.test.js","sourceRoot":"","sources":["../../../../test/engine/unsplash-source.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { afterEach, describe, expect, it, vi } from 'vitest';\nimport type {\n AssetQueryData,\n AssetsQueryResult,\n AssetSource\n} from '@cesdk/engine';\nimport { registerUnsplashSource } from '../../src/engine/unsplash-source.js';\n\n// Minimal engine double: registerUnsplashSource only touches engine.asset.\nfunction makeEngine(existingSources: string[] = []) {\n const added: AssetSource[] = [];\n const engine = {\n asset: {\n findAllSources: vi.fn(() => [\n ...existingSources,\n ...added.map((s) => s.id)\n ]),\n addSource: vi.fn((source: AssetSource) => added.push(source)),\n defaultApplyAsset: vi.fn(async () => 1)\n }\n };\n // registerUnsplashSource is typed against CreativeEngine; the double only\n // implements the slice it uses.\n return {\n engine: engine as unknown as Parameters<typeof registerUnsplashSource>[0],\n added\n };\n}\n\nfunction mockFetch(payload: unknown, ok = true, status = 200) {\n const fetchMock = vi.fn(async (_url: string) => ({\n ok,\n status,\n json: async () => payload\n }));\n vi.stubGlobal('fetch', fetchMock);\n return fetchMock;\n}\n\nconst PHOTO = {\n id: 'abc',\n description: 'A cat',\n width: 800,\n height: 600,\n urls: { regular: 'https://img/regular.jpg', thumb: 'https://img/thumb.jpg' },\n user: { name: 'Jane', links: { html: 'https://unsplash.com/@jane' } },\n tags: [{ title: 'cat' }, { title: 'pet' }]\n};\n\nfunction findAssetsOf(added: AssetSource[]): AssetSource['findAssets'] {\n return added[0].findAssets;\n}\n\nafterEach(() => {\n vi.unstubAllGlobals();\n});\n\ndescribe('registerUnsplashSource', () => {\n it('registers the unsplash source on a fresh engine', () => {\n const { engine, added } = makeEngine();\n registerUnsplashSource(engine);\n expect(added).toHaveLength(1);\n expect(added[0].id).toBe('unsplash');\n });\n\n it('is idempotent — skips when the source is already present', () => {\n const { engine, added } = makeEngine(['unsplash']);\n registerUnsplashSource(engine);\n expect(added).toHaveLength(0);\n });\n\n it('searches via /search/photos when a query is set and maps the result', async () => {\n const fetchMock = mockFetch({\n results: [PHOTO],\n total: 50,\n total_pages: 5\n });\n const { engine, added } = makeEngine();\n registerUnsplashSource(engine);\n\n const query: AssetQueryData = {\n query: 'cats',\n page: 0,\n perPage: 10,\n locale: 'en'\n } as AssetQueryData;\n const res = (await findAssetsOf(added)(query)) as AssetsQueryResult;\n\n const url = fetchMock.mock.calls[0][0];\n expect(url).toContain('/search/photos');\n expect(url).toContain('query=cats');\n expect(url).toContain('page=1'); // 0-based query → 1-based Unsplash\n expect(url).toContain('per_page=10');\n\n expect(res.assets).toHaveLength(1);\n expect(res.assets[0]).toMatchObject({\n id: 'abc',\n label: 'A cat',\n tags: ['cat', 'pet'],\n meta: {\n uri: 'https://img/regular.jpg',\n thumbUri: 'https://img/thumb.jpg'\n },\n credits: { name: 'Jane', url: 'https://unsplash.com/@jane' }\n });\n expect(res.nextPage).toBe(1); // page 0 of 5 → next is 1\n });\n\n it('browses popular photos via /photos when no query is set', async () => {\n const fetchMock = mockFetch([PHOTO, PHOTO]);\n const { engine, added } = makeEngine();\n registerUnsplashSource(engine);\n\n const query: AssetQueryData = {\n page: 0,\n perPage: 2,\n locale: 'en'\n } as AssetQueryData;\n const res = (await findAssetsOf(added)(query)) as AssetsQueryResult;\n\n const url = fetchMock.mock.calls[0][0];\n expect(url).toContain('/photos?order_by=popular');\n expect(res.assets).toHaveLength(2);\n expect(res.nextPage).toBe(1); // full page → there may be more\n });\n\n it('stops paginating when a partial page comes back', async () => {\n mockFetch([PHOTO]); // 1 < perPage(2)\n const { engine, added } = makeEngine();\n registerUnsplashSource(engine);\n const res = (await findAssetsOf(added)({\n page: 0,\n perPage: 2,\n locale: 'en'\n } as AssetQueryData)) as AssetsQueryResult;\n expect(res.nextPage).toBeUndefined();\n });\n\n it('throws when the proxy returns a non-OK status', async () => {\n mockFetch({}, false, 502);\n const { engine, added } = makeEngine();\n registerUnsplashSource(engine);\n await expect(\n findAssetsOf(added)({\n page: 0,\n perPage: 5,\n locale: 'en'\n } as AssetQueryData)\n ).rejects.toThrow('unsplash proxy: HTTP 502');\n });\n});\n"]}