Compare commits
3 Commits
main
...
fix/heartb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e059934ecc | ||
|
|
ee83bab1f6 | ||
|
|
4b710d79fe |
@ -1018,6 +1018,7 @@ describe("runHeartbeatOnce", () => {
|
||||
reason?: "interval" | "wake";
|
||||
queueCronEvent?: boolean;
|
||||
replyText?: string;
|
||||
cfgOverrides?: Partial<OpenClawConfig>;
|
||||
}) {
|
||||
const tmpDir = await createCaseDir("openclaw-hb");
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
@ -1041,7 +1042,7 @@ describe("runHeartbeatOnce", () => {
|
||||
await fs.mkdir(path.join(workspaceDir, "HEARTBEAT.md"), { recursive: true });
|
||||
}
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
const cfgBase: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: workspaceDir,
|
||||
@ -1051,6 +1052,9 @@ describe("runHeartbeatOnce", () => {
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
};
|
||||
const cfg: OpenClawConfig = params.cfgOverrides
|
||||
? { ...cfgBase, ...params.cfgOverrides }
|
||||
: cfgBase;
|
||||
const sessionKey = resolveMainSessionKey(cfg);
|
||||
await fs.writeFile(
|
||||
storePath,
|
||||
@ -1083,7 +1087,7 @@ describe("runHeartbeatOnce", () => {
|
||||
return { res, replySpy, sendWhatsApp, workspaceDir };
|
||||
}
|
||||
|
||||
it("adds explicit workspace HEARTBEAT.md path guidance to heartbeat prompts", async () => {
|
||||
it("adds explicit workspace HEARTBEAT.md path guidance to default heartbeat prompts", async () => {
|
||||
const { res, replySpy, sendWhatsApp, workspaceDir } = await runHeartbeatFileScenario({
|
||||
fileState: "actionable",
|
||||
reason: "interval",
|
||||
@ -1102,6 +1106,35 @@ describe("runHeartbeatOnce", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not mutate a custom heartbeat prompt", async () => {
|
||||
const customPrompt =
|
||||
"Read HEARTBEAT.md if it exists (workspace context). Use the system prompt only.";
|
||||
const { res, replySpy } = await runHeartbeatFileScenario({
|
||||
fileState: "actionable",
|
||||
reason: "interval",
|
||||
replyText: "Checked logs and PRs",
|
||||
cfgOverrides: {
|
||||
agents: {
|
||||
defaults: {
|
||||
heartbeat: {
|
||||
prompt: customPrompt,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
expect(res.status).toBe("ran");
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const calledCtx = replySpy.mock.calls[0]?.[0] as { Body?: string };
|
||||
expect(calledCtx.Body).toContain(customPrompt);
|
||||
expect(calledCtx.Body).not.toContain("Do not read docs/heartbeat.md.");
|
||||
expect(calledCtx.Body).not.toContain("use workspace file");
|
||||
} finally {
|
||||
replySpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("applies HEARTBEAT.md gating rules across file states and triggers", async () => {
|
||||
const cases: Array<{
|
||||
name: string;
|
||||
|
||||
@ -560,7 +560,15 @@ type HeartbeatPromptResolution = {
|
||||
hasCronEvents: boolean;
|
||||
};
|
||||
|
||||
function appendHeartbeatWorkspacePathHint(prompt: string, workspaceDir: string): string {
|
||||
function appendHeartbeatWorkspacePathHint(params: {
|
||||
prompt: string;
|
||||
workspaceDir: string;
|
||||
shouldAppend: boolean;
|
||||
}): string {
|
||||
const { prompt, workspaceDir, shouldAppend } = params;
|
||||
if (!shouldAppend) {
|
||||
return prompt;
|
||||
}
|
||||
if (!/heartbeat\.md/i.test(prompt)) {
|
||||
return prompt;
|
||||
}
|
||||
@ -597,7 +605,20 @@ function resolveHeartbeatRunPrompt(params: {
|
||||
: hasCronEvents
|
||||
? buildCronEventPrompt(cronEvents, { deliverToUser: params.canRelayToUser })
|
||||
: resolveHeartbeatPrompt(params.cfg, params.heartbeat);
|
||||
const prompt = appendHeartbeatWorkspacePathHint(basePrompt, params.workspaceDir);
|
||||
const configuredPromptRaw =
|
||||
params.heartbeat?.prompt ?? params.cfg.agents?.defaults?.heartbeat?.prompt;
|
||||
const effectivePrompt = resolveHeartbeatPromptText(configuredPromptRaw);
|
||||
const defaultPrompt = resolveHeartbeatPromptText(undefined);
|
||||
const isUsingDefaultPrompt = effectivePrompt === defaultPrompt;
|
||||
|
||||
const prompt = appendHeartbeatWorkspacePathHint({
|
||||
prompt: basePrompt,
|
||||
workspaceDir: params.workspaceDir,
|
||||
// Only append the hint when using the built-in default heartbeat prompt.
|
||||
// If the user configured a custom prompt, do not mutate it.
|
||||
// Also: treat empty-string overrides as a request to fall back to the default.
|
||||
shouldAppend: isUsingDefaultPrompt && !hasExecCompletion && !hasCronEvents,
|
||||
});
|
||||
|
||||
return { prompt, hasExecCompletion, hasCronEvents };
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { withEnv } from "../test-utils/env.js";
|
||||
async function importFreshPluginTestModules() {
|
||||
@ -1354,37 +1352,25 @@ describe("loadOpenClawPlugins", () => {
|
||||
};`,
|
||||
});
|
||||
|
||||
const loaderModuleUrl = pathToFileURL(
|
||||
path.join(process.cwd(), "src", "plugins", "loader.ts"),
|
||||
).href;
|
||||
const script = `
|
||||
import { loadOpenClawPlugins } from ${JSON.stringify(loaderModuleUrl)};
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
workspaceDir: ${JSON.stringify(plugin.dir)},
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [${JSON.stringify(plugin.file)}] },
|
||||
allow: ["legacy-root-import"],
|
||||
},
|
||||
},
|
||||
});
|
||||
const record = registry.plugins.find((entry) => entry.id === "legacy-root-import");
|
||||
if (!record || record.status !== "loaded") {
|
||||
console.error(record?.error ?? "legacy-root-import missing");
|
||||
process.exit(1);
|
||||
}
|
||||
`;
|
||||
|
||||
execFileSync(process.execPath, ["--import", "tsx", "--input-type=module", "-e", script], {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
const registry = withEnv(
|
||||
{
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins",
|
||||
},
|
||||
encoding: "utf-8",
|
||||
stdio: "pipe",
|
||||
});
|
||||
() =>
|
||||
loadOpenClawPlugins({
|
||||
cache: false,
|
||||
workspaceDir: plugin.dir,
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [plugin.file] },
|
||||
allow: ["legacy-root-import"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const record = registry.plugins.find((entry) => entry.id === "legacy-root-import");
|
||||
expect(record?.status).toBe("loaded");
|
||||
});
|
||||
|
||||
it("prefers dist plugin-sdk alias when loader runs from dist", () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user