@@ -6,6 +6,7 @@ import { execSync, ExecSyncOptionsWithStringEncoding } from 'node:child_process'
66import { Command } from 'commander' ;
77import { registerCommands , type CliDependencies } from '../src/cliCommands.js' ;
88import { extractUrlFromCurlArguments } from '../src/curl.js' ;
9+ import { hasGraphicalEnvironment } from '../src/playwrightUtils.js' ;
910import { EncryptedStorage } from '../src/encryptedStorage.js' ;
1011import { Config } from '../src/config.js' ;
1112import { Registry } from '../src/registry.js' ;
@@ -77,12 +78,20 @@ interface TestEnv {
7778 LATCHKEY_DISABLE_BROWSER ?: string ;
7879}
7980
80- function runCli ( args : string [ ] , env : TestEnv ) : CliResult {
81- const options : ExecSyncOptionsWithStringEncoding = {
81+ interface RunCliOptions {
82+ removeEnvVars ?: string [ ] ;
83+ }
84+
85+ function runCli ( args : string [ ] , env : TestEnv , options ?: RunCliOptions ) : CliResult {
86+ const keysToRemove = new Set ( options ?. removeEnvVars ?? [ ] ) ;
87+ const baseEnv = Object . fromEntries (
88+ Object . entries ( process . env ) . filter ( ( [ key ] ) => ! keysToRemove . has ( key ) )
89+ ) ;
90+ const execOptions : ExecSyncOptionsWithStringEncoding = {
8291 cwd : join ( __dirname , '..' ) ,
8392 encoding : 'utf-8' ,
8493 env : {
85- ...process . env ,
94+ ...baseEnv ,
8695 LATCHKEY_ENCRYPTION_KEY : TEST_ENCRYPTION_KEY ,
8796 ...env ,
8897 } ,
@@ -93,7 +102,7 @@ function runCli(args: string[], env: TestEnv): CliResult {
93102 if ( ! cliPath ) {
94103 throw new Error ( 'CLI not built' ) ;
95104 }
96- const stdout = execSync ( `node ${ cliPath } ${ args . join ( ' ' ) } ` , options ) ;
105+ const stdout = execSync ( `node ${ cliPath } ${ args . join ( ' ' ) } ` , execOptions ) ;
97106 return { exitCode : 0 , stdout, stderr : '' } ;
98107 } catch ( error ) {
99108 const execError = error as ExecError ;
@@ -163,6 +172,110 @@ describe('extractUrlFromCurlArguments', () => {
163172 } ) ;
164173} ) ;
165174
175+ describe ( 'hasGraphicalEnvironment' , ( ) => {
176+ const originalPlatform = process . platform ;
177+
178+ afterEach ( ( ) => {
179+ Object . defineProperty ( process , 'platform' , { value : originalPlatform } ) ;
180+ } ) ;
181+
182+ it ( 'should return true on non-linux platforms' , ( ) => {
183+ Object . defineProperty ( process , 'platform' , { value : 'darwin' } ) ;
184+ expect ( hasGraphicalEnvironment ( ) ) . toBe ( true ) ;
185+
186+ Object . defineProperty ( process , 'platform' , { value : 'win32' } ) ;
187+ expect ( hasGraphicalEnvironment ( ) ) . toBe ( true ) ;
188+ } ) ;
189+
190+ it ( 'should return true on linux when DISPLAY is set' , ( ) => {
191+ Object . defineProperty ( process , 'platform' , { value : 'linux' } ) ;
192+ const originalDisplay = process . env . DISPLAY ;
193+ const originalWayland = process . env . WAYLAND_DISPLAY ;
194+ try {
195+ process . env . DISPLAY = ':0' ;
196+ delete process . env . WAYLAND_DISPLAY ;
197+ expect ( hasGraphicalEnvironment ( ) ) . toBe ( true ) ;
198+ } finally {
199+ if ( originalDisplay !== undefined ) {
200+ process . env . DISPLAY = originalDisplay ;
201+ } else {
202+ delete process . env . DISPLAY ;
203+ }
204+ if ( originalWayland !== undefined ) {
205+ process . env . WAYLAND_DISPLAY = originalWayland ;
206+ } else {
207+ delete process . env . WAYLAND_DISPLAY ;
208+ }
209+ }
210+ } ) ;
211+
212+ it ( 'should return true on linux when WAYLAND_DISPLAY is set' , ( ) => {
213+ Object . defineProperty ( process , 'platform' , { value : 'linux' } ) ;
214+ const originalDisplay = process . env . DISPLAY ;
215+ const originalWayland = process . env . WAYLAND_DISPLAY ;
216+ try {
217+ delete process . env . DISPLAY ;
218+ process . env . WAYLAND_DISPLAY = 'wayland-0' ;
219+ expect ( hasGraphicalEnvironment ( ) ) . toBe ( true ) ;
220+ } finally {
221+ if ( originalDisplay !== undefined ) {
222+ process . env . DISPLAY = originalDisplay ;
223+ } else {
224+ delete process . env . DISPLAY ;
225+ }
226+ if ( originalWayland !== undefined ) {
227+ process . env . WAYLAND_DISPLAY = originalWayland ;
228+ } else {
229+ delete process . env . WAYLAND_DISPLAY ;
230+ }
231+ }
232+ } ) ;
233+
234+ it ( 'should return false on linux when neither DISPLAY nor WAYLAND_DISPLAY is set' , ( ) => {
235+ Object . defineProperty ( process , 'platform' , { value : 'linux' } ) ;
236+ const originalDisplay = process . env . DISPLAY ;
237+ const originalWayland = process . env . WAYLAND_DISPLAY ;
238+ try {
239+ delete process . env . DISPLAY ;
240+ delete process . env . WAYLAND_DISPLAY ;
241+ expect ( hasGraphicalEnvironment ( ) ) . toBe ( false ) ;
242+ } finally {
243+ if ( originalDisplay !== undefined ) {
244+ process . env . DISPLAY = originalDisplay ;
245+ } else {
246+ delete process . env . DISPLAY ;
247+ }
248+ if ( originalWayland !== undefined ) {
249+ process . env . WAYLAND_DISPLAY = originalWayland ;
250+ } else {
251+ delete process . env . WAYLAND_DISPLAY ;
252+ }
253+ }
254+ } ) ;
255+
256+ it ( 'should return false on linux when DISPLAY is empty string' , ( ) => {
257+ Object . defineProperty ( process , 'platform' , { value : 'linux' } ) ;
258+ const originalDisplay = process . env . DISPLAY ;
259+ const originalWayland = process . env . WAYLAND_DISPLAY ;
260+ try {
261+ process . env . DISPLAY = '' ;
262+ delete process . env . WAYLAND_DISPLAY ;
263+ expect ( hasGraphicalEnvironment ( ) ) . toBe ( false ) ;
264+ } finally {
265+ if ( originalDisplay !== undefined ) {
266+ process . env . DISPLAY = originalDisplay ;
267+ } else {
268+ delete process . env . DISPLAY ;
269+ }
270+ if ( originalWayland !== undefined ) {
271+ process . env . WAYLAND_DISPLAY = originalWayland ;
272+ } else {
273+ delete process . env . WAYLAND_DISPLAY ;
274+ }
275+ }
276+ } ) ;
277+ } ) ;
278+
166279describe ( 'CLI commands with dependency injection' , ( ) => {
167280 let tempDir : string ;
168281 let capturedArgs : string [ ] ;
@@ -1002,6 +1115,38 @@ describe('CLI commands with dependency injection', () => {
10021115 expect ( exitCode ) . toBe ( 1 ) ;
10031116 } ) ;
10041117
1118+ it ( 'should return error when no graphical environment is available' , async ( ) => {
1119+ const storePath = join ( tempDir , 'credentials.json' ) ;
1120+ writeSecureFile ( storePath , '{}' ) ;
1121+
1122+ const originalPlatform = process . platform ;
1123+ const originalDisplay = process . env . DISPLAY ;
1124+ const originalWayland = process . env . WAYLAND_DISPLAY ;
1125+ try {
1126+ Object . defineProperty ( process , 'platform' , { value : 'linux' } ) ;
1127+ delete process . env . DISPLAY ;
1128+ delete process . env . WAYLAND_DISPLAY ;
1129+
1130+ const deps = createMockDependencies ( ) ;
1131+ await runCommand ( [ 'auth' , 'browser' , 'slack' ] , deps ) ;
1132+
1133+ expect ( exitCode ) . toBe ( 1 ) ;
1134+ expect ( errorLogs [ 0 ] ) . toContain ( 'No graphical environment detected' ) ;
1135+ } finally {
1136+ Object . defineProperty ( process , 'platform' , { value : originalPlatform } ) ;
1137+ if ( originalDisplay !== undefined ) {
1138+ process . env . DISPLAY = originalDisplay ;
1139+ } else {
1140+ delete process . env . DISPLAY ;
1141+ }
1142+ if ( originalWayland !== undefined ) {
1143+ process . env . WAYLAND_DISPLAY = originalWayland ;
1144+ } else {
1145+ delete process . env . WAYLAND_DISPLAY ;
1146+ }
1147+ }
1148+ } ) ;
1149+
10051150 it ( 'should suggest set-nocurl when service supports nocurl credentials' , async ( ) => {
10061151 const nocurlService : Service = {
10071152 name : 'nocurl-only' ,
@@ -1845,6 +1990,18 @@ describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
18451990 expect ( result . exitCode ) . toBe ( 1 ) ;
18461991 } ) ;
18471992
1993+ it ( 'should return error for auth browser when no graphical environment is available on linux' , ( ) => {
1994+ // This test only makes sense on Linux where the check is active
1995+ if ( process . platform !== 'linux' ) {
1996+ return ;
1997+ }
1998+ const result = runCli ( [ 'auth' , 'browser' , 'slack' ] , testEnv , {
1999+ removeEnvVars : [ 'DISPLAY' , 'WAYLAND_DISPLAY' ] ,
2000+ } ) ;
2001+ expect ( result . exitCode ) . toBe ( 1 ) ;
2002+ expect ( result . stderr ) . toContain ( 'No graphical environment detected' ) ;
2003+ } ) ;
2004+
18482005 it ( 'should list services as JSON' , ( ) => {
18492006 const result = runCli ( [ 'services' , 'list' ] , testEnv ) ;
18502007 expect ( result . exitCode ) . toBe ( 0 ) ;
0 commit comments