From 5ca780fa78682b60f953339eebe8693e5db3763c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 12 Mar 2026 02:55:25 +0000 Subject: [PATCH] feat: expose runtime version in gateway status --- src/commands/status.summary.redaction.test.ts | 2 + src/commands/status.summary.test.ts | 85 +++++++++++++++++++ src/commands/status.summary.ts | 2 + src/commands/status.types.ts | 1 + src/tui/tui-status-summary.ts | 3 + src/tui/tui-types.ts | 1 + 6 files changed, 94 insertions(+) create mode 100644 src/commands/status.summary.test.ts diff --git a/src/commands/status.summary.redaction.test.ts b/src/commands/status.summary.redaction.test.ts index 02eaecbcb35..26e28887560 100644 --- a/src/commands/status.summary.redaction.test.ts +++ b/src/commands/status.summary.redaction.test.ts @@ -22,6 +22,7 @@ function createRecentSessionRow() { describe("redactSensitiveStatusSummary", () => { it("removes sensitive session and path details while preserving summary structure", () => { const input: StatusSummary = { + runtimeVersion: "2026.3.8", heartbeat: { defaultAgentId: "main", agents: [{ agentId: "main", enabled: true, every: "5m", everyMs: 300_000 }], @@ -50,6 +51,7 @@ describe("redactSensitiveStatusSummary", () => { expect(redacted.sessions.recent).toEqual([]); expect(redacted.sessions.byAgent[0]?.path).toBe("[redacted]"); expect(redacted.sessions.byAgent[0]?.recent).toEqual([]); + expect(redacted.runtimeVersion).toBe("2026.3.8"); expect(redacted.heartbeat).toEqual(input.heartbeat); expect(redacted.channelSummary).toEqual(input.channelSummary); }); diff --git a/src/commands/status.summary.test.ts b/src/commands/status.summary.test.ts new file mode 100644 index 00000000000..addda823a23 --- /dev/null +++ b/src/commands/status.summary.test.ts @@ -0,0 +1,85 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../agents/context.js", () => ({ + resolveContextTokensForModel: vi.fn(() => 200_000), +})); + +vi.mock("../agents/defaults.js", () => ({ + DEFAULT_CONTEXT_TOKENS: 200_000, + DEFAULT_MODEL: "gpt-5.2", + DEFAULT_PROVIDER: "openai", +})); + +vi.mock("../agents/model-selection.js", () => ({ + resolveConfiguredModelRef: vi.fn(() => ({ + provider: "openai", + model: "gpt-5.2", + })), +})); + +vi.mock("../config/config.js", () => ({ + loadConfig: vi.fn(() => ({})), +})); + +vi.mock("../config/sessions.js", () => ({ + loadSessionStore: vi.fn(() => ({})), + resolveFreshSessionTotalTokens: vi.fn(() => undefined), + resolveMainSessionKey: vi.fn(() => "main"), + resolveStorePath: vi.fn(() => "/tmp/sessions.json"), +})); + +vi.mock("../gateway/session-utils.js", () => ({ + classifySessionKey: vi.fn(() => "direct"), + listAgentsForGateway: vi.fn(() => ({ + defaultId: "main", + agents: [{ id: "main" }], + })), + resolveSessionModelRef: vi.fn(() => ({ + provider: "openai", + model: "gpt-5.2", + })), +})); + +vi.mock("../infra/channel-summary.js", () => ({ + buildChannelSummary: vi.fn(async () => ["ok"]), +})); + +vi.mock("../infra/heartbeat-runner.js", () => ({ + resolveHeartbeatSummaryForAgent: vi.fn(() => ({ + enabled: true, + every: "5m", + everyMs: 300_000, + })), +})); + +vi.mock("../infra/system-events.js", () => ({ + peekSystemEvents: vi.fn(() => []), +})); + +vi.mock("../routing/session-key.js", () => ({ + parseAgentSessionKey: vi.fn(() => null), +})); + +vi.mock("../version.js", () => ({ + resolveRuntimeServiceVersion: vi.fn(() => "2026.3.8"), +})); + +vi.mock("./status.link-channel.js", () => ({ + resolveLinkChannelContext: vi.fn(async () => undefined), +})); + +describe("getStatusSummary", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("includes runtimeVersion in the status payload", async () => { + const { getStatusSummary } = await import("./status.summary.js"); + + const summary = await getStatusSummary(); + + expect(summary.runtimeVersion).toBe("2026.3.8"); + expect(summary.heartbeat.defaultAgentId).toBe("main"); + expect(summary.channelSummary).toEqual(["ok"]); + }); +}); diff --git a/src/commands/status.summary.ts b/src/commands/status.summary.ts index 3a71464973f..79a51f0d9d3 100644 --- a/src/commands/status.summary.ts +++ b/src/commands/status.summary.ts @@ -19,6 +19,7 @@ import { buildChannelSummary } from "../infra/channel-summary.js"; import { resolveHeartbeatSummaryForAgent } from "../infra/heartbeat-runner.js"; import { peekSystemEvents } from "../infra/system-events.js"; import { parseAgentSessionKey } from "../routing/session-key.js"; +import { resolveRuntimeServiceVersion } from "../version.js"; import { resolveLinkChannelContext } from "./status.link-channel.js"; import type { HeartbeatStatus, SessionStatus, StatusSummary } from "./status.types.js"; @@ -210,6 +211,7 @@ export async function getStatusSummary( const totalSessions = allSessions.length; const summary: StatusSummary = { + runtimeVersion: resolveRuntimeServiceVersion(process.env), linkChannel: linkContext ? { id: linkContext.plugin.id, diff --git a/src/commands/status.types.ts b/src/commands/status.types.ts index a3e0a5ca8e2..ec157b3488a 100644 --- a/src/commands/status.types.ts +++ b/src/commands/status.types.ts @@ -34,6 +34,7 @@ export type HeartbeatStatus = { }; export type StatusSummary = { + runtimeVersion?: string | null; linkChannel?: { id: ChannelId; label: string; diff --git a/src/tui/tui-status-summary.ts b/src/tui/tui-status-summary.ts index 64fc00adad6..dcbcd00329d 100644 --- a/src/tui/tui-status-summary.ts +++ b/src/tui/tui-status-summary.ts @@ -6,6 +6,9 @@ import type { GatewayStatusSummary } from "./tui-types.js"; export function formatStatusSummary(summary: GatewayStatusSummary) { const lines: string[] = []; lines.push("Gateway status"); + if (summary.runtimeVersion) { + lines.push(`Version: ${summary.runtimeVersion}`); + } if (!summary.linkChannel) { lines.push("Link channel: unknown"); diff --git a/src/tui/tui-types.ts b/src/tui/tui-types.ts index 087d7958950..e0af351d462 100644 --- a/src/tui/tui-types.ts +++ b/src/tui/tui-types.ts @@ -49,6 +49,7 @@ export type AgentSummary = { }; export type GatewayStatusSummary = { + runtimeVersion?: string | null; linkChannel?: { id?: string; label?: string;