chore: Fix types in tests 38/N.
This commit is contained in:
@@ -7,10 +7,15 @@ function createLimiterSpy(): AuthRateLimiter & {
|
|||||||
recordFailure: ReturnType<typeof vi.fn>;
|
recordFailure: ReturnType<typeof vi.fn>;
|
||||||
reset: ReturnType<typeof vi.fn>;
|
reset: ReturnType<typeof vi.fn>;
|
||||||
} {
|
} {
|
||||||
|
const check = vi.fn<AuthRateLimiter["check"]>(
|
||||||
|
(_ip, _scope) => ({ allowed: true, remaining: 10, retryAfterMs: 0 }) as const,
|
||||||
|
);
|
||||||
|
const recordFailure = vi.fn<AuthRateLimiter["recordFailure"]>((_ip, _scope) => {});
|
||||||
|
const reset = vi.fn<AuthRateLimiter["reset"]>((_ip, _scope) => {});
|
||||||
return {
|
return {
|
||||||
check: vi.fn(() => ({ allowed: true, remaining: 10, retryAfterMs: 0 })),
|
check,
|
||||||
recordFailure: vi.fn(),
|
recordFailure,
|
||||||
reset: vi.fn(),
|
reset,
|
||||||
size: () => 0,
|
size: () => 0,
|
||||||
prune: () => {},
|
prune: () => {},
|
||||||
dispose: () => {},
|
dispose: () => {},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type { SessionScope } from "../config/sessions/types.js";
|
||||||
|
|
||||||
const agentCommand = vi.fn();
|
const agentCommand = vi.fn();
|
||||||
|
|
||||||
@@ -14,7 +15,12 @@ const { resolveStorePath } = await import("../config/sessions/paths.js");
|
|||||||
const { loadSessionStore, saveSessionStore } = await import("../config/sessions/store.js");
|
const { loadSessionStore, saveSessionStore } = await import("../config/sessions/store.js");
|
||||||
|
|
||||||
describe("runBootOnce", () => {
|
describe("runBootOnce", () => {
|
||||||
const resolveMainStore = (cfg: { session?: { store?: string } } = {}) => {
|
const resolveMainStore = (
|
||||||
|
cfg: {
|
||||||
|
session?: { store?: string; scope?: SessionScope; mainKey?: string };
|
||||||
|
agents?: { list?: Array<{ id?: string; default?: boolean }> };
|
||||||
|
} = {},
|
||||||
|
) => {
|
||||||
const sessionKey = resolveMainSessionKey(cfg);
|
const sessionKey = resolveMainSessionKey(cfg);
|
||||||
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
||||||
const storePath = resolveStorePath(cfg.session?.store, { agentId });
|
const storePath = resolveStorePath(cfg.session?.store, { agentId });
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { parseModelRef } from "../agents/model-selection.js";
|
|||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import { isTruthyEnvValue } from "../infra/env.js";
|
import { isTruthyEnvValue } from "../infra/env.js";
|
||||||
import { getFreePortBlockWithPermissionFallback } from "../test-utils/ports.js";
|
import { getFreePortBlockWithPermissionFallback } from "../test-utils/ports.js";
|
||||||
|
import { GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||||
import { GatewayClient } from "./client.js";
|
import { GatewayClient } from "./client.js";
|
||||||
import { renderCatNoncePngBase64 } from "./live-image-probe.js";
|
import { renderCatNoncePngBase64 } from "./live-image-probe.js";
|
||||||
import { startGatewayServer } from "./server.js";
|
import { startGatewayServer } from "./server.js";
|
||||||
@@ -144,7 +145,7 @@ async function connectClient(params: { url: string; token: string }) {
|
|||||||
const client = new GatewayClient({
|
const client = new GatewayClient({
|
||||||
url: params.url,
|
url: params.url,
|
||||||
token: params.token,
|
token: params.token,
|
||||||
clientName: "vitest-live-cli-backend",
|
clientName: GATEWAY_CLIENT_NAMES.TEST,
|
||||||
clientVersion: "dev",
|
clientVersion: "dev",
|
||||||
mode: "test",
|
mode: "test",
|
||||||
onHelloOk: () => stop(undefined, client),
|
onHelloOk: () => stop(undefined, client),
|
||||||
|
|||||||
@@ -36,11 +36,10 @@ describe("GatewayClient", () => {
|
|||||||
wsMockState.last = null;
|
wsMockState.last = null;
|
||||||
const client = new GatewayClient({ url: "ws://127.0.0.1:1" });
|
const client = new GatewayClient({ url: "ws://127.0.0.1:1" });
|
||||||
client.start();
|
client.start();
|
||||||
|
const last = wsMockState.last as { url: unknown; opts: unknown } | null;
|
||||||
|
|
||||||
expect(wsMockState.last?.url).toBe("ws://127.0.0.1:1");
|
expect(last?.url).toBe("ws://127.0.0.1:1");
|
||||||
expect(wsMockState.last?.opts).toEqual(
|
expect(last?.opts).toEqual(expect.objectContaining({ maxPayload: 25 * 1024 * 1024 }));
|
||||||
expect.objectContaining({ maxPayload: 25 * 1024 * 1024 }),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -153,7 +152,8 @@ describe("late-arriving invoke results", () => {
|
|||||||
context,
|
context,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [ok, payload, error] = respond.mock.lastCall ?? [];
|
const [ok, rawPayload, error] = respond.mock.lastCall ?? [];
|
||||||
|
const payload = rawPayload as { ok?: boolean; ignored?: boolean } | undefined;
|
||||||
|
|
||||||
// Late-arriving results return success instead of error to reduce log noise.
|
// Late-arriving results return success instead of error to reduce log noise.
|
||||||
expect(ok).toBe(true);
|
expect(ok).toBe(true);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { getApiKeyForModel } from "../agents/model-auth.js";
|
|||||||
import { ensureOpenClawModelsJson } from "../agents/models-config.js";
|
import { ensureOpenClawModelsJson } from "../agents/models-config.js";
|
||||||
import { discoverAuthStorage, discoverModels } from "../agents/pi-model-discovery.js";
|
import { discoverAuthStorage, discoverModels } from "../agents/pi-model-discovery.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import type { OpenClawConfig, ModelProviderConfig } from "../config/types.js";
|
import type { ModelsConfig, OpenClawConfig, ModelProviderConfig } from "../config/types.js";
|
||||||
import { isTruthyEnvValue } from "../infra/env.js";
|
import { isTruthyEnvValue } from "../infra/env.js";
|
||||||
import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
|
import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
|
||||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||||
@@ -401,6 +401,7 @@ function buildLiveGatewayConfig(params: {
|
|||||||
...providerOverrides,
|
...providerOverrides,
|
||||||
};
|
};
|
||||||
const providers = Object.keys(nextProviders).length > 0 ? nextProviders : baseProviders;
|
const providers = Object.keys(nextProviders).length > 0 ? nextProviders : baseProviders;
|
||||||
|
const baseModels = params.cfg.models;
|
||||||
return {
|
return {
|
||||||
...params.cfg,
|
...params.cfg,
|
||||||
agents: {
|
agents: {
|
||||||
@@ -418,7 +419,9 @@ function buildLiveGatewayConfig(params: {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
models:
|
models:
|
||||||
Object.keys(providers).length > 0 ? { ...params.cfg.models, providers } : params.cfg.models,
|
Object.keys(providers).length > 0
|
||||||
|
? ({ ...baseModels, providers } as ModelsConfig)
|
||||||
|
: baseModels,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1149,10 +1152,10 @@ describeLive("gateway live (dev agent, profile keys)", () => {
|
|||||||
await fs.writeFile(toolProbePath, `nonceA=${nonceA}\nnonceB=${nonceB}\n`);
|
await fs.writeFile(toolProbePath, `nonceA=${nonceA}\nnonceB=${nonceB}\n`);
|
||||||
|
|
||||||
const port = await getFreeGatewayPort();
|
const port = await getFreeGatewayPort();
|
||||||
const server = await startGatewayServer({
|
const server = await startGatewayServer(port, {
|
||||||
configPath: cfg.__meta?.path,
|
bind: "loopback",
|
||||||
port,
|
auth: { mode: "token", token },
|
||||||
token,
|
controlUiEnabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = await connectClient({
|
const client = await connectClient({
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ describe("hooks mapping", () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
expect(result?.ok).toBe(true);
|
expect(result?.ok).toBe(true);
|
||||||
if (result?.ok && result.action.kind === "agent") {
|
if (result?.ok && result.action && result.action.kind === "agent") {
|
||||||
expect(result.action.model).toBe("openai/gpt-4.1-mini");
|
expect(result.action.model).toBe("openai/gpt-4.1-mini");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -126,7 +126,8 @@ function createErrnoError(code: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mockWorkspaceStateRead(params: { onboardingCompletedAt?: string; errorCode?: string }) {
|
function mockWorkspaceStateRead(params: { onboardingCompletedAt?: string; errorCode?: string }) {
|
||||||
mocks.fsReadFile.mockImplementation(async (filePath: string | URL | number) => {
|
mocks.fsReadFile.mockImplementation(async (...args: unknown[]) => {
|
||||||
|
const filePath = args[0];
|
||||||
if (String(filePath).endsWith("workspace-state.json")) {
|
if (String(filePath).endsWith("workspace-state.json")) {
|
||||||
if (params.errorCode) {
|
if (params.errorCode) {
|
||||||
throw createErrnoError(params.errorCode);
|
throw createErrnoError(params.errorCode);
|
||||||
|
|||||||
@@ -107,6 +107,9 @@ describe("chat abort transcript persistence", () => {
|
|||||||
params: { sessionKey: "main", runId },
|
params: { sessionKey: "main", runId },
|
||||||
respond,
|
respond,
|
||||||
context: context as never,
|
context: context as never,
|
||||||
|
req: {} as never,
|
||||||
|
client: null,
|
||||||
|
isWebchatConnect: () => false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [ok1, payload1] = respond.mock.calls.at(-1) ?? [];
|
const [ok1, payload1] = respond.mock.calls.at(-1) ?? [];
|
||||||
@@ -121,6 +124,9 @@ describe("chat abort transcript persistence", () => {
|
|||||||
params: { sessionKey: "main", runId },
|
params: { sessionKey: "main", runId },
|
||||||
respond,
|
respond,
|
||||||
context: context as never,
|
context: context as never,
|
||||||
|
req: {} as never,
|
||||||
|
client: null,
|
||||||
|
isWebchatConnect: () => false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const lines = await readTranscriptLines(transcriptPath);
|
const lines = await readTranscriptLines(transcriptPath);
|
||||||
@@ -178,6 +184,9 @@ describe("chat abort transcript persistence", () => {
|
|||||||
params: { sessionKey: "main" },
|
params: { sessionKey: "main" },
|
||||||
respond,
|
respond,
|
||||||
context: context as never,
|
context: context as never,
|
||||||
|
req: {} as never,
|
||||||
|
client: null,
|
||||||
|
isWebchatConnect: () => false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [ok, payload] = respond.mock.calls.at(-1) ?? [];
|
const [ok, payload] = respond.mock.calls.at(-1) ?? [];
|
||||||
@@ -235,7 +244,9 @@ describe("chat abort transcript persistence", () => {
|
|||||||
},
|
},
|
||||||
respond,
|
respond,
|
||||||
context: context as never,
|
context: context as never,
|
||||||
client: undefined,
|
req: {} as never,
|
||||||
|
client: null,
|
||||||
|
isWebchatConnect: () => false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [ok, payload] = respond.mock.calls.at(-1) ?? [];
|
const [ok, payload] = respond.mock.calls.at(-1) ?? [];
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ describe("gateway chat.inject transcript writes", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
vi.doMock("../session-utils.js", async (importOriginal) => {
|
vi.doMock("../session-utils.js", async (importOriginal) => {
|
||||||
const original = await importOriginal();
|
const original = await importOriginal<typeof import("../session-utils.js")>();
|
||||||
return {
|
return {
|
||||||
...original,
|
...original,
|
||||||
loadSessionEntry: () => ({
|
loadSessionEntry: () => ({
|
||||||
@@ -50,7 +50,10 @@ describe("gateway chat.inject transcript writes", () => {
|
|||||||
await chatHandlers["chat.inject"]({
|
await chatHandlers["chat.inject"]({
|
||||||
params: { sessionKey: "k1", message: "hello" },
|
params: { sessionKey: "k1", message: "hello" },
|
||||||
respond,
|
respond,
|
||||||
context,
|
req: {} as never,
|
||||||
|
client: null as never,
|
||||||
|
isWebchatConnect: () => false,
|
||||||
|
context: context as unknown as GatewayRequestContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(respond).toHaveBeenCalled();
|
expect(respond).toHaveBeenCalled();
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ describe("skills.update", () => {
|
|||||||
skillKey: "brave-search",
|
skillKey: "brave-search",
|
||||||
apiKey: "abc\r\ndef",
|
apiKey: "abc\r\ndef",
|
||||||
},
|
},
|
||||||
|
req: {} as never,
|
||||||
|
client: null as never,
|
||||||
|
isWebchatConnect: () => false,
|
||||||
|
context: {} as never,
|
||||||
respond: (success, _result, err) => {
|
respond: (success, _result, err) => {
|
||||||
ok = success;
|
ok = success;
|
||||||
error = err;
|
error = err;
|
||||||
|
|||||||
@@ -106,7 +106,9 @@ describe("sessions.usage", () => {
|
|||||||
|
|
||||||
expect(respond).toHaveBeenCalledTimes(1);
|
expect(respond).toHaveBeenCalledTimes(1);
|
||||||
expect(respond.mock.calls[0]?.[0]).toBe(true);
|
expect(respond.mock.calls[0]?.[0]).toBe(true);
|
||||||
const result = respond.mock.calls[0]?.[1] as unknown as { sessions: Array<unknown> };
|
const result = respond.mock.calls[0]?.[1] as unknown as {
|
||||||
|
sessions: Array<{ key: string; agentId: string }>;
|
||||||
|
};
|
||||||
expect(result.sessions).toHaveLength(2);
|
expect(result.sessions).toHaveLength(2);
|
||||||
|
|
||||||
// Sorted by most recent first (mtime=200 -> opus first).
|
// Sorted by most recent first (mtime=200 -> opus first).
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
|
|
||||||
vi.mock("../../infra/session-cost-usage.js", async () => {
|
vi.mock("../../infra/session-cost-usage.js", async () => {
|
||||||
const actual = await vi.importActual<typeof import("../../infra/session-cost-usage.js")>(
|
const actual = await vi.importActual<typeof import("../../infra/session-cost-usage.js")>(
|
||||||
@@ -63,7 +64,7 @@ describe("gateway usage helpers", () => {
|
|||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
vi.setSystemTime(new Date("2026-02-05T00:00:00.000Z"));
|
vi.setSystemTime(new Date("2026-02-05T00:00:00.000Z"));
|
||||||
|
|
||||||
const config = {} as unknown as ReturnType<import("../../config/config.js").loadConfig>;
|
const config = {} as OpenClawConfig;
|
||||||
const a = await __test.loadCostUsageSummaryCached({
|
const a = await __test.loadCostUsageSummaryCached({
|
||||||
startMs: 1,
|
startMs: 1,
|
||||||
endMs: 2,
|
endMs: 2,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const createRegistry = (diagnostics: PluginDiagnostic[]): PluginRegistry => ({
|
|||||||
hooks: [],
|
hooks: [],
|
||||||
typedHooks: [],
|
typedHooks: [],
|
||||||
channels: [],
|
channels: [],
|
||||||
|
commands: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
gatewayHandlers: {},
|
gatewayHandlers: {},
|
||||||
httpHandlers: [],
|
httpHandlers: [],
|
||||||
|
|||||||
@@ -306,9 +306,14 @@ describe("gateway server agent", () => {
|
|||||||
|
|
||||||
const ack = await ackP;
|
const ack = await ackP;
|
||||||
const final = await finalP;
|
const final = await finalP;
|
||||||
expect(ack.payload.runId).toBeDefined();
|
const ackPayload = ack.payload;
|
||||||
expect(final.payload.runId).toBe(ack.payload.runId);
|
const finalPayload = final.payload;
|
||||||
expect(final.payload.status).toBe("ok");
|
if (!ackPayload || !finalPayload) {
|
||||||
|
throw new Error("missing websocket payload");
|
||||||
|
}
|
||||||
|
expect(ackPayload.runId).toBeDefined();
|
||||||
|
expect(finalPayload.runId).toBe(ackPayload.runId);
|
||||||
|
expect(finalPayload.status).toBe("ok");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("agent dedupes by idempotencyKey after completion", async () => {
|
test("agent dedupes by idempotencyKey after completion", async () => {
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ async function withCanvasGatewayHarness(params: {
|
|||||||
const canvasWss = new WebSocketServer({ noServer: true });
|
const canvasWss = new WebSocketServer({ noServer: true });
|
||||||
const canvasHost: CanvasHostHandler = {
|
const canvasHost: CanvasHostHandler = {
|
||||||
rootDir: "test",
|
rootDir: "test",
|
||||||
|
basePath: "/canvas",
|
||||||
close: async () => {},
|
close: async () => {},
|
||||||
handleUpgrade: (req, socket, head) => {
|
handleUpgrade: (req, socket, head) => {
|
||||||
const url = new URL(req.url ?? "/", "http://localhost");
|
const url = new URL(req.url ?? "/", "http://localhost");
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ describe("gateway server chat", () => {
|
|||||||
test("smoke: supports abort and idempotent completion", async () => {
|
test("smoke: supports abort and idempotent completion", async () => {
|
||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
const { server, ws } = await startServerWithClient();
|
const { server, ws } = await startServerWithClient();
|
||||||
const spy = vi.mocked(getReplyFromConfig);
|
const spy = vi.mocked(getReplyFromConfig) as unknown as ReturnType<typeof vi.fn>;
|
||||||
let aborted = false;
|
let aborted = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ describe("gateway config.apply", () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const res = await onceMessage<{ ok: boolean; error?: { message?: string } }>(
|
const res = await onceMessage<{ ok: boolean; error?: { message?: string } }>(ws, (o) => {
|
||||||
ws,
|
const msg = o as { type?: string; id?: string };
|
||||||
(o) => o.type === "res" && o.id === id,
|
return msg.type === "res" && msg.id === id;
|
||||||
);
|
});
|
||||||
expect(res.ok).toBe(false);
|
expect(res.ok).toBe(false);
|
||||||
expect(res.error?.message ?? "").toMatch(/invalid|SyntaxError/i);
|
expect(res.error?.message ?? "").toMatch(/invalid|SyntaxError/i);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -69,10 +69,10 @@ describe("gateway config.apply", () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const res = await onceMessage<{ ok: boolean; error?: { message?: string } }>(
|
const res = await onceMessage<{ ok: boolean; error?: { message?: string } }>(ws, (o) => {
|
||||||
ws,
|
const msg = o as { type?: string; id?: string };
|
||||||
(o) => o.type === "res" && o.id === id,
|
return msg.type === "res" && msg.id === id;
|
||||||
);
|
});
|
||||||
expect(res.ok).toBe(false);
|
expect(res.ok).toBe(false);
|
||||||
expect(res.error?.message ?? "").toContain("raw");
|
expect(res.error?.message ?? "").toContain("raw");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -561,7 +561,7 @@ describe("gateway server cron", () => {
|
|||||||
await yieldToEventLoop();
|
await yieldToEventLoop();
|
||||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
cronIsolatedRun.mockResolvedValueOnce({ status: "ok" });
|
cronIsolatedRun.mockResolvedValueOnce({ status: "ok", summary: "" });
|
||||||
const noSummaryRes = await rpcReq(ws, "cron.add", {
|
const noSummaryRes = await rpcReq(ws, "cron.add", {
|
||||||
name: "webhook no summary",
|
name: "webhook no summary",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -268,10 +268,11 @@ describe("node.invoke approval bypass", () => {
|
|||||||
});
|
});
|
||||||
expect(invoke.ok).toBe(true);
|
expect(invoke.ok).toBe(true);
|
||||||
|
|
||||||
expect(lastInvokeParams).toBeTruthy();
|
const invokeParams = lastInvokeParams as Record<string, unknown> | null;
|
||||||
expect(lastInvokeParams?.approved).toBe(true);
|
expect(invokeParams).toBeTruthy();
|
||||||
expect(lastInvokeParams?.approvalDecision).toBe("allow-once");
|
expect(invokeParams?.["approved"]).toBe(true);
|
||||||
expect(lastInvokeParams?.injected).toBeUndefined();
|
expect(invokeParams?.["approvalDecision"]).toBe("allow-once");
|
||||||
|
expect(invokeParams?.["injected"]).toBeUndefined();
|
||||||
|
|
||||||
ws.close();
|
ws.close();
|
||||||
ws2.close();
|
ws2.close();
|
||||||
|
|||||||
Reference in New Issue
Block a user