@@ -2,7 +2,7 @@ import { headers } from 'next/headers';
22import  type  {  MockedFunction  }  from  'vitest' ; 
33import  {  afterEach ,  beforeEach ,  describe ,  expect ,  it ,  vi  }  from  'vitest' ; 
44
5- import  {  collectKeylessMetadata ,  formatMetadataHeaders  }  from  '../server/keyless-custom-headers' ; 
5+ import  {  CI_ENV_VARS ,   collectKeylessMetadata ,  formatMetadataHeaders  }  from  '../server/keyless-custom-headers' ; 
66
77// Default mock headers for keyless-custom-headers.ts 
88const  defaultMockHeaders  =  new  Headers ( { 
@@ -64,6 +64,13 @@ interface MockHeaders {
6464  values ( ) : IterableIterator < string > ; 
6565} 
6666
67+ // Helper function to clear all CI environment variables 
68+ function  clearAllCIEnvironmentVariables ( ) : void   { 
69+   CI_ENV_VARS . forEach ( indicator  =>  { 
70+     vi . stubEnv ( indicator ,  undefined ) ; 
71+   } ) ; 
72+ } 
73+ 
6774// Helper function to create custom header mocks for specific tests 
6875function  createMockHeaders ( customHeaders : Record < string ,  string  |  null >  =  { } ) : MockHeaders  { 
6976  const  defaultHeadersObj : Record < string ,  string >  =  { } ; 
@@ -134,6 +141,7 @@ describe('keyless-custom-headers', () => {
134141        xPort : '3000' , 
135142        xProtocol : 'https' , 
136143        xClerkAuthStatus : 'signed-out' , 
144+         isCI : true , 
137145      } ; 
138146
139147      const  result  =  formatMetadataHeaders ( metadata ) ; 
@@ -149,6 +157,7 @@ describe('keyless-custom-headers', () => {
149157      expect ( result . get ( 'Clerk-X-Port' ) ) . toBe ( '3000' ) ; 
150158      expect ( result . get ( 'Clerk-X-Protocol' ) ) . toBe ( 'https' ) ; 
151159      expect ( result . get ( 'Clerk-Auth-Status' ) ) . toBe ( 'signed-out' ) ; 
160+       expect ( result . get ( 'Clerk-Is-CI' ) ) . toBe ( 'true' ) ; 
152161    } ) ; 
153162
154163    it ( 'should handle missing optional fields gracefully' ,  ( )  =>  { 
@@ -159,6 +168,7 @@ describe('keyless-custom-headers', () => {
159168        xPort : '3000' , 
160169        xProtocol : 'https' , 
161170        xClerkAuthStatus : 'signed-out' , 
171+         isCI : false , 
162172        // Missing: nodeVersion, nextVersion, npmConfigUserAgent, port 
163173      } ; 
164174
@@ -177,6 +187,7 @@ describe('keyless-custom-headers', () => {
177187      expect ( result . get ( 'Clerk-Next-Version' ) ) . toBeNull ( ) ; 
178188      expect ( result . get ( 'Clerk-NPM-Config-User-Agent' ) ) . toBeNull ( ) ; 
179189      expect ( result . get ( 'Clerk-Node-Port' ) ) . toBeNull ( ) ; 
190+       expect ( result . get ( 'Clerk-Is-CI' ) ) . toBeNull ( ) ; 
180191    } ) ; 
181192
182193    it ( 'should handle undefined values for optional fields' ,  ( )  =>  { 
@@ -191,6 +202,7 @@ describe('keyless-custom-headers', () => {
191202        xPort : 'test-x-port' , 
192203        xProtocol : 'test-x-protocol' , 
193204        xClerkAuthStatus : 'test-auth-status' , 
205+         isCI : false , 
194206      } ; 
195207
196208      const  result  =  formatMetadataHeaders ( metadata ) ; 
@@ -208,6 +220,7 @@ describe('keyless-custom-headers', () => {
208220      expect ( result . get ( 'Clerk-X-Port' ) ) . toBe ( 'test-x-port' ) ; 
209221      expect ( result . get ( 'Clerk-X-Protocol' ) ) . toBe ( 'test-x-protocol' ) ; 
210222      expect ( result . get ( 'Clerk-Auth-Status' ) ) . toBe ( 'test-auth-status' ) ; 
223+       expect ( result . get ( 'Clerk-Is-CI' ) ) . toBeNull ( ) ; 
211224    } ) ; 
212225
213226    it ( 'should handle empty string values' ,  ( )  =>  { 
@@ -222,6 +235,7 @@ describe('keyless-custom-headers', () => {
222235        xPort : '' , 
223236        xProtocol : '' , 
224237        xClerkAuthStatus : '' , 
238+         isCI : false , 
225239      } ; 
226240
227241      const  result  =  formatMetadataHeaders ( metadata ) ; 
@@ -237,6 +251,7 @@ describe('keyless-custom-headers', () => {
237251      expect ( result . get ( 'Clerk-X-Port' ) ) . toBeNull ( ) ; 
238252      expect ( result . get ( 'Clerk-X-Protocol' ) ) . toBeNull ( ) ; 
239253      expect ( result . get ( 'Clerk-Auth-Status' ) ) . toBeNull ( ) ; 
254+       expect ( result . get ( 'Clerk-Is-CI' ) ) . toBeNull ( ) ; 
240255    } ) ; 
241256  } ) ; 
242257
@@ -274,6 +289,9 @@ describe('keyless-custom-headers', () => {
274289    } ) ; 
275290
276291    it ( 'should collect metadata with all fields present' ,  async  ( )  =>  { 
292+       // Clear all CI environment variables first to ensure clean test state 
293+       clearAllCIEnvironmentVariables ( ) ; 
294+ 
277295      // Setup environment variables 
278296      vi . stubEnv ( 'PORT' ,  '3000' ) ; 
279297      vi . stubEnv ( 'npm_config_user_agent' ,  'npm/9.8.1 node/v18.17.0 darwin x64' ) ; 
@@ -334,6 +352,7 @@ describe('keyless-custom-headers', () => {
334352        xHost : 'example.com' , 
335353        xProtocol : 'https' , 
336354        xClerkAuthStatus : 'signed-out' , 
355+         isCI : false , 
337356      } ) ; 
338357
339358      // Restore original values 
@@ -419,9 +438,96 @@ describe('keyless-custom-headers', () => {
419438      expect ( result . xHost ) . toBe ( 'forwarded-test-host.example.com' ) ; 
420439      expect ( result . xProtocol ) . toBe ( 'https' ) ; 
421440    } ) ; 
441+ 
442+     it ( 'should detect CI environment with truthy values' ,  async  ( )  =>  { 
443+       const  truthyValues  =  [ '1' ,  'true' ,  '0.1' ] ; 
444+       const  ciPlatforms  =  [ 'CI' ,  'GITHUB_ACTIONS' ,  'VERCEL' ] ; 
445+ 
446+       for  ( const  platform  of  ciPlatforms )  { 
447+         for  ( const  value  of  truthyValues )  { 
448+           // Clear all environment variables first to prevent any CI leakage 
449+           vi . unstubAllEnvs ( ) ; 
450+           // Explicitly clear all known CI environment variables 
451+           clearAllCIEnvironmentVariables ( ) ; 
452+           // Then stub only the current platform with the test value 
453+           vi . stubEnv ( platform ,  value ) ; 
454+ 
455+           // Recreate headers mock for each iteration to avoid state pollution 
456+           mockHeaders . mockImplementation ( async  ( )  =>  createMockHeaders ( ) ) ; 
457+ 
458+           const  result  =  await  collectKeylessMetadata ( ) ; 
459+           expect ( result . isCI ) . toBe ( true ) ; 
460+         } 
461+       } 
462+     } ) ; 
463+ 
464+     it ( 'should not detect CI environment with falsy values' ,  async  ( )  =>  { 
465+       const  falsyValues  =  [ '0' ,  'false' ,  '' ] ; 
466+       const  ciPlatforms  =  [ 'CI' ,  'GITHUB_ACTIONS' ] ; 
467+ 
468+       for  ( const  platform  of  ciPlatforms )  { 
469+         for  ( const  value  of  falsyValues )  { 
470+           // Clear all environment variables first to prevent any CI leakage 
471+           vi . unstubAllEnvs ( ) ; 
472+           // Explicitly clear all known CI environment variables 
473+           clearAllCIEnvironmentVariables ( ) ; 
474+           // Then stub only the current platform with the test value 
475+           vi . stubEnv ( platform ,  value ) ; 
476+ 
477+           // Recreate headers mock for each iteration to avoid state pollution 
478+           mockHeaders . mockImplementation ( async  ( )  =>  createMockHeaders ( ) ) ; 
479+ 
480+           const  result  =  await  collectKeylessMetadata ( ) ; 
481+           expect ( result . isCI ) . toBe ( false ) ; 
482+         } 
483+       } 
484+     } ) ; 
485+ 
486+     it ( 'should not detect CI environment when no CI indicators are present' ,  async  ( )  =>  { 
487+       // Clear all CI-related environment variables 
488+       clearAllCIEnvironmentVariables ( ) ; 
489+ 
490+       const  result  =  await  collectKeylessMetadata ( ) ; 
491+ 
492+       expect ( result . isCI ) . toBe ( false ) ; 
493+     } ) ; 
494+ 
495+     it ( 'should only add Clerk-Is-CI header when isCI is true' ,  ( )  =>  { 
496+       const  metadataWithCI  =  { 
497+         userAgent : 'test-user-agent' , 
498+         host : 'test-host' , 
499+         xHost : 'test-x-host' , 
500+         xPort : 'test-x-port' , 
501+         xProtocol : 'test-x-protocol' , 
502+         xClerkAuthStatus : 'test-auth-status' , 
503+         isCI : true , 
504+       } ; 
505+ 
506+       const  metadataWithoutCI  =  { 
507+         userAgent : 'test-user-agent' , 
508+         host : 'test-host' , 
509+         xHost : 'test-x-host' , 
510+         xPort : 'test-x-port' , 
511+         xProtocol : 'test-x-protocol' , 
512+         xClerkAuthStatus : 'test-auth-status' , 
513+         isCI : false , 
514+       } ; 
515+ 
516+       const  resultWithCI  =  formatMetadataHeaders ( metadataWithCI ) ; 
517+       const  resultWithoutCI  =  formatMetadataHeaders ( metadataWithoutCI ) ; 
518+ 
519+       // When isCI is true, header should be set to 'true' 
520+       expect ( resultWithCI . get ( 'Clerk-Is-CI' ) ) . toBe ( 'true' ) ; 
521+ 
522+       // When isCI is false, header should not be set 
523+       expect ( resultWithoutCI . get ( 'Clerk-Is-CI' ) ) . toBeNull ( ) ; 
524+     } ) ; 
422525  } ) ; 
423526
424527  it ( 'should format metadata collected from collectKeylessMetadata correctly' ,  async  ( )  =>  { 
528+     // Clear all CI environment variables first to ensure clean test state 
529+     clearAllCIEnvironmentVariables ( ) ; 
530+ 
425531    // Setup environment 
426532    vi . stubEnv ( 'PORT' ,  '4000' ) ; 
427533    vi . stubEnv ( 'npm_config_user_agent' ,  'test-npm-agent' ) ; 
@@ -475,5 +581,67 @@ describe('keyless-custom-headers', () => {
475581    expect ( headers . get ( 'Clerk-X-Protocol' ) ) . toBe ( 'https' ) ; 
476582    expect ( headers . get ( 'Clerk-Auth-Status' ) ) . toBe ( 'integration-status' ) ; 
477583    expect ( headers . get ( 'Clerk-NPM-Config-User-Agent' ) ) . toBe ( 'test-npm-agent' ) ; 
584+     expect ( headers . get ( 'Clerk-Is-CI' ) ) . toBeNull ( ) ;  // Should be null when no CI environment is detected 
585+   } ) ; 
586+ 
587+   it ( 'should format metadata with CI environment detected correctly' ,  async  ( )  =>  { 
588+     // Reset mock to ensure clean state from previous test 
589+     mockHeaders . mockReset ( ) ; 
590+ 
591+     // Setup environment with CI detection 
592+     vi . stubEnv ( 'PORT' ,  '4000' ) ; 
593+     vi . stubEnv ( 'npm_config_user_agent' ,  'test-npm-agent' ) ; 
594+     vi . stubEnv ( 'CI' ,  '1' ) ;  // Set CI environment variable 
595+ 
596+     const  mockHeaderStore  =  new  Headers ( { 
597+       'User-Agent' : 'Integration-Test-Agent' , 
598+       host : 'localhost:4000' , 
599+       'x-forwarded-port' : '4000' , 
600+       'x-forwarded-host' : 'integration-forwarded-host' , 
601+       'x-forwarded-proto' : 'https' , 
602+       'x-clerk-auth-status' : 'integration-status' , 
603+     } ) ; 
604+ 
605+     mockHeaders . mockResolvedValue ( { 
606+       get : ( key : string )  =>  mockHeaderStore . get ( key )  ||  null , 
607+       has : ( key : string )  =>  mockHeaderStore . has ( key ) , 
608+       forEach : ( )  =>  { } , 
609+       entries : function *  ( )  { 
610+         const  headerEntries : [ string ,  string ] [ ]  =  [ ] ; 
611+         mockHeaderStore . forEach ( ( value ,  key )  =>  headerEntries . push ( [ key ,  value ] ) ) ; 
612+         for  ( const  entry  of  headerEntries )  { 
613+           yield  entry ; 
614+         } 
615+       } , 
616+       keys : function *  ( )  { 
617+         const  headerKeys : string [ ]  =  [ ] ; 
618+         mockHeaderStore . forEach ( ( _ ,  key )  =>  headerKeys . push ( key ) ) ; 
619+         for  ( const  key  of  headerKeys )  { 
620+           yield  key ; 
621+         } 
622+       } , 
623+       values : function *  ( )  { 
624+         const  headerValues : string [ ]  =  [ ] ; 
625+         mockHeaderStore . forEach ( value  =>  headerValues . push ( value ) ) ; 
626+         for  ( const  value  of  headerValues )  { 
627+           yield  value ; 
628+         } 
629+       } , 
630+     }  as  MockHeaders ) ; 
631+ 
632+     // Collect metadata and format headers 
633+     const  metadata  =  await  collectKeylessMetadata ( ) ; 
634+     const  headers  =  formatMetadataHeaders ( metadata ) ; 
635+ 
636+     // Verify the full pipeline works correctly with CI detection 
637+     expect ( headers . get ( 'Clerk-Client-User-Agent' ) ) . toBe ( 'Integration-Test-Agent' ) ; 
638+     expect ( headers . get ( 'Clerk-Client-Host' ) ) . toBe ( 'localhost:4000' ) ; 
639+     expect ( headers . get ( 'Clerk-Node-Port' ) ) . toBe ( '4000' ) ; 
640+     expect ( headers . get ( 'Clerk-X-Port' ) ) . toBe ( '4000' ) ; 
641+     expect ( headers . get ( 'Clerk-X-Host' ) ) . toBe ( 'integration-forwarded-host' ) ; 
642+     expect ( headers . get ( 'Clerk-X-Protocol' ) ) . toBe ( 'https' ) ; 
643+     expect ( headers . get ( 'Clerk-Auth-Status' ) ) . toBe ( 'integration-status' ) ; 
644+     expect ( headers . get ( 'Clerk-NPM-Config-User-Agent' ) ) . toBe ( 'test-npm-agent' ) ; 
645+     expect ( headers . get ( 'Clerk-Is-CI' ) ) . toBe ( 'true' ) ;  // Should be 'true' when CI environment is detected 
478646  } ) ; 
479647} ) ; 
0 commit comments