fix: allow no-agent cron edits without prompt

This commit is contained in:
Michael Lam
2026-05-07 10:22:44 -07:00
committed by nesquena-hermes
parent 0ed63968b6
commit 48773e8ff7
3 changed files with 87 additions and 5 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+16 -5
View File
@@ -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 ? `<div class="detail-row"><div class="detail-row-label">${esc(t('error_prefix').replace(/:\s*$/,''))}</div><div class="detail-row-value" style="color:var(--accent-text)">${esc(job.last_error)}</div></div>` : '';
@@ -450,6 +453,8 @@ function _renderCronDetail(job){
<div class="detail-row"><div class="detail-row-label">${esc(t('cron_next'))}</div><div class="detail-row-value">${esc(nextRun)}</div></div>
<div class="detail-row"><div class="detail-row-label">${esc(t('cron_last'))}</div><div class="detail-row-value">${esc(lastRun)}</div></div>
<div class="detail-row"><div class="detail-row-label">Deliver</div><div class="detail-row-value">${esc(deliver)}</div></div>
<div class="detail-row"><div class="detail-row-label">Mode</div><div class="detail-row-value"><span class="detail-badge" id="cronJobMode">${esc(cronJobMode)}</span></div></div>
${isNoAgent ? `<div class="detail-row"><div class="detail-row-label">No-agent script</div><div class="detail-row-value"><code>${esc(script || '—')}</code></div></div>` : ''}
<div class="detail-row"><div class="detail-row-label">${esc(t('cron_profile_label') || 'Profile')}</div><div class="detail-row-value"><span class="detail-badge active" title="${esc(profileTitle)}">${esc(profileLabel)}</span></div></div>
<div class="detail-row"><div class="detail-row-label">Skills</div><div class="detail-row-value">${esc(skills)}</div></div>
${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) => `<option value="${v}"${deliver===v?' selected':''}>${esc(l)}</option>`;
body.innerHTML = `
@@ -714,9 +722,10 @@ function _renderCronForm({ name, schedule, prompt, deliver, profile, isEdit }){
<input type="text" id="cronFormSchedule" value="${esc(schedule || '')}" placeholder="0 9 * * * — every 1h — @daily" autocomplete="off" required>
<div class="detail-form-hint">${esc(t('cron_schedule_hint') || "Cron expression or shorthand like 'every 1h'.")}</div>
</div>
<div class="detail-form-row">
<div class="detail-form-row ${isNoAgent ? 'cron-no-agent-prompt-row' : ''}">
<label for="cronFormPrompt">${esc(t('cron_prompt_label') || 'Prompt')}</label>
<textarea id="cronFormPrompt" rows="6" placeholder="${esc(t('cron_prompt_placeholder') || 'Must be self-contained')}" required>${esc(prompt || '')}</textarea>
<textarea id="cronFormPrompt" rows="6" placeholder="${esc(t('cron_prompt_placeholder') || 'Must be self-contained')}"${isNoAgent ? ' disabled' : ' required'}>${esc(prompt || '')}</textarea>
${isNoAgent ? `<div class="detail-form-hint cron-no-agent-hint">No-agent mode runs the configured script directly; Prompt is unused. No-agent script: <code>${esc(script || '—')}</code></div>` : ''}
</div>
<div class="detail-form-row">
<label for="cronFormDeliver">${esc(t('cron_deliver_label') || 'Deliver output to')}</label>
@@ -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;
+71
View File
@@ -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