Expose public task restart boundary

This commit is contained in:
Michel Paulissen 2026-07-03 21:28:56 +02:00
parent f586b48893
commit 0b14889c79
5 changed files with 419 additions and 8 deletions

View file

@ -368,6 +368,7 @@ struct ProcessCancelArgs {
#[derive(Clone, Debug, Subcommand)]
enum TaskCommands {
List(TaskListArgs),
Restart(TaskRestartArgs),
}
#[derive(Clone, Debug, Parser)]
@ -378,6 +379,17 @@ struct TaskListArgs {
process: Option<String>,
}
#[derive(Clone, Debug, Parser)]
struct TaskRestartArgs {
#[command(flatten)]
scope: CliScopeArgs,
task: String,
#[arg(long, default_value = "vp-current")]
process: String,
#[arg(long)]
yes: bool,
}
#[derive(Clone, Debug, Parser)]
struct LogsArgs {
#[command(flatten)]
@ -1307,6 +1319,44 @@ fn task_list_report(args: TaskListArgs) -> Result<Value> {
}))
}
fn task_restart_report(args: TaskRestartArgs) -> Result<Value> {
if let Some(coordinator) = &args.scope.coordinator {
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request_allow_error(json!({
"type": "restart_task",
"tenant": args.scope.tenant,
"project": args.scope.project,
"actor_user": args.scope.user,
"process": args.process,
"task": args.task,
}))?;
let restart_request = task_restart_request_summary(&response, !args.yes);
return Ok(json!({
"command": "task restart",
"coordinator": coordinator,
"process": args.process,
"task": args.task,
"requires_confirmation": !args.yes,
"restart_request": restart_request,
"response": response,
"coordinator_session_requests": session.requests(),
}));
}
Ok(json!({
"command": "task restart",
"status": "requires_coordinator",
"requires_confirmation": !args.yes,
"process": args.process,
"task": args.task,
"restart_request": {
"status": "requires_coordinator",
"operation": "restart_selected_task",
"explicit_user_action": true,
"clean_boundary_required": true,
},
}))
}
fn logs_report(args: LogsArgs) -> Result<Value> {
let events = list_task_events_if_available(
args.scope.coordinator.as_deref(),
@ -2048,11 +2098,18 @@ fn main() -> Result<()> {
};
emit_report(&report, json_output)?;
}
Commands::Task {
command: TaskCommands::List(args),
} => {
let json_output = args.scope.json;
emit_report(&task_list_report(args)?, json_output)?;
Commands::Task { command } => {
let (report, json_output) = match command {
TaskCommands::List(args) => {
let json_output = args.scope.json;
(task_list_report(args)?, json_output)
}
TaskCommands::Restart(args) => {
let json_output = args.scope.json;
(task_restart_report(args)?, json_output)
}
};
emit_report(&report, json_output)?;
}
Commands::Logs(args) => {
let json_output = args.scope.json;
@ -2592,6 +2649,49 @@ fn process_cancel_request_summary(response: &Value, requires_confirmation: bool)
})
}
fn task_restart_request_summary(response: &Value, requires_confirmation: bool) -> Value {
if response.get("type").and_then(Value::as_str) != Some("task_restart") {
return json!({
"status": response.get("type").and_then(Value::as_str).unwrap_or("coordinator_response"),
"operation": "restart_selected_task",
"accepted": false,
"requires_confirmation": requires_confirmation,
"explicit_user_action": true,
"clean_boundary_required": true,
"error": response.get("message").cloned().unwrap_or(Value::Null),
});
}
json!({
"status": "task_restart",
"operation": "restart_selected_task",
"accepted": response.get("accepted").cloned().unwrap_or_else(|| json!(false)),
"process": response.get("process").cloned().unwrap_or(Value::Null),
"task": response.get("task").cloned().unwrap_or(Value::Null),
"requires_confirmation": requires_confirmation,
"explicit_user_action": true,
"clean_boundary_required": true,
"clean_boundary_available": response
.get("clean_boundary_available")
.cloned()
.unwrap_or_else(|| json!(false)),
"active_task": response
.get("active_task")
.cloned()
.unwrap_or_else(|| json!(false)),
"completed_event_observed": response
.get("completed_event_observed")
.cloned()
.unwrap_or_else(|| json!(false)),
"requires_whole_process_restart": response
.get("requires_whole_process_restart")
.cloned()
.unwrap_or_else(|| json!(true)),
"message": response.get("message").cloned().unwrap_or(Value::Null),
"website_required": false,
})
}
fn artifact_download_session_summary(response: &Value) -> Value {
if response.get("type").and_then(Value::as_str) != Some("artifact_download_link") {
return json!({
@ -4368,6 +4468,7 @@ mod tests {
&["disasmer", "process", "restart", "--yes"],
&["disasmer", "process", "cancel", "--yes"],
&["disasmer", "task", "list"],
&["disasmer", "task", "restart", "compile-linux", "--yes"],
&["disasmer", "logs"],
&["disasmer", "artifact", "list"],
&["disasmer", "artifact", "download", "artifact"],
@ -5191,6 +5292,60 @@ mod tests {
assert_eq!(cancel["cancel_request"]["new_task_launches_blocked"], true);
}
#[test]
fn task_restart_reports_clean_boundary_requirements() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains(r#""type":"restart_task""#));
assert!(line.contains(r#""tenant":"tenant""#));
assert!(line.contains(r#""project":"project""#));
assert!(line.contains(r#""actor_user":"user""#));
assert!(line.contains(r#""process":"vp""#));
assert!(line.contains(r#""task":"compile-linux""#));
stream
.write_all(
br#"{"type":"task_restart","process":"vp","task":"compile-linux","actor":"user","accepted":false,"clean_boundary_available":false,"active_task":true,"completed_event_observed":false,"requires_whole_process_restart":true,"message":"selected task is still active; clean task restart requires a captured checkpoint boundary"}"#,
)
.unwrap();
stream.write_all(b"\n").unwrap();
});
let report = task_restart_report(TaskRestartArgs {
scope: CliScopeArgs {
coordinator: Some(addr),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
task: "compile-linux".to_owned(),
process: "vp".to_owned(),
yes: true,
})
.unwrap();
server.join().unwrap();
assert_eq!(report["command"], "task restart");
assert_eq!(
report["restart_request"]["operation"],
"restart_selected_task"
);
assert_eq!(report["restart_request"]["accepted"], false);
assert_eq!(report["restart_request"]["clean_boundary_available"], false);
assert_eq!(
report["restart_request"]["requires_whole_process_restart"],
true
);
assert_eq!(report["restart_request"]["active_task"], true);
assert_eq!(report["restart_request"]["website_required"], false);
assert_eq!(report["coordinator_session_requests"], 1);
}
#[test]
fn build_command_reuses_bundle_inspection_without_full_repo_upload() {
let temp = tempfile::tempdir().unwrap();