@crvouga/atlas 0.4.0 → 0.5.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 +7 -1
- package/README.md +178 -617
- 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 +30 -1
- package/dist/core/src/config.d.ts +70 -4
- package/dist/core/src/config.js +62 -3
- 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 +69 -16
- package/dist/core/src/run/execute.js +67 -16
- 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 +83 -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 +52 -0
- package/dist/reports/src/http-store.js +145 -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 +364 -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/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 +485 -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/schema/src/api.d.ts +799 -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 +14 -0
- package/dist/schema/src/index.js +14 -0
- package/dist/schema/src/layer.d.ts +2273 -0
- package/dist/schema/src/layer.js +462 -0
- package/dist/schema/src/manifest.d.ts +95 -0
- package/dist/schema/src/manifest.js +84 -0
- package/dist/schema/src/report-sources.d.ts +28 -0
- package/dist/schema/src/report-sources.js +25 -0
- package/dist/schema/src/runs-index.d.ts +16 -0
- package/dist/schema/src/runs-index.js +15 -0
- package/dist/schema/src/server-config.d.ts +93 -0
- package/dist/schema/src/server-config.js +73 -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 +62 -0
- package/dist/server/src/app.js +419 -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 +187 -0
- package/dist/server/src/dev.d.ts +12 -0
- package/dist/server/src/dev.js +80 -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 +16 -0
- package/dist/server/src/index.js +16 -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 +112 -0
- package/dist/server/src/openapi.d.ts +59 -0
- package/dist/server/src/openapi.js +374 -0
- package/dist/server/src/problem.d.ts +8 -0
- package/dist/server/src/problem.js +39 -0
- package/dist/server/src/site.d.ts +13 -0
- package/dist/server/src/site.js +43 -0
- package/dist/server/src/store-api.d.ts +28 -0
- package/dist/server/src/store-api.js +159 -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 +146 -0
- package/docs/reference/core.md +2426 -0
- package/docs/reference/detox.md +150 -0
- package/docs/reference/http-api.md +303 -0
- package/docs/reference/playwright.md +303 -0
- package/docs/reference/reports.md +7098 -0
- package/docs/reference/schema.md +1651 -0
- package/docs/reference/server.md +685 -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/server.md +285 -0
- package/docs/statecharts.md +641 -0
- package/llms-full.txt +3506 -0
- package/llms.txt +47 -0
- package/package.json +32 -4
- package/packages/core/README.md +2 -1
- package/packages/reports/README.md +65 -0
- package/packages/reports/STORAGE.md +4 -0
- package/packages/schema/README.md +14 -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 +22 -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/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/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 +194 -0
- package/packages/schema/schemas/server-info.schema.json +114 -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 +6699 -0
- package/packages/visualizer/README.md +21 -0
- package/packages/visualizer/src/app/AppShell.tsx +9 -2
- package/packages/visualizer/src/app/report-sources.tsx +6 -4
- package/packages/visualizer/src/app/router.tsx +3 -3
- package/packages/visualizer/src/components/Header.module.css +35 -0
- package/packages/visualizer/src/components/Header.tsx +40 -3
- package/packages/visualizer/src/data/adapters/adapter.ts +5 -17
- package/packages/visualizer/src/data/adapters/index.ts +10 -1
- package/packages/visualizer/src/evidence/api.ts +14 -2
- 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 +3 -1
- package/packages/visualizer/src/evidence/evidence-context.tsx +74 -12
- package/packages/visualizer/src/evidence/spec-page.tsx +23 -16
- package/packages/visualizer/src/find/Finder.tsx +1 -7
- package/packages/visualizer/src/find/open.ts +4 -0
- package/packages/visualizer/src/live/live-stream.ts +204 -0
- package/packages/visualizer/src/runtime.ts +19 -0
- package/packages/visualizer/vite/atlas-server-proxy.ts +36 -0
- package/packages/visualizer/vite.config.ts +11 -3
|
@@ -1,21 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Layer types
|
|
3
|
+
*
|
|
4
|
+
* The in-memory shapes of a layer: `LayerSummary` (everything but its captured files), `ReportLayer` (the summary plus
|
|
5
|
+
* the index of those files) and `ReportOutline` (the specs a report is laid over). The stored form is the layer
|
|
6
|
+
* document of `packages/schema/src/layer.ts`; these types are what the reader and publisher code hands around after
|
|
7
|
+
* parsing it. Fields follow the layer document, so adding a field here means adding an optional field there.
|
|
8
|
+
*
|
|
9
|
+
* @see docs/report-contract.md#the-layer-document
|
|
10
|
+
*/
|
|
1
11
|
import type { BundleIndex } from './bundle.js';
|
|
2
12
|
import type { ReportPart, SpecEntry } from './explorer-types.js';
|
|
3
13
|
import type { SourceRecord } from './protocol.js';
|
|
14
|
+
/** String choices (such as `{ device: 'iphone-15' }`) that make a result its own slot. Compared as sorted key/value pairs. */
|
|
4
15
|
export type LayerParameters = Readonly<Record<string, string>>;
|
|
5
|
-
|
|
16
|
+
/** What a layer may carry as a whole. All optional and open: see docs/extending.md. */
|
|
17
|
+
export type LayerExtras = Partial<Pick<import('../../schema/src/layer.js').LayerDocument, 'tags' | 'labels' | 'links' | 'attachments' | 'annotations' | 'extensions'>>;
|
|
18
|
+
/** A layer without its captured files: what composition and the viewer's lists read. */
|
|
19
|
+
export type LayerSummary = LayerExtras & {
|
|
20
|
+
/** `modified`: recorded from a checkout with uncommitted changes. `unknown`: origin not captured. Absent for a clean checkout. Advisory; never changes a status. */
|
|
6
21
|
readonly freshness?: 'unknown' | 'modified';
|
|
22
|
+
/** The layer's content digest: 64 lowercase hex digits. Opaque to readers. Legacy commit-keyed recordings use `part-<sha>-<id>` or `source-<sha>-<id>` instead. */
|
|
7
23
|
readonly id: string;
|
|
24
|
+
/** When the checks ran, epoch milliseconds. The newest completed result wins its slot. */
|
|
8
25
|
readonly recordedAtMs: number;
|
|
26
|
+
/** The stable name of the suite or tool that made the layer. Part of the slot. */
|
|
9
27
|
readonly producer: string;
|
|
28
|
+
/** Provenance: the commit recorded, or `null`. Never part of the slot. */
|
|
10
29
|
readonly sha: string | null;
|
|
30
|
+
/** Provenance: the branch recorded, or `null`. Never part of the slot. */
|
|
11
31
|
readonly branch: string | null;
|
|
12
32
|
/** The client this layer checked, when it said. Normalized when the layer is read. */
|
|
13
33
|
readonly environment?: string | null;
|
|
34
|
+
/** Provenance: the CI run that produced the layer, or `null`. Never part of the slot. */
|
|
14
35
|
readonly runUrl: string | null;
|
|
36
|
+
/** Free choices that make a result its own slot. The `environment` parameter is read as the environment (see `layerPlace`). */
|
|
15
37
|
readonly parameters: LayerParameters;
|
|
38
|
+
/** The specs the layer was recorded against, with fingerprints of their text. */
|
|
16
39
|
readonly specs: readonly SpecEntry[];
|
|
40
|
+
/** The checks, grouped by producer-defined kind. */
|
|
17
41
|
readonly parts: readonly ReportPart[];
|
|
42
|
+
/** The recorded statechart run behind the layer (timelines and tests), or `null` for a layer of plain checks. */
|
|
18
43
|
readonly source: SourceRecord | null;
|
|
44
|
+
/** Where the layer's timelines and captured files are read from: its own bundle (`layer`), an older commit-keyed recording (`commit`), or nowhere (`null`). */
|
|
19
45
|
readonly recording: {
|
|
20
46
|
readonly type: 'layer';
|
|
21
47
|
} | {
|
|
@@ -24,11 +50,16 @@ export type LayerSummary = {
|
|
|
24
50
|
readonly source: string;
|
|
25
51
|
} | null;
|
|
26
52
|
};
|
|
53
|
+
/** A layer with the index that locates its captured files (screenshots, frames) in the store. */
|
|
27
54
|
export type ReportLayer = LayerSummary & {
|
|
28
55
|
readonly bundle: BundleIndex;
|
|
29
56
|
};
|
|
57
|
+
/** The current specs a report is laid over, and the commit and changes freshness warnings are judged against. */
|
|
30
58
|
export type ReportOutline = {
|
|
59
|
+
/** The specs that make the outline of the report. A spec here with no layer shows as having no results. */
|
|
31
60
|
readonly specs: readonly SpecEntry[];
|
|
61
|
+
/** The commit the specs are from, or `null` when unknown. */
|
|
32
62
|
readonly sha: string | null;
|
|
63
|
+
/** Paths changed since the layers were recorded; a result touching one is warned as potentially outdated. */
|
|
33
64
|
readonly changedFiles: readonly string[];
|
|
34
65
|
};
|
|
@@ -1,20 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Layers HTTP routes
|
|
3
|
+
*
|
|
4
|
+
* The read-only HTTP API the viewer uses to see layers: a paged list of layer headers merged from every backend, the
|
|
5
|
+
* rows of one layer, its attachments and recorded timelines, the live runs still writing on this machine, and the
|
|
6
|
+
* spec outline. It is mounted under `/api/reports` by `reportsHost` and by the Atlas server. Immutable layers (64-hex
|
|
7
|
+
* ids) are read once and cached for good and answered with long-lived `immutable` cache headers; mutable commit-keyed
|
|
8
|
+
* recordings are re-read after a short wait. A backend that cannot be listed or a layer that cannot be read becomes a
|
|
9
|
+
* notice in the page, never a silent omission and never a pass. Layers the host's `include` filter rejects are absent
|
|
10
|
+
* from every route here, so a filter cannot be undone by asking for history.
|
|
11
|
+
*
|
|
12
|
+
* @see docs/server.md
|
|
13
|
+
* @see docs/report-contract.md#failure-behaviour
|
|
14
|
+
*/
|
|
1
15
|
import { Hono } from 'hono';
|
|
2
16
|
import type { ObjectStore } from './object-store.js';
|
|
3
17
|
import type { ReportsAppDeps } from './reports-route.js';
|
|
4
|
-
/** A further store whose layers join the combined report, beside the local and the shared one. */
|
|
18
|
+
/** A further store whose layers join the combined report, beside the local and the shared one. Passed as `backends` in `ReportsAppDeps`. */
|
|
5
19
|
export type ReportBackend = {
|
|
6
|
-
/** Stable and unique: letters, digits and hyphens. `local` and `shared` are taken. */
|
|
20
|
+
/** Stable and unique: lowercase letters, digits and hyphens. `local` and `shared` are taken. Appears in cursors and URLs. */
|
|
7
21
|
readonly id: string;
|
|
22
|
+
/** Human name shown for the backend in the viewer. */
|
|
8
23
|
readonly label: string;
|
|
24
|
+
/** Where the backend's layers are read from. */
|
|
9
25
|
readonly store: ObjectStore;
|
|
10
26
|
};
|
|
11
27
|
type Backend = ReportBackend & {
|
|
12
28
|
readonly live: boolean;
|
|
13
29
|
};
|
|
30
|
+
/** The id of the backend for this machine's own store (`localStore`), where runs may still be writing. */
|
|
14
31
|
export declare const LOCAL_BACKEND = "local";
|
|
32
|
+
/** The id of the backend for the shared reports store (`store`). */
|
|
15
33
|
export declare const SHARED_BACKEND = "shared";
|
|
16
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* The stores that contribute layers, in the order they answer for a recording: `localStore` (id `local`, live), then
|
|
36
|
+
* `store` (id `shared`) when it is a different store, then each of `backends`.
|
|
37
|
+
*
|
|
38
|
+
* Throws `Invalid report backend id: "<id>"` when an extra backend's id is not lowercase letters, digits and hyphens
|
|
39
|
+
* or repeats an earlier id (including `local` and `shared`).
|
|
40
|
+
*/
|
|
17
41
|
export declare function reportBackends(deps: ReportsAppDeps): Backend[];
|
|
18
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Builds the Hono app that serves layers: bounded pages of layer origins, each layer's checks read once, and the
|
|
44
|
+
* runs still writing. Throws (as `reportBackends`) when a backend id is invalid.
|
|
45
|
+
*
|
|
46
|
+
* Routes, relative to where it is mounted:
|
|
47
|
+
* - `GET /outline` the current specs as `{ specs, sha, changedFiles }` (503 when the host has no outline), cached 30 s.
|
|
48
|
+
* - `GET /outline/spec?path=` one outline spec's text; 404 for a path not in the outline.
|
|
49
|
+
* - `GET /evidence?cursor=&retry=` one page of layer headers (`EvidencePage`) from 40 catalog entries across all
|
|
50
|
+
* backends: immutable layers newest first, then commit-keyed recordings. Without a cursor it also carries the live
|
|
51
|
+
* runs of this machine. `cursor` is the previous page's; 409 when it no longer matches the listing (refresh).
|
|
52
|
+
* Any `retry` value re-lists backends that were unavailable now instead of after the wait.
|
|
53
|
+
* - `GET /evidence/rows/:backend/:id` the checks and spec fingerprints of one layer (`LayerRows`).
|
|
54
|
+
* - `GET /evidence/detail/:backend/:id?check=&kind=` the run-level evidence of an immutable layer, or one check's detail.
|
|
55
|
+
* - `GET /layers/:id/blobs/*` an attachment of an immutable layer by its bundle key, served by `attachmentResponse`.
|
|
56
|
+
* - `GET /layers/:id/*` the recorded `frame.css`, `tests/<test>/report.json` and `tests/<test>/files/<frame>` of a layer.
|
|
57
|
+
* - `GET /live/:source/frame.css` and `GET /live/:source/tests/:testId/*` the same for a run still writing (uncached).
|
|
58
|
+
*
|
|
59
|
+
* Directory listings are cached for 30 s (1.5 s for the local store). A listing that fails keeps the previous keys and
|
|
60
|
+
* marks the backend unavailable; the page then carries a notice.
|
|
61
|
+
*/
|
|
19
62
|
export declare function createLayersApp(deps: ReportsAppDeps): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
|
|
20
63
|
export {};
|
|
@@ -1,11 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Layers HTTP routes
|
|
3
|
+
*
|
|
4
|
+
* The read-only HTTP API the viewer uses to see layers: a paged list of layer headers merged from every backend, the
|
|
5
|
+
* rows of one layer, its attachments and recorded timelines, the live runs still writing on this machine, and the
|
|
6
|
+
* spec outline. It is mounted under `/api/reports` by `reportsHost` and by the Atlas server. Immutable layers (64-hex
|
|
7
|
+
* ids) are read once and cached for good and answered with long-lived `immutable` cache headers; mutable commit-keyed
|
|
8
|
+
* recordings are re-read after a short wait. A backend that cannot be listed or a layer that cannot be read becomes a
|
|
9
|
+
* notice in the page, never a silent omission and never a pass. Layers the host's `include` filter rejects are absent
|
|
10
|
+
* from every route here, so a filter cannot be undone by asking for history.
|
|
11
|
+
*
|
|
12
|
+
* @see docs/server.md
|
|
13
|
+
* @see docs/report-contract.md#failure-behaviour
|
|
14
|
+
*/
|
|
1
15
|
import { Hono } from 'hono';
|
|
2
16
|
import { createBundleReader } from './bundle.js';
|
|
3
17
|
import { eachLimited } from './completion-order.js';
|
|
4
18
|
import { createLiveEvidence, liveRunRecord } from './evidence-live.js';
|
|
5
19
|
import { isFramePath, isKeySegment } from './keys.js';
|
|
6
20
|
import { catalogEntries, catalogLayers } from './layer-catalog.js';
|
|
21
|
+
import { attachmentResponse } from './attachment-response.js';
|
|
22
|
+
import { checkDetailOf, runEvidence } from './evidence-detail.js';
|
|
7
23
|
import { layerEvidence } from './layer-merge.js';
|
|
8
|
-
import { readLayer } from './layer-store.js';
|
|
24
|
+
import { layerSummary, readLayer } from './layer-store.js';
|
|
9
25
|
import { readBytes, readUtf8 } from './object-io.js';
|
|
10
26
|
import { SPEC_RUN_ID } from './publish.js';
|
|
11
27
|
import { testFrameKey, testReportKey } from './recording-keys.js';
|
|
@@ -13,9 +29,17 @@ const TTL = 30_000;
|
|
|
13
29
|
const LOCAL_TTL = 1_500;
|
|
14
30
|
const PAGE = 40;
|
|
15
31
|
const CACHED_ENTRIES = 4_096;
|
|
32
|
+
/** The id of the backend for this machine's own store (`localStore`), where runs may still be writing. */
|
|
16
33
|
export const LOCAL_BACKEND = 'local';
|
|
34
|
+
/** The id of the backend for the shared reports store (`store`). */
|
|
17
35
|
export const SHARED_BACKEND = 'shared';
|
|
18
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* The stores that contribute layers, in the order they answer for a recording: `localStore` (id `local`, live), then
|
|
38
|
+
* `store` (id `shared`) when it is a different store, then each of `backends`.
|
|
39
|
+
*
|
|
40
|
+
* Throws `Invalid report backend id: "<id>"` when an extra backend's id is not lowercase letters, digits and hyphens
|
|
41
|
+
* or repeats an earlier id (including `local` and `shared`).
|
|
42
|
+
*/
|
|
19
43
|
export function reportBackends(deps) {
|
|
20
44
|
const backends = [];
|
|
21
45
|
if (deps.localStore)
|
|
@@ -29,12 +53,34 @@ export function reportBackends(deps) {
|
|
|
29
53
|
}
|
|
30
54
|
return backends;
|
|
31
55
|
}
|
|
56
|
+
/** Whether a layer id is a content digest (64 lowercase hex digits), that is an immutable layer. */
|
|
32
57
|
function archived(id) {
|
|
33
58
|
return /^[0-9a-f]{64}$/.test(id);
|
|
34
59
|
}
|
|
35
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Builds the Hono app that serves layers: bounded pages of layer origins, each layer's checks read once, and the
|
|
62
|
+
* runs still writing. Throws (as `reportBackends`) when a backend id is invalid.
|
|
63
|
+
*
|
|
64
|
+
* Routes, relative to where it is mounted:
|
|
65
|
+
* - `GET /outline` the current specs as `{ specs, sha, changedFiles }` (503 when the host has no outline), cached 30 s.
|
|
66
|
+
* - `GET /outline/spec?path=` one outline spec's text; 404 for a path not in the outline.
|
|
67
|
+
* - `GET /evidence?cursor=&retry=` one page of layer headers (`EvidencePage`) from 40 catalog entries across all
|
|
68
|
+
* backends: immutable layers newest first, then commit-keyed recordings. Without a cursor it also carries the live
|
|
69
|
+
* runs of this machine. `cursor` is the previous page's; 409 when it no longer matches the listing (refresh).
|
|
70
|
+
* Any `retry` value re-lists backends that were unavailable now instead of after the wait.
|
|
71
|
+
* - `GET /evidence/rows/:backend/:id` the checks and spec fingerprints of one layer (`LayerRows`).
|
|
72
|
+
* - `GET /evidence/detail/:backend/:id?check=&kind=` the run-level evidence of an immutable layer, or one check's detail.
|
|
73
|
+
* - `GET /layers/:id/blobs/*` an attachment of an immutable layer by its bundle key, served by `attachmentResponse`.
|
|
74
|
+
* - `GET /layers/:id/*` the recorded `frame.css`, `tests/<test>/report.json` and `tests/<test>/files/<frame>` of a layer.
|
|
75
|
+
* - `GET /live/:source/frame.css` and `GET /live/:source/tests/:testId/*` the same for a run still writing (uncached).
|
|
76
|
+
*
|
|
77
|
+
* Directory listings are cached for 30 s (1.5 s for the local store). A listing that fails keeps the previous keys and
|
|
78
|
+
* marks the backend unavailable; the page then carries a notice.
|
|
79
|
+
*/
|
|
36
80
|
export function createLayersApp(deps) {
|
|
37
81
|
const backends = reportBackends(deps);
|
|
82
|
+
// Layers the host leaves out are left out of every route, so a filter cannot be undone by asking for history.
|
|
83
|
+
const include = deps.include ?? (() => true);
|
|
38
84
|
const listings = backends.map(() => undefined);
|
|
39
85
|
let pending = null;
|
|
40
86
|
const loaded = new Map();
|
|
@@ -109,8 +155,9 @@ export function createLayersApp(deps) {
|
|
|
109
155
|
let reader = readers.get(id);
|
|
110
156
|
if (!reader) {
|
|
111
157
|
for (const backend of backends) {
|
|
112
|
-
const layer = await readLayer(backend.store, id);
|
|
113
|
-
|
|
158
|
+
const layer = await readLayer(backend.store, id).catch(() => null);
|
|
159
|
+
// A layer the host leaves out has no files to give either.
|
|
160
|
+
if (!layer || !include(layerEvidence(layerSummary(layer), backend.id).layer))
|
|
114
161
|
continue;
|
|
115
162
|
reader = createBundleReader(backend.store, layer.bundle);
|
|
116
163
|
if (readers.size >= 8)
|
|
@@ -185,7 +232,8 @@ export function createLayersApp(deps) {
|
|
|
185
232
|
await eachLimited(selected, 8, async (entry) => {
|
|
186
233
|
try {
|
|
187
234
|
for (const evidence of await entryEvidence(entry))
|
|
188
|
-
|
|
235
|
+
if (include(evidence.layer))
|
|
236
|
+
layers.push(evidence.layer);
|
|
189
237
|
}
|
|
190
238
|
catch {
|
|
191
239
|
notices.push('A report layer could not be loaded. Its outcome is unknown.');
|
|
@@ -207,11 +255,42 @@ export function createLayersApp(deps) {
|
|
|
207
255
|
const backend = backends.findIndex(known => known.id === c.req.param('backend'));
|
|
208
256
|
const id = c.req.param('id');
|
|
209
257
|
const evidence = backend < 0 ? null : await findEvidence(backend, id);
|
|
210
|
-
if (!evidence)
|
|
258
|
+
if (!evidence || !include(evidence.layer))
|
|
211
259
|
return c.notFound();
|
|
212
260
|
const rows = { id: evidence.id, rows: evidence.rows, fingerprints: evidence.fingerprints };
|
|
213
261
|
return c.json(rows, 200, { 'cache-control': archived(id) ? 'public, max-age=31536000, immutable' : 'no-cache' });
|
|
214
262
|
});
|
|
263
|
+
// The files a layer carries for its checks: screenshots, videos, traces, logs, snapshots, anything.
|
|
264
|
+
app.get('/layers/:id/blobs/*', async (c) => {
|
|
265
|
+
const id = c.req.param('id');
|
|
266
|
+
let key;
|
|
267
|
+
try {
|
|
268
|
+
key = decodeURIComponent(new URL(c.req.url).pathname.split(`/layers/${id}/blobs/`)[1] ?? '');
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return c.json({ error: 'Invalid attachment path' }, 400);
|
|
272
|
+
}
|
|
273
|
+
if (!archived(id) || !key || key.split('/').some(part => !part || part === '.' || part === '..'))
|
|
274
|
+
return c.notFound();
|
|
275
|
+
const entry = await file(id, key);
|
|
276
|
+
return entry ? attachmentResponse(c.req.raw, entry, `"${id.slice(0, 16)}-${key.replace(/[^\w.-]/g, '_')}"`) : c.notFound();
|
|
277
|
+
});
|
|
278
|
+
// Everything one result carries: its timeline, attachments, annotations, retries and extensions.
|
|
279
|
+
app.get('/evidence/detail/:backend/:id', async (c) => {
|
|
280
|
+
const backend = backends.find(known => known.id === c.req.param('backend'));
|
|
281
|
+
const id = c.req.param('id');
|
|
282
|
+
if (!backend || !archived(id))
|
|
283
|
+
return c.notFound();
|
|
284
|
+
const layer = await readLayer(backend.store, id).catch(() => null);
|
|
285
|
+
if (!layer || !include(layerEvidence(layerSummary(layer), backend.id).layer))
|
|
286
|
+
return c.notFound();
|
|
287
|
+
const checkId = c.req.query('check');
|
|
288
|
+
const headers = { 'cache-control': 'public, max-age=31536000, immutable' };
|
|
289
|
+
if (checkId === undefined)
|
|
290
|
+
return c.json({ layer: id, run: runEvidence(layer) }, 200, headers);
|
|
291
|
+
const detail = checkDetailOf(layer, checkId, c.req.query('kind'));
|
|
292
|
+
return detail ? c.json(detail, 200, headers) : c.notFound();
|
|
293
|
+
});
|
|
215
294
|
app.get('/layers/:id/*', async (c) => {
|
|
216
295
|
const id = c.req.param('id');
|
|
217
296
|
let path;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Media types by file extension
|
|
3
|
+
*
|
|
4
|
+
* A small fixed table from file extension to media type, used when a captured file arrives without a type. It is a
|
|
5
|
+
* fallback guess from the name only: the bytes are never sniffed, and a type the producer gave always wins.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The media type for a file name, from its last extension, case-insensitively. Anything not in the table, and a name
|
|
9
|
+
* with no extension, is `application/octet-stream`. Never reads the content.
|
|
10
|
+
*/
|
|
11
|
+
export declare function contentTypeOf(file: string): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Media types by file extension
|
|
3
|
+
*
|
|
4
|
+
* A small fixed table from file extension to media type, used when a captured file arrives without a type. It is a
|
|
5
|
+
* fallback guess from the name only: the bytes are never sniffed, and a type the producer gave always wins.
|
|
6
|
+
*/
|
|
7
|
+
/** Media types by file extension (lowercase, no dot), for attachments whose producer did not say. */
|
|
8
|
+
const TYPES = {
|
|
9
|
+
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', avif: 'image/avif', svg: 'image/svg+xml', ico: 'image/x-icon', bmp: 'image/bmp',
|
|
10
|
+
mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime', mkv: 'video/x-matroska', ogv: 'video/ogg',
|
|
11
|
+
mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', m4a: 'audio/mp4', flac: 'audio/flac',
|
|
12
|
+
vtt: 'text/vtt', srt: 'application/x-subrip', txt: 'text/plain; charset=utf-8', log: 'text/plain; charset=utf-8', md: 'text/markdown; charset=utf-8', csv: 'text/csv', html: 'text/html; charset=utf-8', htm: 'text/html; charset=utf-8', css: 'text/css',
|
|
13
|
+
json: 'application/json', ndjson: 'application/x-ndjson', har: 'application/json', xml: 'application/xml', yaml: 'application/yaml', yml: 'application/yaml', pdf: 'application/pdf',
|
|
14
|
+
zip: 'application/zip', gz: 'application/gzip', tgz: 'application/gzip', tar: 'application/x-tar', mmd: 'text/plain; charset=utf-8', diff: 'text/x-diff', patch: 'text/x-diff'
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The media type for a file name, from its last extension, case-insensitively. Anything not in the table, and a name
|
|
18
|
+
* with no extension, is `application/octet-stream`. Never reads the content.
|
|
19
|
+
*/
|
|
20
|
+
export function contentTypeOf(file) {
|
|
21
|
+
return TYPES[file.split('.').at(-1)?.toLowerCase() ?? ''] ?? 'application/octet-stream';
|
|
22
|
+
}
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Object key guards
|
|
3
|
+
*
|
|
4
|
+
* Throws on object-store keys that could escape their prefix. The checks are lexical only (empty, leading `/`, `..`,
|
|
5
|
+
* missing namespace); they do not normalise a key or decide whether an object exists.
|
|
6
|
+
*/
|
|
1
7
|
/**
|
|
2
8
|
* Validates a wire key before any remote object-store I/O. Every S3 command
|
|
3
9
|
* for the shared bucket must pass this gate so projects cannot clobber
|
|
4
10
|
* each other's prefixes.
|
|
11
|
+
*
|
|
12
|
+
* Throws `Error` when `key` is empty, starts with `/`, contains `..` anywhere, is exactly `namespace` (with or without
|
|
13
|
+
* a trailing `/`), or does not start with `namespace`. Returns nothing when the key is acceptable.
|
|
5
14
|
*/
|
|
6
15
|
export declare function assertRemoteObjectKey(key: string, namespace: string): void;
|
|
7
16
|
/**
|
|
8
17
|
* Validates a logical key before {@link ObjectStoreWithPrefix} scopes it under
|
|
9
|
-
* a namespace or feature prefix.
|
|
18
|
+
* a namespace or feature prefix. Throws `Error` when `key` is empty, starts with `/`, or contains `..` anywhere.
|
|
10
19
|
*/
|
|
11
20
|
export declare function assertLogicalObjectKey(key: string): void;
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Object key guards
|
|
3
|
+
*
|
|
4
|
+
* Throws on object-store keys that could escape their prefix. The checks are lexical only (empty, leading `/`, `..`,
|
|
5
|
+
* missing namespace); they do not normalise a key or decide whether an object exists.
|
|
6
|
+
*/
|
|
1
7
|
/**
|
|
2
8
|
* Validates a wire key before any remote object-store I/O. Every S3 command
|
|
3
9
|
* for the shared bucket must pass this gate so projects cannot clobber
|
|
4
10
|
* each other's prefixes.
|
|
11
|
+
*
|
|
12
|
+
* Throws `Error` when `key` is empty, starts with `/`, contains `..` anywhere, is exactly `namespace` (with or without
|
|
13
|
+
* a trailing `/`), or does not start with `namespace`. Returns nothing when the key is acceptable.
|
|
5
14
|
*/
|
|
6
15
|
export function assertRemoteObjectKey(key, namespace) {
|
|
7
16
|
if (key.length === 0) {
|
|
@@ -25,7 +34,7 @@ export function assertRemoteObjectKey(key, namespace) {
|
|
|
25
34
|
}
|
|
26
35
|
/**
|
|
27
36
|
* Validates a logical key before {@link ObjectStoreWithPrefix} scopes it under
|
|
28
|
-
* a namespace or feature prefix.
|
|
37
|
+
* a namespace or feature prefix. Throws `Error` when `key` is empty, starts with `/`, or contains `..` anywhere.
|
|
29
38
|
*/
|
|
30
39
|
export function assertLogicalObjectKey(key) {
|
|
31
40
|
if (key.length === 0) {
|
|
@@ -1,8 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Explorer navigation host
|
|
3
|
+
*
|
|
4
|
+
* The seam between a reports explorer and the page it is embedded in: where the address (hash), keyboard and title
|
|
5
|
+
* live. It is a type only; a host (usually the browser) supplies the implementation, which keeps the explorer free of
|
|
6
|
+
* `window` and testable.
|
|
7
|
+
*/
|
|
1
8
|
/** Host navigation and keyboard events, scoped to one reports explorer. */
|
|
2
9
|
export type ReportNavigation = {
|
|
10
|
+
/** The current location hash, as the host holds it, with or without the leading `#` (the route parser accepts both). */
|
|
3
11
|
readonly readHash: () => string;
|
|
12
|
+
/** Sets the hash (given with its leading `#`, like `#/test/<id>`); `replace` true replaces the current history entry instead of adding one. */
|
|
4
13
|
readonly writeHash: (hash: string, replace: boolean) => void;
|
|
14
|
+
/** Calls `listener` whenever the hash changes; returns the function that stops listening. */
|
|
5
15
|
readonly subscribeHash: (listener: () => void) => () => void;
|
|
16
|
+
/** Calls `listener` for keyboard events scoped to this explorer; returns the function that stops listening. */
|
|
6
17
|
readonly subscribeKeys: (listener: (event: KeyboardEvent) => void) => () => void;
|
|
18
|
+
/** Sets the page title. */
|
|
7
19
|
readonly setTitle: (title: string) => void;
|
|
8
20
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Explorer navigation host
|
|
3
|
+
*
|
|
4
|
+
* The seam between a reports explorer and the page it is embedded in: where the address (hash), keyboard and title
|
|
5
|
+
* live. It is a type only; a host (usually the browser) supplies the implementation, which keeps the explorer free of
|
|
6
|
+
* `window` and testable.
|
|
7
|
+
*/
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Reading and writing whole objects
|
|
3
|
+
*
|
|
4
|
+
* Small helpers over `ObjectStore` for the common case of an object read or written whole: draining a stream,
|
|
5
|
+
* decoding UTF-8, and encoding text for `put`. They hold the object in memory, so they are for manifests, catalog
|
|
6
|
+
* entries and bounded blobs, not for large files. Errors from the store are not caught.
|
|
7
|
+
*/
|
|
1
8
|
import type { ObjectStore } from './object-store.js';
|
|
2
|
-
/**
|
|
9
|
+
/** Drains a stream into one byte array (it reads the stream to its end; it holds every chunk in memory). */
|
|
3
10
|
export declare function readBytes(stream: ReadableStream<Uint8Array>): Promise<Uint8Array>;
|
|
4
|
-
/**
|
|
11
|
+
/** Reads an object as UTF-8 text, or `null` when the key is missing. Invalid UTF-8 is decoded leniently (replacement characters), not rejected. Transport errors throw. */
|
|
5
12
|
export declare function readUtf8(store: ObjectStore, key: string): Promise<string | null>;
|
|
6
|
-
/**
|
|
13
|
+
/** Writes UTF-8 text under `key` with the given content type (default `text/plain; charset=utf-8`). Overwrites; transport errors throw. */
|
|
7
14
|
export declare function writeUtf8(store: ObjectStore, key: string, text: string, contentType?: string): Promise<void>;
|
|
8
|
-
/**
|
|
15
|
+
/** Reads an object's bytes whole, or `null` when the key is missing. Transport errors throw. */
|
|
9
16
|
export declare function readObjectBytes(store: ObjectStore, key: string): Promise<Uint8Array | null>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Drains a stream into one byte array (it reads the stream to its end; it holds every chunk in memory). */
|
|
2
2
|
export async function readBytes(stream) {
|
|
3
3
|
const reader = stream.getReader();
|
|
4
4
|
const chunks = [];
|
|
@@ -20,7 +20,7 @@ export async function readBytes(stream) {
|
|
|
20
20
|
}
|
|
21
21
|
return out;
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
23
|
+
/** Reads an object as UTF-8 text, or `null` when the key is missing. Invalid UTF-8 is decoded leniently (replacement characters), not rejected. Transport errors throw. */
|
|
24
24
|
export async function readUtf8(store, key) {
|
|
25
25
|
const obj = await store.get(key);
|
|
26
26
|
if (obj === null)
|
|
@@ -28,11 +28,11 @@ export async function readUtf8(store, key) {
|
|
|
28
28
|
const bytes = await readBytes(obj.body);
|
|
29
29
|
return new TextDecoder().decode(bytes);
|
|
30
30
|
}
|
|
31
|
-
/**
|
|
31
|
+
/** Writes UTF-8 text under `key` with the given content type (default `text/plain; charset=utf-8`). Overwrites; transport errors throw. */
|
|
32
32
|
export async function writeUtf8(store, key, text, contentType = 'text/plain; charset=utf-8') {
|
|
33
33
|
await store.put(key, new TextEncoder().encode(text), contentType);
|
|
34
34
|
}
|
|
35
|
-
/**
|
|
35
|
+
/** Reads an object's bytes whole, or `null` when the key is missing. Transport errors throw. */
|
|
36
36
|
export async function readObjectBytes(store, key) {
|
|
37
37
|
const obj = await store.get(key);
|
|
38
38
|
if (obj === null)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ObjectStore } from './object-store.js';
|
|
2
|
+
/** The outcome of `verifyObjectStore`: `ok` is true exactly when `failures` is empty. */
|
|
3
|
+
export type StoreVerification = {
|
|
4
|
+
readonly ok: boolean;
|
|
5
|
+
readonly failures: readonly string[];
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Checks a backend against the {@link ObjectStore} contract before trusting it with evidence. It
|
|
9
|
+
* writes a few objects under a scratch prefix and removes them. Use it in the test suite of an S3,
|
|
10
|
+
* GCS, R2 or HTTP adapter; an adapter that passes composes with every other backend.
|
|
11
|
+
*
|
|
12
|
+
* It is a plain async function, so it runs under any test runner and from a script. It checks: a missing key reads
|
|
13
|
+
* `null` (`get`, `getUri`) and `false` (`head`); `put` overwrites and round-trips bytes, size and content type;
|
|
14
|
+
* `listKeys` is in code-unit order, filters by any string prefix and is empty for an unused one; `withPrefix` scopes
|
|
15
|
+
* reads and writes; `delete` and `deleteMany` accept missing keys. A failed expectation is reported in `failures`;
|
|
16
|
+
* an exception from the store is reported as one `threw: ...` failure rather than thrown. It does not check
|
|
17
|
+
* `listPage`.
|
|
18
|
+
*
|
|
19
|
+
* @param store the backend to verify.
|
|
20
|
+
* @param scratch the key prefix to write under; defaults to a random `atlas-conformance-<id>/`. If you pass one,
|
|
21
|
+
* nothing else may live under it: the check asserts that `listKeys(scratch)` returns exactly the four objects it wrote.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* const { ok, failures } = await verifyObjectStore(myStore);
|
|
25
|
+
* if (!ok) throw new Error(failures.join('\n'));
|
|
26
|
+
*/
|
|
27
|
+
export declare function verifyObjectStore(store: ObjectStore, scratch?: string): Promise<StoreVerification>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module ObjectStore conformance check
|
|
3
|
+
*
|
|
4
|
+
* Runs the `ObjectStore` contract against any implementation, so a new backend (GCS, an HTTP service, a device lab) can
|
|
5
|
+
* prove it composes with the others before it holds evidence. It is a plain async function with no test-runner
|
|
6
|
+
* dependency. It writes only under a scratch prefix and removes what it wrote.
|
|
7
|
+
*
|
|
8
|
+
* @see docs/report-contract.md#backends
|
|
9
|
+
*/
|
|
10
|
+
import { readBytes } from './object-io.js';
|
|
11
|
+
const text = (value) => new TextEncoder().encode(value);
|
|
12
|
+
/**
|
|
13
|
+
* Checks a backend against the {@link ObjectStore} contract before trusting it with evidence. It
|
|
14
|
+
* writes a few objects under a scratch prefix and removes them. Use it in the test suite of an S3,
|
|
15
|
+
* GCS, R2 or HTTP adapter; an adapter that passes composes with every other backend.
|
|
16
|
+
*
|
|
17
|
+
* It is a plain async function, so it runs under any test runner and from a script. It checks: a missing key reads
|
|
18
|
+
* `null` (`get`, `getUri`) and `false` (`head`); `put` overwrites and round-trips bytes, size and content type;
|
|
19
|
+
* `listKeys` is in code-unit order, filters by any string prefix and is empty for an unused one; `withPrefix` scopes
|
|
20
|
+
* reads and writes; `delete` and `deleteMany` accept missing keys. A failed expectation is reported in `failures`;
|
|
21
|
+
* an exception from the store is reported as one `threw: ...` failure rather than thrown. It does not check
|
|
22
|
+
* `listPage`.
|
|
23
|
+
*
|
|
24
|
+
* @param store the backend to verify.
|
|
25
|
+
* @param scratch the key prefix to write under; defaults to a random `atlas-conformance-<id>/`. If you pass one,
|
|
26
|
+
* nothing else may live under it: the check asserts that `listKeys(scratch)` returns exactly the four objects it wrote.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* const { ok, failures } = await verifyObjectStore(myStore);
|
|
30
|
+
* if (!ok) throw new Error(failures.join('\n'));
|
|
31
|
+
*/
|
|
32
|
+
export async function verifyObjectStore(store, scratch = `atlas-conformance-${Math.random().toString(36).slice(2, 10)}/`) {
|
|
33
|
+
const failures = [];
|
|
34
|
+
const expect = (ok, message) => { if (!ok)
|
|
35
|
+
failures.push(message); };
|
|
36
|
+
const key = (name) => `${scratch}${name}`;
|
|
37
|
+
const created = [];
|
|
38
|
+
try {
|
|
39
|
+
expect((await store.get(key('missing'))) === null, 'get returns null for a missing key');
|
|
40
|
+
expect((await store.head(key('missing'))) === false, 'head returns false for a missing key');
|
|
41
|
+
expect((await store.getUri(key('missing'))) === null, 'getUri returns null for a missing key');
|
|
42
|
+
await store.delete(key('missing'));
|
|
43
|
+
const payload = new Uint8Array([0, 1, 2, 254, 255]);
|
|
44
|
+
created.push(key('a/b.json'));
|
|
45
|
+
await store.put(key('a/b.json'), payload, 'application/json');
|
|
46
|
+
await store.put(key('a/b.json'), payload, 'application/json');
|
|
47
|
+
const stored = await store.get(key('a/b.json'));
|
|
48
|
+
expect(stored !== null, 'a written key can be read');
|
|
49
|
+
if (stored) {
|
|
50
|
+
expect(stored.contentType.startsWith('application/json'), 'the content type round-trips');
|
|
51
|
+
const bytes = await readBytes(stored.body);
|
|
52
|
+
expect(bytes.length === payload.length && payload.every((byte, i) => bytes[i] === byte), 'bytes round-trip exactly');
|
|
53
|
+
expect(stored.size === payload.length, 'size is the byte length');
|
|
54
|
+
}
|
|
55
|
+
expect(await store.head(key('a/b.json')), 'head is true for a written key');
|
|
56
|
+
await store.put(key('a/b.json'), text('{}'), 'application/json');
|
|
57
|
+
const replaced = await store.get(key('a/b.json'));
|
|
58
|
+
expect(replaced !== null && (await readBytes(replaced.body)).length === 2, 'put overwrites the same key');
|
|
59
|
+
for (const name of ['a/c.json', 'a/d/e.json', 'z.json']) {
|
|
60
|
+
created.push(key(name));
|
|
61
|
+
await store.put(key(name), text(name), 'text/plain');
|
|
62
|
+
}
|
|
63
|
+
const all = await store.listKeys(scratch);
|
|
64
|
+
expect(JSON.stringify(all) === JSON.stringify([...all].sort()), 'listKeys is sorted by code unit');
|
|
65
|
+
expect(all.length === 4, 'listKeys lists every key under the prefix, nothing else');
|
|
66
|
+
const nested = await store.listKeys(key('a/'));
|
|
67
|
+
expect(nested.length === 3 && nested.every((name) => name.startsWith(key('a/'))), 'listKeys filters by prefix, including across directories');
|
|
68
|
+
expect((await store.listKeys(key('a/c'))).length === 1, 'a prefix need not end at a separator');
|
|
69
|
+
expect((await store.listKeys(key('nothing/'))).length === 0, 'listKeys is empty for an unused prefix');
|
|
70
|
+
const scoped = store.withPrefix(scratch);
|
|
71
|
+
expect((await scoped.listKeys()).length === 4 && (await scoped.listKeys())[0] === 'a/b.json', 'withPrefix scopes keys, which are relative to it');
|
|
72
|
+
await scoped.put('scoped.json', text('s'), 'text/plain');
|
|
73
|
+
created.push(key('scoped.json'));
|
|
74
|
+
expect(await store.head(key('scoped.json')), 'a write through withPrefix lands under the prefix');
|
|
75
|
+
await store.delete(key('z.json'));
|
|
76
|
+
expect(!(await store.head(key('z.json'))), 'delete removes a key');
|
|
77
|
+
await store.deleteMany([key('a/c.json'), key('a/d/e.json'), key('missing-too')]);
|
|
78
|
+
expect(!(await store.head(key('a/c.json'))) && !(await store.head(key('a/d/e.json'))), 'deleteMany removes keys, missing ones included');
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
failures.push(`threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await store.deleteMany(created).catch(() => { });
|
|
85
|
+
}
|
|
86
|
+
return { ok: failures.length === 0, failures };
|
|
87
|
+
}
|