Classify CLI failure reports
This commit is contained in:
parent
218ab65e47
commit
50d298ead7
5 changed files with 370 additions and 6 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"kind": "disasmer-filtered-public-tree",
|
"kind": "disasmer-filtered-public-tree",
|
||||||
"source_commit": "9b295127cfd7e4e87d4518a817563ca8e695ed0b",
|
"source_commit": "3d37a45472228c553477f8462927dfa6a6622882",
|
||||||
"release_name": "dryrun-9b295127cfd7",
|
"release_name": "dryrun-3d37a4547222",
|
||||||
"filtered_out": [
|
"filtered_out": [
|
||||||
"private/**",
|
"private/**",
|
||||||
"experiments/**",
|
"experiments/**",
|
||||||
|
|
|
||||||
|
|
@ -1807,6 +1807,8 @@ fn human_report(value: &Value) -> String {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
push_machine_error_line(&mut lines, value.get("machine_error"));
|
||||||
|
push_machine_error_line(&mut lines, value.pointer("/run_start/machine_error"));
|
||||||
if let Some(flow) = value.get("human_flow") {
|
if let Some(flow) = value.get("human_flow") {
|
||||||
if let Some(device) = flow.get("Device") {
|
if let Some(device) = flow.get("Device") {
|
||||||
lines.push("flow: device".to_owned());
|
lines.push("flow: device".to_owned());
|
||||||
|
|
@ -2013,10 +2015,221 @@ fn push_nested_string_field(lines: &mut Vec<String>, value: &Value, key: &str, l
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_machine_error_line(lines: &mut Vec<String>, machine_error: Option<&Value>) {
|
||||||
|
let Some(machine_error) = machine_error else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if machine_error.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let category = machine_error
|
||||||
|
.get("category")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
let exit_code = machine_error
|
||||||
|
.get("stable_exit_code")
|
||||||
|
.and_then(Value::as_i64)
|
||||||
|
.unwrap_or(1);
|
||||||
|
lines.push(format!("error category: {category} (exit {exit_code})"));
|
||||||
|
if let Some(next_actions) = machine_error.get("next_actions").and_then(Value::as_array) {
|
||||||
|
let actions = next_actions
|
||||||
|
.iter()
|
||||||
|
.filter_map(Value::as_str)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !actions.is_empty() {
|
||||||
|
lines.push(format!("error next: {}", actions.join("; ")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn compact_json(value: &Value) -> String {
|
fn compact_json(value: &Value) -> String {
|
||||||
serde_json::to_string(value).unwrap_or_else(|_| "<unprintable>".to_owned())
|
serde_json::to_string(value).unwrap_or_else(|_| "<unprintable>".to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cli_error_summary(message: &str) -> Value {
|
||||||
|
let category = classify_cli_error_message(message);
|
||||||
|
cli_error_summary_for_category(category, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cli_error_summary_with_default(message: &str, default_category: &'static str) -> Value {
|
||||||
|
let category = classify_cli_error_message(message);
|
||||||
|
let category = if category == "unknown" {
|
||||||
|
default_category
|
||||||
|
} else {
|
||||||
|
category
|
||||||
|
};
|
||||||
|
cli_error_summary_for_category(category, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cli_error_summary_for_category(category: &'static str, message: &str) -> Value {
|
||||||
|
json!({
|
||||||
|
"category": category,
|
||||||
|
"stable_exit_code": cli_error_exit_code(category),
|
||||||
|
"process_exit_code_applied": false,
|
||||||
|
"retryable_after_user_action": cli_error_retryable_after_user_action(category),
|
||||||
|
"message": message,
|
||||||
|
"safe_failure": true,
|
||||||
|
"next_actions": cli_error_next_actions(category),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classify_cli_error_message(message: &str) -> &'static str {
|
||||||
|
let message = message.to_ascii_lowercase();
|
||||||
|
if message.contains("already has active virtual process") || message.contains("active process")
|
||||||
|
{
|
||||||
|
return "active_process";
|
||||||
|
}
|
||||||
|
if message.contains("no capable node")
|
||||||
|
|| message.contains("missing capability")
|
||||||
|
|| message.contains("capability")
|
||||||
|
|| message.contains("placement failed")
|
||||||
|
|| message.contains("cmd.run")
|
||||||
|
|| message.contains("node required")
|
||||||
|
{
|
||||||
|
return "capability";
|
||||||
|
}
|
||||||
|
if message.contains("quota")
|
||||||
|
|| message.contains("resource limit")
|
||||||
|
|| message.contains("limit exceeded")
|
||||||
|
|| message.contains("metering")
|
||||||
|
{
|
||||||
|
return "quota";
|
||||||
|
}
|
||||||
|
if message.contains("policy")
|
||||||
|
|| message.contains("disallowed")
|
||||||
|
|| message.contains("not allowed")
|
||||||
|
|| message.contains("suspended")
|
||||||
|
|| message.contains("blocked by")
|
||||||
|
|| message.contains("denied")
|
||||||
|
{
|
||||||
|
return "policy";
|
||||||
|
}
|
||||||
|
if message.contains("unauthenticated")
|
||||||
|
|| message.contains("not authenticated")
|
||||||
|
|| message.contains("login required")
|
||||||
|
|| message.contains("browser login required")
|
||||||
|
|| message.contains("missing session")
|
||||||
|
|| message.contains("no cli session")
|
||||||
|
|| message.contains("401")
|
||||||
|
{
|
||||||
|
return "authentication";
|
||||||
|
}
|
||||||
|
if message.contains("unauthorized")
|
||||||
|
|| message.contains("not authorized")
|
||||||
|
|| message.contains("forbidden")
|
||||||
|
|| message.contains("permission")
|
||||||
|
|| message.contains("tenant mismatch")
|
||||||
|
|| message.contains("project mismatch")
|
||||||
|
{
|
||||||
|
return "authorization";
|
||||||
|
}
|
||||||
|
if message.contains("failed to connect")
|
||||||
|
|| message.contains("connection refused")
|
||||||
|
|| message.contains("closed session")
|
||||||
|
|| message.contains("timed out")
|
||||||
|
|| message.contains("timeout")
|
||||||
|
|| message.contains("name resolution")
|
||||||
|
|| message.contains("network unreachable")
|
||||||
|
|| message.contains("dns")
|
||||||
|
{
|
||||||
|
return "connectivity";
|
||||||
|
}
|
||||||
|
if message.contains("missing environment")
|
||||||
|
|| message.contains("environment mismatch")
|
||||||
|
|| message.contains("incompatible environment")
|
||||||
|
|| message.contains("containerfile")
|
||||||
|
|| message.contains("dockerfile")
|
||||||
|
|| message.contains("envs/")
|
||||||
|
{
|
||||||
|
return "environment";
|
||||||
|
}
|
||||||
|
if message.contains("task exited")
|
||||||
|
|| message.contains("exit status")
|
||||||
|
|| message.contains("status code")
|
||||||
|
|| message.contains("stderr")
|
||||||
|
|| message.contains("panic")
|
||||||
|
|| message.contains("program error")
|
||||||
|
|| message.contains("command failed")
|
||||||
|
{
|
||||||
|
return "program";
|
||||||
|
}
|
||||||
|
"unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cli_error_exit_code(category: &str) -> i64 {
|
||||||
|
match category {
|
||||||
|
"authentication" => 20,
|
||||||
|
"authorization" => 21,
|
||||||
|
"quota" => 22,
|
||||||
|
"policy" => 23,
|
||||||
|
"capability" => 24,
|
||||||
|
"connectivity" => 25,
|
||||||
|
"environment" => 26,
|
||||||
|
"program" => 27,
|
||||||
|
"active_process" => 28,
|
||||||
|
_ => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cli_error_retryable_after_user_action(category: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
category,
|
||||||
|
"authentication"
|
||||||
|
| "authorization"
|
||||||
|
| "quota"
|
||||||
|
| "policy"
|
||||||
|
| "capability"
|
||||||
|
| "connectivity"
|
||||||
|
| "environment"
|
||||||
|
| "program"
|
||||||
|
| "active_process"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cli_error_next_actions(category: &str) -> Vec<&'static str> {
|
||||||
|
match category {
|
||||||
|
"authentication" => vec!["disasmer login --browser", "disasmer auth status"],
|
||||||
|
"authorization" => vec![
|
||||||
|
"disasmer auth status",
|
||||||
|
"check tenant/project selection",
|
||||||
|
"ask an admin to grant access",
|
||||||
|
],
|
||||||
|
"quota" => vec![
|
||||||
|
"disasmer quota status",
|
||||||
|
"reduce concurrent work or wait for usage to fall",
|
||||||
|
],
|
||||||
|
"policy" => vec![
|
||||||
|
"disasmer doctor",
|
||||||
|
"check coordinator policy for this action",
|
||||||
|
],
|
||||||
|
"capability" => vec![
|
||||||
|
"disasmer node list",
|
||||||
|
"attach a node with the required capabilities",
|
||||||
|
"check tenant/project on the attached node",
|
||||||
|
],
|
||||||
|
"connectivity" => vec![
|
||||||
|
"disasmer doctor",
|
||||||
|
"check the coordinator endpoint and network reachability",
|
||||||
|
],
|
||||||
|
"environment" => vec![
|
||||||
|
"disasmer inspect",
|
||||||
|
"check envs/<name>/Containerfile or envs/<name>/Dockerfile",
|
||||||
|
],
|
||||||
|
"program" => vec!["disasmer logs", "fix the program or task command and rerun"],
|
||||||
|
"active_process" => vec![
|
||||||
|
"disasmer process status",
|
||||||
|
"disasmer logs",
|
||||||
|
"disasmer process restart --yes",
|
||||||
|
"disasmer process cancel --yes",
|
||||||
|
"wait for the active process to finish",
|
||||||
|
],
|
||||||
|
_ => vec![
|
||||||
|
"disasmer doctor",
|
||||||
|
"rerun with --json for machine-readable details",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
match cli.command {
|
match cli.command {
|
||||||
|
|
@ -2568,6 +2781,7 @@ fn task_summaries(task_events: Option<&Value>) -> Value {
|
||||||
"explanation_available": placement.is_some(),
|
"explanation_available": placement.is_some(),
|
||||||
},
|
},
|
||||||
"failure_reason": task_failure_reason(event),
|
"failure_reason": task_failure_reason(event),
|
||||||
|
"machine_error": task_failure_machine_error(event),
|
||||||
"stdout_bytes": event_u64(event, "stdout_bytes").unwrap_or(0),
|
"stdout_bytes": event_u64(event, "stdout_bytes").unwrap_or(0),
|
||||||
"stderr_bytes": event_u64(event, "stderr_bytes").unwrap_or(0),
|
"stderr_bytes": event_u64(event, "stderr_bytes").unwrap_or(0),
|
||||||
})
|
})
|
||||||
|
|
@ -2594,6 +2808,20 @@ fn task_failure_reason(event: &Value) -> Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn task_failure_machine_error(event: &Value) -> Value {
|
||||||
|
match event.get("terminal_state").and_then(Value::as_str) {
|
||||||
|
Some("failed") => {
|
||||||
|
let reason = task_failure_reason(event)
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("task failed")
|
||||||
|
.to_owned();
|
||||||
|
cli_error_summary_with_default(&reason, "program")
|
||||||
|
}
|
||||||
|
Some("cancelled") => cli_error_summary_for_category("program", "task cancelled"),
|
||||||
|
_ => Value::Null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn process_state_from_tasks(task_events: Option<&Value>) -> &'static str {
|
fn process_state_from_tasks(task_events: Option<&Value>) -> &'static str {
|
||||||
let events = task_event_values(task_events);
|
let events = task_event_values(task_events);
|
||||||
if events.is_empty() {
|
if events.is_empty() {
|
||||||
|
|
@ -2933,7 +3161,11 @@ fn quota_next_blocked_action(current_usage: &Value) -> Value {
|
||||||
"action": "node_work_requires_online_attached_node",
|
"action": "node_work_requires_online_attached_node",
|
||||||
"category": "capability",
|
"category": "capability",
|
||||||
"quota_related": false,
|
"quota_related": false,
|
||||||
"message": "no online attached node is visible for work that requires a user node"
|
"message": "no online attached node is visible for work that requires a user node",
|
||||||
|
"machine_error": cli_error_summary_for_category(
|
||||||
|
"capability",
|
||||||
|
"no online attached node is visible for work that requires a user node"
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Value::Null
|
Value::Null
|
||||||
|
|
@ -3568,10 +3800,26 @@ fn run_start_summary(response: &Value) -> Value {
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.unwrap_or("coordinator rejected run");
|
.unwrap_or("coordinator rejected run");
|
||||||
let active_conflict = message.contains("already has active virtual process");
|
let active_conflict = message.contains("already has active virtual process");
|
||||||
|
let machine_error = if active_conflict {
|
||||||
|
cli_error_summary_for_category("active_process", message)
|
||||||
|
} else {
|
||||||
|
cli_error_summary(message)
|
||||||
|
};
|
||||||
|
let error_category = machine_error
|
||||||
|
.get("category")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| json!("unknown"));
|
||||||
|
let stable_exit_code = machine_error
|
||||||
|
.get("stable_exit_code")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| json!(1));
|
||||||
json!({
|
json!({
|
||||||
"status": if active_conflict { "blocked_active_process" } else { "coordinator_rejected" },
|
"status": if active_conflict { "blocked_active_process" } else { "coordinator_rejected" },
|
||||||
"accepted": false,
|
"accepted": false,
|
||||||
"category": if active_conflict { "active_process_already_running" } else { "coordinator" },
|
"category": if active_conflict { "active_process_already_running" } else { "coordinator" },
|
||||||
|
"error_category": error_category,
|
||||||
|
"stable_exit_code": stable_exit_code,
|
||||||
|
"machine_error": machine_error,
|
||||||
"message": message,
|
"message": message,
|
||||||
"restart": false,
|
"restart": false,
|
||||||
"single_active_process_boundary": true,
|
"single_active_process_boundary": true,
|
||||||
|
|
@ -4034,7 +4282,21 @@ impl JsonLineSession {
|
||||||
fn request(&mut self, value: Value) -> Result<Value> {
|
fn request(&mut self, value: Value) -> Result<Value> {
|
||||||
let response = self.request_allow_error(value)?;
|
let response = self.request_allow_error(value)?;
|
||||||
if response.get("type").and_then(Value::as_str) == Some("error") {
|
if response.get("type").and_then(Value::as_str) == Some("error") {
|
||||||
anyhow::bail!("coordinator error: {response}");
|
let message = response
|
||||||
|
.get("message")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(str::to_owned)
|
||||||
|
.unwrap_or_else(|| response.to_string());
|
||||||
|
let machine_error = cli_error_summary(&message);
|
||||||
|
let category = machine_error
|
||||||
|
.get("category")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
let exit_code = machine_error
|
||||||
|
.get("stable_exit_code")
|
||||||
|
.and_then(Value::as_i64)
|
||||||
|
.unwrap_or(1);
|
||||||
|
anyhow::bail!("coordinator error ({category}, exit {exit_code}): {response}");
|
||||||
}
|
}
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
@ -4111,6 +4373,48 @@ mod tests {
|
||||||
Cli::parse_from(args)
|
Cli::parse_from(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_error_classifier_distinguishes_mvp_failure_categories() {
|
||||||
|
for (message, category, exit_code) in [
|
||||||
|
(
|
||||||
|
"login required before running this command",
|
||||||
|
"authentication",
|
||||||
|
20,
|
||||||
|
),
|
||||||
|
("unauthorized tenant action", "authorization", 21),
|
||||||
|
("quota unavailable: resource limit exceeded", "quota", 22),
|
||||||
|
("policy denied native command execution", "policy", 23),
|
||||||
|
(
|
||||||
|
"scheduler placement failed: no capable node for placement: project mismatch",
|
||||||
|
"capability",
|
||||||
|
24,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"failed to connect to coordinator: connection refused",
|
||||||
|
"connectivity",
|
||||||
|
25,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"missing environment envs/linux/Containerfile",
|
||||||
|
"environment",
|
||||||
|
26,
|
||||||
|
),
|
||||||
|
("task exited with status 101 after panic", "program", 27),
|
||||||
|
(
|
||||||
|
"project already has active virtual process vp-current",
|
||||||
|
"active_process",
|
||||||
|
28,
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let summary = cli_error_summary(message);
|
||||||
|
assert_eq!(summary["category"], category, "{message}");
|
||||||
|
assert_eq!(summary["stable_exit_code"], exit_code, "{message}");
|
||||||
|
assert_eq!(summary["safe_failure"], true);
|
||||||
|
assert_eq!(summary["process_exit_code_applied"], false);
|
||||||
|
assert!(summary["next_actions"].as_array().unwrap().len() >= 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn top_level_version_is_available() {
|
fn top_level_version_is_available() {
|
||||||
let command = Cli::command();
|
let command = Cli::command();
|
||||||
|
|
@ -4417,6 +4721,16 @@ mod tests {
|
||||||
blocked["run_start"]["category"],
|
blocked["run_start"]["category"],
|
||||||
"active_process_already_running"
|
"active_process_already_running"
|
||||||
);
|
);
|
||||||
|
assert_eq!(blocked["run_start"]["error_category"], "active_process");
|
||||||
|
assert_eq!(blocked["run_start"]["stable_exit_code"], 28);
|
||||||
|
assert_eq!(
|
||||||
|
blocked["run_start"]["machine_error"]["category"],
|
||||||
|
"active_process"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
blocked["run_start"]["machine_error"]["stable_exit_code"],
|
||||||
|
28
|
||||||
|
);
|
||||||
assert_eq!(blocked["run_start"]["safe_failure"], true);
|
assert_eq!(blocked["run_start"]["safe_failure"], true);
|
||||||
assert!(blocked["run_start"]["next_actions"]
|
assert!(blocked["run_start"]["next_actions"]
|
||||||
.as_array()
|
.as_array()
|
||||||
|
|
@ -4425,6 +4739,24 @@ mod tests {
|
||||||
.any(|action| action == "disasmer process restart --yes"));
|
.any(|action| action == "disasmer process restart --yes"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_rejection_reports_machine_readable_error_category() {
|
||||||
|
let rejected = run_start_summary(&json!({
|
||||||
|
"type": "error",
|
||||||
|
"message": "quota unavailable: resource limit exceeded for api_calls"
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert_eq!(rejected["status"], "coordinator_rejected");
|
||||||
|
assert_eq!(rejected["error_category"], "quota");
|
||||||
|
assert_eq!(rejected["stable_exit_code"], 22);
|
||||||
|
assert_eq!(rejected["machine_error"]["category"], "quota");
|
||||||
|
assert!(rejected["machine_error"]["next_actions"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.any(|action| action == "disasmer quota status"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn local_only_run_executes_ephemeral_local_services() {
|
fn local_only_run_executes_ephemeral_local_services() {
|
||||||
let Cli {
|
let Cli {
|
||||||
|
|
@ -5312,6 +5644,14 @@ mod tests {
|
||||||
status["next_blocked_action"]["action"],
|
status["next_blocked_action"]["action"],
|
||||||
"node_work_requires_online_attached_node"
|
"node_work_requires_online_attached_node"
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
status["next_blocked_action"]["machine_error"]["category"],
|
||||||
|
"capability"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
status["next_blocked_action"]["machine_error"]["stable_exit_code"],
|
||||||
|
24
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -5449,6 +5789,8 @@ mod tests {
|
||||||
assert!(rendered_tasks.contains("placement task-a: node-a"));
|
assert!(rendered_tasks.contains("placement task-a: node-a"));
|
||||||
assert!(rendered_tasks.contains("source snapshot already local"));
|
assert!(rendered_tasks.contains("source snapshot already local"));
|
||||||
assert_eq!(tasks["tasks"][1]["failure_reason"], "boom");
|
assert_eq!(tasks["tasks"][1]["failure_reason"], "boom");
|
||||||
|
assert_eq!(tasks["tasks"][1]["machine_error"]["category"], "program");
|
||||||
|
assert_eq!(tasks["tasks"][1]["machine_error"]["stable_exit_code"], 27);
|
||||||
assert_eq!(logs["log_entries"].as_array().unwrap().len(), 1);
|
assert_eq!(logs["log_entries"].as_array().unwrap().len(), 1);
|
||||||
assert_eq!(logs["log_entries"][0]["task"], "task-a");
|
assert_eq!(logs["log_entries"][0]["task"], "task-a");
|
||||||
assert_eq!(logs["log_entries"][0]["stdout_tail"], "ok");
|
assert_eq!(logs["log_entries"][0]["stdout_tail"], "ok");
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,8 @@ for (const [name, pattern] of [
|
||||||
["project local config coverage", /fn project_init_select_and_status_use_local_project_config\(\)/],
|
["project local config coverage", /fn project_init_select_and_status_use_local_project_config\(\)/],
|
||||||
["project coordinator status coverage", /fn project_status_queries_public_coordinator_state\(\)/],
|
["project coordinator status coverage", /fn project_status_queries_public_coordinator_state\(\)/],
|
||||||
["run coordinator active-process coverage", /fn run_contacts_configured_coordinator_and_reports_active_process_conflicts\(\)/],
|
["run coordinator active-process coverage", /fn run_contacts_configured_coordinator_and_reports_active_process_conflicts\(\)/],
|
||||||
|
["CLI error classifier coverage", /fn cli_error_classifier_distinguishes_mvp_failure_categories\(\)/],
|
||||||
|
["CLI run rejection category coverage", /fn run_rejection_reports_machine_readable_error_category\(\)/],
|
||||||
["node attach grant disclosure coverage", /fn node_attach_discloses_dangerous_capability_grants\(\)/],
|
["node attach grant disclosure coverage", /fn node_attach_discloses_dangerous_capability_grants\(\)/],
|
||||||
["quota local status coverage", /fn quota_status_uses_project_config_and_generic_public_limits\(\)/],
|
["quota local status coverage", /fn quota_status_uses_project_config_and_generic_public_limits\(\)/],
|
||||||
["quota coordinator usage coverage", /fn quota_status_queries_public_coordinator_usage\(\)/],
|
["quota coordinator usage coverage", /fn quota_status_queries_public_coordinator_usage\(\)/],
|
||||||
|
|
@ -218,6 +220,26 @@ expect(
|
||||||
"CLI renders task placement reasons",
|
"CLI renders task placement reasons",
|
||||||
/fn push_task_placement_reasons[\s\S]*placement \{task_name\}: \{node\}/
|
/fn push_task_placement_reasons[\s\S]*placement \{task_name\}: \{node\}/
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
cli,
|
||||||
|
"CLI classifies machine-readable error categories",
|
||||||
|
/fn classify_cli_error_message[\s\S]*"authentication"[\s\S]*"authorization"[\s\S]*"quota"[\s\S]*"policy"[\s\S]*"capability"[\s\S]*"connectivity"[\s\S]*"environment"[\s\S]*"program"/
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cli,
|
||||||
|
"CLI reports stable error-code contract",
|
||||||
|
/fn cli_error_exit_code[\s\S]*"authentication" => 20[\s\S]*"authorization" => 21[\s\S]*"quota" => 22[\s\S]*"policy" => 23[\s\S]*"capability" => 24[\s\S]*"connectivity" => 25[\s\S]*"environment" => 26[\s\S]*"program" => 27/
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cli,
|
||||||
|
"CLI attaches machine errors to run and task reports",
|
||||||
|
/run_start_summary[\s\S]*"machine_error": machine_error/
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cli,
|
||||||
|
"CLI attaches machine errors to task failures",
|
||||||
|
/task_failure_machine_error[\s\S]*cli_error_summary_with_default/
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
cli,
|
cli,
|
||||||
"CLI parses dangerous capability overrides",
|
"CLI parses dangerous capability overrides",
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ for (const script of [publicAcceptance, publicSplit]) {
|
||||||
|
|
||||||
expect(cliInstall, "CLI install from project path", /cargo[\s\S]*install[\s\S]*crates\/disasmer-cli/);
|
expect(cliInstall, "CLI install from project path", /cargo[\s\S]*install[\s\S]*crates\/disasmer-cli/);
|
||||||
expect(cliInstall, "CLI install smoke targets flagship project", /const project = path\.join\(repo, "examples\/launch-build-demo"\)/);
|
expect(cliInstall, "CLI install smoke targets flagship project", /const project = path\.join\(repo, "examples\/launch-build-demo"\)/);
|
||||||
expect(cliInstall, "installed CLI inspects flagship project", /installedBin[\s\S]*\["bundle", "inspect", "--project", project\]/);
|
expect(cliInstall, "installed CLI inspects flagship project", /installedBin[\s\S]*\["bundle", "inspect", "--project", project, "--json"\]/);
|
||||||
|
|
||||||
expect(flagship, "flagship project source is Rust workflow", /examples\/launch-build-demo[\s\S]*src\/build\.rs/);
|
expect(flagship, "flagship project source is Rust workflow", /examples\/launch-build-demo[\s\S]*src\/build\.rs/);
|
||||||
expect(flagship, "flagship source avoids local machine assumptions", /forbiddenSourceAssumptions/);
|
expect(flagship, "flagship source avoids local machine assumptions", /forbiddenSourceAssumptions/);
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ function criterionLines() {
|
||||||
expect("header", /^# Disasmer Minimal Website Inventory/m);
|
expect("header", /^# Disasmer Minimal Website Inventory/m);
|
||||||
expect(
|
expect(
|
||||||
"acceptance-style status",
|
"acceptance-style status",
|
||||||
/\*\*Status:\*\* working MVP website inventory aligned with `acceptance_criteria\.md`, `acceptance_criteria_phase2\.md`, and `cli_acceptance_criteria\.md`/
|
/\*\*Status:\*\* hosted website addendum to `acceptance_criteria\.md`, `acceptance_criteria_phase2\.md`, and `cli_acceptance_criteria\.md`/
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
"hosted signup exception",
|
"hosted signup exception",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue