fix(check): repair status report typing drift

This commit is contained in:
Vincent Koc
2026-04-06 13:25:26 +01:00
parent 7e0e2f81e5
commit 5fa166ed11
24 changed files with 210 additions and 262 deletions

View File

@@ -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> {

View File

@@ -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);
}
}

View File

@@ -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)", () => {

View File

@@ -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…");

View File

@@ -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;

View File

@@ -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[] {

View File

@@ -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;

View File

@@ -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,

View File

@@ -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,

View File

@@ -75,7 +75,7 @@ export async function resolveStatusJsonOutput(params: {
return buildStatusJsonPayload({
summary: scan.summary,
surface: buildStatusOverviewSurfaceFromScan({
scan,
scan: scan as never,
gatewayService,
nodeService,
}),

View File

@@ -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) },

View File

@@ -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,

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,

View File

@@ -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;

View File

@@ -192,6 +192,7 @@ export async function statusCommand(
const {
buildStatusUpdateSurface,
formatCliCommand,
formatHealthChannelLines,
formatKTokens,
formatPromptCacheCompact,
formatPluginCompatibilityNotice,

View File

@@ -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",

View File

@@ -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: [],
}),
);

View File

@@ -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(),
},

View File

@@ -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) =>

View File

@@ -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> {

View File

@@ -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();

View File

@@ -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;
}