mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:41:38 +00:00
refactor(test): centralize trigger and cron test helpers
This commit is contained in:
@@ -4,21 +4,10 @@ import { createTempHomeHarness, makeReplyConfig } from "./reply.test-harness.js"
|
|||||||
|
|
||||||
const runEmbeddedPiAgentMock = vi.fn();
|
const runEmbeddedPiAgentMock = vi.fn();
|
||||||
|
|
||||||
vi.mock("../agents/model-fallback.js", () => ({
|
vi.mock(
|
||||||
runWithModelFallback: async ({
|
"../agents/model-fallback.js",
|
||||||
provider,
|
async () => await import("../test-utils/model-fallback.mock.js"),
|
||||||
model,
|
);
|
||||||
run,
|
|
||||||
}: {
|
|
||||||
provider: string;
|
|
||||||
model: string;
|
|
||||||
run: (provider: string, model: string) => Promise<unknown>;
|
|
||||||
}) => ({
|
|
||||||
result: await run(provider, model),
|
|
||||||
provider,
|
|
||||||
model,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../agents/pi-embedded.js", () => ({
|
vi.mock("../agents/pi-embedded.js", () => ({
|
||||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { beforeAll, describe, expect, it } from "vitest";
|
import { beforeAll, describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
|
expectDirectElevatedToggleOn,
|
||||||
getRunEmbeddedPiAgentMock,
|
getRunEmbeddedPiAgentMock,
|
||||||
installTriggerHandlingE2eTestHooks,
|
installTriggerHandlingE2eTestHooks,
|
||||||
loadGetReplyFromConfig,
|
loadGetReplyFromConfig,
|
||||||
MAIN_SESSION_KEY,
|
MAIN_SESSION_KEY,
|
||||||
makeWhatsAppElevatedCfg,
|
makeWhatsAppElevatedCfg,
|
||||||
requireSessionStorePath,
|
requireSessionStorePath,
|
||||||
runDirectElevatedToggleAndLoadStore,
|
|
||||||
withTempHome,
|
withTempHome,
|
||||||
} from "./reply.triggers.trigger-handling.test-harness.js";
|
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||||
|
|
||||||
@@ -20,15 +20,7 @@ installTriggerHandlingE2eTestHooks();
|
|||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("allows approved sender to toggle elevated mode", async () => {
|
it("allows approved sender to toggle elevated mode", async () => {
|
||||||
await withTempHome(async (home) => {
|
await expectDirectElevatedToggleOn({ getReplyFromConfig });
|
||||||
const cfg = makeWhatsAppElevatedCfg(home);
|
|
||||||
const { text, store } = await runDirectElevatedToggleAndLoadStore({
|
|
||||||
cfg,
|
|
||||||
getReplyFromConfig,
|
|
||||||
});
|
|
||||||
expect(text).toContain("Elevated mode set to ask");
|
|
||||||
expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBe("on");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
it("rejects elevated toggles when disabled", async () => {
|
it("rejects elevated toggles when disabled", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { beforeAll, describe, expect, it } from "vitest";
|
import { beforeAll, describe, expect, it } from "vitest";
|
||||||
import { loadSessionStore } from "../config/sessions.js";
|
import { loadSessionStore } from "../config/sessions.js";
|
||||||
import {
|
import {
|
||||||
|
expectDirectElevatedToggleOn,
|
||||||
installTriggerHandlingE2eTestHooks,
|
installTriggerHandlingE2eTestHooks,
|
||||||
loadGetReplyFromConfig,
|
loadGetReplyFromConfig,
|
||||||
MAIN_SESSION_KEY,
|
|
||||||
makeWhatsAppElevatedCfg,
|
makeWhatsAppElevatedCfg,
|
||||||
readSessionStore,
|
readSessionStore,
|
||||||
requireSessionStorePath,
|
requireSessionStorePath,
|
||||||
runDirectElevatedToggleAndLoadStore,
|
|
||||||
withTempHome,
|
withTempHome,
|
||||||
} from "./reply.triggers.trigger-handling.test-harness.js";
|
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||||
|
|
||||||
@@ -72,14 +71,6 @@ describe("trigger handling", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("allows elevated directive in direct chats without mentions", async () => {
|
it("allows elevated directive in direct chats without mentions", async () => {
|
||||||
await withTempHome(async (home) => {
|
await expectDirectElevatedToggleOn({ getReplyFromConfig });
|
||||||
const cfg = makeWhatsAppElevatedCfg(home);
|
|
||||||
const { text, store } = await runDirectElevatedToggleAndLoadStore({
|
|
||||||
cfg,
|
|
||||||
getReplyFromConfig,
|
|
||||||
});
|
|
||||||
expect(text).toContain("Elevated mode set to ask");
|
|
||||||
expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBe("on");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,6 +53,22 @@ async function runCommandAndCollectReplies(params: {
|
|||||||
return { blockReplies, replies };
|
return { blockReplies, replies };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function expectStopAbortWithoutAgent(params: { home: string; body: string; from: string }) {
|
||||||
|
const res = await getReplyFromConfig(
|
||||||
|
{
|
||||||
|
Body: params.body,
|
||||||
|
From: params.from,
|
||||||
|
To: "+2000",
|
||||||
|
CommandAuthorized: true,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
makeCfg(params.home),
|
||||||
|
);
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toBe("⚙️ Agent was aborted.");
|
||||||
|
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("filters usage summary to the current model provider", async () => {
|
it("filters usage summary to the current model provider", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
@@ -228,36 +244,20 @@ describe("trigger handling", () => {
|
|||||||
});
|
});
|
||||||
it("aborts even with timestamp prefix", async () => {
|
it("aborts even with timestamp prefix", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const res = await getReplyFromConfig(
|
await expectStopAbortWithoutAgent({
|
||||||
{
|
home,
|
||||||
Body: "[Dec 5 10:00] stop",
|
body: "[Dec 5 10:00] stop",
|
||||||
From: "+1000",
|
from: "+1000",
|
||||||
To: "+2000",
|
});
|
||||||
CommandAuthorized: true,
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
makeCfg(home),
|
|
||||||
);
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
|
||||||
expect(text).toBe("⚙️ Agent was aborted.");
|
|
||||||
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("handles /stop without invoking the agent", async () => {
|
it("handles /stop without invoking the agent", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const res = await getReplyFromConfig(
|
await expectStopAbortWithoutAgent({
|
||||||
{
|
home,
|
||||||
Body: "/stop",
|
body: "/stop",
|
||||||
From: "+1003",
|
from: "+1003",
|
||||||
To: "+2000",
|
});
|
||||||
CommandAuthorized: true,
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
makeCfg(home),
|
|
||||||
);
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
|
||||||
expect(text).toBe("⚙️ Agent was aborted.");
|
|
||||||
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { beforeAll, describe, expect, it } from "vitest";
|
import { beforeAll, describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
createBlockReplyCollector,
|
expectInlineCommandHandledAndStripped,
|
||||||
getRunEmbeddedPiAgentMock,
|
getRunEmbeddedPiAgentMock,
|
||||||
installTriggerHandlingE2eTestHooks,
|
installTriggerHandlingE2eTestHooks,
|
||||||
loadGetReplyFromConfig,
|
loadGetReplyFromConfig,
|
||||||
makeCfg,
|
makeCfg,
|
||||||
mockRunEmbeddedPiAgentOk,
|
|
||||||
withTempHome,
|
withTempHome,
|
||||||
} from "./reply.triggers.trigger-handling.test-harness.js";
|
} from "./reply.triggers.trigger-handling.test-harness.js";
|
||||||
|
|
||||||
@@ -48,52 +47,28 @@ async function expectUnauthorizedCommandDropped(home: string, body: "/status" |
|
|||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("handles inline /commands and strips it before the agent", async () => {
|
it("handles inline /commands and strips it before the agent", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const runEmbeddedPiAgentMock = mockRunEmbeddedPiAgentOk();
|
await expectInlineCommandHandledAndStripped({
|
||||||
const { blockReplies, handlers } = createBlockReplyCollector();
|
home,
|
||||||
const res = await getReplyFromConfig(
|
getReplyFromConfig,
|
||||||
{
|
body: "please /commands now",
|
||||||
Body: "please /commands now",
|
stripToken: "/commands",
|
||||||
From: "+1002",
|
blockReplyContains: "Slash commands",
|
||||||
To: "+2000",
|
});
|
||||||
CommandAuthorized: true,
|
|
||||||
},
|
|
||||||
handlers,
|
|
||||||
makeCfg(home),
|
|
||||||
);
|
|
||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
|
||||||
expect(blockReplies.length).toBe(1);
|
|
||||||
expect(blockReplies[0]?.text).toContain("Slash commands");
|
|
||||||
expect(runEmbeddedPiAgentMock).toHaveBeenCalled();
|
|
||||||
const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? "";
|
|
||||||
expect(prompt).not.toContain("/commands");
|
|
||||||
expect(text).toBe("ok");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles inline /whoami and strips it before the agent", async () => {
|
it("handles inline /whoami and strips it before the agent", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const runEmbeddedPiAgentMock = mockRunEmbeddedPiAgentOk();
|
await expectInlineCommandHandledAndStripped({
|
||||||
const { blockReplies, handlers } = createBlockReplyCollector();
|
home,
|
||||||
const res = await getReplyFromConfig(
|
getReplyFromConfig,
|
||||||
{
|
body: "please /whoami now",
|
||||||
Body: "please /whoami now",
|
stripToken: "/whoami",
|
||||||
From: "+1002",
|
blockReplyContains: "Identity",
|
||||||
To: "+2000",
|
requestOverrides: {
|
||||||
SenderId: "12345",
|
SenderId: "12345",
|
||||||
CommandAuthorized: true,
|
|
||||||
},
|
},
|
||||||
handlers,
|
});
|
||||||
makeCfg(home),
|
|
||||||
);
|
|
||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
|
||||||
expect(blockReplies.length).toBe(1);
|
|
||||||
expect(blockReplies[0]?.text).toContain("Identity");
|
|
||||||
expect(runEmbeddedPiAgentMock).toHaveBeenCalled();
|
|
||||||
const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? "";
|
|
||||||
expect(prompt).not.toContain("/whoami");
|
|
||||||
expect(text).toBe("ok");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { beforeAll, describe, expect, it } from "vitest";
|
|||||||
import { resolveSessionKey } from "../config/sessions.js";
|
import { resolveSessionKey } from "../config/sessions.js";
|
||||||
import {
|
import {
|
||||||
createBlockReplyCollector,
|
createBlockReplyCollector,
|
||||||
|
expectInlineCommandHandledAndStripped,
|
||||||
getRunEmbeddedPiAgentMock,
|
getRunEmbeddedPiAgentMock,
|
||||||
installTriggerHandlingE2eTestHooks,
|
installTriggerHandlingE2eTestHooks,
|
||||||
loadGetReplyFromConfig,
|
loadGetReplyFromConfig,
|
||||||
@@ -116,25 +117,13 @@ describe("trigger handling", () => {
|
|||||||
|
|
||||||
it("handles inline /help and strips it before the agent", async () => {
|
it("handles inline /help and strips it before the agent", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const runEmbeddedPiAgentMock = mockRunEmbeddedPiAgentOk();
|
await expectInlineCommandHandledAndStripped({
|
||||||
const { blockReplies, handlers } = createBlockReplyCollector();
|
home,
|
||||||
const res = await getReplyFromConfig(
|
getReplyFromConfig,
|
||||||
{
|
body: "please /help now",
|
||||||
Body: "please /help now",
|
stripToken: "/help",
|
||||||
From: "+1002",
|
blockReplyContains: "Help",
|
||||||
To: "+2000",
|
});
|
||||||
CommandAuthorized: true,
|
|
||||||
},
|
|
||||||
handlers,
|
|
||||||
makeCfg(home),
|
|
||||||
);
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
|
||||||
expect(blockReplies.length).toBe(1);
|
|
||||||
expect(blockReplies[0]?.text).toContain("Help");
|
|
||||||
expect(runEmbeddedPiAgentMock).toHaveBeenCalled();
|
|
||||||
const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? "";
|
|
||||||
expect(prompt).not.toContain("/help");
|
|
||||||
expect(text).toBe("ok");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -214,6 +214,51 @@ export async function runDirectElevatedToggleAndLoadStore(params: {
|
|||||||
return { text, store };
|
return { text, store };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function expectDirectElevatedToggleOn(params: {
|
||||||
|
getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig;
|
||||||
|
}) {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
const cfg = makeWhatsAppElevatedCfg(home);
|
||||||
|
const { text, store } = await runDirectElevatedToggleAndLoadStore({
|
||||||
|
cfg,
|
||||||
|
getReplyFromConfig: params.getReplyFromConfig,
|
||||||
|
});
|
||||||
|
expect(text).toContain("Elevated mode set to ask");
|
||||||
|
expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBe("on");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function expectInlineCommandHandledAndStripped(params: {
|
||||||
|
home: string;
|
||||||
|
getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig;
|
||||||
|
body: string;
|
||||||
|
stripToken: string;
|
||||||
|
blockReplyContains: string;
|
||||||
|
requestOverrides?: Record<string, unknown>;
|
||||||
|
}) {
|
||||||
|
const runEmbeddedPiAgentMock = mockRunEmbeddedPiAgentOk();
|
||||||
|
const { blockReplies, handlers } = createBlockReplyCollector();
|
||||||
|
const res = await params.getReplyFromConfig(
|
||||||
|
{
|
||||||
|
Body: params.body,
|
||||||
|
From: "+1002",
|
||||||
|
To: "+2000",
|
||||||
|
CommandAuthorized: true,
|
||||||
|
...params.requestOverrides,
|
||||||
|
},
|
||||||
|
handlers,
|
||||||
|
makeCfg(params.home),
|
||||||
|
);
|
||||||
|
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(blockReplies.length).toBe(1);
|
||||||
|
expect(blockReplies[0]?.text).toContain(params.blockReplyContains);
|
||||||
|
expect(runEmbeddedPiAgentMock).toHaveBeenCalled();
|
||||||
|
const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? "";
|
||||||
|
expect(prompt).not.toContain(params.stripToken);
|
||||||
|
expect(text).toBe("ok");
|
||||||
|
}
|
||||||
|
|
||||||
export async function runGreetingPromptForBareNewOrReset(params: {
|
export async function runGreetingPromptForBareNewOrReset(params: {
|
||||||
home: string;
|
home: string;
|
||||||
body: "/new" | "/reset";
|
body: "/new" | "/reset";
|
||||||
|
|||||||
@@ -8,21 +8,10 @@ import { createMockTypingController } from "./test-helpers.js";
|
|||||||
|
|
||||||
const runEmbeddedPiAgentMock = vi.fn();
|
const runEmbeddedPiAgentMock = vi.fn();
|
||||||
|
|
||||||
vi.mock("../../agents/model-fallback.js", () => ({
|
vi.mock(
|
||||||
runWithModelFallback: async ({
|
"../../agents/model-fallback.js",
|
||||||
provider,
|
async () => await import("../../test-utils/model-fallback.mock.js"),
|
||||||
model,
|
);
|
||||||
run,
|
|
||||||
}: {
|
|
||||||
provider: string;
|
|
||||||
model: string;
|
|
||||||
run: (provider: string, model: string) => Promise<unknown>;
|
|
||||||
}) => ({
|
|
||||||
result: await run(provider, model),
|
|
||||||
provider,
|
|
||||||
model,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../../agents/pi-embedded.js", () => ({
|
vi.mock("../../agents/pi-embedded.js", () => ({
|
||||||
runEmbeddedPiAgent: (params: unknown) => runEmbeddedPiAgentMock(params),
|
runEmbeddedPiAgent: (params: unknown) => runEmbeddedPiAgentMock(params),
|
||||||
|
|||||||
@@ -1,35 +1,9 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { createMockCronStateForJobs } from "./service.test-harness.js";
|
||||||
import { recomputeNextRunsForMaintenance } from "./service/jobs.js";
|
import { recomputeNextRunsForMaintenance } from "./service/jobs.js";
|
||||||
import type { CronServiceState } from "./service/state.js";
|
|
||||||
import type { CronJob } from "./types.js";
|
import type { CronJob } from "./types.js";
|
||||||
|
|
||||||
describe("issue #13992 regression - cron jobs skip execution", () => {
|
describe("issue #13992 regression - cron jobs skip execution", () => {
|
||||||
function createMockState(jobs: CronJob[]): CronServiceState {
|
|
||||||
return {
|
|
||||||
store: { version: 1, jobs },
|
|
||||||
running: false,
|
|
||||||
timer: null,
|
|
||||||
storeLoadedAtMs: Date.now(),
|
|
||||||
storeFileMtimeMs: null,
|
|
||||||
op: Promise.resolve(),
|
|
||||||
warnedDisabled: false,
|
|
||||||
deps: {
|
|
||||||
storePath: "/mock/path",
|
|
||||||
cronEnabled: true,
|
|
||||||
nowMs: () => Date.now(),
|
|
||||||
enqueueSystemEvent: () => {},
|
|
||||||
requestHeartbeatNow: () => {},
|
|
||||||
runIsolatedAgentJob: async () => ({ status: "ok" }),
|
|
||||||
log: {
|
|
||||||
debug: () => {},
|
|
||||||
info: () => {},
|
|
||||||
warn: () => {},
|
|
||||||
error: () => {},
|
|
||||||
} as never,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
it("should NOT recompute nextRunAtMs for past-due jobs during maintenance", () => {
|
it("should NOT recompute nextRunAtMs for past-due jobs during maintenance", () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const pastDue = now - 60_000; // 1 minute ago
|
const pastDue = now - 60_000; // 1 minute ago
|
||||||
@@ -49,7 +23,7 @@ describe("issue #13992 regression - cron jobs skip execution", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = createMockState([job]);
|
const state = createMockCronStateForJobs({ jobs: [job], nowMs: now });
|
||||||
recomputeNextRunsForMaintenance(state);
|
recomputeNextRunsForMaintenance(state);
|
||||||
|
|
||||||
// Should not have changed the past-due nextRunAtMs
|
// Should not have changed the past-due nextRunAtMs
|
||||||
@@ -74,7 +48,7 @@ describe("issue #13992 regression - cron jobs skip execution", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = createMockState([job]);
|
const state = createMockCronStateForJobs({ jobs: [job], nowMs: now });
|
||||||
recomputeNextRunsForMaintenance(state);
|
recomputeNextRunsForMaintenance(state);
|
||||||
|
|
||||||
// Should have computed a nextRunAtMs
|
// Should have computed a nextRunAtMs
|
||||||
@@ -101,7 +75,7 @@ describe("issue #13992 regression - cron jobs skip execution", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = createMockState([job]);
|
const state = createMockCronStateForJobs({ jobs: [job], nowMs: now });
|
||||||
recomputeNextRunsForMaintenance(state);
|
recomputeNextRunsForMaintenance(state);
|
||||||
|
|
||||||
// Should have cleared nextRunAtMs for disabled job
|
// Should have cleared nextRunAtMs for disabled job
|
||||||
@@ -129,7 +103,7 @@ describe("issue #13992 regression - cron jobs skip execution", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = createMockState([job]);
|
const state = createMockCronStateForJobs({ jobs: [job], nowMs: now });
|
||||||
recomputeNextRunsForMaintenance(state);
|
recomputeNextRunsForMaintenance(state);
|
||||||
|
|
||||||
// Should have cleared stuck running marker
|
// Should have cleared stuck running marker
|
||||||
@@ -172,7 +146,7 @@ describe("issue #13992 regression - cron jobs skip execution", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = createMockState([dueJob, malformedJob]);
|
const state = createMockCronStateForJobs({ jobs: [dueJob, malformedJob], nowMs: now });
|
||||||
|
|
||||||
expect(() => recomputeNextRunsForMaintenance(state)).not.toThrow();
|
expect(() => recomputeNextRunsForMaintenance(state)).not.toThrow();
|
||||||
expect(dueJob.state.nextRunAtMs).toBe(pastDue);
|
expect(dueJob.state.nextRunAtMs).toBe(pastDue);
|
||||||
|
|||||||
@@ -1,26 +1,16 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { CronService } from "./service.js";
|
import { CronService } from "./service.js";
|
||||||
import { createStartedCronServiceWithFinishedBarrier } from "./service.test-harness.js";
|
import {
|
||||||
|
createStartedCronServiceWithFinishedBarrier,
|
||||||
|
setupCronServiceSuite,
|
||||||
|
} from "./service.test-harness.js";
|
||||||
|
|
||||||
const noopLogger = {
|
const { logger: noopLogger, makeStorePath } = setupCronServiceSuite({
|
||||||
debug: vi.fn(),
|
prefix: "openclaw-cron-16156-",
|
||||||
info: vi.fn(),
|
baseTimeIso: "2025-12-13T00:00:00.000Z",
|
||||||
warn: vi.fn(),
|
});
|
||||||
error: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let fixtureRoot = "";
|
|
||||||
let caseId = 0;
|
|
||||||
|
|
||||||
async function makeStorePath() {
|
|
||||||
const dir = path.join(fixtureRoot, `case-${caseId++}`);
|
|
||||||
const storePath = path.join(dir, "cron", "jobs.json");
|
|
||||||
await fs.mkdir(path.dirname(storePath), { recursive: true });
|
|
||||||
return { storePath };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeJobsStore(storePath: string, jobs: unknown[]) {
|
async function writeJobsStore(storePath: string, jobs: unknown[]) {
|
||||||
await fs.mkdir(path.dirname(storePath), { recursive: true });
|
await fs.mkdir(path.dirname(storePath), { recursive: true });
|
||||||
@@ -39,29 +29,6 @@ function createCronFromStorePath(storePath: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("#16156: cron.list() must not silently advance past-due recurring jobs", () => {
|
describe("#16156: cron.list() must not silently advance past-due recurring jobs", () => {
|
||||||
beforeAll(async () => {
|
|
||||||
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cron-16156-"));
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
if (fixtureRoot) {
|
|
||||||
await fs.rm(fixtureRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
vi.setSystemTime(new Date("2025-12-13T00:00:00.000Z"));
|
|
||||||
noopLogger.debug.mockClear();
|
|
||||||
noopLogger.info.mockClear();
|
|
||||||
noopLogger.warn.mockClear();
|
|
||||||
noopLogger.error.mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not skip a cron job when list() is called while the job is past-due", async () => {
|
it("does not skip a cron job when list() is called while the job is past-due", async () => {
|
||||||
const store = await makeStorePath();
|
const store = await makeStorePath();
|
||||||
const { cron, enqueueSystemEvent, finished } = createStartedCronServiceWithFinishedBarrier({
|
const { cron, enqueueSystemEvent, finished } = createStartedCronServiceWithFinishedBarrier({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { createMockCronStateForJobs } from "./service.test-harness.js";
|
||||||
import { recomputeNextRuns, recomputeNextRunsForMaintenance } from "./service/jobs.js";
|
import { recomputeNextRuns, recomputeNextRunsForMaintenance } from "./service/jobs.js";
|
||||||
import type { CronServiceState } from "./service/state.js";
|
|
||||||
import type { CronJob } from "./types.js";
|
import type { CronJob } from "./types.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,32 +19,6 @@ describe("issue #17852 - daily cron jobs should not skip days", () => {
|
|||||||
const HOUR_MS = 3_600_000;
|
const HOUR_MS = 3_600_000;
|
||||||
const DAY_MS = 24 * HOUR_MS;
|
const DAY_MS = 24 * HOUR_MS;
|
||||||
|
|
||||||
function createMockState(jobs: CronJob[], nowMs: number): CronServiceState {
|
|
||||||
return {
|
|
||||||
store: { version: 1, jobs },
|
|
||||||
running: false,
|
|
||||||
timer: null,
|
|
||||||
storeLoadedAtMs: nowMs,
|
|
||||||
storeFileMtimeMs: null,
|
|
||||||
op: Promise.resolve(),
|
|
||||||
warnedDisabled: false,
|
|
||||||
deps: {
|
|
||||||
storePath: "/mock/path",
|
|
||||||
cronEnabled: true,
|
|
||||||
nowMs: () => nowMs,
|
|
||||||
enqueueSystemEvent: () => {},
|
|
||||||
requestHeartbeatNow: () => {},
|
|
||||||
runIsolatedAgentJob: async () => ({ status: "ok" }),
|
|
||||||
log: {
|
|
||||||
debug: () => {},
|
|
||||||
info: () => {},
|
|
||||||
warn: () => {},
|
|
||||||
error: () => {},
|
|
||||||
} as never,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDailyThreeAmJob(threeAM: number): CronJob {
|
function createDailyThreeAmJob(threeAM: number): CronJob {
|
||||||
return {
|
return {
|
||||||
id: "daily-job",
|
id: "daily-job",
|
||||||
@@ -71,7 +45,7 @@ describe("issue #17852 - daily cron jobs should not skip days", () => {
|
|||||||
|
|
||||||
const job = createDailyThreeAmJob(threeAM);
|
const job = createDailyThreeAmJob(threeAM);
|
||||||
|
|
||||||
const state = createMockState([job], now);
|
const state = createMockCronStateForJobs({ jobs: [job], nowMs: now });
|
||||||
recomputeNextRunsForMaintenance(state);
|
recomputeNextRunsForMaintenance(state);
|
||||||
|
|
||||||
// Maintenance should NOT touch existing past-due nextRunAtMs.
|
// Maintenance should NOT touch existing past-due nextRunAtMs.
|
||||||
@@ -88,7 +62,7 @@ describe("issue #17852 - daily cron jobs should not skip days", () => {
|
|||||||
|
|
||||||
const job = createDailyThreeAmJob(threeAM);
|
const job = createDailyThreeAmJob(threeAM);
|
||||||
|
|
||||||
const state = createMockState([job], now);
|
const state = createMockCronStateForJobs({ jobs: [job], nowMs: now });
|
||||||
recomputeNextRuns(state);
|
recomputeNextRuns(state);
|
||||||
|
|
||||||
// The full recomputeNextRuns advances it to TOMORROW — skipping today's
|
// The full recomputeNextRuns advances it to TOMORROW — skipping today's
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import path from "node:path";
|
|||||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import * as schedule from "./schedule.js";
|
import * as schedule from "./schedule.js";
|
||||||
import { CronService } from "./service.js";
|
import { CronService } from "./service.js";
|
||||||
import { createRunningCronServiceState } from "./service.test-harness.js";
|
import { createDeferred, createRunningCronServiceState } from "./service.test-harness.js";
|
||||||
import { computeJobNextRunAtMs } from "./service/jobs.js";
|
import { computeJobNextRunAtMs } from "./service/jobs.js";
|
||||||
import { createCronServiceState, type CronEvent } from "./service/state.js";
|
import { createCronServiceState, type CronEvent } from "./service/state.js";
|
||||||
import { onTimer } from "./service/timer.js";
|
import { onTimer } from "./service/timer.js";
|
||||||
@@ -38,16 +38,6 @@ async function makeStorePath() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDeferred<T>() {
|
|
||||||
let resolve!: (value: T) => void;
|
|
||||||
let reject!: (reason?: unknown) => void;
|
|
||||||
const promise = new Promise<T>((res, rej) => {
|
|
||||||
resolve = res;
|
|
||||||
reject = rej;
|
|
||||||
});
|
|
||||||
return { promise, resolve, reject };
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDueIsolatedJob(params: {
|
function createDueIsolatedJob(params: {
|
||||||
id: string;
|
id: string;
|
||||||
nowMs: number;
|
nowMs: number;
|
||||||
@@ -563,7 +553,6 @@ describe("Cron issue regressions", () => {
|
|||||||
|
|
||||||
let now = scheduledAt;
|
let now = scheduledAt;
|
||||||
let fireCount = 0;
|
let fireCount = 0;
|
||||||
const events: CronEvent[] = [];
|
|
||||||
const state = createCronServiceState({
|
const state = createCronServiceState({
|
||||||
cronEnabled: true,
|
cronEnabled: true,
|
||||||
storePath: store.storePath,
|
storePath: store.storePath,
|
||||||
@@ -571,9 +560,6 @@ describe("Cron issue regressions", () => {
|
|||||||
nowMs: () => now,
|
nowMs: () => now,
|
||||||
enqueueSystemEvent: vi.fn(),
|
enqueueSystemEvent: vi.fn(),
|
||||||
requestHeartbeatNow: vi.fn(),
|
requestHeartbeatNow: vi.fn(),
|
||||||
onEvent: (evt) => {
|
|
||||||
events.push(evt);
|
|
||||||
},
|
|
||||||
runIsolatedAgentJob: vi.fn(async () => {
|
runIsolatedAgentJob: vi.fn(async () => {
|
||||||
// Job completes very quickly (7ms) — still within the same second
|
// Job completes very quickly (7ms) — still within the same second
|
||||||
now += 7;
|
now += 7;
|
||||||
|
|||||||
@@ -2,16 +2,10 @@ import fs from "node:fs/promises";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { CronService } from "./service.js";
|
import { CronService } from "./service.js";
|
||||||
import {
|
import { setupCronServiceSuite } from "./service.test-harness.js";
|
||||||
createCronStoreHarness,
|
|
||||||
createNoopLogger,
|
|
||||||
installCronTestHooks,
|
|
||||||
} from "./service.test-harness.js";
|
|
||||||
|
|
||||||
const noopLogger = createNoopLogger();
|
const { logger: noopLogger, makeStorePath } = setupCronServiceSuite({
|
||||||
const { makeStorePath } = createCronStoreHarness({ prefix: "openclaw-cron-" });
|
prefix: "openclaw-cron-",
|
||||||
installCronTestHooks({
|
|
||||||
logger: noopLogger,
|
|
||||||
baseTimeIso: "2025-12-13T17:00:00.000Z",
|
baseTimeIso: "2025-12-13T17:00:00.000Z",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
import type { HeartbeatRunResult } from "../infra/heartbeat-wake.js";
|
import type { HeartbeatRunResult } from "../infra/heartbeat-wake.js";
|
||||||
import type { CronEvent, CronServiceDeps } from "./service.js";
|
import type { CronEvent, CronServiceDeps } from "./service.js";
|
||||||
import { CronService } from "./service.js";
|
import { CronService } from "./service.js";
|
||||||
import { createNoopLogger, installCronTestHooks } from "./service.test-harness.js";
|
import { createDeferred, createNoopLogger, installCronTestHooks } from "./service.test-harness.js";
|
||||||
|
|
||||||
const noopLogger = createNoopLogger();
|
const noopLogger = createNoopLogger();
|
||||||
installCronTestHooks({ logger: noopLogger });
|
installCronTestHooks({ logger: noopLogger });
|
||||||
@@ -196,16 +196,6 @@ beforeEach(() => {
|
|||||||
ensureDir(fixturesRoot);
|
ensureDir(fixturesRoot);
|
||||||
});
|
});
|
||||||
|
|
||||||
function createDeferred<T>() {
|
|
||||||
let resolve!: (value: T) => void;
|
|
||||||
let reject!: (reason?: unknown) => void;
|
|
||||||
const promise = new Promise<T>((res, rej) => {
|
|
||||||
resolve = res;
|
|
||||||
reject = rej;
|
|
||||||
});
|
|
||||||
return { promise, resolve, reject };
|
|
||||||
}
|
|
||||||
|
|
||||||
function createCronEventHarness() {
|
function createCronEventHarness() {
|
||||||
const events: CronEvent[] = [];
|
const events: CronEvent[] = [];
|
||||||
const waiters: Array<{
|
const waiters: Array<{
|
||||||
|
|||||||
@@ -2,16 +2,10 @@ import fs from "node:fs/promises";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { CronService } from "./service.js";
|
import { CronService } from "./service.js";
|
||||||
import {
|
import { setupCronServiceSuite } from "./service.test-harness.js";
|
||||||
createCronStoreHarness,
|
|
||||||
createNoopLogger,
|
|
||||||
installCronTestHooks,
|
|
||||||
} from "./service.test-harness.js";
|
|
||||||
|
|
||||||
const noopLogger = createNoopLogger();
|
const { logger: noopLogger, makeStorePath } = setupCronServiceSuite({
|
||||||
const { makeStorePath } = createCronStoreHarness({ prefix: "openclaw-cron-" });
|
prefix: "openclaw-cron-",
|
||||||
installCronTestHooks({
|
|
||||||
logger: noopLogger,
|
|
||||||
baseTimeIso: "2026-02-06T17:00:00.000Z",
|
baseTimeIso: "2026-02-06T17:00:00.000Z",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { afterAll, afterEach, beforeAll, beforeEach, vi } from "vitest";
|
|||||||
import type { MockFn } from "../test-utils/vitest-mock-fn.js";
|
import type { MockFn } from "../test-utils/vitest-mock-fn.js";
|
||||||
import type { CronEvent } from "./service.js";
|
import type { CronEvent } from "./service.js";
|
||||||
import { CronService } from "./service.js";
|
import { CronService } from "./service.js";
|
||||||
import { createCronServiceState } from "./service/state.js";
|
import { createCronServiceState, type CronServiceState } from "./service/state.js";
|
||||||
import type { CronJob } from "./types.js";
|
import type { CronJob } from "./types.js";
|
||||||
|
|
||||||
export type NoopLogger = {
|
export type NoopLogger = {
|
||||||
@@ -85,6 +85,16 @@ export function installCronTestHooks(options: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setupCronServiceSuite(options?: { prefix?: string; baseTimeIso?: string }) {
|
||||||
|
const logger = createNoopLogger();
|
||||||
|
const { makeStorePath } = createCronStoreHarness({ prefix: options?.prefix });
|
||||||
|
installCronTestHooks({
|
||||||
|
logger,
|
||||||
|
baseTimeIso: options?.baseTimeIso,
|
||||||
|
});
|
||||||
|
return { logger, makeStorePath };
|
||||||
|
}
|
||||||
|
|
||||||
export function createFinishedBarrier() {
|
export function createFinishedBarrier() {
|
||||||
const resolvers = new Map<string, (evt: CronEvent) => void>();
|
const resolvers = new Map<string, (evt: CronEvent) => void>();
|
||||||
return {
|
return {
|
||||||
@@ -152,3 +162,43 @@ export function createRunningCronServiceState(params: {
|
|||||||
};
|
};
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createDeferred<T>() {
|
||||||
|
let resolve!: (value: T) => void;
|
||||||
|
let reject!: (reason?: unknown) => void;
|
||||||
|
const promise = new Promise<T>((res, rej) => {
|
||||||
|
resolve = res;
|
||||||
|
reject = rej;
|
||||||
|
});
|
||||||
|
return { promise, resolve, reject };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMockCronStateForJobs(params: {
|
||||||
|
jobs: CronJob[];
|
||||||
|
nowMs?: number;
|
||||||
|
}): CronServiceState {
|
||||||
|
const nowMs = params.nowMs ?? Date.now();
|
||||||
|
return {
|
||||||
|
store: { version: 1, jobs: params.jobs },
|
||||||
|
running: false,
|
||||||
|
timer: null,
|
||||||
|
storeLoadedAtMs: nowMs,
|
||||||
|
storeFileMtimeMs: null,
|
||||||
|
op: Promise.resolve(),
|
||||||
|
warnedDisabled: false,
|
||||||
|
deps: {
|
||||||
|
storePath: "/mock/path",
|
||||||
|
cronEnabled: true,
|
||||||
|
nowMs: () => nowMs,
|
||||||
|
enqueueSystemEvent: () => {},
|
||||||
|
requestHeartbeatNow: () => {},
|
||||||
|
runIsolatedAgentJob: async () => ({ status: "ok" }),
|
||||||
|
log: {
|
||||||
|
debug: () => {},
|
||||||
|
info: () => {},
|
||||||
|
warn: () => {},
|
||||||
|
error: () => {},
|
||||||
|
} as never,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { telegramPlugin } from "../../extensions/telegram/src/channel.js";
|
|
||||||
import { setTelegramRuntime } from "../../extensions/telegram/src/runtime.js";
|
|
||||||
import * as replyModule from "../auto-reply/reply.js";
|
import * as replyModule from "../auto-reply/reply.js";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
|
||||||
import { createPluginRuntime } from "../plugins/runtime/index.js";
|
|
||||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
|
||||||
import { runHeartbeatOnce } from "./heartbeat-runner.js";
|
import { runHeartbeatOnce } from "./heartbeat-runner.js";
|
||||||
import { seedMainSessionStore, withTempHeartbeatSandbox } from "./heartbeat-runner.test-utils.js";
|
import {
|
||||||
|
seedMainSessionStore,
|
||||||
|
setupTelegramHeartbeatPluginRuntimeForTests,
|
||||||
|
withTempHeartbeatSandbox,
|
||||||
|
} from "./heartbeat-runner.test-utils.js";
|
||||||
import { enqueueSystemEvent, resetSystemEventsForTest } from "./system-events.js";
|
import { enqueueSystemEvent, resetSystemEventsForTest } from "./system-events.js";
|
||||||
|
|
||||||
// Avoid pulling optional runtime deps during isolated runs.
|
// Avoid pulling optional runtime deps during isolated runs.
|
||||||
vi.mock("jiti", () => ({ createJiti: () => () => ({}) }));
|
vi.mock("jiti", () => ({ createJiti: () => () => ({}) }));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const runtime = createPluginRuntime();
|
setupTelegramHeartbeatPluginRuntimeForTests();
|
||||||
setTelegramRuntime(runtime);
|
|
||||||
setActivePluginRegistry(
|
|
||||||
createTestRegistry([{ pluginId: "telegram", plugin: telegramPlugin, source: "test" }]),
|
|
||||||
);
|
|
||||||
resetSystemEventsForTest();
|
resetSystemEventsForTest();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,14 @@ 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 { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
import { telegramPlugin } from "../../extensions/telegram/src/channel.js";
|
||||||
|
import { setTelegramRuntime } from "../../extensions/telegram/src/runtime.js";
|
||||||
import * as replyModule from "../auto-reply/reply.js";
|
import * as replyModule from "../auto-reply/reply.js";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { resolveMainSessionKey } from "../config/sessions.js";
|
import { resolveMainSessionKey } from "../config/sessions.js";
|
||||||
|
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||||
|
import { createPluginRuntime } from "../plugins/runtime/index.js";
|
||||||
|
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||||
|
|
||||||
export type HeartbeatSessionSeed = {
|
export type HeartbeatSessionSeed = {
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
@@ -91,3 +96,11 @@ export async function withTempTelegramHeartbeatSandbox<T>(
|
|||||||
unsetEnvVars: ["TELEGRAM_BOT_TOKEN"],
|
unsetEnvVars: ["TELEGRAM_BOT_TOKEN"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setupTelegramHeartbeatPluginRuntimeForTests() {
|
||||||
|
const runtime = createPluginRuntime();
|
||||||
|
setTelegramRuntime(runtime);
|
||||||
|
setActivePluginRegistry(
|
||||||
|
createTestRegistry([{ pluginId: "telegram", plugin: telegramPlugin, source: "test" }]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
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 { telegramPlugin } from "../../extensions/telegram/src/channel.js";
|
|
||||||
import { setTelegramRuntime } from "../../extensions/telegram/src/runtime.js";
|
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { resolveMainSessionKey } from "../config/sessions.js";
|
import { resolveMainSessionKey } from "../config/sessions.js";
|
||||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
|
||||||
import { createPluginRuntime } from "../plugins/runtime/index.js";
|
|
||||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
|
||||||
import { runHeartbeatOnce } from "./heartbeat-runner.js";
|
import { runHeartbeatOnce } from "./heartbeat-runner.js";
|
||||||
import {
|
import {
|
||||||
seedSessionStore,
|
seedSessionStore,
|
||||||
|
setupTelegramHeartbeatPluginRuntimeForTests,
|
||||||
withTempTelegramHeartbeatSandbox,
|
withTempTelegramHeartbeatSandbox,
|
||||||
} from "./heartbeat-runner.test-utils.js";
|
} from "./heartbeat-runner.test-utils.js";
|
||||||
|
|
||||||
@@ -18,11 +14,7 @@ import {
|
|||||||
vi.mock("jiti", () => ({ createJiti: () => () => ({}) }));
|
vi.mock("jiti", () => ({ createJiti: () => () => ({}) }));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const runtime = createPluginRuntime();
|
setupTelegramHeartbeatPluginRuntimeForTests();
|
||||||
setTelegramRuntime(runtime);
|
|
||||||
setActivePluginRegistry(
|
|
||||||
createTestRegistry([{ pluginId: "telegram", plugin: telegramPlugin, source: "test" }]),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("heartbeat transcript pruning", () => {
|
describe("heartbeat transcript pruning", () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { withTempDir } from "../test-utils/temp-dir.js";
|
||||||
import {
|
import {
|
||||||
getChannelActivity,
|
getChannelActivity,
|
||||||
recordChannelActivity,
|
recordChannelActivity,
|
||||||
@@ -20,15 +20,6 @@ import {
|
|||||||
setVoiceWakeTriggers,
|
setVoiceWakeTriggers,
|
||||||
} from "./voicewake.js";
|
} from "./voicewake.js";
|
||||||
|
|
||||||
async function withTempDir(prefix: string, run: (dir: string) => Promise<void>) {
|
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
||||||
try {
|
|
||||||
await run(dir);
|
|
||||||
} finally {
|
|
||||||
await fs.rm(dir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("infra store", () => {
|
describe("infra store", () => {
|
||||||
describe("state migrations fs", () => {
|
describe("state migrations fs", () => {
|
||||||
it("treats array session stores as invalid", async () => {
|
it("treats array session stores as invalid", async () => {
|
||||||
|
|||||||
11
src/test-utils/model-fallback.mock.ts
Normal file
11
src/test-utils/model-fallback.mock.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export async function runWithModelFallback(params: {
|
||||||
|
provider: string;
|
||||||
|
model: string;
|
||||||
|
run: (provider: string, model: string) => Promise<unknown>;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
result: await params.run(params.provider, params.model),
|
||||||
|
provider: params.provider,
|
||||||
|
model: params.model,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user