From 341d3e3493dea4f8eca69ef3b0758435cca17f77 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 20:21:43 +0000 Subject: [PATCH] test: add shared helper coverage --- src/shared/device-auth.test.ts | 16 +++++ src/shared/gateway-bind-url.test.ts | 94 +++++++++++++++++++++++++++ src/shared/process-scoped-map.test.ts | 29 +++++++++ src/shared/subagents-format.test.ts | 58 +++++++++++++++++ 4 files changed, 197 insertions(+) create mode 100644 src/shared/device-auth.test.ts create mode 100644 src/shared/gateway-bind-url.test.ts create mode 100644 src/shared/process-scoped-map.test.ts create mode 100644 src/shared/subagents-format.test.ts diff --git a/src/shared/device-auth.test.ts b/src/shared/device-auth.test.ts new file mode 100644 index 00000000000..4675f866e54 --- /dev/null +++ b/src/shared/device-auth.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { normalizeDeviceAuthRole, normalizeDeviceAuthScopes } from "./device-auth.js"; + +describe("shared/device-auth", () => { + it("trims device auth roles without further rewriting", () => { + expect(normalizeDeviceAuthRole(" operator ")).toBe("operator"); + expect(normalizeDeviceAuthRole("")).toBe(""); + }); + + it("dedupes, trims, sorts, and filters auth scopes", () => { + expect( + normalizeDeviceAuthScopes([" node.invoke ", "operator.read", "", "node.invoke", "a.scope"]), + ).toEqual(["a.scope", "node.invoke", "operator.read"]); + expect(normalizeDeviceAuthScopes(undefined)).toEqual([]); + }); +}); diff --git a/src/shared/gateway-bind-url.test.ts b/src/shared/gateway-bind-url.test.ts new file mode 100644 index 00000000000..23dd855c4e6 --- /dev/null +++ b/src/shared/gateway-bind-url.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it, vi } from "vitest"; +import { resolveGatewayBindUrl } from "./gateway-bind-url.js"; + +describe("shared/gateway-bind-url", () => { + it("returns null for loopback/default binds", () => { + expect( + resolveGatewayBindUrl({ + scheme: "ws", + port: 18789, + pickTailnetHost: () => "100.64.0.1", + pickLanHost: () => "192.168.1.2", + }), + ).toBeNull(); + }); + + it("resolves custom binds only when custom host is present after trimming", () => { + expect( + resolveGatewayBindUrl({ + bind: "custom", + customBindHost: " gateway.local ", + scheme: "wss", + port: 443, + pickTailnetHost: vi.fn(), + pickLanHost: vi.fn(), + }), + ).toEqual({ + url: "wss://gateway.local:443", + source: "gateway.bind=custom", + }); + + expect( + resolveGatewayBindUrl({ + bind: "custom", + customBindHost: " ", + scheme: "ws", + port: 18789, + pickTailnetHost: vi.fn(), + pickLanHost: vi.fn(), + }), + ).toEqual({ + error: "gateway.bind=custom requires gateway.customBindHost.", + }); + }); + + it("resolves tailnet and lan binds or returns clear errors", () => { + expect( + resolveGatewayBindUrl({ + bind: "tailnet", + scheme: "ws", + port: 18789, + pickTailnetHost: () => "100.64.0.1", + pickLanHost: vi.fn(), + }), + ).toEqual({ + url: "ws://100.64.0.1:18789", + source: "gateway.bind=tailnet", + }); + expect( + resolveGatewayBindUrl({ + bind: "tailnet", + scheme: "ws", + port: 18789, + pickTailnetHost: () => null, + pickLanHost: vi.fn(), + }), + ).toEqual({ + error: "gateway.bind=tailnet set, but no tailnet IP was found.", + }); + + expect( + resolveGatewayBindUrl({ + bind: "lan", + scheme: "wss", + port: 8443, + pickTailnetHost: vi.fn(), + pickLanHost: () => "192.168.1.2", + }), + ).toEqual({ + url: "wss://192.168.1.2:8443", + source: "gateway.bind=lan", + }); + expect( + resolveGatewayBindUrl({ + bind: "lan", + scheme: "ws", + port: 18789, + pickTailnetHost: vi.fn(), + pickLanHost: () => null, + }), + ).toEqual({ + error: "gateway.bind=lan set, but no private LAN IP was found.", + }); + }); +}); diff --git a/src/shared/process-scoped-map.test.ts b/src/shared/process-scoped-map.test.ts new file mode 100644 index 00000000000..dd4e9d492c8 --- /dev/null +++ b/src/shared/process-scoped-map.test.ts @@ -0,0 +1,29 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { resolveProcessScopedMap } from "./process-scoped-map.js"; + +const MAP_KEY = Symbol("process-scoped-map:test"); +const OTHER_MAP_KEY = Symbol("process-scoped-map:other"); + +afterEach(() => { + delete (process as Record)[MAP_KEY]; + delete (process as Record)[OTHER_MAP_KEY]; +}); + +describe("shared/process-scoped-map", () => { + it("reuses the same map for the same symbol", () => { + const first = resolveProcessScopedMap(MAP_KEY); + first.set("a", 1); + + const second = resolveProcessScopedMap(MAP_KEY); + + expect(second).toBe(first); + expect(second.get("a")).toBe(1); + }); + + it("keeps distinct maps for distinct symbols", () => { + const first = resolveProcessScopedMap(MAP_KEY); + const second = resolveProcessScopedMap(OTHER_MAP_KEY); + + expect(second).not.toBe(first); + }); +}); diff --git a/src/shared/subagents-format.test.ts b/src/shared/subagents-format.test.ts new file mode 100644 index 00000000000..34d1f9a8d5d --- /dev/null +++ b/src/shared/subagents-format.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; +import { + formatDurationCompact, + formatTokenShort, + formatTokenUsageDisplay, + resolveIoTokens, + resolveTotalTokens, + truncateLine, +} from "./subagents-format.js"; + +describe("shared/subagents-format", () => { + it("formats compact durations across minute, hour, and day buckets", () => { + expect(formatDurationCompact()).toBe("n/a"); + expect(formatDurationCompact(30_000)).toBe("1m"); + expect(formatDurationCompact(61 * 60_000)).toBe("1h1m"); + expect(formatDurationCompact(25 * 60 * 60_000)).toBe("1d1h"); + }); + + it("formats token counts with integer, kilo, and million branches", () => { + expect(formatTokenShort()).toBeUndefined(); + expect(formatTokenShort(999.9)).toBe("999"); + expect(formatTokenShort(1_500)).toBe("1.5k"); + expect(formatTokenShort(15_400)).toBe("15k"); + expect(formatTokenShort(1_250_000)).toBe("1.3m"); + }); + + it("truncates lines only when needed", () => { + expect(truncateLine("short", 10)).toBe("short"); + expect(truncateLine("trim me ", 7)).toBe("trim me..."); + }); + + it("resolves token totals and io breakdowns from valid numeric fields only", () => { + expect(resolveTotalTokens()).toBeUndefined(); + expect(resolveTotalTokens({ totalTokens: 42 })).toBe(42); + expect(resolveTotalTokens({ inputTokens: 10, outputTokens: 5 })).toBe(15); + expect(resolveTotalTokens({ inputTokens: Number.NaN, outputTokens: 5 })).toBeUndefined(); + + expect(resolveIoTokens({ inputTokens: 10, outputTokens: 5 })).toEqual({ + input: 10, + output: 5, + total: 15, + }); + expect(resolveIoTokens({ inputTokens: Number.NaN, outputTokens: 0 })).toBeUndefined(); + }); + + it("formats io and prompt-cache usage displays with fallback branches", () => { + expect( + formatTokenUsageDisplay({ + inputTokens: 1_200, + outputTokens: 300, + totalTokens: 2_100, + }), + ).toBe("tokens 1.5k (in 1.2k / out 300), prompt/cache 2.1k"); + + expect(formatTokenUsageDisplay({ totalTokens: 500 })).toBe("tokens 500 prompt/cache"); + expect(formatTokenUsageDisplay({ inputTokens: 0, outputTokens: 0, totalTokens: 0 })).toBe(""); + }); +});