-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathIOSCompiler.characterization.test.ts
More file actions
305 lines (277 loc) · 10.5 KB
/
Copy pathIOSCompiler.characterization.test.ts
File metadata and controls
305 lines (277 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
/**
* IOSCompiler — Characterization Tests (W4-T3 pre-split lock)
*
* **Purpose**: Lock current iOS codegen output hashes so the Wave-1
* split (W1-T1: split 6,031 LOC into IOSCodegen / IOSManifest /
* IOSResource / IOSBuildOrchestrator) can ship safely. Any behavior
* change during the split breaks a hash here, not just a behavior
* assertion.
*
* **Discipline**: lock tests, not behavior tests. Failing hash =
* either (a) regression to fix, or (b) intentional change to re-lock
* in the same commit, explicitly called out in the commit message.
*
* **Scope**: covers the four split concerns —
* - Codegen (viewFile / sceneFile / stateFile)
* - Manifest (infoPlist)
* - Resource (per-trait files: roomPlanFile, lidarScannerFile,
* npuSceneFile, handTrackingFile, portalARFile, faceTrackingFile,
* objectCaptureFile, sharePlayFile, uwbPositioningFile,
* spatialAudioFile)
* - Build orchestrator (composite end-to-end)
*
* Each trait family has its own dedicated lock so the split can
* isolate regressions to a specific emission path.
*
* **See**: ai-ecosystem research/2026-04-21_audit-mode-backlog.md §W4-T3 / W1-T1
* packages/core/src/compiler/IOSCompiler.ts (6,031 LOC target)
* packages/core/src/compiler/IOSCompiler.test.ts (existing behavior tests)
* packages/core/src/HoloScriptRuntime.characterization.test.ts (sister lock file)
* packages/core/src/parser/HoloCompositionParser.characterization.test.ts (sister lock file)
*/
import { createHash } from 'crypto';
import { describe, it, expect, beforeEach } from 'vitest';
import { IOSCompiler } from './IOSCompiler';
import type { HoloComposition, HoloObjectDecl } from '../parser/HoloCompositionTypes';
// Mirror the existing IOSCompiler.test.ts pattern — instantiate directly
// and call compile() without an agentToken. The CompilerBase RBAC
// validator treats an absent token as a dev-mode bypass; the literal
// 'test-token' string is treated as a real token and fails verification
// (compileToIOS helper falls into that second path and errors).
let compiler: IOSCompiler;
beforeEach(() => {
compiler = new IOSCompiler();
});
function compile(composition: HoloComposition) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (compiler as any).compile(composition);
}
// Fields stripped as non-deterministic. iOS codegen is expected to
// be deterministic given a fixed composition; this defends against
// future drift (e.g., if codegen adds a generation-timestamp comment).
const NONDET_KEYS = new Set<string>([
'timestamp',
'runId',
'created',
'createdAt',
'modifiedAt',
'updatedAt',
'_generated_at',
'generatedAt',
'compiledAt',
'buildTimeMs',
]);
// Regex for embedded timestamp/date strings in generated source.
// Matches ISO-8601 dates and Unix ms timestamps > 2020. Replaces with
// stable sentinel so codegen timestamp comments don't break the lock.
const EMBEDDED_TIMESTAMP_RE = /(\d{13,})|(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)/g;
function canonicalizeString(s: string): string {
// Replace embedded timestamps with sentinel before hashing
return s.replace(EMBEDDED_TIMESTAMP_RE, '<TIMESTAMP>');
}
function hashResult(result: unknown): string {
const stripped = stripNondeterministic(result);
return createHash('sha256').update(stableStringify(stripped)).digest('hex').slice(0, 16);
}
function stripNondeterministic(v: unknown): unknown {
if (v === null || typeof v !== 'object') {
if (typeof v === 'string') return canonicalizeString(v);
return v;
}
if (Array.isArray(v)) return v.map(stripNondeterministic);
const out: Record<string, unknown> = {};
for (const k of Object.keys(v as object)) {
if (NONDET_KEYS.has(k)) continue;
out[k] = stripNondeterministic((v as Record<string, unknown>)[k]);
}
return out;
}
function stableStringify(v: unknown): string {
if (v === null || typeof v !== 'object') return JSON.stringify(v);
if (Array.isArray(v)) return '[' + v.map(stableStringify).join(',') + ']';
const keys = Object.keys(v as object).sort();
return (
'{' +
keys
.map((k) => JSON.stringify(k) + ':' + stableStringify((v as Record<string, unknown>)[k]))
.join(',') +
'}'
);
}
// ── Composition builders (mirror existing IOSCompiler.test.ts helpers) ────
function createComposition(overrides: Partial<HoloComposition> = {}): HoloComposition {
return {
type: 'Composition',
name: 'CharARScene',
objects: [],
templates: [],
spatialGroups: [],
lights: [],
imports: [],
timelines: [],
audio: [],
zones: [],
transitions: [],
conditionals: [],
iterators: [],
npcs: [],
quests: [],
abilities: [],
dialogues: [],
stateMachines: [],
achievements: [],
talentTrees: [],
shapes: [],
...overrides,
};
}
function createObject(name: string, overrides: Partial<HoloObjectDecl> = {}): HoloObjectDecl {
return {
name,
properties: [],
traits: [],
...overrides,
} as HoloObjectDecl;
}
function objectWithTrait(name: string, traitName: string): HoloObjectDecl {
return createObject(name, {
traits: [{ name: traitName, config: [] }],
} as unknown as Partial<HoloObjectDecl>);
}
describe('IOSCompiler characterization (W4-T3 pre-split lock for W1-T1)', () => {
describe('Codegen concern (view / scene / state files)', () => {
it('[I1] empty composition locks baseline codegen', () => {
const result = compile(createComposition());
expect(hashResult(result)).toMatchSnapshot('I1-empty');
});
it('[I2] composition with single object locks codegen', () => {
const result = compile(createComposition({ objects: [createObject('Cube')] }));
expect(hashResult(result)).toMatchSnapshot('I2-singleObject');
});
it('[I3] composition with multiple objects locks codegen', () => {
const result = compile(
createComposition({
objects: [createObject('Cube'), createObject('Sphere'), createObject('Plane')],
})
);
expect(hashResult(result)).toMatchSnapshot('I3-multiObject');
});
});
describe('Manifest concern (infoPlist)', () => {
it('[I4] minimal infoPlist locks manifest', () => {
const result = compile(createComposition());
// Focus the lock specifically on the manifest field
expect(hashResult({ infoPlist: result.infoPlist })).toMatchSnapshot('I4-infoPlistMinimal');
});
it('[I5] infoPlist with scene-tagged composition locks manifest', () => {
const result = compile(
createComposition({
name: 'WorldScene',
objects: [createObject('Anchor')],
})
);
expect(hashResult({ infoPlist: result.infoPlist })).toMatchSnapshot('I5-infoPlistWithScene');
});
});
describe('Resource concern (per-trait file emission)', () => {
it('[I6] lidar_* trait triggers lidarScannerFile — locks output', () => {
const result = compile(
createComposition({
objects: [objectWithTrait('Scanner', 'lidar_capture')],
})
);
expect(hashResult(result)).toMatchSnapshot('I6-lidar');
});
it('[I7] face_* trait triggers faceTrackingFile — locks output', () => {
const result = compile(
createComposition({
objects: [objectWithTrait('Avatar', 'face_tracking')],
})
);
expect(hashResult(result)).toMatchSnapshot('I7-faceTracking');
});
it('[I8] camera_hand_* trait triggers handTrackingFile — locks output', () => {
const result = compile(
createComposition({
objects: [objectWithTrait('Hand', 'camera_hand_tracking')],
})
);
expect(hashResult(result)).toMatchSnapshot('I8-handTracking');
});
it('[I9] roomplan_scan trait triggers roomPlanFile — locks output', () => {
const result = compile(
createComposition({
objects: [objectWithTrait('Room', 'roomplan_scan')],
})
);
expect(hashResult(result)).toMatchSnapshot('I9-roomPlan');
});
it('[I10] portal_* trait triggers portalARFile — locks output', () => {
const result = compile(
createComposition({
objects: [objectWithTrait('Portal', 'portal_ar')],
})
);
expect(hashResult(result)).toMatchSnapshot('I10-portal');
});
it('[I11] spatial_audio trait triggers spatialAudioFile — locks output', () => {
const result = compile(
createComposition({
objects: [objectWithTrait('AudioSource', 'spatial_audio')],
})
);
expect(hashResult(result)).toMatchSnapshot('I11-spatialAudio');
});
it('[I12] object_capture trait triggers objectCaptureFile — locks output', () => {
const result = compile(
createComposition({
objects: [objectWithTrait('Capturable', 'object_capture')],
})
);
expect(hashResult(result)).toMatchSnapshot('I12-objectCapture');
});
it('[I13] shareplay_* trait triggers sharePlayFile — locks output', () => {
const result = compile(
createComposition({
objects: [objectWithTrait('Shared', 'shareplay_session')],
})
);
expect(hashResult(result)).toMatchSnapshot('I13-sharePlay');
});
it('[I14] uwb_* trait triggers uwbPositioningFile — locks output', () => {
const result = compile(
createComposition({
objects: [objectWithTrait('Beacon', 'uwb_positioning')],
})
);
expect(hashResult(result)).toMatchSnapshot('I14-uwb');
});
it('[I15] npu_* trait triggers npuSceneFile — locks output', () => {
const result = compile(
createComposition({
objects: [objectWithTrait('Scene', 'npu_scene_understanding')],
})
);
expect(hashResult(result)).toMatchSnapshot('I15-npu');
});
});
describe('Build-orchestrator concern (composite end-to-end)', () => {
it('[I16] multi-trait composite exercises all concerns + their interactions', () => {
// Stress case: multiple objects, multiple trait families,
// full codegen + manifest + resource emission in one compile.
// The W1-T1 split must preserve this end-to-end.
const result = compile(
createComposition({
name: 'FullScene',
objects: [
objectWithTrait('A', 'lidar_capture'),
objectWithTrait('B', 'face_tracking'),
objectWithTrait('C', 'spatial_audio'),
createObject('PlainObj'),
],
})
);
expect(hashResult(result)).toMatchSnapshot('I16-composite');
});
});
});