diff --git a/dev/src/utils/agent_loader.ts b/dev/src/utils/agent_loader.ts index 448a32aeb..9f60fdede 100644 --- a/dev/src/utils/agent_loader.ts +++ b/dev/src/utils/agent_loader.ts @@ -22,7 +22,7 @@ import { isFolderExists, loadFileData, removeFolder, - tryToFindFileRecursively, + tryToFindFolderRecursively, } from './file_utils.js'; import {AdkLogger} from './logger.js'; @@ -33,6 +33,11 @@ const logger = new AdkLogger({label: 'AgentLoader', colorize: {all: true}}); */ const JS_FILES_EXTENSIONS = ['.js', '.cjs', '.mjs', '.ts', '.mts', '.cts']; +/** + * How many ancestor directories to search for a project's `node_modules`. + */ +const MAX_NODE_MODULES_LOOKUP_LEVELS = 10; + /** * Supported JS/TS file module types. */ @@ -640,21 +645,23 @@ async function linkProjectNodeModules( } } +/** + * Find the `node_modules` a bundled agent should resolve its externals from. + * + * Mirrors Node's own upward `node_modules` walk, so a project whose own + * directory has a `package.json` but no `node_modules` — the npm/pnpm + * workspace layout, where dependencies are hoisted to the workspace root — + * still resolves. Returns `undefined` when nothing is found within the bound. + */ async function getProjectNodeModulesDir( sourceDir: string, ): Promise { try { - const packageJsonPath = await tryToFindFileRecursively( + return await tryToFindFolderRecursively( sourceDir, - 'package.json', - 10, - ); - const nodeModulesDir = path.join( - path.dirname(packageJsonPath), 'node_modules', + MAX_NODE_MODULES_LOOKUP_LEVELS, ); - - return (await isFolderExists(nodeModulesDir)) ? nodeModulesDir : undefined; } catch { return undefined; } diff --git a/dev/src/utils/file_utils.ts b/dev/src/utils/file_utils.ts index 43998bcc6..348e8d5b8 100644 --- a/dev/src/utils/file_utils.ts +++ b/dev/src/utils/file_utils.ts @@ -116,35 +116,75 @@ export function getTempDir(prefix?: string): string { return path.join(...pathParts); } -/** - * Try to find a file recursively in the given folder. - * @param sourceFolder The folder to search in. - * @param fileName The name of the file to find. - * @param maxIterations The maximum number of iterations to perform. - * @returns The absolute path of the found file. - * @throws Error if the file is not found after the maximum number of - * iterations. - */ -export async function tryToFindFileRecursively( +/** Walk `sourceFolder` and its ancestors looking for an entry named `entryName`. */ +async function tryToFindEntryRecursively( sourceFolder: string, - fileName: string, + entryName: string, maxIterations: number, + exists: (entryPath: string) => Promise, ): Promise { let currentFolder = sourceFolder; for (let i = 0; i < maxIterations; i++) { - const filePath = path.join(currentFolder, fileName); + const entryPath = path.join(currentFolder, entryName); - if (await isFileExists(filePath)) { - return filePath; + if (await exists(entryPath)) { + return entryPath; } currentFolder = path.dirname(currentFolder); } throw new Error( - `No ${fileName} found in ${ + `No ${entryName} found in ${ sourceFolder } or its parent folders up to ${maxIterations} levels.`, ); } + +/** + * Try to find a file recursively in the given folder. + * @param sourceFolder The folder to search in. + * @param fileName The name of the file to find. + * @param maxIterations The maximum number of iterations to perform. + * @returns The absolute path of the found file. + * @throws Error if the file is not found after the maximum number of + * iterations. + */ +export async function tryToFindFileRecursively( + sourceFolder: string, + fileName: string, + maxIterations: number, +): Promise { + return tryToFindEntryRecursively( + sourceFolder, + fileName, + maxIterations, + isFileExists, + ); +} + +/** + * Try to find a folder recursively in the given folder. + * + * Directories named `folderName` match; a plain file with the same name does + * not, and the walk continues into the parent folder. + * @param sourceFolder The folder to search in. + * @param folderName The name of the folder to find. + * @param maxIterations The maximum number of iterations to perform. + * @returns The absolute path of the found folder. + * @throws Error if the folder is not found after the maximum number of + * iterations. + */ +export async function tryToFindFolderRecursively( + sourceFolder: string, + folderName: string, + maxIterations: number, +): Promise { + return tryToFindEntryRecursively( + sourceFolder, + folderName, + maxIterations, + isFolderExists, + ); +} diff --git a/dev/test/utils/agent_loader_test.ts b/dev/test/utils/agent_loader_test.ts index d53160d9d..5570cb804 100644 --- a/dev/test/utils/agent_loader_test.ts +++ b/dev/test/utils/agent_loader_test.ts @@ -34,7 +34,7 @@ vi.mock('../../src/utils/file_utils.js', () => ({ isFileExists: vi.fn(), isFolderExists: vi.fn(), removeFolder: vi.fn(), - tryToFindFileRecursively: vi.fn(), + tryToFindFolderRecursively: vi.fn(), })); vi.mock('esbuild', async (importOriginal) => { @@ -184,8 +184,8 @@ describe('AgentLoader', () => { (fileUtils.removeFolder as Mock).mockImplementation((folderPath) => fs.rm(folderPath as string, {recursive: true, force: true}), ); - (fileUtils.tryToFindFileRecursively as Mock).mockImplementation( - async (_sourceFolder, fileName) => path.join(tempAgentsDir, fileName), + (fileUtils.tryToFindFolderRecursively as Mock).mockImplementation( + async (_sourceFolder, folderName) => path.join(tempAgentsDir, folderName), ); }); @@ -309,6 +309,54 @@ describe('AgentLoader', () => { await expect(fs.access(compiledAgentPath)).rejects.toThrow(); }); + it('links an ancestor node_modules into the compiled output directory', async () => { + const nestedDir = path.join(tempAgentsDir, 'nested'); + await fs.mkdir(nestedDir, {recursive: true}); + const agentPath = path.join(nestedDir, 'agent1.js'); + await fs.writeFile(agentPath, agent1JsContent); + + const compiledAgentPath = compiledPath('agent1.cjs'); + (esbuild.build as Mock).mockImplementation(async () => { + await fs.writeFile(compiledAgentPath, agent1JsContent); + return Promise.resolve(); + }); + + const agentFile = new AgentFile(agentPath); + await agentFile.load(); + + await expect(fs.readlink(compiledPath('node_modules'))).resolves.toBe( + path.join(tempAgentsDir, 'node_modules'), + ); + expect(fileUtils.tryToFindFolderRecursively).toHaveBeenCalledWith( + nestedDir, + 'node_modules', + 10, + ); + + await agentFile.dispose(); + }); + + it('skips the node_modules link when no ancestor has one', async () => { + (fileUtils.tryToFindFolderRecursively as Mock).mockRejectedValue( + new Error('No node_modules found in /nowhere'), + ); + const agentPath = path.join(tempAgentsDir, 'agent1.js'); + await fs.writeFile(agentPath, agent1JsContent); + + const compiledAgentPath = compiledPath('agent1.cjs'); + (esbuild.build as Mock).mockImplementation(async () => { + await fs.writeFile(compiledAgentPath, agent1JsContent); + return Promise.resolve(); + }); + + const agentFile = new AgentFile(agentPath); + await agentFile.load(); + + await expect(fs.access(compiledPath('node_modules'))).rejects.toThrow(); + + await agentFile.dispose(); + }); + it('throws when getting file path if agent is not loaded', () => { const agentPath = path.join(tempAgentsDir, 'agent1.js'); const agentFile = new AgentFile(agentPath); diff --git a/dev/test/utils/file_utils_test.ts b/dev/test/utils/file_utils_test.ts index 8bd5dfdcd..0be5fed74 100644 --- a/dev/test/utils/file_utils_test.ts +++ b/dev/test/utils/file_utils_test.ts @@ -15,6 +15,7 @@ import { loadFileData, saveToFile, tryToFindFileRecursively, + tryToFindFolderRecursively, } from '../../src/utils/file_utils.js'; vi.mock('node:fs/promises', async () => { @@ -153,6 +154,67 @@ describe('file_utils', () => { ).rejects.toThrow(/No target.txt found/); }); + /** Makes `folderPaths` look like directories and everything else absent. */ + function mockFoldersOnDisk(folderPaths: string[]) { + fsPromises.access.mockImplementation((p: string) => + folderPaths.includes(p) + ? Promise.resolve() + : Promise.reject(new Error('not found')), + ); + fsPromises.stat.mockImplementation((p: string) => + folderPaths.includes(p) + ? Promise.resolve({isDirectory: () => true}) + : Promise.reject(new Error('not found')), + ); + } + + it('tryToFindFolderRecursively finds a folder in the source folder itself', async () => { + const target = path.join('/a/b/c', 'node_modules'); + mockFoldersOnDisk([target]); + + await expect( + tryToFindFolderRecursively('/a/b/c', 'node_modules', 5), + ).resolves.toBe(target); + }); + + it('tryToFindFolderRecursively finds a folder several levels up', async () => { + const target = path.join('/a', 'node_modules'); + mockFoldersOnDisk([target]); + + await expect( + tryToFindFolderRecursively('/a/b/c', 'node_modules', 5), + ).resolves.toBe(target); + }); + + it('tryToFindFolderRecursively skips a same-named file and keeps walking up', async () => { + const decoyFile = path.join('/a/b/c', 'node_modules'); + const target = path.join('/a', 'node_modules'); + fsPromises.access.mockResolvedValue(undefined); + fsPromises.stat.mockImplementation((p: string) => { + if (p === decoyFile) { + return Promise.resolve({isDirectory: () => false}); + } + if (p === target) { + return Promise.resolve({isDirectory: () => true}); + } + return Promise.reject(new Error('not found')); + }); + + await expect( + tryToFindFolderRecursively('/a/b/c', 'node_modules', 5), + ).resolves.toBe(target); + }); + + it('tryToFindFolderRecursively throws when folder not found within maxIterations', async () => { + mockFoldersOnDisk([]); + + await expect( + tryToFindFolderRecursively('/a/b/c', 'node_modules', 2), + ).rejects.toThrow( + 'No node_modules found in /a/b/c or its parent folders up to 2 levels.', + ); + }); + it('listFiles returns entries', async () => { const files = ['a.txt', 'b.txt']; fsPromises.readdir.mockResolvedValue(files); diff --git a/tests/integration/agent_loader/__dirname/package.json b/tests/integration/agent_loader/__dirname/package.json index 08e19b2d6..94601a65e 100644 --- a/tests/integration/agent_loader/__dirname/package.json +++ b/tests/integration/agent_loader/__dirname/package.json @@ -2,10 +2,6 @@ "name": "dirname-test", "version": "1.0.0", "scripts": { - "start": "npx @google/adk-devtools run agent.ts" - }, - "devDependencies": { - "@google/adk-devtools": "file:../../../../dev", - "@google/adk": "file:../../../../core" + "start": "node ../../../../dev/dist/esm/cli_entrypoint.js run agent.ts" } } diff --git a/tests/integration/agent_loader/__filename/package.json b/tests/integration/agent_loader/__filename/package.json index dbf037833..f2891b548 100644 --- a/tests/integration/agent_loader/__filename/package.json +++ b/tests/integration/agent_loader/__filename/package.json @@ -2,10 +2,6 @@ "name": "filename-test", "version": "1.0.0", "scripts": { - "start": "npx @google/adk-devtools run agent.ts" - }, - "devDependencies": { - "@google/adk-devtools": "file:../../../../dev", - "@google/adk": "file:../../../../core" + "start": "node ../../../../dev/dist/esm/cli_entrypoint.js run agent.ts" } } diff --git a/tests/integration/agent_loader/agent_dirname_test.ts b/tests/integration/agent_loader/agent_dirname_test.ts index 1e71a3a1d..058330787 100644 --- a/tests/integration/agent_loader/agent_dirname_test.ts +++ b/tests/integration/agent_loader/agent_dirname_test.ts @@ -4,14 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {exec, spawn} from 'node:child_process'; -import * as fs from 'node:fs/promises'; +import {spawn} from 'node:child_process'; import * as path from 'node:path'; -import {promisify} from 'node:util'; -import {afterAll, beforeAll, describe, expect, it} from 'vitest'; +import {beforeAll, describe, expect, it} from 'vitest'; import {sendInput} from '../test_case_utils.js'; +import {assertWorkspaceAdkCliAvailable} from '../workspace_cli.js'; -const execAsync = promisify(exec); const dirname = process.cwd(); const TEST_EXECUTION_TIMEOUT = 40000; @@ -25,8 +23,8 @@ describe.each(['__dirname', '__filename', 'import_meta_url'])( ); beforeAll(async () => { - await execAsync('npm install', {cwd: projectPath}); - }, TEST_EXECUTION_TIMEOUT); + await assertWorkspaceAdkCliAvailable(); + }); it( 'should run agent and load params from file nearby via package.json script', @@ -45,17 +43,5 @@ describe.each(['__dirname', '__filename', 'import_meta_url'])( }, TEST_EXECUTION_TIMEOUT, ); - - afterAll(async () => { - await fs - .rm(path.join(projectPath, 'node_modules'), { - recursive: true, - force: true, - }) - .catch(() => {}); - await fs - .unlink(path.join(projectPath, 'package-lock.json')) - .catch(() => {}); - }, TEST_EXECUTION_TIMEOUT); }, ); diff --git a/tests/integration/agent_loader/import_meta_url/package.json b/tests/integration/agent_loader/import_meta_url/package.json index 11701374f..a0ad5bea4 100644 --- a/tests/integration/agent_loader/import_meta_url/package.json +++ b/tests/integration/agent_loader/import_meta_url/package.json @@ -3,10 +3,6 @@ "version": "1.0.0", "type": "module", "scripts": { - "start": "npx @google/adk-devtools run agent.ts" - }, - "devDependencies": { - "@google/adk-devtools": "file:../../../../dev", - "@google/adk": "file:../../../../core" + "start": "node ../../../../dev/dist/esm/cli_entrypoint.js run agent.ts" } } diff --git a/tests/integration/app_loader/app_default/package.json b/tests/integration/app_loader/app_default/package.json index 6ae9d8c19..122826c53 100644 --- a/tests/integration/app_loader/app_default/package.json +++ b/tests/integration/app_loader/app_default/package.json @@ -2,10 +2,6 @@ "name": "app-default-test", "version": "1.0.0", "scripts": { - "start": "npx @google/adk-devtools run app.ts" - }, - "devDependencies": { - "@google/adk-devtools": "file:../../../../dev", - "@google/adk": "file:../../../../core" + "start": "node ../../../../dev/dist/esm/cli_entrypoint.js run app.ts" } } diff --git a/tests/integration/app_loader/app_js/package.json b/tests/integration/app_loader/app_js/package.json index d4424f58d..beafca6c4 100644 --- a/tests/integration/app_loader/app_js/package.json +++ b/tests/integration/app_loader/app_js/package.json @@ -2,10 +2,6 @@ "name": "app-js-test", "version": "1.0.0", "scripts": { - "start": "npx @google/adk-devtools run app.js" - }, - "devDependencies": { - "@google/adk-devtools": "file:../../../../dev", - "@google/adk": "file:../../../../core" + "start": "node ../../../../dev/dist/esm/cli_entrypoint.js run app.js" } } diff --git a/tests/integration/app_loader/app_loader_test.ts b/tests/integration/app_loader/app_loader_test.ts index 4367307b8..30657c49a 100644 --- a/tests/integration/app_loader/app_loader_test.ts +++ b/tests/integration/app_loader/app_loader_test.ts @@ -5,15 +5,13 @@ */ import {App, isApp, isBaseAgent} from '@google/adk'; -import {exec, spawn} from 'node:child_process'; -import * as fs from 'node:fs/promises'; +import {spawn} from 'node:child_process'; import * as path from 'node:path'; -import {promisify} from 'node:util'; import {afterAll, beforeAll, describe, expect, it} from 'vitest'; import {AgentLoader} from '../../../dev/src/utils/agent_loader.js'; import {sendInput} from '../test_case_utils.js'; +import {assertWorkspaceAdkCliAvailable} from '../workspace_cli.js'; -const execAsync = promisify(exec); const dirname = process.cwd(); const TEST_EXECUTION_TIMEOUT = 40000; @@ -28,8 +26,8 @@ describe('App loader CLI integration', () => { ); beforeAll(async () => { - await execAsync('npm install', {cwd: projectPath}); - }, TEST_EXECUTION_TIMEOUT); + await assertWorkspaceAdkCliAvailable(); + }); it( 'should run app via package.json start script and get responses', @@ -51,18 +49,6 @@ describe('App loader CLI integration', () => { }, TEST_EXECUTION_TIMEOUT, ); - - afterAll(async () => { - await fs - .rm(path.join(projectPath, 'node_modules'), { - recursive: true, - force: true, - }) - .catch(() => {}); - await fs - .unlink(path.join(projectPath, 'package-lock.json')) - .catch(() => {}); - }, TEST_EXECUTION_TIMEOUT); }, ); }); @@ -75,9 +61,9 @@ describe('AgentLoader discovery and loading integration', () => { let loader: AgentLoader; beforeAll(async () => { - await execAsync('npm install', {cwd: projectPath}); + await assertWorkspaceAdkCliAvailable(); loader = new AgentLoader(projectPath); - }, TEST_EXECUTION_TIMEOUT); + }); it( 'should discover apps vs agents across directories and standalone files', @@ -129,14 +115,5 @@ describe('AgentLoader discovery and loading integration', () => { afterAll(async () => { await loader.disposeAll(); - await fs - .rm(path.join(projectPath, 'node_modules'), { - recursive: true, - force: true, - }) - .catch(() => {}); - await fs - .unlink(path.join(projectPath, 'package-lock.json')) - .catch(() => {}); }, TEST_EXECUTION_TIMEOUT); }); diff --git a/tests/integration/app_loader/app_ts/package.json b/tests/integration/app_loader/app_ts/package.json index f113941d3..e891ecf00 100644 --- a/tests/integration/app_loader/app_ts/package.json +++ b/tests/integration/app_loader/app_ts/package.json @@ -2,10 +2,6 @@ "name": "app-ts-test", "version": "1.0.0", "scripts": { - "start": "npx @google/adk-devtools run app.ts" - }, - "devDependencies": { - "@google/adk-devtools": "file:../../../../dev", - "@google/adk": "file:../../../../core" + "start": "node ../../../../dev/dist/esm/cli_entrypoint.js run app.ts" } } diff --git a/tests/integration/app_loader/discovery/package.json b/tests/integration/app_loader/discovery/package.json index 8b137cf40..152e0553b 100644 --- a/tests/integration/app_loader/discovery/package.json +++ b/tests/integration/app_loader/discovery/package.json @@ -1,8 +1,4 @@ { "name": "discovery-test", - "version": "1.0.0", - "devDependencies": { - "@google/adk-devtools": "file:../../../../dev", - "@google/adk": "file:../../../../core" - } + "version": "1.0.0" } diff --git a/tests/integration/skills/script_js/agent_test.ts b/tests/integration/skills/script_js/agent_test.ts index b17df2c86..aad21f773 100644 --- a/tests/integration/skills/script_js/agent_test.ts +++ b/tests/integration/skills/script_js/agent_test.ts @@ -3,13 +3,12 @@ * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -import {exec, spawn} from 'node:child_process'; +import {spawn} from 'node:child_process'; import * as fs from 'node:fs/promises'; -import {promisify} from 'node:util'; import {afterAll, beforeAll, describe, expect, it} from 'vitest'; import {normalizeLineEndings, sendInput} from '../../test_case_utils.js'; +import {assertWorkspaceAdkCliAvailable} from '../../workspace_cli.js'; -const execAsync = promisify(exec); const dirname = process.cwd(); const PROJECT_PATH = `${dirname}/tests/integration/skills/script_js`; const TEST_EXECUTION_TIMEOUT = 60000; @@ -24,14 +23,14 @@ const TEST_EXECUTION_TIMEOUT = 60000; * 3. Asserts that the agent's response matches the expected output, confirming it claims to have created the art and files. * 4. Verifies that the expected files (`ephemeral_entanglement.md`, `index.html`, `sketch.js`) were actually generated in the file system. * 5. Compares the content of these generated files with reference files in the `expected/` directory to ensure correctness. - * 6. Cleans up the generated files and installed dependencies after execution. + * 6. Cleans up the generated files after execution. * * This test ensures the end-to-end flow of an agent using tools to generate and materialize files based on a high-level request. */ describe('Agent with skills that generates JS script and runs it locally', () => { beforeAll(async () => { - await execAsync('npm install', {cwd: PROJECT_PATH}); - }, TEST_EXECUTION_TIMEOUT); + await assertWorkspaceAdkCliAvailable(); + }); it( 'should run agent with skills successfully', @@ -99,10 +98,5 @@ describe('Agent with skills that generates JS script and runs it locally', () => .catch(() => {}); await fs.rm(`${PROJECT_PATH}/index.html`, {force: true}).catch(() => {}); await fs.rm(`${PROJECT_PATH}/sketch.js`, {force: true}).catch(() => {}); - - await fs - .rm(`${PROJECT_PATH}/node_modules`, {recursive: true, force: true}) - .catch(() => {}); - await fs.unlink(`${PROJECT_PATH}/package-lock.json`).catch(() => {}); }); }); diff --git a/tests/integration/skills/script_js/package.json b/tests/integration/skills/script_js/package.json index 352430b54..015dff40a 100644 --- a/tests/integration/skills/script_js/package.json +++ b/tests/integration/skills/script_js/package.json @@ -3,10 +3,6 @@ "version": "1.0.0", "type": "module", "scripts": { - "start": "npx @google/adk-devtools run agent.ts" - }, - "devDependencies": { - "@google/adk-devtools": "file:../../../../dev", - "@google/adk": "file:../../../../core" + "start": "node ../../../../dev/dist/esm/cli_entrypoint.js run agent.ts" } } diff --git a/tests/integration/workspace_cli.ts b/tests/integration/workspace_cli.ts new file mode 100644 index 000000000..b2bf08c04 --- /dev/null +++ b/tests/integration/workspace_cli.ts @@ -0,0 +1,40 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {access} from 'node:fs/promises'; +import * as path from 'node:path'; + +/** + * The built dev-workspace CLI, relative to the repository root. + * + * Fixtures under `tests/integration` invoke this file directly from their + * `start` script, so they need no `node_modules` of their own. `npm install` + * cannot link `node_modules/.bin/adk` for them: npm skips a workspace bin whose + * target does not exist yet, and the repository builds only after installing. + */ +const DEV_CLI_PATH = path.join('dev', 'dist', 'esm', 'cli_entrypoint.js'); + +/** + * Fails fast when the workspace-root build those fixtures rely on is missing. + * + * Without it the spawned `npm run start` dies before writing anything to + * stdout, and the suite reports an empty-response assertion failure that says + * nothing about the cause. + */ +export async function assertWorkspaceAdkCliAvailable( + workspaceRoot = process.cwd(), +): Promise { + const cliPath = path.join(workspaceRoot, DEV_CLI_PATH); + + try { + await access(cliPath); + } catch (e: unknown) { + throw new Error( + `Missing ${cliPath}. Run \`npm install && npm run build\` at the repository root first.`, + {cause: e}, + ); + } +} diff --git a/tests/integration/workspace_cli_test.ts b/tests/integration/workspace_cli_test.ts new file mode 100644 index 000000000..21d5b594b --- /dev/null +++ b/tests/integration/workspace_cli_test.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'node:fs/promises'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import {afterEach, beforeEach, describe, expect, it} from 'vitest'; +import {assertWorkspaceAdkCliAvailable} from './workspace_cli.js'; + +describe('assertWorkspaceAdkCliAvailable', () => { + let workspaceRoot: string; + + beforeEach(async () => { + workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'workspace-cli-')); + }); + + afterEach(async () => { + await fs.rm(workspaceRoot, {recursive: true, force: true}); + }); + + it('resolves when the dev CLI has been built', async () => { + const cliDir = path.join(workspaceRoot, 'dev', 'dist', 'esm'); + await fs.mkdir(cliDir, {recursive: true}); + await fs.writeFile(path.join(cliDir, 'cli_entrypoint.js'), ''); + + await expect( + assertWorkspaceAdkCliAvailable(workspaceRoot), + ).resolves.toBeUndefined(); + }); + + it('rejects naming the missing CLI path when the workspace is not built', async () => { + await expect(assertWorkspaceAdkCliAvailable(workspaceRoot)).rejects.toThrow( + `Missing ${path.join(workspaceRoot, 'dev', 'dist', 'esm', 'cli_entrypoint.js')}. Run \`npm install && npm run build\` at the repository root first.`, + ); + }); +});