@@ -4,7 +4,15 @@ import {
44 textToolResult ,
55} from "openclaw/plugin-sdk/agent-runtime-test-contracts" ;
66// Covers embedded runner extension factories and tool-result middleware bridge.
7- import { SessionManager } from "openclaw/plugin-sdk/agent-sessions" ;
7+ import {
8+ AuthStorage ,
9+ createEventBus ,
10+ createExtensionRuntime ,
11+ ExtensionRunner ,
12+ loadExtensionFromFactory ,
13+ ModelRegistry ,
14+ SessionManager ,
15+ } from "openclaw/plugin-sdk/agent-sessions" ;
816import { afterEach , describe , expect , it , vi } from "vitest" ;
917import { createEmptyPluginRegistry } from "../plugins/registry.js" ;
1018import { setActivePluginRegistry } from "../plugins/runtime.js" ;
@@ -15,6 +23,7 @@ import {
1523import { buildEmbeddedExtensionFactories } from "./embedded-agent-runner/extensions.js" ;
1624import { consumeEmbeddedToolSendReceipt } from "./embedded-agent-runner/tool-send-receipts.js" ;
1725import { cleanupTempPluginTestEnvironment } from "./test-helpers/temp-plugin-extension-fixtures.js" ;
26+ import { jsonResult } from "./tools/common.js" ;
1827
1928const originalBundledPluginsDir = process . env . OPENCLAW_BUNDLED_PLUGINS_DIR ;
2029const tempDirs : string [ ] = [ ] ;
@@ -497,6 +506,168 @@ describe("buildEmbeddedExtensionFactories", () => {
497506 } ) ;
498507 } ) ;
499508
509+ it ( "keeps an accepted sessions_spawn launch successful even when the event is flagged as an error" , async ( ) => {
510+ setActivePluginRegistry ( createEmptyPluginRegistry ( ) ) ;
511+
512+ const factories = buildEmbeddedExtensionFactories ( {
513+ cfg : undefined ,
514+ sessionManager : SessionManager . inMemory ( ) ,
515+ provider : "openai" ,
516+ modelId : "gpt-5.4" ,
517+ model : undefined ,
518+ } ) ;
519+
520+ const factory = factories [ 0 ] ;
521+ expect ( factory ) . toBeDefined ( ) ;
522+ if ( ! factory ) {
523+ throw new Error ( "Expected embedded tool-result extension factory" ) ;
524+ }
525+ const runtime = createExtensionRuntime ( ) ;
526+ const extension = await loadExtensionFromFactory (
527+ factory ,
528+ "/tmp" ,
529+ createEventBus ( ) ,
530+ runtime ,
531+ "<embedded-test>" ,
532+ ) ;
533+ const runner = new ExtensionRunner (
534+ [ extension ] ,
535+ runtime ,
536+ "/tmp" ,
537+ SessionManager . inMemory ( ) ,
538+ ModelRegistry . inMemory ( AuthStorage . inMemory ( ) ) ,
539+ ) ;
540+ const acceptedResult = jsonResult ( {
541+ status : "accepted" ,
542+ childSessionKey : "agent:watcher:subagent:abc" ,
543+ runId : "run-123" ,
544+ mode : "run" ,
545+ } ) ;
546+
547+ const result = await runner . emitToolResult ( {
548+ type : "tool_result" ,
549+ toolName : "sessions_spawn" ,
550+ toolCallId : "call-spawn" ,
551+ input : { } ,
552+ content : acceptedResult . content ,
553+ details : acceptedResult . details ,
554+ isError : true ,
555+ } ) ;
556+
557+ expect ( result ) . toEqual ( { ...acceptedResult , isError : false } ) ;
558+ } ) ;
559+
560+ it ( "still marks a forbidden sessions_spawn as a model-visible failure" , async ( ) => {
561+ setActivePluginRegistry ( createEmptyPluginRegistry ( ) ) ;
562+
563+ const factories = buildEmbeddedExtensionFactories ( {
564+ cfg : undefined ,
565+ sessionManager : SessionManager . inMemory ( ) ,
566+ provider : "openai" ,
567+ modelId : "gpt-5.4" ,
568+ model : undefined ,
569+ } ) ;
570+
571+ const handlers = new Map < string , Function > ( ) ;
572+ await factories [ 0 ] ?.( {
573+ on ( event : string , handler : Function ) {
574+ handlers . set ( event , handler ) ;
575+ } ,
576+ } as never ) ;
577+ const handler = handlers . get ( "tool_result" ) ;
578+ const content = [ { type : "text" , text : "spawn denied" } ] ;
579+ const details = { status : "forbidden" , reason : "subagents disabled" } ;
580+
581+ const result = await handler ?.(
582+ {
583+ toolName : "sessions_spawn" ,
584+ toolCallId : "call-spawn-forbidden" ,
585+ content,
586+ details,
587+ isError : true ,
588+ } ,
589+ { cwd : "/tmp" } ,
590+ ) ;
591+
592+ expect ( result ) . toEqual ( { content, details, isError : true } ) ;
593+ } ) ;
594+
595+ it ( "still flags an accepted-status sessions_spawn that is missing spawn identity" , async ( ) => {
596+ // Only the full accepted contract (runId + childSessionKey) is a launch; an
597+ // accepted status alone must not clear an error flag.
598+ setActivePluginRegistry ( createEmptyPluginRegistry ( ) ) ;
599+
600+ const factories = buildEmbeddedExtensionFactories ( {
601+ cfg : undefined ,
602+ sessionManager : SessionManager . inMemory ( ) ,
603+ provider : "openai" ,
604+ modelId : "gpt-5.4" ,
605+ model : undefined ,
606+ } ) ;
607+
608+ const handlers = new Map < string , Function > ( ) ;
609+ await factories [ 0 ] ?.( {
610+ on ( event : string , handler : Function ) {
611+ handlers . set ( event , handler ) ;
612+ } ,
613+ } as never ) ;
614+ const handler = handlers . get ( "tool_result" ) ;
615+ const content = [ { type : "text" , text : "partial" } ] ;
616+ const details = { status : "accepted" } ;
617+
618+ const result = await handler ?.(
619+ {
620+ toolName : "sessions_spawn" ,
621+ toolCallId : "call-spawn-partial" ,
622+ content,
623+ details,
624+ isError : true ,
625+ } ,
626+ { cwd : "/tmp" } ,
627+ ) ;
628+
629+ expect ( result ) . toEqual ( { content, details, isError : true } ) ;
630+ } ) ;
631+
632+ it ( "does not clear the error flag for a non-spawn tool with accepted-shaped details" , async ( ) => {
633+ setActivePluginRegistry ( createEmptyPluginRegistry ( ) ) ;
634+
635+ const factories = buildEmbeddedExtensionFactories ( {
636+ cfg : undefined ,
637+ sessionManager : SessionManager . inMemory ( ) ,
638+ provider : "openai" ,
639+ modelId : "gpt-5.4" ,
640+ model : undefined ,
641+ } ) ;
642+
643+ const handlers = new Map < string , Function > ( ) ;
644+ await factories [ 0 ] ?.( {
645+ on ( event : string , handler : Function ) {
646+ handlers . set ( event , handler ) ;
647+ } ,
648+ } as never ) ;
649+ const handler = handlers . get ( "tool_result" ) ;
650+ const content = [ { type : "text" , text : "boom" } ] ;
651+ const details = jsonResult ( {
652+ status : "accepted" ,
653+ childSessionKey : "agent:watcher:subagent:abc" ,
654+ runId : "run-123" ,
655+ } ) . details ;
656+
657+ const result = await handler ?.(
658+ {
659+ toolName : "exec" ,
660+ toolCallId : "call-exec-accepted-shape" ,
661+ content,
662+ details,
663+ isError : true ,
664+ } ,
665+ { cwd : "/tmp" } ,
666+ ) ;
667+
668+ expect ( result ) . toEqual ( { content, details, isError : true } ) ;
669+ } ) ;
670+
500671 it ( "does not mark results as errors when status is absent or non-error" , async ( ) => {
501672 setActivePluginRegistry ( createEmptyPluginRegistry ( ) ) ;
502673
0 commit comments