beeos-claw 0.5.50 → 0.5.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveRuntimeToolArguments,resolveRuntimeToolCallId,resolveRuntimeToolName}from"./tool-envelope-metadata.js";const isNonEmptyString=e=>"string"==typeof e&&e.length>0,extractBodyUpdate=(e,t)=>{if(!isNonEmptyString(t))return{kind:"noop"};if(t===e.text)return{kind:"noop"};if(t.startsWith(e.text)){const n=t.slice(e.text.length);return e.text=t,{kind:"append",text:n}}return e.text.startsWith(t)?{kind:"noop"}:(e.text=t,{kind:"replace",text:t})};export function createImReplySink(e){const t={text:""};let n,r="",o="";const s=new Map,i=new Map,a=[];let
|
|
1
|
+
import{resolveRuntimeToolArguments,resolveRuntimeToolCallId,resolveRuntimeToolName}from"./tool-envelope-metadata.js";const isNonEmptyString=e=>"string"==typeof e&&e.length>0,extractBodyUpdate=(e,t)=>{if(!isNonEmptyString(t))return{kind:"noop"};if(t===e.text)return{kind:"noop"};if(t.startsWith(e.text)){const n=t.slice(e.text.length);return e.text=t,{kind:"append",text:n}}return e.text.startsWith(t)?{kind:"noop"}:(e.text=t,{kind:"replace",text:t})};export function createImReplySink(e){const t={text:""};let n,r="",o="";const s=new Map,i=new Map,a=[],l=[];let d=0,u=0;const m=(t,n,r)=>{const o=u;return e.stream.appendToolUse(t,n,r,"streaming"),u+=1,o},g=()=>(d+=1,`tool_${e.requestId}_${d}`),c=e=>{const t=a.indexOf(e);t>=0&&a.splice(t,1)},p=e=>{const t=e?a.filter(t=>s.get(t)?.name===e):a;return 1===t.length?t[0]:void 0},h=e=>{const t=l.map((e,t)=>({hint:e,index:t})).filter(({hint:t})=>!e||t.name===e);if(1!==t.length)return;const[{hint:n,index:r}]=t;return l.splice(r,1),n},f=(e,t)=>{i.set(e,t)},y=e=>i.get(e)??e,I=e=>null!=e&&("object"!=typeof e||(Array.isArray(e)?e.length>0:Object.keys(e).length>0)),v=(t,n,r,o)=>{const i=s.get(t);if(i){const s="tool"===i.name&&"tool"!==n?n:i.name,l=!I(i.arguments)&&I(r)?r:i.arguments;return s===i.name&&l===i.arguments||(e.stream.replacePart(i.partIndex,{type:"tool_use",id:i.id,name:s,arguments:l,state:i.state}),i.name=s,i.arguments=l),o&&!a.includes(t)&&"streaming"===i.state&&a.push(t),i}const l={id:t,name:n,arguments:r,partIndex:m(t,n,r),state:"streaming",resultSignatures:new Set,itemEventObserved:!1,toolStartObserved:!1};return s.set(t,l),o&&a.push(t),l},$=t=>{"done"!==t.state&&(e.stream.replacePart(t.partIndex,{type:"tool_use",id:t.id,name:t.name,arguments:t.arguments,state:"done"}),t.state="done")},x=(t,n,r)=>{const o=`${r?"1":"0"}:${"string"==typeof n?n:JSON.stringify(n)}`;t.resultSignatures.has(o)||(((t,n,r)=>{e.stream.appendToolResult(t,n,r),u+=1})(t.id,n,r),t.resultSignatures.add(o))},b=(e,t,n)=>{const r=i.get(e);if(r)return v(r,t??"tool",n??{},!1);if(s.has(e))return v(e,t??"tool",n??{},!1);const o=p(t);return o?(f(e,o),v(o,t??"tool",n??{},!1)):v(e,t??"tool",n??{},!1)},k=(e,t)=>{const n=i.get(e);if(n)return v(n,t,{},!1);if(s.has(e))return v(e,t,{},!1);const r=(e=>{const t=a.filter(t=>{const n=s.get(t);return!(void 0===n||e&&n.name!==e||n.itemEventObserved)});return 1===t.length?t[0]:void 0})(t);if(r)return f(e,r),v(r,t,{},!1);const o=h(t);return v(e,t,o?.arguments??{},!1)},S={tool:0,block:0,final:0};let _=!1;const R=(t,n)=>{e.callbackQueue.enqueueSync(()=>{if((t=>e.abortSignal.aborted?(e.logger.debug?.(`[im-sink] ${t} ignored after abort requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1):e.stream.isTerminated?(e.logger.debug?.(`[im-sink] ${t} ignored after stream_terminated requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1):!_||(e.logger.debug?.(`[im-sink] ${t} ignored after mark_complete requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1))(t))try{n()}catch(n){const r=n instanceof Error?n.message:String(n);e.logger.warn?.(`[im-sink] ${t} mapping failed requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId} error=${r}`)}})};return{dispatcher:{sendToolResult:t=>(R("tool_result",()=>{const n=resolveRuntimeToolCallId(t),r=resolveRuntimeToolName(t),o=n?void 0:r?p(r):p(),i=n||o?void 0:h(r);if(!n&&!o&&!i)return void e.logger.debug?.(`[im-sink] ambiguous id-less tool result ignored requestId=${e.requestId} channel=${e.channelId} tool=${r??"unknown"} pending=${a.length}`);const l=n?b(n,r,resolveRuntimeToolArguments(t)):v(o??g(),r??s.get(o??"")?.name??i.name,I(resolveRuntimeToolArguments(t))?resolveRuntimeToolArguments(t):i?.arguments??{},!1);S.tool+=1,$(l),x(l,(e=>{const t=[e.mediaUrl,...e.mediaUrls??[]].filter(e=>isNonEmptyString(e));return isNonEmptyString(e.text)&&t.length>0?{text:e.text,media_urls:t}:isNonEmptyString(e.text)?e.text:1===t.length?t[0]:t.length>1?{media_urls:t}:!0===e.isError?"Tool execution failed.":""})(t),!0===t.isError),c(l.id)}),!0),sendBlockReply:n=>(R("block_reply",()=>{S.block+=1,isNonEmptyString(n.text)&&(e.stream.setBody(n.text),t.text=n.text)}),!0),sendFinalReply:n=>(R("final_reply",()=>{S.final+=1,isNonEmptyString(n.text)&&(e.stream.setBody(n.text),t.text=n.text)}),!0),waitForIdle:()=>e.callbackQueue.waitForIdle(),getQueuedCounts:()=>({...S}),markComplete:t=>{if(1===l.length){const e=l.shift();v(g(),e.name,e.arguments,!1)}for(const n of s.values())try{"success"===t&&$(n),c(n.id)}catch(t){const r=t instanceof Error?t.message:String(t);e.logger.warn?.(`[im-sink] tool finalization failed requestId=${e.requestId} toolUseId=${n.id} error=${r}`)}l.length=0,_=!0}},replyOptions:{abortSignal:e.abortSignal,onAgentRunStart:t=>{e.logger.info?.(`[im-sink] run_start requestId=${e.requestId} runId=${t} channel=${e.channelId} in_reply_to=${e.inboundMessageId} origin=${e.origin??"im"}`)},onPartialReply:n=>{R("partial",()=>{const r=extractBodyUpdate(t,n.text);switch(r.kind){case"append":e.stream.appendBody(r.text);break;case"replace":e.stream.setBody(r.text)}})},onReasoningStream:t=>{R("reasoning",()=>{if(!isNonEmptyString(t.text))return;if(!n&&(n=e.openReasoningStream?.(),!n))return;const s=t.text;""===o&&""!==r&&(n.appendBody("\n"),r+="\n"),s!==o&&(s.startsWith(o)?(n.appendBody(s.slice(o.length)),o=s):o.startsWith(s)||(n.setBody(r+s),o=s))})},onReasoningEnd:()=>{R("reasoning_end",()=>{var t;""!==o&&(t=o,e.stream.appendThinking(t,"done"),u+=1,r+=o,o="")})},onAssistantMessageStart:()=>{e.logger.debug?.(`[im-sink] assistant_message_start requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`)},onToolStart:t=>{R("tool_start",()=>{const n="string"==typeof t.phase?t.phase:"start",r=resolveRuntimeToolName(t)??"tool",o=resolveRuntimeToolCallId(t);if("update"===n){if(o){const e=y(o);s.has(e)&&v(e,r,resolveRuntimeToolArguments(t),!0)}return}const i=resolveRuntimeToolArguments(t);if(o){const e=b(o,r,i);return e.toolStartObserved=!0,void(a.includes(e.id)||"streaming"!==e.state||a.push(e.id))}const d=(u=r,a.map(e=>s.get(e)).filter(e=>void 0!==e&&(!u||e.name===u)&&e.itemEventObserved));var u;if(1===d.length){return void(v(d[0].id,r,i,!0).toolStartObserved=!0)}d.length>1?e.logger.debug?.(`[im-sink] ambiguous id-less tool start ignored requestId=${e.requestId} channel=${e.channelId} tool=${r} candidates=${d.length}`):l.push({name:r,arguments:i})})},onItemEvent:e=>{R("item_event",()=>{const t="string"==typeof e.kind?e.kind.toLowerCase():"";if(t&&"tool"!==t)return;const n="string"==typeof e.phase?e.phase.toLowerCase():"update",r=resolveRuntimeToolCallId(e)??("string"==typeof e.itemId&&e.itemId.trim()?e.itemId.trim():void 0),o=resolveRuntimeToolName(e)??("string"==typeof e.title&&e.title.trim()?e.title.trim():void 0);if(!r||!o)return;if("update"===n){const e=y(r),t=s.get(e);return void(t&&(v(t.id,o,{},!0).itemEventObserved=!0))}const i=k(r,o);if(i.itemEventObserved=!0,a.includes(i.id)||"streaming"!==i.state||a.push(i.id),"end"!==n)return;const l="string"==typeof e.status?e.status.toLowerCase():"",d="failed"===l||"error"===l||"cancelled"===l,u=isNonEmptyString(e.summary)?e.summary:isNonEmptyString(e.meta)?e.meta:"";$(i),u&&x(i,u,d),c(i.id)})}}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{describe,it,expect,beforeEach}from"vitest";import{SerializedCallbackQueue}from"./callback-queue.js";import{createImReplySink}from"./im-reply-sink.js";class FakeMessageStream{calls=[];parts=[];body="";isTerminated=!1;appendBody(e){if(this.isTerminated)throw new Error("terminated");this.body+=e,this.calls.push({type:"appendBody",chunk:e})}setBody(e){if(this.isTerminated)throw new Error("terminated");this.body=e,this.calls.push({type:"setBody",text:e})}addPart(e){if(this.isTerminated)throw new Error("terminated");this.parts.push(e),this.calls.push({type:"addPart",part:e})}replacePart(e,t){if(this.isTerminated)throw new Error("terminated");this.parts[e]=t,this.calls.push({type:"replacePart",index:e,part:t})}appendThinking(e,t="done"){if(this.isTerminated)throw new Error("terminated");this.parts.push({type:"thinking",text:e,state:t}),this.calls.push({type:"appendThinking",text:e,state:t})}appendToolUse(e,t,a,o="streaming"){if(this.isTerminated)throw new Error("terminated");this.parts.push({type:"tool_use",id:e,name:t,arguments:a,state:o}),this.calls.push({type:"appendToolUse",id:e,name:t,args:a})}appendToolResult(e,t,a=!1){if(this.isTerminated)throw new Error("terminated");this.parts.push({type:"tool_result",tool_use_id:e,content:t,...a?{is_error:!0}:{}}),this.calls.push({type:"appendToolResult",toolUseId:e,content:t,...a?{isError:!0}:{}})}}const stubLogger=()=>{const e=[],t=t=>a=>e.push(`[${t}] ${a}`);return{lines:e,debug:t("debug"),info:t("info"),warn:t("warn"),error:t("error")}},buildDeps=(e,t,a,o)=>({stream:e,openReasoningStream:o?()=>o:void 0,abortSignal:t,callbackQueue:a,logger:stubLogger(),requestId:"im_req_test",channelId:"ch_test",inboundMessageId:"msg_test",origin:"im"});describe("createImReplySink",()=>{let e,t,a,o;beforeEach(()=>{e=new FakeMessageStream,t=new FakeMessageStream,a=new AbortController,o=new SerializedCallbackQueue}),it("onPartialReply: extracts delta from cumulative snapshot and appendBody's the suffix",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onPartialReply({text:"Hello"}),t.replyOptions.onPartialReply({text:"Hello, world"}),t.replyOptions.onPartialReply({text:"Hello, world!"}),await o.waitForIdle(),expect(e.calls).toEqual([{type:"appendBody",chunk:"Hello"},{type:"appendBody",chunk:", world"},{type:"appendBody",chunk:"!"}]),expect(e.body).toBe("Hello, world!")}),it("onPartialReply: drops shorter-prefix snapshots (runtime echo of older state)",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onPartialReply({text:"abcdef"}),t.replyOptions.onPartialReply({text:"abc"}),t.replyOptions.onPartialReply({text:"abcdefg"}),await o.waitForIdle(),expect(e.calls).toEqual([{type:"appendBody",chunk:"abcdef"},{type:"appendBody",chunk:"g"}])}),it("onPartialReply: treats non-prefix snapshot as a reset (setBody replace, NOT append)",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onPartialReply({text:"alpha"}),t.replyOptions.onPartialReply({text:"beta"}),await o.waitForIdle(),expect(e.calls).toEqual([{type:"appendBody",chunk:"alpha"},{type:"setBody",text:"beta"}]),expect(e.body).toBe("beta")}),it("sendFinalReply: setBody to the canonical full text",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onPartialReply({text:"partial..."}),t.dispatcher.sendFinalReply({text:"canonical final body"}),await o.waitForIdle(),expect(e.calls.at(-1)).toEqual({type:"setBody",text:"canonical final body"}),expect(e.body).toBe("canonical final body"),expect(t.dispatcher.getQueuedCounts()).toEqual({tool:0,block:0,final:1})}),it("sendBlockReply: setBody for finalized block (block_reply semantics)",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.dispatcher.sendBlockReply({text:"block contents"}),await o.waitForIdle(),expect(e.calls).toEqual([{type:"setBody",text:"block contents"}]),expect(t.dispatcher.getQueuedCounts()).toEqual({tool:0,block:1,final:0})}),it("onReasoningStream: appends cumulative-snapshot deltas onto the agent_reasoning row (NOT the reply row)",async()=>{const l=createImReplySink(buildDeps(e,a.signal,o,t));l.replyOptions.onReasoningStream({text:"thinking step 1"}),l.replyOptions.onReasoningStream({text:"thinking step 1 and 2"}),l.replyOptions.onReasoningEnd(),await o.waitForIdle(),expect(t.calls).toEqual([{type:"appendBody",chunk:"thinking step 1"},{type:"appendBody",chunk:" and 2"}]),expect(t.body).toBe("thinking step 1 and 2"),expect(e.calls).toEqual([{type:"appendThinking",text:"thinking step 1 and 2",state:"done"}])}),it("onReasoningStream → onReasoningEnd → onReasoningStream: second burst appends after a newline; two reply thinking parts",async()=>{const l=createImReplySink(buildDeps(e,a.signal,o,t));l.replyOptions.onReasoningStream({text:"first"}),l.replyOptions.onReasoningEnd(),l.replyOptions.onReasoningStream({text:"second"}),l.replyOptions.onReasoningEnd(),await o.waitForIdle(),expect(t.calls).toEqual([{type:"appendBody",chunk:"first"},{type:"appendBody",chunk:"\n"},{type:"appendBody",chunk:"second"}]),expect(t.body).toBe("first\nsecond"),expect(e.calls).toEqual([{type:"appendThinking",text:"first",state:"done"},{type:"appendThinking",text:"second",state:"done"}])}),it("onReasoningStream: non-prefix branch mid-burst replaces the row (committed prefix + new text)",async()=>{const l=createImReplySink(buildDeps(e,a.signal,o,t));l.replyOptions.onReasoningStream({text:"alpha"}),l.replyOptions.onReasoningStream({text:"beta gamma"}),await o.waitForIdle(),expect(t.calls).toEqual([{type:"appendBody",chunk:"alpha"},{type:"setBody",text:"beta gamma"}]),expect(t.body).toBe("beta gamma")}),it("onReasoningStream without an openReasoningStream factory: reasoning chunks are dropped",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onReasoningStream({text:"ignored"}),t.replyOptions.onReasoningEnd(),await o.waitForIdle(),expect(e.calls).toEqual([])}),it("uses the runtime toolCallId and completes tool_use before appending its result",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"calc",toolCallId:"call_calc",arguments:{expression:"6*7"}}),t.dispatcher.sendToolResult({text:"42",toolCallId:"call_calc"}),await o.waitForIdle(),expect(e.calls).toEqual([{type:"appendToolUse",id:"call_calc",name:"calc",args:{expression:"6*7"}},{type:"replacePart",index:0,part:{type:"tool_use",id:"call_calc",name:"calc",arguments:{expression:"6*7"},state:"done"}},{type:"appendToolResult",toolUseId:"call_calc",content:"42"}]),expect(t.dispatcher.getQueuedCounts()).toEqual({tool:1,block:0,final:0})}),it("resolves tool_call_id from channelData.im before generic channelData",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"nested",channelData:{tool_call_id:"generic_id",im:{tool_call_id:"im_id"}}}),t.dispatcher.sendToolResult({text:"nested result",channelData:{tool_call_id:"generic_id",im:{toolCallId:"im_id"}}}),await o.waitForIdle(),expect(e.calls[0]).toMatchObject({type:"appendToolUse",id:"im_id"}),expect(e.calls.at(-1)).toEqual({type:"appendToolResult",toolUseId:"im_id",content:"nested result"})}),it("drops id-less results when multiple pending tools make correlation ambiguous",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"first"}),t.replyOptions.onToolStart({name:"second"}),t.dispatcher.sendToolResult({text:"first result"}),t.dispatcher.sendToolResult({text:"second result"}),await o.waitForIdle(),expect(e.calls.filter(e=>"appendToolResult"===e.type)).toEqual([]),t.dispatcher.markComplete("success"),expect(e.calls.filter(e=>"replacePart"===e.type)).toHaveLength(2)}),it("preserves an id-less result when exactly one pending tool makes correlation unambiguous",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"read",args:{path:"/tmp/a"}}),t.dispatcher.sendToolResult({text:"file contents",mediaUrls:["beeos-file://preview"],isError:!0}),await o.waitForIdle(),expect(e.calls.filter(e=>"appendToolUse"===e.type)).toHaveLength(1),expect(e.calls.filter(e=>"appendToolResult"===e.type)).toEqual([{type:"appendToolResult",toolUseId:"tool_im_req_test_1",content:{text:"file contents",media_urls:["beeos-file://preview"]},isError:!0}])}),it("does not attach a named result to the only pending tool when names disagree",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"read",phase:"start"}),t.dispatcher.sendToolResult({text:"opened",toolName:"browser"}),await o.waitForIdle(),expect(e.calls.filter(e=>"appendToolUse"===e.type)).toHaveLength(1),expect(e.calls.filter(e=>"appendToolResult"===e.type)).toEqual([])}),it("matches the OpenClaw 2026.5.22 item-start then id-less tool-start order without duplicate cards",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onItemEvent({kind:"tool",itemId:"tool:call1",name:"read",phase:"start"}),t.replyOptions.onToolStart({name:"read",args:{path:"/tmp/a"},phase:"start"}),t.replyOptions.onItemEvent({kind:"command",itemId:"command:call1",name:"cat /tmp/a",phase:"start"}),t.dispatcher.sendToolResult({text:"file contents",toolName:"read"}),await o.waitForIdle(),expect(e.calls.filter(e=>"appendToolUse"===e.type)).toEqual([{type:"appendToolUse",id:"tool:call1",name:"read",args:{}}]),expect(e.calls.filter(e=>"replacePart"===e.type)).toContainEqual({type:"replacePart",index:0,part:{type:"tool_use",id:"tool:call1",name:"read",arguments:{path:"/tmp/a"},state:"streaming"}}),expect(e.calls.filter(e=>"appendToolResult"===e.type)).toEqual([{type:"appendToolResult",toolUseId:"tool:call1",content:"file contents"}])}),it("ignores update and non-tool item events before creating any tool entity",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onItemEvent({kind:"tool",itemId:"tool:unknown",name:"read",phase:"update"}),t.replyOptions.onItemEvent({kind:"command",itemId:"command:unknown",name:"cat file",phase:"end",summary:"output"}),await o.waitForIdle(),expect(e.calls).toEqual([])}),it("keeps concurrent stable item starts with the same tool name distinct",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onItemEvent({kind:"tool",itemId:"tool:call1",name:"read",phase:"start"}),t.replyOptions.onItemEvent({kind:"tool",itemId:"tool:call2",name:"read",phase:"start"}),await o.waitForIdle(),expect(e.calls.filter(e=>"appendToolUse"===e.type)).toEqual([{type:"appendToolUse",id:"tool:call1",name:"read",args:{}},{type:"appendToolUse",id:"tool:call2",name:"read",args:{}}])}),it("uses stable item events to correlate concurrent tools that finish out of order",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"exec",phase:"start"}),t.replyOptions.onToolStart({name:"process",phase:"start"}),t.replyOptions.onItemEvent({itemId:"item-process",name:"process",phase:"end",status:"completed",summary:"process done"}),t.replyOptions.onItemEvent({itemId:"item-exec",name:"exec",phase:"end",status:"completed",summary:"exec done"}),await o.waitForIdle(),expect(e.calls.filter(e=>"appendToolResult"===e.type)).toEqual([{type:"appendToolResult",toolUseId:"tool_im_req_test_2",content:"process done"},{type:"appendToolResult",toolUseId:"tool_im_req_test_1",content:"exec done"}])}),it("reuses an explicit tool id for multiple result events without duplicating tool_use",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"process",toolCallId:"call_process"}),t.dispatcher.sendToolResult({text:"running",toolCallId:"call_process"}),t.dispatcher.sendToolResult({text:"complete",tool_call_id:"call_process"}),t.dispatcher.sendToolResult({text:"complete",tool_call_id:"call_process"}),await o.waitForIdle(),expect(e.calls.filter(e=>"appendToolUse"===e.type)).toHaveLength(1),expect(e.calls.filter(e=>"replacePart"===e.type)).toHaveLength(1),expect(e.calls.filter(e=>"appendToolResult"===e.type)).toEqual([{type:"appendToolResult",toolUseId:"call_process",content:"running"},{type:"appendToolResult",toolUseId:"call_process",content:"complete"}])}),it("creates a matching tool_use when an explicit result arrives before start",async()=>{createImReplySink(buildDeps(e,a.signal,o)).dispatcher.sendToolResult({text:"early result",toolCallId:"call_early",toolName:"early-tool"}),await o.waitForIdle(),expect(e.calls).toEqual([{type:"appendToolUse",id:"call_early",name:"early-tool",args:{}},{type:"replacePart",index:0,part:{type:"tool_use",id:"call_early",name:"early-tool",arguments:{},state:"done"}},{type:"appendToolResult",toolUseId:"call_early",content:"early result"}])}),it("ignores an orphan legacy summary instead of inventing a generic tool card",async()=>{createImReplySink(buildDeps(e,a.signal,o)).dispatcher.sendToolResult({text:"orphan",isError:!0}),await o.waitForIdle(),expect(e.calls).toEqual([])}),it("ignores id-less update callbacks and aliases a later item id to the original card",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"browser",phase:"start",args:{action:"open"}}),t.replyOptions.onToolStart({name:"browser",phase:"update"}),t.replyOptions.onItemEvent({itemId:"item-browser",name:"browser",phase:"end",status:"completed",summary:"opened"}),await o.waitForIdle(),expect(e.calls.filter(e=>"appendToolUse"===e.type)).toHaveLength(1),expect(e.calls.filter(e=>"appendToolResult"===e.type)).toEqual([{type:"appendToolResult",toolUseId:"tool_im_req_test_1",content:"opened"}])}),it("preserves callback order across interleaved partial / reasoning / tool events",async()=>{const l=createImReplySink(buildDeps(e,a.signal,o,t));l.replyOptions.onPartialReply({text:"A"}),l.replyOptions.onReasoningStream({text:"think 1"}),l.replyOptions.onPartialReply({text:"AB"}),l.replyOptions.onToolStart({name:"tool1",phase:"start"}),l.replyOptions.onItemEvent({itemId:"item-tool1",name:"tool1",phase:"end",status:"completed",summary:"result1"}),l.replyOptions.onPartialReply({text:"ABC"}),l.replyOptions.onReasoningEnd(),l.dispatcher.sendFinalReply({text:"ABC final"}),await o.waitForIdle();const n=e.calls.map(e=>e.type);expect(n).toEqual(["appendBody","appendBody","appendToolUse","replacePart","appendToolResult","appendBody","appendThinking","setBody"]),expect(t.calls).toEqual([{type:"appendBody",chunk:"think 1"}])}),it("aborted signal suppresses ALL further dispatcher / replyOptions calls",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onPartialReply({text:"before"}),await o.waitForIdle(),a.abort("test_abort"),t.replyOptions.onPartialReply({text:"before-after"}),t.dispatcher.sendFinalReply({text:"should-not-fire"}),t.replyOptions.onReasoningStream({text:"blocked"}),t.replyOptions.onToolStart({name:"blocked"}),t.dispatcher.sendToolResult({text:"blocked"}),await o.waitForIdle(),expect(e.calls).toEqual([{type:"appendBody",chunk:"before"}])}),it("markComplete suppresses subsequent callbacks (idempotent terminal)",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onPartialReply({text:"live"}),t.dispatcher.markComplete("success"),t.replyOptions.onPartialReply({text:"live-after"}),t.dispatcher.sendFinalReply({text:"after-complete"}),await o.waitForIdle(),expect(e.calls).toEqual([{type:"appendBody",chunk:"live"}])}),it("does not mark unfinished tools done when the enclosing run fails",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"browser",phase:"start"}),t.dispatcher.markComplete("failure"),await o.waitForIdle(),expect(e.calls).toEqual([{type:"appendToolUse",id:"tool_im_req_test_1",name:"browser",args:{}}]),expect(e.parts).toEqual([{type:"tool_use",id:"tool_im_req_test_1",name:"browser",arguments:{},state:"streaming"}])}),it("getQueuedCounts returns a snapshot copy (mutation does not leak)",()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.dispatcher.getQueuedCounts().tool=999,expect(t.dispatcher.getQueuedCounts()).toEqual({tool:0,block:0,final:0})}),it("when the stream is already terminated, callbacks are silently dropped",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));e.isTerminated=!0,t.replyOptions.onPartialReply({text:"dropped"}),t.dispatcher.sendFinalReply({text:"dropped"}),await o.waitForIdle(),expect(e.calls).toEqual([])}),it("sink callback throwing inside guarded does not poison the queue (logged as warn)",async()=>{const t=buildDeps(e,a.signal,o),l=createImReplySink(t),n=e.appendBody.bind(e);let s=!0;e.appendBody=e=>{if(s)throw s=!1,new Error("synthetic appendBody failure");return n(e)},l.replyOptions.onPartialReply({text:"trigger"}),l.replyOptions.onPartialReply({text:"trigger ok"}),await o.waitForIdle(),expect(e.body).toBe(" ok"),expect(t.logger.lines.some(e=>e.includes("[im-sink] partial mapping failed"))).toBe(!0)})});
|
|
1
|
+
import{describe,it,expect,beforeEach}from"vitest";import{SerializedCallbackQueue}from"./callback-queue.js";import{createImReplySink}from"./im-reply-sink.js";class FakeMessageStream{calls=[];parts=[];body="";isTerminated=!1;appendBody(e){if(this.isTerminated)throw new Error("terminated");this.body+=e,this.calls.push({type:"appendBody",chunk:e})}setBody(e){if(this.isTerminated)throw new Error("terminated");this.body=e,this.calls.push({type:"setBody",text:e})}addPart(e){if(this.isTerminated)throw new Error("terminated");this.parts.push(e),this.calls.push({type:"addPart",part:e})}replacePart(e,t){if(this.isTerminated)throw new Error("terminated");this.parts[e]=t,this.calls.push({type:"replacePart",index:e,part:t})}appendThinking(e,t="done"){if(this.isTerminated)throw new Error("terminated");this.parts.push({type:"thinking",text:e,state:t}),this.calls.push({type:"appendThinking",text:e,state:t})}appendToolUse(e,t,a,o="streaming"){if(this.isTerminated)throw new Error("terminated");this.parts.push({type:"tool_use",id:e,name:t,arguments:a,state:o}),this.calls.push({type:"appendToolUse",id:e,name:t,args:a})}appendToolResult(e,t,a=!1){if(this.isTerminated)throw new Error("terminated");this.parts.push({type:"tool_result",tool_use_id:e,content:t,...a?{is_error:!0}:{}}),this.calls.push({type:"appendToolResult",toolUseId:e,content:t,...a?{isError:!0}:{}})}}const stubLogger=()=>{const e=[],t=t=>a=>e.push(`[${t}] ${a}`);return{lines:e,debug:t("debug"),info:t("info"),warn:t("warn"),error:t("error")}},buildDeps=(e,t,a,o)=>({stream:e,openReasoningStream:o?()=>o:void 0,abortSignal:t,callbackQueue:a,logger:stubLogger(),requestId:"im_req_test",channelId:"ch_test",inboundMessageId:"msg_test",origin:"im"});describe("createImReplySink",()=>{let e,t,a,o;beforeEach(()=>{e=new FakeMessageStream,t=new FakeMessageStream,a=new AbortController,o=new SerializedCallbackQueue}),it("onPartialReply: extracts delta from cumulative snapshot and appendBody's the suffix",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onPartialReply({text:"Hello"}),t.replyOptions.onPartialReply({text:"Hello, world"}),t.replyOptions.onPartialReply({text:"Hello, world!"}),await o.waitForIdle(),expect(e.calls).toEqual([{type:"appendBody",chunk:"Hello"},{type:"appendBody",chunk:", world"},{type:"appendBody",chunk:"!"}]),expect(e.body).toBe("Hello, world!")}),it("onPartialReply: drops shorter-prefix snapshots (runtime echo of older state)",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onPartialReply({text:"abcdef"}),t.replyOptions.onPartialReply({text:"abc"}),t.replyOptions.onPartialReply({text:"abcdefg"}),await o.waitForIdle(),expect(e.calls).toEqual([{type:"appendBody",chunk:"abcdef"},{type:"appendBody",chunk:"g"}])}),it("onPartialReply: treats non-prefix snapshot as a reset (setBody replace, NOT append)",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onPartialReply({text:"alpha"}),t.replyOptions.onPartialReply({text:"beta"}),await o.waitForIdle(),expect(e.calls).toEqual([{type:"appendBody",chunk:"alpha"},{type:"setBody",text:"beta"}]),expect(e.body).toBe("beta")}),it("sendFinalReply: setBody to the canonical full text",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onPartialReply({text:"partial..."}),t.dispatcher.sendFinalReply({text:"canonical final body"}),await o.waitForIdle(),expect(e.calls.at(-1)).toEqual({type:"setBody",text:"canonical final body"}),expect(e.body).toBe("canonical final body"),expect(t.dispatcher.getQueuedCounts()).toEqual({tool:0,block:0,final:1})}),it("sendBlockReply: setBody for finalized block (block_reply semantics)",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.dispatcher.sendBlockReply({text:"block contents"}),await o.waitForIdle(),expect(e.calls).toEqual([{type:"setBody",text:"block contents"}]),expect(t.dispatcher.getQueuedCounts()).toEqual({tool:0,block:1,final:0})}),it("onReasoningStream: appends cumulative-snapshot deltas onto the agent_reasoning row (NOT the reply row)",async()=>{const l=createImReplySink(buildDeps(e,a.signal,o,t));l.replyOptions.onReasoningStream({text:"thinking step 1"}),l.replyOptions.onReasoningStream({text:"thinking step 1 and 2"}),l.replyOptions.onReasoningEnd(),await o.waitForIdle(),expect(t.calls).toEqual([{type:"appendBody",chunk:"thinking step 1"},{type:"appendBody",chunk:" and 2"}]),expect(t.body).toBe("thinking step 1 and 2"),expect(e.calls).toEqual([{type:"appendThinking",text:"thinking step 1 and 2",state:"done"}])}),it("onReasoningStream → onReasoningEnd → onReasoningStream: second burst appends after a newline; two reply thinking parts",async()=>{const l=createImReplySink(buildDeps(e,a.signal,o,t));l.replyOptions.onReasoningStream({text:"first"}),l.replyOptions.onReasoningEnd(),l.replyOptions.onReasoningStream({text:"second"}),l.replyOptions.onReasoningEnd(),await o.waitForIdle(),expect(t.calls).toEqual([{type:"appendBody",chunk:"first"},{type:"appendBody",chunk:"\n"},{type:"appendBody",chunk:"second"}]),expect(t.body).toBe("first\nsecond"),expect(e.calls).toEqual([{type:"appendThinking",text:"first",state:"done"},{type:"appendThinking",text:"second",state:"done"}])}),it("onReasoningStream: non-prefix branch mid-burst replaces the row (committed prefix + new text)",async()=>{const l=createImReplySink(buildDeps(e,a.signal,o,t));l.replyOptions.onReasoningStream({text:"alpha"}),l.replyOptions.onReasoningStream({text:"beta gamma"}),await o.waitForIdle(),expect(t.calls).toEqual([{type:"appendBody",chunk:"alpha"},{type:"setBody",text:"beta gamma"}]),expect(t.body).toBe("beta gamma")}),it("onReasoningStream without an openReasoningStream factory: reasoning chunks are dropped",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onReasoningStream({text:"ignored"}),t.replyOptions.onReasoningEnd(),await o.waitForIdle(),expect(e.calls).toEqual([])}),it("uses the runtime toolCallId and completes tool_use before appending its result",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"calc",toolCallId:"call_calc",arguments:{expression:"6*7"}}),t.dispatcher.sendToolResult({text:"42",toolCallId:"call_calc"}),await o.waitForIdle(),expect(e.calls).toEqual([{type:"appendToolUse",id:"call_calc",name:"calc",args:{expression:"6*7"}},{type:"replacePart",index:0,part:{type:"tool_use",id:"call_calc",name:"calc",arguments:{expression:"6*7"},state:"done"}},{type:"appendToolResult",toolUseId:"call_calc",content:"42"}]),expect(t.dispatcher.getQueuedCounts()).toEqual({tool:1,block:0,final:0})}),it("resolves tool_call_id from channelData.im before generic channelData",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"nested",channelData:{tool_call_id:"generic_id",im:{tool_call_id:"im_id"}}}),t.dispatcher.sendToolResult({text:"nested result",channelData:{tool_call_id:"generic_id",im:{toolCallId:"im_id"}}}),await o.waitForIdle(),expect(e.calls[0]).toMatchObject({type:"appendToolUse",id:"im_id"}),expect(e.calls.at(-1)).toEqual({type:"appendToolResult",toolUseId:"im_id",content:"nested result"})}),it("drops id-less results when multiple pending tools make correlation ambiguous",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"first"}),t.replyOptions.onToolStart({name:"second"}),t.dispatcher.sendToolResult({text:"first result"}),t.dispatcher.sendToolResult({text:"second result"}),await o.waitForIdle(),expect(e.calls.filter(e=>"appendToolResult"===e.type)).toEqual([]),t.dispatcher.markComplete("success"),expect(e.calls).toEqual([])}),it("preserves an id-less result when exactly one pending tool makes correlation unambiguous",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"read",args:{path:"/tmp/a"}}),t.dispatcher.sendToolResult({text:"file contents",mediaUrls:["beeos-file://preview"],isError:!0}),await o.waitForIdle(),expect(e.calls.filter(e=>"appendToolUse"===e.type)).toHaveLength(1),expect(e.calls.filter(e=>"appendToolResult"===e.type)).toEqual([{type:"appendToolResult",toolUseId:"tool_im_req_test_1",content:{text:"file contents",media_urls:["beeos-file://preview"]},isError:!0}])}),it("does not guess identity when repeated id-less starts are ambiguous",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"exec",args:{command:"long task"}}),t.replyOptions.onToolStart({name:"exec"}),t.replyOptions.onToolStart({name:"exec"}),t.dispatcher.sendToolResult({text:"done",toolName:"exec"}),await o.waitForIdle(),expect(e.calls).toEqual([])}),it("does not attach a named result to the only pending tool when names disagree",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"read",phase:"start"}),t.dispatcher.sendToolResult({text:"opened",toolName:"browser"}),await o.waitForIdle(),expect(e.calls).toEqual([])}),it("matches the OpenClaw 2026.5.22 item-start then id-less tool-start order without duplicate cards",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onItemEvent({kind:"tool",itemId:"tool:call1",name:"read",phase:"start"}),t.replyOptions.onToolStart({name:"read",args:{path:"/tmp/a"},phase:"start"}),t.replyOptions.onItemEvent({kind:"command",itemId:"command:call1",name:"cat /tmp/a",phase:"start"}),t.dispatcher.sendToolResult({text:"file contents",toolName:"read"}),await o.waitForIdle(),expect(e.calls.filter(e=>"appendToolUse"===e.type)).toEqual([{type:"appendToolUse",id:"tool:call1",name:"read",args:{}}]),expect(e.calls.filter(e=>"replacePart"===e.type)).toContainEqual({type:"replacePart",index:0,part:{type:"tool_use",id:"tool:call1",name:"read",arguments:{path:"/tmp/a"},state:"streaming"}}),expect(e.calls.filter(e=>"appendToolResult"===e.type)).toEqual([{type:"appendToolResult",toolUseId:"tool:call1",content:"file contents"}])}),it("correlates legacy-start then item-start then repeated legacy-start",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"exec",args:{command:"sleep 5"},phase:"start"}),t.replyOptions.onItemEvent({kind:"tool",itemId:"item-exec",name:"exec",phase:"start"}),t.replyOptions.onToolStart({name:"exec",phase:"start"}),t.dispatcher.sendToolResult({text:"done",toolName:"exec"}),t.replyOptions.onItemEvent({kind:"tool",itemId:"item-exec",name:"exec",phase:"end",status:"completed"}),t.dispatcher.sendToolResult({text:"done",toolName:"exec"}),await o.waitForIdle(),expect(e.calls.filter(e=>"appendToolUse"===e.type)).toEqual([{type:"appendToolUse",id:"item-exec",name:"exec",args:{command:"sleep 5"}}]),expect(e.calls.filter(e=>"appendToolResult"===e.type)).toEqual([{type:"appendToolResult",toolUseId:"item-exec",content:"done"}])}),it("never binds a named legacy start to a differently named stable item",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onItemEvent({kind:"tool",itemId:"item-read",name:"read",phase:"start"}),t.replyOptions.onToolStart({name:"exec",phase:"start"}),t.dispatcher.sendToolResult({text:"exec done",toolName:"exec"}),await o.waitForIdle(),expect(e.calls.filter(e=>"appendToolUse"===e.type)).toEqual([{type:"appendToolUse",id:"item-read",name:"read",args:{}},{type:"appendToolUse",id:"tool_im_req_test_1",name:"exec",args:{}}]),expect(e.calls.filter(e=>"appendToolResult"===e.type)).toEqual([{type:"appendToolResult",toolUseId:"tool_im_req_test_1",content:"exec done"}])}),it("ignores update and non-tool item events before creating any tool entity",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onItemEvent({kind:"tool",itemId:"tool:unknown",name:"read",phase:"update"}),t.replyOptions.onItemEvent({kind:"command",itemId:"command:unknown",name:"cat file",phase:"end",summary:"output"}),await o.waitForIdle(),expect(e.calls).toEqual([])}),it("keeps concurrent stable item starts with the same tool name distinct",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onItemEvent({kind:"tool",itemId:"tool:call1",name:"read",phase:"start"}),t.replyOptions.onItemEvent({kind:"tool",itemId:"tool:call2",name:"read",phase:"start"}),await o.waitForIdle(),expect(e.calls.filter(e=>"appendToolUse"===e.type)).toEqual([{type:"appendToolUse",id:"tool:call1",name:"read",args:{}},{type:"appendToolUse",id:"tool:call2",name:"read",args:{}}])}),it("uses stable item events to correlate concurrent tools that finish out of order",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"exec",phase:"start"}),t.replyOptions.onToolStart({name:"process",phase:"start"}),t.replyOptions.onItemEvent({itemId:"item-process",name:"process",phase:"end",status:"completed",summary:"process done"}),t.replyOptions.onItemEvent({itemId:"item-exec",name:"exec",phase:"end",status:"completed",summary:"exec done"}),await o.waitForIdle(),expect(e.calls.filter(e=>"appendToolResult"===e.type)).toEqual([{type:"appendToolResult",toolUseId:"item-process",content:"process done"},{type:"appendToolResult",toolUseId:"item-exec",content:"exec done"}])}),it("reuses an explicit tool id for multiple result events without duplicating tool_use",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"process",toolCallId:"call_process"}),t.dispatcher.sendToolResult({text:"running",toolCallId:"call_process"}),t.dispatcher.sendToolResult({text:"complete",tool_call_id:"call_process"}),t.dispatcher.sendToolResult({text:"complete",tool_call_id:"call_process"}),await o.waitForIdle(),expect(e.calls.filter(e=>"appendToolUse"===e.type)).toHaveLength(1),expect(e.calls.filter(e=>"replacePart"===e.type)).toHaveLength(1),expect(e.calls.filter(e=>"appendToolResult"===e.type)).toEqual([{type:"appendToolResult",toolUseId:"call_process",content:"running"},{type:"appendToolResult",toolUseId:"call_process",content:"complete"}])}),it("creates a matching tool_use when an explicit result arrives before start",async()=>{createImReplySink(buildDeps(e,a.signal,o)).dispatcher.sendToolResult({text:"early result",toolCallId:"call_early",toolName:"early-tool"}),await o.waitForIdle(),expect(e.calls).toEqual([{type:"appendToolUse",id:"call_early",name:"early-tool",args:{}},{type:"replacePart",index:0,part:{type:"tool_use",id:"call_early",name:"early-tool",arguments:{},state:"done"}},{type:"appendToolResult",toolUseId:"call_early",content:"early result"}])}),it("ignores an orphan legacy summary instead of inventing a generic tool card",async()=>{createImReplySink(buildDeps(e,a.signal,o)).dispatcher.sendToolResult({text:"orphan",isError:!0}),await o.waitForIdle(),expect(e.calls).toEqual([])}),it("ignores id-less update callbacks and aliases a later item id to the original card",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"browser",phase:"start",args:{action:"open"}}),t.replyOptions.onToolStart({name:"browser",phase:"update"}),t.replyOptions.onItemEvent({itemId:"item-browser",name:"browser",phase:"end",status:"completed",summary:"opened"}),await o.waitForIdle(),expect(e.calls.filter(e=>"appendToolUse"===e.type)).toHaveLength(1),expect(e.calls.filter(e=>"appendToolResult"===e.type)).toEqual([{type:"appendToolResult",toolUseId:"item-browser",content:"opened"}])}),it("preserves callback order across interleaved partial / reasoning / tool events",async()=>{const l=createImReplySink(buildDeps(e,a.signal,o,t));l.replyOptions.onPartialReply({text:"A"}),l.replyOptions.onReasoningStream({text:"think 1"}),l.replyOptions.onPartialReply({text:"AB"}),l.replyOptions.onToolStart({name:"tool1",phase:"start"}),l.replyOptions.onItemEvent({itemId:"item-tool1",name:"tool1",phase:"end",status:"completed",summary:"result1"}),l.replyOptions.onPartialReply({text:"ABC"}),l.replyOptions.onReasoningEnd(),l.dispatcher.sendFinalReply({text:"ABC final"}),await o.waitForIdle();const n=e.calls.map(e=>e.type);expect(n).toEqual(["appendBody","appendBody","appendToolUse","replacePart","appendToolResult","appendBody","appendThinking","setBody"]),expect(t.calls).toEqual([{type:"appendBody",chunk:"think 1"}])}),it("aborted signal suppresses ALL further dispatcher / replyOptions calls",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onPartialReply({text:"before"}),await o.waitForIdle(),a.abort("test_abort"),t.replyOptions.onPartialReply({text:"before-after"}),t.dispatcher.sendFinalReply({text:"should-not-fire"}),t.replyOptions.onReasoningStream({text:"blocked"}),t.replyOptions.onToolStart({name:"blocked"}),t.dispatcher.sendToolResult({text:"blocked"}),await o.waitForIdle(),expect(e.calls).toEqual([{type:"appendBody",chunk:"before"}])}),it("markComplete suppresses subsequent callbacks (idempotent terminal)",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onPartialReply({text:"live"}),t.dispatcher.markComplete("success"),t.replyOptions.onPartialReply({text:"live-after"}),t.dispatcher.sendFinalReply({text:"after-complete"}),await o.waitForIdle(),expect(e.calls).toEqual([{type:"appendBody",chunk:"live"}])}),it("does not mark unfinished tools done when the enclosing run fails",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.replyOptions.onToolStart({name:"browser",phase:"start"}),t.dispatcher.markComplete("failure"),await o.waitForIdle(),expect(e.calls).toEqual([{type:"appendToolUse",id:"tool_im_req_test_1",name:"browser",args:{}}]),expect(e.parts).toEqual([{type:"tool_use",id:"tool_im_req_test_1",name:"browser",arguments:{},state:"streaming"}])}),it("getQueuedCounts returns a snapshot copy (mutation does not leak)",()=>{const t=createImReplySink(buildDeps(e,a.signal,o));t.dispatcher.getQueuedCounts().tool=999,expect(t.dispatcher.getQueuedCounts()).toEqual({tool:0,block:0,final:0})}),it("when the stream is already terminated, callbacks are silently dropped",async()=>{const t=createImReplySink(buildDeps(e,a.signal,o));e.isTerminated=!0,t.replyOptions.onPartialReply({text:"dropped"}),t.dispatcher.sendFinalReply({text:"dropped"}),await o.waitForIdle(),expect(e.calls).toEqual([])}),it("sink callback throwing inside guarded does not poison the queue (logged as warn)",async()=>{const t=buildDeps(e,a.signal,o),l=createImReplySink(t),n=e.appendBody.bind(e);let s=!0;e.appendBody=e=>{if(s)throw s=!1,new Error("synthetic appendBody failure");return n(e)},l.replyOptions.onPartialReply({text:"trigger"}),l.replyOptions.onPartialReply({text:"trigger ok"}),await o.waitForIdle(),expect(e.body).toBe(" ok"),expect(t.logger.lines.some(e=>e.includes("[im-sink] partial mapping failed"))).toBe(!0)})});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeos-claw",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.52",
|
|
4
4
|
"description": "OpenClaw plugin that bridges the BeeOS platform with the local OpenClaw Gateway, providing file transfer, ACP bridge, and observability.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "BeeOS <dev@beeos.ai>",
|