Files
moltbot/src/plugins/hooks.before-agent-start.test.ts
2026-03-02 19:57:33 +00:00

179 lines
5.9 KiB
TypeScript

/**
* Layer 1: Hook Merger Tests for before_agent_start
*
* Validates that modelOverride and providerOverride fields are correctly
* propagated through the hook merger, including priority ordering and
* backward compatibility.
*/
import { beforeEach, describe, expect, it } from "vitest";
import { createHookRunner } from "./hooks.js";
import { addTestHook, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-helpers.js";
import { createEmptyPluginRegistry, type PluginRegistry } from "./registry.js";
import type { PluginHookBeforeAgentStartResult, PluginHookRegistration } from "./types.js";
function addBeforeAgentStartHook(
registry: PluginRegistry,
pluginId: string,
handler: () => PluginHookBeforeAgentStartResult | Promise<PluginHookBeforeAgentStartResult>,
priority?: number,
) {
addTestHook({
registry,
pluginId,
hookName: "before_agent_start",
handler: handler as PluginHookRegistration["handler"],
priority,
});
}
const stubCtx = TEST_PLUGIN_AGENT_CTX;
describe("before_agent_start hook merger", () => {
let registry: PluginRegistry;
beforeEach(() => {
registry = createEmptyPluginRegistry();
});
const runWithSingleHook = async (result: PluginHookBeforeAgentStartResult, priority?: number) => {
addBeforeAgentStartHook(registry, "plugin-a", () => result, priority);
const runner = createHookRunner(registry);
return await runner.runBeforeAgentStart({ prompt: "hello" }, stubCtx);
};
const expectSingleModelOverride = async (modelOverride: string) => {
const result = await runWithSingleHook({ modelOverride });
expect(result?.modelOverride).toBe(modelOverride);
return result;
};
it("returns modelOverride from a single plugin", async () => {
await expectSingleModelOverride("llama3.3:8b");
});
it("returns providerOverride from a single plugin", async () => {
const result = await runWithSingleHook({
providerOverride: "ollama",
});
expect(result?.providerOverride).toBe("ollama");
});
it("returns both modelOverride and providerOverride together", async () => {
addBeforeAgentStartHook(registry, "plugin-a", () => ({
modelOverride: "llama3.3:8b",
providerOverride: "ollama",
}));
const runner = createHookRunner(registry);
const result = await runner.runBeforeAgentStart({ prompt: "hello" }, stubCtx);
expect(result?.modelOverride).toBe("llama3.3:8b");
expect(result?.providerOverride).toBe("ollama");
});
it("higher-priority plugin wins for modelOverride", async () => {
addBeforeAgentStartHook(registry, "low-priority", () => ({ modelOverride: "gpt-4o" }), 1);
addBeforeAgentStartHook(
registry,
"high-priority",
() => ({ modelOverride: "llama3.3:8b" }),
10,
);
const runner = createHookRunner(registry);
const result = await runner.runBeforeAgentStart({ prompt: "PII prompt" }, stubCtx);
expect(result?.modelOverride).toBe("llama3.3:8b");
});
it("lower-priority plugin does not overwrite if it returns undefined", async () => {
addBeforeAgentStartHook(
registry,
"high-priority",
() => ({ modelOverride: "llama3.3:8b", providerOverride: "ollama" }),
10,
);
addBeforeAgentStartHook(
registry,
"low-priority",
() => ({ prependContext: "some context" }),
1,
);
const runner = createHookRunner(registry);
const result = await runner.runBeforeAgentStart({ prompt: "hello" }, stubCtx);
// High-priority ran first (priority 10), low-priority ran second (priority 1).
// Low-priority didn't return modelOverride, so ?? falls back to acc's value.
expect(result?.modelOverride).toBe("llama3.3:8b");
expect(result?.providerOverride).toBe("ollama");
expect(result?.prependContext).toBe("some context");
});
it("prependContext still concatenates when modelOverride is present", async () => {
addBeforeAgentStartHook(
registry,
"plugin-a",
() => ({
prependContext: "context A",
modelOverride: "llama3.3:8b",
}),
10,
);
addBeforeAgentStartHook(
registry,
"plugin-b",
() => ({
prependContext: "context B",
}),
1,
);
const runner = createHookRunner(registry);
const result = await runner.runBeforeAgentStart({ prompt: "hello" }, stubCtx);
expect(result?.prependContext).toBe("context A\n\ncontext B");
expect(result?.modelOverride).toBe("llama3.3:8b");
});
it("backward compat: plugin returning only prependContext produces no modelOverride", async () => {
addBeforeAgentStartHook(registry, "legacy-plugin", () => ({
prependContext: "legacy context",
}));
const runner = createHookRunner(registry);
const result = await runner.runBeforeAgentStart({ prompt: "hello" }, stubCtx);
expect(result?.prependContext).toBe("legacy context");
expect(result?.modelOverride).toBeUndefined();
expect(result?.providerOverride).toBeUndefined();
});
it("modelOverride without providerOverride leaves provider undefined", async () => {
const result = await expectSingleModelOverride("llama3.3:8b");
expect(result?.providerOverride).toBeUndefined();
});
it("returns undefined when no hooks are registered", async () => {
const runner = createHookRunner(registry);
const result = await runner.runBeforeAgentStart({ prompt: "hello" }, stubCtx);
expect(result).toBeUndefined();
});
it("systemPrompt merges correctly alongside model overrides", async () => {
addBeforeAgentStartHook(registry, "plugin-a", () => ({
systemPrompt: "You are a helpful assistant",
modelOverride: "llama3.3:8b",
providerOverride: "ollama",
}));
const runner = createHookRunner(registry);
const result = await runner.runBeforeAgentStart({ prompt: "hello" }, stubCtx);
expect(result?.systemPrompt).toBe("You are a helpful assistant");
expect(result?.modelOverride).toBe("llama3.3:8b");
expect(result?.providerOverride).toBe("ollama");
});
});