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. |
| // 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>
|
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 |
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.Closes kosli-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