@forgeax/engine-pack 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.
- package/LICENSE +202 -0
- package/README.md +370 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/ai-recovery.unit.test.d.ts +2 -0
- package/dist/__tests__/ai-recovery.unit.test.d.ts.map +1 -0
- package/dist/__tests__/artifact-path.test.d.ts +2 -0
- package/dist/__tests__/artifact-path.test.d.ts.map +1 -0
- package/dist/__tests__/asset-evidence-error-hints.test.d.ts +2 -0
- package/dist/__tests__/asset-evidence-error-hints.test.d.ts.map +1 -0
- package/dist/__tests__/asset-evidence-lifecycle.test.d.ts +2 -0
- package/dist/__tests__/asset-evidence-lifecycle.test.d.ts.map +1 -0
- package/dist/__tests__/asset-evidence-schema.test.d.ts +2 -0
- package/dist/__tests__/asset-evidence-schema.test.d.ts.map +1 -0
- package/dist/__tests__/atlas-cli-region-mismatch.test.d.ts +2 -0
- package/dist/__tests__/atlas-cli-region-mismatch.test.d.ts.map +1 -0
- package/dist/__tests__/atlas-error-code-owner.test-d.d.ts +2 -0
- package/dist/__tests__/atlas-error-code-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/cli-asset-evidence.integration.test.d.ts +2 -0
- package/dist/__tests__/cli-asset-evidence.integration.test.d.ts.map +1 -0
- package/dist/__tests__/cli.unit.test.d.ts +2 -0
- package/dist/__tests__/cli.unit.test.d.ts.map +1 -0
- package/dist/__tests__/evidence-source-inventory.unit.test.d.ts +2 -0
- package/dist/__tests__/evidence-source-inventory.unit.test.d.ts.map +1 -0
- package/dist/__tests__/guid-collision.unit.test.d.ts +2 -0
- package/dist/__tests__/guid-collision.unit.test.d.ts.map +1 -0
- package/dist/__tests__/guid-validator-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/guid-validator-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/load-asset-config.test.d.ts +2 -0
- package/dist/__tests__/load-asset-config.test.d.ts.map +1 -0
- package/dist/__tests__/material-cook-dependencies.unit.test.d.ts +2 -0
- package/dist/__tests__/material-cook-dependencies.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-cook-receipt.unit.test.d.ts +2 -0
- package/dist/__tests__/material-cook-receipt.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-cook-schema.unit.test.d.ts +2 -0
- package/dist/__tests__/material-cook-schema.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin-consumer-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin-consumer-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meta-schema-optional.test.d.ts +2 -0
- package/dist/__tests__/meta-schema-optional.test.d.ts.map +1 -0
- package/dist/__tests__/meta-source-overrides.unit.test.d.ts +2 -0
- package/dist/__tests__/meta-source-overrides.unit.test.d.ts.map +1 -0
- package/dist/__tests__/native-cooker-registry.test.d.ts +2 -0
- package/dist/__tests__/native-cooker-registry.test.d.ts.map +1 -0
- package/dist/__tests__/offline-evidence-artifact-verification-owner.test-d.d.ts +2 -0
- package/dist/__tests__/offline-evidence-artifact-verification-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/offline-evidence.integration.test.d.ts +2 -0
- package/dist/__tests__/offline-evidence.integration.test.d.ts.map +1 -0
- package/dist/__tests__/pack-v2-fixture-migration.test.d.ts +2 -0
- package/dist/__tests__/pack-v2-fixture-migration.test.d.ts.map +1 -0
- package/dist/__tests__/pack-v2-schema.test.d.ts +2 -0
- package/dist/__tests__/pack-v2-schema.test.d.ts.map +1 -0
- package/dist/__tests__/pack.unit.test.d.ts +2 -0
- package/dist/__tests__/pack.unit.test.d.ts.map +1 -0
- package/dist/__tests__/producer-contract-schema.unit.test.d.ts +2 -0
- package/dist/__tests__/producer-contract-schema.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-asset-source.test.d.ts +2 -0
- package/dist/__tests__/resolve-asset-source.test.d.ts.map +1 -0
- package/dist/__tests__/runtime-projection.unit.test.d.ts +2 -0
- package/dist/__tests__/runtime-projection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scanner-inventory.test.d.ts +2 -0
- package/dist/__tests__/scanner-inventory.test.d.ts.map +1 -0
- package/dist/__tests__/scanner-mount-cycle.test.d.ts +2 -0
- package/dist/__tests__/scanner-mount-cycle.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack.test-d.d.ts +2 -0
- package/dist/__tests__/scriptable-pack.test-d.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack.unit.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack.unit.test.d.ts.map +1 -0
- package/dist/__tests__/topology-actions.unit.test.d.ts +2 -0
- package/dist/__tests__/topology-actions.unit.test.d.ts.map +1 -0
- package/dist/__tests__/topology.unit.test.d.ts +2 -0
- package/dist/__tests__/topology.unit.test.d.ts.map +1 -0
- package/dist/artifact-path.d.ts +9 -0
- package/dist/artifact-path.d.ts.map +1 -0
- package/dist/atlas/run-atlas.d.ts +8 -0
- package/dist/atlas/run-atlas.d.ts.map +1 -0
- package/dist/atlas/shelf-pack.d.ts +55 -0
- package/dist/atlas/shelf-pack.d.ts.map +1 -0
- package/dist/bridge.d.ts +2 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.mjs +3 -0
- package/dist/bridge.mjs.map +1 -0
- package/dist/builtin.d.ts +24 -0
- package/dist/builtin.d.ts.map +1 -0
- package/dist/cli-asset.d.ts +22 -0
- package/dist/cli-asset.d.ts.map +1 -0
- package/dist/cli-asset.mjs +9950 -0
- package/dist/cli-asset.mjs.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.mjs +29 -0
- package/dist/config.mjs.map +1 -0
- package/dist/deriveAssetName.d.ts +15 -0
- package/dist/deriveAssetName.d.ts.map +1 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.mjs +19 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/evidence/material-cook.d.ts +70 -0
- package/dist/evidence/material-cook.d.ts.map +1 -0
- package/dist/evidence/offline-evidence.d.ts +25 -0
- package/dist/evidence/offline-evidence.d.ts.map +1 -0
- package/dist/evidence/source-inventory.d.ts +29 -0
- package/dist/evidence/source-inventory.d.ts.map +1 -0
- package/dist/guid.d.ts +45 -0
- package/dist/guid.d.ts.map +1 -0
- package/dist/guid.mjs +94 -0
- package/dist/guid.mjs.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +8475 -0
- package/dist/index.mjs.map +1 -0
- package/dist/material/artifact-writer.d.ts +14 -0
- package/dist/material/artifact-writer.d.ts.map +1 -0
- package/dist/mesh-bin-contract.d.ts +36 -0
- package/dist/mesh-bin-contract.d.ts.map +1 -0
- package/dist/name.mjs +22 -0
- package/dist/name.mjs.map +1 -0
- package/dist/native-cooker-registry.d.ts +28 -0
- package/dist/native-cooker-registry.d.ts.map +1 -0
- package/dist/native-cooker.d.ts +3 -0
- package/dist/native-cooker.d.ts.map +1 -0
- package/dist/native-cooker.mjs +105 -0
- package/dist/native-cooker.mjs.map +1 -0
- package/dist/producer-contract.d.ts +19 -0
- package/dist/producer-contract.d.ts.map +1 -0
- package/dist/resolve-asset-source.d.ts +4 -0
- package/dist/resolve-asset-source.d.ts.map +1 -0
- package/dist/resolve-asset-source.mjs +99 -0
- package/dist/resolve-asset-source.mjs.map +1 -0
- package/dist/runtime-projection.d.ts +14 -0
- package/dist/runtime-projection.d.ts.map +1 -0
- package/dist/scanner.d.ts +80 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.mjs +9015 -0
- package/dist/scanner.mjs.map +1 -0
- package/dist/schema-compiled.d.ts +12 -0
- package/dist/schema-compiled.d.ts.map +1 -0
- package/dist/schema.mjs +7369 -0
- package/dist/schema.mjs.map +1 -0
- package/dist/scriptable-pack-node.d.ts +67 -0
- package/dist/scriptable-pack-node.d.ts.map +1 -0
- package/dist/scriptable-pack-node.mjs +1590 -0
- package/dist/scriptable-pack-node.mjs.map +1 -0
- package/dist/scriptable-pack-worker.d.ts +2 -0
- package/dist/scriptable-pack-worker.d.ts.map +1 -0
- package/dist/scriptable-pack-worker.mjs +223 -0
- package/dist/scriptable-pack-worker.mjs.map +1 -0
- package/dist/scriptable-pack.d.ts +525 -0
- package/dist/scriptable-pack.d.ts.map +1 -0
- package/dist/scriptable-pack.mjs +583 -0
- package/dist/scriptable-pack.mjs.map +1 -0
- package/dist/topology.d.ts +10 -0
- package/dist/topology.d.ts.map +1 -0
- package/package.json +129 -0
- package/schema/meta.schema.json +173 -0
- package/schema/pack.schema.json +232 -0
- package/src/__tests__/ai-recovery.unit.test.ts +114 -0
- package/src/__tests__/artifact-path.test.ts +41 -0
- package/src/__tests__/asset-evidence-error-hints.test.ts +46 -0
- package/src/__tests__/asset-evidence-lifecycle.test.ts +104 -0
- package/src/__tests__/asset-evidence-schema.test.ts +86 -0
- package/src/__tests__/atlas-cli-region-mismatch.test.ts +134 -0
- package/src/__tests__/atlas-error-code-owner.test-d.ts +23 -0
- package/src/__tests__/cli-asset-evidence.integration.test.ts +172 -0
- package/src/__tests__/cli.unit.test.ts +448 -0
- package/src/__tests__/evidence-source-inventory.unit.test.ts +37 -0
- package/src/__tests__/guid-collision.unit.test.ts +112 -0
- package/src/__tests__/guid-validator-surface.unit.test.ts +20 -0
- package/src/__tests__/load-asset-config.test.ts +121 -0
- package/src/__tests__/material-cook-dependencies.unit.test.ts +45 -0
- package/src/__tests__/material-cook-receipt.unit.test.ts +91 -0
- package/src/__tests__/material-cook-schema.unit.test.ts +98 -0
- package/src/__tests__/mesh-bin-consumer-surface.unit.test.ts +21 -0
- package/src/__tests__/mesh-bin-contract.unit.test.ts +32 -0
- package/src/__tests__/meta-schema-optional.test.ts +116 -0
- package/src/__tests__/meta-source-overrides.unit.test.ts +71 -0
- package/src/__tests__/native-cooker-registry.test.ts +64 -0
- package/src/__tests__/offline-evidence-artifact-verification-owner.test-d.ts +64 -0
- package/src/__tests__/offline-evidence.integration.test.ts +106 -0
- package/src/__tests__/pack-v2-fixture-migration.test.ts +64 -0
- package/src/__tests__/pack-v2-schema.test.ts +142 -0
- package/src/__tests__/pack.unit.test.ts +2218 -0
- package/src/__tests__/producer-contract-schema.unit.test.ts +92 -0
- package/src/__tests__/resolve-asset-source.test.ts +121 -0
- package/src/__tests__/runtime-projection.unit.test.ts +87 -0
- package/src/__tests__/scanner-inventory.test.ts +72 -0
- package/src/__tests__/scanner-mount-cycle.test.ts +232 -0
- package/src/__tests__/scriptable-pack-cli.integration.test.ts +47 -0
- package/src/__tests__/scriptable-pack-ddc-key.unit.test.ts +63 -0
- package/src/__tests__/scriptable-pack.test-d.ts +131 -0
- package/src/__tests__/scriptable-pack.unit.test.ts +747 -0
- package/src/__tests__/topology-actions.unit.test.ts +94 -0
- package/src/__tests__/topology.unit.test.ts +85 -0
- package/src/artifact-path.ts +69 -0
- package/src/atlas/run-atlas.ts +358 -0
- package/src/atlas/shelf-pack.ts +176 -0
- package/src/atlas/upng-ambient.d.ts +13 -0
- package/src/bridge.ts +2 -0
- package/src/builtin.ts +79 -0
- package/src/cli-asset.ts +590 -0
- package/src/config.ts +36 -0
- package/src/deriveAssetName.ts +42 -0
- package/src/errors.ts +47 -0
- package/src/evidence/material-cook.ts +332 -0
- package/src/evidence/offline-evidence.ts +90 -0
- package/src/evidence/source-inventory.ts +193 -0
- package/src/guid.ts +99 -0
- package/src/index.ts +104 -0
- package/src/material/artifact-writer.ts +26 -0
- package/src/mesh-bin-contract.ts +150 -0
- package/src/native-cooker-registry.ts +163 -0
- package/src/native-cooker.ts +2 -0
- package/src/producer-contract.ts +277 -0
- package/src/resolve-asset-source.ts +93 -0
- package/src/runtime-projection.ts +100 -0
- package/src/scanner.ts +716 -0
- package/src/schema/material-cook.schema.json +79 -0
- package/src/schema-compiled.ts +238 -0
- package/src/scriptable-pack-node.ts +1390 -0
- package/src/scriptable-pack-worker.ts +306 -0
- package/src/scriptable-pack.ts +884 -0
- package/src/topology.ts +156 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { validateProducerContract, validateProducerOutputs } from '../producer-contract.js';
|
|
3
|
+
import { validateMeta, validatePack } from '../schema-compiled.js';
|
|
4
|
+
|
|
5
|
+
const GUID = '01890000-0000-7000-8000-aaaaaaaaaaaa';
|
|
6
|
+
|
|
7
|
+
function producerFields(): Record<string, unknown> {
|
|
8
|
+
return {
|
|
9
|
+
packageId: 'pkg/fixture',
|
|
10
|
+
provenance: { provider: 'fixture-provider', version: '1.0.0' },
|
|
11
|
+
revision: { digest: 'sha256:fixture', observedAt: 123, rootId: 'root-a' },
|
|
12
|
+
diagnostics: [
|
|
13
|
+
{
|
|
14
|
+
code: 'fixture-warning',
|
|
15
|
+
severity: 'warning',
|
|
16
|
+
subject: { type: 'package', id: 'pkg/fixture' },
|
|
17
|
+
expected: 'one stable source key',
|
|
18
|
+
hint: 'declare sourceKey on every imported output',
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('producer contract schema and semantic validation', () => {
|
|
25
|
+
it('accepts open provider and asset kinds with producer facts', () => {
|
|
26
|
+
const meta = {
|
|
27
|
+
schemaVersion: 1,
|
|
28
|
+
kind: 'external-asset-package',
|
|
29
|
+
importer: 'host-provider',
|
|
30
|
+
...producerFields(),
|
|
31
|
+
importSettings: {},
|
|
32
|
+
subAssets: [{ guid: GUID, sourceIndex: 0, sourceKey: 'blob/main', kind: 'host/blob' }],
|
|
33
|
+
};
|
|
34
|
+
expect(validateMeta(meta)).toBe(true);
|
|
35
|
+
expect(validateProducerContract(meta).ok).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('keeps sourceIndex valid without using it to invent sourceKey', () => {
|
|
39
|
+
const declaration = { guid: GUID, sourceIndex: 0, kind: 'host/blob' };
|
|
40
|
+
const result = validateProducerContract(declaration);
|
|
41
|
+
expect(result).toEqual({
|
|
42
|
+
ok: false,
|
|
43
|
+
error: expect.objectContaining({ code: 'missing-source-key' }),
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('accepts structured fields on internal pack assets', () => {
|
|
48
|
+
const pack = {
|
|
49
|
+
schemaVersion: '1.0.0',
|
|
50
|
+
kind: 'internal-text-package',
|
|
51
|
+
...producerFields(),
|
|
52
|
+
assets: [
|
|
53
|
+
{
|
|
54
|
+
guid: GUID,
|
|
55
|
+
kind: 'host/blob',
|
|
56
|
+
execution: 'direct',
|
|
57
|
+
sourceKey: 'blob/main',
|
|
58
|
+
sourceIndex: 0,
|
|
59
|
+
payload: {},
|
|
60
|
+
refs: [],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
expect(validatePack(pack)).toBe(true);
|
|
65
|
+
expect(validateProducerContract(pack.assets[0]).ok).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('rejects an authoring Pack asset without explicit execution', () => {
|
|
69
|
+
const pack = {
|
|
70
|
+
schemaVersion: '2.0.0',
|
|
71
|
+
kind: 'internal-text-package',
|
|
72
|
+
packageId: 'pkg/authoring',
|
|
73
|
+
assets: [{ guid: GUID, kind: 'host/blob', payload: {}, refs: [], artifacts: {} }],
|
|
74
|
+
};
|
|
75
|
+
expect(validatePack(pack)).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('rejects duplicate and sourceIndex-only imported output declarations', () => {
|
|
79
|
+
const outputs = [
|
|
80
|
+
{ guid: GUID, sourceIndex: 0, sourceKey: 'blob/main', kind: 'host/blob' },
|
|
81
|
+
{ guid: `${GUID.slice(0, -1)}b`, sourceIndex: 1, sourceKey: 'blob/main', kind: 'host/blob' },
|
|
82
|
+
];
|
|
83
|
+
const duplicate = validateProducerOutputs(outputs);
|
|
84
|
+
expect(duplicate.ok).toBe(false);
|
|
85
|
+
if (!duplicate.ok) expect(duplicate.error.code).toBe('duplicate-source-key');
|
|
86
|
+
const sourceIndexOnly = validateProducerOutputs(
|
|
87
|
+
outputs.map(({ sourceKey: _sourceKey, ...output }) => output),
|
|
88
|
+
);
|
|
89
|
+
expect(sourceIndexOnly.ok).toBe(false);
|
|
90
|
+
if (!sourceIndexOnly.ok) expect(sourceIndexOnly.error.code).toBe('source-index-ambiguous');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// @ts-nocheck — PackErrorDetail is a discriminated union narrowed by runtime .code
|
|
2
|
+
// checks; TS cannot follow the narrowing through PackError.detail.
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { resolveAssetSource } from '../resolve-asset-source.js';
|
|
6
|
+
|
|
7
|
+
describe('resolveAssetSource — omit derivation (w4)', () => {
|
|
8
|
+
const metaDir = '/project/assets';
|
|
9
|
+
|
|
10
|
+
it('omitted source derives companion file from meta filename (AC-2)', () => {
|
|
11
|
+
const metaPath = resolve(metaDir, 'foo.png.meta.json');
|
|
12
|
+
const result = resolveAssetSource(metaPath, undefined, {});
|
|
13
|
+
expect(result.ok).toBe(true);
|
|
14
|
+
if (result.ok) {
|
|
15
|
+
expect(result.value).toBe(resolve(metaDir, 'foo.png'));
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('renamed audio meta derives correct companion (AC-3)', () => {
|
|
20
|
+
const metaPath = resolve(metaDir, 'bleep.wav.meta.json');
|
|
21
|
+
const result = resolveAssetSource(metaPath, undefined, {});
|
|
22
|
+
expect(result.ok).toBe(true);
|
|
23
|
+
if (result.ok) {
|
|
24
|
+
expect(result.value).toBe(resolve(metaDir, 'bleep.wav'));
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('derivation is a single rule: strip .meta.json suffix only', () => {
|
|
29
|
+
const cases = [
|
|
30
|
+
{ meta: 'model.glb.meta.json', expected: 'model.glb' },
|
|
31
|
+
{ meta: 'sprite.png.meta.json', expected: 'sprite.png' },
|
|
32
|
+
{ meta: 'font.ttf.meta.json', expected: 'font.ttf' },
|
|
33
|
+
{ meta: 'bgm.mp3.meta.json', expected: 'bgm.mp3' },
|
|
34
|
+
];
|
|
35
|
+
for (const { meta, expected } of cases) {
|
|
36
|
+
const metaPath = resolve(metaDir, meta);
|
|
37
|
+
const result = resolveAssetSource(metaPath, undefined, {});
|
|
38
|
+
expect(result.ok).toBe(true);
|
|
39
|
+
if (result.ok) {
|
|
40
|
+
expect(result.value).toBe(resolve(metaDir, expected));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('resolveAssetSource — @name/ path + boundary + legacy (w5)', () => {
|
|
47
|
+
const metaDir = '/project/assets';
|
|
48
|
+
const metaPath = resolve(metaDir, 'foo.png.meta.json');
|
|
49
|
+
|
|
50
|
+
it('resolves @name/rest to absolute path via paths table (AC-5)', () => {
|
|
51
|
+
const paths = { shared: '/lib/assets' };
|
|
52
|
+
const result = resolveAssetSource(metaPath, '@shared/foo.png', paths);
|
|
53
|
+
expect(result.ok).toBe(true);
|
|
54
|
+
if (result.ok) {
|
|
55
|
+
expect(result.value).toBe(resolve('/lib/assets', 'foo.png'));
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('returns pack-unknown-path for undeclared @name (AC-8)', () => {
|
|
60
|
+
const paths = { shared: '/lib/assets' };
|
|
61
|
+
const result = resolveAssetSource(metaPath, '@nope/x.png', paths);
|
|
62
|
+
expect(result.ok).toBe(false);
|
|
63
|
+
if (!result.ok) {
|
|
64
|
+
expect(result.error.code).toBe('pack-unknown-path');
|
|
65
|
+
expect(result.error.detail.pathName).toBe('nope');
|
|
66
|
+
expect(result.error.detail.knownNames).toEqual(['shared']);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns pack-malformed-path-ref for bare @ with no name or rest (AC-9)', () => {
|
|
71
|
+
const result = resolveAssetSource(metaPath, '@', { shared: '/lib/assets' });
|
|
72
|
+
expect(result.ok).toBe(false);
|
|
73
|
+
if (!result.ok) {
|
|
74
|
+
expect(result.error.code).toBe('pack-malformed-path-ref');
|
|
75
|
+
expect(result.error.detail.rawSource).toBe('@');
|
|
76
|
+
if (result.error.detail.code === 'pack-malformed-path-ref') {
|
|
77
|
+
expect(result.error.detail.reason).toBe('format');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('returns pack-malformed-path-ref for @onlyname with no /rest (AC-9)', () => {
|
|
83
|
+
const result = resolveAssetSource(metaPath, '@onlyname', { shared: '/lib/assets' });
|
|
84
|
+
expect(result.ok).toBe(false);
|
|
85
|
+
if (!result.ok) {
|
|
86
|
+
expect(result.error.code).toBe('pack-malformed-path-ref');
|
|
87
|
+
expect(result.error.detail.rawSource).toBe('@onlyname');
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('returns pack-malformed-path-ref for ../ escape (AC-11)', () => {
|
|
92
|
+
const paths = { shared: '/data/shared' };
|
|
93
|
+
const result = resolveAssetSource(metaPath, '@shared/../escape.png', paths);
|
|
94
|
+
expect(result.ok).toBe(false);
|
|
95
|
+
if (!result.ok) {
|
|
96
|
+
expect(result.error.code).toBe('pack-malformed-path-ref');
|
|
97
|
+
expect(result.error.detail.rawSource).toBe('@shared/../escape.png');
|
|
98
|
+
if (result.error.detail.code === 'pack-malformed-path-ref') {
|
|
99
|
+
expect(result.error.detail.reason).toBe('escape');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('resolves legacy relative path unchanged', () => {
|
|
105
|
+
const result = resolveAssetSource(metaPath, 'sub/foo.png', {});
|
|
106
|
+
expect(result.ok).toBe(true);
|
|
107
|
+
if (result.ok) {
|
|
108
|
+
expect(result.value).toBe(resolve(metaDir, 'sub/foo.png'));
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('returns pack-unknown-path for @name/ when paths is empty', () => {
|
|
113
|
+
const result = resolveAssetSource(metaPath, '@any/x.png', {});
|
|
114
|
+
expect(result.ok).toBe(false);
|
|
115
|
+
if (!result.ok) {
|
|
116
|
+
expect(result.error.code).toBe('pack-unknown-path');
|
|
117
|
+
expect(result.error.detail.pathName).toBe('any');
|
|
118
|
+
expect(result.error.detail.knownNames).toEqual([]);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { projectRuntimePack } from '../runtime-projection.js';
|
|
4
|
+
|
|
5
|
+
const artifact = {
|
|
6
|
+
path: 'mesh.bin',
|
|
7
|
+
mediaType: 'application/octet-stream',
|
|
8
|
+
contentEncoding: 'identity' as const,
|
|
9
|
+
byteLength: 3,
|
|
10
|
+
integrity: { algorithm: 'sha256' as const, digest: 'sha256:mesh' },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const asset = {
|
|
14
|
+
guid: '019e2cc6-0c86-79da-aa76-b0984c86d45a',
|
|
15
|
+
kind: 'mesh',
|
|
16
|
+
payload: { kind: 'mesh' },
|
|
17
|
+
refs: [],
|
|
18
|
+
artifacts: { mesh: artifact },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe('runtime Pack projection', () => {
|
|
22
|
+
it('preserves the material payload and artifact tuple without a runtime adapter', () => {
|
|
23
|
+
const material = {
|
|
24
|
+
guid: '019e2cc6-0c86-79da-aa76-b0984c86d45b',
|
|
25
|
+
kind: 'material',
|
|
26
|
+
payload: {
|
|
27
|
+
kind: 'material',
|
|
28
|
+
passes: [{ name: 'forward', program: { module: 'forgeax::standard' } }],
|
|
29
|
+
values: { baseColor: [1, 1, 1, 1] },
|
|
30
|
+
},
|
|
31
|
+
refs: [],
|
|
32
|
+
artifacts: {},
|
|
33
|
+
};
|
|
34
|
+
const result = projectRuntimePack({
|
|
35
|
+
scopeId: 'material-session',
|
|
36
|
+
generation: 1,
|
|
37
|
+
digest: 'sha256:material-pack',
|
|
38
|
+
outputSetDigest: 'sha256:material-outputs',
|
|
39
|
+
assets: [material],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(result).toMatchObject({ ok: true, value: { assets: [material] } });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('publishes one atomic tuple and one Pack v2 envelope', () => {
|
|
46
|
+
const result = projectRuntimePack({
|
|
47
|
+
scopeId: 'play-session',
|
|
48
|
+
generation: 7,
|
|
49
|
+
digest: 'sha256:pack',
|
|
50
|
+
outputSetDigest: 'sha256:outputs',
|
|
51
|
+
assets: [asset],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(result).toEqual({
|
|
55
|
+
ok: true,
|
|
56
|
+
value: {
|
|
57
|
+
schemaVersion: '2.0.0',
|
|
58
|
+
kind: 'internal-text-package',
|
|
59
|
+
scopeId: 'play-session',
|
|
60
|
+
generation: 7,
|
|
61
|
+
digest: 'sha256:pack',
|
|
62
|
+
outputSetDigest: 'sha256:outputs',
|
|
63
|
+
assets: [asset],
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('rejects legacy and duplicate-row input before publication', () => {
|
|
69
|
+
const legacy = projectRuntimePack({ schemaVersion: '1.0.0', assets: [asset] });
|
|
70
|
+
expect(legacy).toMatchObject({
|
|
71
|
+
ok: false,
|
|
72
|
+
error: { code: 'pack-v2-envelope-invalid', detail: { expected: 'runtime Pack v2' } },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const duplicate = projectRuntimePack({
|
|
76
|
+
scopeId: 'play-session',
|
|
77
|
+
generation: 7,
|
|
78
|
+
digest: 'sha256:pack',
|
|
79
|
+
outputSetDigest: 'sha256:outputs',
|
|
80
|
+
assets: [asset, asset],
|
|
81
|
+
});
|
|
82
|
+
expect(duplicate).toMatchObject({
|
|
83
|
+
ok: false,
|
|
84
|
+
error: { code: 'pack-v2-duplicate-guid', detail: { guid: asset.guid } },
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { mkdtemp, realpath, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
5
|
+
import { scanInventory } from '../scanner.js';
|
|
6
|
+
|
|
7
|
+
describe('AssetInventory', () => {
|
|
8
|
+
const guid = '00000000-0000-0000-0000-000000000002';
|
|
9
|
+
let root: string | undefined;
|
|
10
|
+
let sourcePath: string | undefined;
|
|
11
|
+
let result: Awaited<ReturnType<typeof scanInventory>> | undefined;
|
|
12
|
+
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
root = await mkdtemp(join(tmpdir(), 'forgeax-scanner-inventory-'));
|
|
15
|
+
sourcePath = join(root, 'generated.pack.ts');
|
|
16
|
+
await writeFile(
|
|
17
|
+
sourcePath,
|
|
18
|
+
[
|
|
19
|
+
'const packageId = new Uint8Array(16);',
|
|
20
|
+
'packageId[15] = 1;',
|
|
21
|
+
'const meshGuid = new Uint8Array(16);',
|
|
22
|
+
'meshGuid[15] = 2;',
|
|
23
|
+
'export default {',
|
|
24
|
+
"schemaVersion: '1.0.0', packageId,",
|
|
25
|
+
"assets: { mesh: { guid: meshGuid, kind: 'mesh' } }, externalAssets: {},",
|
|
26
|
+
"build: () => ({ ok: true, value: { mesh: { kind: 'mesh' } } }),",
|
|
27
|
+
'};',
|
|
28
|
+
].join('\n'),
|
|
29
|
+
);
|
|
30
|
+
result = await scanInventory([root]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterAll(async () => {
|
|
34
|
+
if (root !== undefined) await rm(root, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const getFixture = () => {
|
|
38
|
+
if (result === undefined || sourcePath === undefined) {
|
|
39
|
+
throw new Error('scanner fixture was not initialized');
|
|
40
|
+
}
|
|
41
|
+
if (!result.ok) throw new Error('scanner fixture failed');
|
|
42
|
+
return { inventory: result.value, sourcePath };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
it('projects the ScriptablePack declaration and owner outputs', () => {
|
|
46
|
+
const { inventory, sourcePath } = getFixture();
|
|
47
|
+
const declaration = inventory.declarationsByPath.get(sourcePath);
|
|
48
|
+
expect(declaration).toMatchObject({
|
|
49
|
+
sourcePath,
|
|
50
|
+
producer: 'scriptable-pack',
|
|
51
|
+
outputs: [{ guid, sourceKey: 'mesh', kind: 'mesh', refs: [] }],
|
|
52
|
+
externalGuids: [],
|
|
53
|
+
diagnostics: [],
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('projects source closure evidence', async () => {
|
|
58
|
+
const { inventory, sourcePath } = getFixture();
|
|
59
|
+
const declaration = inventory.declarationsByPath.get(sourcePath);
|
|
60
|
+
expect(declaration?.sourceClosure?.map((entry) => entry.path)).toEqual([
|
|
61
|
+
await realpath(sourcePath),
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('indexes the output GUID and ScriptablePack metadata', () => {
|
|
66
|
+
const { inventory, sourcePath } = getFixture();
|
|
67
|
+
expect(inventory.sourceByGuid.get(guid)).toBe(sourcePath);
|
|
68
|
+
expect(inventory.scriptablePackMeta.get(sourcePath)?.subAssets).toEqual([
|
|
69
|
+
{ guid, sourceIndex: 0, sourceKey: 'mesh', kind: 'mesh' },
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// scanner-mount-cycle.test - scanner step-6 mount-asset cycle coverage
|
|
2
|
+
// (feat-20260608-scene-nesting-ecs-fication M1 / w13).
|
|
3
|
+
//
|
|
4
|
+
// Coverage (AC-12; plan-strategy D-1):
|
|
5
|
+
// (a) acyclic mount chain A -> B -> C: scanner returns Ok (no cycle).
|
|
6
|
+
// (b) two-asset cycle A -> B -> A: scanner returns
|
|
7
|
+
// Err(PackError{ code: 'pack-cyclic-reference', detail.kind: 'mount-asset',
|
|
8
|
+
// detail.cycle: GUID[] }) with the GUIDs in cycle order (first === last).
|
|
9
|
+
// (c) three-asset cycle A -> B -> C -> A: same, longer cycle.
|
|
10
|
+
// (d) self-mount A -> A: same, single-element cycle.
|
|
11
|
+
//
|
|
12
|
+
// The fixtures are hand-authored scene .pack.json files in a tmpdir;
|
|
13
|
+
// each scene asset uses `payload.mounts[].source` as an integer index into
|
|
14
|
+
// the parent scene's `refs[]` (D-1 + research Finding 11). The mount window
|
|
15
|
+
// (memberFirst / memberCount) is set conservatively so the scanner step-6
|
|
16
|
+
// cycle pass triggers before any mount-window collision check.
|
|
17
|
+
//
|
|
18
|
+
// TDD red signal: until w14 wires `payload.mounts[].source` -> `refsByGuid`
|
|
19
|
+
// edge into scanner step-6, the scanner does NOT see the mount edge in the
|
|
20
|
+
// DFS and the cycle assertions miss. With w14 the cycle producer surfaces
|
|
21
|
+
// `pack-cyclic-reference + detail.kind: 'mount-asset'` (w8 landed the
|
|
22
|
+
// detail.kind tagging at the producer).
|
|
23
|
+
|
|
24
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
25
|
+
import { tmpdir } from 'node:os';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
28
|
+
import { scan } from '../scanner.js';
|
|
29
|
+
|
|
30
|
+
const GUID_A = 'aa000000-0000-4000-8000-000000000aaa';
|
|
31
|
+
const GUID_B = 'bb000000-0000-4000-8000-000000000bbb';
|
|
32
|
+
const GUID_C = 'cc000000-0000-4000-8000-000000000ccc';
|
|
33
|
+
|
|
34
|
+
interface MountSpec {
|
|
35
|
+
readonly localId: number;
|
|
36
|
+
readonly source: number;
|
|
37
|
+
readonly memberFirst: number;
|
|
38
|
+
readonly memberCount: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface SceneAssetSpec {
|
|
42
|
+
readonly guid: string;
|
|
43
|
+
/** GUIDs the scene asset's `payload.mounts[].source` integers index into. */
|
|
44
|
+
readonly refs: readonly string[];
|
|
45
|
+
readonly mounts: readonly MountSpec[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sceneAssetPack(spec: SceneAssetSpec): unknown {
|
|
49
|
+
return {
|
|
50
|
+
schemaVersion: '1.0.0',
|
|
51
|
+
kind: 'internal-text-package',
|
|
52
|
+
assets: [
|
|
53
|
+
{
|
|
54
|
+
guid: spec.guid,
|
|
55
|
+
kind: 'scene',
|
|
56
|
+
refs: [...spec.refs],
|
|
57
|
+
payload: {
|
|
58
|
+
kind: 'scene',
|
|
59
|
+
entities: [{ localId: 0, components: {} }],
|
|
60
|
+
mounts: spec.mounts.map((m) => ({
|
|
61
|
+
localId: m.localId,
|
|
62
|
+
source: m.source,
|
|
63
|
+
memberFirst: m.memberFirst,
|
|
64
|
+
memberCount: m.memberCount,
|
|
65
|
+
})),
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface CycleErrorShape {
|
|
73
|
+
readonly code: string;
|
|
74
|
+
readonly detail: {
|
|
75
|
+
readonly code?: string;
|
|
76
|
+
readonly kind?: string;
|
|
77
|
+
readonly cycle?: readonly string[];
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe('scanner step-6 mount-asset cycle detection (AC-12; D-1)', () => {
|
|
82
|
+
let dir: string;
|
|
83
|
+
|
|
84
|
+
beforeEach(async () => {
|
|
85
|
+
dir = await mkdtemp(join(tmpdir(), 'pack-mount-cycle-'));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterEach(async () => {
|
|
89
|
+
await rm(dir, { recursive: true, force: true });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('(a) accepts an acyclic mount chain A -> B -> C', async () => {
|
|
93
|
+
// A mounts B (source=0 in A.refs) -> B mounts C (source=0 in B.refs) ->
|
|
94
|
+
// C mounts nothing.
|
|
95
|
+
await writeFile(
|
|
96
|
+
join(dir, 'a.pack.json'),
|
|
97
|
+
JSON.stringify(
|
|
98
|
+
sceneAssetPack({
|
|
99
|
+
guid: GUID_A,
|
|
100
|
+
refs: [GUID_B],
|
|
101
|
+
mounts: [{ localId: 1, source: 0, memberFirst: 2, memberCount: 1 }],
|
|
102
|
+
}),
|
|
103
|
+
),
|
|
104
|
+
'utf-8',
|
|
105
|
+
);
|
|
106
|
+
await writeFile(
|
|
107
|
+
join(dir, 'b.pack.json'),
|
|
108
|
+
JSON.stringify(
|
|
109
|
+
sceneAssetPack({
|
|
110
|
+
guid: GUID_B,
|
|
111
|
+
refs: [GUID_C],
|
|
112
|
+
mounts: [{ localId: 1, source: 0, memberFirst: 2, memberCount: 1 }],
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
'utf-8',
|
|
116
|
+
);
|
|
117
|
+
await writeFile(
|
|
118
|
+
join(dir, 'c.pack.json'),
|
|
119
|
+
JSON.stringify(sceneAssetPack({ guid: GUID_C, refs: [], mounts: [] })),
|
|
120
|
+
'utf-8',
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const result = await scan([dir]);
|
|
124
|
+
expect(result.ok).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('(b) rejects a two-asset cycle A -> B -> A with kind: mount-asset', async () => {
|
|
128
|
+
await writeFile(
|
|
129
|
+
join(dir, 'a.pack.json'),
|
|
130
|
+
JSON.stringify(
|
|
131
|
+
sceneAssetPack({
|
|
132
|
+
guid: GUID_A,
|
|
133
|
+
refs: [GUID_B],
|
|
134
|
+
mounts: [{ localId: 1, source: 0, memberFirst: 2, memberCount: 1 }],
|
|
135
|
+
}),
|
|
136
|
+
),
|
|
137
|
+
'utf-8',
|
|
138
|
+
);
|
|
139
|
+
await writeFile(
|
|
140
|
+
join(dir, 'b.pack.json'),
|
|
141
|
+
JSON.stringify(
|
|
142
|
+
sceneAssetPack({
|
|
143
|
+
guid: GUID_B,
|
|
144
|
+
refs: [GUID_A],
|
|
145
|
+
mounts: [{ localId: 1, source: 0, memberFirst: 2, memberCount: 1 }],
|
|
146
|
+
}),
|
|
147
|
+
),
|
|
148
|
+
'utf-8',
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const result = await scan([dir]);
|
|
152
|
+
expect(result.ok).toBe(false);
|
|
153
|
+
if (result.ok) return;
|
|
154
|
+
const err = result.error as unknown as CycleErrorShape;
|
|
155
|
+
expect(err.code).toBe('pack-cyclic-reference');
|
|
156
|
+
expect(err.detail.kind).toBe('mount-asset');
|
|
157
|
+
const cycle = err.detail.cycle ?? [];
|
|
158
|
+
expect(cycle.length).toBeGreaterThanOrEqual(2);
|
|
159
|
+
expect(cycle[0]).toBe(cycle[cycle.length - 1]);
|
|
160
|
+
// Both GUIDs participate in the cycle (case-insensitive compare; scanner
|
|
161
|
+
// lowercases before storing).
|
|
162
|
+
const lower = cycle.map((g) => g.toLowerCase());
|
|
163
|
+
expect(lower).toContain(GUID_A.toLowerCase());
|
|
164
|
+
expect(lower).toContain(GUID_B.toLowerCase());
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('(c) rejects a three-asset cycle A -> B -> C -> A with kind: mount-asset', async () => {
|
|
168
|
+
await writeFile(
|
|
169
|
+
join(dir, 'a.pack.json'),
|
|
170
|
+
JSON.stringify(
|
|
171
|
+
sceneAssetPack({
|
|
172
|
+
guid: GUID_A,
|
|
173
|
+
refs: [GUID_B],
|
|
174
|
+
mounts: [{ localId: 1, source: 0, memberFirst: 2, memberCount: 1 }],
|
|
175
|
+
}),
|
|
176
|
+
),
|
|
177
|
+
'utf-8',
|
|
178
|
+
);
|
|
179
|
+
await writeFile(
|
|
180
|
+
join(dir, 'b.pack.json'),
|
|
181
|
+
JSON.stringify(
|
|
182
|
+
sceneAssetPack({
|
|
183
|
+
guid: GUID_B,
|
|
184
|
+
refs: [GUID_C],
|
|
185
|
+
mounts: [{ localId: 1, source: 0, memberFirst: 2, memberCount: 1 }],
|
|
186
|
+
}),
|
|
187
|
+
),
|
|
188
|
+
'utf-8',
|
|
189
|
+
);
|
|
190
|
+
await writeFile(
|
|
191
|
+
join(dir, 'c.pack.json'),
|
|
192
|
+
JSON.stringify(
|
|
193
|
+
sceneAssetPack({
|
|
194
|
+
guid: GUID_C,
|
|
195
|
+
refs: [GUID_A],
|
|
196
|
+
mounts: [{ localId: 1, source: 0, memberFirst: 2, memberCount: 1 }],
|
|
197
|
+
}),
|
|
198
|
+
),
|
|
199
|
+
'utf-8',
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const result = await scan([dir]);
|
|
203
|
+
expect(result.ok).toBe(false);
|
|
204
|
+
if (result.ok) return;
|
|
205
|
+
const err = result.error as unknown as CycleErrorShape;
|
|
206
|
+
expect(err.code).toBe('pack-cyclic-reference');
|
|
207
|
+
expect(err.detail.kind).toBe('mount-asset');
|
|
208
|
+
const cycle = err.detail.cycle ?? [];
|
|
209
|
+
expect(cycle.length).toBeGreaterThanOrEqual(3);
|
|
210
|
+
expect(cycle[0]).toBe(cycle[cycle.length - 1]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('(d) rejects a self-mount A -> A', async () => {
|
|
214
|
+
await writeFile(
|
|
215
|
+
join(dir, 'a.pack.json'),
|
|
216
|
+
JSON.stringify(
|
|
217
|
+
sceneAssetPack({
|
|
218
|
+
guid: GUID_A,
|
|
219
|
+
refs: [GUID_A],
|
|
220
|
+
mounts: [{ localId: 1, source: 0, memberFirst: 2, memberCount: 1 }],
|
|
221
|
+
}),
|
|
222
|
+
),
|
|
223
|
+
'utf-8',
|
|
224
|
+
);
|
|
225
|
+
const result = await scan([dir]);
|
|
226
|
+
expect(result.ok).toBe(false);
|
|
227
|
+
if (result.ok) return;
|
|
228
|
+
const err = result.error as unknown as CycleErrorShape;
|
|
229
|
+
expect(err.code).toBe('pack-cyclic-reference');
|
|
230
|
+
expect(err.detail.kind).toBe('mount-asset');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @perf-budget-skip: intentional ScriptablePack CLI integration gate.
|
|
2
|
+
|
|
3
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { runCliAsset } from '../cli-asset.js';
|
|
8
|
+
|
|
9
|
+
describe('ScriptablePack CLI Meta inspection', () => {
|
|
10
|
+
it('loads the definition without invoking build', async () => {
|
|
11
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-pack-'));
|
|
12
|
+
try {
|
|
13
|
+
const source = join(root, 'house.pack.mjs');
|
|
14
|
+
await writeFile(
|
|
15
|
+
source,
|
|
16
|
+
`
|
|
17
|
+
const guid = (hex) => new Uint8Array(hex.match(/../g).map((byte) => Number.parseInt(byte, 16)));
|
|
18
|
+
export default {
|
|
19
|
+
schemaVersion: '1.0.0',
|
|
20
|
+
packageId: guid('019ffa978b397ad284cc273268591ab4'),
|
|
21
|
+
assets: {
|
|
22
|
+
wall: { guid: guid('019ffa97a5ee7645b613043323952808'), kind: 'mesh' }
|
|
23
|
+
},
|
|
24
|
+
externalAssets: {},
|
|
25
|
+
build() { throw new Error('META_MUST_NOT_BUILD'); }
|
|
26
|
+
};
|
|
27
|
+
`,
|
|
28
|
+
);
|
|
29
|
+
const stdout: string[] = [];
|
|
30
|
+
const stderr: string[] = [];
|
|
31
|
+
const code = await runCliAsset(['meta', source, '--json'], {
|
|
32
|
+
stdoutWrite: (line) => stdout.push(line),
|
|
33
|
+
stderrWrite: (line) => stderr.push(line),
|
|
34
|
+
cwd: root,
|
|
35
|
+
});
|
|
36
|
+
expect(code).toBe(0);
|
|
37
|
+
expect(stderr).toEqual([]);
|
|
38
|
+
expect(JSON.parse(stdout[0] ?? '{}')).toMatchObject({
|
|
39
|
+
importer: 'pack-ts',
|
|
40
|
+
source,
|
|
41
|
+
subAssets: [{ sourceKey: 'wall', kind: 'mesh' }],
|
|
42
|
+
});
|
|
43
|
+
} finally {
|
|
44
|
+
await rm(root, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|