1- import { beforeEach , describe , expect , it , vi } from "vitest" ;
1+ import fs from "node:fs" ;
2+ import os from "node:os" ;
3+ import path from "node:path" ;
4+ import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
25import { loadRootHelpRenderOptionsForConfigSensitivePlugins } from "./root-help-live-config.js" ;
36
47const readConfigFileSnapshotMock = vi . hoisted ( ( ) => vi . fn ( ) ) ;
@@ -8,10 +11,134 @@ vi.mock("../config/config.js", () => ({
811} ) ) ;
912
1013describe ( "root help live config" , ( ) => {
14+ const originalEnv = {
15+ HOME : process . env . HOME ,
16+ OPENCLAW_CONFIG_PATH : process . env . OPENCLAW_CONFIG_PATH ,
17+ OPENCLAW_HOME : process . env . OPENCLAW_HOME ,
18+ OPENCLAW_STATE_DIR : process . env . OPENCLAW_STATE_DIR ,
19+ OPENCLAW_BUNDLED_PLUGINS_DIR : process . env . OPENCLAW_BUNDLED_PLUGINS_DIR ,
20+ OPENCLAW_DISABLE_BUNDLED_PLUGINS : process . env . OPENCLAW_DISABLE_BUNDLED_PLUGINS ,
21+ } ;
22+
1123 beforeEach ( ( ) => {
1224 vi . clearAllMocks ( ) ;
1325 } ) ;
1426
27+ afterEach ( ( ) => {
28+ vi . restoreAllMocks ( ) ;
29+ for ( const [ key , value ] of Object . entries ( originalEnv ) ) {
30+ if ( value === undefined ) {
31+ delete process . env [ key ] ;
32+ } else {
33+ process . env [ key ] = value ;
34+ }
35+ }
36+ } ) ;
37+
38+ it ( "skips the heavy config module when no plugin-sensitive config file exists" , async ( ) => {
39+ const tempDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-root-help-" ) ) ;
40+ process . env . OPENCLAW_STATE_DIR = path . join ( tempDir , "state" ) ;
41+ process . env . OPENCLAW_CONFIG_PATH = path . join ( tempDir , "missing-openclaw.json" ) ;
42+ delete process . env . OPENCLAW_BUNDLED_PLUGINS_DIR ;
43+ delete process . env . OPENCLAW_DISABLE_BUNDLED_PLUGINS ;
44+
45+ await expect (
46+ loadRootHelpRenderOptionsForConfigSensitivePlugins ( process . env ) ,
47+ ) . resolves . toBeNull ( ) ;
48+
49+ expect ( readConfigFileSnapshotMock ) . not . toHaveBeenCalled ( ) ;
50+ fs . rmSync ( tempDir , { recursive : true , force : true } ) ;
51+ } ) ;
52+
53+ it ( "checks live config when plugin-sensitive env only exists in legacy gateway env" , async ( ) => {
54+ const tempDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-root-help-" ) ) ;
55+ try {
56+ const cwdDir = path . join ( tempDir , "cwd" ) ;
57+ const gatewayEnvPath = path . join ( tempDir , ".config" , "openclaw" , "gateway.env" ) ;
58+ fs . mkdirSync ( cwdDir , { recursive : true } ) ;
59+ fs . mkdirSync ( path . dirname ( gatewayEnvPath ) , { recursive : true } ) ;
60+ fs . writeFileSync ( gatewayEnvPath , "OPENCLAW_DISABLE_BUNDLED_PLUGINS=1\n" , "utf8" ) ;
61+ vi . spyOn ( process , "cwd" ) . mockReturnValue ( cwdDir ) ;
62+ process . env . HOME = tempDir ;
63+ process . env . OPENCLAW_STATE_DIR = path . join ( tempDir , ".openclaw" ) ;
64+ delete process . env . OPENCLAW_CONFIG_PATH ;
65+ delete process . env . OPENCLAW_HOME ;
66+ delete process . env . OPENCLAW_BUNDLED_PLUGINS_DIR ;
67+ delete process . env . OPENCLAW_DISABLE_BUNDLED_PLUGINS ;
68+ readConfigFileSnapshotMock . mockResolvedValueOnce ( {
69+ valid : true ,
70+ sourceConfig : { } ,
71+ runtimeConfig : { } ,
72+ } ) ;
73+
74+ await expect (
75+ loadRootHelpRenderOptionsForConfigSensitivePlugins ( process . env ) ,
76+ ) . resolves . toBeNull ( ) ;
77+
78+ expect ( readConfigFileSnapshotMock ) . toHaveBeenCalledOnce ( ) ;
79+ } finally {
80+ fs . rmSync ( tempDir , { recursive : true , force : true } ) ;
81+ }
82+ } ) ;
83+
84+ it ( "ignores legacy gateway env during precheck for an explicit custom state dir" , async ( ) => {
85+ const tempDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-root-help-" ) ) ;
86+ try {
87+ const cwdDir = path . join ( tempDir , "cwd" ) ;
88+ const gatewayEnvPath = path . join ( tempDir , ".config" , "openclaw" , "gateway.env" ) ;
89+ fs . mkdirSync ( cwdDir , { recursive : true } ) ;
90+ fs . mkdirSync ( path . dirname ( gatewayEnvPath ) , { recursive : true } ) ;
91+ fs . writeFileSync ( gatewayEnvPath , "OPENCLAW_DISABLE_BUNDLED_PLUGINS=1\n" , "utf8" ) ;
92+ vi . spyOn ( process , "cwd" ) . mockReturnValue ( cwdDir ) ;
93+ process . env . HOME = tempDir ;
94+ process . env . OPENCLAW_STATE_DIR = path . join ( tempDir , "custom-state" ) ;
95+ process . env . OPENCLAW_CONFIG_PATH = path . join ( tempDir , "missing-openclaw.json" ) ;
96+ delete process . env . OPENCLAW_HOME ;
97+ delete process . env . OPENCLAW_BUNDLED_PLUGINS_DIR ;
98+ delete process . env . OPENCLAW_DISABLE_BUNDLED_PLUGINS ;
99+
100+ await expect (
101+ loadRootHelpRenderOptionsForConfigSensitivePlugins ( process . env ) ,
102+ ) . resolves . toBeNull ( ) ;
103+
104+ expect ( readConfigFileSnapshotMock ) . not . toHaveBeenCalled ( ) ;
105+ } finally {
106+ fs . rmSync ( tempDir , { recursive : true , force : true } ) ;
107+ }
108+ } ) ;
109+
110+ it ( "checks live config when plugin-sensitive env exists beside an explicit config path" , async ( ) => {
111+ const tempDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-root-help-" ) ) ;
112+ try {
113+ const cwdDir = path . join ( tempDir , "cwd" ) ;
114+ const configPath = path . join ( tempDir , "custom" , "missing-openclaw.json" ) ;
115+ const configDirEnvPath = path . join ( path . dirname ( configPath ) , ".env" ) ;
116+ fs . mkdirSync ( cwdDir , { recursive : true } ) ;
117+ fs . mkdirSync ( path . dirname ( configDirEnvPath ) , { recursive : true } ) ;
118+ fs . writeFileSync ( configDirEnvPath , "OPENCLAW_DISABLE_BUNDLED_PLUGINS=1\n" , "utf8" ) ;
119+ vi . spyOn ( process , "cwd" ) . mockReturnValue ( cwdDir ) ;
120+ process . env . HOME = tempDir ;
121+ process . env . OPENCLAW_CONFIG_PATH = configPath ;
122+ delete process . env . OPENCLAW_STATE_DIR ;
123+ delete process . env . OPENCLAW_HOME ;
124+ delete process . env . OPENCLAW_BUNDLED_PLUGINS_DIR ;
125+ delete process . env . OPENCLAW_DISABLE_BUNDLED_PLUGINS ;
126+ readConfigFileSnapshotMock . mockResolvedValueOnce ( {
127+ valid : true ,
128+ sourceConfig : { } ,
129+ runtimeConfig : { } ,
130+ } ) ;
131+
132+ await expect (
133+ loadRootHelpRenderOptionsForConfigSensitivePlugins ( process . env ) ,
134+ ) . resolves . toBeNull ( ) ;
135+
136+ expect ( readConfigFileSnapshotMock ) . toHaveBeenCalledOnce ( ) ;
137+ } finally {
138+ fs . rmSync ( tempDir , { recursive : true , force : true } ) ;
139+ }
140+ } ) ;
141+
15142 it ( "uses precomputed help when plugin-sensitive config is invalid" , async ( ) => {
16143 readConfigFileSnapshotMock . mockResolvedValueOnce ( {
17144 valid : false ,
0 commit comments