Skip to content

End-to-end tests for automatic approval, against a stubbed AWS SSO - #89

Merged
popen2 merged 2 commits into
mainfrom
claude/frost-auto-approve-aws-o8yleo
Sep 14, 2026
Merged

popen2 merged 2 commits into
mainfrom
claude/frost-auto-approve-aws-o8yleo

Conversation

@popen2

@popen2 popen2 commented Aug 31, 2026

Copy link
Copy Markdown
Owner

This is the testing PR. It changes no behaviour — it tests what main does today. The behaviour change that came out of writing it is #90, stacked on this.

Rebased onto main at 7522d2e (after #92).

Why

Automatic approval is a script clicking buttons on pages Frost does not own, in a window the user cannot see. Every way it can be wrong is quiet — a button never found (the refresh hangs until the device code expires), a button that should not have been clicked (the request is denied), a hand-over that never happens (the user waits in front of nothing). Testing the matching rules alone misses what actually makes it work: the whole path.

So npm run test:auto-approve drives the real refresh() — the entry point the tray, the hotkey and the timer use — and asserts on what the user would have seen.

Ten… nine scenarios

Scenario What must be true
Portal session live Token collected, no window ever shown, no notification
AWS redirects to the IdP, session live there Same — the hop happens and still nothing is shown
AWS redirects to the IdP, it wants a password Window shown; after the test signs in, the driver finishes the approval
Notify mode Nothing opens until triggerPendingAuth(), even when the approval needs nobody
Default-browser mode openExternal gets the verification URL, no window shown
Automatic approval off Window visible from the start, nothing driven
IdP page with an "Allow access" button Never clicked — the label the driver wants, on a host that is not ours
AWS page with nothing recognisable Nothing clicked, including a refusal wearing cli_login_button's id
Nobody at the machine (#92's parking) The login is held hidden, then shown when presence returns

What makes it possible

Four interceptions, none of which asks the app to know it is under test:

  • AWS_ENDPOINT_URL_SSO_OIDC / AWS_ENDPOINT_URL_SSO — an AWS SDK feature, so the device authorization, the polling and its AuthorizationPendingException are the real client speaking a real protocol. The token only becomes redeemable when the stub's approval page is actually fetched, so nothing passes without a real click.
  • session.protocol.handle("https", …) — pages served at their real names, so the renderer gets https://d-1234567890.awsapps.com, a secure context, and a genuine cross-origin 302 to https://idp.example.test. Served from localhost this would prove nothing; the host rule is the point.
  • Notification.prototype.show and shell.openExternal recorded, not performed — a CI runner has neither a notification daemon nor a browser. Notification is a non-configurable export, hence the prototype.
  • powerMonitor.getSystemIdleTime() answers what the scenario saysKeep trying an overnight sign-in instead of giving up on it #92 made Frost show a login page only to somebody who is there, so real idle time would make these depend on whether anyone touched the keyboard: green on a fresh CI runner, red on a desktop five minutes after you start them and walk away.

HOME and the electron-store move to a temp directory, so a run touches nothing of the developer's.

It fails when the feature breaks

Each mutation fails exactly one scenario:

Mutation Fails
isApprovalHostfalse silent approval, federated session
isApprovalHosttrue identity provider (clicks its "Allow access")
isDenialfalse unrecognised page (clicks the id trap)
isUserPresenttrue nobody at the machine (shows a window to an empty room)

Also here: the tests report as tests

The first commit renames check:overlaytest:overlay (and its file), and moves it out of the job called 🧹 Lint app into 🧪 End-to-end tests, which build waits on. Both real-app tests now report under a name that answers "did the tests run" from the check list.

Still not covered

A real desktop — no security key, no keychain, no dock — and the real AWS pages, so the id and label matching against what AWS serves today still wants a smoke test on a real machine.

🤖 Generated with Claude Code

https://claude.ai/code/session_018XK1mi3rSPLzxu7XUhyC6a

It is an end-to-end test: it boots the app and drives a real BrowserWindow
through a WebAuthn wait. Everything naming it said "check" — the script,
the file, the text it printed — and it ran as a step inside the job called
"🧹 Lint app", so a pull request's check list never said whether it had
run at all.

`npm run test:overlay`, `tools/test-login-overlay.js`, and a job named
🧪 End-to-end tests that `build` waits on. It costs a second npm ci and
build, which run in parallel with lint, and buys a check line that goes
red on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XK1mi3rSPLzxu7XUhyC6a
Automatic approval is a script clicking buttons on pages Frost does not
own, in a window the user cannot see. Every way it can be wrong is quiet:
a button never found (the refresh hangs until the device code expires), a
button that should not have been clicked (the request is denied), a
hand-over that never happens (the user waits in front of nothing). None of
that is visible to a type-check, and testing the matching rules alone
misses what actually makes it work — the whole path.

So `npm run test:auto-approve` drives the real refresh(), the same entry
point the tray, the hotkey and the timer use, and asserts on what the user
would have seen. Four interceptions make that possible without the app
knowing it is under test:

- AWS_ENDPOINT_URL_SSO_OIDC / AWS_ENDPOINT_URL_SSO, an AWS SDK feature,
  point the SDK at a stub HTTP service. The device authorization, the
  polling and its AuthorizationPendingException are the real client
  speaking a real protocol, and the token only becomes redeemable when the
  stub's approval page is actually fetched.
- session.protocol.handle("https", ...) serves the pages at their real
  names, so the renderer gets https://d-….awsapps.com, a secure context,
  and a genuine cross-origin redirect to the identity provider. Served
  from localhost it would prove nothing — the host rule is the point.
- Notification.prototype.show and shell.openExternal are recorded rather
  than performed: what the user was told and where they were sent are the
  assertions, and a CI runner has neither a notification daemon nor a
  browser.
- powerMonitor.getSystemIdleTime() answers whatever the scenario says.
  Frost only shows a login page to somebody who is there, so real idle
  time would make these tests depend on whether anyone had touched the
  keyboard.

Nine scenarios, one per outcome, including the two that decide whether
anybody is interrupted: an identity provider hop that the session carries
through stays silent, and one that asks for a password brings the window
up. HOME and the electron-store move to a temp directory, so a run touches
nothing of the developer's.

Each mutation fails exactly one scenario: the host rule returning false
fails both approval scenarios and returning true fails the
identity-provider one; dropping the refusal rule fails the unrecognised
page; isUserPresent() returning true unconditionally fails the unattended
one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XK1mi3rSPLzxu7XUhyC6a
@popen2
popen2 force-pushed the claude/frost-auto-approve-aws-o8yleo branch from 52bba75 to ce9c29c Compare September 14, 2026 10:10
@popen2 popen2 changed the title Check automatic approval end to end, against a stubbed AWS SSO End-to-end tests for automatic approval, against a stubbed AWS SSO Sep 14, 2026
@popen2
popen2 merged commit 9c19dc1 into main Sep 14, 2026
10 checks passed
@popen2
popen2 deleted the claude/frost-auto-approve-aws-o8yleo branch September 14, 2026 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants