@viewscript/renderer 0.1.0-202605140721 → 0.1.0-202605141229
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/dist/runtime/__tests__/ffi-dispatcher.test.d.ts +11 -0
- package/dist/runtime/__tests__/ffi-dispatcher.test.js +408 -0
- package/dist/runtime/__tests__/ffi-integration.test.d.ts +9 -0
- package/dist/runtime/__tests__/ffi-integration.test.js +514 -0
- package/dist/runtime/__tests__/wasm-solver-bridge.test.d.ts +9 -0
- package/dist/runtime/__tests__/wasm-solver-bridge.test.js +214 -0
- package/dist/runtime/ffi-dispatcher.d.ts +217 -0
- package/dist/runtime/ffi-dispatcher.js +291 -0
- package/dist/runtime/render-loop.d.ts +46 -1
- package/dist/runtime/render-loop.js +67 -1
- package/dist/runtime/wasm-solver-bridge.d.ts +122 -0
- package/dist/runtime/wasm-solver-bridge.js +168 -0
- package/package.json +1 -1
- package/src/runtime/__tests__/ffi-dispatcher.test.ts +519 -0
- package/src/runtime/__tests__/ffi-integration.test.ts +650 -0
- package/src/runtime/__tests__/wasm-solver-bridge.test.ts +276 -0
- package/src/runtime/ffi-dispatcher.ts +451 -0
- package/src/runtime/render-loop.ts +90 -2
- package/src/runtime/wasm-solver-bridge.ts +268 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for WASM Solver Bridge
|
|
3
|
+
*
|
|
4
|
+
* Validates:
|
|
5
|
+
* 1. QSnapshot construction from mutations + FFI results
|
|
6
|
+
* 2. WASM tick() delegation and result parsing
|
|
7
|
+
* 3. FFI result injection lifecycle
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
11
|
+
import {
|
|
12
|
+
WasmSolverBridge,
|
|
13
|
+
createWasmSolverBridge,
|
|
14
|
+
type WasmEngine,
|
|
15
|
+
type TStateMutation,
|
|
16
|
+
} from '../wasm-solver-bridge.js';
|
|
17
|
+
import type { FfiResult } from '../ffi-dispatcher.js';
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Mock WASM Engine
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
function createMockEngine(): WasmEngine & { lastInput: string | null } {
|
|
24
|
+
return {
|
|
25
|
+
lastInput: null,
|
|
26
|
+
tick(inputJson: string): string {
|
|
27
|
+
this.lastInput = inputJson;
|
|
28
|
+
return JSON.stringify({ pending_ffi_calls: [] });
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createMockEngineWithCalls(calls: unknown[]): WasmEngine {
|
|
34
|
+
return {
|
|
35
|
+
tick(): string {
|
|
36
|
+
return JSON.stringify({ pending_ffi_calls: calls });
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Tests
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
describe('WasmSolverBridge', () => {
|
|
46
|
+
let bridge: WasmSolverBridge;
|
|
47
|
+
let mockEngine: WasmEngine & { lastInput: string | null };
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
mockEngine = createMockEngine();
|
|
51
|
+
bridge = createWasmSolverBridge(mockEngine);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ===========================================================================
|
|
55
|
+
// Basic Evaluation
|
|
56
|
+
// ===========================================================================
|
|
57
|
+
|
|
58
|
+
describe('evaluate', () => {
|
|
59
|
+
it('calls engine.tick() with QSnapshot JSON', () => {
|
|
60
|
+
const mutations: TStateMutation[] = [];
|
|
61
|
+
|
|
62
|
+
bridge.evaluate(mutations);
|
|
63
|
+
|
|
64
|
+
expect(mockEngine.lastInput).not.toBeNull();
|
|
65
|
+
const snapshot = JSON.parse(mockEngine.lastInput!);
|
|
66
|
+
expect(snapshot).toHaveProperty('values');
|
|
67
|
+
expect(snapshot).toHaveProperty('mutations');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns empty bounds and pendingFfiCalls by default', () => {
|
|
71
|
+
const result = bridge.evaluate([]);
|
|
72
|
+
|
|
73
|
+
expect(result.bounds.size).toBe(0);
|
|
74
|
+
expect(result.pendingFfiCalls).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('extracts pendingFfiCalls from tick result', () => {
|
|
78
|
+
const calls = [{ ffi_id: 1, args: [1, 2, 3] }];
|
|
79
|
+
bridge = createWasmSolverBridge(createMockEngineWithCalls(calls));
|
|
80
|
+
|
|
81
|
+
const result = bridge.evaluate([]);
|
|
82
|
+
|
|
83
|
+
expect(result.pendingFfiCalls).toEqual(calls);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ===========================================================================
|
|
88
|
+
// Mutation Conversion
|
|
89
|
+
// ===========================================================================
|
|
90
|
+
|
|
91
|
+
describe('mutation conversion', () => {
|
|
92
|
+
it('converts TStateMutation to QSnapshot values', () => {
|
|
93
|
+
const mutations: TStateMutation[] = [
|
|
94
|
+
{ entityId: 1, state: 'hover', value: 1, timestamp: 100 },
|
|
95
|
+
{ entityId: 2, state: 'scroll_y', value: 0.5, timestamp: 100 },
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
bridge.evaluate(mutations);
|
|
99
|
+
|
|
100
|
+
const snapshot = JSON.parse(mockEngine.lastInput!);
|
|
101
|
+
expect(snapshot.values['1_hover']).toEqual({ type: 'float', value: 1 });
|
|
102
|
+
expect(snapshot.values['2_scroll_y']).toEqual({ type: 'float', value: 0.5 });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('includes mutations array for legacy format', () => {
|
|
106
|
+
const mutations: TStateMutation[] = [
|
|
107
|
+
{ entityId: 1, state: 'pressed', value: 1, timestamp: 100 },
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
bridge.evaluate(mutations);
|
|
111
|
+
|
|
112
|
+
const snapshot = JSON.parse(mockEngine.lastInput!);
|
|
113
|
+
expect(snapshot.mutations).toHaveLength(1);
|
|
114
|
+
expect(snapshot.mutations[0]).toMatchObject({
|
|
115
|
+
entity_id: 1,
|
|
116
|
+
state: 'pressed',
|
|
117
|
+
value: 1,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ===========================================================================
|
|
123
|
+
// FFI Result Injection
|
|
124
|
+
// ===========================================================================
|
|
125
|
+
|
|
126
|
+
describe('injectFfiResults', () => {
|
|
127
|
+
it('includes injected FFI results in QSnapshot values', () => {
|
|
128
|
+
const ffiResults: FfiResult[] = [
|
|
129
|
+
{ name: 'clamped_opacity', value: 0.75, timestamp: 100 },
|
|
130
|
+
{ name: 'distance', value: 42, timestamp: 100 },
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
bridge.injectFfiResults(ffiResults);
|
|
134
|
+
bridge.evaluate([]);
|
|
135
|
+
|
|
136
|
+
const snapshot = JSON.parse(mockEngine.lastInput!);
|
|
137
|
+
expect(snapshot.values['clamped_opacity']).toEqual({ type: 'float', value: 0.75 });
|
|
138
|
+
expect(snapshot.values['distance']).toEqual({ type: 'float', value: 42 });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('clears FFI results after evaluate()', () => {
|
|
142
|
+
bridge.injectFfiResults([{ name: 'test', value: 1, timestamp: 100 }]);
|
|
143
|
+
|
|
144
|
+
// First evaluate includes FFI result
|
|
145
|
+
bridge.evaluate([]);
|
|
146
|
+
let snapshot = JSON.parse(mockEngine.lastInput!);
|
|
147
|
+
expect(snapshot.values['test']).toBeDefined();
|
|
148
|
+
|
|
149
|
+
// Second evaluate should NOT include FFI result
|
|
150
|
+
bridge.evaluate([]);
|
|
151
|
+
snapshot = JSON.parse(mockEngine.lastInput!);
|
|
152
|
+
expect(snapshot.values['test']).toBeUndefined();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('FFI results override mutation values with same name', () => {
|
|
156
|
+
// This tests the merge order: FFI results come first, then mutations
|
|
157
|
+
// If there's a naming collision, the mutation should win
|
|
158
|
+
const ffiResults: FfiResult[] = [
|
|
159
|
+
{ name: '1_hover', value: 0.5, timestamp: 100 },
|
|
160
|
+
];
|
|
161
|
+
const mutations: TStateMutation[] = [
|
|
162
|
+
{ entityId: 1, state: 'hover', value: 1, timestamp: 100 },
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
bridge.injectFfiResults(ffiResults);
|
|
166
|
+
bridge.evaluate(mutations);
|
|
167
|
+
|
|
168
|
+
const snapshot = JSON.parse(mockEngine.lastInput!);
|
|
169
|
+
// Mutation overwrites FFI result (processed later)
|
|
170
|
+
expect(snapshot.values['1_hover'].value).toBe(1);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ===========================================================================
|
|
175
|
+
// Error Handling
|
|
176
|
+
// ===========================================================================
|
|
177
|
+
|
|
178
|
+
describe('error handling', () => {
|
|
179
|
+
it('handles engine.tick() throwing', () => {
|
|
180
|
+
const errorEngine: WasmEngine = {
|
|
181
|
+
tick(): string {
|
|
182
|
+
throw new Error('WASM panic');
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
bridge = createWasmSolverBridge(errorEngine);
|
|
186
|
+
|
|
187
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
188
|
+
|
|
189
|
+
const result = bridge.evaluate([]);
|
|
190
|
+
|
|
191
|
+
expect(result.bounds.size).toBe(0);
|
|
192
|
+
expect(result.pendingFfiCalls).toEqual([]);
|
|
193
|
+
expect(errorSpy).toHaveBeenCalled();
|
|
194
|
+
|
|
195
|
+
errorSpy.mockRestore();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('preserves FFI results when tick() throws for next frame retry', () => {
|
|
199
|
+
let callCount = 0;
|
|
200
|
+
const flakyEngine: WasmEngine & { lastInput: string | null } = {
|
|
201
|
+
lastInput: null,
|
|
202
|
+
tick(inputJson: string): string {
|
|
203
|
+
callCount++;
|
|
204
|
+
if (callCount === 1) {
|
|
205
|
+
throw new Error('Transient WASM error');
|
|
206
|
+
}
|
|
207
|
+
this.lastInput = inputJson;
|
|
208
|
+
return JSON.stringify({ pending_ffi_calls: [] });
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
bridge = createWasmSolverBridge(flakyEngine);
|
|
212
|
+
|
|
213
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
214
|
+
|
|
215
|
+
// Inject FFI results
|
|
216
|
+
bridge.injectFfiResults([{ name: 'important_value', value: 42, timestamp: 100 }]);
|
|
217
|
+
|
|
218
|
+
// First evaluate fails - FFI results should be preserved
|
|
219
|
+
bridge.evaluate([]);
|
|
220
|
+
expect(errorSpy).toHaveBeenCalled();
|
|
221
|
+
|
|
222
|
+
// Second evaluate succeeds - FFI results should still be included
|
|
223
|
+
bridge.evaluate([]);
|
|
224
|
+
const snapshot = JSON.parse(flakyEngine.lastInput!);
|
|
225
|
+
expect(snapshot.values['important_value']).toEqual({ type: 'float', value: 42 });
|
|
226
|
+
|
|
227
|
+
errorSpy.mockRestore();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('handles invalid JSON from tick()', () => {
|
|
231
|
+
const badEngine: WasmEngine = {
|
|
232
|
+
tick(): string {
|
|
233
|
+
return 'not valid json';
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
bridge = createWasmSolverBridge(badEngine);
|
|
237
|
+
|
|
238
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
239
|
+
|
|
240
|
+
const result = bridge.evaluate([]);
|
|
241
|
+
|
|
242
|
+
expect(result.pendingFfiCalls).toEqual([]);
|
|
243
|
+
expect(errorSpy).toHaveBeenCalled();
|
|
244
|
+
|
|
245
|
+
errorSpy.mockRestore();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('handles missing pending_ffi_calls in result', () => {
|
|
249
|
+
const partialEngine: WasmEngine = {
|
|
250
|
+
tick(): string {
|
|
251
|
+
return JSON.stringify({}); // No pending_ffi_calls field
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
bridge = createWasmSolverBridge(partialEngine);
|
|
255
|
+
|
|
256
|
+
const result = bridge.evaluate([]);
|
|
257
|
+
|
|
258
|
+
expect(result.pendingFfiCalls).toEqual([]);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// ===========================================================================
|
|
263
|
+
// Factory Function
|
|
264
|
+
// ===========================================================================
|
|
265
|
+
|
|
266
|
+
describe('createWasmSolverBridge', () => {
|
|
267
|
+
it('creates a new instance', () => {
|
|
268
|
+
const engine = createMockEngine();
|
|
269
|
+
const b1 = createWasmSolverBridge(engine);
|
|
270
|
+
const b2 = createWasmSolverBridge(engine);
|
|
271
|
+
|
|
272
|
+
expect(b1).not.toBe(b2);
|
|
273
|
+
expect(b1).toBeInstanceOf(WasmSolverBridge);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
});
|