@crvouga/atlas 0.16.0 → 1.0.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/README.md +52 -8
- package/dist/core/src/cli.d.ts +1 -1
- package/dist/core/src/cli.js +1 -1
- package/dist/reports/src/attachment-files.d.ts +3 -1
- package/dist/reports/src/attachment-files.js +14 -4
- package/dist/reports/src/evidence-types.d.ts +22 -0
- package/dist/reports/src/garbage.d.ts +39 -0
- package/dist/reports/src/garbage.js +82 -0
- package/dist/reports/src/layer-merge.js +5 -0
- 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-index.d.ts +6 -0
- package/dist/reports/src/report-index.js +14 -0
- 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/schema/src/api.d.ts +65 -0
- package/dist/schema/src/api.js +7 -2
- package/dist/schema/src/cli-results.d.ts +107 -2
- package/dist/schema/src/cli-results.js +11 -3
- package/dist/schema/src/index.d.ts +1 -0
- package/dist/schema/src/index.js +1 -0
- package/dist/schema/src/layer.d.ts +165 -0
- package/dist/schema/src/layer.js +72 -1
- package/dist/schema/src/server-config.d.ts +7 -49
- package/dist/schema/src/server-config.js +8 -21
- package/dist/schema/src/user-state.d.ts +115 -0
- package/dist/schema/src/user-state.js +54 -0
- package/dist/server/src/app.d.ts +16 -2
- package/dist/server/src/app.js +62 -0
- package/dist/server/src/config.d.ts +16 -21
- package/dist/server/src/config.js +48 -57
- package/dist/server/src/dev.d.ts +1 -1
- package/dist/server/src/dev.js +20 -7
- package/dist/server/src/index.d.ts +5 -1
- package/dist/server/src/index.js +5 -1
- package/dist/server/src/main.d.ts +23 -7
- package/dist/server/src/main.js +72 -26
- package/dist/server/src/openapi.js +24 -5
- package/dist/server/src/projects.js +89 -14
- package/dist/server/src/settings.d.ts +23 -7
- package/dist/server/src/settings.js +50 -60
- package/dist/server/src/storage.d.ts +68 -0
- package/dist/server/src/storage.js +125 -0
- package/dist/server/src/user-state.d.ts +31 -0
- package/dist/server/src/user-state.js +74 -0
- package/docs/agents.md +1 -1
- package/docs/deploying.md +38 -39
- package/docs/reference/cli.md +17 -7
- package/docs/reference/http-api.md +30 -2
- package/docs/reference/reports.md +165 -2
- package/docs/reference/schema.md +161 -16
- package/docs/reference/server.md +263 -41
- package/docs/report-backends.md +0 -144
- package/docs/report-contract.md +1 -1
- package/docs/rich-evidence.md +29 -0
- package/docs/runners.md +1 -1
- package/docs/server.md +141 -36
- package/docs/statecharts.md +1 -1
- package/docs/ui.md +1 -1
- package/docs/workspaces.md +5 -2
- package/llms-full.txt +265 -234
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/packages/reports/STORAGE.md +1 -1
- package/packages/schema/schemas/check-detail.schema.json +20 -0
- package/packages/schema/schemas/effective-delta.schema.json +61 -0
- package/packages/schema/schemas/effective-evidence.schema.json +61 -0
- package/packages/schema/schemas/evidence-page.schema.json +45 -0
- package/packages/schema/schemas/gc-policy.schema.json +36 -0
- package/packages/schema/schemas/gc-run-result.schema.json +106 -0
- package/packages/schema/schemas/gc-snapshot.schema.json +122 -0
- package/packages/schema/schemas/layer-rows.schema.json +16 -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-status.schema.json +104 -0
- package/packages/schema/schemas/server-config.schema.json +0 -41
- package/packages/schema/schemas/server-editable-settings.schema.json +0 -41
- package/packages/schema/schemas/server-settings-snapshot.schema.json +0 -82
- package/packages/schema/schemas/user-state.schema.json +140 -0
- package/packages/server/README.md +11 -6
- package/packages/server/openapi.json +843 -128
- package/packages/visualizer/src/admin/SettingsPage.tsx +183 -88
- package/packages/visualizer/src/evidence/marks.tsx +16 -0
|
@@ -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. */
|
|
@@ -43,6 +43,7 @@ export const REPORT_USAGE = `atlas report — layered evidence for any kind of s
|
|
|
43
43
|
[--param key=value]... [--kind <k>] [--title <t>] [--recorded-at <iso|ms>]
|
|
44
44
|
[--attach [role=]<file>]... [--label key=value]... [--link [name=]<url>]...
|
|
45
45
|
[--attach-root <dir>]... [--max-attachment-mb <n>] [--max-total-attachment-mb <n>]
|
|
46
|
+
[--require-recordings]
|
|
46
47
|
atlas report stream [--store <dir|s3://…|https://atlas.example.com>] --producer <name> [--environment <name>]
|
|
47
48
|
[--branch <b>] [--sha <s>] [--run-url <u>] [--kind <k>] [--param key=value]...
|
|
48
49
|
[--run-id <id>] [--run-title <t>]
|
|
@@ -79,6 +80,10 @@ export const REPORT_USAGE = `atlas report — layered evidence for any kind of s
|
|
|
79
80
|
Files a report names (CTRF attachments, JUnit [[ATTACHMENT|path]] lines) come from the report's own
|
|
80
81
|
folder and each --attach-root, and never from a hidden file or a link out: a report is data from a
|
|
81
82
|
test run, not instructions. One that is missing or not allowed is skipped with a warning.
|
|
83
|
+
--require-recordings exit 1 when a check that declared recordingExpectation "required" has no
|
|
84
|
+
uploaded video (absent, left out as too large or not allowed, or refused). The layer is
|
|
85
|
+
still written and the checks keep their results; only this command fails. The receipt of
|
|
86
|
+
what was uploaded (counts, sizes, checksums) is in the JSON result under layer.recordings.
|
|
82
87
|
From a checkout with uncommitted changes the layer is marked "modified".
|
|
83
88
|
stream Send checks as they finish: one JSON object per line on standard input (the fields of one
|
|
84
89
|
check: id, title, status, and any evidence). Each second's worth becomes one layer, so a server
|
|
@@ -310,7 +315,7 @@ export async function reportMain(argv, io = { out: text => process.stdout.write(
|
|
|
310
315
|
const json = wantsJson(rest);
|
|
311
316
|
const result = {
|
|
312
317
|
ok: true, store: target.label,
|
|
313
|
-
layer: { id: layer.id, producer, environment: layer.environment ?? null, checks: ingested.checks.length, passed: counts.passed, failed: counts.failed, notRun: counts.other, linkedToSpec: linkedCount, ...(freshness ? { freshness } : {}), attachmentFiles: attachedFiles, attachmentBytes: attachedBytes },
|
|
318
|
+
layer: { id: layer.id, producer, environment: layer.environment ?? null, checks: ingested.checks.length, passed: counts.passed, failed: counts.failed, notRun: counts.other, linkedToSpec: linkedCount, ...(freshness ? { freshness } : {}), attachmentFiles: attachedFiles, attachmentBytes: attachedBytes, ...(layer.recordings ? { recordings: layer.recordings } : {}) },
|
|
314
319
|
warnings: warnings.map(warning => `attachment skipped: ${warning}`)
|
|
315
320
|
};
|
|
316
321
|
if (!json)
|
|
@@ -318,6 +323,19 @@ export async function reportMain(argv, io = { out: text => process.stdout.write(
|
|
|
318
323
|
if (!json)
|
|
319
324
|
for (const warning of warnings)
|
|
320
325
|
io.out(`! attachment skipped: ${warning}\n`);
|
|
326
|
+
// Recording completeness is reported apart from the checks' results. Only --require-recordings turns a gap into a failing exit.
|
|
327
|
+
const gaps = (layer.recordings?.items ?? []).filter(item => item.state !== 'uploaded' && item.state !== 'referenced' && item.state !== 'not-applicable');
|
|
328
|
+
const strict = rest.includes('--require-recordings');
|
|
329
|
+
if (layer.recordings) {
|
|
330
|
+
result.recordings = { strict, complete: gaps.length === 0, expected: layer.recordings.expected, uploaded: layer.recordings.uploaded, gaps: gaps.map(({ check, state, reason }) => ({ check, state, ...(reason ? { reason } : {}) })) };
|
|
331
|
+
if (!json) {
|
|
332
|
+
io.out(`Recordings: ${layer.recordings.uploaded + layer.recordings.referenced} of ${layer.recordings.expected} expected published${layer.recordings.notApplicable ? `, ${layer.recordings.notApplicable} not applicable` : ''}\n`);
|
|
333
|
+
for (const gap of gaps)
|
|
334
|
+
io.out(`! recording ${gap.state}: ${gap.check}${gap.reason ? ` (${gap.reason})` : ''}\n`);
|
|
335
|
+
}
|
|
336
|
+
if (strict && gaps.length)
|
|
337
|
+
result.ok = false;
|
|
338
|
+
}
|
|
321
339
|
if (specsDirectory) {
|
|
322
340
|
const files = await collectSpecFiles(path.resolve(base, specsDirectory));
|
|
323
341
|
// A spec named with --spec that the directory does not hold (or whose kind is not a spec file) is published too.
|
|
@@ -343,7 +361,7 @@ export async function reportMain(argv, io = { out: text => process.stdout.write(
|
|
|
343
361
|
}
|
|
344
362
|
if (json)
|
|
345
363
|
io.out(`${JSON.stringify(result, null, 2)}\n`);
|
|
346
|
-
return 0;
|
|
364
|
+
return result.ok ? 0 : 1;
|
|
347
365
|
}
|
|
348
366
|
if (command === 'stream') {
|
|
349
367
|
const target = writeStoreOf(value(rest, 'store'), base, env);
|
|
@@ -93,6 +93,12 @@ export declare class ReportIndex {
|
|
|
93
93
|
* and the run list are built from. Read-only for callers; `history.rev` changes whenever a layer is taken in.
|
|
94
94
|
*/
|
|
95
95
|
get history(): TestHistory;
|
|
96
|
+
/**
|
|
97
|
+
* The ids of layers this index has read and that no longer decide any result. Removing one from its store cannot
|
|
98
|
+
* change the report (every slot it filled is held by a newer layer), which is what makes it safe for garbage
|
|
99
|
+
* collection to delete. A layer that failed to load, is hidden by `include`, or still holds a result is not here.
|
|
100
|
+
*/
|
|
101
|
+
superseded(): ReadonlySet<string>;
|
|
96
102
|
/** For `/readyz` and logs. */
|
|
97
103
|
stats(): {
|
|
98
104
|
backends: number;
|
|
@@ -50,6 +50,8 @@ export class ReportIndex {
|
|
|
50
50
|
/** Layers that stopped holding any winner, and when. Older removals are forgotten; `#forgotten` says how far. */
|
|
51
51
|
#removed = [];
|
|
52
52
|
#forgotten = 0;
|
|
53
|
+
/** Layers that were read, took part in the report, and no longer decide any result: all their checks were replaced by newer layers. */
|
|
54
|
+
#superseded = new Set();
|
|
53
55
|
#refreshing = null;
|
|
54
56
|
#timer = null;
|
|
55
57
|
#refreshedAt = 0;
|
|
@@ -248,6 +250,9 @@ export class ReportIndex {
|
|
|
248
250
|
if (wins.size) {
|
|
249
251
|
this.#held.set(id, { evidence, wins, rev: ++this.#rev, entry: null, json: null, digest: null });
|
|
250
252
|
}
|
|
253
|
+
else {
|
|
254
|
+
this.#superseded.add(id);
|
|
255
|
+
}
|
|
251
256
|
}
|
|
252
257
|
/** What this layer sends has changed. */
|
|
253
258
|
#touch(held) {
|
|
@@ -264,6 +269,7 @@ export class ReportIndex {
|
|
|
264
269
|
return;
|
|
265
270
|
}
|
|
266
271
|
this.#held.delete(layerId);
|
|
272
|
+
this.#superseded.add(layerId);
|
|
267
273
|
this.#removed.push({ id: layerId, rev: ++this.#rev });
|
|
268
274
|
if (this.#removed.length > REMEMBERED_REMOVALS)
|
|
269
275
|
this.#forgotten = this.#removed.splice(0, this.#removed.length - REMEMBERED_REMOVALS).at(-1).rev;
|
|
@@ -342,6 +348,14 @@ export class ReportIndex {
|
|
|
342
348
|
get history() {
|
|
343
349
|
return this.#history;
|
|
344
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* The ids of layers this index has read and that no longer decide any result. Removing one from its store cannot
|
|
353
|
+
* change the report (every slot it filled is held by a newer layer), which is what makes it safe for garbage
|
|
354
|
+
* collection to delete. A layer that failed to load, is hidden by `include`, or still holds a result is not here.
|
|
355
|
+
*/
|
|
356
|
+
superseded() {
|
|
357
|
+
return this.#superseded;
|
|
358
|
+
}
|
|
345
359
|
/** For `/readyz` and logs. */
|
|
346
360
|
stats() {
|
|
347
361
|
return { backends: this.#backends.length, layers: this.#attribution.size, slots: this.#winners.size, holding: this.#held.size, failed: this.#failed.size, refreshedAt: this.#refreshedAt };
|
|
@@ -26,6 +26,8 @@ export type StatusOptions = {
|
|
|
26
26
|
* each required environment with no checks (the name is normalised, so `iOS` means `ios`), then every notice of the
|
|
27
27
|
* report (an unreadable layer or backend). `failing` lists each failed or running winner, in a fixed order that does
|
|
28
28
|
* not depend on read order; diagnostic `performance` checks are left out unless nothing else was recorded.
|
|
29
|
+
* `recordings` reports recording completeness of the same results apart from `ok`: an incomplete or unknown
|
|
30
|
+
* completeness never changes the gate (use `atlas report publish --require-recordings` to fail on a missing video).
|
|
29
31
|
*
|
|
30
32
|
* @example
|
|
31
33
|
* const status = reportStatus(await combineBackends(backends), { requireEnvironments: ['ios'] });
|
|
@@ -12,6 +12,7 @@ import { REPORT_STATUS_SCHEMA_VERSION } from '../../schema/src/layer.js';
|
|
|
12
12
|
import { countResults } from './compose.js';
|
|
13
13
|
import { normalizeEnvironment } from './layer-axes.js';
|
|
14
14
|
import { isDiagnosticKind } from './layer-merge.js';
|
|
15
|
+
import { recordingCompleteness } from './recording-receipt.js';
|
|
15
16
|
/** Total string order by code unit. */
|
|
16
17
|
const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
17
18
|
/**
|
|
@@ -23,6 +24,8 @@ const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
|
23
24
|
* each required environment with no checks (the name is normalised, so `iOS` means `ios`), then every notice of the
|
|
24
25
|
* report (an unreadable layer or backend). `failing` lists each failed or running winner, in a fixed order that does
|
|
25
26
|
* not depend on read order; diagnostic `performance` checks are left out unless nothing else was recorded.
|
|
27
|
+
* `recordings` reports recording completeness of the same results apart from `ok`: an incomplete or unknown
|
|
28
|
+
* completeness never changes the gate (use `atlas report publish --require-recordings` to fail on a missing video).
|
|
26
29
|
*
|
|
27
30
|
* @example
|
|
28
31
|
* const status = reportStatus(await combineBackends(backends), { requireEnvironments: ['ios'] });
|
|
@@ -85,7 +88,12 @@ export function reportStatus(report, options = {}) {
|
|
|
85
88
|
...report.combined.specs.map(entry => specOutcome(entry.spec.path, entry.spec.title, entry.spec.kind, entry.results)),
|
|
86
89
|
...(report.combined.unassigned.length ? [specOutcome(null, 'No spec', 'unassigned', report.combined.unassigned)] : [])
|
|
87
90
|
],
|
|
88
|
-
failing
|
|
91
|
+
failing,
|
|
92
|
+
// Apart from `ok`: whether the expected videos were published says nothing about whether the checks passed.
|
|
93
|
+
recordings: recordingCompleteness(outcomes.map(result => ({
|
|
94
|
+
spec: result.row.specPath, check: result.row.id, environment: result.layer.environment, layer: result.layer.id,
|
|
95
|
+
hasReceipt: result.layer.recordings !== undefined, ...(result.row.recording ? { recording: result.row.recording } : {})
|
|
96
|
+
})))
|
|
89
97
|
};
|
|
90
98
|
}
|
|
91
99
|
/** `spec › check`, unless the check's id already starts with its spec. */
|
|
@@ -120,6 +128,14 @@ export function formatStatusText(status) {
|
|
|
120
128
|
const where = [item.environment, item.branch, item.sha?.slice(0, 7)].filter(Boolean).join(' · ');
|
|
121
129
|
lines.push(`✗ ${item.status === 'running' ? '[running] ' : ''}${label(item)}${where ? ` (${where})` : ''}${item.detail ? `\n ${item.detail.split('\n')[0]}` : ''}`);
|
|
122
130
|
}
|
|
131
|
+
const recordings = status.recordings;
|
|
132
|
+
// Quiet for a report that never mentioned recordings; the JSON says `unknown` explicitly.
|
|
133
|
+
if (recordings && !(recordings.state === 'unknown' && recordings.expected === 0 && recordings.notApplicable === 0)) {
|
|
134
|
+
const published = recordings.uploaded + recordings.referenced;
|
|
135
|
+
lines.push(`· recordings: ${recordings.state} — ${published} of ${recordings.expected} expected published${recordings.notApplicable ? `, ${recordings.notApplicable} not applicable` : ''}${recordings.layersWithoutReceipt ? `, ${recordings.layersWithoutReceipt} layer${recordings.layersWithoutReceipt === 1 ? '' : 's'} unknown` : ''}`);
|
|
136
|
+
for (const gap of recordings.gaps)
|
|
137
|
+
lines.push(` ! recording ${gap.state}: ${label({ spec: gap.spec, check: gap.check })}${gap.reason ? ` (${gap.reason})` : ''}`);
|
|
138
|
+
}
|
|
123
139
|
for (const notice of status.notices)
|
|
124
140
|
lines.push(`! ${notice}`);
|
|
125
141
|
for (const reason of status.reasons)
|
|
@@ -82,6 +82,7 @@ export declare function createReportsExplorerApp(deps: ReportsExplorerDeps): imp
|
|
|
82
82
|
height?: number | undefined;
|
|
83
83
|
durationMs?: number | undefined;
|
|
84
84
|
description?: string | undefined;
|
|
85
|
+
omitted?: string | undefined;
|
|
85
86
|
tags?: string[] | undefined;
|
|
86
87
|
at?: {
|
|
87
88
|
[x: string]: import("hono/utils/types").JSONValue;
|
|
@@ -126,6 +127,7 @@ export declare function createReportsExplorerApp(deps: ReportsExplorerDeps): imp
|
|
|
126
127
|
height?: number | undefined;
|
|
127
128
|
durationMs?: number | undefined;
|
|
128
129
|
description?: string | undefined;
|
|
130
|
+
omitted?: string | undefined;
|
|
129
131
|
tags?: string[] | undefined;
|
|
130
132
|
at?: {
|
|
131
133
|
[x: string]: import("hono/utils/types").JSONValue;
|
|
@@ -183,6 +185,7 @@ export declare function createReportsExplorerApp(deps: ReportsExplorerDeps): imp
|
|
|
183
185
|
height?: number | undefined;
|
|
184
186
|
durationMs?: number | undefined;
|
|
185
187
|
description?: string | undefined;
|
|
188
|
+
omitted?: string | undefined;
|
|
186
189
|
tags?: string[] | undefined;
|
|
187
190
|
at?: {
|
|
188
191
|
[x: string]: import("hono/utils/types").JSONValue;
|
|
@@ -250,6 +253,7 @@ export declare function createReportsExplorerApp(deps: ReportsExplorerDeps): imp
|
|
|
250
253
|
height?: number | undefined;
|
|
251
254
|
durationMs?: number | undefined;
|
|
252
255
|
description?: string | undefined;
|
|
256
|
+
omitted?: string | undefined;
|
|
253
257
|
tags?: string[] | undefined;
|
|
254
258
|
at?: {
|
|
255
259
|
[x: string]: import("hono/utils/types").JSONValue;
|
|
@@ -356,6 +360,7 @@ export declare function createReportsExplorerApp(deps: ReportsExplorerDeps): imp
|
|
|
356
360
|
height?: number | undefined;
|
|
357
361
|
durationMs?: number | undefined;
|
|
358
362
|
description?: string | undefined;
|
|
363
|
+
omitted?: string | undefined;
|
|
359
364
|
tags?: string[] | undefined;
|
|
360
365
|
at?: {
|
|
361
366
|
[x: string]: import("hono/utils/types").JSONValue;
|
|
@@ -400,6 +405,7 @@ export declare function createReportsExplorerApp(deps: ReportsExplorerDeps): imp
|
|
|
400
405
|
height?: number | undefined;
|
|
401
406
|
durationMs?: number | undefined;
|
|
402
407
|
description?: string | undefined;
|
|
408
|
+
omitted?: string | undefined;
|
|
403
409
|
tags?: string[] | undefined;
|
|
404
410
|
at?: {
|
|
405
411
|
[x: string]: import("hono/utils/types").JSONValue;
|
|
@@ -457,6 +463,7 @@ export declare function createReportsExplorerApp(deps: ReportsExplorerDeps): imp
|
|
|
457
463
|
height?: number | undefined;
|
|
458
464
|
durationMs?: number | undefined;
|
|
459
465
|
description?: string | undefined;
|
|
466
|
+
omitted?: string | undefined;
|
|
460
467
|
tags?: string[] | undefined;
|
|
461
468
|
at?: {
|
|
462
469
|
[x: string]: import("hono/utils/types").JSONValue;
|
|
@@ -524,6 +531,7 @@ export declare function createReportsExplorerApp(deps: ReportsExplorerDeps): imp
|
|
|
524
531
|
height?: number | undefined;
|
|
525
532
|
durationMs?: number | undefined;
|
|
526
533
|
description?: string | undefined;
|
|
534
|
+
omitted?: string | undefined;
|
|
527
535
|
tags?: string[] | undefined;
|
|
528
536
|
at?: {
|
|
529
537
|
[x: string]: import("hono/utils/types").JSONValue;
|
|
@@ -120,6 +120,7 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
|
|
|
120
120
|
height?: number | undefined;
|
|
121
121
|
durationMs?: number | undefined;
|
|
122
122
|
description?: string | undefined;
|
|
123
|
+
omitted?: string | undefined;
|
|
123
124
|
tags?: string[] | undefined;
|
|
124
125
|
at?: {
|
|
125
126
|
[x: string]: import("hono/utils/types").JSONValue;
|
|
@@ -164,6 +165,7 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
|
|
|
164
165
|
height?: number | undefined;
|
|
165
166
|
durationMs?: number | undefined;
|
|
166
167
|
description?: string | undefined;
|
|
168
|
+
omitted?: string | undefined;
|
|
167
169
|
tags?: string[] | undefined;
|
|
168
170
|
at?: {
|
|
169
171
|
[x: string]: import("hono/utils/types").JSONValue;
|
|
@@ -221,6 +223,7 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
|
|
|
221
223
|
height?: number | undefined;
|
|
222
224
|
durationMs?: number | undefined;
|
|
223
225
|
description?: string | undefined;
|
|
226
|
+
omitted?: string | undefined;
|
|
224
227
|
tags?: string[] | undefined;
|
|
225
228
|
at?: {
|
|
226
229
|
[x: string]: import("hono/utils/types").JSONValue;
|
|
@@ -288,6 +291,7 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
|
|
|
288
291
|
height?: number | undefined;
|
|
289
292
|
durationMs?: number | undefined;
|
|
290
293
|
description?: string | undefined;
|
|
294
|
+
omitted?: string | undefined;
|
|
291
295
|
tags?: string[] | undefined;
|
|
292
296
|
at?: {
|
|
293
297
|
[x: string]: import("hono/utils/types").JSONValue;
|
|
@@ -394,6 +398,7 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
|
|
|
394
398
|
height?: number | undefined;
|
|
395
399
|
durationMs?: number | undefined;
|
|
396
400
|
description?: string | undefined;
|
|
401
|
+
omitted?: string | undefined;
|
|
397
402
|
tags?: string[] | undefined;
|
|
398
403
|
at?: {
|
|
399
404
|
[x: string]: import("hono/utils/types").JSONValue;
|
|
@@ -438,6 +443,7 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
|
|
|
438
443
|
height?: number | undefined;
|
|
439
444
|
durationMs?: number | undefined;
|
|
440
445
|
description?: string | undefined;
|
|
446
|
+
omitted?: string | undefined;
|
|
441
447
|
tags?: string[] | undefined;
|
|
442
448
|
at?: {
|
|
443
449
|
[x: string]: import("hono/utils/types").JSONValue;
|
|
@@ -495,6 +501,7 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
|
|
|
495
501
|
height?: number | undefined;
|
|
496
502
|
durationMs?: number | undefined;
|
|
497
503
|
description?: string | undefined;
|
|
504
|
+
omitted?: string | undefined;
|
|
498
505
|
tags?: string[] | undefined;
|
|
499
506
|
at?: {
|
|
500
507
|
[x: string]: import("hono/utils/types").JSONValue;
|
|
@@ -562,6 +569,7 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
|
|
|
562
569
|
height?: number | undefined;
|
|
563
570
|
durationMs?: number | undefined;
|
|
564
571
|
description?: string | undefined;
|
|
572
|
+
omitted?: string | undefined;
|
|
565
573
|
tags?: string[] | undefined;
|
|
566
574
|
at?: {
|
|
567
575
|
[x: string]: import("hono/utils/types").JSONValue;
|
package/dist/schema/src/api.d.ts
CHANGED
|
@@ -165,6 +165,14 @@ export declare const LayerHeaderSchema: z.ZodObject<{
|
|
|
165
165
|
available: z.ZodBoolean;
|
|
166
166
|
}, z.core.$loose>>>;
|
|
167
167
|
}, z.core.$loose>>;
|
|
168
|
+
recordings: z.ZodOptional<z.ZodObject<{
|
|
169
|
+
expected: z.ZodNumber;
|
|
170
|
+
uploaded: z.ZodNumber;
|
|
171
|
+
referenced: z.ZodNumber;
|
|
172
|
+
skipped: z.ZodNumber;
|
|
173
|
+
unresolved: z.ZodNumber;
|
|
174
|
+
notApplicable: z.ZodNumber;
|
|
175
|
+
}, z.core.$loose>>;
|
|
168
176
|
}, z.core.$loose>;
|
|
169
177
|
/** One check of a layer as the report shows it. */
|
|
170
178
|
export declare const LayerRowSchema: z.ZodObject<{
|
|
@@ -207,6 +215,10 @@ export declare const LayerRowSchema: z.ZodObject<{
|
|
|
207
215
|
available: z.ZodBoolean;
|
|
208
216
|
}, z.core.$loose>>>;
|
|
209
217
|
}, z.core.$loose>>;
|
|
218
|
+
recording: z.ZodOptional<z.ZodObject<{
|
|
219
|
+
state: z.ZodString;
|
|
220
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
221
|
+
}, z.core.$loose>>;
|
|
210
222
|
}, z.core.$loose>;
|
|
211
223
|
/** A layer with its checks: the rows of one layer, read apart from its header. */
|
|
212
224
|
export declare const LayerRowsSchema: z.ZodObject<{
|
|
@@ -251,6 +263,10 @@ export declare const LayerRowsSchema: z.ZodObject<{
|
|
|
251
263
|
available: z.ZodBoolean;
|
|
252
264
|
}, z.core.$loose>>>;
|
|
253
265
|
}, z.core.$loose>>;
|
|
266
|
+
recording: z.ZodOptional<z.ZodObject<{
|
|
267
|
+
state: z.ZodString;
|
|
268
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
269
|
+
}, z.core.$loose>>;
|
|
254
270
|
}, z.core.$loose>>;
|
|
255
271
|
fingerprints: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
256
272
|
}, z.core.$loose>;
|
|
@@ -297,6 +313,10 @@ export declare const LayerEvidenceSchema: z.ZodObject<{
|
|
|
297
313
|
available: z.ZodBoolean;
|
|
298
314
|
}, z.core.$loose>>>;
|
|
299
315
|
}, z.core.$loose>>;
|
|
316
|
+
recording: z.ZodOptional<z.ZodObject<{
|
|
317
|
+
state: z.ZodString;
|
|
318
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
319
|
+
}, z.core.$loose>>;
|
|
300
320
|
}, z.core.$loose>>;
|
|
301
321
|
fingerprints: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
302
322
|
layer: z.ZodObject<{
|
|
@@ -348,6 +368,14 @@ export declare const LayerEvidenceSchema: z.ZodObject<{
|
|
|
348
368
|
available: z.ZodBoolean;
|
|
349
369
|
}, z.core.$loose>>>;
|
|
350
370
|
}, z.core.$loose>>;
|
|
371
|
+
recordings: z.ZodOptional<z.ZodObject<{
|
|
372
|
+
expected: z.ZodNumber;
|
|
373
|
+
uploaded: z.ZodNumber;
|
|
374
|
+
referenced: z.ZodNumber;
|
|
375
|
+
skipped: z.ZodNumber;
|
|
376
|
+
unresolved: z.ZodNumber;
|
|
377
|
+
notApplicable: z.ZodNumber;
|
|
378
|
+
}, z.core.$loose>>;
|
|
351
379
|
}, z.core.$loose>;
|
|
352
380
|
}, z.core.$loose>;
|
|
353
381
|
/**
|
|
@@ -399,6 +427,10 @@ export declare const EffectiveEvidenceSchema: z.ZodObject<{
|
|
|
399
427
|
available: z.ZodBoolean;
|
|
400
428
|
}, z.core.$loose>>>;
|
|
401
429
|
}, z.core.$loose>>;
|
|
430
|
+
recording: z.ZodOptional<z.ZodObject<{
|
|
431
|
+
state: z.ZodString;
|
|
432
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
433
|
+
}, z.core.$loose>>;
|
|
402
434
|
}, z.core.$loose>>;
|
|
403
435
|
fingerprints: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
404
436
|
layer: z.ZodObject<{
|
|
@@ -450,6 +482,14 @@ export declare const EffectiveEvidenceSchema: z.ZodObject<{
|
|
|
450
482
|
available: z.ZodBoolean;
|
|
451
483
|
}, z.core.$loose>>>;
|
|
452
484
|
}, z.core.$loose>>;
|
|
485
|
+
recordings: z.ZodOptional<z.ZodObject<{
|
|
486
|
+
expected: z.ZodNumber;
|
|
487
|
+
uploaded: z.ZodNumber;
|
|
488
|
+
referenced: z.ZodNumber;
|
|
489
|
+
skipped: z.ZodNumber;
|
|
490
|
+
unresolved: z.ZodNumber;
|
|
491
|
+
notApplicable: z.ZodNumber;
|
|
492
|
+
}, z.core.$loose>>;
|
|
453
493
|
}, z.core.$loose>;
|
|
454
494
|
}, z.core.$loose>>;
|
|
455
495
|
backends: z.ZodArray<z.ZodObject<{
|
|
@@ -522,6 +562,10 @@ export declare const EffectiveDeltaSchema: z.ZodObject<{
|
|
|
522
562
|
available: z.ZodBoolean;
|
|
523
563
|
}, z.core.$loose>>>;
|
|
524
564
|
}, z.core.$loose>>;
|
|
565
|
+
recording: z.ZodOptional<z.ZodObject<{
|
|
566
|
+
state: z.ZodString;
|
|
567
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
568
|
+
}, z.core.$loose>>;
|
|
525
569
|
}, z.core.$loose>>;
|
|
526
570
|
fingerprints: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
527
571
|
layer: z.ZodObject<{
|
|
@@ -573,6 +617,14 @@ export declare const EffectiveDeltaSchema: z.ZodObject<{
|
|
|
573
617
|
available: z.ZodBoolean;
|
|
574
618
|
}, z.core.$loose>>>;
|
|
575
619
|
}, z.core.$loose>>;
|
|
620
|
+
recordings: z.ZodOptional<z.ZodObject<{
|
|
621
|
+
expected: z.ZodNumber;
|
|
622
|
+
uploaded: z.ZodNumber;
|
|
623
|
+
referenced: z.ZodNumber;
|
|
624
|
+
skipped: z.ZodNumber;
|
|
625
|
+
unresolved: z.ZodNumber;
|
|
626
|
+
notApplicable: z.ZodNumber;
|
|
627
|
+
}, z.core.$loose>>;
|
|
576
628
|
}, z.core.$loose>;
|
|
577
629
|
}, z.core.$loose>>;
|
|
578
630
|
removed: z.ZodArray<z.ZodString>;
|
|
@@ -648,6 +700,14 @@ export declare const EvidencePageSchema: z.ZodObject<{
|
|
|
648
700
|
available: z.ZodBoolean;
|
|
649
701
|
}, z.core.$loose>>>;
|
|
650
702
|
}, z.core.$loose>>;
|
|
703
|
+
recordings: z.ZodOptional<z.ZodObject<{
|
|
704
|
+
expected: z.ZodNumber;
|
|
705
|
+
uploaded: z.ZodNumber;
|
|
706
|
+
referenced: z.ZodNumber;
|
|
707
|
+
skipped: z.ZodNumber;
|
|
708
|
+
unresolved: z.ZodNumber;
|
|
709
|
+
notApplicable: z.ZodNumber;
|
|
710
|
+
}, z.core.$loose>>;
|
|
651
711
|
}, z.core.$loose>>;
|
|
652
712
|
cursor: z.ZodNullable<z.ZodString>;
|
|
653
713
|
notices: z.ZodArray<z.ZodString>;
|
|
@@ -687,6 +747,7 @@ export declare const CheckDetailSchema: z.ZodObject<{
|
|
|
687
747
|
height: z.ZodOptional<z.ZodNumber>;
|
|
688
748
|
durationMs: z.ZodOptional<z.ZodNumber>;
|
|
689
749
|
description: z.ZodOptional<z.ZodString>;
|
|
750
|
+
omitted: z.ZodOptional<z.ZodString>;
|
|
690
751
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
691
752
|
at: z.ZodOptional<z.ZodObject<{
|
|
692
753
|
step: z.ZodOptional<z.ZodString>;
|
|
@@ -752,6 +813,7 @@ export declare const CheckDetailSchema: z.ZodObject<{
|
|
|
752
813
|
height: z.ZodOptional<z.ZodNumber>;
|
|
753
814
|
durationMs: z.ZodOptional<z.ZodNumber>;
|
|
754
815
|
description: z.ZodOptional<z.ZodString>;
|
|
816
|
+
omitted: z.ZodOptional<z.ZodString>;
|
|
755
817
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
756
818
|
at: z.ZodOptional<z.ZodObject<{
|
|
757
819
|
step: z.ZodOptional<z.ZodString>;
|
|
@@ -797,6 +859,7 @@ export declare const CheckDetailSchema: z.ZodObject<{
|
|
|
797
859
|
height: z.ZodOptional<z.ZodNumber>;
|
|
798
860
|
durationMs: z.ZodOptional<z.ZodNumber>;
|
|
799
861
|
description: z.ZodOptional<z.ZodString>;
|
|
862
|
+
omitted: z.ZodOptional<z.ZodString>;
|
|
800
863
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
801
864
|
at: z.ZodOptional<z.ZodObject<{
|
|
802
865
|
step: z.ZodOptional<z.ZodString>;
|
|
@@ -849,6 +912,7 @@ export declare const CheckDetailSchema: z.ZodObject<{
|
|
|
849
912
|
height: z.ZodOptional<z.ZodNumber>;
|
|
850
913
|
durationMs: z.ZodOptional<z.ZodNumber>;
|
|
851
914
|
description: z.ZodOptional<z.ZodString>;
|
|
915
|
+
omitted: z.ZodOptional<z.ZodString>;
|
|
852
916
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
853
917
|
at: z.ZodOptional<z.ZodObject<{
|
|
854
918
|
step: z.ZodOptional<z.ZodString>;
|
|
@@ -896,6 +960,7 @@ export declare const CheckDetailSchema: z.ZodObject<{
|
|
|
896
960
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
897
961
|
}, z.core.$loose>>>;
|
|
898
962
|
durationMs: z.ZodOptional<z.ZodNumber>;
|
|
963
|
+
recordingExpectation: z.ZodOptional<z.ZodString>;
|
|
899
964
|
}, z.core.$loose>;
|
|
900
965
|
run: z.ZodObject<{}, z.core.$loose>;
|
|
901
966
|
}, z.core.$loose>;
|
package/dist/schema/src/api.js
CHANGED
|
@@ -108,7 +108,11 @@ export const LayerHeaderSchema = z.looseObject({
|
|
|
108
108
|
live: z.boolean(),
|
|
109
109
|
counts: z.record(EvidenceStatusSchema, z.number().int().nonnegative()),
|
|
110
110
|
execution: z.string(),
|
|
111
|
-
evidence: EvidenceSummarySchema.optional()
|
|
111
|
+
evidence: EvidenceSummarySchema.optional(),
|
|
112
|
+
recordings: z.looseObject({
|
|
113
|
+
expected: z.number().int().nonnegative(), uploaded: z.number().int().nonnegative(), referenced: z.number().int().nonnegative(),
|
|
114
|
+
skipped: z.number().int().nonnegative(), unresolved: z.number().int().nonnegative(), notApplicable: z.number().int().nonnegative()
|
|
115
|
+
}).optional().describe('How many of the recordings the layer expected were published. Absent: the layer declared no expectations, so its completeness is unknown, not complete')
|
|
112
116
|
});
|
|
113
117
|
/** One check of a layer as the report shows it. */
|
|
114
118
|
export const LayerRowSchema = z.looseObject({
|
|
@@ -122,7 +126,8 @@ export const LayerRowSchema = z.looseObject({
|
|
|
122
126
|
testId: z.string().nullable(),
|
|
123
127
|
variant: z.record(z.string(), z.string()),
|
|
124
128
|
durationMs: z.number().nullable(),
|
|
125
|
-
evidence: EvidenceSummarySchema.optional()
|
|
129
|
+
evidence: EvidenceSummarySchema.optional(),
|
|
130
|
+
recording: z.looseObject({ state: z.string(), reason: z.string().optional() }).optional().describe('What became of the check\'s expected recording: uploaded, referenced, skipped, unresolved or not-applicable. Independent of `status`')
|
|
126
131
|
});
|
|
127
132
|
/** A layer with its checks: the rows of one layer, read apart from its header. */
|
|
128
133
|
export const LayerRowsSchema = z.looseObject({
|