@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,121 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, 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 { loadAssetConfig } from '../config.js';
|
|
6
|
+
|
|
7
|
+
describe('loadAssetConfig SSOT (w8 / AC-13)', () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'forgeax-test-loadAssetConfig-'));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterAll(async () => {
|
|
15
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('missing package.json returns default roots + empty paths', () => {
|
|
19
|
+
const result = loadAssetConfig(join(tmpDir, 'nonexistent'));
|
|
20
|
+
expect(result.roots).toEqual([join(join(tmpDir, 'nonexistent'), 'assets')]);
|
|
21
|
+
expect(result.paths).toEqual({});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('package.json without forgeax.assets returns default', async () => {
|
|
25
|
+
const dir = join(tmpDir, 'no-forgeax');
|
|
26
|
+
await mkdir(dir);
|
|
27
|
+
await writeFile(join(dir, 'package.json'), JSON.stringify({ name: 'test' }));
|
|
28
|
+
const result = loadAssetConfig(dir);
|
|
29
|
+
expect(result.roots).toEqual([join(dir, 'assets')]);
|
|
30
|
+
expect(result.paths).toEqual({});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('reads forgeax.assets.roots from package.json', async () => {
|
|
34
|
+
const dir = join(tmpDir, 'with-roots');
|
|
35
|
+
await mkdir(dir);
|
|
36
|
+
await writeFile(
|
|
37
|
+
join(dir, 'package.json'),
|
|
38
|
+
JSON.stringify({
|
|
39
|
+
name: 'test',
|
|
40
|
+
forgeax: { assets: { roots: ['my-assets', 'more'] } },
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
const result = loadAssetConfig(dir);
|
|
44
|
+
expect(result.roots).toEqual([join(dir, 'my-assets'), join(dir, 'more')]);
|
|
45
|
+
expect(result.paths).toEqual({});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('empty forgeax.assets.roots array falls back to default', async () => {
|
|
49
|
+
const dir = join(tmpDir, 'empty-roots');
|
|
50
|
+
await mkdir(dir);
|
|
51
|
+
await writeFile(
|
|
52
|
+
join(dir, 'package.json'),
|
|
53
|
+
JSON.stringify({
|
|
54
|
+
name: 'test',
|
|
55
|
+
forgeax: { assets: { roots: [] } },
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
const result = loadAssetConfig(dir);
|
|
59
|
+
expect(result.roots).toEqual([join(dir, 'assets')]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('reads forgeax.assets.paths with cwd-relative resolution', async () => {
|
|
63
|
+
const dir = join(tmpDir, 'with-paths');
|
|
64
|
+
await mkdir(dir);
|
|
65
|
+
await writeFile(
|
|
66
|
+
join(dir, 'package.json'),
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
name: 'test',
|
|
69
|
+
forgeax: {
|
|
70
|
+
assets: {
|
|
71
|
+
paths: {
|
|
72
|
+
shared: 'lib/assets',
|
|
73
|
+
vendor: '../vendor-assets',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
const result = loadAssetConfig(dir);
|
|
80
|
+
expect(result.paths).toEqual({
|
|
81
|
+
shared: join(dir, 'lib/assets'),
|
|
82
|
+
vendor: join(dir, '../vendor-assets'),
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('paths absent defaults to empty object', async () => {
|
|
87
|
+
const dir = join(tmpDir, 'roots-only');
|
|
88
|
+
await mkdir(dir);
|
|
89
|
+
await writeFile(
|
|
90
|
+
join(dir, 'package.json'),
|
|
91
|
+
JSON.stringify({
|
|
92
|
+
name: 'test',
|
|
93
|
+
forgeax: { assets: { roots: ['a'] } },
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
const result = loadAssetConfig(dir);
|
|
97
|
+
expect(result.paths).toEqual({});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('non-string path values are skipped', async () => {
|
|
101
|
+
const dir = join(tmpDir, 'bad-paths');
|
|
102
|
+
await mkdir(dir);
|
|
103
|
+
await writeFile(
|
|
104
|
+
join(dir, 'package.json'),
|
|
105
|
+
JSON.stringify({
|
|
106
|
+
name: 'test',
|
|
107
|
+
forgeax: {
|
|
108
|
+
assets: {
|
|
109
|
+
paths: {
|
|
110
|
+
good: 'lib/a',
|
|
111
|
+
badNum: 42,
|
|
112
|
+
badBool: true,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
const result = loadAssetConfig(dir);
|
|
119
|
+
expect(result.paths).toEqual({ good: join(dir, 'lib/a') });
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AssetGuid } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { collectMaterialCookRefs } from '../evidence/material-cook.js';
|
|
4
|
+
|
|
5
|
+
describe('material cook dependency closure', () => {
|
|
6
|
+
it('collects parent, texture, sampler, and module references', () => {
|
|
7
|
+
expect(
|
|
8
|
+
collectMaterialCookRefs({
|
|
9
|
+
parent: 'mat-parent' as unknown as AssetGuid,
|
|
10
|
+
passes: [{ name: 'forward', program: { module: 'module/pbr' } }],
|
|
11
|
+
values: {
|
|
12
|
+
baseColor: {
|
|
13
|
+
texture: 'texture/albedo' as unknown as AssetGuid,
|
|
14
|
+
sampler: 'sampler/linear' as unknown as AssetGuid,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
}),
|
|
18
|
+
).toEqual({
|
|
19
|
+
parent: ['mat-parent'],
|
|
20
|
+
textures: ['texture/albedo'],
|
|
21
|
+
samplers: ['sampler/linear'],
|
|
22
|
+
modules: ['module/pbr'],
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('collects string shorthands only from declared texture parameters', () => {
|
|
27
|
+
expect(
|
|
28
|
+
collectMaterialCookRefs({
|
|
29
|
+
parameters: [
|
|
30
|
+
{ name: 'surface', type: 'texture' },
|
|
31
|
+
{ name: 'label', type: 'f32' },
|
|
32
|
+
],
|
|
33
|
+
values: { surface: 'texture/albedo', label: 'not-a-texture-ref' },
|
|
34
|
+
}),
|
|
35
|
+
).toEqual({ parent: [], textures: ['texture/albedo'], samplers: [], modules: [] });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('treats a string value as a texture shorthand when parameters are omitted', () => {
|
|
39
|
+
expect(
|
|
40
|
+
collectMaterialCookRefs({
|
|
41
|
+
values: { baseColorTexture: 'texture/albedo' },
|
|
42
|
+
}),
|
|
43
|
+
).toEqual({ parent: [], textures: ['texture/albedo'], samplers: [], modules: [] });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
type MaterialCookReceipt,
|
|
4
|
+
serializeMaterialCookReceipt,
|
|
5
|
+
validateMaterialCookReceipt,
|
|
6
|
+
} from '../evidence/material-cook.js';
|
|
7
|
+
|
|
8
|
+
const layoutIdentity = 'sha256:layout-identity';
|
|
9
|
+
const artifactDigest = 'sha256:output';
|
|
10
|
+
|
|
11
|
+
function receipt(overrides: Record<string, unknown> = {}): MaterialCookReceipt {
|
|
12
|
+
return {
|
|
13
|
+
schemaVersion: 'material-cook/2',
|
|
14
|
+
sourceClosure: ['b.wgsl', 'a.material.json'],
|
|
15
|
+
profile: 'webgpu/v1',
|
|
16
|
+
compilerVersion: 'compiler/1',
|
|
17
|
+
inputDigest: 'sha256:input',
|
|
18
|
+
outputDigest: artifactDigest,
|
|
19
|
+
layoutIdentity,
|
|
20
|
+
derivedInterface: { layoutIdentity },
|
|
21
|
+
...overrides,
|
|
22
|
+
} as unknown as MaterialCookReceipt;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('material cook receipt', () => {
|
|
26
|
+
it('serializes a material-cook/2 receipt stably for equivalent cooks', () => {
|
|
27
|
+
const first = serializeMaterialCookReceipt(receipt());
|
|
28
|
+
const second = serializeMaterialCookReceipt(
|
|
29
|
+
receipt({ sourceClosure: ['a.material.json', 'b.wgsl'] }),
|
|
30
|
+
);
|
|
31
|
+
expect(first).toBe(second);
|
|
32
|
+
expect(JSON.parse(first)).toMatchObject({
|
|
33
|
+
schemaVersion: 'material-cook/2',
|
|
34
|
+
layoutIdentity,
|
|
35
|
+
derivedInterface: { layoutIdentity },
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('accepts the expected identity and rejects schema or interface mutations', () => {
|
|
40
|
+
const expected = { layoutIdentity, artifactDigest, inputDigest: 'sha256:input' };
|
|
41
|
+
expect(validateMaterialCookReceipt(receipt(), expected)).toEqual({
|
|
42
|
+
ok: true,
|
|
43
|
+
value: receipt(),
|
|
44
|
+
});
|
|
45
|
+
expect(
|
|
46
|
+
validateMaterialCookReceipt(receipt({ layoutIdentity: 'sha256:changed-layout' }), expected),
|
|
47
|
+
).toMatchObject({ ok: false, error: { code: 'material-cook-record-invalid' } });
|
|
48
|
+
expect(
|
|
49
|
+
validateMaterialCookReceipt(
|
|
50
|
+
receipt({ derivedInterface: { layoutIdentity: 'sha256:changed-layout' } }),
|
|
51
|
+
expected,
|
|
52
|
+
),
|
|
53
|
+
).toMatchObject({ ok: false, error: { code: 'material-cook-record-invalid' } });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('rejects missing, stale, mismatched, and material-cook/1 receipts fail-fast', () => {
|
|
57
|
+
const expected = { layoutIdentity, artifactDigest, inputDigest: 'sha256:input' };
|
|
58
|
+
for (const value of [
|
|
59
|
+
receipt({ layoutIdentity: undefined }),
|
|
60
|
+
receipt({ outputDigest: 'sha256:stale-artifact' }),
|
|
61
|
+
receipt({ inputDigest: 'sha256:stale-input' }),
|
|
62
|
+
{
|
|
63
|
+
sourceClosure: [],
|
|
64
|
+
profile: 'webgpu/v1',
|
|
65
|
+
compilerVersion: 'compiler/1',
|
|
66
|
+
inputDigest: 'sha256:input',
|
|
67
|
+
outputDigest: artifactDigest,
|
|
68
|
+
},
|
|
69
|
+
]) {
|
|
70
|
+
expect(validateMaterialCookReceipt(value as MaterialCookReceipt, expected)).toMatchObject({
|
|
71
|
+
ok: false,
|
|
72
|
+
error: { code: 'material-cook-record-invalid' },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('records the coordinator-owned publication generation', () => {
|
|
78
|
+
const receipt = {
|
|
79
|
+
sourceClosure: ['materials/mat-root.material.json'],
|
|
80
|
+
profile: 'webgpu/v1',
|
|
81
|
+
compilerVersion: 'compiler/1',
|
|
82
|
+
inputDigest: 'sha256:input',
|
|
83
|
+
outputDigest: 'sha256:artifact',
|
|
84
|
+
publicationGeneration: 11,
|
|
85
|
+
};
|
|
86
|
+
expect(receipt).toMatchObject({
|
|
87
|
+
publicationGeneration: 11,
|
|
88
|
+
outputDigest: 'sha256:artifact',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
type CookedMaterialRecord,
|
|
6
|
+
createMaterialArtifactDigest,
|
|
7
|
+
projectCookedMaterialRecord,
|
|
8
|
+
serializeCookedMaterialRecord,
|
|
9
|
+
validateCookedMaterialRecord,
|
|
10
|
+
} from '../evidence/material-cook.js';
|
|
11
|
+
|
|
12
|
+
const record: CookedMaterialRecord = {
|
|
13
|
+
schemaVersion: 'material-cook/2',
|
|
14
|
+
guid: 'mat-root',
|
|
15
|
+
authored: {
|
|
16
|
+
kind: 'material',
|
|
17
|
+
passes: [{ name: 'forward', program: { module: 'core/pbr' } }],
|
|
18
|
+
parameters: [{ name: 'roughness', type: 'f32', static: true }],
|
|
19
|
+
values: { roughness: 0.5 },
|
|
20
|
+
},
|
|
21
|
+
resolved: {
|
|
22
|
+
passes: [{ name: 'forward', program: { module: 'core/pbr' } }],
|
|
23
|
+
parameters: [{ name: 'roughness', type: 'f32', static: true }],
|
|
24
|
+
values: { roughness: 0.5 },
|
|
25
|
+
},
|
|
26
|
+
refs: { parent: [], textures: [], samplers: [], modules: ['core/pbr'] },
|
|
27
|
+
artifact: {
|
|
28
|
+
mediaType: 'text/wgsl',
|
|
29
|
+
path: 'materials/mat-root/forward.wgsl',
|
|
30
|
+
digest: 'sha256:artifact',
|
|
31
|
+
bytes: new TextEncoder().encode('shader'),
|
|
32
|
+
},
|
|
33
|
+
receipt: {
|
|
34
|
+
schemaVersion: 'material-cook/2',
|
|
35
|
+
sourceClosure: ['materials/mat-root.material.json'],
|
|
36
|
+
profile: 'webgpu/v1',
|
|
37
|
+
compilerVersion: 'compiler/1',
|
|
38
|
+
inputDigest: 'sha256:input',
|
|
39
|
+
outputDigest: 'sha256:artifact',
|
|
40
|
+
layoutIdentity: 'sha256:layout',
|
|
41
|
+
derivedInterface: { layoutIdentity: 'sha256:layout' },
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
describe('cooked material schema', () => {
|
|
46
|
+
it('requires material-cook/2 identity and derived interface association', () => {
|
|
47
|
+
const schema = JSON.parse(
|
|
48
|
+
readFileSync(
|
|
49
|
+
fileURLToPath(new URL('../schema/material-cook.schema.json', import.meta.url)),
|
|
50
|
+
'utf8',
|
|
51
|
+
),
|
|
52
|
+
) as {
|
|
53
|
+
properties: Record<
|
|
54
|
+
string,
|
|
55
|
+
{ const?: string; required?: string[]; properties?: Record<string, unknown> }
|
|
56
|
+
>;
|
|
57
|
+
required: string[];
|
|
58
|
+
};
|
|
59
|
+
expect(schema.properties.schemaVersion?.const).toBe('material-cook/2');
|
|
60
|
+
expect(schema.properties.receipt?.required).toEqual(
|
|
61
|
+
expect.arrayContaining(['layoutIdentity', 'derivedInterface']),
|
|
62
|
+
);
|
|
63
|
+
expect(schema.properties.receipt?.properties?.derivedInterface).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('serializes a stable cooked DTO and derives an artifact digest', () => {
|
|
67
|
+
const first = serializeCookedMaterialRecord(record);
|
|
68
|
+
const second = serializeCookedMaterialRecord({ ...record });
|
|
69
|
+
|
|
70
|
+
expect(first).toBe(second);
|
|
71
|
+
expect(createMaterialArtifactDigest(record.artifact.bytes)).toBe(
|
|
72
|
+
'sha256:e137e75a5e0e7a623ca39de480667a55b43e0eedec58767634ebaef07c33383a',
|
|
73
|
+
);
|
|
74
|
+
expect(validateCookedMaterialRecord(JSON.parse(first))).toEqual({ ok: true, value: record });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('projects cooked data without exposing an authored GUID entry', () => {
|
|
78
|
+
const projection = projectCookedMaterialRecord(record);
|
|
79
|
+
|
|
80
|
+
expect(projection.resolved.values).toEqual({ roughness: 0.5 });
|
|
81
|
+
expect(projection.artifact.digest).toBe('sha256:artifact');
|
|
82
|
+
expect(projection).not.toHaveProperty('guid');
|
|
83
|
+
expect(projection).not.toHaveProperty('authored');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('rejects missing fields and unsupported schema versions', () => {
|
|
87
|
+
expect(validateCookedMaterialRecord({ ...record, refs: undefined })).toMatchObject({
|
|
88
|
+
ok: false,
|
|
89
|
+
error: { code: 'material-cook-record-invalid' },
|
|
90
|
+
});
|
|
91
|
+
expect(
|
|
92
|
+
validateCookedMaterialRecord({ ...record, schemaVersion: 'material-cook/1' }),
|
|
93
|
+
).toMatchObject({
|
|
94
|
+
ok: false,
|
|
95
|
+
error: { code: 'material-cook-record-invalid' },
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const encoderSource = readFileSync(
|
|
5
|
+
new URL('../../../import/src/mesh-bin.ts', import.meta.url),
|
|
6
|
+
'utf8',
|
|
7
|
+
);
|
|
8
|
+
const decoderSource = readFileSync(
|
|
9
|
+
new URL('../../../geometry/src/assets/mesh-binary.ts', import.meta.url),
|
|
10
|
+
'utf8',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
describe('mesh-bin consumer owner surface', () => {
|
|
14
|
+
it('routes both production halves through Pack-owned facts', () => {
|
|
15
|
+
for (const source of [encoderSource, decoderSource]) {
|
|
16
|
+
expect(source).toContain("from '@forgeax/engine-pack'");
|
|
17
|
+
expect(source).toContain('MESH_BIN_HEADER_V4_BYTES');
|
|
18
|
+
expect(source).not.toContain('const HEADER_V2_BYTES = 28');
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
decodeMeshBinHeader,
|
|
4
|
+
MESH_BIN_HEADER_V4_BYTES,
|
|
5
|
+
MESH_BIN_VERSION,
|
|
6
|
+
} from '../mesh-bin-contract.js';
|
|
7
|
+
|
|
8
|
+
describe('mesh-bin v4 wire contract', () => {
|
|
9
|
+
it('owns a fixed v4 header and rejects legacy versions at the contract boundary', () => {
|
|
10
|
+
expect(MESH_BIN_VERSION).toBe(4);
|
|
11
|
+
expect(MESH_BIN_HEADER_V4_BYTES).toBeGreaterThan(28);
|
|
12
|
+
expect(decodeMeshBinHeader(new Uint8Array(MESH_BIN_HEADER_V4_BYTES))).toMatchObject({
|
|
13
|
+
ok: false,
|
|
14
|
+
error: { code: 'mesh-bin-version-unsupported' },
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('reports truncated headers as structured failures with recovery context', () => {
|
|
19
|
+
const result = decodeMeshBinHeader(new Uint8Array(MESH_BIN_HEADER_V4_BYTES - 1), 'mesh/source');
|
|
20
|
+
expect(result).toMatchObject({
|
|
21
|
+
ok: false,
|
|
22
|
+
error: {
|
|
23
|
+
code: 'mesh-bin-header-truncated',
|
|
24
|
+
subject: 'mesh-bin',
|
|
25
|
+
sourceKey: 'mesh/source',
|
|
26
|
+
expected: expect.any(String),
|
|
27
|
+
actual: expect.any(String),
|
|
28
|
+
recovery: expect.stringContaining('re-cook'),
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { validateMeta } from '../schema-compiled.js';
|
|
3
|
+
import { SCRIPTABLE_PACK_ASSET_KINDS } from '../scriptable-pack.js';
|
|
4
|
+
|
|
5
|
+
describe('meta.schema.json source optional (w8 / AC-4)', () => {
|
|
6
|
+
const minimalMeta = {
|
|
7
|
+
schemaVersion: '1.0.0',
|
|
8
|
+
kind: 'external-asset-package',
|
|
9
|
+
importer: 'image',
|
|
10
|
+
importSettings: {},
|
|
11
|
+
subAssets: [{ guid: '00000000-0000-4000-8000-000000000001', sourceIndex: 0, kind: 'texture' }],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
it('meta without source key passes ajv validation (AC-4)', () => {
|
|
15
|
+
const valid = validateMeta(minimalMeta);
|
|
16
|
+
expect(valid).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('meta with explicit source passes ajv validation (AC-1 back-compat)', () => {
|
|
20
|
+
const metaWithSource = { ...minimalMeta, source: 'foo.png' };
|
|
21
|
+
const valid = validateMeta(metaWithSource);
|
|
22
|
+
expect(valid).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('meta missing required field importer fails validation', () => {
|
|
26
|
+
const { importer: _, ...withoutImporter } = minimalMeta;
|
|
27
|
+
const valid = validateMeta(withoutImporter);
|
|
28
|
+
expect(valid).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('ajv reinstantiation idempotent — same validator returns same result', () => {
|
|
32
|
+
const valid1 = validateMeta(minimalMeta);
|
|
33
|
+
const valid2 = validateMeta(minimalMeta);
|
|
34
|
+
expect(valid1).toBe(valid2);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('meta.schema.json subAssets[].kind open-string (w6 / D-1 scheme A)', () => {
|
|
39
|
+
const metaWithSubKind = (kind: string) => ({
|
|
40
|
+
schemaVersion: '1.0.0',
|
|
41
|
+
kind: 'external-asset-package',
|
|
42
|
+
importer: 'image',
|
|
43
|
+
importSettings: {},
|
|
44
|
+
subAssets: [{ guid: '00000000-0000-4000-8000-000000000001', sourceIndex: 0, kind }],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// (a) Host custom kind passes validation — RED against current closed enum,
|
|
48
|
+
// GREEN after schema enum is dropped to open string with minLength: 1.
|
|
49
|
+
// When this test is red it proves the enum wall exists; when it turns
|
|
50
|
+
// green it proves D-1 scheme A is effective.
|
|
51
|
+
it('(a) host custom kind passes validation (red→green)', () => {
|
|
52
|
+
const valid = validateMeta(metaWithSubKind('reel-game-blob'));
|
|
53
|
+
expect(valid).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// (b) Zero-regression: all 12 known kind values from the prior closed enum
|
|
57
|
+
// still pass validation. 'image' is included here because the schema
|
|
58
|
+
// layer does NOT decommission it — the fold layer (M4) handles that.
|
|
59
|
+
const knownKinds = [
|
|
60
|
+
'mesh',
|
|
61
|
+
'material',
|
|
62
|
+
'scene',
|
|
63
|
+
'texture',
|
|
64
|
+
'image',
|
|
65
|
+
'cube-texture',
|
|
66
|
+
'material-shader',
|
|
67
|
+
'skeleton',
|
|
68
|
+
'skin',
|
|
69
|
+
'animation-clip',
|
|
70
|
+
'audio',
|
|
71
|
+
'font',
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
for (const kind of knownKinds) {
|
|
75
|
+
it(`(b) known kind '${kind}' passes validation (zero-regression)`, () => {
|
|
76
|
+
const valid = validateMeta(metaWithSubKind(kind));
|
|
77
|
+
expect(valid).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const kind of SCRIPTABLE_PACK_ASSET_KINDS) {
|
|
82
|
+
it(`published durable kind '${kind}' remains valid in open Meta schema`, () => {
|
|
83
|
+
expect(validateMeta(metaWithSubKind(kind))).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// (c) Empty string rejected by minLength: 1 (schema layer gate).
|
|
88
|
+
it('(c) empty-string kind fails validation (minLength: 1)', () => {
|
|
89
|
+
const valid = validateMeta(metaWithSubKind(''));
|
|
90
|
+
expect(valid).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// (d) subAssets[].kind field still exists in the schema and is typed as
|
|
94
|
+
// string (AC-06 + OOS-2: field not deleted, only enum removed).
|
|
95
|
+
it('(d) subAssets[].kind field is string-typed and required', () => {
|
|
96
|
+
// Verify kind field is required — missing it fails.
|
|
97
|
+
const withoutKind = {
|
|
98
|
+
schemaVersion: '1.0.0',
|
|
99
|
+
kind: 'external-asset-package',
|
|
100
|
+
importer: 'image',
|
|
101
|
+
importSettings: {},
|
|
102
|
+
subAssets: [{ guid: '00000000-0000-4000-8000-000000000001', sourceIndex: 0 }],
|
|
103
|
+
};
|
|
104
|
+
expect(validateMeta(withoutKind)).toBe(false);
|
|
105
|
+
|
|
106
|
+
// Verify kind with non-string value (number) fails type check.
|
|
107
|
+
const numberKind = {
|
|
108
|
+
schemaVersion: '1.0.0',
|
|
109
|
+
kind: 'external-asset-package',
|
|
110
|
+
importer: 'image',
|
|
111
|
+
importSettings: {},
|
|
112
|
+
subAssets: [{ guid: '00000000-0000-4000-8000-000000000001', sourceIndex: 0, kind: 42 }],
|
|
113
|
+
};
|
|
114
|
+
expect(validateMeta(numberKind)).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { validateProducerContract } from '../producer-contract.js';
|
|
3
|
+
import { validateMeta } from '../schema-compiled.js';
|
|
4
|
+
|
|
5
|
+
const baseMeta = () => ({
|
|
6
|
+
schemaVersion: '1.0.0',
|
|
7
|
+
kind: 'external-asset-package',
|
|
8
|
+
importer: 'fixture',
|
|
9
|
+
importSettings: {},
|
|
10
|
+
subAssets: [
|
|
11
|
+
{
|
|
12
|
+
guid: '11111111-1111-4111-8111-111111111111',
|
|
13
|
+
sourceIndex: 0,
|
|
14
|
+
sourceKey: 'mesh/main',
|
|
15
|
+
kind: 'mesh',
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('Meta sourceOverrides', () => {
|
|
21
|
+
it('accepts a legal optional override without narrowing its payload', () => {
|
|
22
|
+
const meta = {
|
|
23
|
+
...baseMeta(),
|
|
24
|
+
sourceOverrides: { 'mesh/main': { lod: 2, customProducerField: true } },
|
|
25
|
+
};
|
|
26
|
+
expect(validateMeta(meta)).toBe(true);
|
|
27
|
+
expect(validateProducerContract(meta)).toMatchObject({ ok: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('rejects unknown or duplicate source keys before publication', () => {
|
|
31
|
+
const unknown = {
|
|
32
|
+
...baseMeta(),
|
|
33
|
+
sourceOverrides: { 'mesh/other': { lod: 1 } },
|
|
34
|
+
};
|
|
35
|
+
const unknownResult = validateProducerContract(unknown);
|
|
36
|
+
expect(unknownResult).toMatchObject({
|
|
37
|
+
ok: false,
|
|
38
|
+
error: {
|
|
39
|
+
code: 'unknown-source-key',
|
|
40
|
+
authority: 'pack',
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const duplicate = {
|
|
45
|
+
...baseMeta(),
|
|
46
|
+
subAssets: [
|
|
47
|
+
...baseMeta().subAssets,
|
|
48
|
+
{
|
|
49
|
+
guid: '22222222-2222-4222-8222-222222222222',
|
|
50
|
+
sourceIndex: 1,
|
|
51
|
+
sourceKey: 'mesh/main',
|
|
52
|
+
kind: 'mesh',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
sourceOverrides: { 'mesh/main': { lod: 1 } },
|
|
56
|
+
};
|
|
57
|
+
expect(validateProducerContract(duplicate)).toMatchObject({
|
|
58
|
+
ok: false,
|
|
59
|
+
error: { code: 'duplicate-source-key' },
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('keeps omitted and empty overrides equivalent to legacy Meta', () => {
|
|
64
|
+
const legacy = baseMeta();
|
|
65
|
+
const empty = { ...legacy, sourceOverrides: {} };
|
|
66
|
+
expect(validateMeta(legacy)).toBe(true);
|
|
67
|
+
expect(validateMeta(empty)).toBe(true);
|
|
68
|
+
expect(validateProducerContract(legacy)).toMatchObject({ ok: true });
|
|
69
|
+
expect(validateProducerContract(empty)).toMatchObject({ ok: true });
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { NativeCookerRegistry } from '../native-cooker-registry.js';
|
|
3
|
+
|
|
4
|
+
describe('NativeCookerRegistry artifact boundary', () => {
|
|
5
|
+
it('keeps native artifact bytes available before converting to CookProduct descriptors', async () => {
|
|
6
|
+
const bytes = new TextEncoder().encode('{"program":true}');
|
|
7
|
+
const registry = new NativeCookerRegistry();
|
|
8
|
+
registry.register({
|
|
9
|
+
key: 'test-effect',
|
|
10
|
+
cook: () => ({
|
|
11
|
+
guid: '019e9c00-0000-7000-8000-000000000000',
|
|
12
|
+
payload: { kind: 'test-effect' },
|
|
13
|
+
refs: ['019e9c00-0000-7000-8000-000000000001'],
|
|
14
|
+
artifacts: {
|
|
15
|
+
'test-effect/program.json': { mediaType: 'application/json', bytes },
|
|
16
|
+
},
|
|
17
|
+
inputFingerprint: 'sha256:test-input',
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const draft = await registry.runDraft('test-effect', {});
|
|
22
|
+
expect(draft.ok).toBe(true);
|
|
23
|
+
if (!draft.ok) throw new Error(draft.error.hint);
|
|
24
|
+
expect(draft.value.artifacts['test-effect/program.json']?.bytes).toEqual(bytes);
|
|
25
|
+
|
|
26
|
+
const product = await registry.run('test-effect', {});
|
|
27
|
+
expect(product.ok).toBe(true);
|
|
28
|
+
if (!product.ok) throw new Error(product.error.hint);
|
|
29
|
+
expect(product.value.artifacts['test-effect/program.json']).toMatchObject({
|
|
30
|
+
path: 'test-effect/program.json',
|
|
31
|
+
mediaType: 'application/json',
|
|
32
|
+
byteLength: bytes.byteLength,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('discovers a typed producer input before cooking and preserves the typed output contract', async () => {
|
|
37
|
+
const registry = new NativeCookerRegistry();
|
|
38
|
+
registry.register({
|
|
39
|
+
key: 'typed-effect',
|
|
40
|
+
discover: (input) => {
|
|
41
|
+
if (typeof input !== 'object' || input === null || !('source' in input)) {
|
|
42
|
+
throw new Error('source is required');
|
|
43
|
+
}
|
|
44
|
+
return { source: String(input.source), revision: 3 };
|
|
45
|
+
},
|
|
46
|
+
cook: (input: { readonly source: string; readonly revision: number }) => ({
|
|
47
|
+
guid: '019e9c00-0000-7000-8000-000000000010',
|
|
48
|
+
payload: { source: input.source, revision: input.revision },
|
|
49
|
+
refs: [],
|
|
50
|
+
artifacts: {
|
|
51
|
+
'typed-effect/program.json': {
|
|
52
|
+
mediaType: 'application/json',
|
|
53
|
+
bytes: new TextEncoder().encode(JSON.stringify(input)),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
inputFingerprint: `sha256:${input.source}`,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const result = await registry.runDraft('typed-effect', { source: 'module.wgsl' });
|
|
61
|
+
expect(result.ok).toBe(true);
|
|
62
|
+
if (result.ok) expect(result.value.payload).toEqual({ source: 'module.wgsl', revision: 3 });
|
|
63
|
+
});
|
|
64
|
+
});
|