@forgeax/engine-project 0.0.0-dev.8d955ade1c79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +77 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/ac14-migration.test.d.ts +2 -0
- package/dist/__tests__/ac14-migration.test.d.ts.map +1 -0
- package/dist/__tests__/errors.test-d.d.ts +2 -0
- package/dist/__tests__/errors.test-d.d.ts.map +1 -0
- package/dist/__tests__/errors.test.d.ts +2 -0
- package/dist/__tests__/errors.test.d.ts.map +1 -0
- package/dist/__tests__/loader.test.d.ts +2 -0
- package/dist/__tests__/loader.test.d.ts.map +1 -0
- package/dist/__tests__/resolve.test.d.ts +2 -0
- package/dist/__tests__/resolve.test.d.ts.map +1 -0
- package/dist/__tests__/schema.test.d.ts +2 -0
- package/dist/__tests__/schema.test.d.ts.map +1 -0
- package/dist/__tests__/structural.test.d.ts +2 -0
- package/dist/__tests__/structural.test.d.ts.map +1 -0
- package/dist/errors.d.ts +76 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +244 -0
- package/dist/index.mjs.map +1 -0
- package/dist/loader.d.ts +96 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/paths.d.ts +3 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/schema.d.ts +216 -0
- package/dist/schema.d.ts.map +1 -0
- package/package.json +65 -0
- package/src/__tests__/ac14-migration.test.ts +124 -0
- package/src/__tests__/errors.test-d.ts +69 -0
- package/src/__tests__/errors.test.ts +233 -0
- package/src/__tests__/fixtures/games/cow-level/forge.json +7 -0
- package/src/__tests__/fixtures/games/cow-survivor/forge.json +8 -0
- package/src/__tests__/fixtures/games/fps/forge.json +7 -0
- package/src/__tests__/fixtures/games/hellforge/forge.json +24 -0
- package/src/__tests__/fixtures/games/shoot-opt/forge.json +6 -0
- package/src/__tests__/fixtures/games/spin-cube/forge.json +6 -0
- package/src/__tests__/loader.test.ts +279 -0
- package/src/__tests__/resolve.test.ts +116 -0
- package/src/__tests__/schema.test.ts +317 -0
- package/src/__tests__/structural.test.ts +187 -0
- package/src/errors.ts +118 -0
- package/src/index.ts +35 -0
- package/src/loader.ts +282 -0
- package/src/paths.ts +7 -0
- package/src/schema.ts +149 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// loader.test.ts — w4: loadGameProject 5 return paths + sync companion tests
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { loadGameProject, loadGameProjectSync, validateGameProject } from '../loader.js';
|
|
4
|
+
|
|
5
|
+
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
6
|
+
function makeRead(content: string): (path: string) => Promise<string> {
|
|
7
|
+
return async (_path: string) => content;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function makeReadThrow(err: Error): (path: string) => Promise<string> {
|
|
11
|
+
return async (_path: string) => {
|
|
12
|
+
throw err;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const VALID_FORGE_JSON = JSON.stringify({
|
|
17
|
+
id: 'test-game',
|
|
18
|
+
name: 'Test Game',
|
|
19
|
+
schemaVersion: '1.0.0',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const VALID_WITH_DEFAULT_SCENE = JSON.stringify({
|
|
23
|
+
id: 'test-game',
|
|
24
|
+
name: 'Test Game',
|
|
25
|
+
schemaVersion: '1.0.0',
|
|
26
|
+
defaultScene: '15acc839-d847-527c-8284-bfb36d7c50de',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const INVALID_JSON = '{id: broken';
|
|
30
|
+
|
|
31
|
+
const WITH_SCENES_ARRAY = JSON.stringify({
|
|
32
|
+
id: 'has-scenes',
|
|
33
|
+
name: 'Has Scenes',
|
|
34
|
+
schemaVersion: '1.0.0',
|
|
35
|
+
scenes: [{ id: 'l1', name: 'Level 1', pack: 'scenes/level1.pack.json' }],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const WITH_BAD_GUID = JSON.stringify({
|
|
39
|
+
id: 'bad-guid',
|
|
40
|
+
name: 'Bad GUID',
|
|
41
|
+
schemaVersion: '1.0.0',
|
|
42
|
+
defaultScene: 'not-a-guid',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const WITHOUT_DEFAULT_SCENE = JSON.stringify({
|
|
46
|
+
id: 'no-default',
|
|
47
|
+
name: 'No Default',
|
|
48
|
+
schemaVersion: '1.0.0',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const WITH_NPC_POLICY = JSON.stringify({
|
|
52
|
+
id: 'npc-game',
|
|
53
|
+
name: 'NPC Game',
|
|
54
|
+
schemaVersion: '1.0.0',
|
|
55
|
+
npc: {
|
|
56
|
+
model: 'deepseek-v4-flash-openai',
|
|
57
|
+
maxTokens: 220,
|
|
58
|
+
budget: { maxCallsPerMinute: 30, maxTokensPerMinute: 120000, maxConcurrent: 4 },
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ── return path 1: forge.json missing ──────────────────────────────────────
|
|
63
|
+
describe('loadGameProject — return path 1: file missing', () => {
|
|
64
|
+
it('returns {ok:false} with code forge-missing when read throws', async () => {
|
|
65
|
+
const notFound = new Error('ENOENT: no such file');
|
|
66
|
+
(notFound as NodeJS.ErrnoException).code = 'ENOENT';
|
|
67
|
+
const result = await loadGameProject(makeReadThrow(notFound));
|
|
68
|
+
expect(result.ok).toBe(false);
|
|
69
|
+
if (!result.ok) {
|
|
70
|
+
expect(result.error.code).toBe('forge-missing');
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('returns {ok:false} when read rejects', async () => {
|
|
75
|
+
const result = await loadGameProject(async () => {
|
|
76
|
+
throw new Error('read failed');
|
|
77
|
+
});
|
|
78
|
+
expect(result.ok).toBe(false);
|
|
79
|
+
if (!result.ok) {
|
|
80
|
+
expect(result.error.code).toBe('forge-missing');
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ── return path 2: invalid JSON ─────────────────────────────────────────────
|
|
86
|
+
describe('loadGameProject — return path 2: parse failed', () => {
|
|
87
|
+
it('returns {ok:false} with code forge-parse-failed for invalid JSON', async () => {
|
|
88
|
+
const result = await loadGameProject(makeRead(INVALID_JSON));
|
|
89
|
+
expect(result.ok).toBe(false);
|
|
90
|
+
if (!result.ok) {
|
|
91
|
+
expect(result.error.code).toBe('forge-parse-failed');
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── return path 3: strict rejects unknown fields (scenes[]) ─────────────────
|
|
97
|
+
describe('loadGameProject — return path 3: unknown fields', () => {
|
|
98
|
+
it('returns {ok:false} with code forge-unknown-field when scenes[] present', async () => {
|
|
99
|
+
const result = await loadGameProject(makeRead(WITH_SCENES_ARRAY));
|
|
100
|
+
expect(result.ok).toBe(false);
|
|
101
|
+
if (!result.ok) {
|
|
102
|
+
expect(result.error.code).toBe('forge-unknown-field');
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ── return path 4: defaultScene GUID format invalid ─────────────────────────
|
|
108
|
+
describe('loadGameProject — return path 4: GUID malformed', () => {
|
|
109
|
+
it('returns {ok:false} with code forge-guid-malformed for bad GUID', async () => {
|
|
110
|
+
const result = await loadGameProject(makeRead(WITH_BAD_GUID));
|
|
111
|
+
expect(result.ok).toBe(false);
|
|
112
|
+
if (!result.ok) {
|
|
113
|
+
expect(result.error.code).toBe('forge-guid-malformed');
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ── return path 5: completely valid forge.json ──────────────────────────────
|
|
119
|
+
describe('loadGameProject — return path 5: valid forge.json', () => {
|
|
120
|
+
it('returns {ok:true} with GameProject value for valid forge.json', async () => {
|
|
121
|
+
const result = await loadGameProject(makeRead(VALID_FORGE_JSON));
|
|
122
|
+
expect(result.ok).toBe(true);
|
|
123
|
+
if (result.ok) {
|
|
124
|
+
expect(result.value).toBeDefined();
|
|
125
|
+
expect(result.value.id).toBe('test-game');
|
|
126
|
+
expect(result.value.name).toBe('Test Game');
|
|
127
|
+
expect(result.value.schemaVersion).toBe('1.0.0');
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('returns {ok:true} for valid forge.json with defaultScene', async () => {
|
|
132
|
+
const result = await loadGameProject(makeRead(VALID_WITH_DEFAULT_SCENE));
|
|
133
|
+
expect(result.ok).toBe(true);
|
|
134
|
+
if (result.ok) {
|
|
135
|
+
expect(result.value.defaultScene).toBeDefined();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('returns {ok:true} when defaultScene is absent (optional)', async () => {
|
|
140
|
+
const result = await loadGameProject(makeRead(WITHOUT_DEFAULT_SCENE));
|
|
141
|
+
expect(result.ok).toBe(true);
|
|
142
|
+
if (result.ok) {
|
|
143
|
+
expect(result.value.id).toBe('no-default');
|
|
144
|
+
expect(result.value.defaultScene).toBeUndefined();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('accepts the Digital Life NPC policy in forge.json', async () => {
|
|
149
|
+
const result = await loadGameProject(makeRead(WITH_NPC_POLICY));
|
|
150
|
+
expect(result.ok).toBe(true);
|
|
151
|
+
if (result.ok) {
|
|
152
|
+
expect(result.value.npc?.model).toBe('deepseek-v4-flash-openai');
|
|
153
|
+
expect(result.value.npc?.budget?.maxConcurrent).toBe(4);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('is idempotent — same read returns same result', async () => {
|
|
158
|
+
const read = makeRead(VALID_FORGE_JSON);
|
|
159
|
+
const r1 = await loadGameProject(read);
|
|
160
|
+
const r2 = await loadGameProject(read);
|
|
161
|
+
expect(r1.ok).toBe(r2.ok);
|
|
162
|
+
if (r1.ok && r2.ok) {
|
|
163
|
+
expect(r1.value.id).toBe(r2.value.id);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('does NOT call resolveGuid internally', async () => {
|
|
168
|
+
// loadGameProject only accepts `read` — no resolveGuid in signature
|
|
169
|
+
// Signature-level verification: the function is callable with just read
|
|
170
|
+
const read = makeRead(VALID_FORGE_JSON);
|
|
171
|
+
const result = await loadGameProject(read);
|
|
172
|
+
expect(result).toBeDefined();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ── missing schemaVersion → schema-invalid ──────────────────────────────────
|
|
177
|
+
describe('loadGameProject — missing required fields', () => {
|
|
178
|
+
it('returns {ok:false} when schemaVersion is missing', async () => {
|
|
179
|
+
const noSchemaVersion = JSON.stringify({ id: 'test', name: 'Test' });
|
|
180
|
+
const result = await loadGameProject(makeRead(noSchemaVersion));
|
|
181
|
+
expect(result.ok).toBe(false);
|
|
182
|
+
if (!result.ok) {
|
|
183
|
+
expect(result.error.code).toBe('forge-schema-invalid');
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ── validateGameProject: sync pure core ─────────────────────────────────────
|
|
189
|
+
function makeSyncRead(content: string): (path: string) => string {
|
|
190
|
+
return (_path: string) => content;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function makeSyncReadThrow(err: Error): (path: string) => string {
|
|
194
|
+
return (_path: string) => {
|
|
195
|
+
throw err;
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
describe('validateGameProject — sync pure core', () => {
|
|
200
|
+
it('returns {ok:true} for valid forge.json', () => {
|
|
201
|
+
const result = validateGameProject(VALID_FORGE_JSON);
|
|
202
|
+
expect(result.ok).toBe(true);
|
|
203
|
+
if (result.ok) {
|
|
204
|
+
expect(result.value.id).toBe('test-game');
|
|
205
|
+
expect(result.value.name).toBe('Test Game');
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('returns {ok:false, code:forge-parse-failed} for invalid JSON', () => {
|
|
210
|
+
const result = validateGameProject(INVALID_JSON);
|
|
211
|
+
expect(result.ok).toBe(false);
|
|
212
|
+
if (!result.ok) {
|
|
213
|
+
expect(result.error.code).toBe('forge-parse-failed');
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('returns {ok:false, code:forge-unknown-field} when scenes[] present', () => {
|
|
218
|
+
const result = validateGameProject(WITH_SCENES_ARRAY);
|
|
219
|
+
expect(result.ok).toBe(false);
|
|
220
|
+
if (!result.ok) {
|
|
221
|
+
expect(result.error.code).toBe('forge-unknown-field');
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('returns {ok:false, code:forge-guid-malformed} for bad GUID', () => {
|
|
226
|
+
const result = validateGameProject(WITH_BAD_GUID);
|
|
227
|
+
expect(result.ok).toBe(false);
|
|
228
|
+
if (!result.ok) {
|
|
229
|
+
expect(result.error.code).toBe('forge-guid-malformed');
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('returns {ok:false, code:forge-schema-invalid} when schemaVersion missing', () => {
|
|
234
|
+
const noSchemaVersion = JSON.stringify({ id: 'test', name: 'Test' });
|
|
235
|
+
const result = validateGameProject(noSchemaVersion);
|
|
236
|
+
expect(result.ok).toBe(false);
|
|
237
|
+
if (!result.ok) {
|
|
238
|
+
expect(result.error.code).toBe('forge-schema-invalid');
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// ── loadGameProjectSync: sync companion ─────────────────────────────────────
|
|
244
|
+
describe('loadGameProjectSync — sync companion', () => {
|
|
245
|
+
it('returns {ok:true} for valid forge.json', () => {
|
|
246
|
+
const result = loadGameProjectSync(makeSyncRead(VALID_FORGE_JSON));
|
|
247
|
+
expect(result.ok).toBe(true);
|
|
248
|
+
if (result.ok) {
|
|
249
|
+
expect(result.value.id).toBe('test-game');
|
|
250
|
+
expect(result.value.name).toBe('Test Game');
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('returns {ok:false, code:forge-missing} when read throws', () => {
|
|
255
|
+
const notFound = new Error('ENOENT: no such file');
|
|
256
|
+
(notFound as NodeJS.ErrnoException).code = 'ENOENT';
|
|
257
|
+
const result = loadGameProjectSync(makeSyncReadThrow(notFound));
|
|
258
|
+
expect(result.ok).toBe(false);
|
|
259
|
+
if (!result.ok) {
|
|
260
|
+
expect(result.error.code).toBe('forge-missing');
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('returns {ok:false, code:forge-unknown-field} when scenes[] present', () => {
|
|
265
|
+
const result = loadGameProjectSync(makeSyncRead(WITH_SCENES_ARRAY));
|
|
266
|
+
expect(result.ok).toBe(false);
|
|
267
|
+
if (!result.ok) {
|
|
268
|
+
expect(result.error.code).toBe('forge-unknown-field');
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('returns {ok:false, code:forge-guid-malformed} for bad GUID', () => {
|
|
273
|
+
const result = loadGameProjectSync(makeSyncRead(WITH_BAD_GUID));
|
|
274
|
+
expect(result.ok).toBe(false);
|
|
275
|
+
if (!result.ok) {
|
|
276
|
+
expect(result.error.code).toBe('forge-guid-malformed');
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// resolve.test.ts — w5: resolveDefaultScene double-injection tests
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { resolveDefaultScene } from '../loader.js';
|
|
4
|
+
|
|
5
|
+
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
6
|
+
function makeRead(content: string): (path: string) => Promise<string> {
|
|
7
|
+
return async (_path: string) => content;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const VALID_WITH_SCENE = JSON.stringify({
|
|
11
|
+
id: 'test-game',
|
|
12
|
+
name: 'Test Game',
|
|
13
|
+
schemaVersion: '1.0.0',
|
|
14
|
+
defaultScene: '15acc839-d847-527c-8284-bfb36d7c50de',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const WITHOUT_SCENE = JSON.stringify({
|
|
18
|
+
id: 'no-scene',
|
|
19
|
+
name: 'No Scene',
|
|
20
|
+
schemaVersion: '1.0.0',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// ── signature verification ──────────────────────────────────────────────────
|
|
24
|
+
describe('resolveDefaultScene — signature', () => {
|
|
25
|
+
it('accepts {read, resolveGuid} as both injection points', async () => {
|
|
26
|
+
const read = makeRead(VALID_WITH_SCENE);
|
|
27
|
+
const resolveGuid = async (guid: string) => ({
|
|
28
|
+
ok: true as const,
|
|
29
|
+
value: { kind: 'scene', guid },
|
|
30
|
+
});
|
|
31
|
+
const result = await resolveDefaultScene({ read, resolveGuid });
|
|
32
|
+
expect(result.ok).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ── resolve success path ────────────────────────────────────────────────────
|
|
37
|
+
describe('resolveDefaultScene — success path', () => {
|
|
38
|
+
it('resolves when resolveGuid returns {ok:true} with kind=scene', async () => {
|
|
39
|
+
const read = makeRead(VALID_WITH_SCENE);
|
|
40
|
+
const resolveGuid = vi.fn(async (_guid: string) => ({
|
|
41
|
+
ok: true as const,
|
|
42
|
+
value: { kind: 'scene', guid: _guid },
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
const result = await resolveDefaultScene({ read, resolveGuid });
|
|
46
|
+
expect(result.ok).toBe(true);
|
|
47
|
+
if (result.ok) {
|
|
48
|
+
expect(result.value.kind).toBe('scene');
|
|
49
|
+
expect(result.value.guid).toBe('15acc839-d847-527c-8284-bfb36d7c50de');
|
|
50
|
+
}
|
|
51
|
+
expect(resolveGuid).toHaveBeenCalledWith('15acc839-d847-527c-8284-bfb36d7c50de');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ── resolve failure: GUID not found ─────────────────────────────────────────
|
|
56
|
+
describe('resolveDefaultScene — GUID not found', () => {
|
|
57
|
+
it('returns {ok:false} when resolveGuid returns {ok:false}', async () => {
|
|
58
|
+
const read = makeRead(VALID_WITH_SCENE);
|
|
59
|
+
const resolveGuid = async (_guid: string) => ({
|
|
60
|
+
ok: false as const,
|
|
61
|
+
error: new Error('not found'),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const result = await resolveDefaultScene({ read, resolveGuid });
|
|
65
|
+
expect(result.ok).toBe(false);
|
|
66
|
+
if (!result.ok) {
|
|
67
|
+
expect(result.error.code).toBe('forge-scene-unresolved');
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ── resolve failure: kind not scene ─────────────────────────────────────────
|
|
73
|
+
describe('resolveDefaultScene — kind mismatch', () => {
|
|
74
|
+
it('returns {ok:false} when resolved asset kind !== scene', async () => {
|
|
75
|
+
const read = makeRead(VALID_WITH_SCENE);
|
|
76
|
+
const resolveGuid = async (_guid: string) => ({
|
|
77
|
+
ok: true as const,
|
|
78
|
+
value: { kind: 'texture', guid: _guid },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const result = await resolveDefaultScene({ read, resolveGuid });
|
|
82
|
+
expect(result.ok).toBe(false);
|
|
83
|
+
if (!result.ok) {
|
|
84
|
+
expect(result.error.code).toBe('forge-scene-unresolved');
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ── no defaultScene ─────────────────────────────────────────────────────────
|
|
90
|
+
describe('resolveDefaultScene — no defaultScene', () => {
|
|
91
|
+
it('returns {ok:false} when forge.json has no defaultScene', async () => {
|
|
92
|
+
const read = makeRead(WITHOUT_SCENE);
|
|
93
|
+
const resolveGuid = async (_guid: string) => ({
|
|
94
|
+
ok: true as const,
|
|
95
|
+
value: { kind: 'scene', guid: _guid },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const result = await resolveDefaultScene({ read, resolveGuid });
|
|
99
|
+
expect(result.ok).toBe(false);
|
|
100
|
+
if (!result.ok) {
|
|
101
|
+
expect(result.error.code).toBe('forge-scene-unresolved');
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ── loadGameProject isolation ───────────────────────────────────────────────
|
|
107
|
+
describe('resolveDefaultScene — loadGameProject isolation', () => {
|
|
108
|
+
it('resolveDefaultScene accepts {read, resolveGuid} — loadGameProject accepts read only', () => {
|
|
109
|
+
// Type-level verification:
|
|
110
|
+
// loadGameProject: (read: (path)=>Promise<string>) => ...
|
|
111
|
+
// resolveDefaultScene: ({read, resolveGuid}) => ...
|
|
112
|
+
// The dual-injection signature separates resolve from format.
|
|
113
|
+
const _read = makeRead(VALID_WITH_SCENE);
|
|
114
|
+
expect(typeof _read).toBe('function');
|
|
115
|
+
});
|
|
116
|
+
});
|