Skip to content

Stop CI deciding whether the TUI golden capture has colour - #5

Merged
packetloss404 merged 1 commit into
mainfrom
fix/tui-golden-check
Sep 5, 2026
Merged

Stop CI deciding whether the TUI golden capture has colour#5
packetloss404 merged 1 commit into
mainfrom
fix/tui-golden-check

Conversation

@packetloss404

Copy link
Copy Markdown
Owner

The TUI golden and protocol safety job has never passed. Not "started failing" — every run on record, back to 2026-08-30, fails the same way.

Each committed golden ends with a -- cell styles -- block and the fresh capture has none. The rendered text matches exactly, character for character; only the styling is missing, on all 17 snapshots.

Cause

One line in termenv (v0.16.0, termenv.go:28):

func (o *Output) isTTY() bool {
	if o.assumeTTY || o.unsafe {
		return true
	}
	if len(o.environ.Getenv("CI")) > 0 {
		return false
	}
	if f, ok := o.Writer().(*os.File); ok {
		return isatty.IsTerminal(f.Fd())
	}
	return false
}

isTTY() answers "no" whenever CI is set, before it looks at the file descriptor at all. GitHub Actions sets CI=true, and scripts/tui_capture.py passed the host environment through to the captured process. So:

CI=trueisTTY() false → ColorProfile() returns Ascii → lipgloss emits no SGR → pyte sees only default-styled cells → render_snapshot omits the entire style section → all 17 goldens mismatch.

Locally, where CI is unset, the identical capture resolves TrueColor and produces the spans. That is how goldens containing styles came to be committed against a check that could not reproduce them anywhere but a developer's machine.

Fix

The harness now builds the child environment itself and drops CI alongside the NO_COLOR it already removed. It owns a real PTY and has already set the window size on it, so "this is a terminal" is the truthful answer; letting the host argue otherwise is the one thing a golden harness must not permit.

CLICOLOR_FORCE=1 is not a substitute, and this is worth being explicit about since it looks like the obvious lever. EnvColorProfile() only consults it as a floor:

p := o.ColorProfile()          // Ascii, because isTTY() is false
if o.cliColorForced() && p == Ascii {
    return ANSI                // 16 colours, not TrueColor
}

It would lift Ascii to 16-colour ANSI, and the goldens record TrueColor (fg=00d9ff). The captures would still mismatch, just differently.

The goldens themselves are unchanged. This PR changes only the harness.

Stopping it recurring

Two changes beyond the one-line cause, both aimed at the same weakness — this failure was invisible from inside the harness.

child_env() is now a pure function holding the environment decision, so it is unit-tested directly. Previously that logic was reachable only through capture(), which needs a PTY and which refuses to run on Windows at all:

if os.name == "nt":
    raise RuntimeError("the PTY capture harness requires POSIX ...")

So on a Windows machine there was no way to exercise it, and the three new ChildEnvTests now run everywhere.

assert_styled() rejects a capture containing no styled cells. A render where every cell is default-styled is not a snapshot of this TUI, it is a snapshot of colour being switched off. That previously failed silently in both directions: check reported all 17 goldens as changed with no indication why, and update would have cheerfully overwritten the reviewed goldens with colourless ones — turning a broken environment into the new baseline.

Verification

check result
python -m unittest scripts/tui_capture_test.py 20 tests, OK (1 skipped: pyte not installed locally, runs in CI)
negative control reverting the CI removal fails test_ci_is_removed_so_termenv_sees_a_terminal

Five tests are new: three on child_env and two on assert_styled.

The end-to-end result can only be confirmed by CI, because the capture harness will not run on Windows. If the golden check passes on this PR, that is the first time it has ever passed. If any snapshot still differs after this, the remaining diff is a genuine rendering difference between the machine that generated the goldens and Linux CI, and the goldens need regenerating — but the styles-versus-no-styles difference is fully explained by the above.

Not addressed here

vulncheck, lint, and the platform test failures are separate and untouched. lint is fixed in a separate PR.

🤖 Generated with Claude Code

The TUI golden job has never passed. Not "started failing" -- every run on
record, back to 2026-08-30, fails the same way: each committed golden
carries a "-- cell styles --" block and the fresh capture carries none. The
rendered text matches exactly; only the styling is missing.

The cause is one line in termenv. Output.isTTY() returns false whenever CI
is set in the environment, before it looks at the file descriptor at all:

    if len(o.environ.Getenv("CI")) > 0 {
        return false
    }

GitHub Actions sets CI=true. The capture harness passed the host
environment through to the child, so lipgloss resolved the Ascii profile,
the binary emitted no SGR, pyte found no styled cells, and render_snapshot
omitted the whole section. Locally, where CI is unset, the same capture
produces TrueColor spans -- which is how goldens containing styles came to
be committed against a check that could never reproduce them.

So the harness now builds the child environment itself and drops CI along
with NO_COLOR. It owns a real PTY and has already set the size on it, so
"this is a terminal" is the truthful answer; letting the host argue
otherwise is the one thing a golden harness must not permit. CLICOLOR_FORCE
would not have fixed it: it only lifts Ascii to 16-colour ANSI, while the
goldens record TrueColor.

Two things stop this recurring. The environment decision moves into
child_env(), a pure function that is unit-tested rather than reachable only
through a PTY the harness refuses to open on Windows. And assert_styled()
now fails a capture that contains no styled cells at all, because that is
not a snapshot of this TUI, it is a snapshot of colour being off -- which
previously failed silently in both directions, reporting every golden as
changed under `check` and overwriting reviewed goldens under `update`.

The goldens themselves are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-05T16:53:30.071094Z ebd52f2 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@packetloss404
packetloss404 merged commit 2b682ad into main Sep 5, 2026
12 of 16 checks passed
@packetloss404
packetloss404 deleted the fix/tui-golden-check branch September 5, 2026 17:30
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