mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 02:40:24 +00:00
refactor(reply): share verbose gate helpers
This commit is contained in:
111
src/auto-reply/reply/agent-runner-helpers.test.ts
Normal file
111
src/auto-reply/reply/agent-runner-helpers.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
|
||||
const hoisted = vi.hoisted(() => {
|
||||
const loadSessionStoreMock = vi.fn();
|
||||
const scheduleFollowupDrainMock = vi.fn();
|
||||
return { loadSessionStoreMock, scheduleFollowupDrainMock };
|
||||
});
|
||||
|
||||
vi.mock("../../config/sessions.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../config/sessions.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadSessionStore: (...args: unknown[]) => hoisted.loadSessionStoreMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./queue.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./queue.js")>();
|
||||
return {
|
||||
...actual,
|
||||
scheduleFollowupDrain: (...args: unknown[]) => hoisted.scheduleFollowupDrainMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
const {
|
||||
createShouldEmitToolOutput,
|
||||
createShouldEmitToolResult,
|
||||
finalizeWithFollowup,
|
||||
isAudioPayload,
|
||||
signalTypingIfNeeded,
|
||||
} = await import("./agent-runner-helpers.js");
|
||||
|
||||
describe("agent runner helpers", () => {
|
||||
beforeEach(() => {
|
||||
hoisted.loadSessionStoreMock.mockReset();
|
||||
hoisted.scheduleFollowupDrainMock.mockReset();
|
||||
});
|
||||
|
||||
it("detects audio payloads from mediaUrl/mediaUrls", () => {
|
||||
expect(isAudioPayload({ mediaUrl: "https://example.test/audio.mp3" })).toBe(true);
|
||||
expect(isAudioPayload({ mediaUrls: ["https://example.test/video.mp4"] })).toBe(false);
|
||||
expect(isAudioPayload({ mediaUrls: ["https://example.test/voice.m4a"] })).toBe(true);
|
||||
});
|
||||
|
||||
it("uses fallback verbose level when session context is missing", () => {
|
||||
expect(createShouldEmitToolResult({ resolvedVerboseLevel: "off" })()).toBe(false);
|
||||
expect(createShouldEmitToolResult({ resolvedVerboseLevel: "on" })()).toBe(true);
|
||||
expect(createShouldEmitToolOutput({ resolvedVerboseLevel: "on" })()).toBe(false);
|
||||
expect(createShouldEmitToolOutput({ resolvedVerboseLevel: "full" })()).toBe(true);
|
||||
});
|
||||
|
||||
it("uses session verbose level when present", () => {
|
||||
hoisted.loadSessionStoreMock.mockReturnValue({
|
||||
"agent:main:main": { verboseLevel: "full" },
|
||||
});
|
||||
const shouldEmitResult = createShouldEmitToolResult({
|
||||
sessionKey: "agent:main:main",
|
||||
storePath: "/tmp/store.json",
|
||||
resolvedVerboseLevel: "off",
|
||||
});
|
||||
const shouldEmitOutput = createShouldEmitToolOutput({
|
||||
sessionKey: "agent:main:main",
|
||||
storePath: "/tmp/store.json",
|
||||
resolvedVerboseLevel: "off",
|
||||
});
|
||||
expect(shouldEmitResult()).toBe(true);
|
||||
expect(shouldEmitOutput()).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back when store read fails or session value is invalid", () => {
|
||||
hoisted.loadSessionStoreMock.mockImplementation(() => {
|
||||
throw new Error("boom");
|
||||
});
|
||||
const fallbackOn = createShouldEmitToolResult({
|
||||
sessionKey: "agent:main:main",
|
||||
storePath: "/tmp/store.json",
|
||||
resolvedVerboseLevel: "on",
|
||||
});
|
||||
expect(fallbackOn()).toBe(true);
|
||||
|
||||
hoisted.loadSessionStoreMock.mockReset();
|
||||
hoisted.loadSessionStoreMock.mockReturnValue({
|
||||
"agent:main:main": { verboseLevel: "weird" },
|
||||
});
|
||||
const fallbackFull = createShouldEmitToolOutput({
|
||||
sessionKey: "agent:main:main",
|
||||
storePath: "/tmp/store.json",
|
||||
resolvedVerboseLevel: "full",
|
||||
});
|
||||
expect(fallbackFull()).toBe(true);
|
||||
});
|
||||
|
||||
it("schedules followup drain and returns the original value", () => {
|
||||
const runFollowupTurn = vi.fn();
|
||||
const value = { ok: true };
|
||||
expect(finalizeWithFollowup(value, "queue-key", runFollowupTurn)).toBe(value);
|
||||
expect(hoisted.scheduleFollowupDrainMock).toHaveBeenCalledWith("queue-key", runFollowupTurn);
|
||||
});
|
||||
|
||||
it("signals typing only when any payload has text or media", async () => {
|
||||
const signalRunStart = vi.fn().mockResolvedValue(undefined);
|
||||
const typingSignals = { signalRunStart };
|
||||
const emptyPayloads: ReplyPayload[] = [{ text: " " }, {}];
|
||||
await signalTypingIfNeeded(emptyPayloads, typingSignals);
|
||||
expect(signalRunStart).not.toHaveBeenCalled();
|
||||
|
||||
await signalTypingIfNeeded([{ mediaUrl: "https://example.test/img.png" }], typingSignals);
|
||||
expect(signalRunStart).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -11,54 +11,43 @@ const hasAudioMedia = (urls?: string[]): boolean =>
|
||||
export const isAudioPayload = (payload: ReplyPayload): boolean =>
|
||||
hasAudioMedia(payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : undefined));
|
||||
|
||||
export const createShouldEmitToolResult = (params: {
|
||||
type VerboseGateParams = {
|
||||
sessionKey?: string;
|
||||
storePath?: string;
|
||||
resolvedVerboseLevel: VerboseLevel;
|
||||
}): (() => boolean) => {
|
||||
// Normalize verbose values from session store/config so false/"false" still means off.
|
||||
const fallbackVerbose = normalizeVerboseLevel(String(params.resolvedVerboseLevel ?? "")) ?? "off";
|
||||
return () => {
|
||||
if (!params.sessionKey || !params.storePath) {
|
||||
return fallbackVerbose !== "off";
|
||||
}
|
||||
try {
|
||||
const store = loadSessionStore(params.storePath);
|
||||
const entry = store[params.sessionKey];
|
||||
const current = normalizeVerboseLevel(String(entry?.verboseLevel ?? ""));
|
||||
if (current) {
|
||||
return current !== "off";
|
||||
}
|
||||
} catch {
|
||||
// ignore store read failures
|
||||
}
|
||||
return fallbackVerbose !== "off";
|
||||
};
|
||||
};
|
||||
|
||||
export const createShouldEmitToolOutput = (params: {
|
||||
sessionKey?: string;
|
||||
storePath?: string;
|
||||
resolvedVerboseLevel: VerboseLevel;
|
||||
}): (() => boolean) => {
|
||||
function resolveCurrentVerboseLevel(params: VerboseGateParams): VerboseLevel | undefined {
|
||||
if (!params.sessionKey || !params.storePath) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const store = loadSessionStore(params.storePath);
|
||||
const entry = store[params.sessionKey];
|
||||
return normalizeVerboseLevel(String(entry?.verboseLevel ?? ""));
|
||||
} catch {
|
||||
// ignore store read failures
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function createVerboseGate(
|
||||
params: VerboseGateParams,
|
||||
shouldEmit: (level: VerboseLevel) => boolean,
|
||||
): () => boolean {
|
||||
// Normalize verbose values from session store/config so false/"false" still means off.
|
||||
const fallbackVerbose = normalizeVerboseLevel(String(params.resolvedVerboseLevel ?? "")) ?? "off";
|
||||
return () => {
|
||||
if (!params.sessionKey || !params.storePath) {
|
||||
return fallbackVerbose === "full";
|
||||
}
|
||||
try {
|
||||
const store = loadSessionStore(params.storePath);
|
||||
const entry = store[params.sessionKey];
|
||||
const current = normalizeVerboseLevel(String(entry?.verboseLevel ?? ""));
|
||||
if (current) {
|
||||
return current === "full";
|
||||
}
|
||||
} catch {
|
||||
// ignore store read failures
|
||||
}
|
||||
return fallbackVerbose === "full";
|
||||
return shouldEmit(resolveCurrentVerboseLevel(params) ?? fallbackVerbose);
|
||||
};
|
||||
}
|
||||
|
||||
export const createShouldEmitToolResult = (params: VerboseGateParams): (() => boolean) => {
|
||||
return createVerboseGate(params, (level) => level !== "off");
|
||||
};
|
||||
|
||||
export const createShouldEmitToolOutput = (params: VerboseGateParams): (() => boolean) => {
|
||||
return createVerboseGate(params, (level) => level === "full");
|
||||
};
|
||||
|
||||
export const finalizeWithFollowup = <T>(
|
||||
|
||||
Reference in New Issue
Block a user