From c93b2540ecdb0e4b12174415929a0660feb062fc Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 17 Apr 2026 08:10:24 +0100 Subject: [PATCH] test: mock status command scan seam --- src/commands/status.test.ts | 261 +++++++++++++++++++++++++++++++++++- 1 file changed, 260 insertions(+), 1 deletion(-) diff --git a/src/commands/status.test.ts b/src/commands/status.test.ts index 0a1606d08be..b595e8e19ed 100644 --- a/src/commands/status.test.ts +++ b/src/commands/status.test.ts @@ -175,6 +175,207 @@ function createDefaultSecurityAuditResult() { }; } +async function createStatusServiceSummary( + service: ReturnType<(typeof mocks)["resolveGatewayService"]>, +) { + const [loaded, runtime, command] = await Promise.all([ + service.isLoaded(), + service.readRuntime(), + service.readCommand(), + ]); + return { + label: service.label, + installed: Boolean(command) || runtime?.status === "running", + loaded, + managedByOpenClaw: Boolean(command), + externallyManaged: !command && runtime?.status === "running", + loadedText: service.loadedText, + runtime, + runtimeShort: runtime?.pid ? `pid ${runtime.pid}` : null, + }; +} + +function createSessionStatusRows() { + const agents = mocks.listGatewayAgentsBasic().agents ?? [{ id: "main", name: "Main" }]; + const byAgent = agents.map((agent: { id: string }) => { + const path = mocks.resolveStorePath("sessions", { agentId: agent.id }); + const store = mocks.loadSessionStore(path) as Record< + string, + ReturnType + >; + const recent = Object.entries(store).map(([key, entry]) => { + const contextTokens = typeof entry.contextTokens === "number" ? entry.contextTokens : null; + const freshTotal = + typeof entry.totalTokens === "number" && entry.totalTokensFresh !== false + ? entry.totalTokens + : null; + return { + agentId: agent.id, + key, + kind: key.startsWith("+") ? ("direct" as const) : ("unknown" as const), + sessionId: entry.sessionId, + updatedAt: entry.updatedAt ?? null, + age: typeof entry.updatedAt === "number" ? Math.max(0, Date.now() - entry.updatedAt) : null, + thinkingLevel: entry.thinkingLevel, + verboseLevel: entry.verboseLevel, + inputTokens: entry.inputTokens, + outputTokens: entry.outputTokens, + totalTokens: freshTotal, + totalTokensFresh: freshTotal !== null, + cacheRead: entry.cacheRead, + cacheWrite: entry.cacheWrite, + remainingTokens: + freshTotal !== null && contextTokens !== null + ? Math.max(0, contextTokens - freshTotal) + : null, + percentUsed: + freshTotal !== null && contextTokens + ? Math.round((freshTotal / contextTokens) * 100) + : null, + model: typeof entry.model === "string" ? entry.model : null, + contextTokens, + flags: [ + ...(entry.verboseLevel ? [`verbose:${entry.verboseLevel}`] : []), + ...(entry.thinkingLevel ? [`think:${entry.thinkingLevel}`] : []), + ], + }; + }); + return { agentId: agent.id, path, count: recent.length, recent }; + }); + const recent = byAgent.flatMap((entry) => entry.recent); + return { + paths: byAgent.map((entry) => entry.path), + count: recent.length, + defaults: { + model: recent[0]?.model ?? "pi:opus", + contextTokens: recent[0]?.contextTokens ?? 10_000, + }, + recent, + byAgent, + }; +} + +async function createMockStatusScanResult(params: { includePluginCompatibility?: boolean } = {}) { + const cfg = mocks.loadConfig(); + const gatewayProbe = await mocks.probeGateway(); + const gatewayReachable = gatewayProbe.ok === true; + const gatewayAuthWarning = + cfg.gateway?.auth?.token && typeof cfg.gateway.auth.token === "object" + ? "gateway.auth.token unavailable" + : undefined; + const agentStatus = { + ...mocks.listGatewayAgentsBasic(), + bootstrapPendingCount: 0, + totalSessions: 1, + agents: mocks.listGatewayAgentsBasic().agents.map((agent: { id: string; name?: string }) => ({ + ...agent, + bootstrapPending: false, + activeSessions: 1, + })), + }; + const sessions = createSessionStatusRows(); + const channelIssues = gatewayReachable + ? [ + { + channel: "signal", + accountId: "default", + message: "gateway: signal-cli unreachable", + }, + { + channel: "imessage", + accountId: "default", + message: "gateway: imessage permission denied", + }, + ] + : [ + { + channel: "signal", + accountId: "default", + message: "Channel error: signal-cli unreachable", + }, + { + channel: "imessage", + accountId: "default", + message: "Channel error: imessage permission denied", + }, + ]; + const pluginCompatibility = + params.includePluginCompatibility === false ? [] : mocks.buildPluginCompatibilityNotices(); + return { + cfg, + sourceConfig: cfg, + secretDiagnostics: gatewayAuthWarning ? ["gateway.auth.token unavailable"] : [], + osSummary: { + platform: "darwin", + arch: "arm64", + release: "23.0.0", + label: "macos 14.0 (arm64)", + }, + tailscaleMode: "off", + tailscaleDns: null, + tailscaleHttpsUrl: null, + update: { + root: "/tmp/openclaw", + installKind: "git", + packageManager: "pnpm", + git: { + root: "/tmp/openclaw", + branch: "main", + upstream: "origin/main", + dirty: false, + ahead: 0, + behind: 0, + fetchOk: true, + }, + deps: { + manager: "pnpm", + status: "ok", + lockfilePath: "/tmp/openclaw/pnpm-lock.yaml", + markerPath: "/tmp/openclaw/node_modules/.modules.yaml", + }, + registry: { latestVersion: "0.0.0" }, + }, + gatewayConnection: { url: "ws://127.0.0.1:18789" }, + remoteUrlMissing: false, + gatewayMode: "local" as const, + gatewayProbeAuth: process.env.OPENCLAW_GATEWAY_TOKEN + ? { token: process.env.OPENCLAW_GATEWAY_TOKEN } + : {}, + gatewayProbeAuthWarning: gatewayAuthWarning, + gatewayProbe, + gatewayReachable, + gatewaySelf: gatewayProbe.presence ? { host: "gateway", ip: "127.0.0.1" } : null, + channelIssues, + agentStatus, + channels: { + rows: [ + { id: "whatsapp", label: "WhatsApp", enabled: true, state: "ok", detail: "linked" }, + { id: "signal", label: "Signal", enabled: true, state: "warn", detail: "gateway warning" }, + { + id: "imessage", + label: "iMessage", + enabled: true, + state: "warn", + detail: "gateway warning", + }, + ], + details: [], + }, + summary: { + runtimeVersion: null, + heartbeat: { defaultAgentId: "main", agents: [] }, + channelSummary: [], + queuedSystemEvents: [], + tasks: mocks.getInspectableTaskRegistrySummary(), + taskAudit: mocks.getInspectableTaskAuditSummary(), + sessions, + }, + memory: null, + memoryPlugin: { enabled: true, slot: "memory-core" }, + pluginCompatibility, + }; +} + async function withEnvVar(key: string, value: string, run: () => Promise): Promise { const prevValue = process.env[key]; process.env[key] = value; @@ -517,7 +718,65 @@ vi.mock("../plugins/status.js", () => ({ `${notice.pluginId} ${notice.message}`, })); -import { statusCommand } from "./status.js"; +vi.mock("./status.scan.fast-json.js", () => ({ + scanStatusJsonFast: vi.fn(async () => + createMockStatusScanResult({ includePluginCompatibility: false }), + ), +})); + +vi.mock("./status.scan.js", () => ({ + scanStatus: vi.fn(async () => createMockStatusScanResult()), +})); + +vi.mock("./status-runtime-shared.ts", () => ({ + loadStatusProviderUsageModule: vi.fn(async () => ({ + formatUsageReportLines: vi.fn(() => []), + })), + resolveStatusGatewayHealth: vi.fn(async () => ({})), + resolveStatusSecurityAudit: vi.fn(async (input: unknown) => + mocks.runSecurityAudit({ + ...(typeof input === "object" && input ? input : {}), + deep: false, + includeFilesystem: true, + includeChannelSecurity: true, + }), + ), + resolveStatusUsageSummary: vi.fn(async () => undefined), + resolveStatusRuntimeSnapshot: vi.fn( + async (params: { + includeSecurityAudit?: boolean; + resolveSecurityAudit?: (input: unknown) => Promise; + config: unknown; + sourceConfig: unknown; + }) => { + const securityAudit = params.includeSecurityAudit + ? await ( + params.resolveSecurityAudit ?? + (async (input) => + await mocks.runSecurityAudit({ + ...(typeof input === "object" && input ? input : {}), + deep: false, + includeFilesystem: true, + includeChannelSecurity: true, + })) + )({ + config: params.config, + sourceConfig: params.sourceConfig, + }) + : undefined; + return { + securityAudit, + usage: undefined, + health: undefined, + lastHeartbeat: null, + gatewayService: await createStatusServiceSummary(mocks.resolveGatewayService()), + nodeService: await createStatusServiceSummary(mocks.resolveNodeService()), + }; + }, + ), +})); + +import { statusCommand } from "./status.command.js"; const runtime = { log: vi.fn(),