mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 21:08:25 +00:00
test: dedupe cron and slack monitor test harness setup
This commit is contained in:
@@ -11,6 +11,12 @@ const noopLogger = {
|
|||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type IsolatedRunResult = {
|
||||||
|
status: "ok" | "error" | "skipped";
|
||||||
|
summary?: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
||||||
let timeout: NodeJS.Timeout | undefined;
|
let timeout: NodeJS.Timeout | undefined;
|
||||||
try {
|
try {
|
||||||
@@ -48,6 +54,27 @@ async function makeStorePath() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createDeferredIsolatedRun() {
|
||||||
|
let resolveRun: ((value: IsolatedRunResult) => void) | undefined;
|
||||||
|
let resolveRunStarted: (() => void) | undefined;
|
||||||
|
const runStarted = new Promise<void>((resolve) => {
|
||||||
|
resolveRunStarted = resolve;
|
||||||
|
});
|
||||||
|
const runIsolatedAgentJob = vi.fn(async () => {
|
||||||
|
resolveRunStarted?.();
|
||||||
|
return await new Promise<IsolatedRunResult>((resolve) => {
|
||||||
|
resolveRun = resolve;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
runIsolatedAgentJob,
|
||||||
|
runStarted,
|
||||||
|
completeRun: (result: IsolatedRunResult) => {
|
||||||
|
resolveRun?.(result);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
describe("CronService read ops while job is running", () => {
|
describe("CronService read ops while job is running", () => {
|
||||||
it("keeps list and status responsive during a long isolated run", async () => {
|
it("keeps list and status responsive during a long isolated run", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
@@ -60,25 +87,7 @@ describe("CronService read ops while job is running", () => {
|
|||||||
resolveFinished = resolve;
|
resolveFinished = resolve;
|
||||||
});
|
});
|
||||||
|
|
||||||
let resolveRun:
|
const isolatedRun = createDeferredIsolatedRun();
|
||||||
| ((value: { status: "ok" | "error" | "skipped"; summary?: string; error?: string }) => void)
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
let resolveRunStarted: (() => void) | undefined;
|
|
||||||
const runStarted = new Promise<void>((resolve) => {
|
|
||||||
resolveRunStarted = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
const runIsolatedAgentJob = vi.fn(async () => {
|
|
||||||
resolveRunStarted?.();
|
|
||||||
return await new Promise<{
|
|
||||||
status: "ok" | "error" | "skipped";
|
|
||||||
summary?: string;
|
|
||||||
error?: string;
|
|
||||||
}>((resolve) => {
|
|
||||||
resolveRun = resolve;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const cron = new CronService({
|
const cron = new CronService({
|
||||||
storePath: store.storePath,
|
storePath: store.storePath,
|
||||||
@@ -86,7 +95,7 @@ describe("CronService read ops while job is running", () => {
|
|||||||
log: noopLogger,
|
log: noopLogger,
|
||||||
enqueueSystemEvent,
|
enqueueSystemEvent,
|
||||||
requestHeartbeatNow,
|
requestHeartbeatNow,
|
||||||
runIsolatedAgentJob,
|
runIsolatedAgentJob: isolatedRun.runIsolatedAgentJob,
|
||||||
onEvent: (evt) => {
|
onEvent: (evt) => {
|
||||||
if (evt.action === "finished" && evt.status === "ok") {
|
if (evt.action === "finished" && evt.status === "ok") {
|
||||||
resolveFinished?.();
|
resolveFinished?.();
|
||||||
@@ -115,8 +124,8 @@ describe("CronService read ops while job is running", () => {
|
|||||||
vi.setSystemTime(new Date("2025-12-13T00:00:01.000Z"));
|
vi.setSystemTime(new Date("2025-12-13T00:00:01.000Z"));
|
||||||
await vi.runOnlyPendingTimersAsync();
|
await vi.runOnlyPendingTimersAsync();
|
||||||
|
|
||||||
await runStarted;
|
await isolatedRun.runStarted;
|
||||||
expect(runIsolatedAgentJob).toHaveBeenCalledTimes(1);
|
expect(isolatedRun.runIsolatedAgentJob).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
await expect(cron.list({ includeDisabled: true })).resolves.toBeTypeOf("object");
|
await expect(cron.list({ includeDisabled: true })).resolves.toBeTypeOf("object");
|
||||||
await expect(cron.status()).resolves.toBeTypeOf("object");
|
await expect(cron.status()).resolves.toBeTypeOf("object");
|
||||||
@@ -124,7 +133,7 @@ describe("CronService read ops while job is running", () => {
|
|||||||
const running = await cron.list({ includeDisabled: true });
|
const running = await cron.list({ includeDisabled: true });
|
||||||
expect(running[0]?.state.runningAtMs).toBeTypeOf("number");
|
expect(running[0]?.state.runningAtMs).toBeTypeOf("number");
|
||||||
|
|
||||||
resolveRun?.({ status: "ok", summary: "done" });
|
isolatedRun.completeRun({ status: "ok", summary: "done" });
|
||||||
|
|
||||||
// Wait until the scheduler writes the result back to the store.
|
// Wait until the scheduler writes the result back to the store.
|
||||||
await finished;
|
await finished;
|
||||||
@@ -182,24 +191,7 @@ describe("CronService read ops while job is running", () => {
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
|
||||||
let resolveRun:
|
const isolatedRun = createDeferredIsolatedRun();
|
||||||
| ((value: { status: "ok" | "error" | "skipped"; summary?: string; error?: string }) => void)
|
|
||||||
| undefined;
|
|
||||||
let resolveRunStarted: (() => void) | undefined;
|
|
||||||
const runStarted = new Promise<void>((resolve) => {
|
|
||||||
resolveRunStarted = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
const runIsolatedAgentJob = vi.fn(async () => {
|
|
||||||
resolveRunStarted?.();
|
|
||||||
return await new Promise<{
|
|
||||||
status: "ok" | "error" | "skipped";
|
|
||||||
summary?: string;
|
|
||||||
error?: string;
|
|
||||||
}>((resolve) => {
|
|
||||||
resolveRun = resolve;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const cron = new CronService({
|
const cron = new CronService({
|
||||||
storePath: store.storePath,
|
storePath: store.storePath,
|
||||||
@@ -208,12 +200,13 @@ describe("CronService read ops while job is running", () => {
|
|||||||
nowMs: () => nowMs,
|
nowMs: () => nowMs,
|
||||||
enqueueSystemEvent,
|
enqueueSystemEvent,
|
||||||
requestHeartbeatNow,
|
requestHeartbeatNow,
|
||||||
runIsolatedAgentJob,
|
runIsolatedAgentJob: isolatedRun.runIsolatedAgentJob,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const startPromise = cron.start();
|
const startPromise = cron.start();
|
||||||
await runStarted;
|
await isolatedRun.runStarted;
|
||||||
|
expect(isolatedRun.runIsolatedAgentJob).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
withTimeout(cron.list({ includeDisabled: true }), 300, "cron.list during startup"),
|
withTimeout(cron.list({ includeDisabled: true }), 300, "cron.list during startup"),
|
||||||
@@ -222,7 +215,7 @@ describe("CronService read ops while job is running", () => {
|
|||||||
expect.objectContaining({ enabled: true, storePath: store.storePath }),
|
expect.objectContaining({ enabled: true, storePath: store.storePath }),
|
||||||
);
|
);
|
||||||
|
|
||||||
resolveRun?.({ status: "ok", summary: "done" });
|
isolatedRun.completeRun({ status: "ok", summary: "done" });
|
||||||
await startPromise;
|
await startPromise;
|
||||||
|
|
||||||
const jobs = await cron.list({ includeDisabled: true });
|
const jobs = await cron.list({ includeDisabled: true });
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ function createArgMenusHarness() {
|
|||||||
const commands = new Map<string, (args: unknown) => Promise<void>>();
|
const commands = new Map<string, (args: unknown) => Promise<void>>();
|
||||||
const actions = new Map<string, (args: unknown) => Promise<void>>();
|
const actions = new Map<string, (args: unknown) => Promise<void>>();
|
||||||
const options = new Map<string, (args: unknown) => Promise<void>>();
|
const options = new Map<string, (args: unknown) => Promise<void>>();
|
||||||
|
const optionsReceiverContexts: unknown[] = [];
|
||||||
|
|
||||||
const postEphemeral = vi.fn().mockResolvedValue({ ok: true });
|
const postEphemeral = vi.fn().mockResolvedValue({ ok: true });
|
||||||
const app = {
|
const app = {
|
||||||
@@ -226,7 +227,8 @@ function createArgMenusHarness() {
|
|||||||
action: (id: string, handler: (args: unknown) => Promise<void>) => {
|
action: (id: string, handler: (args: unknown) => Promise<void>) => {
|
||||||
actions.set(id, handler);
|
actions.set(id, handler);
|
||||||
},
|
},
|
||||||
options: (id: string, handler: (args: unknown) => Promise<void>) => {
|
options: function (this: unknown, id: string, handler: (args: unknown) => Promise<void>) {
|
||||||
|
optionsReceiverContexts.push(this);
|
||||||
options.set(id, handler);
|
options.set(id, handler);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -264,7 +266,16 @@ function createArgMenusHarness() {
|
|||||||
config: { commands: { native: true, nativeSkills: false } },
|
config: { commands: { native: true, nativeSkills: false } },
|
||||||
} as unknown;
|
} as unknown;
|
||||||
|
|
||||||
return { commands, actions, options, postEphemeral, ctx, account };
|
return {
|
||||||
|
commands,
|
||||||
|
actions,
|
||||||
|
options,
|
||||||
|
optionsReceiverContexts,
|
||||||
|
postEphemeral,
|
||||||
|
ctx,
|
||||||
|
account,
|
||||||
|
app,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function requireHandler(
|
function requireHandler(
|
||||||
@@ -379,59 +390,12 @@ describe("Slack native command argument menus", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("registers options handlers without losing app receiver binding", async () => {
|
it("registers options handlers without losing app receiver binding", async () => {
|
||||||
const commands = new Map<string, (args: unknown) => Promise<void>>();
|
const testHarness = createArgMenusHarness();
|
||||||
const actions = new Map<string, (args: unknown) => Promise<void>>();
|
await registerCommands(testHarness.ctx, testHarness.account);
|
||||||
const options = new Map<string, (args: unknown) => Promise<void>>();
|
expect(testHarness.commands.size).toBeGreaterThan(0);
|
||||||
const postEphemeral = vi.fn().mockResolvedValue({ ok: true });
|
expect(testHarness.actions.has("openclaw_cmdarg")).toBe(true);
|
||||||
const app = {
|
expect(testHarness.options.has("openclaw_cmdarg")).toBe(true);
|
||||||
client: { chat: { postEphemeral } },
|
expect(testHarness.optionsReceiverContexts[0]).toBe(testHarness.app);
|
||||||
command: (name: string, handler: (args: unknown) => Promise<void>) => {
|
|
||||||
commands.set(name, handler);
|
|
||||||
},
|
|
||||||
action: (id: string, handler: (args: unknown) => Promise<void>) => {
|
|
||||||
actions.set(id, handler);
|
|
||||||
},
|
|
||||||
options: function (this: unknown, id: string, handler: (args: unknown) => Promise<void>) {
|
|
||||||
expect(this).toBe(app);
|
|
||||||
options.set(id, handler);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const ctx = {
|
|
||||||
cfg: { commands: { native: true, nativeSkills: false } },
|
|
||||||
runtime: {},
|
|
||||||
botToken: "bot-token",
|
|
||||||
botUserId: "bot",
|
|
||||||
teamId: "T1",
|
|
||||||
allowFrom: ["*"],
|
|
||||||
dmEnabled: true,
|
|
||||||
dmPolicy: "open",
|
|
||||||
groupDmEnabled: false,
|
|
||||||
groupDmChannels: [],
|
|
||||||
defaultRequireMention: true,
|
|
||||||
groupPolicy: "open",
|
|
||||||
useAccessGroups: false,
|
|
||||||
channelsConfig: undefined,
|
|
||||||
slashCommand: {
|
|
||||||
enabled: true,
|
|
||||||
name: "openclaw",
|
|
||||||
ephemeral: true,
|
|
||||||
sessionPrefix: "slack:slash",
|
|
||||||
},
|
|
||||||
textLimit: 4000,
|
|
||||||
app,
|
|
||||||
isChannelAllowed: () => true,
|
|
||||||
resolveChannelName: async () => ({ name: "dm", type: "im" }),
|
|
||||||
resolveUserName: async () => ({ name: "Ada" }),
|
|
||||||
} as unknown;
|
|
||||||
const account = {
|
|
||||||
accountId: "acct",
|
|
||||||
config: { commands: { native: true, nativeSkills: false } },
|
|
||||||
} as unknown;
|
|
||||||
|
|
||||||
await registerCommands(ctx, account);
|
|
||||||
expect(commands.size).toBeGreaterThan(0);
|
|
||||||
expect(actions.has("openclaw_cmdarg")).toBe(true);
|
|
||||||
expect(options.has("openclaw_cmdarg")).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows a button menu when required args are omitted", async () => {
|
it("shows a button menu when required args are omitted", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user