refactor(reply): share fast-mode session persistence (#147847)

This commit is contained in:
Vincent Koc 2026-09-14 13:15:08 +08:00 • committed by GitHub
parent ba92d2420b
commit 9d5d33201a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 103 additions and 20 deletions

View file

@ -1,11 +1,14 @@
// Tests session usage command output and token accounting summaries.
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { loadSessionEntry, replaceSessionEntry } from "../../config/sessions/session-accessor.js";
import type {
CostUsageSummary,
CostUsageTotals,
SessionCostSummary,
} from "../../infra/session-cost-usage.js";
import { withTempDir } from "../../test-utils/temp-dir.js";
import { handleFastCommand, handleUsageCommand } from "./commands-session.js";
import type { HandleCommandsParams } from "./commands-types.js";
@ -436,6 +439,95 @@ describe("handleFastCommand", () => {
expect(sessionEntry?.fastMode).toBe(true);
});
it.each([
{ mode: "on", value: true, text: "⚙️ Fast mode enabled." },
{ mode: "off", value: false, text: "⚙️ Fast mode disabled." },
{ mode: "auto", value: "auto", text: "⚙️ Fast mode set to auto." },
] as const)("persists /fast $mode on the canonical target", async ({ mode, value, text }) => {
await withTempDir("openclaw-fast-command-", async (dir) => {
const params = buildUsageParams();
params.command.commandBodyNormalized = `/fast ${mode}`;
params.storePath = path.join(dir, "sessions.json");
params.sessionEntry = { sessionId: "wrapper-session", updatedAt: 1, fastMode: false };
const target: NonNullable<HandleCommandsParams["sessionEntry"]> = {
sessionId: "target-session",
updatedAt: 1,
fastMode: mode !== "on",
};
params.initialSessionEntry = { ...target };
params.sessionStore = { [params.sessionKey]: target };
await replaceSessionEntry(
{ storePath: params.storePath, sessionKey: params.sessionKey },
{ ...target },
);
const result = await handleFastCommand(params, true);
expect(result).toEqual({ shouldContinue: false, reply: { text } });
expect(
loadSessionEntry({ storePath: params.storePath, sessionKey: params.sessionKey })?.fastMode,
).toBe(value);
expect(params.sessionStore[params.sessionKey]?.fastMode).toBe(value);
expect(params.sessionEntry.fastMode).toBe(false);
expect(resolveFastModeStateMock).not.toHaveBeenCalled();
});
});
it.each([
["on", "⚙️ Fast mode enabled."],
["off", "⚙️ Fast mode disabled."],
["auto", "⚙️ Fast mode set to auto."],
["default", "⚙️ Fast mode reset to default."],
])("keeps the /fast %s reply without a target entry", async (mode, text) => {
const params = buildUsageParams();
params.command.commandBodyNormalized = `/fast ${mode}`;
params.sessionEntry = undefined;
expect(await handleFastCommand(params, true)).toEqual({
shouldContinue: false,
reply: { text },
});
expect(params.sessionStore).toBeUndefined();
expect(resolveFastModeStateMock).not.toHaveBeenCalled();
});
it("rejects an invalid /fast mode without mutating the target", async () => {
const params = buildUsageParams();
params.command.commandBodyNormalized = "/fast invalid";
const target = { sessionId: "target-session", updatedAt: 1, fastMode: true };
params.sessionEntry = target;
params.sessionStore = { [params.sessionKey]: target };
expect(await handleFastCommand(params, true)).toEqual({
shouldContinue: false,
reply: { text: "⚙️ Usage: /fast status|auto|on|off|default" },
});
expect(target).toEqual({ sessionId: "target-session", updatedAt: 1, fastMode: true });
expect(resolveFastModeStateMock).not.toHaveBeenCalled();
});
it.each(["default", "off"])("reports a /fast %s persistence conflict", async (mode) => {
await withTempDir("openclaw-fast-conflict-", async (dir) => {
const params = buildUsageParams();
params.command.commandBodyNormalized = `/fast ${mode}`;
params.storePath = path.join(dir, "sessions.json");
const target = { sessionId: "missing-session", updatedAt: 1, fastMode: true };
params.sessionEntry = target;
params.initialSessionEntry = { ...target };
params.sessionStore = { [params.sessionKey]: target };
expect(await handleFastCommand(params, true)).toEqual({
shouldContinue: false,
reply: {
text: "⚠️ Session changed before this setting could be saved. Retry the command.",
},
});
expect(
loadSessionEntry({ storePath: params.storePath, sessionKey: params.sessionKey }),
).toBeUndefined();
});
});
it("clears fast mode for /fast default", async () => {
const params = buildUsageParams();
params.command.commandBodyNormalized = "/fast default";

View file

@ -325,27 +325,16 @@ export const handleFastCommand: CommandHandler = defineAuthorizedTextCommand(
const targetSessionEntry = params.sessionStore?.[params.sessionKey] ?? params.sessionEntry;
const resetsToDefault = isSessionDefaultDirectiveValue(rawMode);
const nextMode = resetsToDefault ? undefined : normalizeFastMode(rawMode);
if (nextMode === undefined) {
if (resetsToDefault) {
if (targetSessionEntry && params.sessionStore && params.sessionKey) {
delete targetSessionEntry.fastMode;
if (
!(await persistCommandSession({
...params,
sessionEntry: targetSessionEntry,
touchedFields: ["fastMode"],
}))
) {
return sessionEntryPersistenceConflictReply();
}
}
return sessionCommandReply("⚙️ Fast mode reset to default.");
}
if (nextMode === undefined && !resetsToDefault) {
return sessionCommandReply("⚙️ Usage: /fast status|auto|on|off|default");
}
if (targetSessionEntry && params.sessionStore && params.sessionKey) {
targetSessionEntry.fastMode = nextMode;
if (resetsToDefault) {
delete targetSessionEntry.fastMode;
} else {
targetSessionEntry.fastMode = nextMode;
}
if (
!(await persistCommandSession({
...params,
@ -358,9 +347,11 @@ export const handleFastCommand: CommandHandler = defineAuthorizedTextCommand(
}
return sessionCommandReply(
nextMode === "auto"
? "⚙️ Fast mode set to auto."
: `⚙️ Fast mode ${nextMode ? "enabled" : "disabled"}.`,
resetsToDefault
? "⚙️ Fast mode reset to default."
: nextMode === "auto"
? "⚙️ Fast mode set to auto."
: `⚙️ Fast mode ${nextMode ? "enabled" : "disabled"}.`,
);
},
);