@crvouga/atlas 0.4.0 → 0.6.0

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 (444) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/README.md +191 -611
  3. package/dist/bun/src/driver.d.ts +41 -7
  4. package/dist/bun/src/driver.js +47 -6
  5. package/dist/bun/src/index.d.ts +8 -0
  6. package/dist/bun/src/index.js +8 -0
  7. package/dist/core/src/cli.d.ts +14 -1
  8. package/dist/core/src/cli.js +35 -1
  9. package/dist/core/src/config.d.ts +77 -4
  10. package/dist/core/src/config.js +110 -17
  11. package/dist/core/src/events/cloudevents.d.ts +54 -9
  12. package/dist/core/src/events/cloudevents.js +60 -9
  13. package/dist/core/src/graph.d.ts +21 -0
  14. package/dist/core/src/graph.js +26 -0
  15. package/dist/core/src/index.d.ts +11 -0
  16. package/dist/core/src/index.js +11 -0
  17. package/dist/core/src/lint.d.ts +30 -0
  18. package/dist/core/src/lint.js +13 -0
  19. package/dist/core/src/plan.d.ts +39 -1
  20. package/dist/core/src/plan.js +28 -1
  21. package/dist/core/src/privacy.d.ts +29 -2
  22. package/dist/core/src/privacy.js +19 -2
  23. package/dist/core/src/report/formats.d.ts +34 -4
  24. package/dist/core/src/report/formats.js +24 -4
  25. package/dist/core/src/report/markdown.d.ts +15 -0
  26. package/dist/core/src/report/markdown.js +8 -0
  27. package/dist/core/src/run/clients.d.ts +44 -5
  28. package/dist/core/src/run/clients.js +46 -2
  29. package/dist/core/src/run/execute.d.ts +75 -16
  30. package/dist/core/src/run/execute.js +71 -18
  31. package/dist/core/src/run/frames.d.ts +13 -4
  32. package/dist/core/src/run/frames.js +19 -4
  33. package/dist/core/src/run/http.d.ts +34 -1
  34. package/dist/core/src/run/http.js +14 -1
  35. package/dist/core/src/run/types.d.ts +76 -3
  36. package/dist/core/src/run/types.js +19 -3
  37. package/dist/core/src/signals.d.ts +49 -5
  38. package/dist/core/src/signals.js +22 -5
  39. package/dist/core/src/spec/compose.d.ts +23 -1
  40. package/dist/core/src/spec/compose.js +10 -1
  41. package/dist/core/src/spec/index.d.ts +9 -1
  42. package/dist/core/src/spec/index.js +9 -1
  43. package/dist/core/src/spec/load.d.ts +22 -1
  44. package/dist/core/src/spec/load.js +31 -1
  45. package/dist/core/src/spec/parse.d.ts +16 -2
  46. package/dist/core/src/spec/parse.js +25 -2
  47. package/dist/core/src/spec/scxml.d.ts +25 -4
  48. package/dist/core/src/spec/scxml.js +36 -4
  49. package/dist/core/src/spec/types.d.ts +52 -1
  50. package/dist/core/src/spec/types.js +24 -0
  51. package/dist/detox/src/index.d.ts +54 -11
  52. package/dist/detox/src/index.js +45 -7
  53. package/dist/playwright/src/driver.d.ts +39 -3
  54. package/dist/playwright/src/driver.js +27 -3
  55. package/dist/playwright/src/index.d.ts +10 -0
  56. package/dist/playwright/src/index.js +10 -0
  57. package/dist/playwright/src/media.d.ts +50 -4
  58. package/dist/playwright/src/media.js +55 -4
  59. package/dist/playwright/src/signals.d.ts +44 -5
  60. package/dist/playwright/src/signals.js +29 -5
  61. package/dist/reports/src/attachment-files.d.ts +44 -0
  62. package/dist/reports/src/attachment-files.js +121 -0
  63. package/dist/reports/src/attachment-response.d.ts +31 -0
  64. package/dist/reports/src/attachment-response.js +55 -0
  65. package/dist/reports/src/aws-credentials.d.ts +48 -0
  66. package/dist/reports/src/aws-credentials.js +221 -0
  67. package/dist/reports/src/browser-navigation.d.ts +14 -0
  68. package/dist/reports/src/browser-navigation.js +7 -0
  69. package/dist/reports/src/bundle.d.ts +34 -2
  70. package/dist/reports/src/bundle.js +38 -2
  71. package/dist/reports/src/catalog.d.ts +34 -3
  72. package/dist/reports/src/catalog.js +17 -3
  73. package/dist/reports/src/cli-stores.d.ts +36 -0
  74. package/dist/reports/src/cli-stores.js +73 -0
  75. package/dist/reports/src/clock.d.ts +7 -2
  76. package/dist/reports/src/clock.js +5 -2
  77. package/dist/reports/src/completion-order.d.ts +18 -5
  78. package/dist/reports/src/completion-order.js +18 -5
  79. package/dist/reports/src/compose.d.ts +80 -0
  80. package/dist/reports/src/compose.js +117 -0
  81. package/dist/reports/src/copy-store.d.ts +29 -0
  82. package/dist/reports/src/copy-store.js +52 -0
  83. package/dist/reports/src/directory-outline.d.ts +38 -0
  84. package/dist/reports/src/directory-outline.js +103 -0
  85. package/dist/reports/src/directory-store.d.ts +20 -0
  86. package/dist/reports/src/directory-store.js +166 -0
  87. package/dist/reports/src/doctor.d.ts +29 -0
  88. package/dist/reports/src/doctor.js +85 -0
  89. package/dist/reports/src/effective-delta.d.ts +34 -0
  90. package/dist/reports/src/effective-delta.js +25 -0
  91. package/dist/reports/src/evidence-detail.d.ts +64 -0
  92. package/dist/reports/src/evidence-detail.js +92 -0
  93. package/dist/reports/src/evidence-live.d.ts +35 -5
  94. package/dist/reports/src/evidence-live.js +18 -2
  95. package/dist/reports/src/evidence-types.d.ts +68 -1
  96. package/dist/reports/src/evidence-types.js +5 -1
  97. package/dist/reports/src/evidence-view.d.ts +43 -4
  98. package/dist/reports/src/evidence-view.js +13 -2
  99. package/dist/reports/src/explorer-types.d.ts +40 -3
  100. package/dist/reports/src/explorer-types.js +8 -0
  101. package/dist/reports/src/explorer-versions.d.ts +9 -1
  102. package/dist/reports/src/explorer-versions.js +18 -1
  103. package/dist/reports/src/export-evidence.d.ts +16 -1
  104. package/dist/reports/src/export-evidence.js +52 -1
  105. package/dist/reports/src/fetcher.d.ts +27 -3
  106. package/dist/reports/src/fetcher.js +11 -3
  107. package/dist/reports/src/format-date.d.ts +6 -0
  108. package/dist/reports/src/format-date.js +6 -0
  109. package/dist/reports/src/format.d.ts +16 -0
  110. package/dist/reports/src/format.js +10 -0
  111. package/dist/reports/src/frames.d.ts +36 -3
  112. package/dist/reports/src/frames.js +22 -3
  113. package/dist/reports/src/hash.d.ts +18 -0
  114. package/dist/reports/src/hash.js +13 -0
  115. package/dist/reports/src/host.d.ts +29 -0
  116. package/dist/reports/src/host.js +64 -0
  117. package/dist/reports/src/html-frame.d.ts +5 -6
  118. package/dist/reports/src/html-frame.js +12 -1
  119. package/dist/reports/src/http-store.d.ts +56 -0
  120. package/dist/reports/src/http-store.js +148 -0
  121. package/dist/reports/src/http.d.ts +7 -0
  122. package/dist/reports/src/http.js +6 -0
  123. package/dist/reports/src/ingest.d.ts +54 -0
  124. package/dist/reports/src/ingest.js +210 -0
  125. package/dist/reports/src/init.d.ts +40 -0
  126. package/dist/reports/src/init.js +369 -0
  127. package/dist/reports/src/junit.d.ts +33 -6
  128. package/dist/reports/src/junit.js +87 -21
  129. package/dist/reports/src/keyboard.d.ts +20 -0
  130. package/dist/reports/src/keyboard.js +11 -0
  131. package/dist/reports/src/keys.d.ts +21 -3
  132. package/dist/reports/src/keys.js +21 -3
  133. package/dist/reports/src/layer-axes.d.ts +35 -5
  134. package/dist/reports/src/layer-axes.js +25 -5
  135. package/dist/reports/src/layer-catalog.d.ts +28 -1
  136. package/dist/reports/src/layer-catalog.js +18 -1
  137. package/dist/reports/src/layer-merge.d.ts +98 -9
  138. package/dist/reports/src/layer-merge.js +147 -31
  139. package/dist/reports/src/layer-source.d.ts +18 -0
  140. package/dist/reports/src/layer-source.js +11 -0
  141. package/dist/reports/src/layer-store.d.ts +42 -0
  142. package/dist/reports/src/layer-store.js +63 -5
  143. package/dist/reports/src/layer-types.d.ts +32 -1
  144. package/dist/reports/src/layers-route.d.ts +47 -4
  145. package/dist/reports/src/layers-route.js +86 -7
  146. package/dist/reports/src/live-reporter.d.ts +34 -0
  147. package/dist/reports/src/live-reporter.js +122 -0
  148. package/dist/reports/src/media-types.d.ts +11 -0
  149. package/dist/reports/src/media-types.js +22 -0
  150. package/dist/reports/src/namespace-guard.d.ts +10 -1
  151. package/dist/reports/src/namespace-guard.js +10 -1
  152. package/dist/reports/src/navigation.d.ts +12 -0
  153. package/dist/reports/src/navigation.js +7 -0
  154. package/dist/reports/src/object-io.d.ts +11 -4
  155. package/dist/reports/src/object-io.js +4 -4
  156. package/dist/reports/src/object-store-conformance.d.ts +27 -0
  157. package/dist/reports/src/object-store-conformance.js +87 -0
  158. package/dist/reports/src/object-store.d.ts +33 -24
  159. package/dist/reports/src/object-store.js +17 -24
  160. package/dist/reports/src/parse-with.d.ts +16 -0
  161. package/dist/reports/src/parse-with.js +14 -0
  162. package/dist/reports/src/performance-part.d.ts +25 -1
  163. package/dist/reports/src/performance-part.js +15 -1
  164. package/dist/reports/src/performance.d.ts +22 -2
  165. package/dist/reports/src/performance.js +12 -2
  166. package/dist/reports/src/persist.d.ts +12 -3
  167. package/dist/reports/src/persist.js +12 -3
  168. package/dist/reports/src/phone.d.ts +18 -1
  169. package/dist/reports/src/phone.js +18 -1
  170. package/dist/reports/src/protocol.d.ts +26 -4
  171. package/dist/reports/src/protocol.js +15 -4
  172. package/dist/reports/src/publication.d.ts +21 -1
  173. package/dist/reports/src/publication.js +21 -1
  174. package/dist/reports/src/publish-checks.d.ts +35 -0
  175. package/dist/reports/src/publish-checks.js +122 -0
  176. package/dist/reports/src/publish.d.ts +42 -9
  177. package/dist/reports/src/publish.js +36 -7
  178. package/dist/reports/src/read.d.ts +46 -16
  179. package/dist/reports/src/read.js +38 -14
  180. package/dist/reports/src/recording-bundle.d.ts +13 -2
  181. package/dist/reports/src/recording-bundle.js +4 -2
  182. package/dist/reports/src/recording-keys.d.ts +15 -1
  183. package/dist/reports/src/recording-keys.js +15 -1
  184. package/dist/reports/src/remote-report.d.ts +28 -7
  185. package/dist/reports/src/remote-report.js +15 -7
  186. package/dist/reports/src/report-cli.d.ts +51 -0
  187. package/dist/reports/src/report-cli.js +486 -0
  188. package/dist/reports/src/report-index.d.ts +99 -0
  189. package/dist/reports/src/report-index.js +337 -0
  190. package/dist/reports/src/report-loader.d.ts +10 -0
  191. package/dist/reports/src/report-loader.js +6 -0
  192. package/dist/reports/src/report-parts.d.ts +29 -3
  193. package/dist/reports/src/report-parts.js +37 -3
  194. package/dist/reports/src/report-status.d.ts +48 -0
  195. package/dist/reports/src/report-status.js +156 -0
  196. package/dist/reports/src/report-types.d.ts +73 -0
  197. package/dist/reports/src/report-types.js +3 -0
  198. package/dist/reports/src/reporter.d.ts +67 -0
  199. package/dist/reports/src/reporter.js +128 -0
  200. package/dist/reports/src/reports-explorer-route.d.ts +541 -0
  201. package/dist/reports/src/reports-explorer-route.js +11 -0
  202. package/dist/reports/src/reports-list-route.d.ts +9 -0
  203. package/dist/reports/src/reports-list-route.js +9 -0
  204. package/dist/reports/src/reports-route.d.ts +561 -22
  205. package/dist/reports/src/reports-route.js +11 -3
  206. package/dist/reports/src/reports-store.d.ts +19 -2
  207. package/dist/reports/src/reports-store.js +10 -2
  208. package/dist/reports/src/resilient-cache.d.ts +14 -1
  209. package/dist/reports/src/resilient-cache.js +8 -1
  210. package/dist/reports/src/retention.d.ts +17 -1
  211. package/dist/reports/src/retention.js +5 -1
  212. package/dist/reports/src/run-bundle.d.ts +63 -0
  213. package/dist/reports/src/run-bundle.js +159 -0
  214. package/dist/reports/src/s3-store.d.ts +79 -0
  215. package/dist/reports/src/s3-store.js +295 -0
  216. package/dist/reports/src/scenario.d.ts +14 -4
  217. package/dist/reports/src/scenario.js +5 -4
  218. package/dist/reports/src/sigv4.d.ts +72 -0
  219. package/dist/reports/src/sigv4.js +96 -0
  220. package/dist/reports/src/snapshot-bundle.d.ts +24 -1
  221. package/dist/reports/src/snapshot-bundle.js +27 -2
  222. package/dist/reports/src/snapshot-surface.d.ts +20 -9
  223. package/dist/reports/src/snapshot-surface.js +32 -19
  224. package/dist/reports/src/source-content.d.ts +23 -0
  225. package/dist/reports/src/source-content.js +30 -1
  226. package/dist/reports/src/source-entries.d.ts +13 -0
  227. package/dist/reports/src/source-entries.js +6 -0
  228. package/dist/reports/src/source-part.d.ts +15 -0
  229. package/dist/reports/src/source-part.js +8 -0
  230. package/dist/reports/src/source-reader.d.ts +11 -0
  231. package/dist/reports/src/source-reader.js +5 -0
  232. package/dist/reports/src/spec-evidence.d.ts +12 -2
  233. package/dist/reports/src/spec-evidence.js +6 -2
  234. package/dist/reports/src/spec-inventory.d.ts +15 -1
  235. package/dist/reports/src/spec-inventory.js +22 -1
  236. package/dist/reports/src/spec-link.d.ts +28 -0
  237. package/dist/reports/src/spec-link.js +70 -0
  238. package/dist/reports/src/spec-set.d.ts +63 -0
  239. package/dist/reports/src/spec-set.js +177 -0
  240. package/dist/reports/src/status.d.ts +10 -0
  241. package/dist/reports/src/status.js +5 -0
  242. package/dist/reports/src/store-prefix.d.ts +28 -0
  243. package/dist/reports/src/store-prefix.js +22 -0
  244. package/dist/reports/src/store-url.d.ts +89 -0
  245. package/dist/reports/src/store-url.js +160 -0
  246. package/dist/reports/src/test-catalog-entity.d.ts +11 -0
  247. package/dist/reports/src/test-catalog-entity.js +6 -0
  248. package/dist/reports/src/types.d.ts +6 -1
  249. package/dist/reports/src/use-report-nav.d.ts +35 -0
  250. package/dist/reports/src/use-report-nav.js +12 -0
  251. package/dist/runners/src/codecept.d.ts +14 -0
  252. package/dist/runners/src/codecept.js +16 -0
  253. package/dist/runners/src/jest.d.ts +50 -0
  254. package/dist/runners/src/jest.js +52 -0
  255. package/dist/runners/src/mocha.d.ts +40 -0
  256. package/dist/runners/src/mocha.js +39 -0
  257. package/dist/runners/src/node-test.d.ts +28 -0
  258. package/dist/runners/src/node-test.js +44 -0
  259. package/dist/runners/src/publish.d.ts +30 -0
  260. package/dist/runners/src/publish.js +68 -0
  261. package/dist/schema/src/api.d.ts +800 -0
  262. package/dist/schema/src/api.js +201 -0
  263. package/dist/schema/src/cli-results.d.ts +210 -0
  264. package/dist/schema/src/cli-results.js +124 -0
  265. package/dist/schema/src/common.d.ts +53 -0
  266. package/dist/schema/src/common.js +42 -0
  267. package/dist/schema/src/index.d.ts +16 -0
  268. package/dist/schema/src/index.js +16 -0
  269. package/dist/schema/src/layer.d.ts +2273 -0
  270. package/dist/schema/src/layer.js +462 -0
  271. package/dist/schema/src/live-run.d.ts +123 -0
  272. package/dist/schema/src/live-run.js +30 -0
  273. package/dist/schema/src/manifest.d.ts +97 -2
  274. package/dist/schema/src/manifest.js +84 -0
  275. package/dist/schema/src/project.d.ts +20 -0
  276. package/dist/schema/src/project.js +10 -0
  277. package/dist/schema/src/report-sources.d.ts +29 -0
  278. package/dist/schema/src/report-sources.js +27 -1
  279. package/dist/schema/src/runs-index.d.ts +17 -1
  280. package/dist/schema/src/runs-index.js +15 -0
  281. package/dist/schema/src/server-config.d.ts +177 -0
  282. package/dist/schema/src/server-config.js +85 -0
  283. package/dist/schema/src/spec.d.ts +69 -0
  284. package/dist/schema/src/spec.js +58 -0
  285. package/dist/server/src/app.d.ts +72 -0
  286. package/dist/server/src/app.js +557 -0
  287. package/dist/server/src/auth.d.ts +41 -0
  288. package/dist/server/src/auth.js +59 -0
  289. package/dist/server/src/config.d.ts +83 -0
  290. package/dist/server/src/config.js +204 -0
  291. package/dist/server/src/dev.d.ts +12 -0
  292. package/dist/server/src/dev.js +89 -0
  293. package/dist/server/src/events.d.ts +29 -0
  294. package/dist/server/src/events.js +101 -0
  295. package/dist/server/src/host.d.ts +30 -0
  296. package/dist/server/src/host.js +115 -0
  297. package/dist/server/src/index.d.ts +18 -0
  298. package/dist/server/src/index.js +18 -0
  299. package/dist/server/src/live-runs.d.ts +52 -0
  300. package/dist/server/src/live-runs.js +229 -0
  301. package/dist/server/src/llms.d.ts +19 -0
  302. package/dist/server/src/llms.js +63 -0
  303. package/dist/server/src/main.d.ts +30 -0
  304. package/dist/server/src/main.js +114 -0
  305. package/dist/server/src/openapi.d.ts +56 -0
  306. package/dist/server/src/openapi.js +438 -0
  307. package/dist/server/src/problem.d.ts +8 -0
  308. package/dist/server/src/problem.js +39 -0
  309. package/dist/server/src/projects.d.ts +3 -0
  310. package/dist/server/src/projects.js +154 -0
  311. package/dist/server/src/settings-page.d.ts +9 -0
  312. package/dist/server/src/settings-page.js +93 -0
  313. package/dist/server/src/settings.d.ts +23 -0
  314. package/dist/server/src/settings.js +72 -0
  315. package/dist/server/src/site.d.ts +15 -0
  316. package/dist/server/src/site.js +78 -0
  317. package/dist/server/src/store-api.d.ts +28 -0
  318. package/dist/server/src/store-api.js +180 -0
  319. package/dist/server/src/validate.d.ts +71 -0
  320. package/dist/server/src/validate.js +197 -0
  321. package/dist/vitest/src/index.d.ts +27 -5
  322. package/dist/vitest/src/index.js +18 -5
  323. package/dist/webdriver/src/driver.d.ts +63 -11
  324. package/dist/webdriver/src/driver.js +57 -6
  325. package/dist/webdriver/src/index.d.ts +8 -0
  326. package/dist/webdriver/src/index.js +1 -0
  327. package/dist/webdriver/src/protocol.d.ts +75 -6
  328. package/dist/webdriver/src/protocol.js +79 -6
  329. package/docs/agents.md +137 -0
  330. package/docs/deploying.md +345 -0
  331. package/docs/dogfooding.md +175 -0
  332. package/docs/extending.md +242 -0
  333. package/docs/integrating.md +485 -0
  334. package/docs/reference/bun.md +141 -0
  335. package/docs/reference/cli.md +148 -0
  336. package/docs/reference/core.md +2432 -0
  337. package/docs/reference/detox.md +150 -0
  338. package/docs/reference/http-api.md +799 -0
  339. package/docs/reference/playwright.md +303 -0
  340. package/docs/reference/reports.md +7152 -0
  341. package/docs/reference/runners.md +169 -0
  342. package/docs/reference/schema.md +1762 -0
  343. package/docs/reference/server.md +825 -0
  344. package/docs/reference/vitest.md +45 -0
  345. package/docs/reference/webdriver.md +330 -0
  346. package/docs/report-backends.md +144 -0
  347. package/docs/report-contract.md +400 -0
  348. package/docs/reporting-issues.md +160 -0
  349. package/docs/runners.md +69 -0
  350. package/docs/server.md +321 -0
  351. package/docs/statecharts.md +643 -0
  352. package/llms-full.txt +4114 -0
  353. package/llms.txt +49 -0
  354. package/package.json +61 -11
  355. package/packages/core/README.md +2 -1
  356. package/packages/reports/README.md +65 -0
  357. package/packages/reports/STORAGE.md +4 -0
  358. package/packages/runners/README.md +6 -0
  359. package/packages/runners/compat/codecept.cjs +2 -0
  360. package/packages/runners/compat/jest.cjs +2 -0
  361. package/packages/runners/compat/mocha.cjs +2 -0
  362. package/packages/schema/README.md +14 -0
  363. package/packages/schema/schemas/atlas-change.schema.json +4 -0
  364. package/packages/schema/schemas/check-detail.schema.json +1032 -0
  365. package/packages/schema/schemas/checks-result.schema.json +26 -0
  366. package/packages/schema/schemas/command-error.schema.json +35 -0
  367. package/packages/schema/schemas/doctor-result.schema.json +60 -0
  368. package/packages/schema/schemas/effective-delta.schema.json +490 -0
  369. package/packages/schema/schemas/effective-evidence.schema.json +471 -0
  370. package/packages/schema/schemas/evidence-page.schema.json +271 -0
  371. package/packages/schema/schemas/hello-event.schema.json +26 -0
  372. package/packages/schema/schemas/init-plan.schema.json +347 -0
  373. package/packages/schema/schemas/layer-rows.schema.json +199 -0
  374. package/packages/schema/schemas/layer.schema.json +2888 -0
  375. package/packages/schema/schemas/live-run-start.schema.json +42 -0
  376. package/packages/schema/schemas/live-run-update.schema.json +93 -0
  377. package/packages/schema/schemas/live-run.schema.json +225 -0
  378. package/packages/schema/schemas/merge-result.schema.json +40 -0
  379. package/packages/schema/schemas/outline.schema.json +62 -0
  380. package/packages/schema/schemas/problem.schema.json +37 -0
  381. package/packages/schema/schemas/project-slug.schema.json +8 -0
  382. package/packages/schema/schemas/project.schema.json +22 -0
  383. package/packages/schema/schemas/projects.schema.json +33 -0
  384. package/packages/schema/schemas/publish-checks.schema.json +1413 -0
  385. package/packages/schema/schemas/publish-result.schema.json +168 -0
  386. package/packages/schema/schemas/ready.schema.json +68 -0
  387. package/packages/schema/schemas/report-event.schema.json +100 -0
  388. package/packages/schema/schemas/report-status.schema.json +370 -0
  389. package/packages/schema/schemas/server-config.schema.json +208 -0
  390. package/packages/schema/schemas/server-editable-settings.schema.json +132 -0
  391. package/packages/schema/schemas/server-info.schema.json +114 -0
  392. package/packages/schema/schemas/server-settings-snapshot.schema.json +285 -0
  393. package/packages/schema/schemas/spec-set.schema.json +103 -0
  394. package/packages/schema/schemas/store-listing.schema.json +29 -0
  395. package/packages/schema/schemas/store-write-result.schema.json +28 -0
  396. package/packages/schema/schemas/stream-result.schema.json +59 -0
  397. package/packages/server/README.md +43 -0
  398. package/packages/server/openapi.json +11021 -0
  399. package/packages/visualizer/README.md +32 -11
  400. package/packages/visualizer/bin/atlas-client.mjs +176 -0
  401. package/packages/visualizer/bin/atlas-visualizer.mjs +2 -175
  402. package/packages/visualizer/src/app/AppShell.tsx +32 -16
  403. package/packages/visualizer/src/app/report-sources.tsx +7 -4
  404. package/packages/visualizer/src/app/router.tsx +3 -3
  405. package/packages/visualizer/src/components/ActivityIndicator.module.css +4 -0
  406. package/packages/visualizer/src/components/ActivityIndicator.tsx +6 -0
  407. package/packages/visualizer/src/components/Header.module.css +101 -192
  408. package/packages/visualizer/src/components/Header.tsx +88 -13
  409. package/packages/visualizer/src/components/ProjectPicker.tsx +64 -0
  410. package/packages/visualizer/src/data/adapters/adapter.ts +5 -17
  411. package/packages/visualizer/src/data/adapters/index.ts +10 -1
  412. package/packages/visualizer/src/data/adapters/report-backend.test.ts +3 -2
  413. package/packages/visualizer/src/data/adapters/report-backend.ts +1 -1
  414. package/packages/visualizer/src/data/queries.ts +14 -7
  415. package/packages/visualizer/src/evidence/api.ts +16 -3
  416. package/packages/visualizer/src/evidence/artifacts.module.css +213 -0
  417. package/packages/visualizer/src/evidence/artifacts.tsx +272 -0
  418. package/packages/visualizer/src/evidence/check-detail.tsx +21 -4
  419. package/packages/visualizer/src/evidence/document-page.tsx +6 -2
  420. package/packages/visualizer/src/evidence/evidence-context.tsx +79 -12
  421. package/packages/visualizer/src/evidence/evidence.module.css +11 -1
  422. package/packages/visualizer/src/evidence/feature-page.tsx +7 -3
  423. package/packages/visualizer/src/evidence/model-page.tsx +6 -2
  424. package/packages/visualizer/src/evidence/project-home.tsx +45 -0
  425. package/packages/visualizer/src/evidence/recording-page.tsx +4 -1
  426. package/packages/visualizer/src/evidence/spec-index.tsx +2 -1
  427. package/packages/visualizer/src/evidence/spec-page.tsx +23 -16
  428. package/packages/visualizer/src/evidence/test-filter.tsx +21 -0
  429. package/packages/visualizer/src/find/Finder.tsx +1 -7
  430. package/packages/visualizer/src/find/open.ts +4 -0
  431. package/packages/visualizer/src/live/live-query.ts +21 -0
  432. package/packages/visualizer/src/live/live-runs.tsx +69 -0
  433. package/packages/visualizer/src/live/live-stream.ts +225 -0
  434. package/packages/visualizer/src/live/report-progress.ts +62 -0
  435. package/packages/visualizer/src/live/test-progress.tsx +34 -0
  436. package/packages/visualizer/src/runtime.ts +35 -0
  437. package/packages/visualizer/src/state/ui-store.ts +3 -2
  438. package/packages/visualizer/src/views/ChartPage.tsx +2 -0
  439. package/packages/visualizer/src/views/ChartTests.tsx +43 -0
  440. package/packages/visualizer/src/views/SidePanel.tsx +2 -0
  441. package/packages/visualizer/vite/atlas-files.test.ts +4 -4
  442. package/packages/visualizer/vite/atlas-files.ts +1 -5
  443. package/packages/visualizer/vite/atlas-server-proxy.ts +37 -0
  444. package/packages/visualizer/vite.config.ts +11 -3
@@ -0,0 +1,225 @@
1
+ import { AtlasChangeSchema, LiveRunSchema, type LiveRun, type AtlasChange } from '@crvouga/atlas/schema';
2
+ import { useSyncExternalStore } from 'react';
3
+
4
+ import { runtimeConfig, runtimeUrl } from '../runtime';
5
+
6
+ /** What the server tells a viewer as it happens. */
7
+ export type LiveEvent =
8
+ | { readonly type: 'open' }
9
+ | { readonly type: 'run'; readonly run: LiveRun }
10
+ | { readonly type: 'projects' }
11
+ | { readonly type: 'report'; readonly report: ReportAnnouncement }
12
+ | { readonly type: 'change'; readonly change: AtlasChange };
13
+
14
+ /** One announcement that results arrived: enough to say who sent them without asking again. */
15
+ export type ReportAnnouncement = {
16
+ readonly version: string;
17
+ readonly totalLayers: number;
18
+ readonly addedTotal: number;
19
+ readonly added: readonly {
20
+ readonly id: string;
21
+ readonly producer: string;
22
+ readonly environment?: string | null;
23
+ readonly branch?: string | null;
24
+ readonly counts?: Readonly<Record<string, number>>;
25
+ }[];
26
+ };
27
+
28
+ /** `connecting` before the first answer, `live` while the stream is open, `reconnecting` after it was lost. */
29
+ export type LiveStatus = 'off' | 'connecting' | 'live' | 'reconnecting';
30
+
31
+ type Listener = (event: LiveEvent) => void;
32
+
33
+ /** The server sends a comment every 15 seconds; this long without a word means the connection is dead. */
34
+ const SILENCE_MS = 45_000;
35
+ const BACKOFF_MS = [1_000, 2_000, 5_000, 10_000, 30_000] as const;
36
+
37
+ type Stream = {
38
+ readonly url: string;
39
+ readonly listeners: Set<Listener>;
40
+ source: EventSource | null;
41
+ status: LiveStatus;
42
+ attempts: number;
43
+ timer: ReturnType<typeof setTimeout> | null;
44
+ watchdog: ReturnType<typeof setTimeout> | null;
45
+ };
46
+
47
+ const streams = new Map<string, Stream>();
48
+ const statusListeners = new Set<() => void>();
49
+
50
+ function setStatus(stream: Stream, status: LiveStatus) {
51
+ if (stream.status === status) return;
52
+ stream.status = status;
53
+ for (const listener of statusListeners) listener();
54
+ }
55
+
56
+ function emit(stream: Stream, event: LiveEvent) {
57
+ for (const listener of [...stream.listeners]) {
58
+ try {
59
+ listener(event);
60
+ } catch {
61
+ // One listener's mistake must not stop the others hearing.
62
+ }
63
+ }
64
+ }
65
+
66
+ function parse(data: string): unknown {
67
+ try {
68
+ return JSON.parse(data) as unknown;
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ function connect(stream: Stream) {
75
+ if (typeof EventSource === 'undefined') return;
76
+ stream.timer = null;
77
+ const source = new EventSource(stream.url);
78
+ stream.source = source;
79
+ let silenceMs = SILENCE_MS;
80
+ const on = (name: string, handler: (event: Event) => void) => source.addEventListener(name, event => {
81
+ if (stream.source === source) handler(event);
82
+ });
83
+ const heard = () => {
84
+ if (stream.watchdog) clearTimeout(stream.watchdog);
85
+ stream.watchdog = setTimeout(() => lose(stream), silenceMs);
86
+ };
87
+ on('open', () => {
88
+ heard();
89
+ stream.attempts = 0;
90
+ setStatus(stream, 'live');
91
+ // Anything may have happened while the stream was away, so a (re)connection reads everything again.
92
+ emit(stream, { type: 'open' });
93
+ });
94
+ on('hello', event => {
95
+ const hello = parse((event as MessageEvent<string>).data) as { heartbeatMs?: unknown } | null;
96
+ if (typeof hello?.heartbeatMs === 'number' && Number.isFinite(hello.heartbeatMs) && hello.heartbeatMs > 0) silenceMs = Math.max(SILENCE_MS, hello.heartbeatMs * 3);
97
+ heard();
98
+ });
99
+ on('ping', heard);
100
+ on('report', (event) => {
101
+ heard();
102
+ const report = parse((event as MessageEvent<string>).data) as ReportAnnouncement | null;
103
+ if (report && typeof report.version === 'string') emit(stream, { type: 'report', report: { ...report, added: Array.isArray(report.added) ? report.added : [] } });
104
+ });
105
+ on('run', event => {
106
+ heard();
107
+ const run = LiveRunSchema.safeParse(parse((event as MessageEvent<string>).data));
108
+ if (run.success) emit(stream, { type: 'run', run: run.data });
109
+ });
110
+ on('projects', () => { heard(); emit(stream, { type: 'projects' }); });
111
+ on('atlas:change', (event) => {
112
+ heard();
113
+ const change = AtlasChangeSchema.safeParse(parse((event as MessageEvent<string>).data));
114
+ if (change.success) emit(stream, { type: 'change', change: change.data });
115
+ });
116
+ // The server's heartbeat is a named `ping` event, so a quiet server is told apart from a dead connection.
117
+ on('error', () => {
118
+ setStatus(stream, stream.attempts === 0 && stream.status === 'connecting' ? 'connecting' : 'reconnecting');
119
+ // The browser retries a dropped stream by itself. A refused one (401, 503) it gives up on: retry that here, slowly.
120
+ if (source.readyState === EventSource.CLOSED) lose(stream);
121
+ });
122
+ heard();
123
+ }
124
+
125
+ function lose(stream: Stream) {
126
+ if (stream.watchdog) clearTimeout(stream.watchdog);
127
+ stream.watchdog = null;
128
+ stream.source?.close();
129
+ stream.source = null;
130
+ if (!stream.listeners.size) return;
131
+ setStatus(stream, 'reconnecting');
132
+ if (stream.timer) return;
133
+ const wait = BACKOFF_MS[Math.min(stream.attempts, BACKOFF_MS.length - 1)]!;
134
+ stream.attempts += 1;
135
+ stream.timer = setTimeout(() => connect(stream), wait);
136
+ }
137
+
138
+ function release(stream: Stream) {
139
+ if (stream.listeners.size) return;
140
+ if (stream.timer) clearTimeout(stream.timer);
141
+ if (stream.watchdog) clearTimeout(stream.watchdog);
142
+ stream.source?.close();
143
+ stream.source = null;
144
+ streams.delete(stream.url);
145
+ setStatus(stream, 'off');
146
+ }
147
+
148
+ /**
149
+ * Hear what the server at `url` announces. Everything in the page that listens to one address shares one
150
+ * connection, which is opened with the first listener and closed with the last.
151
+ */
152
+ export function listenLive(url: string, listener: Listener): () => void {
153
+ let stream = streams.get(url);
154
+ if (!stream) {
155
+ stream = { url, listeners: new Set(), source: null, status: 'connecting', attempts: 0, timer: null, watchdog: null };
156
+ streams.set(url, stream);
157
+ stream.listeners.add(listener);
158
+ connect(stream);
159
+ for (const notify of statusListeners) notify();
160
+ } else {
161
+ stream.listeners.add(listener);
162
+ }
163
+ const mine = stream;
164
+ return () => {
165
+ mine.listeners.delete(listener);
166
+ release(mine);
167
+ };
168
+ }
169
+
170
+ /** Changes to specs and runs, in the form the data layer already understands. */
171
+ export function listenChanges(url: string, onChange: (change: AtlasChange) => void): () => void {
172
+ return listenLive(url, (event) => {
173
+ if (event.type === 'change') onChange(event.change);
174
+ // A fresh connection may have missed changes while it was away.
175
+ if (event.type === 'open') {
176
+ onChange({ scope: 'specs', files: [], runIds: [] });
177
+ onChange({ scope: 'runs', files: [], runIds: [] });
178
+ }
179
+ if (event.type === 'report') onChange({ scope: 'runs', files: [], runIds: [] });
180
+ });
181
+ }
182
+
183
+ /** The address of this server's event stream, when the page has one. */
184
+ export const eventsAddress = () => runtimeUrl(runtimeConfig()?.eventsUrl);
185
+
186
+ function snapshot(): LiveStatus {
187
+ const url = eventsAddress();
188
+ return (url && streams.get(url)?.status) || (url ? 'connecting' : 'off');
189
+ }
190
+
191
+ function subscribeStatus(listener: () => void) {
192
+ statusListeners.add(listener);
193
+ return () => statusListeners.delete(listener);
194
+ }
195
+
196
+ /** Whether the report on screen is being kept up to date by the server. `off` for a static site. */
197
+ export function useLiveStatus(): LiveStatus {
198
+ return useSyncExternalStore(subscribeStatus, snapshot, () => 'off');
199
+ }
200
+
201
+ /** Test seam: forget every stream. */
202
+ export function resetLiveStreams() {
203
+ for (const stream of streams.values()) {
204
+ stream.listeners.clear();
205
+ release(stream);
206
+ }
207
+ streams.clear();
208
+ }
209
+
210
+ /**
211
+ * `act` at once, and then no more than once per `ms` however often it is called: a call made inside the wait
212
+ * runs when the wait ends. The last word is never lost.
213
+ */
214
+ export function throttled(act: () => void, ms: number): (() => void) & { cancel: () => void } {
215
+ let last = -Infinity;
216
+ let timer: ReturnType<typeof setTimeout> | null = null;
217
+ const call = () => {
218
+ if (timer) return;
219
+ const wait = last + ms - Date.now();
220
+ if (wait <= 0) { last = Date.now(); act(); return; }
221
+ timer = setTimeout(() => { timer = null; last = Date.now(); act(); }, wait);
222
+ };
223
+ call.cancel = () => { if (timer) clearTimeout(timer); timer = null; };
224
+ return call;
225
+ }
@@ -0,0 +1,62 @@
1
+ import { addSummaries, summarizeSlots, type CheckSlot, type EvidenceView, type SpecEvidence } from '@crvouga/atlas/reports/evidence-view';
2
+ import type { LiveTest } from './live-runs';
3
+
4
+ type LiveIndex = ReadonlyMap<string, readonly LiveTest[]>;
5
+ const testKey = (id: string, file: string | null) => JSON.stringify([file, id]);
6
+
7
+ /** Build once per server update so rendering a row only looks up that test's executions. */
8
+ export function indexLiveTests(tests: readonly LiveTest[]): LiveIndex {
9
+ const index = new Map<string, LiveTest[]>();
10
+ for (const test of tests) {
11
+ const key = testKey(test.check.id, test.check.specPath ?? null);
12
+ const values = index.get(key);
13
+ if (values) values.push(test);
14
+ else index.set(key, [test]);
15
+ }
16
+ return index;
17
+ }
18
+
19
+ /** Match identity and spec, never a display title: different specs can use the same test name. */
20
+ export function testsForSlot(index: LiveIndex, slot: Pick<CheckSlot, 'id' | 'specPath' | 'latestAtMs'>) {
21
+ const seen = new Set<string>();
22
+ return (index.get(testKey(slot.id, slot.specPath)) ?? []).filter(({ run }) => {
23
+ const identity = JSON.stringify([run.producer, run.environment, run.branch]);
24
+ if (seen.has(identity)) return false;
25
+ seen.add(identity);
26
+ return run.status === 'running' || (run.finishedAtMs ?? run.lastProgressAtMs) >= slot.latestAtMs;
27
+ });
28
+ }
29
+
30
+ /** Display current execution beside history without changing immutable layers or report composition. */
31
+ export function withLiveProgress(view: EvidenceView, tests: readonly LiveTest[]): EvidenceView {
32
+ if (!tests.length) return view;
33
+ const specs = new Map(view.specs);
34
+ const index = indexLiveTests(tests);
35
+ const bySpec = new Map<string | null, Map<string, CheckSlot>>();
36
+ for (const { check } of tests) {
37
+ const file = check.specPath ?? null;
38
+ if (!bySpec.has(file)) bySpec.set(file, new Map((file ? specs.get(file)?.slots ?? [] : view.unassigned).map(slot => [slot.id, slot])));
39
+ const slots = bySpec.get(file)!;
40
+ if (!slots.has(check.id)) slots.set(check.id, { key: testKey(check.id, file), id: check.id, title: check.title, specPath: file,
41
+ status: check.status, outcomes: [], diagnostics: [], latestAtMs: 0, live: false });
42
+ }
43
+ const update = (slots: CheckSlot[]) => slots.map(slot => {
44
+ const matches = testsForSlot(index, slot);
45
+ if (!matches.length) return slot;
46
+ const running = matches.some(({ run, check }) => run.status === 'running' && check.status === 'running');
47
+ const latest = matches[0]!;
48
+ const status = running ? 'running' : slot.outcomes.length ? slot.status : latest.check.status === 'running' ? 'not-run' : latest.check.status;
49
+ const interrupted = matches.some(({ run, check }) => run.status === 'interrupted' && check.status === 'not-run');
50
+ return { ...slot, status: interrupted && !running ? 'not-run' as const : status, live: running,
51
+ latestAtMs: Math.max(slot.latestAtMs, ...matches.map(({ run }) => run.lastProgressAtMs)) };
52
+ });
53
+ let unassigned = view.unassigned;
54
+ for (const [file, slots] of bySpec) {
55
+ const updated = update([...slots.values()]);
56
+ if (file) {
57
+ const entry: SpecEvidence = { slots: updated, summary: summarizeSlots(updated) };
58
+ specs.set(file, entry);
59
+ } else unassigned = updated;
60
+ }
61
+ return { specs, unassigned, total: addSummaries([...specs.values()].map(entry => entry.summary)) };
62
+ }
@@ -0,0 +1,34 @@
1
+ import type { CheckSlot } from '@crvouga/atlas/reports/evidence-view';
2
+ import { StatusMark } from '../evidence/marks';
3
+ import styles from '../evidence/evidence.module.css';
4
+ import { useLiveTests, type LiveTest } from './live-runs';
5
+ import { testsForSlot } from './report-progress';
6
+
7
+ export function useTestProgress(slot: CheckSlot | null) {
8
+ const { index, error } = useLiveTests();
9
+ return { tests: slot ? testsForSlot(index, slot) : [], error };
10
+ }
11
+
12
+ /** A test's own progress log. Historical evidence stays available below it. */
13
+ export function TestProgress({ tests, error }: { tests: readonly LiveTest[]; error?: string | null }) {
14
+ if (!tests.length) return null;
15
+ return <div aria-label="Test progress">
16
+ {tests.map(({ run, check }) => {
17
+ const running = run.status === 'running' && check.status === 'running';
18
+ const interrupted = run.status === 'interrupted' && check.status === 'not-run';
19
+ const status = running ? 'running' : interrupted ? 'not-run' : check.status === 'running' ? 'not-run' : check.status;
20
+ return <div key={`${run.id}:${check.id}`}>
21
+ <p className={styles.detailMeta} role="status"><StatusMark status={status} />
22
+ <strong>{running ? 'Running' : interrupted ? 'Interrupted' : status === 'passed' ? 'Passed' : status === 'failed' ? 'Failed' : 'Not run'}</strong>
23
+ <span>{run.producer}{run.environment ? ` · ${run.environment}` : ''}</span>
24
+ </p>
25
+ {check.detail && check.detail !== run.reason && <pre className={styles.pre}>{check.detail}</pre>}
26
+ {!!check.steps?.length && <ol className={styles.steps} aria-label="Progress log">{check.steps.map((step, index) => <li key={index}>
27
+ <div className={styles.stepRow}><StatusMark status={step.status === 'passed' || step.status === 'failed' || step.status === 'running' ? step.status : 'none'} size={14} /><span className={styles.stepText}>{step.name}</span></div>
28
+ </li>)}</ol>}
29
+ {interrupted && <p className={styles.notice} role="status">{run.reason ?? 'The test run was interrupted.'} No completed outcome was received for this test.</p>}
30
+ </div>;
31
+ })}
32
+ {error && <p className={styles.notice} role="alert">{error}</p>}
33
+ </div>;
34
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The address book an Atlas server writes into the page it serves (`window.__ATLAS_CONFIG__`), so one build
3
+ * of the viewer works for every project and on every host. Under `atlas dev` the Vite dev server writes the
4
+ * same thing, so the viewer cannot tell the two apart: both are one host, with `/api` behind it.
5
+ */
6
+ export type RuntimeConfig = {
7
+ readonly api?: string;
8
+ readonly reportsUrl?: string;
9
+ readonly dataUrl?: string;
10
+ readonly eventsUrl?: string;
11
+ readonly effective?: boolean;
12
+ readonly projectsUrl?: string;
13
+ };
14
+
15
+ /** The project selected in the page address. A full navigation on change isolates caches and connections. */
16
+ export const currentProject = () => typeof window === 'undefined' ? null : new URL(window.location.href).searchParams.get('project');
17
+
18
+ export const runtimeConfig = (): RuntimeConfig | undefined => {
19
+ const config = (globalThis as { __ATLAS_CONFIG__?: RuntimeConfig }).__ATLAS_CONFIG__;
20
+ const project = currentProject();
21
+ if (!config?.api || !project || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(project) || project.length > 64) return config;
22
+ const api = new URL(config.api, window.location.href).href.replace(/\/$/, '');
23
+ const scoped = `${api}/projects/${project}`;
24
+ const scope = (address: string | undefined) => {
25
+ if (!address) return address;
26
+ const absolute = new URL(address, window.location.href).href;
27
+ return absolute === api || absolute.startsWith(`${api}/`) ? scoped + absolute.slice(api.length) : absolute;
28
+ };
29
+ return { ...config, api: scoped, reportsUrl: scope(config.reportsUrl), dataUrl: scope(config.dataUrl), eventsUrl: scope(config.eventsUrl) };
30
+ };
31
+
32
+ /** An address from the config, resolved against the page, as an absolute URL. */
33
+ export function runtimeUrl(value: string | undefined): string | undefined {
34
+ return value ? new URL(value, window.location.href).href : undefined;
35
+ }
@@ -2,9 +2,10 @@ import { ALL_SOURCES, type SourceSelection } from '@crvouga/atlas/reports/eviden
2
2
  import type { Viewport } from '@xyflow/react';
3
3
  import { create } from 'zustand';
4
4
  import { createJSONStorage, persist } from 'zustand/middleware';
5
+ import { currentProject } from '../runtime';
5
6
 
6
7
  /** Which specs the index lists, by what has been recorded for them. */
7
- export type ResultFilter = 'all' | 'results' | 'failed' | 'none';
8
+ export type ResultFilter = 'all' | 'results' | 'running' | 'failed' | 'none';
8
9
 
9
10
  type UiState = {
10
11
  viewports: Record<string, Viewport>;
@@ -36,7 +37,7 @@ export const useUiStore = create<UiState>()(
36
37
  setExcluded: (excluded) => set({ excluded })
37
38
  }),
38
39
  {
39
- name: 'atlas-ui',
40
+ name: currentProject() ? `atlas-ui:${currentProject()}` : 'atlas-ui',
40
41
  storage: createJSONStorage(() => sessionStorage),
41
42
  partialize: (s) => ({ viewports: s.viewports, excluded: s.excluded, resultFilter: s.resultFilter }),
42
43
  merge: (persisted, current) => {
@@ -5,6 +5,7 @@ import { mapFrame } from '../layout/graph';
5
5
  import { ChartMap } from '../map/ChartMap';
6
6
  import { SidePanel } from './SidePanel';
7
7
  import styles from './views.module.css';
8
+ import { ChartTests } from './ChartTests';
8
9
 
9
10
  export function ChartPage() {
10
11
  const { chartId } = useParams({ from: '/chart/$chartId' });
@@ -47,6 +48,7 @@ export function ChartPage() {
47
48
  <div className={styles.mapArea}>
48
49
  <ChartMap key={`${chartId}:${search.group ?? ''}`} view={view} chartId={chartId} selection={selection} group={search.group} />
49
50
  </div>
51
+ <ChartTests view={view} chartId={chartId} compact />
50
52
  </div>
51
53
  );
52
54
  }
@@ -0,0 +1,43 @@
1
+ import { lazy, Suspense, useState } from 'react';
2
+ import type { AtlasView } from '../data/model';
3
+ import { useEvidence } from '../evidence/evidence-context';
4
+ import { CheckDetail, ResultRow, type CheckSelection } from '../evidence/check-detail';
5
+ import { TestFilter, useRunningFilter } from '../evidence/test-filter';
6
+ import { Bounded } from '../evidence/marks';
7
+ import { outlinePath } from '../spec-kind';
8
+ import styles from '../evidence/evidence.module.css';
9
+
10
+ const RecordingPage = lazy(() => import('../evidence/recording-page').then(module => ({ default: module.RecordingPage })));
11
+
12
+ /** Statechart test reports stay in the chart, including while their recordings are still arriving. */
13
+ export function ChartTests({ view, chartId, compact = false }: { view: AtlasView; chartId: string; compact?: boolean }) {
14
+ const evidence = useEvidence();
15
+ const runningOnly = useRunningFilter();
16
+ const [selection, setSelection] = useState<CheckSelection>({});
17
+ const [selectedKey, setSelectedKey] = useState<string | null>(null);
18
+ const [recording, setRecording] = useState(false);
19
+ const [moment, setMoment] = useState<number | null>(null);
20
+ const files = new Set<string>();
21
+ let chart = view.charts.get(chartId);
22
+ while (chart && !files.has(outlinePath(chart.file))) {
23
+ files.add(outlinePath(chart.file));
24
+ chart = chart.parent ? view.charts.get(chart.parent.chartId) : undefined;
25
+ }
26
+ const slots = [...files].flatMap(file => evidence.view.specs.get(file)?.slots ?? []);
27
+ const visible = slots.filter(slot => !runningOnly || slot.status === 'running');
28
+ if (!slots.length) return null;
29
+ return <section aria-label="Chart tests" className={styles.block} style={compact ? { maxHeight: '40vh', overflowY: 'auto', flexShrink: 0 } : undefined}>
30
+ <h3 className={styles.blockTitle}>Tests</h3>
31
+ <TestFilter slots={slots} />
32
+ <ul className={styles.results}><Bounded items={visible} size={40} more="More tests">{slot =>
33
+ <ResultRow key={slot.key} label={slot.title} slot={slot} open={selectedKey === slot.key}
34
+ onToggle={() => { setSelectedKey(selectedKey === slot.key ? null : slot.key); setSelection({ check: slot.id }); setRecording(false); setMoment(null); }}>
35
+ {recording ? <Suspense fallback={<p role="status">Loading the recording…</p>}><RecordingPage slot={slot} layerId={selection.layer} label={slot.title} back="Test report" moment={moment} onMoment={setMoment} onClose={() => setRecording(false)} /></Suspense>
36
+ : <CheckDetail slot={slot} layerId={selection.layer} reads="steps" select={patch => {
37
+ setSelection({ check: patch.check ?? slot.id, layer: patch.layer ?? undefined });
38
+ if (patch.view === 'recording') { setRecording(true); setMoment(null); }
39
+ }} />}
40
+ </ResultRow>
41
+ }</Bounded></ul>
42
+ </section>;
43
+ }
@@ -12,6 +12,7 @@ import { Stage } from '../looks/Stage';
12
12
  import { mapScope } from '../layout/graph';
13
13
  import type { MapSelection } from '../map/ChartMap';
14
14
  import styles from './views.module.css';
15
+ import { ChartTests } from './ChartTests';
15
16
 
16
17
  function Section({ title, children }: { title: string; children: ReactNode }) {
17
18
  return (
@@ -314,6 +315,7 @@ export function SidePanel({ view, chartId, selection }: { view: AtlasView; chart
314
315
  return (
315
316
  <article className={styles.plateScroll} aria-label={label}>
316
317
  {body}
318
+ <ChartTests view={view} chartId={chartId} />
317
319
  <Link
318
320
  to="/chart/$chartId"
319
321
  params={{ chartId }}
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
 
5
5
  import { afterAll, describe, expect, it } from 'bun:test';
6
6
 
7
- import { DEFAULT_FIXTURE, FIXTURES_ROOT, isSpecFile, listSpecFiles, publicReportSources, resolveReportSources, resolveRoots, safeJoin, summarizeRun } from './atlas-files';
7
+ import { FIXTURES_ROOT, isSpecFile, listSpecFiles, publicReportSources, resolveReportSources, resolveRoots, safeJoin, summarizeRun } from './atlas-files';
8
8
 
9
9
  const tmp = mkdtempSync(path.join(os.tmpdir(), 'atlas-files-'));
10
10
  afterAll(() => rmSync(tmp, { recursive: true, force: true }));
@@ -31,10 +31,10 @@ describe('resolveRoots', () => {
31
31
  expect(resolveRoots({ ATLAS_SPECS_ROOT: 'x' }, project).specs).toBe(path.join(project, 'x'));
32
32
  });
33
33
 
34
- it('shows the example when nothing is configured and there is no specs/ directory', () => {
34
+ it('keeps an unconfigured project empty instead of loading a bundled example', () => {
35
35
  const roots = resolveRoots({ INIT_CWD: path.join(tmp, 'empty') });
36
- expect(roots.specs).toBe(path.join(FIXTURES_ROOT, DEFAULT_FIXTURE, 'specs'));
37
- expect(roots.label).toMatch(/^example/);
36
+ expect(roots.specs).toBe(path.join(tmp, 'empty', 'specs'));
37
+ expect(roots.label).toBe('configured');
38
38
  });
39
39
 
40
40
  it('reads a named fixture and rejects an unknown one', () => {
@@ -53,17 +53,13 @@ export function fixtureRoots(name: string): AtlasRoots {
53
53
  * (`ATLAS_FIXTURE`), explicit roots (`ATLAS_SPECS_ROOT`, `ATLAS_RUNS_ROOT`), then `specs/` and
54
54
  * `atlas-runs/` where the command was run. Relative paths resolve against `INIT_CWD` (where pnpm
55
55
  * was run) or the current directory. With nothing configured and no `specs/` there, the bundled
56
- * example fixture is shown.
56
+ * project stays empty. Bundled examples require an explicit fixture selection.
57
57
  */
58
58
  export function resolveRoots(env: NodeJS.ProcessEnv = process.env, cwd = process.cwd()): AtlasRoots {
59
59
  if (env.ATLAS_FIXTURE) return fixtureRoots(env.ATLAS_FIXTURE);
60
60
  const base = env.INIT_CWD || cwd;
61
- const specsSet = Boolean(env.ATLAS_SPECS_ROOT);
62
61
  const specs = path.resolve(base, env.ATLAS_SPECS_ROOT || DEFAULT_SPECS_DIR);
63
62
  const runs = path.resolve(base, env.ATLAS_RUNS_ROOT || DEFAULT_RUNS_DIR);
64
- if (!specsSet && !env.ATLAS_RUNS_ROOT && !existsSync(specs)) {
65
- return { ...fixtureRoots(DEFAULT_FIXTURE), label: `example (no ${DEFAULT_SPECS_DIR}/ in ${base}; set ATLAS_SPECS_ROOT)` };
66
- }
67
63
  return { specs, runs, label: 'configured' };
68
64
  }
69
65
 
@@ -0,0 +1,37 @@
1
+ import type { Plugin, ProxyOptions } from 'vite';
2
+
3
+ /** What the viewer is told about the server it is served with: the same words `atlas serve` writes into its page. */
4
+ export const serverPageConfig = { api: './api', reportsUrl: './api/reports', dataUrl: './api/data/', eventsUrl: './api/events', projectsUrl: './api/projects', effective: true } as const;
5
+
6
+ /**
7
+ * Put the Atlas server behind the dev server. The browser talks to one host; `/api` goes to the server, so the
8
+ * page, its data and its event stream have one origin in development exactly as they do when deployed.
9
+ */
10
+ export function atlasServerProxy(target: string): { readonly proxy: Record<string, ProxyOptions> } {
11
+ return {
12
+ proxy: {
13
+ '/admin': { target, changeOrigin: false },
14
+ '/api': {
15
+ target,
16
+ changeOrigin: false,
17
+ ws: false,
18
+ // The event stream stays open for hours: no timeout, and nothing may hold its bytes back.
19
+ timeout: 0,
20
+ proxyTimeout: 0,
21
+ configure: (proxy) => {
22
+ proxy.on('proxyRes', (response) => {
23
+ if (String(response.headers['content-type'] ?? '').startsWith('text/event-stream')) response.headers['cache-control'] = 'no-cache, no-transform';
24
+ });
25
+ }
26
+ }
27
+ }
28
+ };
29
+ }
30
+
31
+ /** Write the server's address book into the page the dev server serves. */
32
+ export function atlasServerPage(): Plugin {
33
+ return {
34
+ name: 'atlas-server-page',
35
+ transformIndexHtml: () => [{ tag: 'script', children: `window.__ATLAS_CONFIG__=${JSON.stringify(serverPageConfig)}`, injectTo: 'head-prepend' }]
36
+ };
37
+ }
@@ -8,14 +8,22 @@ import { resolveRoots } from './vite/atlas-files';
8
8
  import { servesExampleReports } from './vite/example-reports';
9
9
  import { reportsDevPlugin } from './vite/reports-dev-plugin';
10
10
  import { atlasDevPlugin } from './vite/atlas-dev-plugin';
11
+ import { atlasServerPage, atlasServerProxy } from './vite/atlas-server-proxy';
11
12
 
12
13
  const appRoot = path.dirname(fileURLToPath(import.meta.url));
13
- const reportsModule = process.env.ATLAS_REPORTS_MODULE || (servesExampleReports(resolveRoots().specs) ? path.join(appRoot, 'vite/example-reports.ts') : '');
14
+
15
+ // `atlas dev` runs the Atlas server on a port of its own and tells this config where. The browser then talks to
16
+ // this dev server alone: `/api` is proxied to the server, and the page gets the same address book a served
17
+ // build gets, so the viewer cannot tell the two apart. Without a server, the viewer reads the working tree.
18
+ const serverUrl = process.env.ATLAS_API_URL || '';
19
+ const reportsModule = serverUrl ? '' : process.env.ATLAS_REPORTS_MODULE || (servesExampleReports(resolveRoots().specs) ? path.join(appRoot, 'vite/example-reports.ts') : '');
14
20
 
15
21
  export default defineConfig({
16
22
  base: './',
17
- plugins: [react(), atlasDevPlugin(), ...(reportsModule ? [reportsDevPlugin({ repoRoot: process.env.INIT_CWD || process.cwd(), appModule: reportsModule })] : [])],
23
+ plugins: serverUrl
24
+ ? [react(), atlasServerPage()]
25
+ : [react(), atlasDevPlugin(), ...(reportsModule ? [reportsDevPlugin({ repoRoot: process.env.INIT_CWD || process.cwd(), appModule: reportsModule })] : [])],
18
26
  worker: { format: 'es' },
19
- server: { port: Number(process.env.PORT ?? 5180), strictPort: false },
27
+ server: { port: Number(process.env.PORT ?? 5180), strictPort: false, ...(serverUrl ? atlasServerProxy(serverUrl) : {}) },
20
28
  build: { outDir: 'dist', emptyOutDir: true, chunkSizeWarningLimit: 2_500 }
21
29
  });