mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 01:31:23 +00:00
test: dedupe fixtures and test harness setup
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
|
||||
const loadConfig = vi.fn();
|
||||
const resolveGatewayPort = vi.fn();
|
||||
const pickPrimaryTailnetIPv4 = vi.fn();
|
||||
const pickPrimaryLanIPv4 = vi.fn();
|
||||
import {
|
||||
loadConfigMock as loadConfig,
|
||||
pickPrimaryLanIPv4Mock as pickPrimaryLanIPv4,
|
||||
pickPrimaryTailnetIPv4Mock as pickPrimaryTailnetIPv4,
|
||||
resolveGatewayPortMock as resolveGatewayPort,
|
||||
} from "./gateway-connection.test-mocks.js";
|
||||
|
||||
let lastClientOptions: {
|
||||
url?: string;
|
||||
@@ -19,27 +20,6 @@ let startMode: StartMode = "hello";
|
||||
let closeCode = 1006;
|
||||
let closeReason = "";
|
||||
|
||||
vi.mock("../config/config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../config/config.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadConfig,
|
||||
resolveGatewayPort,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../infra/tailnet.js", () => ({
|
||||
pickPrimaryTailnetIPv4,
|
||||
}));
|
||||
|
||||
vi.mock("./net.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./net.js")>();
|
||||
return {
|
||||
...actual,
|
||||
pickPrimaryLanIPv4,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./client.js", () => ({
|
||||
describeGatewayCloseCode: (code: number) => {
|
||||
if (code === 1000) {
|
||||
|
||||
27
src/gateway/gateway-connection.test-mocks.ts
Normal file
27
src/gateway/gateway-connection.test-mocks.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
export const loadConfigMock = vi.fn();
|
||||
export const resolveGatewayPortMock = vi.fn();
|
||||
export const pickPrimaryTailnetIPv4Mock = vi.fn();
|
||||
export const pickPrimaryLanIPv4Mock = vi.fn();
|
||||
|
||||
vi.mock("../config/config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../config/config.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadConfig: loadConfigMock,
|
||||
resolveGatewayPort: resolveGatewayPortMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../infra/tailnet.js", () => ({
|
||||
pickPrimaryTailnetIPv4: pickPrimaryTailnetIPv4Mock,
|
||||
}));
|
||||
|
||||
vi.mock("./net.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./net.js")>();
|
||||
return {
|
||||
...actual,
|
||||
pickPrimaryLanIPv4: pickPrimaryLanIPv4Mock,
|
||||
};
|
||||
});
|
||||
@@ -17,6 +17,8 @@ vi.mock("./hooks.js", async (importOriginal) => {
|
||||
|
||||
import { createHooksRequestHandler } from "./server-http.js";
|
||||
|
||||
type HooksHandlerDeps = Parameters<typeof createHooksRequestHandler>[0];
|
||||
|
||||
function createHooksConfig(): HooksConfigResolved {
|
||||
return {
|
||||
basePath: "/hooks",
|
||||
@@ -66,6 +68,30 @@ function createResponse(): {
|
||||
return { res, end, setHeader };
|
||||
}
|
||||
|
||||
function createHandler(params?: {
|
||||
dispatchWakeHook?: HooksHandlerDeps["dispatchWakeHook"];
|
||||
dispatchAgentHook?: HooksHandlerDeps["dispatchAgentHook"];
|
||||
}) {
|
||||
return createHooksRequestHandler({
|
||||
getHooksConfig: () => createHooksConfig(),
|
||||
bindHost: "127.0.0.1",
|
||||
port: 18789,
|
||||
logHooks: {
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as ReturnType<typeof createSubsystemLogger>,
|
||||
dispatchWakeHook:
|
||||
params?.dispatchWakeHook ??
|
||||
((() => {
|
||||
return;
|
||||
}) as HooksHandlerDeps["dispatchWakeHook"]),
|
||||
dispatchAgentHook:
|
||||
params?.dispatchAgentHook ?? ((() => "run-1") as HooksHandlerDeps["dispatchAgentHook"]),
|
||||
});
|
||||
}
|
||||
|
||||
describe("createHooksRequestHandler timeout status mapping", () => {
|
||||
beforeEach(() => {
|
||||
readJsonBodyMock.mockClear();
|
||||
@@ -75,19 +101,7 @@ describe("createHooksRequestHandler timeout status mapping", () => {
|
||||
readJsonBodyMock.mockResolvedValue({ ok: false, error: "request body timeout" });
|
||||
const dispatchWakeHook = vi.fn();
|
||||
const dispatchAgentHook = vi.fn(() => "run-1");
|
||||
const handler = createHooksRequestHandler({
|
||||
getHooksConfig: () => createHooksConfig(),
|
||||
bindHost: "127.0.0.1",
|
||||
port: 18789,
|
||||
logHooks: {
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as ReturnType<typeof createSubsystemLogger>,
|
||||
dispatchWakeHook,
|
||||
dispatchAgentHook,
|
||||
});
|
||||
const handler = createHandler({ dispatchWakeHook, dispatchAgentHook });
|
||||
const req = createRequest();
|
||||
const { res, end } = createResponse();
|
||||
|
||||
@@ -101,19 +115,7 @@ describe("createHooksRequestHandler timeout status mapping", () => {
|
||||
});
|
||||
|
||||
test("shares hook auth rate-limit bucket across ipv4 and ipv4-mapped ipv6 forms", async () => {
|
||||
const handler = createHooksRequestHandler({
|
||||
getHooksConfig: () => createHooksConfig(),
|
||||
bindHost: "127.0.0.1",
|
||||
port: 18789,
|
||||
logHooks: {
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as ReturnType<typeof createSubsystemLogger>,
|
||||
dispatchWakeHook: vi.fn(),
|
||||
dispatchAgentHook: vi.fn(() => "run-1"),
|
||||
});
|
||||
const handler = createHandler();
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const req = createRequest({
|
||||
|
||||
@@ -124,6 +124,40 @@ function createChatContext(): Pick<
|
||||
};
|
||||
}
|
||||
|
||||
type ChatContext = ReturnType<typeof createChatContext>;
|
||||
|
||||
async function runNonStreamingChatSend(params: {
|
||||
context: ChatContext;
|
||||
respond: ReturnType<typeof vi.fn>;
|
||||
idempotencyKey: string;
|
||||
message?: string;
|
||||
}) {
|
||||
await chatHandlers["chat.send"]({
|
||||
params: {
|
||||
sessionKey: "main",
|
||||
message: params.message ?? "hello",
|
||||
idempotencyKey: params.idempotencyKey,
|
||||
},
|
||||
respond: params.respond as unknown as Parameters<
|
||||
(typeof chatHandlers)["chat.send"]
|
||||
>[0]["respond"],
|
||||
req: {} as never,
|
||||
client: null,
|
||||
isWebchatConnect: () => false,
|
||||
context: params.context as GatewayRequestContext,
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(
|
||||
(params.context.broadcast as unknown as ReturnType<typeof vi.fn>).mock.calls.length,
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
const chatCall = (params.context.broadcast as unknown as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
expect(chatCall?.[0]).toBe("chat");
|
||||
return chatCall?.[1];
|
||||
}
|
||||
|
||||
describe("chat directive tag stripping for non-streaming final payloads", () => {
|
||||
it("chat.inject keeps message defined when directive tag is the only content", async () => {
|
||||
createTranscriptFixture("openclaw-chat-inject-directive-only-");
|
||||
@@ -160,33 +194,20 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
|
||||
const respond = vi.fn();
|
||||
const context = createChatContext();
|
||||
|
||||
await chatHandlers["chat.send"]({
|
||||
params: {
|
||||
sessionKey: "main",
|
||||
message: "hello",
|
||||
idempotencyKey: "idem-directive-only",
|
||||
},
|
||||
const payload = await runNonStreamingChatSend({
|
||||
context,
|
||||
respond,
|
||||
req: {} as never,
|
||||
client: null,
|
||||
isWebchatConnect: () => false,
|
||||
context: context as GatewayRequestContext,
|
||||
idempotencyKey: "idem-directive-only",
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect((context.broadcast as unknown as ReturnType<typeof vi.fn>).mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
const chatCall = (context.broadcast as unknown as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
expect(chatCall?.[0]).toBe("chat");
|
||||
expect(chatCall?.[1]).toEqual(
|
||||
expect(payload).toEqual(
|
||||
expect.objectContaining({
|
||||
runId: "idem-directive-only",
|
||||
state: "final",
|
||||
message: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
expect(extractFirstTextBlock(chatCall?.[1])).toBe("");
|
||||
expect(extractFirstTextBlock(payload)).toBe("");
|
||||
});
|
||||
|
||||
it("chat.inject strips external untrusted wrapper metadata from final payload text", async () => {
|
||||
@@ -218,25 +239,11 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
|
||||
const respond = vi.fn();
|
||||
const context = createChatContext();
|
||||
|
||||
await chatHandlers["chat.send"]({
|
||||
params: {
|
||||
sessionKey: "main",
|
||||
message: "hello",
|
||||
idempotencyKey: "idem-untrusted-context",
|
||||
},
|
||||
const payload = await runNonStreamingChatSend({
|
||||
context,
|
||||
respond,
|
||||
req: {} as never,
|
||||
client: null,
|
||||
isWebchatConnect: () => false,
|
||||
context: context as GatewayRequestContext,
|
||||
idempotencyKey: "idem-untrusted-context",
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect((context.broadcast as unknown as ReturnType<typeof vi.fn>).mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
const chatCall = (context.broadcast as unknown as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
expect(chatCall?.[0]).toBe("chat");
|
||||
expect(extractFirstTextBlock(chatCall?.[1])).toBe("hello");
|
||||
expect(extractFirstTextBlock(payload)).toBe("hello");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,24 +13,32 @@ import {
|
||||
|
||||
installGatewayTestHooks({ scope: "suite" });
|
||||
|
||||
let server: Awaited<ReturnType<typeof startServerWithClient>>["server"];
|
||||
let ws: Awaited<ReturnType<typeof startServerWithClient>>["ws"];
|
||||
let startedServer: Awaited<ReturnType<typeof startServerWithClient>> | null = null;
|
||||
|
||||
function requireWs(): Awaited<ReturnType<typeof startServerWithClient>>["ws"] {
|
||||
if (!startedServer) {
|
||||
throw new Error("gateway test server not started");
|
||||
}
|
||||
return startedServer.ws;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
const started = await startServerWithClient(undefined, { controlUiEnabled: true });
|
||||
server = started.server;
|
||||
ws = started.ws;
|
||||
await connectOk(ws);
|
||||
startedServer = await startServerWithClient(undefined, { controlUiEnabled: true });
|
||||
await connectOk(requireWs());
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
ws.close();
|
||||
await server.close();
|
||||
if (!startedServer) {
|
||||
return;
|
||||
}
|
||||
startedServer.ws.close();
|
||||
await startedServer.server.close();
|
||||
startedServer = null;
|
||||
});
|
||||
|
||||
describe("gateway config methods", () => {
|
||||
it("rejects config.patch when raw is not an object", async () => {
|
||||
const res = await rpcReq<{ ok?: boolean }>(ws, "config.patch", {
|
||||
const res = await rpcReq<{ ok?: boolean }>(requireWs(), "config.patch", {
|
||||
raw: "[]",
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
@@ -78,7 +86,7 @@ describe("gateway server sessions", () => {
|
||||
|
||||
const homeSessions = await rpcReq<{
|
||||
sessions: Array<{ key: string }>;
|
||||
}>(ws, "sessions.list", {
|
||||
}>(requireWs(), "sessions.list", {
|
||||
includeGlobal: false,
|
||||
includeUnknown: false,
|
||||
agentId: "home",
|
||||
@@ -91,7 +99,7 @@ describe("gateway server sessions", () => {
|
||||
|
||||
const workSessions = await rpcReq<{
|
||||
sessions: Array<{ key: string }>;
|
||||
}>(ws, "sessions.list", {
|
||||
}>(requireWs(), "sessions.list", {
|
||||
includeGlobal: false,
|
||||
includeUnknown: false,
|
||||
agentId: "work",
|
||||
@@ -119,13 +127,13 @@ describe("gateway server sessions", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const resolved = await rpcReq<{ ok: true; key: string }>(ws, "sessions.resolve", {
|
||||
const resolved = await rpcReq<{ ok: true; key: string }>(requireWs(), "sessions.resolve", {
|
||||
key: "main",
|
||||
});
|
||||
expect(resolved.ok).toBe(true);
|
||||
expect(resolved.payload?.key).toBe("agent:ops:work");
|
||||
|
||||
const patched = await rpcReq<{ ok: true; key: string }>(ws, "sessions.patch", {
|
||||
const patched = await rpcReq<{ ok: true; key: string }>(requireWs(), "sessions.patch", {
|
||||
key: "main",
|
||||
thinkingLevel: "medium",
|
||||
});
|
||||
|
||||
@@ -85,6 +85,28 @@ async function waitForCondition(check: () => boolean, timeoutMs = 2000) {
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupCronTestRun(params: {
|
||||
ws: { close: () => void };
|
||||
server: { close: () => Promise<void> };
|
||||
dir: string;
|
||||
prevSkipCron: string | undefined;
|
||||
clearSessionConfig?: boolean;
|
||||
}) {
|
||||
params.ws.close();
|
||||
await params.server.close();
|
||||
await rmTempDir(params.dir);
|
||||
testState.cronStorePath = undefined;
|
||||
if (params.clearSessionConfig) {
|
||||
testState.sessionConfig = undefined;
|
||||
}
|
||||
testState.cronEnabled = undefined;
|
||||
if (params.prevSkipCron === undefined) {
|
||||
delete process.env.OPENCLAW_SKIP_CRON;
|
||||
return;
|
||||
}
|
||||
process.env.OPENCLAW_SKIP_CRON = params.prevSkipCron;
|
||||
}
|
||||
|
||||
describe("gateway server cron", () => {
|
||||
test("handles cron CRUD, normalization, and patch semantics", { timeout: 120_000 }, async () => {
|
||||
const prevSkipCron = process.env.OPENCLAW_SKIP_CRON;
|
||||
@@ -352,17 +374,13 @@ describe("gateway server cron", () => {
|
||||
const disabled = disableUpdateRes.payload as { enabled?: unknown } | undefined;
|
||||
expect(disabled?.enabled).toBe(false);
|
||||
} finally {
|
||||
ws.close();
|
||||
await server.close();
|
||||
await rmTempDir(dir);
|
||||
testState.cronStorePath = undefined;
|
||||
testState.sessionConfig = undefined;
|
||||
testState.cronEnabled = undefined;
|
||||
if (prevSkipCron === undefined) {
|
||||
delete process.env.OPENCLAW_SKIP_CRON;
|
||||
} else {
|
||||
process.env.OPENCLAW_SKIP_CRON = prevSkipCron;
|
||||
}
|
||||
await cleanupCronTestRun({
|
||||
ws,
|
||||
server,
|
||||
dir,
|
||||
prevSkipCron,
|
||||
clearSessionConfig: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -466,16 +484,7 @@ describe("gateway server cron", () => {
|
||||
const runs = autoEntries?.entries ?? [];
|
||||
expect(runs.at(-1)?.jobId).toBe(autoJobId);
|
||||
} finally {
|
||||
ws.close();
|
||||
await server.close();
|
||||
await rmTempDir(dir);
|
||||
testState.cronStorePath = undefined;
|
||||
testState.cronEnabled = undefined;
|
||||
if (prevSkipCron === undefined) {
|
||||
delete process.env.OPENCLAW_SKIP_CRON;
|
||||
} else {
|
||||
process.env.OPENCLAW_SKIP_CRON = prevSkipCron;
|
||||
}
|
||||
await cleanupCronTestRun({ ws, server, dir, prevSkipCron });
|
||||
}
|
||||
}, 45_000);
|
||||
|
||||
@@ -650,16 +659,7 @@ describe("gateway server cron", () => {
|
||||
await yieldToEventLoop();
|
||||
expect(fetchWithSsrFGuardMock).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
ws.close();
|
||||
await server.close();
|
||||
await rmTempDir(dir);
|
||||
testState.cronStorePath = undefined;
|
||||
testState.cronEnabled = undefined;
|
||||
if (prevSkipCron === undefined) {
|
||||
delete process.env.OPENCLAW_SKIP_CRON;
|
||||
} else {
|
||||
process.env.OPENCLAW_SKIP_CRON = prevSkipCron;
|
||||
}
|
||||
await cleanupCronTestRun({ ws, server, dir, prevSkipCron });
|
||||
}
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user