refactor: deduplicate shared helpers and test setup

This commit is contained in:
Peter Steinberger
2026-02-23 20:40:38 +00:00
parent 1f5e6444ee
commit 75423a00d6
33 changed files with 999 additions and 1112 deletions

View File

@@ -76,6 +76,19 @@ describe("runBootOnce", () => {
});
};
const expectMainSessionRestored = (params: {
storePath: string;
sessionKey: string;
expectedSessionId?: string;
}) => {
const restored = loadSessionStore(params.storePath, { skipCache: true });
if (params.expectedSessionId === undefined) {
expect(restored[params.sessionKey]).toBeUndefined();
return;
}
expect(restored[params.sessionKey]?.sessionId).toBe(params.expectedSessionId);
};
it("skips when BOOT.md is missing", async () => {
await withBootWorkspace({}, async (workspaceDir) => {
await expect(runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir })).resolves.toEqual({
@@ -226,8 +239,7 @@ describe("runBootOnce", () => {
status: "ran",
});
const restored = loadSessionStore(storePath, { skipCache: true });
expect(restored[sessionKey]?.sessionId).toBe(existingSessionId);
expectMainSessionRestored({ storePath, sessionKey, expectedSessionId: existingSessionId });
});
});
@@ -242,8 +254,7 @@ describe("runBootOnce", () => {
status: "ran",
});
const restored = loadSessionStore(storePath, { skipCache: true });
expect(restored[sessionKey]).toBeUndefined();
expectMainSessionRestored({ storePath, sessionKey });
});
});
});

View File

@@ -132,6 +132,22 @@ function createClientWithIdentity(
});
}
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("openclaw doctor --fix");
if (params?.expectTailscaleHint) {
expect(error.message).toContain("Tailscale Serve/Funnel");
}
}
describe("GatewayClient security checks", () => {
beforeEach(() => {
wsInstances.length = 0;
@@ -146,14 +162,7 @@ describe("GatewayClient security checks", () => {
client.start();
expect(onConnectError).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining("SECURITY ERROR"),
}),
);
const error = onConnectError.mock.calls[0]?.[0] as Error;
expect(error.message).toContain("openclaw doctor --fix");
expect(error.message).toContain("Tailscale Serve/Funnel");
expectSecurityConnectError(onConnectError, { expectTailscaleHint: true });
expect(wsInstances.length).toBe(0); // No WebSocket created
client.stop();
});
@@ -168,13 +177,7 @@ describe("GatewayClient security checks", () => {
// Should not throw
expect(() => client.start()).not.toThrow();
expect(onConnectError).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining("SECURITY ERROR"),
}),
);
const error = onConnectError.mock.calls[0]?.[0] as Error;
expect(error.message).toContain("openclaw doctor --fix");
expectSecurityConnectError(onConnectError);
expect(wsInstances.length).toBe(0); // No WebSocket created
client.stop();
});

View File

@@ -40,6 +40,18 @@ describe("sanitizeSystemRunParamsForForwarding", () => {
};
}
function expectAllowOnceForwardingResult(
result: ReturnType<typeof sanitizeSystemRunParamsForForwarding>,
) {
expect(result.ok).toBe(true);
if (!result.ok) {
throw new Error("unreachable");
}
const params = result.params as Record<string, unknown>;
expect(params.approved).toBe(true);
expect(params.approvalDecision).toBe("allow-once");
}
test("rejects cmd.exe /c trailing-arg mismatch against rawCommand", () => {
const result = sanitizeSystemRunParamsForForwarding({
rawParams: {
@@ -74,13 +86,7 @@ describe("sanitizeSystemRunParamsForForwarding", () => {
execApprovalManager: manager(makeRecord("echo SAFE&&whoami")),
nowMs: now,
});
expect(result.ok).toBe(true);
if (!result.ok) {
throw new Error("unreachable");
}
const params = result.params as Record<string, unknown>;
expect(params.approved).toBe(true);
expect(params.approvalDecision).toBe("allow-once");
expectAllowOnceForwardingResult(result);
});
test("rejects env-assignment shell wrapper when approval command omits env prelude", () => {
@@ -117,12 +123,6 @@ describe("sanitizeSystemRunParamsForForwarding", () => {
),
nowMs: now,
});
expect(result.ok).toBe(true);
if (!result.ok) {
throw new Error("unreachable");
}
const params = result.params as Record<string, unknown>;
expect(params.approved).toBe(true);
expect(params.approvalDecision).toBe("allow-once");
expectAllowOnceForwardingResult(result);
});
});

View File

@@ -19,6 +19,31 @@ vi.mock("../../memory/index.js", () => ({
import { doctorHandlers } from "./doctor.js";
const invokeDoctorMemoryStatus = async (respond: ReturnType<typeof vi.fn>) => {
await doctorHandlers["doctor.memory.status"]({
req: {} as never,
params: {} as never,
respond: respond as never,
context: {} as never,
client: null,
isWebchatConnect: () => false,
});
};
const expectEmbeddingErrorResponse = (respond: ReturnType<typeof vi.fn>, error: string) => {
expect(respond).toHaveBeenCalledWith(
true,
{
agentId: "main",
embedding: {
ok: false,
error,
},
},
undefined,
);
};
describe("doctor.memory.status", () => {
beforeEach(() => {
loadConfig.mockClear();
@@ -37,14 +62,7 @@ describe("doctor.memory.status", () => {
});
const respond = vi.fn();
await doctorHandlers["doctor.memory.status"]({
req: {} as never,
params: {} as never,
respond: respond as never,
context: {} as never,
client: null,
isWebchatConnect: () => false,
});
await invokeDoctorMemoryStatus(respond);
expect(getMemorySearchManager).toHaveBeenCalledWith({
cfg: expect.any(Object),
@@ -70,26 +88,9 @@ describe("doctor.memory.status", () => {
});
const respond = vi.fn();
await doctorHandlers["doctor.memory.status"]({
req: {} as never,
params: {} as never,
respond: respond as never,
context: {} as never,
client: null,
isWebchatConnect: () => false,
});
await invokeDoctorMemoryStatus(respond);
expect(respond).toHaveBeenCalledWith(
true,
{
agentId: "main",
embedding: {
ok: false,
error: "memory search unavailable",
},
},
undefined,
);
expectEmbeddingErrorResponse(respond, "memory search unavailable");
});
it("returns probe failure when manager probe throws", async () => {
@@ -103,26 +104,9 @@ describe("doctor.memory.status", () => {
});
const respond = vi.fn();
await doctorHandlers["doctor.memory.status"]({
req: {} as never,
params: {} as never,
respond: respond as never,
context: {} as never,
client: null,
isWebchatConnect: () => false,
});
await invokeDoctorMemoryStatus(respond);
expect(respond).toHaveBeenCalledWith(
true,
{
agentId: "main",
embedding: {
ok: false,
error: "gateway memory probe failed: timeout",
},
},
undefined,
);
expectEmbeddingErrorResponse(respond, "gateway memory probe failed: timeout");
expect(close).toHaveBeenCalled();
});
});

View File

@@ -11,6 +11,17 @@ vi.mock("../memory/index.js", () => ({
import { startGatewayMemoryBackend } from "./server-startup-memory.js";
function createQmdConfig(agents: OpenClawConfig["agents"]): OpenClawConfig {
return {
agents,
memory: { backend: "qmd", qmd: {} },
} as OpenClawConfig;
}
function createGatewayLogMock() {
return { info: vi.fn(), warn: vi.fn() };
}
describe("startGatewayMemoryBackend", () => {
beforeEach(() => {
getMemorySearchManagerMock.mockClear();
@@ -31,11 +42,8 @@ describe("startGatewayMemoryBackend", () => {
});
it("initializes qmd backend for each configured agent", async () => {
const cfg = {
agents: { list: [{ id: "ops", default: true }, { id: "main" }] },
memory: { backend: "qmd", qmd: {} },
} as OpenClawConfig;
const log = { info: vi.fn(), warn: vi.fn() };
const cfg = createQmdConfig({ list: [{ id: "ops", default: true }, { id: "main" }] });
const log = createGatewayLogMock();
getMemorySearchManagerMock.mockResolvedValue({ manager: { search: vi.fn() } });
await startGatewayMemoryBackend({ cfg, log });
@@ -55,11 +63,8 @@ describe("startGatewayMemoryBackend", () => {
});
it("logs a warning when qmd manager init fails and continues with other agents", async () => {
const cfg = {
agents: { list: [{ id: "main", default: true }, { id: "ops" }] },
memory: { backend: "qmd", qmd: {} },
} as OpenClawConfig;
const log = { info: vi.fn(), warn: vi.fn() };
const cfg = createQmdConfig({ list: [{ id: "main", default: true }, { id: "ops" }] });
const log = createGatewayLogMock();
getMemorySearchManagerMock
.mockResolvedValueOnce({ manager: null, error: "qmd missing" })
.mockResolvedValueOnce({ manager: { search: vi.fn() } });
@@ -75,17 +80,14 @@ describe("startGatewayMemoryBackend", () => {
});
it("skips agents with memory search disabled", async () => {
const cfg = {
agents: {
defaults: { memorySearch: { enabled: true } },
list: [
{ id: "main", default: true },
{ id: "ops", memorySearch: { enabled: false } },
],
},
memory: { backend: "qmd", qmd: {} },
} as OpenClawConfig;
const log = { info: vi.fn(), warn: vi.fn() };
const cfg = createQmdConfig({
defaults: { memorySearch: { enabled: true } },
list: [
{ id: "main", default: true },
{ id: "ops", memorySearch: { enabled: false } },
],
});
const log = createGatewayLogMock();
getMemorySearchManagerMock.mockResolvedValue({ manager: { search: vi.fn() } });
await startGatewayMemoryBackend({ cfg, log });

View File

@@ -107,16 +107,33 @@ async function cleanupCronTestRun(params: {
process.env.OPENCLAW_SKIP_CRON = params.prevSkipCron;
}
async function setupCronTestRun(params: {
tempPrefix: string;
cronEnabled?: boolean;
sessionConfig?: { mainKey: string };
jobs?: unknown[];
}): Promise<{ prevSkipCron: string | undefined; dir: string }> {
const prevSkipCron = process.env.OPENCLAW_SKIP_CRON;
process.env.OPENCLAW_SKIP_CRON = "0";
const dir = await fs.mkdtemp(path.join(os.tmpdir(), params.tempPrefix));
testState.cronStorePath = path.join(dir, "cron", "jobs.json");
testState.sessionConfig = params.sessionConfig;
testState.cronEnabled = params.cronEnabled;
await fs.mkdir(path.dirname(testState.cronStorePath), { recursive: true });
await fs.writeFile(
testState.cronStorePath,
JSON.stringify({ version: 1, jobs: params.jobs ?? [] }),
);
return { prevSkipCron, dir };
}
describe("gateway server cron", () => {
test("handles cron CRUD, normalization, and patch semantics", { timeout: 120_000 }, async () => {
const prevSkipCron = process.env.OPENCLAW_SKIP_CRON;
process.env.OPENCLAW_SKIP_CRON = "0";
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-cron-"));
testState.cronStorePath = path.join(dir, "cron", "jobs.json");
testState.sessionConfig = { mainKey: "primary" };
testState.cronEnabled = false;
await fs.mkdir(path.dirname(testState.cronStorePath), { recursive: true });
await fs.writeFile(testState.cronStorePath, JSON.stringify({ version: 1, jobs: [] }));
const { prevSkipCron, dir } = await setupCronTestRun({
tempPrefix: "openclaw-gw-cron-",
sessionConfig: { mainKey: "primary" },
cronEnabled: false,
});
const { server, ws } = await startServerWithClient();
await connectOk(ws);
@@ -385,13 +402,9 @@ describe("gateway server cron", () => {
});
test("writes cron run history and auto-runs due jobs", async () => {
const prevSkipCron = process.env.OPENCLAW_SKIP_CRON;
process.env.OPENCLAW_SKIP_CRON = "0";
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-cron-log-"));
testState.cronStorePath = path.join(dir, "cron", "jobs.json");
testState.cronEnabled = undefined;
await fs.mkdir(path.dirname(testState.cronStorePath), { recursive: true });
await fs.writeFile(testState.cronStorePath, JSON.stringify({ version: 1, jobs: [] }));
const { prevSkipCron, dir } = await setupCronTestRun({
tempPrefix: "openclaw-gw-cron-log-",
});
const { server, ws } = await startServerWithClient();
await connectOk(ws);
@@ -489,13 +502,6 @@ describe("gateway server cron", () => {
}, 45_000);
test("posts webhooks for delivery mode and legacy notify fallback only when summary exists", async () => {
const prevSkipCron = process.env.OPENCLAW_SKIP_CRON;
process.env.OPENCLAW_SKIP_CRON = "0";
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-cron-webhook-"));
testState.cronStorePath = path.join(dir, "cron", "jobs.json");
testState.cronEnabled = false;
await fs.mkdir(path.dirname(testState.cronStorePath), { recursive: true });
const legacyNotifyJob = {
id: "legacy-notify-job",
name: "legacy notify job",
@@ -509,10 +515,11 @@ describe("gateway server cron", () => {
payload: { kind: "systemEvent", text: "legacy webhook" },
state: {},
};
await fs.writeFile(
testState.cronStorePath,
JSON.stringify({ version: 1, jobs: [legacyNotifyJob] }),
);
const { prevSkipCron, dir } = await setupCronTestRun({
tempPrefix: "openclaw-gw-cron-webhook-",
cronEnabled: false,
jobs: [legacyNotifyJob],
});
const configPath = process.env.OPENCLAW_CONFIG_PATH;
expect(typeof configPath).toBe("string");