@crvouga/atlas 0.15.0 → 0.17.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 +12 -0
- package/dist/reports/src/attachment-files.d.ts +3 -1
- package/dist/reports/src/attachment-files.js +14 -4
- package/dist/reports/src/evidence-detail.d.ts +19 -2
- package/dist/reports/src/evidence-detail.js +63 -14
- package/dist/reports/src/evidence-types.d.ts +53 -0
- package/dist/reports/src/layer-merge.js +7 -2
- package/dist/reports/src/layer-types.d.ts +1 -1
- package/dist/reports/src/publish-checks.js +11 -3
- package/dist/reports/src/recording-receipt.d.ts +48 -0
- package/dist/reports/src/recording-receipt.js +103 -0
- package/dist/reports/src/report-cli.d.ts +1 -1
- package/dist/reports/src/report-cli.js +20 -2
- package/dist/reports/src/report-status.d.ts +2 -0
- package/dist/reports/src/report-status.js +17 -1
- package/dist/reports/src/reports-explorer-route.d.ts +8 -0
- package/dist/reports/src/reports-route.d.ts +8 -0
- package/dist/reports/src/test-catalog.d.ts +24 -3
- package/dist/reports/src/test-catalog.js +37 -11
- package/dist/schema/src/api.d.ts +232 -0
- package/dist/schema/src/api.js +35 -6
- package/dist/schema/src/cli-results.d.ts +107 -2
- package/dist/schema/src/cli-results.js +11 -3
- package/dist/schema/src/layer.d.ts +165 -0
- package/dist/schema/src/layer.js +72 -1
- package/dist/server/src/app.js +3 -2
- package/dist/server/src/llms.js +2 -1
- package/dist/server/src/openapi.js +2 -1
- package/docs/reference/cli.md +5 -0
- package/docs/reference/http-api.md +4 -2
- package/docs/reference/reports.md +204 -5
- package/docs/reference/schema.md +75 -0
- package/docs/rich-evidence.md +29 -0
- package/docs/server.md +1 -1
- package/llms-full.txt +39 -3
- package/package.json +1 -1
- package/packages/schema/schemas/check-detail.schema.json +20 -0
- package/packages/schema/schemas/effective-delta.schema.json +143 -0
- package/packages/schema/schemas/effective-evidence.schema.json +143 -0
- package/packages/schema/schemas/evidence-page.schema.json +86 -0
- package/packages/schema/schemas/layer-rows.schema.json +57 -0
- package/packages/schema/schemas/layer.schema.json +153 -0
- package/packages/schema/schemas/publish-checks.schema.json +24 -0
- package/packages/schema/schemas/publish-result.schema.json +161 -1
- package/packages/schema/schemas/report-run.schema.json +71 -0
- package/packages/schema/schemas/report-runs.schema.json +38 -1
- package/packages/schema/schemas/report-status.schema.json +104 -0
- package/packages/schema/schemas/test-catalog.schema.json +41 -0
- package/packages/schema/schemas/test-history.schema.json +82 -0
- package/packages/server/openapi.json +830 -4
- package/packages/visualizer/src/evidence/marks.tsx +16 -0
- package/packages/visualizer/src/explorer/RunsView.tsx +128 -10
- package/packages/visualizer/src/explorer/catalog.tsx +16 -6
- package/packages/visualizer/src/explorer/search.ts +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog: @crvouga/atlas
|
|
2
2
|
|
|
3
|
+
## 0.17.0 (2026-10-28)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- report recording completeness separately from test success ([a8dd9d4](https://github.com/crvouga/atlas/commit/a8dd9d4b12d0c9cfbb7cb94c48d571e4aa5050ef))
|
|
8
|
+
|
|
9
|
+
## 0.16.0 (2026-10-28)
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- show video availability and filter runs with recordings ([dece195](https://github.com/crvouga/atlas/commit/dece195ef419a3fc6eaedb643544be29ffb341c3))
|
|
14
|
+
|
|
3
15
|
## 0.15.0 (2026-10-28)
|
|
4
16
|
|
|
5
17
|
### Features
|
|
@@ -34,7 +34,9 @@ export type ReadOptions = {
|
|
|
34
34
|
* attachment gave none.
|
|
35
35
|
*
|
|
36
36
|
* A file that is missing, not allowed or too large is left out with a warning: evidence that could not be
|
|
37
|
-
* found never stops the results from being published.
|
|
37
|
+
* found never stops the results from being published. The exception in kind is a video of a check that declared
|
|
38
|
+
* `recordingExpectation: "required"`: it stays as an attachment without bytes whose `omitted` says why (`not-found`,
|
|
39
|
+
* `not-allowed`, `too-large`, `over-budget`), so the recording receipt can name the gap. Attachments without a `path` are kept untouched.
|
|
38
40
|
*
|
|
39
41
|
* @param holder A check, step, attempt or any object that may have `attachments`, `steps` and `attempts`.
|
|
40
42
|
* @param roots Folders a relative path is resolved against (in order), and the only folders an untrusted path may name.
|
|
@@ -11,13 +11,16 @@
|
|
|
11
11
|
import { readFile, realpath, stat } from 'node:fs/promises';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { contentTypeOf } from './media-types.js';
|
|
14
|
+
import { isVideoAttachment } from './recording-receipt.js';
|
|
14
15
|
/**
|
|
15
16
|
* Reads the files that attachments name (`path`), at every level (the holder, its `steps` and its `attempts`,
|
|
16
17
|
* recursively), and hands their bytes over as `data` with a `contentType` guessed from the file name when the
|
|
17
18
|
* attachment gave none.
|
|
18
19
|
*
|
|
19
20
|
* A file that is missing, not allowed or too large is left out with a warning: evidence that could not be
|
|
20
|
-
* found never stops the results from being published.
|
|
21
|
+
* found never stops the results from being published. The exception in kind is a video of a check that declared
|
|
22
|
+
* `recordingExpectation: "required"`: it stays as an attachment without bytes whose `omitted` says why (`not-found`,
|
|
23
|
+
* `not-allowed`, `too-large`, `over-budget`), so the recording receipt can name the gap. Attachments without a `path` are kept untouched.
|
|
21
24
|
*
|
|
22
25
|
* @param holder A check, step, attempt or any object that may have `attachments`, `steps` and `attempts`.
|
|
23
26
|
* @param roots Folders a relative path is resolved against (in order), and the only folders an untrusted path may name.
|
|
@@ -27,7 +30,7 @@ export async function readAttachmentFiles(holder, roots, options = {}) {
|
|
|
27
30
|
const warnings = [];
|
|
28
31
|
const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
29
32
|
let attached = 0, bytes = 0;
|
|
30
|
-
async function walk(value) {
|
|
33
|
+
async function walk(value, expectsRecording = value.recordingExpectation === 'required') {
|
|
31
34
|
const out = { ...value };
|
|
32
35
|
if (Array.isArray(out.attachments)) {
|
|
33
36
|
const kept = [];
|
|
@@ -40,17 +43,24 @@ export async function readAttachmentFiles(holder, roots, options = {}) {
|
|
|
40
43
|
const name = String(given.name ?? file);
|
|
41
44
|
const { path: _path, ...rest } = given;
|
|
42
45
|
const found = await locate(file, roots, options.trusted === true);
|
|
46
|
+
// A video that a check was expected to have stays in the layer as metadata with the reason it was left out
|
|
47
|
+
// (never the path), so the recording receipt can name the gap instead of the file vanishing.
|
|
48
|
+
const left = (reason) => { if (expectsRecording && isVideoAttachment({ ...rest, contentType: rest.contentType ?? contentTypeOf(name) }))
|
|
49
|
+
kept.push({ ...rest, omitted: reason }); };
|
|
43
50
|
if (found.problem) {
|
|
44
51
|
warnings.push(`${name}: ${found.problem}`);
|
|
52
|
+
left(found.problem.startsWith('file not found') ? 'not-found' : 'not-allowed');
|
|
45
53
|
continue;
|
|
46
54
|
}
|
|
47
55
|
const size = (await stat(found.file)).size;
|
|
48
56
|
if (size > maxBytes) {
|
|
49
57
|
warnings.push(`${name}: ${(size / 1024 / 1024).toFixed(0)} MiB is over the limit of ${Math.round(maxBytes / 1024 / 1024)} MiB`);
|
|
58
|
+
left('too-large');
|
|
50
59
|
continue;
|
|
51
60
|
}
|
|
52
61
|
if (options.budget && size > options.budget.remaining) {
|
|
53
62
|
warnings.push(`${name}: the total size of attachments would pass the budget (raise --max-total-attachment-mb, or attach a uri)`);
|
|
63
|
+
left('over-budget');
|
|
54
64
|
continue;
|
|
55
65
|
}
|
|
56
66
|
if (options.budget)
|
|
@@ -64,13 +74,13 @@ export async function readAttachmentFiles(holder, roots, options = {}) {
|
|
|
64
74
|
if (Array.isArray(out.steps)) {
|
|
65
75
|
const steps = [];
|
|
66
76
|
for (const step of out.steps)
|
|
67
|
-
steps.push(await walk(step));
|
|
77
|
+
steps.push(await walk(step, expectsRecording));
|
|
68
78
|
out.steps = steps;
|
|
69
79
|
}
|
|
70
80
|
if (Array.isArray(out.attempts)) {
|
|
71
81
|
const attempts = [];
|
|
72
82
|
for (const attempt of out.attempts)
|
|
73
|
-
attempts.push(await walk(attempt));
|
|
83
|
+
attempts.push(await walk(attempt, expectsRecording));
|
|
74
84
|
out.attempts = attempts;
|
|
75
85
|
}
|
|
76
86
|
return out;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* @see docs/report-contract.md (evidence)
|
|
11
11
|
*/
|
|
12
12
|
import type { Annotation, Attachment, Attempt, Check, Step } from '../../schema/src/layer.js';
|
|
13
|
-
import type { EvidenceSummary } from './evidence-types.js';
|
|
13
|
+
import type { EvidenceSummary, VideoCounts, VideoRef } from './evidence-types.js';
|
|
14
14
|
/** Anything that may carry evidence: a layer, a part or a check. */
|
|
15
15
|
export type EvidenceHolder = {
|
|
16
16
|
readonly tags?: readonly string[];
|
|
@@ -34,9 +34,26 @@ export type EvidenceHolder = {
|
|
|
34
34
|
* Several holders (for example a check and its layer) are summed: `attachments` also counts those on steps and
|
|
35
35
|
* attempts, `steps` counts only each holder's own top-level steps, tags are de-duplicated, and for a repeated
|
|
36
36
|
* label the later holder wins. `artifactTags` are the attachments' own normalized tags and are never inherited. `roles` are the attachments' roles (or, without one, what their media type or file
|
|
37
|
-
* name implies), sorted and distinct.
|
|
37
|
+
* name implies), sorted and distinct. `videos` lists every attachment whose effective media type is video, with the
|
|
38
|
+
* check, retry attempt or step that holds it (see {@link isVideoAttachment}).
|
|
38
39
|
*/
|
|
39
40
|
export declare function summarizeEvidence(...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Like {@link summarizeEvidence} for a layer and its parts: the same summary, but a video attached to the layer or a
|
|
43
|
+
* part is owned by the `layer`, not by a check.
|
|
44
|
+
*/
|
|
45
|
+
export declare function summarizeLayerEvidence(...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Whether an attachment is a video, by its effective media type: the producer's `contentType`, else the one its file
|
|
48
|
+
* name implies. A role never decides this on its own (`demo` on a PNG is not a video), except that a `video` role
|
|
49
|
+
* settles a file whose type nothing says (`application/octet-stream`).
|
|
50
|
+
*/
|
|
51
|
+
export declare function isVideoAttachment(attachment: Pick<Attachment, 'name' | 'role' | 'contentType'>): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Counts references, distinct playable recordings and unplayable references. Undefined when there is no reference, so
|
|
54
|
+
* "no video" and "nothing known about video" look the same on the wire.
|
|
55
|
+
*/
|
|
56
|
+
export declare function videoCounts(videos: readonly VideoRef[] | undefined): VideoCounts | undefined;
|
|
40
57
|
/** A role to show when a producer gave none, from the media type: `image`, `video`, `audio`, `snapshot` (text/html), `text` (text, JSON, XML) or `file`. Roles are an open vocabulary; a producer's own role always wins. */
|
|
41
58
|
export declare function roleOfType(contentType: string | undefined): string;
|
|
42
59
|
/** All of one result's evidence, read when someone opens it. */
|
|
@@ -11,31 +11,48 @@ function count(holder) {
|
|
|
11
11
|
* Several holders (for example a check and its layer) are summed: `attachments` also counts those on steps and
|
|
12
12
|
* attempts, `steps` counts only each holder's own top-level steps, tags are de-duplicated, and for a repeated
|
|
13
13
|
* label the later holder wins. `artifactTags` are the attachments' own normalized tags and are never inherited. `roles` are the attachments' roles (or, without one, what their media type or file
|
|
14
|
-
* name implies), sorted and distinct.
|
|
14
|
+
* name implies), sorted and distinct. `videos` lists every attachment whose effective media type is video, with the
|
|
15
|
+
* check, retry attempt or step that holds it (see {@link isVideoAttachment}).
|
|
15
16
|
*/
|
|
16
17
|
export function summarizeEvidence(...holders) {
|
|
18
|
+
return summarize(holders, 'check');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Like {@link summarizeEvidence} for a layer and its parts: the same summary, but a video attached to the layer or a
|
|
22
|
+
* part is owned by the `layer`, not by a check.
|
|
23
|
+
*/
|
|
24
|
+
export function summarizeLayerEvidence(...holders) {
|
|
25
|
+
return summarize(holders, 'layer');
|
|
26
|
+
}
|
|
27
|
+
function summarize(holders, owner) {
|
|
17
28
|
if (!holders.some(holder => count(holder) > 0))
|
|
18
29
|
return undefined;
|
|
19
30
|
const roles = new Set();
|
|
20
31
|
const tags = new Set();
|
|
21
32
|
const artifactTags = new Set();
|
|
22
33
|
const labels = {};
|
|
34
|
+
const videos = [];
|
|
23
35
|
let attachments = 0, steps = 0, annotations = 0, attempts = 0, links = 0;
|
|
24
|
-
const attachmentsOf = (list
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
const attachmentsOf = (list, by, where = {}) => {
|
|
37
|
+
for (const item of list ?? []) {
|
|
38
|
+
attachments += 1;
|
|
39
|
+
for (const tag of normalizeTags(item.tags))
|
|
40
|
+
artifactTags.add(tag);
|
|
41
|
+
roles.add(item.role ?? roleOfType(item.contentType ?? contentTypeOf(item.name)));
|
|
42
|
+
const video = videoOf(item, by, where);
|
|
43
|
+
if (video)
|
|
44
|
+
videos.push(video);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const stepsOf = (list, attempt) => { for (const step of list ?? [])
|
|
48
|
+
attachmentsOf(step.attachments, 'step', { ...(attempt === undefined ? {} : { attempt }), step: step.name }); };
|
|
30
49
|
for (const holder of holders) {
|
|
31
|
-
attachmentsOf(holder.attachments);
|
|
50
|
+
attachmentsOf(holder.attachments, owner);
|
|
32
51
|
// Attachments on steps and attempts are part of the check too.
|
|
33
|
-
|
|
34
|
-
attachmentsOf(step.attachments);
|
|
52
|
+
stepsOf(holder.steps);
|
|
35
53
|
for (const attempt of holder.attempts ?? []) {
|
|
36
|
-
attachmentsOf(attempt.attachments);
|
|
37
|
-
|
|
38
|
-
attachmentsOf(step.attachments);
|
|
54
|
+
attachmentsOf(attempt.attachments, 'attempt', { attempt: attempt.index });
|
|
55
|
+
stepsOf(attempt.steps, attempt.index);
|
|
39
56
|
}
|
|
40
57
|
steps += holder.steps?.length ?? 0;
|
|
41
58
|
annotations += holder.annotations?.length ?? 0;
|
|
@@ -45,7 +62,39 @@ export function summarizeEvidence(...holders) {
|
|
|
45
62
|
tags.add(tag);
|
|
46
63
|
Object.assign(labels, holder.labels);
|
|
47
64
|
}
|
|
48
|
-
return { attachments, steps, annotations, attempts, links, tags: [...tags], ...(artifactTags.size ? { artifactTags: [...artifactTags].sort() } : {}), labels, roles: [...roles].sort() };
|
|
65
|
+
return { attachments, steps, annotations, attempts, links, tags: [...tags], ...(artifactTags.size ? { artifactTags: [...artifactTags].sort() } : {}), labels, roles: [...roles].sort(), ...(videos.length ? { videos } : {}) };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether an attachment is a video, by its effective media type: the producer's `contentType`, else the one its file
|
|
69
|
+
* name implies. A role never decides this on its own (`demo` on a PNG is not a video), except that a `video` role
|
|
70
|
+
* settles a file whose type nothing says (`application/octet-stream`).
|
|
71
|
+
*/
|
|
72
|
+
export function isVideoAttachment(attachment) {
|
|
73
|
+
const type = (attachment.contentType ?? contentTypeOf(attachment.name)).toLowerCase();
|
|
74
|
+
return type.startsWith('video/') || (attachment.role === 'video' && type.startsWith('application/octet-stream'));
|
|
75
|
+
}
|
|
76
|
+
function videoOf(item, owner, where) {
|
|
77
|
+
if (!isVideoAttachment(item))
|
|
78
|
+
return undefined;
|
|
79
|
+
const recording = item.sha256 ? `sha256:${item.sha256}` : item.bundleKey ?? item.uri ?? (item.text === undefined ? undefined : `inline:${item.name}`);
|
|
80
|
+
return { name: item.name, owner, ...where, ...(recording === undefined ? {} : { recording }), available: recording !== undefined };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Counts references, distinct playable recordings and unplayable references. Undefined when there is no reference, so
|
|
84
|
+
* "no video" and "nothing known about video" look the same on the wire.
|
|
85
|
+
*/
|
|
86
|
+
export function videoCounts(videos) {
|
|
87
|
+
if (!videos?.length)
|
|
88
|
+
return undefined;
|
|
89
|
+
const distinct = new Set();
|
|
90
|
+
let unavailable = 0;
|
|
91
|
+
for (const video of videos) {
|
|
92
|
+
if (video.available && video.recording !== undefined)
|
|
93
|
+
distinct.add(video.recording);
|
|
94
|
+
else
|
|
95
|
+
unavailable += 1;
|
|
96
|
+
}
|
|
97
|
+
return { references: videos.length, recordings: distinct.size, unavailable };
|
|
49
98
|
}
|
|
50
99
|
/** A role to show when a producer gave none, from the media type: `image`, `video`, `audio`, `snapshot` (text/html), `text` (text, JSON, XML) or `file`. Roles are an open vocabulary; a producer's own role always wins. */
|
|
51
100
|
export function roleOfType(contentType) {
|
|
@@ -36,6 +36,35 @@ export type BackendInfo = {
|
|
|
36
36
|
};
|
|
37
37
|
/** How many results have each status (`passed`, `failed`, `running`, `not-run`). */
|
|
38
38
|
export type StatusCounts = Readonly<Record<EvidenceStatus, number>>;
|
|
39
|
+
/** Who holds a recording: the whole layer (or one of its parts), one check, one retry attempt of a check, or one step. */
|
|
40
|
+
export type VideoOwner = 'layer' | 'check' | 'attempt' | 'step';
|
|
41
|
+
/**
|
|
42
|
+
* One reference to a video, found by its effective media type (the producer's `contentType`, else the one the file
|
|
43
|
+
* name implies; a `video` role alone counts only when nothing else says what the file is). A reference is not a
|
|
44
|
+
* recording: the same bytes referenced from three checks are three references and one recording.
|
|
45
|
+
*/
|
|
46
|
+
export type VideoRef = {
|
|
47
|
+
/** The attachment's name, for a person. */
|
|
48
|
+
readonly name: string;
|
|
49
|
+
readonly owner: VideoOwner;
|
|
50
|
+
/** The try that holds it, when `owner` is `attempt` or the step is inside one. */
|
|
51
|
+
readonly attempt?: number;
|
|
52
|
+
/** The name of the step that holds it, when `owner` is `step`. */
|
|
53
|
+
readonly step?: string;
|
|
54
|
+
/** What identifies the bytes (digest, bundle key or address); references with the same one are the same recording. Absent when `available` is false. */
|
|
55
|
+
readonly recording?: string;
|
|
56
|
+
/** False when the reference names no bytes at all (no bundle key, address or inline text), so it can never be played. */
|
|
57
|
+
readonly available: boolean;
|
|
58
|
+
};
|
|
59
|
+
/** Video availability as counts: references, distinct playable recordings (deduplicated), and references that cannot be played. */
|
|
60
|
+
export type VideoCounts = {
|
|
61
|
+
/** Every reference to a video, playable or not. */
|
|
62
|
+
readonly references: number;
|
|
63
|
+
/** Distinct playable recordings: identical bytes referenced twice are one. */
|
|
64
|
+
readonly recordings: number;
|
|
65
|
+
/** References that name no bytes. A report that predates availability metadata has none of these and none of the others. */
|
|
66
|
+
readonly unavailable: number;
|
|
67
|
+
};
|
|
39
68
|
/**
|
|
40
69
|
* What a result (or a run) carries beyond its outcome, as counts and short facts: attachments, steps,
|
|
41
70
|
* annotations, retries, tags, labels, links. It is only there when there is something. The evidence itself
|
|
@@ -55,6 +84,8 @@ export type EvidenceSummary = {
|
|
|
55
84
|
readonly labels: Readonly<Record<string, string>>;
|
|
56
85
|
/** The distinct roles of its attachments (or what their media types say), to show what kind of evidence there is. */
|
|
57
86
|
readonly roles: readonly string[];
|
|
87
|
+
/** Its references to videos, wherever they are held (layer, check, attempt, step). Only there when it has any. */
|
|
88
|
+
readonly videos?: readonly VideoRef[];
|
|
58
89
|
};
|
|
59
90
|
/** A report layer's origin and totals. Its checks are read separately, once. */
|
|
60
91
|
export type LayerHeader = {
|
|
@@ -92,6 +123,23 @@ export type LayerHeader = {
|
|
|
92
123
|
readonly execution: string;
|
|
93
124
|
/** What the run as a whole carries: its logs, links and labels. */
|
|
94
125
|
readonly evidence?: EvidenceSummary;
|
|
126
|
+
/** Counts from the layer's recording receipt: how many expected recordings were published. Absent when the layer declared no expectations: its completeness is unknown, not complete. Never changes a status. */
|
|
127
|
+
readonly recordings?: RecordingCounts;
|
|
128
|
+
};
|
|
129
|
+
/** How many recordings a layer expected and what became of them (see `RecordingReceipt`). */
|
|
130
|
+
export type RecordingCounts = {
|
|
131
|
+
/** Checks that required a recording: `uploaded + referenced + skipped + unresolved`. */
|
|
132
|
+
readonly expected: number;
|
|
133
|
+
/** Stored in the layer, with a checksum. */
|
|
134
|
+
readonly uploaded: number;
|
|
135
|
+
/** Given only by `uri`: Atlas cannot vouch for it. */
|
|
136
|
+
readonly referenced: number;
|
|
137
|
+
/** Captured but left out, with a reason (too large, not allowed…). */
|
|
138
|
+
readonly skipped: number;
|
|
139
|
+
/** No video was attached at all. */
|
|
140
|
+
readonly unresolved: number;
|
|
141
|
+
/** Intentionally unrecorded checks: neither expected nor missing. */
|
|
142
|
+
readonly notApplicable: number;
|
|
95
143
|
};
|
|
96
144
|
/** One check of one report part. */
|
|
97
145
|
export type LayerRow = {
|
|
@@ -114,6 +162,11 @@ export type LayerRow = {
|
|
|
114
162
|
readonly durationMs: number | null;
|
|
115
163
|
/** What this result carries beyond its outcome. Absent when nothing. */
|
|
116
164
|
readonly evidence?: EvidenceSummary;
|
|
165
|
+
/** What became of this check's expected recording: `uploaded`, `referenced`, `skipped`, `unresolved` or `not-applicable`, with a reason when it is not uploaded. Absent when the check declared no expectation. Independent of `status`. */
|
|
166
|
+
readonly recording?: {
|
|
167
|
+
readonly state: string;
|
|
168
|
+
readonly reason?: string;
|
|
169
|
+
};
|
|
117
170
|
};
|
|
118
171
|
/** The checks of one layer, read separately from its header. */
|
|
119
172
|
export type LayerRows = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { summarizeEvidence } from './evidence-detail.js';
|
|
1
|
+
import { summarizeEvidence, summarizeLayerEvidence } from './evidence-detail.js';
|
|
2
2
|
import { layerPlace } from './layer-axes.js';
|
|
3
3
|
/**
|
|
4
4
|
* A short, deterministic fingerprint of a spec's text, shaped `<length>:<hash>:<hash>` (the length in UTF-16 code
|
|
@@ -74,10 +74,13 @@ export function layerEvidence(summary, backend) {
|
|
|
74
74
|
return found;
|
|
75
75
|
};
|
|
76
76
|
const rows = [];
|
|
77
|
+
const receipt = summary.recordings;
|
|
78
|
+
const expectations = receipt ? new Map(receipt.items.map(item => [item.check, item])) : null;
|
|
77
79
|
for (const part of summary.parts) {
|
|
78
80
|
for (const check of part.checks) {
|
|
79
81
|
const test = tests.get(check.id);
|
|
80
82
|
const evidence = summarizeEvidence(check);
|
|
83
|
+
const item = expectations?.get(check.id);
|
|
81
84
|
rows.push({
|
|
82
85
|
kind: part.kind,
|
|
83
86
|
id: check.id,
|
|
@@ -90,11 +93,12 @@ export function layerEvidence(summary, backend) {
|
|
|
90
93
|
variant: test ? { system: test.system, runtime: test.runtime, seed: test.seed } : {},
|
|
91
94
|
durationMs: test?.durationMs ?? check.durationMs ?? null,
|
|
92
95
|
...(evidence ? { evidence } : {}),
|
|
96
|
+
...(item ? { recording: { state: item.state, ...(item.reason ? { reason: item.reason } : {}) } } : {}),
|
|
93
97
|
});
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
100
|
const recording = summary.recording;
|
|
97
|
-
const layerSummaryEvidence =
|
|
101
|
+
const layerSummaryEvidence = summarizeLayerEvidence(summary, ...summary.parts);
|
|
98
102
|
const layer = {
|
|
99
103
|
...(summary.freshness ? { freshness: summary.freshness } : {}),
|
|
100
104
|
...(summary.run ? { run: summary.run } : {}),
|
|
@@ -113,6 +117,7 @@ export function layerEvidence(summary, backend) {
|
|
|
113
117
|
counts: outcomeCounts(rows),
|
|
114
118
|
execution: textFingerprint(layerExecutionKey(summary)),
|
|
115
119
|
...(layerSummaryEvidence ? { evidence: layerSummaryEvidence } : {}),
|
|
120
|
+
...(receipt ? { recordings: { expected: receipt.expected, uploaded: receipt.uploaded, referenced: receipt.referenced, skipped: receipt.skipped, unresolved: receipt.unresolved, notApplicable: receipt.notApplicable } } : {}),
|
|
116
121
|
};
|
|
117
122
|
const fingerprints = {};
|
|
118
123
|
for (const spec of summary.specs)
|
|
@@ -15,7 +15,7 @@ import type { SourceRecord } from './protocol.js';
|
|
|
15
15
|
/** String choices (such as `{ device: 'iphone-15' }`) that make a result its own slot. Compared as sorted key/value pairs. */
|
|
16
16
|
export type LayerParameters = Readonly<Record<string, string>>;
|
|
17
17
|
/** What a layer may carry as a whole. All optional and open: see docs/extending.md. */
|
|
18
|
-
export type LayerExtras = Partial<Pick<import('../../schema/src/layer.js').LayerDocument, 'tags' | 'labels' | 'links' | 'attachments' | 'annotations' | 'extensions'>>;
|
|
18
|
+
export type LayerExtras = Partial<Pick<import('../../schema/src/layer.js').LayerDocument, 'tags' | 'labels' | 'links' | 'attachments' | 'annotations' | 'extensions' | 'recordings'>>;
|
|
19
19
|
/** A layer without its captured files: what composition and the viewer's lists read. */
|
|
20
20
|
export type LayerSummary = LayerExtras & {
|
|
21
21
|
/** `modified`: recorded from a checkout with uncommitted changes. `unknown`: origin not captured. Absent for a clean checkout. Advisory; never changes a status. */
|
|
@@ -15,6 +15,7 @@ import { normalizeEnvironment } from './layer-axes.js';
|
|
|
15
15
|
import { publishLayerBundle } from './layer-store.js';
|
|
16
16
|
import { contentTypeOf } from './media-types.js';
|
|
17
17
|
import { parseWith } from './parse-with.js';
|
|
18
|
+
import { isVideoAttachment, recordingReceipt } from './recording-receipt.js';
|
|
18
19
|
import { sha256Hex } from './sigv4.js';
|
|
19
20
|
/** A small text value stays in the layer's own document; anything longer is stored as a file. */
|
|
20
21
|
const INLINE_TEXT_BYTES = 8 * 1024;
|
|
@@ -28,7 +29,7 @@ function decodeBase64(text) {
|
|
|
28
29
|
* screenshot attached ten times is stored once), referred to by `bundleKey`. What a producer wrote as `data`,
|
|
29
30
|
* `base64` or a long `text` becomes a reference; a short `text` stays inline; a `uri` is left alone.
|
|
30
31
|
*/
|
|
31
|
-
async function materialize(holder, entries, limit) {
|
|
32
|
+
async function materialize(holder, entries, limit, expectsRecording = holder.recordingExpectation === 'required') {
|
|
32
33
|
const out = { ...holder };
|
|
33
34
|
if (Array.isArray(out.attachments)) {
|
|
34
35
|
out.attachments = await Promise.all(out.attachments.map(async (given) => {
|
|
@@ -41,6 +42,11 @@ async function materialize(holder, entries, limit) {
|
|
|
41
42
|
bytes = encoder.encode(text);
|
|
42
43
|
if (!bytes)
|
|
43
44
|
return { ...kept, ...(text !== undefined ? { bytes: encoder.encode(text).length } : {}) };
|
|
45
|
+
if (bytes.length > limit && expectsRecording && isVideoAttachment({ ...kept, ...(kept.contentType ? {} : { contentType: contentTypeOf(String(kept.name)) }) })) {
|
|
46
|
+
// A recording the check was expected to have is reported as left out (and why) instead of failing the whole publish.
|
|
47
|
+
const { text: _inline, ...rest } = kept;
|
|
48
|
+
return { ...rest, contentType: kept.contentType ?? contentTypeOf(String(kept.name)), bytes: bytes.length, sha256: await sha256Hex(bytes), omitted: 'too-large' };
|
|
49
|
+
}
|
|
44
50
|
if (bytes.length > limit)
|
|
45
51
|
throw new Error(`publishChecks: attachment "${String(kept.name)}" is ${bytes.length} bytes, over the limit of ${limit}. Store it elsewhere and attach its uri`);
|
|
46
52
|
const sha256 = await sha256Hex(bytes);
|
|
@@ -51,9 +57,9 @@ async function materialize(holder, entries, limit) {
|
|
|
51
57
|
}));
|
|
52
58
|
}
|
|
53
59
|
if (Array.isArray(out.steps))
|
|
54
|
-
out.steps = await Promise.all(out.steps.map(step => materialize(step, entries, limit)));
|
|
60
|
+
out.steps = await Promise.all(out.steps.map(step => materialize(step, entries, limit, expectsRecording)));
|
|
55
61
|
if (Array.isArray(out.attempts))
|
|
56
|
-
out.attempts = await Promise.all(out.attempts.map(attempt => materialize(attempt, entries, limit)));
|
|
62
|
+
out.attempts = await Promise.all(out.attempts.map(attempt => materialize(attempt, entries, limit, expectsRecording)));
|
|
57
63
|
return out;
|
|
58
64
|
}
|
|
59
65
|
/**
|
|
@@ -81,6 +87,7 @@ export async function publishChecks(store, input, options = {}) {
|
|
|
81
87
|
const entries = new Map();
|
|
82
88
|
const checks = await Promise.all(args.checks.map(check => materialize(check, entries, limit)));
|
|
83
89
|
const layerEvidence = await materialize({ attachments: args.attachments }, entries, limit);
|
|
90
|
+
const recordings = recordingReceipt(checks);
|
|
84
91
|
const total = [...entries.values()].reduce((sum, entry) => sum + entry.bytes.length, 0);
|
|
85
92
|
const totalLimit = options.maxTotalAttachmentBytes ?? 2 * 1024 * 1024 * 1024;
|
|
86
93
|
if (total > totalLimit)
|
|
@@ -110,6 +117,7 @@ export async function publishChecks(store, input, options = {}) {
|
|
|
110
117
|
parts: [part],
|
|
111
118
|
source: null,
|
|
112
119
|
recording: null,
|
|
120
|
+
...(recordings ? { recordings } : {}),
|
|
113
121
|
...(args.tags?.length ? { tags: args.tags } : {}),
|
|
114
122
|
...(args.labels && Object.keys(args.labels).length ? { labels: Object.fromEntries(Object.entries(args.labels).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) } : {}),
|
|
115
123
|
...(args.links?.length ? { links: args.links } : {}),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Recording receipts
|
|
3
|
+
*
|
|
4
|
+
* Answers "did the recordings we expected get published?" apart from "did the tests pass?". `recordingReceipt` reads the
|
|
5
|
+
* checks a producer is about to publish (after their files were stored) and describes, per check that declared a
|
|
6
|
+
* `recordingExpectation`, whether its video was uploaded, only referenced, left out (with a reason) or never attached.
|
|
7
|
+
* `recordingCompleteness` reduces the winning results of a report to the figures `atlas report status` prints. Neither
|
|
8
|
+
* reads a clock or the environment, and neither changes a status, a slot or a winner: a receipt describes publication, it
|
|
9
|
+
* does not decide a result. A layer with no receipt is Unknown, never Complete.
|
|
10
|
+
*
|
|
11
|
+
* @see docs/rich-evidence.md#recording-completeness
|
|
12
|
+
*/
|
|
13
|
+
import type { RecordingCompleteness, RecordingReceipt } from '../../schema/src/layer.js';
|
|
14
|
+
type Loose = Record<string, unknown>;
|
|
15
|
+
/** Is this attachment a screen recording: a role of `video`, or a `video/*` media type? */
|
|
16
|
+
export declare function isVideoAttachment(attachment: Loose): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The receipt for the checks of one layer, or `undefined` when no check declares a `recordingExpectation` (the layer then
|
|
19
|
+
* claims nothing). Call it with checks whose attachment bytes are already stored (`bundleKey`, `bytes` and `sha256` set).
|
|
20
|
+
*
|
|
21
|
+
* A `required` check is `uploaded` when any video of it has stored bytes, `referenced` when one has only a `uri`,
|
|
22
|
+
* `skipped` when its videos were left out (the attachment's `omitted` is the reason) and `unresolved` when it has no
|
|
23
|
+
* video at all. A `not-applicable` check is listed but is neither expected nor missing. Items are sorted by check id so the
|
|
24
|
+
* same input is always the same receipt.
|
|
25
|
+
*/
|
|
26
|
+
export declare function recordingReceipt(checks: readonly Loose[]): RecordingReceipt | undefined;
|
|
27
|
+
/** One result as {@link recordingCompleteness} reads it. */
|
|
28
|
+
export type RecordingResult = {
|
|
29
|
+
readonly spec: string | null;
|
|
30
|
+
readonly check: string;
|
|
31
|
+
readonly environment: string | null;
|
|
32
|
+
readonly layer: string;
|
|
33
|
+
/** Whether the layer the result came from carried a receipt. */
|
|
34
|
+
readonly hasReceipt: boolean;
|
|
35
|
+
/** The result's item in that receipt, when its check declared an expectation. */
|
|
36
|
+
readonly recording?: {
|
|
37
|
+
readonly state: string;
|
|
38
|
+
readonly reason?: string;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Reduces the winning results of a report to its recording completeness. `incomplete` when any expected recording is
|
|
43
|
+
* skipped or unresolved; otherwise `unknown` when a contributing layer has no receipt (nothing is claimed for it, and a
|
|
44
|
+
* report with no results at all is unknown too); otherwise `complete` when something was expected and `none-expected` when
|
|
45
|
+
* nothing was. Gaps are sorted so the output never depends on read order.
|
|
46
|
+
*/
|
|
47
|
+
export declare function recordingCompleteness(results: readonly RecordingResult[]): RecordingCompleteness;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { contentTypeOf } from './media-types.js';
|
|
2
|
+
/** Is this attachment a screen recording: a role of `video`, or a `video/*` media type? */
|
|
3
|
+
export function isVideoAttachment(attachment) {
|
|
4
|
+
if (attachment.role === 'video')
|
|
5
|
+
return true;
|
|
6
|
+
const type = typeof attachment.contentType === 'string' ? attachment.contentType : typeof attachment.name === 'string' ? contentTypeOf(attachment.name) : '';
|
|
7
|
+
return type.startsWith('video/');
|
|
8
|
+
}
|
|
9
|
+
/** Every video attachment of a holder, its steps and its attempts, at any depth. */
|
|
10
|
+
function videosOf(holder) {
|
|
11
|
+
const found = [];
|
|
12
|
+
const take = (list) => { if (Array.isArray(list))
|
|
13
|
+
for (const item of list)
|
|
14
|
+
if (isVideoAttachment(item))
|
|
15
|
+
found.push(item); };
|
|
16
|
+
take(holder.attachments);
|
|
17
|
+
for (const step of holder.steps ?? [])
|
|
18
|
+
take(step.attachments);
|
|
19
|
+
for (const attempt of holder.attempts ?? []) {
|
|
20
|
+
take(attempt.attachments);
|
|
21
|
+
for (const step of attempt.steps ?? [])
|
|
22
|
+
take(step.attachments);
|
|
23
|
+
}
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The receipt for the checks of one layer, or `undefined` when no check declares a `recordingExpectation` (the layer then
|
|
28
|
+
* claims nothing). Call it with checks whose attachment bytes are already stored (`bundleKey`, `bytes` and `sha256` set).
|
|
29
|
+
*
|
|
30
|
+
* A `required` check is `uploaded` when any video of it has stored bytes, `referenced` when one has only a `uri`,
|
|
31
|
+
* `skipped` when its videos were left out (the attachment's `omitted` is the reason) and `unresolved` when it has no
|
|
32
|
+
* video at all. A `not-applicable` check is listed but is neither expected nor missing. Items are sorted by check id so the
|
|
33
|
+
* same input is always the same receipt.
|
|
34
|
+
*/
|
|
35
|
+
export function recordingReceipt(checks) {
|
|
36
|
+
const items = [];
|
|
37
|
+
const count = { uploaded: 0, referenced: 0, skipped: 0, unresolved: 0, notApplicable: 0 };
|
|
38
|
+
for (const check of checks) {
|
|
39
|
+
const expectation = check.recordingExpectation;
|
|
40
|
+
if (expectation !== 'required' && expectation !== 'not-applicable')
|
|
41
|
+
continue;
|
|
42
|
+
const check_ = String(check.id);
|
|
43
|
+
if (expectation === 'not-applicable') {
|
|
44
|
+
count.notApplicable += 1;
|
|
45
|
+
items.push({ check: check_, state: 'not-applicable' });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const files = videosOf(check).map(video => {
|
|
49
|
+
const state = typeof video.bundleKey === 'string' ? 'uploaded' : typeof video.uri === 'string' ? 'referenced' : 'skipped';
|
|
50
|
+
return {
|
|
51
|
+
name: String(video.name),
|
|
52
|
+
state,
|
|
53
|
+
...(typeof video.bytes === 'number' ? { bytes: video.bytes } : {}),
|
|
54
|
+
...(typeof video.sha256 === 'string' ? { sha256: video.sha256 } : {}),
|
|
55
|
+
...(state === 'skipped' ? { reason: typeof video.omitted === 'string' ? video.omitted : 'bytes-not-attached' } : {})
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
const first = (state) => files.find(file => file.state === state);
|
|
59
|
+
const state = first('uploaded') ? 'uploaded' : first('referenced') ? 'referenced' : first('skipped') ? 'skipped' : 'unresolved';
|
|
60
|
+
const reason = state === 'skipped' ? first('skipped')?.reason : state === 'unresolved' ? 'no-video-attached' : undefined;
|
|
61
|
+
count[state] += 1;
|
|
62
|
+
items.push({ check: check_, state, ...(reason ? { reason } : {}), ...(files.length ? { files } : {}) });
|
|
63
|
+
}
|
|
64
|
+
if (!items.length)
|
|
65
|
+
return undefined;
|
|
66
|
+
items.sort((a, b) => (a.check < b.check ? -1 : a.check > b.check ? 1 : 0));
|
|
67
|
+
return { expected: count.uploaded + count.referenced + count.skipped + count.unresolved, ...count, items };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Reduces the winning results of a report to its recording completeness. `incomplete` when any expected recording is
|
|
71
|
+
* skipped or unresolved; otherwise `unknown` when a contributing layer has no receipt (nothing is claimed for it, and a
|
|
72
|
+
* report with no results at all is unknown too); otherwise `complete` when something was expected and `none-expected` when
|
|
73
|
+
* nothing was. Gaps are sorted so the output never depends on read order.
|
|
74
|
+
*/
|
|
75
|
+
export function recordingCompleteness(results) {
|
|
76
|
+
const count = { expected: 0, uploaded: 0, referenced: 0, skipped: 0, unresolved: 0, notApplicable: 0 };
|
|
77
|
+
const gaps = [];
|
|
78
|
+
const without = new Set();
|
|
79
|
+
for (const result of results) {
|
|
80
|
+
if (!result.hasReceipt) {
|
|
81
|
+
without.add(result.layer);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const state = result.recording?.state;
|
|
85
|
+
if (state === undefined)
|
|
86
|
+
continue;
|
|
87
|
+
if (state === 'not-applicable') {
|
|
88
|
+
count.notApplicable += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
count.expected += 1;
|
|
92
|
+
if (state === 'uploaded' || state === 'referenced' || state === 'skipped' || state === 'unresolved')
|
|
93
|
+
count[state] += 1;
|
|
94
|
+
// A state this reader does not know is a gap: it is never counted as uploaded.
|
|
95
|
+
if (state !== 'uploaded' && state !== 'referenced') {
|
|
96
|
+
gaps.push({ spec: result.spec, check: result.check, layer: result.layer, environment: result.environment, state, ...(result.recording?.reason ? { reason: result.recording.reason } : {}) });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const text = (value) => value ?? '';
|
|
100
|
+
gaps.sort((a, b) => (text(a.spec) + a.check + text(a.environment) + a.layer < text(b.spec) + b.check + text(b.environment) + b.layer ? -1 : 1));
|
|
101
|
+
const state = gaps.length ? 'incomplete' : without.size || !results.length ? 'unknown' : count.expected ? 'complete' : 'none-expected';
|
|
102
|
+
return { state, ...count, layersWithoutReceipt: without.size, gaps };
|
|
103
|
+
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* @see docs/agents.md
|
|
14
14
|
*/
|
|
15
15
|
/** The text `atlas report --help` prints: every subcommand, its flags, what each does and the exit codes. Also the source of the command reference in `docs/reference/cli.md`. */
|
|
16
|
-
export declare const REPORT_USAGE = "atlas report \u2014 layered evidence for any kind of spec\n\n atlas report publish [--store <dir|s3://bucket/prefix>] [--producer <name>]\n ( --ctrf <file> | --junit <file> | --manifest <file> | --checks <file.json> )\n [--environment <name>] [--branch <b>] [--sha <s>] [--run-url <u>]\n [--run-id <id>] [--run-title <t>] [--run-started-at <iso|ms>] [--run-ended-at <iso|ms>]\n [--spec <path> | --specs <dir>] [--upload-run]\n [--param key=value]... [--kind <k>] [--title <t>] [--recorded-at <iso|ms>]\n [--attach [role=]<file>]... [--label key=value]... [--link [name=]<url>]...\n [--attach-root <dir>]... [--max-attachment-mb <n>] [--max-total-attachment-mb <n>]\n atlas report stream [--store <dir|s3://\u2026|https://atlas.example.com>] --producer <name> [--environment <name>]\n [--branch <b>] [--sha <s>] [--run-url <u>] [--kind <k>] [--param key=value]...\n [--run-id <id>] [--run-title <t>]\n [--flush-ms 1000] [--batch 100] (checks as JSON lines on standard input)\n atlas report status [--store <dir|s3://\u2026|id=url>]... [--backends <module>]\n [--specs <dir>] [--format text|json|markdown]\n [--require-environments a,b] [--require-all-specs]\n [--branch <name>]... [--ignore-modified]\n atlas report merge [--store \u2026]... [--backends <module>] --into <dir|s3://\u2026>\n atlas report doctor [--store \u2026]... [--role publisher|reader|both]\n atlas report policy --store s3://bucket/prefix [--role publisher|reader|both]\n\n A store is a directory or s3://bucket/prefix (add ?region=\u2026&endpoint=\u2026 for R2, MinIO and the\n rest). Select a project on an Atlas node with --project <slug> or ATLAS_PROJECT. This scopes specs, reports and recordings together.\n When no --store is given, ATLAS_STORE is used. Keys never go in the URL: set\n AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or sign in with `aws sso login`, or use a role.\n\n publish Write one immutable layer: the checks of one producer, on one environment. Safe to repeat.\n --producer is required, unless the --checks file names one\n --spec <path> every check in the report belongs to this spec (a report per feature file)\n --specs <dir> link checks to the spec files in <dir> by path, or by the suite, class or\n file name the report gives; also publishes the specs themselves, so the\n node can show them without a checkout\n --run-id <id> name this execution (a local or manual run with no CI run URL; also ATLAS_RUN_ID).\n Layers that share an id are one run, however many shards or batches uploaded them;\n another invocation at the same commit gets another id. Never part of what a check is\n --run-title <t> a human name for the run (also ATLAS_RUN_TITLE); --run-started-at and --run-ended-at\n record when it ran. Without --run-id a run is its CI run URL, else branch@sha\n --upload-run with --manifest: also upload the run's screenshots and clips for the viewer\n --attach <file> attach a file to the whole layer: a CI log, a coverage report, a trace archive\n (screenshots, videos and traces of a single test go in its CTRF or checks file)\n --label k=v a fact about the run, not part of what it checks: build, os, browser\n --link [n=]<url> a link to the run, a dashboard, an issue\n Files a report names (CTRF attachments, JUnit [[ATTACHMENT|path]] lines) come from the report's own\n folder and each --attach-root, and never from a hidden file or a link out: a report is data from a\n test run, not instructions. One that is missing or not allowed is skipped with a warning.\n From a checkout with uncommitted changes the layer is marked \"modified\".\n stream Send checks as they finish: one JSON object per line on standard input (the fields of one\n check: id, title, status, and any evidence). Each second's worth becomes one layer, so a server\n shows the run while it is still going. A check sent again with the same id (running, then\n passed) is replaced by the later. Ends when the input does; exit 1 if anything could not be sent.\n Point it at an Atlas server with --store https://\u2026 and a token in ATLAS_TOKEN.\n status Combine every layer of every store, and exit 1 unless the report passes.\n --specs <dir> lay the results over these spec files (default: the specs in the store)\n --require-all-specs fail while any spec has no results\n --branch <name> only layers recorded on this branch (repeatable)\n --ignore-modified leave out layers from modified checkouts and unknown origins\n merge Copy the layers of several stores into one.\n doctor Check that a store works: credentials, listing, writing, and what it holds.\n policy Print the least-privilege IAM policy for publishers, readers, or both.\n\n --backends <module> A TypeScript or JavaScript module whose default export is a list of\n { id, label?, store } (or a function returning it): a device lab, anything\n that implements ObjectStore.\n\n --format json (alias --json) publish, stream, merge, doctor and status print one JSON document instead of prose\n (schemas: publish-result, stream-result, merge-result, doctor-result, report-status). A command\n that is wrong is {\"ok\":false,\"error\":{\"message\":\u2026,\"code\":\"usage\"}} on standard error.\n A store may be an Atlas server (https://\u2026): ATLAS_STORE=https://atlas.example.com, with ATLAS_TOKEN.\n\n Exit codes: 0 ok, 1 the report does not pass (or a check failed), 2 the command was wrong.\n";
|
|
16
|
+
export declare const REPORT_USAGE = "atlas report \u2014 layered evidence for any kind of spec\n\n atlas report publish [--store <dir|s3://bucket/prefix>] [--producer <name>]\n ( --ctrf <file> | --junit <file> | --manifest <file> | --checks <file.json> )\n [--environment <name>] [--branch <b>] [--sha <s>] [--run-url <u>]\n [--run-id <id>] [--run-title <t>] [--run-started-at <iso|ms>] [--run-ended-at <iso|ms>]\n [--spec <path> | --specs <dir>] [--upload-run]\n [--param key=value]... [--kind <k>] [--title <t>] [--recorded-at <iso|ms>]\n [--attach [role=]<file>]... [--label key=value]... [--link [name=]<url>]...\n [--attach-root <dir>]... [--max-attachment-mb <n>] [--max-total-attachment-mb <n>]\n [--require-recordings]\n atlas report stream [--store <dir|s3://\u2026|https://atlas.example.com>] --producer <name> [--environment <name>]\n [--branch <b>] [--sha <s>] [--run-url <u>] [--kind <k>] [--param key=value]...\n [--run-id <id>] [--run-title <t>]\n [--flush-ms 1000] [--batch 100] (checks as JSON lines on standard input)\n atlas report status [--store <dir|s3://\u2026|id=url>]... [--backends <module>]\n [--specs <dir>] [--format text|json|markdown]\n [--require-environments a,b] [--require-all-specs]\n [--branch <name>]... [--ignore-modified]\n atlas report merge [--store \u2026]... [--backends <module>] --into <dir|s3://\u2026>\n atlas report doctor [--store \u2026]... [--role publisher|reader|both]\n atlas report policy --store s3://bucket/prefix [--role publisher|reader|both]\n\n A store is a directory or s3://bucket/prefix (add ?region=\u2026&endpoint=\u2026 for R2, MinIO and the\n rest). Select a project on an Atlas node with --project <slug> or ATLAS_PROJECT. This scopes specs, reports and recordings together.\n When no --store is given, ATLAS_STORE is used. Keys never go in the URL: set\n AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or sign in with `aws sso login`, or use a role.\n\n publish Write one immutable layer: the checks of one producer, on one environment. Safe to repeat.\n --producer is required, unless the --checks file names one\n --spec <path> every check in the report belongs to this spec (a report per feature file)\n --specs <dir> link checks to the spec files in <dir> by path, or by the suite, class or\n file name the report gives; also publishes the specs themselves, so the\n node can show them without a checkout\n --run-id <id> name this execution (a local or manual run with no CI run URL; also ATLAS_RUN_ID).\n Layers that share an id are one run, however many shards or batches uploaded them;\n another invocation at the same commit gets another id. Never part of what a check is\n --run-title <t> a human name for the run (also ATLAS_RUN_TITLE); --run-started-at and --run-ended-at\n record when it ran. Without --run-id a run is its CI run URL, else branch@sha\n --upload-run with --manifest: also upload the run's screenshots and clips for the viewer\n --attach <file> attach a file to the whole layer: a CI log, a coverage report, a trace archive\n (screenshots, videos and traces of a single test go in its CTRF or checks file)\n --label k=v a fact about the run, not part of what it checks: build, os, browser\n --link [n=]<url> a link to the run, a dashboard, an issue\n Files a report names (CTRF attachments, JUnit [[ATTACHMENT|path]] lines) come from the report's own\n folder and each --attach-root, and never from a hidden file or a link out: a report is data from a\n test run, not instructions. One that is missing or not allowed is skipped with a warning.\n --require-recordings exit 1 when a check that declared recordingExpectation \"required\" has no\n uploaded video (absent, left out as too large or not allowed, or refused). The layer is\n still written and the checks keep their results; only this command fails. The receipt of\n what was uploaded (counts, sizes, checksums) is in the JSON result under layer.recordings.\n From a checkout with uncommitted changes the layer is marked \"modified\".\n stream Send checks as they finish: one JSON object per line on standard input (the fields of one\n check: id, title, status, and any evidence). Each second's worth becomes one layer, so a server\n shows the run while it is still going. A check sent again with the same id (running, then\n passed) is replaced by the later. Ends when the input does; exit 1 if anything could not be sent.\n Point it at an Atlas server with --store https://\u2026 and a token in ATLAS_TOKEN.\n status Combine every layer of every store, and exit 1 unless the report passes.\n --specs <dir> lay the results over these spec files (default: the specs in the store)\n --require-all-specs fail while any spec has no results\n --branch <name> only layers recorded on this branch (repeatable)\n --ignore-modified leave out layers from modified checkouts and unknown origins\n merge Copy the layers of several stores into one.\n doctor Check that a store works: credentials, listing, writing, and what it holds.\n policy Print the least-privilege IAM policy for publishers, readers, or both.\n\n --backends <module> A TypeScript or JavaScript module whose default export is a list of\n { id, label?, store } (or a function returning it): a device lab, anything\n that implements ObjectStore.\n\n --format json (alias --json) publish, stream, merge, doctor and status print one JSON document instead of prose\n (schemas: publish-result, stream-result, merge-result, doctor-result, report-status). A command\n that is wrong is {\"ok\":false,\"error\":{\"message\":\u2026,\"code\":\"usage\"}} on standard error.\n A store may be an Atlas server (https://\u2026): ATLAS_STORE=https://atlas.example.com, with ATLAS_TOKEN.\n\n Exit codes: 0 ok, 1 the report does not pass (or a check failed), 2 the command was wrong.\n";
|
|
17
17
|
type Env = Readonly<Record<string, string | undefined>>;
|
|
18
18
|
type Git = (args: readonly string[]) => string | null;
|
|
19
19
|
/** What `reportMain` is given besides arguments: where output goes (`out`), and, for tests and embedding, the environment, working directory, git, clock and standard input (`stdin`, where `stream` reads checks from). Defaults are the real ones. */
|