@crvouga/atlas 1.3.0 → 1.4.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 +6 -0
- package/dist/reports/src/evidence-detail.d.ts +23 -0
- package/dist/reports/src/evidence-detail.js +19 -1
- package/dist/reports/src/export-evidence.js +1 -1
- package/dist/reports/src/layers-route.d.ts +2 -1
- package/dist/reports/src/layers-route.js +8 -4
- package/dist/reports/src/test-catalog.d.ts +27 -0
- package/dist/reports/src/test-catalog.js +8 -2
- package/dist/schema/src/api.d.ts +22 -0
- package/dist/schema/src/api.js +10 -1
- package/dist/server/src/openapi.js +2 -2
- package/docs/reference/http-api.md +4 -4
- package/docs/reference/reports.md +64 -7
- package/docs/rich-evidence.md +23 -0
- package/docs/ui.md +1 -1
- package/llms-full.txt +28 -5
- package/package.json +1 -1
- package/packages/schema/schemas/check-detail.schema.json +24 -0
- package/packages/schema/schemas/report-run.schema.json +85 -0
- package/packages/schema/schemas/report-runs.schema.json +6 -0
- package/packages/server/openapi.json +119 -4
- package/packages/visualizer/src/evidence/artifacts.tsx +54 -12
- package/packages/visualizer/src/explorer/Explorer.tsx +9 -5
- package/packages/visualizer/src/explorer/RunArtifacts.tsx +246 -0
- package/packages/visualizer/src/explorer/RunsView.tsx +40 -9
- package/packages/visualizer/src/explorer/search.ts +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog: @crvouga/atlas
|
|
2
2
|
|
|
3
|
+
## 1.4.0 (2026-10-28)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- surface run attachments and link directly to a recording ([879f049](https://github.com/crvouga/atlas/commit/879f049e9f7bdae1ff1d869125640aa063a9f653))
|
|
8
|
+
|
|
3
9
|
## 1.3.0 (2026-10-28)
|
|
4
10
|
|
|
5
11
|
### Features
|
|
@@ -57,6 +57,12 @@ export declare function summarizeLayerEvidence(...holders: readonly EvidenceHold
|
|
|
57
57
|
* settles a file whose type nothing says (`application/octet-stream`).
|
|
58
58
|
*/
|
|
59
59
|
export declare function isVideoAttachment(attachment: Pick<Attachment, 'name' | 'role' | 'contentType'>): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* What identifies an attachment's bytes: its digest, else its bundle key, address or inline text. Attachments with the
|
|
62
|
+
* same identity are the same file however many results reference them. Undefined when it names no bytes at all (it can
|
|
63
|
+
* never be opened). This is the `recording` of a {@link VideoRef}.
|
|
64
|
+
*/
|
|
65
|
+
export declare function attachmentIdentity(item: Pick<Attachment, 'name' | 'sha256' | 'bundleKey' | 'uri' | 'text'>): string | undefined;
|
|
60
66
|
/**
|
|
61
67
|
* Counts references, distinct playable recordings and unplayable references. Undefined when there is no reference, so
|
|
62
68
|
* "no video" and "nothing known about video" look the same on the wire.
|
|
@@ -88,5 +94,22 @@ export declare function checkDetailOf(layer: {
|
|
|
88
94
|
} & EvidenceHolder, checkId: string, kind?: string): CheckDetail | null;
|
|
89
95
|
/** What a layer carries as a whole (its scope, tags, labels, links, attachments, annotations and extensions; not its parts' checks). Absent fields stay absent. */
|
|
90
96
|
export declare function runEvidence(layer: EvidenceHolder): EvidenceHolder;
|
|
97
|
+
/** What one part of a layer (one kind of report) carries as a whole, named by the part. */
|
|
98
|
+
export type PartEvidence = EvidenceHolder & {
|
|
99
|
+
readonly kind: string;
|
|
100
|
+
readonly id: string;
|
|
101
|
+
readonly title: string;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* The parts of a layer that carry evidence of their own (attachments, links, labels…), each with its name. Parts
|
|
105
|
+
* that carry none are left out. A part's files belong to the run, not to any one check.
|
|
106
|
+
*/
|
|
107
|
+
export declare function partsEvidence(layer: {
|
|
108
|
+
readonly parts: readonly (EvidenceHolder & {
|
|
109
|
+
readonly kind: string;
|
|
110
|
+
readonly id: string;
|
|
111
|
+
readonly title: string;
|
|
112
|
+
})[];
|
|
113
|
+
}): PartEvidence[];
|
|
91
114
|
/** Every distinct bundle key (`bundleKey`) that the attachments of a holder, its steps and its attempts point to, so a layer's files can be copied. Attachments held by `uri` or inline `text` have none. */
|
|
92
115
|
export declare function bundleKeysOf(holder: EvidenceHolder): string[];
|
|
@@ -85,10 +85,18 @@ export function isVideoAttachment(attachment) {
|
|
|
85
85
|
const type = (attachment.contentType ?? contentTypeOf(attachment.name)).toLowerCase();
|
|
86
86
|
return type.startsWith('video/') || (attachment.role === 'video' && type.startsWith('application/octet-stream'));
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* What identifies an attachment's bytes: its digest, else its bundle key, address or inline text. Attachments with the
|
|
90
|
+
* same identity are the same file however many results reference them. Undefined when it names no bytes at all (it can
|
|
91
|
+
* never be opened). This is the `recording` of a {@link VideoRef}.
|
|
92
|
+
*/
|
|
93
|
+
export function attachmentIdentity(item) {
|
|
94
|
+
return item.sha256 ? `sha256:${item.sha256}` : item.bundleKey ?? item.uri ?? (item.text === undefined ? undefined : `inline:${item.name}`);
|
|
95
|
+
}
|
|
88
96
|
function videoOf(item, owner, where, scope) {
|
|
89
97
|
if (!isVideoAttachment(item))
|
|
90
98
|
return undefined;
|
|
91
|
-
const recording =
|
|
99
|
+
const recording = attachmentIdentity(item);
|
|
92
100
|
const tags = normalizeTags(item.tags);
|
|
93
101
|
return { name: item.name, owner, ...where, ...(tags.length ? { tags } : {}), ...(recording === undefined ? {} : { recording }), available: recording !== undefined, ...(scope ? { scope } : {}) };
|
|
94
102
|
}
|
|
@@ -142,6 +150,16 @@ export function runEvidence(layer) {
|
|
|
142
150
|
const { tags, labels, links, attachments, annotations, extensions, scope } = layer;
|
|
143
151
|
return { ...(scope ? { scope } : {}), ...(tags ? { tags } : {}), ...(labels ? { labels } : {}), ...(links ? { links } : {}), ...(attachments ? { attachments } : {}), ...(annotations ? { annotations } : {}), ...(extensions ? { extensions } : {}) };
|
|
144
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* The parts of a layer that carry evidence of their own (attachments, links, labels…), each with its name. Parts
|
|
155
|
+
* that carry none are left out. A part's files belong to the run, not to any one check.
|
|
156
|
+
*/
|
|
157
|
+
export function partsEvidence(layer) {
|
|
158
|
+
return layer.parts.flatMap(part => {
|
|
159
|
+
const evidence = runEvidence(part);
|
|
160
|
+
return Object.keys(evidence).length ? [{ kind: part.kind, id: part.id, title: part.title, ...evidence }] : [];
|
|
161
|
+
});
|
|
162
|
+
}
|
|
145
163
|
/** Every distinct bundle key (`bundleKey`) that the attachments of a holder, its steps and its attempts point to, so a layer's files can be copied. Attachments held by `uri` or inline `text` have none. */
|
|
146
164
|
export function bundleKeysOf(holder) {
|
|
147
165
|
const keys = new Set();
|
|
@@ -109,7 +109,7 @@ export async function exportEvidence(args) {
|
|
|
109
109
|
if (!text)
|
|
110
110
|
return;
|
|
111
111
|
const read = JSON.parse(text);
|
|
112
|
-
const keys = new Set([...bundleKeysOf('check' in read ? read.check : {}), ...bundleKeysOf(read.run)]);
|
|
112
|
+
const keys = new Set([...bundleKeysOf('check' in read ? read.check : {}), ...bundleKeysOf(read.run), ...('parts' in read ? (read.parts ?? []).flatMap(bundleKeysOf) : [])]);
|
|
113
113
|
await eachLimited([...keys], 4, key => copy(`/layers/${encodeURIComponent(layer.id)}/blobs/${key.split('/').map(encodeURIComponent).join('/')}`).then(() => { }));
|
|
114
114
|
};
|
|
115
115
|
const base = `/evidence/detail/${encodeURIComponent(layer.backend)}/${encodeURIComponent(layer.id)}`;
|
|
@@ -51,7 +51,8 @@ export declare function reportBackends(deps: ReportsAppDeps): Backend[];
|
|
|
51
51
|
* runs of this machine. `cursor` is the previous page's; 409 when it no longer matches the listing (refresh).
|
|
52
52
|
* Any `retry` value re-lists backends that were unavailable now instead of after the wait.
|
|
53
53
|
* - `GET /evidence/rows/:backend/:id` the checks and spec fingerprints of one layer (`LayerRows`).
|
|
54
|
-
* - `GET /evidence/detail/:backend/:id?check=&kind=` the run-level evidence of an immutable layer,
|
|
54
|
+
* - `GET /evidence/detail/:backend/:id?check=&kind=` the run-level evidence of an immutable layer (and, as `parts`, what its
|
|
55
|
+
* parts carry as a whole), or one check's detail.
|
|
55
56
|
* - `GET /layers/:id/blobs/*` an attachment of an immutable layer by its bundle key, served by `attachmentResponse`.
|
|
56
57
|
* - `GET /layers/:id/*` the recorded `frame.css`, `tests/<test>/report.json` and `tests/<test>/files/<frame>` of a layer.
|
|
57
58
|
* - `GET /live/:source/frame.css` and `GET /live/:source/tests/:testId/*` the same for a run still writing (uncached).
|
|
@@ -19,7 +19,7 @@ import { createLiveEvidence, liveRunRecord } from './evidence-live.js';
|
|
|
19
19
|
import { isFramePath, isKeySegment } from './keys.js';
|
|
20
20
|
import { catalogEntries, catalogLayers } from './layer-catalog.js';
|
|
21
21
|
import { attachmentResponse } from './attachment-response.js';
|
|
22
|
-
import { checkDetailOf, runEvidence } from './evidence-detail.js';
|
|
22
|
+
import { checkDetailOf, partsEvidence, runEvidence } from './evidence-detail.js';
|
|
23
23
|
import { layerEvidence } from './layer-merge.js';
|
|
24
24
|
import { layerSummary, readLayer } from './layer-store.js';
|
|
25
25
|
import { readBytes, readUtf8 } from './object-io.js';
|
|
@@ -69,7 +69,8 @@ function archived(id) {
|
|
|
69
69
|
* runs of this machine. `cursor` is the previous page's; 409 when it no longer matches the listing (refresh).
|
|
70
70
|
* Any `retry` value re-lists backends that were unavailable now instead of after the wait.
|
|
71
71
|
* - `GET /evidence/rows/:backend/:id` the checks and spec fingerprints of one layer (`LayerRows`).
|
|
72
|
-
* - `GET /evidence/detail/:backend/:id?check=&kind=` the run-level evidence of an immutable layer,
|
|
72
|
+
* - `GET /evidence/detail/:backend/:id?check=&kind=` the run-level evidence of an immutable layer (and, as `parts`, what its
|
|
73
|
+
* parts carry as a whole), or one check's detail.
|
|
73
74
|
* - `GET /layers/:id/blobs/*` an attachment of an immutable layer by its bundle key, served by `attachmentResponse`.
|
|
74
75
|
* - `GET /layers/:id/*` the recorded `frame.css`, `tests/<test>/report.json` and `tests/<test>/files/<frame>` of a layer.
|
|
75
76
|
* - `GET /live/:source/frame.css` and `GET /live/:source/tests/:testId/*` the same for a run still writing (uncached).
|
|
@@ -286,8 +287,11 @@ export function createLayersApp(deps) {
|
|
|
286
287
|
return c.notFound();
|
|
287
288
|
const checkId = c.req.query('check');
|
|
288
289
|
const headers = { 'cache-control': 'public, max-age=31536000, immutable' };
|
|
289
|
-
if (checkId === undefined)
|
|
290
|
-
|
|
290
|
+
if (checkId === undefined) {
|
|
291
|
+
// The parts' own evidence is optional: a client that does not know `parts` still has the run's.
|
|
292
|
+
const parts = partsEvidence(layer);
|
|
293
|
+
return c.json({ layer: id, run: runEvidence(layer), ...(parts.length ? { parts } : {}) }, 200, headers);
|
|
294
|
+
}
|
|
291
295
|
const detail = checkDetailOf(layer, checkId, c.req.query('kind'));
|
|
292
296
|
return detail ? c.json(detail, 200, headers) : c.notFound();
|
|
293
297
|
});
|
|
@@ -270,6 +270,11 @@ export type RunSummary = {
|
|
|
270
270
|
* and references that cannot be played. Absent when it has none, and in a report that carries no video information.
|
|
271
271
|
*/
|
|
272
272
|
readonly videos?: VideoCounts;
|
|
273
|
+
/**
|
|
274
|
+
* Files attached to the run itself, in its layers (and their parts), not to any one check: review movies, archives,
|
|
275
|
+
* summaries. All kinds, videos included. Absent when none, and in a report that carries no such information.
|
|
276
|
+
*/
|
|
277
|
+
readonly attachments?: number;
|
|
273
278
|
/** The normalized tags of the attachments of its results and layers, sorted and distinct. Absent when none is tagged. */
|
|
274
279
|
readonly artifactTags?: readonly string[];
|
|
275
280
|
/** The tags of its checks and layers, which its attachments inherit (what `inherited=1` also matches), sorted and distinct. Absent when none. */
|
|
@@ -314,12 +319,33 @@ export type RunResult = {
|
|
|
314
319
|
/** Every video its evidence references, with who holds it (check, retry attempt or step). Absent when there is none. */
|
|
315
320
|
readonly videos?: readonly VideoRef[];
|
|
316
321
|
};
|
|
322
|
+
/**
|
|
323
|
+
* One layer of a run, as the run's artifact hub names it: who recorded it, how many checks it holds (zero is possible
|
|
324
|
+
* and is not a pass) and the files it carries as a whole. Read the files with `GET /evidence/detail/{backend}/{id}`.
|
|
325
|
+
*/
|
|
326
|
+
export type RunLayer = {
|
|
327
|
+
/** The layer's id: 64 hex digits for an archived layer. */
|
|
328
|
+
readonly id: string;
|
|
329
|
+
/** The backend it was read from, for `GET /evidence/detail/{backend}/{id}`. */
|
|
330
|
+
readonly backend: string;
|
|
331
|
+
readonly producer: string;
|
|
332
|
+
readonly environment: string | null;
|
|
333
|
+
readonly recordedAtMs: number;
|
|
334
|
+
/** How many checks the layer recorded. A layer of zero checks is evidence only: it is not a passing test. */
|
|
335
|
+
readonly checks: number;
|
|
336
|
+
/** Files the layer carries as a whole (not those of its checks). */
|
|
337
|
+
readonly attachments: number;
|
|
338
|
+
/** Its videos, when it has any. */
|
|
339
|
+
readonly videos?: VideoCounts;
|
|
340
|
+
};
|
|
317
341
|
/** `GET /api/reports/runs/detail`: one run and every result it recorded. */
|
|
318
342
|
export type RunDetail = {
|
|
319
343
|
readonly schemaVersion: typeof TEST_CATALOG_VERSION;
|
|
320
344
|
readonly version: string;
|
|
321
345
|
readonly run: RunSummary;
|
|
322
346
|
readonly results: readonly RunResult[];
|
|
347
|
+
/** Every layer of the run, oldest first, so its run-level files can be found even in a layer with no checks. Absent from an older server. */
|
|
348
|
+
readonly layers?: readonly RunLayer[];
|
|
323
349
|
};
|
|
324
350
|
/**
|
|
325
351
|
* One recorded result that carries evidence (an attachment: a recording, a screenshot, a trace), with where and when it
|
|
@@ -423,6 +449,7 @@ export declare class TestHistory {
|
|
|
423
449
|
run(key: string): {
|
|
424
450
|
readonly run: RunSummary;
|
|
425
451
|
readonly results: RunResult[];
|
|
452
|
+
readonly layers: RunLayer[];
|
|
426
453
|
} | null;
|
|
427
454
|
/**
|
|
428
455
|
* Every recorded result that carries an attachment, newest first, with the values a query looks at. Built once per
|
|
@@ -132,7 +132,7 @@ export class TestHistory {
|
|
|
132
132
|
const key = runKey(layer);
|
|
133
133
|
let run = this.#runs.get(key);
|
|
134
134
|
if (!run) {
|
|
135
|
-
run = { key, runUrl: layer.runUrl, branch: layer.branch, sha: layer.sha, start: layer.recordedAtMs, end: layer.recordedAtMs, firstAt: layer.recordedAtMs, first: layer.id, producers: new Set(), environments: new Set(), layers: 0, runId: layer.run?.id, title: undefined, titledAt: 0, titledBy: '', freshness: undefined, videos: [], evidence: { tags: new Set(), artifactTags: new Set(), roles: new Set(), labels: new Set() } };
|
|
135
|
+
run = { key, runUrl: layer.runUrl, branch: layer.branch, sha: layer.sha, start: layer.recordedAtMs, end: layer.recordedAtMs, firstAt: layer.recordedAtMs, first: layer.id, producers: new Set(), environments: new Set(), layers: 0, runId: layer.run?.id, title: undefined, titledAt: 0, titledBy: '', freshness: undefined, videos: [], attachments: 0, members: [], evidence: { tags: new Set(), artifactTags: new Set(), roles: new Set(), labels: new Set() } };
|
|
136
136
|
this.#runs.set(key, run);
|
|
137
137
|
}
|
|
138
138
|
// A run is told by its first layer (by time, then id), whatever order its layers arrive in.
|
|
@@ -161,6 +161,10 @@ export class TestHistory {
|
|
|
161
161
|
run.layers += 1;
|
|
162
162
|
if (layer.evidence?.videos)
|
|
163
163
|
run.videos.push(...layer.evidence.videos);
|
|
164
|
+
const held = layer.evidence?.attachments ?? 0;
|
|
165
|
+
run.attachments += held;
|
|
166
|
+
const layerVideos = videoCounts(layer.evidence?.videos);
|
|
167
|
+
run.members.push({ id: layer.id, backend, producer: layer.producer, environment: layer.environment, recordedAtMs: layer.recordedAtMs, checks: rows.length, attachments: held, ...(layerVideos ? { videos: layerVideos } : {}) });
|
|
164
168
|
if (layer.evidence)
|
|
165
169
|
takeEvidence(run.evidence, layer.evidence);
|
|
166
170
|
const origin = originOf(layer, backend);
|
|
@@ -253,7 +257,8 @@ export class TestHistory {
|
|
|
253
257
|
...(r.e?.videos ? { videos: r.e.videos } : {}),
|
|
254
258
|
}));
|
|
255
259
|
results.sort((a, b) => FAILURE_RANK[b.status] - FAILURE_RANK[a.status] || compareText(a.specPath ?? '', b.specPath ?? '') || compareText(a.title, b.title) || compareText(a.environment ?? '', b.environment ?? '') || compareText(a.producer, b.producer) || compareText(a.key, b.key));
|
|
256
|
-
|
|
260
|
+
const layers = [...run.members].sort((a, b) => a.recordedAtMs - b.recordedAtMs || compareText(a.id, b.id));
|
|
261
|
+
return { run: runSummary(run, cells), results, layers };
|
|
257
262
|
}
|
|
258
263
|
/**
|
|
259
264
|
* Every recorded result that carries an attachment, newest first, with the values a query looks at. Built once per
|
|
@@ -352,6 +357,7 @@ function runSummary(run, cells) {
|
|
|
352
357
|
producers: [...run.producers].sort(), environments: [...run.environments].sort(),
|
|
353
358
|
layers: run.layers, tests: tests.size, counts, totalDurationMs: measured ? total : null, failing,
|
|
354
359
|
...(video ? { videos: video } : {}),
|
|
360
|
+
...(run.attachments ? { attachments: run.attachments } : {}),
|
|
355
361
|
...(carried.artifactTags.size ? { artifactTags: [...carried.artifactTags].sort() } : {}),
|
|
356
362
|
...(carried.roles.size ? { types: typesOf(carried.roles, (video?.recordings ?? 0) > 0) } : {}),
|
|
357
363
|
...(carried.labels.size ? { labels: [...carried.labels].sort() } : {}),
|
package/dist/schema/src/api.d.ts
CHANGED
|
@@ -1317,6 +1317,11 @@ export declare const CheckDetailSchema: z.ZodObject<{
|
|
|
1317
1317
|
recordingExpectation: z.ZodOptional<z.ZodString>;
|
|
1318
1318
|
}, z.core.$loose>;
|
|
1319
1319
|
run: z.ZodObject<{}, z.core.$loose>;
|
|
1320
|
+
parts: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
1321
|
+
kind: z.ZodString;
|
|
1322
|
+
id: z.ZodString;
|
|
1323
|
+
title: z.ZodString;
|
|
1324
|
+
}, z.core.$loose>>>;
|
|
1320
1325
|
inheritedScope: z.ZodOptional<z.ZodObject<{
|
|
1321
1326
|
surface: z.ZodOptional<z.ZodString>;
|
|
1322
1327
|
deviceKind: z.ZodOptional<z.ZodString>;
|
|
@@ -2188,6 +2193,7 @@ export declare const ReportRunSchema: z.ZodObject<{
|
|
|
2188
2193
|
recordings: z.ZodNumber;
|
|
2189
2194
|
unavailable: z.ZodNumber;
|
|
2190
2195
|
}, z.core.$loose>>;
|
|
2196
|
+
attachments: z.ZodOptional<z.ZodNumber>;
|
|
2191
2197
|
artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
2192
2198
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
2193
2199
|
types: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -2274,6 +2280,7 @@ export declare const RunListSchema: z.ZodObject<{
|
|
|
2274
2280
|
recordings: z.ZodNumber;
|
|
2275
2281
|
unavailable: z.ZodNumber;
|
|
2276
2282
|
}, z.core.$loose>>;
|
|
2283
|
+
attachments: z.ZodOptional<z.ZodNumber>;
|
|
2277
2284
|
artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
2278
2285
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
2279
2286
|
types: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -2514,6 +2521,7 @@ export declare const RunDetailSchema: z.ZodObject<{
|
|
|
2514
2521
|
recordings: z.ZodNumber;
|
|
2515
2522
|
unavailable: z.ZodNumber;
|
|
2516
2523
|
}, z.core.$loose>>;
|
|
2524
|
+
attachments: z.ZodOptional<z.ZodNumber>;
|
|
2517
2525
|
artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
2518
2526
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
2519
2527
|
types: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -2565,4 +2573,18 @@ export declare const RunDetailSchema: z.ZodObject<{
|
|
|
2565
2573
|
}, z.core.$loose>>;
|
|
2566
2574
|
}, z.core.$loose>>>;
|
|
2567
2575
|
}, z.core.$loose>>;
|
|
2576
|
+
layers: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
2577
|
+
id: z.ZodString;
|
|
2578
|
+
backend: z.ZodString;
|
|
2579
|
+
producer: z.ZodString;
|
|
2580
|
+
environment: z.ZodNullable<z.ZodString>;
|
|
2581
|
+
recordedAtMs: z.ZodNumber;
|
|
2582
|
+
checks: z.ZodNumber;
|
|
2583
|
+
attachments: z.ZodNumber;
|
|
2584
|
+
videos: z.ZodOptional<z.ZodObject<{
|
|
2585
|
+
references: z.ZodNumber;
|
|
2586
|
+
recordings: z.ZodNumber;
|
|
2587
|
+
unavailable: z.ZodNumber;
|
|
2588
|
+
}, z.core.$loose>>;
|
|
2589
|
+
}, z.core.$loose>>>;
|
|
2568
2590
|
}, z.core.$loose>;
|
package/dist/schema/src/api.js
CHANGED
|
@@ -191,6 +191,7 @@ export const CheckDetailSchema = z.looseObject({
|
|
|
191
191
|
kind: z.string(),
|
|
192
192
|
check: CheckSchema,
|
|
193
193
|
run: z.looseObject({}).describe('What the run as a whole carries: its logs, links, labels, annotations, scope and extensions'),
|
|
194
|
+
parts: z.array(z.looseObject({ kind: z.string(), id: z.string(), title: z.string() })).optional().describe('Without `check` only: the parts of the layer that carry files or links of their own, each with what it carries. Absent when none'),
|
|
194
195
|
inheritedScope: EvidenceScopeSchema.optional().describe('The scope the check inherits: its part\'s declaration, else its layer\'s. Absent when neither declared one: unknown, not live or physical')
|
|
195
196
|
});
|
|
196
197
|
/** `GET /api/store?prefix=…`: one page of keys under `layers/`, in code-unit order. */
|
|
@@ -401,6 +402,7 @@ export const ReportRunSchema = z.looseObject({
|
|
|
401
402
|
totalDurationMs: z.number().nullable(),
|
|
402
403
|
failing: z.array(z.looseObject({ key: z.string(), title: z.string(), environment: z.string().nullable(), detail: z.string() })),
|
|
403
404
|
videos: VideoCountsSchema.optional().describe('Its videos, from its layers and the last result of each test. Absent when it has none or the report carries no video information'),
|
|
405
|
+
attachments: z.number().int().positive().optional().describe('Files attached to the run itself, in its layers, not to a check. Absent when there are none'),
|
|
404
406
|
artifactTags: z.array(z.string()).optional().describe('The normalized tags of the attachments of its results, sorted and distinct. Absent when none is tagged, and in reports of older servers'),
|
|
405
407
|
tags: z.array(z.string()).optional().describe('The tags of its checks and layers, which its attachments inherit (what `inherited=1` also matches). Absent when none'),
|
|
406
408
|
types: z.array(z.string()).optional().describe('The media types (roles) of its attachments, sorted and distinct; `video` only for a playable recording. Absent when it has no attachment'),
|
|
@@ -457,5 +459,12 @@ export const RunDetailSchema = z.looseObject({
|
|
|
457
459
|
key: z.string(), id: z.string(), title: z.string(), specPath: z.string().nullable(), environment: z.string().nullable(), producer: z.string(),
|
|
458
460
|
status: EvidenceStatusSchema, durationMs: z.number().nullable(), at: z.number(), layer: z.string(), detail: z.string(),
|
|
459
461
|
videos: z.array(VideoRefSchema).optional().describe('Every video its evidence references, with who holds it')
|
|
460
|
-
}))
|
|
462
|
+
})),
|
|
463
|
+
layers: z.array(z.looseObject({
|
|
464
|
+
id: z.string().describe('The layer id; read its files with `/api/reports/evidence/detail/{backend}/{id}`'),
|
|
465
|
+
backend: z.string(), producer: z.string(), environment: z.string().nullable(), recordedAtMs: z.number(),
|
|
466
|
+
checks: z.number().int().nonnegative().describe('How many checks the layer recorded. Zero means evidence only, not a pass'),
|
|
467
|
+
attachments: z.number().int().nonnegative().describe('Files the layer carries as a whole, not those of its checks'),
|
|
468
|
+
videos: VideoCountsSchema.optional()
|
|
469
|
+
})).optional().describe('Every layer of the run, oldest first. Absent from an older server')
|
|
461
470
|
});
|
|
@@ -393,7 +393,7 @@ const BASE_OPERATIONS = [
|
|
|
393
393
|
{
|
|
394
394
|
route: 'GET /api/reports/runs/detail', path: '/api/reports/runs/detail', operationId: 'getReportRun', tag: 'Report', role: 'read',
|
|
395
395
|
summary: 'Every test one run recorded',
|
|
396
|
-
description: 'One run and its last result for every test, environment and producer it recorded, failures first. Send `format=markdown` for Markdown.',
|
|
396
|
+
description: 'One run and its last result for every test, environment and producer it recorded, failures first, and `layers`: each layer of the run with its backend, how many checks it recorded (zero means evidence only, never a pass) and how many files it carries as a whole. Open a layer\'s files with `/api/reports/evidence/detail/{backend}/{id}`. Send `format=markdown` for Markdown.',
|
|
397
397
|
parameters: [
|
|
398
398
|
{ name: 'key', in: 'query', required: true, description: 'The run\'s `key` from `/api/reports/runs`, URL-encoded.', schema: str },
|
|
399
399
|
{ name: 'format', in: 'query', description: '`markdown` for Markdown instead of JSON.', schema: { type: 'string', enum: ['json', 'markdown'] } }
|
|
@@ -430,7 +430,7 @@ const BASE_OPERATIONS = [
|
|
|
430
430
|
{
|
|
431
431
|
route: 'GET /api/reports/evidence/detail/:backend/:id', path: '/api/reports/evidence/detail/{backend}/{id}', operationId: 'getCheckDetail', tag: 'Report', role: 'read',
|
|
432
432
|
summary: 'Everything one result carries',
|
|
433
|
-
description: 'The timeline, annotations, retries, attachments and extensions of one check, and what its run carries as a whole. Without `check`, only the run\'s. Attachment bytes are fetched from `/api/reports/layers/{id}/blobs/{bundleKey}`.',
|
|
433
|
+
description: 'The timeline, annotations, retries, attachments and extensions of one check, and what its run carries as a whole. Without `check`, only the run\'s, and `parts`: what each part of the layer that carries files or links of its own holds. Attachment bytes are fetched from `/api/reports/layers/{id}/blobs/{bundleKey}`.',
|
|
434
434
|
parameters: [BACKEND, LAYER_ID, { name: 'check', in: 'query', description: 'The check id.', schema: str }, { name: 'kind', in: 'query', description: 'The kind of the part, when the same id is in several.', schema: str }],
|
|
435
435
|
responses: { '200': json('The detail.', 'CheckDetail'), '401': UNAUTHORIZED, '404': problem('No such layer or check.') }
|
|
436
436
|
},
|
|
@@ -756,7 +756,7 @@ Responses: `200` The matching results and the facets.; `304` Unchanged since the
|
|
|
756
756
|
|
|
757
757
|
`GET /api/reports/runs/detail` · role: read
|
|
758
758
|
|
|
759
|
-
One run and its last result for every test, environment and producer it recorded, failures first. Send `format=markdown` for Markdown.
|
|
759
|
+
One run and its last result for every test, environment and producer it recorded, failures first, and `layers`: each layer of the run with its backend, how many checks it recorded (zero means evidence only, never a pass) and how many files it carries as a whole. Open a layer's files with `/api/reports/evidence/detail/{backend}/{id}`. Send `format=markdown` for Markdown.
|
|
760
760
|
|
|
761
761
|
- `key` (query, required): The run's `key` from `/api/reports/runs`, URL-encoded.
|
|
762
762
|
- `format` (query): `markdown` for Markdown instead of JSON.
|
|
@@ -768,7 +768,7 @@ Responses: `200` The run and its results.; `304` Unchanged since the `If-None-Ma
|
|
|
768
768
|
|
|
769
769
|
`GET /api/projects/{project}/reports/runs/detail` · role: read
|
|
770
770
|
|
|
771
|
-
One run and its last result for every test, environment and producer it recorded, failures first. Send `format=markdown` for Markdown.
|
|
771
|
+
One run and its last result for every test, environment and producer it recorded, failures first, and `layers`: each layer of the run with its backend, how many checks it recorded (zero means evidence only, never a pass) and how many files it carries as a whole. Open a layer's files with `/api/reports/evidence/detail/{backend}/{id}`. Send `format=markdown` for Markdown.
|
|
772
772
|
|
|
773
773
|
Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
|
|
774
774
|
|
|
@@ -878,7 +878,7 @@ Responses: `200` The rows.; `401` No token, or an unknown one. Send `Authorizati
|
|
|
878
878
|
|
|
879
879
|
`GET /api/reports/evidence/detail/{backend}/{id}` · role: read
|
|
880
880
|
|
|
881
|
-
The timeline, annotations, retries, attachments and extensions of one check, and what its run carries as a whole. Without `check`, only the run's. Attachment bytes are fetched from `/api/reports/layers/{id}/blobs/{bundleKey}`.
|
|
881
|
+
The timeline, annotations, retries, attachments and extensions of one check, and what its run carries as a whole. Without `check`, only the run's, and `parts`: what each part of the layer that carries files or links of its own holds. Attachment bytes are fetched from `/api/reports/layers/{id}/blobs/{bundleKey}`.
|
|
882
882
|
|
|
883
883
|
- `backend` (path, required): A backend id from `/api/server`.
|
|
884
884
|
- `id` (path, required): A layer id: 64 lowercase hex digits.
|
|
@@ -892,7 +892,7 @@ Responses: `200` The detail.; `401` No token, or an unknown one. Send `Authoriza
|
|
|
892
892
|
|
|
893
893
|
`GET /api/projects/{project}/reports/evidence/detail/{backend}/{id}` · role: read
|
|
894
894
|
|
|
895
|
-
The timeline, annotations, retries, attachments and extensions of one check, and what its run carries as a whole. Without `check`, only the run's. Attachment bytes are fetched from `/api/reports/layers/{id}/blobs/{bundleKey}`.
|
|
895
|
+
The timeline, annotations, retries, attachments and extensions of one check, and what its run carries as a whole. Without `check`, only the run's, and `parts`: what each part of the layer that carries files or links of its own holds. Attachment bytes are fetched from `/api/reports/layers/{id}/blobs/{bundleKey}`.
|
|
896
896
|
|
|
897
897
|
Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
|
|
898
898
|
|
|
@@ -1713,6 +1713,16 @@ Whether an attachment is a video, by its effective media type: the producer's `c
|
|
|
1713
1713
|
name implies. A role never decides this on its own (`demo` on a PNG is not a video), except that a `video` role
|
|
1714
1714
|
settles a file whose type nothing says (`application/octet-stream`).
|
|
1715
1715
|
|
|
1716
|
+
#### `attachmentIdentity`
|
|
1717
|
+
|
|
1718
|
+
```ts
|
|
1719
|
+
export function attachmentIdentity(item: Pick<Attachment, 'name' | 'sha256' | 'bundleKey' | 'uri' | 'text'>): string | undefined
|
|
1720
|
+
```
|
|
1721
|
+
|
|
1722
|
+
What identifies an attachment's bytes: its digest, else its bundle key, address or inline text. Attachments with the
|
|
1723
|
+
same identity are the same file however many results reference them. Undefined when it names no bytes at all (it can
|
|
1724
|
+
never be opened). This is the `recording` of a {@link VideoRef}.
|
|
1725
|
+
|
|
1716
1726
|
#### `videoCounts`
|
|
1717
1727
|
|
|
1718
1728
|
```ts
|
|
@@ -1765,6 +1775,27 @@ export function runEvidence(layer: EvidenceHolder): EvidenceHolder
|
|
|
1765
1775
|
|
|
1766
1776
|
What a layer carries as a whole (its scope, tags, labels, links, attachments, annotations and extensions; not its parts' checks). Absent fields stay absent.
|
|
1767
1777
|
|
|
1778
|
+
#### `PartEvidence`
|
|
1779
|
+
|
|
1780
|
+
```ts
|
|
1781
|
+
export type PartEvidence = EvidenceHolder & {
|
|
1782
|
+
readonly kind: string;
|
|
1783
|
+
readonly id: string;
|
|
1784
|
+
readonly title: string;
|
|
1785
|
+
};
|
|
1786
|
+
```
|
|
1787
|
+
|
|
1788
|
+
What one part of a layer (one kind of report) carries as a whole, named by the part.
|
|
1789
|
+
|
|
1790
|
+
#### `partsEvidence`
|
|
1791
|
+
|
|
1792
|
+
```ts
|
|
1793
|
+
export function partsEvidence(layer: { readonly parts: readonly (EvidenceHolder & { readonly kind: string; readonly id: string; readonly title: string })[] }): PartEvidence[]
|
|
1794
|
+
```
|
|
1795
|
+
|
|
1796
|
+
The parts of a layer that carry evidence of their own (attachments, links, labels…), each with its name. Parts
|
|
1797
|
+
that carry none are left out. A part's files belong to the run, not to any one check.
|
|
1798
|
+
|
|
1768
1799
|
#### `bundleKeysOf`
|
|
1769
1800
|
|
|
1770
1801
|
```ts
|
|
@@ -4639,7 +4670,8 @@ Routes, relative to where it is mounted:
|
|
|
4639
4670
|
runs of this machine. `cursor` is the previous page's; 409 when it no longer matches the listing (refresh).
|
|
4640
4671
|
Any `retry` value re-lists backends that were unavailable now instead of after the wait.
|
|
4641
4672
|
- `GET /evidence/rows/:backend/:id` the checks and spec fingerprints of one layer (`LayerRows`).
|
|
4642
|
-
- `GET /evidence/detail/:backend/:id?check=&kind=` the run-level evidence of an immutable layer,
|
|
4673
|
+
- `GET /evidence/detail/:backend/:id?check=&kind=` the run-level evidence of an immutable layer (and, as `parts`, what its
|
|
4674
|
+
parts carry as a whole), or one check's detail.
|
|
4643
4675
|
- `GET /layers/:id/blobs/*` an attachment of an immutable layer by its bundle key, served by `attachmentResponse`.
|
|
4644
4676
|
- `GET /layers/:id/*` the recorded `frame.css`, `tests/<test>/report.json` and `tests/<test>/files/<frame>` of a layer.
|
|
4645
4677
|
- `GET /live/:source/frame.css` and `GET /live/:source/tests/:testId/*` the same for a run still writing (uncached).
|
|
@@ -8601,14 +8633,14 @@ export type RunSummary = {
|
|
|
8601
8633
|
* and references that cannot be played. Absent when it has none, and in a report that carries no video information.
|
|
8602
8634
|
*/
|
|
8603
8635
|
readonly videos?: VideoCounts;
|
|
8636
|
+
/**
|
|
8637
|
+
* Files attached to the run itself, in its layers (and their parts), not to any one check: review movies, archives,
|
|
8638
|
+
* summaries. All kinds, videos included. Absent when none, and in a report that carries no such information.
|
|
8639
|
+
*/
|
|
8640
|
+
readonly attachments?: number;
|
|
8604
8641
|
/** The normalized tags of the attachments of its results and layers, sorted and distinct. Absent when none is tagged. */
|
|
8605
8642
|
readonly artifactTags?: readonly string[];
|
|
8606
8643
|
/** The tags of its checks and layers, which its attachments inherit (what `inherited=1` also matches), sorted and distinct. Absent when none. */
|
|
8607
|
-
readonly tags?: readonly string[];
|
|
8608
|
-
/** The media types (roles) of its attachments, sorted and distinct; `video` only for a playable recording. Absent when it has no attachment. */
|
|
8609
|
-
readonly types?: readonly string[];
|
|
8610
|
-
/** The `key=value` labels of its results and layers, sorted and distinct. Absent when none. */
|
|
8611
|
-
readonly labels?: readonly string[];
|
|
8612
8644
|
…
|
|
8613
8645
|
```
|
|
8614
8646
|
|
|
@@ -8666,6 +8698,29 @@ export type RunResult = {
|
|
|
8666
8698
|
|
|
8667
8699
|
One test's last result within one run.
|
|
8668
8700
|
|
|
8701
|
+
#### `RunLayer`
|
|
8702
|
+
|
|
8703
|
+
```ts
|
|
8704
|
+
export type RunLayer = {
|
|
8705
|
+
/** The layer's id: 64 hex digits for an archived layer. */
|
|
8706
|
+
readonly id: string;
|
|
8707
|
+
/** The backend it was read from, for `GET /evidence/detail/{backend}/{id}`. */
|
|
8708
|
+
readonly backend: string;
|
|
8709
|
+
readonly producer: string;
|
|
8710
|
+
readonly environment: string | null;
|
|
8711
|
+
readonly recordedAtMs: number;
|
|
8712
|
+
/** How many checks the layer recorded. A layer of zero checks is evidence only: it is not a passing test. */
|
|
8713
|
+
readonly checks: number;
|
|
8714
|
+
/** Files the layer carries as a whole (not those of its checks). */
|
|
8715
|
+
readonly attachments: number;
|
|
8716
|
+
/** Its videos, when it has any. */
|
|
8717
|
+
readonly videos?: VideoCounts;
|
|
8718
|
+
};
|
|
8719
|
+
```
|
|
8720
|
+
|
|
8721
|
+
One layer of a run, as the run's artifact hub names it: who recorded it, how many checks it holds (zero is possible
|
|
8722
|
+
and is not a pass) and the files it carries as a whole. Read the files with `GET /evidence/detail/{backend}/{id}`.
|
|
8723
|
+
|
|
8669
8724
|
#### `RunDetail`
|
|
8670
8725
|
|
|
8671
8726
|
```ts
|
|
@@ -8674,6 +8729,8 @@ export type RunDetail = {
|
|
|
8674
8729
|
readonly version: string;
|
|
8675
8730
|
readonly run: RunSummary;
|
|
8676
8731
|
readonly results: readonly RunResult[];
|
|
8732
|
+
/** Every layer of the run, oldest first, so its run-level files can be found even in a layer with no checks. Absent from an older server. */
|
|
8733
|
+
readonly layers?: readonly RunLayer[];
|
|
8677
8734
|
};
|
|
8678
8735
|
```
|
|
8679
8736
|
|
|
@@ -8779,7 +8836,7 @@ const catalog = testCatalog(catalogModel(layers, { history }));
|
|
|
8779
8836
|
- `of(key: string): readonly TestResultEntry[]` — The recorded results of one test (by `TestSummary.key`), newest first. Empty when none.
|
|
8780
8837
|
- `get runCount(): number { return this.#runs.size; }` — How many runs it knows of.
|
|
8781
8838
|
- `runs(): RunSummary[]` — Every run, newest first.
|
|
8782
|
-
- `run(key: string): { readonly run: RunSummary; readonly results: RunResult[] } | null` — One run and its results (failed first, then by spec and title), or null.
|
|
8839
|
+
- `run(key: string): { readonly run: RunSummary; readonly results: RunResult[]; readonly layers: RunLayer[] } | null` — One run and its results (failed first, then by spec and title), or null.
|
|
8783
8840
|
- `evidenceResults(): readonly { readonly result: ArtifactResult; readonly fields: FilterFields }[]` — Every recorded result that carries an attachment, newest first, with the values a query looks at. Built once per layer added. The domain is left to the caller (`artifactList`), which may know more specs than the layers recorded.
|
|
8784
8841
|
- `testsIn(key: string): ReadonlySet<string>` — The keys of the tests one run recorded.
|
|
8785
8842
|
|
package/docs/rich-evidence.md
CHANGED
|
@@ -303,6 +303,29 @@ or the command line, an attachment may name a `path` instead; `atlas report publ
|
|
|
303
303
|
`atlas report publish --ctrf` keeps it ([native runner reporters](runners.md) cover CodeceptJS, Mocha, Jest and Node's
|
|
304
304
|
test runner).
|
|
305
305
|
|
|
306
|
+
## Run artifacts and direct links
|
|
307
|
+
|
|
308
|
+
Files attached to a run as a whole (top-level `attachments`: a review movie, a ZIP, an HTML gallery, a summary) are
|
|
309
|
+
shown under **Artifacts** when you open a run in the viewer's Runs tab, without opening any test. The movie plays
|
|
310
|
+
inline, an archive is a download, and each file keeps its name, role, type, size, duration and tags. They are grouped
|
|
311
|
+
by the layer that carries them, with who recorded it and when. A layer may hold no checks at all, only files: it is
|
|
312
|
+
listed with `No checks in this layer`, and a run of only such layers shows `No checks recorded`, never `0 passed`.
|
|
313
|
+
|
|
314
|
+
- **Direct link.** *Link to this file* gives an address that names the project, the run, one immutable layer and one
|
|
315
|
+
attachment: `#/?tab=runs&openRun=<run key>&layer=<layer id>&artifact=<id:… | key:… | name:…>`. Opened in a fresh tab
|
|
316
|
+
it selects that same file of that same layer, whatever runs arrived since. A link whose file is not in the layer says
|
|
317
|
+
so instead of showing another file.
|
|
318
|
+
- **Shared recordings.** Tests that reference the same bytes (same SHA-256 or bundle key) are one recording. The run's
|
|
319
|
+
*Recordings in tests* lists each once, with every test that references it, and a run-level file that a test also
|
|
320
|
+
references says so.
|
|
321
|
+
- **Unplayable files.** A file with no stored bytes, with an address that is not a web address, in a type the viewer
|
|
322
|
+
has no preview for, or that the browser cannot play, shows what is wrong and, when it can, a download.
|
|
323
|
+
- **Safety is unchanged.** Files are served only through the attachment route (never sniffed, sandboxed), and an HTML
|
|
324
|
+
archive renders in an iframe with an empty `sandbox`: it cannot run script.
|
|
325
|
+
- **API.** `GET /api/reports/runs/detail` lists the run's `layers` (backend, check count, file count) and
|
|
326
|
+
`GET /api/reports/evidence/detail/{backend}/{id}` returns what a layer carries as a whole (and, as `parts`, what its
|
|
327
|
+
parts carry). Both only gained optional fields.
|
|
328
|
+
|
|
306
329
|
## Recording completeness
|
|
307
330
|
|
|
308
331
|
A green run does not say whether the videos you expected were kept and published. Atlas answers that separately from
|
package/docs/ui.md
CHANGED
|
@@ -125,7 +125,7 @@ The narrowly scoped exceptions are:
|
|
|
125
125
|
| Area | Screens/interactions | Mantine composition |
|
|
126
126
|
| --- | --- | --- |
|
|
127
127
|
| Shared shell | Project switching, spec navigation, run selection, connection status, diagnostics, mobile index | AppShell, Burger, NavLink, Menu, Popover, NativeSelect, Drawer |
|
|
128
|
-
| Project home | Tests/runs/artifacts tabs, metrics, search, status/environment/domain/artifact-tag filters, run and artifact facets with saved views, grouping, sorting, show more | Container, Tabs, SimpleGrid, Paper, TextInput, MultiSelect, Popover, Button, Accordion |
|
|
128
|
+
| Project home | Tests/runs/artifacts tabs, a run's Artifacts (files of its layers, shared recordings, direct links), metrics, search, status/environment/domain/artifact-tag filters, run and artifact facets with saved views, grouping, sorting, show more | Container, Tabs, SimpleGrid, Paper, TextInput, MultiSelect, Popover, Button, Accordion |
|
|
129
129
|
| Test detail | Current outcomes, statistics, duration graph, history, artifacts, recording; the `layer` address parameter pins it to one run's result, labelled "This run" against "Latest combined evidence" | Drawer, Table, Badge, Alert, Stage |
|
|
130
130
|
| Specs | All six existing kinds, overview/index, Gherkin rules/scenarios/examples, Quint declarations/traces, DMN tables, source text | SpecHeader, Accordion, Table, CodeHighlightTabs, ResultRow |
|
|
131
131
|
| Statecharts | Graph, screen/event/journey details, developer metadata, chart test results | Mantine cards/navigation and product compositions; graph exception above |
|
package/llms-full.txt
CHANGED
|
@@ -1290,6 +1290,29 @@ or the command line, an attachment may name a `path` instead; `atlas report publ
|
|
|
1290
1290
|
`atlas report publish --ctrf` keeps it ([native runner reporters](runners.md) cover CodeceptJS, Mocha, Jest and Node's
|
|
1291
1291
|
test runner).
|
|
1292
1292
|
|
|
1293
|
+
## Run artifacts and direct links
|
|
1294
|
+
|
|
1295
|
+
Files attached to a run as a whole (top-level `attachments`: a review movie, a ZIP, an HTML gallery, a summary) are
|
|
1296
|
+
shown under **Artifacts** when you open a run in the viewer's Runs tab, without opening any test. The movie plays
|
|
1297
|
+
inline, an archive is a download, and each file keeps its name, role, type, size, duration and tags. They are grouped
|
|
1298
|
+
by the layer that carries them, with who recorded it and when. A layer may hold no checks at all, only files: it is
|
|
1299
|
+
listed with `No checks in this layer`, and a run of only such layers shows `No checks recorded`, never `0 passed`.
|
|
1300
|
+
|
|
1301
|
+
- **Direct link.** *Link to this file* gives an address that names the project, the run, one immutable layer and one
|
|
1302
|
+
attachment: `#/?tab=runs&openRun=<run key>&layer=<layer id>&artifact=<id:… | key:… | name:…>`. Opened in a fresh tab
|
|
1303
|
+
it selects that same file of that same layer, whatever runs arrived since. A link whose file is not in the layer says
|
|
1304
|
+
so instead of showing another file.
|
|
1305
|
+
- **Shared recordings.** Tests that reference the same bytes (same SHA-256 or bundle key) are one recording. The run's
|
|
1306
|
+
*Recordings in tests* lists each once, with every test that references it, and a run-level file that a test also
|
|
1307
|
+
references says so.
|
|
1308
|
+
- **Unplayable files.** A file with no stored bytes, with an address that is not a web address, in a type the viewer
|
|
1309
|
+
has no preview for, or that the browser cannot play, shows what is wrong and, when it can, a download.
|
|
1310
|
+
- **Safety is unchanged.** Files are served only through the attachment route (never sniffed, sandboxed), and an HTML
|
|
1311
|
+
archive renders in an iframe with an empty `sandbox`: it cannot run script.
|
|
1312
|
+
- **API.** `GET /api/reports/runs/detail` lists the run's `layers` (backend, check count, file count) and
|
|
1313
|
+
`GET /api/reports/evidence/detail/{backend}/{id}` returns what a layer carries as a whole (and, as `parts`, what its
|
|
1314
|
+
parts carry). Both only gained optional fields.
|
|
1315
|
+
|
|
1293
1316
|
## Recording completeness
|
|
1294
1317
|
|
|
1295
1318
|
A green run does not say whether the videos you expected were kept and published. Atlas answers that separately from
|
|
@@ -3934,7 +3957,7 @@ The narrowly scoped exceptions are:
|
|
|
3934
3957
|
| Area | Screens/interactions | Mantine composition |
|
|
3935
3958
|
| --- | --- | --- |
|
|
3936
3959
|
| Shared shell | Project switching, spec navigation, run selection, connection status, diagnostics, mobile index | AppShell, Burger, NavLink, Menu, Popover, NativeSelect, Drawer |
|
|
3937
|
-
| Project home | Tests/runs/artifacts tabs, metrics, search, status/environment/domain/artifact-tag filters, run and artifact facets with saved views, grouping, sorting, show more | Container, Tabs, SimpleGrid, Paper, TextInput, MultiSelect, Popover, Button, Accordion |
|
|
3960
|
+
| Project home | Tests/runs/artifacts tabs, a run's Artifacts (files of its layers, shared recordings, direct links), metrics, search, status/environment/domain/artifact-tag filters, run and artifact facets with saved views, grouping, sorting, show more | Container, Tabs, SimpleGrid, Paper, TextInput, MultiSelect, Popover, Button, Accordion |
|
|
3938
3961
|
| Test detail | Current outcomes, statistics, duration graph, history, artifacts, recording; the `layer` address parameter pins it to one run's result, labelled "This run" against "Latest combined evidence" | Drawer, Table, Badge, Alert, Stage |
|
|
3939
3962
|
| Specs | All six existing kinds, overview/index, Gherkin rules/scenarios/examples, Quint declarations/traces, DMN tables, source text | SpecHeader, Accordion, Table, CodeHighlightTabs, ResultRow |
|
|
3940
3963
|
| Statecharts | Graph, screen/event/journey details, developer metadata, chart test results | Mantine cards/navigation and product compositions; graph exception above |
|
|
@@ -5230,7 +5253,7 @@ Responses: `200` The matching results and the facets.; `304` Unchanged since the
|
|
|
5230
5253
|
|
|
5231
5254
|
`GET /api/reports/runs/detail` · role: read
|
|
5232
5255
|
|
|
5233
|
-
One run and its last result for every test, environment and producer it recorded, failures first. Send `format=markdown` for Markdown.
|
|
5256
|
+
One run and its last result for every test, environment and producer it recorded, failures first, and `layers`: each layer of the run with its backend, how many checks it recorded (zero means evidence only, never a pass) and how many files it carries as a whole. Open a layer's files with `/api/reports/evidence/detail/{backend}/{id}`. Send `format=markdown` for Markdown.
|
|
5234
5257
|
|
|
5235
5258
|
- `key` (query, required): The run's `key` from `/api/reports/runs`, URL-encoded.
|
|
5236
5259
|
- `format` (query): `markdown` for Markdown instead of JSON.
|
|
@@ -5242,7 +5265,7 @@ Responses: `200` The run and its results.; `304` Unchanged since the `If-None-Ma
|
|
|
5242
5265
|
|
|
5243
5266
|
`GET /api/projects/{project}/reports/runs/detail` · role: read
|
|
5244
5267
|
|
|
5245
|
-
One run and its last result for every test, environment and producer it recorded, failures first. Send `format=markdown` for Markdown.
|
|
5268
|
+
One run and its last result for every test, environment and producer it recorded, failures first, and `layers`: each layer of the run with its backend, how many checks it recorded (zero means evidence only, never a pass) and how many files it carries as a whole. Open a layer's files with `/api/reports/evidence/detail/{backend}/{id}`. Send `format=markdown` for Markdown.
|
|
5246
5269
|
|
|
5247
5270
|
Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
|
|
5248
5271
|
|
|
@@ -5352,7 +5375,7 @@ Responses: `200` The rows.; `401` No token, or an unknown one. Send `Authorizati
|
|
|
5352
5375
|
|
|
5353
5376
|
`GET /api/reports/evidence/detail/{backend}/{id}` · role: read
|
|
5354
5377
|
|
|
5355
|
-
The timeline, annotations, retries, attachments and extensions of one check, and what its run carries as a whole. Without `check`, only the run's. Attachment bytes are fetched from `/api/reports/layers/{id}/blobs/{bundleKey}`.
|
|
5378
|
+
The timeline, annotations, retries, attachments and extensions of one check, and what its run carries as a whole. Without `check`, only the run's, and `parts`: what each part of the layer that carries files or links of its own holds. Attachment bytes are fetched from `/api/reports/layers/{id}/blobs/{bundleKey}`.
|
|
5356
5379
|
|
|
5357
5380
|
- `backend` (path, required): A backend id from `/api/server`.
|
|
5358
5381
|
- `id` (path, required): A layer id: 64 lowercase hex digits.
|
|
@@ -5366,7 +5389,7 @@ Responses: `200` The detail.; `401` No token, or an unknown one. Send `Authoriza
|
|
|
5366
5389
|
|
|
5367
5390
|
`GET /api/projects/{project}/reports/evidence/detail/{backend}/{id}` · role: read
|
|
5368
5391
|
|
|
5369
|
-
The timeline, annotations, retries, attachments and extensions of one check, and what its run carries as a whole. Without `check`, only the run's. Attachment bytes are fetched from `/api/reports/layers/{id}/blobs/{bundleKey}`.
|
|
5392
|
+
The timeline, annotations, retries, attachments and extensions of one check, and what its run carries as a whole. Without `check`, only the run's, and `parts`: what each part of the layer that carries files or links of its own holds. Attachment bytes are fetched from `/api/reports/layers/{id}/blobs/{bundleKey}`.
|
|
5370
5393
|
|
|
5371
5394
|
Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
|
|
5372
5395
|
|