test: clear gateway client broad matchers

This commit is contained in:
Peter Steinberger
2026-05-10 14:08:33 +01:00
parent 7194a89469
commit 4e5980eab4

View File

@@ -144,6 +144,39 @@ function getLatestWs(): MockWebSocket {
return ws;
}
function requireRecord(value: unknown, label: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new Error(`expected ${label} to be an object`);
}
return value as Record<string, unknown>;
}
function expectRecordFields(
value: unknown,
expected: Record<string, unknown>,
label: string,
): Record<string, unknown> {
const record = requireRecord(value, label);
for (const [key, expectedValue] of Object.entries(expected)) {
expect(record[key], `${label}.${key}`).toEqual(expectedValue);
}
return record;
}
async function expectGatewayRequestError(
promise: Promise<unknown>,
expected: Record<string, unknown>,
): Promise<void> {
let rejected: unknown;
try {
await promise;
} catch (error) {
rejected = error;
}
const error = expectRecordFields(rejected, expected, "gateway request error");
expectRecordFields(error.details, { method: "chat.history" }, "gateway request error details");
}
function createClientWithIdentity(
deviceId: string,
onClose: (code: number, reason: string) => void,
@@ -164,12 +197,8 @@ function expectSecurityConnectError(
onConnectError: ReturnType<typeof vi.fn>,
params?: { expectTailscaleHint?: boolean },
) {
expect(onConnectError).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining("SECURITY ERROR"),
}),
);
const error = onConnectError.mock.calls[0]?.[0] as Error;
expect(error.message).toContain("SECURITY ERROR");
expect(error.message).toContain("openclaw doctor --fix");
if (params?.expectTailscaleHint) {
expect(error.message).toContain("Tailscale Serve/Funnel");
@@ -271,12 +300,14 @@ describe("GatewayClient security checks", () => {
expect(onConnectError).not.toHaveBeenCalled();
expect(wsInstances.length).toBe(1);
expect(getLatestWs().options).not.toMatchObject({ agent: expect.any(Object) });
expect((global as Record<string, unknown>)["GLOBAL_AGENT"]).toEqual(
expect.objectContaining({
expect(requireRecord(getLatestWs().options, "websocket options").agent).toBeUndefined();
expectRecordFields(
(global as Record<string, unknown>)["GLOBAL_AGENT"],
{
HTTP_PROXY: "http://127.0.0.1:3128",
HTTPS_PROXY: "http://127.0.0.1:3128",
}),
},
"global agent",
);
client.stop();
});
@@ -299,7 +330,7 @@ describe("GatewayClient security checks", () => {
expect(onConnectError).not.toHaveBeenCalled();
expect(wsInstances.length).toBe(1);
expect(getLatestWs().options).not.toMatchObject({ agent: expect.any(Object) });
expect(requireRecord(getLatestWs().options, "websocket options").agent).toBeUndefined();
} finally {
client.stop();
await stopProxy(handle);
@@ -420,12 +451,11 @@ describe("GatewayClient request errors", () => {
}),
);
await expect(requestPromise).rejects.toMatchObject({
await expectGatewayRequestError(requestPromise, {
name: "GatewayClientRequestError",
gatewayCode: "UNAVAILABLE",
retryable: true,
retryAfterMs: 250,
details: { method: "chat.history" },
});
client.stop();
@@ -711,6 +741,10 @@ describe("GatewayClient connect auth payload", () => {
return parseConnectRequest(ws).params?.auth ?? {};
}
function expectConnectAuthFields(ws: MockWebSocket, expected: Record<string, unknown>): void {
expectRecordFields(connectFrameFrom(ws), expected, "connect auth");
}
function connectScopesFrom(ws: MockWebSocket) {
return parseConnectRequest(ws).params?.scopes ?? [];
}
@@ -823,9 +857,7 @@ describe("GatewayClient connect auth payload", () => {
ws.emitOpen();
emitConnectChallenge(ws);
expect(connectFrameFrom(ws)).toMatchObject({
token: "shared-token",
});
expectConnectAuthFields(ws, { token: "shared-token" });
expect(connectFrameFrom(ws).deviceToken).toBeUndefined();
client.stop();
});
@@ -838,9 +870,7 @@ describe("GatewayClient connect auth payload", () => {
const { ws, connect } = startClientWithEarlyChallenge({ client });
expect(connectFrameFrom(ws)).toMatchObject({
token: "shared-token",
});
expectConnectAuthFields(ws, { token: "shared-token" });
emitHelloOk(ws, connect.id);
client.stop();
});
@@ -857,11 +887,10 @@ describe("GatewayClient connect auth payload", () => {
ws.autoCloseOnClose = false;
client.stop();
await vi.waitFor(() =>
expect(onConnectError).toHaveBeenCalledWith(
expect.objectContaining({ message: "gateway client stopped" }),
),
);
await vi.waitFor(() => {
const error = onConnectError.mock.calls[0]?.[0] as Error | undefined;
expect(error?.message).toBe("gateway client stopped");
});
expect(logDebugMock).toHaveBeenCalledWith(
"gateway connect failed: Error: gateway client stopped",
);
@@ -883,9 +912,7 @@ describe("GatewayClient connect auth payload", () => {
ws.emitOpen();
emitConnectChallenge(ws);
expect(connectFrameFrom(ws)).toMatchObject({
password: "shared-password", // pragma: allowlist secret
});
expectConnectAuthFields(ws, { password: "shared-password" }); // pragma: allowlist secret
expect(connectFrameFrom(ws).token).toBeUndefined();
expect(connectFrameFrom(ws).deviceToken).toBeUndefined();
client.stop();
@@ -903,9 +930,7 @@ describe("GatewayClient connect auth payload", () => {
ws.emitOpen();
emitConnectChallenge(ws);
expect(connectFrameFrom(ws)).toMatchObject({
password: "shared-password", // pragma: allowlist secret
});
expectConnectAuthFields(ws, { password: "shared-password" }); // pragma: allowlist secret
expect(connectFrameFrom(ws).bootstrapToken).toBeUndefined();
expect(connectFrameFrom(ws).token).toBeUndefined();
client.stop();
@@ -925,7 +950,7 @@ describe("GatewayClient connect auth payload", () => {
ws.emitOpen();
emitConnectChallenge(ws);
expect(connectFrameFrom(ws)).toMatchObject({
expectConnectAuthFields(ws, {
token: "stored-device-token",
deviceToken: "stored-device-token",
});
@@ -948,7 +973,7 @@ describe("GatewayClient connect auth payload", () => {
ws.emitOpen();
emitConnectChallenge(ws);
expect(connectFrameFrom(ws)).toMatchObject({
expectConnectAuthFields(ws, {
token: "stored-device-token",
deviceToken: "stored-device-token",
});
@@ -975,14 +1000,16 @@ describe("GatewayClient connect auth payload", () => {
ws.emitOpen();
emitConnectChallenge(ws);
expect(loadDeviceAuthTokenMock).toHaveBeenCalledWith(
expect.objectContaining({
deviceId: expect.any(String),
const loadTokenParams = expectRecordFields(
loadDeviceAuthTokenMock.mock.calls[0]?.[0],
{
role: "operator",
env,
}),
},
"load device token params",
);
expect(connectFrameFrom(ws)).toMatchObject({
expect(loadTokenParams.deviceId).toBeTypeOf("string");
expectConnectAuthFields(ws, {
token: "stored-device-token",
deviceToken: "stored-device-token",
});
@@ -1001,9 +1028,7 @@ describe("GatewayClient connect auth payload", () => {
ws.emitOpen();
emitConnectChallenge(ws);
expect(connectFrameFrom(ws)).toMatchObject({
bootstrapToken: "bootstrap-token",
});
expectConnectAuthFields(ws, { bootstrapToken: "bootstrap-token" });
expect(connectFrameFrom(ws).token).toBeUndefined();
expect(connectFrameFrom(ws).deviceToken).toBeUndefined();
client.stop();
@@ -1025,7 +1050,7 @@ describe("GatewayClient connect auth payload", () => {
ws.emitOpen();
emitConnectChallenge(ws);
expect(connectFrameFrom(ws)).toMatchObject({
expectConnectAuthFields(ws, {
token: "explicit-device-token",
deviceToken: "explicit-device-token",
});
@@ -1048,7 +1073,7 @@ describe("GatewayClient connect auth payload", () => {
ws.emitOpen();
emitConnectChallenge(ws);
expect(connectFrameFrom(ws)).toMatchObject({
expectConnectAuthFields(ws, {
token: "stored-device-token",
deviceToken: "stored-device-token",
});
@@ -1075,10 +1100,14 @@ describe("GatewayClient connect auth payload", () => {
connectId: firstConnect.id,
failureDetails: { code: "AUTH_TOKEN_MISMATCH", canRetryWithDeviceToken: true },
});
expect(retriedAuth).toMatchObject({
token: "shared-token",
deviceToken: "stored-device-token",
});
expectRecordFields(
retriedAuth,
{
token: "shared-token",
deviceToken: "stored-device-token",
},
"retried connect auth",
);
const ws = getLatestWs();
expect(connectScopesFrom(ws)).toEqual(["operator.read"]);
client.stop();
@@ -1097,10 +1126,14 @@ describe("GatewayClient connect auth payload", () => {
connectId: firstConnect.id,
failureDetails: { code: "AUTH_UNAUTHORIZED", recommendedNextStep: "retry_with_device_token" },
});
expect(retriedAuth).toMatchObject({
token: "shared-token",
deviceToken: "stored-device-token",
});
expectRecordFields(
retriedAuth,
{
token: "shared-token",
deviceToken: "stored-device-token",
},
"retried connect auth",
);
client.stop();
});
@@ -1145,10 +1178,12 @@ describe("GatewayClient connect auth payload", () => {
connectId: firstConnect.id,
failureDetails: { code: "AUTH_DEVICE_TOKEN_MISMATCH" },
});
expect(clearDeviceAuthTokenMock).toHaveBeenCalledWith({
deviceId: expect.any(String),
role: "operator",
});
const clearTokenParams = expectRecordFields(
clearDeviceAuthTokenMock.mock.calls[0]?.[0],
{ role: "operator" },
"clear device token params",
);
expect(clearTokenParams.deviceId).toBeTypeOf("string");
expect(onReconnectPaused).toHaveBeenCalledWith({
code: 1008,
reason: "connect failed",