@@ -5,7 +5,6 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vites
55import type { LineAccountConfig } from "./types.js" ;
66
77type MessageEvent = webhook . MessageEvent ;
8- type PostbackEvent = webhook . PostbackEvent ;
98
109// Avoid pulling in globals/pairing/media dependencies; this suite only asserts
1110// allowlist/groupPolicy gating and message-context wiring.
@@ -176,7 +175,6 @@ vi.mock("./bot-message-context.js", () => ({
176175} ) ) ;
177176
178177let handleLineWebhookEvents : typeof import ( "./bot-handlers.js" ) . handleLineWebhookEvents ;
179- let createLineWebhookReplayCache : typeof import ( "./bot-handlers.js" ) . createLineWebhookReplayCache ;
180178type LineWebhookContext = Parameters < typeof import ( "./bot-handlers.js" ) . handleLineWebhookEvents > [ 1 ] ;
181179
182180const createRuntime = ( ) => ( { log : vi . fn ( ) , error : vi . fn ( ) , exit : vi . fn ( ) } ) ;
@@ -228,7 +226,6 @@ function createLineWebhookTestContext(params: {
228226 groupAllowFrom ?: LineAccountConfig [ "groupAllowFrom" ] ;
229227 requireMention ?: boolean ;
230228 groupHistories ?: Map < string , HistoryEntry [ ] > ;
231- replayCache ?: ReturnType < typeof createLineWebhookReplayCache > ;
232229 accessGroups ?: Record < string , { type : "message.senders" ; members : Record < string , string [ ] > } > ;
233230} ) : Parameters < typeof handleLineWebhookEvents > [ 1 ] {
234231 const allowFrom = params . allowFrom ?? ( params . dmPolicy === "open" ? [ "*" ] : undefined ) ;
@@ -260,22 +257,9 @@ function createLineWebhookTestContext(params: {
260257 mediaMaxBytes : 1 ,
261258 processMessage : params . processMessage ,
262259 ...( params . groupHistories ? { groupHistories : params . groupHistories } : { } ) ,
263- ...( params . replayCache ? { replayCache : params . replayCache } : { } ) ,
264260 } ;
265261}
266262
267- function createOpenGroupReplayContext (
268- processMessage : LineWebhookContext [ "processMessage" ] ,
269- replayCache : ReturnType < typeof createLineWebhookReplayCache > ,
270- ) : Parameters < typeof handleLineWebhookEvents > [ 1 ] {
271- return createLineWebhookTestContext ( {
272- processMessage,
273- groupPolicy : "open" ,
274- requireMention : false ,
275- replayCache,
276- } ) ;
277- }
278-
279263async function expectGroupMessageBlocked ( params : {
280264 processMessage : LineWebhookContext [ "processMessage" ] ;
281265 event : MessageEvent ;
@@ -300,23 +284,9 @@ async function expectRequireMentionGroupMessageProcessed(event: MessageEvent) {
300284 expect ( processMessage ) . toHaveBeenCalledTimes ( 1 ) ;
301285}
302286
303- async function startInflightReplayDuplicate ( params : {
304- event : MessageEvent ;
305- processMessage : LineWebhookContext [ "processMessage" ] ;
306- } ) {
307- const context = createOpenGroupReplayContext (
308- params . processMessage ,
309- createLineWebhookReplayCache ( ) ,
310- ) ;
311- const firstRun = handleLineWebhookEvents ( [ params . event ] , context ) ;
312- await Promise . resolve ( ) ;
313- const secondRun = handleLineWebhookEvents ( [ params . event ] , context ) ;
314- return { firstRun, secondRun } ;
315- }
316-
317287describe ( "handleLineWebhookEvents" , ( ) => {
318288 beforeAll ( async ( ) => {
319- ( { handleLineWebhookEvents, createLineWebhookReplayCache } = await import ( "./bot-handlers.js" ) ) ;
289+ ( { handleLineWebhookEvents } = await import ( "./bot-handlers.js" ) ) ;
320290 } ) ;
321291
322292 afterAll ( ( ) => {
@@ -803,173 +773,6 @@ describe("handleLineWebhookEvents", () => {
803773 expect ( pairingRequest ?. accountId ) . toBe ( "work" ) ;
804774 } ) ;
805775
806- it ( "deduplicates replayed webhook events by webhookEventId before processing" , async ( ) => {
807- const processMessage = vi . fn ( ) ;
808- const event = createReplayMessageEvent ( {
809- messageId : "m-replay" ,
810- groupId : "group-replay" ,
811- userId : "user-replay" ,
812- webhookEventId : "evt-replay-1" ,
813- isRedelivery : true ,
814- } ) ;
815- const context = createOpenGroupReplayContext ( processMessage , createLineWebhookReplayCache ( ) ) ;
816-
817- await handleLineWebhookEvents ( [ event ] , context ) ;
818- await handleLineWebhookEvents ( [ event ] , context ) ;
819-
820- expect ( buildLineMessageContextMock ) . toHaveBeenCalledTimes ( 1 ) ;
821- expect ( processMessage ) . toHaveBeenCalledTimes ( 1 ) ;
822- } ) ;
823-
824- it ( "skips concurrent redeliveries while the first event is still processing" , async ( ) => {
825- let resolveFirst : ( ( ) => void ) | undefined ;
826- const firstDone = new Promise < void > ( ( resolve ) => {
827- resolveFirst = resolve ;
828- } ) ;
829- const processMessage = vi . fn ( async ( ) => {
830- await firstDone ;
831- } ) ;
832- const event = createReplayMessageEvent ( {
833- messageId : "m-inflight" ,
834- groupId : "group-inflight" ,
835- userId : "user-inflight" ,
836- webhookEventId : "evt-inflight-1" ,
837- isRedelivery : true ,
838- } ) ;
839- const { firstRun, secondRun } = await startInflightReplayDuplicate ( { event, processMessage } ) ;
840- resolveFirst ?.( ) ;
841- await Promise . all ( [ firstRun , secondRun ] ) ;
842-
843- expect ( buildLineMessageContextMock ) . toHaveBeenCalledTimes ( 1 ) ;
844- expect ( processMessage ) . toHaveBeenCalledTimes ( 1 ) ;
845- } ) ;
846-
847- it ( "commits in-flight failures so concurrent duplicates do not retry" , async ( ) => {
848- let rejectFirst : ( ( err : Error ) => void ) | undefined ;
849- const firstDone = new Promise < void > ( ( _ , reject ) => {
850- rejectFirst = reject ;
851- } ) ;
852- const processMessage = vi . fn ( async ( ) => {
853- await firstDone ;
854- } ) ;
855- const event = createReplayMessageEvent ( {
856- messageId : "m-inflight-fail" ,
857- groupId : "group-inflight" ,
858- userId : "user-inflight" ,
859- webhookEventId : "evt-inflight-fail-1" ,
860- isRedelivery : true ,
861- } ) ;
862- const { firstRun, secondRun } = await startInflightReplayDuplicate ( { event, processMessage } ) ;
863- const firstFailure = expect ( firstRun ) . rejects . toThrow ( "transient inflight failure" ) ;
864- rejectFirst ?.( new Error ( "transient inflight failure" ) ) ;
865-
866- await firstFailure ;
867- await expect ( secondRun ) . resolves . toBeUndefined ( ) ;
868- expect ( processMessage ) . toHaveBeenCalledTimes ( 1 ) ;
869- } ) ;
870-
871- it ( "deduplicates redeliveries by LINE message id when webhookEventId changes" , async ( ) => {
872- const processMessage = vi . fn ( ) ;
873- const event = {
874- type : "message" ,
875- message : { id : "m-dup-1" , type : "text" , text : "hello" } ,
876- replyToken : "reply-token" ,
877- timestamp : Date . now ( ) ,
878- source : { type : "group" , groupId : "group-dup" , userId : "user-dup" } ,
879- mode : "active" ,
880- webhookEventId : "evt-dup-1" ,
881- deliveryContext : { isRedelivery : false } ,
882- } as MessageEvent ;
883-
884- const context : Parameters < typeof handleLineWebhookEvents > [ 1 ] = {
885- cfg : {
886- channels : { line : { groupPolicy : "allowlist" , groupAllowFrom : [ "user-dup" ] } } ,
887- } ,
888- account : {
889- accountId : "default" ,
890- enabled : true ,
891- channelAccessToken : "token" ,
892- channelSecret : "secret" ,
893- tokenSource : "config" ,
894- config : {
895- groupPolicy : "allowlist" ,
896- groupAllowFrom : [ "user-dup" ] ,
897- groups : { "*" : { requireMention : false } } ,
898- } ,
899- } ,
900- runtime : createRuntime ( ) ,
901- mediaMaxBytes : 1 ,
902- processMessage,
903- replayCache : createLineWebhookReplayCache ( ) ,
904- } ;
905-
906- await handleLineWebhookEvents ( [ event ] , context ) ;
907- await handleLineWebhookEvents (
908- [
909- {
910- ...event ,
911- webhookEventId : "evt-dup-redelivery" ,
912- deliveryContext : { isRedelivery : true } ,
913- } as MessageEvent ,
914- ] ,
915- context ,
916- ) ;
917-
918- expect ( buildLineMessageContextMock ) . toHaveBeenCalledTimes ( 1 ) ;
919- expect ( processMessage ) . toHaveBeenCalledTimes ( 1 ) ;
920- } ) ;
921-
922- it ( "deduplicates postback redeliveries by webhookEventId when replyToken changes" , async ( ) => {
923- const processMessage = vi . fn ( ) ;
924- buildLinePostbackContextMock . mockResolvedValue ( {
925- ctxPayload : { From : "line:user:user-postback" } ,
926- route : { agentId : "default" } ,
927- isGroup : false ,
928- accountId : "default" ,
929- } ) ;
930- const event = {
931- type : "postback" ,
932- postback : { data : "action=confirm" } ,
933- replyToken : "reply-token-1" ,
934- timestamp : Date . now ( ) ,
935- source : { type : "user" , userId : "user-postback" } ,
936- mode : "active" ,
937- webhookEventId : "evt-postback-1" ,
938- deliveryContext : { isRedelivery : false } ,
939- } as PostbackEvent ;
940-
941- const context : Parameters < typeof handleLineWebhookEvents > [ 1 ] = {
942- cfg : { channels : { line : { dmPolicy : "open" , allowFrom : [ "*" ] } } } ,
943- account : {
944- accountId : "default" ,
945- enabled : true ,
946- channelAccessToken : "token" ,
947- channelSecret : "secret" ,
948- tokenSource : "config" ,
949- config : { dmPolicy : "open" , allowFrom : [ "*" ] } ,
950- } ,
951- runtime : createRuntime ( ) ,
952- mediaMaxBytes : 1 ,
953- processMessage,
954- replayCache : createLineWebhookReplayCache ( ) ,
955- } ;
956-
957- await handleLineWebhookEvents ( [ event ] , context ) ;
958- await handleLineWebhookEvents (
959- [
960- {
961- ...event ,
962- replyToken : "reply-token-2" ,
963- deliveryContext : { isRedelivery : true } ,
964- } as PostbackEvent ,
965- ] ,
966- context ,
967- ) ;
968-
969- expect ( buildLinePostbackContextMock ) . toHaveBeenCalledTimes ( 1 ) ;
970- expect ( processMessage ) . toHaveBeenCalledTimes ( 1 ) ;
971- } ) ;
972-
973776 it ( "skips group messages by default when requireMention is not configured" , async ( ) => {
974777 const processMessage = vi . fn ( ) ;
975778 const event = createTestMessageEvent ( {
@@ -1508,29 +1311,5 @@ describe("handleLineWebhookEvents", () => {
15081311 // Should be skipped because there is a non-bot mention and the bot was not mentioned.
15091312 expect ( processMessage ) . not . toHaveBeenCalled ( ) ;
15101313 } ) ;
1511-
1512- it ( "keeps replay cache committed after a non-retryable event failure" , async ( ) => {
1513- const processMessage = vi
1514- . fn ( )
1515- . mockRejectedValueOnce ( new Error ( "transient failure" ) )
1516- . mockResolvedValueOnce ( undefined ) ;
1517- const event = createReplayMessageEvent ( {
1518- messageId : "m-fail-then-retry" ,
1519- groupId : "group-retry" ,
1520- userId : "user-retry" ,
1521- webhookEventId : "evt-fail-then-retry" ,
1522- isRedelivery : false ,
1523- } ) ;
1524- const context = createOpenGroupReplayContext ( processMessage , createLineWebhookReplayCache ( ) ) ;
1525-
1526- await expect ( handleLineWebhookEvents ( [ event ] , context ) ) . rejects . toThrow ( "transient failure" ) ;
1527- await handleLineWebhookEvents ( [ event ] , context ) ;
1528-
1529- expect ( buildLineMessageContextMock ) . toHaveBeenCalledTimes ( 1 ) ;
1530- expect ( processMessage ) . toHaveBeenCalledTimes ( 1 ) ;
1531- expect ( context . runtime . error ) . toHaveBeenCalledWith (
1532- "line: event handler failed: Error: transient failure" ,
1533- ) ;
1534- } ) ;
15351314} ) ;
15361315/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
0 commit comments