Uh oh!
There was an error while loading. Please reload this page.
test: pin fingerprint capture cleanliness contract - #846
Conversation
Adds three tests that defend the customer-facing contract for `kosli fingerprint`: stdout is exactly the fingerprint, stderr is empty on the success path, and stderr remains a functional channel for opt-in debug output. The version-notice-on-stderr bug has been re-introduced three times in two weeks (PR #781 → PR #799 → PR #840) because no test pinned the actual contract customers depend on — that the output of `kosli fingerprint` is shell-capturable. Each round added a test narrowly aimed at the symptom the author was thinking about; none asserted the contract. These tests assert the contract directly via: TestFingerprintFile_CaptureCleanliness stdout == "<sha256>\n", stderr == "", combined == stdout — matches the customer pattern FP=$(kosli fingerprint ... 2>&1). TestFingerprintDir_CaptureCleanliness Same contract for --artifact-type=dir, the slow path that triggered the cyber-dojo failure (the goroutine had time to complete and pollute stderr). TestFingerprintFile_DebugModeIsAllowedToWriteStderr Pins the inverse: --debug=true MUST produce stderr output containing "calculated fingerprint", catching anyone who over-corrects a CaptureCleanliness regression by silencing the logger inside the fingerprint code path. Closes #5564 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Claude finished @AlexKantor87's task in 2m 47s —— View job PR Review: Fingerprint Capture Cleanliness Tests
Static AnalysisAll three checks pass cleanly — Review SummaryThis is a well-designed PR. The contract tests are cause-agnostic (asserting What works well
Nits (non-blocking)Two minor items posted as inline comments:
No issues found with
Verdict: Looks good to merge. The two inline nits are optional polish — neither affects correctness. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| // original cyber-dojo bug fired here because the dir/oci paths run long | ||
| // enough for the background version-check goroutine to complete and write | ||
| // to stderr before the command exits. Same contract as the file variant. | ||
| func (suite *FingerprintCaptureTestSuite) TestFingerprintDir_CaptureCleanliness() { |
There was a problem hiding this comment.
Do we expect this to be different than file test? 🤔
There was a problem hiding this comment.
Good catch — the dir fingerprint of testdata/folder1 is already pinned in fingerprint_test.go:35 as c43808cb..., so no reason to use Regexp here. Just pushed 4faf6f1 aligning the dir test with the file test — same shape, same three contracts (exact stdout, empty stderr, exact combined). Thanks for the prompt.
There was a problem hiding this comment.
These tests are good. The one checking for no extra output even when --debug=true in particular.
I think it is worth also testing that you also get no extra output when the --artifact-type is docker or oci. The code paths for those flag values could try to print debug info. It may be trickier for these because generating a fingerprint is a path for all the kosli-attest commands as well as for kosli-fingerprint.
Address review feedback on PR #846: - @mbevc1 (and the claude bot): align TestFingerprintDir_CaptureCleanliness with the file variant. The dir fingerprint of testdata/folder1 is already pinned in fingerprint_test.go, so use Equal with that exact value plus the combined-stream assertion. Both tests now have the same shape and the same three contracts. - @JonJagger: add TestFingerprintDocker_CaptureCleanliness covering --artifact-type=docker, which goes through internal/docker.GetImageFingerprint and is a separate code path from file/dir hashing. Mirrors the existing docker test pattern (alpine pinned by digest, pulled in SetupSuite). OCI variant and broader attest-command coverage tracked as follow-ups in #848 and #849 — both are real engineering work that warrants separate PRs (OCI needs registry scaffolding; attest needs auth/server + a contract surface audit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AlexKantor87
commented
May 1, 2026
Thanks @JonJagger — both fair points. Just pushed 4faf6f1 which adds For the other two pieces I've raised follow-ups rather than expand this PR, since both involve real engineering work that warrants a separate review pass:
Capture cleanliness contract now covers |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
Adds three tests that pin the customer-facing contract for
kosli fingerprint: stdout is exactly the fingerprint, stderr is empty on the success path, and stderr remains a functional channel for opt-in--debugoutput.Closeskosli-dev/server#5564. Follow-up to #840, which fixed the most recent regression but did not pin the underlying contract.
Why
The version-notice-on-stderr bug has shipped three times in two weeks:
3097f68d76c084--output=json; missedfingerprint(no--outputflag)eb93aaaEach round added a test for the case the author was thinking about. None pinned the customer contract:
@tooky asked in the issue: "With my customer's hat on I would really appreciate it if, before this bug is fixed, a failing test is added so that it cannot reappear." This PR addresses that — and not just for the version-check bug. The contract is cause-agnostic.
What the tests assert
TestFingerprintFile_CaptureCleanlinessStubs the update check to return a notice (so the test fires deterministically), runs
fingerprint --artifact-type=file testdata/file1, asserts:stdout == "<sha256>\n"exactly — anything else breaks$(...)capturestderr == ""exactly — anything else breaks2>&1captureThe exact-empty assertion on stderr is the key generalisation. The previous test used
NotContains "A new version", which only catches the version-notice string. Equal-empty catches anything — a deprecation warning, a telemetry log, a future framework upgrade that adds startup output.TestFingerprintDir_CaptureCleanlinessSame contract for
--artifact-type=dir. The directory and OCI paths are slow enough for a background goroutine to complete and pollute stderr — this is the path that triggered the cyber-dojo failure.TestFingerprintFile_DebugModeIsAllowedToWriteStderrPins the inverse: with
--debug=true, stderr MUST contain"calculated fingerprint". Stops a future contributor from "fixing" a CaptureCleanliness regression by silencing the logger inside the fingerprint code path. Asserting on the fingerprint-specific debug line (not justNotEmpty(stderr)) ensures earlier framework debug logs fromPreRunEdon't mask a real silencing regression.How the test fails on the bug (proven locally)
Reverting
eb93aaa4and re-running:The "actual" line is the same shape of output cyber-dojo's CI captured.
How the test catches future regressions (also proven locally)
Adding a stray
logger.Warn(...)tofingerprint.go'srun()— a totally different cause from the version check — fails with:A call-site-restriction test on
version.CheckForUpdatewould not have caught this. The contract test does.What this test does not block
The test runs with no
--debugflag, no deprecated flags, no config file, no auth. So it doesn't fire on:logger.Debugoutput (gated on--debug=true)~/.kosli.ymlhas plain text)The line it draws: the happy path of
kosli fingerprintwith default flags must produce no stderr. Anything firing unconditionally on every invocation has to actively justify itself by updating these tests in code review — which is exactly the conversation that wasn't happening for the last three rounds.Test plan
go test ./cmd/kosli/ -run TestFingerprintCaptureTestSuite -v— all three passmake lint vet fmt— green (golangci-lint 2.11.4)root.go, passes after fix: remove version checks on all commands #840's fixlogger.Warnin fingerprint path); confirmed test catches itmake test_integrationnot run locally (requiresKOSLI_API_TOKEN_PROD+ AWS CLI access). The new file is pure in-process Go with no server, network, or auth dependency, so the risk surface is low. Letting CI validate.🤖 Generated with Claude Code