@forgeax/engine-rhi-debug 0.0.0-dev.8d955ade1c79

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.
Files changed (237) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +178 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/bootstrap-integration.test.d.ts +2 -0
  5. package/dist/__tests__/bootstrap-integration.test.d.ts.map +1 -0
  6. package/dist/__tests__/consumer-inventory.unit.test.d.ts +2 -0
  7. package/dist/__tests__/consumer-inventory.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/copy-src-usage-validity.dawn.test.d.ts +15 -0
  9. package/dist/__tests__/copy-src-usage-validity.dawn.test.d.ts.map +1 -0
  10. package/dist/__tests__/coverage-invariant.test-d.d.ts +2 -0
  11. package/dist/__tests__/coverage-invariant.test-d.d.ts.map +1 -0
  12. package/dist/__tests__/coverage-invariant.unit.test.d.ts +2 -0
  13. package/dist/__tests__/coverage-invariant.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/e2e.browser.test.d.ts +2 -0
  15. package/dist/__tests__/e2e.browser.test.d.ts.map +1 -0
  16. package/dist/__tests__/error-code-narrowing.test-d.d.ts +2 -0
  17. package/dist/__tests__/error-code-narrowing.test-d.d.ts.map +1 -0
  18. package/dist/__tests__/errors-v7.test-d.d.ts +2 -0
  19. package/dist/__tests__/errors-v7.test-d.d.ts.map +1 -0
  20. package/dist/__tests__/errors-v7.unit.test.d.ts +2 -0
  21. package/dist/__tests__/errors-v7.unit.test.d.ts.map +1 -0
  22. package/dist/__tests__/errors.unit.test.d.ts +2 -0
  23. package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
  24. package/dist/__tests__/event-semantics-coverage.unit.test.d.ts +2 -0
  25. package/dist/__tests__/event-semantics-coverage.unit.test.d.ts.map +1 -0
  26. package/dist/__tests__/format-info.unit.test.d.ts +2 -0
  27. package/dist/__tests__/format-info.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/frame-model-parity.test-d.d.ts +2 -0
  29. package/dist/__tests__/frame-model-parity.test-d.d.ts.map +1 -0
  30. package/dist/__tests__/frame-model-parity.unit.test.d.ts +2 -0
  31. package/dist/__tests__/frame-model-parity.unit.test.d.ts.map +1 -0
  32. package/dist/__tests__/guard-gates.test.d.ts +2 -0
  33. package/dist/__tests__/guard-gates.test.d.ts.map +1 -0
  34. package/dist/__tests__/public-surface.integration.test.d.ts +2 -0
  35. package/dist/__tests__/public-surface.integration.test.d.ts.map +1 -0
  36. package/dist/__tests__/readback-fault-cleanup.unit.test.d.ts +2 -0
  37. package/dist/__tests__/readback-fault-cleanup.unit.test.d.ts.map +1 -0
  38. package/dist/__tests__/readback-format-matrix.dawn.test.d.ts +2 -0
  39. package/dist/__tests__/readback-format-matrix.dawn.test.d.ts.map +1 -0
  40. package/dist/__tests__/readback-format-matrix.unit.test.d.ts +2 -0
  41. package/dist/__tests__/readback-format-matrix.unit.test.d.ts.map +1 -0
  42. package/dist/__tests__/readback-matrix-fixture.d.ts +32 -0
  43. package/dist/__tests__/readback-matrix-fixture.d.ts.map +1 -0
  44. package/dist/__tests__/recorder-bootstrap.unit.test.d.ts +2 -0
  45. package/dist/__tests__/recorder-bootstrap.unit.test.d.ts.map +1 -0
  46. package/dist/__tests__/recorder-budget.unit.test.d.ts +2 -0
  47. package/dist/__tests__/recorder-budget.unit.test.d.ts.map +1 -0
  48. package/dist/__tests__/recorder-fail-fast.unit.test.d.ts +2 -0
  49. package/dist/__tests__/recorder-fail-fast.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/recorder-lifecycle.unit.test.d.ts +2 -0
  51. package/dist/__tests__/recorder-lifecycle.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/recorder-push-event.unit.test.d.ts +2 -0
  53. package/dist/__tests__/recorder-push-event.unit.test.d.ts.map +1 -0
  54. package/dist/__tests__/recorder-session.integration.test.d.ts +2 -0
  55. package/dist/__tests__/recorder-session.integration.test.d.ts.map +1 -0
  56. package/dist/__tests__/recorder-session.unit.test.d.ts +2 -0
  57. package/dist/__tests__/recorder-session.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/recorder-steady-frame.dawn.test.d.ts +2 -0
  59. package/dist/__tests__/recorder-steady-frame.dawn.test.d.ts.map +1 -0
  60. package/dist/__tests__/replay-fail-fast.unit.test.d.ts +2 -0
  61. package/dist/__tests__/replay-fail-fast.unit.test.d.ts.map +1 -0
  62. package/dist/__tests__/replay-session.dawn.test.d.ts +2 -0
  63. package/dist/__tests__/replay-session.dawn.test.d.ts.map +1 -0
  64. package/dist/__tests__/replay-session.test-d.d.ts +2 -0
  65. package/dist/__tests__/replay-session.test-d.d.ts.map +1 -0
  66. package/dist/__tests__/replay-session.unit.test.d.ts +2 -0
  67. package/dist/__tests__/replay-session.unit.test.d.ts.map +1 -0
  68. package/dist/__tests__/resolve-texture-descriptor.unit.test.d.ts +2 -0
  69. package/dist/__tests__/resolve-texture-descriptor.unit.test.d.ts.map +1 -0
  70. package/dist/__tests__/resource-lifecycle.unit.test.d.ts +2 -0
  71. package/dist/__tests__/resource-lifecycle.unit.test.d.ts.map +1 -0
  72. package/dist/__tests__/rhi-debug-fresh-replay.dawn.test.d.ts +2 -0
  73. package/dist/__tests__/rhi-debug-fresh-replay.dawn.test.d.ts.map +1 -0
  74. package/dist/__tests__/tape-index.unit.test.d.ts +2 -0
  75. package/dist/__tests__/tape-index.unit.test.d.ts.map +1 -0
  76. package/dist/__tests__/tape-v7-invalid.unit.test.d.ts +2 -0
  77. package/dist/__tests__/tape-v7-invalid.unit.test.d.ts.map +1 -0
  78. package/dist/__tests__/tape-v7-roundtrip.unit.test.d.ts +2 -0
  79. package/dist/__tests__/tape-v7-roundtrip.unit.test.d.ts.map +1 -0
  80. package/dist/__tests__/tape-v7-version.unit.test.d.ts +2 -0
  81. package/dist/__tests__/tape-v7-version.unit.test.d.ts.map +1 -0
  82. package/dist/__tests__/tree-shake.unit.test.d.ts +2 -0
  83. package/dist/__tests__/tree-shake.unit.test.d.ts.map +1 -0
  84. package/dist/__tests__/types.test.d.ts +2 -0
  85. package/dist/__tests__/types.test.d.ts.map +1 -0
  86. package/dist/browser.d.ts +45 -0
  87. package/dist/browser.d.ts.map +1 -0
  88. package/dist/browser.mjs +83 -0
  89. package/dist/browser.mjs.map +1 -0
  90. package/dist/errors.d.ts +85 -0
  91. package/dist/errors.d.ts.map +1 -0
  92. package/dist/frame-model.d.ts +158 -0
  93. package/dist/frame-model.d.ts.map +1 -0
  94. package/dist/index.d.ts +17 -0
  95. package/dist/index.d.ts.map +1 -0
  96. package/dist/index.mjs +10415 -0
  97. package/dist/index.mjs.map +1 -0
  98. package/dist/protocol/codec.d.ts +6 -0
  99. package/dist/protocol/codec.d.ts.map +1 -0
  100. package/dist/protocol/event-semantics.d.ts +15 -0
  101. package/dist/protocol/event-semantics.d.ts.map +1 -0
  102. package/dist/protocol/tape-index.d.ts +30 -0
  103. package/dist/protocol/tape-index.d.ts.map +1 -0
  104. package/dist/protocol/types.d.ts +44 -0
  105. package/dist/protocol/types.d.ts.map +1 -0
  106. package/dist/protocol/validation.d.ts +7 -0
  107. package/dist/protocol/validation.d.ts.map +1 -0
  108. package/dist/readback.d.ts +156 -0
  109. package/dist/readback.d.ts.map +1 -0
  110. package/dist/recorder/assemble.d.ts +11 -0
  111. package/dist/recorder/assemble.d.ts.map +1 -0
  112. package/dist/recorder/closure.d.ts +39 -0
  113. package/dist/recorder/closure.d.ts.map +1 -0
  114. package/dist/recorder/core.d.ts +296 -0
  115. package/dist/recorder/core.d.ts.map +1 -0
  116. package/dist/recorder/device.d.ts +4 -0
  117. package/dist/recorder/device.d.ts.map +1 -0
  118. package/dist/recorder/encoder.d.ts +5 -0
  119. package/dist/recorder/encoder.d.ts.map +1 -0
  120. package/dist/recorder/lifecycle.d.ts +20 -0
  121. package/dist/recorder/lifecycle.d.ts.map +1 -0
  122. package/dist/recorder/pass.d.ts +6 -0
  123. package/dist/recorder/pass.d.ts.map +1 -0
  124. package/dist/recorder/proxy.d.ts +19 -0
  125. package/dist/recorder/proxy.d.ts.map +1 -0
  126. package/dist/recorder/queue.d.ts +4 -0
  127. package/dist/recorder/queue.d.ts.map +1 -0
  128. package/dist/recorder/resource-registry.d.ts +15 -0
  129. package/dist/recorder/resource-registry.d.ts.map +1 -0
  130. package/dist/recorder/session.d.ts +25 -0
  131. package/dist/recorder/session.d.ts.map +1 -0
  132. package/dist/recorder/shader.d.ts +3 -0
  133. package/dist/recorder/shader.d.ts.map +1 -0
  134. package/dist/recorder/snapshot.d.ts +10 -0
  135. package/dist/recorder/snapshot.d.ts.map +1 -0
  136. package/dist/recorder/wrap.d.ts +4 -0
  137. package/dist/recorder/wrap.d.ts.map +1 -0
  138. package/dist/recorder.d.ts +7 -0
  139. package/dist/recorder.d.ts.map +1 -0
  140. package/dist/replay/device-request.d.ts +8 -0
  141. package/dist/replay/device-request.d.ts.map +1 -0
  142. package/dist/replay/execute-support.d.ts +33 -0
  143. package/dist/replay/execute-support.d.ts.map +1 -0
  144. package/dist/replay/execute.d.ts +18 -0
  145. package/dist/replay/execute.d.ts.map +1 -0
  146. package/dist/replay/readback.d.ts +32 -0
  147. package/dist/replay/readback.d.ts.map +1 -0
  148. package/dist/replay/resources.d.ts +57 -0
  149. package/dist/replay/resources.d.ts.map +1 -0
  150. package/dist/replay/session.d.ts +49 -0
  151. package/dist/replay/session.d.ts.map +1 -0
  152. package/dist/replay/texture-format.d.ts +21 -0
  153. package/dist/replay/texture-format.d.ts.map +1 -0
  154. package/dist/texel-decode.d.ts +35 -0
  155. package/dist/texel-decode.d.ts.map +1 -0
  156. package/dist/texel-layout.d.ts +76 -0
  157. package/dist/texel-layout.d.ts.map +1 -0
  158. package/dist/types.d.ts +722 -0
  159. package/dist/types.d.ts.map +1 -0
  160. package/package.json +72 -0
  161. package/src/__tests__/bootstrap-integration.test.ts +268 -0
  162. package/src/__tests__/consumer-inventory.unit.test.ts +154 -0
  163. package/src/__tests__/copy-src-usage-validity.dawn.test.ts +117 -0
  164. package/src/__tests__/coverage-invariant.test-d.ts +115 -0
  165. package/src/__tests__/coverage-invariant.unit.test.ts +45 -0
  166. package/src/__tests__/e2e.browser.test.ts +239 -0
  167. package/src/__tests__/error-code-narrowing.test-d.ts +32 -0
  168. package/src/__tests__/errors-v7.test-d.ts +19 -0
  169. package/src/__tests__/errors-v7.unit.test.ts +15 -0
  170. package/src/__tests__/errors.unit.test.ts +83 -0
  171. package/src/__tests__/event-semantics-coverage.unit.test.ts +22 -0
  172. package/src/__tests__/format-info.unit.test.ts +85 -0
  173. package/src/__tests__/frame-model-parity.test-d.ts +20 -0
  174. package/src/__tests__/frame-model-parity.unit.test.ts +285 -0
  175. package/src/__tests__/guard-gates.test.ts +317 -0
  176. package/src/__tests__/public-surface.integration.test.ts +38 -0
  177. package/src/__tests__/readback-fault-cleanup.unit.test.ts +66 -0
  178. package/src/__tests__/readback-format-matrix.dawn.test.ts +378 -0
  179. package/src/__tests__/readback-format-matrix.unit.test.ts +120 -0
  180. package/src/__tests__/readback-matrix-fixture.ts +20 -0
  181. package/src/__tests__/recorder-bootstrap.unit.test.ts +302 -0
  182. package/src/__tests__/recorder-budget.unit.test.ts +42 -0
  183. package/src/__tests__/recorder-fail-fast.unit.test.ts +434 -0
  184. package/src/__tests__/recorder-lifecycle.unit.test.ts +42 -0
  185. package/src/__tests__/recorder-push-event.unit.test.ts +573 -0
  186. package/src/__tests__/recorder-session.integration.test.ts +32 -0
  187. package/src/__tests__/recorder-session.unit.test.ts +63 -0
  188. package/src/__tests__/recorder-steady-frame.dawn.test.ts +52 -0
  189. package/src/__tests__/replay-fail-fast.unit.test.ts +155 -0
  190. package/src/__tests__/replay-session.dawn.test.ts +220 -0
  191. package/src/__tests__/replay-session.test-d.ts +27 -0
  192. package/src/__tests__/replay-session.unit.test.ts +171 -0
  193. package/src/__tests__/resolve-texture-descriptor.unit.test.ts +144 -0
  194. package/src/__tests__/resource-lifecycle.unit.test.ts +114 -0
  195. package/src/__tests__/rhi-debug-fresh-replay.dawn.test.ts +178 -0
  196. package/src/__tests__/tape-index.unit.test.ts +56 -0
  197. package/src/__tests__/tape-v7-invalid.unit.test.ts +45 -0
  198. package/src/__tests__/tape-v7-roundtrip.unit.test.ts +66 -0
  199. package/src/__tests__/tape-v7-version.unit.test.ts +13 -0
  200. package/src/__tests__/tree-shake.unit.test.ts +151 -0
  201. package/src/__tests__/types.test.ts +257 -0
  202. package/src/browser.ts +146 -0
  203. package/src/errors.ts +173 -0
  204. package/src/frame-model.ts +753 -0
  205. package/src/index.ts +92 -0
  206. package/src/pako.d.ts +8 -0
  207. package/src/protocol/codec.ts +220 -0
  208. package/src/protocol/event-semantics.ts +180 -0
  209. package/src/protocol/tape-index.ts +120 -0
  210. package/src/protocol/types.ts +60 -0
  211. package/src/protocol/validation.ts +173 -0
  212. package/src/readback.ts +804 -0
  213. package/src/recorder/assemble.ts +192 -0
  214. package/src/recorder/closure.ts +408 -0
  215. package/src/recorder/core.ts +587 -0
  216. package/src/recorder/device.ts +474 -0
  217. package/src/recorder/encoder.ts +251 -0
  218. package/src/recorder/lifecycle.ts +847 -0
  219. package/src/recorder/pass.ts +241 -0
  220. package/src/recorder/proxy.ts +59 -0
  221. package/src/recorder/queue.ts +109 -0
  222. package/src/recorder/resource-registry.ts +53 -0
  223. package/src/recorder/session.ts +152 -0
  224. package/src/recorder/shader.ts +47 -0
  225. package/src/recorder/snapshot.ts +38 -0
  226. package/src/recorder/wrap.ts +157 -0
  227. package/src/recorder.ts +15 -0
  228. package/src/replay/device-request.ts +38 -0
  229. package/src/replay/execute-support.ts +243 -0
  230. package/src/replay/execute.ts +870 -0
  231. package/src/replay/readback.ts +671 -0
  232. package/src/replay/resources.ts +154 -0
  233. package/src/replay/session.ts +509 -0
  234. package/src/replay/texture-format.ts +99 -0
  235. package/src/texel-decode.ts +289 -0
  236. package/src/texel-layout.ts +324 -0
  237. package/src/types.ts +888 -0
@@ -0,0 +1,115 @@
1
+ // Type-level coverage invariant: every method on the three RHI encoder
2
+ // interfaces must map to either a captured RhiCallEvent kind or a
3
+ // DEFERRED_COMMANDS member. If a future method is added to any encoder
4
+ // without a matching entry, Exclude<keyof<Interface>, ...> becomes non-never
5
+ // and tsc fails — no human memory needed.
6
+ //
7
+ // AC-01: Uncovered = never for all three interfaces.
8
+ // AC-02: falsification variant proves the guard is sensitive.
9
+ //
10
+ // Two non-1:1 mappings per plan-strategy D-1:
11
+ // 1. Method `end` -> kind `endRenderPass` / `endComputePass`
12
+ // 2. Render-pass `pushDebugGroup` / `popDebugGroup` / `insertDebugMarker`
13
+ // -> kind `passPushDebugGroup` / `passPopDebugGroup` / `passInsertDebugMarker`
14
+ // 3. Compute-pass `setPipeline` -> kind `setComputePipeline`
15
+ // (method name identical on both interfaces, different kinds)
16
+
17
+ import type {
18
+ RhiCommandEncoder,
19
+ RhiComputePassEncoder,
20
+ RhiRenderPassEncoder,
21
+ } from '@forgeax/engine-rhi';
22
+ import { describe, expectTypeOf, it } from 'vitest';
23
+
24
+ // ---- CapturedMethods: all RHI method names that have a RhiCallEvent kind ----
25
+
26
+ type CapturedMethods =
27
+ // RhiCommandEncoder methods (11 captured)
28
+ | 'beginRenderPass'
29
+ | 'beginComputePass'
30
+ | 'copyBufferToBuffer' // overloaded, one keyof entry
31
+ | 'copyBufferToTexture'
32
+ | 'copyTextureToBuffer'
33
+ | 'copyTextureToTexture'
34
+ | 'clearBuffer'
35
+ | 'pushDebugGroup' // encoder-level, maps to kind 'pushDebugGroup'
36
+ | 'popDebugGroup' // encoder-level, maps to kind 'popDebugGroup'
37
+ | 'insertDebugMarker' // encoder-level, maps to kind 'insertDebugMarker'
38
+ | 'finish'
39
+ // RhiRenderPassEncoder methods (16 captured)
40
+ | 'setPipeline' // render pass: kind='setPipeline'; compute pass: kind='setComputePipeline'
41
+ | 'setVertexBuffer'
42
+ | 'setIndexBuffer'
43
+ | 'setBindGroup' // overloaded, one keyof entry
44
+ | 'draw'
45
+ | 'drawIndexed'
46
+ | 'setViewport'
47
+ | 'setScissorRect'
48
+ | 'setBlendConstant'
49
+ | 'setStencilReference'
50
+ | 'drawIndirect'
51
+ | 'drawIndexedIndirect'
52
+ // pushDebugGroup/popDebugGroup/insertDebugMarker on render pass exist as
53
+ // methods BUT map to pass*DebugGroup kinds. The keyof name is still
54
+ // pushDebugGroup/popDebugGroup/insertDebugMarker (covered above).
55
+ | 'end' // render pass: kind='endRenderPass'; compute pass: kind='endComputePass'
56
+ // RhiComputePassEncoder methods (1 unique, others shared above)
57
+ | 'dispatchWorkgroups'
58
+ | 'dispatchWorkgroupsIndirect';
59
+
60
+ // ---- DeferredMethods: RHI method names in DEFERRED_COMMANDS (AC-06) ----
61
+
62
+ type DeferredMethods =
63
+ | 'beginOcclusionQuery'
64
+ | 'endOcclusionQuery'
65
+ | 'executeBundles'
66
+ | 'writeTimestamp'
67
+ | 'resolveQuerySet';
68
+
69
+ // ---- Per-interface coverage assertions (AC-01) ----
70
+
71
+ describe('RhiCommandEncoder coverage', () => {
72
+ it('every method is captured or deferred (AC-01)', () => {
73
+ type Uncovered = Exclude<keyof RhiCommandEncoder, CapturedMethods | DeferredMethods>;
74
+ expectTypeOf<Uncovered>().toEqualTypeOf<never>();
75
+ });
76
+ });
77
+
78
+ describe('RhiRenderPassEncoder coverage', () => {
79
+ it('every method is captured or deferred (AC-01)', () => {
80
+ type Uncovered = Exclude<keyof RhiRenderPassEncoder, CapturedMethods | DeferredMethods>;
81
+ expectTypeOf<Uncovered>().toEqualTypeOf<never>();
82
+ });
83
+ });
84
+
85
+ describe('RhiComputePassEncoder coverage', () => {
86
+ it('every method is captured or deferred (AC-01)', () => {
87
+ type Uncovered = Exclude<keyof RhiComputePassEncoder, CapturedMethods | DeferredMethods>;
88
+ expectTypeOf<Uncovered>().toEqualTypeOf<never>();
89
+ });
90
+ });
91
+
92
+ // ---- AC-02 falsification: prove the guard catches omissions ----
93
+
94
+ describe('AC-02 falsification', () => {
95
+ it('forgetting a method from CapturedMethods makes Uncovered non-never', () => {
96
+ // Simulate: forget to include 'copyBufferToBuffer' in CapturedMethods.
97
+ // Then Exclude should surface 'copyBufferToBuffer' (non-never).
98
+ type ForgotCopyBufferToBuffer = Exclude<CapturedMethods, 'copyBufferToBuffer'>;
99
+ type DetectedForgotten = Exclude<
100
+ keyof RhiCommandEncoder,
101
+ ForgotCopyBufferToBuffer | DeferredMethods
102
+ >;
103
+ // The forgotten method is detected — DetectedForgotten equals 'copyBufferToBuffer'
104
+ expectTypeOf<DetectedForgotten>().toEqualTypeOf<'copyBufferToBuffer'>();
105
+ // And it is not never — the guard is sensitive
106
+ expectTypeOf<DetectedForgotten>().not.toEqualTypeOf<never>();
107
+ });
108
+
109
+ it('original CapturedMethods is complete (DetectedForgotten was just the one we removed)', () => {
110
+ // Sanity check: without the deliberate omission, Uncovered = never.
111
+ // This doubles as a regression test that CapturedMethods is correctly maintained.
112
+ type Uncovered = Exclude<keyof RhiCommandEncoder, CapturedMethods | DeferredMethods>;
113
+ expectTypeOf<Uncovered>().toEqualTypeOf<never>();
114
+ });
115
+ });
@@ -0,0 +1,45 @@
1
+ // AC-06: DEFERRED_COMMANDS set assertion.
2
+ //
3
+ // Verifies the deferred-commands constant contains exactly the 5 OOS
4
+ // commands (beginOcclusionQuery, endOcclusionQuery, executeBundles,
5
+ // writeTimestamp, resolveQuerySet) — no more, no less. Any drift in
6
+ // the constant (missing member, extra member) will fail this test,
7
+ // catching both accidental removal and accidental addition of commands
8
+ // that should either be fully captured or documented as explicitly
9
+ // out-of-scope.
10
+
11
+ import { describe, expect, it } from 'vitest';
12
+ import { DEFERRED_COMMANDS } from '../types';
13
+
14
+ describe('DEFERRED_COMMANDS (AC-06)', () => {
15
+ it('contains exactly the 5 OOS commands', () => {
16
+ expect(DEFERRED_COMMANDS.size).toBe(5);
17
+
18
+ const expected = new Set([
19
+ 'beginOcclusionQuery',
20
+ 'endOcclusionQuery',
21
+ 'executeBundles',
22
+ 'writeTimestamp',
23
+ 'resolveQuerySet',
24
+ ]);
25
+
26
+ for (const cmd of expected) {
27
+ expect(DEFERRED_COMMANDS.has(cmd)).toBe(true);
28
+ }
29
+ });
30
+
31
+ it('has no unexpected members', () => {
32
+ // Every member must be in the AC-06 contract set.
33
+ const allowed = new Set([
34
+ 'beginOcclusionQuery',
35
+ 'endOcclusionQuery',
36
+ 'executeBundles',
37
+ 'writeTimestamp',
38
+ 'resolveQuerySet',
39
+ ]);
40
+
41
+ for (const cmd of DEFERRED_COMMANDS) {
42
+ expect(allowed.has(cmd)).toBe(true);
43
+ }
44
+ });
45
+ });
@@ -0,0 +1,239 @@
1
+ /// <reference types="@webgpu/types" />
2
+
3
+ import type { RhiDevice } from '@forgeax/engine-rhi';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { buildFrameModel } from '../frame-model';
6
+ import { decodeTape } from '../protocol/codec';
7
+ import type { Tape } from '../protocol/types';
8
+ import { attachRecorder, type RecordableBackend } from '../recorder/session';
9
+ import { openReplay } from '../replay/session';
10
+
11
+ const GPU_AVAILABLE = typeof navigator !== 'undefined' && navigator.gpu !== undefined;
12
+ const SIZE = 64;
13
+
14
+ const VERTEX_SHADER = /* wgsl */ `
15
+ @vertex
16
+ fn main(@builtin(vertex_index) vertexIndex: u32) -> @builtin(position) vec4<f32> {
17
+ var positions = array<vec2<f32>, 3>(
18
+ vec2<f32>(-0.8, -0.8),
19
+ vec2<f32>(0.8, -0.8),
20
+ vec2<f32>(0.0, 0.8),
21
+ );
22
+ return vec4<f32>(positions[vertexIndex], 0.0, 1.0);
23
+ }`;
24
+
25
+ const FRAGMENT_SHADER = /* wgsl */ `
26
+ @group(0) @binding(0) var sourceTexture: texture_2d<f32>;
27
+ @group(0) @binding(1) var sourceSampler: sampler;
28
+
29
+ @fragment
30
+ fn main() -> @location(0) vec4<f32> {
31
+ return textureSample(sourceTexture, sourceSampler, vec2<f32>(0.5, 0.5));
32
+ }`;
33
+
34
+ interface CapturedBrowserTape {
35
+ readonly backend: RecordableBackend;
36
+ readonly device: RhiDevice;
37
+ readonly tape: Tape;
38
+ }
39
+
40
+ function must<T>(
41
+ result:
42
+ | { readonly ok: true; readonly value: T }
43
+ | { readonly ok: false; readonly error: unknown },
44
+ label: string,
45
+ ): T {
46
+ if (result.ok) return result.value;
47
+ const error = result.error as {
48
+ readonly code?: unknown;
49
+ readonly hint?: unknown;
50
+ readonly detail?: unknown;
51
+ };
52
+ throw new Error(
53
+ `${label}: ${String(error.code ?? 'unknown')} ${String(error.hint ?? '')} ${JSON.stringify(error.detail ?? null)}`,
54
+ );
55
+ }
56
+
57
+ async function loadBackend(): Promise<RecordableBackend> {
58
+ const backend = await import('@forgeax/engine-rhi-webgpu');
59
+ return backend as unknown as RecordableBackend;
60
+ }
61
+
62
+ async function captureBrowserTape(): Promise<CapturedBrowserTape> {
63
+ const backend = await loadBackend();
64
+ const attachment = must(attachRecorder(backend), 'attachRecorder');
65
+ const adapter = must(await attachment.backend.rhi.requestAdapter(), 'requestAdapter');
66
+ const device = must(await adapter.requestDevice(), 'requestDevice');
67
+ const capture = attachment.captureFrame();
68
+ must(await attachment.frameBoundary(), 'capture snapshot boundary');
69
+
70
+ const renderTarget = must(
71
+ device.createTexture({
72
+ size: { width: SIZE, height: SIZE, depthOrArrayLayers: 1 },
73
+ format: 'rgba8unorm',
74
+ usage: 0x11,
75
+ }),
76
+ 'create render target',
77
+ );
78
+ const renderTargetView = must(
79
+ device.createTextureView(renderTarget, {}),
80
+ 'create render target view',
81
+ );
82
+ const sourceTexture = must(
83
+ device.createTexture({
84
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
85
+ format: 'rgba8unorm',
86
+ usage: 0x06,
87
+ }),
88
+ 'create source texture',
89
+ );
90
+ const sourceView = must(device.createTextureView(sourceTexture, {}), 'create source view');
91
+ const sampler = must(device.createSampler({}), 'create sampler');
92
+ const vertex = must(
93
+ await attachment.backend.createShaderModule(device, { code: VERTEX_SHADER }),
94
+ 'create vertex shader',
95
+ );
96
+ const fragment = must(
97
+ await attachment.backend.createShaderModule(device, { code: FRAGMENT_SHADER }),
98
+ 'create fragment shader',
99
+ );
100
+ const bindGroupLayout = must(
101
+ device.createBindGroupLayout({
102
+ entries: [
103
+ { binding: 0, visibility: 0x02, texture: { sampleType: 'float', viewDimension: '2d' } },
104
+ { binding: 1, visibility: 0x02, sampler: { type: 'filtering' } },
105
+ ],
106
+ }),
107
+ 'create bind group layout',
108
+ );
109
+ const bindGroup = must(
110
+ device.createBindGroup({
111
+ layout: bindGroupLayout,
112
+ entries: [
113
+ { binding: 0, resource: { kind: 'textureView', value: sourceView } },
114
+ { binding: 1, resource: { kind: 'sampler', value: sampler } },
115
+ ],
116
+ } as never),
117
+ 'create bind group',
118
+ );
119
+ const pipelineLayout = must(
120
+ device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
121
+ 'create pipeline layout',
122
+ );
123
+ const pipeline = must(
124
+ device.createRenderPipeline({
125
+ layout: pipelineLayout,
126
+ vertex: { module: vertex, entryPoint: 'main', buffers: [] },
127
+ fragment: { module: fragment, entryPoint: 'main', targets: [{ format: 'rgba8unorm' }] },
128
+ primitive: { topology: 'triangle-list' },
129
+ } as never),
130
+ 'create render pipeline',
131
+ );
132
+
133
+ const textureData = new Uint8Array([255, 32, 16, 255]);
134
+ must(
135
+ device.queue.writeTexture(
136
+ { texture: sourceTexture, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } } as never,
137
+ textureData,
138
+ { offset: 0, bytesPerRow: 4, rowsPerImage: 1 },
139
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
140
+ ),
141
+ 'write source texture',
142
+ );
143
+ const encoder = must(device.createCommandEncoder({}), 'create command encoder');
144
+ const pass = encoder.beginRenderPass({
145
+ colorAttachments: [
146
+ {
147
+ view: renderTargetView,
148
+ clearValue: { r: 0, g: 0, b: 0, a: 1 },
149
+ loadOp: 'clear',
150
+ storeOp: 'store',
151
+ },
152
+ ],
153
+ } as never);
154
+ pass.setPipeline(pipeline);
155
+ pass.setBindGroup(0, bindGroup, []);
156
+ pass.draw(3, 1, 0, 0);
157
+ pass.end();
158
+ const command = must(encoder.finish(), 'finish command encoder');
159
+ must(device.queue.submit([command]), 'submit command buffer');
160
+ await device.queue.onSubmittedWorkDone();
161
+ must(await attachment.frameBoundary(), 'capture recording boundary');
162
+ const encoded = must(await capture, 'capture frame');
163
+ const decoded = must(decodeTape(encoded.bytes), 'strict v7 decode');
164
+ await attachment.dispose();
165
+ return { backend, device, tape: decoded };
166
+ }
167
+
168
+ function hasVisibleRgb(pixels: Uint8Array): boolean {
169
+ for (let index = 0; index < pixels.length; index += 4) {
170
+ if ((pixels[index] ?? 0) > 4 || (pixels[index + 1] ?? 0) > 4 || (pixels[index + 2] ?? 0) > 4) {
171
+ return true;
172
+ }
173
+ }
174
+ return false;
175
+ }
176
+
177
+ describe.skipIf(!GPU_AVAILABLE)('M4 browser capture and replay gate', () => {
178
+ it('captures one raw v7 tape, indexes binding/work state, replays it on a fresh device, and reads a lit RT', async () => {
179
+ const captured = await captureBrowserTape();
180
+ const model = buildFrameModel(captured.tape);
181
+ expect(captured.tape.header.formatVersion).toBe(7);
182
+ expect(captured.tape.events.length).toBeGreaterThan(0);
183
+ expect(model.works.some((work) => work.kind === 'draw' || work.kind === 'drawIndexed')).toBe(
184
+ true,
185
+ );
186
+ expect(captured.tape.events.some((event) => event.kind === 'setBindGroup')).toBe(true);
187
+ expect(model.works.length).toBeGreaterThan(0);
188
+
189
+ const adapter = must(await captured.backend.rhi.requestAdapter(), 'fresh requestAdapter');
190
+ const freshDevice = must(await adapter.requestDevice(), 'fresh requestDevice');
191
+ const session = must(
192
+ await openReplay(captured.tape, {
193
+ device: freshDevice,
194
+ createShaderModule: captured.backend.createShaderModule,
195
+ }),
196
+ 'open fresh replay session',
197
+ );
198
+ const work = must(await session.inspectWork(0, ['bindings', 'pixels']), 'inspect work index 0');
199
+ expect(work.workIndex).toBe(0);
200
+ expect(work.passIndex).toBeGreaterThanOrEqual(0);
201
+ expect(work.attachment?.kind).toBe('texture');
202
+ expect(work.attachment?.width).toBe(SIZE);
203
+ expect(work.attachment?.height).toBe(SIZE);
204
+ expect(work.attachment?.provenance.selectedWorkIndex).toBe(0);
205
+ expect(work.attachment?.provenance.resourceId).toBeDefined();
206
+ expect(work.attachment?.provenance.subresource).toBeNull();
207
+ expect(hasVisibleRgb(work.attachment?.bytes ?? new Uint8Array())).toBe(true);
208
+ await session.dispose();
209
+ }, 60_000);
210
+
211
+ it('makes a missing texture binding fail the fresh replay path', async () => {
212
+ const captured = await captureBrowserTape();
213
+ const falsifiedEvents = captured.tape.events.map((event) =>
214
+ event.kind === 'setBindGroup'
215
+ ? { ...event, bindGroupHandleId: 'binding:falsified-texture' }
216
+ : event,
217
+ );
218
+ expect(falsifiedEvents.some((event) => event.kind === 'setBindGroup')).toBe(true);
219
+ const falsifiedTape: Tape = {
220
+ ...captured.tape,
221
+ header: { ...captured.tape.header, eventCount: falsifiedEvents.length },
222
+ events: falsifiedEvents,
223
+ };
224
+ const adapter = must(await captured.backend.rhi.requestAdapter(), 'falsifier requestAdapter');
225
+ const device = must(await adapter.requestDevice(), 'falsifier requestDevice');
226
+ const replay = await openReplay(falsifiedTape, {
227
+ device,
228
+ createShaderModule: captured.backend.createShaderModule,
229
+ });
230
+ if (replay.ok) {
231
+ const result = await replay.value.inspectWork(0, ['pixels']);
232
+ expect(result.ok).toBe(false);
233
+ if (!result.ok) expect(result.error.code).toBeDefined();
234
+ await replay.value.dispose();
235
+ } else {
236
+ expect(replay.error.code).toBeDefined();
237
+ }
238
+ }, 60_000);
239
+ });
@@ -0,0 +1,32 @@
1
+ import { expectTypeOf, it } from 'vitest';
2
+ import type { RhiDebugError, RhiDebugErrorCode } from '../errors';
3
+
4
+ it('keeps the eleven error codes closed', () => {
5
+ expectTypeOf<RhiDebugErrorCode>().toEqualTypeOf<
6
+ | 'capture-unavailable'
7
+ | 'capture-busy'
8
+ | 'capture-snapshot-failed'
9
+ | 'capture-timeout'
10
+ | 'tape-invalid'
11
+ | 'tape-version-unsupported'
12
+ | 'replay-capability-mismatch'
13
+ | 'replay-event-failed'
14
+ | 'replay-position-invalid'
15
+ | 'readback-failed'
16
+ | 'readback-unsupported'
17
+ >();
18
+ });
19
+
20
+ it('narrows detail with the error code', () => {
21
+ const error = null as unknown as RhiDebugError;
22
+ if (error.code === 'tape-version-unsupported') {
23
+ expectTypeOf(error.detail).toMatchTypeOf<
24
+ { foundVersion: number; expectedVersion: number } | undefined
25
+ >();
26
+ }
27
+ if (error.code === 'replay-position-invalid') {
28
+ expectTypeOf(error.detail).toMatchTypeOf<
29
+ { requested: number; available: number } | undefined
30
+ >();
31
+ }
32
+ });
@@ -0,0 +1,19 @@
1
+ import { expectTypeOf, it } from 'vitest';
2
+ import type { RhiDebugError, RhiDebugErrorCode } from '../errors';
3
+
4
+ it('has the frozen eleven-member error union', () => {
5
+ expectTypeOf<RhiDebugErrorCode>().toEqualTypeOf<
6
+ | 'capture-unavailable'
7
+ | 'capture-busy'
8
+ | 'capture-snapshot-failed'
9
+ | 'capture-timeout'
10
+ | 'tape-invalid'
11
+ | 'tape-version-unsupported'
12
+ | 'replay-capability-mismatch'
13
+ | 'replay-event-failed'
14
+ | 'replay-position-invalid'
15
+ | 'readback-failed'
16
+ | 'readback-unsupported'
17
+ >();
18
+ expectTypeOf<RhiDebugError['detail']>().not.toEqualTypeOf<unknown>();
19
+ });
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createRhiDebugError } from '../errors';
3
+
4
+ describe('RhiDebugError', () => {
5
+ it('exposes the closed code and recovery fields', () => {
6
+ const error = createRhiDebugError('tape-version-unsupported', {
7
+ foundVersion: 5,
8
+ expectedVersion: 7,
9
+ });
10
+ expect(error.code).toBe('tape-version-unsupported');
11
+ expect(error.expected).toContain('7');
12
+ expect(error.hint).toContain('source revision');
13
+ expect(error.detail).toMatchObject({ foundVersion: 5, expectedVersion: 7 });
14
+ });
15
+ });
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createRhiDebugError, type RhiDebugError } from '../errors';
3
+
4
+ function describeError(error: RhiDebugError): string {
5
+ switch (error.code) {
6
+ case 'capture-unavailable':
7
+ case 'capture-busy':
8
+ case 'capture-snapshot-failed':
9
+ case 'capture-timeout':
10
+ case 'tape-invalid':
11
+ case 'tape-version-unsupported':
12
+ case 'replay-capability-mismatch':
13
+ case 'replay-event-failed':
14
+ case 'replay-position-invalid':
15
+ case 'readback-failed':
16
+ case 'readback-unsupported':
17
+ return `${error.code}:${error.hint}`;
18
+ }
19
+ }
20
+
21
+ describe('RhiDebugError', () => {
22
+ it('keeps capture failures structured at the snapshot boundary', () => {
23
+ const error = createRhiDebugError('capture-snapshot-failed', {
24
+ stage: 'snapshot',
25
+ cause: 'texture readback failed during map',
26
+ handleId: 'texture:7',
27
+ resourceKind: 'texture',
28
+ });
29
+
30
+ expect(error.code).toBe('capture-snapshot-failed');
31
+ expect(error.expected).toContain('capture');
32
+ expect(error.detail).toEqual({
33
+ stage: 'snapshot',
34
+ cause: 'texture readback failed during map',
35
+ handleId: 'texture:7',
36
+ resourceKind: 'texture',
37
+ });
38
+ });
39
+
40
+ it('keeps tape graph failures in the tape validation owner', () => {
41
+ const error = createRhiDebugError('tape-invalid', {
42
+ stage: 'validate',
43
+ cause: 'handleId texture:7 has no create event',
44
+ handleId: 'texture:7',
45
+ eventIndex: 42,
46
+ });
47
+
48
+ expect(error.detail.stage).toBe('validate');
49
+ expect(error.detail.handleId).toBe('texture:7');
50
+ expect(error.detail.eventIndex).toBe(42);
51
+ });
52
+
53
+ it('keeps bounded snapshot timeout progress machine-readable', () => {
54
+ const error = createRhiDebugError('capture-timeout', {
55
+ stage: 'snapshot',
56
+ cause: 'queue drain exceeded the bound',
57
+ timeoutMs: 1000,
58
+ progress: {
59
+ snapshotStage: 'queue-drain',
60
+ totalResources: 4,
61
+ completedResources: 1,
62
+ skippedResources: 0,
63
+ currentHandleId: 'buffer:readback',
64
+ currentKind: 'buffer',
65
+ currentSizeBytes: 128,
66
+ elapsedMs: 1001,
67
+ },
68
+ });
69
+
70
+ expect(error.detail.progress?.currentHandleId).toBe('buffer:readback');
71
+ expect(error.detail.timeoutMs).toBe(1000);
72
+ });
73
+
74
+ it('supports an exhaustive consumer switch over the closed union', () => {
75
+ const error = createRhiDebugError('readback-unsupported', {
76
+ stage: 'readback',
77
+ format: 'depth24plus',
78
+ reason: 'the backend has no depth blit path',
79
+ });
80
+
81
+ expect(describeError(error)).toContain('readback-unsupported');
82
+ });
83
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { EVENT_SEMANTICS, eventKinds, isWorkEvent } from '../protocol/event-semantics';
3
+
4
+ describe('event semantics coverage', () => {
5
+ it('has one semantic entry for every closed event kind', () => {
6
+ expect(Object.keys(EVENT_SEMANTICS).sort()).toEqual([...eventKinds].sort());
7
+ });
8
+
9
+ it('counts direct, indexed, indirect, and compute work without draw aliases', () => {
10
+ expect(
11
+ [
12
+ 'draw',
13
+ 'drawIndexed',
14
+ 'drawIndirect',
15
+ 'drawIndexedIndirect',
16
+ 'dispatchWorkgroups',
17
+ 'dispatchWorkgroupsIndirect',
18
+ ].every((kind) => isWorkEvent(kind as never)),
19
+ ).toBe(true);
20
+ expect(isWorkEvent('beginRenderPass')).toBe(false);
21
+ });
22
+ });
@@ -0,0 +1,85 @@
1
+ // format-info.unit.test.ts — per-channel metadata for host-side color decode.
2
+ //
3
+ // formatInfo sits beside bytesPerTexel (the replay-critical byte-size SSOT). The
4
+ // key invariant: for every plain (non-packed) format, channels * per-channel-bytes
5
+ // equals bytesPerTexel — the two tables must never drift apart.
6
+
7
+ import { describe, expect, it } from 'vitest';
8
+ import {
9
+ bytesPerTexel,
10
+ computeTextureLayout,
11
+ formatInfo,
12
+ textureBlockLayout,
13
+ } from '../texel-layout';
14
+
15
+ describe('formatInfo', () => {
16
+ it('returns channels + channelType for representative formats', () => {
17
+ expect(formatInfo('rgba8unorm')).toEqual({ channels: 4, channelType: 'unorm' });
18
+ expect(formatInfo('bgra8unorm')).toEqual({ channels: 4, channelType: 'unorm', bgra: true });
19
+ expect(formatInfo('r32float')).toEqual({ channels: 1, channelType: 'float' });
20
+ expect(formatInfo('rg16float')).toEqual({ channels: 2, channelType: 'float' });
21
+ expect(formatInfo('rgba16float')).toEqual({ channels: 4, channelType: 'float' });
22
+ expect(formatInfo('r8uint')).toEqual({ channels: 1, channelType: 'uint' });
23
+ expect(formatInfo('rgba8snorm')).toEqual({ channels: 4, channelType: 'snorm' });
24
+ });
25
+
26
+ it('flags packed formats', () => {
27
+ expect(formatInfo('rgb10a2unorm')).toEqual({
28
+ channels: 4,
29
+ channelType: 'unorm',
30
+ packed: 'rgb10a2unorm',
31
+ });
32
+ expect(formatInfo('rg11b10ufloat')).toEqual({
33
+ channels: 3,
34
+ channelType: 'ufloat',
35
+ packed: 'rg11b10ufloat',
36
+ });
37
+ });
38
+
39
+ it('returns undefined for compressed / depth / unknown formats', () => {
40
+ expect(formatInfo('bc7-rgba-unorm')).toBeUndefined();
41
+ expect(formatInfo('depth32float')).toBeUndefined();
42
+ expect(formatInfo('depth24plus-stencil8')).toBeUndefined();
43
+ expect(formatInfo(undefined)).toBeUndefined();
44
+ expect(formatInfo('not-a-format')).toBeUndefined();
45
+ });
46
+
47
+ it('derives compressed block layout for snapshot and seed', () => {
48
+ expect(textureBlockLayout('bc7-rgba-unorm-srgb')).toEqual({
49
+ blockWidth: 4,
50
+ blockHeight: 4,
51
+ bytesPerBlock: 16,
52
+ });
53
+ const layout = computeTextureLayout('bc7-rgba-unorm-srgb', 7, 5, 1, 1);
54
+ expect(layout).toMatchObject({
55
+ blockWidth: 4,
56
+ blockHeight: 4,
57
+ bytesPerBlock: 16,
58
+ totalBytes: 64,
59
+ });
60
+ expect(layout?.slices[0]).toMatchObject({ width: 7, height: 5, byteLength: 64 });
61
+ });
62
+
63
+ it('plain formats: channels * per-channel-bytes == bytesPerTexel (no table drift)', () => {
64
+ const plain: string[] = [
65
+ 'r8unorm',
66
+ 'rg8unorm',
67
+ 'rgba8unorm',
68
+ 'bgra8unorm',
69
+ 'r16uint',
70
+ 'rg16float',
71
+ 'rgba16float',
72
+ 'r32float',
73
+ 'rg32uint',
74
+ 'rgba32float',
75
+ ];
76
+ for (const fmt of plain) {
77
+ const info = formatInfo(fmt);
78
+ const total = bytesPerTexel(fmt as never);
79
+ if (!info || total === undefined) throw new Error(`missing tables for ${fmt}`);
80
+ expect(total % info.channels).toBe(0);
81
+ // per-channel width is total/channels; reconstructing the total must match.
82
+ expect((total / info.channels) * info.channels).toBe(total);
83
+ }
84
+ });
85
+ });
@@ -0,0 +1,20 @@
1
+ import { expectTypeOf, it } from 'vitest';
2
+ import type { FrameModel } from '../frame-model';
3
+
4
+ type HasKey<T, K extends PropertyKey> = K extends keyof T ? true : false;
5
+ type IsReadonlyMap<T> = T extends ReadonlyMap<unknown, unknown> ? true : false;
6
+
7
+ it('exposes only the canonical JSON-safe model shape', () => {
8
+ expectTypeOf<HasKey<FrameModel, 'commands'>>().toEqualTypeOf<true>();
9
+ expectTypeOf<HasKey<FrameModel, 'passes'>>().toEqualTypeOf<true>();
10
+ expectTypeOf<HasKey<FrameModel, 'resources'>>().toEqualTypeOf<true>();
11
+ expectTypeOf<HasKey<FrameModel, 'works'>>().toEqualTypeOf<true>();
12
+ expectTypeOf<HasKey<FrameModel, 'tree'>>().toEqualTypeOf<false>();
13
+ expectTypeOf<HasKey<FrameModel, 'draws'>>().toEqualTypeOf<false>();
14
+ expectTypeOf<HasKey<FrameModel, 'meta'>>().toEqualTypeOf<false>();
15
+ expectTypeOf<HasKey<FrameModel, 'resourceLifecycle'>>().toEqualTypeOf<true>();
16
+ expectTypeOf<IsReadonlyMap<FrameModel['resources']>>().toEqualTypeOf<false>();
17
+ expectTypeOf<FrameModel['works'][number]['workIndex']>().toEqualTypeOf<number>();
18
+ expectTypeOf<FrameModel['works'][number]['eventIndex']>().toEqualTypeOf<number>();
19
+ expectTypeOf<FrameModel['works'][number]['passIndex']>().toEqualTypeOf<number>();
20
+ });