@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
44import { deleteTestEnvValue , setTestEnvValue } from "../test-utils/env.js" ;
55import { GATEWAY_SERVICE_KIND , GATEWAY_SERVICE_MARKER } from "./constants.js" ;
66import {
7+ LAUNCH_AGENT_ENV_WRAPPER_SHELL ,
78 LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS ,
89 LAUNCH_AGENT_PROCESS_TYPE ,
910 LAUNCH_AGENT_STDIN_PATH ,
@@ -81,6 +82,13 @@ function countMatching<T>(items: readonly T[], predicate: (item: T) => boolean):
8182 return count ;
8283}
8384
85+ function readPlistProgramArgumentStrings ( plist : string ) : string [ ] {
86+ const match = plist . match ( / < k e y > P r o g r a m A r g u m e n t s < \/ k e y > \s * < a r r a y > ( [ \s \S ] * ?) < \/ a r r a y > / i) ;
87+ return Array . from ( ( match ?. [ 1 ] ?? "" ) . matchAll ( / < s t r i n g > ( [ \s \S ] * ?) < \/ s t r i n g > / gi) ) . map (
88+ ( item ) => item [ 1 ] ?? "" ,
89+ ) ;
90+ }
91+
8492function createDefaultLaunchdEnv ( ) : Record < string , string | undefined > {
8593 return {
8694 HOME : "/Users/test" ,
@@ -838,8 +846,12 @@ describe("launchd install", () => {
838846 const plist = state . files . get ( plistPath ) ?? "" ;
839847 expect ( plist ) . not . toContain ( "<key>EnvironmentVariables</key>" ) ;
840848 expect ( plist ) . not . toContain ( apiKey ) ;
841- expect ( plist ) . toContain ( `<string>${ wrapperPath } </string>` ) ;
842- expect ( plist ) . toContain ( `<string>${ envFilePath } </string>` ) ;
849+ expect ( readPlistProgramArgumentStrings ( plist ) ) . toEqual ( [
850+ LAUNCH_AGENT_ENV_WRAPPER_SHELL ,
851+ wrapperPath ,
852+ envFilePath ,
853+ ...defaultProgramArguments ,
854+ ] ) ;
843855 const envFile = state . files . get ( envFilePath ) ?? "" ;
844856 expect ( envFile ) . toContain ( `export TMPDIR='${ tmpDir } '` ) ;
845857 expect ( envFile ) . toContain ( `export OPENAI_API_KEY='${ apiKey } '` ) ;
@@ -930,6 +942,49 @@ describe("launchd install", () => {
930942 expect ( state . files . get ( wrapperPath ) ) . toBe ( generatedWrapper ) ;
931943 } ) ;
932944
945+ it ( "rewrites legacy LaunchAgent environment wrappers to a system shell executable" , async ( ) => {
946+ const env = createDefaultLaunchdEnv ( ) ;
947+ const envFilePath = "/Users/test/.openclaw/service-env/ai.openclaw.gateway.env" ;
948+ const wrapperPath = "/Users/test/.openclaw/service-env/ai.openclaw.gateway-env-wrapper.sh" ;
949+ await installLaunchAgent ( {
950+ env,
951+ stdout : new PassThrough ( ) ,
952+ programArguments : defaultProgramArguments ,
953+ environment : { OPENCLAW_GATEWAY_PORT : "19007" } ,
954+ } ) ;
955+
956+ const plistPath = resolveLaunchAgentPlistPath ( env ) ;
957+ const legacyPlist = ( state . files . get ( plistPath ) ?? "" ) . replace (
958+ [
959+ `<string>${ LAUNCH_AGENT_ENV_WRAPPER_SHELL } </string>` ,
960+ `<string>${ wrapperPath } </string>` ,
961+ `<string>${ envFilePath } </string>` ,
962+ ] . join ( "\n " ) ,
963+ [ `<string>${ wrapperPath } </string>` , `<string>${ envFilePath } </string>` ] . join ( "\n " ) ,
964+ ) ;
965+ expect ( readPlistProgramArgumentStrings ( legacyPlist ) ) . toEqual ( [
966+ wrapperPath ,
967+ envFilePath ,
968+ ...defaultProgramArguments ,
969+ ] ) ;
970+ state . files . set ( plistPath , legacyPlist ) ;
971+ state . launchctlCalls . length = 0 ;
972+
973+ await restartLaunchAgent ( {
974+ env,
975+ stdout : new PassThrough ( ) ,
976+ } ) ;
977+
978+ const rewritten = state . files . get ( plistPath ) ?? "" ;
979+ expect ( readPlistProgramArgumentStrings ( rewritten ) ) . toEqual ( [
980+ LAUNCH_AGENT_ENV_WRAPPER_SHELL ,
981+ wrapperPath ,
982+ envFilePath ,
983+ ...defaultProgramArguments ,
984+ ] ) ;
985+ expect ( cleanStaleGatewayProcessesSync ) . toHaveBeenCalledWith ( 19007 ) ;
986+ } ) ;
987+
933988 it ( "repairs a mangled label-derived service-env wrapper path on restart" , async ( ) => {
934989 const callerEnv = createDefaultLaunchdEnv ( ) ;
935990 const serviceEnv = {
@@ -976,8 +1031,12 @@ describe("launchd install", () => {
9761031 } ) ;
9771032
9781033 const rewritten = state . files . get ( plistPath ) ?? "" ;
979- expect ( rewritten ) . toContain ( `<string>${ callerWrapperPath } </string>` ) ;
980- expect ( rewritten ) . toContain ( `<string>${ callerEnvFilePath } </string>` ) ;
1034+ expect ( readPlistProgramArgumentStrings ( rewritten ) ) . toEqual ( [
1035+ LAUNCH_AGENT_ENV_WRAPPER_SHELL ,
1036+ callerWrapperPath ,
1037+ callerEnvFilePath ,
1038+ ...defaultProgramArguments ,
1039+ ] ) ;
9811040 expect ( rewritten ) . not . toContain ( mangledEnvFilePath ) ;
9821041 expect ( rewritten ) . not . toContain ( mangledWrapperPath ) ;
9831042 const rewrittenEnv = state . files . get ( callerEnvFilePath ) ?? "" ;
0 commit comments