diff --git a/src/infra/machine-name.test.ts b/src/infra/machine-name.test.ts new file mode 100644 index 00000000000..f36efd6ceee --- /dev/null +++ b/src/infra/machine-name.test.ts @@ -0,0 +1,54 @@ +import os from "node:os"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { importFreshModule } from "../../test/helpers/import-fresh.js"; + +const execFileMock = vi.hoisted(() => vi.fn()); + +vi.mock("node:child_process", () => ({ + execFile: (...args: unknown[]) => execFileMock(...args), +})); + +const originalVitest = process.env.VITEST; +const originalNodeEnv = process.env.NODE_ENV; + +async function importMachineName(scope: string) { + return await importFreshModule( + import.meta.url, + `./machine-name.js?scope=${scope}`, + ); +} + +afterEach(() => { + execFileMock.mockReset(); + vi.restoreAllMocks(); + if (originalVitest === undefined) { + delete process.env.VITEST; + } else { + process.env.VITEST = originalVitest; + } + if (originalNodeEnv === undefined) { + delete process.env.NODE_ENV; + } else { + process.env.NODE_ENV = originalNodeEnv; + } +}); + +describe("getMachineDisplayName", () => { + it("uses the hostname fallback in test mode and trims .local", async () => { + const hostnameSpy = vi.spyOn(os, "hostname").mockReturnValue(" clawbox.local "); + const machineName = await importMachineName("test-fallback"); + + await expect(machineName.getMachineDisplayName()).resolves.toBe("clawbox.local"); + await expect(machineName.getMachineDisplayName()).resolves.toBe("clawbox.local"); + expect(hostnameSpy).toHaveBeenCalledTimes(1); + expect(execFileMock).not.toHaveBeenCalled(); + }); + + it("falls back to the default product name when hostname is blank", async () => { + vi.spyOn(os, "hostname").mockReturnValue(" "); + const machineName = await importMachineName("blank-hostname"); + + await expect(machineName.getMachineDisplayName()).resolves.toBe("openclaw"); + expect(execFileMock).not.toHaveBeenCalled(); + }); +}); diff --git a/src/infra/outbound/channel-adapters.test.ts b/src/infra/outbound/channel-adapters.test.ts new file mode 100644 index 00000000000..ee2b5fe6dc8 --- /dev/null +++ b/src/infra/outbound/channel-adapters.test.ts @@ -0,0 +1,48 @@ +import { Separator, TextDisplay } from "@buape/carbon"; +import { describe, expect, it } from "vitest"; +import { DiscordUiContainer } from "../../discord/ui.js"; +import { getChannelMessageAdapter } from "./channel-adapters.js"; + +describe("getChannelMessageAdapter", () => { + it("returns the default adapter for non-discord channels", () => { + expect(getChannelMessageAdapter("telegram")).toEqual({ + supportsComponentsV2: false, + }); + }); + + it("returns the discord adapter with a cross-context component builder", () => { + const adapter = getChannelMessageAdapter("discord"); + + expect(adapter.supportsComponentsV2).toBe(true); + expect(adapter.buildCrossContextComponents).toBeTypeOf("function"); + + const components = adapter.buildCrossContextComponents?.({ + originLabel: "Telegram", + message: "Hello from chat", + cfg: {} as never, + accountId: "primary", + }); + const container = components?.[0] as DiscordUiContainer | undefined; + + expect(components).toHaveLength(1); + expect(container).toBeInstanceOf(DiscordUiContainer); + expect(container?.components).toEqual([ + expect.any(TextDisplay), + expect.any(Separator), + expect.any(TextDisplay), + ]); + }); + + it("omits the message body block when the cross-context message is blank", () => { + const adapter = getChannelMessageAdapter("discord"); + const components = adapter.buildCrossContextComponents?.({ + originLabel: "Signal", + message: " ", + cfg: {} as never, + }); + const container = components?.[0] as DiscordUiContainer | undefined; + + expect(components).toHaveLength(1); + expect(container?.components).toEqual([expect.any(TextDisplay)]); + }); +});