From 7c58de294e269f230add44ace8a1e24815bb1a15 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 19:56:04 +0000 Subject: [PATCH] test: dedupe activity and diagnostic coverage --- src/infra/channel-activity.test.ts | 72 ++++++++++++++++++++++++ src/infra/channels-status-issues.test.ts | 49 ++++++++++++++++ src/infra/diagnostic-flags.test.ts | 65 +++++++++++++++++++++ src/infra/infra-parsing.test.ts | 29 ---------- src/infra/infra-store.test.ts | 51 +---------------- 5 files changed, 187 insertions(+), 79 deletions(-) create mode 100644 src/infra/channel-activity.test.ts create mode 100644 src/infra/channels-status-issues.test.ts create mode 100644 src/infra/diagnostic-flags.test.ts diff --git a/src/infra/channel-activity.test.ts b/src/infra/channel-activity.test.ts new file mode 100644 index 00000000000..17791056f5b --- /dev/null +++ b/src/infra/channel-activity.test.ts @@ -0,0 +1,72 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + getChannelActivity, + recordChannelActivity, + resetChannelActivityForTest, +} from "./channel-activity.js"; + +describe("channel activity", () => { + beforeEach(() => { + resetChannelActivityForTest(); + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-01-08T00:00:00Z")); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("uses the default account for blank inputs and falls back to null timestamps", () => { + expect(getChannelActivity({ channel: "telegram" })).toEqual({ + inboundAt: null, + outboundAt: null, + }); + + recordChannelActivity({ + channel: "telegram", + accountId: " ", + direction: "inbound", + }); + + expect(getChannelActivity({ channel: "telegram", accountId: null })).toEqual({ + inboundAt: 1767830400000, + outboundAt: null, + }); + }); + + it("keeps inbound and outbound timestamps independent and trims account ids", () => { + recordChannelActivity({ + channel: "whatsapp", + accountId: " team-a ", + direction: "inbound", + at: 10, + }); + recordChannelActivity({ + channel: "whatsapp", + accountId: "team-a", + direction: "outbound", + at: 20, + }); + recordChannelActivity({ + channel: "whatsapp", + accountId: "team-a", + direction: "inbound", + at: 30, + }); + + expect(getChannelActivity({ channel: "whatsapp", accountId: " team-a " })).toEqual({ + inboundAt: 30, + outboundAt: 20, + }); + }); + + it("reset clears previously recorded activity", () => { + recordChannelActivity({ channel: "line", direction: "outbound", at: 7 }); + resetChannelActivityForTest(); + + expect(getChannelActivity({ channel: "line" })).toEqual({ + inboundAt: null, + outboundAt: null, + }); + }); +}); diff --git a/src/infra/channels-status-issues.test.ts b/src/infra/channels-status-issues.test.ts new file mode 100644 index 00000000000..92b4008707c --- /dev/null +++ b/src/infra/channels-status-issues.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it, vi } from "vitest"; + +const listChannelPluginsMock = vi.hoisted(() => vi.fn()); + +vi.mock("../channels/plugins/index.js", () => ({ + listChannelPlugins: () => listChannelPluginsMock(), +})); + +import { collectChannelStatusIssues } from "./channels-status-issues.js"; + +describe("collectChannelStatusIssues", () => { + it("returns no issues when payload accounts are missing or not arrays", () => { + const collectTelegramIssues = vi.fn(() => [{ code: "telegram" }]); + listChannelPluginsMock.mockReturnValue([ + { id: "telegram", status: { collectStatusIssues: collectTelegramIssues } }, + ]); + + expect(collectChannelStatusIssues({})).toEqual([]); + expect(collectChannelStatusIssues({ channelAccounts: { telegram: { bad: true } } })).toEqual( + [], + ); + expect(collectTelegramIssues).not.toHaveBeenCalled(); + }); + + it("skips plugins without collectors and concatenates collector output in plugin order", () => { + const collectTelegramIssues = vi.fn(() => [{ code: "telegram.down" }]); + const collectSlackIssues = vi.fn(() => [{ code: "slack.warn" }, { code: "slack.auth" }]); + const telegramAccounts = [{ id: "tg-1" }]; + const slackAccounts = [{ id: "sl-1" }]; + listChannelPluginsMock.mockReturnValueOnce([ + { id: "discord" }, + { id: "telegram", status: { collectStatusIssues: collectTelegramIssues } }, + { id: "slack", status: { collectStatusIssues: collectSlackIssues } }, + ]); + + expect( + collectChannelStatusIssues({ + channelAccounts: { + discord: [{ id: "dc-1" }], + telegram: telegramAccounts, + slack: slackAccounts, + }, + }), + ).toEqual([{ code: "telegram.down" }, { code: "slack.warn" }, { code: "slack.auth" }]); + + expect(collectTelegramIssues).toHaveBeenCalledWith(telegramAccounts); + expect(collectSlackIssues).toHaveBeenCalledWith(slackAccounts); + }); +}); diff --git a/src/infra/diagnostic-flags.test.ts b/src/infra/diagnostic-flags.test.ts new file mode 100644 index 00000000000..7c4c3b0a62d --- /dev/null +++ b/src/infra/diagnostic-flags.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import { + isDiagnosticFlagEnabled, + matchesDiagnosticFlag, + resolveDiagnosticFlags, +} from "./diagnostic-flags.js"; + +describe("resolveDiagnosticFlags", () => { + it("normalizes and dedupes config and env flags", () => { + const cfg = { + diagnostics: { flags: [" Telegram.Http ", "cache.*", "CACHE.*"] }, + } as OpenClawConfig; + const env = { + OPENCLAW_DIAGNOSTICS: " foo, Cache.* telegram.http ", + } as NodeJS.ProcessEnv; + + expect(resolveDiagnosticFlags(cfg, env)).toEqual(["telegram.http", "cache.*", "foo"]); + }); + + it("treats false-like env values as no extra flags", () => { + const cfg = { + diagnostics: { flags: ["telegram.http"] }, + } as OpenClawConfig; + + for (const raw of ["0", "false", "off", "none", " "]) { + expect( + resolveDiagnosticFlags(cfg, { + OPENCLAW_DIAGNOSTICS: raw, + } as NodeJS.ProcessEnv), + ).toEqual(["telegram.http"]); + } + }); +}); + +describe("matchesDiagnosticFlag", () => { + it("matches exact, namespace, prefix, and wildcard rules", () => { + expect(matchesDiagnosticFlag("telegram.http", ["telegram.http"])).toBe(true); + expect(matchesDiagnosticFlag("cache", ["cache.*"])).toBe(true); + expect(matchesDiagnosticFlag("cache.hit", ["cache.*"])).toBe(true); + expect(matchesDiagnosticFlag("tool.exec.fast", ["tool.exec*"])).toBe(true); + expect(matchesDiagnosticFlag("anything", ["all"])).toBe(true); + expect(matchesDiagnosticFlag("anything", ["*"])).toBe(true); + }); + + it("rejects blank and non-matching flags", () => { + expect(matchesDiagnosticFlag(" ", ["*"])).toBe(false); + expect(matchesDiagnosticFlag("cache.hit", ["cache.miss", "tool.*"])).toBe(false); + }); +}); + +describe("isDiagnosticFlagEnabled", () => { + it("resolves config and env together before matching", () => { + const cfg = { + diagnostics: { flags: ["gateway.*"] }, + } as OpenClawConfig; + const env = { + OPENCLAW_DIAGNOSTICS: "telegram.http", + } as NodeJS.ProcessEnv; + + expect(isDiagnosticFlagEnabled("gateway.ws", cfg, env)).toBe(true); + expect(isDiagnosticFlagEnabled("telegram.http", cfg, env)).toBe(true); + expect(isDiagnosticFlagEnabled("slack.http", cfg, env)).toBe(false); + }); +}); diff --git a/src/infra/infra-parsing.test.ts b/src/infra/infra-parsing.test.ts index 10590c96790..8bded6fef27 100644 --- a/src/infra/infra-parsing.test.ts +++ b/src/infra/infra-parsing.test.ts @@ -1,38 +1,9 @@ import { describe, expect, it } from "vitest"; -import type { OpenClawConfig } from "../config/config.js"; -import { isDiagnosticFlagEnabled, resolveDiagnosticFlags } from "./diagnostic-flags.js"; import { isMainModule } from "./is-main.js"; import { buildNodeShellCommand } from "./node-shell.js"; import { parseSshTarget } from "./ssh-tunnel.js"; describe("infra parsing", () => { - describe("diagnostic flags", () => { - it("merges config + env flags", () => { - const cfg = { - diagnostics: { flags: ["telegram.http", "cache.*"] }, - } as OpenClawConfig; - const env = { - OPENCLAW_DIAGNOSTICS: "foo,bar", - } as NodeJS.ProcessEnv; - - const flags = resolveDiagnosticFlags(cfg, env); - expect(flags).toEqual(expect.arrayContaining(["telegram.http", "cache.*", "foo", "bar"])); - expect(isDiagnosticFlagEnabled("telegram.http", cfg, env)).toBe(true); - expect(isDiagnosticFlagEnabled("cache.hit", cfg, env)).toBe(true); - expect(isDiagnosticFlagEnabled("foo", cfg, env)).toBe(true); - }); - - it("treats env true as wildcard", () => { - const env = { OPENCLAW_DIAGNOSTICS: "1" } as NodeJS.ProcessEnv; - expect(isDiagnosticFlagEnabled("anything.here", undefined, env)).toBe(true); - }); - - it("treats env false as disabled", () => { - const env = { OPENCLAW_DIAGNOSTICS: "0" } as NodeJS.ProcessEnv; - expect(isDiagnosticFlagEnabled("telegram.http", undefined, env)).toBe(false); - }); - }); - describe("isMainModule", () => { it("returns true when argv[1] matches current file", () => { expect( diff --git a/src/infra/infra-store.test.ts b/src/infra/infra-store.test.ts index 1f65b005652..3faefcf7041 100644 --- a/src/infra/infra-store.test.ts +++ b/src/infra/infra-store.test.ts @@ -1,12 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import { withTempDir } from "../test-utils/temp-dir.js"; -import { - getChannelActivity, - recordChannelActivity, - resetChannelActivityForTest, -} from "./channel-activity.js"; import { createDedupeCache } from "./dedupe.js"; import { emitDiagnosticEvent, @@ -145,50 +140,6 @@ describe("infra store", () => { }); }); - describe("channel activity", () => { - beforeEach(() => { - resetChannelActivityForTest(); - vi.useFakeTimers(); - vi.setSystemTime(new Date("2026-01-08T00:00:00Z")); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - it("records inbound/outbound separately", () => { - recordChannelActivity({ channel: "telegram", direction: "inbound" }); - vi.advanceTimersByTime(1000); - recordChannelActivity({ channel: "telegram", direction: "outbound" }); - const res = getChannelActivity({ channel: "telegram" }); - expect(res.inboundAt).toBe(1767830400000); - expect(res.outboundAt).toBe(1767830401000); - }); - - it("isolates accounts", () => { - recordChannelActivity({ - channel: "whatsapp", - accountId: "a", - direction: "inbound", - at: 1, - }); - recordChannelActivity({ - channel: "whatsapp", - accountId: "b", - direction: "inbound", - at: 2, - }); - expect(getChannelActivity({ channel: "whatsapp", accountId: "a" })).toEqual({ - inboundAt: 1, - outboundAt: null, - }); - expect(getChannelActivity({ channel: "whatsapp", accountId: "b" })).toEqual({ - inboundAt: 2, - outboundAt: null, - }); - }); - }); - describe("createDedupeCache", () => { it("marks duplicates within TTL", () => { const cache = createDedupeCache({ ttlMs: 1000, maxSize: 10 });