@reventlessdev/reventless-gwt 1.0.0-alpha.100

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 (184) hide show
  1. package/CHANGELOG.md +916 -0
  2. package/LICENSE +202 -0
  3. package/bin/reventless-dev.mjs +21 -0
  4. package/package.json +54 -0
  5. package/rescript.json +36 -0
  6. package/src/.gwtignore +10 -0
  7. package/src/Automation_GWT.res +207 -0
  8. package/src/Automation_GWT.res.mjs +160 -0
  9. package/src/Behavior_GWT.res +569 -0
  10. package/src/Behavior_GWT.res.mjs +523 -0
  11. package/src/Bind.res +29 -0
  12. package/src/Bind.res.mjs +22 -0
  13. package/src/BuildClassifier.res +147 -0
  14. package/src/BuildClassifier.res.mjs +136 -0
  15. package/src/Cancellation.res +37 -0
  16. package/src/Cancellation.res.mjs +46 -0
  17. package/src/ChildProcess.res +102 -0
  18. package/src/ChildProcess.res.mjs +107 -0
  19. package/src/Cli.res +905 -0
  20. package/src/Cli.res.mjs +952 -0
  21. package/src/Collector.res +212 -0
  22. package/src/Collector.res.mjs +218 -0
  23. package/src/ComponentMeta.res +77 -0
  24. package/src/ComponentMeta.res.mjs +91 -0
  25. package/src/ComponentScan.res +103 -0
  26. package/src/ComponentScan.res.mjs +105 -0
  27. package/src/Delegate_GWT.res +303 -0
  28. package/src/Delegate_GWT.res.mjs +372 -0
  29. package/src/Diff.res +91 -0
  30. package/src/Diff.res.mjs +98 -0
  31. package/src/Discovery.res +105 -0
  32. package/src/Discovery.res.mjs +99 -0
  33. package/src/DomainDeadCode.res +98 -0
  34. package/src/DomainDeadCode.res.mjs +72 -0
  35. package/src/DomainGraph.res +0 -0
  36. package/src/DomainGraph.res.mjs +0 -0
  37. package/src/EventMapping_GWT.res +29 -0
  38. package/src/EventMapping_GWT.res.mjs +44 -0
  39. package/src/Filter.res +45 -0
  40. package/src/Filter.res.mjs +47 -0
  41. package/src/Flow_GWT.res +664 -0
  42. package/src/Flow_GWT.res.mjs +604 -0
  43. package/src/FormatterHuman.res +109 -0
  44. package/src/FormatterHuman.res.mjs +115 -0
  45. package/src/FormatterJson.res +264 -0
  46. package/src/FormatterJson.res.mjs +322 -0
  47. package/src/FormatterJunit.res +93 -0
  48. package/src/FormatterJunit.res.mjs +90 -0
  49. package/src/FormatterTap.res +100 -0
  50. package/src/FormatterTap.res.mjs +131 -0
  51. package/src/FormatterVsCode.res +352 -0
  52. package/src/FormatterVsCode.res.mjs +546 -0
  53. package/src/Hint.res +119 -0
  54. package/src/Hint.res.mjs +124 -0
  55. package/src/InboundTranslation_GWT.res +83 -0
  56. package/src/InboundTranslation_GWT.res.mjs +94 -0
  57. package/src/JestBind.res +70 -0
  58. package/src/JestBind.res.mjs +63 -0
  59. package/src/Loader.res +24 -0
  60. package/src/Loader.res.mjs +23 -0
  61. package/src/LocalHost.res +119 -0
  62. package/src/LocalHost.res.mjs +99 -0
  63. package/src/Mapping_GWT.res +474 -0
  64. package/src/Mapping_GWT.res.mjs +322 -0
  65. package/src/MismatchRender.res +72 -0
  66. package/src/MismatchRender.res.mjs +127 -0
  67. package/src/MultiSourceProjection_GWT.res +316 -0
  68. package/src/MultiSourceProjection_GWT.res.mjs +354 -0
  69. package/src/OutboundTranslation_GWT.res +238 -0
  70. package/src/OutboundTranslation_GWT.res.mjs +186 -0
  71. package/src/Outcome.res +97 -0
  72. package/src/Outcome.res.mjs +93 -0
  73. package/src/PackageScan.res +80 -0
  74. package/src/PackageScan.res.mjs +81 -0
  75. package/src/PlatformRunner.res +181 -0
  76. package/src/PlatformRunner.res.mjs +176 -0
  77. package/src/PlatformScan.res +142 -0
  78. package/src/PlatformScan.res.mjs +132 -0
  79. package/src/ProcessManager.res +135 -0
  80. package/src/ProcessManager.res.mjs +119 -0
  81. package/src/Projection_GWT.res +312 -0
  82. package/src/Projection_GWT.res.mjs +332 -0
  83. package/src/Query_GWT.res +379 -0
  84. package/src/Query_GWT.res.mjs +367 -0
  85. package/src/RenderRescript.res +125 -0
  86. package/src/RenderRescript.res.mjs +141 -0
  87. package/src/RunWorker.res +24 -0
  88. package/src/RunWorker.res.mjs +16 -0
  89. package/src/RunnerTypes.res +68 -0
  90. package/src/RunnerTypes.res.mjs +60 -0
  91. package/src/SideEffect_GWT.res +157 -0
  92. package/src/SideEffect_GWT.res.mjs +113 -0
  93. package/src/StateChange/ExternalAddCategorySlice.res +25 -0
  94. package/src/StateChange/ExternalAddCategorySlice.res.mjs +34 -0
  95. package/src/StateChange/ExternalAddCategorySlice_Behavior.res +19 -0
  96. package/src/StateChange/ExternalAddCategorySlice_Behavior.res.mjs +46 -0
  97. package/src/StateChange/WithFixtures.res +22 -0
  98. package/src/StateChange/WithFixtures.res.mjs +31 -0
  99. package/src/StateChange/WithFixtures_Behavior.res +17 -0
  100. package/src/StateChange/WithFixtures_Behavior.res.mjs +37 -0
  101. package/src/StateChange/WithManualOpen.res +23 -0
  102. package/src/StateChange/WithManualOpen.res.mjs +31 -0
  103. package/src/StateChange/WithManualOpen_Behavior.res +17 -0
  104. package/src/StateChange/WithManualOpen_Behavior.res.mjs +37 -0
  105. package/src/StubRuntime.res +54 -0
  106. package/src/StubRuntime.res.mjs +55 -0
  107. package/src/TestFixtures.res +16 -0
  108. package/src/TestFixtures.res.mjs +34 -0
  109. package/src/Watch.res +108 -0
  110. package/src/Watch.res.mjs +85 -0
  111. package/src/WatcherProbe.res +56 -0
  112. package/src/WatcherProbe.res.mjs +59 -0
  113. package/src/Worker.res +24 -0
  114. package/src/Worker.res.mjs +9 -0
  115. package/tests/AutomationGwtTest.res +71 -0
  116. package/tests/AutomationGwtTest.res.mjs +189 -0
  117. package/tests/BuildClassifierTest.res +133 -0
  118. package/tests/BuildClassifierTest.res.mjs +176 -0
  119. package/tests/CliRunEntryTest.res +57 -0
  120. package/tests/CliRunEntryTest.res.mjs +65 -0
  121. package/tests/CliWatchScopeTest.res +62 -0
  122. package/tests/CliWatchScopeTest.res.mjs +95 -0
  123. package/tests/ComponentMetaTest.res +67 -0
  124. package/tests/ComponentMetaTest.res.mjs +71 -0
  125. package/tests/DelegateGwtTest.res +487 -0
  126. package/tests/DelegateGwtTest.res.mjs +891 -0
  127. package/tests/DiscoveryTest.res +42 -0
  128. package/tests/DiscoveryTest.res.mjs +43 -0
  129. package/tests/DomainDeadCodeTest.res +119 -0
  130. package/tests/DomainDeadCodeTest.res.mjs +158 -0
  131. package/tests/DomainGraphTest.res +448 -0
  132. package/tests/DomainGraphTest.res.mjs +505 -0
  133. package/tests/FlowAggregateGwtTest.res +171 -0
  134. package/tests/FlowAggregateGwtTest.res.mjs +273 -0
  135. package/tests/FlowCrossPluginGwtTest.res +340 -0
  136. package/tests/FlowCrossPluginGwtTest.res.mjs +798 -0
  137. package/tests/FlowGwtTest.res +222 -0
  138. package/tests/FlowGwtTest.res.mjs +433 -0
  139. package/tests/FormatterGoldenTest.res +125 -0
  140. package/tests/FormatterGoldenTest.res.mjs +162 -0
  141. package/tests/FormatterVsCodeMessageTest.res +41 -0
  142. package/tests/FormatterVsCodeMessageTest.res.mjs +42 -0
  143. package/tests/InboundTranslationGwtTest.res +34 -0
  144. package/tests/InboundTranslationGwtTest.res.mjs +99 -0
  145. package/tests/LocalHostIntegration.res +70 -0
  146. package/tests/LocalHostIntegration.res.mjs +64 -0
  147. package/tests/LocalHostTest.res +71 -0
  148. package/tests/LocalHostTest.res.mjs +63 -0
  149. package/tests/MappingGwtTest.res +350 -0
  150. package/tests/MappingGwtTest.res.mjs +596 -0
  151. package/tests/OutboundTranslationGwtTest.res +59 -0
  152. package/tests/OutboundTranslationGwtTest.res.mjs +139 -0
  153. package/tests/PlatformRunnerTest.res +66 -0
  154. package/tests/PlatformRunnerTest.res.mjs +85 -0
  155. package/tests/PlatformScanTest.res +61 -0
  156. package/tests/PlatformScanTest.res.mjs +56 -0
  157. package/tests/QueryGwtTest.res +161 -0
  158. package/tests/QueryGwtTest.res.mjs +314 -0
  159. package/tests/QueryResolverGwtTest.res +71 -0
  160. package/tests/QueryResolverGwtTest.res.mjs +136 -0
  161. package/tests/RunnerUnitTest.res +219 -0
  162. package/tests/RunnerUnitTest.res.mjs +244 -0
  163. package/tests/SideEffect/FakeOrderNotification.res +23 -0
  164. package/tests/SideEffect/FakeOrderNotification.res.mjs +36 -0
  165. package/tests/SideEffect/FakeOrderNotificationGwtTest.res +35 -0
  166. package/tests/SideEffect/FakeOrderNotificationGwtTest.res.mjs +73 -0
  167. package/tests/SideEffect/MockEmail.res +18 -0
  168. package/tests/SideEffect/MockEmail.res.mjs +47 -0
  169. package/tests/StateChange/ExternalAddCategorySlice_GWT.res +27 -0
  170. package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +70 -0
  171. package/tests/StateChange/WithFixtures_Fixtures.res +9 -0
  172. package/tests/StateChange/WithFixtures_Fixtures.res.mjs +20 -0
  173. package/tests/StateChange/WithFixtures_GWT.res +23 -0
  174. package/tests/StateChange/WithFixtures_GWT.res.mjs +56 -0
  175. package/tests/StateChange/WithManualOpen_Fixtures.res +7 -0
  176. package/tests/StateChange/WithManualOpen_Fixtures.res.mjs +20 -0
  177. package/tests/StateChange/WithManualOpen_GWT.res +17 -0
  178. package/tests/StateChange/WithManualOpen_GWT.res.mjs +56 -0
  179. package/tests/StateChangeSliceGwtTest.res +160 -0
  180. package/tests/StateChangeSliceGwtTest.res.mjs +280 -0
  181. package/tests/StateViewSliceGwtTest.res +64 -0
  182. package/tests/StateViewSliceGwtTest.res.mjs +161 -0
  183. package/tests/WatchDebounceTest.res +106 -0
  184. package/tests/WatchDebounceTest.res.mjs +128 -0
@@ -0,0 +1,352 @@
1
+ // NDJSON event stream tailored for the VS Code Testing API. Each line maps directly
2
+ // to a `TestRun` method call; field names mirror `TestItem` / `TestMessage` so the
3
+ // extension needs no translation layer.
4
+ //
5
+ // The contract is the shared `@reventlessdev/reventless-vscode-protocol` `Protocol`
6
+ // module — the SAME sury `@schema` the extension decodes with. Every event is built as
7
+ // a `Protocol.streamEvent` variant and serialized via `Protocol.toJsonLine`, so a
8
+ // `protocolVersion` bump or field change touches that one definition instead of
9
+ // drifting between this emitter and the extension's decoder.
10
+
11
+ module P = ReventlessVscodeProtocol.Protocol
12
+
13
+ @val external processStdout: {"write": string => unit} = "process.stdout"
14
+ let write = (s: string) => processStdout["write"](s)
15
+ let writeLine = (s: string) => write(s ++ "\n")
16
+
17
+ // Serialize one contract event to its NDJSON line (single source: Protocol).
18
+ let emit = (e: P.streamEvent) => writeLine(P.toJsonLine(e))
19
+
20
+ let uriOf = (path: string) => "file://" ++ path
21
+
22
+ @val external processCwd: unit => string = "process.cwd"
23
+ @module("node:path") external pathBasename: string => string = "basename"
24
+ @module("node:path") external pathRelative: (string, string) => string = "relative"
25
+
26
+ let fileLabelOf = (abs: string) => {
27
+ let base = pathBasename(abs)
28
+ if String.endsWith(base, ".res.mjs") {
29
+ String.slice(base, ~start=0, ~end=String.length(base) - 8)
30
+ } else {
31
+ base
32
+ }
33
+ }
34
+
35
+ let relativeToCwd = (abs: string) => pathRelative(processCwd(), abs)
36
+
37
+ // ─── .res-source locator ───────────────────────────────────────────────────
38
+ // ReScript v12 does not emit JS source maps, so V8 stack frames (the basis for
39
+ // Collector.captureLocation) point at `.res.mjs`. Every GWT combinator takes a string
40
+ // literal as its first argument — scanning the sibling `.res` file for that literal
41
+ // yields an exact line, turning tree-node clicks into `.res` jumps.
42
+
43
+ @module("node:fs") external _readFileSync: (string, string) => string = "readFileSync"
44
+ @module("node:fs") external _existsSync: string => bool = "existsSync"
45
+
46
+ let sourceLineCache: Dict.t<option<array<string>>> = Dict.make()
47
+
48
+ let mjsToRes = (mjsPath: string): option<string> =>
49
+ if String.endsWith(mjsPath, ".res.mjs") {
50
+ Some(String.slice(mjsPath, ~start=0, ~end=String.length(mjsPath) - 4))
51
+ } else {
52
+ None
53
+ }
54
+
55
+ let readLinesCached = (resPath: string): option<array<string>> =>
56
+ switch sourceLineCache->Dict.get(resPath) {
57
+ | Some(v) => v
58
+ | None => {
59
+ let v = try {
60
+ _existsSync(resPath)
61
+ ? Some(_readFileSync(resPath, "utf8")->String.split("\n"))
62
+ : None
63
+ } catch {
64
+ | _ => None
65
+ }
66
+ sourceLineCache->Dict.set(resPath, v)
67
+ v
68
+ }
69
+ }
70
+
71
+ let escapeForDouble = (s: string) =>
72
+ s->String.replaceAll("\\", "\\\\")->String.replaceAll("\"", "\\\"")
73
+
74
+ let escapeForSingle = (s: string) =>
75
+ s->String.replaceAll("\\", "\\\\")->String.replaceAll("'", "\\'")
76
+
77
+ let locateInSource = (mjsPath: string, label: string): option<Collector.location> =>
78
+ switch mjsToRes(mjsPath) {
79
+ | None => None
80
+ | Some(resPath) =>
81
+ switch readLinesCached(resPath) {
82
+ | None => None
83
+ | Some(lines) => {
84
+ let needleD = "\"" ++ escapeForDouble(label) ++ "\""
85
+ let needleS = "'" ++ escapeForSingle(label) ++ "'"
86
+ let total = lines->Array.length
87
+ let rec find = i =>
88
+ if i >= total {
89
+ None
90
+ } else {
91
+ let line = lines->Array.getUnsafe(i)
92
+ let idxD = String.indexOf(line, needleD)
93
+ let idxS = String.indexOf(line, needleS)
94
+ let col = switch (idxD, idxS) {
95
+ | (-1, -1) => -1
96
+ | (-1, c) => c + 1
97
+ | (c, -1) => c + 1
98
+ | (a, b) => (a < b ? a : b) + 1
99
+ }
100
+ if col < 0 {
101
+ find(i + 1)
102
+ } else {
103
+ Some({Collector.file: resPath, line: i + 1, column: col})
104
+ }
105
+ }
106
+ find(0)
107
+ }
108
+ }
109
+ }
110
+
111
+ // 1-based Collector.location → 0-based vscode range (both ends at the token start, as
112
+ // the previous emitter did).
113
+ let rangeOf = (loc: Collector.location): P.vsRange => {
114
+ let pos: P.position = {line: loc.line - 1, character: loc.column - 1}
115
+ {start: pos, end: pos}
116
+ }
117
+
118
+ let locationOf = (loc: Collector.location): P.failLocation => {
119
+ uri: uriOf(loc.file),
120
+ range: rangeOf(loc),
121
+ }
122
+
123
+ // Bumped whenever the event contract changes — single source in `Protocol`.
124
+ let protocolVersion = P.protocolVersion
125
+
126
+ // First line of every `--format=vscode` invocation, so a client can detect a
127
+ // version-skewed CLI before interpreting the stream.
128
+ let hello = () => emit(Hello({protocol: P.protocolVersion}))
129
+
130
+ let discoverStart = () => emit(DiscoverStart({}))
131
+
132
+ // Used in both `discover` and `run` flows — emit one `item` per file, one per describe,
133
+ // one per test so the tree populates before any test runs. File / suite / test URIs
134
+ // point at `.res` sources whenever the sibling `.res` is readable.
135
+ let emitDiscoveryItems = (files: array<(string, array<RunnerTypes.testResult>)>) => {
136
+ files->Array.forEach(((path, tests)) => {
137
+ let fileId = path
138
+ let fileUri = switch mjsToRes(path) {
139
+ | Some(resPath) if _existsSync(resPath) => uriOf(resPath)
140
+ | _ => uriOf(path)
141
+ }
142
+ let component =
143
+ ComponentMeta.componentOfTestFile(path)->Option.map((c): P.componentMeta => {
144
+ // `ComponentKind.folderName` spellings ARE the typed vocabulary's constructor
145
+ // names, so the identity conversion lands every known kind on its constructor.
146
+ kind: P.componentKindOfString(c.kind),
147
+ name: c.name,
148
+ })
149
+ emit(
150
+ Item({
151
+ id: fileId,
152
+ kind: File,
153
+ label: fileLabelOf(path),
154
+ description: relativeToCwd(path),
155
+ uri: fileUri,
156
+ component: ?component,
157
+ }),
158
+ )
159
+ // Emit describe suites and test leaves.
160
+ let seenDescribes = Dict.make()
161
+ tests->Array.forEach(t => {
162
+ let stack = ref([])
163
+ t.describePath->Array.forEach(label => {
164
+ let parent =
165
+ stack.contents->Array.length == 0
166
+ ? fileId
167
+ : fileId ++ "::" ++ stack.contents->Array.join("::")
168
+ stack := Array.concat(stack.contents, [label])
169
+ let id = fileId ++ "::" ++ stack.contents->Array.join("::")
170
+ switch seenDescribes->Dict.get(id) {
171
+ | Some(_) => ()
172
+ | None => {
173
+ seenDescribes->Dict.set(id, true)
174
+ let loc = locateInSource(path, label)
175
+ emit(
176
+ Item({
177
+ id,
178
+ parent,
179
+ kind: Suite,
180
+ label,
181
+ uri: ?loc->Option.map(l => uriOf(l.file)),
182
+ range: ?loc->Option.map(rangeOf),
183
+ }),
184
+ )
185
+ }
186
+ }
187
+ })
188
+ let parent =
189
+ t.describePath->Array.length == 0
190
+ ? fileId
191
+ : fileId ++ "::" ++ t.describePath->Array.join("::")
192
+ let testId = fileId ++ "::" ++ t.id
193
+ let resLoc = locateInSource(path, t.name)
194
+ let loc = switch (resLoc, t.location) {
195
+ | (Some(loc), _) | (None, Some(loc)) => Some(loc)
196
+ | (None, None) => None
197
+ }
198
+ emit(
199
+ Item({
200
+ id: testId,
201
+ parent,
202
+ kind: Test,
203
+ label: t.name,
204
+ uri: ?loc->Option.map(l => uriOf(l.file)),
205
+ range: ?loc->Option.map(rangeOf),
206
+ }),
207
+ )
208
+ })
209
+ })
210
+ }
211
+
212
+ let discoverEnd = (total: int) => emit(DiscoverEnd({total: total}))
213
+
214
+ // The set of workspace packages owning discovered tests — the build set a watch
215
+ // session keeps compiling.
216
+ let packages = (pkgs: array<PackageScan.pkg>) =>
217
+ emit(
218
+ Packages({
219
+ packages: pkgs->Array.map((p): P.packageInfo => {name: p.name, dir: p.dir, build: p.build}),
220
+ }),
221
+ )
222
+
223
+ // The full component inventory across the owning packages — every Aggregate / slice /
224
+ // ReadModel / … in src/, whether or not it has a GWT test.
225
+ let components = (comps: array<ComponentScan.component>) =>
226
+ emit(
227
+ Components({
228
+ components: comps->Array.map((c): P.componentRef => {
229
+ dir: c.dir,
230
+ kind: P.componentKindOfString(c.kind),
231
+ name: c.name,
232
+ files: c.files,
233
+ }),
234
+ }),
235
+ )
236
+
237
+ // Domain-level dead code (Phase 5) — produced events no component consumes.
238
+ let deadCode = (findings: array<DomainDeadCode.finding>) =>
239
+ emit(
240
+ DeadCode({
241
+ findings: findings->Array.map((f): P.deadCodeFinding => {
242
+ kind: f.kind,
243
+ plugin: f.pluginName,
244
+ component: f.componentName,
245
+ detail: f.detail,
246
+ }),
247
+ }),
248
+ )
249
+
250
+ // Event-Modeling graph (Phase 6) — the node/edge model assembled from each plugin's
251
+ // `pluginStructure` + cross-plugin edges.
252
+ // `DomainGraph` already builds `P.graphNode`/`P.graphEdge`, so the graph event
253
+ // forwards them directly — no field-by-field re-map to drift from the contract.
254
+ let graph = (g: DomainGraph.graph) => emit(Graph({nodes: g.nodes, edges: g.edges}))
255
+
256
+ // Component definitions (Phase 6.3) — one `encodePluginStructureEntry` JSON object
257
+ // per plugin (commands/events with field schemas, read-side state schemas), used by
258
+ // the extension to render field rows.
259
+ let definitions = (entries: array<JSON.t>) => emit(Definitions({entries: entries}))
260
+
261
+ // ─── Build status (watch mode) ──────────────────────────────────────────────
262
+
263
+ let buildStart = (pkg: string) => emit(BuildStart({package: pkg}))
264
+ let buildOk = (pkg: string, durationMs: float) => emit(BuildOk({package: pkg, durationMs: durationMs}))
265
+ let buildFail = (pkg: string, message: string) => emit(BuildFail({package: pkg, message: message}))
266
+ let buildExternal = (pkg: string) => emit(BuildExternal({package: pkg}))
267
+
268
+ // ── Local platform runner events (protocol 7) ───────────────────────────────
269
+
270
+ let platformStart = (~package: string, ~dir: string, ~domainPort: int, ~platformPort: int) =>
271
+ emit(PlatformStart({package, dir, domainPort, platformPort}))
272
+
273
+ let platformReady = (~domainEndpoint: string) => emit(PlatformReady({domainEndpoint: domainEndpoint}))
274
+
275
+ // `payload` is the parsed JSON object the LocalBus tap emitted (already shaped
276
+ // `{event:"domainEvent", seq, topic, service, payload, ts}`) — re-emitted verbatim as
277
+ // one NDJSON line so the extension parses it like any other engine event.
278
+ let domainEvent = (payload: JSON.t) => writeLine(JSON.stringify(payload))
279
+
280
+ let platformLog = (~line: string) => emit(PlatformLog({line: line}))
281
+
282
+ let platformStop = (~code: option<int>) => emit(PlatformStop({code: ?code}))
283
+
284
+ let runStart = (~total: int, ~filter: array<string>) => emit(RunStart({total, filter}))
285
+
286
+ let testStart = (id: string) => emit(TestStart({id: id}))
287
+
288
+ let testPass = (id: string, durationMs: float) => emit(TestPass({id, durationMs}))
289
+
290
+ let testSkip = (id: string, reason: string) => emit(TestSkip({id, reason}))
291
+
292
+ // The failure payload the extension renders — built as a typed `Protocol.failMessage`.
293
+ let messagePayload = (t: RunnerTypes.testResult): P.failMessage =>
294
+ switch t.mismatch {
295
+ | Some(m) =>
296
+ let hint = Hint.forMismatch(~slice=?t.slice, m)
297
+ // Expected / actual for the diff view, rendered as ReScript syntax.
298
+ let (expected, actual) = switch m {
299
+ | EventsMismatch({expected, actual}) => (
300
+ Some(RenderRescript.renderMany(expected)),
301
+ Some(RenderRescript.renderMany(actual)),
302
+ )
303
+ | ErrorMismatch({expected, actual, actualEvents}) =>
304
+ let actStr = switch actual {
305
+ | Some(v) => "Error(" ++ RenderRescript.render(v) ++ ")"
306
+ | None => "Ok(" ++ RenderRescript.renderMany(actualEvents) ++ ")"
307
+ }
308
+ (Some("Error(" ++ RenderRescript.render(expected) ++ ")"), Some(actStr))
309
+ | StateMismatch({expected, actual, _}) => (
310
+ Some(RenderRescript.renderOption(expected)),
311
+ Some(RenderRescript.renderOption(actual)),
312
+ )
313
+ | NoEventExpected({actual}) => (Some("[]"), Some(RenderRescript.renderMany(actual)))
314
+ | QueryRowsMismatch({expected, actual}) => (
315
+ Some(RenderRescript.renderMany(expected)),
316
+ Some(RenderRescript.renderMany(actual)),
317
+ )
318
+ | PublishedActionsMismatch({expected, actual}) => (
319
+ Some(RenderRescript.renderMany(expected)),
320
+ Some(RenderRescript.renderMany(actual)),
321
+ )
322
+ | AppendConditionMismatch({expected, actual}) => (
323
+ Some(RenderRescript.render(expected)),
324
+ Some(RenderRescript.render(actual)),
325
+ )
326
+ | TranslateError({expected, actual}) => (Some(expected), Some(actual->Option.getOr("(none)")))
327
+ | TodoMismatch(_) => (Some(Outcome.format(m)), Some(""))
328
+ | Throw({error}) => (Some(""), Some(error))
329
+ }
330
+ // Location points at the test file — prefer the sibling `.res` resolved via the
331
+ // test label so Cmd+Click jumps to readable source, not compiled `.res.mjs`.
332
+ let location =
333
+ t.location->Option.map(loc => locationOf(locateInSource(loc.file, t.name)->Option.getOr(loc)))
334
+ {
335
+ message: hint.message,
336
+ // The mismatch family — lets a client gate the apply-expected quick-fix.
337
+ // `Outcome.kindName` stays string-returning (JUnit/JSON formatters consume it
338
+ // raw); its spellings are the assertionKind constructor names, so the identity
339
+ // conversion types it losslessly.
340
+ kind: ?Some(P.assertionKindOfString(Outcome.kindName(m))),
341
+ expected: ?expected,
342
+ actual: ?actual,
343
+ location: ?location,
344
+ }
345
+ | None => {message: "failed"}
346
+ }
347
+
348
+ let testFail = (t: RunnerTypes.testResult, id: string) =>
349
+ emit(TestFail({id, durationMs: t.durationMs, messages: [messagePayload(t)]}))
350
+
351
+ let runEnd = (s: RunnerTypes.summary) =>
352
+ emit(RunEnd({passed: s.passed, failed: s.failed, skipped: s.skipped, durationMs: s.durationMs}))