Report quota resource categories in CLI errors

This commit is contained in:
Michel Paulissen 2026-07-04 11:37:29 +02:00
parent eb8dc3bf12
commit 1c4b045a6a
4 changed files with 87 additions and 5 deletions

View file

@ -1,7 +1,7 @@
{ {
"kind": "disasmer-filtered-public-tree", "kind": "disasmer-filtered-public-tree",
"source_commit": "f4707862d419b08f13d072a31f8f5e3cb9ddda66", "source_commit": "28a8b4578c0b3fd289b938c1e6f07cc15e6848b2",
"release_name": "dryrun-f4707862d419", "release_name": "dryrun-28a8b4578c0b",
"filtered_out": [ "filtered_out": [
"private/**", "private/**",
"experiments/**", "experiments/**",

View file

@ -2759,7 +2759,7 @@ fn cli_error_summary_with_default(message: &str, default_category: &'static str)
} }
fn cli_error_summary_for_category(category: &'static str, message: &str) -> Value { fn cli_error_summary_for_category(category: &'static str, message: &str) -> Value {
json!({ let mut summary = json!({
"category": category, "category": category,
"stable_exit_code": cli_error_exit_code(category), "stable_exit_code": cli_error_exit_code(category),
"process_exit_code_applied": false, "process_exit_code_applied": false,
@ -2767,7 +2767,56 @@ fn cli_error_summary_for_category(category: &'static str, message: &str) -> Valu
"message": message, "message": message,
"safe_failure": true, "safe_failure": true,
"next_actions": cli_error_next_actions(category), "next_actions": cli_error_next_actions(category),
});
if category == "quota" {
if let Some(object) = summary.as_object_mut() {
object.insert(
"resource_category".to_owned(),
json!(
quota_error_resource_category(message).unwrap_or_else(|| "unknown".to_owned())
),
);
object.insert("community_tier_language".to_owned(), json!(true));
object.insert("private_abuse_heuristics_exposed".to_owned(), json!(false));
}
}
summary
}
fn quota_error_resource_category(message: &str) -> Option<String> {
let lower = message.to_ascii_lowercase();
for marker in [
"resource limit exceeded for ",
"quota unavailable for ",
"quota exceeded for ",
"limit exceeded for ",
"metering limit exceeded for ",
"quota limit for ",
"resource category ",
"resource=",
"resource:",
] {
if let Some(index) = lower.find(marker) {
let start = index + marker.len();
let rest = &message[start..];
let value: String = rest
.chars()
.skip_while(|ch| ch.is_ascii_whitespace())
.take_while(|ch| {
ch.is_ascii_alphanumeric()
|| *ch == '_'
|| *ch == '-'
|| *ch == '.'
|| *ch == ':'
}) })
.collect();
let value = value.trim_matches(|ch: char| ch == '.' || ch == ':' || ch == ',');
if !value.is_empty() {
return Some(value.to_owned());
}
}
}
None
} }
fn classify_cli_error_message(message: &str) -> &'static str { fn classify_cli_error_message(message: &str) -> &'static str {
@ -5679,7 +5728,11 @@ mod tests {
20, 20,
), ),
("unauthorized tenant action", "authorization", 21), ("unauthorized tenant action", "authorization", 21),
("quota unavailable: resource limit exceeded", "quota", 22), (
"quota unavailable: resource limit exceeded for api_calls",
"quota",
22,
),
("policy denied native command execution", "policy", 23), ("policy denied native command execution", "policy", 23),
( (
"scheduler placement failed: no capable node for placement: project mismatch", "scheduler placement failed: no capable node for placement: project mismatch",
@ -5714,6 +5767,11 @@ mod tests {
assert_eq!(summary["safe_failure"], true); assert_eq!(summary["safe_failure"], true);
assert_eq!(summary["process_exit_code_applied"], false); assert_eq!(summary["process_exit_code_applied"], false);
assert!(summary["next_actions"].as_array().unwrap().len() >= 2); assert!(summary["next_actions"].as_array().unwrap().len() >= 2);
if category == "quota" {
assert_eq!(summary["resource_category"], "api_calls");
assert_eq!(summary["community_tier_language"], true);
assert_eq!(summary["private_abuse_heuristics_exposed"], false);
}
} }
} }
@ -6118,6 +6176,12 @@ mod tests {
assert_eq!(rejected["error_category"], "quota"); assert_eq!(rejected["error_category"], "quota");
assert_eq!(rejected["stable_exit_code"], 22); assert_eq!(rejected["stable_exit_code"], 22);
assert_eq!(rejected["machine_error"]["category"], "quota"); assert_eq!(rejected["machine_error"]["category"], "quota");
assert_eq!(rejected["machine_error"]["resource_category"], "api_calls");
assert_eq!(rejected["machine_error"]["community_tier_language"], true);
assert_eq!(
rejected["machine_error"]["private_abuse_heuristics_exposed"],
false
);
assert!(rejected["machine_error"]["next_actions"] assert!(rejected["machine_error"]["next_actions"]
.as_array() .as_array()
.unwrap() .unwrap()

View file

@ -100,6 +100,12 @@ async function main() {
const report = JSON.parse(result.stdout); const report = JSON.parse(result.stdout);
assert.strictEqual(report.status, "coordinator_rejected"); assert.strictEqual(report.status, "coordinator_rejected");
assert.strictEqual(report.run_start.machine_error.category, "quota"); assert.strictEqual(report.run_start.machine_error.category, "quota");
assert.strictEqual(report.run_start.machine_error.resource_category, "api_calls");
assert.strictEqual(report.run_start.machine_error.community_tier_language, true);
assert.strictEqual(
report.run_start.machine_error.private_abuse_heuristics_exposed,
false
);
assert.strictEqual(report.run_start.machine_error.stable_exit_code, 22); assert.strictEqual(report.run_start.machine_error.stable_exit_code, 22);
assert.strictEqual( assert.strictEqual(
report.run_start.machine_error.process_exit_code_applied, report.run_start.machine_error.process_exit_code_applied,

View file

@ -102,6 +102,11 @@ expect(
"log redaction criteria", "log redaction criteria",
/Logs are capped, truncated honestly[\s\S]*preserve byte counts and truncation flags[\s\S]*Secret-like values are redacted[\s\S]*common token\/password\/bearer patterns/ /Logs are capped, truncated honestly[\s\S]*preserve byte counts and truncation flags[\s\S]*Secret-like values are redacted[\s\S]*common token\/password\/bearer patterns/
); );
expect(
criteria,
"quota resource-category criteria",
/Hitting a quota produces a clear error[\s\S]*resource category[\s\S]*private abuse heuristics[\s\S]*quota machine errors now extract the resource category/
);
expect( expect(
cliFirstAcceptance, cliFirstAcceptance,
"CLI-first acceptance report", "CLI-first acceptance report",
@ -364,6 +369,11 @@ expect(
"CLI classifies machine-readable error categories", "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"/ /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 exposes quota machine-error posture",
/fn cli_error_summary_for_category[\s\S]*resource_category[\s\S]*quota_error_resource_category[\s\S]*community_tier_language[\s\S]*private_abuse_heuristics_exposed[\s\S]*fn quota_error_resource_category[\s\S]*resource limit exceeded for /
);
expect( expect(
cli, cli,
"CLI reports stable error-code contract", "CLI reports stable error-code contract",
@ -479,6 +489,8 @@ for (const [name, pattern] of [
["fake coordinator quota rejection", /quota unavailable: resource limit exceeded for api_calls/], ["fake coordinator quota rejection", /quota unavailable: resource limit exceeded for api_calls/],
["run uses JSON mode", /"run"[\s\S]*"--json"/], ["run uses JSON mode", /"run"[\s\S]*"--json"/],
["actual quota exit code", /assert\.strictEqual\(result\.code, 22/], ["actual quota exit code", /assert\.strictEqual\(result\.code, 22/],
["actual quota resource-category assertion", /machine_error\.resource_category, "api_calls"/],
["actual quota abuse-heuristics assertion", /machine_error\.private_abuse_heuristics_exposed,[\s\S]*false/],
["exit-code application in JSON", /process_exit_code_applied[\s\S]*true/], ["exit-code application in JSON", /process_exit_code_applied[\s\S]*true/],
]) { ]) {
expect(errorExitSmoke, name, pattern); expect(errorExitSmoke, name, pattern);