@@ -11,6 +11,12 @@ import {
1111 triggerInternalHook ,
1212} from "../hooks/internal-hooks.js" ;
1313import { emitDiagnosticEvent } from "../infra/diagnostic-events.js" ;
14+ import {
15+ clearDetachedTaskLifecycleRuntimeRegistration ,
16+ getDetachedTaskLifecycleRuntimeRegistration ,
17+ registerDetachedTaskLifecycleRuntime ,
18+ type DetachedTaskLifecycleRuntime ,
19+ } from "../tasks/detached-task-runtime-state.js" ;
1420import { withEnv } from "../test-utils/env.js" ;
1521import { clearPluginCommands , getPluginCommandSpecs } from "./command-registry-state.js" ;
1622import { getGlobalHookRunner , resetGlobalHookRunner } from "./hook-runner-global.js" ;
@@ -65,6 +71,26 @@ import {
6571import type { PluginSdkResolutionPreference } from "./sdk-alias.js" ;
6672let cachedBundledTelegramDir = "" ;
6773let cachedBundledMemoryDir = "" ;
74+
75+ function createDetachedTaskRuntimeStub ( id : string ) : DetachedTaskLifecycleRuntime {
76+ const fail = ( name : string ) : never => {
77+ throw new Error ( `detached runtime ${ id } should not execute ${ name } in this test` ) ;
78+ } ;
79+ return {
80+ createQueuedTaskRun : ( ) => fail ( "createQueuedTaskRun" ) ,
81+ createRunningTaskRun : ( ) => fail ( "createRunningTaskRun" ) ,
82+ startTaskRunByRunId : ( ) => fail ( "startTaskRunByRunId" ) ,
83+ recordTaskRunProgressByRunId : ( ) => fail ( "recordTaskRunProgressByRunId" ) ,
84+ completeTaskRunByRunId : ( ) => fail ( "completeTaskRunByRunId" ) ,
85+ failTaskRunByRunId : ( ) => fail ( "failTaskRunByRunId" ) ,
86+ setDetachedTaskDeliveryStatusByRunId : ( ) => fail ( "setDetachedTaskDeliveryStatusByRunId" ) ,
87+ cancelDetachedTaskRunById : async ( ) => ( {
88+ found : true ,
89+ cancelled : true ,
90+ } ) ,
91+ } ;
92+ }
93+
6894const BUNDLED_TELEGRAM_PLUGIN_BODY = `module.exports = {
6995 id: "telegram",
7096 register(api) {
@@ -2025,6 +2051,155 @@ module.exports = { id: "throws-after-import", register() {} };`,
20252051 expect ( listMemoryEmbeddingProviders ( ) ) . toEqual ( [ ] ) ;
20262052 } ) ;
20272053
2054+ it ( "does not replace the active detached task runtime during non-activating loads" , ( ) => {
2055+ useNoBundledPlugins ( ) ;
2056+ const activeRuntime = createDetachedTaskRuntimeStub ( "active" ) ;
2057+ registerDetachedTaskLifecycleRuntime ( "active-runtime" , activeRuntime ) ;
2058+
2059+ const plugin = writePlugin ( {
2060+ id : "snapshot-detached-runtime" ,
2061+ filename : "snapshot-detached-runtime.cjs" ,
2062+ body : `module.exports = {
2063+ id: "snapshot-detached-runtime",
2064+ register(api) {
2065+ api.registerDetachedTaskRuntime({
2066+ createQueuedTaskRun() { throw new Error("snapshot createQueuedTaskRun should not run"); },
2067+ createRunningTaskRun() { throw new Error("snapshot createRunningTaskRun should not run"); },
2068+ startTaskRunByRunId() { throw new Error("snapshot startTaskRunByRunId should not run"); },
2069+ recordTaskRunProgressByRunId() { throw new Error("snapshot recordTaskRunProgressByRunId should not run"); },
2070+ completeTaskRunByRunId() { throw new Error("snapshot completeTaskRunByRunId should not run"); },
2071+ failTaskRunByRunId() { throw new Error("snapshot failTaskRunByRunId should not run"); },
2072+ setDetachedTaskDeliveryStatusByRunId() { throw new Error("snapshot setDetachedTaskDeliveryStatusByRunId should not run"); },
2073+ async cancelDetachedTaskRunById() { return { found: true, cancelled: true }; },
2074+ });
2075+ },
2076+ };` ,
2077+ } ) ;
2078+
2079+ const scoped = loadOpenClawPlugins ( {
2080+ cache : false ,
2081+ activate : false ,
2082+ workspaceDir : plugin . dir ,
2083+ config : {
2084+ plugins : {
2085+ load : { paths : [ plugin . file ] } ,
2086+ allow : [ "snapshot-detached-runtime" ] ,
2087+ } ,
2088+ } ,
2089+ onlyPluginIds : [ "snapshot-detached-runtime" ] ,
2090+ } ) ;
2091+
2092+ expect ( scoped . plugins . find ( ( entry ) => entry . id === "snapshot-detached-runtime" ) ?. status ) . toBe (
2093+ "loaded" ,
2094+ ) ;
2095+ expect ( getDetachedTaskLifecycleRuntimeRegistration ( ) ) . toMatchObject ( {
2096+ pluginId : "active-runtime" ,
2097+ runtime : activeRuntime ,
2098+ } ) ;
2099+ } ) ;
2100+
2101+ it ( "clears newly-registered detached task runtimes when plugin register fails" , ( ) => {
2102+ useNoBundledPlugins ( ) ;
2103+ const plugin = writePlugin ( {
2104+ id : "failing-detached-runtime" ,
2105+ filename : "failing-detached-runtime.cjs" ,
2106+ body : `module.exports = {
2107+ id: "failing-detached-runtime",
2108+ register(api) {
2109+ api.registerDetachedTaskRuntime({
2110+ createQueuedTaskRun() { throw new Error("failing createQueuedTaskRun should not run"); },
2111+ createRunningTaskRun() { throw new Error("failing createRunningTaskRun should not run"); },
2112+ startTaskRunByRunId() { throw new Error("failing startTaskRunByRunId should not run"); },
2113+ recordTaskRunProgressByRunId() { throw new Error("failing recordTaskRunProgressByRunId should not run"); },
2114+ completeTaskRunByRunId() { throw new Error("failing completeTaskRunByRunId should not run"); },
2115+ failTaskRunByRunId() { throw new Error("failing failTaskRunByRunId should not run"); },
2116+ setDetachedTaskDeliveryStatusByRunId() { throw new Error("failing setDetachedTaskDeliveryStatusByRunId should not run"); },
2117+ async cancelDetachedTaskRunById() { return { found: true, cancelled: true }; },
2118+ });
2119+ throw new Error("detached runtime register failed");
2120+ },
2121+ };` ,
2122+ } ) ;
2123+
2124+ const registry = loadOpenClawPlugins ( {
2125+ cache : false ,
2126+ workspaceDir : plugin . dir ,
2127+ config : {
2128+ plugins : {
2129+ load : { paths : [ plugin . file ] } ,
2130+ allow : [ "failing-detached-runtime" ] ,
2131+ } ,
2132+ } ,
2133+ onlyPluginIds : [ "failing-detached-runtime" ] ,
2134+ } ) ;
2135+
2136+ expect ( registry . plugins . find ( ( entry ) => entry . id === "failing-detached-runtime" ) ?. status ) . toBe (
2137+ "error" ,
2138+ ) ;
2139+ expect ( getDetachedTaskLifecycleRuntimeRegistration ( ) ) . toBeUndefined ( ) ;
2140+ } ) ;
2141+
2142+ it ( "restores cached detached task runtime registrations on cache hits" , ( ) => {
2143+ useNoBundledPlugins ( ) ;
2144+ const plugin = writePlugin ( {
2145+ id : "cached-detached-runtime" ,
2146+ filename : "cached-detached-runtime.cjs" ,
2147+ body : `module.exports = {
2148+ id: "cached-detached-runtime",
2149+ register(api) {
2150+ api.registerDetachedTaskRuntime({
2151+ createQueuedTaskRun() { throw new Error("cached createQueuedTaskRun should not run"); },
2152+ createRunningTaskRun() { throw new Error("cached createRunningTaskRun should not run"); },
2153+ startTaskRunByRunId() { throw new Error("cached startTaskRunByRunId should not run"); },
2154+ recordTaskRunProgressByRunId() { throw new Error("cached recordTaskRunProgressByRunId should not run"); },
2155+ completeTaskRunByRunId() { throw new Error("cached completeTaskRunByRunId should not run"); },
2156+ failTaskRunByRunId() { throw new Error("cached failTaskRunByRunId should not run"); },
2157+ setDetachedTaskDeliveryStatusByRunId() { throw new Error("cached setDetachedTaskDeliveryStatusByRunId should not run"); },
2158+ async cancelDetachedTaskRunById() { return { found: true, cancelled: true }; },
2159+ });
2160+ },
2161+ };` ,
2162+ } ) ;
2163+
2164+ const loadOptions = {
2165+ workspaceDir : plugin . dir ,
2166+ config : {
2167+ plugins : {
2168+ load : { paths : [ plugin . file ] } ,
2169+ allow : [ "cached-detached-runtime" ] ,
2170+ } ,
2171+ } ,
2172+ onlyPluginIds : [ "cached-detached-runtime" ] ,
2173+ } satisfies Parameters < typeof loadOpenClawPlugins > [ 0 ] ;
2174+
2175+ loadOpenClawPlugins ( loadOptions ) ;
2176+ expect ( getDetachedTaskLifecycleRuntimeRegistration ( ) ?. pluginId ) . toBe ( "cached-detached-runtime" ) ;
2177+
2178+ clearDetachedTaskLifecycleRuntimeRegistration ( ) ;
2179+ expect ( getDetachedTaskLifecycleRuntimeRegistration ( ) ) . toBeUndefined ( ) ;
2180+
2181+ loadOpenClawPlugins ( loadOptions ) ;
2182+
2183+ expect ( getDetachedTaskLifecycleRuntimeRegistration ( ) ?. pluginId ) . toBe ( "cached-detached-runtime" ) ;
2184+ } ) ;
2185+
2186+ it ( "clears stale detached task runtime registrations on active reloads when no plugin re-registers one" , ( ) => {
2187+ useNoBundledPlugins ( ) ;
2188+ registerDetachedTaskLifecycleRuntime ( "stale-runtime" , createDetachedTaskRuntimeStub ( "stale" ) ) ;
2189+
2190+ loadOpenClawPlugins ( {
2191+ cache : false ,
2192+ config : {
2193+ plugins : {
2194+ load : { paths : [ ] } ,
2195+ allow : [ ] ,
2196+ } ,
2197+ } ,
2198+ } ) ;
2199+
2200+ expect ( getDetachedTaskLifecycleRuntimeRegistration ( ) ) . toBeUndefined ( ) ;
2201+ } ) ;
2202+
20282203 it ( "restores cached memory capability public artifacts on cache hits" , async ( ) => {
20292204 useNoBundledPlugins ( ) ;
20302205 const workspaceDir = makeTempDir ( ) ;
0 commit comments