test: replace exec directive e2e with pure coverage

This commit is contained in:
Peter Steinberger
2026-04-08 22:55:38 +01:00
parent 10c87527d5
commit 9ffe216a52
4 changed files with 158 additions and 186 deletions

View File

@@ -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" });
});
});
});

View File

@@ -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",

View 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",
});
});
});

View 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 };
}