Skip to main content

CLI reference

prototest reads ProtoTest evidence from a terminal. It prints a run summary, builds a page over a folder of runs, checks two reports, and posts the feedback digest. It needs no agent and no browser. The feedback action installs it and calls the same commands in CI, so a local run and a CI step read the same archive the same way.

Install​

dotnet tool install --global ProtoTest.Cli

The tool command is prototest. The package targets .NET 8; on a machine with only a newer runtime, set DOTNET_ROLL_FORWARD=LatestMajor so the tool starts. The action sets that for you.

The verbs​

usage: prototest summary <file.prototrace>
prototest index <folder>
prototest feedback <file.prototrace> [--digest <path>]
prototest verify <baseline-report.json> <current-report.json>
You want toRunIt writes
read one runsummary <file.prototrace>nothing
share a folder of runsindex <folder>index.html and a .digest.json beside each archive
check a run against a baselineverify <baseline.json> <current.json>nothing
post the digestfeedback <file.prototrace> [--digest <path>]the --digest file, and the posts

An unknown verb, or the wrong arguments, prints that usage to stderr and exits 1. There is no --help verb: prototest --help prints the same usage to stderr and exits 1.

summary​

Reads one trace and prints the deterministic diagnosis as text: the run id, the outcome counts, every test that did not fully succeed with its error, source location and failing operation, and the run gates. It is the document get_diagnosis returns as JSON.

prototest summary TestResults/ProtoTest/run.prototrace
ProtoTest trace 2.0 · run 761778e6dc82498a9f9965fa1e6b5a24 · 2026-09-29 06:19:01Z - 2026-09-29 06:19:05Z
1 tests · 1 failed

FAILED Northstar.ProtoTest.FailureDrills.TheAddressWasHardcodedForOneMachine (2.66 s)
ConnectionError reaching http://127.0.0.1:5099: connection refused.
test.execution Test execution · failed
cause: runner-reported failure

Run ids and timestamps are new on every run; compare the shape, not the values.

A run with nothing to report prints the header, the counts and the words All green.:

ProtoTest trace 2.0 · run 761778e6dc82498a9f9965fa1e6b5a24 · 2026-09-29 06:19:01Z - 2026-09-29 06:19:05Z
1 tests · 1 succeeded
All green.

Diagnosis explains every line of the failing block and the rules behind the cause.

Exit 0 when the summary printed. Exit 1 when the file is missing or cannot be read, with the reason on stderr:

Trace file not found: TestResults/ProtoTest/run.prototrace

index​

Discovers the .prototrace archives under the folder, newest first, and writes the evidence into a folder you can share:

  • index.html in the folder, listing each run's outcome counts, the tests that did not pass, links to its trace and its digest, and every archive that could not be read with the reason.
  • A .digest.json file beside each archive, the diagnosis JSON the page links to.
prototest index TestResults
Indexed 2 runs into 'C:\dev\your-repo\TestResults\index.html'.

Discovery looks at the folder's TestResults/ first and walks the tree only when that yields no readable run; Setup has the details. An archive it could not read is named, not guessed at:

Indexed 1 run into 'C:\dev\your-repo\TestResults\index.html'.
Skipped 'C:\dev\your-repo\TestResults\broken.prototrace': Central Directory corrupt.

Exit 0 when the page is written. Exit 1 when the folder is missing, holds no readable archive, or the page cannot be written.

verify​

Compares two JSON reports from a ProtoTest.Reporting sink:

prototest verify baseline.json TestResults/ProtoTest/report.json

The baseline is the default branch report. The current report is the run under review. The failing findings print one ::error workflow command each, which a GitHub runner turns into an annotation, and the verdict then lists the findings and the coverage deltas. The default severities make the verb a pull request gate: regressed, stale-spec and gate-failed fail, and added-uncovered warns.

::error::regressed: Target 'Northstar:Api' unit 'GET /api/v1/orders' in category 'OpenAPI' was covered in the baseline and is uncovered now.
ProtoTest verification failed: 1 failing, 0 warning(s), 0 info
fail regressed: Target 'Northstar:Api' unit 'GET /api/v1/orders' in category 'OpenAPI' was covered in the baseline and is uncovered now.
coverage deltas:
Northstar:Api · OpenAPI: 1/2 covered -> 0/2 covered (-50 points, 1 regressed, 0 added uncovered)

Verification explains each finding class and the specification identity.

Exit 0 when no finding is a fail and 1 when one is. Exit 1 also when a report file is missing or cannot be read:

Report file not found: baseline.json

feedback​

Reads one run's digest and posts it:

prototest feedback TestResults/ProtoTest/run.prototrace --digest digest.json

Two streams, and the split is the point:

StreamWhat carries
stdoutone ::error workflow command per failing test and per failed run gate
stderrone line per channel: posted, skipped with the reason, or failed
a filedigest.json, written by --digest

The stderr half, from a run with no pull request configured:

prototest feedback: github-annotations posted (1 annotation.)
prototest feedback: github-pr-comment skipped (No GitHub token: set GITHUB_TOKEN.)
prototest feedback: webhook skipped (No webhook URL: set PROTOTEST_FEEDBACK_WEBHOOK_URL.)

The stdout half is the annotation GitHub renders on the pull request, and The evidence loop shows the same command with both streams. A failure with a source location renders it as properties (::error file=path/to/OrderTests.cs,line=42::message); without one it is the bare ::error::message form, and run-gate annotations never carry a location.

Every channel reports its outcome on stderr. A channel with no target, or nothing to post, skips. Only a channel that reached its target and failed makes the verb exit 1. With no target configured the verb is safe to run locally.

Environment targets​

feedback reads its channel targets from the environment. The names are the GitHub Actions convention, so CI needs no extra inputs.

VariableChannelMeaning
GITHUB_TOKENcommentthe token that posts the comment; the workflow needs issues: write
GITHUB_REPOSITORYcommentthe repository as owner/name
GITHUB_EVENT_PATHcommentthe event payload file; the pull request number is read from it
GITHUB_API_URLcommentthe GitHub REST base URL; defaults to https://api.github.com
PROTOTEST_FEEDBACK_TRACE_URLcommentthe artifact URL the comment links to
PROTOTEST_FEEDBACK_WEBHOOK_URLwebhookthe address the digest JSON is posted to
PROTOTEST_FEEDBACK_WEBHOOK_SECRETwebhookthe shared-secret header value; no header is sent without it
PROTOTEST_FEEDBACK_WEBHOOK_SECRET_HEADERwebhookthe shared-secret header name; defaults to X-ProtoTest-Secret

The feedback action maps its inputs to these names, so a local command and the action take the same path.

Webhook payload​

The webhook posts the run's diagnosis digest: the same document prototest summary prints as text and get_diagnosis returns as JSON. It is POSTed to PROTOTEST_FEEDBACK_WEBHOOK_URL with content type application/json, serialized with camel-case property names. A green run is posted too: the digest carries empty failures, so a machine consumer decides what to do with it (Feedback_ShouldPostTheWebhookForAGreenRun in tests/ProtoTest.Feedback.Tests). Only a missing URL skips.

{
"digestVersion": "1",
"traceFormatVersion": "2.0",
"runId": "761778e6dc82498a9f9965fa1e6b5a24",
"traceFile": "TestResults/prototest-761778e6.run.prototrace",
"startedAtUtc": "2026-09-29T06:19:01Z",
"completedAtUtc": "2026-09-29T06:19:05Z",
"environment": { "runtime": ".NET 10", "os": "Linux" },
"outcomes": { "passed": 12, "failed": 1 },
"failures": [
{
"testId": "…",
"name": "Orders_endpoint_responds",
"className": "Shop.Tests.OrderTests",
"methodName": "Orders_endpoint_responds",
"outcome": "failed",
"durationMs": 2660.0,
"failure": {
"kind": "http.request",
"name": "GET /api/orders",
"phase": "Execution",
"status": "Failed",
"errorType": "ConnectionError",
"errorMessage": "Connection refused.",
"sourceFile": "Shop.Tests/OrderTests.cs",
"sourceLine": 42
},
"rule": "operationError"
}
],
"gates": [{ "name": "NoRegressions", "verdict": "failed", "message": "…", "details": [] }],
"findings": [],
"coverage": { "total": 24, "covered": 23, "uncovered": 1, "percentage": 95.8 },
"coverageAbsentReason": null
}

Trimmed: each failure also carries its mismatches, findings and artifacts, each gate its details, and the coverage its report artifact path. A run with no failures posts the same shape with empty failures, gates and findings and no stdout annotations. Run ids, file paths and timestamps differ on every run.

The secret header rules:

FactRule
No PROTOTEST_FEEDBACK_WEBHOOK_SECRETNo header is sent
Secret set, no custom header nameThe secret rides X-ProtoTest-Secret
PROTOTEST_FEEDBACK_WEBHOOK_SECRET_HEADER setThe secret rides that header name instead
The endpoint refuses or does not answerThe channel reports failed and the verb exits 1

Exit codes​

CodeMeaning
0the verb did its job
1the input was missing or unreadable
1index found no readable archive or could not write the page
1the verdict has a fail finding
1a feedback channel that reached its target failed

Limits​

  • The verbs read files. The only writes are the index page, the digests beside the traces and the --digest file.
  • No network call happens unless a target is configured. A missing target is a named skip, never a failure.
  • The verbs take no other arguments, and there is no verb that reruns a suite, writes a trace or changes an archive.
  • The CLI writes UTF-8 without a BOM and sets the console output encoding, so the · separator renders on a default Windows console; redirected output stays parsing-friendly.
  • The digest is built from the written archive after the run, so it reflects what the run recorded (The evidence loop).
  • These four verbs are the whole prototest surface.

Run prototest summary over the newest archive, or prototest index over the results folder, and the same evidence your agent reads is on your terminal.