@forgeax/engine-pack 0.1.21 → 0.1.24
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/README.md +167 -48
- package/dist/__tests__/inventory-schema.test.d.ts +2 -0
- package/dist/__tests__/inventory-schema.test.d.ts.map +1 -0
- package/dist/__tests__/material-cook-receipt-layer-plan.unit.test.d.ts +2 -0
- package/dist/__tests__/material-cook-receipt-layer-plan.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pack-authoring-gateway.unit.test.d.ts +2 -0
- package/dist/__tests__/pack-authoring-gateway.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pack-authoring.unit.test.d.ts +2 -0
- package/dist/__tests__/pack-authoring.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pack-scanner.unit.test.d.ts +2 -0
- package/dist/__tests__/pack-scanner.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scanner-blacklist.test.d.ts +2 -0
- package/dist/__tests__/scanner-blacklist.test.d.ts.map +1 -0
- package/dist/build.d.ts +9 -4
- package/dist/build.d.ts.map +1 -1
- package/dist/build.mjs +3449 -897
- package/dist/build.mjs.map +1 -1
- package/dist/builtin.mjs +37 -4
- package/dist/builtin.mjs.map +1 -1
- package/dist/catalog-builder.d.ts.map +1 -1
- package/dist/cli-asset.d.ts.map +1 -1
- package/dist/cli-asset.mjs +1361 -845
- package/dist/cli-asset.mjs.map +1 -1
- package/dist/deriveAssetName.d.ts +11 -7
- package/dist/deriveAssetName.d.ts.map +1 -1
- package/dist/evidence/material-cook.d.ts +11 -0
- package/dist/evidence/material-cook.d.ts.map +1 -1
- package/dist/evidence/source-inventory.d.ts.map +1 -1
- package/dist/guid.d.ts +15 -0
- package/dist/guid.d.ts.map +1 -1
- package/dist/guid.mjs +53 -5
- package/dist/guid.mjs.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +1297 -562
- package/dist/index.mjs.map +1 -1
- package/dist/inventory/binding.d.ts +6 -0
- package/dist/inventory/binding.d.ts.map +1 -0
- package/dist/inventory/declaration.d.ts +24 -0
- package/dist/inventory/declaration.d.ts.map +1 -0
- package/dist/inventory/index.d.ts +3 -0
- package/dist/inventory/index.d.ts.map +1 -0
- package/dist/inventory/sync.d.ts +5 -0
- package/dist/inventory/sync.d.ts.map +1 -0
- package/dist/material-cook.d.ts +1 -1
- package/dist/material-cook.d.ts.map +1 -1
- package/dist/material-cook.mjs +50 -3
- package/dist/material-cook.mjs.map +1 -1
- package/dist/name.mjs +4 -7
- package/dist/name.mjs.map +1 -1
- package/dist/pack-authoring-node.d.ts +26 -0
- package/dist/pack-authoring-node.d.ts.map +1 -0
- package/dist/pack-authoring-node.mjs +11089 -0
- package/dist/pack-authoring-node.mjs.map +1 -0
- package/dist/pack-authoring.d.ts +230 -0
- package/dist/pack-authoring.d.ts.map +1 -0
- package/dist/pack-authoring.mjs +1137 -0
- package/dist/pack-authoring.mjs.map +1 -0
- package/dist/package-finalizer.d.ts +2 -20
- package/dist/package-finalizer.d.ts.map +1 -1
- package/dist/resolve-asset-source.d.ts +1 -3
- package/dist/resolve-asset-source.d.ts.map +1 -1
- package/dist/resolve-asset-source.mjs +6 -81
- package/dist/resolve-asset-source.mjs.map +1 -1
- package/dist/runtime.mjs +77 -7
- package/dist/runtime.mjs.map +1 -1
- package/dist/scanner.d.ts +18 -7
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.mjs +1164 -687
- package/dist/scanner.mjs.map +1 -1
- package/dist/schema-compiled.d.ts.map +1 -1
- package/dist/schema.mjs +79 -7
- package/dist/schema.mjs.map +1 -1
- package/dist/scriptable-pack-node.d.ts +7 -31
- package/dist/scriptable-pack-node.d.ts.map +1 -1
- package/dist/scriptable-pack-node.mjs +572 -1019
- package/dist/scriptable-pack-node.mjs.map +1 -1
- package/dist/scriptable-pack-worker.mjs +29 -11
- package/dist/scriptable-pack-worker.mjs.map +1 -1
- package/dist/scriptable-pack.d.ts +20 -532
- package/dist/scriptable-pack.d.ts.map +1 -1
- package/dist/scriptable-pack.mjs +1038 -527
- package/dist/scriptable-pack.mjs.map +1 -1
- package/package.json +2 -7
- package/schema/meta.schema.json +1 -1
- package/schema/pack.schema.json +76 -6
- package/src/__tests__/guid-collision.unit.test.ts +18 -9
- package/src/__tests__/inventory-schema.test.ts +109 -0
- package/src/__tests__/material-cook-receipt-layer-plan.unit.test.ts +16 -0
- package/src/__tests__/material-cook-schema.unit.test.ts +62 -0
- package/src/__tests__/pack-authoring-gateway.unit.test.ts +249 -0
- package/src/__tests__/pack-authoring.unit.test.ts +266 -0
- package/src/__tests__/pack-scanner.unit.test.ts +123 -0
- package/src/__tests__/pack.unit.test.ts +1 -238
- package/src/__tests__/package-finalizer.contract.test.ts +55 -0
- package/src/__tests__/resolve-asset-source.test.ts +15 -98
- package/src/__tests__/scanner-blacklist.test.ts +55 -0
- package/src/__tests__/scanner-inventory.contract.test.ts +11 -14
- package/src/__tests__/scanner-inventory.test.ts +23 -10
- package/src/__tests__/scriptable-pack-cli.integration.test.ts +4 -9
- package/src/__tests__/topology.unit.test.ts +23 -0
- package/src/build.ts +28 -18
- package/src/catalog-builder.ts +60 -33
- package/src/cli-asset.ts +68 -83
- package/src/deriveAssetName.ts +14 -13
- package/src/evidence/material-cook.ts +79 -3
- package/src/evidence/source-inventory.ts +34 -21
- package/src/guid.ts +65 -4
- package/src/index.ts +15 -11
- package/src/inventory/binding.ts +25 -0
- package/src/inventory/declaration.ts +168 -0
- package/src/inventory/index.ts +18 -0
- package/src/inventory/sync.ts +22 -0
- package/src/material-cook.ts +1 -0
- package/src/pack-authoring-node.ts +1966 -0
- package/src/pack-authoring.ts +1478 -0
- package/src/package-finalizer.ts +2 -42
- package/src/resolve-asset-source.ts +4 -76
- package/src/scanner.ts +236 -56
- package/src/schema/material-cook.schema.json +4 -1
- package/src/schema-compiled.ts +2 -0
- package/src/scriptable-pack-node.ts +108 -719
- package/src/scriptable-pack-worker.ts +48 -16
- package/src/scriptable-pack.ts +28 -939
- package/dist/.tsbuildinfo +0 -1
- package/dist/__tests__/load-asset-config.test.d.ts +0 -2
- package/dist/__tests__/load-asset-config.test.d.ts.map +0 -1
- package/dist/__tests__/scriptable-pack.test-d.d.ts +0 -2
- package/dist/__tests__/scriptable-pack.test-d.d.ts.map +0 -1
- package/dist/__tests__/scriptable-pack.unit.test.d.ts +0 -2
- package/dist/__tests__/scriptable-pack.unit.test.d.ts.map +0 -1
- package/dist/config.d.ts +0 -6
- package/dist/config.d.ts.map +0 -1
- package/dist/config.mjs +0 -29
- package/dist/config.mjs.map +0 -1
- package/src/__tests__/load-asset-config.test.ts +0 -121
- package/src/__tests__/scriptable-pack.test-d.ts +0 -131
- package/src/__tests__/scriptable-pack.unit.test.ts +0 -789
- package/src/config.ts +0 -36
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { type AuthorInventory, validateAuthorInventory } from '../inventory/declaration.js';
|
|
3
|
+
|
|
4
|
+
const guid = '00000000-0000-0000-0000-000000000010';
|
|
5
|
+
|
|
6
|
+
describe('author inventory declaration contract', () => {
|
|
7
|
+
it('requires one semantic sourceKey for every author row', () => {
|
|
8
|
+
const result = validateAuthorInventory({
|
|
9
|
+
declarations: [{ guid, kind: 'mesh', payload: {}, refs: [] }],
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(result.ok).toBe(false);
|
|
13
|
+
if (!result.ok) {
|
|
14
|
+
expect(result.error.code).toBe('inventory-source-key-missing');
|
|
15
|
+
expect(result.error.detail.sourceKey).toBeUndefined();
|
|
16
|
+
expect(result.error.expected).toContain('sourceKey');
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('rejects duplicate GUID and sourceKey before projection', () => {
|
|
21
|
+
const rows: AuthorInventory = {
|
|
22
|
+
declarations: [
|
|
23
|
+
{ guid, sourceKey: 'hero/body', kind: 'mesh', payload: {}, refs: [] },
|
|
24
|
+
{ guid, sourceKey: 'hero/body', kind: 'material', payload: {}, refs: [] },
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
const result = validateAuthorInventory(rows);
|
|
28
|
+
|
|
29
|
+
expect(result.ok).toBe(false);
|
|
30
|
+
if (!result.ok) {
|
|
31
|
+
expect(['inventory-guid-duplicate', 'inventory-source-key-duplicate']).toContain(
|
|
32
|
+
result.error.code,
|
|
33
|
+
);
|
|
34
|
+
expect(result.error.detail).toHaveProperty('guid');
|
|
35
|
+
expect(result.error.hint).toContain('rename');
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('rejects GUID duplicates regardless of hex casing and padded source keys', () => {
|
|
40
|
+
const result = validateAuthorInventory({
|
|
41
|
+
declarations: [
|
|
42
|
+
{ guid, sourceKey: 'hero/body', kind: 'mesh', payload: {}, refs: [] },
|
|
43
|
+
{
|
|
44
|
+
guid: guid.toUpperCase(),
|
|
45
|
+
sourceKey: 'hero/other',
|
|
46
|
+
kind: 'mesh',
|
|
47
|
+
payload: {},
|
|
48
|
+
refs: [],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
expect(result).toMatchObject({ ok: false, error: { code: 'inventory-guid-duplicate' } });
|
|
53
|
+
|
|
54
|
+
const padded = validateAuthorInventory({
|
|
55
|
+
declarations: [{ guid, sourceKey: ' hero/body', kind: 'mesh', payload: {}, refs: [] }],
|
|
56
|
+
});
|
|
57
|
+
expect(padded).toMatchObject({ ok: false, error: { code: 'inventory-source-key-missing' } });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('projects declared scene binding keys without using display names', () => {
|
|
61
|
+
const result = validateAuthorInventory({
|
|
62
|
+
declarations: [
|
|
63
|
+
{
|
|
64
|
+
guid,
|
|
65
|
+
sourceKey: 'world/main',
|
|
66
|
+
kind: 'scene',
|
|
67
|
+
payload: {
|
|
68
|
+
kind: 'scene',
|
|
69
|
+
entities: [
|
|
70
|
+
{ localId: 0, bindingKey: 'player', components: { Name: { value: 'Renamed' } } },
|
|
71
|
+
{ localId: 1, bindingKey: 'camera', components: {} },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
refs: [],
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(result).toMatchObject({
|
|
80
|
+
ok: true,
|
|
81
|
+
value: { declarations: [{ sceneBindings: ['player', 'camera'] }] },
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('allows decorative scene entities without a gameplay binding', () => {
|
|
86
|
+
const result = validateAuthorInventory({
|
|
87
|
+
declarations: [
|
|
88
|
+
{
|
|
89
|
+
guid,
|
|
90
|
+
sourceKey: 'world/decorative',
|
|
91
|
+
kind: 'scene',
|
|
92
|
+
payload: {
|
|
93
|
+
kind: 'scene',
|
|
94
|
+
entities: [
|
|
95
|
+
{ localId: 0, components: { Transform: { position: [0, 0, 0] } } },
|
|
96
|
+
{ localId: 1, bindingKey: 'player', components: {} },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
refs: [],
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(result).toMatchObject({
|
|
105
|
+
ok: true,
|
|
106
|
+
value: { declarations: [{ sceneBindings: ['player'] }] },
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { deriveStandardLayerPlan } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('material cook receipt layer identity', () => {
|
|
5
|
+
it('derives one stable identity from the published parameter contract', () => {
|
|
6
|
+
const parameters = [
|
|
7
|
+
{ name: 'baseColor', type: 'color' as const },
|
|
8
|
+
{ name: 'metallic', type: 'f32' as const },
|
|
9
|
+
{ name: 'roughness', type: 'f32' as const },
|
|
10
|
+
];
|
|
11
|
+
const first = deriveStandardLayerPlan(parameters);
|
|
12
|
+
const second = deriveStandardLayerPlan(parameters);
|
|
13
|
+
expect(first.identity).toBe(second.identity);
|
|
14
|
+
expect(first.passFamily).toEqual(['forward', 'deferred', 'shadow']);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { deriveStandardLayerPlan } from '@forgeax/engine-types';
|
|
3
4
|
import { describe, expect, it } from 'vitest';
|
|
4
5
|
import {
|
|
5
6
|
type CookedMaterialRecord,
|
|
@@ -149,4 +150,65 @@ describe('cooked material schema', () => {
|
|
|
149
150
|
error: { code: 'material-cook-record-invalid' },
|
|
150
151
|
});
|
|
151
152
|
});
|
|
153
|
+
|
|
154
|
+
it('rejects a Standard record whose receipt layer plan is stale', () => {
|
|
155
|
+
const standardPass = { name: 'forward', program: { module: 'forgeax::default-standard-pbr' } };
|
|
156
|
+
const standardPlan = deriveStandardLayerPlan(record.resolved.parameters, [standardPass]);
|
|
157
|
+
const standardRecord = {
|
|
158
|
+
...record,
|
|
159
|
+
resolved: { ...record.resolved, passes: [standardPass] },
|
|
160
|
+
receipt: {
|
|
161
|
+
...record.receipt,
|
|
162
|
+
derivedInterface: {
|
|
163
|
+
layoutIdentity: 'sha256:layout',
|
|
164
|
+
layerPlanIdentity: standardPlan.identity,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
expect(validateCookedMaterialRecord(standardRecord)).toMatchObject({ ok: true });
|
|
169
|
+
expect(
|
|
170
|
+
validateCookedMaterialRecord({
|
|
171
|
+
...standardRecord,
|
|
172
|
+
receipt: {
|
|
173
|
+
...standardRecord.receipt,
|
|
174
|
+
derivedInterface: { layoutIdentity: 'sha256:layout', layerPlanIdentity: 'stale' },
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
177
|
+
).toMatchObject({
|
|
178
|
+
ok: false,
|
|
179
|
+
error: {
|
|
180
|
+
detail: { field: 'receipt.derivedInterface.layerPlanIdentity' },
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('rejects a Standard record whose receipt omits the layer plan identity', () => {
|
|
186
|
+
const standardPass = { name: 'forward', program: { module: 'forgeax::default-standard-pbr' } };
|
|
187
|
+
const standardPlan = deriveStandardLayerPlan(record.resolved.parameters, [standardPass]);
|
|
188
|
+
const standardRecord = {
|
|
189
|
+
...record,
|
|
190
|
+
resolved: { ...record.resolved, passes: [standardPass] },
|
|
191
|
+
receipt: {
|
|
192
|
+
...record.receipt,
|
|
193
|
+
derivedInterface: {
|
|
194
|
+
layoutIdentity: 'sha256:layout',
|
|
195
|
+
layerPlanIdentity: standardPlan.identity,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
expect(
|
|
200
|
+
validateCookedMaterialRecord({
|
|
201
|
+
...standardRecord,
|
|
202
|
+
receipt: {
|
|
203
|
+
...standardRecord.receipt,
|
|
204
|
+
derivedInterface: { layoutIdentity: 'sha256:layout' },
|
|
205
|
+
},
|
|
206
|
+
}),
|
|
207
|
+
).toMatchObject({
|
|
208
|
+
ok: false,
|
|
209
|
+
error: {
|
|
210
|
+
detail: { field: 'receipt.derivedInterface.layerPlanIdentity' },
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
});
|
|
152
214
|
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { AssetGuid, PackageId } from '../guid.js';
|
|
6
|
+
import { createFileSystemPackAuthoringGateway } from '../pack-authoring-node.js';
|
|
7
|
+
|
|
8
|
+
const DIRECT_PACKAGE = '01900000-0000-7000-8000-000000000070';
|
|
9
|
+
const CLONE_PACKAGE = '01900000-0000-7000-8000-000000000071';
|
|
10
|
+
const SOURCE_PACKAGE = '01900000-0000-7000-8000-000000000072';
|
|
11
|
+
const INSTANCE_PACKAGE = '01900000-0000-7000-8000-000000000073';
|
|
12
|
+
|
|
13
|
+
const roots: string[] = [];
|
|
14
|
+
|
|
15
|
+
function packageId(value: string) {
|
|
16
|
+
const parsed = PackageId.parse(value);
|
|
17
|
+
if (!parsed.ok) throw parsed.error;
|
|
18
|
+
return parsed.value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
const root = roots.pop();
|
|
23
|
+
if (root !== undefined) await rm(root, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('Pack authoring gateway', () => {
|
|
27
|
+
it('writes guid-less direct JSON, derives identity, and makes mutations idempotent', async () => {
|
|
28
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-pack-gateway-direct-'));
|
|
29
|
+
roots.push(root);
|
|
30
|
+
const gateway = createFileSystemPackAuthoringGateway({ gameRoot: root });
|
|
31
|
+
const operation = {
|
|
32
|
+
operation: 'asset-source.create' as const,
|
|
33
|
+
requestId: 'create-direct',
|
|
34
|
+
targetPath: 'assets/direct.pack.json',
|
|
35
|
+
format: 'pack.json' as const,
|
|
36
|
+
packageId: DIRECT_PACKAGE,
|
|
37
|
+
initialAssets: {
|
|
38
|
+
'mesh/main': { kind: 'mesh', payload: { vertices: [] }, refs: [] },
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const created = await gateway.execute(operation);
|
|
43
|
+
expect(created).toMatchObject({
|
|
44
|
+
ok: true,
|
|
45
|
+
value: { packageId: DIRECT_PACKAGE, format: 'pack.json', targetPath: operation.targetPath },
|
|
46
|
+
});
|
|
47
|
+
const raw = JSON.parse(await readFile(join(root, operation.targetPath), 'utf8')) as Record<
|
|
48
|
+
string,
|
|
49
|
+
unknown
|
|
50
|
+
>;
|
|
51
|
+
expect(raw).toEqual({
|
|
52
|
+
schemaVersion: '3.0.0',
|
|
53
|
+
packageId: DIRECT_PACKAGE,
|
|
54
|
+
assets: { 'mesh/main': { kind: 'mesh', payload: { vertices: [] }, refs: [] } },
|
|
55
|
+
});
|
|
56
|
+
expect(JSON.stringify(raw)).not.toContain('guid');
|
|
57
|
+
|
|
58
|
+
const replay = await gateway.execute(operation);
|
|
59
|
+
expect(replay).toEqual(created);
|
|
60
|
+
const conflictingReplay = await gateway.execute({ ...operation, initialAssets: {} });
|
|
61
|
+
expect(conflictingReplay).toMatchObject({
|
|
62
|
+
ok: false,
|
|
63
|
+
error: { code: 'pack-source-revision-conflict' },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const unresolved = await gateway.execute({
|
|
67
|
+
operation: 'asset.resolve',
|
|
68
|
+
requestId: 'resolve-direct',
|
|
69
|
+
packageId: DIRECT_PACKAGE,
|
|
70
|
+
sourceKey: 'mesh/main',
|
|
71
|
+
require: 'ready',
|
|
72
|
+
});
|
|
73
|
+
expect(unresolved).toMatchObject({ ok: false, error: { code: 'asset-not-ready' } });
|
|
74
|
+
|
|
75
|
+
const publishedGateway = createFileSystemPackAuthoringGateway({
|
|
76
|
+
gameRoot: root,
|
|
77
|
+
materialized: () => [
|
|
78
|
+
{
|
|
79
|
+
packageId: DIRECT_PACKAGE,
|
|
80
|
+
sourceKey: 'mesh/main',
|
|
81
|
+
guid: AssetGuid.format(AssetGuid.derive(packageId(DIRECT_PACKAGE), 'mesh/main')),
|
|
82
|
+
kind: 'mesh',
|
|
83
|
+
sourcePath: 'dist/pack.json',
|
|
84
|
+
ready: true,
|
|
85
|
+
artifactsReady: true,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
const resolved = await publishedGateway.execute({
|
|
90
|
+
operation: 'asset.resolve',
|
|
91
|
+
requestId: 'resolve-direct-published',
|
|
92
|
+
packageId: DIRECT_PACKAGE,
|
|
93
|
+
sourceKey: 'mesh/main',
|
|
94
|
+
require: 'ready',
|
|
95
|
+
});
|
|
96
|
+
expect(resolved).toMatchObject({
|
|
97
|
+
ok: true,
|
|
98
|
+
value: {
|
|
99
|
+
packageId: DIRECT_PACKAGE,
|
|
100
|
+
sourceKey: 'mesh/main',
|
|
101
|
+
status: 'ready',
|
|
102
|
+
guid: AssetGuid.format(AssetGuid.derive(packageId(DIRECT_PACKAGE), 'mesh/main')),
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const clone = await gateway.execute({
|
|
107
|
+
operation: 'asset-source.clone',
|
|
108
|
+
requestId: 'clone-direct',
|
|
109
|
+
sourcePath: operation.targetPath,
|
|
110
|
+
targetPath: 'assets/clone.pack.json',
|
|
111
|
+
packageId: CLONE_PACKAGE,
|
|
112
|
+
});
|
|
113
|
+
expect(clone).toMatchObject({ ok: true, value: { packageId: CLONE_PACKAGE } });
|
|
114
|
+
const cloneResolved = await gateway.execute({
|
|
115
|
+
operation: 'asset.resolve',
|
|
116
|
+
requestId: 'resolve-clone',
|
|
117
|
+
packageId: CLONE_PACKAGE,
|
|
118
|
+
sourceKey: 'mesh/main',
|
|
119
|
+
require: 'identity',
|
|
120
|
+
});
|
|
121
|
+
expect(cloneResolved).toMatchObject({ ok: true, value: { status: 'identity' } });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('creates ScriptablePack source and Pack instance documents with CAS-protected value edits', async () => {
|
|
125
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-pack-gateway-instance-'));
|
|
126
|
+
roots.push(root);
|
|
127
|
+
const gateway = createFileSystemPackAuthoringGateway({ gameRoot: root });
|
|
128
|
+
const source = await gateway.execute({
|
|
129
|
+
operation: 'asset-source.create',
|
|
130
|
+
requestId: 'create-source',
|
|
131
|
+
targetPath: 'assets/source.pack.ts',
|
|
132
|
+
format: 'pack.ts',
|
|
133
|
+
packageId: SOURCE_PACKAGE,
|
|
134
|
+
parameters: [{ name: 'count', type: 'u32', default: 1, minimum: 1 }],
|
|
135
|
+
});
|
|
136
|
+
expect(source).toMatchObject({
|
|
137
|
+
ok: true,
|
|
138
|
+
value: { packageId: SOURCE_PACKAGE, format: 'pack.ts' },
|
|
139
|
+
});
|
|
140
|
+
const sourceText = await readFile(join(root, 'assets/source.pack.ts'), 'utf8');
|
|
141
|
+
expect(sourceText).toContain(`definePackageId("${SOURCE_PACKAGE}")`);
|
|
142
|
+
expect(sourceText).not.toContain('assets:');
|
|
143
|
+
|
|
144
|
+
const listed = await gateway.execute({ operation: 'asset.list', requestId: 'list-source' });
|
|
145
|
+
expect(listed).toMatchObject({
|
|
146
|
+
ok: true,
|
|
147
|
+
value: { sources: [{ packageId: SOURCE_PACKAGE, sourcePath: 'assets/source.pack.ts' }] },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const instance = await gateway.execute({
|
|
151
|
+
operation: 'asset-source.create-instance',
|
|
152
|
+
requestId: 'create-instance',
|
|
153
|
+
targetPath: 'assets/instance.pack.json',
|
|
154
|
+
packageId: INSTANCE_PACKAGE,
|
|
155
|
+
parentPackageId: SOURCE_PACKAGE,
|
|
156
|
+
values: { count: 4 },
|
|
157
|
+
});
|
|
158
|
+
expect(instance).toMatchObject({
|
|
159
|
+
ok: true,
|
|
160
|
+
value: {
|
|
161
|
+
packageId: INSTANCE_PACKAGE,
|
|
162
|
+
parentPackageId: SOURCE_PACKAGE,
|
|
163
|
+
effectiveValues: { count: 4 },
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
if (!instance.ok) return;
|
|
167
|
+
const instanceRevision = instance.value.revision;
|
|
168
|
+
if (instanceRevision === undefined)
|
|
169
|
+
throw new Error('create-instance did not return a revision');
|
|
170
|
+
const applied = await gateway.execute({
|
|
171
|
+
operation: 'asset-source.apply-values',
|
|
172
|
+
requestId: 'apply-values',
|
|
173
|
+
sourcePath: 'assets/instance.pack.json',
|
|
174
|
+
expectedRevision: instanceRevision,
|
|
175
|
+
values: { count: 5 },
|
|
176
|
+
});
|
|
177
|
+
expect(applied).toMatchObject({ ok: true, value: { effectiveValues: { count: 5 } } });
|
|
178
|
+
const stale = await gateway.execute({
|
|
179
|
+
operation: 'asset-source.apply-values',
|
|
180
|
+
requestId: 'apply-stale',
|
|
181
|
+
sourcePath: 'assets/instance.pack.json',
|
|
182
|
+
expectedRevision: instanceRevision,
|
|
183
|
+
values: { count: 6 },
|
|
184
|
+
});
|
|
185
|
+
expect(stale).toMatchObject({ ok: false, error: { code: 'pack-source-revision-conflict' } });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('fails closed when the current materialized projection violates derived identity', async () => {
|
|
189
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-pack-gateway-projection-'));
|
|
190
|
+
roots.push(root);
|
|
191
|
+
const gateway = createFileSystemPackAuthoringGateway({
|
|
192
|
+
gameRoot: root,
|
|
193
|
+
materialized: () => [
|
|
194
|
+
{
|
|
195
|
+
packageId: DIRECT_PACKAGE,
|
|
196
|
+
sourceKey: 'mesh/main',
|
|
197
|
+
guid: AssetGuid.format(AssetGuid.derive(packageId(DIRECT_PACKAGE), 'mesh/other')),
|
|
198
|
+
kind: 'mesh',
|
|
199
|
+
ready: true,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const result = await gateway.execute({ operation: 'asset.list', requestId: 'bad-projection' });
|
|
205
|
+
expect(result).toMatchObject({
|
|
206
|
+
ok: false,
|
|
207
|
+
error: {
|
|
208
|
+
code: 'pack-guid-collision',
|
|
209
|
+
detail: {
|
|
210
|
+
expectedGuid: AssetGuid.format(AssetGuid.derive(packageId(DIRECT_PACKAGE), 'mesh/main')),
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('does not fall back from a missing sourcePath to another package locator', async () => {
|
|
217
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-pack-gateway-locator-'));
|
|
218
|
+
roots.push(root);
|
|
219
|
+
const gateway = createFileSystemPackAuthoringGateway({ gameRoot: root });
|
|
220
|
+
await gateway.execute({
|
|
221
|
+
operation: 'asset-source.create',
|
|
222
|
+
requestId: 'create-locator-source',
|
|
223
|
+
targetPath: 'assets/direct.pack.json',
|
|
224
|
+
format: 'pack.json',
|
|
225
|
+
packageId: DIRECT_PACKAGE,
|
|
226
|
+
initialAssets: {},
|
|
227
|
+
});
|
|
228
|
+
const result = await gateway.execute({
|
|
229
|
+
operation: 'asset.inspect',
|
|
230
|
+
requestId: 'inspect-missing-locator',
|
|
231
|
+
sourcePath: 'assets/missing.pack.json',
|
|
232
|
+
packageId: DIRECT_PACKAGE,
|
|
233
|
+
});
|
|
234
|
+
expect(result).toMatchObject({ ok: false, error: { code: 'pack-source-not-found' } });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('converts a broken materialized projection callback into a structured error', async () => {
|
|
238
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-pack-gateway-projection-callback-'));
|
|
239
|
+
roots.push(root);
|
|
240
|
+
const gateway = createFileSystemPackAuthoringGateway({
|
|
241
|
+
gameRoot: root,
|
|
242
|
+
materialized: () => {
|
|
243
|
+
throw new Error('projection unavailable');
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
const result = await gateway.execute({ operation: 'asset.list', requestId: 'bad-callback' });
|
|
247
|
+
expect(result).toMatchObject({ ok: false, error: { code: 'pack-parameter-invalid' } });
|
|
248
|
+
});
|
|
249
|
+
});
|