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:
@@ -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)', () => {
|
||||
|
||||
Reference in New Issue
Block a user