Treating every undefined parse as empty config let a file that is not JSON
at all (YAML, plain text) read as {}, so a later write would back it up and
replace it - the same data loss this fix is meant to prevent. Only a
comment-only parse, where ValueExpected is the sole error, counts as empty.
150 lines
5.7 KiB
TypeScript
150 lines
5.7 KiB
TypeScript
import { afterEach, beforeEach, describe, test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
import { listPluginEntries, updateMcpConfig } from './opencodeConfig';
|
|
|
|
const PARTIAL_PARSE_CONFIG = [
|
|
'{',
|
|
' "$schema": "https://opencode.ai/config.json",',
|
|
' plugin: ["opencode-see-image"],',
|
|
' mcp: {',
|
|
' openproject: {',
|
|
' type: "remote",',
|
|
' url: "https://openproject.example.com/mcp",',
|
|
' enabled: true',
|
|
' }',
|
|
' },',
|
|
' provider: {',
|
|
' "ollama-cloud": {',
|
|
' npm: "@ai-sdk/openai-compatible",',
|
|
' name: "Ollama Cloud"',
|
|
' }',
|
|
' }',
|
|
'}',
|
|
'',
|
|
].join('\n');
|
|
|
|
const VALID_CONFIG = [
|
|
'{',
|
|
' "$schema": "https://opencode.ai/config.json",',
|
|
' "plugin": ["opencode-see-image"],',
|
|
' "mcp": {',
|
|
' "openproject": {',
|
|
' "type": "remote",',
|
|
' "url": "https://openproject.example.com/mcp",',
|
|
' "enabled": true',
|
|
' }',
|
|
' },',
|
|
' "provider": {',
|
|
' "ollama-cloud": {',
|
|
' "npm": "@ai-sdk/openai-compatible",',
|
|
' "name": "Ollama Cloud"',
|
|
' }',
|
|
' }',
|
|
'}',
|
|
'',
|
|
].join('\n');
|
|
|
|
const isInvalidJsoncError = (error: unknown): boolean => {
|
|
if (!(error instanceof Error) || !/cannot be loaded safely/.test(error.message)) {
|
|
return false;
|
|
}
|
|
// SAFETY: the config layer throws Error instances carrying the coded `code` field.
|
|
return (error as Error & { code?: string }).code === 'INVALID_JSONC';
|
|
};
|
|
|
|
describe('opencodeConfig JSONC parse safety (issue #2923)', () => {
|
|
let tempDir: string;
|
|
let previousOpenCodeConfig: string | undefined;
|
|
|
|
beforeEach(() => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-vscode-config-parse-'));
|
|
previousOpenCodeConfig = process.env.OPENCODE_CONFIG;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (previousOpenCodeConfig === undefined) delete process.env.OPENCODE_CONFIG;
|
|
else process.env.OPENCODE_CONFIG = previousOpenCodeConfig;
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('refuses MCP updates that would overwrite a partial-parse config', () => {
|
|
const configPath = path.join(tempDir, 'opencode.jsonc');
|
|
fs.writeFileSync(configPath, PARTIAL_PARSE_CONFIG, 'utf8');
|
|
process.env.OPENCODE_CONFIG = configPath;
|
|
|
|
assert.throws(() => updateMcpConfig('openproject', { enabled: true }), isInvalidJsoncError);
|
|
assert.equal(fs.readFileSync(configPath, 'utf8'), PARTIAL_PARSE_CONFIG);
|
|
assert.equal(fs.existsSync(`${configPath}.openchamber.backup`), false);
|
|
});
|
|
|
|
test('preserves unrelated keys when updating a valid MCP config', () => {
|
|
const configPath = path.join(tempDir, 'opencode.jsonc');
|
|
fs.writeFileSync(configPath, VALID_CONFIG, 'utf8');
|
|
process.env.OPENCODE_CONFIG = configPath;
|
|
|
|
updateMcpConfig('openproject', { enabled: false });
|
|
|
|
const rewritten = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
assert.deepEqual(rewritten.plugin, ['opencode-see-image']);
|
|
assert.equal(rewritten.provider['ollama-cloud'].name, 'Ollama Cloud');
|
|
assert.equal(rewritten.mcp.openproject.enabled, false);
|
|
assert.equal(fs.readFileSync(`${configPath}.openchamber.backup`, 'utf8'), VALID_CONFIG);
|
|
});
|
|
|
|
test('returns an empty object for a comment-only config file', () => {
|
|
const configPath = path.join(tempDir, 'comments.jsonc');
|
|
fs.writeFileSync(configPath, '// placeholder\n/* still empty */\n', 'utf8');
|
|
process.env.OPENCODE_CONFIG = configPath;
|
|
|
|
assert.deepEqual(listPluginEntries(), []);
|
|
});
|
|
|
|
test('refuses MCP updates against content that yields no JSON value at all', () => {
|
|
const configPath = path.join(tempDir, 'yamlish.jsonc');
|
|
const contents = 'mcp:\n openproject:\n type: remote\n';
|
|
fs.writeFileSync(configPath, contents, 'utf8');
|
|
process.env.OPENCODE_CONFIG = configPath;
|
|
|
|
assert.throws(() => updateMcpConfig('openproject', { enabled: true }), isInvalidJsoncError);
|
|
assert.equal(fs.readFileSync(configPath, 'utf8'), contents);
|
|
assert.equal(fs.existsSync(`${configPath}.openchamber.backup`), false);
|
|
});
|
|
|
|
test('lists custom-layer plugins when a project layer is unparseable', () => {
|
|
const customPath = path.join(tempDir, 'custom.jsonc');
|
|
const projectDir = path.join(tempDir, 'project');
|
|
const projectFile = path.join(projectDir, '.opencode', 'opencode.jsonc');
|
|
fs.writeFileSync(customPath, VALID_CONFIG, 'utf8');
|
|
fs.mkdirSync(path.dirname(projectFile), { recursive: true });
|
|
fs.writeFileSync(projectFile, PARTIAL_PARSE_CONFIG, 'utf8');
|
|
process.env.OPENCODE_CONFIG = customPath;
|
|
|
|
const specs = listPluginEntries(projectDir).map((entry) => entry.spec);
|
|
assert.deepEqual(specs, ['opencode-see-image']);
|
|
assert.equal(fs.readFileSync(projectFile, 'utf8'), PARTIAL_PARSE_CONFIG);
|
|
assert.equal(fs.existsSync(`${projectFile}.openchamber.backup`), false);
|
|
});
|
|
|
|
test('keeps a valid custom layer writable when a project layer is unparseable', () => {
|
|
const customPath = path.join(tempDir, 'custom.jsonc');
|
|
const projectDir = path.join(tempDir, 'project');
|
|
const projectFile = path.join(projectDir, '.opencode', 'opencode.jsonc');
|
|
fs.writeFileSync(customPath, VALID_CONFIG, 'utf8');
|
|
fs.mkdirSync(path.dirname(projectFile), { recursive: true });
|
|
fs.writeFileSync(projectFile, PARTIAL_PARSE_CONFIG, 'utf8');
|
|
process.env.OPENCODE_CONFIG = customPath;
|
|
|
|
updateMcpConfig('openproject', { enabled: false }, projectDir);
|
|
|
|
const rewritten = JSON.parse(fs.readFileSync(customPath, 'utf8'));
|
|
assert.deepEqual(rewritten.plugin, ['opencode-see-image']);
|
|
assert.equal(rewritten.mcp.openproject.enabled, false);
|
|
assert.equal(fs.readFileSync(projectFile, 'utf8'), PARTIAL_PARSE_CONFIG);
|
|
assert.equal(fs.existsSync(`${projectFile}.openchamber.backup`), false);
|
|
});
|
|
});
|