refactor(runtime): consolidate followup, gateway, and provider dedupe paths

This commit is contained in:
Peter Steinberger
2026-02-22 14:06:03 +00:00
parent 38752338dc
commit d116bcfb14
36 changed files with 848 additions and 908 deletions

View File

@@ -46,6 +46,12 @@ describe("boot-md handler", () => {
return cfg;
}
function setupSingleMainAgentBootConfig(cfg: unknown) {
listAgentIds.mockReturnValue(["main"]);
resolveAgentWorkspaceDir.mockReturnValue(MAIN_WORKSPACE_DIR);
return cfg;
}
beforeEach(() => {
vi.clearAllMocks();
});
@@ -82,9 +88,7 @@ describe("boot-md handler", () => {
});
it("runs boot for single default agent when no agents configured", async () => {
const cfg = {};
listAgentIds.mockReturnValue(["main"]);
resolveAgentWorkspaceDir.mockReturnValue(MAIN_WORKSPACE_DIR);
const cfg = setupSingleMainAgentBootConfig({});
runBootOnce.mockResolvedValue({ status: "skipped", reason: "missing" });
await runBootChecklist(makeEvent({ context: { cfg } }));
@@ -112,9 +116,7 @@ describe("boot-md handler", () => {
});
it("logs debug details when a per-agent boot run is skipped", async () => {
const cfg = { agents: { list: [{ id: "main" }] } };
listAgentIds.mockReturnValue(["main"]);
resolveAgentWorkspaceDir.mockReturnValue(MAIN_WORKSPACE_DIR);
const cfg = setupSingleMainAgentBootConfig({ agents: { list: [{ id: "main" }] } });
runBootOnce.mockResolvedValue({ status: "skipped", reason: "missing" });
await runBootChecklist(makeEvent({ context: { cfg } }));

View File

@@ -133,6 +133,33 @@ async function createSessionMemoryWorkspace(params?: {
return { tempDir, sessionsDir, activeSessionFile };
}
async function loadMemoryFromActiveSessionPointer(params: {
tempDir: string;
activeSessionFile: string;
}): Promise<string> {
const { memoryContent } = await runNewWithPreviousSessionEntry({
tempDir: params.tempDir,
previousSessionEntry: {
sessionId: "test-123",
sessionFile: params.activeSessionFile,
},
});
return memoryContent;
}
function expectMemoryConversation(params: {
memoryContent: string;
user: string;
assistant: string;
absent?: string;
}) {
expect(params.memoryContent).toContain(`user: ${params.user}`);
expect(params.memoryContent).toContain(`assistant: ${params.assistant}`);
if (params.absent) {
expect(params.memoryContent).not.toContain(params.absent);
}
}
describe("session-memory hook", () => {
it("skips non-command events", async () => {
const tempDir = await makeTempWorkspace("openclaw-session-memory-");
@@ -415,17 +442,17 @@ describe("session-memory hook", () => {
]),
});
const { memoryContent } = await runNewWithPreviousSessionEntry({
const memoryContent = await loadMemoryFromActiveSessionPointer({
tempDir,
previousSessionEntry: {
sessionId: "test-123",
sessionFile: activeSessionFile!,
},
activeSessionFile: activeSessionFile!,
});
expect(memoryContent).toContain("user: Newest rotated transcript");
expect(memoryContent).toContain("assistant: Newest summary");
expect(memoryContent).not.toContain("Older rotated transcript");
expectMemoryConversation({
memoryContent,
user: "Newest rotated transcript",
assistant: "Newest summary",
absent: "Older rotated transcript",
});
});
it("prefers active transcript when it is non-empty even with reset candidates", async () => {
@@ -448,17 +475,17 @@ describe("session-memory hook", () => {
]),
});
const { memoryContent } = await runNewWithPreviousSessionEntry({
const memoryContent = await loadMemoryFromActiveSessionPointer({
tempDir,
previousSessionEntry: {
sessionId: "test-123",
sessionFile: activeSessionFile!,
},
activeSessionFile: activeSessionFile!,
});
expect(memoryContent).toContain("user: Active transcript message");
expect(memoryContent).toContain("assistant: Active transcript summary");
expect(memoryContent).not.toContain("Reset fallback message");
expectMemoryConversation({
memoryContent,
user: "Active transcript message",
assistant: "Active transcript summary",
absent: "Reset fallback message",
});
});
it("handles empty session files gracefully", async () => {

View File

@@ -33,6 +33,25 @@ describe("loader", () => {
process.env.OPENCLAW_BUNDLED_HOOKS_DIR = "/nonexistent/bundled/hooks";
});
async function writeHandlerModule(
fileName: string,
code = "export default async function() {}",
): Promise<string> {
const handlerPath = path.join(tmpDir, fileName);
await fs.writeFile(handlerPath, code, "utf-8");
return handlerPath;
}
function createEnabledHooksConfig(
handlers?: Array<{ event: string; module: string; export?: string }>,
): OpenClawConfig {
return {
hooks: {
internal: handlers ? { enabled: true, handlers } : { enabled: true },
},
};
}
afterEach(async () => {
clearInternalHooks();
envSnapshot.restore();
@@ -67,27 +86,18 @@ describe("loader", () => {
it("should load a handler from a module", async () => {
// Create a test handler module
const handlerPath = path.join(tmpDir, "test-handler.js");
const handlerCode = `
export default async function(event) {
// Test handler
}
`;
await fs.writeFile(handlerPath, handlerCode, "utf-8");
const cfg: OpenClawConfig = {
hooks: {
internal: {
enabled: true,
handlers: [
{
event: "command:new",
module: path.basename(handlerPath),
},
],
},
const handlerPath = await writeHandlerModule("test-handler.js", handlerCode);
const cfg = createEnabledHooksConfig([
{
event: "command:new",
module: path.basename(handlerPath),
},
};
]);
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(1);
@@ -98,23 +108,13 @@ describe("loader", () => {
it("should load multiple handlers", async () => {
// Create test handler modules
const handler1Path = path.join(tmpDir, "handler1.js");
const handler2Path = path.join(tmpDir, "handler2.js");
const handler1Path = await writeHandlerModule("handler1.js");
const handler2Path = await writeHandlerModule("handler2.js");
await fs.writeFile(handler1Path, "export default async function() {}", "utf-8");
await fs.writeFile(handler2Path, "export default async function() {}", "utf-8");
const cfg: OpenClawConfig = {
hooks: {
internal: {
enabled: true,
handlers: [
{ event: "command:new", module: path.basename(handler1Path) },
{ event: "command:stop", module: path.basename(handler2Path) },
],
},
},
};
const cfg = createEnabledHooksConfig([
{ event: "command:new", module: path.basename(handler1Path) },
{ event: "command:stop", module: path.basename(handler2Path) },
]);
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(2);
@@ -126,47 +126,32 @@ describe("loader", () => {
it("should support named exports", async () => {
// Create a handler module with named export
const handlerPath = path.join(tmpDir, "named-export.js");
const handlerCode = `
export const myHandler = async function(event) {
// Named export handler
}
`;
await fs.writeFile(handlerPath, handlerCode, "utf-8");
const handlerPath = await writeHandlerModule("named-export.js", handlerCode);
const cfg: OpenClawConfig = {
hooks: {
internal: {
enabled: true,
handlers: [
{
event: "command:new",
module: path.basename(handlerPath),
export: "myHandler",
},
],
},
const cfg = createEnabledHooksConfig([
{
event: "command:new",
module: path.basename(handlerPath),
export: "myHandler",
},
};
]);
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(1);
});
it("should handle module loading errors gracefully", async () => {
const cfg: OpenClawConfig = {
hooks: {
internal: {
enabled: true,
handlers: [
{
event: "command:new",
module: "missing-handler.js",
},
],
},
const cfg = createEnabledHooksConfig([
{
event: "command:new",
module: "missing-handler.js",
},
};
]);
// Should not throw and should return 0 (handler failed to load)
const count = await loadInternalHooks(cfg, tmpDir);
@@ -175,22 +160,17 @@ describe("loader", () => {
it("should handle non-function exports", async () => {
// Create a module with a non-function export
const handlerPath = path.join(tmpDir, "bad-export.js");
await fs.writeFile(handlerPath, 'export default "not a function";', "utf-8");
const handlerPath = await writeHandlerModule(
"bad-export.js",
'export default "not a function";',
);
const cfg: OpenClawConfig = {
hooks: {
internal: {
enabled: true,
handlers: [
{
event: "command:new",
module: path.basename(handlerPath),
},
],
},
const cfg = createEnabledHooksConfig([
{
event: "command:new",
module: path.basename(handlerPath),
},
};
]);
// Should not throw and should return 0 (handler is not a function)
const count = await loadInternalHooks(cfg, tmpDir);
@@ -199,25 +179,17 @@ describe("loader", () => {
it("should handle relative paths", async () => {
// Create a handler module
const handlerPath = path.join(tmpDir, "relative-handler.js");
await fs.writeFile(handlerPath, "export default async function() {}", "utf-8");
const handlerPath = await writeHandlerModule("relative-handler.js");
// Relative to workspaceDir (tmpDir)
const relativePath = path.relative(tmpDir, handlerPath);
const cfg: OpenClawConfig = {
hooks: {
internal: {
enabled: true,
handlers: [
{
event: "command:new",
module: relativePath,
},
],
},
const cfg = createEnabledHooksConfig([
{
event: "command:new",
module: relativePath,
},
};
]);
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(1);
@@ -225,7 +197,6 @@ describe("loader", () => {
it("should actually call the loaded handler", async () => {
// Create a handler that we can verify was called
const handlerPath = path.join(tmpDir, "callable-handler.js");
const handlerCode = `
let callCount = 0;
export default async function(event) {
@@ -235,21 +206,14 @@ describe("loader", () => {
return callCount;
}
`;
await fs.writeFile(handlerPath, handlerCode, "utf-8");
const handlerPath = await writeHandlerModule("callable-handler.js", handlerCode);
const cfg: OpenClawConfig = {
hooks: {
internal: {
enabled: true,
handlers: [
{
event: "command:new",
module: path.basename(handlerPath),
},
],
},
const cfg = createEnabledHooksConfig([
{
event: "command:new",
module: path.basename(handlerPath),
},
};
]);
await loadInternalHooks(cfg, tmpDir);
@@ -288,13 +252,7 @@ describe("loader", () => {
return;
}
const cfg: OpenClawConfig = {
hooks: {
internal: {
enabled: true,
},
},
};
const cfg = createEnabledHooksConfig();
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(0);
@@ -312,19 +270,12 @@ describe("loader", () => {
return;
}
const cfg: OpenClawConfig = {
hooks: {
internal: {
enabled: true,
handlers: [
{
event: "command:new",
module: "legacy-handler.js",
},
],
},
const cfg = createEnabledHooksConfig([
{
event: "command:new",
module: "legacy-handler.js",
},
};
]);
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(0);