import fs from "node:fs" ;
import path from "node:path" ;
import { afterEach, beforeEach, describe, expect, it } from "vitest" ;
import { normalizeTestText } from "../../../test/helpers/normalize-text.js" ;
import { withTempHome } from "../../../test/helpers/temp-home.js" ;
import { clearAgentHarnesses, registerAgentHarness } from "../../agents/harness/registry.js" ;
import type { AgentHarness } from "../../agents/harness/types.js" ;
import {
addSubagentRunForTests,
resetSubagentRegistryForTests,
} from "../../agents/subagent-registry.js" ;
import {
completeTaskRunByRunId,
createQueuedTaskRun,
createRunningTaskRun,
failTaskRunByRunId,
} from "../../tasks/task-executor.js" ;
import { resetTaskRegistryForTests } from "../../tasks/task-registry.js" ;
import { buildStatusReply, buildStatusText } from "./commands-status.js" ;
import {
baseCommandTestConfig,
buildCommandTestParams,
configureInMemoryTaskRegistryStoreForTests,
} from "./commands.test-harness.js" ;
const baseCfg = baseCommandTestConfig;
async function buildStatusReplyForTest(params: { sessionKey?: string; verbose?: boolean }) {
const commandParams = buildCommandTestParams("/status" , baseCfg);
const sessionKey = params.sessionKey ?? commandParams.sessionKey;
return await buildStatusReply({
cfg: baseCfg,
command: commandParams.command,
sessionEntry: commandParams.sessionEntry,
sessionKey,
parentSessionKey: sessionKey,
sessionScope: commandParams.sessionScope,
storePath: commandParams.storePath,
provider: "anthropic" ,
model: "claude-opus-4-6" ,
contextTokens: 0 ,
resolvedThinkLevel: commandParams.resolvedThinkLevel,
resolvedFastMode: false ,
resolvedVerboseLevel: params.verbose ? "on" : commandParams.resolvedVerboseLevel,
resolvedReasoningLevel: commandParams.resolvedReasoningLevel,
resolvedElevatedLevel: commandParams.resolvedElevatedLevel,
resolveDefaultThinkingLevel: commandParams.resolveDefaultThinkingLevel,
isGroup: commandParams.isGroup,
defaultGroupActivation: commandParams.defaultGroupActivation,
modelAuthOverride: "api-key" ,
activeModelAuthOverride: "api-key" ,
});
}
function registerStatusCodexHarness(): void {
const harness: AgentHarness = {
id: "codex" ,
label: "Codex" ,
supports: (ctx) =>
ctx.provider === "codex" ? { supported: true , priority: 100 } : { supported: false },
runAttempt: async () => {
throw new Error("not used in status tests" );
},
};
registerAgentHarness(harness, { ownerPluginId: "codex" });
}
afterEach(() => {
clearAgentHarnesses();
});
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,
},
}),
"utf-8" ,
);
}
describe("buildStatusReply subagent summary" , () => {
beforeEach(() => {
resetSubagentRegistryForTests();
resetTaskRegistryForTests({ persist: false });
configureInMemoryTaskRegistryStoreForTests();
});
afterEach(() => {
resetSubagentRegistryForTests();
resetTaskRegistryForTests({ persist: false });
});
it("counts ended orchestrators with active descendants as active" , async () => {
const parentKey = "agent:main:subagent:status-ended-parent" ;
addSubagentRunForTests({
runId: "run-status-ended-parent" ,
childSessionKey: parentKey,
requesterSessionKey: "agent:main:main" ,
requesterDisplayKey: "main" ,
task: "status orchestrator" ,
cleanup: "keep" ,
createdAt: Date.now() - 120 _000 ,
startedAt: Date.now() - 120 _000 ,
endedAt: Date.now() - 110 _000 ,
outcome: { status: "ok" },
});
addSubagentRunForTests({
runId: "run-status-active-child" ,
childSessionKey: "agent:main:subagent:status-ended-parent:subagent:child" ,
requesterSessionKey: parentKey,
requesterDisplayKey: "subagent:status-ended-parent" ,
task: "status child still running" ,
cleanup: "keep" ,
createdAt: Date.now() - 60 _000 ,
startedAt: Date.now() - 60 _000 ,
});
const reply = await buildStatusReplyForTest({});
expect(reply?.text).toContain(" Subagents: 1 active" );
});
it("dedupes stale rows in the verbose subagent status summary" , async () => {
const childSessionKey = "agent:main:subagent:status-dedupe-worker" ;
addSubagentRunForTests({
runId: "run-status-current" ,
childSessionKey,
requesterSessionKey: "agent:main:main" ,
requesterDisplayKey: "main" ,
task: "current status worker" ,
cleanup: "keep" ,
createdAt: Date.now() - 60 _000 ,
startedAt: Date.now() - 60 _000 ,
});
addSubagentRunForTests({
runId: "run-status-stale" ,
childSessionKey,
requesterSessionKey: "agent:main:main" ,
requesterDisplayKey: "main" ,
task: "stale status worker" ,
cleanup: "keep" ,
createdAt: Date.now() - 120 _000 ,
startedAt: Date.now() - 120 _000 ,
endedAt: Date.now() - 90 _000 ,
outcome: { status: "ok" },
});
const reply = await buildStatusReplyForTest({ verbose: true });
expect(reply?.text).toContain(" Subagents: 1 active" );
expect(reply?.text).not.toContain("· 1 done" );
});
it("does not count a child session that moved to a newer parent in the old parent's status" , async () => {
const oldParentKey = "agent:main:subagent:status-old-parent" ;
const newParentKey = "agent:main:subagent:status-new-parent" ;
const childSessionKey = "agent:main:subagent:status-shared-child" ;
addSubagentRunForTests({
runId: "run-status-old-parent" ,
childSessionKey: oldParentKey,
requesterSessionKey: "agent:main:main" ,
requesterDisplayKey: "main" ,
task: "old parent" ,
cleanup: "keep" ,
createdAt: Date.now() - 120 _000 ,
startedAt: Date.now() - 120 _000 ,
});
addSubagentRunForTests({
runId: "run-status-new-parent" ,
childSessionKey: newParentKey,
requesterSessionKey: "agent:main:main" ,
requesterDisplayKey: "main" ,
task: "new parent" ,
cleanup: "keep" ,
createdAt: Date.now() - 90 _000 ,
startedAt: Date.now() - 90 _000 ,
});
addSubagentRunForTests({
runId: "run-status-child-stale-old-parent" ,
childSessionKey,
requesterSessionKey: oldParentKey,
requesterDisplayKey: oldParentKey,
controllerSessionKey: oldParentKey,
task: "stale old parent child" ,
cleanup: "keep" ,
createdAt: Date.now() - 60 _000 ,
startedAt: Date.now() - 60 _000 ,
});
addSubagentRunForTests({
runId: "run-status-child-current-new-parent" ,
childSessionKey,
requesterSessionKey: newParentKey,
requesterDisplayKey: newParentKey,
controllerSessionKey: newParentKey,
task: "current new parent child" ,
cleanup: "keep" ,
createdAt: Date.now() - 30 _000 ,
startedAt: Date.now() - 30 _000 ,
});
const reply = await buildStatusReplyForTest({ sessionKey: oldParentKey, verbose: true });
expect(reply?.text).not.toContain(" Subagents: 1 active" );
expect(reply?.text).not.toContain("stale old parent child" );
});
it("counts controller-owned runs even when the latest child requester differs" , async () => {
addSubagentRunForTests({
runId: "run-status-controller-owned" ,
childSessionKey: "agent:main:subagent:status-controller-owned" ,
requesterSessionKey: "agent:main:requester-only" ,
requesterDisplayKey: "requester-only" ,
controllerSessionKey: "agent:main:main" ,
task: "controller-owned status worker" ,
cleanup: "keep" ,
createdAt: Date.now() - 60 _000 ,
startedAt: Date.now() - 60 _000 ,
});
const reply = await buildStatusReplyForTest({});
expect(reply?.text).toContain(" Subagents: 1 active" );
});
it("includes active and total task counts for the current session" , async () => {
createRunningTaskRun({
runtime: "subagent" ,
requesterSessionKey: "agent:main:main" ,
childSessionKey: "agent:main:subagent:status-task-running" ,
runId: "run-status-task-running" ,
task: "active background task" ,
progressSummary: "still working" ,
});
createQueuedTaskRun({
runtime: "cron" ,
requesterSessionKey: "agent:main:main" ,
childSessionKey: "agent:main:subagent:status-task-queued" ,
runId: "run-status-task-queued" ,
task: "queued background task" ,
});
const reply = await buildStatusReplyForTest({});
expect(reply?.text).toContain(" Tasks: 2 active · 2 total" );
expect(reply?.text).toMatch(/ Tasks: 2 active · 2 total · (subagent|cron) · /);
});
it("hides stale completed task rows from the session task line" , async () => {
createRunningTaskRun({
runtime: "subagent" ,
requesterSessionKey: "agent:main:main" ,
childSessionKey: "agent:main:subagent:status-task-live" ,
runId: "run-status-task-live" ,
task: "live background task" ,
progressSummary: "still working" ,
});
createQueuedTaskRun({
runtime: "cron" ,
requesterSessionKey: "agent:main:main" ,
childSessionKey: "agent:main:subagent:status-task-stale-done" ,
runId: "run-status-task-stale-done" ,
task: "stale completed task" ,
});
completeTaskRunByRunId({
runId: "run-status-task-stale-done" ,
endedAt: Date.now() - 10 * 60 _000 ,
terminalSummary: "done a while ago" ,
});
const reply = await buildStatusReplyForTest({});
expect(reply?.text).toContain(" Tasks: 1 active · 1 total" );
expect(reply?.text).toContain("live background task" );
expect(reply?.text).not.toContain("stale completed task" );
expect(reply?.text).not.toContain("done a while ago" );
});
it("shows a recent failure when no active tasks remain" , async () => {
createRunningTaskRun({
runtime: "acp" ,
requesterSessionKey: "agent:main:main" ,
childSessionKey: "agent:main:acp:status-task-failed" ,
runId: "run-status-task-failed" ,
task: "failed background task" ,
});
failTaskRunByRunId({
runId: "run-status-task-failed" ,
endedAt: Date.now(),
error: "approval denied" ,
});
const reply = await buildStatusReplyForTest({});
expect(reply?.text).toContain(" Tasks: 1 recent failure" );
expect(reply?.text).toContain("failed background task" );
expect(reply?.text).toContain("approval denied" );
});
it("does not leak internal runtime context through the task status line" , async () => {
createRunningTaskRun({
runtime: "subagent" ,
requesterSessionKey: "agent:main:main" ,
childSessionKey: "agent:main:subagent:status-task-leak" ,
runId: "run-status-task-leak" ,
task: "leaked context task" ,
});
failTaskRunByRunId({
runId: "run-status-task-leak" ,
endedAt: Date.now(),
error: [
"OpenClaw runtime context (internal):" ,
"This context is runtime-generated, not user-authored. Keep internal details private." ,
"" ,
"[Internal task completion event]" ,
"source: subagent" ,
].join("\n" ),
});
const reply = await buildStatusReplyForTest({});
expect(reply?.text).toContain(" Tasks: 1 recent failure" );
expect(reply?.text).toContain("leaked context task" );
expect(reply?.text).not.toContain("OpenClaw runtime context (internal):" );
expect(reply?.text).not.toContain("Internal task completion event" );
});
it("truncates long task titles and details in the session task line" , async () => {
createRunningTaskRun({
runtime: "subagent" ,
requesterSessionKey: "agent:main:main" ,
childSessionKey: "agent:main:subagent:status-task-truncated" ,
runId: "run-status-task-truncated" ,
task: "This is a deliberately long task prompt that should never be emitted in full by /status because it can include internal instructions and file paths that are not appropriate for the headline line shown to users." ,
progressSummary:
"This progress detail is also intentionally long so the status surface proves it truncates verbose task context instead of dumping a multi-sentence internal update into the reply output." ,
});
const reply = await buildStatusReplyForTest({});
expect(reply?.text).toContain(
"This is a deliberately long task prompt that should never be emitted in full by…" ,
);
expect(reply?.text).toContain(
"This progress detail is also intentionally long so the status surface proves it truncates verbose task context instead…" ,
);
expect(reply?.text).not.toContain("internal instructions and file paths" );
expect(reply?.text).not.toContain("dumping a multi-sentence internal update" );
});
it("prefers failure context over newer success context when showing recent failures" , async () => {
createRunningTaskRun({
runtime: "acp" ,
requesterSessionKey: "agent:main:main" ,
childSessionKey: "agent:main:acp:status-task-failed-priority" ,
runId: "run-status-task-failed-priority" ,
task: "failed background task" ,
});
failTaskRunByRunId({
runId: "run-status-task-failed-priority" ,
endedAt: Date.now() - 30 _000 ,
error: "approval denied" ,
});
createRunningTaskRun({
runtime: "subagent" ,
requesterSessionKey: "agent:main:main" ,
childSessionKey: "agent:main:subagent:status-task-succeeded-later" ,
runId: "run-status-task-succeeded-later" ,
task: "later successful task" ,
});
completeTaskRunByRunId({
runId: "run-status-task-succeeded-later" ,
endedAt: Date.now(),
terminalSummary: "all done" ,
});
const reply = await buildStatusReplyForTest({});
expect(reply?.text).toContain(" Tasks: 1 recent failure" );
expect(reply?.text).toContain("failed background task" );
expect(reply?.text).toContain("approval denied" );
expect(reply?.text).not.toContain("later successful task" );
expect(reply?.text).not.toContain("all done" );
});
it("falls back to same-agent task counts without details when the current session has none" , async () => {
createRunningTaskRun({
runtime: "subagent" ,
requesterSessionKey: "agent:main:other" ,
childSessionKey: "agent:main:subagent:status-agent-fallback-running" ,
runId: "run-status-agent-fallback-running" ,
agentId: "main" ,
task: "hidden task title" ,
progressSummary: "hidden progress detail" ,
});
createQueuedTaskRun({
runtime: "cron" ,
requesterSessionKey: "agent:main:another" ,
childSessionKey: "agent:main:subagent:status-agent-fallback-queued" ,
runId: "run-status-agent-fallback-queued" ,
agentId: "main" ,
task: "another hidden task title" ,
});
const reply = await buildStatusReplyForTest({ sessionKey: "agent:main:empty-session" });
expect(reply?.text).toContain(" Tasks: 2 active · 2 total · agent-local" );
expect(reply?.text).not.toContain("hidden task title" );
expect(reply?.text).not.toContain("hidden progress detail" );
expect(reply?.text).not.toContain("subagent" );
expect(reply?.text).not.toContain("cron" );
});
it("uses transcript usage fallback in /status output" , async () => {
await withTempHome(async (dir) => {
const sessionId = "sess-status-transcript" ;
writeTranscriptUsageLog({
dir,
agentId: "main" ,
sessionId,
usage: {
input: 1 ,
output: 2 ,
cacheRead: 1000 ,
cacheWrite: 0 ,
totalTokens: 1003 ,
},
});
const text = await buildStatusText({
cfg: baseCfg,
sessionEntry: {
sessionId,
updatedAt: 0 ,
totalTokens: 3 ,
contextTokens: 32 _000 ,
},
sessionKey: "agent:main:main" ,
parentSessionKey: "agent:main:main" ,
sessionScope: "per-sender" ,
statusChannel: "mobilechat" ,
provider: "anthropic" ,
model: "claude-opus-4-5" ,
contextTokens: 32 _000 ,
resolvedFastMode: false ,
resolvedVerboseLevel: "off" ,
resolvedReasoningLevel: "off" ,
resolveDefaultThinkingLevel: async () => undefined,
isGroup: false ,
defaultGroupActivation: () => "mention" ,
modelAuthOverride: "api-key" ,
activeModelAuthOverride: "api-key" ,
});
expect(normalizeTestText(text)).toContain("Context: 1.0k/32k" );
});
});
it("shows the effective non-PI embedded harness in /status" , async () => {
registerStatusCodexHarness();
const text = await buildStatusText({
cfg: {
...baseCfg,
agents: {
defaults: {
embeddedHarness: { runtime: "codex" },
},
},
},
sessionEntry: {
sessionId: "sess-status-codex" ,
updatedAt: 0 ,
fastMode: true ,
},
sessionKey: "agent:main:main" ,
parentSessionKey: "agent:main:main" ,
sessionScope: "per-sender" ,
statusChannel: "mobilechat" ,
provider: "openai" ,
model: "gpt-5.4" ,
contextTokens: 32 _000 ,
resolvedFastMode: true ,
resolvedVerboseLevel: "off" ,
resolvedReasoningLevel: "off" ,
resolveDefaultThinkingLevel: async () => undefined,
isGroup: false ,
defaultGroupActivation: () => "mention" ,
modelAuthOverride: "api-key" ,
activeModelAuthOverride: "api-key" ,
});
const normalized = normalizeTestText(text);
expect(normalized).toContain("Runtime: OpenAI Codex" );
expect(normalized).toContain("Fast" );
expect(normalized).not.toContain("Fast · codex" );
});
it("keeps /status on a session-pinned PI harness after config changes" , async () => {
registerStatusCodexHarness();
const text = await buildStatusText({
cfg: {
...baseCfg,
agents: {
defaults: {
embeddedHarness: { runtime: "codex" },
},
},
},
sessionEntry: {
sessionId: "sess-status-pinned-pi" ,
updatedAt: 0 ,
fastMode: true ,
agentHarnessId: "pi" ,
},
sessionKey: "agent:main:main" ,
parentSessionKey: "agent:main:main" ,
sessionScope: "per-sender" ,
statusChannel: "mobilechat" ,
provider: "openai" ,
model: "gpt-5.4" ,
contextTokens: 32 _000 ,
resolvedFastMode: true ,
resolvedVerboseLevel: "off" ,
resolvedReasoningLevel: "off" ,
resolveDefaultThinkingLevel: async () => undefined,
isGroup: false ,
defaultGroupActivation: () => "mention" ,
modelAuthOverride: "api-key" ,
activeModelAuthOverride: "api-key" ,
});
const normalized = normalizeTestText(text);
expect(normalized).toContain("Fast" );
expect(normalized).not.toContain("codex" );
});
});
Messung V0.5 in Prozent C=99 H=99 G=98
¤ Dauer der Verarbeitung: 0.6 Sekunden
¤
*© Formatika GbR, Deutschland