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
11 changes: 11 additions & 0 deletions packages/agent-cdp/src/__tests__/console.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ function createSession(transport: CdpTransport): RuntimeSession {
sourceUrl: "http://example.test",
} satisfies TargetDescriptor,
transport,
metadata: {
connectedAt: 0,
clockCalibration: {
state: "unavailable",
hostRequestTimeMs: 0,
hostResponseTimeMs: 0,
hostMidpointTimeMs: 0,
roundTripTimeMs: 0,
reason: "not needed in test",
},
},
ensureConnected: () => Promise.resolve(),
close: () => Promise.resolve(),
};
Expand Down
27 changes: 27 additions & 0 deletions packages/agent-cdp/src/__tests__/formatters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,31 @@ describe("formatStatus", () => {
}),
).toContain("session:disconnected");
});

it("renders session calibration details in verbose mode", () => {
expect(
formatStatus(
{
daemonRunning: true,
uptime: 2300,
providerCount: 2,
sessionState: "connected",
selectedTarget: null,
tracingActive: false,
sessionDetails: {
connectedAt: Date.UTC(2026, 0, 2, 3, 4, 5),
clockCalibration: {
state: "unavailable",
hostRequestTimeMs: 1,
hostResponseTimeMs: 3,
hostMidpointTimeMs: 2,
roundTripTimeMs: 2,
reason: "Runtime evaluation failed",
},
},
},
true,
),
).toContain("Session clock: unavailable (Runtime evaluation failed)");
});
});
11 changes: 11 additions & 0 deletions packages/agent-cdp/src/__tests__/network.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ function createSession(transport: CdpTransport): RuntimeSession {
sourceUrl: "http://example.test",
} satisfies TargetDescriptor,
transport,
metadata: {
connectedAt: 0,
clockCalibration: {
state: "unavailable",
hostRequestTimeMs: 0,
hostResponseTimeMs: 0,
hostMidpointTimeMs: 0,
roundTripTimeMs: 0,
reason: "not needed in test",
},
},
ensureConnected: () => Promise.resolve(),
close: () => Promise.resolve(),
};
Expand Down
103 changes: 77 additions & 26 deletions packages/agent-cdp/src/__tests__/session-manager.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { SessionManager } from "../session-manager.js";
import type { CdpEventMessage, CdpTransport, TargetDescriptor, TargetProvider } from "../types.js";
import type { CdpEventMessage, CdpTransport, TargetProvider } from "../types.js";

const CHROME_TEST_ID = "chrome:ZXhhbXBsZS50ZXN0:page-1";
const REACT_NATIVE_TEST_ID = "react-native:ZXhhbXBsZS50ZXN0:page-1";

class FakeTransport implements CdpTransport {
connected = false;
calibrationResult: unknown = {
result: {
value: {
monotonic: 123.45,
timeOrigin: 1_700_000_000_000,
wall: 1_700_000_000_123.45,
},
},
};
readonly sentMethods: string[] = [];

connect(): Promise<void> {
this.connected = true;
Expand All @@ -21,7 +31,11 @@ class FakeTransport implements CdpTransport {
return this.connected;
}

send(): Promise<unknown> {
send(method: string): Promise<unknown> {
this.sentMethods.push(method);
if (method === "Runtime.evaluate") {
return Promise.resolve(this.calibrationResult);
}
return Promise.resolve(undefined);
}

Expand All @@ -32,9 +46,12 @@ class FakeTransport implements CdpTransport {

class FakeProvider implements TargetProvider {
readonly kind = "chrome" as const;
readonly transports: FakeTransport[] = [];

createTransport(): CdpTransport {
return new FakeTransport();
const transport = new FakeTransport();
this.transports.push(transport);
return transport;
}
}

Expand Down Expand Up @@ -72,11 +89,52 @@ describe("SessionManager", () => {
title: "Example",
});
expect(manager.getSessionState()).toBe("connected");
expect(manager.getSession()?.metadata.clockCalibration).toMatchObject({
state: "calibrated",
targetMonotonicTimeMs: 123.45,
targetTimeOriginMs: 1_700_000_000_000,
targetWallTimeMs: 1_700_000_000_123.45,
});
await manager.clearTarget();
expect(manager.getSelectedTarget()).toBeNull();
expect(manager.getSessionState()).toBe("disconnected");
});

it("records explicit unavailable calibration when the target runtime cannot provide one", async () => {
const targets = [
{
id: CHROME_TEST_ID,
rawId: "page-1",
title: "Example",
kind: "chrome" as const,
description: "Test page",
webSocketDebuggerUrl: "ws://example.test/devtools/page/1",
sourceUrl: "http://example.test",
},
];
const provider = new FakeProvider();
const manager = new SessionManager([provider], () => Promise.resolve(targets));
provider.createTransport = () => {
const transport = new FakeTransport();
transport.calibrationResult = {
result: {
value: {
monotonic: null,
},
},
};
provider.transports.push(transport);
return transport;
};

await manager.selectTarget(CHROME_TEST_ID, {});

expect(manager.getSession()?.metadata.clockCalibration).toMatchObject({
state: "unavailable",
reason: "Target runtime did not provide performance.now()",
});
});

it("rejects mismatched explicit urls when selecting a target", async () => {
const targets = [
{
Expand All @@ -99,32 +157,20 @@ describe("SessionManager", () => {
it("reconnects react native targets by logical device id", async () => {
class FakeReactNativeProvider implements TargetProvider {
readonly kind = "react-native" as const;
private attempt = 0;
private transportAttempt = 0;

async listTargets(): Promise<TargetDescriptor[]> {
this.attempt += 1;
return [
{
id: `react-native:ZXhhbXBsZS50ZXN0:page-${this.attempt}`,
rawId: `page-${this.attempt}`,
title: "React Native Experimental",
kind: "react-native",
description: "RN target",
appId: "com.example.app",
webSocketDebuggerUrl: `ws://example.test/inspector/debug?page=${this.attempt}`,
sourceUrl: "http://example.test",
reactNative: {
logicalDeviceId: "device-1",
capabilities: {
nativePageReloads: true,
},
createTransport(): CdpTransport {
this.transportAttempt += 1;
const transport = new FakeTransport();
transport.calibrationResult = {
result: {
value: {
monotonic: this.transportAttempt * 100,
timeOrigin: 1_700_000_000_000,
},
},
];
}

createTransport(): CdpTransport {
return new FakeTransport();
};
return transport;
}
}

Expand Down Expand Up @@ -166,5 +212,10 @@ describe("SessionManager", () => {
rawId: "page-2",
});
expect(manager.getSessionState()).toBe("connected");
expect(manager.getSession()?.metadata.clockCalibration).toMatchObject({
state: "calibrated",
targetMonotonicTimeMs: 200,
targetWallTimeMs: 1_700_000_000_200,
});
});
});
11 changes: 11 additions & 0 deletions packages/agent-cdp/src/__tests__/trace-analysis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ function createTraceSession(transport: CdpTransport): RuntimeSession {
sourceUrl: "http://example.test",
} satisfies TargetDescriptor,
transport,
metadata: {
connectedAt: 0,
clockCalibration: {
state: "unavailable",
hostRequestTimeMs: 0,
hostResponseTimeMs: 0,
hostMidpointTimeMs: 0,
roundTripTimeMs: 0,
reason: "not needed in test",
},
},
ensureConnected: () => Promise.resolve(),
close: () => Promise.resolve(),
};
Expand Down
11 changes: 11 additions & 0 deletions packages/agent-cdp/src/__tests__/trace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ function createTraceSession(transport: CdpTransport): RuntimeSession {
sourceUrl: "http://example.test",
} satisfies TargetDescriptor,
transport,
metadata: {
connectedAt: 0,
clockCalibration: {
state: "unavailable",
hostRequestTimeMs: 0,
hostResponseTimeMs: 0,
hostMidpointTimeMs: 0,
roundTripTimeMs: 0,
reason: "not needed in test",
},
},
ensureConnected: () => Promise.resolve(),
close: () => Promise.resolve(),
};
Expand Down
1 change: 1 addition & 0 deletions packages/agent-cdp/src/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,7 @@ class Daemon {
providerCount: this.providers.length,
sessionState: this.sessionManager.getSessionState(),
tracingActive: this.traceManager.isActive(),
sessionDetails: this.sessionManager.getSession()?.metadata ?? null,
};

return { ok: true, data: status };
Expand Down
8 changes: 8 additions & 0 deletions packages/agent-cdp/src/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ export function formatStatus(info: StatusInfo, verbose = false): string {
} else {
lines.push("Target: none selected");
}
if (info.sessionDetails) {
lines.push(`Connected at: ${new Date(info.sessionDetails.connectedAt).toISOString()}`);
lines.push(
info.sessionDetails.clockCalibration.state === "calibrated"
? `Session clock: calibrated target=${Math.round(info.sessionDetails.clockCalibration.targetMonotonicTimeMs)}ms rtt=${Math.round(info.sessionDetails.clockCalibration.roundTripTimeMs)}ms`
: `Session clock: unavailable (${info.sessionDetails.clockCalibration.reason})`,
);
}
return lines.join("\n");
}

Expand Down
Loading
Loading