mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 21:50:42 +00:00
Summary: - The PR adds a `before_agent_run` plugin hook with pass/block decisions, redacted blocked-turn persistence, diagnostics/docs/changelog updates, and focused runner, gateway, session, and plugin tests. - Reproducibility: not applicable. as a feature PR rather than a current-main bug report. Current main lacks ` ... un`, while the PR head adds source coverage and copied live Gateway/WebChat log proof for the new behavior. Automerge notes: - PR branch already contained follow-up commit before automerge: fix: trim before agent hook PR scope - PR branch already contained follow-up commit before automerge: fix: keep before-agent blocks redacted - PR branch already contained follow-up commit before automerge: fix: keep runtime context out of model prompt - PR branch already contained follow-up commit before automerge: docs: refresh config baseline after rebase - PR branch already contained follow-up commit before automerge: fix: align blocked turn clients with redacted content - PR branch already contained follow-up commit before automerge: fix: remove out-of-scope client block UI changes Validation: - ClawSweeper review passed for head767e46fde8. - Required merge gates passed before the squash merge. Prepared head SHA:767e46fde8Review: https://github.com/openclaw/openclaw/pull/75035#issuecomment-4351843275 Co-authored-by: Jesse Merhi <jessejmerhi@gmail.com> Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
82 lines
3.4 KiB
TypeScript
82 lines
3.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
BLOCK_MESSAGE_PREFIX,
|
|
type HookDecision,
|
|
type HookDecisionBlock,
|
|
mergeHookDecisions,
|
|
isHookDecision,
|
|
resolveBlockMessage,
|
|
} from "./hook-decision-types.js";
|
|
|
|
describe("HookDecision helpers", () => {
|
|
describe("isHookDecision", () => {
|
|
it("recognizes supported outcomes", () => {
|
|
expect(isHookDecision({ outcome: "pass" })).toBe(true);
|
|
expect(isHookDecision({ outcome: "block", reason: "policy" })).toBe(true);
|
|
});
|
|
|
|
it("rejects non-decision values", () => {
|
|
expect(isHookDecision(null)).toBe(false);
|
|
expect(isHookDecision(undefined)).toBe(false);
|
|
expect(isHookDecision("pass")).toBe(false);
|
|
expect(isHookDecision({ block: true })).toBe(false);
|
|
expect(isHookDecision({ outcome: "ask", reason: "check" })).toBe(false);
|
|
expect(isHookDecision({ outcome: "invalid" })).toBe(false);
|
|
expect(isHookDecision({ outcome: "pass", message: "typo" })).toBe(false);
|
|
expect(isHookDecision({ outcome: "pass", reason: "typo" })).toBe(false);
|
|
expect(isHookDecision({ outcome: "block" })).toBe(false);
|
|
expect(isHookDecision({ outcome: "block", reason: "" })).toBe(false);
|
|
expect(isHookDecision({ outcome: "block", reason: "policy", message: "" })).toBe(false);
|
|
expect(isHookDecision({ outcome: "block", reason: "policy", message: 3 })).toBe(false);
|
|
expect(isHookDecision({ outcome: "block", reason: "policy", ask: true })).toBe(false);
|
|
expect(isHookDecision({ outcome: "block", reason: "policy", metadata: [] })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("mergeHookDecisions", () => {
|
|
const passDecision: HookDecision = { outcome: "pass" };
|
|
const blockDecision: HookDecision = { outcome: "block", reason: "policy" };
|
|
|
|
it("uses most-restrictive-wins ordering", () => {
|
|
expect(mergeHookDecisions(undefined, passDecision)).toBe(passDecision);
|
|
expect(mergeHookDecisions(passDecision, blockDecision)).toBe(blockDecision);
|
|
expect(mergeHookDecisions(blockDecision, passDecision)).toBe(blockDecision);
|
|
});
|
|
|
|
it("keeps the first decision when outcomes have the same severity", () => {
|
|
const secondBlock: HookDecision = { outcome: "block", reason: "second" };
|
|
|
|
expect(mergeHookDecisions(passDecision, { outcome: "pass" })).toBe(passDecision);
|
|
expect(mergeHookDecisions(blockDecision, secondBlock)).toBe(blockDecision);
|
|
});
|
|
});
|
|
|
|
describe("resolveBlockMessage", () => {
|
|
it("returns explicit or default block messages", () => {
|
|
const explicit: HookDecisionBlock = {
|
|
outcome: "block",
|
|
reason: "policy",
|
|
message: "Please rephrase your request.",
|
|
};
|
|
const fallback: HookDecisionBlock = {
|
|
outcome: "block",
|
|
reason: "policy",
|
|
};
|
|
|
|
expect(resolveBlockMessage(explicit)).toBe(
|
|
`${BLOCK_MESSAGE_PREFIX}: Please rephrase your request.`,
|
|
);
|
|
expect(resolveBlockMessage(fallback)).toBe(`${BLOCK_MESSAGE_PREFIX}: blocked`);
|
|
expect(resolveBlockMessage(fallback, { blockedBy: "policy-plugin" })).toBe(
|
|
`${BLOCK_MESSAGE_PREFIX}: blocked by policy-plugin`,
|
|
);
|
|
expect(resolveBlockMessage(explicit, { blockedBy: "policy-plugin" })).toBe(
|
|
`${BLOCK_MESSAGE_PREFIX}: Please rephrase your request. (blocked by policy-plugin)`,
|
|
);
|
|
expect(resolveBlockMessage({ ...explicit, message: " " })).toBe(
|
|
`${BLOCK_MESSAGE_PREFIX}: blocked`,
|
|
);
|
|
});
|
|
});
|
|
});
|