@@ -9,6 +9,10 @@ const mocks = vi.hoisted(() => ({
99 disposeBundleRuntime : vi . fn ( ) ,
1010 loadModelCatalog : vi . fn ( async ( ) : Promise < Array < Record < string , unknown > > > => [ ] ) ,
1111 normalizeProviderToolSchemasWithPlugin : vi . fn ( ) ,
12+ buildGatewayProbeConnectionDetails : vi . fn ( ) ,
13+ probeGatewayStatus : vi . fn ( ) ,
14+ readGatewayServiceState : vi . fn ( ) ,
15+ resolveGatewayService : vi . fn ( ( ) => ( { label : "openclaw-gateway" } ) ) ,
1216 resolvePluginProviders : vi . fn ( ( ) : Array < Record < string , unknown > > => [ ] ) ,
1317 resolveDefaultModelForAgent : vi . fn ( ( ) => ( { provider : "openai" , model : "gpt-5.5" } ) ) ,
1418} ) ) ;
@@ -35,6 +39,19 @@ vi.mock("../agents/agent-tools.js", () => ({
3539 createOpenClawCodingTools : mocks . createOpenClawCodingTools ,
3640} ) ) ;
3741
42+ vi . mock ( "../gateway/call.js" , ( ) => ( {
43+ buildGatewayProbeConnectionDetails : mocks . buildGatewayProbeConnectionDetails ,
44+ } ) ) ;
45+
46+ vi . mock ( "../cli/daemon-cli/probe.js" , ( ) => ( {
47+ probeGatewayStatus : mocks . probeGatewayStatus ,
48+ } ) ) ;
49+
50+ vi . mock ( "../daemon/service.js" , ( ) => ( {
51+ readGatewayServiceState : mocks . readGatewayServiceState ,
52+ resolveGatewayService : mocks . resolveGatewayService ,
53+ } ) ) ;
54+
3855vi . mock ( "../plugins/provider-runtime.js" , ( ) => ( {
3956 inspectProviderToolSchemasWithPlugin : ( ) => [ ] ,
4057 normalizeProviderToolSchemasWithPlugin : mocks . normalizeProviderToolSchemasWithPlugin ,
@@ -48,8 +65,12 @@ vi.mock("../plugins/providers.runtime.js", () => ({
4865 resolvePluginProviders : mocks . resolvePluginProviders ,
4966} ) ) ;
5067
51- const { collectProviderCatalogProjectionFindings, collectRuntimeToolSchemaFindings } =
52- await import ( "./doctor-core-checks.runtime.js" ) ;
68+ const {
69+ collectGatewayDaemonFindings,
70+ collectGatewayHealthFindings,
71+ collectProviderCatalogProjectionFindings,
72+ collectRuntimeToolSchemaFindings,
73+ } = await import ( "./doctor-core-checks.runtime.js" ) ;
5374
5475function tool ( name : string , parameters : unknown ) : AnyAgentTool {
5576 return {
@@ -79,6 +100,22 @@ describe("doctor runtime tool schema checks", () => {
79100 mocks . normalizeProviderToolSchemasWithPlugin
80101 . mockReset ( )
81102 . mockImplementation ( ( { context } ) => context . tools ) ;
103+ mocks . buildGatewayProbeConnectionDetails . mockReset ( ) . mockResolvedValue ( {
104+ url : "http://127.0.0.1:5829" ,
105+ } ) ;
106+ mocks . probeGatewayStatus . mockReset ( ) . mockResolvedValue ( {
107+ ok : true ,
108+ server : { version : "2026.6.26" } ,
109+ } ) ;
110+ mocks . readGatewayServiceState . mockReset ( ) . mockResolvedValue ( {
111+ installed : true ,
112+ loaded : true ,
113+ running : true ,
114+ env : { } ,
115+ command : { programArguments : [ "openclaw" , "gateway" ] , sourcePath : "/tmp/gateway.service" } ,
116+ runtime : { status : "running" } ,
117+ } ) ;
118+ mocks . resolveGatewayService . mockClear ( ) ;
82119 mocks . resolvePluginProviders . mockReset ( ) . mockReturnValue ( [ ] ) ;
83120 mocks . resolveDefaultModelForAgent . mockClear ( ) ;
84121 } ) ;
@@ -503,6 +540,100 @@ describe("doctor runtime tool schema checks", () => {
503540 } ) ;
504541} ) ;
505542
543+ describe ( "doctor gateway runtime checks" , ( ) => {
544+ beforeEach ( ( ) => {
545+ mocks . buildGatewayProbeConnectionDetails . mockReset ( ) . mockResolvedValue ( {
546+ url : "http://127.0.0.1:5829" ,
547+ } ) ;
548+ mocks . probeGatewayStatus . mockReset ( ) . mockResolvedValue ( {
549+ ok : true ,
550+ server : { version : "2026.6.26" } ,
551+ } ) ;
552+ mocks . readGatewayServiceState . mockReset ( ) . mockResolvedValue ( {
553+ installed : true ,
554+ loaded : true ,
555+ running : true ,
556+ env : { } ,
557+ command : { programArguments : [ "openclaw" , "gateway" ] , sourcePath : "/tmp/gateway.service" } ,
558+ runtime : { status : "running" } ,
559+ } ) ;
560+ mocks . resolveGatewayService . mockReset ( ) . mockReturnValue ( { label : "openclaw-gateway" } ) ;
561+ } ) ;
562+
563+ it ( "reports unreachable gateway health probes" , async ( ) => {
564+ mocks . probeGatewayStatus . mockResolvedValueOnce ( {
565+ ok : false ,
566+ error : "connect ECONNREFUSED 127.0.0.1:5829" ,
567+ } ) ;
568+
569+ await expect (
570+ collectGatewayHealthFindings ( { cfg : { gateway : { mode : "local" } } } ) ,
571+ ) . resolves . toContainEqual ( {
572+ checkId : "core/doctor/gateway-health" ,
573+ severity : "warning" ,
574+ message : "Gateway is not reachable: connect ECONNREFUSED 127.0.0.1:5829" ,
575+ path : "gateway.mode" ,
576+ target : "http://127.0.0.1:5829" ,
577+ fixHint :
578+ "Start the Gateway service or run `openclaw doctor --fix` for service repair prompts." ,
579+ } ) ;
580+ } ) ;
581+
582+ it ( "redacts sensitive remote gateway URLs from health finding targets" , async ( ) => {
583+ mocks . buildGatewayProbeConnectionDetails . mockResolvedValueOnce ( {
584+ url :
"wss://user:[email protected] /rpc?token=secret&safe=value" , 585+ } ) ;
586+ mocks . probeGatewayStatus . mockResolvedValueOnce ( {
587+ ok : false ,
588+ error : "remote gateway did not answer" ,
589+ } ) ;
590+
591+ const findings = await collectGatewayHealthFindings ( {
592+ cfg : { gateway : { mode : "remote" , remote : { url : "wss://gateway.example.test/rpc" } } } ,
593+ } ) ;
594+
595+ expect ( findings ) . toContainEqual ( {
596+ checkId : "core/doctor/gateway-health" ,
597+ severity : "warning" ,
598+ message : "Gateway is not reachable: remote gateway did not answer" ,
599+ path : "gateway.remote.url" ,
600+ target : "wss://***:***@gateway.example.test/rpc?token=***&safe=value" ,
601+ fixHint : "Verify the remote Gateway URL, network path, TLS settings, and credentials." ,
602+ } ) ;
603+ expect ( JSON . stringify ( findings ) ) . not . toContain ( "user:pass" ) ;
604+ expect ( JSON . stringify ( findings ) ) . not . toContain ( "token=secret" ) ;
605+ } ) ;
606+
607+ it ( "reports missing local gateway daemon service" , async ( ) => {
608+ mocks . readGatewayServiceState . mockResolvedValueOnce ( {
609+ installed : false ,
610+ loaded : false ,
611+ running : false ,
612+ env : { } ,
613+ command : null ,
614+ } ) ;
615+
616+ await expect (
617+ collectGatewayDaemonFindings ( { cfg : { gateway : { mode : "local" } } } ) ,
618+ ) . resolves . toContainEqual ( {
619+ checkId : "core/doctor/gateway-daemon" ,
620+ severity : "warning" ,
621+ message : "Gateway service is not installed." ,
622+ path : "gateway.mode" ,
623+ target : "openclaw-gateway" ,
624+ fixHint : "Run `openclaw doctor --fix` or `openclaw gateway install` to install it." ,
625+ } ) ;
626+ } ) ;
627+
628+ it ( "skips daemon findings for remote gateway mode" , async ( ) => {
629+ await expect (
630+ collectGatewayDaemonFindings ( { cfg : { gateway : { mode : "remote" } } } ) ,
631+ ) . resolves . toEqual ( [ ] ) ;
632+
633+ expect ( mocks . readGatewayServiceState ) . not . toHaveBeenCalled ( ) ;
634+ } ) ;
635+ } ) ;
636+
506637describe ( "doctor provider catalog projection checks" , ( ) => {
507638 beforeEach ( ( ) => {
508639 mocks . resolvePluginProviders . mockReset ( ) . mockReturnValue ( [ ] ) ;
0 commit comments