mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
fix: prefer target entry for reply directives
This commit is contained in:
246
src/auto-reply/reply/get-reply-directives.target-session.test.ts
Normal file
246
src/auto-reply/reply/get-reply-directives.target-session.test.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import type { TemplateContext } from "../templating.js";
|
||||
import { buildTestCtx } from "./test-ctx.js";
|
||||
import { importFreshModule } from "../../../test/helpers/import-fresh.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
createModelSelectionState: vi.fn(),
|
||||
applyInlineDirectiveOverrides: vi.fn(),
|
||||
resolveFastModeState: vi.fn(),
|
||||
resolveReplyExecOverrides: vi.fn(),
|
||||
}));
|
||||
|
||||
function makeSessionEntry(overrides: Partial<SessionEntry> = {}): SessionEntry {
|
||||
return {
|
||||
sessionId: "session-id",
|
||||
updatedAt: Date.now(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadResolveReplyDirectivesForTest() {
|
||||
vi.resetModules();
|
||||
vi.doMock("../../agents/agent-scope.js", () => ({
|
||||
listAgentEntries: vi.fn(() => []),
|
||||
}));
|
||||
vi.doMock("../../agents/defaults.js", () => ({
|
||||
DEFAULT_CONTEXT_TOKENS: 8192,
|
||||
}));
|
||||
vi.doMock("../../agents/fast-mode.js", () => ({
|
||||
resolveFastModeState: (...args: unknown[]) => mocks.resolveFastModeState(...args),
|
||||
}));
|
||||
vi.doMock("../../agents/sandbox/runtime-status.js", () => ({
|
||||
resolveSandboxRuntimeStatus: vi.fn(() => ({ sandboxed: false })),
|
||||
}));
|
||||
vi.doMock("../../routing/session-key.js", () => ({
|
||||
normalizeAgentId: (value: string) => value,
|
||||
}));
|
||||
vi.doMock("../commands-text-routing.js", () => ({
|
||||
shouldHandleTextCommands: vi.fn(() => false),
|
||||
}));
|
||||
vi.doMock("./commands-context.js", () => ({
|
||||
buildCommandContext: vi.fn(() => ({
|
||||
surface: "whatsapp",
|
||||
channel: "whatsapp",
|
||||
channelId: "whatsapp",
|
||||
ownerList: [],
|
||||
senderIsOwner: false,
|
||||
isAuthorizedSender: false,
|
||||
senderId: undefined,
|
||||
abortKey: "abort-key",
|
||||
rawBodyNormalized: "hello",
|
||||
commandBodyNormalized: "hello",
|
||||
from: "whatsapp:+1000",
|
||||
to: "whatsapp:+2000",
|
||||
})),
|
||||
}));
|
||||
vi.doMock("./directive-handling.parse.js", () => ({
|
||||
parseInlineDirectives: vi.fn((body: string) => ({
|
||||
cleaned: body,
|
||||
hasThinkDirective: false,
|
||||
hasVerboseDirective: false,
|
||||
hasFastDirective: false,
|
||||
hasReasoningDirective: false,
|
||||
hasElevatedDirective: false,
|
||||
hasExecDirective: false,
|
||||
hasModelDirective: false,
|
||||
hasQueueDirective: false,
|
||||
hasStatusDirective: false,
|
||||
queueReset: false,
|
||||
thinkLevel: undefined,
|
||||
verboseLevel: undefined,
|
||||
fastMode: undefined,
|
||||
reasoningLevel: undefined,
|
||||
elevatedLevel: undefined,
|
||||
rawElevatedLevel: undefined,
|
||||
rawModelDirective: undefined,
|
||||
execSecurity: undefined,
|
||||
})),
|
||||
}));
|
||||
vi.doMock("./get-reply-directive-aliases.js", () => ({
|
||||
reserveSkillCommandNames: vi.fn(),
|
||||
resolveConfiguredDirectiveAliases: vi.fn(() => []),
|
||||
}));
|
||||
vi.doMock("./get-reply-directives-apply.js", () => ({
|
||||
applyInlineDirectiveOverrides: (...args: unknown[]) => mocks.applyInlineDirectiveOverrides(...args),
|
||||
}));
|
||||
vi.doMock("./get-reply-exec-overrides.js", () => ({
|
||||
resolveReplyExecOverrides: (...args: unknown[]) => mocks.resolveReplyExecOverrides(...args),
|
||||
}));
|
||||
vi.doMock("./get-reply-fast-path.js", () => ({
|
||||
shouldUseReplyFastTestRuntime: vi.fn(() => false),
|
||||
}));
|
||||
vi.doMock("./groups.js", () => ({
|
||||
defaultGroupActivation: vi.fn(() => "always"),
|
||||
resolveGroupRequireMention: vi.fn(async () => false),
|
||||
}));
|
||||
vi.doMock("./model-selection.js", () => ({
|
||||
createFastTestModelSelectionState: vi.fn(),
|
||||
createModelSelectionState: (...args: unknown[]) => mocks.createModelSelectionState(...args),
|
||||
resolveContextTokens: vi.fn(() => 4096),
|
||||
}));
|
||||
vi.doMock("./reply-elevated.js", () => ({
|
||||
formatElevatedUnavailableMessage: vi.fn(() => "elevated unavailable"),
|
||||
resolveElevatedPermissions: vi.fn(() => ({
|
||||
enabled: true,
|
||||
allowed: true,
|
||||
failures: [],
|
||||
})),
|
||||
}));
|
||||
return await importFreshModule<typeof import("./get-reply-directives.js")>(
|
||||
import.meta.url,
|
||||
"./get-reply-directives.js",
|
||||
);
|
||||
}
|
||||
|
||||
describe("resolveReplyDirectives", () => {
|
||||
beforeEach(() => {
|
||||
mocks.createModelSelectionState.mockReset();
|
||||
mocks.applyInlineDirectiveOverrides.mockReset();
|
||||
mocks.resolveFastModeState.mockReset();
|
||||
mocks.resolveReplyExecOverrides.mockReset();
|
||||
|
||||
mocks.createModelSelectionState.mockResolvedValue({
|
||||
provider: "openai",
|
||||
model: "gpt-4o-mini",
|
||||
allowedModelKeys: new Set<string>(),
|
||||
allowedModelCatalog: [],
|
||||
resetModelOverride: false,
|
||||
resolveDefaultThinkingLevel: vi.fn(async () => "off"),
|
||||
resolveDefaultReasoningLevel: vi.fn(async () => "off"),
|
||||
});
|
||||
mocks.applyInlineDirectiveOverrides.mockImplementation(async (params) => ({
|
||||
kind: "continue",
|
||||
directives: params.directives,
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
contextTokens: params.contextTokens,
|
||||
}));
|
||||
mocks.resolveFastModeState.mockImplementation(({ sessionEntry }) => ({
|
||||
enabled: sessionEntry?.sessionId === "target-session",
|
||||
}));
|
||||
mocks.resolveReplyExecOverrides.mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
it("prefers the target session entry from sessionStore for directive state", async () => {
|
||||
const { resolveReplyDirectives } = await loadResolveReplyDirectivesForTest();
|
||||
const wrapperSessionEntry = makeSessionEntry({
|
||||
sessionId: "wrapper-session",
|
||||
thinkingLevel: "low",
|
||||
verboseLevel: "off",
|
||||
reasoningLevel: "off",
|
||||
elevatedLevel: "off",
|
||||
parentSessionKey: "wrapper-parent",
|
||||
});
|
||||
const targetSessionEntry = makeSessionEntry({
|
||||
sessionId: "target-session",
|
||||
thinkingLevel: "high",
|
||||
verboseLevel: "full",
|
||||
reasoningLevel: "high",
|
||||
elevatedLevel: "on",
|
||||
parentSessionKey: "target-parent",
|
||||
});
|
||||
|
||||
const result = await resolveReplyDirectives({
|
||||
ctx: buildTestCtx({
|
||||
Body: "hello",
|
||||
CommandBody: "hello",
|
||||
ParentSessionKey: "ctx-parent",
|
||||
}),
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
agentDir: "/tmp/main-agent",
|
||||
workspaceDir: "/tmp",
|
||||
agentCfg: {},
|
||||
sessionCtx: {
|
||||
Body: "hello",
|
||||
BodyStripped: "hello",
|
||||
BodyForAgent: "hello",
|
||||
CommandBody: "hello",
|
||||
Provider: "whatsapp",
|
||||
} as TemplateContext,
|
||||
sessionEntry: wrapperSessionEntry,
|
||||
sessionStore: {
|
||||
"agent:main:whatsapp:+2000": targetSessionEntry,
|
||||
},
|
||||
sessionKey: "agent:main:whatsapp:+2000",
|
||||
storePath: "/tmp/sessions.json",
|
||||
sessionScope: "per-sender",
|
||||
groupResolution: undefined,
|
||||
isGroup: false,
|
||||
triggerBodyNormalized: "hello",
|
||||
commandAuthorized: false,
|
||||
defaultProvider: "openai",
|
||||
defaultModel: "gpt-4o-mini",
|
||||
aliasIndex: new Map(),
|
||||
provider: "openai",
|
||||
model: "gpt-4o-mini",
|
||||
hasResolvedHeartbeatModelOverride: false,
|
||||
typing: {
|
||||
onReplyStart: async () => {},
|
||||
startTypingLoop: async () => {},
|
||||
startTypingOnText: async () => {},
|
||||
refreshTypingTtl: () => {},
|
||||
isActive: () => false,
|
||||
markRunComplete: () => {},
|
||||
markDispatchIdle: () => {},
|
||||
cleanup: vi.fn(),
|
||||
},
|
||||
opts: undefined,
|
||||
skillFilter: undefined,
|
||||
});
|
||||
|
||||
expect(mocks.resolveFastModeState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionEntry: targetSessionEntry,
|
||||
}),
|
||||
);
|
||||
expect(mocks.createModelSelectionState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionEntry: targetSessionEntry,
|
||||
parentSessionKey: "target-parent",
|
||||
}),
|
||||
);
|
||||
expect(mocks.applyInlineDirectiveOverrides).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionEntry: targetSessionEntry,
|
||||
}),
|
||||
);
|
||||
expect(mocks.resolveReplyExecOverrides).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionEntry: targetSessionEntry,
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual({
|
||||
kind: "continue",
|
||||
result: expect.objectContaining({
|
||||
resolvedThinkLevel: "high",
|
||||
resolvedFastMode: true,
|
||||
resolvedVerboseLevel: "full",
|
||||
resolvedReasoningLevel: "high",
|
||||
resolvedElevatedLevel: "on",
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -177,6 +177,7 @@ export async function resolveReplyDirectives(params: {
|
||||
const agentEntry = listAgentEntries(cfg).find(
|
||||
(entry) => normalizeAgentId(entry.id) === normalizeAgentId(agentId),
|
||||
);
|
||||
const targetSessionEntry = sessionStore[sessionKey] ?? sessionEntry;
|
||||
let provider = initialProvider;
|
||||
let model = initialModel;
|
||||
|
||||
@@ -380,7 +381,7 @@ export async function resolveReplyDirectives(params: {
|
||||
});
|
||||
const defaultActivation = defaultGroupActivation(requireMention);
|
||||
const resolvedThinkLevel =
|
||||
directives.thinkLevel ?? (sessionEntry?.thinkingLevel as ThinkLevel | undefined);
|
||||
directives.thinkLevel ?? (targetSessionEntry?.thinkingLevel as ThinkLevel | undefined);
|
||||
const resolvedFastMode =
|
||||
directives.fastMode ??
|
||||
resolveFastModeState({
|
||||
@@ -388,21 +389,21 @@ export async function resolveReplyDirectives(params: {
|
||||
provider,
|
||||
model,
|
||||
agentId,
|
||||
sessionEntry,
|
||||
sessionEntry: targetSessionEntry,
|
||||
}).enabled;
|
||||
|
||||
const resolvedVerboseLevel =
|
||||
directives.verboseLevel ??
|
||||
(sessionEntry?.verboseLevel as VerboseLevel | undefined) ??
|
||||
(targetSessionEntry?.verboseLevel as VerboseLevel | undefined) ??
|
||||
(agentCfg?.verboseDefault as VerboseLevel | undefined);
|
||||
let resolvedReasoningLevel: ReasoningLevel =
|
||||
directives.reasoningLevel ??
|
||||
(sessionEntry?.reasoningLevel as ReasoningLevel | undefined) ??
|
||||
(targetSessionEntry?.reasoningLevel as ReasoningLevel | undefined) ??
|
||||
(agentEntry?.reasoningDefault as ReasoningLevel | undefined) ??
|
||||
"off";
|
||||
const resolvedElevatedLevel = elevatedAllowed
|
||||
? (directives.elevatedLevel ??
|
||||
(sessionEntry?.elevatedLevel as ElevatedLevel | undefined) ??
|
||||
(targetSessionEntry?.elevatedLevel as ElevatedLevel | undefined) ??
|
||||
(agentCfg?.elevatedDefault as ElevatedLevel | undefined) ??
|
||||
"on")
|
||||
: "off";
|
||||
@@ -430,8 +431,8 @@ export async function resolveReplyDirectives(params: {
|
||||
useFastReplyRuntime &&
|
||||
!directives.hasModelDirective &&
|
||||
!hasResolvedHeartbeatModelOverride &&
|
||||
!normalizeOptionalString(sessionEntry?.modelOverride) &&
|
||||
!normalizeOptionalString(sessionEntry?.providerOverride)
|
||||
!normalizeOptionalString(targetSessionEntry?.modelOverride) &&
|
||||
!normalizeOptionalString(targetSessionEntry?.providerOverride)
|
||||
? createFastTestModelSelectionState({
|
||||
agentCfg,
|
||||
provider,
|
||||
@@ -441,10 +442,10 @@ export async function resolveReplyDirectives(params: {
|
||||
cfg,
|
||||
agentId,
|
||||
agentCfg,
|
||||
sessionEntry,
|
||||
sessionEntry: targetSessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
parentSessionKey: ctx.ParentSessionKey,
|
||||
parentSessionKey: targetSessionEntry?.parentSessionKey ?? ctx.ParentSessionKey,
|
||||
storePath,
|
||||
defaultProvider,
|
||||
defaultModel,
|
||||
@@ -467,7 +468,8 @@ export async function resolveReplyDirectives(params: {
|
||||
agentEntry?.reasoningDefault !== undefined && agentEntry?.reasoningDefault !== null;
|
||||
const reasoningExplicitlySet =
|
||||
directives.reasoningLevel !== undefined ||
|
||||
(sessionEntry?.reasoningLevel !== undefined && sessionEntry?.reasoningLevel !== null) ||
|
||||
(targetSessionEntry?.reasoningLevel !== undefined &&
|
||||
targetSessionEntry?.reasoningLevel !== null) ||
|
||||
hasAgentReasoningDefault;
|
||||
const thinkingActive = resolvedThinkLevelWithDefault !== "off";
|
||||
if (!reasoningExplicitlySet && resolvedReasoningLevel === "off" && !thinkingActive) {
|
||||
@@ -502,7 +504,7 @@ export async function resolveReplyDirectives(params: {
|
||||
agentDir,
|
||||
agentCfg,
|
||||
agentEntry,
|
||||
sessionEntry,
|
||||
sessionEntry: targetSessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
storePath,
|
||||
@@ -539,7 +541,7 @@ export async function resolveReplyDirectives(params: {
|
||||
const { directiveAck, perMessageQueueMode, perMessageQueueOptions } = applyResult;
|
||||
const execOverrides = resolveReplyExecOverrides({
|
||||
directives,
|
||||
sessionEntry,
|
||||
sessionEntry: targetSessionEntry,
|
||||
agentExecDefaults: agentEntry?.tools?.exec,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user