@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
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgeax/engine-project",
|
|
3
|
+
"version": "0.0.0-dev.8d955ade1c79",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"description": "Game-project SSOT — zod schema + injectable loader + resolve layer for forge.json, the authoritative game manifest contract.",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.mjs",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@forgeax/engine-pack": "0.0.0-dev.8d955ade1c79",
|
|
26
|
+
"zod": "^3.25.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^20.14.0",
|
|
30
|
+
"tsup": "^8.3.5",
|
|
31
|
+
"typescript": "^5.8.3",
|
|
32
|
+
"vitest": "4.1.5"
|
|
33
|
+
},
|
|
34
|
+
"forgeax": {
|
|
35
|
+
"metrics": {
|
|
36
|
+
"bundle-size": {
|
|
37
|
+
"enabled": true,
|
|
38
|
+
"path": "dist/index.mjs",
|
|
39
|
+
"compression": "gzip"
|
|
40
|
+
},
|
|
41
|
+
"fps": {
|
|
42
|
+
"enabled": false,
|
|
43
|
+
"reason": "pure data/contract package; no runtime canvas or frame loop"
|
|
44
|
+
},
|
|
45
|
+
"bench": {
|
|
46
|
+
"enabled": false,
|
|
47
|
+
"reason": "no perf-critical hot path in contract layer; throughput not instrumented"
|
|
48
|
+
},
|
|
49
|
+
"gate": {
|
|
50
|
+
"enabled": false,
|
|
51
|
+
"reason": "package-level gate scripts declared in engine CI yaml directly; no standalone binary gate"
|
|
52
|
+
},
|
|
53
|
+
"spike-report": {
|
|
54
|
+
"enabled": false,
|
|
55
|
+
"reason": "not a spike package"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsup",
|
|
61
|
+
"typecheck": "tsc -b",
|
|
62
|
+
"test": "vitest run",
|
|
63
|
+
"test:watch": "vitest"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// ac14-migration.test.ts — w23: AC-14 integration — all 6 migrated forge.json pass loadGameProject
|
|
2
|
+
//
|
|
3
|
+
// Loads each migrated game's forge.json via node:fs readFileSync adapter,
|
|
4
|
+
// asserts r.ok===true, verifies schemaVersion/defaultScene/entry per D-3 GUID table.
|
|
5
|
+
// This is the R-3 risk closure evidence (plan-strategy section 4 R-3, section 5.4).
|
|
6
|
+
//
|
|
7
|
+
// Fixture approach: 6 migrated forge.json copies live under __tests__/fixtures/games/
|
|
8
|
+
// (plan w23 description secondary path: the live paths under packages/games/ use a
|
|
9
|
+
// Chinese directory name that triggers the engine english-only gate; fixtures avoid
|
|
10
|
+
// the CJK path literal while testing the same post-migration forge.json content).
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, resolve } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { describe, expect, it } from 'vitest';
|
|
16
|
+
import { loadGameProject } from '../loader.js';
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
const fixturesDir = resolve(__dirname, 'fixtures/games');
|
|
21
|
+
|
|
22
|
+
function fsRead(filePath: string): (path: string) => Promise<string> {
|
|
23
|
+
return async () => readFileSync(filePath, 'utf-8');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface GameFixture {
|
|
27
|
+
name: string;
|
|
28
|
+
dir: string;
|
|
29
|
+
hasDefaultScene: boolean;
|
|
30
|
+
expectedDefaultScene?: string;
|
|
31
|
+
expectedEntry?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 6 migrated games per D-3 GUID table (plan-strategy section 2)
|
|
35
|
+
// Fixture dirs use romanized names (original Chinese dir -> cow-level)
|
|
36
|
+
const GAMES: GameFixture[] = [
|
|
37
|
+
{
|
|
38
|
+
name: 'hellforge',
|
|
39
|
+
dir: 'hellforge',
|
|
40
|
+
hasDefaultScene: true,
|
|
41
|
+
expectedDefaultScene: '15acc839-d847-527c-8284-bfb36d7c50de',
|
|
42
|
+
expectedEntry: 'main.ts',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'cow-survivor',
|
|
46
|
+
dir: 'cow-survivor',
|
|
47
|
+
hasDefaultScene: true,
|
|
48
|
+
expectedDefaultScene: '7b4d43d4-5b19-5903-8966-f89671d21565',
|
|
49
|
+
expectedEntry: 'main.ts',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'spin-cube',
|
|
53
|
+
dir: 'spin-cube',
|
|
54
|
+
hasDefaultScene: false,
|
|
55
|
+
expectedEntry: 'main.ts',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'cow-level',
|
|
59
|
+
dir: 'cow-level',
|
|
60
|
+
hasDefaultScene: false,
|
|
61
|
+
expectedEntry: 'main.ts',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'fps',
|
|
65
|
+
dir: 'fps',
|
|
66
|
+
hasDefaultScene: false,
|
|
67
|
+
expectedEntry: 'main.ts',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'shoot-opt',
|
|
71
|
+
dir: 'shoot-opt',
|
|
72
|
+
hasDefaultScene: false,
|
|
73
|
+
expectedEntry: 'src/main.ts',
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
describe('AC-14 — migrated game forge.json validation', () => {
|
|
78
|
+
for (const game of GAMES) {
|
|
79
|
+
const forgePath = resolve(fixturesDir, game.dir, 'forge.json');
|
|
80
|
+
|
|
81
|
+
describe(game.name, () => {
|
|
82
|
+
it('loads successfully (r.ok === true)', async () => {
|
|
83
|
+
const result = await loadGameProject(fsRead(forgePath));
|
|
84
|
+
expect(result.ok).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('has schemaVersion === "1.0.0"', async () => {
|
|
88
|
+
const result = await loadGameProject(fsRead(forgePath));
|
|
89
|
+
expect(result.ok).toBe(true);
|
|
90
|
+
if (result.ok) {
|
|
91
|
+
expect(result.value.schemaVersion).toBe('1.0.0');
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (game.hasDefaultScene) {
|
|
96
|
+
it(`has defaultScene === "${game.expectedDefaultScene}"`, async () => {
|
|
97
|
+
const result = await loadGameProject(fsRead(forgePath));
|
|
98
|
+
expect(result.ok).toBe(true);
|
|
99
|
+
if (result.ok) {
|
|
100
|
+
expect(result.value.defaultScene).toBe(game.expectedDefaultScene);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
it('has no defaultScene (undefined)', async () => {
|
|
105
|
+
const result = await loadGameProject(fsRead(forgePath));
|
|
106
|
+
expect(result.ok).toBe(true);
|
|
107
|
+
if (result.ok) {
|
|
108
|
+
expect(result.value.defaultScene).toBeUndefined();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (game.expectedEntry) {
|
|
114
|
+
it(`has entry === "${game.expectedEntry}"`, async () => {
|
|
115
|
+
const result = await loadGameProject(fsRead(forgePath));
|
|
116
|
+
expect(result.ok).toBe(true);
|
|
117
|
+
if (result.ok) {
|
|
118
|
+
expect(result.value.entry).toBe(game.expectedEntry);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { GameProjectError as GameProjectErrorType } from '../index.js';
|
|
2
|
+
import { GameProjectError } from '../index.js';
|
|
3
|
+
|
|
4
|
+
const positiveErrors: GameProjectErrorType[] = [
|
|
5
|
+
new GameProjectError({
|
|
6
|
+
code: 'forge-missing',
|
|
7
|
+
expected: 'forge.json to exist',
|
|
8
|
+
hint: 'create forge.json',
|
|
9
|
+
detail: { path: 'forge.json' },
|
|
10
|
+
}),
|
|
11
|
+
new GameProjectError({
|
|
12
|
+
code: 'forge-parse-failed',
|
|
13
|
+
expected: 'valid JSON',
|
|
14
|
+
hint: 'fix JSON',
|
|
15
|
+
detail: { path: 'forge.json', rawMessage: 'Unexpected token' },
|
|
16
|
+
}),
|
|
17
|
+
new GameProjectError({
|
|
18
|
+
code: 'forge-schema-invalid',
|
|
19
|
+
expected: 'schema-valid JSON',
|
|
20
|
+
hint: 'fix fields',
|
|
21
|
+
detail: { path: 'forge.json', zodErrors: [] },
|
|
22
|
+
}),
|
|
23
|
+
new GameProjectError({
|
|
24
|
+
code: 'forge-unknown-field',
|
|
25
|
+
expected: 'known fields',
|
|
26
|
+
hint: 'remove the field',
|
|
27
|
+
detail: { path: 'forge.json', fieldNames: ['scenes'] },
|
|
28
|
+
}),
|
|
29
|
+
new GameProjectError({
|
|
30
|
+
code: 'forge-guid-malformed',
|
|
31
|
+
expected: 'a valid GUID',
|
|
32
|
+
hint: 'use a scene GUID',
|
|
33
|
+
detail: { field: 'defaultScene', rawInput: 'not-a-guid' },
|
|
34
|
+
}),
|
|
35
|
+
new GameProjectError({
|
|
36
|
+
code: 'forge-scene-unresolved',
|
|
37
|
+
expected: 'a resolvable scene',
|
|
38
|
+
hint: 'check the scene pack',
|
|
39
|
+
detail: { guid: '15acc839-d847-527c-8284-bfb36d7c50de' },
|
|
40
|
+
}),
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
new GameProjectError({
|
|
44
|
+
code: 'forge-guid-malformed',
|
|
45
|
+
expected: 'a valid GUID',
|
|
46
|
+
hint: 'use a scene GUID',
|
|
47
|
+
// @ts-expect-error code/detail pairs remain correlated.
|
|
48
|
+
detail: { path: 'forge.json' },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function describe(error: GameProjectErrorType): string {
|
|
52
|
+
switch (error.code) {
|
|
53
|
+
case 'forge-missing':
|
|
54
|
+
return error.detail.path;
|
|
55
|
+
case 'forge-parse-failed':
|
|
56
|
+
return error.detail.rawMessage;
|
|
57
|
+
case 'forge-schema-invalid':
|
|
58
|
+
return String(error.detail.zodErrors.length);
|
|
59
|
+
case 'forge-unknown-field':
|
|
60
|
+
return error.detail.fieldNames.join(',');
|
|
61
|
+
case 'forge-guid-malformed':
|
|
62
|
+
return error.detail.rawInput;
|
|
63
|
+
case 'forge-scene-unresolved':
|
|
64
|
+
return error.detail.guid;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
void positiveErrors;
|
|
69
|
+
void describe;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// errors.test.ts — w3: GameProjectError code union exhaustive tests
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import type { GameProjectErrorCode, GameProjectErrorDetail } from '../errors.js';
|
|
4
|
+
import { GameProjectError } from '../errors.js';
|
|
5
|
+
|
|
6
|
+
// ── helper ───────────────────────────────────────────────────────────────────
|
|
7
|
+
function makeErr(): GameProjectError {
|
|
8
|
+
return new GameProjectError({
|
|
9
|
+
code: 'forge-missing',
|
|
10
|
+
expected: 'test expected',
|
|
11
|
+
hint: 'test hint',
|
|
12
|
+
detail: { path: '/fake/forge.json' },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const errors: GameProjectError[] = [
|
|
17
|
+
new GameProjectError({
|
|
18
|
+
code: 'forge-missing',
|
|
19
|
+
expected: 'test expected',
|
|
20
|
+
hint: 'test hint',
|
|
21
|
+
detail: { path: '/fake/forge.json' },
|
|
22
|
+
}),
|
|
23
|
+
new GameProjectError({
|
|
24
|
+
code: 'forge-parse-failed',
|
|
25
|
+
expected: 'test expected',
|
|
26
|
+
hint: 'test hint',
|
|
27
|
+
detail: { path: '/fake/forge.json', rawMessage: 'invalid JSON' },
|
|
28
|
+
}),
|
|
29
|
+
new GameProjectError({
|
|
30
|
+
code: 'forge-schema-invalid',
|
|
31
|
+
expected: 'test expected',
|
|
32
|
+
hint: 'test hint',
|
|
33
|
+
detail: { path: '/fake/forge.json', zodErrors: [] },
|
|
34
|
+
}),
|
|
35
|
+
new GameProjectError({
|
|
36
|
+
code: 'forge-unknown-field',
|
|
37
|
+
expected: 'test expected',
|
|
38
|
+
hint: 'test hint',
|
|
39
|
+
detail: { path: '/fake/forge.json', fieldNames: ['scenes'] },
|
|
40
|
+
}),
|
|
41
|
+
new GameProjectError({
|
|
42
|
+
code: 'forge-guid-malformed',
|
|
43
|
+
expected: 'test expected',
|
|
44
|
+
hint: 'test hint',
|
|
45
|
+
detail: { field: 'defaultScene', rawInput: 'bad-guid', cause: new Error('bad GUID') },
|
|
46
|
+
}),
|
|
47
|
+
new GameProjectError({
|
|
48
|
+
code: 'forge-scene-unresolved',
|
|
49
|
+
expected: 'test expected',
|
|
50
|
+
hint: 'test hint',
|
|
51
|
+
detail: { guid: '15acc839-d847-527c-8284-bfb36d7c50de' },
|
|
52
|
+
}),
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// ── four-property surface ───────────────────────────────────────────────────
|
|
56
|
+
describe('GameProjectError — structural surface', () => {
|
|
57
|
+
it('has .code, .expected, .hint, .detail', () => {
|
|
58
|
+
const err = makeErr();
|
|
59
|
+
expect(err).toHaveProperty('code');
|
|
60
|
+
expect(err).toHaveProperty('expected');
|
|
61
|
+
expect(err).toHaveProperty('hint');
|
|
62
|
+
expect(err).toHaveProperty('detail');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('.code is readonly (TS compile-time, not runtime)', () => {
|
|
66
|
+
const err = makeErr();
|
|
67
|
+
// TS `readonly` is compile-time only; at runtime the property exists and
|
|
68
|
+
// holds its value. AI users cannot assign via the type (charter P2).
|
|
69
|
+
expect(err.code).toBe('forge-missing');
|
|
70
|
+
expect(typeof err.code).toBe('string');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('.expected is readonly (TS compile-time)', () => {
|
|
74
|
+
const err = makeErr();
|
|
75
|
+
expect(err.expected).toBe('test expected');
|
|
76
|
+
expect(typeof err.expected).toBe('string');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('.hint is readonly (TS compile-time)', () => {
|
|
80
|
+
const err = makeErr();
|
|
81
|
+
expect(err.hint).toBe('test hint');
|
|
82
|
+
expect(typeof err.hint).toBe('string');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('.detail is readonly (TS compile-time)', () => {
|
|
86
|
+
const err = makeErr();
|
|
87
|
+
expect(err.detail).toBeDefined();
|
|
88
|
+
expect(typeof err.detail).toBe('object');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ── code union membership ───────────────────────────────────────────────────
|
|
93
|
+
describe('GameProjectError — code union', () => {
|
|
94
|
+
const codes: GameProjectErrorCode[] = [
|
|
95
|
+
'forge-missing',
|
|
96
|
+
'forge-parse-failed',
|
|
97
|
+
'forge-schema-invalid',
|
|
98
|
+
'forge-unknown-field',
|
|
99
|
+
'forge-guid-malformed',
|
|
100
|
+
'forge-scene-unresolved',
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
it('code union has exactly 6 members', () => {
|
|
104
|
+
expect(codes).toHaveLength(6);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('each code is constructable', () => {
|
|
108
|
+
expect(errors.map((error) => error.code)).toEqual(codes);
|
|
109
|
+
for (const err of errors) {
|
|
110
|
+
expect(err).toBeInstanceOf(Error);
|
|
111
|
+
expect(err).toBeInstanceOf(GameProjectError);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('compiles exhaustive switch without default', () => {
|
|
116
|
+
function switchOnCode(code: GameProjectErrorCode): string {
|
|
117
|
+
switch (code) {
|
|
118
|
+
case 'forge-missing':
|
|
119
|
+
return 'missing';
|
|
120
|
+
case 'forge-parse-failed':
|
|
121
|
+
return 'parse-failed';
|
|
122
|
+
case 'forge-schema-invalid':
|
|
123
|
+
return 'schema-invalid';
|
|
124
|
+
case 'forge-unknown-field':
|
|
125
|
+
return 'unknown-field';
|
|
126
|
+
case 'forge-guid-malformed':
|
|
127
|
+
return 'guid-malformed';
|
|
128
|
+
case 'forge-scene-unresolved':
|
|
129
|
+
return 'scene-unresolved';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
expect(switchOnCode('forge-missing')).toBe('missing');
|
|
133
|
+
expect(switchOnCode('forge-scene-unresolved')).toBe('scene-unresolved');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
function describeError(error: GameProjectError): string {
|
|
138
|
+
switch (error.code) {
|
|
139
|
+
case 'forge-missing':
|
|
140
|
+
return `missing:${error.detail.path}`;
|
|
141
|
+
case 'forge-parse-failed':
|
|
142
|
+
return `parse:${error.detail.path}:${error.detail.rawMessage}`;
|
|
143
|
+
case 'forge-schema-invalid':
|
|
144
|
+
return `schema:${error.detail.path}:${error.detail.zodErrors.length}`;
|
|
145
|
+
case 'forge-unknown-field':
|
|
146
|
+
return `unknown:${error.detail.path}:${error.detail.fieldNames.join(',')}`;
|
|
147
|
+
case 'forge-guid-malformed':
|
|
148
|
+
return `guid:${error.detail.field}:${error.detail.rawInput}`;
|
|
149
|
+
case 'forge-scene-unresolved':
|
|
150
|
+
return `scene:${error.detail.guid}`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
describe('GameProjectError — code-correlated error narrowing', () => {
|
|
155
|
+
it('narrows representative detail payloads from error.code', () => {
|
|
156
|
+
expect(errors.map(describeError)).toEqual([
|
|
157
|
+
'missing:/fake/forge.json',
|
|
158
|
+
'parse:/fake/forge.json:invalid JSON',
|
|
159
|
+
'schema:/fake/forge.json:0',
|
|
160
|
+
'unknown:/fake/forge.json:scenes',
|
|
161
|
+
'guid:defaultScene:bad-guid',
|
|
162
|
+
'scene:15acc839-d847-527c-8284-bfb36d7c50de',
|
|
163
|
+
]);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ── per-code detail narrowing ───────────────────────────────────────────────
|
|
168
|
+
describe('GameProjectError — detail narrowing per code', () => {
|
|
169
|
+
it('forge-missing detail contains path:string', () => {
|
|
170
|
+
const _detail: Extract<GameProjectErrorDetail, { path: string }> = {
|
|
171
|
+
path: '/some/game/forge.json',
|
|
172
|
+
};
|
|
173
|
+
expect(_detail.path).toBe('/some/game/forge.json');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('forge-parse-failed detail contains path + rawMessage', () => {
|
|
177
|
+
const _detail: Extract<GameProjectErrorDetail, { path: string; rawMessage: string }> = {
|
|
178
|
+
path: '/some/forge.json',
|
|
179
|
+
rawMessage: 'Unexpected token',
|
|
180
|
+
};
|
|
181
|
+
expect(_detail.rawMessage).toBe('Unexpected token');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('forge-schema-invalid detail contains path + zodErrors', () => {
|
|
185
|
+
const _detail: Extract<GameProjectErrorDetail, { path: string; zodErrors: unknown }> = {
|
|
186
|
+
path: '/some/forge.json',
|
|
187
|
+
zodErrors: [],
|
|
188
|
+
};
|
|
189
|
+
expect(_detail.path).toBe('/some/forge.json');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('forge-unknown-field detail contains path + fieldNames', () => {
|
|
193
|
+
const _detail: Extract<GameProjectErrorDetail, { path: string; fieldNames: string[] }> = {
|
|
194
|
+
path: '/some/forge.json',
|
|
195
|
+
fieldNames: ['scenes'],
|
|
196
|
+
};
|
|
197
|
+
expect(_detail.fieldNames).toEqual(['scenes']);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('forge-guid-malformed detail contains field + rawInput', () => {
|
|
201
|
+
const _detail: Extract<GameProjectErrorDetail, { field: string; rawInput: string }> = {
|
|
202
|
+
field: 'defaultScene',
|
|
203
|
+
rawInput: 'rogue-encampment',
|
|
204
|
+
};
|
|
205
|
+
expect(_detail.rawInput).toBe('rogue-encampment');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('forge-scene-unresolved detail contains guid:string', () => {
|
|
209
|
+
const _detail: Extract<GameProjectErrorDetail, { guid: string }> = {
|
|
210
|
+
guid: '15acc839-d847-527c-8284-bfb36d7c50de',
|
|
211
|
+
};
|
|
212
|
+
expect(_detail.guid).toBe('15acc839-d847-527c-8284-bfb36d7c50de');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ── error message ───────────────────────────────────────────────────────────
|
|
217
|
+
describe('GameProjectError — message', () => {
|
|
218
|
+
it('is an instance of Error', () => {
|
|
219
|
+
const err = makeErr();
|
|
220
|
+
expect(err).toBeInstanceOf(Error);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('has name GameProjectError', () => {
|
|
224
|
+
const err = makeErr();
|
|
225
|
+
expect(err.name).toBe('GameProjectError');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('message contains code for human readability', () => {
|
|
229
|
+
const err = makeErr();
|
|
230
|
+
expect(err.message).toContain('GameProjectError');
|
|
231
|
+
expect(err.message).toContain('forge-missing');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "hellforge",
|
|
3
|
+
"name": "ForgeaX: Hellforge",
|
|
4
|
+
"schemaVersion": "1.0.0",
|
|
5
|
+
"entry": "main.ts",
|
|
6
|
+
"physics": "3d",
|
|
7
|
+
"pointerLock": true,
|
|
8
|
+
"defaultScene": "15acc839-d847-527c-8284-bfb36d7c50de",
|
|
9
|
+
"preview": {
|
|
10
|
+
"skin": {
|
|
11
|
+
"sceneGuid": "5e3028dd-ddf6-4104-86d9-318d3e8fb5a6",
|
|
12
|
+
"clipGuids": [
|
|
13
|
+
"c530adf2-8de6-486a-afaa-9af3a6e6dfd1",
|
|
14
|
+
"f9355148-5ddc-45a4-80d4-ef80fce559b0",
|
|
15
|
+
"9bb05e7d-6156-424f-8a20-f80373507f65",
|
|
16
|
+
"7faedc58-49cd-4fc9-93b7-66eca1b79674",
|
|
17
|
+
"ca6e7f12-8e1a-4b3c-9d50-2a4f1b8c6d04"
|
|
18
|
+
],
|
|
19
|
+
"clipDefault": "idle",
|
|
20
|
+
"scale": 1,
|
|
21
|
+
"pos": [0, 0, 0]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|