Repository navigation
Add control-plane E2E tests with device-flow login - #2411
Conversation
…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
There was a problem hiding this comment.
🟡 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: falseonly prevents GitHub's concurrency manager from cancelling an older run; manual cancellation, runner loss, and the job timeout can still skipt.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 stalee2e-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 asTestMain, so even the local fake-CLI regression invokesTestMainfirst, requires allE2E_GH_*secrets, and performs a live five-minute device-flow login before it can test a 50 ms timeout. Runninggo test -tags=e2e -run TestRunEntireTimeoutAllowsCleanuptherefore cannot be an offline/local regression; move this test/helper to a package or test target without the production-loginTestMain.
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.NotEmptyaborts 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
FailNowbeforet.Cleanupis 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
mustRunEntiretreats a temporaryno repo named/404 fromrepo 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.IsolateMainonly neutralizes Git config variables; it does not clearGIT_DIR,GIT_WORK_TREE, orGIT_INDEX_FILE. When the suite is launched from a Git hook or another Git child, the test'scmd.Dirand the latergit clonecan 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-runselection—includingTestRunEntireTimeoutAllowsCleanup—executes the five-minute production device login first. The local fake-CLI regression therefore still requires realE2E_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.
…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
…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
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
The temporary push trigger for
feat/e2e-controlplaneremains for branch validation and should be removed before merge.Verification
TestOpenCodeSeedRepoPlantsDeps: the shared OpenCode cache was missingpackage.json.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
entireCLI without a coding agent:TestMainperforms oneentire login --deviceper run, with headless Chromium (playwright-go) completing GitHub password + TOTP and Entire device authorization usingE2E_GH_*secrets.The main scenario creates org → project → repo (
e2e-cp-*), waits until the repo is clonable, clones and checks theentire://remote, then deletes resources in order witht.Cleanupfallbacks. CLI helpers use bounded timeouts andWaitDelayso 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: falsefor the shared three-org test account), artifact upload, ande2e/README.mdupdates.playwright-gois added togo.mod. The workflow still has a temporary push trigger onfeat/e2e-controlplaneto remove before merge.Reviewed by Cursor Bugbot for commit 76950a8. Configure here.