@crvouga/atlas 0.9.0 → 0.10.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/playwright-reporter/src/evidence.d.ts +122 -0
- package/dist/playwright-reporter/src/evidence.js +327 -0
- package/dist/playwright-reporter/src/index.d.ts +114 -0
- package/dist/playwright-reporter/src/index.js +312 -0
- package/dist/reports/src/compose.js +7 -14
- package/dist/reports/src/recorded-specs.d.ts +33 -0
- package/dist/reports/src/recorded-specs.js +42 -0
- package/dist/reports/src/report-index.d.ts +6 -0
- package/dist/reports/src/report-index.js +14 -2
- package/dist/reports/src/test-catalog.d.ts +386 -0
- package/dist/reports/src/test-catalog.js +655 -0
- package/dist/schema/src/api.d.ts +585 -0
- package/dist/schema/src/api.js +105 -0
- package/dist/server/src/app.js +82 -4
- package/dist/server/src/llms.js +14 -0
- package/dist/server/src/openapi.d.ts +1 -1
- package/dist/server/src/openapi.js +60 -3
- package/docs/agents.md +1 -0
- package/docs/integrating.md +1 -0
- package/docs/reference/http-api.md +134 -2
- package/docs/reference/playwright-reporter.md +332 -0
- package/docs/reference/reports.md +632 -0
- package/docs/reference/schema.md +57 -0
- package/docs/rich-evidence.md +198 -0
- package/docs/server.md +41 -0
- package/llms-full.txt +378 -2
- package/llms.txt +2 -0
- package/package.json +6 -1
- package/packages/schema/schemas/report-run.schema.json +251 -0
- package/packages/schema/schemas/report-runs.schema.json +177 -0
- package/packages/schema/schemas/test-catalog.schema.json +1177 -0
- package/packages/schema/schemas/test-history.schema.json +696 -0
- package/packages/server/openapi.json +8676 -5351
- package/packages/visualizer/src/app/AppShell.tsx +7 -3
- package/packages/visualizer/src/app/router.tsx +26 -3
- package/packages/visualizer/src/components/Header.tsx +2 -2
- package/packages/visualizer/src/evidence/evidence-context.tsx +10 -1
- package/packages/visualizer/src/explorer/Explorer.tsx +433 -0
- package/packages/visualizer/src/explorer/ExplorerIndex.tsx +120 -0
- package/packages/visualizer/src/explorer/RunsView.tsx +164 -0
- package/packages/visualizer/src/explorer/TestDetail.tsx +214 -0
- package/packages/visualizer/src/explorer/catalog.tsx +274 -0
- package/packages/visualizer/src/explorer/explorer.module.css +1238 -0
- package/packages/visualizer/src/explorer/groups.test.ts +87 -0
- package/packages/visualizer/src/explorer/groups.ts +126 -0
- package/packages/visualizer/src/explorer/navigate.ts +36 -0
- package/packages/visualizer/src/explorer/parts.tsx +59 -0
- package/packages/visualizer/src/explorer/search.ts +97 -0
- package/packages/visualizer/src/find/Finder.tsx +2 -0
- package/packages/visualizer/src/find/corpus.ts +13 -6
- package/packages/visualizer/src/find/rank.ts +2 -0
- package/packages/visualizer/src/evidence/project-home.tsx +0 -45
package/dist/server/src/app.js
CHANGED
|
@@ -25,6 +25,7 @@ import { reportStatus } from '../../reports/src/report-status.js';
|
|
|
25
25
|
import { ReportIndex } from '../../reports/src/report-index.js';
|
|
26
26
|
import { listRunSummaries, readRunFile } from '../../reports/src/run-bundle.js';
|
|
27
27
|
import { latestSpecSet, readSpecBody } from '../../reports/src/spec-set.js';
|
|
28
|
+
import { catalogMarkdown, catalogModel, historyMarkdown, parseTestQuery, runDetail, runList, runMarkdown, runsMarkdown, testCatalog, testHistory } from '../../reports/src/test-catalog.js';
|
|
28
29
|
import { llmsTxt } from './llms.js';
|
|
29
30
|
import { openApiDocument } from './openapi.js';
|
|
30
31
|
import { asProblem, problemResponse } from './problem.js';
|
|
@@ -132,8 +133,9 @@ function createProjectApplication(options) {
|
|
|
132
133
|
});
|
|
133
134
|
const outline = async () => {
|
|
134
135
|
const found = await specs();
|
|
136
|
+
// Without a published spec set, the report is laid over the specs its layers recorded, as the CLI does.
|
|
135
137
|
if (!found)
|
|
136
|
-
return { specs: [], sha: null, changedFiles: [], readSpec: async () => null };
|
|
138
|
+
return { specs: [...index.history.specs.entries()], sha: null, changedFiles: [], readSpec: async () => null };
|
|
137
139
|
const entries = found.set.files.map(({ path: file, domain, kind, title, fingerprint }) => ({ path: file, domain, kind, title, fingerprint }));
|
|
138
140
|
return { specs: entries, sha: found.set.sha, changedFiles: [], readSpec: async (file) => {
|
|
139
141
|
const entry = found.set.files.find(item => item.path === file);
|
|
@@ -251,7 +253,7 @@ function createProjectApplication(options) {
|
|
|
251
253
|
name: 'atlas', version: options.version ?? null, role: identity?.role ?? 'read', open: auth.open,
|
|
252
254
|
write: options.write, ready: index.ready, viewers: events.count, time: now(),
|
|
253
255
|
backends: snapshot.backends.map(({ id, label, available }) => ({ id, label, available, write: id === options.write })),
|
|
254
|
-
...(({ backends: _unused, ...rest }) => rest)(index.stats()), capabilities: { events: true, ingest: true, store: true, checks: true, status: true, effective: true, live: true }
|
|
256
|
+
...(({ backends: _unused, ...rest }) => rest)(index.stats()), capabilities: { events: true, ingest: true, store: true, checks: true, status: true, effective: true, live: true, tests: true }
|
|
255
257
|
});
|
|
256
258
|
});
|
|
257
259
|
app.get('/api/live', async (c) => c.json({ runs: await live.list() }, 200, { 'cache-control': 'no-store' }));
|
|
@@ -354,6 +356,81 @@ function createProjectApplication(options) {
|
|
|
354
356
|
return body(c, delta.text, 'application/json', headers);
|
|
355
357
|
return body(c, text, 'application/json', headers, `effective|${version}`);
|
|
356
358
|
});
|
|
359
|
+
// Tests, their history and the runs that recorded them: the report as a person or an agent explores it.
|
|
360
|
+
let model = null;
|
|
361
|
+
const catalog = async () => {
|
|
362
|
+
const snapshot = index.snapshot();
|
|
363
|
+
const set = await specs();
|
|
364
|
+
const key = `${snapshot.version}|${index.history.rev}|${set?.set.id ?? ''}`;
|
|
365
|
+
if (model?.key !== key) {
|
|
366
|
+
const entries = set?.set.files.map(({ path: file, domain, kind, title }) => ({ path: file, domain, kind, title }));
|
|
367
|
+
model = { key, value: catalogModel(snapshot.layers, { history: index.history, version: createHash('sha256').update(key).digest('hex').slice(0, 20), now: now(), ...(entries ? { specs: entries } : {}) }) };
|
|
368
|
+
}
|
|
369
|
+
return model.value;
|
|
370
|
+
};
|
|
371
|
+
const wantsMarkdown = (c) => {
|
|
372
|
+
const format = c.req.query('format');
|
|
373
|
+
return format === 'markdown' || format === 'md' || (format === undefined && /\btext\/markdown\b/.test(c.req.header('accept') ?? ''));
|
|
374
|
+
};
|
|
375
|
+
const notReady = (c) => c.json({ error: 'Atlas is still reading its backends; try again in a moment.' }, 503, { 'retry-after': '1' });
|
|
376
|
+
// What was written for a version and a question is written once, however many viewers poll for it.
|
|
377
|
+
const rendered = new Map();
|
|
378
|
+
const answer = async (c, version, document, markdown, cacheKey) => {
|
|
379
|
+
const key = `${version}|${cacheKey}`;
|
|
380
|
+
const etag = `"${version}-${createHash('sha256').update(cacheKey).digest('hex').slice(0, 12)}"`;
|
|
381
|
+
const headers = { etag, 'cache-control': 'no-cache' };
|
|
382
|
+
if (c.req.header('if-none-match') === etag)
|
|
383
|
+
return new Response(null, { status: 304, headers });
|
|
384
|
+
const asMarkdown = wantsMarkdown(c);
|
|
385
|
+
let text = rendered.get(key);
|
|
386
|
+
if (text === undefined) {
|
|
387
|
+
text = asMarkdown ? markdown() : JSON.stringify(document());
|
|
388
|
+
if (rendered.size >= 32)
|
|
389
|
+
rendered.delete(rendered.keys().next().value);
|
|
390
|
+
rendered.set(key, text);
|
|
391
|
+
}
|
|
392
|
+
return body(c, text, asMarkdown ? 'text/markdown; charset=utf-8' : 'application/json', headers, key);
|
|
393
|
+
};
|
|
394
|
+
app.get('/api/reports/tests', async (c) => {
|
|
395
|
+
if (!index.ready)
|
|
396
|
+
return notReady(c);
|
|
397
|
+
const found = await catalog();
|
|
398
|
+
const query = parseTestQuery(new URL(c.req.url).searchParams);
|
|
399
|
+
const document = () => testCatalog(found, query);
|
|
400
|
+
return answer(c, found.version, document, () => catalogMarkdown(document()), `tests|${new URL(c.req.url).search}|${wantsMarkdown(c)}`);
|
|
401
|
+
});
|
|
402
|
+
app.get('/api/reports/tests/history', async (c) => {
|
|
403
|
+
if (!index.ready)
|
|
404
|
+
return notReady(c);
|
|
405
|
+
const key = c.req.query('key');
|
|
406
|
+
if (!key)
|
|
407
|
+
return problemResponse(400, 'Name the test with ?key=, a key from GET /api/reports/tests.');
|
|
408
|
+
const found = await catalog();
|
|
409
|
+
const document = testHistory(found, key);
|
|
410
|
+
if (!document)
|
|
411
|
+
return problemResponse(404, 'No test in the report has that key.');
|
|
412
|
+
return answer(c, found.version, () => document, () => historyMarkdown(document), `history|${key}|${wantsMarkdown(c)}`);
|
|
413
|
+
});
|
|
414
|
+
app.get('/api/reports/runs', async (c) => {
|
|
415
|
+
if (!index.ready)
|
|
416
|
+
return notReady(c);
|
|
417
|
+
const found = await catalog();
|
|
418
|
+
const limit = /^\d+$/.test(c.req.query('limit') ?? '') ? Number(c.req.query('limit')) : 100;
|
|
419
|
+
const document = () => runList(index.history, found.version, limit);
|
|
420
|
+
return answer(c, found.version, document, () => runsMarkdown(document()), `runs|${limit}|${wantsMarkdown(c)}`);
|
|
421
|
+
});
|
|
422
|
+
app.get('/api/reports/runs/detail', async (c) => {
|
|
423
|
+
if (!index.ready)
|
|
424
|
+
return notReady(c);
|
|
425
|
+
const key = c.req.query('key');
|
|
426
|
+
if (!key)
|
|
427
|
+
return problemResponse(400, 'Name the run with ?key=, a key from GET /api/reports/runs.');
|
|
428
|
+
const found = await catalog();
|
|
429
|
+
const document = runDetail(index.history, found.version, key);
|
|
430
|
+
if (!document)
|
|
431
|
+
return problemResponse(404, 'No run has that key.');
|
|
432
|
+
return answer(c, found.version, () => document, () => runMarkdown(document), `run|${key}|${wantsMarkdown(c)}`);
|
|
433
|
+
});
|
|
357
434
|
app.route('/api/reports', layers);
|
|
358
435
|
// Upload: the store protocol, and a simple JSON way in for anything that can make a request.
|
|
359
436
|
app.route('/api/store', storeApi({
|
|
@@ -410,7 +487,7 @@ function createProjectApplication(options) {
|
|
|
410
487
|
const snapshot = index.snapshot();
|
|
411
488
|
const found = await outline();
|
|
412
489
|
// The gate is a function of the report and the specs, so it is worked out once for each pair, however often CI asks.
|
|
413
|
-
const asked = `${snapshot.version}|${(await specs())?.set.id ??
|
|
490
|
+
const asked = `${snapshot.version}|${(await specs())?.set.id ?? `recorded:${index.history.specs.size}`}|${[...branches].sort().join(',')}|${ignoreModified}|${required.join(',')}|${requireAllSpecs}`;
|
|
414
491
|
if (!verdicts.has(asked)) {
|
|
415
492
|
if (verdicts.size >= 16)
|
|
416
493
|
verdicts.delete(verdicts.keys().next().value);
|
|
@@ -426,7 +503,8 @@ function createProjectApplication(options) {
|
|
|
426
503
|
if (!filtered.has(key)) {
|
|
427
504
|
if (filtered.size >= 8)
|
|
428
505
|
filtered.delete(filtered.keys().next().value);
|
|
429
|
-
|
|
506
|
+
const specSet = await specs();
|
|
507
|
+
filtered.set(key, combineBackends(backends, { ...(specSet ? { outline: found } : {}), include: layer => (options.include?.(layer) ?? true) && (!branches.length || (layer.branch !== null && branches.includes(layer.branch))) && (!ignoreModified || layer.freshness === undefined) }));
|
|
430
508
|
}
|
|
431
509
|
report = await filtered.get(key);
|
|
432
510
|
}
|
package/dist/server/src/llms.js
CHANGED
|
@@ -60,6 +60,20 @@ bunx atlas report status --format json --branch main --ignore-modified # exit
|
|
|
60
60
|
curl ${context.open ? '' : '-H "authorization: Bearer $ATLAS_TOKEN" '}"${url}/api/status?project=$ATLAS_PROJECT" # the same verdict; header X-Atlas-Ok: true|false
|
|
61
61
|
\`\`\`
|
|
62
62
|
|
|
63
|
+
## Read the tests, their history and the runs
|
|
64
|
+
|
|
65
|
+
Every endpoint below answers JSON, or Markdown with \`format=markdown\`. Prefix it with \`/api/projects/<slug>\` (or add \`?project=<slug>\`) for a project; \`GET ${url}/api/projects\` lists them.
|
|
66
|
+
|
|
67
|
+
\`\`\`sh
|
|
68
|
+
curl "${url}/api/projects/$ATLAS_PROJECT/reports/tests?format=markdown&limit=50" # summary, environments, domains, failing, flaky, slowest, then the tests
|
|
69
|
+
curl "${url}/api/projects/$ATLAS_PROJECT/reports/tests?status=failed,flaky&environment=desktop-web" # filter: q, status, environment, producer, domain, spec, tag, run; sort, offset, limit
|
|
70
|
+
curl "${url}/api/projects/$ATLAS_PROJECT/reports/tests/history?key=<url-encoded test key>" # one test: every recorded result, durations, branches, CI runs
|
|
71
|
+
curl "${url}/api/projects/$ATLAS_PROJECT/reports/runs?format=markdown" # CI runs, newest first, with their results by status
|
|
72
|
+
curl "${url}/api/projects/$ATLAS_PROJECT/reports/runs/detail?key=<url-encoded run key>" # every test one run recorded
|
|
73
|
+
\`\`\`
|
|
74
|
+
|
|
75
|
+
A test's \`key\` is \`JSON.stringify([specPath, id])\`; its \`outcomes\` are its current results per environment and \`stats\` its pass rate, flips, flakiness and durations. The evidence of one result (attachments, steps, retries) is at \`/api/reports/evidence/detail/<backend>/<layer>?check=<id>\`, its files at \`/api/reports/layers/<layer>/blobs/<bundleKey>\`.
|
|
76
|
+
|
|
63
77
|
## Rules worth knowing
|
|
64
78
|
|
|
65
79
|
- **Layered, never partitioned.** A check is identified by its \`id\` (keep it stable across runs) plus producer, kind, spec, environment and parameters. A layer that re-ran one check leaves every other check as the last layer that ran it.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The version of the API described, `major.minor.patch`: the minor grows when an operation or field is added, the major never within `/api`. */
|
|
2
|
-
export declare const API_VERSION = "1.
|
|
2
|
+
export declare const API_VERSION = "1.4.0";
|
|
3
3
|
/** Who may call an operation: `none` (anyone), or the least role that may (`read` < `publish` < `admin`). */
|
|
4
4
|
export type Role = 'none' | 'read' | 'publish' | 'admin';
|
|
5
5
|
type JsonSchema = Record<string, unknown>;
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { z } from 'zod/v4';
|
|
16
16
|
import { ProjectsSchema } from '../../schema/src/project.js';
|
|
17
|
-
import { EffectiveDeltaSchema, EffectiveEvidenceSchema, EvidencePageSchema, HealthSchema, LayerRowsSchema, CheckDetailSchema, OutlineSchema, ProblemSchema, ReadySchema, ServerInfoSchema, StoreListingSchema, StoreWriteResultSchema, ChecksResultSchema, HelloEventSchema, ReportEventSchema, PingEventSchema } from '../../schema/src/api.js';
|
|
17
|
+
import { EffectiveDeltaSchema, EffectiveEvidenceSchema, EvidencePageSchema, HealthSchema, LayerRowsSchema, CheckDetailSchema, OutlineSchema, ProblemSchema, ReadySchema, ServerInfoSchema, StoreListingSchema, StoreWriteResultSchema, ChecksResultSchema, HelloEventSchema, ReportEventSchema, PingEventSchema, TestCatalogSchema, TestHistorySchema, RunListSchema, RunDetailSchema } from '../../schema/src/api.js';
|
|
18
18
|
import { LiveRunSchema, LiveRunStartSchema, LiveRunUpdateSchema, ServerEditableSettingsSchema, ServerSettingsSnapshotSchema, AtlasChangeSchema, PublishChecksSchema, ReportSourcesSchema, ReportStatusSchema, RunsIndexSchema, SpecIndexSchema } from '../../schema/src/index.js';
|
|
19
19
|
/** The version of the API described, `major.minor.patch`: the minor grows when an operation or field is added, the major never within `/api`. */
|
|
20
|
-
export const API_VERSION = '1.
|
|
20
|
+
export const API_VERSION = '1.4.0';
|
|
21
21
|
/** The JSON Schema of a Zod schema, as OpenAPI 3.1 embeds it. `output` for what the server sends, `input` for what it accepts. */
|
|
22
22
|
function schemaOf(schema, io = 'output') {
|
|
23
23
|
const { $schema: _dialect, ...json } = z.toJSONSchema(schema, { target: 'draft-2020-12', unrepresentable: 'any', io });
|
|
@@ -35,6 +35,7 @@ const COMPONENTS = {
|
|
|
35
35
|
PublishChecks: [PublishChecksSchema.partial({ recordedAtMs: true }), 'input'], ChecksResult: [ChecksResultSchema, 'output'],
|
|
36
36
|
StoreListing: [StoreListingSchema, 'output'], StoreWriteResult: [StoreWriteResultSchema, 'output'],
|
|
37
37
|
Outline: [OutlineSchema, 'output'], EvidencePage: [EvidencePageSchema, 'output'], LayerRows: [LayerRowsSchema, 'output'], CheckDetail: [CheckDetailSchema, 'output'],
|
|
38
|
+
TestCatalog: [TestCatalogSchema, 'output'], TestHistory: [TestHistorySchema, 'output'], RunList: [RunListSchema, 'output'], RunDetail: [RunDetailSchema, 'output'],
|
|
38
39
|
SpecIndex: [SpecIndexSchema, 'output'], ReportSources: [ReportSourcesSchema, 'output'], RunsIndex: [RunsIndexSchema, 'output'],
|
|
39
40
|
RunEvent: [LiveRunSchema, 'output'], HelloEvent: [HelloEventSchema, 'output'], ReportEvent: [ReportEventSchema, 'output'], AtlasChangeEvent: [AtlasChangeSchema, 'output'], PingEvent: [PingEventSchema, 'output']
|
|
40
41
|
};
|
|
@@ -252,10 +253,66 @@ const BASE_OPERATIONS = [
|
|
|
252
253
|
parameters: [KEY],
|
|
253
254
|
responses: { '204': { description: 'Removed (or it was not there).' }, '401': UNAUTHORIZED, '403': problem('Not a scratch object, or the token may not.') }
|
|
254
255
|
},
|
|
256
|
+
{
|
|
257
|
+
route: 'GET /api/reports/tests', path: '/api/reports/tests', operationId: 'getTests', tag: 'Report', role: 'read',
|
|
258
|
+
summary: 'Every test, with insights, facets and history statistics',
|
|
259
|
+
description: [
|
|
260
|
+
'The report as a list of tests, for exploring it or for an agent to read in one request. A test is a check identity (its spec path and id) across producers and environments; `outcomes` holds its current result on each environment, and `stats` how it went over its recorded history (pass rate, flips, flaky, durations avg/p50/p95, runs, branches).',
|
|
261
|
+
'',
|
|
262
|
+
'`summary` and `facets` cover every test (counts by status, by environment, by producer and by domain, the slowest, flakiest and failing tests); `tests` is the page the query matched. Without a published spec set, specs come from the `specs[]` the layers recorded.',
|
|
263
|
+
'',
|
|
264
|
+
'Send `format=markdown` (or `Accept: text/markdown`) for the same as a Markdown report. The current status of a test is always the report\'s winner; history never changes it.'
|
|
265
|
+
].join('\n'),
|
|
266
|
+
parameters: [
|
|
267
|
+
{ name: 'q', in: 'query', description: 'Words that must all appear in the title, id, spec, domain, tags, labels, environments or producers. Case does not matter.', schema: str, example: 'checkout card' },
|
|
268
|
+
{ name: 'status', in: 'query', description: 'Current status, or `flaky`. Repeatable or comma-separated.', schema: str, example: 'failed,flaky' },
|
|
269
|
+
{ name: 'environment', in: 'query', description: 'Only tests with a result on this environment. Repeatable or comma-separated.', schema: str, example: 'desktop-web' },
|
|
270
|
+
{ name: 'producer', in: 'query', description: 'Only tests from this producer. Repeatable or comma-separated.', schema: str },
|
|
271
|
+
{ name: 'domain', in: 'query', description: 'Only tests whose spec is in this domain (`(no spec)` for none). Repeatable or comma-separated.', schema: str },
|
|
272
|
+
{ name: 'spec', in: 'query', description: 'Only tests of this spec path. Repeatable or comma-separated.', schema: str, example: 'specs/checkout/pay.feature' },
|
|
273
|
+
{ name: 'tag', in: 'query', description: 'Only tests with this tag. Repeatable or comma-separated.', schema: str },
|
|
274
|
+
{ name: 'run', in: 'query', description: 'Only tests recorded in this run (a `key` from `/api/reports/runs`).', schema: str },
|
|
275
|
+
{ name: 'sort', in: 'query', description: 'The order: `spec` (default: domain, spec, title), `title`, `recent`, `slowest`, `failures`, `flaky` or `runs`.', schema: { type: 'string', enum: ['spec', 'title', 'recent', 'slowest', 'failures', 'flaky', 'runs'] } },
|
|
276
|
+
{ name: 'offset', in: 'query', description: 'Skip this many matching tests.', schema: { type: 'integer', minimum: 0 } },
|
|
277
|
+
{ name: 'limit', in: 'query', description: 'At most this many tests (default 500, at most 20000).', schema: { type: 'integer', minimum: 0, maximum: 20000 } },
|
|
278
|
+
{ name: 'format', in: 'query', description: '`markdown` for a Markdown report instead of JSON.', schema: { type: 'string', enum: ['json', 'markdown'] } }
|
|
279
|
+
],
|
|
280
|
+
responses: { '200': { ...json('The catalog.', 'TestCatalog'), headers: { ETag: { description: 'Changes with the report, its history and the query.', schema: str } } }, '304': { description: 'Unchanged since the `If-None-Match` ETag.' }, '401': UNAUTHORIZED, '503': NOT_READY }
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
route: 'GET /api/reports/tests/history', path: '/api/reports/tests/history', operationId: 'getTestHistory', tag: 'Report', role: 'read',
|
|
284
|
+
summary: 'One test and every result recorded for it',
|
|
285
|
+
description: 'The test (as `/api/reports/tests` lists it) and up to 100 recorded results, newest first: status, duration, environment, producer, branch, commit, CI run, layer and the first 500 characters of the detail. Open a result\'s evidence (attachments, steps, retries) with `/api/reports/evidence/detail/{backend}/{id}?check=<id>`. Send `format=markdown` for Markdown.',
|
|
286
|
+
parameters: [
|
|
287
|
+
{ name: 'key', in: 'query', required: true, description: 'The test\'s `key` from `/api/reports/tests`: `JSON.stringify([specPath, id])`, URL-encoded.', schema: str, example: '["specs/checkout/pay.feature","Pay with a card"]' },
|
|
288
|
+
{ name: 'format', in: 'query', description: '`markdown` for Markdown instead of JSON.', schema: { type: 'string', enum: ['json', 'markdown'] } }
|
|
289
|
+
],
|
|
290
|
+
responses: { '200': json('The test and its history.', 'TestHistory'), '304': { description: 'Unchanged since the `If-None-Match` ETag.' }, '400': problem('No `key`.'), '401': UNAUTHORIZED, '404': problem('No test in the report has that key.'), '503': NOT_READY }
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
route: 'GET /api/reports/runs', path: '/api/reports/runs', operationId: 'getReportRuns', tag: 'Report', role: 'read',
|
|
294
|
+
summary: 'The runs that recorded the report',
|
|
295
|
+
description: 'Every run, newest first: layers grouped by their CI run URL (else branch and commit), with their environments, producers, how many tests they recorded, their results by status, the time they spanned and up to twenty failures. Send `format=markdown` for Markdown.',
|
|
296
|
+
parameters: [
|
|
297
|
+
{ name: 'limit', in: 'query', description: 'At most this many runs (default 100).', schema: { type: 'integer', minimum: 0 } },
|
|
298
|
+
{ name: 'format', in: 'query', description: '`markdown` for Markdown instead of JSON.', schema: { type: 'string', enum: ['json', 'markdown'] } }
|
|
299
|
+
],
|
|
300
|
+
responses: { '200': json('The runs.', 'RunList'), '304': { description: 'Unchanged since the `If-None-Match` ETag.' }, '401': UNAUTHORIZED, '503': NOT_READY }
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
route: 'GET /api/reports/runs/detail', path: '/api/reports/runs/detail', operationId: 'getReportRun', tag: 'Report', role: 'read',
|
|
304
|
+
summary: 'Every test one run recorded',
|
|
305
|
+
description: 'One run and its last result for every test, environment and producer it recorded, failures first. Send `format=markdown` for Markdown.',
|
|
306
|
+
parameters: [
|
|
307
|
+
{ name: 'key', in: 'query', required: true, description: 'The run\'s `key` from `/api/reports/runs`, URL-encoded.', schema: str },
|
|
308
|
+
{ name: 'format', in: 'query', description: '`markdown` for Markdown instead of JSON.', schema: { type: 'string', enum: ['json', 'markdown'] } }
|
|
309
|
+
],
|
|
310
|
+
responses: { '200': json('The run and its results.', 'RunDetail'), '304': { description: 'Unchanged since the `If-None-Match` ETag.' }, '400': problem('No `key`.'), '401': UNAUTHORIZED, '404': problem('No run has that key.'), '503': NOT_READY }
|
|
311
|
+
},
|
|
255
312
|
{
|
|
256
313
|
route: 'GET /api/reports/outline', path: '/api/reports/outline', operationId: 'getOutline', tag: 'Report', role: 'read',
|
|
257
314
|
summary: 'The specs a report is laid over',
|
|
258
|
-
description: 'The specs of the newest committed spec set (of `main`, else the newest), so the report needs no checkout. A spec here with no result shows as having none.',
|
|
315
|
+
description: 'The specs of the newest committed spec set (of `main`, else the newest), so the report needs no checkout. Without a spec set, the specs the layers recorded (`specs[]`), newest per path. A spec here with no result shows as having none.',
|
|
259
316
|
responses: { '200': json('The outline.', 'Outline'), '401': UNAUTHORIZED }
|
|
260
317
|
},
|
|
261
318
|
{
|
package/docs/agents.md
CHANGED
package/docs/integrating.md
CHANGED
|
@@ -221,6 +221,7 @@ bunx atlas report publish --producer e2e --junit out.xml --attach ci.log --attac
|
|
|
221
221
|
Files are stored in the layer by content, shown by what they are (pictures, videos, page snapshots, text) and
|
|
222
222
|
offered for download otherwise. [Evidence](extending.md) is the full model, and why a new kind of evidence never
|
|
223
223
|
breaks anything.
|
|
224
|
+
[Send rich evidence](rich-evidence.md) lists what to send and what the viewer does with it, and covers the Playwright Test reporter.
|
|
224
225
|
|
|
225
226
|
### Statecharts
|
|
226
227
|
|
|
@@ -450,11 +450,143 @@ Scoped to one project. The same project may be selected on the unprefixed API us
|
|
|
450
450
|
|
|
451
451
|
Responses: `204` Removed (or it was not there).; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` Not a scratch object, or the token may not.
|
|
452
452
|
|
|
453
|
+
### Every test, with insights, facets and history statistics
|
|
454
|
+
|
|
455
|
+
`GET /api/reports/tests` · role: read
|
|
456
|
+
|
|
457
|
+
The report as a list of tests, for exploring it or for an agent to read in one request. A test is a check identity (its spec path and id) across producers and environments; `outcomes` holds its current result on each environment, and `stats` how it went over its recorded history (pass rate, flips, flaky, durations avg/p50/p95, runs, branches).
|
|
458
|
+
|
|
459
|
+
`summary` and `facets` cover every test (counts by status, by environment, by producer and by domain, the slowest, flakiest and failing tests); `tests` is the page the query matched. Without a published spec set, specs come from the `specs[]` the layers recorded.
|
|
460
|
+
|
|
461
|
+
Send `format=markdown` (or `Accept: text/markdown`) for the same as a Markdown report. The current status of a test is always the report's winner; history never changes it.
|
|
462
|
+
|
|
463
|
+
- `q` (query): Words that must all appear in the title, id, spec, domain, tags, labels, environments or producers. Case does not matter.
|
|
464
|
+
- `status` (query): Current status, or `flaky`. Repeatable or comma-separated.
|
|
465
|
+
- `environment` (query): Only tests with a result on this environment. Repeatable or comma-separated.
|
|
466
|
+
- `producer` (query): Only tests from this producer. Repeatable or comma-separated.
|
|
467
|
+
- `domain` (query): Only tests whose spec is in this domain (`(no spec)` for none). Repeatable or comma-separated.
|
|
468
|
+
- `spec` (query): Only tests of this spec path. Repeatable or comma-separated.
|
|
469
|
+
- `tag` (query): Only tests with this tag. Repeatable or comma-separated.
|
|
470
|
+
- `run` (query): Only tests recorded in this run (a `key` from `/api/reports/runs`).
|
|
471
|
+
- `sort` (query): The order: `spec` (default: domain, spec, title), `title`, `recent`, `slowest`, `failures`, `flaky` or `runs`.
|
|
472
|
+
- `offset` (query): Skip this many matching tests.
|
|
473
|
+
- `limit` (query): At most this many tests (default 500, at most 20000).
|
|
474
|
+
- `format` (query): `markdown` for a Markdown report instead of JSON.
|
|
475
|
+
- `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
476
|
+
|
|
477
|
+
Responses: `200` The catalog.; `304` Unchanged since the `If-None-Match` ETag.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
|
|
478
|
+
|
|
479
|
+
### Every test, with insights, facets and history statistics
|
|
480
|
+
|
|
481
|
+
`GET /api/projects/{project}/reports/tests` · role: read
|
|
482
|
+
|
|
483
|
+
The report as a list of tests, for exploring it or for an agent to read in one request. A test is a check identity (its spec path and id) across producers and environments; `outcomes` holds its current result on each environment, and `stats` how it went over its recorded history (pass rate, flips, flaky, durations avg/p50/p95, runs, branches).
|
|
484
|
+
|
|
485
|
+
`summary` and `facets` cover every test (counts by status, by environment, by producer and by domain, the slowest, flakiest and failing tests); `tests` is the page the query matched. Without a published spec set, specs come from the `specs[]` the layers recorded.
|
|
486
|
+
|
|
487
|
+
Send `format=markdown` (or `Accept: text/markdown`) for the same as a Markdown report. The current status of a test is always the report's winner; history never changes it.
|
|
488
|
+
|
|
489
|
+
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.
|
|
490
|
+
|
|
491
|
+
- `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
492
|
+
- `q` (query): Words that must all appear in the title, id, spec, domain, tags, labels, environments or producers. Case does not matter.
|
|
493
|
+
- `status` (query): Current status, or `flaky`. Repeatable or comma-separated.
|
|
494
|
+
- `environment` (query): Only tests with a result on this environment. Repeatable or comma-separated.
|
|
495
|
+
- `producer` (query): Only tests from this producer. Repeatable or comma-separated.
|
|
496
|
+
- `domain` (query): Only tests whose spec is in this domain (`(no spec)` for none). Repeatable or comma-separated.
|
|
497
|
+
- `spec` (query): Only tests of this spec path. Repeatable or comma-separated.
|
|
498
|
+
- `tag` (query): Only tests with this tag. Repeatable or comma-separated.
|
|
499
|
+
- `run` (query): Only tests recorded in this run (a `key` from `/api/reports/runs`).
|
|
500
|
+
- `sort` (query): The order: `spec` (default: domain, spec, title), `title`, `recent`, `slowest`, `failures`, `flaky` or `runs`.
|
|
501
|
+
- `offset` (query): Skip this many matching tests.
|
|
502
|
+
- `limit` (query): At most this many tests (default 500, at most 20000).
|
|
503
|
+
- `format` (query): `markdown` for a Markdown report instead of JSON.
|
|
504
|
+
|
|
505
|
+
Responses: `200` The catalog.; `304` Unchanged since the `If-None-Match` ETag.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
|
|
506
|
+
|
|
507
|
+
### One test and every result recorded for it
|
|
508
|
+
|
|
509
|
+
`GET /api/reports/tests/history` · role: read
|
|
510
|
+
|
|
511
|
+
The test (as `/api/reports/tests` lists it) and up to 100 recorded results, newest first: status, duration, environment, producer, branch, commit, CI run, layer and the first 500 characters of the detail. Open a result's evidence (attachments, steps, retries) with `/api/reports/evidence/detail/{backend}/{id}?check=<id>`. Send `format=markdown` for Markdown.
|
|
512
|
+
|
|
513
|
+
- `key` (query, required): The test's `key` from `/api/reports/tests`: `JSON.stringify([specPath, id])`, URL-encoded.
|
|
514
|
+
- `format` (query): `markdown` for Markdown instead of JSON.
|
|
515
|
+
- `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
516
|
+
|
|
517
|
+
Responses: `200` The test and its history.; `304` Unchanged since the `If-None-Match` ETag.; `400` No `key`.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No test in the report has that key.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
|
|
518
|
+
|
|
519
|
+
### One test and every result recorded for it
|
|
520
|
+
|
|
521
|
+
`GET /api/projects/{project}/reports/tests/history` · role: read
|
|
522
|
+
|
|
523
|
+
The test (as `/api/reports/tests` lists it) and up to 100 recorded results, newest first: status, duration, environment, producer, branch, commit, CI run, layer and the first 500 characters of the detail. Open a result's evidence (attachments, steps, retries) with `/api/reports/evidence/detail/{backend}/{id}?check=<id>`. Send `format=markdown` for Markdown.
|
|
524
|
+
|
|
525
|
+
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.
|
|
526
|
+
|
|
527
|
+
- `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
528
|
+
- `key` (query, required): The test's `key` from `/api/reports/tests`: `JSON.stringify([specPath, id])`, URL-encoded.
|
|
529
|
+
- `format` (query): `markdown` for Markdown instead of JSON.
|
|
530
|
+
|
|
531
|
+
Responses: `200` The test and its history.; `304` Unchanged since the `If-None-Match` ETag.; `400` No `key`.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No test in the report has that key.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
|
|
532
|
+
|
|
533
|
+
### The runs that recorded the report
|
|
534
|
+
|
|
535
|
+
`GET /api/reports/runs` · role: read
|
|
536
|
+
|
|
537
|
+
Every run, newest first: layers grouped by their CI run URL (else branch and commit), with their environments, producers, how many tests they recorded, their results by status, the time they spanned and up to twenty failures. Send `format=markdown` for Markdown.
|
|
538
|
+
|
|
539
|
+
- `limit` (query): At most this many runs (default 100).
|
|
540
|
+
- `format` (query): `markdown` for Markdown instead of JSON.
|
|
541
|
+
- `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
542
|
+
|
|
543
|
+
Responses: `200` The runs.; `304` Unchanged since the `If-None-Match` ETag.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
|
|
544
|
+
|
|
545
|
+
### The runs that recorded the report
|
|
546
|
+
|
|
547
|
+
`GET /api/projects/{project}/reports/runs` · role: read
|
|
548
|
+
|
|
549
|
+
Every run, newest first: layers grouped by their CI run URL (else branch and commit), with their environments, producers, how many tests they recorded, their results by status, the time they spanned and up to twenty failures. Send `format=markdown` for Markdown.
|
|
550
|
+
|
|
551
|
+
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.
|
|
552
|
+
|
|
553
|
+
- `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
554
|
+
- `limit` (query): At most this many runs (default 100).
|
|
555
|
+
- `format` (query): `markdown` for Markdown instead of JSON.
|
|
556
|
+
|
|
557
|
+
Responses: `200` The runs.; `304` Unchanged since the `If-None-Match` ETag.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
|
|
558
|
+
|
|
559
|
+
### Every test one run recorded
|
|
560
|
+
|
|
561
|
+
`GET /api/reports/runs/detail` · role: read
|
|
562
|
+
|
|
563
|
+
One run and its last result for every test, environment and producer it recorded, failures first. Send `format=markdown` for Markdown.
|
|
564
|
+
|
|
565
|
+
- `key` (query, required): The run's `key` from `/api/reports/runs`, URL-encoded.
|
|
566
|
+
- `format` (query): `markdown` for Markdown instead of JSON.
|
|
567
|
+
- `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
568
|
+
|
|
569
|
+
Responses: `200` The run and its results.; `304` Unchanged since the `If-None-Match` ETag.; `400` No `key`.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No run has that key.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
|
|
570
|
+
|
|
571
|
+
### Every test one run recorded
|
|
572
|
+
|
|
573
|
+
`GET /api/projects/{project}/reports/runs/detail` · role: read
|
|
574
|
+
|
|
575
|
+
One run and its last result for every test, environment and producer it recorded, failures first. Send `format=markdown` for Markdown.
|
|
576
|
+
|
|
577
|
+
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.
|
|
578
|
+
|
|
579
|
+
- `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
580
|
+
- `key` (query, required): The run's `key` from `/api/reports/runs`, URL-encoded.
|
|
581
|
+
- `format` (query): `markdown` for Markdown instead of JSON.
|
|
582
|
+
|
|
583
|
+
Responses: `200` The run and its results.; `304` Unchanged since the `If-None-Match` ETag.; `400` No `key`.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No run has that key.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
|
|
584
|
+
|
|
453
585
|
### The specs a report is laid over
|
|
454
586
|
|
|
455
587
|
`GET /api/reports/outline` · role: read
|
|
456
588
|
|
|
457
|
-
The specs of the newest committed spec set (of `main`, else the newest), so the report needs no checkout. A spec here with no result shows as having none.
|
|
589
|
+
The specs of the newest committed spec set (of `main`, else the newest), so the report needs no checkout. Without a spec set, the specs the layers recorded (`specs[]`), newest per path. A spec here with no result shows as having none.
|
|
458
590
|
|
|
459
591
|
- `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
460
592
|
|
|
@@ -464,7 +596,7 @@ Responses: `200` The outline.; `401` No token, or an unknown one. Send `Authoriz
|
|
|
464
596
|
|
|
465
597
|
`GET /api/projects/{project}/reports/outline` · role: read
|
|
466
598
|
|
|
467
|
-
The specs of the newest committed spec set (of `main`, else the newest), so the report needs no checkout. A spec here with no result shows as having none.
|
|
599
|
+
The specs of the newest committed spec set (of `main`, else the newest), so the report needs no checkout. Without a spec set, the specs the layers recorded (`specs[]`), newest per path. A spec here with no result shows as having none.
|
|
468
600
|
|
|
469
601
|
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.
|
|
470
602
|
|