Skip to content

Add control-plane E2E tests with device-flow login - #2411

Merged
gtrrz-victor merged 16 commits into
mainfrom
feat/e2e-controlplane
Sep 15, 2026
Merged

gtrrz-victor merged 16 commits into
mainfrom
feat/e2e-controlplane

Conversation

@gtrrz-victor

@gtrrz-victor gtrrz-victor commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

https://entire.io/gh/entireio/cli/trails/1324

Summary

Add a production control-plane E2E suite that signs in through the device flow and exercises organization, project, and repository creation, cloning, and deletion without a coding agent.

What Changed

  • Complete GitHub sign-in and authenticator verification in headless Chromium using the dedicated test account.
  • Isolate CLI credentials from developer state and uploaded artifacts, and delete test resources in reverse order.
  • Bound CLI calls to two minutes and cleanup calls to 30 seconds, with a backstop for inherited output pipes. Add a local regression test covering timeout followed by cleanup.
  • Add the mise task and serialized GitHub Actions workflow, using the E2E_GH_* secrets.

The temporary push trigger for feat/e2e-controlplane remains for branch validation and should be removed before merge.

Verification

  • Formatting and full lint passed.
  • Timeout regression test passed using a local fake CLI.
  • Control-plane unit tests, execx tests, and E2E package compilation passed.
  • Full unit/integration tests with race detection were run. With local agent shims excluded from PATH, the only failure was the unrelated TestOpenCodeSeedRepoPlantsDeps: the shared OpenCode cache was missing package.json.
  • Deterministic Vogon E2E canary passed.

Note

Medium Risk
Tests hit production with real credentials and provision/delete live orgs/repos; CI concurrency rules matter because cancelled runs can leak quota-limited resources.

Overview
Adds a production control-plane E2E suite that runs the entire CLI without a coding agent: TestMain performs one entire login --device per run, with headless Chromium (playwright-go) completing GitHub password + TOTP and Entire device authorization using E2E_GH_* secrets.

The main scenario creates org → project → repo (e2e-cp-*), waits until the repo is clonable, clones and checks the entire:// remote, then deletes resources in order with t.Cleanup fallbacks. CLI helpers use bounded timeouts and WaitDelay so cleanup still runs after hangs; a small regression test covers timeout-then-cleanup.

CI and tooling: new mise run test:e2e:controlplane [filter] (Playwright/Chromium install, no --rerun-fails), serialized .github/workflows/e2e-controlplane.yml (cancel-in-progress: false for the shared three-org test account), artifact upload, and e2e/README.md updates. playwright-go is added to go.mod. The workflow still has a temporary push trigger on feat/e2e-controlplane to remove before merge.

Reviewed by Cursor Bugbot for commit 76950a8. Configure here.

gtrrz-victor and others added 7 commits September 14, 2026 17:05
…e test

Add e2e/controlplane, a package that drives the entire binary against the
production control plane with no coding agent involved. Its TestMain logs
in once per run with `entire login --device`, completing GitHub sign-in and
the device approval in headless Chromium through playwright-go, so every
test starts from that session.

TestControlPlane_CreateCloneDelete creates an org, a project in it, and a
repo in that project, waits until the read path reports the repo clonable,
clones it, checks both `git remote -v` lines, and deletes repo, project,
and org in that order.

`mise run test:e2e:controlplane` builds the binaries, installs the
Playwright driver and Chromium, and runs the package; the new
e2e-controlplane.yml workflow runs it on manual dispatch and pushes to
main using the E2E_GH_USERNAME and E2E_GH_PASSWORD secrets.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2G77A53V4ERFBBZGH2ZP0BD
Lets the workflow run from the feature branch before it exists on main.
Revert before merge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2G7T5P9ZEV9CWV9QBFQQ662
Those are the names the repository secrets were created under; the test
process keeps its E2E_-prefixed variables.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2G801KYYD9MHBK63NKWV9YZ
On Linux Playwright resolves its browser directory from XDG_CACHE_HOME,
which TestMain redirects to an isolated temp dir for the CLI, so Chromium
installed by the mise task was not found. Pin PLAYWRIGHT_BROWSERS_PATH to
the real user cache first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2G86EBB13ND16W1KF8NBQ3B
Signing in from a fresh CI runner hits GitHub's new-device verification,
which emails a code and cannot be automated. GitHub skips it for accounts
with 2FA enabled, so the test account uses an authenticator app and the
browser step now fills the RFC 6238 code computed from E2E_GH_TOTP_SECRET,
required alongside the username and password.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2G8TWAEMQ6T2ZB2AEP0NK0F
The repository secrets now carry the same names as the test's variables.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2G908725NNX3QRF5EZRV6T1
Entire-Checkpoint: 01M2GB520NW38WGV1WVWRFJZFY
Copilot AI lite review requested due to automatic review settings September 14, 2026 16:14

Copilot AI left a comment

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.

🟡 Changes recommended

Unresolved workflow safety, cleanup, login cancellation, and environment-isolation issues block approval.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds production control-plane E2E coverage using device-flow authentication and live organization, project, and repository lifecycle tests.

Changes:

  • Adds headless GitHub/TOTP login and isolated CLI credentials.
  • Adds lifecycle, timeout, cleanup, and TOTP tests.
  • Adds mise tooling, documentation, dependencies, and serialized CI workflow.
File summaries
File Description
mise-tasks/test/e2e/controlplane Local control-plane E2E runner
go.sum Dependency checksums
go.mod Adds Playwright dependency
e2e/README.md E2E usage and secret documentation
e2e/controlplane/totp.go TOTP generation
e2e/controlplane/totp_test.go TOTP tests
e2e/controlplane/main_test.go Test initialization and environment isolation
e2e/controlplane/login_test.go Device-flow and browser authentication
e2e/controlplane/lifecycle_test.go Resource lifecycle and clone scenario
e2e/controlplane/cli_timeout_test.go Timeout and cleanup regression test
e2e/controlplane/cli_test.go Bounded CLI execution helpers
.github/workflows/e2e-controlplane.yml Production E2E workflow and concurrency controls
Review details

Suppressed comments (8)

.github/workflows/e2e-controlplane.yml:18

  • cancel-in-progress: false only prevents GitHub's concurrency manager from cancelling an older run; manual cancellation, runner loss, and the job timeout can still skip t.Cleanup. With a three-organization account quota, one interrupted run can leave resources that block all later runs until someone cleans them up manually. Add a startup reaper for stale e2e-cp-* organizations (or an out-of-band cleanup workflow) before relying on this suite in production CI.
# One shared test account with a three-org quota: never two runs at once, and
# never cancel one mid-flight — a cancelled run skips t.Cleanup and leaks the
# resources it created.

e2e/controlplane/cli_timeout_test.go:17

  • This test is in the same e2e-tagged package as TestMain, so even the local fake-CLI regression invokes TestMain first, requires all E2E_GH_* secrets, and performs a live five-minute device-flow login before it can test a 50 ms timeout. Running go test -tags=e2e -run TestRunEntireTimeoutAllowsCleanup therefore cannot be an offline/local regression; move this test/helper to a package or test target without the production-login TestMain.
func TestRunEntireTimeoutAllowsCleanup(t *testing.T) {

e2e/controlplane/lifecycle_test.go:29

  • If the organization is created successfully but the CLI returns malformed JSON or omits id, decodeJSON/require.NotEmpty aborts the test before this cleanup is registered. That leaves a live organization behind and can exhaust the account's three-organization quota; register cleanup before validation with a name-based fallback (the delete command accepts names) or otherwise retain a cleanup handle as soon as creation starts.
	t.Cleanup(func() { deleteResource(t, dir, "org", org.ID) })

e2e/controlplane/lifecycle_test.go:36

  • The project cleanup has the same leak window: a successful create followed by invalid output or a missing ID reaches FailNow before t.Cleanup is installed. Keep a cleanup registered from the create boundary and fall back to the project name if the response cannot be decoded.
	t.Cleanup(func() { deleteResource(t, dir, "project", project.ID) })

e2e/controlplane/lifecycle_test.go:41

  • The repository can likewise be provisioned before JSON decoding or ID validation fails, but cleanup is only added afterward. A malformed/partial create response therefore leaves the repo (and its parent resources) behind; register a cleanup fallback at the create boundary rather than waiting for a valid ID.
	t.Cleanup(func() { deleteResource(t, dir, "repo", created.ID) })

e2e/controlplane/lifecycle_test.go:74

  • The comment says the by-name read can lag after creation, but mustRunEntire treats a temporary no repo named/404 from repo get /et/... as fatal. That makes the test fail during the stated propagation window instead of retrying, while only the already-registered cleanup can run. Use a retryable lookup that distinguishes a not-yet-visible reference from a real provisioning failure.
		stdout, _ := mustRunEntire(t, dir, "repo", "get", ref, "--json")
		repo := decodeJSON[repoJSON](t, stdout)

e2e/controlplane/main_test.go:93

  • gitenv.IsolateMain only neutralizes Git config variables; it does not clear GIT_DIR, GIT_WORK_TREE, or GIT_INDEX_FILE. When the suite is launched from a Git hook or another Git child, the test's cmd.Dir and the later git clone can therefore target the inherited repository instead of the temporary test directory. Scrub these repo-selector variables in the TestMain environment before spawning the CLI and Git commands.
	gitenv.IsolateMain()

e2e/controlplane/main_test.go:96

  • Because this is a package-level TestMain, every -run selection—including TestRunEntireTimeoutAllowsCleanup—executes the five-minute production device login first. The local fake-CLI regression therefore still requires real E2E_GH_* secrets and can mutate production state even though it does not need authentication. Move that regression into a separate non-production package/TestMain so it can run independently.
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
	err = deviceLogin(ctx, username, password, totpSecret)
  • Files reviewed: 11/12 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread e2e/controlplane/login_test.go
Comment thread .github/workflows/e2e-controlplane.yml Outdated
gtrrz-victor and others added 6 commits September 15, 2026 11:29
…d pages

TestMain logs out after the run so the shared test account does not
accumulate a live login per CI run. The clonable poll treats a by-name
not-found from `repo get` as not ready yet instead of failing the test.
The browser stepper reports the page a login is stuck on after a minute
without navigation rather than waiting for the context deadline. The
TOTP key is decoded unpadded as GitHub displays it, and the task
description names the TOTP secret it requires.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2J6DNWZ0D54WQ1NB7S3KGY1
The branch trigger was a temporary aid for validating the workflow from
its own branch; the suite is green there, so drop it before merge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2J6M23316W98E65XV3DBBKT
Each call to runEntireWithTimeout creates its own context, so cleanup
getting a fresh deadline holds by construction, and the fixture never
spawned the descendant that WaitDelay exists for. It also ran only after
a production login and pinned the package to a single binary override.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2J6XHN1QAAJBY9AYVXZFBJ6
totpCode is support code for the suite's login, which exercises it end
to end on every run; a second layer of tests around it is not worth
keeping.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2J8GFB19SGNZAS19KZ7VP75
The package has one test, so a regex over it selects everything or
nothing. The mise task keeps its filter argument for local use.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2J95ZV95H4S1066PAY6PWBX
GitHub shows the key unpadded, but a base32 key copied from elsewhere
carries trailing "=", which the unpadded decoder rejected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2J9SPZP6J9RBTKE4QREAPM3
@gtrrz-victor
gtrrz-victor marked this pull request as ready for review September 15, 2026 10:33
@gtrrz-victor
gtrrz-victor requested a review from a team as a code owner September 15, 2026 10:33
gtrrz-victor and others added 3 commits September 15, 2026 12:43
…utput

A create that succeeded but printed unusable JSON failed the test before
its cleanup was registered and left the resource behind on the shared
account. Cleanups now register by name as soon as the create returns and
switch to the ULID once decoded, since an already-deleted ULID exits 0
while a name that no longer resolves is an error.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2JAMC63BH2AGBW1HWNZ7ZGC
…cleanup

t.Cleanup cannot run when the process is killed: a package timeout, a
cancelled job, or a lost runner. With a three-org quota on the test
account, one such run blocked every later one until someone deleted the
leftovers by hand. Each run now starts by deleting e2e-cp-* orgs older
than 30 minutes, with their projects and repos; the age gate leaves a
run in progress elsewhere alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2JB5PS1Y58CFQ6PN2N1QGXE
The sweep of leftover orgs is now best effort: a leftover it cannot list
or delete is logged and skipped instead of failing the current run, and
"swept" is logged only once the org delete succeeded. The device login
runs in the isolated state dir rather than the checkout, a stalled
about:blank page is named instead of printed as an empty host, and the
sweep no longer reuses one output variable across nested listings.

Restores the offline RFC 6238 vector test, now also covering a padded
key: the production workflow runs only on main and manual dispatch, so
this is the only check of the algorithm that runs on every PR.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M2JRD2XPQH9Y8VSHBTC88VNX
@gtrrz-victor
gtrrz-victor merged commit c21ef7f into main Sep 15, 2026
17 checks passed
@gtrrz-victor
gtrrz-victor deleted the feat/e2e-controlplane branch September 15, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants