* fix(ci): skip unrelated required checks cleanly Matrix jobs had to start no-op runners to preserve interpolated required-check names. Static named jobs let GitHub report genuine skipped conclusions while keeping those names stable. * fix(ci): gate changes to install patching script * fix(ci): route cross-package test contracts narrowly * fix(ci): keep routed contracts merge-blocking * fix(server): exclude nested test utilities from builds * fix(ci): cover app-owned native and browser domains * refactor(ci): align tests with runtime domains Own browser specs, desktop specs, and Electron-only app code by stable directories so PR routing does not depend on filename conventions. Keep desktop integration coverage inside the required desktop check. * fix(ci): keep routing and desktop suites isolated The routing regression must run before dependency installation, and desktop Vitest must not collect Playwright-owned specs. * fix(ci): isolate desktop e2e runtime * perf(test): isolate Playwright workers * perf(test): parallelize Playwright within specs * fix(test): keep Playwright file scheduling * fix(test): share dynamic Metro fixture * fix(test): preserve own-host restart fixtures * fix(ci): preserve routed contracts after rebase * Stabilize Playwright wheel-scroll assertions
87 lines
2.5 KiB
TypeScript
87 lines
2.5 KiB
TypeScript
import type { Page, WebSocketRoute } from "@playwright/test";
|
|
import { daemonWsRoutePattern } from "./daemon-port";
|
|
|
|
type WebSocketMessage = string | Buffer;
|
|
|
|
interface SendAgentMessageRequest {
|
|
type: "send_agent_message_request";
|
|
requestId: string;
|
|
agentId: string;
|
|
}
|
|
|
|
function readSendRequest(message: WebSocketMessage): SendAgentMessageRequest | null {
|
|
if (typeof message !== "string") return null;
|
|
try {
|
|
const envelope = JSON.parse(message) as {
|
|
type?: unknown;
|
|
message?: Record<string, unknown>;
|
|
};
|
|
const request = envelope.type === "session" ? envelope.message : null;
|
|
if (
|
|
request?.type !== "send_agent_message_request" ||
|
|
typeof request.requestId !== "string" ||
|
|
typeof request.agentId !== "string"
|
|
) {
|
|
return null;
|
|
}
|
|
return {
|
|
type: "send_agent_message_request",
|
|
requestId: request.requestId,
|
|
agentId: request.agentId,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function gateNextAgentMessage(page: Page) {
|
|
let serverSocket: WebSocketRoute | null = null;
|
|
let browserSocket: WebSocketRoute | null = null;
|
|
const heldMessages: Array<WebSocketMessage | null> = [];
|
|
const requests: SendAgentMessageRequest[] = [];
|
|
const requestWaiters = new Set<() => void>();
|
|
|
|
await page.routeWebSocket(daemonWsRoutePattern(), (ws) => {
|
|
browserSocket = ws;
|
|
const server = ws.connectToServer();
|
|
serverSocket = server;
|
|
|
|
ws.onMessage((message) => {
|
|
const request = readSendRequest(message);
|
|
if (request) {
|
|
heldMessages.push(message);
|
|
requests.push(request);
|
|
for (const resolve of requestWaiters) resolve();
|
|
requestWaiters.clear();
|
|
return;
|
|
}
|
|
server.send(message);
|
|
});
|
|
|
|
server.onMessage((message) => ws.send(message));
|
|
});
|
|
|
|
const waitForRequest = async (count = 1): Promise<SendAgentMessageRequest> => {
|
|
while (requests.length < count) {
|
|
await new Promise<void>((resolve) => requestWaiters.add(resolve));
|
|
}
|
|
return requests[count - 1];
|
|
};
|
|
|
|
return {
|
|
waitForRequest,
|
|
accept(index = 0) {
|
|
const heldMessage = heldMessages[index];
|
|
if (!serverSocket || !heldMessage) {
|
|
throw new Error("No held send-agent-message request to accept");
|
|
}
|
|
serverSocket.send(heldMessage);
|
|
heldMessages[index] = null;
|
|
},
|
|
async disconnect(): Promise<void> {
|
|
if (!browserSocket) throw new Error("No browser daemon socket to disconnect");
|
|
await browserSocket.close({ code: 1008, reason: "Dropped by submission test." });
|
|
},
|
|
};
|
|
}
|