From ed080ae988ca142162f151d57060ec7890903bb4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 2 Dec 2025 10:56:10 +0000 Subject: [PATCH] Tests: cover agents and fix web defaults Co-authored-by: RealSid08 --- src/agents/agents.test.ts | 118 +++++++++++++++++++++++++++ src/agents/claude.ts | 26 +++--- src/agents/codex.ts | 23 ++++-- src/agents/index.ts | 1 - src/agents/opencode.ts | 34 +++++--- src/agents/pi.ts | 20 +++-- src/agents/types.ts | 1 - src/auto-reply/command-reply.test.ts | 26 +++++- src/auto-reply/command-reply.ts | 21 +++-- src/auto-reply/opencode.ts | 1 - src/commands/send.test.ts | 4 + src/commands/send.ts | 10 ++- src/web/auto-reply.test.ts | 21 ++--- src/web/auto-reply.ts | 36 +++++--- src/web/inbound.ts | 9 +- src/web/ipc.ts | 10 +-- src/web/monitor-inbox.test.ts | 6 +- 17 files changed, 285 insertions(+), 82 deletions(-) create mode 100644 src/agents/agents.test.ts diff --git a/src/agents/agents.test.ts b/src/agents/agents.test.ts new file mode 100644 index 000000000..06e64037e --- /dev/null +++ b/src/agents/agents.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, it } from "vitest"; + +import { CLAUDE_IDENTITY_PREFIX } from "../auto-reply/claude.js"; +import { OPENCODE_IDENTITY_PREFIX } from "../auto-reply/opencode.js"; +import { claudeSpec } from "./claude.js"; +import { codexSpec } from "./codex.js"; +import { opencodeSpec } from "./opencode.js"; +import { piSpec } from "./pi.js"; + +describe("agent buildArgs + parseOutput helpers", () => { + it("claudeSpec injects flags and identity once", () => { + const argv = ["claude", "hi"]; + const built = claudeSpec.buildArgs({ + argv, + bodyIndex: 1, + isNewSession: true, + sessionId: "sess", + sendSystemOnce: false, + systemSent: false, + identityPrefix: undefined, + format: "json", + }); + expect(built).toContain("--output-format"); + expect(built).toContain("json"); + expect(built).toContain("-p"); + expect(built.at(-1)).toContain(CLAUDE_IDENTITY_PREFIX); + + const builtNoIdentity = claudeSpec.buildArgs({ + argv, + bodyIndex: 1, + isNewSession: false, + sessionId: "sess", + sendSystemOnce: true, + systemSent: true, + identityPrefix: undefined, + format: "json", + }); + expect(builtNoIdentity.at(-1)).not.toContain(CLAUDE_IDENTITY_PREFIX); + }); + + it("opencodeSpec adds format flag and identity prefix when needed", () => { + const argv = ["opencode", "body"]; + const built = opencodeSpec.buildArgs({ + argv, + bodyIndex: 1, + isNewSession: true, + sessionId: "sess", + sendSystemOnce: false, + systemSent: false, + identityPrefix: undefined, + format: "json", + }); + expect(built).toContain("--format"); + expect(built).toContain("json"); + expect(built.at(-1)).toContain(OPENCODE_IDENTITY_PREFIX); + }); + + it("piSpec parses final assistant message and preserves usage meta", () => { + const stdout = [ + '{"type":"message_start","message":{"role":"assistant"}}', + '{"type":"message_end","message":{"role":"assistant","content":[{"type":"text","text":"hello world"}],"usage":{"input":10,"output":5},"model":"pi-1","provider":"inflection","stopReason":"end"}}', + ].join("\n"); + const parsed = piSpec.parseOutput(stdout); + expect(parsed.text).toBe("hello world"); + expect(parsed.meta?.provider).toBe("inflection"); + expect((parsed.meta?.usage as { output?: number })?.output).toBe(5); + }); + + it("codexSpec parses agent_message and aggregates usage", () => { + const stdout = [ + '{"type":"item.completed","item":{"type":"agent_message","text":"hi there"}}', + '{"type":"turn.completed","usage":{"input_tokens":50,"output_tokens":10,"cached_input_tokens":5}}', + ].join("\n"); + const parsed = codexSpec.parseOutput(stdout); + expect(parsed.text).toBe("hi there"); + const usage = parsed.meta?.usage as { + input?: number; + output?: number; + cacheRead?: number; + total?: number; + }; + expect(usage?.input).toBe(50); + expect(usage?.output).toBe(10); + expect(usage?.cacheRead).toBe(5); + expect(usage?.total).toBe(65); + }); + + it("opencodeSpec parses streamed events and summarizes meta", () => { + const stdout = [ + '{"type":"step_start","timestamp":0}', + '{"type":"text","part":{"text":"hi"}}', + '{"type":"step_finish","timestamp":1200,"part":{"cost":0.002,"tokens":{"input":100,"output":20}}}', + ].join("\n"); + const parsed = opencodeSpec.parseOutput(stdout); + expect(parsed.text).toBe("hi"); + expect(parsed.meta?.extra?.summary).toContain("duration=1200ms"); + expect(parsed.meta?.extra?.summary).toContain("cost=$0.0020"); + expect(parsed.meta?.extra?.summary).toContain("tokens=100+20"); + }); + + it("codexSpec buildArgs enforces exec/json/sandbox defaults", () => { + const argv = ["codex", "hello world"]; + const built = codexSpec.buildArgs({ + argv, + bodyIndex: 1, + isNewSession: true, + sessionId: "sess", + sendSystemOnce: false, + systemSent: false, + identityPrefix: undefined, + format: "json", + }); + expect(built[1]).toBe("exec"); + expect(built).toContain("--json"); + expect(built).toContain("--skip-git-repo-check"); + expect(built).toContain("read-only"); + }); +}); diff --git a/src/agents/claude.ts b/src/agents/claude.ts index 80cd767bb..261dfe4ce 100644 --- a/src/agents/claude.ts +++ b/src/agents/claude.ts @@ -3,16 +3,11 @@ import path from "node:path"; import { CLAUDE_BIN, CLAUDE_IDENTITY_PREFIX, + type ClaudeJsonParseResult, parseClaudeJson, summarizeClaudeMetadata, - type ClaudeJsonParseResult, } from "../auto-reply/claude.js"; -import type { - AgentMeta, - AgentParseResult, - AgentSpec, - BuildArgsContext, -} from "./types.js"; +import type { AgentMeta, AgentSpec } from "./types.js"; function toMeta(parsed?: ClaudeJsonParseResult): AgentMeta | undefined { if (!parsed?.parsed) return undefined; @@ -22,10 +17,11 @@ function toMeta(parsed?: ClaudeJsonParseResult): AgentMeta | undefined { export const claudeSpec: AgentSpec = { kind: "claude", - isInvocation: (argv) => argv.length > 0 && path.basename(argv[0]) === CLAUDE_BIN, + isInvocation: (argv) => + argv.length > 0 && path.basename(argv[0]) === CLAUDE_BIN, buildArgs: (ctx) => { - // Work off a split of "before body" and "after body" so we don't lose the - // body index when inserting flags. + // Split around the body so we can inject flags without losing the body + // position. This keeps templated prompts intact even when we add flags. const argv = [...ctx.argv]; const body = argv[ctx.bodyIndex] ?? ""; const beforeBody = argv.slice(0, ctx.bodyIndex); @@ -34,14 +30,18 @@ export const claudeSpec: AgentSpec = { const wantsOutputFormat = typeof ctx.format === "string"; if (wantsOutputFormat) { const hasOutputFormat = argv.some( - (part) => part === "--output-format" || part.startsWith("--output-format="), + (part) => + part === "--output-format" || part.startsWith("--output-format="), ); if (!hasOutputFormat) { - beforeBody.push("--output-format", ctx.format!); + const outputFormat = ctx.format ?? "json"; + beforeBody.push("--output-format", outputFormat); } } - const hasPrintFlag = argv.some((part) => part === "-p" || part === "--print"); + const hasPrintFlag = argv.some( + (part) => part === "-p" || part === "--print", + ); if (!hasPrintFlag) { beforeBody.push("-p"); } diff --git a/src/agents/codex.ts b/src/agents/codex.ts index 3b2066d05..da1cd29a2 100644 --- a/src/agents/codex.ts +++ b/src/agents/codex.ts @@ -1,6 +1,6 @@ import path from "node:path"; -import type { AgentMeta, AgentParseResult, AgentSpec, BuildArgsContext } from "./types.js"; +import type { AgentMeta, AgentParseResult, AgentSpec } from "./types.js"; function parseCodexJson(raw: string): AgentParseResult { const lines = raw.split(/\n+/).filter((l) => l.trim().startsWith("{")); @@ -9,11 +9,25 @@ function parseCodexJson(raw: string): AgentParseResult { for (const line of lines) { try { - const ev = JSON.parse(line) as { type?: string; item?: { type?: string; text?: string }; usage?: unknown }; - if (ev.type === "item.completed" && ev.item?.type === "agent_message" && typeof ev.item.text === "string") { + const ev = JSON.parse(line) as { + type?: string; + item?: { type?: string; text?: string }; + usage?: unknown; + }; + // Codex streams multiple events; capture the last agent_message text and + // the final turn usage for cost/telemetry. + if ( + ev.type === "item.completed" && + ev.item?.type === "agent_message" && + typeof ev.item.text === "string" + ) { text = ev.item.text; } - if (ev.type === "turn.completed" && ev.usage && typeof ev.usage === "object") { + if ( + ev.type === "turn.completed" && + ev.usage && + typeof ev.usage === "object" + ) { const u = ev.usage as { input_tokens?: number; cached_input_tokens?: number; @@ -63,4 +77,3 @@ export const codexSpec: AgentSpec = { }, parseOutput: parseCodexJson, }; - diff --git a/src/agents/index.ts b/src/agents/index.ts index 508b3811c..231d8a3eb 100644 --- a/src/agents/index.ts +++ b/src/agents/index.ts @@ -16,4 +16,3 @@ export function getAgentSpec(kind: AgentKind): AgentSpec { } export { AgentKind, AgentMeta, AgentParseResult } from "./types.js"; - diff --git a/src/agents/opencode.ts b/src/agents/opencode.ts index a19bfae7b..c458d94c1 100644 --- a/src/agents/opencode.ts +++ b/src/agents/opencode.ts @@ -6,42 +6,50 @@ import { parseOpencodeJson, summarizeOpencodeMetadata, } from "../auto-reply/opencode.js"; -import type { AgentMeta, AgentParseResult, AgentSpec, BuildArgsContext } from "./types.js"; +import type { AgentMeta, AgentSpec } from "./types.js"; -function toMeta(parsed: ReturnType): AgentMeta | undefined { +function toMeta( + parsed: ReturnType, +): AgentMeta | undefined { const summary = summarizeOpencodeMetadata(parsed.meta); return summary ? { extra: { summary } } : undefined; } export const opencodeSpec: AgentSpec = { kind: "opencode", - isInvocation: (argv) => argv.length > 0 && path.basename(argv[0]) === OPENCODE_BIN, + isInvocation: (argv) => + argv.length > 0 && path.basename(argv[0]) === OPENCODE_BIN, buildArgs: (ctx) => { + // Split around the body so we can insert flags without losing the prompt. const argv = [...ctx.argv]; + const body = argv[ctx.bodyIndex] ?? ""; + const beforeBody = argv.slice(0, ctx.bodyIndex); + const afterBody = argv.slice(ctx.bodyIndex + 1); const wantsJson = ctx.format === "json"; // Ensure format json for parsing if (wantsJson) { - const hasFormat = argv.some( + const hasFormat = [...beforeBody, body, ...afterBody].some( (part) => part === "--format" || part.startsWith("--format="), ); if (!hasFormat) { - const insertBeforeBody = Math.max(argv.length - 1, 0); - argv.splice(insertBeforeBody, 0, "--format", "json"); + beforeBody.push("--format", "json"); } } // Session args default to --session // Identity prefix + // Opencode streams text tokens; we still seed an identity so the agent + // keeps context on first turn. const shouldPrependIdentity = !(ctx.sendSystemOnce && ctx.systemSent); - if (shouldPrependIdentity && argv[ctx.bodyIndex]) { - const existingBody = argv[ctx.bodyIndex]; - argv[ctx.bodyIndex] = [ctx.identityPrefix ?? OPENCODE_IDENTITY_PREFIX, existingBody] - .filter(Boolean) - .join("\n\n"); - } + const bodyWithIdentity = + shouldPrependIdentity && body + ? [ctx.identityPrefix ?? OPENCODE_IDENTITY_PREFIX, body] + .filter(Boolean) + .join("\n\n") + : body; - return argv; + return [...beforeBody, bodyWithIdentity, ...afterBody]; }, parseOutput: (rawStdout) => { const parsed = parseOpencodeJson(rawStdout); diff --git a/src/agents/pi.ts b/src/agents/pi.ts index 18efc0531..c7359b98e 100644 --- a/src/agents/pi.ts +++ b/src/agents/pi.ts @@ -1,6 +1,6 @@ import path from "node:path"; -import type { AgentMeta, AgentParseResult, AgentSpec, BuildArgsContext } from "./types.js"; +import type { AgentMeta, AgentParseResult, AgentSpec } from "./types.js"; type PiAssistantMessage = { role?: string; @@ -16,7 +16,11 @@ function parsePiJson(raw: string): AgentParseResult { let lastMessage: PiAssistantMessage | undefined; for (const line of lines) { try { - const ev = JSON.parse(line) as { type?: string; message?: PiAssistantMessage }; + const ev = JSON.parse(line) as { + type?: string; + message?: PiAssistantMessage; + }; + // Pi emits a stream; we only care about the terminal assistant message_end. if (ev.type === "message_end" && ev.message?.role === "assistant") { lastMessage = ev.message; } @@ -50,14 +54,20 @@ export const piSpec: AgentSpec = { if (!argv.includes("-p") && !argv.includes("--print")) { argv.splice(argv.length - 1, 0, "-p"); } - if (ctx.format === "json" && !argv.includes("--mode") && !argv.some((a) => a === "--mode")) { + if ( + ctx.format === "json" && + !argv.includes("--mode") && + !argv.some((a) => a === "--mode") + ) { argv.splice(argv.length - 1, 0, "--mode", "json"); } // Session defaults - // Identity prefix optional; Pi usually doesn't need, but allow + // Identity prefix optional; Pi usually doesn't need it, but allow injection if (!(ctx.sendSystemOnce && ctx.systemSent) && argv[ctx.bodyIndex]) { const existingBody = argv[ctx.bodyIndex]; - argv[ctx.bodyIndex] = [ctx.identityPrefix, existingBody].filter(Boolean).join("\n\n"); + argv[ctx.bodyIndex] = [ctx.identityPrefix, existingBody] + .filter(Boolean) + .join("\n\n"); } return argv; }, diff --git a/src/agents/types.ts b/src/agents/types.ts index 76b6ba1d2..d430fb296 100644 --- a/src/agents/types.ts +++ b/src/agents/types.ts @@ -39,4 +39,3 @@ export interface AgentSpec { buildArgs: (ctx: BuildArgsContext) => string[]; parseOutput: (rawStdout: string) => AgentParseResult; } - diff --git a/src/auto-reply/command-reply.test.ts b/src/auto-reply/command-reply.test.ts index 39ca340b8..01a32ca0e 100644 --- a/src/auto-reply/command-reply.test.ts +++ b/src/auto-reply/command-reply.test.ts @@ -193,7 +193,11 @@ describe("runCommandReply", () => { throw { stdout: "partial output here", killed: true, signal: "SIGKILL" }; }); const { payload, meta } = await runCommandReply({ - reply: { mode: "command", command: ["echo", "hi"], agent: { kind: "claude" } }, + reply: { + mode: "command", + command: ["echo", "hi"], + agent: { kind: "claude" }, + }, templatingCtx: noopTemplateCtx, sendSystemOnce: false, isNewSession: true, @@ -214,7 +218,12 @@ describe("runCommandReply", () => { throw { stdout: "", killed: true, signal: "SIGKILL" }; }); const { payload } = await runCommandReply({ - reply: { mode: "command", command: ["echo", "hi"], cwd: "/tmp/work", agent: { kind: "claude" } }, + reply: { + mode: "command", + command: ["echo", "hi"], + cwd: "/tmp/work", + agent: { kind: "claude" }, + }, templatingCtx: noopTemplateCtx, sendSystemOnce: false, isNewSession: true, @@ -236,7 +245,12 @@ describe("runCommandReply", () => { stdout: `hi\nMEDIA:${tmp}\nMEDIA:https://example.com/img.jpg`, }); const { payload } = await runCommandReply({ - reply: { mode: "command", command: ["echo", "hi"], mediaMaxMb: 1, agent: { kind: "claude" } }, + reply: { + mode: "command", + command: ["echo", "hi"], + mediaMaxMb: 1, + agent: { kind: "claude" }, + }, templatingCtx: noopTemplateCtx, sendSystemOnce: false, isNewSession: true, @@ -279,7 +293,11 @@ describe("runCommandReply", () => { it("captures queue wait metrics in meta", async () => { const runner = makeRunner({ stdout: "ok" }); const { meta } = await runCommandReply({ - reply: { mode: "command", command: ["echo", "{{Body}}"], agent: { kind: "claude" } }, + reply: { + mode: "command", + command: ["echo", "{{Body}}"], + agent: { kind: "claude" }, + }, templatingCtx: noopTemplateCtx, sendSystemOnce: false, isNewSession: true, diff --git a/src/auto-reply/command-reply.ts b/src/auto-reply/command-reply.ts index a897520ba..eb1838be0 100644 --- a/src/auto-reply/command-reply.ts +++ b/src/auto-reply/command-reply.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { getAgentSpec } from "../agents/index.js"; +import { type AgentKind, getAgentSpec } from "../agents/index.js"; import type { AgentMeta } from "../agents/types.js"; import type { WarelayConfig } from "../config/config.js"; import { isVerbose, logVerbose } from "../globals.js"; @@ -116,7 +116,8 @@ export async function runCommandReply( throw new Error("reply.command is required for mode=command"); } const agentCfg = reply.agent ?? { kind: "claude" }; - const agent = getAgentSpec(agentCfg.kind as any); + const agentKind: AgentKind = agentCfg.kind ?? "claude"; + const agent = getAgentSpec(agentKind); let argv = reply.command.map((part) => applyTemplate(part, templatingCtx)); const templatePrefix = @@ -142,14 +143,18 @@ export async function runCommandReply( : ["--session", "{{SessionId}}"]; const sessionArgList = ( isNewSession - ? reply.session.sessionArgNew ?? defaultNew - : reply.session.sessionArgResume ?? defaultResume + ? (reply.session.sessionArgNew ?? defaultNew) + : (reply.session.sessionArgResume ?? defaultResume) ).map((p) => applyTemplate(p, templatingCtx)); if (sessionArgList.length) { const insertBeforeBody = reply.session.sessionArgBeforeBody ?? true; const insertAt = insertBeforeBody && argv.length > 1 ? argv.length - 1 : argv.length; - argv = [...argv.slice(0, insertAt), ...sessionArgList, ...argv.slice(insertAt)]; + argv = [ + ...argv.slice(0, insertAt), + ...sessionArgList, + ...argv.slice(insertAt), + ]; bodyIndex = Math.max(argv.length - 1, 0); } } @@ -198,6 +203,8 @@ export async function runCommandReply( } const parsed = trimmed ? agent.parseOutput(trimmed) : undefined; + // Treat empty string as "no content" so we can fall back to the friendly + // "(command produced no output)" message instead of echoing raw JSON. if (parsed && parsed.text !== undefined) { trimmed = parsed.text.trim(); } @@ -223,7 +230,9 @@ export async function runCommandReply( `Command auto-reply exited with code ${code ?? "unknown"} (signal: ${signal ?? "none"})`, ); // Include any partial output or stderr in error message - const partialOut = trimmed ? `\n\nOutput: ${trimmed.slice(0, 500)}${trimmed.length > 500 ? "..." : ""}` : ""; + const partialOut = trimmed + ? `\n\nOutput: ${trimmed.slice(0, 500)}${trimmed.length > 500 ? "..." : ""}` + : ""; const errorText = `⚠️ Command exited with code ${code ?? "unknown"}${signal ? ` (${signal})` : ""}${partialOut}`; return { payload: { text: errorText }, diff --git a/src/auto-reply/opencode.ts b/src/auto-reply/opencode.ts index 19b16055d..859b81d60 100644 --- a/src/auto-reply/opencode.ts +++ b/src/auto-reply/opencode.ts @@ -102,4 +102,3 @@ export function summarizeOpencodeMetadata( } return parts.length ? parts.join(", ") : undefined; } - diff --git a/src/commands/send.test.ts b/src/commands/send.test.ts index 7e7fb9dd8..7cc54e7c4 100644 --- a/src/commands/send.test.ts +++ b/src/commands/send.test.ts @@ -4,6 +4,10 @@ import type { CliDeps } from "../cli/deps.js"; import type { RuntimeEnv } from "../runtime.js"; import { sendCommand } from "./send.js"; +vi.mock("../web/ipc.js", () => ({ + sendViaIpc: vi.fn().mockResolvedValue(null), +})); + const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn(), diff --git a/src/commands/send.ts b/src/commands/send.ts index 45b770f74..30b99f057 100644 --- a/src/commands/send.ts +++ b/src/commands/send.ts @@ -45,7 +45,9 @@ export async function sendCommand( const ipcResult = await sendViaIpc(opts.to, opts.message, opts.media); if (ipcResult) { if (ipcResult.success) { - runtime.log(success(`✅ Sent via relay IPC. Message ID: ${ipcResult.messageId}`)); + runtime.log( + success(`✅ Sent via relay IPC. Message ID: ${ipcResult.messageId}`), + ); if (opts.json) { runtime.log( JSON.stringify( @@ -64,7 +66,11 @@ export async function sendCommand( return; } // IPC failed but relay is running - warn and fall back - runtime.log(info(`IPC send failed (${ipcResult.error}), falling back to direct connection`)); + runtime.log( + info( + `IPC send failed (${ipcResult.error}), falling back to direct connection`, + ), + ); } // Fall back to direct connection (creates new Baileys socket) diff --git a/src/web/auto-reply.test.ts b/src/web/auto-reply.test.ts index 945df6212..3ed436571 100644 --- a/src/web/auto-reply.test.ts +++ b/src/web/auto-reply.test.ts @@ -1,10 +1,4 @@ -// Import test-helpers FIRST to set up mocks before other imports -import { - resetBaileysMocks, - resetLoadConfigMock, - setLoadConfigMock, -} from "./test-helpers.js"; - +import "./test-helpers.js"; import crypto from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; @@ -13,8 +7,8 @@ import sharp from "sharp"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { WarelayConfig } from "../config/config.js"; -import * as commandQueue from "../process/command-queue.js"; import { resetLogger, setLoggerOverride } from "../logging.js"; +import * as commandQueue from "../process/command-queue.js"; import { HEARTBEAT_PROMPT, HEARTBEAT_TOKEN, @@ -25,8 +19,11 @@ import { stripHeartbeatToken, } from "./auto-reply.js"; import type { sendMessageWeb } from "./outbound.js"; -import * as commandQueue from "../process/command-queue.js"; -import { getQueueSize } from "../process/command-queue.js"; +import { + resetBaileysMocks, + resetLoadConfigMock, + setLoadConfigMock, +} from "./test-helpers.js"; const makeSessionStore = async ( entries: Record = {}, @@ -535,9 +532,7 @@ describe("web auto-reply", () => { const storePath = path.join(tmpDir, "sessions.json"); await fs.writeFile(storePath, JSON.stringify({})); - const queueSpy = vi - .spyOn(commandQueue, "getQueueSize") - .mockReturnValue(2); + const queueSpy = vi.spyOn(commandQueue, "getQueueSize").mockReturnValue(2); const replyResolver = vi.fn(); const listenerFactory = vi.fn(async () => { const onClose = new Promise(() => { diff --git a/src/web/auto-reply.ts b/src/web/auto-reply.ts index 7862f25cf..9b6e9e917 100644 --- a/src/web/auto-reply.ts +++ b/src/web/auto-reply.ts @@ -12,13 +12,13 @@ import { import { danger, info, isVerbose, logVerbose, success } from "../globals.js"; import { logInfo } from "../logger.js"; import { getChildLogger } from "../logging.js"; +import { enqueueCommand, getQueueSize } from "../process/command-queue.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { normalizeE164 } from "../utils.js"; import { monitorWebInbox } from "./inbound.js"; import { sendViaIpc, startIpcServer, stopIpcServer } from "./ipc.js"; import { loadWebMedia } from "./media.js"; import { sendMessageWeb } from "./outbound.js"; -import { enqueueCommand, getQueueSize } from "../process/command-queue.js"; import { computeBackoff, newConnectionId, @@ -697,9 +697,7 @@ export async function monitorWebProvider( } } catch (err) { console.error( - danger( - `Failed sending web auto-reply to ${from}: ${String(err)}`, - ), + danger(`Failed sending web auto-reply to ${from}: ${String(err)}`), ); } }; @@ -713,7 +711,8 @@ export async function monitorWebProvider( if (getQueueSize() === 0) { await processBatch(msg.from); } else { - bucket.timer = bucket.timer ?? setTimeout(() => void processBatch(msg.from), 150); + bucket.timer = + bucket.timer ?? setTimeout(() => void processBatch(msg.from), 150); } }; @@ -754,7 +753,12 @@ export async function monitorWebProvider( mediaBuffer = media.buffer; mediaType = media.contentType; } - const result = await listener.sendMessage(to, message, mediaBuffer, mediaType); + const result = await listener.sendMessage( + to, + message, + mediaBuffer, + mediaType, + ); // Add to echo detection so we don't process our own message if (message) { recentlySent.add(message); @@ -763,7 +767,10 @@ export async function monitorWebProvider( if (firstKey) recentlySent.delete(firstKey); } } - logInfo(`📤 IPC send to ${to}: ${message.substring(0, 50)}...`, runtime); + logInfo( + `📤 IPC send to ${to}: ${message.substring(0, 50)}...`, + runtime, + ); // Show typing indicator after send so user knows more may be coming try { await listener.sendComposingTo(to); @@ -807,7 +814,10 @@ export async function monitorWebProvider( // Warn if no messages in 30+ minutes if (minutesSinceLastMessage && minutesSinceLastMessage > 30) { - heartbeatLogger.warn(logData, "⚠️ web relay heartbeat - no messages in 30+ minutes"); + heartbeatLogger.warn( + logData, + "⚠️ web relay heartbeat - no messages in 30+ minutes", + ); } else { heartbeatLogger.info(logData, "web relay heartbeat"); } @@ -818,7 +828,9 @@ export async function monitorWebProvider( if (lastMessageAt) { const timeSinceLastMessage = Date.now() - lastMessageAt; if (timeSinceLastMessage > MESSAGE_TIMEOUT_MS) { - const minutesSinceLastMessage = Math.floor(timeSinceLastMessage / 60000); + const minutesSinceLastMessage = Math.floor( + timeSinceLastMessage / 60000, + ); heartbeatLogger.warn( { connectionId, @@ -978,7 +990,11 @@ export async function monitorWebProvider( // Apply response prefix if configured (same as regular messages) let finalText = stripped.text; const responsePrefix = cfg.inbound?.responsePrefix; - if (responsePrefix && finalText && !finalText.startsWith(responsePrefix)) { + if ( + responsePrefix && + finalText && + !finalText.startsWith(responsePrefix) + ) { finalText = `${responsePrefix} ${finalText}`; } diff --git a/src/web/inbound.ts b/src/web/inbound.ts index 9d6a4df6a..f10a48247 100644 --- a/src/web/inbound.ts +++ b/src/web/inbound.ts @@ -103,8 +103,13 @@ export async function monitorWebInbox(options: { const isSamePhone = from === selfE164; if (!isSamePhone && Array.isArray(allowFrom) && allowFrom.length > 0) { - if (!allowFrom.includes("*") && !allowFrom.map(normalizeE164).includes(from)) { - logVerbose(`Blocked unauthorized sender ${from} (not in allowFrom list)`); + if ( + !allowFrom.includes("*") && + !allowFrom.map(normalizeE164).includes(from) + ) { + logVerbose( + `Blocked unauthorized sender ${from} (not in allowFrom list)`, + ); continue; // Skip processing entirely } } diff --git a/src/web/ipc.ts b/src/web/ipc.ts index 99e09e0f5..ed70b525b 100644 --- a/src/web/ipc.ts +++ b/src/web/ipc.ts @@ -78,13 +78,13 @@ export function startIpcServer(sendHandler: SendHandler): void { success: true, messageId: result.messageId, }; - conn.write(JSON.stringify(response) + "\n"); + conn.write(`${JSON.stringify(response)}\n`); } catch (err) { const response: IpcSendResponse = { success: false, error: String(err), }; - conn.write(JSON.stringify(response) + "\n"); + conn.write(`${JSON.stringify(response)}\n`); } } } catch (err) { @@ -93,7 +93,7 @@ export function startIpcServer(sendHandler: SendHandler): void { success: false, error: "Invalid request format", }; - conn.write(JSON.stringify(response) + "\n"); + conn.write(`${JSON.stringify(response)}\n`); } } }); @@ -174,7 +174,7 @@ export async function sendViaIpc( message, mediaUrl, }; - client.write(JSON.stringify(request) + "\n"); + client.write(`${JSON.stringify(request)}\n`); }); client.on("data", (data) => { @@ -198,7 +198,7 @@ export async function sendViaIpc( } }); - client.on("error", (err) => { + client.on("error", (_err) => { if (!resolved) { resolved = true; clearTimeout(timeout); diff --git a/src/web/monitor-inbox.test.ts b/src/web/monitor-inbox.test.ts index b696d2513..829cade1f 100644 --- a/src/web/monitor-inbox.test.ts +++ b/src/web/monitor-inbox.test.ts @@ -251,7 +251,11 @@ describe("web monitor inbox", () => { type: "notify", messages: [ { - key: { id: "unauth1", fromMe: false, remoteJid: "999@s.whatsapp.net" }, + key: { + id: "unauth1", + fromMe: false, + remoteJid: "999@s.whatsapp.net", + }, message: { conversation: "unauthorized message" }, messageTimestamp: 1_700_000_000, },