diff --git a/docs/pr-media/1820/no-agent-cron-edit.png b/docs/pr-media/1820/no-agent-cron-edit.png new file mode 100644 index 00000000..ffb1af9f Binary files /dev/null and b/docs/pr-media/1820/no-agent-cron-edit.png differ diff --git a/static/panels.js b/static/panels.js index 0add57b8..76cfd2e7 100644 --- a/static/panels.js +++ b/static/panels.js @@ -422,6 +422,9 @@ function _renderCronDetail(job){ const schedule = job.schedule_display || (job.schedule && job.schedule.expression) || ''; const skills = Array.isArray(job.skills) && job.skills.length ? job.skills.join(', ') : '—'; const deliver = job.deliver || 'local'; + const isNoAgent = !!job.no_agent; + const cronJobMode = isNoAgent ? 'no-agent' : 'agent'; + const script = job.script || ''; const profileLabel = _cronProfileLabel(job.profile); const profileTitle = _cronProfileTitle(job.profile); const lastError = job.last_error ? `
${esc(t('error_prefix').replace(/:\s*$/,''))}
${esc(job.last_error)}
` : ''; @@ -450,6 +453,8 @@ function _renderCronDetail(job){
${esc(t('cron_next'))}
${esc(nextRun)}
${esc(t('cron_last'))}
${esc(lastRun)}
Deliver
${esc(deliver)}
+
Mode
${esc(cronJobMode)}
+ ${isNoAgent ? `
No-agent script
${esc(script || '—')}
` : ''}
${esc(t('cron_profile_label') || 'Profile')}
${esc(profileLabel)}
Skills
${esc(skills)}
${lastError} @@ -685,6 +690,8 @@ function openCronEdit(job){ prompt: job.prompt || '', deliver: job.deliver || 'local', profile: job.profile || '', + no_agent: !!job.no_agent, + script: job.script || '', isEdit: true, }); if (!_cronSkillsCache) { @@ -695,11 +702,12 @@ function openCronEdit(job){ loadCronProfiles().then(()=>_refreshCronProfileSelect(job.profile || '')).catch(()=>{}); } -function _renderCronForm({ name, schedule, prompt, deliver, profile, isEdit }){ +function _renderCronForm({ name, schedule, prompt, deliver, profile, no_agent=false, script='', isEdit }){ const title = $('taskDetailTitle'); const body = $('taskDetailBody'); const empty = $('taskDetailEmpty'); if (!body || !title) return; + const isNoAgent = !!no_agent; title.textContent = isEdit ? (t('edit') + ' · ' + (name || schedule || t('scheduled_jobs'))) : t('new_job'); const deliverOpt = (v,l) => ``; body.innerHTML = ` @@ -714,9 +722,10 @@ function _renderCronForm({ name, schedule, prompt, deliver, profile, isEdit }){
${esc(t('cron_schedule_hint') || "Cron expression or shorthand like 'every 1h'.")}
-
+
- + + ${isNoAgent ? `
No-agent mode runs the configured script directly; Prompt is unused. No-agent script: ${esc(script || '—')}
` : ''}
@@ -825,12 +834,14 @@ async function saveCronForm(){ const prompt=promptEl.value.trim(); const deliver=delivEl?delivEl.value:'local'; const profile=profileEl?profileEl.value:''; + const isNoAgent = !!(_currentCronDetail && _currentCronDetail.no_agent); errEl.style.display='none'; if(!schedule){errEl.textContent=t('cron_schedule_required_example');errEl.style.display='';return;} - if(!prompt){errEl.textContent=t('cron_prompt_required');errEl.style.display='';return;} + if(!isNoAgent && !prompt){errEl.textContent=t('cron_prompt_required');errEl.style.display='';return;} try{ if (_editingCronId) { - const updates = {job_id: _editingCronId, schedule, prompt, profile: profile}; + const updates = {job_id: _editingCronId, schedule, profile: profile}; + if (!isNoAgent) updates.prompt = prompt; if (name) updates.name = name; await api('/api/crons/update', {method:'POST', body: JSON.stringify(updates)}); const editedId = _editingCronId; diff --git a/tests/test_cron_no_agent_edit.py b/tests/test_cron_no_agent_edit.py new file mode 100644 index 00000000..a77a3590 --- /dev/null +++ b/tests/test_cron_no_agent_edit.py @@ -0,0 +1,71 @@ +"""Regression coverage for issue #1820: no-agent cron edits do not require prompts.""" + +from __future__ import annotations + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PANELS_JS = (ROOT / "static" / "panels.js").read_text() + + +def _function_body(name: str) -> str: + marker = f"function {name}(" + start = PANELS_JS.find(marker) + assert start != -1, f"{name} not found" + paren = PANELS_JS.find("(", start) + assert paren != -1, f"{name} params not found" + depth = 0 + for idx in range(paren, len(PANELS_JS)): + ch = PANELS_JS[idx] + if ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + if depth == 0: + brace = PANELS_JS.find("{", idx) + break + else: + raise AssertionError(f"{name} params did not terminate") + assert brace != -1, f"{name} body not found" + depth = 0 + for idx in range(brace, len(PANELS_JS)): + ch = PANELS_JS[idx] + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + return PANELS_JS[brace + 1 : idx] + raise AssertionError(f"{name} body did not terminate") + + +def test_open_cron_edit_plumbs_no_agent_and_script_to_form(): + body = _function_body("openCronEdit") + assert "no_agent: !!job.no_agent" in body + assert "script: job.script || ''" in body + + +def test_no_agent_form_drops_prompt_required_attribute_and_shows_script_context(): + body = _function_body("_renderCronForm") + assert "no_agent" in body and "script" in body + assert "const isNoAgent = !!no_agent;" in body + assert "cron-no-agent-hint" in body + assert "No-agent script" in body + assert "${isNoAgent ? ' disabled' : ' required'}" in body + + +def test_save_cron_form_keeps_agent_prompt_required_but_skips_no_agent_edits(): + body = _function_body("saveCronForm") + assert "const isNoAgent = !!(_currentCronDetail && _currentCronDetail.no_agent);" in body + assert "if(!isNoAgent && !prompt)" in body + assert "cron_prompt_required" in body + assert "if (!isNoAgent) updates.prompt = prompt;" in body + + +def test_no_agent_detail_displays_mode_and_script(): + body = _function_body("_renderCronDetail") + assert "const isNoAgent = !!job.no_agent;" in body + assert "No-agent script" in body + assert "cronJobMode" in body + assert "job.script" in body