From 75cd0efb0454c5d7704887b87995eda18ca61652 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 13 Jul 2026 23:54:12 -0700 Subject: [PATCH] refactor(line): remove dead exports (#107231) --- extensions/line/src/accounts.test.ts | 8 +- extensions/line/src/accounts.ts | 2 - .../line/src/auto-reply-delivery.test.ts | 3 +- extensions/line/src/auto-reply-delivery.ts | 2 +- extensions/line/src/bot-handlers.test.ts | 33 ++------- extensions/line/src/bot-handlers.ts | 13 +--- extensions/line/src/message-cards.test.ts | 3 +- extensions/line/src/monitor.lifecycle.test.ts | 22 ++---- extensions/line/src/monitor.ts | 74 ------------------- extensions/line/src/rich-menu.test.ts | 5 +- extensions/line/src/rich-menu.ts | 4 +- extensions/line/src/runtime.ts | 15 ++-- extensions/line/src/setup-surface.test.ts | 8 +- extensions/line/src/template-messages.ts | 2 - scripts/deadcode-exports.baseline.mjs | 11 --- 15 files changed, 30 insertions(+), 175 deletions(-) diff --git a/extensions/line/src/accounts.test.ts b/extensions/line/src/accounts.test.ts index 7e2f636d1017..854705e07fb3 100644 --- a/extensions/line/src/accounts.test.ts +++ b/extensions/line/src/accounts.test.ts @@ -2,14 +2,10 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { - resolveLineAccount, - resolveDefaultLineAccountId, - normalizeAccountId, - DEFAULT_ACCOUNT_ID, -} from "./accounts.js"; +import { resolveLineAccount, resolveDefaultLineAccountId, normalizeAccountId } from "./accounts.js"; describe("LINE accounts", () => { const tempDirs: string[] = []; diff --git a/extensions/line/src/accounts.ts b/extensions/line/src/accounts.ts index d0a56157a474..71784fef3c0e 100644 --- a/extensions/line/src/accounts.ts +++ b/extensions/line/src/accounts.ts @@ -14,8 +14,6 @@ import type { ResolvedLineAccount, } from "./types.js"; -export { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id"; - function readFileIfExists(filePath: string | undefined): string | undefined { return tryReadSecretFileSync(filePath, "LINE credential file", { rejectSymlink: true }); } diff --git a/extensions/line/src/auto-reply-delivery.test.ts b/extensions/line/src/auto-reply-delivery.test.ts index 1c987b6efbfe..ca45c3f7fff0 100644 --- a/extensions/line/src/auto-reply-delivery.test.ts +++ b/extensions/line/src/auto-reply-delivery.test.ts @@ -1,11 +1,12 @@ // Line tests cover auto reply delivery plugin behavior. import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it, vi } from "vitest"; -import type { LineAutoReplyDeps } from "./auto-reply-delivery.js"; import { deliverLineAutoReply } from "./auto-reply-delivery.js"; import { sendLineReplyChunks } from "./reply-chunks.js"; import { createLineSendReceipt } from "./send-receipt.js"; +type LineAutoReplyDeps = Parameters[0]["deps"]; + const createFlexMessage = (altText: string, contents: unknown) => ({ type: "flex" as const, altText, diff --git a/extensions/line/src/auto-reply-delivery.ts b/extensions/line/src/auto-reply-delivery.ts index e6f9d3c0dcde..d84ed9a46dbd 100644 --- a/extensions/line/src/auto-reply-delivery.ts +++ b/extensions/line/src/auto-reply-delivery.ts @@ -10,7 +10,7 @@ import { buildLineQuickReplyFallbackText } from "./quick-reply-fallback.js"; import type { SendLineReplyChunksParams } from "./reply-chunks.js"; import type { LineChannelData, LineTemplateMessagePayload } from "./types.js"; -export type LineAutoReplyDeps = { +type LineAutoReplyDeps = { buildTemplateMessageFromPayload: ( payload: LineTemplateMessagePayload, ) => messagingApi.TemplateMessage | null; diff --git a/extensions/line/src/bot-handlers.test.ts b/extensions/line/src/bot-handlers.test.ts index eb5143f73b8e..c68b5dd769af 100644 --- a/extensions/line/src/bot-handlers.test.ts +++ b/extensions/line/src/bot-handlers.test.ts @@ -175,7 +175,6 @@ vi.mock("./bot-message-context.js", () => ({ let handleLineWebhookEvents: typeof import("./bot-handlers.js").handleLineWebhookEvents; let createLineWebhookReplayCache: typeof import("./bot-handlers.js").createLineWebhookReplayCache; -let LineRetryableWebhookError: typeof import("./bot-handlers.js").LineRetryableWebhookError; type LineWebhookContext = Parameters[1]; const createRuntime = () => ({ log: vi.fn(), error: vi.fn(), exit: vi.fn() }); @@ -315,8 +314,7 @@ async function startInflightReplayDuplicate(params: { describe("handleLineWebhookEvents", () => { beforeAll(async () => { - ({ handleLineWebhookEvents, createLineWebhookReplayCache, LineRetryableWebhookError } = - await import("./bot-handlers.js")); + ({ handleLineWebhookEvents, createLineWebhookReplayCache } = await import("./bot-handlers.js")); }); afterAll(() => { @@ -769,7 +767,7 @@ describe("handleLineWebhookEvents", () => { expect(processMessage).toHaveBeenCalledTimes(1); }); - it("mirrors in-flight retryable replay failures so concurrent duplicates also fail", async () => { + it("commits in-flight failures so concurrent duplicates do not retry", async () => { let rejectFirst: ((err: Error) => void) | undefined; const firstDone = new Promise((_, reject) => { rejectFirst = reject; @@ -786,10 +784,10 @@ describe("handleLineWebhookEvents", () => { }); const { firstRun, secondRun } = await startInflightReplayDuplicate({ event, processMessage }); const firstFailure = expect(firstRun).rejects.toThrow("transient inflight failure"); - const secondFailure = expect(secondRun).rejects.toThrow("transient inflight failure"); - rejectFirst?.(new LineRetryableWebhookError("transient inflight failure")); + rejectFirst?.(new Error("transient inflight failure")); - await Promise.all([firstFailure, secondFailure]); + await firstFailure; + await expect(secondRun).resolves.toBeUndefined(); expect(processMessage).toHaveBeenCalledTimes(1); }); @@ -1164,25 +1162,4 @@ describe("handleLineWebhookEvents", () => { "line: event handler failed: Error: transient failure", ); }); - - it("reopens replay after an explicit retryable event failure", async () => { - const processMessage = vi - .fn() - .mockRejectedValueOnce(new LineRetryableWebhookError("retry me")) - .mockResolvedValueOnce(undefined); - const event = createReplayMessageEvent({ - messageId: "m-fail-then-retryable", - groupId: "group-retry", - userId: "user-retry", - webhookEventId: "evt-fail-then-retryable", - isRedelivery: false, - }); - const context = createOpenGroupReplayContext(processMessage, createLineWebhookReplayCache()); - - await expect(handleLineWebhookEvents([event], context)).rejects.toThrow("retry me"); - await handleLineWebhookEvents([event], context); - - expect(buildLineMessageContextMock).toHaveBeenCalledTimes(2); - expect(processMessage).toHaveBeenCalledTimes(2); - }); }); diff --git a/extensions/line/src/bot-handlers.ts b/extensions/line/src/bot-handlers.ts index 9e7228fcc529..bcd577303113 100644 --- a/extensions/line/src/bot-handlers.ts +++ b/extensions/line/src/bot-handlers.ts @@ -85,13 +85,6 @@ function normalizeLineIngressEntry(value: string): string | null { return normalizeLineAllowEntry(value) || null; } -export class LineRetryableWebhookError extends Error { - constructor(message: string, options?: ErrorOptions) { - super(message, options); - this.name = "LineRetryableWebhookError"; - } -} - export function createLineWebhookReplayCache(): LineWebhookReplayCache { return createClaimableDedupe({ ttlMs: LINE_WEBHOOK_REPLAY_WINDOW_MS, @@ -614,11 +607,7 @@ export async function handleLineWebhookEvents( } } catch (err) { if (replayCandidate) { - if (err instanceof LineRetryableWebhookError) { - replayCandidate.cache.release(replayCandidate.key, { error: err }); - } else { - await replayCandidate.cache.commit(replayCandidate.key); - } + await replayCandidate.cache.commit(replayCandidate.key); } context.runtime.error?.(danger(`line: event handler failed: ${String(err)}`)); firstError ??= err; diff --git a/extensions/line/src/message-cards.test.ts b/extensions/line/src/message-cards.test.ts index 31f9e082cf54..4fbbb7b2ef36 100644 --- a/extensions/line/src/message-cards.test.ts +++ b/extensions/line/src/message-cards.test.ts @@ -1,7 +1,7 @@ // Line tests cover message cards plugin behavior. import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it } from "vitest"; -import { datetimePickerAction, postbackAction, uriAction } from "./actions.js"; +import { datetimePickerAction, messageAction, postbackAction, uriAction } from "./actions.js"; import { registerLineCardCommand } from "./card-command.js"; import { createActionCard, @@ -21,7 +21,6 @@ import { createImageCarousel, createImageCarouselColumn, createProductCarousel, - messageAction, } from "./template-messages.js"; const loneHighSurrogate = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])/; diff --git a/extensions/line/src/monitor.lifecycle.test.ts b/extensions/line/src/monitor.lifecycle.test.ts index bd851f7f9505..89198a3ef3fe 100644 --- a/extensions/line/src/monitor.lifecycle.test.ts +++ b/extensions/line/src/monitor.lifecycle.test.ts @@ -6,7 +6,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { createMockIncomingRequest } from "openclaw/plugin-sdk/test-env"; import { WEBHOOK_IN_FLIGHT_DEFAULTS } from "openclaw/plugin-sdk/webhook-request-guards"; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; type LineNodeWebhookHandler = (req: IncomingMessage, res: ServerResponse) => Promise; type LineHandleWebhook = (...args: unknown[]) => Promise; @@ -29,8 +29,6 @@ const { })); let monitorLineProvider: typeof import("./monitor.js").monitorLineProvider; -let getLineRuntimeState: typeof import("./monitor.js").getLineRuntimeState; -let clearLineRuntimeStateForTests: typeof import("./monitor.js").clearLineRuntimeStateForTests; let innerLineWebhookHandlerMock: ReturnType>; type RegisteredRoute = { @@ -142,8 +140,7 @@ vi.mock("./template-messages.js", () => ({ describe("monitorLineProvider lifecycle", () => { beforeAll(async () => { - ({ monitorLineProvider, getLineRuntimeState, clearLineRuntimeStateForTests } = - await import("./monitor.js")); + ({ monitorLineProvider } = await import("./monitor.js")); }); afterAll(() => { @@ -161,7 +158,6 @@ describe("monitorLineProvider lifecycle", () => { }); beforeEach(() => { - clearLineRuntimeStateForTests(); createLineBotMock.mockReset(); createLineBotMock.mockImplementation(() => ({ account: { accountId: "default" }, @@ -200,10 +196,6 @@ describe("monitorLineProvider lifecycle", () => { }); }); - afterEach(() => { - clearLineRuntimeStateForTests(); - }); - const createRouteResponse = () => { const resObj = { statusCode: 0, @@ -289,7 +281,7 @@ describe("monitorLineProvider lifecycle", () => { expect(unregisterHttpMock).toHaveBeenCalledTimes(1); }); - it("records startup state under configured defaultAccount when accountId is omitted", async () => { + it("registers the configured defaultAccount when accountId is omitted", async () => { const monitor = await monitorLineProvider({ channelAccessToken: "token", channelSecret: "secret", // pragma: allowlist secret @@ -309,13 +301,14 @@ describe("monitorLineProvider lifecycle", () => { runtime: {} as RuntimeEnv, }); - expect(getLineRuntimeState("work")?.running).toBe(true); - expect(getLineRuntimeState("default")).toBeUndefined(); + const registration = requireWebhookRegistration(); + expect(registration.target.accountId).toBe("work"); + expect(registration.route.accountId).toBe("work"); monitor.stop(); }); - it("does not record running state when bot startup fails", async () => { + it("does not register a webhook when bot startup fails", async () => { createLineBotMock.mockImplementation(() => { throw new Error("line bot startup failed"); }); @@ -329,7 +322,6 @@ describe("monitorLineProvider lifecycle", () => { }), ).rejects.toThrow("line bot startup failed"); - expect(getLineRuntimeState("default")?.running).not.toBe(true); expect(registerWebhookTargetWithPluginRouteMock).not.toHaveBeenCalled(); }); diff --git a/extensions/line/src/monitor.ts b/extensions/line/src/monitor.ts index 04c74e2b273b..646ca3908e05 100644 --- a/extensions/line/src/monitor.ts +++ b/extensions/line/src/monitor.ts @@ -63,17 +63,6 @@ interface LineProviderMonitor { stop: () => void; } -const runtimeState = new Map< - string, - { - running: boolean; - lastStartAt: number | null; - lastStopAt: number | null; - lastError: string | null; - lastInboundAt?: number | null; - lastOutboundAt?: number | null; - } ->(); const lineWebhookInFlightLimiter = createWebhookInFlightLimiter(); const LINE_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 64 * 1024; const LINE_WEBHOOK_PREAUTH_BODY_TIMEOUT_MS = 5_000; @@ -88,36 +77,6 @@ type LineWebhookTarget = { const lineWebhookTargets = new Map(); -function recordChannelRuntimeState(params: { - channel: string; - accountId: string; - state: Partial<{ - running: boolean; - lastStartAt: number | null; - lastStopAt: number | null; - lastError: string | null; - lastInboundAt: number | null; - lastOutboundAt: number | null; - }>; -}): void { - const key = `${params.channel}:${params.accountId}`; - const existing = runtimeState.get(key) ?? { - running: false, - lastStartAt: null, - lastStopAt: null, - lastError: null, - }; - runtimeState.set(key, { ...existing, ...params.state }); -} - -export function getLineRuntimeState(accountId: string) { - return runtimeState.get(`line:${accountId}`); -} - -export function clearLineRuntimeStateForTests() { - runtimeState.clear(); -} - function startLineLoadingKeepalive(params: { cfg: OpenClawConfig; userId: string; @@ -188,14 +147,6 @@ export async function monitorLineProvider( const { ctxPayload, replyToken, route } = ctx; - recordChannelRuntimeState({ - channel: "line", - accountId: resolvedAccountId, - state: { - lastInboundAt: Date.now(), - }, - }); - const shouldShowLoading = Boolean(ctx.userId && !ctx.isGroup); const displayNamePromise = ctx.userId @@ -298,14 +249,6 @@ export async function monitorLineProvider( // because this delivery was not a clean success. throw deliveryResult.error; } - - recordChannelRuntimeState({ - channel: "line", - accountId: resolvedAccountId, - state: { - lastOutboundAt: Date.now(), - }, - }); }, onError: (err, info) => { runtime.error?.(danger(`line ${info.kind} reply failed: ${String(err)}`)); @@ -473,15 +416,6 @@ export async function monitorLineProvider( }, }); - recordChannelRuntimeState({ - channel: "line", - accountId: resolvedAccountId, - state: { - running: true, - lastStartAt: Date.now(), - }, - }); - logVerbose(`line: registered webhook handler at ${normalizedPath}`); let stopped = false; @@ -492,14 +426,6 @@ export async function monitorLineProvider( stopped = true; logVerbose(`line: stopping provider for account ${resolvedAccountId}`); unregisterHttp(); - recordChannelRuntimeState({ - channel: "line", - accountId: resolvedAccountId, - state: { - running: false, - lastStopAt: Date.now(), - }, - }); }; if (abortSignal?.aborted) { diff --git a/extensions/line/src/rich-menu.test.ts b/extensions/line/src/rich-menu.test.ts index 85ce06946899..add025814f3b 100644 --- a/extensions/line/src/rich-menu.test.ts +++ b/extensions/line/src/rich-menu.test.ts @@ -4,15 +4,12 @@ import os from "node:os"; import path from "node:path"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { datetimePickerAction, messageAction, postbackAction, uriAction } from "./actions.js"; import { createRichMenu, createDefaultMenuConfig, createGridLayout, - datetimePickerAction, - messageAction, - postbackAction, uploadRichMenuImage, - uriAction, } from "./rich-menu.js"; const { diff --git a/extensions/line/src/rich-menu.ts b/extensions/line/src/rich-menu.ts index fba83a38cbac..15c0a3c41a2b 100644 --- a/extensions/line/src/rich-menu.ts +++ b/extensions/line/src/rich-menu.ts @@ -6,7 +6,7 @@ import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; import { loadWebMediaRaw } from "openclaw/plugin-sdk/web-media"; import { resolveLineAccount } from "./accounts.js"; -import { datetimePickerAction, messageAction, postbackAction, uriAction } from "./actions.js"; +import { messageAction } from "./actions.js"; import { resolveLineChannelAccessToken } from "./channel-access-token.js"; type RichMenuRequest = messagingApi.RichMenuRequest; @@ -253,8 +253,6 @@ export function createGridLayout( ]; } -export { datetimePickerAction, messageAction, postbackAction, uriAction }; - export function createDefaultMenuConfig(): CreateRichMenuParams { return { size: { width: 2500, height: 843 }, diff --git a/extensions/line/src/runtime.ts b/extensions/line/src/runtime.ts index 48cfaacda137..2c725568b276 100644 --- a/extensions/line/src/runtime.ts +++ b/extensions/line/src/runtime.ts @@ -22,12 +22,9 @@ type LineRuntime = PluginRuntime & { }; }; -const { - setRuntime: setLineRuntime, - clearRuntime: clearLineRuntime, - getRuntime: getLineRuntime, -} = createPluginRuntimeStore({ - pluginId: "line", - errorMessage: "LINE runtime not initialized - plugin not registered", -}); -export { clearLineRuntime, getLineRuntime, setLineRuntime }; +const { setRuntime: setLineRuntime, getRuntime: getLineRuntime } = + createPluginRuntimeStore({ + pluginId: "line", + errorMessage: "LINE runtime not initialized - plugin not registered", + }); +export { getLineRuntime, setLineRuntime }; diff --git a/extensions/line/src/setup-surface.test.ts b/extensions/line/src/setup-surface.test.ts index d5593a460247..95574f413afc 100644 --- a/extensions/line/src/setup-surface.test.ts +++ b/extensions/line/src/setup-surface.test.ts @@ -15,9 +15,8 @@ import type { OpenClawConfig, PluginRuntime, ResolvedLineAccount } from "../api. import { linePlugin } from "./channel.js"; import { lineGatewayAdapter } from "./gateway.js"; import { probeLineBot } from "./probe.js"; -import { clearLineRuntime, setLineRuntime } from "./runtime.js"; +import { setLineRuntime } from "./runtime.js"; import { lineSetupWizard } from "./setup-surface.js"; -import { lineStatusAdapter } from "./status.js"; const { getBotInfoMock, MessagingApiClientMock } = vi.hoisted(() => { const getBotInfoMockLocal = vi.fn(); @@ -318,7 +317,6 @@ describe("probeLineBot", () => { }); afterEach(() => { - clearLineRuntime(); vi.useRealTimers(); getBotInfoMock.mockClear(); }); @@ -352,6 +350,8 @@ describe("probeLineBot", () => { describe("linePlugin status.probeAccount", () => { it("falls back to the direct probe helper when runtime is not initialized", async () => { + vi.resetModules(); + const { lineStatusAdapter } = await import("./status.js"); MessagingApiClientMock.mockReset(); MessagingApiClientMock.mockImplementation(function () { return { getBotInfo: getBotInfoMock }; @@ -375,8 +375,6 @@ describe("linePlugin status.probeAccount", () => { timeoutMs: 50, }; - clearLineRuntime(); - await expect(lineStatusAdapter.probeAccount!(params)).resolves.toEqual( await probeLineBot("token", 50), ); diff --git a/extensions/line/src/template-messages.ts b/extensions/line/src/template-messages.ts index affbf162f383..0492af1b22fa 100644 --- a/extensions/line/src/template-messages.ts +++ b/extensions/line/src/template-messages.ts @@ -3,8 +3,6 @@ import type { messagingApi } from "@line/bot-sdk"; import { messageAction, postbackAction, uriAction, type Action } from "./actions.js"; import type { LineTemplateMessagePayload } from "./types.js"; -export { messageAction }; - type TemplateMessage = messagingApi.TemplateMessage; type ConfirmTemplate = messagingApi.ConfirmTemplate; type ButtonsTemplate = messagingApi.ButtonsTemplate; diff --git a/scripts/deadcode-exports.baseline.mjs b/scripts/deadcode-exports.baseline.mjs index fa8c9a154535..e80b08b43563 100644 --- a/scripts/deadcode-exports.baseline.mjs +++ b/scripts/deadcode-exports.baseline.mjs @@ -133,17 +133,6 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [ "extensions/irc/src/control-chars.ts: isIrcControlChar", "extensions/irc/src/monitor.ts: resolveIrcInboundTarget", "extensions/irc/src/runtime.ts: clearIrcRuntime", - "extensions/line/src/accounts.ts: DEFAULT_ACCOUNT_ID", - "extensions/line/src/auto-reply-delivery.ts: LineAutoReplyDeps", - "extensions/line/src/bot-handlers.ts: LineRetryableWebhookError", - "extensions/line/src/monitor.ts: clearLineRuntimeStateForTests", - "extensions/line/src/monitor.ts: getLineRuntimeState", - "extensions/line/src/rich-menu.ts: datetimePickerAction", - "extensions/line/src/rich-menu.ts: messageAction", - "extensions/line/src/rich-menu.ts: postbackAction", - "extensions/line/src/rich-menu.ts: uriAction", - "extensions/line/src/runtime.ts: clearLineRuntime", - "extensions/line/src/template-messages.ts: messageAction", "extensions/llama-cpp/src/embedding-provider.ts: createLlamaCppMemoryEmbeddingProvider", "extensions/llama-cpp/src/embedding-provider.ts: DEFAULT_LLAMA_CPP_EMBEDDING_MODEL", "extensions/llama-cpp/src/embedding-provider.ts: formatLlamaCppSetupError",