Skip to content

Commit 5bda643

Browse files
authored
fix(screenshot): use resolveDriver for session rehydration parity (#456)
* fix(screenshot): use resolveDriver for session rehydration parity * fix(test): type screenshot mock helpers for tsc
1 parent d62f830 commit 5bda643

2 files changed

Lines changed: 134 additions & 8 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { describe, test, expect, jest, beforeEach } from '@jest/globals';
2+
3+
const mockGetDriver = jest.fn((_sessionId?: string): any => null);
4+
const mockSetSession = jest.fn(async () => {});
5+
const mockReadAllPersistedSessions = jest.fn(async (): Promise<any[]> => []);
6+
const mockRemovePersistedSession = jest.fn(async () => {});
7+
const mockAttachToRemoteSession = jest.fn(
8+
async (_opts: any): Promise<any> => ({})
9+
);
10+
const mockGetScreenshot = jest.fn(async () => 'dGVzdA=='); // "test" base64
11+
12+
jest.unstable_mockModule('../../../session-store.js', () => ({
13+
getDriver: mockGetDriver,
14+
setSession: mockSetSession,
15+
}));
16+
17+
jest.unstable_mockModule('../../../persistence.js', () => ({
18+
readAllPersistedSessions: mockReadAllPersistedSessions,
19+
removePersistedSession: mockRemovePersistedSession,
20+
isSessionPersistenceEnabled: jest.fn(() => false),
21+
getPersistenceDir: jest.fn(() => null),
22+
writePersistedSession: jest.fn(async () => {}),
23+
}));
24+
25+
jest.unstable_mockModule('../../../utils/url.js', () => ({
26+
attachToRemoteSession: mockAttachToRemoteSession,
27+
}));
28+
29+
jest.unstable_mockModule('../../../command.js', () => ({
30+
getScreenshot: mockGetScreenshot,
31+
}));
32+
33+
jest.unstable_mockModule('../../../logger.js', () => ({
34+
default: { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
35+
}));
36+
37+
jest.unstable_mockModule('../../../ui/mcp-ui-utils.js', () => ({
38+
createUIResource: jest.fn(() => ({})),
39+
createScreenshotViewerUI: jest.fn(() => ''),
40+
addUIResourceToResponse: jest.fn((response) => response),
41+
}));
42+
43+
const { executeScreenshot } =
44+
await import('../../../tools/interactions/screenshot.js');
45+
46+
function textFromResult(result: {
47+
content: Array<{ type: string; text?: string }>;
48+
isError?: boolean;
49+
}): string | undefined {
50+
const block = result.content[0];
51+
return block && 'text' in block ? block.text : undefined;
52+
}
53+
54+
describe('executeScreenshot resolveDriver', () => {
55+
beforeEach(() => {
56+
mockGetDriver.mockReset();
57+
mockSetSession.mockReset();
58+
mockReadAllPersistedSessions.mockReset();
59+
mockReadAllPersistedSessions.mockResolvedValue([]);
60+
mockRemovePersistedSession.mockReset();
61+
mockAttachToRemoteSession.mockReset();
62+
mockGetScreenshot.mockReset();
63+
mockGetScreenshot.mockResolvedValue('dGVzdA==');
64+
});
65+
66+
test('takes a screenshot when an in-memory driver is available', async () => {
67+
mockGetDriver.mockReturnValue({} as any);
68+
69+
const result = await executeScreenshot({
70+
returnRawBase64: true,
71+
sessionId: 's1',
72+
});
73+
74+
expect(result.isError).toBeFalsy();
75+
expect(result.content[0]).toMatchObject({
76+
type: 'image',
77+
mimeType: 'image/png',
78+
});
79+
expect(mockGetScreenshot).toHaveBeenCalledTimes(1);
80+
});
81+
82+
test('returns no-active-session error when nothing is available to rehydrate', async () => {
83+
mockGetDriver.mockReturnValue(null);
84+
85+
const result = await executeScreenshot({
86+
returnRawBase64: true,
87+
sessionId: 'missing',
88+
});
89+
90+
expect(result.isError).toBe(true);
91+
expect(textFromResult(result)).toMatch(/No active driver session/i);
92+
expect(mockGetScreenshot).not.toHaveBeenCalled();
93+
});
94+
95+
test('rehydrates a persisted attached session before taking a screenshot', async () => {
96+
const remoteClient = {
97+
getTimeouts: jest.fn(async () => ({})),
98+
};
99+
mockGetDriver
100+
.mockReturnValueOnce(null) // first resolveDriver miss
101+
.mockReturnValueOnce({} as any); // after setSession
102+
mockReadAllPersistedSessions.mockResolvedValue([
103+
{
104+
sessionId: 'persisted-1',
105+
remoteServerUrl: 'http://remote:4723',
106+
ownership: 'attached',
107+
platform: 'Android',
108+
automationName: 'UiAutomator2',
109+
deviceName: 'emulator-5554',
110+
capabilities: { platformName: 'Android' },
111+
},
112+
] as any);
113+
mockAttachToRemoteSession.mockResolvedValue(remoteClient);
114+
115+
const result = await executeScreenshot({
116+
returnRawBase64: true,
117+
sessionId: 'persisted-1',
118+
});
119+
120+
expect(result.isError).toBeFalsy();
121+
expect(mockAttachToRemoteSession).toHaveBeenCalledWith({
122+
remoteServerUrl: 'http://remote:4723',
123+
sessionId: 'persisted-1',
124+
capabilities: { platformName: 'Android' },
125+
});
126+
expect(mockSetSession).toHaveBeenCalled();
127+
expect(mockGetScreenshot).toHaveBeenCalledTimes(1);
128+
});
129+
});

src/tools/interactions/screenshot.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import type { FastMCP } from 'fastmcp';
2-
import { getDriver } from '../../session-store.js';
32
import { elementUUIDScheme } from '../../schema.js';
4-
import type { NullableDriverInstance } from '../../session-store.js';
53
import { fs, imageUtil } from '@appium/support';
64
import { join } from 'node:path';
75
import {
@@ -13,16 +11,15 @@ import { getScreenshot } from '../../command.js';
1311
import z from 'zod';
1412
import { resolveScreenshotDir } from '../../utils/paths.js';
1513
import {
14+
resolveDriver,
1615
textResult,
1716
errorResult,
1817
toolErrorMessage,
19-
noActiveDriverSessionResult,
2018
} from '../tool-response.js';
2119

2220
export { resolveScreenshotDir };
2321

2422
export interface ScreenshotDeps {
25-
getDriver: (sessionId?: string) => NullableDriverInstance;
2623
writeFile: (filePath: string, data: Buffer) => Promise<unknown>;
2724
mkdir: (
2825
dirPath: string,
@@ -33,7 +30,6 @@ export interface ScreenshotDeps {
3330
}
3431

3532
const defaultDeps: ScreenshotDeps = {
36-
getDriver,
3733
writeFile: fs.writeFile,
3834
mkdir: async (dirPath) => await fs.mkdirp(dirPath),
3935
resolveScreenshotDir,
@@ -55,10 +51,11 @@ export async function executeScreenshot(opts: {
5551
sessionId,
5652
} = opts;
5753

58-
const driver = deps.getDriver(sessionId);
59-
if (!driver) {
60-
return noActiveDriverSessionResult(sessionId);
54+
const resolved = await resolveDriver(sessionId);
55+
if (!resolved.ok) {
56+
return resolved.result;
6157
}
58+
const { driver } = resolved;
6259

6360
try {
6461
const screenshotBase64 = await getScreenshot(driver, elementId);

0 commit comments

Comments
 (0)