Skip to content

Add integration test harness, push/deploy + TUI tests, document two production bugs#252

Open
l-hellmann wants to merge 34 commits into
mainfrom
cli-tests
Open

Add integration test harness, push/deploy + TUI tests, document two production bugs#252
l-hellmann wants to merge 34 commits into
mainfrom
cli-tests

Conversation

@l-hellmann
Copy link
Copy Markdown
Collaborator

What Type of Change is this?

  • New Feature
  • Fix
  • Improvement
  • Release
  • Other

Description (required)

Builds an in-process integration test harness for zcli commands and adds
unit-level UI tests for the bubbletea models. Includes minimal production-code
seams to enable testing without changing user-facing behavior. Also documents
two confirmed bugs as skipped regression tests (fixes intentionally left for
follow-up PRs to keep this one scoped to test infrastructure).

Production changes (small, backward-compatible)

  • zeropsRestApiClient.NewAuthorizedClient accepts a full URL or a bare host;
    defaults the scheme to https:// when missing.
  • cmdBuilder.RunRootCmd(rootCmd, RunOptions{Ctx, Args, Stdout, Stderr})
    new test-friendly entry point that returns an exit code instead of calling
    os.Exit. ExecuteRootCmd becomes a thin wrapper around it.
  • logger.OutputConfig.Out accepts an io.Writer (defaults to os.Stderr),
    so uxBlock output can be captured.
  • flagParams.New(notice io.Writer) routes "Using config file:" notices
    through the injected stderr.
  • New env var ZEROPS_CLI_YAML_FILE_PATH overrides the per-user .zcli.yml
    location (parity with ZEROPS_CLI_DATA_FILE_PATH and ZEROPS_CLI_LOG_FILE_PATH).
  • Empty ZEROPS_TOKEN env value no longer overrides a token persisted by
    zcli login — previously export ZEROPS_TOKEN= would silently log out.
  • yamlReader.ResetCache() — exported so in-process test re-runs aren't
    served stale bytes from the package-level cache.

New tests

Integration (src/cmd/, //go:build devel, run via make test-integration):

  • Harness with httptest.Server, per-test tempdir, login/scope seeding,
    archive decompression, and git fixture helpers.
  • 14 service-push tests (happy paths, error paths, polling, scope, archive-file-path).
  • 7 git-archive tests (not-init / zero-commits errors, workspace-state
    clean/staged/all, --deploy-git-folder).
  • 1 service-deploy variant.
  • 1 project-list, 1 login, 1 version.

TUI (src/uxBlock/models/, via github.com/charmbracelet/x/exp/teatest):

  • selector (10): single + multi-select, filter-mode, esc, clamping, label.
  • prompt (7): cursor movement, clamping, esc, initial position.
  • input (5): typing, backspace, esc, empty submit, label render.
  • table (11): renderer output, body indexing, deep clone, cell pretty-flag.

Confirmed bugs documented as skipped tests (pushDeploy_bugs_test.go)

  1. --setup flag ignored when the service name happens to match a setup
    name in zerops.yaml (servicePush.go:85, serviceDeploy.go:72).
    Reproduced from a real pipeline running --setup showcase-backend on a
    service named "backend".
  2. SIGSEGV nil-deref on apiProcess.AppVersion.Id / .Build
    (servicePush.go:235, :251) when a process poll returns
    status=RUNNING with appVersion: null. Crashes the binary from the
    spinner goroutine. Confirmed by removing the t.Skip in isolation; stack
    trace is in the test comment.

Both tests are committed as t.Skip'd reproductions so CI stays green;
removing the skip after each fix will lock the regression in.

Test plan

  • make test — unit tests pass.
  • make test-integration — 24 PASS + 2 SKIP, ~16 s.
  • TUI tests pass — 33 across 4 packages, <1 s combined.
  • Branch builds for darwin/arm64, linux/amd64, windows/amd64.
  • make lint reports zero issues in src/ and cmd/ (after running
    tools/install.sh). The lint binary itself reports a pre-existing
    toolchain mismatch in golang.org/x/crypto/.../fips140only_go1.26.go
    when the developer has Go ≥1.26 installed; unrelated to this diff.

Related issues & labels (optional)

l-hellmann added 25 commits May 13, 2026 12:17
…ntext() in TUI tests, checked type assertions
Comment thread src/cmd/doc_test.go Outdated
@@ -0,0 +1,150 @@
//go:build devel
Copy link
Copy Markdown
Contributor

@tikinang tikinang May 18, 2026

Choose a reason for hiding this comment

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

Why devel? I would anticipate something like integration instead.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

// All integration tests build under the `devel` build tag (so the
// production-version-check HTTP call from src/version is stubbed to a no-op)
// and run via `make test-integration`

reference

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Honestly the devel tag will go when the refactor of the self-update lands, then there will be no reason to keep that around.

logPath := filepath.Join(dir, "zcli.log")
t.Setenv(constants.CliDataFilePathEnvVar, dataPath)
t.Setenv(constants.CliLogFilePathEnvVar, logPath)
t.Setenv(constants.CliTokenEnvVar, "")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What is the reason for this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Override for already defined ZEROPS_TOKEN env in shell, so you don't one when running tests

// region named "prg1" by default.
f.HandleJSON("/regions", 200, map[string]any{
"items": []map[string]any{{
"name": "prg1",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, I wonder about the map[string]any approach. What if we used the structures from generated SDK?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I considered it, but I think map[string]any is the right call here — the contract under test is the JSON wire format, not the Go types.

  • Canary on SDK drift. If a zerops-go bump silently renames a field or changes a JSON tag, shared types move both sides together and the test keeps passing against the new contract. Literal JSON fails until I consciously update the fixture — what I want from an integration test.
  • Catches tag bugs on the consumer side. A mis-tagged decoding struct would round-trip through itself and pass with typed fixtures; map[string]any writes keys literally, so assertions catch it.
  • Minimal fixtures. Many response fields aren't read by the CLI (see the GetUserInfo comment). Typed structs force zero values or omitempty, obscuring what the test pins; optional.Null[T] is also awkward to build inline.

The one real win for typed structs would be catching field-type changes at compile time on SDK bump — narrow, and we'd catch it the first test run anyway. If we want stronger SDK-drift guarantees, a separate contract test that decodes a captured real response through zerops-go is the better tool,
orthogonal to these.

Comment thread src/cmd/doc_test.go Outdated
// test. See integration_harness_test.go and pushDeploy_helpers_test.go for
// the shared scaffolding.
//
// # Tests by command
Copy link
Copy Markdown
Contributor

@tikinang tikinang May 18, 2026

Choose a reason for hiding this comment

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

I would much rather see these tests descriptions in the individual test files, it allows this doc file comment drifting appart from what the tests do more easily, which will be confusing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please leave only the description above, if you deem it necessary.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done ea5ddf1

Copy link
Copy Markdown
Contributor

@tikinang tikinang left a comment

Choose a reason for hiding this comment

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

Great improvement, I like the test harness setup. I found just some small things to make it nicer.

Comment thread src/cmdBuilder/executeRootCmd.go Outdated
Comment on lines +34 to +46
type RunOptions struct {
// Ctx is the root context. If nil, a fresh context.Background() is used
// and OS signals are wired to cancel it.
//nolint:containedctx // intentional: callers (tests, ExecuteRootCmd) pass the root ctx by value
Ctx context.Context
// Args overrides os.Args[1:] when non-nil. Useful for tests.
Args []string
// Stdout receives command output. Defaults to os.Stdout when nil.
Stdout io.Writer
// Stderr receives error/log output (including uxBlock messages).
// Defaults to os.Stderr when nil.
Stderr io.Writer
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, private fields with option funcs and sane defaults (as done in the RunRootCmd) would be nice? It may remove the need for //nolint. But this is fine too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

reworked per your comment a9a77d9

Comment on lines +39 to +40
"id": "00000000-0000-0000-0000-0000000000bb",
"clientId": "00000000-0000-0000-0000-0000000000aa",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It probably doesn't matter now, but this wouldn't happen in case of Zerops public API, it uses UUID short (shortened 22-char-base64 version of UUID V4).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

wouldn't matter but switched to 22-char-base64 ids to keep parity ea00ca3

Comment thread src/cmd/pushDeploy_helpers_test.go Outdated
Comment on lines +246 to +247
require.NotEqualf(t, 0, res.ExitCode,
"expected non-zero exit\n--- stderr ---\n%s\n--- stdout ---\n%s", res.Stderr, res.Stdout)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: weird formatting, maybe include in the CLAUDE.md, that it shouldn't produce these weird half line breaks without , on the end... but whatever

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done and noted in CLAUDE.md 6ad3eee

}},
})

f.HandleJSON("/api/rest/public/project/search", 200, map[string]any{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just note: I would love to eventually move from the /search endpoint to the standard read API endpoints that we are rolling out now (just waiting for to publish the new SDK, the read endpoints are already deployed to prod).

That's just for future reference.

@tikinang
Copy link
Copy Markdown
Contributor

And maybe adding the integration tests to the automatic checks? Similar to:

      - name: test
        if: ${{ matrix.runTests }}
        run: env ${{ matrix.osEnv }} go test -v ./cmd/... ./src/...

in .github

Move per-test descriptions from the central doc_test.go file onto each test
function as a godoc-style comment starting with the function name. Lift the
package-wide scaffolding paragraph onto integration_harness_test.go as a file
comment. doc_test.go is removed.
Document the multiline-call style rule in CLAUDE.md and retrofit existing
violations: every multiline function call now puts the open paren on its own
line with each argument on its own line and a trailing comma. Printf-style
format-string + args still ride one line as a logical group.
Match zerops-go ID format (16 raw UUID bytes → URL-safe base64 without
padding = 22 chars). Values are derived deterministically from
UUIDv5(DNS, "zcli-test-fixture-<role>") so the constants stay stable
across runs.
@l-hellmann
Copy link
Copy Markdown
Collaborator Author

l-hellmann commented May 18, 2026

@tikinang

And maybe adding the integration tests to the automatic checks?

Will add in #254

l-hellmann and others added 6 commits May 19, 2026 00:31
Move the root context off the RunOptions struct and onto RunRootCmd as the
first argument, matching Go convention and dropping the containedctx nolint.
ExecuteRootCmd wires SIGINT/SIGTERM via signal.NotifyContext, replacing the
hand-rolled regSignals goroutine.

Convert RunOptions to the functional-options pattern: private runOptions
struct with sane defaults (os.Stdout/os.Stderr) plus public WithArgs,
WithStdout, WithStderr helpers. Variadic RunRootCmd lets ExecuteRootCmd call
without any options.
Threading ctx through RunRootCmd as a real parameter let contextcheck
follow the call graph it couldn't before, and the new signal.NotifyContext
wiring introduced an exitAfterDefer.

- Move stop() before os.Exit so it actually runs (gocritic).
- Thread ctx into version.GetVersionCheckMismatch and printMessageData;
  precompute Latest/LatestUrl in messageData so the template no longer
  swallows the caller's context via context.Background().
- Suppress contextcheck on buildCobraCmd with a note: cobra threads ctx
  to the run-func via ExecuteContext, which is the canonical pattern here.
@l-hellmann l-hellmann requested a review from tikinang May 18, 2026 22:57
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.

2 participants