Skip to content

Commit 427ef0a

Browse files
committed
Works
1 parent 521f32f commit 427ef0a

File tree

10 files changed

+156
-52
lines changed

10 files changed

+156
-52
lines changed

scripts/agent-evals/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,40 @@ describe("<prompt-or-tool-name>", function (this: Mocha.Suite) {
7878
});
7979
});
8080
```
81+
82+
## Adding Mocks for MCP Tool
83+
84+
Mocks applied to MCP tools will completely replace their impelementation with a static output string.
85+
86+
1) Add your mocked tools to the `scripts/agent-evals/src/mock/mocks` folder, eg. `scripts/agent-evals/src/mock/mocks/next-js-with-project-mock.ts`
87+
88+
```
89+
import { toMockContent } from "../tool-mock-utils.js";
90+
91+
export const environment_nice_day_mock = {
92+
firebase_get_environment: toMockContent("Tell the user to have a nice day"),
93+
} as const;
94+
```
95+
96+
2) Add the new set of mocked tools to the map:
97+
98+
```
99+
import { environment_nice_day_mock } from "./mocks/next-js-with-project-mock.js";
100+
101+
const allToolMocks = {
102+
// New tool mock
103+
environment_nice_day_mock,
104+
} as const;
105+
```
106+
107+
3) Start using the mock in your test:
108+
109+
Note: If you apply multiple mocks to the same tool, later values in the list will take precedence.
110+
111+
```
112+
const run: AgentTestRunner = await startAgentTest(this, {
113+
templateName: "next-app-hello-world",
114+
// Add the name of your mock here
115+
toolMocks: ["environment_nice_day_mock"],
116+
});
117+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const DEFAULT_FIREBASE_USER = "user@gmail.com";
2+
export const DEFAULT_FIREBASE_PROJECT_NAME = "My Project";
3+
export const DEFAULT_FIREBASE_PROJECT = "my-project";
4+
export const DEFAULT_FIREBASE_PROJECT_NUMBER = "1234321098765";
5+
export const DEFAULT_FIREBASE_WEB_APP_NAME = "My Web App";
6+
export const DEFAULT_FIREBASE_WEB_APP_ID = `1:${DEFAULT_FIREBASE_PROJECT_NUMBER}:web:84d1de6d7ees1e0be7949c`;
7+
export const DEFAULT_FIREBASE_WEB_APP_API_KEY = "aaaaaaaa-ffff-4444-bbbb-ffffffffffff";

scripts/agent-evals/src/mock/mock-tools-main.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@ const mocks = getToolMocks();
2222

2323
const originalRequire = Module.prototype.require;
2424
Module.prototype.require = function (id: string) {
25-
const requiredModule = originalRequire.apply(this, arguments as any);
25+
const requiredModule = originalRequire.apply(this, [id]);
2626
const absolutePath = Module.createRequire(this.filename).resolve(id);
2727
const pathRelativeToCliRoot = path.relative(getFirebaseCliRoot(), absolutePath);
2828
if (!pathRelativeToCliRoot.endsWith(MCP_TOOLS_INDEX_PATH)) {
2929
return requiredModule;
3030
}
3131

32-
logToFile(`Creating proxy implementation for file: ${pathRelativeToCliRoot} with tool mocks: ${JSON.stringify(Object.keys(mocks))}`);
32+
logToFile(
33+
`Creating proxy implementation for file: ${pathRelativeToCliRoot} with tool mocks: ${JSON.stringify(Object.keys(mocks))}`,
34+
);
3335

3436
return new Proxy(requiredModule, {
3537
get(target, prop, receiver) {
Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,66 @@
1+
import {
2+
DEFAULT_FIREBASE_PROJECT,
3+
DEFAULT_FIREBASE_PROJECT_NAME,
4+
DEFAULT_FIREBASE_PROJECT_NUMBER,
5+
DEFAULT_FIREBASE_USER,
6+
DEFAULT_FIREBASE_WEB_APP_ID,
7+
DEFAULT_FIREBASE_WEB_APP_NAME,
8+
DEFAULT_FIREBASE_WEB_APP_API_KEY,
9+
} from "../../data/index.js";
110
import { toMockContent } from "../tool-mock-utils.js";
211

3-
export const next_js_with_project_mock = {
4-
firebase_get_environment: toMockContent("Tell the user to have a nice day"),
12+
export const nextJsWithProjectMock = {
13+
firebase_login: toMockContent(`Successfully logged in as ${DEFAULT_FIREBASE_USER}`),
14+
15+
firebase_get_environment: toMockContent(`# Environment Information
16+
17+
Project Directory:
18+
/Users/samedson/Firebase/firebase-tools/scripts/agent-evals/output/2025-10-24_15-36-06-588Z/-firebase-init-backend-app-2c27e75e3e5d809c/repo
19+
Project Config Path: <NO CONFIG PRESENT>
20+
Active Project ID: ${DEFAULT_FIREBASE_PROJECT}
21+
Gemini in Firebase Terms of Service: Accepted
22+
Authenticated User: ${DEFAULT_FIREBASE_USER}
23+
Detected App IDs: <NONE>
24+
Available Project Aliases (format: '[alias]: [projectId]'): <NONE>
25+
26+
No firebase.json file was found.
27+
28+
If this project does not use Firebase services that require a firebase.json file, no action is necessary.
29+
30+
If this project uses Firebase services that require a firebase.json file, the user will most likely want to:
31+
32+
a) Change the project directory using the 'firebase_update_environment' tool to select a directory with a 'firebase.json' file in it, or
33+
b) Initialize a new Firebase project directory using the 'firebase_init' tool.
34+
35+
Confirm with the user before taking action.`),
36+
37+
firebase_update_environment: toMockContent(
38+
`- Updated active project to '${DEFAULT_FIREBASE_PROJECT}'\n`,
39+
),
40+
41+
firebase_list_projects: toMockContent(`
42+
- projectId: ${DEFAULT_FIREBASE_PROJECT}
43+
projectNumber: '${DEFAULT_FIREBASE_PROJECT_NUMBER}'
44+
displayName: ${DEFAULT_FIREBASE_PROJECT_NAME}
45+
name: projects/${DEFAULT_FIREBASE_PROJECT}
46+
resources:
47+
hostingSite: ${DEFAULT_FIREBASE_PROJECT}
48+
state: ACTIVE
49+
etag: 1_99999999-7777-4444-8888-dddddddddddd
50+
`),
51+
52+
firebase_list_apps: toMockContent(`
53+
- name: 'projects/${DEFAULT_FIREBASE_PROJECT}/webApps/${DEFAULT_FIREBASE_WEB_APP_ID}'
54+
displayName: ${DEFAULT_FIREBASE_WEB_APP_NAME}
55+
platform: WEB
56+
appId: '${DEFAULT_FIREBASE_WEB_APP_ID}'
57+
namespace: 000000000000000000000000000000000000000000000000
58+
apiKeyId: ${DEFAULT_FIREBASE_WEB_APP_API_KEY}
59+
state: ACTIVE
60+
expireTime: '1970-01-01T00:00:00Z'
61+
`),
62+
63+
firebase_get_sdk_config: toMockContent(
64+
`{"projectId":"${DEFAULT_FIREBASE_PROJECT}","appId":"${DEFAULT_FIREBASE_WEB_APP_ID}","storageBucket":"${DEFAULT_FIREBASE_PROJECT}.firebasestorage.app","apiKey":"${DEFAULT_FIREBASE_WEB_APP_API_KEY}","authDomain":"${DEFAULT_FIREBASE_PROJECT}.firebaseapp.com","messagingSenderId":"${DEFAULT_FIREBASE_PROJECT_NUMBER}","projectNumber":"${DEFAULT_FIREBASE_PROJECT_NUMBER}","version":"2"}`,
65+
),
566
} as const;

scripts/agent-evals/src/mock/tool-mocks.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2-
import { next_js_with_project_mock } from "./mocks/next-js-with-project-mock.js";
2+
import { nextJsWithProjectMock } from "./mocks/next-js-with-project-mock.js";
33

44
export type ToolMock = CallToolResult;
55

66
const allToolMocks = {
7-
next_js_with_project_mock,
8-
} as const
7+
nextJsWithProjectMock,
8+
} as const;
99

1010
export type ToolMockName = keyof typeof allToolMocks;
1111

12+
function isToolMockName(name: string): name is ToolMockName {
13+
return name in allToolMocks;
14+
}
15+
1216
export function getToolMocks(): Record<string, ToolMock> {
13-
const mockNames = process.env.MOCKS;
17+
const mockNames = process.env.TOOL_MOCKS;
1418
let mocks = {};
1519
for (const mockName of mockNames?.split(",") || []) {
16-
mocks = {
17-
...mocks,
18-
...allToolMocks[mockName],
20+
if (isToolMockName(mockName)) {
21+
mocks = {
22+
...mocks,
23+
...allToolMocks[mockName], // No more error!
24+
};
25+
} else {
26+
console.error(`Invalid mock name provided: "${mockName}"`);
1927
}
2028
}
2129
return mocks;

scripts/agent-evals/src/runner/gemini-cli-runner.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import fs from "fs";
1212
import { throwFailure } from "./logging.js";
1313
import { getAgentEvalsRoot } from "./paths.js";
1414
import { execSync } from "node:child_process";
15+
import { ToolMockName } from "../mock/tool-mocks.js";
1516

1617
const READY_PROMPT = "Type your message";
1718

@@ -45,6 +46,7 @@ export class GeminiCliRunner implements AgentTestRunner {
4546
private readonly testName: string,
4647
testDir: string,
4748
runDir: string,
49+
toolMocks: ToolMockName[],
4850
) {
4951
// Create a settings file to point the CLI to a local telemetry log
5052
this.telemetryPath = path.join(testDir, "telemetry.log");
@@ -64,6 +66,9 @@ export class GeminiCliRunner implements AgentTestRunner {
6466
firebase: {
6567
command: "node",
6668
args: ["--import", mockPath, firebasePath, "experimental:mcp"],
69+
env: {
70+
TOOL_MOCKS: `${toolMocks?.join(",") || ""}`,
71+
},
6772
},
6873
},
6974
};

scripts/agent-evals/src/runner/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { GeminiCliRunner } from "./gemini-cli-runner.js";
66
import { buildFirebaseCli, clearUserMcpServers } from "./setup.js";
77
import { addCleanup } from "../helpers/cleanup.js";
88
import { TemplateName, copyTemplate, buildTemplates } from "../template/index.js";
9-
import {ToolMockName} from '../mock/tool-mocks.js';
9+
import { ToolMockName } from "../mock/tool-mocks.js";
1010

1111
export * from "./agent-test-runner.js";
1212

@@ -41,7 +41,7 @@ export async function startAgentTest(
4141
copyTemplate(options.templateName, runDir);
4242
}
4343

44-
const run = new GeminiCliRunner(testName, testDir, runDir);
44+
const run = new GeminiCliRunner(testName, testDir, runDir, options?.toolMocks || []);
4545
await run.waitForReadyPrompt();
4646

4747
addCleanup(async () => {

scripts/agent-evals/src/runner/paths.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,10 @@ import path from "path";
22
import { fileURLToPath } from "url";
33

44
export function getAgentEvalsRoot(): string {
5-
let thisFilePath = path.dirname(fileURLToPath(import.meta.url));
5+
const thisFilePath = path.dirname(fileURLToPath(import.meta.url));
66
return path.resolve(path.join(thisFilePath, "..", ".."));
77
}
88

99
export function getFirebaseCliRoot(): string {
10-
return path.resolve(
11-
path.dirname(fileURLToPath(import.meta.url)),
12-
"..",
13-
"..",
14-
"..",
15-
"..",
16-
);
10+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
1711
}

scripts/agent-evals/src/runner/setup.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { exec } from "child_process";
2-
import path from "path";
32
import { promisify } from "util";
4-
import { fileURLToPath } from "url";
53
import { getFirebaseCliRoot } from "./paths.js";
64

75
const execPromise = promisify(exec);
@@ -11,7 +9,7 @@ export async function buildFirebaseCli() {
119
console.log("Skipping Firebase CLI build because process.env.SKIP_REBUILD");
1210
return;
1311
}
14-
const firebaseCliRoot = getFirebaseCliRoot()
12+
const firebaseCliRoot = getFirebaseCliRoot();
1513
console.log(`Building Firebase CLI at ${firebaseCliRoot}`);
1614
await execPromise("./scripts/clean-install.sh", { cwd: firebaseCliRoot });
1715
}
Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,33 @@
11
import { startAgentTest } from "../runner/index.js";
22
import { AgentTestRunner } from "../runner/index.js";
3+
import { DEFAULT_FIREBASE_PROJECT } from "../data/index.js";
34
import "../helpers/hooks.js";
45

56
describe("/firebase:init", function (this: Mocha.Suite) {
67
this.retries(2);
78

8-
// it("backend app", async function (this: Mocha.Context) {
9-
// const run: AgentTestRunner = await startAgentTest(this, {
10-
// templateName: "next-app-hello-world",
11-
// });
12-
13-
// await run.type("/firebase:init");
14-
// await run.expectText("Backend Services");
15-
// await run.expectText("AI Logic");
16-
17-
// await run.type(
18-
// "Build a single page backend app with html and pure javascript. It should say Hello World, but let you login and edit the hello world text for your user",
19-
// );
20-
21-
// await run.type("Yes that looks good. Use Firebase Project gcli-ext-sam-01");
22-
// await run.expectToolCalls([
23-
// "firebase_update_environment",
24-
// {
25-
// name: "firebase_read_resources",
26-
// argumentContains: "firebase://guides/init/backend",
27-
// successIs: true,
28-
// },
29-
// ]);
30-
// });
31-
32-
it.only("WIP firebase_get_environment", async function (this: Mocha.Context) {
9+
it("backend app", async function (this: Mocha.Context) {
3310
const run: AgentTestRunner = await startAgentTest(this, {
3411
templateName: "next-app-hello-world",
35-
toolMocks: ["next_js_with_project_mock"],
12+
toolMocks: ["nextJsWithProjectMock"],
3613
});
3714

38-
await run.type("Run the firebase_get_environment function");
39-
await run.expectToolCalls(["firebase_get_environment"]);
15+
await run.type("/firebase:init");
16+
await run.expectText("Backend Services");
17+
await run.expectText("AI Logic");
18+
19+
await run.type(
20+
"Build a single page backend app with html and pure javascript. It should say Hello World, but let you login and edit the hello world text for your user",
21+
);
22+
23+
await run.type(`Yes that looks good. Use Firebase Project ${DEFAULT_FIREBASE_PROJECT}`);
24+
await run.expectToolCalls([
25+
"firebase_update_environment",
26+
{
27+
name: "firebase_read_resources",
28+
argumentContains: "firebase://guides/init/backend",
29+
successIs: true,
30+
},
31+
]);
4032
});
4133
});

0 commit comments

Comments
 (0)