maint: integrate reviewed contribution #3798

This commit is contained in:
haelyra committed 2026-10-29 11:34:50 -07:00
commit 2d9b503e9a
3 files changed
+124

No files matched your search

+49
View File
@@ -580,6 +580,17 @@ enum ScheduleCommands {
#[arg(long)]
json: bool,
},
/// Update only the instructions for one existing schedule
Update {
/// Existing schedule ID; never creates another schedule
schedule_id: i64,
/// Replacement task instructions (preserves timing and agent identity)
#[arg(short, long)]
task: String,
/// Emit machine-readable JSON instead of the human summary
#[arg(long)]
json: bool,
},
/// List scheduled tasks
List {
/// Emit machine-readable JSON instead of the human summary
@@ -2708,6 +2719,17 @@ async fn main() -> Result<()> {
);
}
}
ScheduleCommands::Update { schedule_id, task, json } => {
let schedule = session::manager::update_scheduled_task_text(&db, schedule_id, &task)?;
if json {
println!("{}", serde_json::to_string_pretty(&schedule)?);
} else {
println!(
"Updated schedule {} without changing its next run {}",
schedule.id, schedule.next_run_at.to_rfc3339()
);
}
}
ScheduleCommands::List { json } => {
let schedules = session::manager::list_scheduled_tasks(&db)?;
if json {
@@ -9026,6 +9048,33 @@ mod tests {
}
}
#[test]
fn cli_parses_native_schedule_update_without_identity_mutation_options() {
let cli = Cli::try_parse_from([
"ecc", "schedule", "update", "16", "--task",
"Read original ECC native READY items", "--json",
])
.expect("native schedule update must parse");
match cli.command {
Some(Commands::Schedule {
command: ScheduleCommands::Update { schedule_id, task, json }
}) => {
assert_eq!(schedule_id, 16);
assert!(task.contains("READY items"));
assert!(json);
}
_ => panic!("expected native existing schedule update"),
}
assert!(Cli::try_parse_from([
"ecc", "schedule", "update", "16", "--task",
"New instructions", "--cron", "* * * * *",
]).is_err(), "schedule update must not mutate cron");
assert!(Cli::try_parse_from([
"ecc", "schedule", "update", "16", "--task",
"New instructions", "--agent", "codex",
]).is_err(), "schedule update must not mutate the agent");
}
#[test]
fn cli_parses_remote_computer_use_command() {
let cli = Cli::try_parse_from([
+14
View File
@@ -245,6 +245,20 @@ pub fn create_scheduled_task(
)
}
/// Replace only the instruction text on a native schedule. Keep ID, timing,
/// agent/profile, worktree and other immutable scheduling fields unchanged.
pub fn update_scheduled_task_text(
db: &StateStore,
schedule_id: i64,
task: &str,
) -> Result<ScheduledTask> {
anyhow::ensure!(schedule_id > 0, "Schedule ID must be positive");
anyhow::ensure!(!task.trim().is_empty(), "Schedule task must not be empty");
anyhow::ensure!(task.len() <= 16 * 1024, "Schedule task exceeds 16 KiB");
db.update_scheduled_task_text(schedule_id, task)?
.with_context(|| format!("Scheduled task not found: {schedule_id}"))
}
pub fn list_scheduled_tasks(db: &StateStore) -> Result<Vec<ScheduledTask>> {
db.list_scheduled_tasks()
}
+61
View File
@@ -1576,6 +1576,22 @@ impl StateStore {
.ok_or_else(|| anyhow::anyhow!("Scheduled task {id} was not found after insert"))
}
/// Update instructions in place; preserve the existing ID and due time.
pub fn update_scheduled_task_text(
&self,
schedule_id: i64,
task: &str,
) -> Result<Option<ScheduledTask>> {
let modified = self.conn.execute(
"UPDATE scheduled_tasks SET task = ?2, updated_at = ?3 WHERE id = ?1",
rusqlite::params![schedule_id, task, chrono::Utc::now().to_rfc3339()],
)?;
if modified == 0 {
return Ok(None);
}
self.get_scheduled_task(schedule_id)
}
pub fn list_scheduled_tasks(&self) -> Result<Vec<ScheduledTask>> {
let mut stmt = self.conn.prepare(
"SELECT id, cron_expr, task, agent_type, profile_name, working_dir, project, task_group,
@@ -7296,6 +7312,51 @@ mod tests {
Ok(())
}
#[test]
fn scheduled_task_update_keeps_original_identity_and_next_due_time() -> Result<()> {
let tempdir = TestDir::new("store-schedule-inplace-update")?;
let db = StateStore::open(&tempdir.path().join("state.db"))?;
let next = Utc::now() + ChronoDuration::minutes(2);
let original = db.insert_scheduled_task(
"*/2 * * * *",
"Old instructions",
"claude",
Some("lead-profile"),
tempdir.path(),
"pace",
"g0",
false,
next,
)?;
let previous_run = Utc::now() - ChronoDuration::minutes(2);
db.record_scheduled_task_run(original.id, previous_run, next)?;
let original = db
.get_scheduled_task(original.id)?
.context("recorded schedule must exist before update")?;
assert_eq!(original.last_run_at, Some(previous_run));
let updated = crate::session::manager::update_scheduled_task_text(
&db, original.id, "Read ECC native READY items",
)?;
assert_eq!(updated.id, original.id);
assert_eq!(updated.task, "Read ECC native READY items");
assert_eq!(updated.cron_expr, original.cron_expr);
assert_eq!(updated.next_run_at, original.next_run_at);
assert_eq!(updated.last_run_at, original.last_run_at);
assert_eq!(updated.agent_type, original.agent_type);
assert_eq!(updated.profile_name, original.profile_name);
assert_eq!(updated.working_dir, original.working_dir);
assert_eq!(updated.project, original.project);
assert_eq!(updated.task_group, original.task_group);
assert_eq!(updated.use_worktree, original.use_worktree);
assert_eq!(db.list_scheduled_tasks()?.len(), 1);
assert!(crate::session::manager::update_scheduled_task_text(&db, original.id, " ").is_err());
assert!(crate::session::manager::update_scheduled_task_text(&db, original.id, &"x".repeat(16385)).is_err());
assert!(crate::session::manager::update_scheduled_task_text(&db, original.id + 1, "valid task").is_err());
assert_eq!(db.list_scheduled_tasks()?.len(), 1);
Ok(())
}
#[test]
fn context_graph_detail_includes_incoming_and_outgoing_relations() -> Result<()> {
let tempdir = TestDir::new("store-context-relations")?;