@crvouga/atlas 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/reports/src/artifact-query.d.ts +123 -0
- package/dist/reports/src/artifact-query.js +234 -0
- package/dist/reports/src/evidence-detail.d.ts +14 -3
- package/dist/reports/src/evidence-detail.js +34 -19
- package/dist/reports/src/evidence-scope.d.ts +83 -0
- package/dist/reports/src/evidence-scope.js +129 -0
- package/dist/reports/src/evidence-types.d.ts +5 -1
- package/dist/reports/src/explorer-types.d.ts +1 -1
- package/dist/reports/src/layer-merge.js +3 -2
- package/dist/reports/src/layer-types.d.ts +1 -1
- package/dist/reports/src/publish-checks.js +1 -0
- package/dist/reports/src/reporter.d.ts +1 -1
- package/dist/reports/src/reports-explorer-route.d.ts +276 -0
- package/dist/reports/src/reports-route.d.ts +276 -0
- package/dist/reports/src/test-catalog.d.ts +108 -6
- package/dist/reports/src/test-catalog.js +155 -8
- package/dist/schema/src/api.d.ts +745 -0
- package/dist/schema/src/api.js +48 -6
- package/dist/schema/src/layer.d.ts +981 -0
- package/dist/schema/src/layer.js +43 -4
- package/dist/server/src/app.js +16 -5
- package/dist/server/src/openapi.js +42 -3
- package/docs/extending.md +1 -0
- package/docs/reference/http-api.md +86 -2
- package/docs/reference/reports.md +550 -14
- package/docs/reference/schema.md +55 -0
- package/docs/rich-evidence.md +78 -0
- package/docs/server.md +4 -1
- package/docs/ui.md +1 -1
- package/llms-full.txt +170 -4
- package/package.json +1 -1
- package/packages/schema/schemas/check-detail.schema.json +673 -1
- package/packages/schema/schemas/effective-delta.schema.json +206 -0
- package/packages/schema/schemas/effective-evidence.schema.json +206 -0
- package/packages/schema/schemas/evidence-page.schema.json +103 -0
- package/packages/schema/schemas/layer-rows.schema.json +103 -0
- package/packages/schema/schemas/layer.schema.json +1598 -62
- package/packages/schema/schemas/publish-checks.schema.json +768 -0
- package/packages/schema/schemas/report-run.schema.json +131 -0
- package/packages/schema/schemas/report-runs.schema.json +221 -0
- package/packages/schema/schemas/test-catalog.schema.json +135 -0
- package/packages/schema/schemas/test-history.schema.json +216 -0
- package/packages/server/openapi.json +4731 -735
- package/packages/visualizer/src/evidence/artifacts.tsx +24 -18
- package/packages/visualizer/src/evidence/evidence-scope.tsx +106 -0
- package/packages/visualizer/src/evidence/format.ts +11 -0
- package/packages/visualizer/src/explorer/ArtifactsView.tsx +162 -0
- package/packages/visualizer/src/explorer/Explorer.tsx +47 -3
- package/packages/visualizer/src/explorer/RunsView.tsx +43 -19
- package/packages/visualizer/src/explorer/catalog.tsx +97 -12
- package/packages/visualizer/src/explorer/filters.tsx +279 -0
- package/packages/visualizer/src/explorer/search.ts +85 -6
- package/packages/visualizer/src/explorer/views.ts +75 -0
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* always the report's winner (`combineEvidence`), and history only describes how it got there.
|
|
11
11
|
* - `testCatalog` turns the report's winners (plus a history, when there is one) into a `TestCatalog`: a summary with
|
|
12
12
|
* insights (slowest, flaky, failing), facets to filter by, and the tests that match a `TestQuery`.
|
|
13
|
-
* - `testHistory`, `runList` and `runDetail` answer for one test, every run, and one run.
|
|
13
|
+
* - `testHistory`, `runList` and `runDetail` answer for one test, every run, and one run. `runList` and `artifactList`
|
|
14
|
+
* narrow by tag, label, media type, environment, branch, outcome and time (see `artifact-query.ts`); `artifactList` finds
|
|
15
|
+
* the recorded results that carry evidence across the whole history, not only the latest winners.
|
|
14
16
|
* - `catalogMarkdown`, `historyMarkdown`, `runsMarkdown` and `runMarkdown` say the same in Markdown, for an agent or a
|
|
15
17
|
* terminal.
|
|
16
18
|
*
|
|
@@ -21,7 +23,9 @@
|
|
|
21
23
|
* @see docs/server.md#reading-the-report-as-an-agent
|
|
22
24
|
*/
|
|
23
25
|
import { displayTag, normalizeTag, normalizeTags } from './artifact-tags.js';
|
|
26
|
+
import { facetsOf, forRuns, labelList, matchesFilter, outcomeOfCounts, splitList, usedFilters } from './artifact-query.js';
|
|
24
27
|
import { videoCounts } from './evidence-detail.js';
|
|
28
|
+
import { matchesScope, scopeTokens, UNKNOWN_SCOPE } from './evidence-scope.js';
|
|
25
29
|
import { checkSlots } from './evidence-view.js';
|
|
26
30
|
import { combineEvidence, emptyCounts, slotKey, specPath as normalSpecPath } from './layer-merge.js';
|
|
27
31
|
import { RecordedSpecs } from './recorded-specs.js';
|
|
@@ -101,6 +105,7 @@ export class TestHistory {
|
|
|
101
105
|
#seen = new Set();
|
|
102
106
|
#rev = 0;
|
|
103
107
|
#cells = null;
|
|
108
|
+
#evidence = null;
|
|
104
109
|
/** Changes each time a layer is added. */
|
|
105
110
|
get rev() {
|
|
106
111
|
return this.#rev;
|
|
@@ -127,7 +132,7 @@ export class TestHistory {
|
|
|
127
132
|
const key = runKey(layer);
|
|
128
133
|
let run = this.#runs.get(key);
|
|
129
134
|
if (!run) {
|
|
130
|
-
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: [] };
|
|
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() } };
|
|
131
136
|
this.#runs.set(key, run);
|
|
132
137
|
}
|
|
133
138
|
// A run is told by its first layer (by time, then id), whatever order its layers arrive in.
|
|
@@ -156,6 +161,8 @@ export class TestHistory {
|
|
|
156
161
|
run.layers += 1;
|
|
157
162
|
if (layer.evidence?.videos)
|
|
158
163
|
run.videos.push(...layer.evidence.videos);
|
|
164
|
+
if (layer.evidence)
|
|
165
|
+
takeEvidence(run.evidence, layer.evidence);
|
|
159
166
|
const origin = originOf(layer, backend);
|
|
160
167
|
for (const row of rows) {
|
|
161
168
|
// "Still running" is progress, not history: a test running now shows it as its current outcome.
|
|
@@ -248,13 +255,75 @@ export class TestHistory {
|
|
|
248
255
|
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));
|
|
249
256
|
return { run: runSummary(run, cells), results };
|
|
250
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Every recorded result that carries an attachment, newest first, with the values a query looks at. Built once per
|
|
260
|
+
* layer added. The domain is left to the caller (`artifactList`), which may know more specs than the layers recorded.
|
|
261
|
+
*/
|
|
262
|
+
evidenceResults() {
|
|
263
|
+
if (this.#evidence?.rev === this.#rev)
|
|
264
|
+
return this.#evidence.items;
|
|
265
|
+
const items = [];
|
|
266
|
+
for (const [slot, list] of this.#bySlot) {
|
|
267
|
+
const i = this.#idents.get(slot);
|
|
268
|
+
for (const held of list) {
|
|
269
|
+
const e = held.e;
|
|
270
|
+
if (!e || e.attachments < 1)
|
|
271
|
+
continue;
|
|
272
|
+
const playable = (videoCounts(e.videos)?.recordings ?? 0) > 0;
|
|
273
|
+
const types = typesOf(e.roles, playable);
|
|
274
|
+
const artifactTags = [...(e.artifactTags ?? [])];
|
|
275
|
+
const o = held.o;
|
|
276
|
+
const result = {
|
|
277
|
+
key: i.slot, id: i.id, title: i.title, specPath: i.specPath, domain: null, at: o.at, status: held.s, environment: o.environment, producer: o.producer, kind: held.k,
|
|
278
|
+
parameters: o.parameters, branch: o.branch, sha: o.sha, run: o.run, runUrl: o.runUrl, layer: o.layer, backend: o.backend, attachments: e.attachments,
|
|
279
|
+
artifactTags, tags: [...e.tags], types, labels: e.labels, ...(e.videos ? { videos: e.videos } : {}),
|
|
280
|
+
};
|
|
281
|
+
const media = [
|
|
282
|
+
...types.filter(type => type !== 'video').map(type => ({ type, tags: artifactTags })),
|
|
283
|
+
...(e.videos ?? []).filter(video => video.available).map(video => ({ type: 'video', tags: video.tags ?? [] })),
|
|
284
|
+
];
|
|
285
|
+
const labels = labelList(e.labels);
|
|
286
|
+
items.push({
|
|
287
|
+
result,
|
|
288
|
+
fields: {
|
|
289
|
+
text: [i.title, i.id, i.specPath ?? '', o.branch ?? '', o.producer, o.environment ?? '', ...artifactTags, ...e.tags, ...labels].join('\n').toLowerCase(),
|
|
290
|
+
artifactTags, tags: [...new Set([...e.tags, ...artifactTags])], types, media, labels,
|
|
291
|
+
environments: o.environment ? [o.environment] : [], producers: [o.producer], branches: o.branch ? [o.branch] : [], shas: o.sha ? [o.sha.toLowerCase()] : [],
|
|
292
|
+
outcomes: [held.s], at: o.at,
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
items.sort((a, b) => b.result.at - a.result.at || compareText(a.result.layer, b.result.layer) || compareText(a.result.key, b.result.key) || compareText(a.result.kind, b.result.kind) || compareText(a.result.producer, b.result.producer));
|
|
298
|
+
this.#evidence = { rev: this.#rev, items };
|
|
299
|
+
return items;
|
|
300
|
+
}
|
|
251
301
|
/** The keys of the tests one run recorded. */
|
|
252
302
|
testsIn(key) {
|
|
253
303
|
return new Set((this.#runCells().get(key) ?? []).map(cell => cell.i.slot));
|
|
254
304
|
}
|
|
255
305
|
}
|
|
306
|
+
function takeEvidence(into, evidence) {
|
|
307
|
+
for (const tag of evidence.tags)
|
|
308
|
+
into.tags.add(tag);
|
|
309
|
+
for (const tag of evidence.artifactTags ?? [])
|
|
310
|
+
into.artifactTags.add(tag);
|
|
311
|
+
for (const role of evidence.roles)
|
|
312
|
+
into.roles.add(role);
|
|
313
|
+
for (const label of labelList(evidence.labels))
|
|
314
|
+
into.labels.add(label);
|
|
315
|
+
}
|
|
316
|
+
/** The media types of an evidence summary: its roles, with `video` only when a recording can be played. */
|
|
317
|
+
function typesOf(roles, playable) {
|
|
318
|
+
const types = new Set(roles);
|
|
319
|
+
types.delete('video');
|
|
320
|
+
if (playable)
|
|
321
|
+
types.add('video');
|
|
322
|
+
return [...types].sort();
|
|
323
|
+
}
|
|
256
324
|
function runSummary(run, cells) {
|
|
257
325
|
const counts = emptyCounts();
|
|
326
|
+
const carried = { tags: new Set(run.evidence.tags), artifactTags: new Set(run.evidence.artifactTags), roles: new Set(run.evidence.roles), labels: new Set(run.evidence.labels) };
|
|
258
327
|
const tests = new Set();
|
|
259
328
|
let total = 0, measured = false;
|
|
260
329
|
const failed = [];
|
|
@@ -262,6 +331,8 @@ function runSummary(run, cells) {
|
|
|
262
331
|
for (const cell of cells) {
|
|
263
332
|
if (cell.r.e?.videos)
|
|
264
333
|
videos.push(...cell.r.e.videos);
|
|
334
|
+
if (cell.r.e)
|
|
335
|
+
takeEvidence(carried, cell.r.e);
|
|
265
336
|
counts[cell.r.s] += 1;
|
|
266
337
|
tests.add(cell.i.slot);
|
|
267
338
|
if (cell.r.d !== null) {
|
|
@@ -281,6 +352,10 @@ function runSummary(run, cells) {
|
|
|
281
352
|
producers: [...run.producers].sort(), environments: [...run.environments].sort(),
|
|
282
353
|
layers: run.layers, tests: tests.size, counts, totalDurationMs: measured ? total : null, failing,
|
|
283
354
|
...(video ? { videos: video } : {}),
|
|
355
|
+
...(carried.artifactTags.size ? { artifactTags: [...carried.artifactTags].sort() } : {}),
|
|
356
|
+
...(carried.roles.size ? { types: typesOf(carried.roles, (video?.recordings ?? 0) > 0) } : {}),
|
|
357
|
+
...(carried.labels.size ? { labels: [...carried.labels].sort() } : {}),
|
|
358
|
+
...(carried.tags.size ? { tags: [...carried.tags].sort() } : {}),
|
|
284
359
|
};
|
|
285
360
|
}
|
|
286
361
|
function compareText(a, b) {
|
|
@@ -374,10 +449,16 @@ function summaryOf(slot, history, known) {
|
|
|
374
449
|
const labels = {};
|
|
375
450
|
let attachments = 0, steps = 0, attempts = 0;
|
|
376
451
|
const roles = new Set();
|
|
452
|
+
const profiles = new Map();
|
|
377
453
|
for (const [at, outcome] of outcomes.entries()) {
|
|
378
454
|
const evidence = outcome.evidence;
|
|
379
455
|
if (!evidence)
|
|
380
456
|
continue;
|
|
457
|
+
for (const video of evidence.videos ?? [])
|
|
458
|
+
if (video.available) {
|
|
459
|
+
const tokens = scopeTokens(video.scope);
|
|
460
|
+
profiles.set(tokens.join('\n'), tokens);
|
|
461
|
+
}
|
|
381
462
|
if (evidence.attachments > 0)
|
|
382
463
|
for (const tag of normalizeTags([...evidence.tags, ...(slot.outcomes[at]?.layer.evidence?.tags ?? [])]))
|
|
383
464
|
inheritedTags.add(tag);
|
|
@@ -407,6 +488,7 @@ function summaryOf(slot, history, known) {
|
|
|
407
488
|
tags: tagList,
|
|
408
489
|
...(artifactTags.size ? { artifactTags: [...artifactTags].sort() } : {}),
|
|
409
490
|
...([...inheritedTags].some(tag => !artifactTags.has(tag)) ? { inheritedArtifactTags: [...inheritedTags].filter(tag => !artifactTags.has(tag)).sort() } : {}),
|
|
491
|
+
...(profiles.size ? { scopeProfiles: [...profiles.values()] } : {}),
|
|
410
492
|
labels,
|
|
411
493
|
evidence: { attachments, steps, attempts, roles: [...roles].sort() },
|
|
412
494
|
lastRecordedAtMs: slot.latestAtMs,
|
|
@@ -416,7 +498,7 @@ function summaryOf(slot, history, known) {
|
|
|
416
498
|
/** The lowercase text a search matches a test against: title, id, spec, domain, tags, labels, environments and producers. */
|
|
417
499
|
export function haystackOf(test) {
|
|
418
500
|
return [
|
|
419
|
-
test.title, test.id, test.specPath ?? '', test.spec?.title ?? '', test.spec?.domain ?? '', ...test.tags, ...(test.artifactTags ?? []).map(displayTag),
|
|
501
|
+
test.title, test.id, test.specPath ?? '', test.spec?.title ?? '', test.spec?.domain ?? '', ...test.tags, ...(test.artifactTags ?? []).map(displayTag), ...(test.scopeProfiles ?? []).flat().filter(token => token !== UNKNOWN_SCOPE),
|
|
420
502
|
...Object.entries(test.labels).flatMap(([key, value]) => [key, value]), ...test.environments, ...test.producers,
|
|
421
503
|
].join('\n').toLowerCase();
|
|
422
504
|
}
|
|
@@ -523,6 +605,7 @@ export function catalogModel(layers, sources = {}) {
|
|
|
523
605
|
specs: [...specGroups].map(([, list]) => { const g = group(list[0].spec.path, list); return { ...list[0].spec, tests: g.tests, counts: g.counts }; }).sort((a, b) => compareText(a.domain, b.domain) || compareText(a.title, b.title)),
|
|
524
606
|
tags: facet(tests, test => test.tags),
|
|
525
607
|
artifactTags: facet(tests, test => test.artifactTags ?? []),
|
|
608
|
+
scopes: facet(tests, test => (test.scopeProfiles ?? []).flat()),
|
|
526
609
|
branches: facet(tests, test => test.stats.branches),
|
|
527
610
|
labels: Object.fromEntries([...labelKeys].sort().map(key => [key, facet(tests, test => (test.labels[key] === undefined ? [] : [test.labels[key]]))])),
|
|
528
611
|
};
|
|
@@ -559,6 +642,8 @@ export function matchesQuery(model, test, query, inRun) {
|
|
|
559
642
|
return false;
|
|
560
643
|
if (query.artifactTag?.length && !query.artifactTag.some(tag => { const wanted = normalizeTag(tag); return wanted !== null && ((test.artifactTags?.includes(wanted) ?? false) || (query.inherited === true && (test.inheritedArtifactTags?.includes(wanted) ?? false))); }))
|
|
561
644
|
return false;
|
|
645
|
+
if (query.scope?.length && !(test.scopeProfiles ?? []).some(tokens => matchesScope(tokens, query.scope)))
|
|
646
|
+
return false;
|
|
562
647
|
if (inRun && !inRun.has(test.key))
|
|
563
648
|
return false;
|
|
564
649
|
return true;
|
|
@@ -592,24 +677,75 @@ export function hasVideo(run) {
|
|
|
592
677
|
export function runList(history, version, limit = 100, query = {}) {
|
|
593
678
|
const all = history.runs();
|
|
594
679
|
const withVideo = all.filter(hasVideo).length;
|
|
595
|
-
const
|
|
596
|
-
|
|
680
|
+
const filter = forRuns(query);
|
|
681
|
+
const fields = all.map(fieldsOfRun);
|
|
682
|
+
const runs = all.filter((_, at) => matchesFilter(fields[at], filter));
|
|
683
|
+
return {
|
|
684
|
+
schemaVersion: TEST_CATALOG_VERSION, version, total: runs.length, runs: runs.slice(0, Math.max(0, limit)), withVideo,
|
|
685
|
+
filters: usedFilters(filter), facets: facetsOf(fields, filter),
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
/** What a query looks at in a run: see {@link ArtifactFilter}. A run's outcome is `failed` when any result failed. */
|
|
689
|
+
export function fieldsOfRun(run) {
|
|
690
|
+
const artifactTags = run.artifactTags ?? [];
|
|
691
|
+
// A run summary of an older server has no `types`, but it says whether it has a playable video.
|
|
692
|
+
const types = run.types ?? (hasVideo(run) ? ['video'] : []);
|
|
693
|
+
const labels = run.labels ?? [];
|
|
694
|
+
return {
|
|
695
|
+
text: [run.title ?? '', run.key, run.branch ?? '', run.sha ?? '', ...run.producers, ...run.environments, ...artifactTags, ...(run.tags ?? []), ...labels].join('\n').toLowerCase(),
|
|
696
|
+
artifactTags, tags: [...new Set([...(run.tags ?? []), ...artifactTags])], types, media: types.map(type => ({ type, tags: artifactTags })), labels,
|
|
697
|
+
environments: run.environments, producers: run.producers, branches: run.branch ? [run.branch] : [], shas: run.sha ? [run.sha.toLowerCase()] : [],
|
|
698
|
+
outcomes: [outcomeOfCounts(run.counts)], at: run.endedAtMs,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* The recorded results that carry evidence and match a query, newest first, across the whole history (`limit` default
|
|
703
|
+
* 100). `total` and the facet counts apply to every match before `offset` and `limit`. `domainOf` tells the domain of a
|
|
704
|
+
* spec path when the caller knows more than the layers recorded (a server with a spec set); otherwise it is the
|
|
705
|
+
* recorded spec's domain, else the folder the path implies.
|
|
706
|
+
*
|
|
707
|
+
* @example
|
|
708
|
+
* ```ts
|
|
709
|
+
* const list = artifactList(history, 'v1', { artifactTag: ['demo'], type: ['video'], environment: ['ios'] });
|
|
710
|
+
* list.results.map(r => [r.title, r.run, r.sha, r.status]);
|
|
711
|
+
* ```
|
|
712
|
+
*/
|
|
713
|
+
export function artifactList(history, version, query = {}, page = {}) {
|
|
714
|
+
const domains = new Map();
|
|
715
|
+
const domainOf = (path) => {
|
|
716
|
+
if (path === null)
|
|
717
|
+
return null;
|
|
718
|
+
if (!domains.has(path))
|
|
719
|
+
domains.set(path, (page.domainOf?.(path) ?? catalogSpec(path, known => history.specs.get(known))?.domain) || null);
|
|
720
|
+
return domains.get(path);
|
|
721
|
+
};
|
|
722
|
+
const items = history.evidenceResults().map(({ result, fields }) => {
|
|
723
|
+
const domain = domainOf(result.specPath);
|
|
724
|
+
return { result: { ...result, domain }, fields: { ...fields, domains: domain ? [domain] : [], specs: result.specPath ? [normalSpecPath(result.specPath)] : [] } };
|
|
725
|
+
});
|
|
726
|
+
const matched = items.filter(item => matchesFilter(item.fields, query));
|
|
727
|
+
const limit = Math.max(0, page.limit ?? 100);
|
|
728
|
+
const offset = Math.max(0, page.offset ?? 0);
|
|
729
|
+
return {
|
|
730
|
+
schemaVersion: TEST_CATALOG_VERSION, version, total: matched.length, offset, limit,
|
|
731
|
+
results: matched.slice(offset, offset + limit).map(item => item.result), filters: usedFilters(query), facets: facetsOf(items.map(item => item.fields), query),
|
|
732
|
+
};
|
|
597
733
|
}
|
|
598
734
|
/** One run with every result it recorded, or null when no run has that key. */
|
|
599
735
|
export function runDetail(history, version, key) {
|
|
600
736
|
const found = history.run(key);
|
|
601
737
|
return found ? { schemaVersion: TEST_CATALOG_VERSION, version, ...found } : null;
|
|
602
738
|
}
|
|
603
|
-
/** Reads a `TestQuery` from URL search parameters: `q`, `sort`, `offset`, `limit`, `run`, and repeatable or comma-separated `status`, `environment`, `producer`, `domain`, `spec`, `tag`, `artifactTag`, and `inherited=1`. */
|
|
739
|
+
/** Reads a `TestQuery` from URL search parameters: `q`, `sort`, `offset`, `limit`, `run`, and repeatable or comma-separated `status`, `environment`, `producer`, `domain`, `spec`, `tag`, `artifactTag`, `scope` (every token must hold for one recording), and `inherited=1`. */
|
|
604
740
|
export function parseTestQuery(params) {
|
|
605
|
-
const many = (name) => params.getAll(name).flatMap(
|
|
741
|
+
const many = (name) => params.getAll(name).flatMap(splitList);
|
|
606
742
|
const number = (name) => { const value = params.get(name); return value !== null && /^\d+$/.test(value) ? Number(value) : undefined; };
|
|
607
743
|
const sort = params.get('sort');
|
|
608
744
|
const query = {};
|
|
609
745
|
const q = params.get('q');
|
|
610
746
|
if (q)
|
|
611
747
|
query.q = q;
|
|
612
|
-
for (const name of ['status', 'environment', 'producer', 'domain', 'spec', 'tag', 'artifactTag']) {
|
|
748
|
+
for (const name of ['status', 'environment', 'producer', 'domain', 'spec', 'tag', 'artifactTag', 'scope']) {
|
|
613
749
|
const values = many(name);
|
|
614
750
|
if (values.length)
|
|
615
751
|
query[name] = values;
|
|
@@ -690,6 +826,7 @@ export function historyMarkdown(document) {
|
|
|
690
826
|
`- Duration: ${d ? `avg ${formatMs(d.avgMs)}, p50 ${formatMs(d.p50Ms)}, p95 ${formatMs(d.p95Ms)}, min ${formatMs(d.minMs)}, max ${formatMs(d.maxMs)}, last ${formatMs(d.lastMs)}` : 'not measured'}`,
|
|
691
827
|
...(test.tags.length ? [`- Tags: ${test.tags.join(', ')}`] : []),
|
|
692
828
|
...(test.artifactTags?.length ? [`- Artifact tags: ${test.artifactTags.map(displayTag).join(', ')}`] : []),
|
|
829
|
+
...(test.scopeProfiles?.length ? [`- Recording scope: ${test.scopeProfiles.map(tokens => tokens.join(' ')).join('; ')}`] : []),
|
|
693
830
|
...(Object.keys(test.labels).length ? [`- Labels: ${Object.entries(test.labels).map(([k, v]) => `${k}=${v}`).join(', ')}`] : []),
|
|
694
831
|
`- Evidence: ${test.evidence.attachments} attachments${test.evidence.roles.length ? ` (${test.evidence.roles.join(', ')})` : ''}, ${test.evidence.steps} steps`,
|
|
695
832
|
'',
|
|
@@ -726,6 +863,16 @@ export function runsMarkdown(list) {
|
|
|
726
863
|
...list.runs.map(r => `| ${iso(r.endedAtMs)} | ${cell(r.branch ?? '—')} | ${r.sha?.slice(0, 10) ?? '—'}${freshnessNote(r.freshness)} | ${r.environments.join(', ') || '—'} | ${r.tests} | ${r.counts.passed} | ${r.counts.failed} | ${r.counts['not-run']} | ${formatMs(r.spanMs)} | ${videosText(r.videos)} | ${cell(r.title ? `${r.title} (${r.key})` : r.key)} |`),
|
|
727
864
|
].join('\n')}\n`;
|
|
728
865
|
}
|
|
866
|
+
/** The artifact results as Markdown, for an agent or a terminal. */
|
|
867
|
+
export function artifactsMarkdown(list) {
|
|
868
|
+
return `${[
|
|
869
|
+
`# Artifacts (${list.results.length} of ${list.total})`,
|
|
870
|
+
'',
|
|
871
|
+
'| Recorded | Test | Status | Environment | Branch | Commit | Types | Tags | Videos | Run |',
|
|
872
|
+
'|---|---|---|---|---|---|---|---|---|---|',
|
|
873
|
+
...list.results.map(r => `| ${iso(r.at)} | ${cell(r.title)} | ${r.status} | ${r.environment ?? '—'} | ${cell(r.branch ?? '—')} | ${r.sha?.slice(0, 10) ?? '—'} | ${r.types.join(', ') || '—'} | ${r.artifactTags.map(displayTag).join(' ') || '—'} | ${videosText(videoCounts(r.videos))} | ${cell(r.runUrl ?? r.run)} |`),
|
|
874
|
+
].join('\n')}\n`;
|
|
875
|
+
}
|
|
729
876
|
/** One run as Markdown. */
|
|
730
877
|
export function runMarkdown(detail) {
|
|
731
878
|
const { run } = detail;
|