Skip to content

Sprint/hardening - #1

Merged
casablanque-code merged 5 commits into
mainfrom
sprint/hardening
Sep 20, 2026
Merged

casablanque-code merged 5 commits into
mainfrom
sprint/hardening

Conversation

@casablanque-code

Copy link
Copy Markdown
Owner

No description provided.

internal/tui has had zero test files for the whole sprint — every
round of validation-dialog scroll/cursor changes (there were several,
including one outright regression of my own in the middle of them)
was only ever checked by an isolated-build type-check plus manual
reasoning, never by a real regression test. This is the gap flagged
when asked directly what's still weak about the tool.

16 tests across two areas:

  - The scroll/scrollbar math (validationScrollWindow,
    renderValidationScrollbar, validationDialogContentWidth): clamping
    at both ends, never exceeding the report's actual length, never
    dropping below a usable minimum budget, and — directly covering
    the sprint's dialogChrome bug — that accounting for the optional
    banner/legend rows actually produces a smaller budget than not
    accounting for them, for the same terminal height and report.
    Also covers the scrollbar's full-bar-when-everything-fits behavior
    and that the thumb actually reaches both ends at the two extremes
    of scroll, plus a zero-visible-rows edge case that would otherwise
    risk a panic.

  - updateValidation's cursor behavior: Down/Up move the selection one
    warning at a time and stop at the ends; Down at the last warning
    falls through to plain scrolling instead of getting stuck (this
    one initially caught a real bug in the test itself, not the code —
    written against too generous a terminal height, the whole tiny
    report already fit on screen and there was nothing below the last
    warning to scroll to, so scroll legitimately staying 0 looked like
    a failure until the window was shrunk enough to actually have room
    below); Enter on a fixable warning opens the secret-strategy picker
    with the right service/key; Enter on a non-fixable warning is a
    true no-op (doesn't close the dialog); Esc closes it.

These construct Model/validationDialog values directly and call the
Update-layer methods without any bubbletea runtime — no terminal, no
program loop, just Go function calls, which is all this needed.

Verified in the same isolated-module setup used for every internal/tui
change this sprint (Go 1.22, replace directives for golang.org/x/*,
gopkg.in/yaml.v3, gopkg.in/check.v1, older API-compatible bubbletea/
bubbles/lipgloss): go build/vet clean, all 16 new tests passing, full
module test suite (pkg/composer included) still green. Please still
run the real 'go build ./... && go vet ./... && go test ./...' against
your actual go.mod before relying on this.
…t reminding

The whole point of the secrets picker is moving a hardcoded value out
of the compose file; leaving the place it landed unignored defeats
that if the person forgets the follow-up step. Previously the only
safeguard was a text reminder in the success banner ("make sure .env
is in your .gitignore") — easy to miss, and the exact kind of thing
that ends with a secret committed to history anyway.

convertSecretCmd now calls a new ensureGitignoreEntry(path, entry)
right after each disk write: ".env" for the .env strategy, "secrets/"
for the Compose-secret strategy (the whole directory, not the one file
just written, since more secrets can land there later). Creates
.gitignore if it doesn't exist; appends without duplicating if the
entry (with or without a trailing slash — "secrets" and "secrets/"
count as the same entry) is already there on its own line. A failure
to update .gitignore doesn't undo or fail the secret conversion
itself, which already succeeded — the banner just says so instead
("Couldn't update .gitignore automatically — make sure ... yourself"),
same as before for that one case.

7 new tests, using t.TempDir() rather than touching a real .gitignore:
ensureGitignoreEntry (create-if-missing, append-without-clobbering,
no duplicate on an exact match, no duplicate across the trailing-slash
variants) plus two more for appendEnvFileLine and writeSecretFile,
which had disk-IO logic of their own with no coverage either.

Verified the same way as the test-only patch just before this one on
this branch: isolated-module build/vet clean, all new tests passing,
full module suite still green. Please still run the real
'go build ./... && go vet ./... && go test ./...' against your actual
go.mod before relying on this.

README not touched here per your note that you'll handle it yourself
later.
…ecrets picker

The healthcheck warning added earlier already spells out a suggested
healthcheck in its message text, but doing anything about it meant
leaving the dialog and typing it in by hand — the secrets picker got a
one-keystroke fix a while back and this didn't, which was flagged
directly as an inconsistency.

pkg/composer additions:
  - HealthCheckSuggestion{Service, Check} — the structured counterpart
    to the "no healthcheck configured" warning text, same role
    HardcodedSecret plays for secret warnings.
  - ComposeConfig.SuggestedHealthCheck(serviceName) — exported,
    service-name-keyed wrapper around the existing (unexported)
    suggestedHealthCheck(image), so the TUI doesn't need to re-parse
    the warning message to figure out what to offer.
  - ComposeConfig.ApplyHealthCheck(serviceName, hc) — sets it, copying
    hc.Test so a caller mutating their copy afterward can't reach back
    and change what was actually applied.

TUI wiring: validationDialog gets a healthCheckRefs slice parallel to
warnings (same shape as secretRefs), matched in showValidation by a
direct Service+Field=="healthcheck" lookup — simpler than the
quoted-key matching secrets needed, since at most one such warning
fires per service, no ordering ambiguity to resolve. Enter on one
applies the suggestion immediately rather than opening a sub-dialog
the way secrets does — there's only one way to fix this, not three, so
a picker would just be an extra step. Unlike the secrets conversion
this has no disk IO of its own (a healthcheck is just part of the
in-memory config, written out on the next save), so no async tea.Cmd
needed — a synchronous mutation inside updateValidation's Enter case
is enough. Both fixable checks (secretRefs / healthCheckRefs) are now
bounds-checked in the render loop and Enter handler rather than bare
0-indexed lookups.

That bounds-check exists because writing the tests actually panicked
without it: dialogs_test.go's earlier updateValidation tests
constructed a validationDialog with only warnings/secretRefs set,
predating this patch, and buildValidationBodyLines' new
`m.validationDialog.healthCheckRefs[i]` lookup ran off the end of a
nil/shorter slice. Fixed at the source (bounds-check both slices
wherever they're indexed by warning position) rather than patching up
every existing test's fixture, since a real caller constructing one by
hand shouldn't be able to trigger the same panic either.

11 new tests: pkg/composer covers SuggestedHealthCheck (by name, known
image, unrecognized image, unknown service) and ApplyHealthCheck
(applies correctly, error on unknown service, copies rather than
aliases the Test slice — plus that the warning is actually gone from
Validate() after applying); internal/tui covers the Enter-applies-
immediately behavior end to end against a real ComposeConfig.

Verified in the same isolated-module setup as the rest of this branch:
go build/vet clean across the whole module, all new tests passing, the
full existing suite (90+ tests across both packages) still green after
the bounds-check fix. Please still run the real
'go build ./... && go vet ./... && go test ./...' against your actual
go.mod before relying on this.

README not touched here per your note that you'll handle it yourself
later.
pkg/composer/schema/compose-spec.json is vendored rather than fetched
at runtime (see spec.go's doc comment for why), which means it goes
stale silently the moment compose-spec/compose-spec publishes a schema
change — nothing in the existing CI or test suite would ever notice.
Flagged directly as a known gap; this closes it.

.github/workflows/compose-spec-schema-check.yml runs weekly (Mondays
06:00 UTC) plus on-demand via workflow_dispatch: downloads the current
schema/compose-spec.json from compose-spec/compose-spec's master
branch, diffs it against the vendored copy, and if they differ, opens
a tracking issue (or comments on the existing one if it's still open,
rather than opening a duplicate every week) with concrete update
instructions. If a later run finds the vendored copy current again, it
comments and closes that issue automatically — nothing to remember to
close by hand either direction. This workflow never touches the
vendored file itself or fails the build; it only ever reports.

No --label on the created issue: a label like "dependencies" only
exists if the repo happens to have already created it, and `gh issue
create` hard-fails on a label that doesn't exist rather than skipping
it — not worth a scheduled workflow breaking over label bookkeeping it
doesn't control.

Verified everything checkable without actually running it in GitHub
Actions (no way to trigger a real workflow run from here): the YAML
parses (python3's yaml.safe_load), each of the three `run:` blocks
extracted and checked both for bash syntax (bash -n) and with
shellcheck (0.9.0) — clean on both. More usefully, ran the actual
diff logic for real: fetched the live schema from
raw.githubusercontent.com/compose-spec/compose-spec/master and diffed
it against the vendored copy exactly the way the workflow does. It
found a real, substantial difference (76668 vs 89808 bytes, well
beyond whitespace) — the vendored copy is already stale, which is
either a great sign this workflow is worth having or a mildly
embarrassing one, possibly both. Also confirmed the not-stale path:
diffing the fetched copy against itself matches, as it should.

Not included here: updating the vendored schema itself to fix the
staleness this just found — that's a separate, more involved change
(needs the full pkg/composer/... test suite run against whatever the
new schema actually changes) and out of scope for "add the CI job",
which is what was actually asked for.

README not touched here per your note that you'll handle it yourself
later.
TestWriteSecretFile_CreatesParentDirAndWritesValue asserted
info.Mode().Perm() == 0600 after writing a secret file — true on
POSIX systems, but Windows/NTFS doesn't have POSIX permission bits at
all: os.Stat there reports a fixed, coarse mode (essentially
read-write or read-only) regardless of what was passed to WriteFile,
never a specific value like 0600. Confirmed as the failure from the
windows-latest leg of CI's test matrix (.github/workflows/ci.yml runs
ubuntu-latest/macos-latest/windows-latest).

The assertion itself was reasonable to write (0600 vs the compose
file's usual 0644 is real, meaningful behavior worth covering) — it
just needed to only run on platforms where the concept applies, same
as any other Go test asserting POSIX permission bits. Guarded with the
standard runtime.GOOS != "windows" check rather than dropping the
assertion or skipping the whole test, so it still verifies the thing
it's meant to verify everywhere that's actually meaningful.

Verified with 'go test ./... -race -v' (matching the exact command
that surfaced this) across the whole module in the same isolated-build
setup used throughout this branch — Go 1.22, replace directives
routing golang.org/x/*, gopkg.in/yaml.v3, and gopkg.in/check.v1 through
their GitHub mirrors, since this sandbox has no route to
proxy.golang.org or gopkg.in. All 100 tests across both packages pass,
-race included, no data races. I obviously can't run the real Windows
leg of CI myself to double-confirm this was the only failure — please
push this and check that windows-latest goes green; if something else
is still failing there, paste the actual error and I'll keep going.
@casablanque-code
casablanque-code merged commit dad4271 into main Sep 20, 2026
4 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