Skip to content

Gate fixes: stale endpoint responses, stale project target, false magic-link success (PR #100 findings) - #102

Merged
sgardoll merged 9 commits into
milestone/stu-implement-updated-ui-uxfrom
sgardoll/gate-auth-connection
Sep 28, 2026
Merged

sgardoll merged 9 commits into
milestone/stu-implement-updated-ui-uxfrom
sgardoll/gate-auth-connection

Conversation

@sgardoll

@sgardoll sgardoll commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixes the stale-identity class of review findings on the milestone integration PR (#100): any async response or stored value that outlives the key/endpoint it was produced under must not be applied to the current one. Three mechanisms now cover every surface the reviewers flagged:

  • ffEndpointGeneration — monotonic counter bumped on every endpoint change; fetchProjects() and validateFlutterFlowConnection() capture it at start and discard their outcome (status writes, dropdown population, error rendering) if it moved on.
  • Per-kind request sequences (ffProjectFetchSeq / ffConnectionCheckSeq via isCurrentProjectRequest()) — a connection check starting while a dropdown fetch is pending no longer discards the fetch (which stranded the dropdown on "Loading projects..."), and a late response for a replaced key can never overwrite the new key's list or status.
  • Selection identity binding (projectSelectionIdentity + isProjectSelectionConfirmed()) — a project selection is usable as a deploy target only while it names the project and the key + endpoint generation a completed listProjects response confirmed it under. Key replacement, endpoint change, or a completed list that omits the stored target all strip confirmation, so an old-key choice can never become the new key's deploy target. Applied at saveApiKeys(), the editor dropdown, the deploy confirm modal (confirmProjectListIdentity), and every deploy target read.
  • isMagicLinkSuccess(data) in src/authMagicLink.js — success only for the explicit success code, an explicit success: true, or a legacy message-only body; success: false, a truthy error, any other code (incl. PLUS_ALIAS_REJECTED), or a malformed/empty body is an error. Used by both the sign-in modal (handleMagicLinkRequest) and the account "Send new link" controller (createSendLinkController), so a failed send is never reported as "Sent!".

Findings addressed

Finding Fix
G3 (Greptile P1): old endpoint requests replace current projects/status ffEndpointGeneration guards both consumers
D2 (Devin): replacement key keeps stale project target key-replacement clears unconfirmed target; deploy gate re-checks confirmation
G4 (Greptile P1): rejected links appear sent isMagicLinkSuccess in the sign-in handler
Devin: code-less success:false/error bodies claim sent predicate rejects explicit failure fields and malformed bodies
Devin: connection check strands the project dropdown per-kind request sequences
Devin: account resend claims failed links were sent createSendLinkController uses isMagicLinkSuccess, routes failures to its error state
Devin: old-key project becomes the new deploy target selection identity binding at every selection read
Devin: unsaved key preview erases the stored deploy target unconfirmed selection clears the stored project only on a real key replacement; a same-key save keeps the target and reloads its list
Devin: older dropdown fetch overwrites a newer check's outcome shared issue/applied order (ffRequestOrderSeq / ffAppliedOutcomeOrder) across both request kinds gates all shared-state writes; dropdown options stay per-fetch
Devin: direct deploy fails while the post-reload list refreshes getConfirmedStoredProjectId() verifies an unconfirmed stored target against the current key's real list before refusing; the deploy dialog's stored fallback uses the same read
Devin: stale verification revives a removed target the verify refuses its own result when a newer request already applied an outcome; it re-checks endpoint generation + configured key before returning a target
Devin: project-scoped keys lose their deploy target a scoped listProjects denial (403) can no longer disprove the stored target — the push decides reachability; endpoint/key guards still apply
Devin: old endpoint response fills the deploy dialog populateConfirmProjectSelect guards on endpoint generation + stored key and repopulates for the current identity
Devin: deploy combines old key with new project all three deploy paths capture key + endpoint as one identity, pass the captured key into getConfirmedStoredProjectId, and revalidate it after the last async read
Devin: connection card stuck on "Checking connection…" outcome ordering (status) split from confirmation ordering (selection); the dialog fetch claims only confirmations so a pending check always settles the card
Devin: preview key writes account status fetchProjects writes status/claims outcomes only while its key matches the configured key at write time; previews populate their own dropdown only

Verification on head 683f43d (includes merge of milestone/stu-implement-updated-ui-ux@5d040d7)

  • CI=1 npm test — 360 pass / 0 fail
  • CI=1 npm run build — ✓ built
  • CI=1 CCC_TEST_PORT=3106 npx playwright test e2e/stu384-connection.spec.js e2e/stu386-magic-link-signin.spec.js e2e/stu387-account-access.spec.js — 41/41 pass, covering: stale endpoint fetch/check, check-during-fetch, stale old-key fetch, key replacement without/with an old-key selection, stored target omitted by the new list, previewed-key leftover choice on save, a late dropdown fetch after a newer failed check, a direct commit during a pending startup check, a stale verify reviving a removed target, a scoped key keeping its stored target, an old endpoint filling the deploy dialog, a dialog fetch stranding the card, a preview key rejecting the saved account, a mid-deploy key save pairing credentials, HTTP-200 error-code / success:false sends, and the account resend failure paths
  • Fail-before evidence: the nine newest regression tests (preview leftover, stale check overwrite, pending-startup commit, stale verify, scoped key, endpoint-stale dialog, stranded card, preview rejection, mid-deploy pairing) all fail on their respective pre-fix heads and pass here
  • Independent runtime retest of cbe2f39 (headed browser, mocked backends, recorded): all async-race fixes exercised live — see the testing comment on this PR
  • Earlier full suite on pre-merge head f496402: npm run test:ui 209 pass + 1 flaky (pre-existing stu385 race, passes on retry), test:accessibility 7/7, test:deploy-guard 9/9, test:proxies 30/30, scan_bundle_for_leaks.py clean

Merge note

Resolved the saveApiKeys() conflict with milestone/stu-implement-updated-ui-ux by keeping both guards: the base's stored-key check (previousKey from storage, clears when the key changed with no selection) and this branch's confirmation gate — the base's early declaration of selectedProjectId was kept so the merged flow compiles unchanged. Same-endpoint out-of-order responses keep last-write-wins within one request kind, which is the intended scope of the generation guard.

Link to Devin session: https://app.devin.ai/sessions/f52b679964d54e84a7f2d8bf3b1abe37
Open in Devin Desktop: https://app.devin.ai/desktop/session/f52b679964d54e84a7f2d8bf3b1abe37?variant=devin
Requested by: @sgardoll

…success

- G3: a project fetch or connection check started for the previous endpoint
  discards its outcome once the endpoint generation advances, so a late
  response can never overwrite the current endpoint's status or project list.
- D2: replacing the FlutterFlow key without choosing a (different) project
  clears the stored target so a deploy cannot silently use the old project.
- G4: an HTTP-success magic-link response carrying any code other than the
  success code (or none) is surfaced as an error instead of 'Sent!'.
- Regression tests for all three behaviours (unit + e2e).
The held check now resolves with an empty project list, so a stale response
would flip the card to 'no-projects' — visibly different from staging's
'connected' — instead of repeating the same outcome.
@sgardoll
sgardoll marked this pull request as ready for review September 28, 2026 07:02
@greptile-apps

greptile-apps Bot commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

This PR does not match any of the 1 configured review trigger rule.

devin-ai-integration[bot]

This comment was marked as resolved.

Devin finding 1 (app.js): a projects fetch or connection check now captures
the key it was issued with plus a monotonic request sequence. An outcome that
a newer request has superseded, or that was issued for a key since replaced
or cleared, is discarded — so a late old-key response can no longer populate
the new key's project list or connection status when the endpoint did not
change. A preview fetch made with the key being saved stays valid.

Devin finding 2 (src/authMagicLink.js): an explicit success:false or truthy
error is never a success, with or without a code, and an empty or malformed
body is not success-shaped. The legacy message-only response still counts.
getMagicLinkResultMessage follows the same predicate, so a 200 that says it
failed can no longer read as "Sent!".

Regression tests: a stale-old-key fetch held across a key replacement (e2e),
a success:false sign-in response (e2e), and unit tests for the failure,
error-field and malformed-body classification.
devin-ai-integration[bot]

This comment was marked as resolved.

Devin finding 1 (app.js): the dropdown fetch and the connection check shared
one supersession sequence, so a check starting while a fetch was pending
discarded the fetch's response and left the dropdown stuck on "Loading
projects..." forever. Each kind now keeps its own sequence — both still gated
by endpoint generation and the configured key — so a check cannot supersede a
pending fetch and vice versa; same-kind supersession and stale
key/endpoint rejection are unchanged. Regression test in
e2e/stu384-connection.spec.js holds a listProjects fetch, starts a validation,
releases the fetch, and asserts the dropdown populates and the card stays
connected (fails before the fix).

Devin finding 2 (src/accountAccess.js): createSendLinkController marked every
resolved response as "sent" and never consulted isMagicLinkSuccess, so a 200
{code:"SEND_FAILED"} or {success:false} rendered the sent confirmation with the
server's error message. The controller now classifies the response with the
shared magic-link predicate before emitting "sent" and routes unsuccessful
responses through its error state with the server's own message (message, else
error, else a generic fallback). Unit tests cover SEND_FAILED, success:false
and genuine success; an e2e assertion in stu387 covers the row rendering.
Pre-existing tests that used the fictional {code:"sent"} fixture now use
MAGIC_LINK_SUCCESS_CODE.
devin-ai-integration[bot]

This comment was marked as resolved.

sgardoll and others added 2 commits September 28, 2026 21:47
Devin finding 3 (app.js): a project chosen from the old key's dropdown before
the key was replaced was still persisted as the deploy target, so a later
deploy could target a project the new key never listed. Third instance of the
stale-identity class, so the approach changes instead of patching the
instance: a selection now carries an identity binding ({projectId, key,
endpointGeneration}) recorded only from a completed listProjects response, and
every read of a selection — saveApiKeys preservation, the dropdown value, the
deploy confirm modal and every deploy target read — goes through
isProjectSelectionConfirmed (or the modal list-identity gate). A key
replacement or endpoint change moves the binding out of match, so no
previously confirmed selection can be used until a completed fetch
re-confirms it or it is cleared.

Regression tests: selecting a project from the old key's list and replacing
the key now leaves no stored target and requires a fresh selection (fails
before the fix); a stored target the current key's completed list omits is
refused at deploy time (fails before the fix).
…ui-ux' into pr-102-fix

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>

# Conflicts:
#	app.js
devin-ai-integration[bot]

This comment was marked as resolved.

…target

Three follow-on bugs from the identity-binding gate (Devin Review on
the merged head):

- A previewed key's leftover dropdown choice could erase the stored
  target on save: the unconfirmed-selection path now clears the stored
  project only on a real key replacement; an unchanged configured key
  keeps its target and reloads its list instead.
- An older dropdown fetch could overwrite a newer connection check's
  outcome: a shared issue/applied order counter across both request
  kinds now gates writes to ffConnectionState and stored-selection
  confirmation, while the newest fetch still fills its own dropdown.
- A direct commit during the post-reload project refresh refused the
  saved target on an unpopulated binding: the confirmed-stored-target
  read now verifies the project against the current key's real list
  (same completed-response guard, superseded/failed verifies still
  refuse) and the deploy dialog's stored fallback uses the same read.

Regression coverage: preview-then-clear-save preserves the stored
target, a late fetch can't resurrect connected after a failed check,
and a direct commit during a pending startup check still deploys.

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Follow-on fixes for the second Devin Review round on f151de4:

- getConfirmedStoredProjectId() now refuses its own result when a
  newer request already applied an outcome (including one that
  stripped the target's confirmation), and re-checks the configured
  key + endpoint generation before returning a target — a stale
  verification can no longer revive a removed project.
- A project-scoped key whose listProjects is denied (403) no longer
  loses its stored target: the denial cannot disprove it, so the
  deploy proceeds and the push decides reachability; endpoint/key
  guards still apply.
- populateConfirmProjectSelect() now treats an endpoint-generation or
  stored-key move during its fetch as a stale identity and repopulates
  for the current one instead of filling the deploy dialog with
  undeployable options.

Regression coverage: stale verify cannot revive a removed target, a
scoped key keeps its stored target, and an old endpoint's response
cannot fill the deploy dialog (all fail on the previous head).

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…tion ordering

Third Devin Review round on cbe2f39:

- executeCommit, executeBundleCommit and commitToFlutterFlow now capture
  key + endpoint as one deploy identity: the captured key is passed into
  getConfirmedStoredProjectId so the verification describes the same
  credential the push will use, and the identity is revalidated after
  every asynchronous read — a key save or endpoint switch landing
  mid-deploy can no longer pair the old key with a target confirmed for
  a different identity.
- Ordering of shared request state is split by write domain: connection
  outcomes (the account status card) order separately from selection
  confirmations. populateConfirmProjectSelect claims only the
  confirmation order, so a dialog fetch can no longer claim the outcome
  a pending check needs to settle the card — the card no longer sits on
  'Checking connection…' after both requests finish.
- fetchProjects writes account status and claims the connection outcome
  only for the configured key: a preview fetch for a key that is never
  saved can no longer mark the saved account rejected, nor flash
  validating on its card. Preview validity still populates the editor
  dropdown independently.

Regression coverage: a deploy-dialog fetch cannot strand the card on
checking, a preview key's failure cannot mark the saved account
rejected, and a mid-deploy key save cannot pair the old key with a new
target (all fail on the previous head).

Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

E2E-tested head cbe2f39 on the local checkout (npm run dev → :3000) with mocked FlutterFlow/BuildShip backends in a recorded headed session. Every async-race fix exercised at runtime:

  • stale deploy-verify after newer omitting response → commit refused "No confirmed FlutterFlow project target", zero exportCode requests
  • project-scoped key (listProjects 403) → stored target kept → exportCode fired
  • endpoint switched mid-confirm-fetch → dialog repopulated with staging list only (screenshot below)
  • preview-clear-save preserved stored target; late fetch after failed check stayed "rejected"; commit during pending check deployed via verified read
  • magic-link truthfulness: invalid → inline reject w/o request; SEND_FAILED/success:false → errors w/ email kept; success → "Sent!" + "Check your email"

One transient pageerror ("Identifier 'generation'…") seen once on an older profile, not reproduced in the clean run.

Confirm dialog repopulated with staging list only
SEND_FAILED shows error, email kept

More: success path + connected card

Sent! success state
Connected card showing proj-alpha-1

@devin-ai-integration devin-ai-integration Bot 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.

Devin Review found 1 new potential issue.

Devin Review

Comment thread app.js Resolved
@sgardoll
sgardoll merged commit f95a16d into milestone/stu-implement-updated-ui-ux Sep 28, 2026
2 checks passed
sgardoll pushed a commit that referenced this pull request Sep 28, 2026
…ed-ui-ux

The re-sync onto f95a16d pulls in #102, which changes app.js and src/.
The committed bundle was stale against that source, so dist is rebuilt to
keep the published assets and the source in agreement. New bundle:
index-C31N1DOp.js -> index-LEYLuMwg.js. The CSS hash is unchanged and
dist/index.html references only files present in dist/assets/.
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.

1 participant