@@ -100,3 +100,182 @@ describe("codex adapter", () => {
100100 expect ( cap . highest ) . toBe ( 1 ) ;
101101 } ) ;
102102} ) ;
103+
104+ describe ( "codex adapter TOML splicing against real-world configs" , ( ) => {
105+ const cfgPath = ( ) => join ( homeDir , ".codex" , "config.toml" ) ;
106+ const writeCfg = ( text : string ) => {
107+ mkdirSync ( join ( homeDir , ".codex" ) , { recursive : true } ) ;
108+ writeFileSync ( cfgPath ( ) , text , "utf8" ) ;
109+ } ;
110+
111+ it ( "uninstall keeps a following server whose header carries an inline comment" , async ( ) => {
112+ writeCfg (
113+ [
114+ 'model = "gpt-5"' ,
115+ "" ,
116+ "[mcp_servers.graphctx]" ,
117+ 'command = "graphctx"' ,
118+ 'args = ["serve", "--mcp"]' ,
119+ "" ,
120+ "# web search server" ,
121+ "[mcp_servers.exa] # keep me" ,
122+ 'url = "https://mcp.exa.ai/mcp"' ,
123+ 'api_key = "exa-key-123"' ,
124+ "" ,
125+ ] . join ( "\n" ) ,
126+ ) ;
127+
128+ await new CodexAdapter ( workDir , homeDir ) . uninstall ( ) ;
129+
130+ const text = readFileSync ( cfgPath ( ) , "utf8" ) ;
131+ expect ( text ) . toContain ( 'model = "gpt-5"' ) ;
132+ expect ( text ) . toContain ( "# web search server" ) ;
133+ expect ( text ) . toContain ( "[mcp_servers.exa] # keep me" ) ;
134+ expect ( text ) . toContain ( 'api_key = "exa-key-123"' ) ;
135+ expect ( text ) . not . toContain ( "mcp_servers.graphctx" ) ;
136+ } ) ;
137+
138+ it ( "uninstall keeps a following array-of-tables section" , async ( ) => {
139+ writeCfg (
140+ [
141+ "[mcp_servers.graphctx]" ,
142+ 'command = "graphctx"' ,
143+ 'args = ["serve", "--mcp"]' ,
144+ "" ,
145+ "[[profiles]]" ,
146+ 'name = "work"' ,
147+ "" ,
148+ "[[profiles]]" ,
149+ 'name = "home"' ,
150+ "" ,
151+ ] . join ( "\n" ) ,
152+ ) ;
153+
154+ await new CodexAdapter ( workDir , homeDir ) . uninstall ( ) ;
155+
156+ const text = readFileSync ( cfgPath ( ) , "utf8" ) ;
157+ expect ( text . match ( / ^ \[ \[ p r o f i l e s \] \] / gm) ) . toHaveLength ( 2 ) ;
158+ expect ( text ) . toContain ( 'name = "work"' ) ;
159+ expect ( text ) . toContain ( 'name = "home"' ) ;
160+ expect ( text ) . not . toContain ( "mcp_servers.graphctx" ) ;
161+ } ) ;
162+
163+ it ( "reinstall replaces the block without swallowing servers below it" , async ( ) => {
164+ writeCfg (
165+ [
166+ "[mcp_servers.graphctx]" ,
167+ 'command = "graphctx"' ,
168+ 'args = ["serve", "--mcp"]' ,
169+ "" ,
170+ "[mcp_servers.exa] # added after graphctx" ,
171+ 'url = "https://mcp.exa.ai/mcp"' ,
172+ "" ,
173+ ] . join ( "\n" ) ,
174+ ) ;
175+
176+ await new CodexAdapter ( workDir , homeDir ) . install ( {
177+ workspaceDir : workDir ,
178+ binPath : "/abs/path/to/graphctx" ,
179+ } ) ;
180+
181+ const text = readFileSync ( cfgPath ( ) , "utf8" ) ;
182+ expect ( text ) . toContain ( "[mcp_servers.exa] # added after graphctx" ) ;
183+ expect ( text ) . toContain ( 'url = "https://mcp.exa.ai/mcp"' ) ;
184+ expect ( text ) . toContain ( 'command = "/abs/path/to/graphctx"' ) ;
185+ expect ( text . match ( / \[ m c p _ s e r v e r s \. g r a p h c t x \] / g) ) . toHaveLength ( 1 ) ;
186+ } ) ;
187+
188+ it ( "uninstall removes the graphctx env subtable along with the block" , async ( ) => {
189+ writeCfg (
190+ [
191+ "[mcp_servers.graphctx]" ,
192+ 'command = "graphctx"' ,
193+ 'args = ["serve", "--mcp"]' ,
194+ "" ,
195+ "[mcp_servers.graphctx.env]" ,
196+ 'GRAPHCTX_INJECT_TOTAL_BUDGET_TOKENS = "900"' ,
197+ "" ,
198+ "[mcp_servers.other]" ,
199+ 'command = "other"' ,
200+ "" ,
201+ ] . join ( "\n" ) ,
202+ ) ;
203+
204+ await new CodexAdapter ( workDir , homeDir ) . uninstall ( ) ;
205+
206+ const text = readFileSync ( cfgPath ( ) , "utf8" ) ;
207+ expect ( text ) . not . toContain ( "mcp_servers.graphctx" ) ;
208+ expect ( text ) . not . toContain ( "GRAPHCTX_INJECT_TOTAL_BUDGET_TOKENS" ) ;
209+ expect ( text ) . toContain ( "[mcp_servers.other]" ) ;
210+ expect ( text ) . toContain ( 'command = "other"' ) ;
211+ } ) ;
212+
213+ it ( "reinstall preserves a user's graphctx env subtable" , async ( ) => {
214+ writeCfg (
215+ [
216+ "[mcp_servers.graphctx]" ,
217+ 'command = "graphctx"' ,
218+ 'args = ["serve", "--mcp"]' ,
219+ "" ,
220+ "[mcp_servers.graphctx.env]" ,
221+ 'GRAPHCTX_INJECT_TOTAL_BUDGET_TOKENS = "900"' ,
222+ "" ,
223+ ] . join ( "\n" ) ,
224+ ) ;
225+
226+ await new CodexAdapter ( workDir , homeDir ) . install ( {
227+ workspaceDir : workDir ,
228+ binPath : "/abs/path/to/graphctx" ,
229+ } ) ;
230+
231+ const text = readFileSync ( cfgPath ( ) , "utf8" ) ;
232+ expect ( text ) . toContain ( 'command = "/abs/path/to/graphctx"' ) ;
233+ expect ( text ) . toContain ( "[mcp_servers.graphctx.env]" ) ;
234+ expect ( text ) . toContain ( 'GRAPHCTX_INJECT_TOTAL_BUDGET_TOKENS = "900"' ) ;
235+ expect ( text . match ( / \[ m c p _ s e r v e r s \. g r a p h c t x \] / g) ) . toHaveLength ( 1 ) ;
236+ } ) ;
237+
238+ it ( "reinstall replaces the block when the user annotated our header, instead of duplicating it" , async ( ) => {
239+ writeCfg (
240+ [
241+ "[mcp_servers.graphctx] # managed by graphctx" ,
242+ 'command = "graphctx"' ,
243+ 'args = ["serve", "--mcp"]' ,
244+ "" ,
245+ ] . join ( "\n" ) ,
246+ ) ;
247+
248+ await new CodexAdapter ( workDir , homeDir ) . install ( {
249+ workspaceDir : workDir ,
250+ binPath : "/abs/path/to/graphctx" ,
251+ } ) ;
252+
253+ const text = readFileSync ( cfgPath ( ) , "utf8" ) ;
254+ expect ( text . match ( / \[ m c p _ s e r v e r s \. g r a p h c t x \] / g) ) . toHaveLength ( 1 ) ;
255+ expect ( text ) . toContain ( 'command = "/abs/path/to/graphctx"' ) ;
256+ } ) ;
257+
258+ it ( "uninstall keeps servers below the block in a CRLF config" , async ( ) => {
259+ writeCfg (
260+ [
261+ 'model = "gpt-5"' ,
262+ "" ,
263+ "[mcp_servers.graphctx]" ,
264+ 'command = "graphctx"' ,
265+ 'args = ["serve", "--mcp"]' ,
266+ "" ,
267+ "[mcp_servers.exa] # crlf config" ,
268+ 'api_key = "exa-key-123"' ,
269+ "" ,
270+ ] . join ( "\r\n" ) ,
271+ ) ;
272+
273+ await new CodexAdapter ( workDir , homeDir ) . uninstall ( ) ;
274+
275+ const text = readFileSync ( cfgPath ( ) , "utf8" ) ;
276+ expect ( text ) . toContain ( 'model = "gpt-5"' ) ;
277+ expect ( text ) . toContain ( "[mcp_servers.exa] # crlf config" ) ;
278+ expect ( text ) . toContain ( 'api_key = "exa-key-123"' ) ;
279+ expect ( text ) . not . toContain ( "mcp_servers.graphctx" ) ;
280+ } ) ;
281+ } ) ;
0 commit comments