main has been red since 2026-09-22. Because ci.yml's deploy job has needs: [lint, typecheck, test, test-postgres, build], nothing has deployed since - production is still serving 50be0ca, the last green commit, so the auth rework in 437970e has never been live.
Seven tests were failing in both test and test-postgres. Four of them were one bug.
The bug: the app's Auth actions were shadowed by the framework defaults
storage/framework/auto-imports/actions.ts is tracked, and was last regenerated at 2dc1c68 - before 437970e added app/Actions/Auth/. So the four names that commit introduced were still mapped to the framework's own actions:
'Actions/Auth/LoginAction': '../defaults/app/Actions/Auth/LoginAction.ts',
'Actions/Auth/RegisterAction': '../defaults/app/Actions/Auth/RegisterAction.ts',The router's string-handler resolver consults that map first and does not check whether an app file of the same name exists, so every route.post('/login', 'Actions/Auth/LoginAction') in routes/auth.ts resolved to the framework default. The entire auth rework was dead code at runtime:
| Behaviour the app's action implements | What actually happened |
|---|---|
| 10-character password policy | a 9-character password registered fine, 200 |
| email lowercased before lookup | USER@x.test refused 401 |
email-taken pre-check, 409 | 422 from a unique-constraint path |
| the app's own sign-in rate limiting | not applied at all |
201 on register | 200 |
The generator already prefers app/Actions over the defaults - generatePathIndex scans path.appPath("Actions") before frameworkDefaultsDir("app/Actions") and keeps the first key it sees - which is why statushq's registry is correct. This file was simply never regenerated after the actions were added. Regenerated here on the locked 0.74.5; it changes only those four lines.
Worth knowing for next time: autoImportsAreStale() decides by mtime. A fresh CI checkout stamps every file at checkout time, so a stale committed registry is used as though it were current. The resulting failure is silent, because the name still resolves to a real action - just not yours.
The three stale assertions
All three were written against the client pre-paint guard that 437970e deleted along with resources/partials/AuthGuard.stx.
- marketing-links scanned
resources/partials/AuthGuard.stx, which no longer exists, so the test died inreadFileSyncrather than failing an assertion. - reports-surface required
/var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:3620:2492: include error: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' [1m[31m╭──────────────────────────────────────────────────────────────╮[0m [1m[31m│[0m [1m[41m ERROR [0m [31mInclude Error[0m [1m[31m│[0m [1m[31m╰──────────────────────────────────────────────────────────────╯[0m [36m[1mFile:[0m [2mapp.stx[0m [90m(line 3620, col 2492)[0m [33m[1mMessage:[0m Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' [34m[1mContext:[0m [2m 3618: 3619:mainhas been red since 2026-09-22. Becauseci.yml's deploy job hasneeds: [lint, typecheck, test, test-postgres, build], nothing has deployed since - production is still serving50be0ca, the last green commit, so the auth rework in437970ehas never been live.Seven tests were failing in both
testandtest-postgres. Four of them were one bug.The bug: the app's Auth actions were shadowed by the framework defaults
storage/framework/auto-imports/actions.tsis tracked, and was last regenerated at2dc1c68- before437970eaddedapp/Actions/Auth/. So the four names that commit introduced were still mapped to the framework's own actions:'Actions/Auth/LoginAction': '../defaults/app/Actions/Auth/LoginAction.ts', > 3620: 'Actions/Auth/RegisterAction': '../defaults/app/Actions/Auth/RegisterAction.ts',The router's string-handler resolver consults that map first and does not check whether an app file of the same name exists, so every
route.post('/login', 'Actions/Auth/LoginAction')inroutes/auth.tsresolved to the framework default. The entire auth rework was dead code at runtime:Behaviour the app's action implements What actually happened 10-character password policy a 9-character password registered fine, 200email lowercased before lookup USER@x.testrefused401email-taken pre-check, 409422from a unique-constraint paththe app's own sign-in rate limiting not applied at all 201on register200The generator already prefers
app/Actionsover the defaults -generatePathIndexscanspath.appPath("Actions")beforeframeworkDefaultsDir("app/Actions")and keeps the first key it sees - which is why statushq's registry is correct. This file was simply never regenerated after the actions were added. Regenerated here on the locked 0.74.5; it changes only those four lines.Worth knowing for next time:
autoImportsAreStale()decides by mtime. A fresh CI checkout stamps every file at checkout time, so a stale committed registry is used as though it were current. The resulting failure is silent, because the name still resolves to a real action - just not yours.The three stale assertions
All three were written against the client pre-paint guard that
437970edeleted along withresources/partials/AuthGuard.stx.- marketing-links scanned
resources/partials/AuthGuard.stx, which no longer exists, so the test died inreadFileSyncrather than failing an assertion. - reports-surface required
/var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:3724:31: include error: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' [1m[31m╭──────────────────────────────────────────────────────────────╮[0m [1m[31m│[0m [1m[41m ERROR [0m [31mInclude Error[0m [1m[31m│[0m [1m[31m╰──────────────────────────────────────────────────────────────╯[0m [36m[1mFile:[0m [2mapp.stx[0m [90m(line 3724, col 31)[0m [33m[1mMessage:[0m Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' [34m[1mContext:[0m [2m 3722: 3723:and an> 3724: Sign in to comment on this pull request. 3725:
3726: @else[0m#auth-requiredelement on both report pages - that is, it asserted the browser guard was still there. Inverted to assert what the test's own name claims: the server reads the session, data is withheld from an unidentified visitor, and nothing gates in the browser. Matched as the directive and the element rather than the bare word, since both pages mention AuthGuard in a comment explaining its removal. Mutation-checked: re-adding/var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:3752:1: include error: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' [1m[31m╭──────────────────────────────────────────────────────────────╮[0m [1m[31m│[0m [1m[41m ERROR [0m [31mInclude Error[0m [1m[31m│[0m [1m[31m╰──────────────────────────────────────────────────────────────╯[0m [36m[1mFile:[0m [2mapp.stx[0m [90m(line 3752, col 1)[0m [33m[1mMessage:[0m Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' [34m[1mContext:[0m [2m 3750: @if (tab === 'commits') 3751: > 3752:This branch carries no commits of its own.
3753:storage/framework/auto-imports/actions.tsmodified+4-4 Changes to storage/framework/auto-imports/actions.ts @@ -5,6 +5,10 @@ 5 5 // resolves. Values are paths rather than import thunks on purpose: thunks 6 6 // would make every compilation that touches a name resolve every module. 7 7 export const actions = { 8 'Actions/Auth/LoginAction': '../../../app/Actions/Auth/LoginAction.ts', 9 'Actions/Auth/LogoutAction': '../../../app/Actions/Auth/LogoutAction.ts', 10 'Actions/Auth/RegisterAction': '../../../app/Actions/Auth/RegisterAction.ts', 11 'Actions/Auth/VerifyTwoFactorLoginAction': '../../../app/Actions/Auth/VerifyTwoFactorLoginAction.ts', 8 12 'Actions/NotifyUser': '../../../app/Actions/NotifyUser.ts', 9 13 'Actions/SendWelcomeEmail': '../../../app/Actions/SendWelcomeEmail.ts', 10 14 'Actions/AI/AskAction': '../defaults/app/Actions/AI/AskAction.ts', @@ -17,13 +21,10 @@ export const actions = { 17 21 'Actions/Auth/GenerateRegistrationAction': '../defaults/app/Actions/Auth/GenerateRegistrationAction.ts', 18 22 'Actions/Auth/GenerateTwoFactorSecretAction': '../defaults/app/Actions/Auth/GenerateTwoFactorSecretAction.ts', 19 23 'Actions/Auth/ListTokensAction': '../defaults/app/Actions/Auth/ListTokensAction.ts', 20 'Actions/Auth/LoginAction': '../defaults/app/Actions/Auth/LoginAction.ts', 21 'Actions/Auth/LogoutAction': '../defaults/app/Actions/Auth/LogoutAction.ts', 22 24 'Actions/Auth/LogoutAllAction': '../defaults/app/Actions/Auth/LogoutAllAction.ts', 23 25 'Actions/Auth/MagicLinkConsumeAction': '../defaults/app/Actions/Auth/MagicLinkConsumeAction.ts', 24 26 'Actions/Auth/MagicLinkSendAction': '../defaults/app/Actions/Auth/MagicLinkSendAction.ts', 25 27 'Actions/Auth/RefreshTokenAction': '../defaults/app/Actions/Auth/RefreshTokenAction.ts', 26 'Actions/Auth/RegisterAction': '../defaults/app/Actions/Auth/RegisterAction.ts', 27 28 'Actions/Auth/ResendVerificationAction': '../defaults/app/Actions/Auth/ResendVerificationAction.ts', 28 29 'Actions/Auth/RevokeTokenAction': '../defaults/app/Actions/Auth/RevokeTokenAction.ts', 29 30 'Actions/Auth/SocialCallbackAction': '../defaults/app/Actions/Auth/SocialCallbackAction.ts', @@ -32,7 +33,6 @@ export const actions = { 32 33 'Actions/Auth/VerifyAuthenticationAction': '../defaults/app/Actions/Auth/VerifyAuthenticationAction.ts', 33 34 'Actions/Auth/VerifyEmailAction': '../defaults/app/Actions/Auth/VerifyEmailAction.ts', 34 35 'Actions/Auth/VerifyRegistrationAction': '../defaults/app/Actions/Auth/VerifyRegistrationAction.ts', 35 'Actions/Auth/VerifyTwoFactorLoginAction': '../defaults/app/Actions/Auth/VerifyTwoFactorLoginAction.ts', 36 36 'Actions/Auth/token-request': '../defaults/app/Actions/Auth/token-request.ts', 37 37 'Actions/Billable/CheckoutAction': '../defaults/app/Actions/Billable/CheckoutAction.ts', 38 38 'Actions/Blog/BlogDestroyAction': '../defaults/app/Actions/Blog/BlogDestroyAction.ts', tests/feature/marketing-links.test.tsmodified+4-1 Changes to tests/feature/marketing-links.test.ts @@ -45,7 +45,10 @@ function publicTemplates(): string[] { 45 45 join(views, 'forgot.stx'), 46 46 join(views, 'reset.stx'), 47 47 join(views, 'account.stx'), 48 join(partials, 'AuthGuard.stx'), 48 // AuthGuard.stx is deliberately absent. The client pre-paint guard was 49 // deleted when auth moved to the server-rendered session (437970e): a page 50 // that renders its numbers and hides them in the browser still serves them 51 // to curl. reports-surface.test.ts asserts the server gate that replaced it. 49 52 ] 50 53 51 54 for (const group of ['features', 'use-cases', 'compare']) { tests/feature/reports-surface.test.tsmodified+13-2 Changes to tests/feature/reports-surface.test.ts @@ -85,10 +85,21 @@ describe('the reports surface', () => { 85 85 const source = readFileSync(join(root, page), 'utf8') 86 86 87 87 expect(source).toContain("sessionFrom(typeof cookies !== 'undefined' ? cookies : undefined)") 88 expect(source).toContain(` /var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:3923:13776: include error: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' [1m[31m╭──────────────────────────────────────────────────────────────╮[0m [1m[31m│[0m [1m[41m ERROR [0m [31mInclude Error[0m [1m[31m│[0m [1m[31m╰──────────────────────────────────────────────────────────────╯[0m [36m[1mFile:[0m [2mapp.stx[0m [90m(line 3923, col 13776)[0m [33m[1mMessage:[0m Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' [34m[1mContext:[0m [2m 3921: 3922: > 3923:tests/feature/marketing-links.test.tsmodified+4-1 Changes to tests/feature/marketing-links.test.ts @@ -45,7 +45,10 @@ function publicTemplates(): string[] { 45 45 join(views, 'forgot.stx'), 46 46 join(views, 'reset.stx'), 47 47 join(views, 'account.stx'), 48 join(partials, 'AuthGuard.stx'), 48 // AuthGuard.stx is deliberately absent. The client pre-paint guard was 49 // deleted when auth moved to the server-rendered session (437970e): a page 50 // that renders its numbers and hides them in the browser still serves them 51 // to curl. reports-surface.test.ts asserts the server gate that replaced it. 49 52 ] 50 53 51 54 for (const group of ['features', 'use-cases', 'compare']) { 3924: 3925:tests/feature/reports-surface.test.tsmodified+13-2 Changes to tests/feature/reports-surface.test.ts @@ -85,10 +85,21 @@ describe('the reports surface', () => { 85 85 const source = readFileSync(join(root, page), 'utf8') 86 86 87 87 expect(source).toContain("sessionFrom(typeof cookies !== 'undefined' ? cookies : undefined)") 88 expect(source).toContain(` /var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:4128:45: include error: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' [1m[31m╭──────────────────────────────────────────────────────────────╮[0m [1m[31m│[0m [1m[41m ERROR [0m [31mInclude Error[0m [1m[31m│[0m [1m[31m╰──────────────────────────────────────────────────────────────╯[0m [36m[1mFile:[0m [2mapp.stx[0m [90m(line 4128, col 45)[0m [33m[1mMessage:[0m Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' [34m[1mContext:[0m [2m 4126: 4127: > 4128: 4129:)Suggested reviewers
4130: [0m `89 expect(source).toContain('auth-required') 90 88 // And the data is fetched only for an identified visitor. 91 89 expect(source).toMatch(/if \(!user\)|user \?/) 90 91 // The other half of the claim, and the half that used to be asserted by 92 // requiring /var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:4291:48: include error: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' [1m[31m╭──────────────────────────────────────────────────────────────╮[0m [1m[31m│[0m [1m[41m ERROR [0m [31mInclude Error[0m [1m[31m│[0m [1m[31m╰──────────────────────────────────────────────────────────────╯[0m [36m[1mFile:[0m [2mapp.stx[0m [90m(line 4291, col 48)[0m [33m[1mMessage:[0m Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' [34m[1mContext:[0m [2m 4289: 4290: > 4291: Checks
4292: 4293:No checks are required on this branch.
[0m and an #auth-required element. Both were93 // deleted in 437970e when the pre-paint guard was replaced by the server 94 // session, so asserting them asserted the browser guard was still there. 95 // What matters now is the opposite: that nothing gates in the browser, 96 // because a client-side bounce leaves the markup already rendered. 97 // Matched as the directive and the element, not as the word: both pages 98 // name AuthGuard in a comment explaining that the bounce is gone, and a 99 // bare substring check fails on the prose that documents the fix. 100 expect(source).not.toContain(` /var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:4423:35: include error: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' [1m[31m╭──────────────────────────────────────────────────────────────╮[0m [1m[31m│[0m [1m[41m ERROR [0m [31mInclude Error[0m [1m[31m│[0m [1m[31m╰──────────────────────────────────────────────────────────────╯[0m [36m[1mFile:[0m [2mapp.stx[0m [90m(line 4423, col 35)[0m [33m[1mMessage:[0m Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' [34m[1mContext:[0m [2m 4421: 4422: .pr-previews { padding: 13px 17px; margin-bottom: 14px; } > 4423: .pr-preview { margin: 6px 0 0; display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; font-size: 13px; } 4424: .pr-preview a { word-break: break-all; } 4425: [0m `) 101 expect(source).not.toMatch(/id=["']auth-(?:required|pending)["']/) 102 expect(source).not.toMatch(/<script client[\s\S]*?location\.(?:replace|href)/) 92 103 } 93 104 }) 94 105 [0m `)tests/feature/route-gates.test.tsmodified+1-0 Changes to tests/feature/route-gates.test.ts @@ -42,6 +42,7 @@ const ALLOWED = new Map<string, string>([ 42 42 // credential each one checks IS its gate. 43 43 ['POST /register', 'Creating an account is the act of becoming a user. Rate limited per address and per IP.'], 44 44 ['POST /login', 'Checks the password, which is the gate. Rate limited per address and per IP.'], 45 ['POST /verify-two-factor-login', 'Step two of a sign-in, so it cannot require a session either. Its gate is the pair it demands: a single-use challenge token minted by LoginAction only after the password was already checked, plus a TOTP code verified against that account. Neither alone opens a session, and a spent challenge sends the visitor back to /login.'], 45 46 ['POST /forgot', 'Checks nothing and reveals nothing: answers the same either way, so an address cannot be probed. Rate limited.'], 46 47 ['POST /reset', 'The emailed token is the credential. Single use and expiring.'], 47 48 ['POST /logout', 'Revokes whatever token the caller presents. Presenting someone else\'s is signing them out with a credential already held.'], 89 expect(source).toContain('auth-required') 90 88 // And the data is fetched only for an identified visitor. 91 89 expect(source).toMatch(/if \(!user\)|user \?/) 90 91 // The other half of the claim, and the half that used to be asserted by 92 // requiring [1m[31m╭──────────────────────────────────────────────────────────────╮[0m [1m[31m│[0m [1m[41m ERROR [0m [31mInclude Error[0m [1m[31m│[0m [1m[31m╰──────────────────────────────────────────────────────────────╯[0m [36m[1mFile:[0m [2mapp.stx[0m [33m[1mMessage:[0m Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' and an #auth-required element. Both were 93 // deleted in 437970e when the pre-paint guard was replaced by the server 94 // session, so asserting them asserted the browser guard was still there. 95 // What matters now is the opposite: that nothing gates in the browser, 96 // because a client-side bounce leaves the markup already rendered. 97 // Matched as the directive and the element, not as the word: both pages 98 // name AuthGuard in a comment explaining that the bounce is gone, and a 99 // bare substring check fails on the prose that documents the fix. 100 expect(source).not.toContain(` [1m[31m╭──────────────────────────────────────────────────────────────╮[0m [1m[31m│[0m [1m[41m ERROR [0m [31mInclude Error[0m [1m[31m│[0m [1m[31m╰──────────────────────────────────────────────────────────────╯[0m [36m[1mFile:[0m [2mapp.stx[0m [33m[1mMessage:[0m Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' `) 101 expect(source).not.toMatch(/id=["']auth-(?:required|pending)["']/) 102 expect(source).not.toMatch(/<script client[\s\S]*?location\.(?:replace|href)/) 92 103 } 93 104 }) 94 105 tests/feature/route-gates.test.tsmodified+1-0 Changes to tests/feature/route-gates.test.ts @@ -42,6 +42,7 @@ const ALLOWED = new Map<string, string>([ 42 42 // credential each one checks IS its gate. 43 43 ['POST /register', 'Creating an account is the act of becoming a user. Rate limited per address and per IP.'], 44 44 ['POST /login', 'Checks the password, which is the gate. Rate limited per address and per IP.'], 45 ['POST /verify-two-factor-login', 'Step two of a sign-in, so it cannot require a session either. Its gate is the pair it demands: a single-use challenge token minted by LoginAction only after the password was already checked, plus a TOTP code verified against that account. Neither alone opens a session, and a spent challenge sends the visitor back to /login.'], 45 46 ['POST /forgot', 'Checks nothing and reveals nothing: answers the same either way, so an address cannot be probed. Rate limited.'], 46 47 ['POST /reset', 'The emailed token is the credential. Single use and expiring.'], 47 48 ['POST /logout', 'Revokes whatever token the caller presents. Presenting someone else\'s is signing them out with a credential already held.'],
- marketing-links scanned