@@ -32,7 +32,8 @@ export interface SmokeSummary {
3232 webserver : string ;
3333 role : EffectiveRole ;
3434 roleSelection : SmokeRunOptions [ 'role' ] ;
35- include ?: string [ ] ;
35+ /** Tags OR-ed onto the smoke selection via `--also-include`. */
36+ alsoInclude ?: string [ ] ;
3637 exclude ?: string [ ] ;
3738 pages ?: string [ ] ;
3839 grep ?: string ;
@@ -56,27 +57,54 @@ export interface SmokeSummary {
5657/**
5758 * Auto-detect role by hitting the webserver's /server/login endpoint.
5859 *
59- * On failure we conservatively default to `'user'` — fewer privileged
60- * specs attempted is safer than running admin-only specs against a
61- * misclassified account.
60+ * On any failure (network error, non-200 status, malformed JSON,
61+ * `authenticated !== true`) we throw. Silently defaulting to `'user'`
62+ * was masking real misconfiguration — operators ended up with a green
63+ * smoke run that had skipped every admin-tagged spec without saying so.
64+ *
65+ * The body shape we POST here (`{ username, password }`) mirrors what
66+ * the SESSION-mode browser fixture submits to `/server/login`. The
67+ * proper signed-request based detection — using the post-login
68+ * `/func/auth/role` endpoint — is tracked in TODO(FR-2878).
6269 */
6370export async function detectRole ( opts : SmokeRunOptions ) : Promise < EffectiveRole > {
6471 const url = `${ opts . webserver . replace ( / \/ $ / , '' ) } /server/login` ;
6572 // Honour --insecure-tls for the detection call so self-signed certs
6673 // don't crash detection before we even reach Playwright.
74+ //
75+ // Note: this temporarily mutates the *current* (host) process env —
76+ // `NODE_TLS_REJECT_UNAUTHORIZED` is a Node-wide knob and there is no
77+ // per-call switch for the built-in `fetch`. We restore the previous
78+ // value (or delete the key if it was unset) in `finally`, so the
79+ // mutation is scoped to the duration of this detection request.
80+ // The same flag is also forwarded to the spawned Playwright process
81+ // via `buildPlaywrightEnv` so the in-browser HTTPS calls inherit it.
6782 const previousTlsReject = process . env . NODE_TLS_REJECT_UNAUTHORIZED ;
6883 if ( opts . insecureTls ) process . env . NODE_TLS_REJECT_UNAUTHORIZED = '0' ;
6984 try {
70- const res = await fetch ( url , {
71- method : 'POST' ,
72- headers : { 'Content-Type' : 'application/json' } ,
73- body : JSON . stringify ( { username : opts . email , password : opts . password } ) ,
74- } ) ;
85+ let res : Response ;
86+ try {
87+ res = await fetch ( url , {
88+ method : 'POST' ,
89+ headers : { 'Content-Type' : 'application/json' } ,
90+ // TODO(FR-2878): switch to signed-request /func/auth/role for a
91+ // proper detection. This naive form matches what the SESSION-mode
92+ // browser fixture submits to /server/login and works against the
93+ // current staging cluster.
94+ body : JSON . stringify ( { username : opts . email , password : opts . password } ) ,
95+ } ) ;
96+ } catch ( err ) {
97+ throw new Error (
98+ `Failed to auto-detect role for ${ opts . email } against ${ opts . webserver } : ` +
99+ `${ ( err as Error ) . message } . Pass --role admin or --role user explicitly and retry.` ,
100+ ) ;
101+ }
75102 if ( ! res . ok ) {
76- process . stderr . write (
77- `[bai-smoke] role detection: webserver returned HTTP ${ res . status } ; defaulting to "user".\n` ,
103+ throw new Error (
104+ `Failed to auto-detect role for ${ opts . email } against ${ opts . webserver } : ` +
105+ `webserver returned HTTP ${ res . status } . ` +
106+ `Pass --role admin or --role user explicitly and retry.` ,
78107 ) ;
79- return 'user' ;
80108 }
81109 const json = ( await res . json ( ) . catch ( ( ) => null ) ) as
82110 | {
@@ -85,22 +113,18 @@ export async function detectRole(opts: SmokeRunOptions): Promise<EffectiveRole>
85113 }
86114 | null ;
87115 if ( ! json || json . authenticated !== true ) {
88- process . stderr . write (
89- '[bai-smoke] role detection: login not authenticated; defaulting to "user".\n' ,
116+ throw new Error (
117+ `Failed to auto-detect role for ${ opts . email } against ${ opts . webserver } : ` +
118+ `login not authenticated (unexpected response shape or wrong credentials). ` +
119+ `Pass --role admin or --role user explicitly and retry.` ,
90120 ) ;
91- return 'user' ;
92121 }
93122 const role = json . data ?. role ;
94123 const isAdmin = json . data ?. is_admin === true ;
95124 if ( role === 'admin' || role === 'superadmin' || isAdmin ) {
96125 return 'admin' ;
97126 }
98127 return 'user' ;
99- } catch ( err ) {
100- process . stderr . write (
101- `[bai-smoke] role detection failed: ${ ( err as Error ) . message } ; defaulting to "user".\n` ,
102- ) ;
103- return 'user' ;
104128 } finally {
105129 if ( opts . insecureTls ) {
106130 if ( previousTlsReject === undefined ) {
@@ -175,7 +199,7 @@ export async function runSmoke(opts: SmokeRunOptions): Promise<SmokeRunResult> {
175199 webserver : opts . webserver ,
176200 role : effectiveRole ,
177201 roleSelection : opts . role ,
178- include : opts . include ,
202+ alsoInclude : opts . include ,
179203 exclude : opts . exclude ,
180204 pages : opts . pages ,
181205 grep,
0 commit comments