test(integration): make desktop notifier platform mock hermetic

This commit is contained in:
itrytoohard 2026-06-08 02:54:08 +05:30
parent 62297ed174
commit 02a80b6c7f
1 changed files with 26 additions and 2 deletions

View File

@ -4,7 +4,7 @@
* Mocks ONLY the I/O boundary: node:child_process and node:os. * Mocks ONLY the I/O boundary: node:child_process and node:os.
* Everything else runs for real: config parsing, escaping chains, formatting. * Everything else runs for real: config parsing, escaping chains, formatting.
*/ */
import { describe, it, expect, vi, beforeEach, type Mock } from "vitest"; import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from "vitest";
import type { NotifyAction } from "@aoagents/ao-core"; import type { NotifyAction } from "@aoagents/ao-core";
import { makeEvent } from "./helpers/event-factory.js"; import { makeEvent } from "./helpers/event-factory.js";
@ -24,11 +24,17 @@ vi.mock("node:os", () => ({
platform: vi.fn(() => "darwin"), platform: vi.fn(() => "darwin"),
})); }));
import { execFile } from "node:child_process"; import { execFile, execFileSync } from "node:child_process";
import { platform } from "node:os"; import { platform } from "node:os";
const mockExecFile = execFile as unknown as Mock; const mockExecFile = execFile as unknown as Mock;
const mockExecFileSync = execFileSync as unknown as Mock;
const mockPlatform = platform as unknown as Mock; const mockPlatform = platform as unknown as Mock;
const originalProcessPlatform = Object.getOwnPropertyDescriptor(process, "platform");
function setProcessPlatform(value: NodeJS.Platform): void {
Object.defineProperty(process, "platform", { value, configurable: true });
}
// Import the full plugin module — config parsing, escaping, formatting all run for real // Import the full plugin module — config parsing, escaping, formatting all run for real
import desktopPlugin from "@aoagents/ao-plugin-notifier-desktop"; import desktopPlugin from "@aoagents/ao-plugin-notifier-desktop";
@ -37,6 +43,12 @@ describe("notifier-desktop integration", () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
mockPlatform.mockReturnValue("darwin"); mockPlatform.mockReturnValue("darwin");
setProcessPlatform("darwin");
mockExecFileSync.mockImplementation(() => {
const error = new Error("not found") as NodeJS.ErrnoException;
error.code = "ENOENT";
throw error;
});
mockExecFile.mockImplementation((..._args: unknown[]) => { mockExecFile.mockImplementation((..._args: unknown[]) => {
// execFile is called as (cmd, args, cb) on darwin/linux and as // execFile is called as (cmd, args, cb) on darwin/linux and as
// (cmd, args, opts, cb) on win32 — pick whichever trailing arg is the // (cmd, args, opts, cb) on win32 — pick whichever trailing arg is the
@ -48,6 +60,12 @@ describe("notifier-desktop integration", () => {
}); });
}); });
afterEach(() => {
if (originalProcessPlatform) {
Object.defineProperty(process, "platform", originalProcessPlatform);
}
});
describe("config -> behavior flow", () => { describe("config -> behavior flow", () => {
it("sound=false config + urgent event -> no sound clause in osascript", async () => { it("sound=false config + urgent event -> no sound clause in osascript", async () => {
const notifier = desktopPlugin.create({ sound: false }); const notifier = desktopPlugin.create({ sound: false });
@ -124,6 +142,7 @@ describe("notifier-desktop integration", () => {
it("linux -> notify-send with title and message as separate args", async () => { it("linux -> notify-send with title and message as separate args", async () => {
mockPlatform.mockReturnValue("linux"); mockPlatform.mockReturnValue("linux");
setProcessPlatform("linux");
const notifier = desktopPlugin.create(); const notifier = desktopPlugin.create();
await notifier.notify(makeEvent({ sessionId: "backend-1", message: "Test msg" })); await notifier.notify(makeEvent({ sessionId: "backend-1", message: "Test msg" }));
@ -135,6 +154,7 @@ describe("notifier-desktop integration", () => {
it("linux + urgent -> --urgency=critical before title", async () => { it("linux + urgent -> --urgency=critical before title", async () => {
mockPlatform.mockReturnValue("linux"); mockPlatform.mockReturnValue("linux");
setProcessPlatform("linux");
const notifier = desktopPlugin.create(); const notifier = desktopPlugin.create();
await notifier.notify(makeEvent({ priority: "urgent" })); await notifier.notify(makeEvent({ priority: "urgent" }));
@ -144,6 +164,7 @@ describe("notifier-desktop integration", () => {
it("linux + info -> no --urgency flag", async () => { it("linux + info -> no --urgency flag", async () => {
mockPlatform.mockReturnValue("linux"); mockPlatform.mockReturnValue("linux");
setProcessPlatform("linux");
const notifier = desktopPlugin.create(); const notifier = desktopPlugin.create();
await notifier.notify(makeEvent({ priority: "info" })); await notifier.notify(makeEvent({ priority: "info" }));
@ -153,6 +174,7 @@ describe("notifier-desktop integration", () => {
it("win32 -> powershell.exe with EncodedCommand toast XML", async () => { it("win32 -> powershell.exe with EncodedCommand toast XML", async () => {
mockPlatform.mockReturnValue("win32"); mockPlatform.mockReturnValue("win32");
setProcessPlatform("win32");
const notifier = desktopPlugin.create(); const notifier = desktopPlugin.create();
await notifier.notify(makeEvent({ message: "ci test" })); await notifier.notify(makeEvent({ message: "ci test" }));
@ -172,6 +194,7 @@ describe("notifier-desktop integration", () => {
it("unsupported platform -> no execFile, warns", async () => { it("unsupported platform -> no execFile, warns", async () => {
mockPlatform.mockReturnValue("freebsd"); mockPlatform.mockReturnValue("freebsd");
setProcessPlatform("freebsd");
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const notifier = desktopPlugin.create(); const notifier = desktopPlugin.create();
await notifier.notify(makeEvent()); await notifier.notify(makeEvent());
@ -222,6 +245,7 @@ describe("notifier-desktop integration", () => {
it("rejects when execFile fails on linux", async () => { it("rejects when execFile fails on linux", async () => {
mockPlatform.mockReturnValue("linux"); mockPlatform.mockReturnValue("linux");
setProcessPlatform("linux");
mockExecFile.mockImplementation( mockExecFile.mockImplementation(
(_cmd: string, _args: string[], cb: (err: Error | null) => void) => { (_cmd: string, _args: string[], cb: (err: Error | null) => void) => {
cb(new Error("notify-send: not found")); cb(new Error("notify-send: not found"));