Skip to content

Commit 6fd53ba

Browse files
Harden Playwright detach method access in CDP adapter
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent f9961b6 commit 6fd53ba

3 files changed

Lines changed: 73 additions & 1 deletion

File tree

currentState.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,9 @@ HyperAgent exposes a TypeScript SDK for browser automation with three primary pa
215215
- Hardened task error-forwarder listener attach fallback:
216216
- `HyperAgent` now falls back to `errorEmitter.addListener` when `errorEmitter.on` is unavailable/trap-prone during task-forwarder registration.
217217
- Added regression coverage proving in-flight listener wiring still succeeds when `on` getter traps, plus retained warning-path coverage when both `on` and `addListener` getters trap.
218+
- Hardened Playwright CDP session detach method access:
219+
- `PlaywrightSessionAdapter.detach()` now performs trap-safe, receiver-bound `session.detach` method reads/invocation and emits deterministic diagnostics when unavailable.
220+
- Added regressions for `session.detach` getter traps and unavailable detach methods during client disposal.
218221
- Refreshed remaining staged-flow wording in the CDP deep dive around OOPIF discovery to describe current execution-context sync progression without stale "Need Phase 4" phrasing.
219222
- Hardened CDP command dispatch in Playwright session adapter:
220223
- `PlaywrightSessionAdapter.send()` now guards trap-prone `session.send` method reads and wraps sync send failures with sanitized/diagnostic context.

src/cdp/playwright-adapter.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,58 @@ describe("playwright adapter error formatting", () => {
7474
expect(detachWarning?.length ?? 0).toBeLessThan(700);
7575
});
7676

77+
it("surfaces sanitized diagnostics when session.detach getter traps", async () => {
78+
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
79+
const session = {
80+
send: jest.fn().mockResolvedValue({}),
81+
on: jest.fn(),
82+
off: jest.fn(),
83+
get detach() {
84+
throw new Error(`detach getter trap\u0000\n${"x".repeat(10_000)}`);
85+
},
86+
} as unknown as PlaywrightSession;
87+
const page = {
88+
context: () => ({
89+
newCDPSession: jest.fn().mockResolvedValue(session),
90+
}),
91+
once: jest.fn(),
92+
} as unknown as Page;
93+
94+
await getCDPClientForPage(page);
95+
await disposeCDPClientForPage(page);
96+
97+
const detachWarning = warnSpy.mock.calls
98+
.map((call) => String(call[0]))
99+
.find((line) => line.includes("Failed to detach session"));
100+
expect(detachWarning).toBeDefined();
101+
expect(detachWarning).toContain("Failed to read session.detach");
102+
expect(detachWarning).toContain("[truncated");
103+
expect(detachWarning).not.toContain("\u0000");
104+
expect(detachWarning).not.toContain("\n");
105+
});
106+
107+
it("surfaces explicit diagnostics when session.detach is unavailable", async () => {
108+
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
109+
const session = {
110+
send: jest.fn().mockResolvedValue({}),
111+
on: jest.fn(),
112+
off: jest.fn(),
113+
} as unknown as PlaywrightSession;
114+
const page = {
115+
context: () => ({
116+
newCDPSession: jest.fn().mockResolvedValue(session),
117+
}),
118+
once: jest.fn(),
119+
} as unknown as Page;
120+
121+
await getCDPClientForPage(page);
122+
await disposeCDPClientForPage(page);
123+
124+
expect(warnSpy).toHaveBeenCalledWith(
125+
"[CDP][PlaywrightAdapter] Failed to detach session: [CDP][PlaywrightAdapter] session.detach is unavailable"
126+
);
127+
});
128+
77129
it("continues disposing sessions when debug-options lookup traps", async () => {
78130
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
79131
const session = {

src/cdp/playwright-adapter.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,24 @@ class PlaywrightSessionAdapter implements CDPSession {
167167

168168
async detach(): Promise<void> {
169169
try {
170-
await this.session.detach();
170+
let detachMethod: unknown;
171+
try {
172+
detachMethod = (this.session as PlaywrightSession & { detach?: unknown })
173+
.detach;
174+
} catch (error) {
175+
throw new Error(
176+
`[CDP][PlaywrightAdapter] Failed to read session.detach: ${formatPlaywrightAdapterDiagnostic(
177+
error
178+
)}`
179+
);
180+
}
181+
if (typeof detachMethod !== "function") {
182+
throw new Error("[CDP][PlaywrightAdapter] session.detach is unavailable");
183+
}
184+
185+
await (
186+
detachMethod as (this: PlaywrightSession) => Promise<unknown>
187+
).call(this.session);
171188
} catch (error) {
172189
console.warn(
173190
`[CDP][PlaywrightAdapter] Failed to detach session: ${formatPlaywrightAdapterDiagnostic(

0 commit comments

Comments
 (0)