mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 17:51:22 +00:00
fix(check): repair status report typing drift
This commit is contained in:
@@ -44,8 +44,6 @@ const defaultOptions: Required<Omit<RequestClientOptions, "baseUrl" | "tokenHead
|
||||
timeout: 15_000,
|
||||
queueRequests: true,
|
||||
maxQueueSize: 1000,
|
||||
runtimeProfile: "serverless",
|
||||
scheduler: {},
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
|
||||
@@ -50,7 +50,7 @@ function collectMatchingToolResultIds(message: AgentMessage): Set<string> {
|
||||
ids.add(toolResultId);
|
||||
}
|
||||
} else if (role === "tool") {
|
||||
for (const id of extractToolResultIdsFromRecord(message as Record<string, unknown>)) {
|
||||
for (const id of extractToolResultIdsFromRecord(message as unknown as Record<string, unknown>)) {
|
||||
ids.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,10 @@ function createRecentSessionRows(now = Date.now()) {
|
||||
}
|
||||
|
||||
vi.mock("../gateway/call.js", () => ({
|
||||
callGateway: (...args: unknown[]) => callGatewayMock(...args),
|
||||
buildGatewayConnectionDetails: (...args: unknown[]) => buildGatewayConnectionDetailsMock(...args),
|
||||
callGateway: (...args: [unknown, ...unknown[]]) =>
|
||||
Reflect.apply(callGatewayMock, undefined, args),
|
||||
buildGatewayConnectionDetails: (...args: [unknown, ...unknown[]]) =>
|
||||
Reflect.apply(buildGatewayConnectionDetailsMock, undefined, args),
|
||||
}));
|
||||
|
||||
describe("healthCommand (coverage)", () => {
|
||||
|
||||
@@ -23,6 +23,7 @@ type StatusServiceSummaries = Awaited<ReturnType<typeof resolveStatusServiceSumm
|
||||
type StatusGatewayServiceSummary = StatusServiceSummaries[0];
|
||||
type StatusNodeServiceSummary = StatusServiceSummaries[1];
|
||||
type StatusGatewayHealthSafe = Awaited<ReturnType<typeof resolveStatusGatewayHealthSafe>>;
|
||||
type ConfigFileSnapshot = Awaited<ReturnType<typeof readConfigFileSnapshot>>;
|
||||
|
||||
type StatusAllProgress = {
|
||||
setLabel(label: string): void;
|
||||
@@ -46,7 +47,7 @@ async function resolveStatusAllLocalDiagnosis(params: {
|
||||
configPath: string;
|
||||
health: StatusGatewayHealthSafe | undefined;
|
||||
diagnosis: {
|
||||
snap: Awaited<ReturnType<typeof readConfigFileSnapshot>>;
|
||||
snap: ConfigFileSnapshot | null;
|
||||
remoteUrlMissing: boolean;
|
||||
secretDiagnostics: StatusScanOverviewResult["secretDiagnostics"];
|
||||
sentinel: Awaited<ReturnType<typeof readRestartSentinel>> | null;
|
||||
@@ -81,7 +82,7 @@ async function resolveStatusAllLocalDiagnosis(params: {
|
||||
timeoutMs: Math.min(8000, params.timeoutMs ?? 10_000),
|
||||
gatewayReachable: params.gatewayReachable,
|
||||
gatewayProbeError: params.gatewayProbe?.error ?? null,
|
||||
callOverrides: params.gatewayCallOverrides ?? {},
|
||||
...(params.gatewayCallOverrides ? { callOverrides: params.gatewayCallOverrides } : {}),
|
||||
});
|
||||
|
||||
params.progress.setLabel("Checking local state…");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TableColumn } from "../../terminal/table.js";
|
||||
import type { RenderTableOptions, TableColumn } from "../../terminal/table.js";
|
||||
import { buildStatusChannelsTableRows, statusChannelsTableColumns } from "./channels-table.js";
|
||||
import {
|
||||
buildStatusAgentTableRows,
|
||||
@@ -8,11 +8,7 @@ import {
|
||||
} from "./report-tables.js";
|
||||
import type { StatusReportSection } from "./text-report.js";
|
||||
|
||||
type TableRenderer = (input: {
|
||||
width: number;
|
||||
columns: Array<Record<string, unknown>>;
|
||||
rows: Array<Record<string, string>>;
|
||||
}) => string;
|
||||
type TableRenderer = (input: RenderTableOptions) => string;
|
||||
|
||||
export function buildStatusOverviewSection(params: {
|
||||
width: number;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RenderTableOptions } from "../../terminal/table.js";
|
||||
import { formatTimeAgo } from "./format.js";
|
||||
import type { StatusReportSection } from "./text-report.js";
|
||||
|
||||
@@ -53,11 +54,7 @@ export function buildStatusAgentTableRows(params: {
|
||||
export function buildStatusChannelDetailSections(params: {
|
||||
details: ChannelDetailLike[];
|
||||
width: number;
|
||||
renderTable: (input: {
|
||||
width: number;
|
||||
columns: Array<Record<string, unknown>>;
|
||||
rows: Array<Record<string, string>>;
|
||||
}) => string;
|
||||
renderTable: (input: RenderTableOptions) => string;
|
||||
ok: (text: string) => string;
|
||||
warn: (text: string) => string;
|
||||
}): StatusReportSection[] {
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import type { TableColumn } from "../../terminal/table.js";
|
||||
import type { RenderTableOptions, TableColumn } from "../../terminal/table.js";
|
||||
|
||||
type HeadingFn = (text: string) => string;
|
||||
type TableRenderer = (input: {
|
||||
width: number;
|
||||
columns: Array<Record<string, unknown>>;
|
||||
rows: Array<Record<string, string>>;
|
||||
}) => string;
|
||||
type TableRenderer = (input: RenderTableOptions) => string;
|
||||
|
||||
export type StatusReportSection =
|
||||
| {
|
||||
@@ -19,7 +15,7 @@ export type StatusReportSection =
|
||||
title: string;
|
||||
width: number;
|
||||
renderTable: TableRenderer;
|
||||
columns: Array<Record<string, unknown>>;
|
||||
columns: readonly TableColumn[];
|
||||
rows: Array<Record<string, string>>;
|
||||
trailer?: string | null;
|
||||
skipIfEmpty?: boolean;
|
||||
|
||||
@@ -29,12 +29,19 @@ describe("runStatusJsonCommand", () => {
|
||||
cfg: { gateway: {} },
|
||||
sourceConfig: { gateway: {} },
|
||||
summary: { ok: true },
|
||||
update: { installKind: "package", packageManager: "npm" },
|
||||
update: { root: null, installKind: "package" as const, packageManager: "npm" as const },
|
||||
osSummary: { platform: "linux" },
|
||||
memory: null,
|
||||
memoryPlugin: null,
|
||||
tailscaleMode: "off",
|
||||
tailscaleDns: null,
|
||||
tailscaleHttpsUrl: null,
|
||||
gatewayMode: "local" as const,
|
||||
gatewayConnection: { url: "ws://127.0.0.1:18789", urlSource: "config" },
|
||||
gatewayConnection: {
|
||||
url: "ws://127.0.0.1:18789",
|
||||
urlSource: "config",
|
||||
message: "Gateway target: ws://127.0.0.1:18789",
|
||||
},
|
||||
remoteUrlMissing: false,
|
||||
gatewayReachable: true,
|
||||
gatewayProbe: null,
|
||||
|
||||
@@ -25,19 +25,10 @@ describe("status-json-payload", () => {
|
||||
resolveStatusUpdateChannelInfo({
|
||||
updateConfigChannel: "beta",
|
||||
update: {
|
||||
root: "/tmp/openclaw",
|
||||
installKind: "package",
|
||||
packageManager: "npm",
|
||||
git: {
|
||||
root: "/tmp/openclaw",
|
||||
sha: null,
|
||||
tag: "v1.2.3",
|
||||
branch: "main",
|
||||
upstream: null,
|
||||
dirty: false,
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
fetchOk: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -76,8 +67,8 @@ describe("status-json-payload", () => {
|
||||
gatewayProbeAuth: { token: "tok" },
|
||||
gatewaySelf: { host: "gateway" },
|
||||
gatewayProbeAuthWarning: "warn",
|
||||
gatewayService: { label: "LaunchAgent" },
|
||||
nodeService: { label: "node" },
|
||||
gatewayService: { label: "LaunchAgent", installed: true, loadedText: "loaded" },
|
||||
nodeService: { label: "node", installed: true, loadedText: "loaded" },
|
||||
},
|
||||
osSummary: { platform: "linux" },
|
||||
memory: null,
|
||||
|
||||
@@ -75,7 +75,7 @@ export async function resolveStatusJsonOutput(params: {
|
||||
return buildStatusJsonPayload({
|
||||
summary: scan.summary,
|
||||
surface: buildStatusOverviewSurfaceFromScan({
|
||||
scan,
|
||||
scan: scan as never,
|
||||
gatewayService,
|
||||
nodeService,
|
||||
}),
|
||||
|
||||
@@ -80,16 +80,16 @@ describe("status-overview-rows", () => {
|
||||
memory: { files: 1, chunks: 2, vector: {}, fts: {}, cache: {} },
|
||||
memoryPlugin: { enabled: true, slot: "memory" },
|
||||
pluginCompatibility: [{ pluginId: "a", severity: "warn", message: "legacy" }],
|
||||
ok: (value) => `ok(${value})`,
|
||||
warn: (value) => `warn(${value})`,
|
||||
muted: (value) => `muted(${value})`,
|
||||
formatTimeAgo: (value) => `${value}ms`,
|
||||
formatKTokens: (value) => `${Math.round(value / 1000)}k`,
|
||||
ok: (value: string) => `ok(${value})`,
|
||||
warn: (value: string) => `warn(${value})`,
|
||||
muted: (value: string) => `muted(${value})`,
|
||||
formatTimeAgo: (value: number) => `${value}ms`,
|
||||
formatKTokens: (value: number) => `${Math.round(value / 1000)}k`,
|
||||
resolveMemoryVectorState: () => ({ state: "ready", tone: "ok" }),
|
||||
resolveMemoryFtsState: () => ({ state: "ready", tone: "warn" }),
|
||||
resolveMemoryCacheSummary: () => ({ text: "cache warm", tone: "muted" }),
|
||||
updateValue: "available · custom update",
|
||||
}),
|
||||
} as unknown as Parameters<typeof buildStatusCommandOverviewRows>[0]),
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ Item: "OS", Value: `macOS · node ${process.versions.node}` },
|
||||
@@ -148,7 +148,7 @@ describe("status-overview-rows", () => {
|
||||
agents: [{ id: "main", lastActiveAgeMs: 60_000 }],
|
||||
},
|
||||
tailscaleBackendState: "Running",
|
||||
}),
|
||||
} as unknown as Parameters<typeof buildStatusAllOverviewRows>[0]),
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ Item: "Version", Value: expect.any(String) },
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import type { HeartbeatEventPayload } from "../infra/heartbeat-events.js";
|
||||
import type { Tone } from "../memory-host-sdk/status.js";
|
||||
import type { PluginCompatibilityNotice } from "../plugins/status.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import type { HealthSummary } from "./health.js";
|
||||
import type { AgentLocalStatus } from "./status.agent-local.js";
|
||||
import {
|
||||
buildStatusOverviewRowsFromSurface,
|
||||
type StatusOverviewSurface,
|
||||
@@ -19,6 +24,8 @@ import {
|
||||
buildStatusMemoryValue,
|
||||
buildStatusTasksValue,
|
||||
} from "./status.command-sections.js";
|
||||
import type { MemoryPluginStatus, MemoryStatusSnapshot } from "./status.scan.shared.js";
|
||||
import type { StatusSummary } from "./status.types.js";
|
||||
|
||||
export function buildStatusCommandOverviewRows(params: {
|
||||
opts: {
|
||||
@@ -26,69 +33,35 @@ export function buildStatusCommandOverviewRows(params: {
|
||||
};
|
||||
surface: StatusOverviewSurface;
|
||||
osLabel: string;
|
||||
summary: {
|
||||
tasks: {
|
||||
total: number;
|
||||
active: number;
|
||||
failures: number;
|
||||
byStatus: { queued: number; running: number };
|
||||
};
|
||||
taskAudit: {
|
||||
errors: number;
|
||||
warnings: number;
|
||||
};
|
||||
heartbeat: {
|
||||
agents: Array<{
|
||||
agentId: string;
|
||||
enabled?: boolean | null;
|
||||
everyMs?: number | null;
|
||||
every: string;
|
||||
}>;
|
||||
};
|
||||
queuedSystemEvents: string[];
|
||||
sessions: {
|
||||
count: number;
|
||||
paths: string[];
|
||||
defaults: {
|
||||
model?: string | null;
|
||||
contextTokens?: number | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
health?: unknown;
|
||||
lastHeartbeat: unknown;
|
||||
summary: StatusSummary;
|
||||
health?: HealthSummary;
|
||||
lastHeartbeat: HeartbeatEventPayload | null;
|
||||
agentStatus: {
|
||||
defaultId?: string | null;
|
||||
bootstrapPendingCount: number;
|
||||
totalSessions: number;
|
||||
agents: Array<{
|
||||
id: string;
|
||||
lastActiveAgeMs?: number | null;
|
||||
}>;
|
||||
agents: AgentLocalStatus[];
|
||||
};
|
||||
memory: {
|
||||
files: number;
|
||||
chunks: number;
|
||||
dirty?: boolean;
|
||||
sources?: string[];
|
||||
vector?: unknown;
|
||||
fts?: unknown;
|
||||
cache?: unknown;
|
||||
} | null;
|
||||
memoryPlugin: {
|
||||
enabled: boolean;
|
||||
reason?: string | null;
|
||||
slot?: string | null;
|
||||
};
|
||||
pluginCompatibility: Array<{ severity?: "warn" | "info" | null } & Record<string, unknown>>;
|
||||
memory: MemoryStatusSnapshot | null;
|
||||
memoryPlugin: MemoryPluginStatus;
|
||||
pluginCompatibility: PluginCompatibilityNotice[];
|
||||
ok: (value: string) => string;
|
||||
warn: (value: string) => string;
|
||||
muted: (value: string) => string;
|
||||
formatTimeAgo: (ageMs: number) => string;
|
||||
formatKTokens: (value: number) => string;
|
||||
resolveMemoryVectorState: (value: unknown) => { state: string; tone: "ok" | "warn" | "muted" };
|
||||
resolveMemoryFtsState: (value: unknown) => { state: string; tone: "ok" | "warn" | "muted" };
|
||||
resolveMemoryCacheSummary: (value: unknown) => { text: string; tone: "ok" | "warn" | "muted" };
|
||||
resolveMemoryVectorState: (value: NonNullable<MemoryStatusSnapshot["vector"]>) => {
|
||||
state: string;
|
||||
tone: Tone;
|
||||
};
|
||||
resolveMemoryFtsState: (value: NonNullable<MemoryStatusSnapshot["fts"]>) => {
|
||||
state: string;
|
||||
tone: Tone;
|
||||
};
|
||||
resolveMemoryCacheSummary: (value: NonNullable<MemoryStatusSnapshot["cache"]>) => {
|
||||
text: string;
|
||||
tone: Tone;
|
||||
};
|
||||
updateValue?: string;
|
||||
}) {
|
||||
const agentsValue = buildStatusAgentsValue({
|
||||
@@ -112,7 +85,7 @@ export function buildStatusCommandOverviewRows(params: {
|
||||
const lastHeartbeatValue = buildStatusLastHeartbeatValue({
|
||||
deep: params.opts.deep,
|
||||
gatewayReachable: params.surface.gatewayReachable,
|
||||
lastHeartbeat: params.lastHeartbeat as never,
|
||||
lastHeartbeat: params.lastHeartbeat,
|
||||
warn: params.warn,
|
||||
muted: params.muted,
|
||||
formatTimeAgo: params.formatTimeAgo,
|
||||
|
||||
@@ -18,9 +18,13 @@ describe("status-overview-surface", () => {
|
||||
tailscaleHttpsUrl: "https://box.tail.ts.net",
|
||||
gatewayMode: "remote",
|
||||
remoteUrlMissing: false,
|
||||
gatewayConnection: { url: "wss://gateway.example.com", urlSource: "config" },
|
||||
gatewayConnection: {
|
||||
url: "wss://gateway.example.com",
|
||||
urlSource: "config",
|
||||
message: "Gateway target: wss://gateway.example.com",
|
||||
},
|
||||
gatewayReachable: true,
|
||||
gatewayProbe: { connectLatencyMs: 42, error: null },
|
||||
gatewayProbe: { connectLatencyMs: 42, error: null } as never,
|
||||
gatewayProbeAuth: { token: "tok" },
|
||||
gatewayProbeAuthWarning: "warn-text",
|
||||
gatewaySelf: { host: "gateway", version: "1.2.3" },
|
||||
@@ -48,9 +52,13 @@ describe("status-overview-surface", () => {
|
||||
tailscaleHttpsUrl: "https://box.tail.ts.net",
|
||||
gatewayMode: "remote",
|
||||
remoteUrlMissing: false,
|
||||
gatewayConnection: { url: "wss://gateway.example.com", urlSource: "config" },
|
||||
gatewayConnection: {
|
||||
url: "wss://gateway.example.com",
|
||||
urlSource: "config",
|
||||
message: "Gateway target: wss://gateway.example.com",
|
||||
},
|
||||
gatewayReachable: true,
|
||||
gatewayProbe: { connectLatencyMs: 42, error: null },
|
||||
gatewayProbe: { connectLatencyMs: 42, error: null } as never,
|
||||
gatewayProbeAuth: { token: "tok" },
|
||||
gatewayProbeAuthWarning: "warn-text",
|
||||
gatewaySelf: { host: "gateway", version: "1.2.3" },
|
||||
@@ -83,9 +91,13 @@ describe("status-overview-surface", () => {
|
||||
gatewaySnapshot: {
|
||||
gatewayMode: "remote",
|
||||
remoteUrlMissing: false,
|
||||
gatewayConnection: { url: "wss://gateway.example.com", urlSource: "config" },
|
||||
gatewayConnection: {
|
||||
url: "wss://gateway.example.com",
|
||||
urlSource: "config",
|
||||
message: "Gateway target: wss://gateway.example.com",
|
||||
},
|
||||
gatewayReachable: true,
|
||||
gatewayProbe: { connectLatencyMs: 42, error: null },
|
||||
gatewayProbe: { connectLatencyMs: 42, error: null } as never,
|
||||
gatewayProbeAuth: { token: "tok" },
|
||||
gatewayProbeAuthWarning: "warn-text",
|
||||
gatewaySelf: { host: "gateway", version: "1.2.3" },
|
||||
@@ -114,9 +126,13 @@ describe("status-overview-surface", () => {
|
||||
tailscaleHttpsUrl: "https://box.tail.ts.net",
|
||||
gatewayMode: "remote",
|
||||
remoteUrlMissing: false,
|
||||
gatewayConnection: { url: "wss://gateway.example.com", urlSource: "config" },
|
||||
gatewayConnection: {
|
||||
url: "wss://gateway.example.com",
|
||||
urlSource: "config",
|
||||
message: "Gateway target: wss://gateway.example.com",
|
||||
},
|
||||
gatewayReachable: true,
|
||||
gatewayProbe: { connectLatencyMs: 42, error: null },
|
||||
gatewayProbe: { connectLatencyMs: 42, error: null } as never,
|
||||
gatewayProbeAuth: { token: "tok" },
|
||||
gatewayProbeAuthWarning: "warn-text",
|
||||
gatewaySelf: { host: "gateway", version: "1.2.3" },
|
||||
@@ -165,7 +181,7 @@ describe("status-overview-surface", () => {
|
||||
urlSource: "config",
|
||||
},
|
||||
gatewayReachable: true,
|
||||
gatewayProbe: { connectLatencyMs: 42, error: null },
|
||||
gatewayProbe: { connectLatencyMs: 42, error: null } as never,
|
||||
gatewayProbeAuth: { token: "tok" },
|
||||
gatewayProbeAuthWarning: "warn-text",
|
||||
gatewaySelf: { host: "gateway", version: "1.2.3" },
|
||||
@@ -221,35 +237,18 @@ describe("status-overview-surface", () => {
|
||||
expect(
|
||||
buildStatusGatewayJsonPayloadFromSurface({
|
||||
surface: {
|
||||
cfg: { update: { channel: "stable" }, gateway: { bind: "loopback" } },
|
||||
update: { installKind: "package", packageManager: "npm" } as never,
|
||||
tailscaleMode: "serve",
|
||||
gatewayMode: "remote",
|
||||
remoteUrlMissing: false,
|
||||
gatewayConnection: {
|
||||
url: "wss://gateway.example.com",
|
||||
urlSource: "config",
|
||||
message: "Gateway target: wss://gateway.example.com",
|
||||
},
|
||||
gatewayReachable: true,
|
||||
gatewayProbe: { connectLatencyMs: 42, error: null },
|
||||
gatewayProbeAuth: { token: "tok" },
|
||||
gatewayProbe: { connectLatencyMs: 42, error: null } as never,
|
||||
gatewayProbeAuthWarning: "warn-text",
|
||||
gatewaySelf: { host: "gateway", version: "1.2.3" },
|
||||
gatewayService: {
|
||||
label: "LaunchAgent",
|
||||
installed: true,
|
||||
managedByOpenClaw: true,
|
||||
loadedText: "loaded",
|
||||
runtimeShort: "running",
|
||||
},
|
||||
nodeService: {
|
||||
label: "node",
|
||||
installed: true,
|
||||
loadedText: "loaded",
|
||||
runtime: { status: "running", pid: 42 },
|
||||
},
|
||||
nodeOnlyGateway: null,
|
||||
},
|
||||
} as never,
|
||||
}),
|
||||
).toEqual({
|
||||
mode: "remote",
|
||||
|
||||
@@ -88,31 +88,31 @@ describe("buildStatusCommandReportData", () => {
|
||||
pluginCompatibility: [{ pluginId: "a", severity: "warn", message: "legacy" }],
|
||||
pairingRecovery: { requestId: "req-1" },
|
||||
tableWidth: 120,
|
||||
ok: (value) => `ok(${value})`,
|
||||
warn: (value) => `warn(${value})`,
|
||||
muted: (value) => `muted(${value})`,
|
||||
shortenText: (value) => value,
|
||||
formatCliCommand: (value) => `cmd:${value}`,
|
||||
formatTimeAgo: (value) => `${value}ms`,
|
||||
formatKTokens: (value) => `${Math.round(value / 1000)}k`,
|
||||
ok: (value: string) => `ok(${value})`,
|
||||
warn: (value: string) => `warn(${value})`,
|
||||
muted: (value: string) => `muted(${value})`,
|
||||
shortenText: (value: string) => value,
|
||||
formatCliCommand: (value: string) => `cmd:${value}`,
|
||||
formatTimeAgo: (value: number) => `${value}ms`,
|
||||
formatKTokens: (value: number) => `${Math.round(value / 1000)}k`,
|
||||
formatTokensCompact: () => "12k",
|
||||
formatPromptCacheCompact: () => "cache ok",
|
||||
formatHealthChannelLines: () => ["Discord: OK · ready"],
|
||||
formatPluginCompatibilityNotice: (notice) => String(notice.message),
|
||||
formatPluginCompatibilityNotice: (notice: { message?: unknown }) => String(notice.message),
|
||||
formatUpdateAvailableHint: () => "update available",
|
||||
resolveMemoryVectorState: () => ({ state: "ready", tone: "ok" }),
|
||||
resolveMemoryFtsState: () => ({ state: "ready", tone: "warn" }),
|
||||
resolveMemoryCacheSummary: () => ({ text: "cache warm", tone: "muted" }),
|
||||
accentDim: (value) => `accent(${value})`,
|
||||
accentDim: (value: string) => `accent(${value})`,
|
||||
theme: {
|
||||
heading: (value) => `# ${value}`,
|
||||
muted: (value) => `muted(${value})`,
|
||||
warn: (value) => `warn(${value})`,
|
||||
error: (value) => `error(${value})`,
|
||||
heading: (value: string) => `# ${value}`,
|
||||
muted: (value: string) => `muted(${value})`,
|
||||
warn: (value: string) => `warn(${value})`,
|
||||
error: (value: string) => `error(${value})`,
|
||||
},
|
||||
renderTable: ({ rows }) => `table:${rows.length}`,
|
||||
renderTable: ({ rows }: { rows: Array<Record<string, string>> }) => `table:${rows.length}`,
|
||||
updateValue: "available · custom update",
|
||||
});
|
||||
} as unknown as Parameters<typeof buildStatusCommandReportData>[0]);
|
||||
|
||||
expect(result.overviewRows[0]).toEqual({
|
||||
Item: "OS",
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import type { HeartbeatEventPayload } from "../infra/heartbeat-events.js";
|
||||
import type { resolveOsSummary } from "../infra/os-summary.js";
|
||||
import type { Tone } from "../memory-host-sdk/status.js";
|
||||
import type { PluginCompatibilityNotice } from "../plugins/status.js";
|
||||
import type { SecurityAuditReport } from "../security/audit.js";
|
||||
import type { RenderTableOptions, TableColumn } from "../terminal/table.js";
|
||||
import type { HealthSummary } from "./health.js";
|
||||
import type { AgentLocalStatus } from "./status.agent-local.js";
|
||||
import {
|
||||
buildStatusChannelsTableRows,
|
||||
statusChannelsTableColumns,
|
||||
} from "./status-all/channels-table.js";
|
||||
import { buildStatusCommandOverviewRows } from "./status-overview-rows.ts";
|
||||
import { type StatusOverviewSurface } from "./status-overview-surface.ts";
|
||||
import type { StatusOverviewSurface } from "./status-overview-surface.ts";
|
||||
import {
|
||||
buildStatusFooterLines,
|
||||
buildStatusHealthRows,
|
||||
@@ -15,6 +23,8 @@ import {
|
||||
buildStatusSystemEventsTrailer,
|
||||
statusHealthColumns,
|
||||
} from "./status.command-sections.js";
|
||||
import type { MemoryPluginStatus, MemoryStatusSnapshot } from "./status.scan.shared.js";
|
||||
import type { SessionStatus, StatusSummary } from "./status.types.js";
|
||||
|
||||
export async function buildStatusCommandReportData(params: {
|
||||
opts: {
|
||||
@@ -22,63 +32,17 @@ export async function buildStatusCommandReportData(params: {
|
||||
verbose?: boolean;
|
||||
};
|
||||
surface: StatusOverviewSurface;
|
||||
osSummary: { label: string };
|
||||
summary: {
|
||||
tasks: {
|
||||
total: number;
|
||||
active: number;
|
||||
failures: number;
|
||||
byStatus: { queued: number; running: number };
|
||||
};
|
||||
taskAudit: {
|
||||
errors: number;
|
||||
warnings: number;
|
||||
};
|
||||
heartbeat: {
|
||||
agents: Array<{
|
||||
agentId: string;
|
||||
enabled?: boolean | null;
|
||||
everyMs?: number | null;
|
||||
every: string;
|
||||
}>;
|
||||
};
|
||||
queuedSystemEvents: string[];
|
||||
sessions: {
|
||||
count: number;
|
||||
paths: string[];
|
||||
defaults: {
|
||||
model?: string | null;
|
||||
contextTokens?: number | null;
|
||||
};
|
||||
recent: Array<{
|
||||
key: string;
|
||||
kind: string;
|
||||
updatedAt?: number | null;
|
||||
age: number;
|
||||
model?: string | null;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
securityAudit: {
|
||||
summary: { critical: number; warn: number; info: number };
|
||||
findings: Array<{
|
||||
severity: "critical" | "warn" | "info";
|
||||
title: string;
|
||||
detail: string;
|
||||
remediation?: string | null;
|
||||
}>;
|
||||
};
|
||||
health?: unknown;
|
||||
osSummary: ReturnType<typeof resolveOsSummary>;
|
||||
summary: StatusSummary;
|
||||
securityAudit?: SecurityAuditReport;
|
||||
health?: HealthSummary;
|
||||
usageLines?: string[];
|
||||
lastHeartbeat: unknown;
|
||||
lastHeartbeat: HeartbeatEventPayload | null;
|
||||
agentStatus: {
|
||||
defaultId?: string | null;
|
||||
bootstrapPendingCount: number;
|
||||
totalSessions: number;
|
||||
agents: Array<{
|
||||
id: string;
|
||||
lastActiveAgeMs?: number | null;
|
||||
}>;
|
||||
agents: AgentLocalStatus[];
|
||||
};
|
||||
channels: {
|
||||
rows: Array<{
|
||||
@@ -93,21 +57,9 @@ export async function buildStatusCommandReportData(params: {
|
||||
channel: string;
|
||||
message: string;
|
||||
}>;
|
||||
memory: {
|
||||
files: number;
|
||||
chunks: number;
|
||||
dirty?: boolean;
|
||||
sources?: string[];
|
||||
vector?: unknown;
|
||||
fts?: unknown;
|
||||
cache?: unknown;
|
||||
} | null;
|
||||
memoryPlugin: {
|
||||
enabled: boolean;
|
||||
reason?: string | null;
|
||||
slot?: string | null;
|
||||
};
|
||||
pluginCompatibility: Array<{ severity?: "warn" | "info" | null } & Record<string, unknown>>;
|
||||
memory: MemoryStatusSnapshot | null;
|
||||
memoryPlugin: MemoryPluginStatus;
|
||||
pluginCompatibility: PluginCompatibilityNotice[];
|
||||
pairingRecovery: { requestId: string | null } | null;
|
||||
tableWidth: number;
|
||||
ok: (value: string) => string;
|
||||
@@ -117,26 +69,23 @@ export async function buildStatusCommandReportData(params: {
|
||||
formatCliCommand: (value: string) => string;
|
||||
formatTimeAgo: (ageMs: number) => string;
|
||||
formatKTokens: (value: number) => string;
|
||||
formatTokensCompact: (value: {
|
||||
key: string;
|
||||
kind: string;
|
||||
updatedAt?: number | null;
|
||||
age: number;
|
||||
model?: string | null;
|
||||
}) => string;
|
||||
formatPromptCacheCompact: (value: {
|
||||
key: string;
|
||||
kind: string;
|
||||
updatedAt?: number | null;
|
||||
age: number;
|
||||
model?: string | null;
|
||||
}) => string | null;
|
||||
formatHealthChannelLines: (summary: unknown, opts: { accountMode: "all" }) => string[];
|
||||
formatPluginCompatibilityNotice: (notice: Record<string, unknown>) => string;
|
||||
formatUpdateAvailableHint: (update: Record<string, unknown>) => string | null;
|
||||
resolveMemoryVectorState: (value: unknown) => { state: string; tone: "ok" | "warn" | "muted" };
|
||||
resolveMemoryFtsState: (value: unknown) => { state: string; tone: "ok" | "warn" | "muted" };
|
||||
resolveMemoryCacheSummary: (value: unknown) => { text: string; tone: "ok" | "warn" | "muted" };
|
||||
formatTokensCompact: (value: SessionStatus) => string;
|
||||
formatPromptCacheCompact: (value: SessionStatus) => string | null;
|
||||
formatHealthChannelLines: (summary: HealthSummary, opts: { accountMode: "all" }) => string[];
|
||||
formatPluginCompatibilityNotice: (notice: PluginCompatibilityNotice) => string;
|
||||
formatUpdateAvailableHint: (update: StatusOverviewSurface["update"]) => string | null;
|
||||
resolveMemoryVectorState: (value: NonNullable<MemoryStatusSnapshot["vector"]>) => {
|
||||
state: string;
|
||||
tone: Tone;
|
||||
};
|
||||
resolveMemoryFtsState: (value: NonNullable<MemoryStatusSnapshot["fts"]>) => {
|
||||
state: string;
|
||||
tone: Tone;
|
||||
};
|
||||
resolveMemoryCacheSummary: (value: NonNullable<MemoryStatusSnapshot["cache"]>) => {
|
||||
text: string;
|
||||
tone: Tone;
|
||||
};
|
||||
accentDim: (value: string) => string;
|
||||
updateValue?: string;
|
||||
theme: {
|
||||
@@ -145,11 +94,7 @@ export async function buildStatusCommandReportData(params: {
|
||||
warn: (value: string) => string;
|
||||
error: (value: string) => string;
|
||||
};
|
||||
renderTable: (input: {
|
||||
width: number;
|
||||
columns: Array<Record<string, unknown>>;
|
||||
rows: Array<Record<string, string>>;
|
||||
}) => string;
|
||||
renderTable: (input: RenderTableOptions) => string;
|
||||
}) {
|
||||
const overviewRows = buildStatusCommandOverviewRows({
|
||||
opts: params.opts,
|
||||
@@ -180,7 +125,12 @@ export async function buildStatusCommandReportData(params: {
|
||||
{ key: "Model", header: "Model", minWidth: 14 },
|
||||
{ key: "Tokens", header: "Tokens", minWidth: 16 },
|
||||
...(params.opts.verbose ? [{ key: "Cache", header: "Cache", minWidth: 16, flex: true }] : []),
|
||||
];
|
||||
] satisfies TableColumn[];
|
||||
const securityAudit = params.securityAudit ?? {
|
||||
summary: { critical: 0, warn: 0, info: 0 },
|
||||
findings: [],
|
||||
};
|
||||
|
||||
return {
|
||||
heading: params.theme.heading,
|
||||
muted: params.theme.muted,
|
||||
@@ -202,7 +152,7 @@ export async function buildStatusCommandReportData(params: {
|
||||
formatCliCommand: params.formatCliCommand,
|
||||
}),
|
||||
securityAuditLines: buildStatusSecurityAuditLines({
|
||||
securityAudit: params.securityAudit,
|
||||
securityAudit,
|
||||
theme: params.theme,
|
||||
shortenText: params.shortenText,
|
||||
formatCliCommand: params.formatCliCommand,
|
||||
@@ -237,8 +187,8 @@ export async function buildStatusCommandReportData(params: {
|
||||
healthColumns: params.health ? statusHealthColumns : undefined,
|
||||
healthRows: params.health
|
||||
? buildStatusHealthRows({
|
||||
health: params.health as never,
|
||||
formatHealthChannelLines: params.formatHealthChannelLines as never,
|
||||
health: params.health,
|
||||
formatHealthChannelLines: params.formatHealthChannelLines,
|
||||
ok: params.ok,
|
||||
warn: params.warn,
|
||||
muted: params.muted,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TableColumn } from "../terminal/table.js";
|
||||
import type { RenderTableOptions, TableColumn } from "../terminal/table.js";
|
||||
import {
|
||||
buildStatusChannelsTableSection,
|
||||
buildStatusHealthSection,
|
||||
@@ -12,11 +12,7 @@ import { appendStatusReportSections } from "./status-all/text-report.js";
|
||||
export async function buildStatusCommandReportLines(params: {
|
||||
heading: (text: string) => string;
|
||||
muted: (text: string) => string;
|
||||
renderTable: (input: {
|
||||
width: number;
|
||||
columns: TableColumn[];
|
||||
rows: Array<Record<string, string>>;
|
||||
}) => string;
|
||||
renderTable: (input: RenderTableOptions) => string;
|
||||
width: number;
|
||||
overviewRows: Array<{ Item: string; Value: string }>;
|
||||
showTaskMaintenanceHint: boolean;
|
||||
|
||||
@@ -192,6 +192,7 @@ export async function statusCommand(
|
||||
const {
|
||||
buildStatusUpdateSurface,
|
||||
formatCliCommand,
|
||||
formatHealthChannelLines,
|
||||
formatKTokens,
|
||||
formatPromptCacheCompact,
|
||||
formatPluginCompatibilityNotice,
|
||||
|
||||
@@ -30,6 +30,7 @@ describe("status.gateway-connection", () => {
|
||||
remoteUrlMissing: true,
|
||||
gatewayConnection: {
|
||||
url: "ws://127.0.0.1:18789",
|
||||
urlSource: "local loopback",
|
||||
message: "ignored",
|
||||
},
|
||||
bindMode: "loopback",
|
||||
@@ -49,6 +50,7 @@ describe("status.gateway-connection", () => {
|
||||
remoteUrlMissing: false,
|
||||
gatewayConnection: {
|
||||
url: "ws://127.0.0.1:18789",
|
||||
urlSource: "local loopback",
|
||||
message: "Gateway mode: local",
|
||||
},
|
||||
bindMode: "loopback",
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { StatusScanOverviewResult } from "./status.scan-overview.ts";
|
||||
import type { MemoryStatusSnapshot } from "./status.scan.shared.js";
|
||||
|
||||
const { resolveStatusSummaryFromOverview, resolveMemoryPluginStatus } = vi.hoisted(() => ({
|
||||
resolveStatusSummaryFromOverview: vi.fn(async () => ({ sessions: { count: 1 } })),
|
||||
resolveMemoryPluginStatus: vi.fn(() => ({
|
||||
enabled: false,
|
||||
slot: null,
|
||||
reason: "memorySearch not configured",
|
||||
})),
|
||||
}));
|
||||
@@ -44,8 +47,19 @@ describe("executeStatusScanFromOverview", () => {
|
||||
},
|
||||
agentStatus: { agents: [{ id: "main" }], defaultId: "main" },
|
||||
skipColdStartNetworkChecks: false,
|
||||
} as never;
|
||||
const resolveMemory = vi.fn(async () => ({ agentId: "main" }));
|
||||
} as unknown as StatusScanOverviewResult;
|
||||
const resolveMemory = vi.fn<
|
||||
(args: {
|
||||
cfg: unknown;
|
||||
agentStatus: unknown;
|
||||
memoryPlugin: unknown;
|
||||
runtime?: unknown;
|
||||
}) => Promise<MemoryStatusSnapshot>
|
||||
>(async () => ({
|
||||
agentId: "main",
|
||||
backend: "builtin",
|
||||
provider: "memory-core",
|
||||
}));
|
||||
|
||||
const result = await executeStatusScanFromOverview({
|
||||
overview,
|
||||
@@ -61,7 +75,7 @@ describe("executeStatusScanFromOverview", () => {
|
||||
expect(resolveMemory).toHaveBeenCalledWith({
|
||||
cfg: overview.cfg,
|
||||
agentStatus: overview.agentStatus,
|
||||
memoryPlugin: { enabled: false, reason: "memorySearch not configured" },
|
||||
memoryPlugin: { enabled: false, slot: null, reason: "memorySearch not configured" },
|
||||
runtime: {},
|
||||
});
|
||||
expect(result).toEqual(
|
||||
@@ -76,7 +90,7 @@ describe("executeStatusScanFromOverview", () => {
|
||||
gatewayReachable: true,
|
||||
channels: { rows: [], details: [] },
|
||||
summary: { sessions: { count: 1 } },
|
||||
memory: { agentId: "main" },
|
||||
memory: { agentId: "main", backend: "builtin", provider: "memory-core" },
|
||||
pluginCompatibility: [],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -220,7 +220,14 @@ describe("appendAssistantMessageToSessionTranscript", () => {
|
||||
api: "openai-responses",
|
||||
provider: "openclaw",
|
||||
model: "delivery-mirror",
|
||||
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0 },
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "stop",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
@@ -259,7 +266,14 @@ describe("appendAssistantMessageToSessionTranscript", () => {
|
||||
api: "openai-responses",
|
||||
provider: "openclaw",
|
||||
model: "delivery-mirror",
|
||||
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0 },
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "stop",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
|
||||
@@ -665,7 +665,7 @@ function sanitizeChatHistoryMessage(
|
||||
const sanitizedBlocks = updated.map((item) => item.block);
|
||||
const hasPhaseMetadata = hasAssistantPhaseMetadata(entry);
|
||||
if (hasPhaseMetadata) {
|
||||
const stripped = stripInlineDirectiveTagsForDisplay(extractAssistantHistoryText(entry));
|
||||
const stripped = stripInlineDirectiveTagsForDisplay(extractAssistantHistoryText(entry) ?? "");
|
||||
const res = truncateChatHistoryText(stripped.text, maxChars);
|
||||
const nonTextBlocks = sanitizedBlocks.filter(
|
||||
(block) =>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import {
|
||||
appendAssistantMessageToSessionTranscript,
|
||||
@@ -59,8 +60,8 @@ async function seedSession(params?: { text?: string }) {
|
||||
|
||||
function makeTranscriptAssistantMessage(params: {
|
||||
text: string;
|
||||
content?: Array<Record<string, unknown>>;
|
||||
}) {
|
||||
content?: AssistantMessage["content"];
|
||||
}): AssistantMessage {
|
||||
return {
|
||||
role: "assistant" as const,
|
||||
content: params.content ?? [{ type: "text", text: params.text }],
|
||||
@@ -88,7 +89,7 @@ function makeTranscriptAssistantMessage(params: {
|
||||
|
||||
async function appendTranscriptMessage(params: {
|
||||
sessionKey: string;
|
||||
message: ReturnType<typeof makeTranscriptAssistantMessage>;
|
||||
message: AssistantMessage;
|
||||
emitInlineMessage?: boolean;
|
||||
storePath?: string;
|
||||
}): Promise<string> {
|
||||
|
||||
@@ -46,7 +46,12 @@ describe("security audit install metadata findings", () => {
|
||||
});
|
||||
|
||||
it("evaluates install metadata findings", async () => {
|
||||
const cases = [
|
||||
const cases: Array<{
|
||||
name: string;
|
||||
run: () => Promise<Awaited<ReturnType<typeof runInstallMetadataAudit>>>;
|
||||
expectedPresent?: readonly string[];
|
||||
expectedAbsent?: readonly string[];
|
||||
}> = [
|
||||
{
|
||||
name: "warns on unpinned npm install specs and missing integrity metadata",
|
||||
run: async () =>
|
||||
@@ -165,7 +170,7 @@ describe("security audit install metadata findings", () => {
|
||||
},
|
||||
expectedPresent: ["plugins.installs_version_drift", "hooks.installs_version_drift"],
|
||||
},
|
||||
] as const;
|
||||
];
|
||||
|
||||
for (const testCase of cases) {
|
||||
const res = await testCase.run();
|
||||
|
||||
@@ -74,7 +74,12 @@ describe("security audit node command findings", () => {
|
||||
});
|
||||
|
||||
it("evaluates dangerous gateway.nodes.allowCommands findings", () => {
|
||||
const cases = [
|
||||
const cases: Array<{
|
||||
name: string;
|
||||
cfg: OpenClawConfig;
|
||||
expectedSeverity?: "warn" | "critical";
|
||||
expectedAbsent?: boolean;
|
||||
}> = [
|
||||
{
|
||||
name: "loopback gateway",
|
||||
cfg: {
|
||||
@@ -107,14 +112,14 @@ describe("security audit node command findings", () => {
|
||||
} satisfies OpenClawConfig,
|
||||
expectedAbsent: true,
|
||||
},
|
||||
] as const;
|
||||
];
|
||||
|
||||
for (const testCase of cases) {
|
||||
const findings = collectNodeDangerousAllowCommandFindings(testCase.cfg);
|
||||
const finding = findings.find(
|
||||
(entry) => entry.checkId === "gateway.nodes.allow_commands_dangerous",
|
||||
);
|
||||
if ("expectedAbsent" in testCase && testCase.expectedAbsent) {
|
||||
if (testCase.expectedAbsent) {
|
||||
expect(finding, testCase.name).toBeUndefined();
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user