Skip to content

fix(engine): run retention keeps the run a baseline names, so regression checks keep working - #378

Merged
bmscomp merged 2 commits into
mainfrom
fix/audit-09-keep-baselines
Oct 17, 2026
Merged

bmscomp merged 2 commits into
mainfrom
fix/audit-09-keep-baselines

Conversation

@bmscomp

@bmscomp bmscomp commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Why

A baseline names a run by id. baseline_runs (V22) holds one row per test type, with a run_id column that has no foreign key. Run retention ignored it. TestRunRepository.countByStatusCreatedBefore and findIdsByStatusCreatedBefore, the two queries behind RunRetention, matched on status and createdAt only. So the run a baseline names was deleted like any other finished run.

Checked in the code:

  • Set a LOAD baseline on run X. 90 days later (kates.cleanup.retention-days=90), the daily sweep deletes X. A kates test prune or DELETE /api/tests with a later cutoff deletes it sooner.
  • From then on, BaselineService.compareRegression finds the baseline row but not its run, and returns Baseline run not found: X. ReportResource answers that with 404, so GET /api/tests/{id}/report/regression fails for every LOAD run.
  • kates report regression and the regression part of the MCP assess_run tool fail the same way, until someone sets a new baseline by hand.
  • Book 02's retention table said so ("the run it names is deleted at 90 days like any other"). That documented the loss but did not make it right: retention deleted the one run people had pinned to keep.

The failure is loud, not silent: the 404 names the missing run. The run's data is gone, though. The chart's cleanup CronJob is off by default (cleanup.enabled: false), so on a default install the in-app 90-day sweep is the path that deletes it.

Both queries have to change together. If only the id query left the baseline's run out, the count would still include it. remaining would then never reach 0, kates test prune would stop after a call that deleted nothing and exit 1 with a run "left", and the daily sweep would log one still matching every day.

Changes

TestRunRepository

  • countByStatusCreatedBefore and findIdsByStatusCreatedBefore both add AND r.id NOT IN (SELECT b.runId FROM BaselineEntity b). BaselineEntity.runId is NOT NULL, so NOT IN is safe.
  • Each method's javadoc says a run a baseline names is left out, and why.
  • The file grows by 6 lines, all above findWithResults.

TestResource

  • The DELETE /api/tests OpenAPI description ends with "A run a test type's baseline names is never pruned." The description keeps its 7 lines, so no ref into TestResource.java moves.

kates mcp

  • The anchored findWithResults ref in mcp_caveats.go:171 moves from TestRunRepository.java:354-374 to 360-380, by the 6 lines added above it. Its anchors are unchanged.

Docs

  • Book 02's retention table: the run row adds "except a run a baseline names". The baseline row now says retention and kates test prune keep the run it names. kates test delete can still delete it, and the regression check then answers 404 until a new baseline is set.
  • Book 10: test prune and its daily-sweep paragraph say the run a test type's baseline names is kept, however old it is.
  • Book 11: DELETE /api/tests says that run never matches. The daily-sweep paragraph leaves it alone too.
  • Book 12: the sweep paragraph ends with the baseline's run among what a sweep leaves alone, so the regression check has it to compare with.
  • kates/docs/api-reference.md: the same two sentences as book 11.

The docs state the behaviour plainly, with no version in them. The CLI help for kates test prune is unchanged.

Older Kates APIs. A Kates API up to 1.25.1 still deletes baseline runs. The CLI talks to both, and the behaviour belongs to the API.

Tests

Added:

  • TestResourcePruneTest.aBaselineRunIsNeverPruned, on H2 through DELETE /api/tests, so it runs both queries against a real database.
    • Setup. Two DONE runs, at day 1 and day 2. A LOAD baseline is set on the first.
    • Asserts. A dry run answers matched 1. The prune answers matched 1, deleted 1 and remaining 0. The baseline's run is still stored and the other is gone. The audit log has a row for the other run only.
  • TestResourcePruneTest now injects BaselineService and unsets the LOAD baseline in @AfterEach.
  • TestCleanupSchedulerTest and TestResourceDeleteTest mock the repository, so they cannot see the queries. They are unchanged, and so is TestRunRepositoryTest, which fix(engine): a status poll writes only over the status it read, so a cancelled run stays cancelled #377 edits.

Run locally, all passing:

  • git diff --name-only origin/main...HEAD: cli/cmd/mcp_caveats.go, docs/book/{02,10,11,12}, kates/docs/api-reference.md, TestResource.java, TestRunRepository.java and TestResourcePruneTest.java. That triggers the Java, CLI and Book/docs sets. Nothing changed under charts/, .github/ or scripts/, nor in the Makefile or any YAML file.
  • cd kates && ./mvnw -o test -Dquarkus.grpc.codegen.skip=true -Dquarkus.http.test-port=0: 1,467 tests, 0 failures, 1 error.
    • The error was AdvisorResourceTest.analyzeReturns404ForMissingRun, with Mockito WrongTypeOfReturnValue.
    • It is a startup race. TestCleanupScheduler (@Scheduled every=24h) fires when the app starts. It calls the mocked TestRunRepository.countByStatusCreatedBefore while this first test class is stubbing findById.
    • This PR does not touch the scheduler or RunRetention, so it is recorded as a flake.
  • The same command with -Dtest='AdvisorResourceTest,TestResourcePruneTest,RunRetention*Test,TestCleanupScheduler*Test' -Dsurefire.failIfNoSpecifiedTests=false: 32 tests, 0 failures. AdvisorResourceTest 2/2, TestResourcePruneTest 24/24 with the new test, TestCleanupSchedulerTest 6/6.
  • cd kates && JAVA_HOME=$HOME/.sdkman/candidates/java/25.0.2-graalce ./mvnw -o spotless:check -Dquarkus.grpc.codegen.skip=true: BUILD SUCCESS.
  • cd cli && go test ./...: every package ok. TestMCPCaveat* again with -count=1 also passes, so the re-pointed TestRunRepository.java:360-380 ref resolves.
  • cd cli && go vet ./...: clean.
  • cd cli && golangci-lint run --new-from-rev=origin/main ./...: 0 issues.
  • cd cli && govulncheck ./...: no vulnerabilities.
  • scripts/check-cli-style.sh and scripts/check-cli-compat.sh: OK.
  • scripts/check-book-style.sh: OK. The structure ratchet passes, with 38 pages within the baseline.
  • scripts/gen-version-matrix.sh --check and scripts/gen-chart-table.sh --check: in sync.
  • python3 scripts/check-book-contrast.py: all 170 colour pairs meet WCAG AA.
  • scripts/check-mermaid.sh --quiet: 156 diagrams, 0 invalid.
  • scripts/prepare-book-render.sh && quarto render docs/book --to html, on a scratch copy of docs/ and scripts/: 39 chapters prepared, all 38 pages rendered.
  • python3 scripts/check-book-fragments.py on that render: all 7,458 fragment links in 38 pages land on an id.

Not run, because nothing they check changed: helm lint, helm template, check-versions.sh and yamllint (no charts/** or YAML changes); actionlint, shellcheck and make check-help (no .github/**, scripts/** or Makefile changes).

Run only in CI:

  • ci.yml Java job, ./mvnw verify: the *IT failsafe tests, which need Docker.
  • native.yml: the native image build and smoke test, triggered by kates/**.
  • integration.yml: the Kind tests.
  • ci-docker.yml: the Docker image builds.
  • book.yml: the PDF render.
  • CodeQL, and the github-advanced-security AI scan, which fails on every PR on GitHub's side.

Decisions

  • The docs state the behaviour plainly. They do not say "a Kates API newer than 1.25.1". This PR body says it instead: APIs up to 1.25.1 still delete baseline runs.
  • The kates test prune help is unchanged (cli/cmd/prune.go). The behaviour is the API's, and the book covers it.
  • DELETE /api/tests/{id} on a baseline's run still deletes it. Whether that delete should refuse, or unset the baseline, is a follow-up.
  • Baselines already lost stay lost. A baseline whose run the sweep already deleted is not restored by this PR. Set it again with kates test baseline set <run-id>. This belongs in the next release notes.

Deliberately not done

  • DELETE /api/tests/{id} can still leave a baseline dangling. After it deletes a baseline's run, the regression check answers 404 until a new baseline is set, as book 02 now says. Left as a follow-up, by decision.
  • No foreign key on baseline_runs.run_id. The queries leave the run out, so retention needs none, and the table has at most one row per test type. No index is needed either.
  • RunRetention.java and TestCleanupScheduler.java are not touched. Refs cite their lines (RunRetention.java:82, TestCleanupScheduler.java:97), so their javadoc stays as it is.
  • charts/kates/README.md is not touched. It defers to the API, and an edit there would need a chart version bump.
  • Two wording choices differ from the plan:
    • Book 02 writes the 404 in code and splits the baseline cell into two sentences.
    • Book 12 puts the baseline sentence at the end of the sweep paragraph, not into its first sentence. In the first sentence, the "it" of the sentences after it (the failed PENDING or RUNNING run) would have read wrongly.

Landing

Lands after #373 (plan PR 4), #375 (plan PR 6), #376 (plan PR 7) and #377 (plan PR 8), because each edits files this PR also edits:

Recipe once those have merged:

  1. Rebase onto main.
  2. Build a diff line map of TestRunRepository.java from this branch's base to the merged file.
  3. Re-point the moved caveat refs from that map.
  4. Run cd cli && go test ./cmd -run TestMCPCaveat.

Main has also gained #370 since this branch's base. It touches only Makefile and docs/kates-chaos-chart.md, so it does not overlap this PR.

…ion checks keep working

baseline_runs names a run by id, and neither retention query left that run
out. Set a LOAD baseline on a run, and 90 days later the daily sweep, or an
earlier kates test prune, deleted it like any other finished run. From then
on GET /api/tests/{id}/report/regression answered 404 "Baseline run not
found" for every LOAD run, so kates test baseline checks and the MCP
assess_run regression part failed until someone set a new baseline by hand.

Both queries behind RunRetention, the count and the ids, now leave out a run
any baseline names, so DELETE /api/tests and the sweep keep it however old
it is, and matched still equals what a call can delete: remaining reaches 0
and callers stop. DELETE /api/tests/{id} can still delete a baseline's run.
TestResourcePruneTest covers both queries on H2 through the endpoint. The
OpenAPI description, kates/docs/api-reference.md and the book's retention
passages name the exception, and the anchored findWithResults caveat ref
moves down by the six lines added above it.
@bmscomp
bmscomp merged commit aa0cbc7 into main Oct 17, 2026
59 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant