@hydranium/core 1.0.0-next.10 → 1.0.0-next.103

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 (375) hide show
  1. package/lib/documents/ast-document-manager.d.ts +75 -16
  2. package/lib/documents/ast-document-manager.d.ts.map +1 -1
  3. package/lib/documents/ast-document-manager.js +57 -19
  4. package/lib/documents/ast-document-manager.js.map +1 -1
  5. package/lib/documents/client-ids.d.ts +5 -19
  6. package/lib/documents/client-ids.d.ts.map +1 -1
  7. package/lib/documents/client-ids.js +5 -19
  8. package/lib/documents/client-ids.js.map +1 -1
  9. package/lib/documents/hydranium-text-documents.d.ts +124 -36
  10. package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
  11. package/lib/documents/hydranium-text-documents.js +191 -51
  12. package/lib/documents/hydranium-text-documents.js.map +1 -1
  13. package/lib/documents/language-client-text-shadow.d.ts +41 -9
  14. package/lib/documents/language-client-text-shadow.d.ts.map +1 -1
  15. package/lib/documents/language-client-text-shadow.js +56 -11
  16. package/lib/documents/language-client-text-shadow.js.map +1 -1
  17. package/lib/documents/self-save-registry.d.ts +37 -6
  18. package/lib/documents/self-save-registry.d.ts.map +1 -1
  19. package/lib/documents/self-save-registry.js +28 -15
  20. package/lib/documents/self-save-registry.js.map +1 -1
  21. package/lib/index.d.ts +3 -0
  22. package/lib/index.d.ts.map +1 -1
  23. package/lib/index.js +6 -0
  24. package/lib/index.js.map +1 -1
  25. package/lib/langium/ast-extension/ast-node-builder.d.ts +32 -9
  26. package/lib/langium/ast-extension/ast-node-builder.d.ts.map +1 -1
  27. package/lib/langium/ast-extension/ast-node-builder.js +60 -22
  28. package/lib/langium/ast-extension/ast-node-builder.js.map +1 -1
  29. package/lib/langium/bootstrap.d.ts.map +1 -1
  30. package/lib/langium/bootstrap.js +16 -9
  31. package/lib/langium/bootstrap.js.map +1 -1
  32. package/lib/langium/config/configuration-provider.d.ts +81 -0
  33. package/lib/langium/config/configuration-provider.d.ts.map +1 -0
  34. package/lib/langium/config/configuration-provider.js +112 -0
  35. package/lib/langium/config/configuration-provider.js.map +1 -0
  36. package/lib/langium/config/index.d.ts +1 -0
  37. package/lib/langium/config/index.d.ts.map +1 -1
  38. package/lib/langium/config/index.js +1 -0
  39. package/lib/langium/config/index.js.map +1 -1
  40. package/lib/langium/diagnostics/lsp-logger.d.ts +17 -0
  41. package/lib/langium/diagnostics/lsp-logger.d.ts.map +1 -1
  42. package/lib/langium/diagnostics/lsp-logger.js +40 -6
  43. package/lib/langium/diagnostics/lsp-logger.js.map +1 -1
  44. package/lib/langium/document-builder/build-pipeline-integration.d.ts +21 -1
  45. package/lib/langium/document-builder/build-pipeline-integration.d.ts.map +1 -1
  46. package/lib/langium/document-builder/build-pipeline-integration.js +32 -5
  47. package/lib/langium/document-builder/build-pipeline-integration.js.map +1 -1
  48. package/lib/langium/document-builder/build-session.d.ts +93 -0
  49. package/lib/langium/document-builder/build-session.d.ts.map +1 -0
  50. package/lib/langium/document-builder/build-session.js +72 -0
  51. package/lib/langium/document-builder/build-session.js.map +1 -0
  52. package/lib/langium/document-builder/document-builder.d.ts +150 -7
  53. package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
  54. package/lib/langium/document-builder/document-builder.js +298 -11
  55. package/lib/langium/document-builder/document-builder.js.map +1 -1
  56. package/lib/langium/document-builder/index.d.ts +1 -0
  57. package/lib/langium/document-builder/index.d.ts.map +1 -1
  58. package/lib/langium/document-builder/index.js +1 -0
  59. package/lib/langium/document-builder/index.js.map +1 -1
  60. package/lib/langium/integration-services.d.ts +45 -1
  61. package/lib/langium/integration-services.d.ts.map +1 -1
  62. package/lib/langium/integration-services.js +10 -5
  63. package/lib/langium/integration-services.js.map +1 -1
  64. package/lib/langium/integrity/integrity-service.d.ts +16 -0
  65. package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
  66. package/lib/langium/integrity/integrity-service.js +110 -2
  67. package/lib/langium/integrity/integrity-service.js.map +1 -1
  68. package/lib/langium/keys/containment.d.ts +64 -0
  69. package/lib/langium/keys/containment.d.ts.map +1 -0
  70. package/lib/langium/keys/containment.js +53 -0
  71. package/lib/langium/keys/containment.js.map +1 -0
  72. package/lib/langium/keys/index.d.ts +1 -0
  73. package/lib/langium/keys/index.d.ts.map +1 -1
  74. package/lib/langium/keys/index.js +1 -0
  75. package/lib/langium/keys/index.js.map +1 -1
  76. package/lib/langium/keys/name-based-key-provider.d.ts +4 -0
  77. package/lib/langium/keys/name-based-key-provider.d.ts.map +1 -1
  78. package/lib/langium/keys/name-based-key-provider.js +4 -0
  79. package/lib/langium/keys/name-based-key-provider.js.map +1 -1
  80. package/lib/langium/language-module.d.ts +25 -0
  81. package/lib/langium/language-module.d.ts.map +1 -1
  82. package/lib/langium/language-module.js +14 -0
  83. package/lib/langium/language-module.js.map +1 -1
  84. package/lib/langium/model-service/model-service.d.ts +161 -10
  85. package/lib/langium/model-service/model-service.d.ts.map +1 -1
  86. package/lib/langium/model-service/model-service.js +174 -95
  87. package/lib/langium/model-service/model-service.js.map +1 -1
  88. package/lib/langium/module.d.ts +84 -15
  89. package/lib/langium/module.d.ts.map +1 -1
  90. package/lib/langium/module.js +26 -12
  91. package/lib/langium/module.js.map +1 -1
  92. package/lib/langium/naming/name-provider.d.ts +25 -4
  93. package/lib/langium/naming/name-provider.d.ts.map +1 -1
  94. package/lib/langium/naming/name-provider.js +2 -1
  95. package/lib/langium/naming/name-provider.js.map +1 -1
  96. package/lib/langium/naming/name-separator-validation.d.ts +23 -0
  97. package/lib/langium/naming/name-separator-validation.d.ts.map +1 -1
  98. package/lib/langium/naming/name-separator-validation.js +30 -1
  99. package/lib/langium/naming/name-separator-validation.js.map +1 -1
  100. package/lib/langium/residency/cst-residency-service.d.ts +31 -11
  101. package/lib/langium/residency/cst-residency-service.d.ts.map +1 -1
  102. package/lib/langium/residency/cst-residency-service.js +13 -56
  103. package/lib/langium/residency/cst-residency-service.js.map +1 -1
  104. package/lib/langium/scope/hydranium-scope-provider.d.ts +48 -13
  105. package/lib/langium/scope/hydranium-scope-provider.d.ts.map +1 -1
  106. package/lib/langium/scope/hydranium-scope-provider.js +56 -20
  107. package/lib/langium/scope/hydranium-scope-provider.js.map +1 -1
  108. package/lib/langium/serialization/abstract-serializer.d.ts +11 -20
  109. package/lib/langium/serialization/abstract-serializer.d.ts.map +1 -1
  110. package/lib/langium/serialization/abstract-serializer.js +12 -21
  111. package/lib/langium/serialization/abstract-serializer.js.map +1 -1
  112. package/lib/langium/service-registry.d.ts +15 -1
  113. package/lib/langium/service-registry.d.ts.map +1 -1
  114. package/lib/langium/service-registry.js +16 -0
  115. package/lib/langium/service-registry.js.map +1 -1
  116. package/lib/langium/shared-services.d.ts +15 -2
  117. package/lib/langium/shared-services.d.ts.map +1 -1
  118. package/lib/langium/shared-services.js.map +1 -1
  119. package/lib/langium/transfer/transfer-encoder.d.ts +60 -17
  120. package/lib/langium/transfer/transfer-encoder.d.ts.map +1 -1
  121. package/lib/langium/transfer/transfer-encoder.js +38 -16
  122. package/lib/langium/transfer/transfer-encoder.js.map +1 -1
  123. package/lib/langium/trivia/comment-preserver.d.ts +423 -0
  124. package/lib/langium/trivia/comment-preserver.d.ts.map +1 -0
  125. package/lib/langium/trivia/comment-preserver.js +906 -0
  126. package/lib/langium/trivia/comment-preserver.js.map +1 -0
  127. package/lib/langium/trivia/document-ending-preserver.d.ts +43 -0
  128. package/lib/langium/trivia/document-ending-preserver.d.ts.map +1 -0
  129. package/lib/langium/trivia/document-ending-preserver.js +48 -0
  130. package/lib/langium/trivia/document-ending-preserver.js.map +1 -0
  131. package/lib/langium/trivia/index.d.ts +14 -0
  132. package/lib/langium/trivia/index.d.ts.map +1 -0
  133. package/lib/langium/trivia/index.js +14 -0
  134. package/lib/langium/trivia/index.js.map +1 -0
  135. package/lib/langium/trivia/trivia-contribution.d.ts +37 -0
  136. package/lib/langium/trivia/trivia-contribution.d.ts.map +1 -0
  137. package/lib/langium/trivia/trivia-contribution.js +10 -0
  138. package/lib/langium/trivia/trivia-contribution.js.map +1 -0
  139. package/lib/langium/trivia/trivia-preserver.d.ts +50 -0
  140. package/lib/langium/trivia/trivia-preserver.d.ts.map +1 -0
  141. package/lib/langium/trivia/trivia-preserver.js +10 -0
  142. package/lib/langium/trivia/trivia-preserver.js.map +1 -0
  143. package/lib/langium/trivia/trivia-service.d.ts +70 -0
  144. package/lib/langium/trivia/trivia-service.d.ts.map +1 -0
  145. package/lib/langium/trivia/trivia-service.js +56 -0
  146. package/lib/langium/trivia/trivia-service.js.map +1 -0
  147. package/lib/langium/validation/document-validator.d.ts +294 -10
  148. package/lib/langium/validation/document-validator.d.ts.map +1 -1
  149. package/lib/langium/validation/document-validator.js +448 -5
  150. package/lib/langium/validation/document-validator.js.map +1 -1
  151. package/lib/langium/workspace/document-uri-policy.d.ts +3 -4
  152. package/lib/langium/workspace/document-uri-policy.d.ts.map +1 -1
  153. package/lib/langium/workspace/document-uri-policy.js +3 -4
  154. package/lib/langium/workspace/document-uri-policy.js.map +1 -1
  155. package/lib/langium/workspace/file-system-provider.d.ts +9 -1
  156. package/lib/langium/workspace/file-system-provider.d.ts.map +1 -1
  157. package/lib/langium/workspace/file-system-provider.js +40 -7
  158. package/lib/langium/workspace/file-system-provider.js.map +1 -1
  159. package/lib/langium/workspace/hydranium-langium-document-factory.d.ts +25 -0
  160. package/lib/langium/workspace/hydranium-langium-document-factory.d.ts.map +1 -1
  161. package/lib/langium/workspace/hydranium-langium-document-factory.js +25 -0
  162. package/lib/langium/workspace/hydranium-langium-document-factory.js.map +1 -1
  163. package/lib/langium/workspace/hydranium-workspace-manager.d.ts +21 -1
  164. package/lib/langium/workspace/hydranium-workspace-manager.d.ts.map +1 -1
  165. package/lib/langium/workspace/hydranium-workspace-manager.js +28 -0
  166. package/lib/langium/workspace/hydranium-workspace-manager.js.map +1 -1
  167. package/lib/langium/workspace/in-memory-file-system-provider.d.ts +23 -0
  168. package/lib/langium/workspace/in-memory-file-system-provider.d.ts.map +1 -1
  169. package/lib/langium/workspace/in-memory-file-system-provider.js +47 -9
  170. package/lib/langium/workspace/in-memory-file-system-provider.js.map +1 -1
  171. package/lib/langium/workspace/initialize-workspace.d.ts +22 -2
  172. package/lib/langium/workspace/initialize-workspace.d.ts.map +1 -1
  173. package/lib/langium/workspace/initialize-workspace.js +13 -5
  174. package/lib/langium/workspace/initialize-workspace.js.map +1 -1
  175. package/lib/langium/workspace/langium-documents.d.ts +79 -17
  176. package/lib/langium/workspace/langium-documents.d.ts.map +1 -1
  177. package/lib/langium/workspace/langium-documents.js +83 -28
  178. package/lib/langium/workspace/langium-documents.js.map +1 -1
  179. package/lib/langium/workspace/virtual-document.d.ts +18 -3
  180. package/lib/langium/workspace/virtual-document.d.ts.map +1 -1
  181. package/lib/langium/workspace/virtual-document.js +19 -2
  182. package/lib/langium/workspace/virtual-document.js.map +1 -1
  183. package/lib/locale/index.d.ts +10 -0
  184. package/lib/locale/index.d.ts.map +1 -0
  185. package/lib/locale/index.js +10 -0
  186. package/lib/locale/index.js.map +1 -0
  187. package/lib/locale/server-locale.d.ts +73 -0
  188. package/lib/locale/server-locale.d.ts.map +1 -0
  189. package/lib/locale/server-locale.js +61 -0
  190. package/lib/locale/server-locale.js.map +1 -0
  191. package/lib/lsp/hydranium-document-update-handler.js +1 -1
  192. package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
  193. package/lib/lsp/index.d.ts +1 -1
  194. package/lib/lsp/index.d.ts.map +1 -1
  195. package/lib/lsp/index.js +1 -1
  196. package/lib/lsp/index.js.map +1 -1
  197. package/lib/lsp/lsp-latency.d.ts +39 -0
  198. package/lib/lsp/lsp-latency.d.ts.map +1 -0
  199. package/lib/lsp/lsp-latency.js +58 -0
  200. package/lib/lsp/lsp-latency.js.map +1 -0
  201. package/lib/lsp/semantic-token-provider.d.ts +34 -1
  202. package/lib/lsp/semantic-token-provider.d.ts.map +1 -1
  203. package/lib/lsp/semantic-token-provider.js +55 -8
  204. package/lib/lsp/semantic-token-provider.js.map +1 -1
  205. package/lib/messages/carriers.d.ts +54 -0
  206. package/lib/messages/carriers.d.ts.map +1 -0
  207. package/lib/messages/carriers.js +62 -0
  208. package/lib/messages/carriers.js.map +1 -0
  209. package/lib/messages/index.d.ts +25 -0
  210. package/lib/messages/index.d.ts.map +1 -0
  211. package/lib/messages/index.js +25 -0
  212. package/lib/messages/index.js.map +1 -0
  213. package/lib/messages/renderer.d.ts +136 -0
  214. package/lib/messages/renderer.d.ts.map +1 -0
  215. package/lib/messages/renderer.js +159 -0
  216. package/lib/messages/renderer.js.map +1 -0
  217. package/lib/node/heap-ceiling.d.ts +61 -0
  218. package/lib/node/heap-ceiling.d.ts.map +1 -0
  219. package/lib/node/heap-ceiling.js +72 -0
  220. package/lib/node/heap-ceiling.js.map +1 -0
  221. package/lib/node/index.d.ts +1 -0
  222. package/lib/node/index.d.ts.map +1 -1
  223. package/lib/node/index.js +1 -0
  224. package/lib/node/index.js.map +1 -1
  225. package/lib/node/latency-from-env.d.ts +1 -1
  226. package/lib/node/latency-from-env.js +1 -1
  227. package/lib/node/node-file-system-provider.d.ts +35 -1
  228. package/lib/node/node-file-system-provider.d.ts.map +1 -1
  229. package/lib/node/node-file-system-provider.js +140 -45
  230. package/lib/node/node-file-system-provider.js.map +1 -1
  231. package/lib/node/profile-capture.d.ts +8 -2
  232. package/lib/node/profile-capture.d.ts.map +1 -1
  233. package/lib/node/profile-capture.js.map +1 -1
  234. package/lib/node/profiling-run.d.ts +2 -2
  235. package/lib/node/profiling-run.js +1 -1
  236. package/lib/node/profiling-run.js.map +1 -1
  237. package/lib/node/rename-over-open-readers.d.ts +52 -0
  238. package/lib/node/rename-over-open-readers.d.ts.map +1 -0
  239. package/lib/node/rename-over-open-readers.js +66 -0
  240. package/lib/node/rename-over-open-readers.js.map +1 -0
  241. package/lib/node/socket-launcher.d.ts +15 -0
  242. package/lib/node/socket-launcher.d.ts.map +1 -1
  243. package/lib/node/socket-launcher.js +3 -2
  244. package/lib/node/socket-launcher.js.map +1 -1
  245. package/lib/testing/fake-document.d.ts +16 -4
  246. package/lib/testing/fake-document.d.ts.map +1 -1
  247. package/lib/testing/fake-document.js +18 -3
  248. package/lib/testing/fake-document.js.map +1 -1
  249. package/lib/testing/make-noop-shared-services.d.ts +14 -0
  250. package/lib/testing/make-noop-shared-services.d.ts.map +1 -1
  251. package/lib/testing/make-noop-shared-services.js +28 -2
  252. package/lib/testing/make-noop-shared-services.js.map +1 -1
  253. package/lib/testing/make-test-services.d.ts +44 -8
  254. package/lib/testing/make-test-services.d.ts.map +1 -1
  255. package/lib/testing/make-test-services.js +19 -3
  256. package/lib/testing/make-test-services.js.map +1 -1
  257. package/lib/testing/node/scratch-workspace.d.ts.map +1 -1
  258. package/lib/testing/node/scratch-workspace.js +9 -4
  259. package/lib/testing/node/scratch-workspace.js.map +1 -1
  260. package/lib/testing/node/spawned-server.d.ts +14 -0
  261. package/lib/testing/node/spawned-server.d.ts.map +1 -1
  262. package/lib/testing/node/spawned-server.js +33 -1
  263. package/lib/testing/node/spawned-server.js.map +1 -1
  264. package/lib/testing/playwright/flaky-network-proxy.d.ts +76 -0
  265. package/lib/testing/playwright/flaky-network-proxy.d.ts.map +1 -0
  266. package/lib/testing/playwright/flaky-network-proxy.js +164 -0
  267. package/lib/testing/playwright/flaky-network-proxy.js.map +1 -0
  268. package/lib/testing/playwright/index.d.ts +1 -0
  269. package/lib/testing/playwright/index.d.ts.map +1 -1
  270. package/lib/testing/playwright/index.js +1 -0
  271. package/lib/testing/playwright/index.js.map +1 -1
  272. package/lib/testing/playwright/server-log-capture.d.ts +14 -4
  273. package/lib/testing/playwright/server-log-capture.d.ts.map +1 -1
  274. package/lib/testing/playwright/server-log-capture.js +21 -4
  275. package/lib/testing/playwright/server-log-capture.js.map +1 -1
  276. package/lib/testing/playwright/server-log-rename-reporter.d.ts.map +1 -1
  277. package/lib/testing/playwright/server-log-rename-reporter.js +10 -1
  278. package/lib/testing/playwright/server-log-rename-reporter.js.map +1 -1
  279. package/lib/testing/stub-ast-document-manager.d.ts +5 -5
  280. package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
  281. package/lib/testing/stub-ast-document-manager.js +4 -6
  282. package/lib/testing/stub-ast-document-manager.js.map +1 -1
  283. package/lib/testing/stub-document-builder.d.ts +8 -0
  284. package/lib/testing/stub-document-builder.d.ts.map +1 -1
  285. package/lib/testing/stub-document-builder.js +22 -3
  286. package/lib/testing/stub-document-builder.js.map +1 -1
  287. package/lib/testing/stub-langium-documents.d.ts +3 -2
  288. package/lib/testing/stub-langium-documents.d.ts.map +1 -1
  289. package/lib/testing/stub-langium-documents.js.map +1 -1
  290. package/lib/testing/stub-model-service.d.ts +5 -4
  291. package/lib/testing/stub-model-service.d.ts.map +1 -1
  292. package/lib/testing/stub-model-service.js +2 -2
  293. package/lib/testing/stub-model-service.js.map +1 -1
  294. package/package.json +17 -8
  295. package/src/documents/ast-document-manager.ts +129 -36
  296. package/src/documents/client-ids.ts +5 -22
  297. package/src/documents/hydranium-text-documents.ts +248 -75
  298. package/src/documents/language-client-text-shadow.ts +59 -11
  299. package/src/documents/self-save-registry.ts +52 -13
  300. package/src/index.ts +6 -0
  301. package/src/langium/ast-extension/ast-node-builder.ts +66 -22
  302. package/src/langium/bootstrap.ts +16 -9
  303. package/src/langium/config/configuration-provider.ts +119 -0
  304. package/src/langium/config/index.ts +1 -0
  305. package/src/langium/diagnostics/lsp-logger.ts +41 -6
  306. package/src/langium/document-builder/build-pipeline-integration.ts +50 -5
  307. package/src/langium/document-builder/build-session.ts +87 -0
  308. package/src/langium/document-builder/document-builder.ts +339 -11
  309. package/src/langium/document-builder/index.ts +1 -0
  310. package/src/langium/integration-services.ts +67 -6
  311. package/src/langium/integrity/integrity-service.ts +116 -2
  312. package/src/langium/keys/containment.ts +87 -0
  313. package/src/langium/keys/index.ts +1 -0
  314. package/src/langium/keys/name-based-key-provider.ts +4 -0
  315. package/src/langium/language-module.ts +38 -0
  316. package/src/langium/model-service/model-service.ts +264 -32
  317. package/src/langium/module.ts +106 -21
  318. package/src/langium/naming/name-provider.ts +27 -5
  319. package/src/langium/naming/name-separator-validation.ts +34 -5
  320. package/src/langium/residency/cst-residency-service.ts +36 -14
  321. package/src/langium/scope/hydranium-scope-provider.ts +60 -22
  322. package/src/langium/serialization/abstract-serializer.ts +13 -32
  323. package/src/langium/service-registry.ts +19 -2
  324. package/src/langium/shared-services.ts +19 -2
  325. package/src/langium/transfer/transfer-encoder.ts +85 -24
  326. package/src/langium/trivia/comment-preserver.ts +1100 -0
  327. package/src/langium/trivia/document-ending-preserver.ts +55 -0
  328. package/src/langium/trivia/index.ts +14 -0
  329. package/src/langium/trivia/trivia-contribution.ts +39 -0
  330. package/src/langium/trivia/trivia-preserver.ts +54 -0
  331. package/src/langium/trivia/trivia-service.ts +99 -0
  332. package/src/langium/validation/document-validator.ts +537 -11
  333. package/src/langium/workspace/document-uri-policy.ts +3 -4
  334. package/src/langium/workspace/file-system-provider.ts +48 -8
  335. package/src/langium/workspace/hydranium-langium-document-factory.ts +34 -0
  336. package/src/langium/workspace/hydranium-workspace-manager.ts +30 -1
  337. package/src/langium/workspace/in-memory-file-system-provider.ts +53 -10
  338. package/src/langium/workspace/initialize-workspace.ts +34 -5
  339. package/src/langium/workspace/langium-documents.ts +106 -33
  340. package/src/langium/workspace/virtual-document.ts +21 -3
  341. package/src/locale/index.ts +10 -0
  342. package/src/locale/server-locale.ts +86 -0
  343. package/src/lsp/hydranium-document-update-handler.ts +1 -1
  344. package/src/lsp/index.ts +1 -1
  345. package/src/lsp/lsp-latency.ts +62 -0
  346. package/src/lsp/semantic-token-provider.ts +74 -7
  347. package/src/messages/carriers.ts +78 -0
  348. package/src/messages/index.ts +36 -0
  349. package/src/messages/renderer.ts +204 -0
  350. package/src/node/heap-ceiling.ts +108 -0
  351. package/src/node/index.ts +1 -0
  352. package/src/node/latency-from-env.ts +1 -1
  353. package/src/node/node-file-system-provider.ts +154 -47
  354. package/src/node/profile-capture.ts +8 -2
  355. package/src/node/profiling-run.ts +3 -3
  356. package/src/node/rename-over-open-readers.ts +78 -0
  357. package/src/node/socket-launcher.ts +18 -2
  358. package/src/testing/fake-document.ts +24 -5
  359. package/src/testing/make-noop-shared-services.ts +50 -2
  360. package/src/testing/make-test-services.ts +73 -18
  361. package/src/testing/node/scratch-workspace.ts +9 -4
  362. package/src/testing/node/spawned-server.ts +35 -1
  363. package/src/testing/playwright/flaky-network-proxy.ts +240 -0
  364. package/src/testing/playwright/index.ts +1 -0
  365. package/src/testing/playwright/server-log-capture.ts +24 -5
  366. package/src/testing/playwright/server-log-rename-reporter.ts +12 -1
  367. package/src/testing/stub-ast-document-manager.ts +10 -14
  368. package/src/testing/stub-document-builder.ts +32 -3
  369. package/src/testing/stub-langium-documents.ts +3 -2
  370. package/src/testing/stub-model-service.ts +7 -6
  371. package/lib/lsp/instrument-connection.d.ts +0 -36
  372. package/lib/lsp/instrument-connection.d.ts.map +0 -1
  373. package/lib/lsp/instrument-connection.js +0 -62
  374. package/lib/lsp/instrument-connection.js.map +0 -1
  375. package/src/lsp/instrument-connection.ts +0 -66
@@ -9,6 +9,8 @@
9
9
 
10
10
  import { type Clock, DefaultTracer, type Logger, NoopLogger, SystemClock, type Tracer } from '@hydranium/protocol';
11
11
  import type { ServerSharedServicesMinimal } from '../langium/shared-services.js';
12
+ import { DefaultServerLocale, type ServerLocale } from '../locale/server-locale.js';
13
+ import { DefaultMessageRenderer, type MessageRenderer } from '../messages/renderer.js';
12
14
 
13
15
  /**
14
16
  * Overrides for {@link makeNoopSharedServices}. The three observability slots
@@ -29,6 +31,18 @@ export interface NoopSharedServicesOverrides {
29
31
  Logger?: Logger;
30
32
  /** Bound on the top-level `Tracer` slot. Default: a `DefaultTracer` over `Logger` + `Clock`. */
31
33
  Tracer?: Tracer;
34
+ /** Factory for the `ServerLocale` slot. Default: a real one, reporting no locale. */
35
+ ServerLocale?: (services: ServerSharedServicesMinimal) => ServerLocale;
36
+ /**
37
+ * Factory for the `MessageRenderer` slot. Default: the real
38
+ * {@link DefaultMessageRenderer}, whose no-catalogue behaviour is a
39
+ * pass-through — so the default is the framework's own behaviour, not a stub.
40
+ *
41
+ * A factory rather than an instance, because a renderer reads the tree it is
42
+ * bound into. Both slots are resolved lazily, so an override is honoured
43
+ * however late the caller reads them.
44
+ */
45
+ MessageRenderer?: (services: ServerSharedServicesMinimal) => MessageRenderer;
32
46
  /** Per-slot `workspace` overrides. Slots left out resolve to `undefined`. */
33
47
  workspace?: Record<string, unknown>;
34
48
  /** Any other minimal slot (`ServiceRegistry`, `AstReflection`, `additionalDocuments`, …). */
@@ -63,11 +77,21 @@ export interface NoopSharedServicesOverrides {
63
77
  export function makeNoopSharedServices<T extends ServerSharedServicesMinimal = ServerSharedServicesMinimal>(
64
78
  overrides: NoopSharedServicesOverrides = {}
65
79
  ): T {
66
- const { Clock: clock, Logger: logger, Tracer: tracer, workspace, additionalDocuments, ...restTop } = overrides;
80
+ const {
81
+ Clock: clock,
82
+ Logger: logger,
83
+ Tracer: tracer,
84
+ ServerLocale: serverLocale,
85
+ MessageRenderer: messageRenderer,
86
+ workspace,
87
+ additionalDocuments,
88
+ ...restTop
89
+ } = overrides;
67
90
  const resolvedClock = clock ?? new SystemClock();
68
91
  const resolvedLogger = logger ?? new NoopLogger();
69
92
  const resolvedTracer = tracer ?? new DefaultTracer(resolvedLogger, resolvedClock);
70
- return {
93
+ // Assembled in two steps, because both defaults read the tree they belong to.
94
+ const services = {
71
95
  Clock: resolvedClock,
72
96
  Logger: resolvedLogger,
73
97
  Tracer: resolvedTracer,
@@ -75,4 +99,28 @@ export function makeNoopSharedServices<T extends ServerSharedServicesMinimal = S
75
99
  ...restTop,
76
100
  workspace: { ...(workspace ?? {}) }
77
101
  } as unknown as T;
102
+ // Lazy, like Langium's own per-slot construction — and load-bearing here:
103
+ // both services emit an `instantiated` trace, so building them eagerly puts
104
+ // two lines into the capture of every test that passes a capturing logger
105
+ // and asserts on emptiness.
106
+ defineLazySlot(services, 'ServerLocale', () => serverLocale?.(services) ?? new DefaultServerLocale(services));
107
+ defineLazySlot(services, 'MessageRenderer', () => messageRenderer?.(services) ?? new DefaultMessageRenderer(services));
108
+ return services;
109
+ }
110
+
111
+ /** Install `name` as a memoized getter, so nothing is constructed until it is read. */
112
+ function defineLazySlot(target: object, name: string, create: () => unknown): void {
113
+ let resolved: unknown;
114
+ let built = false;
115
+ Object.defineProperty(target, name, {
116
+ configurable: true,
117
+ enumerable: true,
118
+ get: () => {
119
+ if (!built) {
120
+ resolved = create();
121
+ built = true;
122
+ }
123
+ return resolved;
124
+ }
125
+ });
78
126
  }
@@ -18,11 +18,14 @@ import {
18
18
  type TransferDiagnostic,
19
19
  type TransferElement
20
20
  } from '@hydranium/protocol';
21
+ import { DefaultServerLocale, type ServerLocale } from '../locale/server-locale.js';
22
+ import { DefaultMessageRenderer, type MessageRenderer } from '../messages/renderer.js';
21
23
  import type { Harness } from '@hydranium/protocol/testing';
22
24
  import { type AstNode, type AstNodeDescription, type WorkspaceLock } from '@hydranium/langium';
23
25
  import type { ModelService, ModelServiceOptions } from '../langium/model-service/model-service.js';
24
26
  import type { ServerSharedServices } from '../langium/module.js';
25
- import { TransferEncoder } from '../langium/transfer/transfer-encoder.js';
27
+ import type { AstDiagnostic } from '../langium/validation/document-validator.js';
28
+ import { DefaultTransferEncoder, type TransferEncoder } from '../langium/transfer/transfer-encoder.js';
26
29
  import { DefaultDocumentUriPolicy, type DocumentUriPolicy } from '../langium/workspace/document-uri-policy.js';
27
30
  import { HydraniumWorkspaceLock } from '../langium/workspace/hydranium-workspace-lock.js';
28
31
  import type { FakeDocumentOptions } from './fake-document.js';
@@ -46,12 +49,21 @@ import { makeStubWritableFileSystem, type StubWritableFileSystem } from './stub-
46
49
  * to `ServerSharedServices` in {@link makeTestServices} is therefore the
47
50
  * single named place where the "stub stands in for the full service tree"
48
51
  * assertion happens.
52
+ *
53
+ * **`TDiagnostic` and `TTransferDiagnostic` are the two ends of a conversion
54
+ * and must stay separate.** The first is what the build left on
55
+ * `LangiumDocument.diagnostics` and what `ModelService` hands back on an
56
+ * `AstDocument`; the second is what `TransferEncoder.toTransferDiagnostic`
57
+ * produces for the wire. One parameter serving both makes the tree
58
+ * unbindable for an adopter who extends either shape, since extending one
59
+ * says nothing about the other.
49
60
  */
50
61
  export interface TestSharedServices<
51
62
  TAst extends AstNode = AstNode,
52
- TDiagnostic extends TransferDiagnostic = TransferDiagnostic,
63
+ TDiagnostic extends AstDiagnostic = AstDiagnostic,
53
64
  TTransfer extends TransferElement = TransferElement,
54
- TProject extends Project = Project
65
+ TProject extends Project = Project,
66
+ TTransferDiagnostic extends TransferDiagnostic = TransferDiagnostic
55
67
  > {
56
68
  readonly Clock: Clock;
57
69
  readonly Logger: Logger;
@@ -90,17 +102,27 @@ export interface TestSharedServices<
90
102
  WorkspaceLock: WorkspaceLock;
91
103
  };
92
104
  readonly model: {
93
- TransferEncoder: TransferEncoder<unknown, TDiagnostic>;
105
+ TransferEncoder: TransferEncoder<TTransferDiagnostic>;
94
106
  ModelService: ModelService<TAst, TDiagnostic, TTransfer>;
95
107
  };
108
+ /**
109
+ * The REAL services, not stubs — the framework renderer's no-catalogue
110
+ * behaviour is a pass-through, so the stub tree agrees with a production one
111
+ * unless a test installs a catalogue. Bound rather than omitted because
112
+ * `HydraniumDocumentBuilder` renders through the renderer on every
113
+ * `Validated` phase, where an omitted slot is a `TypeError`.
114
+ */
115
+ readonly ServerLocale: ServerLocale;
116
+ readonly MessageRenderer: MessageRenderer;
96
117
  }
97
118
 
98
119
  /** Optional configuration for {@link makeTestServices}. */
99
120
  export interface MakeTestServicesOptions<
100
121
  TAst extends AstNode = AstNode,
101
- TDiagnostic extends TransferDiagnostic = TransferDiagnostic,
122
+ TDiagnostic extends AstDiagnostic = AstDiagnostic,
102
123
  TTransfer extends TransferElement = TransferElement,
103
- TProject extends Project = Project
124
+ TProject extends Project = Project,
125
+ TTransferDiagnostic extends TransferDiagnostic = TransferDiagnostic
104
126
  > {
105
127
  /**
106
128
  * Serialiser used by the bundled `StubModelService`. The stub tree
@@ -112,6 +134,14 @@ export interface MakeTestServicesOptions<
112
134
  * for tests that don't assert on the produced text.
113
135
  */
114
136
  serialize?: (uri: string, root: TTransfer) => string;
137
+ /**
138
+ * Renderer bound on `MessageRenderer`. Default: the framework's own, which
139
+ * passes every sentence through unchanged. Supply one to install a catalogue
140
+ * or to drive the throwing path.
141
+ */
142
+ messageRenderer?: (services: ServerSharedServices<TProject>) => MessageRenderer;
143
+ /** Locale handed to the bundle's {@link ServerLocale}. Default: none, i.e. the framework's English. */
144
+ locale?: string;
115
145
  /**
116
146
  * Languages to register on a {@link StubServiceRegistry} bound on the
117
147
  * `ServiceRegistry` slot. This is how a test gets multi-language routing:
@@ -187,7 +217,7 @@ export interface MakeTestServicesOptions<
187
217
  * Override the {@link TransferEncoder} factory. Default: framework
188
218
  * {@link TransferEncoder} with no overrides.
189
219
  */
190
- transferEncoder?: (services: ServerSharedServices<TProject>) => TransferEncoder<unknown, TDiagnostic>;
220
+ transferEncoder?: (services: ServerSharedServices<TProject>) => TransferEncoder<TTransferDiagnostic>;
191
221
  }
192
222
 
193
223
  /**
@@ -205,9 +235,10 @@ export interface MakeTestServicesOptions<
205
235
  */
206
236
  export interface TestServicesBundle<
207
237
  TAst extends AstNode = AstNode,
208
- TDiagnostic extends TransferDiagnostic = TransferDiagnostic,
238
+ TDiagnostic extends AstDiagnostic = AstDiagnostic,
209
239
  TTransfer extends TransferElement = TransferElement,
210
- TProject extends Project = Project
240
+ TProject extends Project = Project,
241
+ TTransferDiagnostic extends TransferDiagnostic = TransferDiagnostic
211
242
  > extends Harness {
212
243
  readonly services: ServerSharedServices<TProject>;
213
244
  readonly documents: StubLangiumDocuments<TAst, TDiagnostic>;
@@ -229,10 +260,18 @@ export interface TestServicesBundle<
229
260
  */
230
261
  readonly indexManager: StubIndexManager | undefined;
231
262
  readonly modelService: ModelService<TAst, TDiagnostic, TTransfer>;
232
- readonly transferEncoder: TransferEncoder<unknown, TDiagnostic>;
263
+ readonly transferEncoder: TransferEncoder<TTransferDiagnostic>;
233
264
  readonly logger: Logger;
234
265
  /** The clock bound on the `Clock` slot — a `makeFakeClock()` if one was passed. */
235
266
  readonly clock: Clock;
267
+ /**
268
+ * The service bound on `ServerLocale`, so a test can hand over a locale
269
+ * mid-run — which is also how it verifies the renderer reads the locale per
270
+ * render rather than caching it at construction.
271
+ */
272
+ readonly serverLocale: ServerLocale;
273
+ /** The renderer bound on `MessageRenderer` — the framework's own unless one was supplied. */
274
+ readonly messageRenderer: MessageRenderer;
236
275
  }
237
276
 
238
277
  /**
@@ -247,12 +286,13 @@ export interface TestServicesBundle<
247
286
  */
248
287
  export function makeTestServices<
249
288
  TAst extends AstNode = AstNode,
250
- TDiagnostic extends TransferDiagnostic = TransferDiagnostic,
289
+ TDiagnostic extends AstDiagnostic = AstDiagnostic,
251
290
  TTransfer extends TransferElement = TransferElement,
252
- TProject extends Project = Project
291
+ TProject extends Project = Project,
292
+ TTransferDiagnostic extends TransferDiagnostic = TransferDiagnostic
253
293
  >(
254
- options: MakeTestServicesOptions<TAst, TDiagnostic, TTransfer, TProject> = {}
255
- ): TestServicesBundle<TAst, TDiagnostic, TTransfer, TProject> {
294
+ options: MakeTestServicesOptions<TAst, TDiagnostic, TTransfer, TProject, TTransferDiagnostic> = {}
295
+ ): TestServicesBundle<TAst, TDiagnostic, TTransfer, TProject, TTransferDiagnostic> {
256
296
  const documents = makeStubLangiumDocuments<TAst, TDiagnostic>(options.seedDocuments);
257
297
  const textDocuments = makeStubHydraniumTextDocuments();
258
298
  const documentBuilder = makeStubDocumentBuilder();
@@ -289,7 +329,7 @@ export function makeTestServices<
289
329
  // (after the factories run); the literal uses unsafe casts to
290
330
  // partially-built objects to satisfy TestSharedServices, then patches
291
331
  // them in.
292
- const services: TestSharedServices<TAst, TDiagnostic, TTransfer, TProject> = {
332
+ const services: TestSharedServices<TAst, TDiagnostic, TTransfer, TProject, TTransferDiagnostic> = {
293
333
  Clock: clock,
294
334
  Logger: logger,
295
335
  Tracer: tracer,
@@ -311,20 +351,33 @@ export function makeTestServices<
311
351
  ...(indexManager ? { IndexManager: indexManager } : {}),
312
352
  WorkspaceLock: new HydraniumWorkspaceLock()
313
353
  },
314
- model: {} as TestSharedServices<TAst, TDiagnostic, TTransfer, TProject>['model']
354
+ model: {} as TestSharedServices<TAst, TDiagnostic, TTransfer, TProject, TTransferDiagnostic>['model'],
355
+ ServerLocale: {} as ServerLocale,
356
+ MessageRenderer: {} as DefaultMessageRenderer
315
357
  };
316
358
  const sharedServices = services as unknown as ServerSharedServices<TProject>;
317
359
 
360
+ // Patched in after the literal, like `model` below: both read the tree they
361
+ // belong to, and the renderer reads the locale service.
362
+ const mutableMessages = services as { ServerLocale: ServerLocale; MessageRenderer: MessageRenderer };
363
+ const serverLocale = new DefaultServerLocale(sharedServices);
364
+ if (options.locale) {
365
+ serverLocale.accept(options.locale);
366
+ }
367
+ mutableMessages.ServerLocale = serverLocale;
368
+ const messageRenderer = options.messageRenderer?.(sharedServices) ?? new DefaultMessageRenderer(sharedServices);
369
+ mutableMessages.MessageRenderer = messageRenderer;
370
+
318
371
  const serialize = options.serialize ?? ((_uri: string, root: TTransfer) => JSON.stringify(root));
319
372
  const transferEncoder = options.transferEncoder
320
373
  ? options.transferEncoder(sharedServices)
321
- : new TransferEncoder<unknown, TDiagnostic>(sharedServices);
374
+ : new DefaultTransferEncoder<unknown, TTransferDiagnostic>(sharedServices);
322
375
  const modelService = options.modelService
323
376
  ? options.modelService(sharedServices)
324
377
  : makeStubModelService<TAst, TDiagnostic, TTransfer>(sharedServices, serialize, options.modelServiceOptions);
325
378
 
326
379
  const mutableModel = services.model as {
327
- TransferEncoder: TransferEncoder<unknown, TDiagnostic>;
380
+ TransferEncoder: TransferEncoder<TTransferDiagnostic>;
328
381
  ModelService: ModelService<TAst, TDiagnostic, TTransfer>;
329
382
  };
330
383
  mutableModel.TransferEncoder = transferEncoder;
@@ -346,6 +399,8 @@ export function makeTestServices<
346
399
  transferEncoder,
347
400
  logger,
348
401
  clock,
402
+ serverLocale,
403
+ messageRenderer,
349
404
  dispose: () => {
350
405
  // No-op: the bundled stubs hold only in-memory state (maps, arrays),
351
406
  // released with the bundle when it goes out of scope. The hook exists
@@ -21,8 +21,8 @@
21
21
  * committed sample workspace can **rewrite the sample files**, after which
22
22
  * every later run asserts against already-repaired input: a suite that passes
23
23
  * while testing nothing. The same applies to a fixture built in place, which
24
- * additionally loses its explanatory comments, because the write-back goes
25
- * through the serializer.
24
+ * additionally comes back in the serializer's layout, because the write-back
25
+ * goes through it.
26
26
  *
27
27
  * The second reason is cleanup. Hand-rolled `mkdtempSync` without a matching
28
28
  * `rmSync` leaks a directory per test run into the OS temp dir, which nobody
@@ -49,7 +49,7 @@
49
49
  */
50
50
 
51
51
  import { URI } from '@hydranium/langium';
52
- import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
52
+ import { cpSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
53
53
  import { tmpdir } from 'node:os';
54
54
  import * as path from 'node:path';
55
55
 
@@ -106,7 +106,12 @@ export interface ScratchWorkspace {
106
106
  * returned {@link ScratchWorkspace.dispose} with an `afterEach`.
107
107
  */
108
108
  export function makeScratchWorkspace(options: ScratchWorkspaceOptions = {}): ScratchWorkspace {
109
- const root = mkdtempSync(path.join(tmpdir(), options.prefix ?? 'hydranium-scratch-'));
109
+ // RESOLVED, because `tmpdir()` can itself be a symlink — on macOS it answers
110
+ // `/tmp`, which links to `/private/tmp`. A root the server canonicalises to a
111
+ // different path than the one it was handed registers documents under one
112
+ // spelling and reads them under the other, failing initialization with an
113
+ // ENOENT that names neither the link nor this call.
114
+ const root = realpathSync(mkdtempSync(path.join(tmpdir(), options.prefix ?? 'hydranium-scratch-')));
110
115
  if (options.seed) {
111
116
  cpSync(options.seed, root, { recursive: true });
112
117
  }
@@ -203,6 +203,37 @@ export interface SpawnedServer extends Harness {
203
203
  dispose(): Promise<number | null>;
204
204
  }
205
205
 
206
+ /**
207
+ * Every server started and not yet disposed, so {@link disposeSpawnedServers}
208
+ * can reach one a test forgot.
209
+ *
210
+ * Holds the server — and through it the `ChildProcess` — rather than a pid. A
211
+ * pid identifies a process only while that process is alive: once it exits the
212
+ * OS may reissue the number, and Windows does so within seconds. A teardown
213
+ * that signalled a recorded pid would therefore reach whatever holds it now,
214
+ * which on a concurrent build is another task's compiler or test runner.
215
+ */
216
+ const liveServers = new Set<SpawnedServer>();
217
+
218
+ /**
219
+ * Terminate every server this module started that has not been disposed, and
220
+ * forget them.
221
+ *
222
+ * Safe to call when none are outstanding, and safe to call twice. Nothing here
223
+ * can reach a process this module did not spawn, because every kill goes
224
+ * through the `ChildProcess` handle libuv holds and libuv will not signal a
225
+ * child it has already reaped.
226
+ *
227
+ * A suite that disposes each server as it finishes needs this only as a net; a
228
+ * suite that does not needs it, because an orphan holding a port or a pipe
229
+ * makes the NEXT run's verdict meaningless.
230
+ */
231
+ export async function disposeSpawnedServers(): Promise<void> {
232
+ const outstanding = [...liveServers];
233
+ liveServers.clear();
234
+ await Promise.all(outstanding.map(server => server.dispose()));
235
+ }
236
+
206
237
  /** The captured streams, quoted into a boot-failure message. */
207
238
  function bootDiagnosis(stdoutPrefix: string, stderr: string): string {
208
239
  return (
@@ -390,7 +421,7 @@ export async function startSpawnedServer(options: SpawnedServerOptions): Promise
390
421
  return exitCode;
391
422
  }
392
423
 
393
- return {
424
+ const server: SpawnedServer = {
394
425
  connection,
395
426
  pid: child.pid,
396
427
  initializeResult,
@@ -440,8 +471,11 @@ export async function startSpawnedServer(options: SpawnedServerOptions): Promise
440
471
  });
441
472
  },
442
473
  dispose(): Promise<number | null> {
474
+ liveServers.delete(server);
443
475
  disposal ??= terminate();
444
476
  return disposal;
445
477
  }
446
478
  };
479
+ liveServers.add(server);
480
+ return server;
447
481
  }
@@ -0,0 +1,240 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ import { createServer as createHttpServer, type Server as HttpServer } from 'node:http';
11
+ import { connect, createServer as createTcpServer, type Server as TcpServer, type Socket } from 'node:net';
12
+
13
+ /**
14
+ * A TCP proxy that can half-close a connection, so an e2e suite can reproduce a
15
+ * flaky network against a real browser session.
16
+ *
17
+ * Toggling the browser offline is not enough. On localhost the server sees the
18
+ * close immediately and reaps the socket before the browser reconnects, so the
19
+ * window where both sides disagree about the session — the one where reconnect
20
+ * defects live — never opens. Reaching it needs the browser's socket to die
21
+ * while the server still believes it is alive, and nothing inside the app or in
22
+ * Playwright's offline emulation can produce that asymmetry: both close cleanly
23
+ * at both ends.
24
+ *
25
+ * The proxy holds both halves of every connection and lets a test kill them
26
+ * independently:
27
+ *
28
+ * `break` destroy the BROWSER-side sockets, keep the server-side ones open.
29
+ * The browser reconnects through a fresh pair while the server
30
+ * still holds the old one.
31
+ * `release` destroy the orphaned server-side sockets `break` left. The
32
+ * server's disconnect handler fires at once, rather than whenever
33
+ * its next heartbeat write would have failed.
34
+ */
35
+
36
+ /** Where the proxy listens, and the application it fronts. */
37
+ export interface FlakyNetworkProxyOptions {
38
+ /** Port the browser connects to. */
39
+ readonly listenPort: number;
40
+ /** Port of the application being proxied. */
41
+ readonly targetPort: number;
42
+ /** Host of the application being proxied. Defaults to loopback. */
43
+ readonly targetHost?: string;
44
+ /** Port the HTTP control channel listens on. Defaults to `listenPort + 1000`. */
45
+ readonly controlPort?: number;
46
+ }
47
+
48
+ /** The commands the control channel accepts, and the proxy's own vocabulary. */
49
+ export type FlakyNetworkCommand = 'break' | 'release' | 'status';
50
+
51
+ /** Drives a running proxy. Implemented both in-process and over HTTP. */
52
+ export interface FlakyNetworkControl {
53
+ /** Kill the browser side of every live connection; leave the server side up. */
54
+ break(): Promise<string>;
55
+ /** Kill the server side of every connection `break` orphaned. */
56
+ release(): Promise<string>;
57
+ /** How many connections are live and how many are orphaned. */
58
+ status(): Promise<string>;
59
+ }
60
+
61
+ export interface FlakyNetworkProxy extends FlakyNetworkControl {
62
+ /** Base URL a browser should be pointed at, i.e. Playwright's `baseURL`. */
63
+ readonly url: string;
64
+ /** Control channel, for workers that cannot reach this object directly. */
65
+ readonly controlUrl: string;
66
+ close(): Promise<void>;
67
+ }
68
+
69
+ interface ConnectionPair {
70
+ readonly id: number;
71
+ readonly browserSocket: Socket;
72
+ readonly targetSocket: Socket;
73
+ orphaned: boolean;
74
+ }
75
+
76
+ /**
77
+ * Starts the proxy in the calling process.
78
+ *
79
+ * Call it from a Playwright config, which runs in the main process before any
80
+ * worker forks. Workers are separate processes and cannot reach the returned
81
+ * object, which is what {@link controlUrl} and {@link flakyNetworkControlClient}
82
+ * are for.
83
+ */
84
+ export async function startFlakyNetworkProxy(options: FlakyNetworkProxyOptions): Promise<FlakyNetworkProxy> {
85
+ const targetHost = options.targetHost ?? '127.0.0.1';
86
+ const controlPort = options.controlPort ?? options.listenPort + 1000;
87
+ const pairs = new Set<ConnectionPair>();
88
+ let sequence = 0;
89
+ let transcript: string[] = [];
90
+
91
+ const log = (message: string): void => {
92
+ transcript.push(message);
93
+ console.log(`[flaky-network] ${message}`);
94
+ };
95
+
96
+ const proxyServer: TcpServer = createTcpServer(browserSocket => {
97
+ const id = ++sequence;
98
+ const targetSocket = connect(options.targetPort, targetHost);
99
+ const pair: ConnectionPair = { id, browserSocket, targetSocket, orphaned: false };
100
+ pairs.add(pair);
101
+
102
+ // Forwarded by hand rather than with `pipe()`, so killing one side does not
103
+ // tear the other down — which is the entire point of the proxy.
104
+ browserSocket.on('data', chunk => {
105
+ if (!targetSocket.destroyed) {
106
+ targetSocket.write(chunk);
107
+ }
108
+ });
109
+ targetSocket.on('data', chunk => {
110
+ if (!pair.orphaned && !browserSocket.destroyed) {
111
+ browserSocket.write(chunk);
112
+ }
113
+ });
114
+
115
+ const teardown = (origin: 'browser' | 'target'): void => {
116
+ if (pair.orphaned && origin === 'browser') {
117
+ // Expected: `break` killed this side deliberately. The server side
118
+ // stays up until `release`.
119
+ return;
120
+ }
121
+ pairs.delete(pair);
122
+ browserSocket.destroy();
123
+ targetSocket.destroy();
124
+ };
125
+
126
+ browserSocket.on('close', () => teardown('browser'));
127
+ targetSocket.on('close', () => teardown('target'));
128
+ // A half-killed pair reliably raises ECONNRESET on the surviving side; an
129
+ // unhandled 'error' would take the whole runner down with it.
130
+ browserSocket.on('error', () => undefined);
131
+ targetSocket.on('error', () => undefined);
132
+ });
133
+
134
+ const commands: Record<FlakyNetworkCommand, () => void> = {
135
+ break: () => {
136
+ const live = [...pairs].filter(pair => !pair.orphaned);
137
+ if (live.length === 0) {
138
+ log('nothing to break');
139
+ return;
140
+ }
141
+ for (const pair of live) {
142
+ pair.orphaned = true;
143
+ pair.browserSocket.destroy();
144
+ }
145
+ log(`${live.length} connection(s) broken; the browser should reconnect shortly`);
146
+ },
147
+ release: () => {
148
+ const orphans = [...pairs].filter(pair => pair.orphaned);
149
+ if (orphans.length === 0) {
150
+ log('no orphaned connections; run `break` first');
151
+ return;
152
+ }
153
+ for (const pair of orphans) {
154
+ pairs.delete(pair);
155
+ pair.targetSocket.destroy();
156
+ }
157
+ log(`${orphans.length} orphaned connection(s) destroyed; their late disconnect fires now`);
158
+ },
159
+ status: () => {
160
+ const live = [...pairs].filter(pair => !pair.orphaned).length;
161
+ const orphaned = pairs.size - live;
162
+ log(`${live} live, ${orphaned} orphaned`);
163
+ }
164
+ };
165
+
166
+ const run = (name: string): string => {
167
+ transcript = [];
168
+ const command = commands[name as FlakyNetworkCommand];
169
+ if (command) {
170
+ command();
171
+ } else {
172
+ log(`unknown command '${name}'; use: ${Object.keys(commands).join(' | ')}`);
173
+ }
174
+ return transcript.join('\n');
175
+ };
176
+
177
+ const controlServer: HttpServer = createHttpServer((request, response) => {
178
+ const output = run((request.url ?? '').replace(/^\//, '').split('?')[0]);
179
+ response.writeHead(200, { 'content-type': 'text/plain' });
180
+ response.end(`${output}\n`);
181
+ });
182
+
183
+ await Promise.all([listen(proxyServer, options.listenPort), listen(controlServer, controlPort)]);
184
+ // Neither server keeps the process alive, so a runner that starts one from a
185
+ // config does not need a teardown hook to exit — and a caller that wants a
186
+ // deterministic shutdown still has `close()`.
187
+ proxyServer.unref();
188
+ controlServer.unref();
189
+
190
+ return {
191
+ url: `http://localhost:${options.listenPort}`,
192
+ controlUrl: `http://localhost:${controlPort}`,
193
+ break: async () => run('break'),
194
+ release: async () => run('release'),
195
+ status: async () => run('status'),
196
+ close: async () => {
197
+ for (const pair of pairs) {
198
+ pair.browserSocket.destroy();
199
+ pair.targetSocket.destroy();
200
+ }
201
+ pairs.clear();
202
+ await Promise.all([close(proxyServer), close(controlServer)]);
203
+ }
204
+ };
205
+ }
206
+
207
+ /**
208
+ * Drives a proxy running in another process, over its control channel.
209
+ *
210
+ * This is what a spec uses: Playwright workers are separate processes from the
211
+ * config that started the proxy.
212
+ */
213
+ export function flakyNetworkControlClient(controlUrl: string): FlakyNetworkControl {
214
+ const send = async (command: FlakyNetworkCommand): Promise<string> => {
215
+ const response = await fetch(`${controlUrl}/${command}`);
216
+ if (!response.ok) {
217
+ throw new Error(`flaky-network proxy rejected '${command}': ${response.status}`);
218
+ }
219
+ return (await response.text()).trim();
220
+ };
221
+ return {
222
+ break: () => send('break'),
223
+ release: () => send('release'),
224
+ status: () => send('status')
225
+ };
226
+ }
227
+
228
+ function listen(server: TcpServer | HttpServer, port: number): Promise<void> {
229
+ return new Promise((resolve, reject) => {
230
+ server.once('error', reject);
231
+ server.listen(port, () => {
232
+ server.off('error', reject);
233
+ resolve();
234
+ });
235
+ });
236
+ }
237
+
238
+ function close(server: TcpServer | HttpServer): Promise<void> {
239
+ return new Promise(resolve => server.close(() => resolve()));
240
+ }
@@ -11,6 +11,7 @@
11
11
  // `@hydranium/core/testing` barrel) so importing the framework's unit-test
12
12
  // helpers never pulls in `@playwright/test` — which is an OPTIONAL peer here.
13
13
  export * from './e2e-profiling.js';
14
+ export * from './flaky-network-proxy.js';
14
15
  export * from './browser-capture-bridge.js';
15
16
  export * from './server-log-capture.js';
16
17
  export * from './server-log-fixture.js';
@@ -160,16 +160,28 @@ export function serverLogSpecName(file: string): string {
160
160
  * Rename each `<token>.log` (backend) and `<token>.browser.log` (forwarded
161
161
  * browser console) to the spec name recorded in its `<token>.spec` sidecar
162
162
  * (written by {@link markServerLog}), then delete the sidecar. Colliding names —
163
- * a spec that opened more than one workspace — get a `-2`, `-3`, … suffix. Token
164
- * logs without a sidecar are left untouched. Run once, after all server processes
165
- * have exited (e.g. a reporter's `onEnd`). Best-effort.
163
+ * a spec that opened more than one workspace — get a `-2`, `-3`, … suffix. Run
164
+ * once, after all server processes have exited (e.g. a reporter's `onEnd`).
165
+ * Best-effort.
166
+ *
167
+ * Returns the workspace tokens left UNATTRIBUTED — a log the capture wrote that
168
+ * no test claimed, because that spec never reached
169
+ * {@link markServerLog} (no `serverLog` fixture, no `beforeEach` call).
170
+ *
171
+ * **Returned rather than ignored, because the unattributed state is the
172
+ * misleading one.** Such a log still exists, still has content, and is named by
173
+ * an opaque token with no test boundaries in it — so a capture run that covered
174
+ * nothing looks exactly like one that covered everything, and the logs are
175
+ * there to be read. A caller that can warn should warn.
166
176
  */
167
- export function renameServerLogs(dir: string): void {
177
+ export function renameServerLogs(dir: string): string[] {
168
178
  let entries: string[];
169
179
  try {
170
180
  entries = readdirSync(dir);
171
181
  } catch {
172
- return;
182
+ // An unreadable capture dir has nothing to report — including nothing
183
+ // unattributed, since no log was observed at all.
184
+ return [];
173
185
  }
174
186
  // Per suffix (`.log`, `.browser.log`) a `taken` set guards collisions, so the
175
187
  // backend and browser logs for one spec share the same friendly base name.
@@ -190,6 +202,11 @@ export function renameServerLogs(dir: string): void {
190
202
  // ignore
191
203
  }
192
204
  };
205
+ // Every token the capture produced a backend log for, so the ones no sidecar
206
+ // claims can be reported rather than silently left behind.
207
+ const unattributed = new Set(
208
+ entries.filter(entry => entry.endsWith('.log') && !entry.endsWith('.browser.log')).map(entry => entry.slice(0, -'.log'.length))
209
+ );
193
210
  for (const entry of entries) {
194
211
  if (!entry.endsWith('.spec')) {
195
212
  continue;
@@ -210,9 +227,11 @@ export function renameServerLogs(dir: string): void {
210
227
  if (!name) {
211
228
  continue;
212
229
  }
230
+ unattributed.delete(token);
213
231
  rename(token, name, '.log');
214
232
  rename(token, name, '.browser.log');
215
233
  }
234
+ return [...unattributed];
216
235
  }
217
236
 
218
237
  /**