Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions tests/integration/test_api_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
Expand All @@ -37,6 +62,8 @@ export class AdkTsApiServer extends BaseTestServer {
}

async start(): Promise<AdkApiClient> {
assertAdkCliBuilt(CLI_ENTRYPOINT_PATH);

await this.startProcess({
spawnProcess: () => {
return spawn('node', this.getAdkCliArgs(this.params), {
Expand All @@ -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',
Expand Down
59 changes: 59 additions & 0 deletions tests/integration/test_api_server_test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof import('node:fs')>();
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<typeof import('node:child_process')>();
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,
);
});
Loading