fix(installer): Codex TOML block finder preserves trailing array-of-tables siblings (#1351) (#1370)

The Codex installer's `findNextTableHeader` skipped `[[array-of-tables]]` headers instead of treating them as a block boundary, so any `[[...]]` block after `[mcp_servers.codegraph]` in ~/.codex/config.toml was silently deleted on install/upgrade/uninstall. Now treats both `[...]` and `[[...]]` as boundaries, with a small line lexer so header-shaped text inside multiline strings/arrays isn't mistaken for a boundary. Adds round-trip regression coverage (install → reinstall → uninstall) + CHANGELOG entry.

Fixes #1351. Supersedes #624.

Thanks @KtzeAbyss.
This commit is contained in:
Yuc
2026-07-22 15:19:16 -05:00
committed by GitHub
parent ea72e1b190
commit 572d22bfbe
3 changed files with 269 additions and 19 deletions
+149
View File
@@ -876,6 +876,48 @@ describe('Installer targets — partial-state idempotency', () => {
expect(after).not.toContain('enabled = true');
});
it('codex: install, re-install, and uninstall preserve trailing array-of-tables siblings', () => {
const codex = getTarget('codex')!;
const tomlPath = path.join(tmpHome, '.codex', 'config.toml');
fs.mkdirSync(path.dirname(tomlPath), { recursive: true });
const historyTables = [
'[[history]]',
'id = 1',
'note = "keep first"',
'',
'[[history]]',
'id = 2',
'note = "keep second"',
'',
].join('\n');
fs.writeFileSync(tomlPath, [
'[mcp_servers.codegraph]',
'command = "old-codegraph"',
'args = ["old"]',
'description = """',
'header-shaped text inside a multiline string:',
'[[not-a-table]]',
'still part of the string',
'"""',
'',
historyTables,
].join('\n'));
const first = codex.install('global', { autoAllow: false });
expect(first.files.find((f) => f.path === tomlPath)?.action).toBe('updated');
const afterInstall = fs.readFileSync(tomlPath, 'utf-8');
expect(afterInstall).toContain('command = "codegraph"');
expect(afterInstall).not.toContain('[[not-a-table]]');
expect(afterInstall.endsWith(historyTables)).toBe(true);
const second = codex.install('global', { autoAllow: false });
expect(second.files.find((f) => f.path === tomlPath)?.action).toBe('unchanged');
expect(fs.readFileSync(tomlPath, 'utf-8')).toBe(afterInstall);
codex.uninstall('global');
expect(fs.readFileSync(tomlPath, 'utf-8')).toBe(historyTables);
});
it('claude: local install writes ./.mcp.json (project scope), not ./.claude.json', () => {
const claude = getTarget('claude')!;
const result = claude.install('local', { autoAllow: false });
@@ -1292,6 +1334,113 @@ describe('Installer targets — TOML serializer (Codex backbone)', () => {
expect(content.match(/\[\[foo\]\]/g)?.length).toBe(2);
expect(content).toContain('[mcp_servers.codegraph]');
});
it('upsert replaces the managed table without consuming trailing array-of-tables siblings', () => {
const historyTables = [
'[[history]]',
'id = 1',
'note = "keep first"',
'',
'[[history]]',
'id = 2',
'note = "keep second"',
'',
].join('\n');
const existing = [
'[mcp_servers.codegraph]',
'command = "old-codegraph"',
'args = ["old"]',
'',
historyTables,
].join('\n');
const block = buildTomlTable('mcp_servers.codegraph', {
command: 'codegraph',
args: ['serve', '--mcp'],
});
const { content, action } = upsertTomlTable(existing, 'mcp_servers.codegraph', block);
expect(action).toBe('replaced');
expect(content).toBe(`${block}\n\n${historyTables}`);
});
it('remove preserves trailing array-of-tables siblings byte-for-byte', () => {
const historyTables = [
'[[history]]',
'id = 1',
'note = "keep first"',
'',
'[[history]]',
'id = 2',
'note = "keep second"',
'',
].join('\n');
const existing = [
'[mcp_servers.codegraph]',
'command = "codegraph"',
'args = ["serve", "--mcp"]',
'',
historyTables,
].join('\n');
const { content, action } = removeTomlTable(existing, 'mcp_servers.codegraph');
expect(action).toBe('removed');
expect(content).toBe(historyTables);
});
it.each([
['table', '[ mcp_servers.other ]'],
['array-of-tables', '[[ history ]]'],
])('preserves a trailing %s header with inner whitespace', (_kind, siblingHeader) => {
const siblingTable = `${siblingHeader}\nvalue = "keep"\n`;
const existing = [
'[mcp_servers.codegraph]',
'command = "old-codegraph"',
'args = ["old"]',
'',
siblingTable,
].join('\n');
const block = buildTomlTable('mcp_servers.codegraph', {
command: 'codegraph',
args: ['serve', '--mcp'],
});
const upserted = upsertTomlTable(existing, 'mcp_servers.codegraph', block);
const removed = removeTomlTable(existing, 'mcp_servers.codegraph');
expect(upserted.content).toBe(`${block}\n\n${siblingTable}`);
expect(removed.content).toBe(siblingTable);
});
it.each([
['basic', '"""'],
['literal', "'''"],
])('ignores header-shaped text inside a multiline %s string', (_kind, delimiter) => {
const historyTable = '[[history]]\nid = 1\n';
const existing = [
'[mcp_servers.codegraph]',
'command = "old-codegraph"',
'args = [',
` ${delimiter}first line`,
'[[not-a-table]]',
`last line${delimiter},`,
' "serve",',
']',
'',
historyTable,
].join('\n');
const block = buildTomlTable('mcp_servers.codegraph', {
command: 'codegraph',
args: ['serve', '--mcp'],
});
const upserted = upsertTomlTable(existing, 'mcp_servers.codegraph', block);
const removed = removeTomlTable(existing, 'mcp_servers.codegraph');
expect(upserted.content).toBe(`${block}\n\n${historyTable}`);
expect(removed.content).toBe(historyTable);
});
});
describe('Installer — uninstallTargets sweep (codegraph uninstall)', () => {