fix: preserve unreadable event config files (#3861)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Marsel Safin
2026-07-30 16:08:27 +02:00
committed by GitHub
parent 296fdf2ee7
commit e916fd1b3b
2 changed files with 21 additions and 6 deletions

View File

@@ -1983,23 +1983,25 @@ def _drop_marked_entries(entries: list) -> list:
def _load_user_json(path: Path) -> dict | None:
"""Load a user-owned JSON file, aborting (None) on parse failure (#22/#23).
"""Load a user-owned JSON file, aborting (None) on read/parse failure (#22/#23).
Returns the parsed dict, or ``None`` when the file is missing or cannot be
parsed (e.g. JSONC with comments, or temporarily malformed JSON). Callers
must skip the merge rather than resetting user content to ``{}``.
read or parsed (e.g. JSONC with comments, a temporarily malformed JSON
document, or an unreadable path). Callers must skip the merge rather than
resetting user content to ``{}``.
"""
if not path.exists():
return {}
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, ValueError) as exc:
except (json.JSONDecodeError, OSError, ValueError) as exc:
logger.warning(
"Could not parse %s (may contain JSONC comments or be malformed); "
"Could not read or parse %s (it may be unreadable, contain JSONC "
"comments, or be malformed); "
"skipping event-config merge to preserve user content.",
path,
)
logger.debug("Parse error detail: %s", exc)
logger.debug("Read/parse error detail: %s", exc)
return None
if not isinstance(data, dict):
logger.warning("%s is not a JSON object; skipping event-config merge.", path)

View File

@@ -1411,6 +1411,19 @@ class TestTeardownDataSafety:
# User content preserved verbatim — not reset to {}.
assert config_path.read_text() == jsonc
def test_unreadable_config_not_overwritten_on_merge(self, tmp_path):
"""An unreadable user config aborts the merge instead of crashing."""
integration = ClaudeIntegration()
config_path = tmp_path / ".claude/settings.json"
config_path.mkdir(parents=True)
install_integration_events(
integration, tmp_path, _claude_manifest(tmp_path),
{"pre_tool_use": [{"command": "speckit.tdd.validate"}]},
)
assert config_path.is_dir()
def test_jsonc_opencode_config_not_reset(self, tmp_path):
"""#23: a malformed opencode.json is preserved, not reset to {}."""
integration = OpencodeIntegration()