fix(config): isolate broken JSONC layers and treat comment-only as empty

Address review findings on the #2923 fail-closed parse fix:

- Comment-only files produce ValueExpected with no JSON value; treat that
  as empty config instead of INVALID_JSONC. Partial object trees still throw.
- readConfigLayers no longer lets one unparseable layer abort valid sibling
  layers. Mutations still fail closed on the custom/user write target.

Co-authored-by: serkraser <serkraser@gmail.com>
This commit is contained in:
Cursor Agent
2026-08-15 05:13:32 +00:00
co-authored by serkraser
parent 4cc090130c
commit 35563dd78d
6 changed files with 210 additions and 34 deletions
@@ -3,7 +3,7 @@ import fs from 'fs';
import os from 'os';
import path from 'path';
import { parseMdFile, writeMdFile, readConfigFile, writeConfig } from './shared.js';
import { parseMdFile, writeMdFile, readConfigFile, readConfigLayers, writeConfig } from './shared.js';
import { updateAgent } from './agents.js';
import { updateMcpConfig } from './mcp.js';
@@ -337,4 +337,40 @@ describe('readConfigFile / writeConfig JSONC safety (issue #2923)', () => {
else process.env.OPENCODE_CONFIG = previousOpenCodeConfig;
}
});
it('returns an empty object for a comment-only config file', () => {
const file = writeFixture('comments.jsonc', '// placeholder\n/* still empty */\n');
expect(readConfigFile(file)).toEqual({});
});
it('keeps a valid custom layer readable when a project layer is unparseable', () => {
const custom = writeFixture('custom.jsonc', VALID_CONFIG);
const projectDir = path.join(FIXTURE_DIR, 'project');
const projectFile = writeFixture(path.join('project', '.opencode', 'opencode.jsonc'), PARTIAL_PARSE_CONFIG);
const previousOpenCodeConfig = process.env.OPENCODE_CONFIG;
try {
process.env.OPENCODE_CONFIG = custom;
const layers = readConfigLayers(projectDir);
expect(layers.customConfig.plugin).toEqual(['opencode-see-image']);
expect(layers.projectConfig).toEqual({});
expect(layers.mergedConfig.plugin).toEqual(['opencode-see-image']);
expect(layers.layerErrors).toEqual([
expect.objectContaining({
path: projectFile,
code: 'INVALID_JSONC',
}),
]);
updateMcpConfig('openproject', { enabled: false }, projectDir);
const rewritten = JSON.parse(fs.readFileSync(custom, 'utf8'));
expect(rewritten.plugin).toEqual(['opencode-see-image']);
expect(rewritten.mcp.openproject.enabled).toBe(false);
expect(fs.readFileSync(projectFile, 'utf8')).toBe(PARTIAL_PARSE_CONFIG);
expect(fs.existsSync(`${projectFile}.openchamber.backup`)).toBe(false);
} finally {
if (previousOpenCodeConfig === undefined) delete process.env.OPENCODE_CONFIG;
else process.env.OPENCODE_CONFIG = previousOpenCodeConfig;
}
});
});