Docs

Hunt bugs

What a bug report states

Testkit files each bug as an issue in GitHub or Gitea. The issue has five sections, names the test that fails, and ends with a facts block.

View as Markdown

On this page
  1. From a bug record to an issue
  2. The five sections
  3. Write the bug record
  4. Severities
  5. Cause, impact, and escape
  6. The test that comes with it
  7. The issue Testkit files
  8. Labels
  9. Body
  10. Facts block
  11. What testkit bug file checks
  12. Secrets
  13. Known failures
  14. Where the evidence lives
  15. What a report never does

From a bug record to an issue

A bug report starts as a bug record. The bug record is a JSON file that your coding agent writes during a campaign, or that you write yourself. Three commands turn it into an issue.

CommandWhat it does
testkit bug guidePrints the bug record format, the severities, and the cause, impact, and escape lists
testkit bug record <file>Checks a bug record, stores it as a draft under .testkit/bugs/, and prints the draft id
testkit bug file <id>Runs the draft's test and files the bug as an issue

The commands work with or without an active campaign. Bug filing needs no Turnstone account.

The five sections

Every issue that Testkit files has five sections.

SectionWhat it answersWhere it comes from
Expected behaviourWhat should the product do?expected in the bug record
AuthorityWhere is that written down?authority in the bug record
Observed behaviourWhat does the product do instead?observed in the bug record
Root causeWhich code makes it happen?rootCause in the bug record
ReproduceWhich command shows it?Testkit writes the command from test.file

A report with all five can be checked by someone who was not there. Every field of the bug record is required, so no report lacks one.

Write the bug record

$npx testkit bug guide

The command prints the fields of a bug record, the severities, and the lists of cause, impact, and escape ids.

FieldValue
titleOne line of at most 200 characters that names the broken behaviour. It becomes the issue title.
severitycritical, high, medium, or low
expectedWhat the product should do
authorityThe source that says so: documentation, a specification, an API contract, or the code's own stated intent. Name it and quote the part that applies.
observedWhat the product does instead
rootCauseWhy the code behaves this way
test.fileThe Testkit test file that fails because of the bug
test.titleThe failure's title, in one line of at most 200 characters
fault.pathThe source file that holds the mistake
fault.linesThe first and last faulty line as [start, end], counted from 1. One faulty line is [41, 41].
causeOne cause id
impactOne impact id
confidence.causehigh or low. How sure the writer is of the cause.
confidence.impacthigh or low. How sure the writer is of the impact.
escapeOne escape id
bug.json
{
	"title": "A refund pays out twice",
	"severity": "high",
	"expected": "The second POST /orders/{id}/refunds for the same order returns 409 and pays nothing.",
	"authority": "`docs/api/refunds.md`, \"A refund is idempotent per order.\"",
	"observed": "The second request returns 201 and creates a second payout.",
	"rootCause": "`src/checkout/refunds.ts` retries the payout call without an idempotency key.",
	"test": {
		"file": "src/checkout/__testkit__/refund.contract.testkit.ts",
		"title": "a refund pays out once"
	},
	"fault": { "path": "src/checkout/refunds.ts", "lines": [41, 58] },
	"cause": "mishandled-error",
	"impact": "financial-error",
	"confidence": { "cause": "high", "impact": "high" },
	"escape": "no-test"
}

These rules apply to the fields.

  • Write test.file and fault.path relative to the product directory, the directory that holds testkit.config.ts.
  • test.title must be the failure's title exactly as Testkit reports it. In a Vitest or Playwright file, that is the names of the describe blocks around the test and the test's own name, joined by single spaces. In a k6 file, that is the label of the failed assertion.
  • A k6 script that fails as a whole has no failure title. Write in test.title what the script tests. Testkit then files one issue that covers the whole file.
  • Keep credentials, tokens, and passwords out of the record.

Severities

SeverityMeaning
criticalThe product is unsafe to run or to trust.
highA core behaviour is broken with no acceptable workaround.
mediumA behaviour is broken and a workaround exists, or the reach is limited.
lowA minor defect with little consequence.

Cause, impact, and escape

Each of these fields takes one id from a list. testkit bug guide prints each id's definition and the rule that separates it from its neighbours.

FieldQuestionIds
causeWhat kind of mistake is in the code?missing-validation, mishandled-error, race-condition, unbounded-or-inefficient-work, incorrect-query-or-selection, missing-access-control, null-or-default-mishandling, stale-state, interface-mismatch, omitted-step, incorrect-logic, other
impactWhat is at stake for the business?unauthorized-access, financial-error, audit-or-compliance-gap, data-loss-or-corruption, workflow-breakdown, availability-or-performance, blocked-operation, wrong-information-shown, other
escapeWhy did the existing tests not catch the bug?no-test, untested-input, untested-interleaving, wrong-assertion, test-double, other

A bug gets one id for each field. The impact list is in precedence order, and the first id that applies wins. Use other when none fits.

The test that comes with it

The bug record names one test. The test's name states the expected behaviour, so the name is true once the product is fixed.

src/checkout/__testkit__/refund.contract.testkit.ts
import { defineFile } from "@gfdelarue/testkit";
import { contractScenario, expect } from "@gfdelarue/testkit/node-vitest/contract";

export const testkit = defineFile({ engine: "node-vitest" });

contractScenario("a refund pays out once", async ({ http }) => {
	const first = await http().post("/orders/8841/refunds");
	const second = await http().post("/orders/8841/refunds");
	expect(first.status).toBe(201);
	expect(second.status).toBe(409);
});

Against the broken product, the test fails on the line that states the expected behaviour.

$npx testkit run --file src/checkout/__testkit__/refund.contract.testkit.ts
[1/1] FAIL api contract src/checkout/__testkit__/refund.contract.testkit.ts 0s
    AssertionError: expected 201 to be 409 // Object.is equality at
    /repo/src/checkout/__testkit__/refund.contract.testkit.ts:10:24

The issue Testkit files

The issue's title is the title of the bug record.

Labels

LabelMeaning
testkitFiled by Testkit with a regression test. Every issue that Testkit files carries it.
severity:<level>The bug's severity, such as severity:high
area:<id>The product area that holds the fault. The label's description is the area's name.

Testkit creates a label that does not exist yet and never changes one that does. An issue carries an area:<id> label when areas is set.

Severity and area are labels because people re-triage both. To change a bug's severity or area, replace the label. An issue with two severity: labels counts as having none, and the same holds for area: labels.

Body

The body has the five sections, three lines that locate the bug, and the facts block.

The body of the issue for bug.json
## Expected behaviour

The second POST /orders/{id}/refunds for the same order returns 409 and pays nothing.

## Authority

`docs/api/refunds.md`, "A refund is idempotent per order."

## Observed behaviour

The second request returns 201 and creates a second payout.

## Root cause

`src/checkout/refunds.ts` retries the payout call without an idempotency key.

## Reproduce

```sh
npx testkit run --file src/checkout/__testkit__/refund.contract.testkit.ts
```

Test: `src/checkout/__testkit__/refund.contract.testkit.ts`, "a refund pays out once".
Found at `4f0c2d1e9b7a4c3d8e6f5a4b3c2d1e0f9a8b7c6d`.
Fault: `src/checkout/refunds.ts`, lines 41 to 58.

<details>
<summary>Testkit facts</summary>

<!-- testkit-facts:start -->

```json
{
  "v": 1,
  "cause": "mishandled-error",
  "impact": "financial-error",
  "confidence": { "cause": "high", "impact": "high" },
  "escape": "no-test",
  "caughtBy": "campaign",
  "campaign": "refunds-2026-10",
  "testType": "contract",
  "test": {
    "file": "src/checkout/__testkit__/refund.contract.testkit.ts",
    "title": "a refund pays out once"
  },
  "foundAt": "4f0c2d1e9b7a4c3d8e6f5a4b3c2d1e0f9a8b7c6d",
  "fault": { "path": "src/checkout/refunds.ts", "lines": [41, 58] },
  "area": { "id": "checkout", "name": "Checkout", "share": 0.14 },
  "introducedBy": "9b7a4c3d8e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b",
  "introducedMethod": "blame"
}
```

<!-- testkit-facts:end -->

</details>

The file names, the commits, and the campaign in this body are examples.

The reproduce command runs from the repo root. When testkit.config.ts is in a subdirectory, the command carries --dir <directory>, and the --file path is relative to that directory. The Test: and Fault: lines always give paths from the repo root. A fault on one line reads line 41.

Facts block

The facts block is one JSON object between two marker comments. It sits in a collapsed section named Testkit facts, and tools read it. Facts lists what Turnstone reads from it.

FieldWrittenMeaning
vAlwaysThe format version, 1
causeAlwaysThe kind of mistake in the code
impactAlwaysWhat is at stake for the business
confidence.causeAlwaysThe writer's confidence in cause, high or low
confidence.impactAlwaysThe writer's confidence in impact, high or low
escapeAlwaysWhy the existing tests did not catch the bug
caughtByAlwayscampaign when a campaign was active when the bug was recorded, and manual otherwise
campaignWith caughtBy: "campaign"The id of the campaign that caught the bug
testTypeAlwaysThe type of the regression test, from its file suffix
test.fileAlwaysThe regression test file
test.titleWhen the issue covers one testThe failure's title
foundAtAlwaysThe commit the test failed at
fault.pathAlwaysThe source file that holds the mistake
fault.linesAlwaysThe first and last faulty line at foundAt, counted from 1
area.idWhen an area claims fault.pathThe area's key in areas
area.nameWith area.idThe area's name when the bug was filed
area.shareWith area.idThe area's share of the repo's source bytes at foundAt, from 0 to 1
introducedByWhen the blame resolvesThe newest commit among the fault lines at foundAt
introducedMethodWith introducedByHow Testkit found introducedBy. The value is blame.

All paths in the block use / and are relative to the repo root. Commits are full commit ids.

The package ships the full format, with the rules for a tool that reads it, as node_modules/@gfdelarue/testkit/docs/bug-records.md.

What testkit bug file checks

testkit bug file runs the test at the checked-out commit and files the issue only when the test fails. It files nothing in these cases.

testkit bug file stops whenWhat to do
Tracked files have uncommitted changesCommit or discard the changes. Then run the command again.
No remote branch contains the commitPush the commit. Then run the command again.
An open issue already covers the testRead that issue. Testkit keeps one open bug for each test.
areas is set, and no area claims the fault's file or two areas doThe message names the file. Change areas in testkit.config.ts so that one area claims the file.
test.title is not one of the failure titles that the file reportsThe message lists the titles that a bug record can use.

The token needs permission to create issues and labels. Set the token lists the variables that Testkit reads it from.

Secrets

Testkit redacts the secrets it recognises in the title, in every section, and in test.title. It does so when it records the bug and again when it files the issue. Testkit cannot recognise every secret, so keep credentials, tokens, and passwords out of the record.

Testkit does not rewrite a path or an area's name. It stops in these cases.

  • testkit bug record rejects a record whose test.file or fault.path holds text that Testkit redacts.
  • testkit bug file stops when the area's name or the reproduce command holds such text.
  • Testkit files no bug under a failure title that holds such text. Rename the test, or give the assertion a label.

Known failures

A known failure is a failing test that an open issue with the testkit label covers. bugs.knownFailures in testkit.config.ts decides what a known failure does to a run.

ValueWhat a run does
"fail"Every failing test fails the run. This is the default.
"allow"A run whose failures are all known passes. One new failure fails the run.
testkit.config.ts
export default defineConfig({
	bugs: { knownFailures: "allow" },
});

Use "allow" to keep CI passing while a filed bug waits for its fix. These rules apply.

  • An issue covers a failure when its facts block gives the failing file as test.file and the failure's title as test.title. An issue with no test.title covers the whole file.
  • Closing an issue makes its test count again.
  • A failure that names no test is always new. A crash, a timeout, and a file that does not load are such failures.
  • When Testkit cannot read the issues, every failure is new.
  • A run that fails outside its tests fails under both values.

With "allow", the run reads the open issues, so testkit run needs a token. Pass a run whose failures are all filed shows the CI step.

Where the evidence lives

The issue points to evidence where it already is. Do not copy it somewhere new.

EvidenceWhere it lives
The failing testYour repo, at the commit on the issue's Found at line
The command that shows the failureThe issue's Reproduce section
The faulty linesYour repo, at the path and lines on the issue's Fault: line
The full runThe CI job's uploaded .testkit/results

Read a run's results shows how to find a file's output in a run.

Turnstone holds none of this evidence. What Turnstone keeps lists the facts that Turnstone stores.

What a report never does

  • It never skips the failing test.
  • It never asserts the broken behaviour so that the test passes.
  • It never reports a failure without a cause. A failing test with no root cause is a lead, and the agent keeps working on it.
  1. OverviewStartWhat Testkit is, and what you need to run it.
  2. Get startedStartFive steps from install to a merge gate.
  3. Test filesWrite testsWhere a test lives and what its name means.
  4. Contract testsWrite testsCall your running service from Node.
  5. HTTP testsWrite testsDrive routes with k6, signed in as named actors.
  6. Browser testsWrite testsAssert on a real page in Chromium.
  7. Database testsWrite testsRun SQL against a Postgres that Testkit builds.
  8. CIRunOne command decides whether a pull request merges.
  9. ResultsRunWhere a run's evidence lands and how to read it.
  10. CLIRunEvery command and flag.
  11. ConfigurationRunEvery field of testkit.config.ts.
  12. CampaignsHunt bugsGive your agent a target and collect proven bugs.
  13. Bug reportsHunt bugsThe five things every issue states.
  14. TurnstoneTurnstoneWhat the dashboard keeps, and what it never stores.
  15. Connect GitHubTurnstoneConnect a repository, and see the access Turnstone asks for.
  16. Connect GiteaTurnstoneAdd your Gitea host, then connect a repository.
  17. SyncTurnstoneWhen Turnstone reads your issues, and what it reads.
  18. FactsTurnstoneEach fact about a bug, and where Turnstone reads it.