mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 02:11:23 +00:00
refactor: deduplicate shared helpers and test setup
This commit is contained in:
@@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user