grails-test-examples/scaffolding fails intermittently in CI, on four occasions between 24 and 25 August 2026, on four unrelated branches. The root cause is not yet known; this issue records what is established so far.
The test
com.example.UserControllerSpec > User list — grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/UserControllerSpec.groovy
void "User list"() {
when: 'an unauthenticated user requests the user list and signs in when prompted'
via(UserListPage)
at(LoginPage).login()
then: 'the saved request redirects to the user list'
at(UserListPage)
scaffoldTable
}
It fails inside LoginPage.login(), on the wait that follows the click:
void login(String username = 'test@grails.org', String password = 'letmein') {
this.username = username
this.password = password
loginButton.click()
waitFor { title != pageTitle && $('input', name: 'username').empty } // times out here
}
geb.waiting.WaitTimeoutException: condition did not pass in 30 seconds (failed with exception)
at com.example.pages.LoginPage.login(LoginPage.groovy:39)
at com.example.UserControllerSpec.User list(UserControllerSpec.groovy:48)
Caused by: Assertion failed:
title != pageTitle && $('input', name: 'username').empty
| | | |
| | | false
| | 'Please sign in'
| false
'Please sign in'
That is the trace from #16150, #16184 and #16209, which are identical to each other: the same
condition, the same thirty seconds, LoginPage.groovy:39 in login_closure1, and
UserControllerSpec.groovy:48.
The fourth, #16215, differs in one respect: LoginPage.groovy:43, in login_closure2. That branch
carries a proposed change to this page object which adds lines above the wait, so the same wait on
the same condition reports a different line and closure. It is the same failure, not a different
one - and worth noting because it shows the wait that fails is the one after the click, on a branch
where the form is verified as filled before clicking.
Occurrences
None of those four branches touches this application, Spring Security, or Geb. It has been seen on Java 21 and Java 25, with indy on and off, and in two different workflows. In each run the other two specifications of the same application - BookControllerSpec and UserCommunityControllerSpec, which sign in the same way - passed.
What the browser was looking at
@ContainerGebConfiguration(reporting = true) means Geb captures the page at the failure. Three of
the four runs upload it - #16184, #16209 and #16215 - and those three are the same to the byte. The
#16150 run is in a workflow that uploads no Geb report, so there is no capture for it:
- 1089 bytes,
<title>Please sign in</title> - Spring Security's generated login page
- both fields empty, showing their placeholders
- the focus ring still on the username field, where
autofocus puts it on load
- no error: not
/login?error, no "Bad credentials" banner
- a
_csrf hidden field present
Two things follow. The page was loaded after the credentials were typed, because a page that had been typed into would hold the values and would have the focus on the second field. And the request that produced it was not a rejected authentication attempt, because that renders ?error with a banner.
What the failure is not
Determined by injecting faults locally into the same test and comparing the captured page:
| Injected fault |
Page produced |
Matches CI |
| Session cookie deleted between filling and submitting |
1089 bytes, login page, no error, autofocus |
yes, byte for byte |
Valid session, _csrf field overwritten with a stale value |
283 bytes, Whitelabel Error Page (403) |
no |
So the form reaches the server without a session cookie, rather than with a stale or mismatched CSRF token. Missing session and wrong token are answered differently: with no session Spring Security treats the caller as anonymous and returns the login page, while a wrong token on a live session is denied with a 403.
It is also not the credentials or the user data: UserService.loadUserByUsername looks the user up by email, and a lookup failure would produce ?error, which is absent.
What is still unknown
Why the browser has no session cookie when the form is submitted. The specification clears cookies in setup():
void setup() {
clearCookiesQuietly()
}
and ContainerGebSpec holds @Shared static GebTestManager testManager, so the browser is shared between specifications in a worker. Whether the cookie is cleared, never set, scoped to a different host than the one the form posts to, or lost some other way is not established.
A consequence worth noting for anyone attempting a fix: if the session is genuinely gone, Spring Security's saved request goes with it, so simply submitting the form again is not enough - the login then succeeds but lands on / rather than the user list, and the specification fails on at(UserListPage) instead. Confirmed locally.
Reproducing
Not reproduced on demand. Four consecutive local runs of :grails-test-examples-scaffolding:integrationTest passed. The failure signature can be produced deliberately by deleting the browser's cookies between filling the form and clicking, which is how the comparison above was made.
Suggested next step
Capture the browser's cookies and currentUrl into the Geb report when that wait fails. One further occurrence would then distinguish "no cookie at all" from "a cookie scoped to another host", which is what the fix depends on.
Other CI flakes tracked separately
Different tests and different mechanisms; listed so that anyone working through CI flakiness can see what else is open.
grails-test-examples/scaffoldingfails intermittently in CI, on four occasions between 24 and 25 August 2026, on four unrelated branches. The root cause is not yet known; this issue records what is established so far.The test
com.example.UserControllerSpec > User list—grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/UserControllerSpec.groovyIt fails inside
LoginPage.login(), on the wait that follows the click:That is the trace from #16150, #16184 and #16209, which are identical to each other: the same
condition, the same thirty seconds,
LoginPage.groovy:39inlogin_closure1, andUserControllerSpec.groovy:48.The fourth, #16215, differs in one respect:
LoginPage.groovy:43, inlogin_closure2. That branchcarries a proposed change to this page object which adds lines above the wait, so the same wait on
the same condition reports a different line and closure. It is the same failure, not a different
one - and worth noting because it shows the wait that fails is the one after the click, on a branch
where the form is verified as filled before clicking.
Occurrences
None of those four branches touches this application, Spring Security, or Geb. It has been seen on Java 21 and Java 25, with indy on and off, and in two different workflows. In each run the other two specifications of the same application -
BookControllerSpecandUserCommunityControllerSpec, which sign in the same way - passed.What the browser was looking at
@ContainerGebConfiguration(reporting = true)means Geb captures the page at the failure. Three ofthe four runs upload it - #16184, #16209 and #16215 - and those three are the same to the byte. The
#16150 run is in a workflow that uploads no Geb report, so there is no capture for it:
<title>Please sign in</title>- Spring Security's generated login pageautofocusputs it on load/login?error, no "Bad credentials" banner_csrfhidden field presentTwo things follow. The page was loaded after the credentials were typed, because a page that had been typed into would hold the values and would have the focus on the second field. And the request that produced it was not a rejected authentication attempt, because that renders
?errorwith a banner.What the failure is not
Determined by injecting faults locally into the same test and comparing the captured page:
_csrffield overwritten with a stale valueWhitelabel Error Page(403)So the form reaches the server without a session cookie, rather than with a stale or mismatched CSRF token. Missing session and wrong token are answered differently: with no session Spring Security treats the caller as anonymous and returns the login page, while a wrong token on a live session is denied with a 403.
It is also not the credentials or the user data:
UserService.loadUserByUsernamelooks the user up by email, and a lookup failure would produce?error, which is absent.What is still unknown
Why the browser has no session cookie when the form is submitted. The specification clears cookies in
setup():and
ContainerGebSpecholds@Shared static GebTestManager testManager, so the browser is shared between specifications in a worker. Whether the cookie is cleared, never set, scoped to a different host than the one the form posts to, or lost some other way is not established.A consequence worth noting for anyone attempting a fix: if the session is genuinely gone, Spring Security's saved request goes with it, so simply submitting the form again is not enough - the login then succeeds but lands on
/rather than the user list, and the specification fails onat(UserListPage)instead. Confirmed locally.Reproducing
Not reproduced on demand. Four consecutive local runs of
:grails-test-examples-scaffolding:integrationTestpassed. The failure signature can be produced deliberately by deleting the browser's cookies between filling the form and clicking, which is how the comparison above was made.Suggested next step
Capture the browser's cookies and
currentUrlinto the Geb report when that wait fails. One further occurrence would then distinguish "no cookie at all" from "a cookie scoped to another host", which is what the fix depends on.Other CI flakes tracked separately
AsyncPromiseSpectimes out waiting for a response from an endpoint that is bounded well below the client timeout.Different tests and different mechanisms; listed so that anyone working through CI flakiness can see what else is open.