@@ -34,6 +34,70 @@ afterAll(() => {
3434 rmSync ( fixturesDir , { recursive : true , force : true } )
3535} )
3636
37+ describe ( "fetchExistingEnvVars" , ( ) => {
38+ let execSyncMock : ReturnType < typeof vi . fn >
39+
40+ beforeEach ( async ( ) => {
41+ const cp = await import ( "node:child_process" )
42+ execSyncMock = cp . execSync as ReturnType < typeof vi . fn >
43+ } )
44+
45+ afterEach ( ( ) => {
46+ vi . restoreAllMocks ( )
47+ } )
48+
49+ it ( "should return a set of existing env var names" , async ( ) => {
50+ execSyncMock . mockReturnValue (
51+ JSON . stringify ( [
52+ { key : "API_KEY" } ,
53+ { key : "SECRET_TOKEN" } ,
54+ { key : "TOOL_ENDPOINT" } ,
55+ ] ) ,
56+ )
57+
58+ const { fetchExistingEnvVars } = await import ( "../cli/commands/deploy.js" )
59+ const result = fetchExistingEnvVars ( )
60+
61+ expect ( result ) . toBeInstanceOf ( Set )
62+ expect ( result . has ( "API_KEY" ) ) . toBe ( true )
63+ expect ( result . has ( "SECRET_TOKEN" ) ) . toBe ( true )
64+ expect ( result . has ( "TOOL_ENDPOINT" ) ) . toBe ( true )
65+ expect ( result . has ( "NONEXISTENT" ) ) . toBe ( false )
66+ } )
67+
68+ it ( "should return empty set when vercel env ls fails" , async ( ) => {
69+ execSyncMock . mockImplementation ( ( ) => {
70+ throw new Error ( "Not authenticated" )
71+ } )
72+
73+ const { fetchExistingEnvVars } = await import ( "../cli/commands/deploy.js" )
74+ const result = fetchExistingEnvVars ( )
75+
76+ expect ( result ) . toBeInstanceOf ( Set )
77+ expect ( result . size ) . toBe ( 0 )
78+ } )
79+
80+ it ( "should return empty set when vercel env ls returns invalid JSON" , async ( ) => {
81+ execSyncMock . mockReturnValue ( "not valid json" )
82+
83+ const { fetchExistingEnvVars } = await import ( "../cli/commands/deploy.js" )
84+ const result = fetchExistingEnvVars ( )
85+
86+ expect ( result ) . toBeInstanceOf ( Set )
87+ expect ( result . size ) . toBe ( 0 )
88+ } )
89+
90+ it ( "should return empty set for empty array response" , async ( ) => {
91+ execSyncMock . mockReturnValue ( "[]" )
92+
93+ const { fetchExistingEnvVars } = await import ( "../cli/commands/deploy.js" )
94+ const result = fetchExistingEnvVars ( )
95+
96+ expect ( result ) . toBeInstanceOf ( Set )
97+ expect ( result . size ) . toBe ( 0 )
98+ } )
99+ } )
100+
37101describe ( "deploy command (mocked shell)" , ( ) => {
38102 let execSyncMock : ReturnType < typeof vi . fn >
39103 let origCwd : string
@@ -342,4 +406,145 @@ describe("deploy command (mocked shell)", () => {
342406 }
343407 }
344408 } )
409+
410+ it ( "should skip env var prompts for vars already set in Vercel" , async ( ) => {
411+ vi . spyOn ( process , "exit" ) . mockImplementation ( code => {
412+ throw new ExitError ( code as number )
413+ } )
414+ const logSpy = vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => { } )
415+ vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } )
416+
417+ const testDir = setupTestDir ( "skip-existing" , {
418+ envExample : "API_KEY=your-key\nSECRET=your-secret\nTOOL_ENDPOINT=auto\n" ,
419+ manifest : `export const manifest = { name: "my-tool" }` ,
420+ } )
421+
422+ setupExecMock ( {
423+ "vercel --version" : "Vercel CLI 33.0.0" ,
424+ "vercel whoami" : "test-user" ,
425+ "vercel env ls" : JSON . stringify ( [ { key : "API_KEY" } ] ) ,
426+ "vercel env add" : "" ,
427+ "vercel deploy --prod --force" : "https://my-tool-final.vercel.app" ,
428+ "vercel deploy --prod" : "https://my-tool-abc123.vercel.app" ,
429+ } )
430+
431+ const validManifest = {
432+ type : "https://eips.ethereum.org/EIPS/eip-XXXX#tool-manifest-v1" ,
433+ name : "my-tool" ,
434+ description : "A test tool" ,
435+ endpoint : "https://my-tool-final.vercel.app" ,
436+ inputs : { type : "object" , properties : { } } ,
437+ outputs : { type : "object" , properties : { } } ,
438+ creatorAddress : "0xabcdefabcdef1234567890abcdefabcdef123456" ,
439+ }
440+
441+ const originalFetch = globalThis . fetch
442+ globalThis . fetch = vi
443+ . fn ( )
444+ . mockResolvedValue (
445+ new Response ( JSON . stringify ( validManifest ) , { status : 200 } ) ,
446+ )
447+
448+ process . chdir ( testDir )
449+
450+ const origSecret = process . env . SECRET
451+ process . env . SECRET = "secret-value"
452+
453+ try {
454+ const { deployCommand } = await import ( "../cli/commands/deploy.js" )
455+ await deployCommand . parseAsync (
456+ [ "--host" , "vercel" , "--non-interactive" ] ,
457+ { from : "user" } ,
458+ )
459+
460+ expect ( logSpy ) . toHaveBeenCalledWith (
461+ expect . stringContaining ( "API_KEY already set, skipping" ) ,
462+ )
463+
464+ expect ( execSyncMock ) . not . toHaveBeenCalledWith (
465+ expect . stringContaining ( "env add API_KEY" ) ,
466+ expect . anything ( ) ,
467+ )
468+
469+ expect ( logSpy ) . toHaveBeenCalledWith ( expect . stringContaining ( "Set SECRET" ) )
470+ } finally {
471+ globalThis . fetch = originalFetch
472+ if ( origSecret === undefined ) {
473+ delete process . env . SECRET
474+ } else {
475+ process . env . SECRET = origSecret
476+ }
477+ }
478+ } )
479+
480+ it ( "should fall through to prompt flow when vercel env ls fails" , async ( ) => {
481+ vi . spyOn ( process , "exit" ) . mockImplementation ( code => {
482+ throw new ExitError ( code as number )
483+ } )
484+ const logSpy = vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => { } )
485+ vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } )
486+
487+ const testDir = setupTestDir ( "env-ls-fail" , {
488+ envExample : "API_KEY=your-key\nTOOL_ENDPOINT=auto\n" ,
489+ manifest : `export const manifest = { name: "my-tool" }` ,
490+ } )
491+
492+ execSyncMock . mockImplementation ( ( cmd : string ) => {
493+ const cmdStr = String ( cmd )
494+ if ( cmdStr . includes ( "vercel env ls" ) ) throw new Error ( "Network error" )
495+ if ( cmdStr . includes ( "vercel --version" ) ) return "Vercel CLI 33.0.0"
496+ if ( cmdStr . includes ( "vercel whoami" ) ) return "test-user"
497+ if ( cmdStr . includes ( "vercel env add" ) ) return ""
498+ if ( cmdStr . includes ( "vercel deploy --prod --force" ) )
499+ return "https://my-tool-final.vercel.app"
500+ if ( cmdStr . includes ( "vercel deploy --prod" ) )
501+ return "https://my-tool-abc123.vercel.app"
502+ return ""
503+ } )
504+
505+ const validManifest = {
506+ type : "https://eips.ethereum.org/EIPS/eip-XXXX#tool-manifest-v1" ,
507+ name : "my-tool" ,
508+ description : "A test tool" ,
509+ endpoint : "https://my-tool-final.vercel.app" ,
510+ inputs : { type : "object" , properties : { } } ,
511+ outputs : { type : "object" , properties : { } } ,
512+ creatorAddress : "0xabcdefabcdef1234567890abcdefabcdef123456" ,
513+ }
514+
515+ const originalFetch = globalThis . fetch
516+ globalThis . fetch = vi
517+ . fn ( )
518+ . mockResolvedValue (
519+ new Response ( JSON . stringify ( validManifest ) , { status : 200 } ) ,
520+ )
521+
522+ process . chdir ( testDir )
523+
524+ const origApiKey = process . env . API_KEY
525+ process . env . API_KEY = "test-key-value"
526+
527+ try {
528+ const { deployCommand } = await import ( "../cli/commands/deploy.js" )
529+ await deployCommand . parseAsync (
530+ [ "--host" , "vercel" , "--non-interactive" ] ,
531+ { from : "user" } ,
532+ )
533+
534+ expect ( logSpy ) . toHaveBeenCalledWith (
535+ expect . stringContaining ( "Could not fetch existing env vars" ) ,
536+ )
537+
538+ expect ( logSpy ) . toHaveBeenCalledWith (
539+ expect . stringContaining ( "Set API_KEY" ) ,
540+ )
541+ } finally {
542+ globalThis . fetch = originalFetch
543+ if ( origApiKey === undefined ) {
544+ delete process . env . API_KEY
545+ } else {
546+ process . env . API_KEY = origApiKey
547+ }
548+ }
549+ } )
345550} )
0 commit comments