Files
openclaw/src/auto-reply/reply/get-reply.fast-path.test.ts
2026-04-24 04:24:07 +01:00

240 lines
8.1 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import {
buildFastReplyCommandContext,
initFastReplySessionState,
markCompleteReplyConfig,
withFastReplyConfig,
} from "./get-reply-fast-path.js";
import {
buildGetReplyCtx,
createGetReplySessionState,
expectResolvedTelegramTimezone,
registerGetReplyRuntimeOverrides,
} from "./get-reply.test-fixtures.js";
import { loadGetReplyModuleForTest } from "./get-reply.test-loader.js";
import "./get-reply.test-runtime-mocks.js";
const mocks = vi.hoisted(() => ({
ensureAgentWorkspace: vi.fn(),
initSessionState: vi.fn(),
resolveReplyDirectives: vi.fn(),
}));
vi.mock("../../agents/workspace.js", () => ({
DEFAULT_AGENT_WORKSPACE_DIR: "/tmp/openclaw-workspace",
ensureAgentWorkspace: (...args: unknown[]) => mocks.ensureAgentWorkspace(...args),
}));
registerGetReplyRuntimeOverrides(mocks);
let getReplyFromConfig: typeof import("./get-reply.js").getReplyFromConfig;
let loadConfigMock: typeof import("../../config/config.js").loadConfig;
let runPreparedReplyMock: typeof import("./get-reply-run.js").runPreparedReply;
async function loadGetReplyRuntimeForTest() {
({ getReplyFromConfig } = await loadGetReplyModuleForTest({ cacheKey: import.meta.url }));
({ loadConfig: loadConfigMock } = await import("../../config/config.js"));
({ runPreparedReply: runPreparedReplyMock } = await import("./get-reply-run.js"));
}
describe("getReplyFromConfig fast test bootstrap", () => {
beforeAll(async () => {
await loadGetReplyRuntimeForTest();
});
beforeEach(() => {
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
mocks.ensureAgentWorkspace.mockReset();
mocks.initSessionState.mockReset();
mocks.resolveReplyDirectives.mockReset();
vi.mocked(loadConfigMock).mockReset();
vi.mocked(runPreparedReplyMock).mockReset();
vi.mocked(loadConfigMock).mockReturnValue({});
mocks.resolveReplyDirectives.mockResolvedValue({ kind: "reply", reply: { text: "ok" } });
vi.mocked(runPreparedReplyMock).mockResolvedValue({ text: "ok" });
mocks.initSessionState.mockResolvedValue(createGetReplySessionState());
});
afterEach(() => {
vi.unstubAllEnvs();
});
it("fails fast on unmarked config overrides in strict fast-test mode", async () => {
await expect(
getReplyFromConfig(buildGetReplyCtx(), undefined, {} as OpenClawConfig),
).rejects.toThrow(/withFastReplyConfig\(\)\/markCompleteReplyConfig\(\)/);
expect(vi.mocked(loadConfigMock)).not.toHaveBeenCalled();
});
it("skips loadConfig, workspace bootstrap, and session bootstrap for marked test configs", async () => {
const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-fast-reply-"));
const cfg = markCompleteReplyConfig({
agents: {
defaults: {
model: "anthropic/claude-opus-4-6",
workspace: path.join(home, "openclaw"),
},
},
channels: { telegram: { allowFrom: ["*"] } },
session: { store: path.join(home, "sessions.json") },
} as OpenClawConfig);
await expect(getReplyFromConfig(buildGetReplyCtx(), undefined, cfg)).resolves.toEqual({
text: "ok",
});
expect(vi.mocked(loadConfigMock)).not.toHaveBeenCalled();
expect(mocks.ensureAgentWorkspace).not.toHaveBeenCalled();
expect(mocks.initSessionState).not.toHaveBeenCalled();
expect(mocks.resolveReplyDirectives).not.toHaveBeenCalled();
expect(vi.mocked(runPreparedReplyMock)).toHaveBeenCalledWith(
expect.objectContaining({
cfg,
}),
);
});
it("still merges partial config overrides against loadConfig()", async () => {
vi.stubEnv("OPENCLAW_ALLOW_SLOW_REPLY_TESTS", "1");
vi.mocked(loadConfigMock).mockReturnValue({
channels: {
telegram: {
botToken: "resolved-telegram-token",
},
},
} satisfies OpenClawConfig);
await getReplyFromConfig(buildGetReplyCtx(), undefined, {
agents: {
defaults: {
userTimezone: "America/New_York",
},
},
} as OpenClawConfig);
expect(vi.mocked(loadConfigMock)).toHaveBeenCalledOnce();
expect(mocks.initSessionState).toHaveBeenCalledOnce();
expectResolvedTelegramTimezone(mocks.resolveReplyDirectives);
});
it("marks configs through withFastReplyConfig()", async () => {
const cfg = withFastReplyConfig({ session: { store: "/tmp/sessions.json" } } as OpenClawConfig);
await expect(getReplyFromConfig(buildGetReplyCtx(), undefined, cfg)).resolves.toEqual({
text: "ok",
});
expect(vi.mocked(loadConfigMock)).not.toHaveBeenCalled();
expect(mocks.resolveReplyDirectives).not.toHaveBeenCalled();
expect(vi.mocked(runPreparedReplyMock)).toHaveBeenCalledOnce();
});
it("uses native command target session keys during fast bootstrap", () => {
const result = initFastReplySessionState({
ctx: buildGetReplyCtx({
SessionKey: "telegram:slash:123",
CommandSource: "native",
CommandTargetSessionKey: "agent:main:main",
}),
cfg: { session: { store: "/tmp/sessions.json" } } as OpenClawConfig,
agentId: "main",
commandAuthorized: true,
workspaceDir: "/tmp/workspace",
});
expect(result.sessionKey).toBe("agent:main:main");
expect(result.sessionCtx.SessionKey).toBe("agent:main:main");
});
it("maps explicit gateway origin into command context", () => {
const command = buildFastReplyCommandContext({
ctx: buildGetReplyCtx({
Provider: "internal",
Surface: "internal",
OriginatingChannel: "slack",
OriginatingTo: "user:U123",
From: undefined,
To: undefined,
SenderId: "gateway-client",
}),
cfg: {} as OpenClawConfig,
sessionKey: "main",
isGroup: false,
triggerBodyNormalized: "/codex bind",
commandAuthorized: true,
});
expect(command.channel).toBe("slack");
expect(command.channelId).toBe("slack");
expect(command.from).toBe("gateway-client");
expect(command.to).toBe("user:U123");
});
it("keeps the existing session for /reset newline soft during fast bootstrap", async () => {
const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-fast-reset-newline-soft-"));
const storePath = path.join(home, "sessions.json");
const sessionKey = "agent:main:telegram:123";
await fs.writeFile(
storePath,
JSON.stringify({
[sessionKey]: {
sessionId: "existing-fast-reset-newline-soft",
updatedAt: Date.now(),
},
}),
"utf8",
);
const result = initFastReplySessionState({
ctx: buildGetReplyCtx({
Body: "/reset \nsoft",
RawBody: "/reset \nsoft",
CommandBody: "/reset \nsoft",
SessionKey: sessionKey,
}),
cfg: { session: { store: storePath } } as OpenClawConfig,
agentId: "main",
commandAuthorized: true,
workspaceDir: home,
});
expect(result.resetTriggered).toBe(false);
expect(result.isNewSession).toBe(false);
expect(result.sessionId).toBe("existing-fast-reset-newline-soft");
});
it("keeps the existing session for /reset: soft during fast bootstrap", async () => {
const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-fast-reset-colon-soft-"));
const storePath = path.join(home, "sessions.json");
const sessionKey = "agent:main:telegram:123";
await fs.writeFile(
storePath,
JSON.stringify({
[sessionKey]: {
sessionId: "existing-fast-reset-colon-soft",
updatedAt: Date.now(),
},
}),
"utf8",
);
const result = initFastReplySessionState({
ctx: buildGetReplyCtx({
Body: "/reset: soft",
RawBody: "/reset: soft",
CommandBody: "/reset: soft",
SessionKey: sessionKey,
}),
cfg: { session: { store: storePath } } as OpenClawConfig,
agentId: "main",
commandAuthorized: true,
workspaceDir: home,
});
expect(result.resetTriggered).toBe(false);
expect(result.isNewSession).toBe(false);
expect(result.sessionId).toBe("existing-fast-reset-colon-soft");
});
});