1- import path from "node:path" ;
21import { PassThrough } from "node:stream" ;
32import { beforeEach , describe , expect , it , vi } from "vitest" ;
43import {
@@ -50,19 +49,6 @@ const cleanStaleGatewayProcessesSync = vi.hoisted(() =>
5049) ;
5150const defaultProgramArguments = [ "node" , "-e" , "process.exit(0)" ] ;
5251
53- function resolveDisableMarkerPath (
54- env : Record < string , string | undefined > ,
55- label = "ai.openclaw.gateway" ,
56- ) {
57- const profile = env . OPENCLAW_PROFILE ?. trim ( ) ;
58- const suffix = ! profile || profile . toLowerCase ( ) === "default" ? "" : `-${ profile } ` ;
59- return path . join (
60- env . OPENCLAW_STATE_DIR ?? path . join ( env . HOME ?? "/Users/test" , `.openclaw${ suffix } ` ) ,
61- "service" ,
62- `${ encodeURIComponent ( label ) } .launchd-disabled-by-openclaw` ,
63- ) ;
64- }
65-
6652function expectLaunchctlEnableBootstrapOrder ( env : Record < string , string | undefined > ) {
6753 const domain = typeof process . getuid === "function" ? `gui/${ process . getuid ( ) } ` : "gui/501" ;
6854 const label = "ai.openclaw.gateway" ;
@@ -332,24 +318,19 @@ describe("launchctl list detection", () => {
332318} ) ;
333319
334320describe ( "launchd bootstrap repair" , ( ) => {
335- it ( "bootstraps and kickstarts the resolved label without enabling unrelated disabled state " , async ( ) => {
321+ it ( "enables, bootstraps, and kickstarts the resolved label" , async ( ) => {
336322 const env : Record < string , string | undefined > = {
337323 HOME : "/Users/test" ,
338324 OPENCLAW_PROFILE : "default" ,
339325 } ;
340326 const repair = await repairLaunchAgentBootstrap ( { env } ) ;
341327 expect ( repair ) . toEqual ( { ok : true , status : "repaired" } ) ;
342328
343- const domain = typeof process . getuid === "function" ? `gui/${ process . getuid ( ) } ` : "gui/501" ;
344- const serviceId = `${ domain } /ai.openclaw.gateway` ;
345- const bootstrapIndex = state . launchctlCalls . findIndex (
346- ( c ) => c [ 0 ] === "bootstrap" && c [ 1 ] === domain ,
347- ) ;
329+ const { serviceId, bootstrapIndex } = expectLaunchctlEnableBootstrapOrder ( env ) ;
348330 const kickstartIndex = state . launchctlCalls . findIndex (
349331 ( c ) => c [ 0 ] === "kickstart" && c [ 1 ] === "-k" && c [ 2 ] === serviceId ,
350332 ) ;
351333
352- expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "enable" ) ) . toBe ( false ) ;
353334 expect ( kickstartIndex ) . toBeGreaterThanOrEqual ( 0 ) ;
354335 expect ( bootstrapIndex ) . toBeLessThan ( kickstartIndex ) ;
355336 } ) ;
@@ -415,30 +396,6 @@ describe("launchd bootstrap repair", () => {
415396 detail : "launchctl kickstart failed: permission denied" ,
416397 } ) ;
417398 } ) ;
418-
419- it ( "re-enables when the disabled marker shows OpenClaw owns the stop state" , async ( ) => {
420- const env : Record < string , string | undefined > = {
421- HOME : "/Users/test" ,
422- OPENCLAW_PROFILE : "default" ,
423- } ;
424- state . files . set ( resolveDisableMarkerPath ( env ) , "disabled_by_openclaw\n" ) ;
425-
426- await repairLaunchAgentBootstrap ( { env } ) ;
427-
428- expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "enable" ) ) . toBe ( true ) ;
429- expect ( state . files . has ( resolveDisableMarkerPath ( env ) ) ) . toBe ( false ) ;
430- } ) ;
431-
432- it ( "allows explicit repairs to force re-enable disabled services" , async ( ) => {
433- const env : Record < string , string | undefined > = {
434- HOME : "/Users/test" ,
435- OPENCLAW_PROFILE : "default" ,
436- } ;
437-
438- await repairLaunchAgentBootstrap ( { env, forceEnable : true } ) ;
439-
440- expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "enable" ) ) . toBe ( true ) ;
441- } ) ;
442399} ) ;
443400
444401describe ( "launchd install" , ( ) => {
@@ -535,7 +492,6 @@ describe("launchd install", () => {
535492 expect ( state . launchctlCalls ) . toContainEqual ( [ "disable" , serviceId ] ) ;
536493 expect ( state . launchctlCalls ) . toContainEqual ( [ "stop" , "ai.openclaw.gateway" ] ) ;
537494 expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "bootout" ) ) . toBe ( false ) ;
538- expect ( state . files . has ( resolveDisableMarkerPath ( env ) ) ) . toBe ( true ) ;
539495 expect ( output ) . toContain ( "Stopped LaunchAgent" ) ;
540496 } ) ;
541497
@@ -552,7 +508,6 @@ describe("launchd install", () => {
552508
553509 expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "stop" ) ) . toBe ( false ) ;
554510 expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "bootout" ) ) . toBe ( true ) ;
555- expect ( state . files . has ( resolveDisableMarkerPath ( env ) ) ) . toBe ( false ) ;
556511 expect ( output ) . toContain ( "Stopped LaunchAgent (degraded)" ) ;
557512 expect ( output ) . toContain ( "used bootout fallback" ) ;
558513 } ) ;
@@ -649,30 +604,12 @@ describe("launchd install", () => {
649604 const serviceId = `${ domain } /${ label } ` ;
650605 expect ( result ) . toEqual ( { outcome : "completed" } ) ;
651606 expect ( cleanStaleGatewayProcessesSync ) . toHaveBeenCalledWith ( 18789 ) ;
652- expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "enable" ) ) . toBe ( false ) ;
607+ expect ( state . launchctlCalls ) . toContainEqual ( [ "enable" , serviceId ] ) ;
653608 expect ( state . launchctlCalls ) . toContainEqual ( [ "kickstart" , "-k" , serviceId ] ) ;
654609 expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "bootout" ) ) . toBe ( false ) ;
655610 expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "bootstrap" ) ) . toBe ( false ) ;
656611 } ) ;
657612
658- it ( "re-enables before restart when OpenClaw owns the persisted disabled state" , async ( ) => {
659- const env = {
660- ...createDefaultLaunchdEnv ( ) ,
661- OPENCLAW_GATEWAY_PORT : "18789" ,
662- } ;
663- const domain = typeof process . getuid === "function" ? `gui/${ process . getuid ( ) } ` : "gui/501" ;
664- const serviceId = `${ domain } /ai.openclaw.gateway` ;
665- state . files . set ( resolveDisableMarkerPath ( env ) , "disabled_by_openclaw\n" ) ;
666-
667- await restartLaunchAgent ( {
668- env,
669- stdout : new PassThrough ( ) ,
670- } ) ;
671-
672- expect ( state . launchctlCalls ) . toContainEqual ( [ "enable" , serviceId ] ) ;
673- expect ( state . files . has ( resolveDisableMarkerPath ( env ) ) ) . toBe ( false ) ;
674- } ) ;
675-
676613 it ( "uses the configured gateway port for stale cleanup" , async ( ) => {
677614 const env = {
678615 ...createDefaultLaunchdEnv ( ) ,
@@ -716,7 +653,7 @@ describe("launchd install", () => {
716653 ) ;
717654
718655 expect ( result ) . toEqual ( { outcome : "completed" } ) ;
719- expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "enable" ) ) . toBe ( false ) ;
656+ expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "enable" ) ) . toBe ( true ) ;
720657 expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "bootstrap" ) ) . toBe ( true ) ;
721658 expect ( kickstartCalls ) . toHaveLength ( 2 ) ;
722659 expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "bootout" ) ) . toBe ( false ) ;
@@ -734,7 +671,7 @@ describe("launchd install", () => {
734671 } ) ,
735672 ) . rejects . toThrow ( "launchctl kickstart failed: Input/output error" ) ;
736673
737- expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "enable" ) ) . toBe ( false ) ;
674+ expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "enable" ) ) . toBe ( true ) ;
738675 expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "bootstrap" ) ) . toBe ( false ) ;
739676 } ) ;
740677
@@ -751,7 +688,7 @@ describe("launchd install", () => {
751688 } ) ,
752689 ) . rejects . toThrow ( "launchctl kickstart failed: Input/output error" ) ;
753690
754- expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "enable" ) ) . toBe ( false ) ;
691+ expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "enable" ) ) . toBe ( true ) ;
755692 expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "bootstrap" ) ) . toBe ( true ) ;
756693 } ) ;
757694
@@ -767,7 +704,7 @@ describe("launchd install", () => {
767704 } ) ,
768705 ) . rejects . toThrow ( "launchctl kickstart failed: Input/output error" ) ;
769706
770- expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "enable" ) ) . toBe ( false ) ;
707+ expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "enable" ) ) . toBe ( true ) ;
771708 expect ( state . launchctlCalls . some ( ( call ) => call [ 0 ] === "bootstrap" ) ) . toBe ( false ) ;
772709 } ) ;
773710
@@ -784,32 +721,11 @@ describe("launchd install", () => {
784721 expect ( launchdRestartHandoffState . scheduleDetachedLaunchdRestartHandoff ) . toHaveBeenCalledWith ( {
785722 env,
786723 mode : "kickstart" ,
787- shouldEnable : false ,
788724 waitForPid : process . pid ,
789- enableMarkerPath : undefined ,
790725 } ) ;
791726 expect ( state . launchctlCalls ) . toEqual ( [ ] ) ;
792727 } ) ;
793728
794- it ( "passes marker-owned re-enable intent to the detached handoff" , async ( ) => {
795- const env = createDefaultLaunchdEnv ( ) ;
796- state . files . set ( resolveDisableMarkerPath ( env ) , "disabled_by_openclaw\n" ) ;
797- launchdRestartHandoffState . isCurrentProcessLaunchdServiceLabel . mockReturnValue ( true ) ;
798-
799- await restartLaunchAgent ( {
800- env,
801- stdout : new PassThrough ( ) ,
802- } ) ;
803-
804- expect ( launchdRestartHandoffState . scheduleDetachedLaunchdRestartHandoff ) . toHaveBeenCalledWith ( {
805- env,
806- mode : "kickstart" ,
807- shouldEnable : true ,
808- waitForPid : process . pid ,
809- enableMarkerPath : resolveDisableMarkerPath ( env ) ,
810- } ) ;
811- } ) ;
812-
813729 it ( "shows actionable guidance when launchctl gui domain does not support bootstrap" , async ( ) => {
814730 state . bootstrapError = "Bootstrap failed: 125: Domain does not support specified action" ;
815731 const env = createDefaultLaunchdEnv ( ) ;
@@ -883,4 +799,13 @@ describe("resolveLaunchAgentPlistPath", () => {
883799 ] ) ( "$name" , ( { env, expected } ) => {
884800 expect ( resolveLaunchAgentPlistPath ( env ) ) . toBe ( expected ) ;
885801 } ) ;
802+
803+ it ( "rejects invalid launchd labels that contain path separators" , ( ) => {
804+ expect ( ( ) =>
805+ resolveLaunchAgentPlistPath ( {
806+ HOME : "/Users/test" ,
807+ OPENCLAW_LAUNCHD_LABEL : "../evil/label" ,
808+ } ) ,
809+ ) . toThrow ( "Invalid launchd label" ) ;
810+ } ) ;
886811} ) ;
0 commit comments