test: thin runReplyAgent misc runner coverage

This commit is contained in:
Peter Steinberger
2026-04-09 04:44:09 +01:00
parent 20214d4232
commit 53dbae29b7

View File

@@ -1,4 +1,3 @@
import crypto from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@@ -9,9 +8,6 @@ import {
isEmbeddedPiRunActive,
} from "../../agents/pi-embedded-runner/runs.js";
import type { SessionEntry } from "../../config/sessions.js";
import { loadSessionStore, saveSessionStore } from "../../config/sessions.js";
import { onAgentEvent } from "../../infra/agent-events.js";
import { peekSystemEvents, resetSystemEventsForTest } from "../../infra/system-events.js";
import {
clearMemoryPluginState,
registerMemoryFlushPlanResolver,
@@ -57,6 +53,10 @@ vi.mock("../../agents/model-fallback.js", () => ({
Array.isArray((err as { attempts?: unknown[] }).attempts),
}));
vi.mock("../../agents/model-auth.js", () => ({
resolveModelAuthMode: () => "api-key",
}));
vi.mock("../../agents/pi-embedded.js", () => {
return {
compactEmbeddedPiSession: (params: unknown) =>
@@ -94,6 +94,18 @@ vi.mock("./queue.js", () => {
};
});
vi.mock("../../cli/command-secret-gateway.js", () => ({
resolveCommandSecretRefsViaGateway: async ({ config }: { config: unknown }) => ({
resolvedConfig: config,
diagnostics: [],
}),
}));
vi.mock("../../utils/provider-utils.js", () => ({
isReasoningTagProvider: (provider: string | undefined | null) =>
provider === "google" || provider === "google-gemini-cli",
}));
const loadCronStoreMock = vi.fn();
vi.mock("../../cron/store.js", () => {
return {
@@ -131,6 +143,11 @@ beforeEach(() => {
runWithModelFallbackMock.mockClear();
runtimeErrorMock.mockClear();
abortEmbeddedPiRunMock.mockClear();
compactState.compactEmbeddedPiSessionMock.mockReset();
compactState.compactEmbeddedPiSessionMock.mockResolvedValue({
compacted: false,
reason: "test-preflight-disabled",
});
clearSessionQueuesMock.mockReset();
clearSessionQueuesMock.mockReturnValue({ followupCleared: 0, laneCleared: 0, keys: [] });
refreshQueuedFollowupSessionMock.mockReset();
@@ -138,7 +155,6 @@ beforeEach(() => {
loadCronStoreMock.mockClear();
// Default: no cron jobs in store.
loadCronStoreMock.mockResolvedValue({ version: 1, jobs: [] });
resetSystemEventsForTest();
// Default: no provider switch; execute the chosen provider+model.
runWithModelFallbackMock.mockImplementation(
@@ -152,134 +168,12 @@ beforeEach(() => {
afterEach(() => {
vi.useRealTimers();
resetSystemEventsForTest();
clearMemoryPluginState();
replyRunRegistryTesting.resetReplyRunRegistry();
embeddedRunTesting.resetActiveEmbeddedRuns();
});
describe("runReplyAgent onAgentRunStart", () => {
function createRun(params?: {
provider?: string;
model?: string;
opts?: {
runId?: string;
onAgentRunStart?: (runId: string) => void;
};
}) {
const provider = params?.provider ?? "anthropic";
const model = params?.model ?? "claude";
const typing = createMockTypingController();
const sessionCtx = {
Provider: "webchat",
OriginatingTo: "session:1",
AccountId: "primary",
MessageSid: "msg",
} as unknown as TemplateContext;
const resolvedQueue = { mode: "interrupt" } as unknown as QueueSettings;
const followupRun = {
prompt: "hello",
summaryLine: "hello",
enqueuedAt: Date.now(),
run: {
sessionId: "session",
sessionKey: "main",
messageProvider: "webchat",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp",
config:
provider === "claude-cli"
? { agents: { defaults: { cliBackends: { "claude-cli": {} } } } }
: createCliBackendTestConfig(),
skillsSnapshot: {},
provider,
model,
thinkLevel: "low",
verboseLevel: "off",
elevatedLevel: "off",
bashElevated: {
enabled: false,
allowed: false,
defaultLevel: "off",
},
timeoutMs: 1_000,
blockReplyBreak: "message_end",
},
} as unknown as FollowupRun;
return runReplyAgent({
commandBody: "hello",
followupRun,
queueKey: "main",
resolvedQueue,
shouldSteer: false,
shouldFollowup: false,
isActive: false,
isStreaming: false,
opts: params?.opts,
typing,
sessionCtx,
defaultModel: `${provider}/${model}`,
resolvedVerboseLevel: "off",
isNewSession: false,
blockStreamingEnabled: false,
resolvedBlockStreamingBreak: "message_end",
shouldInjectGroupIntro: false,
typingMode: "instant",
});
}
it("does not emit start callback when fallback fails before run start", async () => {
runWithModelFallbackMock.mockRejectedValueOnce(
new Error('No API key found for provider "anthropic".'),
);
const onAgentRunStart = vi.fn();
const result = await createRun({
opts: { runId: "run-no-start", onAgentRunStart },
});
expect(onAgentRunStart).not.toHaveBeenCalled();
expect(result).toMatchObject({
text: expect.stringContaining('No API key found for provider "anthropic".'),
});
});
it("emits start callback when the CLI runner starts", async () => {
runCliAgentMock.mockResolvedValueOnce({
payloads: [{ text: "ok" }],
meta: {
agentMeta: {
provider: "claude-cli",
model: "opus-4.5",
},
},
});
const onAgentRunStart = vi.fn();
const result = await createRun({
provider: "claude-cli",
model: "opus-4.5",
opts: { runId: "run-started", onAgentRunStart },
});
expect(onAgentRunStart).toHaveBeenCalledTimes(1);
expect(onAgentRunStart).toHaveBeenCalledWith("run-started");
expect(result).toMatchObject({ text: "ok" });
});
});
describe("runReplyAgent auto-compaction token update", () => {
type EmbeddedRunParams = {
prompt?: string;
extraSystemPrompt?: string;
abortSignal?: AbortSignal;
onAgentEvent?: (evt: {
stream?: string;
data?: { phase?: string; willRetry?: boolean; completed?: boolean };
}) => void;
};
async function seedSessionStore(params: {
storePath: string;
sessionKey: string;
@@ -293,11 +187,6 @@ describe("runReplyAgent auto-compaction token update", () => {
);
}
async function normalizeComparablePath(filePath: string): Promise<string> {
const parent = await fs.realpath(path.dirname(filePath)).catch(() => path.dirname(filePath));
return path.join(parent, path.basename(filePath));
}
function createBaseRun(params: {
storePath: string;
sessionEntry: Record<string, unknown>;
@@ -340,321 +229,6 @@ describe("runReplyAgent auto-compaction token update", () => {
return { typing, sessionCtx, resolvedQueue, followupRun };
}
it("updates totalTokens after auto-compaction using lastCallUsage", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-compact-tokens-"));
const storePath = path.join(tmp, "sessions.json");
const sessionKey = "main";
const sessionEntry = {
sessionId: "session",
sessionFile: path.join(tmp, "session.jsonl"),
updatedAt: Date.now(),
totalTokens: 181_000,
compactionCount: 0,
};
await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => {
// Simulate auto-compaction during agent run
params.onAgentEvent?.({ stream: "compaction", data: { phase: "start" } });
params.onAgentEvent?.({
stream: "compaction",
data: { phase: "end", willRetry: false, completed: true },
});
return {
payloads: [{ text: "done" }],
meta: {
agentMeta: {
// Accumulated usage across pre+post compaction calls — inflated
usage: { input: 190_000, output: 8_000, total: 198_000 },
// Last individual API call's usage — actual post-compaction context
lastCallUsage: { input: 10_000, output: 3_000, total: 13_000 },
compactionCount: 1,
},
},
};
});
// Disable memory flush so we isolate the auto-compaction path
const config = {
agents: { defaults: { compaction: { memoryFlush: { enabled: false } } } },
};
const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({
storePath,
sessionEntry,
config,
});
await runReplyAgent({
commandBody: "hello",
followupRun,
queueKey: "main",
resolvedQueue,
shouldSteer: false,
shouldFollowup: false,
isActive: false,
isStreaming: false,
typing,
sessionCtx,
sessionEntry,
sessionStore: { [sessionKey]: sessionEntry },
sessionKey,
storePath,
defaultModel: "anthropic/claude-opus-4-6",
agentCfgContextTokens: 200_000,
resolvedVerboseLevel: "off",
isNewSession: false,
blockStreamingEnabled: false,
resolvedBlockStreamingBreak: "message_end",
shouldInjectGroupIntro: false,
typingMode: "instant",
});
const stored = JSON.parse(await fs.readFile(storePath, "utf-8"));
// totalTokens should reflect actual post-compaction context (~10k), not
// the stale pre-compaction value (181k) or the inflated accumulated (190k)
expect(stored[sessionKey].totalTokens).toBe(10_000);
// compactionCount should be incremented
expect(stored[sessionKey].compactionCount).toBe(1);
});
it("tracks auto-compaction from embedded result metadata even when no compaction event is emitted", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-compact-meta-"));
const storePath = path.join(tmp, "sessions.json");
const sessionKey = "main";
const sessionEntry = {
sessionId: "session",
updatedAt: Date.now(),
totalTokens: 181_000,
compactionCount: 0,
};
await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
runEmbeddedPiAgentMock.mockResolvedValue({
payloads: [{ text: "done" }],
meta: {
agentMeta: {
sessionId: "session-rotated",
usage: { input: 190_000, output: 8_000, total: 198_000 },
lastCallUsage: { input: 10_000, output: 3_000, total: 13_000 },
compactionCount: 2,
},
},
});
const config = {
agents: { defaults: { compaction: { memoryFlush: { enabled: false } } } },
};
const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({
storePath,
sessionEntry,
config,
});
await runReplyAgent({
commandBody: "hello",
followupRun,
queueKey: "main",
resolvedQueue,
shouldSteer: false,
shouldFollowup: false,
isActive: false,
isStreaming: false,
typing,
sessionCtx,
sessionEntry,
sessionStore: { [sessionKey]: sessionEntry },
sessionKey,
storePath,
defaultModel: "anthropic/claude-opus-4-6",
agentCfgContextTokens: 200_000,
resolvedVerboseLevel: "off",
isNewSession: false,
blockStreamingEnabled: false,
resolvedBlockStreamingBreak: "message_end",
shouldInjectGroupIntro: false,
typingMode: "instant",
});
const stored = JSON.parse(await fs.readFile(storePath, "utf-8"));
expect(stored[sessionKey].totalTokens).toBe(10_000);
expect(stored[sessionKey].compactionCount).toBe(2);
expect(stored[sessionKey].sessionId).toBe("session-rotated");
expect(await normalizeComparablePath(stored[sessionKey].sessionFile)).toBe(
await normalizeComparablePath(path.join(tmp, "session-rotated.jsonl")),
);
});
it("accumulates compactions across fallback attempts without double-counting a single attempt", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-compact-fallback-"));
const storePath = path.join(tmp, "sessions.json");
const sessionKey = "main";
const sessionEntry = {
sessionId: "session",
updatedAt: Date.now(),
totalTokens: 181_000,
compactionCount: 0,
};
await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
runWithModelFallbackMock.mockImplementationOnce(async ({ run }: RunWithModelFallbackParams) => {
try {
await run("anthropic", "claude");
} catch {
// Expected first-attempt failure.
}
return {
result: await run("openai", "gpt-5.4"),
provider: "openai",
model: "gpt-5.4",
attempts: [{ provider: "anthropic", model: "claude", error: "attempt failed" }],
};
});
runEmbeddedPiAgentMock
.mockImplementationOnce(async (params: EmbeddedRunParams) => {
params.onAgentEvent?.({
stream: "compaction",
data: { phase: "end", willRetry: true, completed: true },
});
throw new Error("attempt failed");
})
.mockResolvedValueOnce({
payloads: [{ text: "done" }],
meta: {
agentMeta: {
usage: { input: 190_000, output: 8_000, total: 198_000 },
lastCallUsage: { input: 10_000, output: 3_000, total: 13_000 },
compactionCount: 2,
},
},
});
const config = {
agents: { defaults: { compaction: { memoryFlush: { enabled: false } } } },
};
const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({
storePath,
sessionEntry,
config,
});
await runReplyAgent({
commandBody: "hello",
followupRun,
queueKey: "main",
resolvedQueue,
shouldSteer: false,
shouldFollowup: false,
isActive: false,
isStreaming: false,
typing,
sessionCtx,
sessionEntry,
sessionStore: { [sessionKey]: sessionEntry },
sessionKey,
storePath,
defaultModel: "anthropic/claude-opus-4-6",
agentCfgContextTokens: 200_000,
resolvedVerboseLevel: "off",
isNewSession: false,
blockStreamingEnabled: false,
resolvedBlockStreamingBreak: "message_end",
shouldInjectGroupIntro: false,
typingMode: "instant",
});
const stored = JSON.parse(await fs.readFile(storePath, "utf-8"));
expect(stored[sessionKey].totalTokens).toBe(10_000);
expect(stored[sessionKey].compactionCount).toBe(3);
});
it("does not count failed compaction end events from earlier fallback attempts", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-compact-fallback-failed-"));
const storePath = path.join(tmp, "sessions.json");
const sessionKey = "main";
const sessionEntry = {
sessionId: "session",
updatedAt: Date.now(),
totalTokens: 181_000,
compactionCount: 0,
};
await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
runWithModelFallbackMock.mockImplementationOnce(async ({ run }: RunWithModelFallbackParams) => {
try {
await run("anthropic", "claude");
} catch {
// Expected first-attempt failure.
}
return {
result: await run("openai", "gpt-5.4"),
provider: "openai",
model: "gpt-5.4",
attempts: [{ provider: "anthropic", model: "claude", error: "attempt failed" }],
};
});
runEmbeddedPiAgentMock
.mockImplementationOnce(async (params: EmbeddedRunParams) => {
params.onAgentEvent?.({
stream: "compaction",
data: { phase: "end", willRetry: true, completed: false },
});
throw new Error("attempt failed");
})
.mockResolvedValueOnce({
payloads: [{ text: "done" }],
meta: {
agentMeta: {
usage: { input: 190_000, output: 8_000, total: 198_000 },
lastCallUsage: { input: 10_000, output: 3_000, total: 13_000 },
compactionCount: 2,
},
},
});
const config = {
agents: { defaults: { compaction: { memoryFlush: { enabled: false } } } },
};
const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({
storePath,
sessionEntry,
config,
});
await runReplyAgent({
commandBody: "hello",
followupRun,
queueKey: "main",
resolvedQueue,
shouldSteer: false,
shouldFollowup: false,
isActive: false,
isStreaming: false,
typing,
sessionCtx,
sessionEntry,
sessionStore: { [sessionKey]: sessionEntry },
sessionKey,
storePath,
defaultModel: "anthropic/claude-opus-4-6",
agentCfgContextTokens: 200_000,
resolvedVerboseLevel: "off",
isNewSession: false,
blockStreamingEnabled: false,
resolvedBlockStreamingBreak: "message_end",
shouldInjectGroupIntro: false,
typingMode: "instant",
});
const stored = JSON.parse(await fs.readFile(storePath, "utf-8"));
expect(stored[sessionKey].totalTokens).toBe(10_000);
expect(stored[sessionKey].compactionCount).toBe(2);
});
it("updates totalTokens from lastCallUsage even without compaction", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-usage-last-"));
const storePath = path.join(tmp, "sessions.json");
@@ -712,87 +286,6 @@ describe("runReplyAgent auto-compaction token update", () => {
// totalTokens should use lastCallUsage (55k), not accumulated (75k)
expect(stored[sessionKey].totalTokens).toBe(55_000);
});
it("does not enqueue legacy post-compaction audit warnings", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-no-audit-warning-"));
const workspaceDir = path.join(tmp, "workspace");
await fs.mkdir(workspaceDir, { recursive: true });
const sessionFile = path.join(tmp, "session.jsonl");
await fs.writeFile(
sessionFile,
`${JSON.stringify({ type: "message", message: { role: "assistant", content: [] } })}\n`,
"utf-8",
);
const storePath = path.join(tmp, "sessions.json");
const sessionKey = "main";
const sessionEntry = {
sessionId: "session",
updatedAt: Date.now(),
totalTokens: 10_000,
compactionCount: 0,
};
await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => {
params.onAgentEvent?.({ stream: "compaction", data: { phase: "start" } });
params.onAgentEvent?.({
stream: "compaction",
data: { phase: "end", willRetry: false, completed: true },
});
return {
payloads: [{ text: "done" }],
meta: {
agentMeta: {
usage: { input: 11_000, output: 500, total: 11_500 },
lastCallUsage: { input: 10_500, output: 500, total: 11_000 },
compactionCount: 1,
},
},
};
});
const config = {
agents: { defaults: { compaction: { memoryFlush: { enabled: false } } } },
};
const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({
storePath,
sessionEntry,
config,
sessionFile,
workspaceDir,
});
await runReplyAgent({
commandBody: "hello",
followupRun,
queueKey: "main",
resolvedQueue,
shouldSteer: false,
shouldFollowup: false,
isActive: false,
isStreaming: false,
typing,
sessionCtx,
sessionEntry,
sessionStore: { [sessionKey]: sessionEntry },
sessionKey,
storePath,
defaultModel: "anthropic/claude-opus-4-6",
agentCfgContextTokens: 200_000,
resolvedVerboseLevel: "off",
isNewSession: false,
blockStreamingEnabled: false,
resolvedBlockStreamingBreak: "message_end",
shouldInjectGroupIntro: false,
typingMode: "instant",
});
const queuedSystemEvents = peekSystemEvents(sessionKey);
expect(queuedSystemEvents.some((event) => event.includes("Post-Compaction Audit"))).toBe(false);
expect(queuedSystemEvents.some((event) => event.includes("WORKFLOW_AUTO.md"))).toBe(false);
});
});
describe("runReplyAgent block streaming", () => {
@@ -989,101 +482,6 @@ describe("runReplyAgent block streaming", () => {
});
});
describe("runReplyAgent claude-cli routing", () => {
function createRun() {
const typing = createMockTypingController();
const sessionCtx = {
Provider: "webchat",
OriginatingTo: "session:1",
AccountId: "primary",
MessageSid: "msg",
} as unknown as TemplateContext;
const resolvedQueue = { mode: "interrupt" } as unknown as QueueSettings;
const followupRun = {
prompt: "hello",
summaryLine: "hello",
enqueuedAt: Date.now(),
run: {
sessionId: "session",
sessionKey: "main",
messageProvider: "webchat",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp",
config: { agents: { defaults: { cliBackends: { "claude-cli": {} } } } },
skillsSnapshot: {},
provider: "claude-cli",
model: "opus-4.5",
thinkLevel: "low",
verboseLevel: "off",
elevatedLevel: "off",
bashElevated: {
enabled: false,
allowed: false,
defaultLevel: "off",
},
timeoutMs: 1_000,
blockReplyBreak: "message_end",
},
} as unknown as FollowupRun;
return runReplyAgent({
commandBody: "hello",
followupRun,
queueKey: "main",
resolvedQueue,
shouldSteer: false,
shouldFollowup: false,
isActive: false,
isStreaming: false,
typing,
sessionCtx,
defaultModel: "claude-cli/opus-4.5",
resolvedVerboseLevel: "off",
isNewSession: false,
blockStreamingEnabled: false,
resolvedBlockStreamingBreak: "message_end",
shouldInjectGroupIntro: false,
typingMode: "instant",
});
}
it("uses the CLI runner for claude-cli provider", async () => {
const runId = "00000000-0000-0000-0000-000000000001";
const randomSpy = vi.spyOn(crypto, "randomUUID").mockReturnValue(runId);
const lifecyclePhases: string[] = [];
const unsubscribe = onAgentEvent((evt) => {
if (evt.runId !== runId) {
return;
}
if (evt.stream !== "lifecycle") {
return;
}
const phase = evt.data?.phase;
if (typeof phase === "string") {
lifecyclePhases.push(phase);
}
});
runCliAgentMock.mockResolvedValueOnce({
payloads: [{ text: "ok" }],
meta: {
agentMeta: {
provider: "claude-cli",
model: "opus-4.5",
},
},
});
const result = await createRun();
unsubscribe();
randomSpy.mockRestore();
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
expect(runCliAgentMock).toHaveBeenCalledTimes(1);
expect(lifecyclePhases).toEqual(["start", "end"]);
expect(result).toMatchObject({ text: "ok" });
});
});
describe("runReplyAgent messaging tool suppression", () => {
function createRun(
messageProvider = "slack",
@@ -1206,108 +604,6 @@ describe("runReplyAgent messaging tool suppression", () => {
expect(result).toMatchObject({ text: "hello world!" });
});
it("persists usage fields even when replies are suppressed", async () => {
const storePath = path.join(
await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-store-")),
"sessions.json",
);
const sessionKey = "main";
const entry: SessionEntry = { sessionId: "session", updatedAt: Date.now() };
await saveSessionStore(storePath, { [sessionKey]: entry });
runEmbeddedPiAgentMock.mockResolvedValueOnce({
payloads: [{ text: "hello world!" }],
messagingToolSentTexts: ["different message"],
messagingToolSentTargets: [{ tool: "slack", provider: "slack", to: "channel:C1" }],
meta: {
agentMeta: {
usage: { input: 10, output: 5 },
model: "claude-opus-4-6",
provider: "anthropic",
},
},
});
const result = await createRun("slack", { storePath, sessionKey });
expect(result).toBeUndefined();
const store = loadSessionStore(storePath, { skipCache: true });
expect(store[sessionKey]?.inputTokens).toBe(10);
expect(store[sessionKey]?.outputTokens).toBe(5);
expect(store[sessionKey]?.totalTokens).toBeUndefined();
expect(store[sessionKey]?.totalTokensFresh).toBe(false);
expect(store[sessionKey]?.model).toBe("claude-opus-4-6");
});
it("persists totalTokens from promptTokens when snapshot is available", async () => {
const storePath = path.join(
await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-store-")),
"sessions.json",
);
const sessionKey = "main";
const entry: SessionEntry = { sessionId: "session", updatedAt: Date.now() };
await saveSessionStore(storePath, { [sessionKey]: entry });
runEmbeddedPiAgentMock.mockResolvedValueOnce({
payloads: [{ text: "hello world!" }],
messagingToolSentTexts: ["different message"],
messagingToolSentTargets: [{ tool: "slack", provider: "slack", to: "channel:C1" }],
meta: {
agentMeta: {
usage: { input: 10, output: 5 },
promptTokens: 42_000,
model: "claude-opus-4-6",
provider: "anthropic",
},
},
});
const result = await createRun("slack", { storePath, sessionKey });
expect(result).toBeUndefined();
const store = loadSessionStore(storePath, { skipCache: true });
expect(store[sessionKey]?.totalTokens).toBe(42_000);
expect(store[sessionKey]?.totalTokensFresh).toBe(true);
expect(store[sessionKey]?.model).toBe("claude-opus-4-6");
});
it("persists totalTokens from promptTokens when provider omits usage", async () => {
const storePath = path.join(
await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-store-")),
"sessions.json",
);
const sessionKey = "main";
const entry: SessionEntry = {
sessionId: "session",
updatedAt: Date.now(),
inputTokens: 111,
outputTokens: 22,
};
await saveSessionStore(storePath, { [sessionKey]: entry });
runEmbeddedPiAgentMock.mockResolvedValueOnce({
payloads: [{ text: "hello world!" }],
messagingToolSentTexts: ["different message"],
messagingToolSentTargets: [{ tool: "slack", provider: "slack", to: "channel:C1" }],
meta: {
agentMeta: {
promptTokens: 41_000,
model: "claude-opus-4-6",
provider: "anthropic",
},
},
});
const result = await createRun("slack", { storePath, sessionKey });
expect(result).toBeUndefined();
const store = loadSessionStore(storePath, { skipCache: true });
expect(store[sessionKey]?.totalTokens).toBe(41_000);
expect(store[sessionKey]?.totalTokensFresh).toBe(true);
expect(store[sessionKey]?.inputTokens).toBe(111);
expect(store[sessionKey]?.outputTokens).toBe(22);
});
});
describe("runReplyAgent reminder commitment guard", () => {