mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 06:20:22 +00:00
test: replace exec directive e2e with pure coverage
This commit is contained in:
@@ -1,154 +0,0 @@
|
||||
import fs from "node:fs/promises";
|
||||
import "./reply.directive.directive-behavior.e2e-mocks.js";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
MAIN_SESSION_KEY,
|
||||
installDirectiveBehaviorE2EHooks,
|
||||
makeEmbeddedTextResult,
|
||||
sessionStorePath,
|
||||
withTempHome,
|
||||
} from "./reply.directive.directive-behavior.e2e-harness.js";
|
||||
import { runEmbeddedPiAgentMock } from "./reply.directive.directive-behavior.e2e-mocks.js";
|
||||
import { withFullRuntimeReplyConfig } from "./reply/get-reply-fast-path.js";
|
||||
|
||||
let getReplyFromConfig: typeof import("./reply/get-reply.js").getReplyFromConfig;
|
||||
|
||||
type ExpectedExecOverrides = {
|
||||
host: "node" | "auto" | "gateway";
|
||||
security: "allowlist" | "deny" | "full";
|
||||
ask: "always" | "off";
|
||||
node: string;
|
||||
};
|
||||
|
||||
const AGENT_EXEC_DEFAULTS = {
|
||||
host: "node",
|
||||
security: "allowlist",
|
||||
ask: "always",
|
||||
node: "worker-alpha",
|
||||
} as const satisfies ExpectedExecOverrides;
|
||||
|
||||
const WHATSAPP_EXEC_PROMPT_REQUEST = {
|
||||
Body: "run a command",
|
||||
From: "+1004",
|
||||
To: "+2000",
|
||||
Provider: "whatsapp",
|
||||
SenderE164: "+1004",
|
||||
} as const;
|
||||
|
||||
const AUTHORIZED_EXEC_DIRECTIVE_REQUEST = {
|
||||
From: "+1004",
|
||||
To: "+2000",
|
||||
CommandAuthorized: true,
|
||||
} as const;
|
||||
|
||||
function makeAgentExecConfig(home: string) {
|
||||
return withFullRuntimeReplyConfig({
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
humanDelay: { mode: "off" },
|
||||
workspace: `${home}/openclaw`,
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
tools: {
|
||||
exec: AGENT_EXEC_DEFAULTS,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: sessionStorePath(home) },
|
||||
});
|
||||
}
|
||||
|
||||
async function runExecPrompt(home: string) {
|
||||
await getReplyFromConfig(WHATSAPP_EXEC_PROMPT_REQUEST, {}, makeAgentExecConfig(home));
|
||||
}
|
||||
|
||||
async function runExecDirective(home: string, body: string) {
|
||||
await getReplyFromConfig(
|
||||
{ ...AUTHORIZED_EXEC_DIRECTIVE_REQUEST, Body: body },
|
||||
{},
|
||||
makeAgentExecConfig(home),
|
||||
);
|
||||
}
|
||||
|
||||
function expectLastExecOverrides(overrides: Partial<ExpectedExecOverrides> = {}) {
|
||||
expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce();
|
||||
const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0];
|
||||
expect(call?.execOverrides).toEqual({
|
||||
...AGENT_EXEC_DEFAULTS,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
describe("directive behavior exec agent defaults", () => {
|
||||
installDirectiveBehaviorE2EHooks();
|
||||
|
||||
beforeAll(async () => {
|
||||
({ getReplyFromConfig } = await import("./reply/get-reply.js"));
|
||||
});
|
||||
|
||||
it("threads per-agent tools.exec defaults into live runs without a persisted session override", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
runEmbeddedPiAgentMock.mockResolvedValue(makeEmbeddedTextResult("done"));
|
||||
|
||||
await runExecPrompt(home);
|
||||
|
||||
expectLastExecOverrides();
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers standalone inline exec directives over per-agent exec defaults on the next live run", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
runEmbeddedPiAgentMock.mockResolvedValue(makeEmbeddedTextResult("done"));
|
||||
|
||||
await runExecDirective(home, "/exec host=auto");
|
||||
|
||||
runEmbeddedPiAgentMock.mockClear();
|
||||
|
||||
await runExecPrompt(home);
|
||||
|
||||
expectLastExecOverrides({ host: "auto" });
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers persisted session exec overrides over per-agent exec defaults", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
runEmbeddedPiAgentMock.mockResolvedValue(makeEmbeddedTextResult("done"));
|
||||
await fs.writeFile(
|
||||
sessionStorePath(home),
|
||||
JSON.stringify({
|
||||
[MAIN_SESSION_KEY]: {
|
||||
sessionId: "main",
|
||||
updatedAt: Date.now(),
|
||||
execHost: "auto",
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
await runExecPrompt(home);
|
||||
|
||||
expectLastExecOverrides({ host: "auto" });
|
||||
});
|
||||
});
|
||||
|
||||
it("replaces a prior deny override with newer exec settings on later turns", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
runEmbeddedPiAgentMock.mockResolvedValue(makeEmbeddedTextResult("done"));
|
||||
|
||||
await runExecDirective(home, "/exec host=gateway security=deny ask=off");
|
||||
|
||||
await runExecDirective(home, "/exec host=gateway security=full ask=always");
|
||||
|
||||
runEmbeddedPiAgentMock.mockClear();
|
||||
|
||||
await runExecPrompt(home);
|
||||
|
||||
expectLastExecOverrides({ host: "gateway", security: "full" });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import { listAgentEntries } from "../../agents/agent-scope.js";
|
||||
import type { ExecToolDefaults } from "../../agents/bash-tools.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||
import { resolveFastModeState } from "../../agents/fast-mode.js";
|
||||
import type { ModelAliasIndex } from "../../agents/model-selection.js";
|
||||
@@ -25,6 +24,7 @@ import {
|
||||
} from "./get-reply-directive-aliases.js";
|
||||
import { applyInlineDirectiveOverrides } from "./get-reply-directives-apply.js";
|
||||
import { clearExecInlineDirectives, clearInlineDirectives } from "./get-reply-directives-utils.js";
|
||||
import { type ReplyExecOverrides, resolveReplyExecOverrides } from "./get-reply-exec-overrides.js";
|
||||
import { shouldUseReplyFastTestRuntime } from "./get-reply-fast-path.js";
|
||||
import { defaultGroupActivation, resolveGroupRequireMention } from "./groups.js";
|
||||
import { CURRENT_MESSAGE_MARKER, stripMentions, stripStructuralPrefixes } from "./mentions.js";
|
||||
@@ -38,8 +38,6 @@ import { stripInlineStatus } from "./reply-inline.js";
|
||||
import type { TypingController } from "./typing.js";
|
||||
|
||||
type AgentDefaults = NonNullable<OpenClawConfig["agents"]>["defaults"];
|
||||
type ExecOverrides = Pick<ExecToolDefaults, "host" | "security" | "ask" | "node">;
|
||||
type AgentEntry = NonNullable<NonNullable<OpenClawConfig["agents"]>["list"]>[number];
|
||||
|
||||
let commandsRegistryPromise: Promise<typeof import("../commands-registry.runtime.js")> | null =
|
||||
null;
|
||||
@@ -96,7 +94,7 @@ export type ReplyDirectiveContinuation = {
|
||||
resolvedVerboseLevel: VerboseLevel | undefined;
|
||||
resolvedReasoningLevel: ReasoningLevel;
|
||||
resolvedElevatedLevel: ElevatedLevel;
|
||||
execOverrides?: ExecOverrides;
|
||||
execOverrides?: ReplyExecOverrides;
|
||||
blockStreamingEnabled: boolean;
|
||||
blockReplyChunking?: {
|
||||
minChars: number;
|
||||
@@ -119,33 +117,6 @@ export type ReplyDirectiveContinuation = {
|
||||
};
|
||||
};
|
||||
|
||||
function resolveExecOverrides(params: {
|
||||
directives: InlineDirectives;
|
||||
sessionEntry?: SessionEntry;
|
||||
agentEntry?: AgentEntry;
|
||||
}): ExecOverrides | undefined {
|
||||
const host =
|
||||
params.directives.execHost ??
|
||||
(params.sessionEntry?.execHost as ExecOverrides["host"]) ??
|
||||
(params.agentEntry?.tools?.exec?.host as ExecOverrides["host"]);
|
||||
const security =
|
||||
params.directives.execSecurity ??
|
||||
(params.sessionEntry?.execSecurity as ExecOverrides["security"]) ??
|
||||
(params.agentEntry?.tools?.exec?.security as ExecOverrides["security"]);
|
||||
const ask =
|
||||
params.directives.execAsk ??
|
||||
(params.sessionEntry?.execAsk as ExecOverrides["ask"]) ??
|
||||
(params.agentEntry?.tools?.exec?.ask as ExecOverrides["ask"]);
|
||||
const node =
|
||||
params.directives.execNode ??
|
||||
params.sessionEntry?.execNode ??
|
||||
params.agentEntry?.tools?.exec?.node;
|
||||
if (!host && !security && !ask && !node) {
|
||||
return undefined;
|
||||
}
|
||||
return { host, security, ask, node };
|
||||
}
|
||||
|
||||
export type ReplyDirectiveResult =
|
||||
| { kind: "reply"; reply: ReplyPayload | ReplyPayload[] | undefined }
|
||||
| { kind: "continue"; result: ReplyDirectiveContinuation };
|
||||
@@ -566,7 +537,11 @@ export async function resolveReplyDirectives(params: {
|
||||
model = applyResult.model;
|
||||
contextTokens = applyResult.contextTokens;
|
||||
const { directiveAck, perMessageQueueMode, perMessageQueueOptions } = applyResult;
|
||||
const execOverrides = resolveExecOverrides({ directives, sessionEntry, agentEntry });
|
||||
const execOverrides = resolveReplyExecOverrides({
|
||||
directives,
|
||||
sessionEntry,
|
||||
agentExecDefaults: agentEntry?.tools?.exec,
|
||||
});
|
||||
|
||||
return {
|
||||
kind: "continue",
|
||||
|
||||
121
src/auto-reply/reply/get-reply-exec-overrides.test.ts
Normal file
121
src/auto-reply/reply/get-reply-exec-overrides.test.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ModelAliasIndex } from "../../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { parseInlineDirectives } from "./directive-handling.parse.js";
|
||||
import { persistInlineDirectives } from "./directive-handling.persist.js";
|
||||
import { type ReplyExecOverrides, resolveReplyExecOverrides } from "./get-reply-exec-overrides.js";
|
||||
|
||||
const AGENT_EXEC_DEFAULTS = {
|
||||
host: "node",
|
||||
security: "allowlist",
|
||||
ask: "always",
|
||||
node: "worker-alpha",
|
||||
} as const satisfies ReplyExecOverrides;
|
||||
|
||||
function createSessionEntry(overrides?: Partial<SessionEntry>): SessionEntry {
|
||||
return {
|
||||
sessionId: "main",
|
||||
updatedAt: Date.now(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function persistExecDirective(params: {
|
||||
sessionEntry: SessionEntry;
|
||||
sessionStore: Record<string, SessionEntry>;
|
||||
body: string;
|
||||
}) {
|
||||
await persistInlineDirectives({
|
||||
directives: parseInlineDirectives(params.body),
|
||||
cfg: { commands: { text: true } } as OpenClawConfig,
|
||||
agentDir: "/tmp/agent",
|
||||
sessionEntry: params.sessionEntry,
|
||||
sessionStore: params.sessionStore,
|
||||
sessionKey: "agent:main:main",
|
||||
elevatedEnabled: false,
|
||||
elevatedAllowed: false,
|
||||
defaultProvider: "anthropic",
|
||||
defaultModel: "claude-opus-4-6",
|
||||
aliasIndex: { byAlias: new Map(), byKey: new Map() } satisfies ModelAliasIndex,
|
||||
allowedModelKeys: new Set(),
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-6",
|
||||
initialModelLabel: "anthropic/claude-opus-4-6",
|
||||
formatModelSwitchEvent: (label) => label,
|
||||
agentCfg: undefined,
|
||||
surface: "whatsapp",
|
||||
});
|
||||
}
|
||||
|
||||
describe("reply exec overrides", () => {
|
||||
it("uses per-agent exec defaults when session and message are unset", () => {
|
||||
expect(
|
||||
resolveReplyExecOverrides({
|
||||
directives: parseInlineDirectives("run a command"),
|
||||
sessionEntry: createSessionEntry(),
|
||||
agentExecDefaults: AGENT_EXEC_DEFAULTS,
|
||||
}),
|
||||
).toEqual(AGENT_EXEC_DEFAULTS);
|
||||
});
|
||||
|
||||
it("prefers inline exec directives, then persisted session overrides, then agent defaults", () => {
|
||||
const sessionEntry = createSessionEntry({
|
||||
execHost: "gateway",
|
||||
execSecurity: "deny",
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveReplyExecOverrides({
|
||||
directives: parseInlineDirectives("/exec host=auto security=full"),
|
||||
sessionEntry,
|
||||
agentExecDefaults: AGENT_EXEC_DEFAULTS,
|
||||
}),
|
||||
).toEqual({
|
||||
...AGENT_EXEC_DEFAULTS,
|
||||
host: "auto",
|
||||
security: "full",
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveReplyExecOverrides({
|
||||
directives: parseInlineDirectives("run a command"),
|
||||
sessionEntry,
|
||||
agentExecDefaults: AGENT_EXEC_DEFAULTS,
|
||||
}),
|
||||
).toEqual({
|
||||
...AGENT_EXEC_DEFAULTS,
|
||||
host: "gateway",
|
||||
security: "deny",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves the latest persisted exec directive for later turns", async () => {
|
||||
const sessionEntry = createSessionEntry();
|
||||
const sessionStore = { "agent:main:main": sessionEntry };
|
||||
|
||||
await persistExecDirective({
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
body: "/exec host=gateway security=deny ask=off",
|
||||
});
|
||||
await persistExecDirective({
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
body: "/exec host=gateway security=full ask=always",
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveReplyExecOverrides({
|
||||
directives: parseInlineDirectives("run a command"),
|
||||
sessionEntry,
|
||||
agentExecDefaults: AGENT_EXEC_DEFAULTS,
|
||||
}),
|
||||
).toEqual({
|
||||
...AGENT_EXEC_DEFAULTS,
|
||||
host: "gateway",
|
||||
security: "full",
|
||||
ask: "always",
|
||||
});
|
||||
});
|
||||
});
|
||||
30
src/auto-reply/reply/get-reply-exec-overrides.ts
Normal file
30
src/auto-reply/reply/get-reply-exec-overrides.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { ExecToolDefaults } from "../../agents/bash-tools.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import type { InlineDirectives } from "./directive-handling.parse.js";
|
||||
|
||||
export type ReplyExecOverrides = Pick<ExecToolDefaults, "host" | "security" | "ask" | "node">;
|
||||
|
||||
export function resolveReplyExecOverrides(params: {
|
||||
directives: InlineDirectives;
|
||||
sessionEntry?: SessionEntry;
|
||||
agentExecDefaults?: ReplyExecOverrides;
|
||||
}): ReplyExecOverrides | undefined {
|
||||
const host =
|
||||
params.directives.execHost ??
|
||||
(params.sessionEntry?.execHost as ReplyExecOverrides["host"]) ??
|
||||
params.agentExecDefaults?.host;
|
||||
const security =
|
||||
params.directives.execSecurity ??
|
||||
(params.sessionEntry?.execSecurity as ReplyExecOverrides["security"]) ??
|
||||
params.agentExecDefaults?.security;
|
||||
const ask =
|
||||
params.directives.execAsk ??
|
||||
(params.sessionEntry?.execAsk as ReplyExecOverrides["ask"]) ??
|
||||
params.agentExecDefaults?.ask;
|
||||
const node =
|
||||
params.directives.execNode ?? params.sessionEntry?.execNode ?? params.agentExecDefaults?.node;
|
||||
if (!host && !security && !ask && !node) {
|
||||
return undefined;
|
||||
}
|
||||
return { host, security, ask, node };
|
||||
}
|
||||
Reference in New Issue
Block a user