77import type { ReplyPayload } from "openclaw/plugin-sdk/reply-dispatch-runtime" ;
88import * as runtimeEnvModule from "openclaw/plugin-sdk/runtime-env" ;
99import { beforeAll , beforeEach , describe , expect , it , vi } from "vitest" ;
10+ import { setReplyPayloadMetadata } from "../../../../src/auto-reply/reply-payload.js" ;
1011import type { DiscordMessagePreflightContext } from "./message-handler.preflight.js" ;
1112
1213const sendMocks = vi . hoisted ( ( ) => ( {
@@ -51,6 +52,16 @@ const editMessageDiscord = deliveryMocks.editMessageDiscord;
5152const deliverDiscordReply = deliveryMocks . deliverDiscordReply ;
5253const createDiscordDraftStream = deliveryMocks . createDiscordDraftStream ;
5354
55+ function createNonTerminalToolWarningPayload ( ) : ReplyPayload {
56+ return setReplyPayloadMetadata (
57+ {
58+ text : "⚠️ 🛠️ `run openclaw definitely-not-a-real-subcommand (agent)` failed" ,
59+ isError : true ,
60+ } ,
61+ { nonTerminalToolErrorWarning : true } ,
62+ ) ;
63+ }
64+
5465vi . mock ( "../send.js" , ( ) => ( {
5566 reactMessageDiscord : async (
5667 channelId : string ,
@@ -2237,14 +2248,11 @@ describe("processDiscordMessage draft streaming", () => {
22372248 expect ( deliverDiscordReply ) . toHaveBeenCalledTimes ( 1 ) ;
22382249 } ) ;
22392250
2240- it ( "keeps finalized previews when later tool warning finals are delivered " , async ( ) => {
2251+ it ( "drops later tool warning finals after preview final replies " , async ( ) => {
22412252 const draftStream = createMockDraftStreamForTest ( ) ;
22422253 dispatchInboundMessage . mockImplementationOnce ( async ( params ?: DispatchInboundParams ) => {
22432254 await params ?. dispatcher . sendFinalReply ( { text : "delivery survived" } ) ;
2244- await params ?. dispatcher . sendFinalReply ( {
2245- text : "⚠️ 🛠️ `run openclaw definitely-not-a-real-subcommand (agent)` failed" ,
2246- isError : true ,
2247- } as never ) ;
2255+ await params ?. dispatcher . sendFinalReply ( createNonTerminalToolWarningPayload ( ) ) ;
22482256 return { queuedFinal : true , counts : { final : 2 , tool : 0 , block : 0 } } ;
22492257 } ) ;
22502258
@@ -2257,6 +2265,44 @@ describe("processDiscordMessage draft streaming", () => {
22572265 expectPreviewEditContent ( "delivery survived" ) ;
22582266 expect ( draftStream . clear ) . not . toHaveBeenCalled ( ) ;
22592267 expect ( draftStream . messageId ( ) ) . toBe ( "preview-1" ) ;
2268+ expect ( deliverDiscordReply ) . not . toHaveBeenCalled ( ) ;
2269+ } ) ;
2270+
2271+ it ( "drops earlier tool warning finals when recovered replies arrive" , async ( ) => {
2272+ const draftStream = createMockDraftStreamForTest ( ) ;
2273+ dispatchInboundMessage . mockImplementationOnce ( async ( params ?: DispatchInboundParams ) => {
2274+ await params ?. dispatcher . sendFinalReply ( createNonTerminalToolWarningPayload ( ) ) ;
2275+ await params ?. dispatcher . sendFinalReply ( { text : "delivery recovered" } ) ;
2276+ return { queuedFinal : true , counts : { final : 2 , tool : 0 , block : 0 } } ;
2277+ } ) ;
2278+
2279+ const ctx = await createAutomaticSourceDeliveryContext ( {
2280+ discordConfig : { streamMode : "partial" , maxLinesPerMessage : 5 } ,
2281+ } ) ;
2282+
2283+ await runProcessDiscordMessage ( ctx ) ;
2284+
2285+ expectPreviewEditContent ( "delivery recovered" ) ;
2286+ expect ( draftStream . clear ) . not . toHaveBeenCalled ( ) ;
2287+ expect ( draftStream . messageId ( ) ) . toBe ( "preview-1" ) ;
2288+ expect ( deliverDiscordReply ) . not . toHaveBeenCalled ( ) ;
2289+ } ) ;
2290+
2291+ it ( "delivers tool warning finals when no recovered reply is available" , async ( ) => {
2292+ const draftStream = createMockDraftStreamForTest ( ) ;
2293+ dispatchInboundMessage . mockImplementationOnce ( async ( params ?: DispatchInboundParams ) => {
2294+ await params ?. dispatcher . sendFinalReply ( createNonTerminalToolWarningPayload ( ) ) ;
2295+ return { queuedFinal : true , counts : { final : 1 , tool : 0 , block : 0 } } ;
2296+ } ) ;
2297+
2298+ const ctx = await createAutomaticSourceDeliveryContext ( {
2299+ discordConfig : { streamMode : "partial" , maxLinesPerMessage : 5 } ,
2300+ } ) ;
2301+
2302+ await runProcessDiscordMessage ( ctx ) ;
2303+
2304+ expect ( editMessageDiscord ) . not . toHaveBeenCalled ( ) ;
2305+ expect ( draftStream . clear ) . toHaveBeenCalledTimes ( 1 ) ;
22602306 expect ( deliverDiscordReply ) . toHaveBeenCalledTimes ( 1 ) ;
22612307 expect ( firstMockArg ( deliverDiscordReply , "deliverDiscordReply" ) ) . toMatchObject ( {
22622308 replies : [
@@ -2268,14 +2314,14 @@ describe("processDiscordMessage draft streaming", () => {
22682314 } ) ;
22692315 } ) ;
22702316
2271- it ( "keeps draft previews when tool warning finals arrive before recovered replies" , async ( ) => {
2317+ it ( "keeps mutating tool warning finals after successful-looking replies" , async ( ) => {
22722318 const draftStream = createMockDraftStreamForTest ( ) ;
22732319 dispatchInboundMessage . mockImplementationOnce ( async ( params ?: DispatchInboundParams ) => {
2320+ await params ?. dispatcher . sendFinalReply ( { text : "Done." } ) ;
22742321 await params ?. dispatcher . sendFinalReply ( {
2275- text : "⚠️ 🛠️ `run openclaw definitely-not-a-real-subcommand (agent)` failed" ,
2322+ text : "⚠️ 🛠️ `write file (agent)` failed" ,
22762323 isError : true ,
22772324 } as never ) ;
2278- await params ?. dispatcher . sendFinalReply ( { text : "delivery recovered" } ) ;
22792325 return { queuedFinal : true , counts : { final : 2 , tool : 0 , block : 0 } } ;
22802326 } ) ;
22812327
@@ -2285,14 +2331,13 @@ describe("processDiscordMessage draft streaming", () => {
22852331
22862332 await runProcessDiscordMessage ( ctx ) ;
22872333
2288- expectPreviewEditContent ( "delivery recovered " ) ;
2334+ expectPreviewEditContent ( "Done. " ) ;
22892335 expect ( draftStream . clear ) . not . toHaveBeenCalled ( ) ;
2290- expect ( draftStream . messageId ( ) ) . toBe ( "preview-1" ) ;
22912336 expect ( deliverDiscordReply ) . toHaveBeenCalledTimes ( 1 ) ;
22922337 expect ( firstMockArg ( deliverDiscordReply , "deliverDiscordReply" ) ) . toMatchObject ( {
22932338 replies : [
22942339 {
2295- text : "⚠️ 🛠️ `run openclaw definitely-not-a-real-subcommand (agent)` failed" ,
2340+ text : "⚠️ 🛠️ `write file (agent)` failed" ,
22962341 isError : true ,
22972342 } ,
22982343 ] ,
@@ -2448,17 +2493,14 @@ describe("processDiscordMessage draft streaming", () => {
24482493 expectPreviewEditContent ( "done" ) ;
24492494 } ) ;
24502495
2451- it ( "keeps finalized progress previews when later tool warning finals are delivered " , async ( ) => {
2496+ it ( "drops later tool warning finals after progress preview final replies " , async ( ) => {
24522497 const draftStream = createMockDraftStreamForTest ( ) ;
24532498
24542499 dispatchInboundMessage . mockImplementationOnce ( async ( params ?: DispatchInboundParams ) => {
24552500 await params ?. replyOptions ?. onToolStart ?.( { name : "exec" , phase : "start" } ) ;
24562501 await params ?. replyOptions ?. onItemEvent ?.( { progressText : "exec done" } ) ;
24572502 await params ?. dispatcher . sendFinalReply ( { text : "delivery survived" } ) ;
2458- await params ?. dispatcher . sendFinalReply ( {
2459- text : "⚠️ 🛠️ `run openclaw definitely-not-a-real-subcommand (agent)` failed" ,
2460- isError : true ,
2461- } as never ) ;
2503+ await params ?. dispatcher . sendFinalReply ( createNonTerminalToolWarningPayload ( ) ) ;
24622504 return { queuedFinal : true , counts : { final : 2 , tool : 0 , block : 0 } } ;
24632505 } ) ;
24642506
@@ -2479,15 +2521,7 @@ describe("processDiscordMessage draft streaming", () => {
24792521 expectPreviewEditContent ( "delivery survived" ) ;
24802522 expect ( draftStream . clear ) . not . toHaveBeenCalled ( ) ;
24812523 expect ( draftStream . messageId ( ) ) . toBe ( "preview-1" ) ;
2482- expect ( deliverDiscordReply ) . toHaveBeenCalledTimes ( 1 ) ;
2483- expect ( firstMockArg ( deliverDiscordReply , "deliverDiscordReply" ) ) . toMatchObject ( {
2484- replies : [
2485- {
2486- text : "⚠️ 🛠️ `run openclaw definitely-not-a-real-subcommand (agent)` failed" ,
2487- isError : true ,
2488- } ,
2489- ] ,
2490- } ) ;
2524+ expect ( deliverDiscordReply ) . not . toHaveBeenCalled ( ) ;
24912525 } ) ;
24922526
24932527 it ( "uses raw tool-progress detail in Discord progress drafts" , async ( ) => {
0 commit comments