diff --git a/src/auto-reply/status.test.ts b/src/auto-reply/status.test.ts index e5f3f2bf0ca..2f3a4ca85cf 100644 --- a/src/auto-reply/status.test.ts +++ b/src/auto-reply/status.test.ts @@ -128,661 +128,664 @@ describe("buildStatusMessage", () => { }); expect(normalizeTestText(text)).toContain("Cortex: minimal (session override)"); - it("shows fast mode when enabled", () => { - const text = buildStatusMessage({ - agent: { - model: "openai/gpt-5.4", - }, - sessionEntry: { - sessionId: "fast", - updatedAt: 0, - fastMode: true, - }, - sessionKey: "agent:main:main", - queue: { mode: "collect", depth: 0 }, - }); - - expect(normalizeTestText(text)).toContain("Fast: on"); - }); - - it("notes channel model overrides in status output", () => { - const text = buildStatusMessage({ - config: { - channels: { - modelByChannel: { - discord: { - "123": "openai/gpt-4.1", - }, - }, + it("shows fast mode when enabled", () => { + const text = buildStatusMessage({ + agent: { + model: "openai/gpt-5.4", }, - } as unknown as OpenClawConfig, - agent: { - model: "openai/gpt-4.1", - }, - sessionEntry: { - sessionId: "abc", - updatedAt: 0, - channel: "discord", - groupId: "123", - }, - sessionKey: "agent:main:discord:channel:123", - sessionScope: "per-sender", - queue: { mode: "collect", depth: 0 }, + sessionEntry: { + sessionId: "fast", + updatedAt: 0, + fastMode: true, + }, + sessionKey: "agent:main:main", + queue: { mode: "collect", depth: 0 }, + }); + + expect(normalizeTestText(text)).toContain("Fast: on"); }); - const normalized = normalizeTestText(text); - expect(normalized).toContain("Model: openai/gpt-4.1"); - expect(normalized).toContain("channel override"); - }); - - it("shows 1M context window when anthropic context1m is enabled", () => { - const text = buildStatusMessage({ - config: { - agents: { - defaults: { - model: "anthropic/claude-opus-4-6", - models: { - "anthropic/claude-opus-4-6": { - params: { context1m: true }, + it("notes channel model overrides in status output", () => { + const text = buildStatusMessage({ + config: { + channels: { + modelByChannel: { + discord: { + "123": "openai/gpt-4.1", }, }, }, + } as unknown as OpenClawConfig, + agent: { + model: "openai/gpt-4.1", }, - } as unknown as OpenClawConfig, - agent: { - model: "anthropic/claude-opus-4-6", - }, - sessionEntry: { - sessionId: "ctx1m", - updatedAt: 0, - totalTokens: 200_000, - }, - sessionKey: "agent:main:main", - sessionScope: "per-sender", - queue: { mode: "collect", depth: 0 }, - }); - - expect(normalizeTestText(text)).toContain("Context: 200k/1.0m"); - }); - - it("recomputes context window from the active model after switching away from a smaller session override", () => { - const sessionEntry = { - sessionId: "switch-back", - updatedAt: 0, - providerOverride: "local", - modelOverride: "small-model", - contextTokens: 4_096, - totalTokens: 1_024, - }; - - applyModelOverrideToSessionEntry({ - entry: sessionEntry, - selection: { - provider: "local", - model: "large-model", - isDefault: true, - }, - }); - - const text = buildStatusMessage({ - agent: { - model: "local/large-model", - contextTokens: 65_536, - }, - sessionEntry, - sessionKey: "agent:main:main", - sessionScope: "per-sender", - queue: { mode: "collect", depth: 0 }, - }); - - expect(normalizeTestText(text)).toContain("Context: 1.0k/66k"); - }); - - it("uses per-agent sandbox config when config and session key are provided", () => { - const text = buildStatusMessage({ - config: { - agents: { - list: [ - { id: "main", default: true }, - { id: "discord", sandbox: { mode: "all" } }, - ], + sessionEntry: { + sessionId: "abc", + updatedAt: 0, + channel: "discord", + groupId: "123", }, - } as unknown as OpenClawConfig, - agent: {}, - sessionKey: "agent:discord:discord:channel:1456350065223270435", - sessionScope: "per-sender", - queue: { mode: "collect", depth: 0 }, + sessionKey: "agent:main:discord:channel:123", + sessionScope: "per-sender", + queue: { mode: "collect", depth: 0 }, + }); + const normalized = normalizeTestText(text); + + expect(normalized).toContain("Model: openai/gpt-4.1"); + expect(normalized).toContain("channel override"); }); - expect(normalizeTestText(text)).toContain("Runtime: docker/all"); - }); - - it("shows verbose/elevated labels only when enabled", () => { - const text = buildStatusMessage({ - agent: { model: "anthropic/claude-opus-4-5" }, - sessionEntry: { sessionId: "v1", updatedAt: 0 }, - sessionKey: "agent:main:main", - sessionScope: "per-sender", - resolvedThink: "low", - resolvedVerbose: "on", - resolvedElevated: "on", - queue: { mode: "collect", depth: 0 }, - }); - - expect(text).toContain("verbose"); - expect(text).toContain("elevated"); - }); - - it("includes media understanding decisions when present", () => { - const text = buildStatusMessage({ - agent: { model: "anthropic/claude-opus-4-5" }, - sessionEntry: { sessionId: "media", updatedAt: 0 }, - sessionKey: "agent:main:main", - queue: { mode: "none" }, - mediaDecisions: [ - createSuccessfulImageMediaDecision() as unknown as NonNullable< - Parameters[0]["mediaDecisions"] - >[number], - { - capability: "audio", - outcome: "skipped", - attachments: [ - { - attachmentIndex: 1, - attempts: [ - { - type: "provider", - outcome: "skipped", - reason: "maxBytes: too large", + it("shows 1M context window when anthropic context1m is enabled", () => { + const text = buildStatusMessage({ + config: { + agents: { + defaults: { + model: "anthropic/claude-opus-4-6", + models: { + "anthropic/claude-opus-4-6": { + params: { context1m: true }, }, - ], + }, }, - ], + }, + } as unknown as OpenClawConfig, + agent: { + model: "anthropic/claude-opus-4-6", }, - ], + sessionEntry: { + sessionId: "ctx1m", + updatedAt: 0, + totalTokens: 200_000, + }, + sessionKey: "agent:main:main", + sessionScope: "per-sender", + queue: { mode: "collect", depth: 0 }, + }); + + expect(normalizeTestText(text)).toContain("Context: 200k/1.0m"); }); - const normalized = normalizeTestText(text); - expect(normalized).toContain("Media: image ok (openai/gpt-5.2) · audio skipped (maxBytes)"); - }); - - it("omits media line when all decisions are none", () => { - const text = buildStatusMessage({ - agent: { model: "anthropic/claude-opus-4-5" }, - sessionEntry: { sessionId: "media-none", updatedAt: 0 }, - sessionKey: "agent:main:main", - queue: { mode: "none" }, - mediaDecisions: [ - { capability: "image", outcome: "no-attachment", attachments: [] }, - { capability: "audio", outcome: "no-attachment", attachments: [] }, - { capability: "video", outcome: "no-attachment", attachments: [] }, - ], - }); - - expect(normalizeTestText(text)).not.toContain("Media:"); - }); - - it("does not show elevated label when session explicitly disables it", () => { - const text = buildStatusMessage({ - agent: { model: "anthropic/claude-opus-4-5", elevatedDefault: "on" }, - sessionEntry: { sessionId: "v1", updatedAt: 0, elevatedLevel: "off" }, - sessionKey: "agent:main:main", - sessionScope: "per-sender", - resolvedThink: "low", - resolvedVerbose: "off", - queue: { mode: "collect", depth: 0 }, - }); - - const optionsLine = text.split("\n").find((line) => line.trim().startsWith("⚙️")); - expect(optionsLine).toBeTruthy(); - expect(optionsLine).not.toContain("elevated"); - }); - - it("shows selected model and active runtime model when they differ", () => { - const text = buildStatusMessage({ - agent: { - model: "anthropic/claude-opus-4-5", - contextTokens: 32_000, - }, - sessionEntry: { - sessionId: "override-1", + it("recomputes context window from the active model after switching away from a smaller session override", () => { + const sessionEntry = { + sessionId: "switch-back", updatedAt: 0, - providerOverride: "openai", - modelOverride: "gpt-4.1-mini", - modelProvider: "anthropic", - model: "claude-haiku-4-5", - fallbackNoticeSelectedModel: "openai/gpt-4.1-mini", - fallbackNoticeActiveModel: "anthropic/claude-haiku-4-5", - fallbackNoticeReason: "rate limit", - contextTokens: 32_000, - }, - sessionKey: "agent:main:main", - sessionScope: "per-sender", - queue: { mode: "collect", depth: 0 }, - modelAuth: "api-key", - activeModelAuth: "api-key di_123…abc (deepinfra:default)", + providerOverride: "local", + modelOverride: "small-model", + contextTokens: 4_096, + totalTokens: 1_024, + }; + + applyModelOverrideToSessionEntry({ + entry: sessionEntry, + selection: { + provider: "local", + model: "large-model", + isDefault: true, + }, + }); + + const text = buildStatusMessage({ + agent: { + model: "local/large-model", + contextTokens: 65_536, + }, + sessionEntry, + sessionKey: "agent:main:main", + sessionScope: "per-sender", + queue: { mode: "collect", depth: 0 }, + }); + + expect(normalizeTestText(text)).toContain("Context: 1.0k/66k"); }); - const normalized = normalizeTestText(text); - expect(normalized).toContain("Model: openai/gpt-4.1-mini"); - expect(normalized).toContain("Fallback: anthropic/claude-haiku-4-5"); - expect(normalized).toContain("(rate limit)"); - expect(normalized).not.toContain(" - Reason:"); - expect(normalized).not.toContain("Active:"); - expect(normalized).toContain("di_123...abc"); - }); + it("uses per-agent sandbox config when config and session key are provided", () => { + const text = buildStatusMessage({ + config: { + agents: { + list: [ + { id: "main", default: true }, + { id: "discord", sandbox: { mode: "all" } }, + ], + }, + } as unknown as OpenClawConfig, + agent: {}, + sessionKey: "agent:discord:discord:channel:1456350065223270435", + sessionScope: "per-sender", + queue: { mode: "collect", depth: 0 }, + }); - it("omits active fallback details when runtime drift does not match fallback state", () => { - const text = buildStatusMessage({ - agent: { - model: "openai/gpt-4.1-mini", - contextTokens: 32_000, - }, - sessionEntry: { - sessionId: "runtime-drift-only", - updatedAt: 0, - modelProvider: "anthropic", - model: "claude-haiku-4-5", - fallbackNoticeSelectedModel: "fireworks/minimax-m2p5", - fallbackNoticeActiveModel: "deepinfra/moonshotai/Kimi-K2.5", - fallbackNoticeReason: "rate limit", - }, - sessionKey: "agent:main:main", - sessionScope: "per-sender", - queue: { mode: "collect", depth: 0 }, - modelAuth: "api-key", - activeModelAuth: "api-key di_123…abc (deepinfra:default)", + expect(normalizeTestText(text)).toContain("Runtime: docker/all"); }); - const normalized = normalizeTestText(text); - expect(normalized).toContain("Model: openai/gpt-4.1-mini"); - expect(normalized).not.toContain("Fallback:"); - expect(normalized).not.toContain("(rate limit)"); - }); + it("shows verbose/elevated labels only when enabled", () => { + const text = buildStatusMessage({ + agent: { model: "anthropic/claude-opus-4-5" }, + sessionEntry: { sessionId: "v1", updatedAt: 0 }, + sessionKey: "agent:main:main", + sessionScope: "per-sender", + resolvedThink: "low", + resolvedVerbose: "on", + resolvedElevated: "on", + queue: { mode: "collect", depth: 0 }, + }); - it("omits active lines when runtime matches selected model", () => { - const text = buildStatusMessage({ - agent: { - model: "openai/gpt-4.1-mini", - contextTokens: 32_000, - }, - sessionEntry: { - sessionId: "selected-active-same", - updatedAt: 0, - modelProvider: "openai", - model: "gpt-4.1-mini", - fallbackNoticeReason: "unknown", - }, - sessionKey: "agent:main:main", - sessionScope: "per-sender", - queue: { mode: "collect", depth: 0 }, - modelAuth: "api-key", + expect(text).toContain("verbose"); + expect(text).toContain("elevated"); }); - const normalized = normalizeTestText(text); - expect(normalized).not.toContain("Fallback:"); - }); - - it("keeps provider prefix from configured model", () => { - const text = buildStatusMessage({ - agent: { - model: "google-antigravity/claude-sonnet-4-5", - }, - sessionScope: "per-sender", - queue: { mode: "collect", depth: 0 }, - modelAuth: "api-key", - }); - - expect(normalizeTestText(text)).toContain("Model: google-antigravity/claude-sonnet-4-5"); - }); - - it("handles missing agent config gracefully", () => { - const text = buildStatusMessage({ - agent: {}, - sessionScope: "per-sender", - queue: { mode: "collect", depth: 0 }, - modelAuth: "api-key", - }); - - const normalized = normalizeTestText(text); - expect(normalized).toContain("Model:"); - expect(normalized).toContain("Context:"); - expect(normalized).toContain("Queue: collect"); - }); - - it("includes group activation for group sessions", () => { - const text = buildStatusMessage({ - agent: {}, - sessionEntry: { - sessionId: "g1", - updatedAt: 0, - groupActivation: "always", - chatType: "group", - }, - sessionKey: "agent:main:whatsapp:group:123@g.us", - sessionScope: "per-sender", - queue: { mode: "collect", depth: 0 }, - modelAuth: "api-key", - }); - - expect(text).toContain("Activation: always"); - }); - - it("shows queue details when overridden", () => { - const text = buildStatusMessage({ - agent: {}, - sessionEntry: { sessionId: "q1", updatedAt: 0 }, - sessionKey: "agent:main:main", - sessionScope: "per-sender", - queue: { - mode: "collect", - depth: 3, - debounceMs: 2000, - cap: 5, - dropPolicy: "old", - showDetails: true, - }, - modelAuth: "api-key", - }); - - expect(text).toContain("Queue: collect (depth 3 · debounce 2s · cap 5 · drop old)"); - }); - - it("inserts usage summary beneath context line", () => { - const text = buildStatusMessage({ - agent: { model: "anthropic/claude-opus-4-5", contextTokens: 32_000 }, - sessionEntry: { sessionId: "u1", updatedAt: 0, totalTokens: 1000 }, - sessionKey: "agent:main:main", - sessionScope: "per-sender", - queue: { mode: "collect", depth: 0 }, - usageLine: "📊 Usage: Claude 80% left (5h)", - modelAuth: "api-key", - }); - - const lines = normalizeTestText(text).split("\n"); - const contextIndex = lines.findIndex((line) => line.includes("Context:")); - expect(contextIndex).toBeGreaterThan(-1); - expect(lines[contextIndex + 1]).toContain("Usage: Claude 80% left (5h)"); - }); - - it("hides cost when not using an API key", () => { - const text = buildStatusMessage({ - config: { - models: { - providers: { - anthropic: { - models: [ - { - id: "claude-opus-4-5", - cost: { - input: 1, - output: 1, - cacheRead: 0, - cacheWrite: 0, + it("includes media understanding decisions when present", () => { + const text = buildStatusMessage({ + agent: { model: "anthropic/claude-opus-4-5" }, + sessionEntry: { sessionId: "media", updatedAt: 0 }, + sessionKey: "agent:main:main", + queue: { mode: "none" }, + mediaDecisions: [ + createSuccessfulImageMediaDecision() as unknown as NonNullable< + Parameters[0]["mediaDecisions"] + >[number], + { + capability: "audio", + outcome: "skipped", + attachments: [ + { + attachmentIndex: 1, + attempts: [ + { + type: "provider", + outcome: "skipped", + reason: "maxBytes: too large", }, - }, - ], + ], + }, + ], + }, + ], + }); + + const normalized = normalizeTestText(text); + expect(normalized).toContain("Media: image ok (openai/gpt-5.2) · audio skipped (maxBytes)"); + }); + + it("omits media line when all decisions are none", () => { + const text = buildStatusMessage({ + agent: { model: "anthropic/claude-opus-4-5" }, + sessionEntry: { sessionId: "media-none", updatedAt: 0 }, + sessionKey: "agent:main:main", + queue: { mode: "none" }, + mediaDecisions: [ + { capability: "image", outcome: "no-attachment", attachments: [] }, + { capability: "audio", outcome: "no-attachment", attachments: [] }, + { capability: "video", outcome: "no-attachment", attachments: [] }, + ], + }); + + expect(normalizeTestText(text)).not.toContain("Media:"); + }); + + it("does not show elevated label when session explicitly disables it", () => { + const text = buildStatusMessage({ + agent: { model: "anthropic/claude-opus-4-5", elevatedDefault: "on" }, + sessionEntry: { sessionId: "v1", updatedAt: 0, elevatedLevel: "off" }, + sessionKey: "agent:main:main", + sessionScope: "per-sender", + resolvedThink: "low", + resolvedVerbose: "off", + queue: { mode: "collect", depth: 0 }, + }); + + const optionsLine = text.split("\n").find((line) => line.trim().startsWith("⚙️")); + expect(optionsLine).toBeTruthy(); + expect(optionsLine).not.toContain("elevated"); + }); + + it("shows selected model and active runtime model when they differ", () => { + const text = buildStatusMessage({ + agent: { + model: "anthropic/claude-opus-4-5", + contextTokens: 32_000, + }, + sessionEntry: { + sessionId: "override-1", + updatedAt: 0, + providerOverride: "openai", + modelOverride: "gpt-4.1-mini", + modelProvider: "anthropic", + model: "claude-haiku-4-5", + fallbackNoticeSelectedModel: "openai/gpt-4.1-mini", + fallbackNoticeActiveModel: "anthropic/claude-haiku-4-5", + fallbackNoticeReason: "rate limit", + contextTokens: 32_000, + }, + sessionKey: "agent:main:main", + sessionScope: "per-sender", + queue: { mode: "collect", depth: 0 }, + modelAuth: "api-key", + activeModelAuth: "api-key di_123…abc (deepinfra:default)", + }); + + const normalized = normalizeTestText(text); + expect(normalized).toContain("Model: openai/gpt-4.1-mini"); + expect(normalized).toContain("Fallback: anthropic/claude-haiku-4-5"); + expect(normalized).toContain("(rate limit)"); + expect(normalized).not.toContain(" - Reason:"); + expect(normalized).not.toContain("Active:"); + expect(normalized).toContain("di_123...abc"); + }); + + it("omits active fallback details when runtime drift does not match fallback state", () => { + const text = buildStatusMessage({ + agent: { + model: "openai/gpt-4.1-mini", + contextTokens: 32_000, + }, + sessionEntry: { + sessionId: "runtime-drift-only", + updatedAt: 0, + modelProvider: "anthropic", + model: "claude-haiku-4-5", + fallbackNoticeSelectedModel: "fireworks/minimax-m2p5", + fallbackNoticeActiveModel: "deepinfra/moonshotai/Kimi-K2.5", + fallbackNoticeReason: "rate limit", + }, + sessionKey: "agent:main:main", + sessionScope: "per-sender", + queue: { mode: "collect", depth: 0 }, + modelAuth: "api-key", + activeModelAuth: "api-key di_123…abc (deepinfra:default)", + }); + + const normalized = normalizeTestText(text); + expect(normalized).toContain("Model: openai/gpt-4.1-mini"); + expect(normalized).not.toContain("Fallback:"); + expect(normalized).not.toContain("(rate limit)"); + }); + + it("omits active lines when runtime matches selected model", () => { + const text = buildStatusMessage({ + agent: { + model: "openai/gpt-4.1-mini", + contextTokens: 32_000, + }, + sessionEntry: { + sessionId: "selected-active-same", + updatedAt: 0, + modelProvider: "openai", + model: "gpt-4.1-mini", + fallbackNoticeReason: "unknown", + }, + sessionKey: "agent:main:main", + sessionScope: "per-sender", + queue: { mode: "collect", depth: 0 }, + modelAuth: "api-key", + }); + + const normalized = normalizeTestText(text); + expect(normalized).not.toContain("Fallback:"); + }); + + it("keeps provider prefix from configured model", () => { + const text = buildStatusMessage({ + agent: { + model: "google-antigravity/claude-sonnet-4-5", + }, + sessionScope: "per-sender", + queue: { mode: "collect", depth: 0 }, + modelAuth: "api-key", + }); + + expect(normalizeTestText(text)).toContain("Model: google-antigravity/claude-sonnet-4-5"); + }); + + it("handles missing agent config gracefully", () => { + const text = buildStatusMessage({ + agent: {}, + sessionScope: "per-sender", + queue: { mode: "collect", depth: 0 }, + modelAuth: "api-key", + }); + + const normalized = normalizeTestText(text); + expect(normalized).toContain("Model:"); + expect(normalized).toContain("Context:"); + expect(normalized).toContain("Queue: collect"); + }); + + it("includes group activation for group sessions", () => { + const text = buildStatusMessage({ + agent: {}, + sessionEntry: { + sessionId: "g1", + updatedAt: 0, + groupActivation: "always", + chatType: "group", + }, + sessionKey: "agent:main:whatsapp:group:123@g.us", + sessionScope: "per-sender", + queue: { mode: "collect", depth: 0 }, + modelAuth: "api-key", + }); + + expect(text).toContain("Activation: always"); + }); + + it("shows queue details when overridden", () => { + const text = buildStatusMessage({ + agent: {}, + sessionEntry: { sessionId: "q1", updatedAt: 0 }, + sessionKey: "agent:main:main", + sessionScope: "per-sender", + queue: { + mode: "collect", + depth: 3, + debounceMs: 2000, + cap: 5, + dropPolicy: "old", + showDetails: true, + }, + modelAuth: "api-key", + }); + + expect(text).toContain("Queue: collect (depth 3 · debounce 2s · cap 5 · drop old)"); + }); + + it("inserts usage summary beneath context line", () => { + const text = buildStatusMessage({ + agent: { model: "anthropic/claude-opus-4-5", contextTokens: 32_000 }, + sessionEntry: { sessionId: "u1", updatedAt: 0, totalTokens: 1000 }, + sessionKey: "agent:main:main", + sessionScope: "per-sender", + queue: { mode: "collect", depth: 0 }, + usageLine: "📊 Usage: Claude 80% left (5h)", + modelAuth: "api-key", + }); + + const lines = normalizeTestText(text).split("\n"); + const contextIndex = lines.findIndex((line) => line.includes("Context:")); + expect(contextIndex).toBeGreaterThan(-1); + expect(lines[contextIndex + 1]).toContain("Usage: Claude 80% left (5h)"); + }); + + it("hides cost when not using an API key", () => { + const text = buildStatusMessage({ + config: { + models: { + providers: { + anthropic: { + models: [ + { + id: "claude-opus-4-5", + cost: { + input: 1, + output: 1, + cacheRead: 0, + cacheWrite: 0, + }, + }, + ], + }, }, }, + } as unknown as OpenClawConfig, + agent: { model: "anthropic/claude-opus-4-5" }, + sessionEntry: { sessionId: "c1", updatedAt: 0, inputTokens: 10 }, + sessionKey: "agent:main:main", + sessionScope: "per-sender", + queue: { mode: "collect", depth: 0 }, + modelAuth: "oauth", + }); + + expect(text).not.toContain("💵 Cost:"); + }); + + function writeTranscriptUsageLog(params: { + dir: string; + agentId: string; + sessionId: string; + usage: { + input: number; + output: number; + cacheRead: number; + cacheWrite: number; + totalTokens: number; + }; + }) { + const logPath = path.join( + params.dir, + ".openclaw", + "agents", + params.agentId, + "sessions", + `${params.sessionId}.jsonl`, + ); + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + fs.writeFileSync( + logPath, + [ + JSON.stringify({ + type: "message", + message: { + role: "assistant", + model: "claude-opus-4-5", + usage: params.usage, + }, + }), + ].join("\n"), + "utf-8", + ); + } + + const baselineTranscriptUsage = { + input: 1, + output: 2, + cacheRead: 1000, + cacheWrite: 0, + totalTokens: 1003, + } as const; + + function writeBaselineTranscriptUsageLog(params: { + dir: string; + agentId: string; + sessionId: string; + }) { + writeTranscriptUsageLog({ + ...params, + usage: baselineTranscriptUsage, + }); + } + + function buildTranscriptStatusText(params: { sessionId: string; sessionKey: string }) { + return buildStatusMessage({ + agent: { + model: "anthropic/claude-opus-4-5", + contextTokens: 32_000, }, - } as unknown as OpenClawConfig, - agent: { model: "anthropic/claude-opus-4-5" }, - sessionEntry: { sessionId: "c1", updatedAt: 0, inputTokens: 10 }, - sessionKey: "agent:main:main", - sessionScope: "per-sender", - queue: { mode: "collect", depth: 0 }, - modelAuth: "oauth", - }); + sessionEntry: { + sessionId: params.sessionId, + updatedAt: 0, + totalTokens: 3, + contextTokens: 32_000, + }, + sessionKey: params.sessionKey, + sessionScope: "per-sender", + queue: { mode: "collect", depth: 0 }, + includeTranscriptUsage: true, + modelAuth: "api-key", + }); + } - expect(text).not.toContain("💵 Cost:"); - }); - - function writeTranscriptUsageLog(params: { - dir: string; - agentId: string; - sessionId: string; - usage: { - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - totalTokens: number; - }; - }) { - const logPath = path.join( - params.dir, - ".openclaw", - "agents", - params.agentId, - "sessions", - `${params.sessionId}.jsonl`, - ); - fs.mkdirSync(path.dirname(logPath), { recursive: true }); - fs.writeFileSync( - logPath, - [ - JSON.stringify({ - type: "message", - message: { - role: "assistant", - model: "claude-opus-4-5", - usage: params.usage, - }, - }), - ].join("\n"), - "utf-8", - ); - } - - const baselineTranscriptUsage = { - input: 1, - output: 2, - cacheRead: 1000, - cacheWrite: 0, - totalTokens: 1003, - } as const; - - function writeBaselineTranscriptUsageLog(params: { - dir: string; - agentId: string; - sessionId: string; - }) { - writeTranscriptUsageLog({ - ...params, - usage: baselineTranscriptUsage, - }); - } - - function buildTranscriptStatusText(params: { sessionId: string; sessionKey: string }) { - return buildStatusMessage({ - agent: { - model: "anthropic/claude-opus-4-5", - contextTokens: 32_000, - }, - sessionEntry: { - sessionId: params.sessionId, - updatedAt: 0, - totalTokens: 3, - contextTokens: 32_000, - }, - sessionKey: params.sessionKey, - sessionScope: "per-sender", - queue: { mode: "collect", depth: 0 }, - includeTranscriptUsage: true, - modelAuth: "api-key", - }); - } - - it("prefers cached prompt tokens from the session log", async () => { - await withTempHome( - async (dir) => { - const sessionId = "sess-1"; - writeBaselineTranscriptUsageLog({ - dir, - agentId: "main", - sessionId, - }); - - const text = buildTranscriptStatusText({ - sessionId, - sessionKey: "agent:main:main", - }); - - expect(normalizeTestText(text)).toContain("Context: 1.0k/32k"); - }, - { prefix: "openclaw-status-" }, - ); - }); - - it("reads transcript usage for non-default agents", async () => { - await withTempHome( - async (dir) => { - const sessionId = "sess-worker1"; - writeBaselineTranscriptUsageLog({ - dir, - agentId: "worker1", - sessionId, - }); - - const text = buildTranscriptStatusText({ - sessionId, - sessionKey: "agent:worker1:telegram:12345", - }); - - expect(normalizeTestText(text)).toContain("Context: 1.0k/32k"); - }, - { prefix: "openclaw-status-" }, - ); - }); - - it("reads transcript usage using explicit agentId when sessionKey is missing", async () => { - await withTempHome( - async (dir) => { - const sessionId = "sess-worker2"; - writeTranscriptUsageLog({ - dir, - agentId: "worker2", - sessionId, - usage: { - input: 2, - output: 3, - cacheRead: 1200, - cacheWrite: 0, - totalTokens: 1205, - }, - }); - - const text = buildStatusMessage({ - agent: { - model: "anthropic/claude-opus-4-5", - contextTokens: 32_000, - }, - agentId: "worker2", - sessionEntry: { + it("prefers cached prompt tokens from the session log", async () => { + await withTempHome( + async (dir) => { + const sessionId = "sess-1"; + writeBaselineTranscriptUsageLog({ + dir, + agentId: "main", sessionId, - updatedAt: 0, - totalTokens: 5, - contextTokens: 32_000, - }, - // Intentionally omitted: sessionKey - sessionScope: "per-sender", - queue: { mode: "collect", depth: 0 }, - includeTranscriptUsage: true, - modelAuth: "api-key", - }); + }); - expect(normalizeTestText(text)).toContain("Context: 1.2k/32k"); - }, - { prefix: "openclaw-status-" }, - ); - }); -}); + const text = buildTranscriptStatusText({ + sessionId, + sessionKey: "agent:main:main", + }); -describe("buildCommandsMessage", () => { - it("lists commands with aliases and hints", () => { - const text = buildCommandsMessage({ - commands: { config: false, debug: false }, - } as unknown as OpenClawConfig); - expect(text).toContain("ℹ️ Slash commands"); - expect(text).toContain("Status"); - expect(text).toContain("/commands - List all slash commands."); - expect(text).toContain("/skill - Run a skill by name."); - expect(text).toContain("/think (/thinking, /t) - Set thinking level."); - expect(text).toContain("/compact - Compact the session context."); - expect(text).toContain( - "/cortex [text] - Inspect or override Cortex prompt mode for this conversation.", - ); - expect(text).toContain( - "Tip: /cortex preview shows the active Cortex context; /status shows mode and source.", - ); - expect(text).not.toContain("/config"); - expect(text).not.toContain("/debug"); - }); - - it("includes skill commands when provided", () => { - const text = buildCommandsMessage( - { - commands: { config: false, debug: false }, - } as unknown as OpenClawConfig, - [ - { - name: "demo_skill", - skillName: "demo-skill", - description: "Demo skill", + expect(normalizeTestText(text)).toContain("Context: 1.0k/32k"); }, - ], - ); - expect(text).toContain("/demo_skill - Demo skill"); - }); -}); + { prefix: "openclaw-status-" }, + ); + }); -describe("buildHelpMessage", () => { - it("hides config/debug when disabled", () => { - const text = buildHelpMessage({ - commands: { config: false, debug: false }, - } as unknown as OpenClawConfig); - expect(text).toContain("Cortex"); - expect(text).toContain("/cortex preview"); - expect(text).toContain("/cortex mode show"); - expect(text).toContain("/cortex mode set "); - expect(text).toContain("Skills"); - expect(text).toContain("/skill [input]"); - expect(text).not.toContain("/config"); - expect(text).not.toContain("/debug"); + it("reads transcript usage for non-default agents", async () => { + await withTempHome( + async (dir) => { + const sessionId = "sess-worker1"; + writeBaselineTranscriptUsageLog({ + dir, + agentId: "worker1", + sessionId, + }); + + const text = buildTranscriptStatusText({ + sessionId, + sessionKey: "agent:worker1:telegram:12345", + }); + + expect(normalizeTestText(text)).toContain("Context: 1.0k/32k"); + }, + { prefix: "openclaw-status-" }, + ); + }); + + it("reads transcript usage using explicit agentId when sessionKey is missing", async () => { + await withTempHome( + async (dir) => { + const sessionId = "sess-worker2"; + writeTranscriptUsageLog({ + dir, + agentId: "worker2", + sessionId, + usage: { + input: 2, + output: 3, + cacheRead: 1200, + cacheWrite: 0, + totalTokens: 1205, + }, + }); + + const text = buildStatusMessage({ + agent: { + model: "anthropic/claude-opus-4-5", + contextTokens: 32_000, + }, + agentId: "worker2", + sessionEntry: { + sessionId, + updatedAt: 0, + totalTokens: 5, + contextTokens: 32_000, + }, + // Intentionally omitted: sessionKey + sessionScope: "per-sender", + queue: { mode: "collect", depth: 0 }, + includeTranscriptUsage: true, + modelAuth: "api-key", + }); + + expect(normalizeTestText(text)).toContain("Context: 1.2k/32k"); + }, + { prefix: "openclaw-status-" }, + ); + }); }); - it("includes /fast in help output", () => { - expect(buildHelpMessage()).toContain("/fast on|off"); - }); -}); - -describe("buildCommandsMessagePaginated", () => { - it("formats telegram output with pages", () => { - const result = buildCommandsMessagePaginated( - { + describe("buildCommandsMessage", () => { + it("lists commands with aliases and hints", () => { + const text = buildCommandsMessage({ commands: { config: false, debug: false }, - } as unknown as OpenClawConfig, - undefined, - { surface: "telegram", page: 1 }, - ); - expect(result.text).toContain("ℹ️ Commands (1/"); - expect(result.text).toContain("Session"); - expect(result.text).toContain("/stop - Stop the current run."); - expect(result.text).toContain("Tip: /cortex preview shows context; /status shows mode/source."); + } as unknown as OpenClawConfig); + expect(text).toContain("ℹ️ Slash commands"); + expect(text).toContain("Status"); + expect(text).toContain("/commands - List all slash commands."); + expect(text).toContain("/skill - Run a skill by name."); + expect(text).toContain("/think (/thinking, /t) - Set thinking level."); + expect(text).toContain("/compact - Compact the session context."); + expect(text).toContain( + "/cortex [text] - Inspect or override Cortex prompt mode for this conversation.", + ); + expect(text).toContain( + "Tip: /cortex preview shows the active Cortex context; /status shows mode and source.", + ); + expect(text).not.toContain("/config"); + expect(text).not.toContain("/debug"); + }); + + it("includes skill commands when provided", () => { + const text = buildCommandsMessage( + { + commands: { config: false, debug: false }, + } as unknown as OpenClawConfig, + [ + { + name: "demo_skill", + skillName: "demo-skill", + description: "Demo skill", + }, + ], + ); + expect(text).toContain("/demo_skill - Demo skill"); + }); }); - it("includes plugin commands in the paginated list", () => { - listPluginCommands.mockReturnValue([ - { name: "plugin_cmd", description: "Plugin command", pluginId: "demo-plugin" }, - ]); - const result = buildCommandsMessagePaginated( - { + describe("buildHelpMessage", () => { + it("hides config/debug when disabled", () => { + const text = buildHelpMessage({ commands: { config: false, debug: false }, - } as unknown as OpenClawConfig, - undefined, - { surface: "telegram", page: 99 }, - ); - expect(result.text).toContain("Plugins"); - expect(result.text).toContain("/plugin_cmd (demo-plugin) - Plugin command"); + } as unknown as OpenClawConfig); + expect(text).toContain("Cortex"); + expect(text).toContain("/cortex preview"); + expect(text).toContain("/cortex mode show"); + expect(text).toContain("/cortex mode set "); + expect(text).toContain("Skills"); + expect(text).toContain("/skill [input]"); + expect(text).not.toContain("/config"); + expect(text).not.toContain("/debug"); + }); + + it("includes /fast in help output", () => { + expect(buildHelpMessage()).toContain("/fast on|off"); + }); + }); + + describe("buildCommandsMessagePaginated", () => { + it("formats telegram output with pages", () => { + const result = buildCommandsMessagePaginated( + { + commands: { config: false, debug: false }, + } as unknown as OpenClawConfig, + undefined, + { surface: "telegram", page: 1 }, + ); + expect(result.text).toContain("ℹ️ Commands (1/"); + expect(result.text).toContain("Session"); + expect(result.text).toContain("/stop - Stop the current run."); + expect(result.text).toContain( + "Tip: /cortex preview shows context; /status shows mode/source.", + ); + }); + + it("includes plugin commands in the paginated list", () => { + listPluginCommands.mockReturnValue([ + { name: "plugin_cmd", description: "Plugin command", pluginId: "demo-plugin" }, + ]); + const result = buildCommandsMessagePaginated( + { + commands: { config: false, debug: false }, + } as unknown as OpenClawConfig, + undefined, + { surface: "telegram", page: 99 }, + ); + expect(result.text).toContain("Plugins"); + expect(result.text).toContain("/plugin_cmd (demo-plugin) - Plugin command"); + }); }); });