Skip to content

Tell the message of a failure in the report files as they are written - #487

Merged
linyows merged 3 commits into
refactor/structurefrom
fix/report-message-masking
Oct 25, 2026
Merged

linyows merged 3 commits into
refactor/structurefrom
fix/report-message-masking

Conversation

@linyows

@linyows linyows commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

#484 masks what the false condition of a test compared as the terminal report shows it, so a secret a later step teaches the masker is hidden whole there. The report files still had the message of the failure as it was made when the test was checked: the condition on one line, its values cut at 200 characters and masked with what the masker knew then. A secret learned after that was left in report.json, the JUnit message attribute and the Markdown report as the part of it the cut kept, which Report.Mask cannot find.

StepFailure now keeps what Message has before the condition (head), and reportMessage is that and the condition told anew from the clause it already keeps, masked with the masker of the run before the values are cut, as failureText does. writeReports builds the report through an unexported buildReport that takes the masker; the exported BuildReport passes none and keeps the message as it was, so its signature does not change. A repeated step adds 2 of 3 iterations failed; the first: … to both the message and the head through prefixed, and the test of a repeated job goes through the same path.

With nothing learned since the check, the message is byte for byte the one the failure was made with. TestBuildReport_KeepsTheMessageOfAFailure pins it for a false test with its operands shown, hidden and with a known secret cut short, one that cannot be evaluated, one that is not a boolean, an action's error and a broken contract, each alone, as the test of a repeated job and as a repeated step. TestWorkflow_Start_MasksWhatALaterStepTeachesInReportFiles writes the JSON, JUnit and Markdown reports of the leak in #484 and fails on the base branch with tk-012345678 in all three.

Other fields of the report files that hold cut text, left as they are:

  • echo is cut by the template engine at 1,000,000 bytes, with the marker after it; a secret that straddles that point would leave its start.
  • An action's error message, and a contract's violations, are the action's own text; if an action cuts a value into them, the masking of the whole report cannot find a secret cut there. Probe cannot tell them anew.

The request and the response under a failure are kept as data and masked whole as the files are written, so they are not cut.

🤖 Generated with Claude Code

https://claude.ai/code/session_01X6CYBxNCxQyo1rbdg9hQ42

The message of a false test tells what its condition compared, cut short,
and was masked as the test was checked. A secret a later step taught the
masker was left in the report files as the part of it the cut kept, which
the masking of the whole report no longer finds, as the terminal report
did before it told the condition as it is shown. The failure now keeps the
condition as the message ends with it, and the report files tell it anew
from the condition as it is, masked with the masker of the run before its
values are cut. With nothing learned since, the message is the one the
failure was made with, which a test pins for each kind of failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X6CYBxNCxQyo1rbdg9hQ42

@linyows linyows left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found. What I verified against b091a53:

  • Suffix replacement: Message only ever ends in summary. evaluateTest appends it last. The only later change is the "%d of %d iterations failed; the first: %s" prefix in repeatStepResult (executor.go:305), and it copies summary with the struct. Because of TrimSuffix plus the HasSuffix guard, a second occurrence of the summary earlier in the message doesn't matter, and an empty summary returns early. In practice the summary is never empty, because clauseSummary returns at least c.Expr.
  • Failure kinds without a summary: test error (an expr-lang file.Error, with no truncation in v1.17.8), type mismatch (%v of the whole value, not cut), action errors, and contract messages/violations (passed through from the action, not cut by probe). None of these messages is cut, so rep.Mask still hides a late-learned secret in them. I checked type mismatch with a late-learned token: it comes out as xxx…<redacted> (string), not a boolean. Echo isn't cut either, except by the 1 MB template cap. The report package doesn't cut Message: the GitHub summary cuts only at block boundaries.
  • Masker state: there is one workflow printer and one masker (startReport), and every job's JobContext.Printer is that same printer. Learn only adds. So the masker at write time always knows at least what it knew at check time. Repeat and baseline tests use ctx.Printer, the same printer. RunStandalone/embedded jobs write no report files. BuildReport still passes nil, so its output is unchanged.
  • Late-learned secret, checked in a scratch worktree: in a repeated step's N of M … the first: message and in a repeated job's test, the token is masked whole with no prefix left. For an || condition (no operands shown), the message is unchanged.
  • Concurrency: for async repeats, summary is set on the failure before it is published to counter.Failure under jCtx.repeat.mu. go test -race -run 'Repeat|Report' passes.

The one inline comment is a nit.

Comment thread result.go Outdated
// the part of it the cut kept, which no masker finds. Without a condition,
// or with no masker, it is Message.
func (f *StepFailure) reportMessage(m *mask.Masker) string {
if m == nil || f.clause == nil || f.summary == "" || !strings.HasSuffix(f.Message, f.summary) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: when HasSuffix fails, this quietly returns the message that was cut at check time. That brings back the leak this PR fixes, and nothing tells you it happened. No current code path reaches it, because Message only ever gets a prefix after the summary is appended. But if a future change appends anything (a hint, a retry count, etc.), report files would quietly lose the masking again. Two options: keep the head instead (f.Message minus the summary, e.g. store prefix and make Message derived), or add a test that pins the suffix invariant for every place that rewrites Message.

linyows and others added 2 commits October 25, 2026 23:33
The report files told the message anew only when Message still ended with
the condition as the test was checked, and fell back to Message otherwise,
which a later change that added to Message would have made it do with no
test failing, and the leak back. The failure now keeps what precedes the
condition, and the message of the report files is that and the condition
told anew, always. A repeated step adds the count of its runs to both
through prefixed, which a test of a secret learned after the check now
covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X6CYBxNCxQyo1rbdg9hQ42
…ort-message-masking

* origin/refactor/structure:
  Read an external action's parameters with actionrpc
  Send a header of nil as empty, and refuse one that is a list or a map
  Cut a long template output with truncate.String
  Back off a truncation no further than a rune can be long
  Count the methods of a .proto file in internal/protocoverage
  Write a number in a map of strings as its text, and drop HeaderToStringValue
  Cut a truncated string at the start of a rune
  Decode JSON with Parse, and keep both in decode.go
  Write a scalar as text in internal/scalar, for jsonutil and mapping
  Name the package that reads a response body internal/body
  Move the action protocol's generated code to internal/pb
  Move the packages only probe itself imports under internal/
@linyows
linyows merged commit 13c3db8 into refactor/structure Oct 25, 2026
7 checks passed
@linyows
linyows deleted the fix/report-message-masking branch October 25, 2026 14:46
linyows added a commit that referenced this pull request Oct 25, 2026
#487 added the mask import beside expr in result.go and report_test.go;
both keep it, with expr at its new path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X6CYBxNCxQyo1rbdg9hQ42
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