mirror of https://github.com/openclaw/openclaw.git
test: consolidate trigger-handling status and heartbeat scenarios
This commit is contained in:
parent
a8a4fa5b88
commit
f7e45ce947
|
|
@ -122,15 +122,19 @@ describe("trigger handling", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
it("emits /status once (no duplicate inline + final)", async () => {
|
||||
it("reports /status once without invoking the agent", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||
const { blockReplies, replies } = await runCommandAndCollectReplies({
|
||||
home,
|
||||
body: "/status",
|
||||
});
|
||||
expect(blockReplies.length).toBe(0);
|
||||
expect(replies.length).toBe(1);
|
||||
expect(String(replies[0]?.text ?? "")).toContain("Model:");
|
||||
const text = String(replies[0]?.text ?? "");
|
||||
expect(text).toContain("Model:");
|
||||
expect(text).toContain("OpenClaw");
|
||||
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
it("sets per-response usage footer via /usage", async () => {
|
||||
|
|
@ -255,39 +259,22 @@ describe("trigger handling", () => {
|
|||
expect(prompt).not.toContain("/status");
|
||||
});
|
||||
});
|
||||
it("aborts even with timestamp prefix", async () => {
|
||||
it("handles /stop command variants without invoking the agent", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
await expectStopAbortWithoutAgent({
|
||||
home,
|
||||
body: "[Dec 5 10:00] stop",
|
||||
from: "+1000",
|
||||
});
|
||||
});
|
||||
});
|
||||
it("handles /stop without invoking the agent", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
await expectStopAbortWithoutAgent({
|
||||
home,
|
||||
body: "/stop",
|
||||
from: "+1003",
|
||||
});
|
||||
for (const testCase of [
|
||||
{ body: "[Dec 5 10:00] stop", from: "+1000" },
|
||||
{ body: "/stop", from: "+1003" },
|
||||
] as const) {
|
||||
await expectStopAbortWithoutAgent({ home, body: testCase.body, from: testCase.from });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("shows endpoint default in /model status when not configured", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const cfg = makeCfg(home);
|
||||
const res = await getReplyFromConfig(modelStatusCtx, {}, cfg);
|
||||
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(normalizeTestText(text ?? "")).toContain("endpoint: default");
|
||||
});
|
||||
});
|
||||
|
||||
it("includes endpoint details in /model status when configured", async () => {
|
||||
it("shows model status defaults and configured endpoint details", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const defaultCfg = makeCfg(home);
|
||||
const cfg = {
|
||||
...makeCfg(home),
|
||||
...defaultCfg,
|
||||
models: {
|
||||
providers: {
|
||||
minimax: {
|
||||
|
|
@ -297,20 +284,27 @@ describe("trigger handling", () => {
|
|||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
const res = await getReplyFromConfig(modelStatusCtx, {}, cfg);
|
||||
const defaultStatus = await getReplyFromConfig(modelStatusCtx, {}, defaultCfg);
|
||||
const configuredStatus = await getReplyFromConfig(modelStatusCtx, {}, cfg);
|
||||
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
const normalized = normalizeTestText(text ?? "");
|
||||
expect(normalized).toContain(
|
||||
expect(
|
||||
normalizeTestText(
|
||||
(Array.isArray(defaultStatus) ? defaultStatus[0]?.text : defaultStatus?.text) ?? "",
|
||||
),
|
||||
).toContain("endpoint: default");
|
||||
const configuredText = Array.isArray(configuredStatus)
|
||||
? configuredStatus[0]?.text
|
||||
: configuredStatus?.text;
|
||||
expect(normalizeTestText(configuredText ?? "")).toContain(
|
||||
"[minimax] endpoint: https://api.minimax.io/anthropic api: anthropic-messages auth:",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("restarts by default", async () => {
|
||||
it("restarts by default and rejects /restart when disabled", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||
const res = await getReplyFromConfig(
|
||||
const enabledRes = await getReplyFromConfig(
|
||||
{
|
||||
Body: " [Dec 5] /restart",
|
||||
From: "+1001",
|
||||
|
|
@ -320,17 +314,13 @@ describe("trigger handling", () => {
|
|||
{},
|
||||
makeCfg(home),
|
||||
);
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text?.startsWith("⚙️ Restarting") || text?.startsWith("⚠️ Restart failed")).toBe(true);
|
||||
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
const enabledText = Array.isArray(enabledRes) ? enabledRes[0]?.text : enabledRes?.text;
|
||||
expect(
|
||||
enabledText?.startsWith("⚙️ Restarting") || enabledText?.startsWith("⚠️ Restart failed"),
|
||||
).toBe(true);
|
||||
|
||||
it("rejects /restart when explicitly disabled", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||
const cfg = { ...makeCfg(home), commands: { restart: false } } as OpenClawConfig;
|
||||
const res = await getReplyFromConfig(
|
||||
const disabledCfg = { ...makeCfg(home), commands: { restart: false } } as OpenClawConfig;
|
||||
const disabledRes = await getReplyFromConfig(
|
||||
{
|
||||
Body: "/restart",
|
||||
From: "+1001",
|
||||
|
|
@ -338,29 +328,11 @@ describe("trigger handling", () => {
|
|||
CommandAuthorized: true,
|
||||
},
|
||||
{},
|
||||
cfg,
|
||||
disabledCfg,
|
||||
);
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toContain("/restart is disabled");
|
||||
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("reports status without invoking the agent", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
Body: "/status",
|
||||
From: "+1002",
|
||||
To: "+2000",
|
||||
CommandAuthorized: true,
|
||||
},
|
||||
{},
|
||||
makeCfg(home),
|
||||
);
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toContain("OpenClaw");
|
||||
const disabledText = Array.isArray(disabledRes) ? disabledRes[0]?.text : disabledRes?.text;
|
||||
expect(disabledText).toContain("/restart is disabled");
|
||||
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import fs from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
|
@ -91,72 +90,66 @@ describe("trigger handling", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("uses heartbeat model override for heartbeat runs", async () => {
|
||||
it("uses heartbeat override when configured and falls back to stored model override", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const runEmbeddedPiAgentMock = mockEmbeddedOkPayload();
|
||||
const cfg = makeCfg(home);
|
||||
await writeStoredModelOverride(cfg);
|
||||
cfg.agents = {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
heartbeat: { model: "anthropic/claude-haiku-4-5-20251001" },
|
||||
const cases = [
|
||||
{
|
||||
label: "heartbeat-override",
|
||||
setup: (cfg: ReturnType<typeof makeCfg>) => {
|
||||
cfg.agents = {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
heartbeat: { model: "anthropic/claude-haiku-4-5-20251001" },
|
||||
},
|
||||
};
|
||||
},
|
||||
expected: { provider: "anthropic", model: "claude-haiku-4-5-20251001" },
|
||||
},
|
||||
};
|
||||
{
|
||||
label: "stored-override",
|
||||
setup: () => undefined,
|
||||
expected: { provider: "openai", model: "gpt-5.2" },
|
||||
},
|
||||
] as const;
|
||||
|
||||
await getReplyFromConfig(BASE_MESSAGE, { isHeartbeat: true }, cfg);
|
||||
for (const testCase of cases) {
|
||||
runEmbeddedPiAgentMock.mockClear();
|
||||
const cfg = makeCfg(home);
|
||||
cfg.session = { ...cfg.session, store: join(home, `${testCase.label}.sessions.json`) };
|
||||
await writeStoredModelOverride(cfg);
|
||||
testCase.setup(cfg);
|
||||
await getReplyFromConfig(BASE_MESSAGE, { isHeartbeat: true }, cfg);
|
||||
|
||||
const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0];
|
||||
expect(call?.provider).toBe("anthropic");
|
||||
expect(call?.model).toBe("claude-haiku-4-5-20251001");
|
||||
const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0];
|
||||
expect(call?.provider).toBe(testCase.expected.provider);
|
||||
expect(call?.model).toBe(testCase.expected.model);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps stored model override for heartbeat runs when heartbeat model is not configured", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const runEmbeddedPiAgentMock = mockEmbeddedOkPayload();
|
||||
const cfg = makeCfg(home);
|
||||
await writeStoredModelOverride(cfg);
|
||||
await getReplyFromConfig(BASE_MESSAGE, { isHeartbeat: true }, cfg);
|
||||
|
||||
const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0];
|
||||
expect(call?.provider).toBe("openai");
|
||||
expect(call?.model).toBe("gpt-5.2");
|
||||
});
|
||||
});
|
||||
|
||||
it("suppresses HEARTBEAT_OK replies outside heartbeat runs", async () => {
|
||||
it("suppresses or strips HEARTBEAT_OK replies outside heartbeat runs", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||
runEmbeddedPiAgentMock.mockResolvedValue({
|
||||
payloads: [{ text: HEARTBEAT_TOKEN }],
|
||||
meta: {
|
||||
durationMs: 1,
|
||||
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||
},
|
||||
});
|
||||
const cases = [
|
||||
{ text: HEARTBEAT_TOKEN, expected: undefined },
|
||||
{ text: `${HEARTBEAT_TOKEN} hello`, expected: "hello" },
|
||||
] as const;
|
||||
|
||||
const res = await getReplyFromConfig(BASE_MESSAGE, {}, makeCfg(home));
|
||||
|
||||
expect(res).toBeUndefined();
|
||||
expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
it("strips HEARTBEAT_OK at edges outside heartbeat runs", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||
runEmbeddedPiAgentMock.mockResolvedValue({
|
||||
payloads: [{ text: `${HEARTBEAT_TOKEN} hello` }],
|
||||
meta: {
|
||||
durationMs: 1,
|
||||
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||
},
|
||||
});
|
||||
|
||||
const res = await getReplyFromConfig(BASE_MESSAGE, {}, makeCfg(home));
|
||||
|
||||
expect(maybeReplyText(res)).toBe("hello");
|
||||
for (const testCase of cases) {
|
||||
runEmbeddedPiAgentMock.mockClear();
|
||||
runEmbeddedPiAgentMock.mockResolvedValue({
|
||||
payloads: [{ text: testCase.text }],
|
||||
meta: {
|
||||
durationMs: 1,
|
||||
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||
},
|
||||
});
|
||||
const res = await getReplyFromConfig(BASE_MESSAGE, {}, makeCfg(home));
|
||||
expect(maybeReplyText(res)).toBe(testCase.expected);
|
||||
expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -187,60 +180,59 @@ describe("trigger handling", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("runs /compact as a gated command", async () => {
|
||||
it("runs /compact for main and non-default agents without invoking the embedded run path", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const storePath = join(tmpdir(), `openclaw-session-test-${Date.now()}.json`);
|
||||
const cfg = makeCfg(home);
|
||||
cfg.session = { ...cfg.session, store: storePath };
|
||||
mockSuccessfulCompaction();
|
||||
{
|
||||
const storePath = join(home, "compact-main.sessions.json");
|
||||
const cfg = makeCfg(home);
|
||||
cfg.session = { ...cfg.session, store: storePath };
|
||||
mockSuccessfulCompaction();
|
||||
|
||||
const request = {
|
||||
Body: "/compact focus on decisions",
|
||||
From: "+1003",
|
||||
To: "+2000",
|
||||
};
|
||||
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
...request,
|
||||
CommandAuthorized: true,
|
||||
},
|
||||
{},
|
||||
cfg,
|
||||
);
|
||||
const text = maybeReplyText(res);
|
||||
expect(text?.startsWith("⚙️ Compacted")).toBe(true);
|
||||
expect(getCompactEmbeddedPiSessionMock()).toHaveBeenCalledOnce();
|
||||
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
||||
const store = loadSessionStore(storePath);
|
||||
const sessionKey = resolveSessionKey("per-sender", request);
|
||||
expect(store[sessionKey]?.compactionCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("runs /compact for non-default agents without transcript path validation failures", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
getCompactEmbeddedPiSessionMock().mockClear();
|
||||
mockSuccessfulCompaction();
|
||||
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
Body: "/compact",
|
||||
From: "+1004",
|
||||
const request = {
|
||||
Body: "/compact focus on decisions",
|
||||
From: "+1003",
|
||||
To: "+2000",
|
||||
SessionKey: "agent:worker1:telegram:12345",
|
||||
CommandAuthorized: true,
|
||||
},
|
||||
{},
|
||||
makeCfg(home),
|
||||
);
|
||||
};
|
||||
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
...request,
|
||||
CommandAuthorized: true,
|
||||
},
|
||||
{},
|
||||
cfg,
|
||||
);
|
||||
const text = maybeReplyText(res);
|
||||
expect(text?.startsWith("⚙️ Compacted")).toBe(true);
|
||||
expect(getCompactEmbeddedPiSessionMock()).toHaveBeenCalledOnce();
|
||||
const store = loadSessionStore(storePath);
|
||||
const sessionKey = resolveSessionKey("per-sender", request);
|
||||
expect(store[sessionKey]?.compactionCount).toBe(1);
|
||||
}
|
||||
|
||||
{
|
||||
getCompactEmbeddedPiSessionMock().mockClear();
|
||||
mockSuccessfulCompaction();
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
Body: "/compact",
|
||||
From: "+1004",
|
||||
To: "+2000",
|
||||
SessionKey: "agent:worker1:telegram:12345",
|
||||
CommandAuthorized: true,
|
||||
},
|
||||
{},
|
||||
makeCfg(home),
|
||||
);
|
||||
|
||||
const text = maybeReplyText(res);
|
||||
expect(text?.startsWith("⚙️ Compacted")).toBe(true);
|
||||
expect(getCompactEmbeddedPiSessionMock()).toHaveBeenCalledOnce();
|
||||
expect(getCompactEmbeddedPiSessionMock().mock.calls[0]?.[0]?.sessionFile).toContain(
|
||||
join("agents", "worker1", "sessions"),
|
||||
);
|
||||
}
|
||||
|
||||
const text = maybeReplyText(res);
|
||||
expect(text?.startsWith("⚙️ Compacted")).toBe(true);
|
||||
expect(getCompactEmbeddedPiSessionMock()).toHaveBeenCalledOnce();
|
||||
expect(getCompactEmbeddedPiSessionMock().mock.calls[0]?.[0]?.sessionFile).toContain(
|
||||
join("agents", "worker1", "sessions"),
|
||||
);
|
||||
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue