Skip to content

The full end-to-end suite for automatic approval, and notify only when the sign-in needs you - #90

Open
popen2 wants to merge 6 commits into
mainfrom
claude/frost-auto-approve-e2e-suite
Open

The full end-to-end suite for automatic approval, and notify only when the sign-in needs you#90
popen2 wants to merge 6 commits into
mainfrom
claude/frost-auto-approve-e2e-suite

Conversation

@popen2

@popen2 popen2 commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Supersedes #89 — its commit is the first of the four here, so merging this delivers everything and #89 can be closed. (If #89 goes in first, this shrinks to the three new commits on its own.)

Eight scenarios, all driving the real refresh() against a stubbed AWS SSO, reported by CI as their own check. Building them turned up a behavior gap, which is one of the commits.

Notify mode was interrupting you about work it was going to do without you

Notify mode is a promise not to put a login page in front of you unannounced, and it kept that promise by announcing every refresh: notification, wait for the hotkey, then the login window. With automatic approval that is backwards — most refreshes now ask you for nothing, so notify mode was interrupting you once a working day about work Frost was about to do silently.

The notification moves to the moment a refresh turns out to need a person. Frost works through the approval off screen; if the page asks for a password, a one-time code or a security key, notify mode says so and waits for the hotkey or a click before showing anything. A refresh your identity provider session covers passes in silence. With automatic approval off, nothing changes — a login page is still about to open, so it is still announced first.

Two details that go with it: the pending trigger is cancelled when the run ends (otherwise hasPendingAuth() keeps saying yes and swallows your next hotkey press), and the hand-over is once-only for every surface, so two things noticing the page needs you cannot raise two notifications.

The suite

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 check signs in on the page, the driver finishes the approval
The same in notify mode Notification first, nothing shown until the trigger fires; then the window, then the token
The same in 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 it wants, on a host that is not ours
AWS page with nothing recognisable Nothing clicked, including a refusal wearing cli_login_button's id; window comes up

Bold rows are new. The IdP redirect is a real cross-origin 302 to another origin — and another renderer process — not a same-origin fake.

What makes it possible

Three 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 redirect to https://idp.example.test. Served from localhost this would prove nothing; the host rule is the point.
  • Notification.prototype.show and shell.openExternal are recorded, not performed — "what was the user told" and "where were they sent" are the assertions, and a CI runner has neither a notification daemon nor a browser. Notification is a non-configurable export, hence the prototype.

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, which is what makes the suite worth having:

Mutation Fails
isApprovalHostfalse silent approval, federated session
isApprovalHosttrue identity provider (clicks its "Allow access")
isDenialfalse unrecognised page (clicks the id trap)
skip the notify branch of the hand-over notify mode (window shown without asking)

In CI, under its own name

Both checks that boot the real app were steps inside the job called 🧹 Lint appcheck:overlay already was, and this suite was appended beside it. A pull request's check list therefore said nothing about either: the only line it showed claimed the work was linting.

They are their own job now, 🧪 End-to-end checks, so the check list answers "did the end-to-end suite run, and did it pass" without opening a log. It costs a second npm ci and build, which run in parallel with lint.

CI timings match local ones to within a few hundred milliseconds, so the waits are not tuned so tightly that a slower runner would flake:

  ok   silent approval: token collected, nothing shown (1.3s)
  ok   federated session: the identity provider hop stays silent too (1.6s)
  ok   sign-in needed: window shown, approval finished afterwards (1.1s)
  ok   notify mode: notified first, shown only on the go-ahead (1.1s)
  ok   default browser: handed over there, no window shown (6.1s)
  ok   setting off: window shown from the start, nothing driven (3.1s)
  ok   identity provider: its buttons are never clicked (1.1s)
  ok   unrecognised page: nothing clicked, not even the id trap, window shown (13.1s)
auto-approve end-to-end check OK (8 scenarios)

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

claude added 3 commits August 31, 2026 06:33
Automatic approval is a script clicking buttons on pages Frost does not
own, in a window the user cannot see, and every way it can be wrong is
quiet: a button that is 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 none of it is
really testable against the matching rules alone — what makes it work is
the whole path.

So `npm run check: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. Two 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.
- `session.protocol.handle("https", ...)` serves the verification 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.

`HOME` and the electron-store move to a temp directory, so a run touches
nothing of the developer's. Three scenarios, one per outcome: a live
identity provider session finishes with no window ever shown; a page
asking for a password shows the window and finishes the approval once the
check signs in on the page; and a page with nothing recognisable —
including a refusal wearing `cli_login_button`'s id — is not clicked at
all, and the window comes up.

It fails when the feature breaks, which is the part worth stating:
disabling the host rule fails both approval scenarios, and disabling the
refusal rule fails the third by clicking the trap. 16 seconds, and CI
runs it under xvfb next to the overlay check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XK1mi3rSPLzxu7XUhyC6a
Notify mode is a promise not to put a login page in front of the user
unannounced, and it kept that promise by announcing every refresh: a
notification, then a wait for the hotkey, before the login window opened.
With automatic approval that is backwards. Most refreshes now ask the user
for nothing at all, so notify mode was interrupting them once a working
day to tell them about work Frost was about to do without them.

The notification moves to the moment a refresh turns out to need a person.
Frost works through the approval off screen first; if the page asks for a
password, a one-time code or a security key, notify mode says so and waits
for the hotkey or a click before showing anything. A refresh the identity
provider session covers passes in silence.

With automatic approval off, nothing changes: the announcement still comes
before every refresh, because a login page is still about to open.

Two details that go with it: the trigger is cancelled when the run ends,
or hasPendingAuth() would keep saying yes and swallow the next hotkey
press; and the hand-over is now once-only for every surface, so two things
noticing the page needs the user cannot raise two notifications.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XK1mi3rSPLzxu7XUhyC6a
The check landed with three scenarios, which left the interesting half
untested: what the user is shown, and where, follows two settings and the
shape of the sign-in, and none of those combinations were exercised.

Five more scenarios, all driving the real refresh() against the stubbed
AWS SSO:

- AWS federates to the identity provider and the session there is still
  live. The hop happens, the redirect comes back, and still nothing is
  shown — a cross-origin navigation is not by itself a reason to
  interrupt anyone.
- The same hop, but the identity provider wants a password: in notify
  mode Frost has to notify and wait, showing nothing until the trigger
  fires, and then finish the approval once the check signs in on the page.
- The same in default-browser mode: openExternal gets the verification
  URL, the probe window is never shown.
- Automatic approval switched off: the window is on screen from the
  start and nothing is driven.
- An identity provider page offering a button labelled "Allow access" —
  the label the driver wants, on a host that is not ours. It must never
  be clicked.

Notifications and openExternal are recorded rather than performed, so the
assertions can be about what the user was told and where they were sent
without a notification daemon or a browser on the runner. Notification is
a non-configurable export, so that patch goes on the prototype.

The scenarios that end with nobody signing in now close the window rather
than destroying it, which is both what a user does and the path that tells
the poll loop to give up — the suite runs in 27s instead of waiting out
device codes.

Each mutation fails exactly one scenario: the host rule returning false
fails the two approval scenarios and returning true fails the identity
provider one; dropping the refusal rule fails the unrecognised page; and
skipping the notify branch of the hand-over fails notify mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XK1mi3rSPLzxu7XUhyC6a
@popen2 popen2 added the feature label Aug 31, 2026 — with Claude
Both checks that boot the real app were steps inside the job named
"🧹 Lint app" — check:overlay already was, and check:auto-approve was
appended next to it. So a pull request's check list said nothing about
them: the one line it showed claimed the work was linting, and answering
"did the end-to-end suite run?" meant opening the job and reading its log.

They get their own job now, named for what they do. It costs a second
npm ci and build, which run in parallel with the lint job, and it buys a
check line that goes red on its own and says which of the two failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XK1mi3rSPLzxu7XUhyC6a
They are end-to-end tests: they boot the app and drive it. Everything
naming them said "check" instead — `check:overlay`, `check:auto-approve`,
and a job called "End-to-end checks" — so a pull request's check list
never plainly answered "did the tests run".

`npm run test:overlay` and `npm run test:auto-approve`, in a job named
🧪 End-to-end tests. The files keep their names; only what you type and
what CI reports change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XK1mi3rSPLzxu7XUhyC6a
The scripts and the CI job were renamed; the files and everything they
printed still said "check". Now `tools/test-login-overlay.js` and
`tools/test-auto-approve.js` print "login overlay test passed" and
"auto-approve end-to-end tests passed".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XK1mi3rSPLzxu7XUhyC6a
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