@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.
- package/CHANGELOG.md +13 -1
- package/README.md +191 -611
- package/dist/bun/src/driver.d.ts +41 -7
- package/dist/bun/src/driver.js +47 -6
- package/dist/bun/src/index.d.ts +8 -0
- package/dist/bun/src/index.js +8 -0
- package/dist/core/src/cli.d.ts +14 -1
- package/dist/core/src/cli.js +35 -1
- package/dist/core/src/config.d.ts +77 -4
- package/dist/core/src/config.js +110 -17
- package/dist/core/src/events/cloudevents.d.ts +54 -9
- package/dist/core/src/events/cloudevents.js +60 -9
- package/dist/core/src/graph.d.ts +21 -0
- package/dist/core/src/graph.js +26 -0
- package/dist/core/src/index.d.ts +11 -0
- package/dist/core/src/index.js +11 -0
- package/dist/core/src/lint.d.ts +30 -0
- package/dist/core/src/lint.js +13 -0
- package/dist/core/src/plan.d.ts +39 -1
- package/dist/core/src/plan.js +28 -1
- package/dist/core/src/privacy.d.ts +29 -2
- package/dist/core/src/privacy.js +19 -2
- package/dist/core/src/report/formats.d.ts +34 -4
- package/dist/core/src/report/formats.js +24 -4
- package/dist/core/src/report/markdown.d.ts +15 -0
- package/dist/core/src/report/markdown.js +8 -0
- package/dist/core/src/run/clients.d.ts +44 -5
- package/dist/core/src/run/clients.js +46 -2
- package/dist/core/src/run/execute.d.ts +75 -16
- package/dist/core/src/run/execute.js +71 -18
- package/dist/core/src/run/frames.d.ts +13 -4
- package/dist/core/src/run/frames.js +19 -4
- package/dist/core/src/run/http.d.ts +34 -1
- package/dist/core/src/run/http.js +14 -1
- package/dist/core/src/run/types.d.ts +76 -3
- package/dist/core/src/run/types.js +19 -3
- package/dist/core/src/signals.d.ts +49 -5
- package/dist/core/src/signals.js +22 -5
- package/dist/core/src/spec/compose.d.ts +23 -1
- package/dist/core/src/spec/compose.js +10 -1
- package/dist/core/src/spec/index.d.ts +9 -1
- package/dist/core/src/spec/index.js +9 -1
- package/dist/core/src/spec/load.d.ts +22 -1
- package/dist/core/src/spec/load.js +31 -1
- package/dist/core/src/spec/parse.d.ts +16 -2
- package/dist/core/src/spec/parse.js +25 -2
- package/dist/core/src/spec/scxml.d.ts +25 -4
- package/dist/core/src/spec/scxml.js +36 -4
- package/dist/core/src/spec/types.d.ts +52 -1
- package/dist/core/src/spec/types.js +24 -0
- package/dist/detox/src/index.d.ts +54 -11
- package/dist/detox/src/index.js +45 -7
- package/dist/playwright/src/driver.d.ts +39 -3
- package/dist/playwright/src/driver.js +27 -3
- package/dist/playwright/src/index.d.ts +10 -0
- package/dist/playwright/src/index.js +10 -0
- package/dist/playwright/src/media.d.ts +50 -4
- package/dist/playwright/src/media.js +55 -4
- package/dist/playwright/src/signals.d.ts +44 -5
- package/dist/playwright/src/signals.js +29 -5
- package/dist/reports/src/attachment-files.d.ts +44 -0
- package/dist/reports/src/attachment-files.js +121 -0
- package/dist/reports/src/attachment-response.d.ts +31 -0
- package/dist/reports/src/attachment-response.js +55 -0
- package/dist/reports/src/aws-credentials.d.ts +48 -0
- package/dist/reports/src/aws-credentials.js +221 -0
- package/dist/reports/src/browser-navigation.d.ts +14 -0
- package/dist/reports/src/browser-navigation.js +7 -0
- package/dist/reports/src/bundle.d.ts +34 -2
- package/dist/reports/src/bundle.js +38 -2
- package/dist/reports/src/catalog.d.ts +34 -3
- package/dist/reports/src/catalog.js +17 -3
- package/dist/reports/src/cli-stores.d.ts +36 -0
- package/dist/reports/src/cli-stores.js +73 -0
- package/dist/reports/src/clock.d.ts +7 -2
- package/dist/reports/src/clock.js +5 -2
- package/dist/reports/src/completion-order.d.ts +18 -5
- package/dist/reports/src/completion-order.js +18 -5
- package/dist/reports/src/compose.d.ts +80 -0
- package/dist/reports/src/compose.js +117 -0
- package/dist/reports/src/copy-store.d.ts +29 -0
- package/dist/reports/src/copy-store.js +52 -0
- package/dist/reports/src/directory-outline.d.ts +38 -0
- package/dist/reports/src/directory-outline.js +103 -0
- package/dist/reports/src/directory-store.d.ts +20 -0
- package/dist/reports/src/directory-store.js +166 -0
- package/dist/reports/src/doctor.d.ts +29 -0
- package/dist/reports/src/doctor.js +85 -0
- package/dist/reports/src/effective-delta.d.ts +34 -0
- package/dist/reports/src/effective-delta.js +25 -0
- package/dist/reports/src/evidence-detail.d.ts +64 -0
- package/dist/reports/src/evidence-detail.js +92 -0
- package/dist/reports/src/evidence-live.d.ts +35 -5
- package/dist/reports/src/evidence-live.js +18 -2
- package/dist/reports/src/evidence-types.d.ts +68 -1
- package/dist/reports/src/evidence-types.js +5 -1
- package/dist/reports/src/evidence-view.d.ts +43 -4
- package/dist/reports/src/evidence-view.js +13 -2
- package/dist/reports/src/explorer-types.d.ts +40 -3
- package/dist/reports/src/explorer-types.js +8 -0
- package/dist/reports/src/explorer-versions.d.ts +9 -1
- package/dist/reports/src/explorer-versions.js +18 -1
- package/dist/reports/src/export-evidence.d.ts +16 -1
- package/dist/reports/src/export-evidence.js +52 -1
- package/dist/reports/src/fetcher.d.ts +27 -3
- package/dist/reports/src/fetcher.js +11 -3
- package/dist/reports/src/format-date.d.ts +6 -0
- package/dist/reports/src/format-date.js +6 -0
- package/dist/reports/src/format.d.ts +16 -0
- package/dist/reports/src/format.js +10 -0
- package/dist/reports/src/frames.d.ts +36 -3
- package/dist/reports/src/frames.js +22 -3
- package/dist/reports/src/hash.d.ts +18 -0
- package/dist/reports/src/hash.js +13 -0
- package/dist/reports/src/host.d.ts +29 -0
- package/dist/reports/src/host.js +64 -0
- package/dist/reports/src/html-frame.d.ts +5 -6
- package/dist/reports/src/html-frame.js +12 -1
- package/dist/reports/src/http-store.d.ts +56 -0
- package/dist/reports/src/http-store.js +148 -0
- package/dist/reports/src/http.d.ts +7 -0
- package/dist/reports/src/http.js +6 -0
- package/dist/reports/src/ingest.d.ts +54 -0
- package/dist/reports/src/ingest.js +210 -0
- package/dist/reports/src/init.d.ts +40 -0
- package/dist/reports/src/init.js +369 -0
- package/dist/reports/src/junit.d.ts +33 -6
- package/dist/reports/src/junit.js +87 -21
- package/dist/reports/src/keyboard.d.ts +20 -0
- package/dist/reports/src/keyboard.js +11 -0
- package/dist/reports/src/keys.d.ts +21 -3
- package/dist/reports/src/keys.js +21 -3
- package/dist/reports/src/layer-axes.d.ts +35 -5
- package/dist/reports/src/layer-axes.js +25 -5
- package/dist/reports/src/layer-catalog.d.ts +28 -1
- package/dist/reports/src/layer-catalog.js +18 -1
- package/dist/reports/src/layer-merge.d.ts +98 -9
- package/dist/reports/src/layer-merge.js +147 -31
- package/dist/reports/src/layer-source.d.ts +18 -0
- package/dist/reports/src/layer-source.js +11 -0
- package/dist/reports/src/layer-store.d.ts +42 -0
- package/dist/reports/src/layer-store.js +63 -5
- package/dist/reports/src/layer-types.d.ts +32 -1
- package/dist/reports/src/layers-route.d.ts +47 -4
- package/dist/reports/src/layers-route.js +86 -7
- package/dist/reports/src/live-reporter.d.ts +34 -0
- package/dist/reports/src/live-reporter.js +122 -0
- package/dist/reports/src/media-types.d.ts +11 -0
- package/dist/reports/src/media-types.js +22 -0
- package/dist/reports/src/namespace-guard.d.ts +10 -1
- package/dist/reports/src/namespace-guard.js +10 -1
- package/dist/reports/src/navigation.d.ts +12 -0
- package/dist/reports/src/navigation.js +7 -0
- package/dist/reports/src/object-io.d.ts +11 -4
- package/dist/reports/src/object-io.js +4 -4
- package/dist/reports/src/object-store-conformance.d.ts +27 -0
- package/dist/reports/src/object-store-conformance.js +87 -0
- package/dist/reports/src/object-store.d.ts +33 -24
- package/dist/reports/src/object-store.js +17 -24
- package/dist/reports/src/parse-with.d.ts +16 -0
- package/dist/reports/src/parse-with.js +14 -0
- package/dist/reports/src/performance-part.d.ts +25 -1
- package/dist/reports/src/performance-part.js +15 -1
- package/dist/reports/src/performance.d.ts +22 -2
- package/dist/reports/src/performance.js +12 -2
- package/dist/reports/src/persist.d.ts +12 -3
- package/dist/reports/src/persist.js +12 -3
- package/dist/reports/src/phone.d.ts +18 -1
- package/dist/reports/src/phone.js +18 -1
- package/dist/reports/src/protocol.d.ts +26 -4
- package/dist/reports/src/protocol.js +15 -4
- package/dist/reports/src/publication.d.ts +21 -1
- package/dist/reports/src/publication.js +21 -1
- package/dist/reports/src/publish-checks.d.ts +35 -0
- package/dist/reports/src/publish-checks.js +122 -0
- package/dist/reports/src/publish.d.ts +42 -9
- package/dist/reports/src/publish.js +36 -7
- package/dist/reports/src/read.d.ts +46 -16
- package/dist/reports/src/read.js +38 -14
- package/dist/reports/src/recording-bundle.d.ts +13 -2
- package/dist/reports/src/recording-bundle.js +4 -2
- package/dist/reports/src/recording-keys.d.ts +15 -1
- package/dist/reports/src/recording-keys.js +15 -1
- package/dist/reports/src/remote-report.d.ts +28 -7
- package/dist/reports/src/remote-report.js +15 -7
- package/dist/reports/src/report-cli.d.ts +51 -0
- package/dist/reports/src/report-cli.js +486 -0
- package/dist/reports/src/report-index.d.ts +99 -0
- package/dist/reports/src/report-index.js +337 -0
- package/dist/reports/src/report-loader.d.ts +10 -0
- package/dist/reports/src/report-loader.js +6 -0
- package/dist/reports/src/report-parts.d.ts +29 -3
- package/dist/reports/src/report-parts.js +37 -3
- package/dist/reports/src/report-status.d.ts +48 -0
- package/dist/reports/src/report-status.js +156 -0
- package/dist/reports/src/report-types.d.ts +73 -0
- package/dist/reports/src/report-types.js +3 -0
- package/dist/reports/src/reporter.d.ts +67 -0
- package/dist/reports/src/reporter.js +128 -0
- package/dist/reports/src/reports-explorer-route.d.ts +541 -0
- package/dist/reports/src/reports-explorer-route.js +11 -0
- package/dist/reports/src/reports-list-route.d.ts +9 -0
- package/dist/reports/src/reports-list-route.js +9 -0
- package/dist/reports/src/reports-route.d.ts +561 -22
- package/dist/reports/src/reports-route.js +11 -3
- package/dist/reports/src/reports-store.d.ts +19 -2
- package/dist/reports/src/reports-store.js +10 -2
- package/dist/reports/src/resilient-cache.d.ts +14 -1
- package/dist/reports/src/resilient-cache.js +8 -1
- package/dist/reports/src/retention.d.ts +17 -1
- package/dist/reports/src/retention.js +5 -1
- package/dist/reports/src/run-bundle.d.ts +63 -0
- package/dist/reports/src/run-bundle.js +159 -0
- package/dist/reports/src/s3-store.d.ts +79 -0
- package/dist/reports/src/s3-store.js +295 -0
- package/dist/reports/src/scenario.d.ts +14 -4
- package/dist/reports/src/scenario.js +5 -4
- package/dist/reports/src/sigv4.d.ts +72 -0
- package/dist/reports/src/sigv4.js +96 -0
- package/dist/reports/src/snapshot-bundle.d.ts +24 -1
- package/dist/reports/src/snapshot-bundle.js +27 -2
- package/dist/reports/src/snapshot-surface.d.ts +20 -9
- package/dist/reports/src/snapshot-surface.js +32 -19
- package/dist/reports/src/source-content.d.ts +23 -0
- package/dist/reports/src/source-content.js +30 -1
- package/dist/reports/src/source-entries.d.ts +13 -0
- package/dist/reports/src/source-entries.js +6 -0
- package/dist/reports/src/source-part.d.ts +15 -0
- package/dist/reports/src/source-part.js +8 -0
- package/dist/reports/src/source-reader.d.ts +11 -0
- package/dist/reports/src/source-reader.js +5 -0
- package/dist/reports/src/spec-evidence.d.ts +12 -2
- package/dist/reports/src/spec-evidence.js +6 -2
- package/dist/reports/src/spec-inventory.d.ts +15 -1
- package/dist/reports/src/spec-inventory.js +22 -1
- package/dist/reports/src/spec-link.d.ts +28 -0
- package/dist/reports/src/spec-link.js +70 -0
- package/dist/reports/src/spec-set.d.ts +63 -0
- package/dist/reports/src/spec-set.js +177 -0
- package/dist/reports/src/status.d.ts +10 -0
- package/dist/reports/src/status.js +5 -0
- package/dist/reports/src/store-prefix.d.ts +28 -0
- package/dist/reports/src/store-prefix.js +22 -0
- package/dist/reports/src/store-url.d.ts +89 -0
- package/dist/reports/src/store-url.js +160 -0
- package/dist/reports/src/test-catalog-entity.d.ts +11 -0
- package/dist/reports/src/test-catalog-entity.js +6 -0
- package/dist/reports/src/types.d.ts +6 -1
- package/dist/reports/src/use-report-nav.d.ts +35 -0
- package/dist/reports/src/use-report-nav.js +12 -0
- package/dist/runners/src/codecept.d.ts +14 -0
- package/dist/runners/src/codecept.js +16 -0
- package/dist/runners/src/jest.d.ts +50 -0
- package/dist/runners/src/jest.js +52 -0
- package/dist/runners/src/mocha.d.ts +40 -0
- package/dist/runners/src/mocha.js +39 -0
- package/dist/runners/src/node-test.d.ts +28 -0
- package/dist/runners/src/node-test.js +44 -0
- package/dist/runners/src/publish.d.ts +30 -0
- package/dist/runners/src/publish.js +68 -0
- package/dist/schema/src/api.d.ts +800 -0
- package/dist/schema/src/api.js +201 -0
- package/dist/schema/src/cli-results.d.ts +210 -0
- package/dist/schema/src/cli-results.js +124 -0
- package/dist/schema/src/common.d.ts +53 -0
- package/dist/schema/src/common.js +42 -0
- package/dist/schema/src/index.d.ts +16 -0
- package/dist/schema/src/index.js +16 -0
- package/dist/schema/src/layer.d.ts +2273 -0
- package/dist/schema/src/layer.js +462 -0
- package/dist/schema/src/live-run.d.ts +123 -0
- package/dist/schema/src/live-run.js +30 -0
- package/dist/schema/src/manifest.d.ts +97 -2
- package/dist/schema/src/manifest.js +84 -0
- package/dist/schema/src/project.d.ts +20 -0
- package/dist/schema/src/project.js +10 -0
- package/dist/schema/src/report-sources.d.ts +29 -0
- package/dist/schema/src/report-sources.js +27 -1
- package/dist/schema/src/runs-index.d.ts +17 -1
- package/dist/schema/src/runs-index.js +15 -0
- package/dist/schema/src/server-config.d.ts +177 -0
- package/dist/schema/src/server-config.js +85 -0
- package/dist/schema/src/spec.d.ts +69 -0
- package/dist/schema/src/spec.js +58 -0
- package/dist/server/src/app.d.ts +72 -0
- package/dist/server/src/app.js +557 -0
- package/dist/server/src/auth.d.ts +41 -0
- package/dist/server/src/auth.js +59 -0
- package/dist/server/src/config.d.ts +83 -0
- package/dist/server/src/config.js +204 -0
- package/dist/server/src/dev.d.ts +12 -0
- package/dist/server/src/dev.js +89 -0
- package/dist/server/src/events.d.ts +29 -0
- package/dist/server/src/events.js +101 -0
- package/dist/server/src/host.d.ts +30 -0
- package/dist/server/src/host.js +115 -0
- package/dist/server/src/index.d.ts +18 -0
- package/dist/server/src/index.js +18 -0
- package/dist/server/src/live-runs.d.ts +52 -0
- package/dist/server/src/live-runs.js +229 -0
- package/dist/server/src/llms.d.ts +19 -0
- package/dist/server/src/llms.js +63 -0
- package/dist/server/src/main.d.ts +30 -0
- package/dist/server/src/main.js +114 -0
- package/dist/server/src/openapi.d.ts +56 -0
- package/dist/server/src/openapi.js +438 -0
- package/dist/server/src/problem.d.ts +8 -0
- package/dist/server/src/problem.js +39 -0
- package/dist/server/src/projects.d.ts +3 -0
- package/dist/server/src/projects.js +154 -0
- package/dist/server/src/settings-page.d.ts +9 -0
- package/dist/server/src/settings-page.js +93 -0
- package/dist/server/src/settings.d.ts +23 -0
- package/dist/server/src/settings.js +72 -0
- package/dist/server/src/site.d.ts +15 -0
- package/dist/server/src/site.js +78 -0
- package/dist/server/src/store-api.d.ts +28 -0
- package/dist/server/src/store-api.js +180 -0
- package/dist/server/src/validate.d.ts +71 -0
- package/dist/server/src/validate.js +197 -0
- package/dist/vitest/src/index.d.ts +27 -5
- package/dist/vitest/src/index.js +18 -5
- package/dist/webdriver/src/driver.d.ts +63 -11
- package/dist/webdriver/src/driver.js +57 -6
- package/dist/webdriver/src/index.d.ts +8 -0
- package/dist/webdriver/src/index.js +1 -0
- package/dist/webdriver/src/protocol.d.ts +75 -6
- package/dist/webdriver/src/protocol.js +79 -6
- package/docs/agents.md +137 -0
- package/docs/deploying.md +345 -0
- package/docs/dogfooding.md +175 -0
- package/docs/extending.md +242 -0
- package/docs/integrating.md +485 -0
- package/docs/reference/bun.md +141 -0
- package/docs/reference/cli.md +148 -0
- package/docs/reference/core.md +2432 -0
- package/docs/reference/detox.md +150 -0
- package/docs/reference/http-api.md +799 -0
- package/docs/reference/playwright.md +303 -0
- package/docs/reference/reports.md +7152 -0
- package/docs/reference/runners.md +169 -0
- package/docs/reference/schema.md +1762 -0
- package/docs/reference/server.md +825 -0
- package/docs/reference/vitest.md +45 -0
- package/docs/reference/webdriver.md +330 -0
- package/docs/report-backends.md +144 -0
- package/docs/report-contract.md +400 -0
- package/docs/reporting-issues.md +160 -0
- package/docs/runners.md +69 -0
- package/docs/server.md +321 -0
- package/docs/statecharts.md +643 -0
- package/llms-full.txt +4114 -0
- package/llms.txt +49 -0
- package/package.json +61 -11
- package/packages/core/README.md +2 -1
- package/packages/reports/README.md +65 -0
- package/packages/reports/STORAGE.md +4 -0
- package/packages/runners/README.md +6 -0
- package/packages/runners/compat/codecept.cjs +2 -0
- package/packages/runners/compat/jest.cjs +2 -0
- package/packages/runners/compat/mocha.cjs +2 -0
- package/packages/schema/README.md +14 -0
- package/packages/schema/schemas/atlas-change.schema.json +4 -0
- package/packages/schema/schemas/check-detail.schema.json +1032 -0
- package/packages/schema/schemas/checks-result.schema.json +26 -0
- package/packages/schema/schemas/command-error.schema.json +35 -0
- package/packages/schema/schemas/doctor-result.schema.json +60 -0
- package/packages/schema/schemas/effective-delta.schema.json +490 -0
- package/packages/schema/schemas/effective-evidence.schema.json +471 -0
- package/packages/schema/schemas/evidence-page.schema.json +271 -0
- package/packages/schema/schemas/hello-event.schema.json +26 -0
- package/packages/schema/schemas/init-plan.schema.json +347 -0
- package/packages/schema/schemas/layer-rows.schema.json +199 -0
- package/packages/schema/schemas/layer.schema.json +2888 -0
- package/packages/schema/schemas/live-run-start.schema.json +42 -0
- package/packages/schema/schemas/live-run-update.schema.json +93 -0
- package/packages/schema/schemas/live-run.schema.json +225 -0
- package/packages/schema/schemas/merge-result.schema.json +40 -0
- package/packages/schema/schemas/outline.schema.json +62 -0
- package/packages/schema/schemas/problem.schema.json +37 -0
- package/packages/schema/schemas/project-slug.schema.json +8 -0
- package/packages/schema/schemas/project.schema.json +22 -0
- package/packages/schema/schemas/projects.schema.json +33 -0
- package/packages/schema/schemas/publish-checks.schema.json +1413 -0
- package/packages/schema/schemas/publish-result.schema.json +168 -0
- package/packages/schema/schemas/ready.schema.json +68 -0
- package/packages/schema/schemas/report-event.schema.json +100 -0
- package/packages/schema/schemas/report-status.schema.json +370 -0
- package/packages/schema/schemas/server-config.schema.json +208 -0
- package/packages/schema/schemas/server-editable-settings.schema.json +132 -0
- package/packages/schema/schemas/server-info.schema.json +114 -0
- package/packages/schema/schemas/server-settings-snapshot.schema.json +285 -0
- package/packages/schema/schemas/spec-set.schema.json +103 -0
- package/packages/schema/schemas/store-listing.schema.json +29 -0
- package/packages/schema/schemas/store-write-result.schema.json +28 -0
- package/packages/schema/schemas/stream-result.schema.json +59 -0
- package/packages/server/README.md +43 -0
- package/packages/server/openapi.json +11021 -0
- package/packages/visualizer/README.md +32 -11
- package/packages/visualizer/bin/atlas-client.mjs +176 -0
- package/packages/visualizer/bin/atlas-visualizer.mjs +2 -175
- package/packages/visualizer/src/app/AppShell.tsx +32 -16
- package/packages/visualizer/src/app/report-sources.tsx +7 -4
- package/packages/visualizer/src/app/router.tsx +3 -3
- package/packages/visualizer/src/components/ActivityIndicator.module.css +4 -0
- package/packages/visualizer/src/components/ActivityIndicator.tsx +6 -0
- package/packages/visualizer/src/components/Header.module.css +101 -192
- package/packages/visualizer/src/components/Header.tsx +88 -13
- package/packages/visualizer/src/components/ProjectPicker.tsx +64 -0
- package/packages/visualizer/src/data/adapters/adapter.ts +5 -17
- package/packages/visualizer/src/data/adapters/index.ts +10 -1
- package/packages/visualizer/src/data/adapters/report-backend.test.ts +3 -2
- package/packages/visualizer/src/data/adapters/report-backend.ts +1 -1
- package/packages/visualizer/src/data/queries.ts +14 -7
- package/packages/visualizer/src/evidence/api.ts +16 -3
- package/packages/visualizer/src/evidence/artifacts.module.css +213 -0
- package/packages/visualizer/src/evidence/artifacts.tsx +272 -0
- package/packages/visualizer/src/evidence/check-detail.tsx +21 -4
- package/packages/visualizer/src/evidence/document-page.tsx +6 -2
- package/packages/visualizer/src/evidence/evidence-context.tsx +79 -12
- package/packages/visualizer/src/evidence/evidence.module.css +11 -1
- package/packages/visualizer/src/evidence/feature-page.tsx +7 -3
- package/packages/visualizer/src/evidence/model-page.tsx +6 -2
- package/packages/visualizer/src/evidence/project-home.tsx +45 -0
- package/packages/visualizer/src/evidence/recording-page.tsx +4 -1
- package/packages/visualizer/src/evidence/spec-index.tsx +2 -1
- package/packages/visualizer/src/evidence/spec-page.tsx +23 -16
- package/packages/visualizer/src/evidence/test-filter.tsx +21 -0
- package/packages/visualizer/src/find/Finder.tsx +1 -7
- package/packages/visualizer/src/find/open.ts +4 -0
- package/packages/visualizer/src/live/live-query.ts +21 -0
- package/packages/visualizer/src/live/live-runs.tsx +69 -0
- package/packages/visualizer/src/live/live-stream.ts +225 -0
- package/packages/visualizer/src/live/report-progress.ts +62 -0
- package/packages/visualizer/src/live/test-progress.tsx +34 -0
- package/packages/visualizer/src/runtime.ts +35 -0
- package/packages/visualizer/src/state/ui-store.ts +3 -2
- package/packages/visualizer/src/views/ChartPage.tsx +2 -0
- package/packages/visualizer/src/views/ChartTests.tsx +43 -0
- package/packages/visualizer/src/views/SidePanel.tsx +2 -0
- package/packages/visualizer/vite/atlas-files.test.ts +4 -4
- package/packages/visualizer/vite/atlas-files.ts +1 -5
- package/packages/visualizer/vite/atlas-server-proxy.ts +37 -0
- package/packages/visualizer/vite.config.ts +11 -3
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module HTTP host for a fetch handler
|
|
3
|
+
*
|
|
4
|
+
* `listen` serves any `(Request) => Promise<Response>` over HTTP: with `Bun.serve` when running on Bun, with a `node:http`
|
|
5
|
+
* bridge (streaming bodies in and out, so server-sent events work) everywhere else. The server's behaviour is the same on
|
|
6
|
+
* both; `bun run release:smoke` proves it under each. Bodies over `maxBodyBytes` are refused by the runtime before the
|
|
7
|
+
* handler sees them.
|
|
8
|
+
*/
|
|
9
|
+
import { createServer } from 'node:http';
|
|
10
|
+
import { Readable } from 'node:stream';
|
|
11
|
+
const display = (host, port) => `http://${host === '0.0.0.0' || host === '::' ? 'localhost' : host}:${port}`;
|
|
12
|
+
/**
|
|
13
|
+
* Serve a fetch handler. On Bun, which is what Atlas is made for, that is `Bun.serve`: one native server for the
|
|
14
|
+
* API, the live streams and the page. On Node it is the same handler behind `node:http`, so the package still runs
|
|
15
|
+
* where Bun is not installed.
|
|
16
|
+
*/
|
|
17
|
+
export async function listen(handler, options) {
|
|
18
|
+
const bun = getBun();
|
|
19
|
+
if (bun)
|
|
20
|
+
return listenBun(bun, handler, options);
|
|
21
|
+
return listenNode(handler, options);
|
|
22
|
+
}
|
|
23
|
+
const getBun = () => globalThis.Bun;
|
|
24
|
+
async function listenBun(bun, handler, options) {
|
|
25
|
+
const server = bun.serve({
|
|
26
|
+
port: options.port,
|
|
27
|
+
hostname: options.host,
|
|
28
|
+
// Live streams are quiet between heartbeats; the longest idle time Bun allows keeps them open.
|
|
29
|
+
idleTimeout: 120,
|
|
30
|
+
maxRequestBodySize: options.maxBodyBytes + 1024 * 1024,
|
|
31
|
+
fetch: handler,
|
|
32
|
+
error: (error) => Response.json({ error: error.message }, { status: 500 })
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
port: server.port,
|
|
36
|
+
url: display(options.host, server.port),
|
|
37
|
+
close: async (graceMs = 10_000) => {
|
|
38
|
+
// Stop taking new connections and let requests in flight finish; end the rest after the grace period.
|
|
39
|
+
const closing = Promise.resolve(server.stop(false));
|
|
40
|
+
const timer = setTimeout(() => { void server.stop(true); }, graceMs);
|
|
41
|
+
timer.unref?.();
|
|
42
|
+
await closing;
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function toRequest(req, host, maxBodyBytes) {
|
|
48
|
+
const headers = new Headers();
|
|
49
|
+
for (const [name, value] of Object.entries(req.headers))
|
|
50
|
+
if (value !== undefined)
|
|
51
|
+
headers.set(name, Array.isArray(value) ? value.join(', ') : value);
|
|
52
|
+
const method = req.method ?? 'GET';
|
|
53
|
+
const hasBody = method !== 'GET' && method !== 'HEAD';
|
|
54
|
+
let size = 0;
|
|
55
|
+
const body = hasBody ? Readable.toWeb(req).pipeThrough(new TransformStream({ transform(chunk, controller) { size += chunk.byteLength; if (size > maxBodyBytes)
|
|
56
|
+
controller.error(new Error('The request body is too large'));
|
|
57
|
+
else
|
|
58
|
+
controller.enqueue(chunk); } })) : null;
|
|
59
|
+
return new Request(`http://${req.headers.host ?? host}${req.url ?? '/'}`, { method, headers, ...(body ? { body, duplex: 'half' } : {}) });
|
|
60
|
+
}
|
|
61
|
+
async function send(response, res, head) {
|
|
62
|
+
const headers = {};
|
|
63
|
+
response.headers.forEach((value, name) => { headers[name] = value; });
|
|
64
|
+
const cookies = response.headers.getSetCookie?.();
|
|
65
|
+
if (cookies?.length)
|
|
66
|
+
headers['set-cookie'] = cookies;
|
|
67
|
+
res.writeHead(response.status, headers);
|
|
68
|
+
if (head || !response.body) {
|
|
69
|
+
res.end();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const reader = response.body.getReader();
|
|
73
|
+
res.on('close', () => { void reader.cancel().catch(() => { }); });
|
|
74
|
+
for (;;) {
|
|
75
|
+
const { done, value } = await reader.read();
|
|
76
|
+
if (done)
|
|
77
|
+
break;
|
|
78
|
+
if (!res.write(value))
|
|
79
|
+
await new Promise(resolve => res.once('drain', resolve));
|
|
80
|
+
}
|
|
81
|
+
res.end();
|
|
82
|
+
}
|
|
83
|
+
async function listenNode(handler, options) {
|
|
84
|
+
const server = createServer((req, res) => {
|
|
85
|
+
void (async () => {
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
res.on('close', () => controller.abort());
|
|
88
|
+
try {
|
|
89
|
+
const request = toRequest(req, options.host, options.maxBodyBytes);
|
|
90
|
+
const abortable = new Request(request, { signal: controller.signal });
|
|
91
|
+
await send(await handler(abortable), res, req.method === 'HEAD');
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (!res.headersSent)
|
|
95
|
+
await send(Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 }), res, false).catch(() => { });
|
|
96
|
+
else
|
|
97
|
+
res.destroy();
|
|
98
|
+
}
|
|
99
|
+
})();
|
|
100
|
+
});
|
|
101
|
+
server.keepAliveTimeout = 65_000;
|
|
102
|
+
server.requestTimeout = 0;
|
|
103
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(options.port, options.host, resolve); });
|
|
104
|
+
const port = server.address().port;
|
|
105
|
+
return {
|
|
106
|
+
port,
|
|
107
|
+
url: display(options.host, port),
|
|
108
|
+
close: (graceMs = 10_000) => new Promise(resolve => {
|
|
109
|
+
server.close(() => resolve());
|
|
110
|
+
server.closeIdleConnections?.();
|
|
111
|
+
const timer = setTimeout(() => { server.closeAllConnections?.(); }, graceMs);
|
|
112
|
+
timer.unref?.();
|
|
113
|
+
})
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Server package entry point (`@crvouga/atlas/server`)
|
|
3
|
+
*
|
|
4
|
+
* Everything needed to run the Atlas server from code: `createAtlasServer` (a `fetch` handler with the live report, uploads
|
|
5
|
+
* and the viewer), `listen` (serve it over HTTP on Bun or Node), `loadServerConfig` (the file, the environment and the flags
|
|
6
|
+
* as one checked configuration), `serveMain` (what `atlas serve` runs), `EventHub`, `createAuth`, and the upload checks.
|
|
7
|
+
*
|
|
8
|
+
* @see docs/server.md
|
|
9
|
+
*/
|
|
10
|
+
export { createAtlasServer, type AtlasServer, type AtlasServerOptions } from './app.js';
|
|
11
|
+
export { createAuth, type Auth, type Identity } from './auth.js';
|
|
12
|
+
export { assertSafeToListen, ConfigError, loadServerConfig, SERVER_USAGE, type ResolvedServerConfig, type ResolvedToken } from './config.js';
|
|
13
|
+
export { EventHub, type EventName } from './events.js';
|
|
14
|
+
export { listen, type RunningServer } from './host.js';
|
|
15
|
+
export { serveMain } from './main.js';
|
|
16
|
+
export { classify, validateUpload, validKey, Refusal } from './validate.js';
|
|
17
|
+
export { fileServerSettings, type ServerSettings, type SettingsSnapshot } from './settings.js';
|
|
18
|
+
export { LiveRuns, LiveRunError } from './live-runs.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Server package entry point (`@crvouga/atlas/server`)
|
|
3
|
+
*
|
|
4
|
+
* Everything needed to run the Atlas server from code: `createAtlasServer` (a `fetch` handler with the live report, uploads
|
|
5
|
+
* and the viewer), `listen` (serve it over HTTP on Bun or Node), `loadServerConfig` (the file, the environment and the flags
|
|
6
|
+
* as one checked configuration), `serveMain` (what `atlas serve` runs), `EventHub`, `createAuth`, and the upload checks.
|
|
7
|
+
*
|
|
8
|
+
* @see docs/server.md
|
|
9
|
+
*/
|
|
10
|
+
export { createAtlasServer } from './app.js';
|
|
11
|
+
export { createAuth } from './auth.js';
|
|
12
|
+
export { assertSafeToListen, ConfigError, loadServerConfig, SERVER_USAGE } from './config.js';
|
|
13
|
+
export { EventHub } from './events.js';
|
|
14
|
+
export { listen } from './host.js';
|
|
15
|
+
export { serveMain } from './main.js';
|
|
16
|
+
export { classify, validateUpload, validKey, Refusal } from './validate.js';
|
|
17
|
+
export { fileServerSettings } from './settings.js';
|
|
18
|
+
export { LiveRuns, LiveRunError } from './live-runs.js';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { z } from 'zod/v4';
|
|
2
|
+
import { type LiveRun, type LiveRunStartSchema, type LiveRunUpdateSchema } from '../../schema/src/live-run.js';
|
|
3
|
+
import type { ObjectStore } from '../../reports/src/object-store.js';
|
|
4
|
+
import type { ReportLayer } from '../../reports/src/layer-types.js';
|
|
5
|
+
type Start = z.output<typeof LiveRunStartSchema>;
|
|
6
|
+
type Update = z.output<typeof LiveRunUpdateSchema>;
|
|
7
|
+
/** A refused run update: 403 another publisher, 404 unknown run, 409 stale/terminal run, or 429 capacity. */
|
|
8
|
+
export declare class LiveRunError extends Error {
|
|
9
|
+
readonly status: 403 | 404 | 409 | 429;
|
|
10
|
+
constructor(status: 403 | 404 | 409 | 429, message: string);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* One server's live feed. Heartbeats never create result layers. Persisted summaries contain at most 20 recent
|
|
14
|
+
* checks and 10 active checks; completed results live in ordinary immutable layers. Leases expire by server time.
|
|
15
|
+
* @example
|
|
16
|
+
* const live = new LiveRuns({ store, timeoutMs: 30_000, stalledMs: 900_000, onChange: () => {}, onLayer: async () => {} });
|
|
17
|
+
*/
|
|
18
|
+
export declare class LiveRuns {
|
|
19
|
+
private readonly options;
|
|
20
|
+
private readonly held;
|
|
21
|
+
private readonly attempts;
|
|
22
|
+
private readonly queues;
|
|
23
|
+
/** Resolves after restoring summaries and interrupting leases left by a previous server process. */
|
|
24
|
+
readonly ready: Promise<void>;
|
|
25
|
+
private readonly now;
|
|
26
|
+
constructor(options: {
|
|
27
|
+
store: ObjectStore;
|
|
28
|
+
timeoutMs: number;
|
|
29
|
+
stalledMs: number;
|
|
30
|
+
now?: () => number;
|
|
31
|
+
onChange: (run: LiveRun) => void;
|
|
32
|
+
onLayer: (key: string, layer: ReportLayer) => Promise<void>;
|
|
33
|
+
});
|
|
34
|
+
private restore;
|
|
35
|
+
private save;
|
|
36
|
+
private locked;
|
|
37
|
+
private state;
|
|
38
|
+
private interrupt;
|
|
39
|
+
private expired;
|
|
40
|
+
private requireLive;
|
|
41
|
+
/** Read the most recent 50 summaries. Progress detail and counters are already bounded and cached. */
|
|
42
|
+
list(): Promise<LiveRun[]>;
|
|
43
|
+
/** Register one producer lease. Retrying an identical start is idempotent. */
|
|
44
|
+
start(input: Start, owner: string): Promise<LiveRun>;
|
|
45
|
+
/** Renew liveness by server receipt time, independently of whether a test made progress. */
|
|
46
|
+
heartbeat(id: string, owner: string): Promise<void>;
|
|
47
|
+
/** Apply one ordered progress batch and publish its completed checks before acknowledging it. */
|
|
48
|
+
update(id: string, input: Update, owner: string): Promise<LiveRun>;
|
|
49
|
+
/** Expire silent producers and optional progress stalls. Heartbeats cannot revive an expired run. */
|
|
50
|
+
sweep(): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/** @module Live run leases
|
|
2
|
+
* Tracks producer heartbeats independently of progress, publishes completed checks incrementally, and persists
|
|
3
|
+
* bounded run summaries. A server restart interrupts outstanding leases rather than inventing continued progress.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { LiveRunSchema } from '../../schema/src/live-run.js';
|
|
7
|
+
import { readUtf8, writeUtf8 } from '../../reports/src/object-io.js';
|
|
8
|
+
import { publishChecks } from '../../reports/src/publish-checks.js';
|
|
9
|
+
import { layerCatalogKey } from '../../reports/src/layer-store.js';
|
|
10
|
+
const PREFIX = 'server/live/';
|
|
11
|
+
const keyOf = (run) => `${PREFIX}${String(Number.MAX_SAFE_INTEGER - run.startedAtMs).padStart(16, '0')}-${run.id}.json`;
|
|
12
|
+
/** A refused run update: 403 another publisher, 404 unknown run, 409 stale/terminal run, or 429 capacity. */
|
|
13
|
+
export class LiveRunError extends Error {
|
|
14
|
+
status;
|
|
15
|
+
constructor(status, message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.status = status;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* One server's live feed. Heartbeats never create result layers. Persisted summaries contain at most 20 recent
|
|
22
|
+
* checks and 10 active checks; completed results live in ordinary immutable layers. Leases expire by server time.
|
|
23
|
+
* @example
|
|
24
|
+
* const live = new LiveRuns({ store, timeoutMs: 30_000, stalledMs: 900_000, onChange: () => {}, onLayer: async () => {} });
|
|
25
|
+
*/
|
|
26
|
+
export class LiveRuns {
|
|
27
|
+
options;
|
|
28
|
+
held = new Map();
|
|
29
|
+
attempts = new Map();
|
|
30
|
+
queues = new Map();
|
|
31
|
+
/** Resolves after restoring summaries and interrupting leases left by a previous server process. */
|
|
32
|
+
ready;
|
|
33
|
+
now;
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this.options = options;
|
|
36
|
+
this.now = options.now ?? Date.now;
|
|
37
|
+
this.ready = this.restore();
|
|
38
|
+
// Requests await ready and surface failures; no unhandled rejection while a backend is unavailable.
|
|
39
|
+
void this.ready.catch(() => undefined);
|
|
40
|
+
}
|
|
41
|
+
async restore() {
|
|
42
|
+
const keys = (await this.options.store.listKeys(PREFIX)).slice(0, 200);
|
|
43
|
+
for (const key of keys) {
|
|
44
|
+
const text = await readUtf8(this.options.store, key);
|
|
45
|
+
if (text === null)
|
|
46
|
+
continue;
|
|
47
|
+
const stored = JSON.parse(text);
|
|
48
|
+
const run = LiveRunSchema.parse(stored.run);
|
|
49
|
+
const state = { run, owner: stored.owner, digest: stored.digest, states: new Map() };
|
|
50
|
+
this.held.set(run.id, state);
|
|
51
|
+
if (run.status === 'running')
|
|
52
|
+
await this.interrupt(state, 'The Atlas server restarted before the run completed.');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async save(state) {
|
|
56
|
+
await writeUtf8(this.options.store, keyOf(state.run), JSON.stringify({ run: state.run, owner: state.owner, digest: state.digest }), 'application/json');
|
|
57
|
+
}
|
|
58
|
+
locked(id, action) {
|
|
59
|
+
const next = (this.queues.get(id) ?? Promise.resolve()).catch(() => { }).then(action);
|
|
60
|
+
this.queues.set(id, next);
|
|
61
|
+
return next.finally(() => { if (this.queues.get(id) === next)
|
|
62
|
+
this.queues.delete(id); });
|
|
63
|
+
}
|
|
64
|
+
state(id, owner) {
|
|
65
|
+
const held = this.held.get(id);
|
|
66
|
+
if (!held)
|
|
67
|
+
throw new LiveRunError(404, 'Unknown run.');
|
|
68
|
+
if (held.owner !== owner)
|
|
69
|
+
throw new LiveRunError(403, 'This run belongs to another publisher.');
|
|
70
|
+
return held;
|
|
71
|
+
}
|
|
72
|
+
async interrupt(state, reason) {
|
|
73
|
+
const next = { ...state, states: new Map(), run: { ...state.run, status: 'interrupted', reason, finishedAtMs: this.now(), running: 0,
|
|
74
|
+
current: state.run.current.map(check => ({ ...check, status: 'not-run', detail: reason })) } };
|
|
75
|
+
await this.save(next);
|
|
76
|
+
this.held.set(next.run.id, next);
|
|
77
|
+
this.options.onChange(next.run);
|
|
78
|
+
}
|
|
79
|
+
expired(run) {
|
|
80
|
+
if (this.now() - run.lastHeartbeatAtMs >= this.options.timeoutMs)
|
|
81
|
+
return 'Producer heartbeat timed out. The run may have exited or lost its connection.';
|
|
82
|
+
if (this.options.stalledMs > 0 && this.now() - run.lastProgressAtMs >= this.options.stalledMs)
|
|
83
|
+
return 'No test progress arrived before the configured stall timeout.';
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
async requireLive(state) {
|
|
87
|
+
if (state.run.status !== 'running')
|
|
88
|
+
throw new LiveRunError(409, 'This run has ended.');
|
|
89
|
+
const reason = this.expired(state.run);
|
|
90
|
+
if (reason) {
|
|
91
|
+
await this.interrupt(state, reason);
|
|
92
|
+
throw new LiveRunError(409, reason);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Read the most recent 50 summaries. Progress detail and counters are already bounded and cached. */
|
|
96
|
+
async list() {
|
|
97
|
+
await this.ready;
|
|
98
|
+
return [...this.held.values()].map(state => state.run).sort((a, b) => b.startedAtMs - a.startedAtMs || a.id.localeCompare(b.id)).slice(0, 50);
|
|
99
|
+
}
|
|
100
|
+
/** Register one producer lease. Retrying an identical start is idempotent. */
|
|
101
|
+
async start(input, owner) {
|
|
102
|
+
await this.ready;
|
|
103
|
+
return this.locked(input.id, async () => {
|
|
104
|
+
const prior = this.held.get(input.id);
|
|
105
|
+
if (prior) {
|
|
106
|
+
const found = this.state(input.id, owner);
|
|
107
|
+
if (Object.entries(input).some(([key, value]) => found.run[key] !== value))
|
|
108
|
+
throw new LiveRunError(409, 'A run id cannot be reused with another identity.');
|
|
109
|
+
return found.run;
|
|
110
|
+
}
|
|
111
|
+
if ([...this.held.values()].filter(state => state.run.status === 'running').length >= 100)
|
|
112
|
+
throw new LiveRunError(429, 'Too many live runs.');
|
|
113
|
+
const now = this.now();
|
|
114
|
+
const state = { owner, digest: '', states: new Map(), run: { ...input, status: 'running', sequence: 0, startedAtMs: now, lastHeartbeatAtMs: now, lastProgressAtMs: now, completed: 0, passed: 0, failed: 0, notRun: 0, running: 0, current: [], recent: [] } };
|
|
115
|
+
await this.save(state);
|
|
116
|
+
this.held.set(input.id, state);
|
|
117
|
+
// Retain bounded terminal history in memory; durable summaries remain in the configured backend.
|
|
118
|
+
if (this.held.size > 200) {
|
|
119
|
+
const old = [...this.held.values()].filter(value => value.run.status !== 'running').sort((a, b) => a.run.startedAtMs - b.run.startedAtMs)[0];
|
|
120
|
+
if (old)
|
|
121
|
+
this.held.delete(old.run.id);
|
|
122
|
+
}
|
|
123
|
+
this.options.onChange(state.run);
|
|
124
|
+
return state.run;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** Renew liveness by server receipt time, independently of whether a test made progress. */
|
|
128
|
+
async heartbeat(id, owner) {
|
|
129
|
+
await this.ready;
|
|
130
|
+
await this.locked(id, async () => {
|
|
131
|
+
const state = this.state(id, owner);
|
|
132
|
+
await this.requireLive(state);
|
|
133
|
+
const next = { ...state, run: { ...state.run, lastHeartbeatAtMs: this.now() } };
|
|
134
|
+
await this.save(next);
|
|
135
|
+
this.held.set(id, next);
|
|
136
|
+
this.options.onChange(next.run);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/** Apply one ordered progress batch and publish its completed checks before acknowledging it. */
|
|
140
|
+
async update(id, input, owner) {
|
|
141
|
+
await this.ready;
|
|
142
|
+
return this.locked(id, async () => {
|
|
143
|
+
const state = this.state(id, owner);
|
|
144
|
+
const digest = createHash('sha256').update(JSON.stringify(input)).digest('hex');
|
|
145
|
+
if (input.sequence === state.run.sequence && digest === state.digest)
|
|
146
|
+
return state.run;
|
|
147
|
+
await this.requireLive(state);
|
|
148
|
+
if (input.sequence !== state.run.sequence + 1)
|
|
149
|
+
throw new LiveRunError(409, 'This run ended or its progress sequence is stale.');
|
|
150
|
+
if (state.states.size + input.checks.filter(check => !state.states.has(check.id)).length > 100_000)
|
|
151
|
+
throw new LiveRunError(429, 'A live run may track at most 100000 check identities.');
|
|
152
|
+
const priorAttempt = this.attempts.get(id);
|
|
153
|
+
if (priorAttempt && (priorAttempt.sequence !== input.sequence || priorAttempt.digest !== digest))
|
|
154
|
+
throw new LiveRunError(409, 'Retry the pending sequence with identical content.');
|
|
155
|
+
const now = priorAttempt?.at ?? Math.max(this.now(), state.run.lastProgressAtMs + 1);
|
|
156
|
+
const states = new Map(state.states);
|
|
157
|
+
const run = { ...state.run, sequence: input.sequence, lastHeartbeatAtMs: now, ...(input.checks.length ? { lastProgressAtMs: now } : {}) };
|
|
158
|
+
const current = new Map(run.current.map(check => [check.id, check]));
|
|
159
|
+
for (const check of input.checks) {
|
|
160
|
+
const prior = states.get(check.id);
|
|
161
|
+
if (prior === 'running')
|
|
162
|
+
run.running--;
|
|
163
|
+
else if (prior) {
|
|
164
|
+
run.completed--;
|
|
165
|
+
if (prior === 'passed')
|
|
166
|
+
run.passed--;
|
|
167
|
+
else if (prior === 'failed')
|
|
168
|
+
run.failed--;
|
|
169
|
+
else
|
|
170
|
+
run.notRun--;
|
|
171
|
+
}
|
|
172
|
+
states.set(check.id, check.status);
|
|
173
|
+
if (check.status === 'running') {
|
|
174
|
+
run.running++;
|
|
175
|
+
current.set(check.id, check);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
run.completed++;
|
|
179
|
+
if (check.status === 'passed')
|
|
180
|
+
run.passed++;
|
|
181
|
+
else if (check.status === 'failed')
|
|
182
|
+
run.failed++;
|
|
183
|
+
else
|
|
184
|
+
run.notRun++;
|
|
185
|
+
current.delete(check.id);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
run.current = [...current.values()].slice(-10);
|
|
189
|
+
run.recent = [...input.checks].reverse().concat(run.recent.filter(check => !input.checks.some(next => next.id === check.id))).slice(0, 20);
|
|
190
|
+
if (input.finish) {
|
|
191
|
+
if (input.finish === 'completed' && run.running)
|
|
192
|
+
throw new LiveRunError(409, 'Finish running checks before completing the run.');
|
|
193
|
+
run.status = input.finish;
|
|
194
|
+
run.finishedAtMs = now;
|
|
195
|
+
run.reason = input.reason;
|
|
196
|
+
if (input.finish === 'interrupted') {
|
|
197
|
+
run.running = 0;
|
|
198
|
+
run.current = run.current.map(check => ({ ...check, status: 'not-run', detail: input.reason ?? 'Run interrupted.' }));
|
|
199
|
+
}
|
|
200
|
+
states.clear();
|
|
201
|
+
}
|
|
202
|
+
const finished = input.checks.filter(check => check.status !== 'running');
|
|
203
|
+
this.attempts.set(id, { sequence: input.sequence, digest, at: now });
|
|
204
|
+
if (finished.length) {
|
|
205
|
+
const layer = await publishChecks(this.options.store, { producer: state.run.producer, environment: state.run.environment, branch: state.run.branch, sha: state.run.sha, recordedAtMs: now,
|
|
206
|
+
labels: { 'atlas.run': id }, checks: finished });
|
|
207
|
+
await this.options.onLayer(layerCatalogKey(layer.recordedAtMs, layer.id), layer);
|
|
208
|
+
}
|
|
209
|
+
const next = { ...state, states, run, digest };
|
|
210
|
+
await this.save(next);
|
|
211
|
+
this.held.set(id, next);
|
|
212
|
+
this.attempts.delete(id);
|
|
213
|
+
this.options.onChange(run);
|
|
214
|
+
return run;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/** Expire silent producers and optional progress stalls. Heartbeats cannot revive an expired run. */
|
|
218
|
+
async sweep() {
|
|
219
|
+
await this.ready;
|
|
220
|
+
await Promise.all([...this.held.keys()].map(id => this.locked(id, async () => {
|
|
221
|
+
const state = this.held.get(id);
|
|
222
|
+
if (state.run.status !== 'running')
|
|
223
|
+
return;
|
|
224
|
+
const reason = this.expired(state.run);
|
|
225
|
+
if (reason)
|
|
226
|
+
await this.interrupt(state, reason);
|
|
227
|
+
})));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module llms.txt of a running server
|
|
3
|
+
*
|
|
4
|
+
* The plain-text guide ([llms.txt](https://llmstxt.org)) a server serves about itself at `GET /llms.txt`, for an AI
|
|
5
|
+
* agent that was handed nothing but the server's address. It is written from this server's own settings (its
|
|
6
|
+
* address, whether it needs a token), so what it says is true of the server that says it, and it points at the
|
|
7
|
+
* machine-readable description (`/api/openapi.json`) rather than repeating it.
|
|
8
|
+
*/
|
|
9
|
+
/** What the guide says about the server that serves it. */
|
|
10
|
+
export type LlmsContext = {
|
|
11
|
+
/** The address clients use, such as `https://atlas.example.com`, no trailing slash. */
|
|
12
|
+
readonly url: string;
|
|
13
|
+
/** True when the server has no tokens: anyone who can reach it may upload. */
|
|
14
|
+
readonly open: boolean;
|
|
15
|
+
/** The id of the backend uploads go to. */
|
|
16
|
+
readonly write: string;
|
|
17
|
+
};
|
|
18
|
+
/** The guide for one server, Markdown. Stable in structure: an agent may read it by its headings. */
|
|
19
|
+
export declare function llmsTxt(context: LlmsContext): string;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module llms.txt of a running server
|
|
3
|
+
*
|
|
4
|
+
* The plain-text guide ([llms.txt](https://llmstxt.org)) a server serves about itself at `GET /llms.txt`, for an AI
|
|
5
|
+
* agent that was handed nothing but the server's address. It is written from this server's own settings (its
|
|
6
|
+
* address, whether it needs a token), so what it says is true of the server that says it, and it points at the
|
|
7
|
+
* machine-readable description (`/api/openapi.json`) rather than repeating it.
|
|
8
|
+
*/
|
|
9
|
+
/** The guide for one server, Markdown. Stable in structure: an agent may read it by its headings. */
|
|
10
|
+
export function llmsTxt(context) {
|
|
11
|
+
const { url } = context;
|
|
12
|
+
return `# Atlas server at ${url}
|
|
13
|
+
|
|
14
|
+
> Atlas shows whether a product still does what it was specified to do. Every environment (laptops, CI, device labs) sends its test results here as immutable "layers"; this server keeps them and shows everyone one combined report, live. The newest completed result of every check wins its slot, whoever sent it.
|
|
15
|
+
|
|
16
|
+
## Machine-readable description
|
|
17
|
+
|
|
18
|
+
- [OpenAPI 3.1](${url}/api/openapi.json): every endpoint, parameter, body, response and error. Generate a client or a test suite from it.
|
|
19
|
+
- [JSON Schemas](https://unpkg.com/@crvouga/atlas/packages/schema/schemas/): the layer format, the checks file, the report status and the server's responses.
|
|
20
|
+
- Errors are RFC 9457 problem documents (\`application/problem+json\`); \`detail\` says what to do.
|
|
21
|
+
- Live updates: \`GET ${url}/api/events\` (server-sent events: \`hello\`, \`report\`, \`atlas:change\`, \`ping\`).
|
|
22
|
+
|
|
23
|
+
## Authenticate
|
|
24
|
+
|
|
25
|
+
${context.open
|
|
26
|
+
? 'This server has **no tokens**: anyone who can reach it may upload. Nothing to configure.'
|
|
27
|
+
: `This server needs a token. Ask the operator for a \`publish\` token (to upload) and a \`read\` token (to look). Send \`Authorization: Bearer <token>\`; the CLI reads \`ATLAS_TOKEN\`. Uploading needs the Bearer header. A token is a secret of this one server: never put it in a URL or send it to another host.`}
|
|
28
|
+
|
|
29
|
+
## Integrate a project (run in the project's repository)
|
|
30
|
+
|
|
31
|
+
\`\`\`sh
|
|
32
|
+
export ATLAS_STORE=${url}${context.open ? '' : '\nexport ATLAS_TOKEN=<a publish token>'}
|
|
33
|
+
bunx @crvouga/atlas init --server ${url} # detects test runners and specs, prints a plan (add --json for a machine-readable one)
|
|
34
|
+
bunx @crvouga/atlas init --server ${url} --write # writes the CI workflow and the AGENTS.md section the plan names
|
|
35
|
+
bunx @crvouga/atlas report doctor # proves the server can be written to and read
|
|
36
|
+
\`\`\`
|
|
37
|
+
|
|
38
|
+
After each suite, send its results (a JUnit or CTRF file, or JSON lines as checks finish):
|
|
39
|
+
|
|
40
|
+
\`\`\`sh
|
|
41
|
+
bunx @crvouga/atlas report publish --producer <suite> --junit out.xml --specs ./specs --environment <env>
|
|
42
|
+
my-suite --json | bunx @crvouga/atlas report stream --producer <suite> --environment <env>
|
|
43
|
+
\`\`\`
|
|
44
|
+
|
|
45
|
+
Or with no tooling at all: \`POST ${url}/api/checks\` with \`{"producer":"smoke","checks":[{"id":"login","title":"can log in","status":"passed"}]}\`.
|
|
46
|
+
|
|
47
|
+
## Ask whether it passes
|
|
48
|
+
|
|
49
|
+
\`\`\`sh
|
|
50
|
+
bunx @crvouga/atlas report status --format json --branch main --ignore-modified # exit 0 pass, 1 does not, 2 wrong command
|
|
51
|
+
curl ${context.open ? '' : '-H "authorization: Bearer $ATLAS_TOKEN" '}${url}/api/status # the same verdict; header X-Atlas-Ok: true|false
|
|
52
|
+
\`\`\`
|
|
53
|
+
|
|
54
|
+
## Rules worth knowing
|
|
55
|
+
|
|
56
|
+
- **Layered, never partitioned.** A check is identified by its \`id\` (keep it stable across runs) plus producer, kind, spec, environment and parameters. A layer that re-ran one check leaves every other check as the last layer that ran it.
|
|
57
|
+
- **Unknown is never a pass.** A check still \`running\`, a layer that could not be read, a backend that is down, or a required environment with no results all fail the gate.
|
|
58
|
+
- **Failures are evidence: publish them.** Run the suite, ignore its exit code, publish, then gate.
|
|
59
|
+
- **Layers are immutable** and named by their content: re-publishing is safe, and a retry never makes a duplicate.
|
|
60
|
+
- Uploads go to the backend \`${context.write}\`. Read \`${url}/api/server\` for the backends and what this server can do (\`capabilities\`).
|
|
61
|
+
- Evidence (screenshots, videos, traces, logs, steps, annotations, your own data) rides on checks: see https://github.com/crvouga/atlas/blob/main/docs/extending.md
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module `atlas serve`
|
|
3
|
+
*
|
|
4
|
+
* Starts the Atlas server from its configuration: opens every backend (creating the data folder), proves the upload
|
|
5
|
+
* backend can be written to before listening, finds or builds the client, listens at once (so a liveness probe finds the
|
|
6
|
+
* process while a large backend is still being read), and stops cleanly on SIGTERM or SIGINT. `serverOptionsOf` is shared
|
|
7
|
+
* with `atlas dev`, so the two run the same server.
|
|
8
|
+
*
|
|
9
|
+
* @see docs/server.md#run-it
|
|
10
|
+
*/
|
|
11
|
+
import type { BackendInput } from '../../reports/src/compose.js';
|
|
12
|
+
import { type AtlasServerOptions } from './app.js';
|
|
13
|
+
import { type ResolvedServerConfig } from './config.js';
|
|
14
|
+
type Env = Readonly<Record<string, string | undefined>>;
|
|
15
|
+
/** What `serveMain` and `devMain` are given besides arguments: where output goes, and, for tests and embedding, the environment, working directory and an `AbortSignal` that stops the server (otherwise SIGTERM and SIGINT do). */
|
|
16
|
+
export type ServeIo = {
|
|
17
|
+
readonly out: (text: string) => void;
|
|
18
|
+
readonly env?: Env;
|
|
19
|
+
readonly cwd?: string;
|
|
20
|
+
readonly signal?: AbortSignal;
|
|
21
|
+
};
|
|
22
|
+
/** Open every backend of a configuration. The data folder is made if it is not there. */
|
|
23
|
+
export declare function openConfiguredBackends(config: ResolvedServerConfig, env: Env, cwd: string): (BackendInput & {
|
|
24
|
+
readonly writable: boolean;
|
|
25
|
+
})[];
|
|
26
|
+
/** What `createAtlasServer` is given for a configuration: `atlas serve` and `atlas dev` run the same server. */
|
|
27
|
+
export declare function serverOptionsOf(config: ResolvedServerConfig, backends: ReturnType<typeof openConfiguredBackends>, log?: (line: Record<string, unknown>) => void): AtlasServerOptions;
|
|
28
|
+
/** `atlas serve`. Runs until it is told to stop; returns the exit code. */
|
|
29
|
+
export declare function serveMain(argv: readonly string[], io: ServeIo): Promise<number>;
|
|
30
|
+
export {};
|