Skip to content

Generated Reports

aryehcitron@gmail.com edited this page Nov 7, 2026 · 137 revisions

After tests complete, the following files are written to the Reports folder inside bin\Debug\net10.0\ (configurable via ReportsFolderPath).

Output Files Summary

File Purpose Contains Diagrams? Machine-Readable? Always Generated?
Specifications.html Feature specifications with embedded sequence diagrams Yes No Yes
Specifications.yml Machine-readable scenario specifications No Yes (YAML) Yes
TestRunReport.html Full test run report with diagrams, CI metadata, failure details Yes No Yes
TestRunReport.labs.html The cross-run History section, every scenario's sparkline and verdict, and the run's report diagnostics, on a page of their own beside the report (from 5.0.0 the report has History and Diagnostics tabs as well). From 5.0.0 it opens in the report's light or dark theme, drawn in the report's tokens, and runs the theme's two scripts, the one in its head that chooses light or dark and the switch's; before, it had no script. Named after the report. See TestRunReport.labs.html No No When the run read history or recorded a diagnostic (4.6.0), unless GenerateLabsReport = false
TestRunReport.krkl (TestRunReport.json with CompressTestRunReport = false) Machine-readable test run results with PlantUML diagrams and HTTP interactions, compressed whole from 5.0.0 Yes (PlantUML source) Yes (JSON) Yes
TestRunReport.schema.json JSON Schema / XSD for the test run data file, for validation and IDE auto-complete; written only with GenerateTestRunReportSchema = true from 5.0.0 No Yes Yes
ComponentDiagram.html C4-style component diagram showing service relationships Yes No When GenerateComponentDiagram = true
DiagnosticReport.html Setup troubleshooting: the configuration, log counts per service and per test, unpaired requests, orphaned test ids, scenarios with no logs and the activity sources seen. See Diagnostics and Debugging No No When DiagnosticMode = true
CiSummary.md CI-friendly markdown summary with failure details Inline PlantUML Partially When WriteCiSummary = true
Failures.md Every failure in context, small enough to read whole: the error, expected/actual, the steps around it, the calls it made, and the address to query next No Partially Yes (3.1.0) — # No failures when nothing failed in the newest run (3.27.0: when the run it replaced had failures, the next line says so and where that run is kept)
Failures.jsonl The same failures, one JSON object per line, under a header line No Yes (JSONL) Yes (3.1.0) — the header line alone when nothing failed
CLAUDE.md, AGENTS.md Byte-identical instruction files telling an agent what these files are and how to query them — two names because Claude Code reads one and Codex, Cursor and Copilot read the other No No Yes (3.1.0)
query.cs kronikol query with nothing installed and no network: dotnet run --file query.cs -- summary . from this directory runs the engine that wrote the report (Querying Reports) No No (C#) Yes (3.32.0) — beside a JSON data file, unless WriteQueryScript is off or the directory is inside a C# project's folder
Specifications.md The specification as prose — every feature, rule, scenario and step, with no run data at all No Partially When GenerateSpecificationsMarkdown = true (3.1.0)
ctrf-report.json The run in Common Test Report Format, for CI tooling that already speaks it No Yes (JSON) When GenerateCtrfReport = true (3.1.0)
service.json The service card: the run's parties, every operation between them and why each call was made, with a hash that an indexer compares across runs; the card kronikol card writes from this run's data file No Yes (JSON) When GenerateServiceCard = true, the default, and the data file is JSON (5.22.0)
History.run.json The run's own line of cross-run history with the roster it needs — what kronikol history record folds into the ledger, and what kronikol query history reads as the run's line. See Cross-Run History No Yes (JSON) Yes (3.9.0) — unless KRONIKOL_HISTORY=off or GenerateHistoryFragment = false
Run.json What this run is and what it wrote: its run id, when it ended, the suite, how many scenarios and how many failed, and the files and attachments that reached disk. Written last, so a directory without one is a run that did not finish. See Earlier runs No Yes (JSON) Yes (3.27.0)
runs/<run>/ The runs before this one, each whole. See Earlier runs - - When KeepRuns > 0: 3 off CI, 0 on CI by default (3.27.0)

Compressed (5.6.0+): TestRunReport.html and Specifications.html are each a small page that holds the report as zstd frames and opens it in the browser. A text search or an HTML parser of the file finds nothing in it, and kronikol decode TestRunReport.html -o plain.html writes the plain page. See The compressed page.

A merged run (kronikol merge) writes its own TestRunReport.html and data file (TestRunReport.krkl when any input is one), and from 5.22.0 a service.json made from that data file; see Merging Parallel Reports.

Data format: Specifications data defaults to .yml (SpecificationsDataFormat); TestRunReport data defaults to .json (TestRunReportDataFormat). Set either to DataFormat.Json, DataFormat.Yaml or DataFormat.Xml to change it.

File names: All file names are configurable via HtmlSpecificationsFileName, HtmlTestRunReportFileName, and YamlSpecificationsFileName on ReportConfigurationOptions. A kronikol query given a folder finds a .krkl of any name, but a JSON data file only when its name ends TestRunReport.json, so with a custom HtmlTestRunReportFileName every command the run prints, and every command in Failures.md, names the data file itself (kronikol query failures Checkout.krkl; Failures.md since 4.20.6).


Specifications.yml

A YAML specification document listing all features and scenarios. The ReqNRoll and BDDfy adapters additionally include the BDD steps (Given/When/Then/And/But) for each scenario.

A scenario's background steps are emitted as a BackgroundSteps collection alongside Steps, matching the shape the TestRunReport data files already used. The YAML writer and Specifications.xml (<BackgroundSteps>) omit the collection entirely when a scenario has none; Specifications.json always emits backgroundSteps, as an empty array when there are none. (Before v3.0.48 these files dropped background steps altogether, although the HTML report showed them.)

Each feature and each scenario lists the tags written at its own level (Tags and Attributes › Tags): Tags in Specifications.yml and Specifications.xml (<Tags><Tag>…</Tag></Tags>), tags in Specifications.json. Before 5.0.0 these were Labels and Categories, so a committed copy of one of these files changes on the upgrade.

Specifications.html

An HTML specification file with embedded sequence diagrams for each test scenario. Features and scenarios are grouped hierarchically.

Behaviour: If any test in the run has failed, or any scenario's result is a default rather than a verdict (since 4.11.1: a test that started and never ended, or a test on xUnit v2's collection-fixture path, which sees no results), this file is written as blank. This prevents publishing stale or misleading specifications when the test suite is not fully passing.

Specifications.md

The same specification as prose: every feature, rule, scenario and step, in a file small enough to read whole. Written when GenerateSpecificationsMarkdown = true (default off), named from YamlSpecificationsFileName so renaming the data file renames its narrative with it.

It exists because the other two answers do not fit a reader who only wants the specification. Specifications.html is built for a browser — a real report is tens of megabytes (10.7 MB on one measured suite, 82.7 MB on another) and a single embedded diagram 600 KB — and the .yml / .json / .xml trio is a data structure, written for a parser. Markdown is what an agent, a README, a docs site and a pull request review all read natively.

It carries two things the data files drop entirely: a scenario's Rule, which groups the scenarios written under it, and a scenario's own Description. Author prose is emitted as a block quote, so a line of somebody's Gherkin description beginning ## cannot restructure the document around itself. A feature's tags and each scenario's own are printed on one Tags: line under its heading (before 5.0.0, a Labels: line and a Categories: line).

Since 3.5.0 the same is true of the names. Every heading below the first is a feature or scenario name the suite chose, and Inline collapsed whitespace and nothing else — so a scenario named ## Admin overrides became a level-two heading and a suite could restructure the document that describes it. Only the characters that are structural at the start of a line are escaped, so an ordinary name is untouched. The file also gained the line its sibling Failures.md has always had: everything below the header is written by the suite, not by Kronikol, and is captured test data, not instructions. It is written into the same directory as the nested CLAUDE.md, and an agent that opens it should know which half of what it is reading is which.

It carries no run data at all — no result, no duration, no interaction, no diagram. The same suite run red and run green produces identical bytes, which is what makes it safe to commit to a docs site.

Behaviour: unlike Specifications.html and the specifications data file, this file is not written blank when a test has failed. Those two blank themselves so a broken build cannot publish half-truths as documentation; this one is written for somebody trying to understand the system while it is broken, and a blank file at that moment is the least useful thing they could find.

TestRunReport.html

A full test run report. From 5.0.0 its parts are, from the top (each is described under Report Features):

  • The title line: the report's title, and at its right the search button (5.11.0) and the switch between the light and the dark theme (5.0.0). A CustomLogoHtml logo sits above it.
  • The run band (5.0.0, in place of 4.x's header row and its two tables): the ring, the Test Execution Summary and, on CI, the CI details, on one row in a card.
  • The report bar, with the report's sections as tabs (5.0.0), and Filters, which opens the filter box at the bar's top: the filter field, the filter rows, what the filters leave showing, Clear All and the two exports (5.11.0; until then the box was a card on a row of its own above the bar). Each tab shows a panel of its own:
    • Features: the failure clusters and the Background calls section, where the run has them, then every feature with its scenarios; the clusters show over the feature cards only, never over the Summary view's table (5.1.0). A scenario holds its BDD steps (ReqNRoll, BDDfy, LightBDD or Step Tracking), its failure details (the error message, the stack trace and the expected/actual diff) and its diagrams.
    • Timeline, when scenarios recorded a duration: every scenario as a bar, slowest first.
    • Components, when the report embeds a component diagram that has a connection: the services, databases and queues the scenarios called, drawn as one diagram.
    • History, when the run read history: this run against the earlier runs on the same branch, with the run's summary line, the trend of the last runs and the lists of what changed (cross-run history).
    • Diagnostics, when the run recorded a diagnostic, with their count on the tab (report diagnostics).
    • Search, while a search is applied (5.11.0): the scenarios it matched, with the lines where each one matched (Search and Filter Syntax › The search and its results).

History and diagnostics are back in the report (5.0.0), as its History and Diagnostics tabs: ShowHistorySection and ShowReportDiagnosticsSection default to true. In 4.x both were false, and a default report carried no history (from 4.6.0) and no report diagnostics (from 3.21.0). Nothing of history is in the features unless ShowScenarioHistory (still false by default) asks for each scenario's sparkline and verdict, and a run that read no history and recorded no diagnostic carries neither anywhere in the file. The labs page beside the report carries both, as before (TestRunReport.labs.html). kronikol ingest shows the Diagnostics tab by default too: --no-diagnostics-section leaves it out, and --diagnostics-section, now the default, is still taken, so a command line written for 4.x runs unchanged. kronikol merge renders its report with the defaults, so a merge given --history has a History tab, and one whose shards recorded a diagnostic has a Diagnostics tab. See Report Configuration.

TestRunReport.labs.html

Since 4.6.0. Cross-run history and the run's report diagnostics on a page of their own beside the report, with each scenario's sparkline and verdict, which the report itself draws only with ShowScenarioHistory. From 4.6.0 to 4.x a default TestRunReport.html left all of these out; from 5.0.0 the report has cross-run history and the run's report diagnostics as its History and Diagnostics tabs, and this page carries both, as before. The page is named after the report, {HtmlTestRunReportFileName}.labs.html (Checkout.labs.html beside Checkout.html), and says under its title: "Labs: views still being designed. Their layout may change in any release." It holds:

  • a header: the report's title with a "Labs" badge and, at its right, the report's light/dark switch (5.0.0), then the suite, the run id, when the run ended, and, when a report is written, a link back to it;
  • the History section, open, when the run read history;
  • Every scenario, collapsed, when the run read history: each scenario's sparkline, verdict (stable when there is nothing to say) and evidence, in report order;
  • the Report diagnostics list, open, when the run recorded a diagnostic.

From 5.0.0 the page opens light or dark as the report does and draws its parts in the report's tokens: the table of every scenario is a card, as the History section and the diagnostics are, and History's pass rate is a ring per run, as in the report (Report Themes). Of the report's scripts it runs only the theme's two: the one in its head that chooses light or dark before anything is drawn, and the switch's. Before 5.0.0 the page had no script.

It is written when the run read history (with EmbedHistoryInReport on) or recorded a diagnostic, and only then; GenerateLabsReport = false turns it off. Its scenario names link into the report by TestRunReport.html#sid-<stableId>, which opens that scenario on load, on the Features tab (Deep links); with GenerateTestRunReport = false they are plain text. It is listed in Run.json and moves with its run under runs/ (Earlier runs), and the run-end pointer lists it after the report. kronikol ingest writes it too, and kronikol merge writes <output>.labs.html beside the merged report.

The compressed page (5.6.0+)

From 5.6.0 TestRunReport.html and Specifications.html are written compressed, by a run, kronikol merge and kronikol ingest alike, with no option to turn it off. Nothing a reader of the report does changes. What changes is for whatever reads the file itself: a text search, an HTML parser, a browser test (below). The component diagram page, the labs page and the diagnostic pages stay plain, and the data file (TestRunReport.krkl) is as it was.

The file is a small loader page. Its head carries <meta name="kronikol-page" content="frames 1" />, and its body, after the loading screen:

  • <script id="kron-frame-page" type="application/octet-stream" data-codec="zstd" data-length="N">: the report page as one zstd frame in base64, N being the length it decodes to;
  • <script id="kron-frame-diagrams-1" data-kron-piece="1" …>, then -2 and on: the diagram sources, cut between sources into pieces of at most 16 MB (a larger source has a piece of its own), a frame each;
  • <script id="kron-diagram-index" type="application/json">: each source's length, piece by piece;
  • <script id="kron-zstd">, the decoder, and <script id="kron-loader">, the loader.

Each frame is zstd at level 9 with a window of 2^23 bytes (8 MB), and carries its checksum and the length it decodes to. Inside the page frame every item is stored as it is: each #puml-data value is a token pointing into the diagram index, #step-data (a held step's sub-steps) holds HTML, the #iflow-segments element's z holds the segments' JSON as a string, and data-flame-z and data-plantuml-z hold their text attribute-escaped. Only the deep-search index, #kron-search-index, stays gzip and base64, byte for byte what 5.5.4 writes.

In the browser the loader paints a loading screen (Opening the report…), decodes the page frame and puts the report in its place with document.open, write and close. A diagram's source is decoded from its piece the first time the diagram is drawn, in a Web Worker, or on the page itself where workers are refused. The decoder is the browser's own DecompressionStream('zstd') where it has one (Firefox with the preference dom.compression_streams.zstd.enabled), else libzstd's decoder built to WebAssembly (from zstddec 0.3.4), else fzstd 0.1.1 where the page's policy refuses WebAssembly.

  • Slow to open: after 4 seconds the loading screen adds what to do if the report never opens, naming kronikol decode and the file.
  • Damaged: a page frame that does not decode to what was written (a damaged file, a wrong checksum, a wrong length) is refused. The loading screen says what failed and how to write a plain copy, and the browser's console says the same, so the page is never left blank.
  • JavaScript off: the page says the report needs JavaScript to open, and names kronikol decode.

A plain copy: kronikol decode

kronikol decode TestRunReport.html -o plain.html writes the plain page: the report as a page written before 5.6.0 holds it, every item in gzip and base64 as before, which a browser opens as it is and which grep or an HTML parser reads. Without -o it writes to stdout. A plain page given to it is written as it is; a damaged page is one error (exit 1), and nothing is written; --indent, which is for the JSON, is refused for a page (exit 2). Given a directory, kronikol decode still reads the data file in it (TestRunReport.krkl or .json), not the page. See Querying Reports › A report page. Code that parses a page's markup itself, such as an HTML parser in a test, reads the plain page with HtmlReportFile.ReadPlain(path) (API Reference).

Even the plain page holds the diagram sources, and the bodies their notes show, in gzip and base64, as every page before 5.6.0 did (Diagram Storage Format). To find text a run captured, ask the data file: kronikol query grep <reports-dir> "<text>".

A script or a test that opens the page in a browser

The file's own load event fires on the loader page, before the report is in place, so a script that reads the page once it has loaded reads the loading screen. Wait for window.__kronikolReady === true instead; with Playwright:

await page.GotoAsync(reportUrl);
await page.WaitForFunctionAsync("() => window.__kronikolReady === true");

The document's kronikol:ready event fires at the same moment, when the report's DOMContentLoaded fires and before the report's own listeners run, for a script inside the report. A listener added before the report is in place, as a browser automation's init script adds one, is erased by document.open: such a script polls window.__kronikolReady, or adds its listener after Document.prototype.open runs.

TestRunReport.json (Machine-Readable Test Run Data)

A machine-readable structured data file containing the complete test run results, including PlantUML diagram source and HTTP interaction traces for each scenario. This file is designed for automated debugging, CI tooling, and programmatic analysis.

Data format: The default format is JSON (TestRunReportDataFormat = DataFormat.Json). Set to DataFormat.Yaml or DataFormat.Xml to produce .yml or .xml files instead.

Compressed (4.32.0+, the default from 5.0.0): with CompressTestRunReport = true, the default from 5.0.0, the run writes TestRunReport.krkl in place of TestRunReport.json; set it to false for the JSON. It holds the JSON this page describes, written compact, with every payload as text (formatVersion 3), and a key whose value is null, [] or {} is left out unless the schema requires it, so a reader takes a missing key as holding nothing. The whole is one zstd frame behind a 24-byte marker, about a twentieth of the JSON. Only Kronikol's tools read it: kronikol query and every other reader from 4.30.0, and kronikol decode writes the JSON out for a script that needs the JSON itself (Querying Reports › Compressed reports). A mergeable run writes its mergeable file the same way.

Escaping (3.34.2+): strings carry only the escapes JSON needs. A quote inside a string is written \", and <, >, &, ', + and non-ASCII text are written as they are. Until 3.34.2 each was a six-byte \uXXXX escape, and on one measured report the escaped quotes alone were a fifth of the file. Every JSON parser reads the same values as before, and every b: address kronikol query prints is unchanged, since it is a hash of the decoded text.

Reading it by hand — or with an agent — is not the way in. A real report is tens of megabytes (10.7 MB and 82.7 MB on two measured suites), with single embedded diagrams past 600 KB. Use kronikol query, which answers questions about the file without loading it. This page documents the contract; that page documents how to use it.

Compressed payloads (3.35.0+)

By default from 4.0.0, and before it with ReportConfigurationOptions.CompressTestRunReportPayloads (or kronikol ingest --payloads compressed), a large payload is written compressed in its place: an interaction's content, or an entry of a scenario's diagrams, of 8,192 characters or more (512 in 4.0.0 and 4.0.1) becomes

"content": {
  "$h": "b:4bdea521",
  "$n": 18342,
  "$z": "H4sIAAAAAAAA..."
}

unless that would not make it smaller. $z is the base64 of the gzip of the text's UTF-8, $h is the text's b: address (the one kronikol query prints, so an address does not move with the option) and $n its length in UTF-16 code units. The file stays JSON: jq, a schema validator and a script read it as before, and only a reader that wants a body's text has to inflate $z (base64 -d | gunzip, or gzip.decompress(base64.b64decode(z)) in Python). A file that holds a compressed payload declares formatVersion 2, which kronikol query and kronikol merge older than 3.35.0 refuse with a message to upgrade, instead of reading a body as missing. kronikol query, kronikol merge, the query.cs beside the report and the skill's query.py of 3.35.0 read both forms, and kronikol merge writes the merged file compressed when any shard was. On BreakfastProvider's xUnit lane the option took the file from 5,855,252 bytes to 4,344,734 (74%); a report whose bodies are larger compresses further.

Schema

The top-level structure contains the test run time window and a list of features with nested scenarios:

{
  "formatVersion": 1,
  "kronikolVersion": "3.1.0",
  "suite": "Orders.Tests.Component",
  "startTime": "2026-01-01T10:00:00Z",
  "endTime": "2026-01-01T10:05:00Z",
  "features": [
    {
      "name": "Order Management",
      "endpoint": "/api/orders",
      "description": "...",
      "tags": ["api"],
      "scenarios": [
        {
          "id": "test-runtime-id",
          "name": "Place order successfully",
          "result": "Passed",
          "durationSeconds": 1.234,
          "isHappyPath": true,
          "errorMessage": null,
          "errorStackTrace": null,
          "failureCause": null,
          "tags": ["smoke", "Integration"],
          "steps": [
            {
              "keyword": "Given",
              "text": "a valid order request",
              "status": "Passed",
              "durationSeconds": 0.1,
              "failureMessage": null,
              "sourceFile": null,
              "sourceLine": null,
              "parameters": [],
              "docString": null,
              "comments": [],
              "subSteps": []
            }
          ],
          "annotations": [
            { "index": 4, "kind": "Row", "text": "Row 3" }
          ],
          "diagrams": [
            "@startuml\nTestClient -> OrderService : POST /api/orders\nOrderService --> TestClient : 201 Created\n@enduml"
          ],
          "httpInteractions": [
            {
              "type": "Request",
              "method": "POST",
              "uri": "https://api.example.com/orders",
              "serviceName": "OrderService",
              "callerName": "TestClient",
              "content": "{\"item\":\"widget\",\"quantity\":1}",
              "headers": [
                { "key": "Content-Type", "value": "application/json" }
              ],
              "statusCode": null,
              "statusText": null,
              "traceId": "guid",
              "requestResponseId": "guid",
              "timestamp": "2026-01-01T10:00:01.000Z",
              "metaType": "Default",
              "dependencyCategory": null,
              "callerDependencyCategory": null,
              "phase": "Action",
              "isUserAction": false,
              "activityTraceId": "0af7651916cd43dd8448eb211c80319c",
              "activitySpanId": "b7ad6b7169203331",
              "capturedBy": null,
              "durationMs": 50.0,
              "stepPath": "0"
            },
            {
              "type": "Response",
              "method": "POST",
              "uri": "https://api.example.com/orders",
              "serviceName": "OrderService",
              "callerName": "TestClient",
              "content": "{\"id\":42}",
              "headers": [
                { "key": "Content-Type", "value": "application/json" }
              ],
              "statusCode": 201,
              "statusText": "Created",
              "traceId": "guid",
              "requestResponseId": "guid",
              "timestamp": "2026-01-01T10:00:01.050Z"
            }
          ]
        }
      ]
    }
  ]
}

Root fields

Field Description
formatVersion The version of the file's shape, written as the first key so a reader can check the contract before parsing anything that depends on it. 3 in a TestRunReport.krkl (4.32.0+, where a key that holds nothing may be left out and no payload is compressed in its place), 2 when the file holds a compressed payload (3.35.0+, [[compressed payloads
kronikolVersion The build that produced the run, not the one doing the writing — so a file written by kronikol merge names the version the tests actually ran on.
suite The test suite this run belongs to (3.1.0). Every stableId in the file is scoped to it, and it is written out so the hash input is inspectable rather than implied. Resolved from the test assembly that produced the run, or set explicitly with SuiteName. null when it could not be resolved, in which case ids are unscoped and byte-identical to the ones Kronikol minted before 3.1.0.
background The interactions that belong to no scenario (3.17.0): calls (a request and its response count once), afterScenarioEnd (per scenario: scenarioId, scenario, calls, lastAt) for calls that inherited a scenario's identity after it had ended, and interactions in the same shape as a scenario's httpInteractions, each with attributionSource: "Expired" and expiredFrom. Always present; calls is 0 when there were none. XML writes <Background> and omits the two containers when empty; YAML writes Background:. See Background-Thread-Correlation.
serviceId, parties Who each party is (5.14.0), written only by a run that declares something ([[Report Configuration › Parties
shapeTemplates The run's HistoryShapeTemplates in the order given (5.19.0), each as pattern and placeholder, after parties and only when the run has rules, so a report without them is the same bytes as before. kronikol query reads them: interactions --group-by route and the call pairing of diff --body and compare key a call under them as the run's history did, and a merge keeps them when every shard records the same ones. A report written before 5.19.0 carries none, whatever its run had. TestRunReport.xml writes ShapeTemplates of ShapeTemplate elements (Pattern, Placeholder) after Parties, TestRunReport.yml ShapeTemplates:, and a mergeable file writes it last.

TestRunReport.xml and TestRunReport.yml open with the same three, as FormatVersion, KronikolVersion and Suite. Both omit Suite entirely when there is none, where JSON writes it null.

Key Fields

Field Description
diagrams Array of PlantUML source text strings for this scenario. A scenario may have multiple diagrams (e.g. when using focused views). Empty array when no diagrams are available.
httpInteractions Array of tracked HTTP request/response log entries for this scenario, in chronological order. Each entry includes the full method, URI, headers, content body, service names, status code, and timestamps. Diagram markers (step bars, assertion notes, custom PlantUML fragments and the Setup/Action boundary) are excluded — they belong to the sequence diagram and carry no interaction of their own.
traceId Groups a request and its corresponding response together. This is Kronikol's own identifier, not a W3C trace id — for that, see activityTraceId.
requestResponseId Unique identifier for each individual log entry.
stableId Deterministic cross-run identifier: the run's suite (3.1.0), feature name, outline id, scenario display name and — since 3.0.47 — the scenario's example values. Use it to match the same test across runs. The example values were added because without them every row of a scenario outline hashed identically, so a cross-run diff could not tell row 1 from row 3. Parameterised scenarios therefore have different stableIds from 3.0.47 onwards. The suite was added because two projects that both have a Cake feature with an Order a cake scenario minted the same id, so anything that combines reports — kronikol merge, an ingest folding two runners, a cross-run ledger — silently paired unrelated scenarios. Measured before the change: 145 of 905 ids collided across suites, and 0 of 1,043 within any single report — it is a correctness fix for combined reports, not a live defect in an ordinary run. Both changes are one-off discontinuities for anything storing ids historically, so a #sid- link written before 3.1.0 no longer resolves against a run whose suite resolves. A Reqnroll outline row is named with its title's placeholders filled from its values (5.4.3), and its id hashes the title as written in place of that name, the name the row bore before, so its id did not change; such a row carries keyedByOutline. Where a lane shows a scenario's name otherwise than 5.16.4 did (5.16.5: a title made from a method's name keeps its words in capitals, and a name the test wrote is shown as written), the id is computed from the name 5.16.4 showed and from the outline id and example values read from it then, which the file writes as stableIdName, stableIdOutlineId and stableIdExampleValues, so no id moved.
annotations Diagram markers carrying information found nowhere else in the file: which row of a tabular input was in flight (kind: "Row"), and fragments injected with DefaultTrackingDiagramOverride.InsertPlantUml (kind: "Custom"). index is the position in httpInteractions the marker preceded. Step and assertion markers are deliberately absent — those are already structured in steps.
backgroundSteps The scenario's background steps (3.0.48+), shaped exactly like steps. Kept as a sibling of steps rather than merged, preserving the b{i}/{i} split that stepPath addresses and interaction attribution use. Always present in JSON (an empty array when the scenario has none); the YAML and XML writers omit it when empty. Declared in both the JSON schema and the XSD.
endedAt When the scenario finished, millisecond UTC, as the framework recorded it (3.17.0); null when the adapter did not record one. A call that inherited this scenario's identity after this instant is background, not the scenario's.
attempt Which run of the scenario produced its result when the runner retries, 1-based: 2 is the first retry. Set by the Cucumber Messages lane and, from 4.2.0, by a tests NDJSON's attempt or its repeated start records (Ingesting-External-Captures); null where the runner reports nothing about attempts. The HTML report shows a later attempt as one retry badge, retry 2 for attempt 3 (5.0.0; the ingest lanes wrote a retry N label per earlier attempt before).
keyedByOutline true on an outline row whose name was filled from its example values, a Reqnroll row (5.4.3): its stableId, its place among its feature's scenarios and its in-page link key on outlineId, the title as written, in place of name, as they did when the row was named by the title. Written only where it is true, so kronikol merge computes the ids the shard wrote.
nameAsWritten true on a scenario whose name the test wrote and the report shows as written: a Gherkin scenario (5.9.1) and, since 5.16.5, a DisplayName, an NUnit TestName or an MSTest DisplayName. Its outline's group is headed by its title as written, and a name ending in brackets is not read as a row's arguments. Written only where it is true.
stableIdName The name the scenario's stableId is computed from, where that is not its name (5.16.5): the name 5.16.4 showed for it, so its id, its history and its #sid- link are as they were. stableIdOutlineId and stableIdExampleValues are the outline id and the example values the id was computed from, written beside it whenever the id was computed with them: beside stableIdName, their absence means the id hashes none. Written only where they apply, so kronikol merge computes the ids the shard wrote.
stableIdName, nameGenerated (feature) A feature's stableIdName is the name its scenarios' ids are computed from, where that is not its name: the name 5.9.0 showed, for a feature 5.9.1 shows as written, and since 5.18.0 the name before TitleWords spelled its words. nameGenerated is true on a feature whose name its lane made from a class's or a library's name (since 5.18.0), the names TitleWords rewrites; kronikol merge pairs two such features by stableIdName, else name, so a shard written with the list and one written without it are one feature. Each is written only where it applies.
outlineTitle The title the scenario's lane gives the outline it is a row of, where that is not the row's name less its arguments (5.16.5): a theory's DisplayName or its method's title, a LightBDD name format as written. The report heads the outline's group with it.
notATest true on a scenario that is not a test, such as the one an ingest folds unattributed traffic into (5.2.6): it is drawn like any scenario, and the run's counts and times, the CI summary, Failures.md, the CTRF report and the specifications leave it out. Written only where it is true, so every other scenario's bytes are as they were; TestRunReport.xml and .yml write NotATest, and the XSD takes it as optional. See [[Ingesting External Captures › The scenario that is not a test
warmUpSeconds How much of the scenario's durationSeconds was the run's first-call warm-up (4.8.0): the time its marked calls (warmUp, below) cover, each instant counted once and never more than durationSeconds, so durationSeconds - warmUpSeconds is the scenario's own time. Written only on a scenario that carries such a call; TestRunReport.xml and .yml write it as WarmUpSeconds, and the XSD takes it as optional.
sourceFile, sourceLine (scenario and feature) Where the scenario is written: a project-relative path with forward slashes and the declaration's line. From the Gherkin document on the BDD lanes and, from 4.2.0, from a tests NDJSON start record's sourceFile and sourceLine; the feature's is the first one seen. null on the unit-test adapters.
tags (feature and scenario) The tags written at that level (5.0.0): a feature's own, and a scenario's own (in Gherkin, its own, its rule's and its Examples: block's). A scenario's tags in the report are its feature's, then these; a feature's tags is written before its scenarios. TestRunReport.xml and .yml write Tags (<Tags><Tag>…</Tag></Tags> in XML). Before 5.0.0 a feature carried labels and a scenario labels and categories: Kronikol.Tool 5.x reads those as tags, and a 4.x tool reads a 5.0.0 file as untagged. See [[Tags and Attributes › Tags

Interaction fields

Everything the sequence diagram knows about a call travels with it in the data file:

Field Description
statusCode, statusText The outcome, as a number and as a label (3.1.0). statusCode is an integer or null; statusText is the label or null. An HTTP response carries both (400 + "BadRequest"); a broker acknowledgement or a cache hit carries text only (null + "Ack", null + "Hit"); the request half of a pair carries neither. Before 3.1.0 there was one field holding the .NET enum name when one existed ("OK", "BadRequest") and raw text otherwise, so its type depended on whether .NET happened to name the code — which is why kronikol query --status 5xx matched nothing on any HTTP call. kronikol query reads both shapes, so a report written before 3.1.0 still answers.
metaType Default for a call: a database query, an HTTP or gRPC call, settling a message, managing a topic or a queue. Event for an operation that moves a message: it hands one to a broker, reads one from it or delivers one to a consumer, whatever came back. See Event-Annotations.
dependencyCategory, callerDependencyCategory What kind of thing each end is — database, cache, queue — which drives participant shape and arrow colour in the diagram.
phase Setup, Action, or Unknown when phase detection is off. See Phase-Aware-Tracking.
isUserAction A UI interaction (click, navigate) rather than a dependency call.
activityTraceId, activitySpanId The W3C ids — the bridge from a report to your OpenTelemetry traces and application logs. Unlike traceId, which is Kronikol's own pair identifier. Consumed by kronikol query trace, which follows one id across the whole run and flags an id leaking across scenarios. A Kafka consume's activitySpanId (4.17.0+) is the span of the producer that sent the message, read from its traceparent, not a span of its own. On a call TestTrackingMessageHandler sends over a socket, the response's activitySpanId (4.26.5+) is the span its transport started for the call, the one it sent in the call's traceparent, while the request's is the span current when the handler saw the call, inside a host the host's request span. Most database and SDK trackers record neither id.
capturedBy Which capture path produced the entry: wire (a proxy or TCP tap) or span (an OpenTelemetry receiver). Matters when capture fidelity is itself the bug.
attributionSource, expiredFrom How the call got its scenario (3.17.0): RequestHeader, TestContext, Scope, GlobalFallback, Detached, None, DocumentOwner (3.19.0: nothing named a scenario, but the call named a document a scenario had written earlier in the run, and it is attributed to that writer for this one call), DocumentFlow (3.20.0: a detached flow had just written a scenario's document and this call came between that write and the flow's next operation on the document, and was held until that operation confirmed it), or Expired when it inherited a scenario's identity after that scenario had ended and was moved to the root background block, expiredFrom naming the scenario. null on a capture that predates the mark.
error When the call threw instead of answering (3.18.0): the exception's message chain (the outer message, then each inner one after Caused by:). The exception's type is statusText behind a bang (!HttpRequestException) and statusCode is null; for Kafka (4.16.2+) it is Confluent's error code behind a bang (!Local_MsgTimedOut), for a failed delivery report too. null on the request half and on a call that answered. From 4.3.0 an ingested capture's error fills it too (Ingesting-External-Captures), kronikol merge keeps it, and Failures.md prints it under the calls table.
durationMs Wall-clock milliseconds for the call, derived from the request and response timestamps — or taken verbatim when the capturer measured it and sent durationMs (see Ingesting-External-Captures). Repeated on both halves of a pair; null when the request went unanswered, and when both halves carry one instant because the call was recorded after the fact (LogPair since 4.7.3; a Kafka consume, subscribe, commit, flush or transaction entry since 4.16.2, while a produce measures its broker round trip).
warmUp On the request record of a call the run's first-call warm-up slowed down (4.8.0): see Why is my first scenario slow?. For each kind of call a test makes (its service, its method and its path with ids folded, or a statement's templated head), the run's first call is marked when it took at least 10 times and 50 ms more than the median of the later calls of its kind, and so is a call that started while it ran and waited for it. kind is first or waited, shape the kind of call (POST /orders/{id}), baselineMs and baselineCalls the median of the later calls and how many it is over, and first, on a call that waited, the requestResponseId of the call it waited for. Absent on every other record.
stepPath Which step the call happened under: an index into the scenario's steps, b-prefixed for a background step (b0, 0, 1), dotted for a sub-step (0.1, since 5.30.0). Where a framework's adapter collected the steps, a call made in a step names that step, whatever else ran. null before the first step, in a step the report does not list (a BDDfy example table's later rows, a BDDfy tear down), and from 5.29.4 for a record from a service's own thread while several steps ran that share none, which waits on no call of the test's. Steps read against their bars (an ingest, steps built by hand, or steps an adapter could only pair by position) are null as well where a step bar does not match its step, up to the next bar that does, which raises a StepAttributionMismatch diagnostic rather than guessing, and there a record a background thread enqueues after the following step's bar reads that step, as nothing in the bars can tell. A response reads its request's step (5.28.4).
testDouble What answered the call in its service's place, when its capturer saw a test double answer it (5.15.0): MSW, WireMock, a mock handler's type name. The tracking handler writes it from its chain, or as its TestDouble option says (5.32.0). The call's last member, written only when set; a call without it is unknown, not the real service's. A party every call to which carries it is a double ([[Report Configuration › Parties

Step fields

Field Description
failureMessage Why the step or assertion failed. Present on both the standard and mergeable files regardless of TestRunReportFullStepDetail — the smaller file exists to save payload bytes, not to withhold why a test failed.
sourceFile, sourceLine Where a tracked assertion lives, when the caller supplied it (see Assertion-Tracking); from 4.2.0 also an ingested step or assertion record's call site, and a Gherkin step's line in its feature file. File name only.
parameters, textSegments The step's inputs: inline values, data tables with columns and rows, tree values. Present unless TestRunReportFullStepDetail is turned off.
docString, docStringMediaType, comments, bypassReason The rest of the step's Gherkin detail.

Note: The diagrams, httpInteractions and annotations fields are only present when diagram/tracking data is available (i.e. when CreateStandardReportsWithDiagrams is used). When calling GenerateTestRunReportData directly without passing diagrams or logs, these fields are omitted.

Schema File

A formal schema file is generated alongside the data file to enable validation and IDE auto-complete:

Data Format Schema File Schema Standard
JSON TestRunReport.schema.json JSON Schema 2020-12
XML TestRunReport.schema.xsd W3C XML Schema (XSD)
YAML TestRunReport.schema.json JSON Schema (YAML has no separate schema standard)

The schema file is always generated in the same Reports folder as the data file. It defines:

  • All field names in the casing the data file uses (camelCase for JSON, PascalCase for YAML and XML)
  • Field types (string, number, boolean, array, object)
  • Enum values for result and status fields (Passed, Failed, Skipped, Bypassed, SkippedAfterFailure)
  • Recursive step/subStep definitions
  • The full HTTP interaction structure including headers
  • formatVersion among the required root keys, alongside startTime, endTime and features (3.1.0) — so a report written before the shape was versioned does not validate against the current schema, which is the honest answer rather than a silent pass
  • additionalProperties: false on every object with a fixed set of keys (3.4.0), so a field the schema does not declare is a validation error rather than something silently allowed. A schema without it permits every key it has not heard of, which is the opposite of what a contract is for.

Two nodes are deliberately left open, and a validator will not complain about what is inside them:

Node Why it is open
internalFlowSegments Mergeable files only. kronikol merge re-serialises these payloads verbatim from the shard files it read, which a different Kronikol version may have written. Pinning them would make a merge emit a file that fails its own schema with no code change on either side.
exampleValues A caller-supplied map of example parameter names to values. The keys are the test author's.

A report written with GenerateMergeableData carries five extra root keys — mergeableFormatVersion, wholeTestVisualization, componentRelationships, internalFlowSegments and wholeTestFlow — under the same file name, with the same schema beside it. They are declared as optional properties of the one document, so both files validate against the schema they ship with.

You can also generate a schema file independently:

ReportGenerator.GenerateTestRunReportSchema("TestRunReport.schema.json", DataFormat.Json);
ReportGenerator.GenerateTestRunReportSchema("TestRunReport.schema.xsd", DataFormat.Xml);

Earlier runs: Run.json and runs/

Since 3.27.0. A test fails, you re-run it, it goes green, and the report of the run that failed is gone: every file in the reports directory is overwritten by the next run. The ledger of cross-run history still has that run's results, but the steps, the calls and the payloads were only ever in the report.

So the reports directory keeps the last few runs. The top level is the newest run, exactly what it always was plus one small Run.json. Before a run writes, the previous run's files are moved to runs/<run>/:

Reports/
  TestRunReport.html  TestRunReport.krkl  Failures.md  …  Run.json     <- the newest run
  runs/
    local_20260918T101611Z_ab12cd34/                                   <- the run before it, whole
      TestRunReport.html  TestRunReport.krkl  Failures.md  History.run.json  attachments/  Run.json
  • It is a move, not a copy: a rename inside one volume, constant time for a report of any size, no second copy on disk.
  • KeepRuns (Report Configuration) is how many are kept: 3 off CI, 0 on CI by default. KRONIKOL_KEEP_RUNS sets it from the environment (a number, or off); the option beats the environment. CI defaults to 0 because a pipeline that uploads or publishes the whole reports directory would otherwise quietly multiply every artifact and publish old runs.
  • The newest kept run that failed is never the one pruned. The directory therefore holds at most KeepRuns + 1 runs, and the last failure survives any number of green re-runs.
  • On CI, the earlier attempts of the same run are kept even at 0. A test runner's retry extension (--retry-failed-tests) re-runs the failed tests in a new process, which used to overwrite the failing run two seconds after it was written, leaving # No failures and All 1 scenarios passed. Runs with another id are overwritten as before, so nothing accumulates on a persistent runner.
  • The folder name is the run id with every character outside [A-Za-z0-9._-] replaced by _ (gh:18273645:1 is gh_18273645_1), the same on every operating system, with -2, -3 appended when the name is taken: two steps of one workflow run share one id. The true id is in the kept run's Run.json.
  • What moves is exactly what the previous run's Run.json lists, plus the manifest. CLAUDE.md, AGENTS.md and query.cs describe the directory and never move. A differently named report a host writes into the same folder is left alone, and an attachment it may link to is copied rather than taken. A directory written before 3.27.0 has no manifest: it is rotated once, by the files this run is about to write.
  • A data file under the other name goes (5.2.7). Once its own data file is written, a run deletes the one of the other extension, the TestRunReport.json a run wrote before a switch of CompressTestRunReport (or the other way round), where no rotation took it: with no runs kept it stayed beside the new one for good, every lookup said the folder held both, and a CI upload took it. Only that one name goes, never a file this process may not write.
  • It never fails the run. A report held open by a reader, a read-only file this process cannot write: the rotation is abandoned whole (the report's data file is moved first, so nothing has moved), anything staged is put back, a ReportRotationFailed diagnostic names the file, and the run overwrites as it always did. Root on Linux and macOS writes a read-only file, so for root the run replaces it either way, and since 3.34.1 it is rotated like any other (before, the rotation stopped and the previous run was overwritten anyway). A kept run is staged under runs/.incoming-<name>/ and published by one directory rename, so it is whole or absent, never half there; a process killed mid-rotation is resumed by the next run.
  • It says what it did, when the run it moved aside had failures: on the console (previous run (1 failed) kept: …/runs/<name> — kronikol query failures <dir> --run <name>; before 4.20.6 it said --run last-failed, the newest run with a failure, which on a failing run is that run itself) and, because dotnet test swallows the console, directly under the heading of the new Failures.md: > The run before this one failed — 1 of its 20 scenarios failed, and that run is kept, whole, in runs/<name>. A retry of the same run reads > An earlier attempt of this run failed.
  • Reading one: every kronikol query verb takes --run - see Querying Reports. kronikol history doctor <reports-dir> says how many are kept, which is the newest failing one, and what an interrupted rotation left behind.
  • kronikol merge neither rotates nor writes a Run.json: its output is derived, and the shards are the evidence. From 5.22.0 it writes a service.json beside the merged report, from the merged data file, so a sharded run has one card for all its shards; with no Run.json of its own, that card's run.id and run.at are null unless a run's Run.json in that folder lists the merged data file. Every recursive sweep (merge <dir>, query <parent-dir>, history record <dir>) skips runs/, as it skips baseline/; a kept run named on the command line is still read.

Run.json:

{ "runManifestVersion": 1, "run": "local:20260918T101611Z:ab12cd34", "at": "2026-09-18T10:16:11Z",
  "suite": "Checkout", "scenarios": 396, "failed": 1, "partial": null, "kronikolVersion": "5.0.0",
  "files": ["TestRunReport.html", "TestRunReport.krkl", "Failures.md", "Failures.jsonl", "History.run.json"],
  "attachments": ["attachments/checkout_failure.png"] }

A run that declares its service (ServiceId, 5.14.0) writes serviceId after attachments; one that does not writes no such member. A run that wrote its service card (5.22.0) writes serviceCard after that, {"file": "service.json", "behaviourHash": "…"}, so an indexer that keeps the last hash it read tells from Run.json alone whether the card changed; a run that wrote none, its data file off or written as XML, or its card not written, writes no such member. A run killed before its Run.json moves its card with its other files, and the manifest rebuilt for it names no card.

run is the history run id when history is on and an id minted the same way when it is off, so a run always has a name. files is what reached disk, collected where the bytes are written: an output that failed is absent, and one that fell back to Failures2.md is listed under that name.

Failures.md and Failures.jsonl

Written on every run since 3.1.0 (GenerateFailuresDigest, default on) — the one file small enough to read whole that says why the run went red. Every failure in context: the error, the expected and actual parsed out of it, the failing step with its file:line, the last few steps before it, the calls made inside it one line each, its attachments wherever they sit (the scenario's own, then each step's with that step's sN/k, from 4.24.0), and the sN / sN/iN address for each. Never a body, a header or a diagram — those are what make the report unreadable in the first place.

Since 3.2.0 each worked failure also carries a Thrown at line: the frame that raised, read out of errorStackTrace, named by method and file:line. It is the frame in the file the producer said the scenario was declared in, or failing that the topmost frame outside a test-framework namespace — not simply the topmost frame with source information, because xUnit v3 ships source-linked PDBs and that rule pointed every failure at StringAsserts.cs.

Since 3.7.0 a Re-run line follows it: the dotnet test --filter "FullyQualifiedName~Namespace.Class.Method" that re-runs the test the frame belongs to, with the compiler's scaffolding (async state machines, lambdas, display classes, generic arity) stripped from the name. It is a contains match, so a parameterised test re-runs with every row. kronikol query repro prints the same line per failure - see Querying Reports for the two limits (nested classes, runners on Microsoft.Testing.Platform's own dotnet test).

The #sid- deep link is written only when an HTML report is being written. With GenerateTestRunReport = false the digest links to nothing rather than to a file that will not exist.

Clustering and its limits. Failures are grouped by the first line of the error message (for a Shouldly message, whose first line is the subject's code, the check on its second line as well; for a Shouldly message written without its source, whose first line is the actual value and which ends but was not, the check and its expected value instead, from 4.20.4) and nothing else — not by the stack trace, and not by similarity. So a group is a strong hint that one cause is behind all of them, not a finding that one is: two unrelated assertions can open with the same line. Because of that the suppression is bounded. A cluster is sampled rather than represented by a single instance — one worked example, plus one more for every ten members, up to five — so a grouping that merged unrelated failures shows more than one kind. Members listed under a cluster heading stop at 20 and the closing table of failures stops at 100, each followed by a line saying how many were not listed; Failures.jsonl and kronikol query failures still have every one of them. A failure in that closing table is not worked through, so from 4.28.0 its row counts its attachments (its own and every step's, as Failures.jsonl lists them), and the sentence above the table names kronikol query attachments <report> sN, which lists them. Before that a failure clustered with another was in no list meant for a person, its trace and video included.

Written on a green run too, as # No failures followed by the scenario count. That is the point of it: if the file were skipped when nothing failed, "nothing broke" and "the run never finished" would be the same absence. Its presence is the signal that generation completed, and its contents say which of the two happened. # No failures means nothing failed in the newest run: since 3.27.0, when the run it replaced had failures, the line under the heading says so and names the folder under runs/ that keeps it (see Earlier runs).

Failures.jsonl is the same information for a script, one JSON object per line:

{"formatVersion":1,"kind":"header","kronikolVersion":"3.1.0","suite":"Orders.Tests.Component","scenarios":905,"failures":3}
{"kind":"failure","address":"s3","id":"0-1287","stableId":"6b1f0c2d3a4b5c6d","feature":"Orders","scenario":"Place order successfully","errorMessage":"Expected 201 but was 500", …}

The header line comes first and is always there, even when no failure line follows it. It states the contract once rather than once per record, and it is what keeps a green run's file from being zero bytes — otherwise "the file is empty" and "the file was never written" would be the same bytes, and the second is the one that means the run did not finish. It also carries the denominator: a consumer reading only the failure lines can tell 3 of 4 from 3 of 4,000, which it has no other way to know. Every following line carries "kind": "failure", so a reader dispatches on kind rather than on line number. A run that declares its service (ServiceId, 5.14.0) adds serviceId to the header, so a script reading the failures of several services' runs can tell them apart.

Fields on a failure record worth knowing about:

Field What it says
callsScope Which question calls answers: failingStep (calls attributed to the step that failed), scenario (none were attributed to it, so these are the scenario's own, failures first) or none (nothing was captured). Before 3.2.0 an empty calls array meant all three and a consumer could not tell them apart.
callsTotal (4.31.0) How many calls calls was drawn from: the failing step's, or the scenario's when none was attributed to a step. calls holds at most eight, and Failures.md says how many it left out and names kronikol query flow for the rest.
stepsBeforeTotal (4.31.0) How many steps came before the failing one. stepsBefore holds the last three, and Failures.md counts the earlier ones and names kronikol query steps.
thrownAt { method, file, line } for the frame that raised, or null when the trace could not be parsed. The runtime localises stack traces, and a guess at a shape is how a reader ends up at a line where nothing happened.
testName / rerun (3.7.0) The fully qualified test name read out of thrownAt.method with the compiler's scaffolding removed, and the dotnet test --filter "FullyQualifiedName~…" line built from it. Both null when no frame names a test.
attachments { name, path } for each of the scenario's own attachments, then { name, path, step } for each step's, in step-tree order (the background steps, then the steps, each followed by its sub-steps, a kept hook step where it ran), step being that step's address (s0/2, s0/b0). Before 4.24.0 only the scenario's own were listed, so a screenshot taken inside a step never reached the digest. Failures.md lists them the same way, a step's line ending with that step's address.
truncated true when any free-text field on the record was cut at 4,000 characters. The full text is in the data file at that record's address. A trailing ellipsis alone cannot be told from a message that genuinely ends in one.

kronikol query failures answers the same question against a live report and deliberately uses the same field names — see Querying Reports. The two are not yet the same object: the digest carries expected, actual, cluster, stepsBefore (with stepsBeforeTotal), callsScope and truncated, which the query envelope does not, and the two nest and cap calls differently. The query nests a failing step's attachments under that step (4.24.0) and, from 4.28.0, prints a list of more than six as its first entry and its last five with a total beside it, the scenario's own and a step's alike (before 4.28.0 the scenario's own stopped at the first four), where the digest lists every one, each step's with its step; kronikol query attachments sN --json lists every one too, paged, in the digest's order and field names. Since 3.7.0 both carry thrownAt, testName and rerun, read from errorStackTrace by the same reader.


Report Features

Both HTML reports include what follows, but for the items marked TestRunReport only. 5.0.0 redesigned the report, and an item marked (5.0.0) is new in it or was changed by it. Migrating to v5: the report lists what a 4.x stylesheet, script, test or setting has to change.

Theme and layout

  • Light and dark themes (5.0.0): a report follows the system's light or dark setting until the reader picks one with the switch at the right of its title, and then keeps that pick, over a reload and over the system's setting. The theme is chosen in the page's head, before anything is drawn. The pick is the one thing a report stores in the browser (kronikol.theme in local storage); the filters live in the page's link. The diagrams the page draws, the internal-flow popup, the History tab and the Diagnostics tab take the theme too, and the Specifications report's violet theme has a light and a dark form. A diagram copied, saved or opened as SVG or PNG carries the look the page gives it, in the light theme's colours whichever theme the page shows. See Report Themes.
  • Custom branding: inject custom CSS, favicon, and logo HTML via ReportConfigurationOptions (CustomCss, CustomFaviconBase64, CustomLogoHtml). From 5.0.0 the page's colours and type are CSS custom properties, --kron-* for the page and --dg-* for its diagrams, each with a light and a dark value, defined first in the page's styles, so a theme or a CustomCss sheet can set them again. A sheet or a theme written against the 4.x rules or colours of the parts 5.0.0 redrew (the features and their steps, the failure diff, the parameter table, the run band, the filter rows) may no longer apply: set the tokens instead (Report Themes).
  • Run band (TestRunReport only, 5.0.0, in place of 4.x's header row and pie chart): the ring, the Test Execution Summary and, on CI, the CI details share one row in a card. The CI details move under the summary when the three do not fit side by side, decided on their measured widths and measured again when the window or the text changes (a reader's text spacing included). The ring has an arc for each outcome the run had (passed, failed, skipped, bypassed), coloured by the theme, and the pass rate in its centre, which reads 100% only when every scenario passed: a rate under 100 rounds down and stops at 99%, where 4.x rounded 213 passed of 214 to 100%. The summary gives the overall status, the run's start and end on one line, and its counts as labelled figures (Scenarios, Passed, Failed, Skipped, Bypassed when there were any, Features and Duration). A scenario a framework skipped after an earlier failure counts as skipped, in the summary and in the ring (4.x left it out of both). A scenario that is not a test (an ingest's fold) is in no count, and Features counts the features that hold a test (5.2.6). A duration of an hour or more reads in hours (1h 5m, where it read 65m 0s), here and in the features table. The hidden Kronikol Version row is gone: the version stays in the page's generator meta. A sheet or a script that reads .header-row, .test-execution-summary, .summary-chart, .ci-chart-group or .ci-metadata finds none of them.
  • CI metadata (TestRunReport only): when running in CI (GitHub Actions or Azure DevOps), the run band's CI details show the provider, the build number, branch, commit SHA, repository and a link to the pipeline run. From 5.0.0 the labels lose their colons (Build #: and 42 are now Build and #42).
  • The report bar and its tabs (5.0.0): the report's sections are tabs of a bar that stays at the top of the window on a desktop: Features; Timeline, where scenarios recorded a duration; in TestRunReport.html only, Components, History and Diagnostics, where it has them (TestRunReport.html); and Search, last, while a search is applied (5.11.0). So Specifications.html has the Features tab, the Timeline tab where its scenarios carry durations, and the Search tab, and none of the others. Each tab shows a panel of its own, and the arrow keys, Home and End move between them. Next Failure, a history link or a scenario's address goes to the Features tab first (Deep links). Right of the tabs the bar shows what the filters leave showing (shortened, then left out, where the bar is narrow), Clear All while anything is filtered, and Filters, which opens the filter box at the bar's top (below). On the Features tab it also carries the tab's tools and, in a browser-rendered report, the display settings, grouped: the payload details first, then the payload's format and width, then the toggles. While the list scrolls under it, the bar's corners square and it casts a shadow. A page with nothing beside its features, such as a Specifications.html whose scenarios carry no duration, has the bar too, with the Features tab alone, so Filters and the Search tab have a home (5.11.0; until then it had no tabs and no bar, and its tools and display settings sat in a toolbar row above the features). The Scenario Timeline and Component Diagram toggles of 4.x are gone, with toggle_timeline and toggle_component_diagram.
  • Feature grouping: tests are grouped by feature (test class / story title / feature file). From 5.0.0 a feature is a card on a tinted ground, with its scenarios as rows inside it and a cross before a failed one, and steps line up on one column with their status circles, the triangles of collapsible steps in the gutter beside it.
  • On a phone (768 px wide or less, 5.0.0): the report bar scrolls away with the page instead of pinning, and its display settings show behind a Display settings gear. The bar's Filters takes a line of its own under the tabs, with what the filters leave showing, and opens the filter box with every row showing (5.11.0; until then the rows folded under a Filters button of the box's own). The search button is a magnifier, and once the title line has scrolled out of view a floating search button above the back-to-top button opens the search (5.11.0; until then the search scrolled away with the filter box). A scenario's settings gear sits on its diagrams' title, the display settings and each scenario's settings are a sheet with their zones (Payloads, Format, Toggle) one under another, and a filter control is 36 px tall (28 px on a desktop). At every width the filter rows reflow on the filter box's own width, not the window's, and the box opens at the top of the report bar (5.11.0; until then it kept a row of its own, and above 1160 px 4.x set it beside the summary and the CI box). Whatever is wider than its scenario stays inside it (3.29.3+): a table, a doc string, and a diagram drawn by a PlantUML server or a local jar, with its source, scroll sideways inside it, and a long token in text (a type name, an identifier, a URL) breaks where it must, there and everywhere else in the report (3.29.4+).

Search and filters

  • The search (5.11.0): the search button on the title line, or /, opens a query builder, and Enter runs the query. The Search tab lists the scenarios that match, with the lines where each one matched; a result's title or line goes to the match itself, with whatever hid it in the diagram turned on and the words marked. A search hides nothing: it reads every scenario, whatever the filters hide, and the Features list stays as the filters leave it (from 5.12.0; 5.11.0 ran the search through the filter, so it hid what it did not match). The query takes words, quoted phrases, &&, ||, !!, brackets, @tag or tag:, $status or status:, and the qualifiers below. Its address key is search= (5.12.0), and a script reads it with search_query(). See Search and Filter Syntax for the query, and Search and Filter Syntax › The search and its results for the builder, the Search tab and going to a match.
  • The filter field (5.0.0, back in 5.12.0): the first line of the filter box, #searchbar, takes the same query and narrows the Features list as it is typed, with ? beside it for the syntax and a chip inside it while deep filtering reads. It runs on input, so a paste or a drop filters at once, and Enter runs it once (5.10.x ran it twice on Enter). Its address key is q=. 5.11.0 took the field away and typed the query into the search's builder; a 5.11.0 link to the Search tab, whose q= was its search, opens as that search.
  • Filter qualifiers (5.0.0): dep:, duration: and is:happy, and from 5.11.0 feature:, narrow the search as the filter rows do. dep:payment keeps the scenarios that call a dependency with payment in its name (case and spaces ignored; quotes for a name with spaces, commas for any of several). duration:>2s, duration:<500ms, duration:1s..5s and duration:>=p90 compare a scenario's own time, the first-call warm-up left out, in ms, s or m, or against the report's percentiles. is:happy keeps the happy paths, and feature:pancakes the scenarios of a feature with pancakes in its name. !!, or from 5.11.0 a - right before it, negates one, and they combine with the rest of the query by AND. From 5.11.0 tag:smoke and status:failed are the same tests as @smoke and $failed, and -tag:slow leaves a tag out. A qualifier that cannot be read is read as text: the field it was typed in turns amber, and for a search the Search tab says why. See Search and Filter Syntax.
  • The filter box (5.0.0): from 5.11.0 hidden until the report bar's Filters opens it, at the top of the bar, with every row showing; until then a card on a row of its own above the bar, under the run band in TestRunReport.html. Its first line, Filtering, holds the readout, Clear All, the two exports and the box's close button; under it is the filter field (5.11.0 had a Search line there instead, showing the query, with a clear of its own). The readout says what the filters leave showing, All 214 scenarios, 73 features or Filtered 12 of 214 scenarios, 3 of 73 features, with a parameterized scenario counted once for each of its examples. Whenever the scenarios shown change, the page raises a kron-filter-change event on the document, with { shown, total } in its detail, and an element of the bar's own tells a screen reader, whether the box is open or not (5.11.0). Clear All clears the filter field, with its qualifiers, and every filter row, and leaves the search. While the Search tab shows, the box, Filters, the readout and Clear All are out of sight, and they come back as they were on any other tab (5.12.0).
  • Filters (5.11.0): the bar's Filters button shows at every width, at rest and pinned, and opens and closes the filter box. It counts the filters on (the filter field's text, each status pressed, each tag or dependency chip set either way, and a duration threshold) and says so in its name ("Filters, 2 on"). The box pins with the bar and covers nothing; pinned, the bar with the box in it takes at most 60% of the window, the box keeping at least 180 px, and the box scrolls inside; the box stays open as the bar comes to rest. Filters, Esc in the box or on the bar's filters, and the box's close button close it, putting the focus back on Filters, and opening and closing keep what was being read just below the bar. From 5.0.0 to 5.10.x the filter field's line ended with More filters, which kept the rows folded on a desktop and counted the filters on in them, the box carried rows-folded while they were folded, a phone folded them under a Filters button of the box's own, and the bar's Filters showed only while the bar was pinned, taking the box into the bar until Esc, the box's close button, a phone's width or scrolling back up put it back. More filters, rows-folded and the phone's own Filters button are gone: a script or test that uses a row's control presses Filters (.nav-filters-toggle) first.
  • The filter rows (5.0.0): each row's label is an uppercase word in a column of its own (Status, where 4.x wrote Status:). Status, Duration and Happy paths are segmented groups, which share one line only where they fit whole, decided on their measured widths (a reader's text spacing and late fonts included), and otherwise each takes a line of its own; tags and dependencies are chips; every control in the rows is 28 px tall. A row's switch reads ANY or ALL (4.x: OR or AND) and keeps the mode in data-mode, which the link writes as before. When the filters leave no scenario, the page says so where the features were, with a Clear All that puts the focus on the search button (5.11.0; in the filter field before). A sheet written against the 4.x rules of .status-toggle, .percentile-btn, .dependency-toggle or .dep-mode-toggle may no longer apply.
  • Status filter: the Status row has a button for each status (Passed, Failed, Skipped, Bypassed). A pressed status shows only its scenarios, and statuses pressed together show them together. A scenario skipped after a failure is under Skipped, as the summary and the ring count it (5.9.1; before, Failed showed it). Each button says whether it is pressed with aria-pressed (5.9.1; before, its status-active class alone said it).
  • Duration filter: Duration ≥ buttons for the run's P50, P90, P95 and P99, and a custom number of seconds, hide every scenario below the threshold, and one with no recorded duration. From 4.9.0 the percentiles and the comparison read each scenario's own time, its first-call warm-up left out, and a percentile is the nearest rank: the P50 of two durations is the smaller, where it was the larger. From 5.0.0 each percentile's tooltip names its threshold and its share of the run (the slowest half for P50), and that the first-call warm-up is left out when the run had one. Export Filtered CSV still writes wall time. The chosen button says so with aria-pressed (5.9.1), beside its percentile-active class.
  • Happy paths filter (5.0.0; a Happy Paths Only toggle before): Show, Only or Hide, on the Status line. It drives the Tags row's Happy Path chip, and a report without a happy path has neither. #hp=1 still opens on Only. .happy-path-toggle, .happy-path-active and filter_happy_paths are gone.
  • Tags filter: when scenarios have tags, the Tags row has a chip per tag with its count, Happy Path first and Untagged for the scenarios with none, an ANY/ALL switch and a 2-WAY / 3-WAY switch (see the ways below). A chip is on or off on a two-way row, and cycles through has, has not and off on a three-way one (Alt+click or right-click for has not), and a feature's tag picks every scenario of the feature. A row of more than 30 tags starts with the 30 the most scenarios carry and an "N more" chip. Every adapter fills it from its framework's own tagging (5.0.0; before, a Categories row showed only the categories LightBDD and ReqNRoll made). See Tags and Attributes › Tags.
  • Dependencies filter: the Dependencies row has a chip for each participant the scenarios' diagrams name, a No dependencies chip for the scenarios whose diagrams name none, an ANY/ALL switch and a 2-WAY / 3-WAY switch. On a three-way row a chip goes from off to has (a scenario must call it) to has not (it must not), and back to off, and Alt+click or a right-click goes straight to has not; on a two-way row, a row's start from 5.10.0, a chip is on or off (5.0.0 to 5.9.1 every row was three-way). A chosen chip carries dependency-active, as in 4.x, and a left-out one dependency-exclude, with aria-pressed true, mixed or false, and the row has a Clear while one of its chips is set. The switch starts on ANY, a scenario with any of the chosen dependencies (5.0.0; 4.x started on AND, a scenario with all of them): ReportToggleDefaults.DependencyFilterMode = FilterCombinationMode.And keeps the 4.x behaviour. A scenario whose diagram makes no call has no dependency and is under No dependencies (5.0.0; 4.x listed the (no interactions) participant as a dependency).
  • A row's ways (5.10.0): the Tags and Dependencies rows each have a switch after ANY/ALL that says 2-WAY or 3-WAY. A row starts two-way: a click on a chip shows only its scenarios and a second shows every scenario again, so no chip is red; Alt+click is a click and a right-click opens the browser's menu. Three-way, a chip goes has, has not (red) and off, and Alt+click or a right-click leaves its scenarios out at once. A row is three-way while its switch says so or while one of its chips is red, which only a link (tagx=, depx=, hpx=1) or Happy paths' Hide can make on a two-way row, and two-way again once the chip goes. Turning a row two-way takes its red chips off (Happy paths back to Show). Each chip's title says what a click does in its row's ways, and carries the count as data-scenarios. The link writes a switch the reader turned on (tagstates=3, depstates=3), Clear All makes both rows two-way, and a row's Clear keeps its ways.
  • The link carries every filter (5.0.0): the page's address carries the search and every filter as they change, so a copied link opens the same view. The Tags row writes tags= for the tags a scenario must have, tagx= for those it must not, and tagmode= when its ANY/ALL switch is not on its default. depx= names the dependencies left out, and hpx=1 says happy paths are hidden. A link copied from a 4.x report opens the same view (hp=1 is still happy paths only), except one that chose dependencies and has no depmode: it meant AND, and on a 5.0.0 report it means OR, the report's own mode (AND only on a report built with DependencyFilterMode = FilterCombinationMode.And). One that says depmode=OR still means OR, and a 5.0.0 report writes depmode=AND when AND is chosen. A 4.x link that chose (no interactions) opens on No dependencies. From 5.2.0 the link names the tab and the Features tab's view too (tab=, view=), and from 5.11.0 the Search tab's own state and the match a search result went to (type=, in=, s=, o=, p=, hit=). The display settings are not in the link: each load starts them from the report's settings. See Deep links and Back and Forward.

The Features tab

  • Full and Summary views (TestRunReport only, 5.0.0): Full and Summary, at the left of the Features tab's tools, show the feature cards, with the failure clusters over them, or the features table alone (until 5.1.0 the clusters showed over the table too), one at a time, and a row of the table opens its feature in the Full view, from a click or from the keyboard (the name is a button). The table has one row per feature. It names its column groups, Scenarios, Steps and Duration, in a row above its headings, and each group's first column reads Total (4.x: Scenarios, Steps and Duration); the Duration group, with Total, Avg and Longest, is there when scenario durations are available. In 4.x the table was the Features Summary disclosure above the run's summary; a sheet or a script that reads .features-summary-details finds none.
  • Sort (TestRunReport only, 5.0.0): a Sort dropdown at the right of the Features tab's tools orders the feature cards and the features table alike: the report's own order, the feature's name A to Z, failures first, most scenarios, and, for a run with durations, slowest total, slowest average and longest scenario. From 5.2.1 the cards show in their new order at once, placed by CSS order, and their order in the page follows within a moment, a few cards at a time; Tab, screen readers, the arrow keys and Next Failure go by that one. Moving a card in the page makes the browser lay out its drawn diagrams again, so until 5.2.1 a sort on 200 scenarios with all 400 diagrams drawn held the page 172 to 213 ms; it now takes under a millisecond, with the next frame within 15 ms.
  • Expand and collapse (5.0.0): a pair of icon buttons for features and a pair for scenarios, in place of 4.x's Expand All buttons whose labels flipped, so what a reader opened by hand can be closed without opening everything first. A button with nothing to do is dimmed (and aria-disabled), and expanding the scenarios opens their features and rules too. toggle_expand_collapse and the .collapse-expand-all class are gone.
  • Failure clustering: failed scenarios are grouped by the first line of the error message (with a Shouldly message's check from its second line; for one Shouldly wrote without its source, its check and expected value), not by the stack trace and not by similarity, and collapsed under one cluster heading. The clusters sit at the top of the Features tab (5.0.0). The report and Failures.md use the same key, so the two cannot disagree about which failures share a cause. Treat a cluster as a strong hint that one cause is behind all of them rather than a finding that one is: two unrelated assertions can open with the same line.
  • Duration badge: a scenario with a recorded duration shows it in its header, green under 2 s, amber under 5 s and red above; a parameterized group's badge is the sum of its rows. From 4.9.0, a scenario that paid the run's first-call warm-up keeps its wall time as the badge's text and adds the warm-up as muted text, as in 1.3s · 1.2s warm-up. Hovering the badge names the calls (1.27 s, 1.19 s of it first-call warm-up: POST /orders 661 ms (later calls 7.6 ms), GET /milk 82 ms (2.3 ms)), and its colour is the one the time left earns, so a scenario slow only because it went first is not coloured slow. A group sums its rows' warm-up as it sums their durations.
  • Step numbering: configurable hierarchical step numbers (e.g. 1., 2., 2.1.) via SpecificationsShowStepNumbers and TestRunReportShowStepNumbers.
  • Rules grouping: scenarios with a Rule property are grouped under a collapsible rule heading. A rule's Background: belongs to that rule's scenarios alone.
  • Background steps: a Gherkin Background: block, or an ingested run's background: true steps, are rendered as the first entries of the scenario's one Steps list, marked with a muted left accent, and numbered continuously with the rest. A repeated primary keyword collapses to And. Set SeparateBackgroundSteps = true to put them back in their own collapsible section. The features table's step counts include background steps too (3.0.48+). See Step Tracking › Background Steps.
  • Examples / Scenario Outline tables: scenarios sharing an OutlineId are rendered as examples tables with parameter columns and per-row status icons. When an outline has multiple named Examples: blocks, a separator band carrying the block name, its optional description and a per-block pass/fail count is drawn between the row groups (3.0.64+; see Named Examples blocks below). From 5.0.0 the parameter table has one even border in a rounded frame and no longer tints its rows by status: the Status column says it.
  • Expected/Actual diff rendering: when a failed scenario's error message contains "Expected: X" / "Actual: Y" patterns (common in assertion libraries), the report parses these and renders an inline visual diff highlighting the differences. From 5.0.0 the expected text is green and the actual text red, under the labels Expected and Actual, no longer followed by a colon. Values whose lengths multiply past 4,000,000 (two of 2,000 characters) keep their common start and end and mark the middle, so a response body compared whole no longer costs the run that writes the report its memory (before 4.22.3, two values of 50,000 characters took a 10 GB table, and where the machine had less, the run had no HTML report); an emoji is shown whole (before 4.22.3, as two U+FFFD).
  • DocString rendering: steps with a DocString property render a <pre> code block below the step text, a block that scrolls a long line sideways (3.29.3+; before, stray text in the stylesheet made the browser drop the block's rule, so a doc string was unstyled and a long line was cut off at the scenario's edge). A DocStringMediaType (e.g. json, xml) is written on the code element as a language-* class, for a stylesheet or script of your own to pick up; the report does not highlight syntax itself.
  • File attachments: steps can include FileAttachment entries (name + relative path) that are rendered in the report: an image inline, anything else as a link, and one the report does not hold as its name and why, with no link (4.29.0). The data files mark such an attachment with problem (NotFound, NotCopied or NotUsable), written only where there is one. Attachments can be added via Track.Attachment() (any framework), LightBDD's step attachment API, or ReqNRoll's IReqnrollOutputHelper.AddAttachment(). See Step Tracking › File Attachments.
  • Step comments: steps with a Comments property render comment text below the step. In LightBDD, comments are added via StepExecution.Current.Comment(). See Tags and Attributes › Step Comments.

Diagrams

  • A scenario's diagrams (5.0.0): a card in the scenario. Where a scenario has more than one view, Sequence Diagrams, Activity Diagrams and Flame Chart are tabs (role="tab") on a bar of their own; the arrow keys, Home and End move between them, and only the one showing is in the tab order. The activity diagram and the flame chart show the whole test's flow, with Internal Flow Tracking on.
  • Diagram look (5.0.0): a sequence diagram starts with flat boxes, participants with rounded corners and thin edges, thin lifelines and note edges, and messages centred over their arrows, and, where a browser engine draws it (BrowserJs and NodeJs rendering), the system's own font. A diagram drawn with a PlantUmlTheme leaves the look out and keeps the theme's. Where the page draws a diagram, every step bar is a rectangle with its label in bold, in place of the black hexagon, and the drawing's parts take the theme's colours by their role (from 5.2.2 in a drawing with one participant too, such as a scenario that made no calls, whose step bars stayed black). A service is drawn as a box, not a circle on a line, and databases are deep cyan (#0E7C86, red before), which DependencyColors still overrides. From 5.9.1 a step bar centres its label and each table in it, a table's header row is centred in its columns in semibold and its other rows start at the left, each cell keeps 8 px either side of its text, and a doc string is one framed cell whose lines keep their indentation, as the step tables in a scenario's steps are drawn (they all sat at the bar's left, with no room in a cell). A bar holding two tables gives each its own ground and shaded header, and a table of a header alone has its header shaded.
  • Participant order: a sequence diagram declares the first caller no service calls, then everyone in the order of their first call. In a run that declares its parties (5.33.0, Report Configuration › Parties), the test's parties come first, then the parties under test, then everyone else in that order, so the service under test stands next to the test even when the test set up its stores first.
  • Why a call failed (5.35.0): the note on a failed answer's return opens with its reason, the call's error (the message chain of what it threw), in the theme's failure ink (--dg-fail-ink, the report's own failure red, see Report Themes): under the answer's headers, which the page starts hidden, and above its body if it had one. A reason is not a body, so LogResponseContent = false leaves it drawn, and a body in the reason's own words (the SQL family's and the OTLP tap's failures) is drawn once, as the reason. Collapsed, the note's one-line preview is the reason's start. The page's copies and kronikol query read it without its ink. A copied picture takes the light theme's ink, as every copy does, and a diagram drawn away from the page (a server-drawn report) shows it in firebrick.
  • Display settings (browser-rendered reports): the same controls act on every sequence diagram in the report from the report bar, and on one scenario's from its settings (below). Payloads (5.0.0; the Details radio before) is Expanded, Collapsed or Truncated; its line count reads "N lines" under a "Payload length" heading and shows only while Truncated is picked. Truncated cuts a note by the lines it shows, so a payload whose YAML view runs longer than its JSON is cut in the YAML view too (5.2.4; until then Truncated left such a note whole). Payload format (JSON or YAML) and Payload width (Wrap or Wide) are dropdowns. The toggles are Headers, and where some diagram has something for them to act on, Assertions, Steps, Databases, Events and Setup. A toggle says its word alone and whether it is on with aria-pressed (5.0.0; 4.x rewrote its label, as in "Databases Shown"). From 5.0.0 every diagram toggle but Headers starts on, and payloads start Truncated at 10 lines (4.x: assertion notes hidden, and 40 lines); to open a report as 4.x did, set ReportToggleDefaults.AssertionsShown to false and TruncateLines to TruncateLineCount.Lines40. Assertions, Steps, Databases, Events and Setup, and from 5.11.0 Payloads and Headers, share one model: a scenario's own choice holds for the diagrams it draws later, and the report's choice holds everywhere, over a scenario's own (until 5.11.0 a diagram a scenario drew after its Payloads or Headers choice took the report's). Each label sits in span.btn-label, which holds its bold width, so switching a control moves nothing beside it, and a control being redrawn keeps its size, with a sweep along its bottom edge in place of 4.x's spinner. A collapsed payload's one-line preview leaves out a JSON body's braces, and each of a note's own buttons has a tooltip saying what a click does (5.0.0). The preview is cut from the form the note shows, its YAML when it is shown as YAML (5.11.0; until then it was cut from the JSON). From 5.4.0 a payload longer than 40 body lines, under Expanded or at a Payload length of 50, 60, 80 or 100, is drawn 40 lines tall with a scroll box over it that holds every line it would have shown, and Expanded's tooltip says so, adding "a long one scrolls in its box". ReportConfigurationOptions.PayloadScrollLines sets the 40 lines, and 0 draws every payload in full, as 5.3 did; see A long payload scrolls in its box.
  • This scenario's settings (browser-rendered reports, 5.0.0): a gear at the top right of a scenario's diagrams, grey until pointed at, opens that scenario's payload settings (Payloads, the format and width dropdowns and the toggles) on a bar that starts "This scenario:", so it reads apart from the report's own. The gear stays where it was, as the bar's last control, so a second click closes it, and the open bar stays under the report's bar while the scenario's diagrams are in view. The settings act on sequence diagrams only, so they step aside on the Activity Diagrams and Flame Chart tabs, and a section with no sequence diagram has none (4.x gave it payload controls that acted on nothing). On a phone the gear sits on the diagrams' title. Each bar starts closed, and nothing is remembered. A script or test that uses a scenario's payload controls opens its settings first.
  • Database toggle: when any diagram in the report contains a database or collections participant (from CosmosDB, EF Core, MongoDB, DynamoDB, Spanner, BigQuery, Redis, distributed caches, or any extension that renders as a database/collections shape), a Databases toggle appears in the display settings (5.0.0; it read "Databases Shown" / "Databases Hidden" before). Turning it off hides all database and cache participants and their associated arrows, simplifying the diagram to show only service-to-service communication. The toggle works at both report level and scenario level. It applies only to scenario sequence diagrams: no toggle changes the component diagram on the Components tab (3.0.82+; its database/collections dependency nodes always stay drawn), whose own Show control follows the scenario filters instead (5.0.0). See Tracking Custom Dependencies for how participants are assigned the database or collections shape.
  • Events and Setup toggles (5.0.0): Events takes the calls to and from message brokers (the participants drawn as queues, such as Kafka, Event Hubs, Pub/Sub, Service Bus, SQS and SNS) out of the sequence diagrams, with their payloads. Setup takes a scenario's setup out: its Setup frame where the report keeps setup apart (SeparateSetup), otherwise everything before its first When step, with the participants only the setup called. Each appears where some diagram has something for it to take out. ReportToggleDefaults.EventsShown and SetupShown (both default true) set their start states.
  • A block goes with what it holds (5.9.1): a loop of identical calls (CollapseConsecutiveIdenticalCalls), the Setup box, and a group, alt, opt, par, break, critical or box of your own markup that a toggle empties are taken out with what they held; until 5.9.1 each was drawn as an empty box. A block that still holds a call, a note, a step bar or a block that stays is kept, every branch of an alt included, and a block written empty stays as it was written. The drawing, the current source the diagram menu copies and the one it opens in the editor tab read the same filtered source. Two readings were corrected with it: a participant whose alias is a block's keyword (a service called Group or Loop) sends messages, which the page took for the start of a block, so a diagram drawn in parts gained stray end lines and the Setup toggle left the setup in; and a note's lines are never statements, so the Databases and Events toggles no longer take out of a kept note a line that reads like a database's call or declaration. An assertion's tooltip names its own line once the Setup toggle has taken earlier assertions out (it named the line of an assertion no longer drawn).
  • Note JSON ⇄ YAML toggle (browser-rendered reports, 3.0.59+): hovering a diagram note whose body is JSON shows a Y button that re-renders the payload as YAML, unfolding \n (and uniform \r\n) escapes in string values (SQL queries, stack traces) into literal block scalars with their original line breaks; J toggles back to the exact original JSON. Since 3.0.66 a dropdown converts every eligible note at once, report-wide or per scenario: from 5.0.0 it is Payload format in the display settings. Reports start in YAML from 4.0.0; ReportConfigurationOptions.NotePayloadFormat (CLI: kronikol ingest --note-format) chooses the format a report starts in. See PlantUML Browser Rendering › JSON ⇄ YAML.
  • Note width control (browser-rendered reports, 3.0.85+): hovering a note whose text wraps shows a ↔ button that widens it to fill the diagram, and →← takes it back; from 5.0.0 their tooltips read "Widen this box to fill the diagram" and "Back to the default width". The Payload width dropdown (named so from 5.0.0), Wrap or Wide, does the same for every note, report-wide or per scenario; it read Fit / Full before 3.22.0. Wide changes only notes that wrap, so on a scenario of short payloads it appears to do nothing. 4.0.0 removed the opt-in monospace control that sat beside it. See Note Appearance.
  • Click-to-reveal PlantUML: click any diagram image to reveal the raw PlantUML code behind it.
  • Context menu: right-click any diagram (SVG, flame chart, or call tree) for copy/save options including PNG, SVG, and diagram source. See Inline SVG Rendering for details. From 5.0.0 the menu is drawn on the page's card in the page's ink, in both themes, and its "in new tab" items open their tab in Firefox too, each tab titled for what it holds ("Diagram (PNG)", "Diagram (SVG)", "PlantUML source", "Box text").
  • Internal-flow popup: an arrow's link label is one Tab stop, named from its whole label, and Enter or Space opens its popup; where links show only on hover, focus shows it (5.0.0; before, the browser engine drew the link as text only a pointer could open, and the Java engine as one link for each line of the label, each named by the segment's id). The popup takes the theme, its Activity and Flame Chart views drawn as tabs (5.0.0). See Internal Flow Tracking.

The other tabs

  • Scenario Timeline (the Timeline tab, 5.0.0; a toolbar toggle before): every scenario's duration as a bar, longest first, coloured by result in the theme's colours (green passed, red failed, grey skipped, blue bypassed, as the run band's ring draws it; orange before 5.0.0), a failed scenario's row opening with a cross. From 4.9.0 the warm-up's share of a marked scenario's bar is shaded from its start, with the badge's tooltip. Its controls (5.0.0) are Show, Filtered (the start) or Full, and where a scenario paid a first-call warm-up, Warm-up, Shown or Hidden, and Order, With or Without warm-up. Filtered draws the scenarios the filters leave showing and says how many of the run's scenarios it shows, in a note that ends (5.1.0) with Clear All, which clears every filter as the button where no scenario is left does, and the bars are drawn to the longest one showing; with the warm-up hidden a bar and its time are the scenario's own. Order With warm-up keeps the bars in elapsed-time order, and Without warm-up orders them by each scenario's own time. From 5.5.0 each row's name is a link to its scenario (#sid-<stableId>): it opens the Features tab at the scenario, and Back returns to the timeline where the reader was, the name focused (see Back and Forward). Under Full, the name of a scenario the filters hide opens the line that says so, with the button that clears them. The name keeps its colour until it is pointed at; its title holds the whole name where the row cuts it short.
  • Components tab (TestRunReport only, 5.0.0; a toolbar toggle before): the embedded component diagram, headed by its title (ComponentDiagramOptions.Title, under every renderer), which the drawing leaves out; ComponentDiagram.html still draws it. Where the page draws the diagram, its controls are Show, Filtered (the start) or Full, where the report knows which scenarios made each connection's calls; Direction, Left to right (the start) or Top to bottom; and Call summaries, Shown (the start) or Hidden. Filtered draws the components and connections used by the scenarios the filters leave showing, read from the run's calls rather than matched by name, and a note says how many scenarios it follows, ending (5.1.0) with Clear All, which clears every filter. A run whose calls give the diagram no connection has no Components tab (4.x drew the title alone).
  • History tab (TestRunReport only, 5.0.0): the History section in a tab of its own, open there. The pass rate is a ring per run, drawn like the run band's ring with the rate inside, never 100% while any scenario of the run did not pass, and a run that failed has a fail arc; each ring keeps its run's tooltip. The duration bars carry their time (850ms, 44s, 2m 26s, 1h 5m), in the bar or just above a short one. An Order control above the two cards shows both newest first, the default from 5.10.0, or oldest first; each card keeps its runs on one line, and a window too long for the card scrolls inside it and opens on the newest run in either order (before 5.10.0 the rings wrapped onto rows, oldest first). The section and its verdict pills take the theme, each kind in its own colour. The svg.history-chart-svg chart and its history-bar-pass, history-bar-fail, history-bar-duration and history-bar-partial classes are gone. See cross-run history.
  • Diagnostics tab (TestRunReport only, 5.0.0): the run's report diagnostics, open in their tab, with their count on it: a card on the theme's colours, with one rule between its entries, a failure's kind in the failure colour and a degraded capture's in amber. See report diagnostics.

Keyboard and tooltips

  • Keyboard: / opens the search's query builder (5.11.0; until then it put the cursor in the filter field), from a report tab too (5.2.7; from 5.2.0 a tab kept it), and never from a field. From 5.11.0 it is left to the browser on an exported page, which has no search, and while an image, an internal-flow popup or the builder is open; until then it was taken there too, doing nothing on an exported page and putting the focus behind an open image or popup. The down and up arrows move between the scenarios shown, giving the keyboard's focus to the one they pick, so Enter opens or folds it (5.2.7; the focus stayed where a click had left it, and Enter folded the scenario clicked before); on the Search tab they scroll the page (5.11.0). A dropdown keeps its own arrow keys (5.0.0; the scenario keys took them from every dropdown, the payload format, width and line count among them), and Enter presses the button that has the focus (5.0.0; once the arrows had picked a scenario, Enter folded it wherever the focus was). The report's tabs, the Features tab's Full and Summary views and a scenario's diagram views move with the arrow keys, Home and End, with only the selected one in the tab order (5.0.0). A run of those keys across the tabs or the views is one step of Back (5.2.0), and while a tab list has the focus the up and down arrows leave the scenarios alone (5.2.0; on a report tab they moved the chosen scenario in the hidden Features panel). From 5.4.0 a long payload's scroll box is a Tab stop that keeps its own keys: while it has the focus, the arrow keys, Page Up, Page Down, Home and End scroll it, and the report's own shortcuts leave it alone.
  • Focus kept clear of the report bar (5.0.0): Tab or Shift+Tab to a control the pinned bar covers, or a scenario's settings stuck under it, brings the control 12 px below them, and to one cut by the top or the foot of the window, 12 px inside it. For a second after, a control the page moves away, as a diagram drawn above it does, is brought back, until the reader scrolls, points or presses a key. Focus a click or a script gives stays where it lands, as does a control in the bar, in those settings or in the internal-flow popup, and a control taller than the space below the bar that already shows in it. On a phone, where the bar scrolls away, nothing moves.
  • Tooltips (5.0.0): each tab of the report bar, each payload control, each filter, each heading of a scenario's sections, each link and the page's own buttons has a tooltip saying what it does or what it holds. A payload control's tooltip names the diagrams it acts on: every sequence diagram in the report on the report bar, the scenario's own on a scenario's bar. Clear All's names the search and every filter row the box has, and a chip's gives the number of scenarios it picks out and then what a click does. A composite step's says how many steps it holds, given when it is first pointed at or focused. In their tabs, the History and Diagnostics headings say what the section is, not that a click opens it. The back-to-top button, the payload line count dropdown and the internal-flow popup's close have names a screen reader can read: "Back to top", "Payload length" and "Close".

Export and start states

  • Export Filtered HTML / CSV: two buttons on the filter box's Filtering line write out whatever the current filters leave visible; where the box is narrow they read Export HTML and Export CSV (5.0.0). From 5.11.0 the Search tab's ⋯ menu has Export results as HTML and Export results as CSV, which write out the search's results whatever the filters show, and whatever Show and Matched in choose (5.12.0; 5.11.0 wrote out what the Features list showed). The HTML export leaves a search's marks out. Export Filtered CSV gives one row per visible scenario (feature, scenario, status, duration in milliseconds). Export Filtered HTML writes a standalone filtered-report.html: the report's <head> (styles and every behaviour script), an <h1>Filtered Report</h1>, the visible details.feature elements, and the diagram sources those features need. From a compressed report (5.6.0) the export is a compressed page too: that page as a gzip frame made with the browser's CompressionStream, and only the pieces of diagram sources the exported scenarios draw, copied as they are, with the report's loader and decoder (The compressed page); kronikol decode filtered-report.html -o plain.html writes its plain page. It is deliberately just the features (no run band, search, filter box, report bar or other tab), so the exported file cannot be re-filtered. A default report's features carry no cross-run history, so neither does its export. In a report drawn with ShowScenarioHistory the sparklines and verdict pills live in the scenario headers and are carried; the History tab is left behind, as the History section was (3.11.0).

    Diagrams in the export were blank before 3.0.83. Under BrowserJs rendering a diagram element is an empty <div class="plantuml-browser" id="puml-N"> and every source lives gzipped in a single <script id="puml-data"> at the end of the body, keyed by that id. The export copied the head and the features and nothing else, so it shipped the whole render machinery with none of the data: source lookup returned null, the render queue silently dropped the element, and every diagram that had not already been drawn before the export stayed permanently blank, with no error and no message, just an empty box. Measured on a real 488-diagram report: 12 of 460 diagrams drew (only the ones already rendered, serialised as inline SVG), 448 did not. The export now carries the body's data payloads, with #puml-data pruned to the ids the exported markup actually contains.

  • Configurable toggle default start states (3.0.80+): every toggle, radio group and stateful dropdown in both reports (Payloads and its line count, headers/assertions/steps/databases/events/setup, payload format and width, feature/scenario expansion, diagram view tab, the tab the page opens on, the Features tab's view, dependency/tag filter modes, and the disclosure sections) can be given a configured default start state via ReportConfigurationOptions.TestRunReportToggleDefaults, with SpecificationsToggleDefaults overriding per property for Specifications.html (inherit-unless-overridden). From 5.0.0 ScenarioTimelineVisible and ComponentDiagramVisible choose the tab the page opens on (the timeline when both are set), FeaturesSummaryOpen opens the Features tab on its Summary view, FeaturesExpanded and ScenariosExpanded start the matching expand button dimmed, EventsShown and SetupShown are new, DependencyFilterMode defaults to Or, and DiagnosticsOpen has nothing left to open. See Report Configuration › Toggle Default Start States.

Execution Result Statuses

Each scenario and step is assigned an ExecutionResult status. The report renders each status with a colour-coded circular icon. See Framework Status Mappings below for full details on what each status renders as, its tooltip text, and which native framework statuses produce it.

Status Icon Colour Meaning
Passed ✓ Green (#1daf26) Ran and succeeded
Failed ✗ Red (#cc0000) Assertion or runtime failure
Skipped Ø Grey (#949494) Deliberately excluded (attribute, filter, inconclusive)
Bypassed ↷ Blue (#2e7bff) Some or all of the step's logic was skipped over at runtime, and the steps after it ran: a LightBDD bypass, a tracked step's SkipIf, an ingested bypassed step, or an ingested Cucumber step reported SKIPPED that a later step ran after. Its reason is drawn under the step (4.6.2+)
SkippedAfterFailure ! Yellow (#fbc800) Cascading non-execution after a prior step failure

A step still running when the report was drawn has no status, and is listed with no icon (from 5.29.4).

A step's icon shows its tooltip when the pointer rests on it or the step's row has keyboard focus. From 4.27.1 the page holds each tooltip text once and gives an icon its text then; it used to write the text on every step, 54 to 209 bytes each.

Combined icons: When a parent step has Passed status but contains descendant sub-steps with Bypassed or Skipped status, the parent's icon changes to reflect this:

Descendant statuses Parent icon Background Meaning
Any Bypassed (no Skipped) ✓ Blue (#2e7bff) Passed overall, but some sub-steps were bypassed
Any Skipped (regardless of Bypassed) ✓ Grey (#949494) Passed overall, but some sub-steps were skipped

This bubbles up recursively through all ancestor levels. Skipped takes priority over Bypassed because skipping is a bigger deal — it is cascading and prevents execution of subsequent steps, whereas bypassed steps still allow subsequent steps to run. Note that SkippedAfterFailure does not trigger this behaviour (it has its own distinct status).

Framework Status Mappings

Each framework adapter maps its native statuses to Kronikol's ExecutionResult enum. The tables below show which native statuses from each framework produce each Kronikol status, split by scenario level and step level.

Scenario-Level Mappings

These mappings determine the overall status icon shown next to each scenario in the report. Every framework provides scenario-level statuses.

Kronikol Status Icon Colour Description xUnit v2 xUnit v3 NUnit 4 MSTest TUnit BDDfy ReqNRoll LightBDD
Passed ✓ Green All assertions passed ITestPassed Passed Passed, Warning Passed Passed Passed OK Passed
Failed ✗ Red An assertion or runtime failure occurred ITestFailed Failed Failed Failed, Error, Timeout, Aborted Failed, Timeout Failed TestError, BindingError Failed
Skipped Ø Grey Either the entire test did not run (e.g. a skip attribute or filter excluded it), or a step was skipped at runtime which also prevented all subsequent steps from executing ITestSkipped Skipped, NotRun Skipped, Inconclusive Inconclusive, InProgress, NotRunnable, Unknown Skipped, Cancelled Inconclusive, NotImplemented, NotExecuted UndefinedStep, StepDefinitionPending, Skipped Ignored, NotRun
Bypassed ↷ Blue Some or all of the logic in a step was intentionally skipped over at runtime without preventing execution of subsequent steps — — — — — — — Bypassed

Note: SkippedAfterFailure does not apply at scenario level — it is a step-level status only. A scenario is Bypassed from the LightBDD adapter and from kronikol ingest (4.6.2+); the other adapters keep their framework's verdict for a scenario with a SkipIf step. In xUnit v2 a tracked test that is skipped, or that fails before its first line runs, is in the report from 4.11.1; before, neither was.

Step-Level Mappings

These mappings determine the status icon shown next to each BDD step (Given/When/Then) within a scenario. Only BDDfy, ReqNRoll, and LightBDD capture step-level statuses; the other frameworks have no step concept.

Kronikol Status Icon Colour Description BDDfy ReqNRoll LightBDD
Passed ✓ Green All assertions in this step passed Passed OK Passed
Failed ✗ Red This step threw an exception or an assertion failed Failed TestError, BindingError Failed
Skipped Ø Grey This step did not execute because it was intentionally skipped, either at the scenario level, or at the step level. In the latter case the skip also prevented execution of subsequent steps Inconclusive, NotImplemented, NotExecuted¹ UndefinedStep, StepDefinitionPending, Skipped³ Ignored⁵, NotRun⁶
Bypassed ↷ Blue Some or all of the logic in this step was intentionally skipped over without preventing execution of subsequent steps — — Bypassed
SkippedAfterFailure ! Yellow This step was never reached because an earlier step failed NotExecuted² Skipped⁴ Ignored⁷, NotRun⁸

BDDfy footnotes — BDDfy's NotExecuted maps to different statuses depending on whether a prior step failed:

  1. NotExecuted → Skipped: When no prior step in the scenario has failed
  2. NotExecuted → SkippedAfterFailure: When a prior step in the same scenario has already failed

ReqNRoll footnotes — ReqNRoll's Skipped maps to different statuses depending on whether a prior step failed:

  1. Skipped → Skipped: When no prior step in the scenario has failed
  2. Skipped → SkippedAfterFailure: When a prior step in the same scenario has already failed

LightBDD footnotes — LightBDD's Ignored and NotRun map to different statuses depending on both scenario-level skip state and prior step failure:

  1. Ignored → Skipped: When the entire scenario is skipped, or when no prior step has failed
  2. NotRun → Skipped: Only when the entire scenario was skipped at scenario level
  3. Ignored → SkippedAfterFailure: When the scenario was not skipped and a prior step has already failed
  4. NotRun → SkippedAfterFailure: When the scenario was not skipped (i.e. a prior failure prevented this step from running)

Parameterized Test Grouping

When GroupParameterizedTests is enabled (default: true), parameterized tests that share the same test method are automatically grouped into a single collapsible entry with an interactive parameter table. This reduces report clutter when a single test method is run with many different inputs.

How Grouping Works

Scenarios are grouped when they share a common OutlineId (set by frameworks like ReqNRoll for Scenario Outlines) or when their display names share the same base name with varying parameter suffixes (e.g. Process(region: UK) and Process(region: US)).

Table Layouts

The parameter table uses one of two layouts:

  • R1 — Scalar Columns: When the group has a small number of well-defined parameters (≤ MaxParameterColumns), each parameter gets its own column under an "Input Parameters" header. This gives a clean, spreadsheet-like view.
  • R0 — Fallback: When there are too many parameters, or parameters cannot be cleanly extracted, each row shows the full test case display name in a single "Test Case" column.

Interactive Features

Each row in the parameter table shows the test's status badge and duration. From 5.0.0 a row is not tinted by its status, since the Status column says it: a row takes the hover colour under the pointer, and the selected row is shaded in the selected colour with a bar in the accent colour at its left edge, in the report's light or dark theme. Through 4.x each row was tinted by its status (green passed, red failed, amber skipped, lavender bypassed), and from 3.29.3 it darkened its tint under the pointer and further when selected; before 3.29.3 the pointer gave no feedback on any row, and a selected skipped or bypassed row looked like the others. A table wider than its scenario scrolls sideways inside it (3.29.3+; before, above 768 px its last columns were cut off with nothing to scroll to). Clicking a row switches the visible:

  • Detail panel — Steps, tabular parameters, and failure details for that specific test case
  • Sequence diagrams — The diagrams rendered for that specific test case (or a single diagram with an "All diagrams identical" badge when every test case produces the same diagram)
  • Activity diagrams and flame charts — Whole-test-flow visualisations, when internal flow tracking is enabled

A detail panel also renders for a scenario whose steps are all background steps (3.0.48+; previously such a scenario produced no panel and its steps vanished from the group).

Additional features on parameterized groups:

  • Copy button — Copies the group name to clipboard
  • Scenario link — Deep-linkable anchor for the group
  • Per-row deep linking — Each row has a data-scenario-id attribute. Navigating to #scenario-test-a-1 will open the group and select the matching row. Since 3.1.0 the row also carries data-stable-id, so #sid-<stableId> reaches an individual example row — which the slug anchor cannot, because every row of an outline shares one display name.
  • Search highlighting: while a search is applied, the matching rows within parameterized groups are marked with a bar along their left edge (blue, violet on the violet theme) and the first matching row is selected. Clearing the search takes the bars off and selects again the row that was selected before, unless you chose another since (5.11.0; until then the bars and the selection stayed).

Deep links (#scenario- and #sid-)

Both HTML reports answer to two kinds of URL fragment.

Fragment Names Since
#scenario-<slug> The scenario or group whose display name slugifies to <slug>. Written by the link button on every scenario and by the failure-cluster list. 3.0.15
#sid-<stableId> The scenario carrying that stableId. Printed by kronikol query failures, kronikol query steps and Failures.md. 3.1.0

Prefer #sid-. The slug is the display name, so it changes the moment anyone renames a test, and two features naming a scenario the same thing produce the same slug (the report disambiguates the second with a -2 suffix, which is positional and therefore not stable across runs either). A stableId hashes the run's suite, the feature name, the display name, the outline id and the example values, so it survives a reorder and tells an outline's rows apart. It is not unique in every case — repeated [Theory] data, the same example row in two Examples: blocks and retries all share one — and a #sid- link then resolves to the first match in document order.

From 5.0.0 a scenario's address goes to the Features tab first, and to its Full view where the Summary view is showing: on load, when the address changes, and when a link to a scenario in the page (a failure cluster's, a History entry's, a Timeline row's name from 5.5.0) or Next Failure is clicked.

Following a link opens every section around the scenario and puts the scenario in the middle of the screen at once, or its title just under the report bar when the scenario is taller than the window (5.2.0). A link to a scenario the filters hide says so where the scenario would be, with a button that clears the filters and shows it (5.2.0; until then the link did nothing anyone could see). Before 3.31.10 the report scrolled there smoothly, and in a report longer than a few features the scroll ended past the scenario, at the bottom of the page: a feature the browser has not drawn yet counts as a 500 px placeholder (content-visibility: auto), and the placeholders shrank to their true size as the scroll passed them. On a phone it also ended above a scenario near the top, because the filter panel folded away as the report loaded. Next Failure and the failure-cluster links jump the same way since 3.31.10.

3.1.0 re-keyed every id. The suite joined the hash, so a #sid- link written against an earlier report — bookmarked, pasted into a ticket, stored in a baseline — resolves to nothing on a report generated by 3.1.0 or later, unless that report's suite could not be resolved. The #scenario- form is unaffected. See stableId for why the suite was added.

A fragment may carry filter state after the anchor, separated by &:

TestRunReport.html#sid-6b1f0c2d3a4b5c6d
TestRunReport.html#sid-6b1f0c2d3a4b5c6d&status=Failed&deps=Payments

The anchor is always the first segment. Changing a filter rewrites everything after it and leaves the anchor alone, and Clear All drops the filters but keeps the anchor — clearing filters is not the same act as leaving the scenario you navigated to. Following an anchor opens every enclosing <details> (feature, parameterized group, the scenario itself) and scrolls it into view; where an outline row is rendered twice, once in the flat parameter table and once in the grouped one, the link resolves to whichever copy is displayed. Setting the hash on an already-open report works too — it does not need a reload.

The Tags row writes tags= for the tags a scenario must have, tagx= for those it must not, and tagmode= when its ANY/ALL switch is not on its default. Each name is percent-encoded on its own and the names are joined with commas, so a tag with a comma, a quote or a space in it survives the link, and a value that does not decode loses only itself. A link a 4.x report wrote keeps its meaning (5.0.0): its cats= is read as tags=, its catmode= as the tag mode when the link has no tagmode=, and __uncategorized__ as Untagged.

The other filters write q= (the search, its qualifiers included), status=, deps= and depx= (the dependencies chosen and left out), depmode= when the Dependencies switch is not on the report's default, hp=1 or hpx=1 (happy paths only, or hidden), dur= and pctl=; depx= and hpx= are new in 5.0.0. A link with chosen dependencies and no depmode takes the report's own mode, so one a default 4.x report wrote for AND opens on OR in a default 5.0.0 report.

From 5.2.0 the link also names the tab and the Features tab's view: tab= (timeline, components, history, diagnostics, or features) and view= (full or summary). Each is written only where it says something the link would not say without it, so the address of the place a report opens on is still the bare file name, and a scenario's address is still #sid-<stableId> alone:

TestRunReport.html#tab=history
TestRunReport.html#view=summary&status=Failed

Reading a link, the tab is tab= where it names a tab of the report, else the Features tab when the link names a scenario, else the tab the report opens on; the view likewise. A link written before 5.2.0 holds neither key and opens as it did, and a 4.x report ignores both. The anchor is kept while the Features tab's Full view shows and dropped on another tab or the Summary view, since leaving the cards is leaving the scenario it names. The display settings, the Features tab's sort and which sections are open are not in the link: each load starts them from the report's settings.

From 5.11.0 tab= takes search too, and the link names the Search tab's own state: type=features (Scenarios when it is left out), in= (names, steps, diagrams or errors), the sort (s=order, s=name, s=status, or s=duration with o=desc or o=asc; Best match when it is left out) and p= from page 2. A search result's title or line writes hit= after the scenario's anchor: the match it went to, which Forward, a reload or a shared link goes to again. Each is written only where it says something, as tab= and view= are, and a link with tab=search and no q= opens as if it had no tab= (Search and Filter Syntax › The search and its results):

TestRunReport.html#tab=search&in=diagrams&s=duration&o=desc&q=pancake
TestRunReport.html#sid-6b1f0c2d3a4b5c6d&hit=s%3A3&q=marmalade

Back and Forward

From 5.2.0 the browser's Back and Forward buttons move between the places a reader has been in the report, as they do between the pages of a site. Until 5.2.0 the report added nothing to the browser's history, so Back left the report from wherever the reader was: following a History tab entry to its scenario and pressing Back went to the page the report was opened from.

What the reader does Back afterwards
Choose a tab, or Full or Summary the tab or view before, where the reader was in it
Follow a link to a scenario (a History tab entry, a Timeline row's name from 5.5.0, a failure cluster's scenario, a scenario's link button) the place the link was on, scrolled where it was, the link focused
Choose a scenario in the query builder (5.11.0) the place the builder was opened on, scrolled where it was, the search button focused
Open a feature from the Summary table the table, where it was
Press Next Failure the failure before, then where the reader started
Use a filter (a status, a chip, a mode, Happy paths, the duration, Clear All, the search's clear) the filters before. A run of changes to one control is one step, such as a chip clicked through its states
Run a search from the query builder (5.11.0) the tab and the search before, in one step
Choose Scenarios or Features, a Matched in place, a sort or a page on the Search tab (5.11.0) the Search tab as it was
Follow a search result's title or line (5.11.0) the Search tab on its page and sort, scrolled where it was, the title or line followed focused; Forward goes to the match again
Open an image, an internal-flow popup or, from 5.11.0, the query builder the image, popup or builder closes; Forward opens it again
Open or close a section, sort the Features tab, change a display setting or a scenario's view, switch the theme nothing: these adjust what is shown and add no step

Going back or forward puts back the tab, the view, the Search tab's page and sort, the filters and the image, popup or query builder, then the scroll position and the control that had the focus, and a status line tells a screen reader what changed. A run of arrow keys across the tabs or the views is one step. Closing an image or a popup with its own button, Esc or a click beside it, or the query builder with Esc or a click beside it, takes its step back out, so the next Back goes where it would have gone without it. A place inside a drawn diagram is kept as the diagram's own box, so Back returns to it after a diagram above it has been drawn (5.11.0; until then it could return to an arrow of another diagram). A reload, or a return to the report by Back from another page (a file an attachment opened), opens the same tab, view and filters, scrolled where the reader was, and a tab clicked again opens where the reader left it. A link clicked with Ctrl or Shift opens the scenario in a new tab or window, the History tab's and the failure clusters' included (until 5.2.0 those two cancelled such a click), and from 5.5.0 a Timeline row's name.

Shown inside another page, as Jenkins' HTML Publisher and the Azure DevOps viewer extensions show it, the report's steps join that page's history, so Back goes through them first. Until 5.2.0 every change of the address threw an error inside an Azure DevOps viewer, which stopped a filter before it opened the one scenario it left; where a page forbids changing the address (Firefox inside a viewer served over http), the report works as before, without steps.

Named Examples blocks

A Gherkin scenario outline may split its rows across several Examples: blocks, each with an optional name and free-text description:

Scenario Outline: Market share movement is reported
    ...
    Examples: the merchant gained share
        | Period  | Change |
        | OneWeek | 2.50   |

    Examples: the merchant lost share
        movement is negative
        | Period    | Change |
        | FourWeeks | -2.50  |

From 3.0.64, the parameter table keeps its single header and aligned columns and draws a full-width separator band before each block's rows, carrying:

  • the block name (Examples: the merchant gained share; an unnamed block sitting among named ones shows the bare Examples keyword),
  • a per-block pass/fail count (2/2 passed, or 1 failed, 1/2 passed),
  • the block's description, when the author wrote one.

Row numbering runs continuously across blocks, band rows are inert (clicking one selects nothing), the bands appear in both the flat and grouped table variants of the flatten toggle, and filtering by a block name or description keeps the section and highlights that block's rows.

Bands only render when the outline actually has block structure — more than one block, or a named block. A single unnamed Examples: block (the overwhelmingly common case) and all non-Gherkin adapters render exactly as before.

Block identity is captured by:

  • ReqNRoll live tracking — resolved from the feature-level Cucumber messages Reqnroll 3.3+ embeds in its generated code. On older ReqNRoll/SpecFlow-generated projects (no embedded messages) the fields stay empty and the report renders as before.
  • Cucumber Messages ingest (kronikol ingest) — read from the gherkinDocument envelopes.
  • Generic NDJSON ingest — via the optional examplesBlockName / examplesBlockDescription / examplesBlockIndex fields on the start event (see Ingesting External Captures).
  • Merged reports — block identity survives the mergeable-JSON round trip (see Merging Parallel Reports).

Configuration

Property Default Description
GroupParameterizedTests true Enable/disable display-name-based grouping. OutlineId-based grouping (from ReqNRoll/BDDfy Scenario Outlines) is always active.
MaxParameterColumns 10 Maximum parameter columns before falling back to R0 layout.

xUnit3 Structured Parameter Extraction

When using xUnit v3, the library attempts to extract parameter names and values directly from xUnit's IXunitTestMethod.Parameters and TestMethodArguments APIs. This provides more reliable parameter column headers than display name parsing, especially for complex parameter types. The library falls back to display name parsing when structured extraction is unavailable.

Integration-Specific Report Details

Each integration framework adds its own enhancements to the reports. The sections below describe what each integration contributes beyond the core report structure.

xUnit

The xUnit integration uses the core report generator directly. Features are derived from test class names and scenarios from test display names. xUnit v3 writes the reports when the test run fixture is disposed; xUnit v2's ReportingTestFramework writes them when the test assembly finishes, before the runner hears that it has.

No additional report content is added beyond the core features.

NUnit 4

The NUnit integration uses the core report generator directly. Features are derived from NUnit's TestContext, with scenario names from test method names. Reports are generated in the GlobalTeardown method after all tests complete.

No additional report content is added beyond the core features.

BDDfy (xUnit v3)

The BDDfy integration provides a custom report generator that enhances both HTML reports with:

  • BDD steps — Each scenario displays its Given/When/Then steps in a styled list with colour-coded keywords.
  • Story descriptions — If a BDDfy story has a description, it is shown below the feature heading in italics.

The YAML specifications also include the BDD steps for each scenario.

Scenario data is collected automatically via BDDfyScenarioCollector during test execution. Reports are typically triggered from an [AssemblyFixture].

ReqNRoll (xUnit v2, xUnit v3 & TUnit)

The ReqNRoll integration provides a custom report generator that enhances both HTML reports with:

  • Gherkin steps — Each scenario displays its Given/When/Then steps in a styled list with colour-coded keywords.
  • Feature descriptions — If a Gherkin feature file has a description, it is shown below the feature heading in italics.

The YAML specifications also include the Gherkin steps for each scenario.

Additionally, the ReqNRoll integration automatically enhances ReqNRoll's native Cucumber HTML report by injecting sequence diagram image attachments and raw PlantUML code into the Cucumber Messages JSON. This means diagrams appear inside the standard Cucumber report alongside the existing Gherkin steps and results, with no extra configuration needed.

Scenario data is collected via ReqNRollScenarioCollector during test execution. Reports are triggered from a [AfterTestRun] hook.

LightBDD (xUnit v2, xUnit v3 & TUnit)

The LightBDD integration takes a different approach — rather than generating its own HTML, it plugs into LightBDD's native report pipeline. Report generation is configured via an extension method on ReportWritersConfiguration:

configuration.CreateStandardReportsWithDiagrams(typeof(MyTest).Assembly, options);

This registers LightBDD's built-in formatters (CustomisableHtmlReportFormatter and YamlReportFormatter) with diagram data injected via the ExampleDiagramsAsCode option. LightBDD handles the HTML rendering, BDD step display, and report lifecycle automatically.

The same three output files are produced, but LightBDD controls the HTML layout and styling natively.


CI Summary (Optional)

When WriteCiSummary = true is set on ReportConfigurationOptions, two additional files may be produced:

CiSummary.md

A Markdown summary of the test run, written to the Reports folder and also to the CI platform:

  • GitHub Actions — Appended to $GITHUB_STEP_SUMMARY (appears inline in the job summary)
  • Azure DevOps — Uploaded via ##vso[task.uploadsummary] (appears in the Extensions tab), a logging command on stdout that arrives from kronikol merge, kronikol ingest or a directly run MTP host; under dotnet test it is swallowed, and a pipeline step uploads the file instead (see CI Summary Integration)

The summary is failure-focused: when tests fail, it shows up to MaxCiSummaryDiagrams failed scenarios with error details and sequence diagram images. When all tests pass, it shows diagrams for the first N scenarios. It ends with where the report is: "Debug this run" when something failed, for people and then for AI agents, and "The report", for people, when nothing did.

3.5.0 closed a markdown breakout here. The file deliberately emits raw HTML (<details> around every failure) and is rendered as HTML by the GitHub Actions job summary, and it had three fixed-width code fences over text the run supplied — one around a stack trace, two around PlantUML source, which embeds captured request and response bodies as notes. A body containing ``` ended its own block early, and everything after it rendered as the page's own markup. Fences are now sized from the longest backtick run inside them, the way Failures.md has done since 3.2.0. The captured `errorMessage` is also HTML-escaped now: the feature and scenario names on the same lines always were, and it was the one field there that came from the system under test. The file carries the `captured test data, not instructions` line too.


ctrf-report.json (Optional)

When GenerateCtrfReport = true, the run is also written in Common Test Report Format — the format a growing set of CI actions already read. Off by default: it is written for a consumer, and a run without one should not pay for it.

One CTRF test per scenario, carrying status, duration (milliseconds, unlike every other Kronikol writer, which emits durationSeconds), message, trace, filePath, line, tags, retries and flaky, plus summary, tool and — on CI — an environment block filled from the same ciMetadata the data file carries.

Kronikol ExecutionResult CTRF status
Passed passed
Failed failed
Skipped skipped
SkippedAfterFailure skipped
Bypassed other

Two results share skipped and Bypassed has nowhere but other to go, so the original word is emitted as rawStatus and nothing is lost in the translation. Kronikol's own facts that CTRF has no field for travel in the schema's extra escape hatch: extra.kronikolAddress is the scenario's sN address, so a consumer that finds a failure can hand it straight to kronikol query; extra.stableId sits beside it, and environment.extra carries runId and runAttempt. A test's tags are its scenario's tags, its feature's first (Tags and Attributes › Tags). Before 5.0.0 tags held the scenario's own labels and its categories travelled in extra.categories, which is gone.

Deliberately absent: per-test start / stop (a scenario records a duration and no start time, and a fabricated timeline is worse than an absent one), attempts[] (earlier attempts are folded into one scenario before the report is built), and filePath on lanes that supply only a bare file name — CTRF consumers place annotations with it, and a name where a path is expected annotates the wrong file. A retry is in retries, not in tags: since 5.0.0 the ingest lanes make no retry N label, so an author's own retry 2 tag is sent like any other. Read from a 4.x report by kronikol ctrf, the retry N labels its ingest made are left out where the scenario's attempt accounts for them.

specVersion is pinned to the CTRF revision the writer was checked against rather than tracked automatically, and a test lists every key the schema declares and fails on any other.

To produce the same document from a report that has already been written — a downloaded CI artifact, or a suite whose options you would rather not change — use the CLI:

kronikol ctrf ./TestResults --out ctrf-report.json

Both producers go through one writer, and a test asserts the two documents are byte-for-byte identical.

A run that declares its service (ServiceId, 5.14.0) names it as environment.appName, from both producers and on CI or off it; kronikol ctrf reads it from the report's serviceId. Without one, environment is written on CI only, as before. See Querying Reports for the verb, and CI Summary Integration for how this sits beside the job summary.


Browser-Rendered Reports

When using PlantUmlRendering.BrowserJs, the standard HTML reports (TestRunReport and Specifications) also render PlantUML diagrams client-side in the browser using the same PlantUML TeaVM JS engine. Diagrams are lazily rendered as the user scrolls, keeping initial page load fast even with thousands of diagrams. See PlantUML Browser Rendering for details.

See CI Summary Integration for full configuration and CI platform setup guides.


CI Artifact Upload (Optional)

When PublishCiArtifacts = true is set on ReportConfigurationOptions, all report files in the Reports directory (.html, .yml, .md, .json, .jsonl, .xml) are published as CI build artifacts — which includes Failures.md, Failures.jsonl, Specifications.md and ctrf-report.json whenever those are written:

  • Azure DevOps — Each file is uploaded via an ##vso[artifact.upload] logging command written to stdout at the end of the run, and since 4.28.1 each attachment the report holds with them, under attachments/. It arrives from kronikol merge, kronikol ingest or a directly run MTP host; under dotnet test it is swallowed, so publish the folder with a PublishPipelineArtifact@1 step (see CI Artifact Upload).
  • GitHub Actions — The reports directory path and retention days are written to $GITHUB_OUTPUT. Add a single upload-artifact step to your workflow.

This works alongside the CI summary — the summary appears inline in the build page while the full reports are available as downloadable artifacts.

See CI Artifact Upload for full configuration and workflow examples.


Diagram Storage Format

From 5.6.0 the HTML report is written compressed (The compressed page), so the file holds no diagram source that a text search or a parser can reach: write the plain page with kronikol decode first, and read that. A page written before 5.6.0 is plain already.

In the plain page the diagrams are stored as gzip-compressed base64. Their sources are in one <script id="puml-data" type="application/json">, a JSON map from each diagram element's id (puml-0, puml-1, …) to its source; an activity diagram carried inline, as in a merged report, holds its source the same way in a data-plantuml-z attribute. The browser-side JavaScript decompresses and renders them dynamically. This means you cannot find diagram content by searching the HTML source with plain text — the content is opaque base64 strings.

To inspect diagram content programmatically:

# PowerShell: decompress a diagram's source from the report
kronikol decode Reports/TestRunReport.html -o plain.html    # 5.6.0+: the plain page
$html = Get-Content "plain.html" -Raw
$m = [regex]::Match($html, '<script id="puml-data" type="application/json">([^<]*)</script>')
$sources = $m.Groups[1].Value | ConvertFrom-Json             # diagram id -> gzip and base64
$bytes = [Convert]::FromBase64String($sources.'puml-0')
$ms = [System.IO.MemoryStream]::new($bytes)
$gz = [System.IO.Compression.GZipStream]::new($ms, [System.IO.Compression.CompressionMode]::Decompress)
$sr = [System.IO.StreamReader]::new($gz)
$sr.ReadToEnd()  # → @startuml ... @enduml

Before 5.6.0 this page's recipe read the first data-plantuml-z attribute, which only an activity diagram carried inline uses, and passed 'Decompress' as a string, which PowerShell 7 cannot match to one GZipStream constructor.

Tip: Use the Click-to-reveal PlantUML feature in the HTML report instead — click any diagram image to see the raw PlantUML source directly in the browser. Without a browser, kronikol query diagram <reports-dir> s3/d0 --out s3-d0.puml writes a scenario's diagram source from the data file (Querying Reports).


ReportGenerator API

The ReportGenerator static class provides methods for generating individual report outputs. Most users call CreateStandardReportsWithDiagrams (which calls the others internally), but the individual methods are available for custom pipelines.

CreateStandardReportsWithDiagrams

Generates all standard reports (HTML specifications, YAML specs, test run report, component diagram) in one call:

public static void CreateStandardReportsWithDiagrams(
    Feature[] features,
    DateTime startRunTime,
    DateTime endRunTime,
    ReportConfigurationOptions options);

GenerateHtmlReport

Generates a single HTML report file with embedded diagrams. Returns the file path of the generated report. This method accepts all the rendering and styling options as parameters:

public static string GenerateHtmlReport(
    DefaultDiagramsFetcher.DiagramAsCode[] diagrams,
    Feature[] features,
    DateTime startRunTime,
    DateTime endRunTime,
    string? stylesheet,
    string fileName,
    string title,
    bool includeTestRunData,
    bool generateBlankOnFailedTests = false,
    bool lazyLoadImages = true,
    DiagramFormat diagramFormat = DiagramFormat.PlantUml,
    PlantUmlRendering plantUmlRendering = PlantUmlRendering.BrowserJs,
    bool inlineSvgRendering = false,
    bool internalFlowTracking = false,
    string internalFlowDataScript = "",
    Dictionary<string, InternalFlowSegment>? wholeTestSegments = null,
    RequestResponseLog[]? trackedLogs = null,
    WholeTestFlowVisualization wholeTestVisualization = WholeTestFlowVisualization.None,
    CiMetadata? ciMetadata = null,
    bool showStepNumbers = false,
    string? customCss = null,
    string? customFaviconBase64 = null,
    string? customLogoHtml = null,
    bool groupParameterizedTests = true,
    int maxParameterColumns = 10,
    bool titleizeParameterNames = true,
    string? componentDiagramPlantUml = null,
    Dictionary<string, Merge.WholeTestFlowFragment>? precomputedWholeTestContent = null,
    bool showNoInteractionsMarker = false,
    IReadOnlyList<DiagnosticEntry>? diagnostics = null,
    BackgroundCalls? background = null,
    int browserRenderWorkers = Constants.TrackingDefaults.BrowserRenderWorkers,
    int browserRenderCacheMegabytes = Constants.TrackingDefaults.BrowserRenderCacheMegabytes,
    int browserFragmentMaxHeight = Constants.TrackingDefaults.BrowserFragmentMaxHeight,
    bool separateBackgroundSteps = false,
    bool collapseRepeatedStepKeywords = true,
    NotePayloadFormat notePayloadFormat = NotePayloadFormat.Yaml,
    bool fullSearchIndex = true,
    SearchIndex.SearchIndexBuildCache? searchIndexCache = null,
    ResolvedToggleDefaults? toggleDefaults = null,
    string? suite = null,
    HistoryVerdicts? history = null,
    bool showHistorySection = false,
    bool showReportDiagnostics = false);

It draws what it is handed. Verdicts in history draw each scenario's sparkline, verdict pill and data-history-verdicts attribute, as they did before 4.6.0, and the History tab with showHistorySection; diagnostics draw the Diagnostics tab with showReportDiagnostics (5.0.0; before, the History and Report diagnostics sections). Both parameters default to false here, where the options a run reads default to true from 5.0.0. Each brings the stylesheet rules it needs, and the verdicts bring the filter code of the verdict operators. A run hands it history only when ShowScenarioHistory or ShowHistorySection asks, and writes the labs page itself: this method does not write it.

GenerateSpecificationsData

Generates a YAML or JSON specifications file. Returns the file path:

public static string GenerateSpecificationsData(
    Feature[] features,
    string fileName,
    string title,
    DataFormat format,
    bool generateBlankOnFailedTests = false);

GenerateTestRunReportData

Generates a test run report in YAML, JSON, or XML format. Returns the file path:

public static string GenerateTestRunReportData(
    Feature[] features,
    DateTime startTime,
    DateTime endTime,
    string fileName,
    DataFormat format,
    DefaultDiagramsFetcher.DiagramAsCode[]? diagrams = null,
    RequestResponseLog[]? trackedLogs = null);

GenerateTestRunReportSchema

Generates a JSON or XML schema file for the test run report format. Returns the file path:

public static string GenerateTestRunReportSchema(
    string fileName,
    DataFormat format);

Troubleshooting

Entries logged but not appearing in report

If you are calling RequestResponseLogger.Log() (e.g. from a custom tracker) and the entries don't appear in the generated report:

  1. Check TestId matches — Entries are grouped by TestId. If the TestId you pass to RequestResponseLogger.Log() doesn't match what BDDfy/xUnit/ReqNRoll uses for the scenario, the entries will be orphaned.
  2. Check TrackingIgnore — Entries with TrackingIgnore = true are excluded from diagram rendering.
  3. Pair your Request and Response — Each interaction needs both a Request and Response entry with matching TraceId and RequestResponseId. An unpaired entry produces an incomplete arrow.
  4. Check timing — Ensure logging happens before report generation. If you log entries after CreateStandardReportsWithDiagrams() runs, they won't appear.
  5. Don't search HTML with plain text — From 5.6.0 the whole page is compressed, so a text search of TestRunReport.html finds nothing at all; in the plain page kronikol decode writes, as in every page before 5.6.0, diagrams are stored as gzip-compressed base64 (see Diagram Storage Format above). Plain text search for participant names or URIs will return zero results. Ask the data file instead: kronikol query grep <reports-dir> "<text>" finds a URI or a body, and kronikol query services <reports-dir> lists every service the run called.
  6. The YAML files don't contain diagrams — The Specifications.yml and TestRunReport.yml files contain scenario metadata only, not PlantUML diagram content. The data file (TestRunReport.krkl, or TestRunReport.json) holds every scenario's diagram sources: kronikol query diagram <reports-dir> s3/d0 --out s3-d0.puml writes one (Querying Reports).

Validating Tracking Configuration Changes

When upgrading Kronikol versions, changing tracking configuration, or refactoring test fixtures, tracked interactions can silently decrease. Tests continue to pass, but diagrams lose detail. Always validate tracking fidelity after configuration changes.

Gold Standard Workflow

  1. Capture a baseline before any changes — run your full test suite and keep the data file where the tool looks for it (TestRunReport.krkl from 5.0.0, TestRunReport.json before it or with CompressTestRunReport = false):

    New-Item -ItemType Directory -Force Reports/baseline | Out-Null
    Copy-Item Reports/TestRunReport.krkl Reports/baseline/TestRunReport.krkl
  2. Make changes one at a time for easier diagnosis.

  3. Re-run the suite, and ask what moved:

    kronikol query diff ./Reports/TestRunReport.krkl --baseline

    The answer names what broke, what was fixed, what is new, what got materially slower, what disappeared — and a Tracking section listing every service that captured fewer calls than it did in the baseline, computed over the scenarios both runs hold, so a test added or removed since the baseline neither hides a loss nor invents one (3.6.0):

    - baseline/TestRunReport.krkl  2026-01-14T22:10:03Z  412 scenarios, 0 failed
    + TestRunReport.krkl           2026-01-15T09:31:44Z  412 scenarios, 0 failed
    
    Tracking (2):
      total  3,908 -> 3,656 calls  (-252)
      PricingApi                    252 -> 0 calls  - no longer tracked
    

    Nothing else in the run says that. Every test still passed: a client that stopped being tracked costs you diagram detail, not a red build, which is exactly why this check has to be deliberate.

  4. Accept increases when explained by the change (e.g. adding verbosity, adding tracking to new clients). A run that captured more than the baseline prints no Tracking section at all.

  5. Reject a service falling to zero. That is a correlation or configuration regression, not a thinner run. Messaging services (Service Bus, Kafka) are the usual casualties, because their correlation depends on headers a client change can silently stop propagating. The diff reports it in three shapes: a service at 0 calls — no longer tracked; a scenario that stopped seeing a service while others still do (PricingApi 5 → 0 calls in s3 Cart is priced — still 5 elsewhere); and a run that captured nothing at all (total N → 0 calls — nothing tracked). Before 3.6.0 the last of those never fired: a run with no interactions was skipped as "absent", and the diff closed with no change in results, timings or tracked calls. The one absence that is still not a loss is a side that cannot carry traffic — a mergeable file written before 3.1.0 — which the diff notes and leaves out.

The other counts the old checklist tracked by hand — features, scenarios, diagrams, service names — are in kronikol query summary and kronikol query services for both files, and the scenario counts are on the diff's own two header lines.

Before 3.1.0 this workflow was a hand-rolled PowerShell script that loaded both reports with ConvertFrom-Json. That is worth avoiding for the reason kronikol query exists at all: a real report is tens of megabytes, and reading one whole to count an array is the thing the tool is built not to do. It also compared a single global total, so a service falling to zero while another grew by the same amount looked like no change.

For CI, Merging Parallel Reports has the GitHub Actions and Azure DevOps recipes for downloading last-green into Reports/baseline/ automatically. If the baseline lives elsewhere, name it once with $KRONIKOL_BASELINE instead of copying it.

Home · 🔍 Search this wiki Full-text search across every page. The “Pages” list above matches page and heading names, not page content.


Demo


Getting Started

Common Tasks

Integration Guides

Uninstrumentable / polyglot backends

Extensions

Configuration

Features

Reference

Clone this wiki locally