import assert from "node:assert/strict"; import { afterEach, beforeEach, describe, it } from "node:test"; import { createNotifier, detectNotificationBackend } from "../src/terminal.ts"; import type { NotifyConfig } from "../src/types.ts"; const ESC = "\x1b"; const ST = `${ESC}\\`; const savedEnv = { KITTY_WINDOW_ID: process.env.KITTY_WINDOW_ID, TMUX: process.env.TMUX, }; let originalWrite: typeof process.stdout.write; let output = ""; beforeEach(() => { delete process.env.KITTY_WINDOW_ID; delete process.env.TMUX; }); afterEach(() => { process.stdout.write = originalWrite; output = ""; for (const [key, value] of Object.entries(savedEnv)) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } }); function captureStdout(): void { originalWrite = process.stdout.write; process.stdout.write = ((chunk: string | Uint8Array) => { output += chunk.toString(); return true; }) as typeof process.stdout.write; } function config(title = "Title", body = "Body"): NotifyConfig { return { enabled: true, title, body, durationMs: 5_000, minDurationMs: 3_000, notificationId: "pi-session:unsafe", visibility: "unfocused", action: "focus", }; } function captureSpawn(): { calls: unknown[][]; spawn: typeof import("node:child_process").spawn } { const calls: unknown[][] = []; const spawn = ((...args: unknown[]) => { calls.push(args); return { on() {}, unref() {} }; }) as unknown as typeof import("node:child_process").spawn; return { calls, spawn }; } describe("terminal notifications", () => { it("writes encoded Kitty OSC 99 payloads with exact-window focus metadata", () => { process.env.KITTY_WINDOW_ID = "1"; const notifier = createNotifier(); captureStdout(); notifier.sendNotification(config("π - pi-notify", "done\nnow")); const title = Buffer.from("π - pi-notify", "utf8").toString("base64"); const body = Buffer.from("done\nnow", "utf8").toString("base64"); assert.equal( output, `${ESC}]99;i=pi-session-unsafe:d=0:e=1:o=unfocused:a=focus;${title}${ST}` + `${ESC}]99;i=pi-session-unsafe:p=body:e=1;${body}${ST}`, ); }); it("wraps Kitty OSC 99 sequences for tmux passthrough", () => { process.env.TMUX = "/tmp/tmux"; const notifier = createNotifier({ readTmuxClientInfo: () => "xterm-kitty xterm-kitty" }); captureStdout(); notifier.sendNotification(config("Pi", "done")); const title = Buffer.from("Pi", "utf8").toString("base64"); const body = Buffer.from("done", "utf8").toString("base64"); const titleSequence = `${ESC}]99;i=pi-session-unsafe:d=0:e=1:o=unfocused:a=focus;${title}${ST}`.replaceAll(ESC, ESC + ESC); const bodySequence = `${ESC}]99;i=pi-session-unsafe:p=body:e=1;${body}${ST}`.replaceAll(ESC, ESC + ESC); assert.equal(output, `${ESC}Ptmux;${titleSequence}${ST}${ESC}Ptmux;${bodySequence}${ST}`); }); it("uses AppleScript safely on macOS when Kitty is not detected", () => { const { calls, spawn } = captureSpawn(); const notifier = createNotifier({ spawn, platform: "darwin" }); captureStdout(); notifier.sendNotification(config('Pi "notify"', "done\\now")); assert.equal(output, ""); assert.deepEqual(calls, [ [ "osascript", ["-e", 'display notification "done\\\\now" with title "Pi \\"notify\\""'], { shell: false, detached: true, stdio: "ignore" }, ], ]); }); it("does not let a stale Kitty variable override the active tmux client", () => { process.env.TMUX = "/tmp/tmux"; process.env.KITTY_WINDOW_ID = "stale"; assert.equal( detectNotificationBackend(() => "xterm-ghostty ghostty", "darwin"), "apple-script", ); }); it("uses a sanitized OSC 777 fallback outside macOS", () => { const notifier = createNotifier({ platform: "linux" }); captureStdout(); notifier.sendNotification(config("Pi;bad", "done\u0007now")); assert.equal(output, `${ESC}]777;notify;Pi bad;done now\x07`); }); });