diff --git a/docs/pr-media/guard-skill-detail-errors/skill-detail-error-after.png b/docs/pr-media/guard-skill-detail-errors/skill-detail-error-after.png new file mode 100644 index 00000000..9bb58ac5 Binary files /dev/null and b/docs/pr-media/guard-skill-detail-errors/skill-detail-error-after.png differ diff --git a/docs/pr-media/guard-skill-detail-errors/skill-detail-error-before.png b/docs/pr-media/guard-skill-detail-errors/skill-detail-error-before.png new file mode 100644 index 00000000..c8897e04 Binary files /dev/null and b/docs/pr-media/guard-skill-detail-errors/skill-detail-error-before.png differ diff --git a/static/panels.js b/static/panels.js index ca7e6733..d6771e21 100644 --- a/static/panels.js +++ b/static/panels.js @@ -3244,6 +3244,21 @@ function _renderSkillDetail(name, content, linkedFiles) { _setSkillHeaderButtons('read'); } +function _renderSkillError(name, message) { + const title = $('skillDetailTitle'); + const body = $('skillDetailBody'); + const empty = $('skillDetailEmpty'); + if (title) title.textContent = name; + if (body) { + body.innerHTML = `
${esc(message || t('skill_load_failed'))}
`; + body.style.display = ''; + } + if (empty) empty.style.display = 'none'; + _currentSkillDetail = null; + _skillMode = 'empty'; + _setSkillHeaderButtons('empty'); +} + function _setSkillHeaderButtons(mode) { const editBtn = $('btnEditSkillDetail'); const delBtn = $('btnDeleteSkillDetail'); @@ -3264,6 +3279,12 @@ async function openSkill(name, el) { _editingSkillName = null; try { const data = await api(`/api/skills/content?name=${encodeURIComponent(name)}`); + if (data && (data.success === false || data.error)) { + const message = data.error || t('skill_load_failed'); + _renderSkillError(name, message); + setStatus(t('skill_load_failed') + message); + return; + } _currentSkillDetail = { name, content: data.content || '', linked_files: data.linked_files || {} }; _renderSkillDetail(name, data.content || '', data.linked_files || {}); } catch(e) { setStatus(t('skill_load_failed') + e.message); } @@ -3272,6 +3293,11 @@ async function openSkill(name, el) { async function openSkillFile(skillName, filePath) { try { const data = await api(`/api/skills/content?name=${encodeURIComponent(skillName)}&file=${encodeURIComponent(filePath)}`); + if (data && data.error) { + _renderSkillError(skillName, data.error); + setStatus(t('skill_file_load_failed') + data.error); + return; + } const body = $('skillDetailBody'); if (!body) return; const ext = (filePath.split('.').pop() || '').toLowerCase(); diff --git a/tests/test_skill_detail_error_guard.py b/tests/test_skill_detail_error_guard.py new file mode 100644 index 00000000..f4cb02aa --- /dev/null +++ b/tests/test_skill_detail_error_guard.py @@ -0,0 +1,28 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent.parent +PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8") + + +def test_skill_detail_api_error_renders_in_detail_pane(): + assert "function _renderSkillError(name, message)" in PANELS_JS + assert "data.success === false || data.error" in PANELS_JS + assert "_renderSkillError(name, message);" in PANELS_JS + assert "_currentSkillDetail = null;" in PANELS_JS + assert "_setSkillHeaderButtons('empty');" in PANELS_JS + + +def test_skill_detail_no_longer_silently_falls_back_on_error_payload(): + success_guard = PANELS_JS.index("data.success === false || data.error") + render_detail = PANELS_JS.index("_renderSkillDetail(name, data.content || ''") + assert success_guard < render_detail + + +def test_skill_linked_file_api_error_renders_in_detail_pane(): + file_fetch = PANELS_JS.index("/api/skills/content?name=${encodeURIComponent(skillName)}&file=") + file_error_guard = PANELS_JS.index("if (data && data.error)", file_fetch) + file_render = PANELS_JS.index("_renderSkillError(skillName, data.error);", file_fetch) + body_lookup = PANELS_JS.index("const body = $('skillDetailBody');", file_fetch) + assert file_error_guard < body_lookup + assert file_render < body_lookup