diff --git a/tests/integration/test_api_server.ts b/tests/integration/test_api_server.ts index 8b6337d40..55159bfd2 100644 --- a/tests/integration/test_api_server.ts +++ b/tests/integration/test_api_server.ts @@ -5,6 +5,7 @@ */ import {spawn} from 'node:child_process'; +import * as fs from 'node:fs'; import * as path from 'node:path'; import {AdkApiClient} from '../../dev/src/server/adk_api_client.js'; import {BaseTestServer} from './test_case_utils.js'; @@ -24,6 +25,30 @@ export interface TestApiServerParams { const DEFAULT_TIMEOUT = 60000; +/** The built ADK CLI entrypoint that the test server spawns. */ +const CLI_ENTRYPOINT_PATH = path.resolve( + __dirname, + '../../dev/dist/esm/cli_entrypoint.js', +); + +/** + * Throws if the ADK CLI has not been built. + * + * The integration tests spawn the compiled CLI. On an unbuilt checkout the + * child exits 1 with `ERR_MODULE_NOT_FOUND` and the harness reports only + * `CLI exited prematurely with code 1`. This check verifies the entrypoint + * alone, not that every workspace is built. + */ +export function assertAdkCliBuilt(cliPath: string): void { + if (!fs.existsSync(cliPath)) { + throw new Error( + `ADK CLI entrypoint not found at ${cliPath}. ` + + 'The integration tests spawn the built CLI; run `npm run build` from ' + + 'the repository root first.', + ); + } +} + /** * ADK API server for testing via the CLI. This is useful for integration tests * that require an ADK API server to be running. @@ -37,6 +62,8 @@ export class AdkTsApiServer extends BaseTestServer { } async start(): Promise { + assertAdkCliBuilt(CLI_ENTRYPOINT_PATH); + await this.startProcess({ spawnProcess: () => { return spawn('node', this.getAdkCliArgs(this.params), { @@ -56,12 +83,8 @@ export class AdkTsApiServer extends BaseTestServer { } private getAdkCliArgs(params: TestApiServerParams): string[] { - const cliPath = path.resolve( - __dirname, - '../../dev/dist/esm/cli_entrypoint.js', - ); const args = [ - cliPath, + CLI_ENTRYPOINT_PATH, params.serveDebugUI ? 'web' : 'api_server', params.agentsDir, '--port', diff --git a/tests/integration/test_api_server_test.ts b/tests/integration/test_api_server_test.ts new file mode 100644 index 000000000..c5fc62830 --- /dev/null +++ b/tests/integration/test_api_server_test.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as childProcess from 'node:child_process'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import {describe, expect, it, vi} from 'vitest'; +import {AdkTsApiServer, assertAdkCliBuilt} from './test_api_server.js'; + +// Reports the CLI entrypoint as missing so start() takes the unbuilt-checkout +// path on a built tree. Every other path keeps the real answer. +vi.mock('node:fs', async (importActual) => { + const actual = await importActual(); + return { + ...actual, + existsSync: (target: fs.PathLike) => + String(target).endsWith('cli_entrypoint.js') + ? false + : actual.existsSync(target), + }; +}); + +vi.mock('node:child_process', async (importActual) => { + const actual = await importActual(); + return {...actual, spawn: vi.fn(actual.spawn)}; +}); + +const START_BUDGET_MS = 5000; + +describe('assertAdkCliBuilt', () => { + it('names the missing entrypoint and the build command', () => { + const missing = path.resolve(__dirname, 'not_built/cli_entrypoint.js'); + + expect(() => assertAdkCliBuilt(missing)).toThrowError(missing); + expect(() => assertAdkCliBuilt(missing)).toThrowError('npm run build'); + }); + + it('accepts an entrypoint that exists', () => { + const present = path.resolve(__dirname, 'test_api_server.ts'); + + expect(() => assertAdkCliBuilt(present)).not.toThrow(); + }); +}); + +describe('AdkTsApiServer.start', () => { + it( + 'rejects before it spawns the CLI', + async () => { + const server = new AdkTsApiServer({agentsDir: __dirname}); + + await expect(server.start()).rejects.toThrowError('npm run build'); + expect(vi.mocked(childProcess.spawn)).not.toHaveBeenCalled(); + }, + START_BUDGET_MS, + ); +});