From 91a2a4a00cc90b5e352a5c94b8e707873403d3b5 Mon Sep 17 00:00:00 2001 From: Amaad Martin Date: Mon, 3 Aug 2026 06:21:55 -0700 Subject: [PATCH] Fix: make cli_create_test hermetic against ambient GOOGLE_CLOUD_* env vars getGcpProject()/getGcpRegion() return process.env.GOOGLE_CLOUD_PROJECT / GOOGLE_CLOUD_LOCATION before they ever reach the mocked execSync, so a developer who exports those vars saw "should handle Vertex AI selection with gcloud defaults" fail with their real project id. The node:child_process mock was never the gap; the unstubbed env read was. Pin both vars to undefined in beforeEach and add vi.unstubAllEnvs() to afterEach, mirroring dev/test/cli/cli_deploy_agent_engine_test.ts. Also cover the two previously untested ambient-input paths: env vars winning over the gcloud lookup, and the empty default when the lookup throws. Test-only; dev/src is byte-identical. Coverage of cli_create.ts rises 91.78 -> 93.71 statements and 82.81 -> 86.56 branches. --- dev/test/cli/cli_create_test.ts | 63 +++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/dev/test/cli/cli_create_test.ts b/dev/test/cli/cli_create_test.ts index 0fc37ed95..015091405 100644 --- a/dev/test/cli/cli_create_test.ts +++ b/dev/test/cli/cli_create_test.ts @@ -70,11 +70,16 @@ describe('createAgent', () => { beforeEach(() => { vi.clearAllMocks(); + // createAgent() reads these before shelling out to gcloud, so a + // developer's exported values would shadow the execSync mock below. + vi.stubEnv('GOOGLE_CLOUD_PROJECT', undefined); + vi.stubEnv('GOOGLE_CLOUD_LOCATION', undefined); (isCancel as unknown as Mock).mockReturnValue(false); (listFiles as Mock).mockResolvedValue(['file1', 'file2']); }); afterEach(() => { + vi.unstubAllEnvs(); vi.restoreAllMocks(); }); @@ -222,6 +227,64 @@ describe('createAgent', () => { ); }); + it('should prefer ambient Google Cloud env vars over gcloud config', async () => { + vi.stubEnv('GOOGLE_CLOUD_PROJECT', 'env-project'); + vi.stubEnv('GOOGLE_CLOUD_LOCATION', 'env-region'); + + (select as Mock).mockResolvedValueOnce('gemini-2.5-flash'); + (select as Mock).mockResolvedValueOnce('ts'); + (select as Mock).mockResolvedValueOnce('vertex'); // Backend + + (execSync as Mock).mockReturnValue('gcloud-project\n'); + + (text as Mock).mockResolvedValueOnce('env-project'); + (text as Mock).mockResolvedValueOnce('env-region'); + + await createAgent(getFreshOptions()); + + expect(execSync).not.toHaveBeenCalled(); + expect(text).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Enter the Google Cloud Project ID', + initialValue: 'env-project', + }), + ); + expect(text).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Enter the Google Cloud Region', + initialValue: 'env-region', + }), + ); + }); + + it('should seed empty defaults when the gcloud lookup fails', async () => { + (select as Mock).mockResolvedValueOnce('gemini-2.5-flash'); + (select as Mock).mockResolvedValueOnce('ts'); + (select as Mock).mockResolvedValueOnce('vertex'); // Backend + + (execSync as Mock).mockImplementation(() => { + throw new Error('gcloud: command not found'); + }); + + (text as Mock).mockResolvedValueOnce('manual-project'); + (text as Mock).mockResolvedValueOnce('manual-region'); + + await createAgent(getFreshOptions()); + + expect(text).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Enter the Google Cloud Project ID', + initialValue: '', + }), + ); + expect(text).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Enter the Google Cloud Region', + initialValue: '', + }), + ); + }); + it('should exit without writing files if project prompt is cancelled', async () => { // Mirror clack's contract: only the raw cancel symbol counts as a cancel. (isCancel as unknown as Mock).mockImplementation(