mirror of
https://github.com/instructkr/claude-code.git
synced 2026-05-30 01:16:43 +00:00
* fix: route all JSON-mode abort envelopes to stdout (#819 #820 #823) All handled errors in --output-format json mode now write the structured abort envelope to stdout (rc=1) and keep stderr empty. Previously the top-level error handler and resume_session JSON branches used eprintln! which sent the envelope to stderr, breaking machine consumers that read stdout for command payloads. Surfaces fixed: - Top-level abort handler (main.rs): export --session <missing>, session <subcommand>, prompt (no text), unknown subcommand fallthrough, flag errors, and all other run() failures - resume_session JSON branches: session load errors, unsupported commands, parse errors, command execution errors Test changes: updated 24 failing contract tests to assert JSON envelopes on stdout. Added stderr-clean assertions where appropriate. 70 contract tests pass (was 68; 2 additional from regression coverage). ROADMAP: #819 (export session-not-found), #820 (interactive_only class), #823 (missing prompt) * style: cargo fmt on main.rs after eprintln->println fix * fix(tests): fmt + update compact_output test for stdout abort envelope routing * fix(tests): update resume_slash_commands stub test for stdout envelope routing
This commit is contained in:
@@ -225,7 +225,10 @@ fn main() {
|
|||||||
let (short_reason, inline_hint) = split_error_hint(&message);
|
let (short_reason, inline_hint) = split_error_hint(&message);
|
||||||
// #781: fall back to a kind-derived hint when the message has no \n-delimited hint
|
// #781: fall back to a kind-derived hint when the message has no \n-delimited hint
|
||||||
let hint = inline_hint.or_else(|| fallback_hint_for_error_kind(kind).map(String::from));
|
let hint = inline_hint.or_else(|| fallback_hint_for_error_kind(kind).map(String::from));
|
||||||
eprintln!(
|
// #819/#820/#823: JSON mode error envelopes must go to stdout so machine
|
||||||
|
// consumers can parse failures from stdout byte 0 (parity with all
|
||||||
|
// non-interactive command guards that already use println! / to_stdout).
|
||||||
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"type": "error",
|
"type": "error",
|
||||||
@@ -3401,7 +3404,9 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
|
|||||||
// #787: fall back to kind-derived hint when message has no \n delimiter
|
// #787: fall back to kind-derived hint when message has no \n delimiter
|
||||||
let hint =
|
let hint =
|
||||||
inline_hint.or_else(|| fallback_hint_for_error_kind(kind).map(String::from));
|
inline_hint.or_else(|| fallback_hint_for_error_kind(kind).map(String::from));
|
||||||
eprintln!(
|
// #819: JSON mode resume errors go to stdout for parity with other
|
||||||
|
// non-interactive command guards.
|
||||||
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"kind": kind,
|
"kind": kind,
|
||||||
@@ -3459,7 +3464,7 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
|
|||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
if STUB_COMMANDS.contains(&cmd_root) {
|
if STUB_COMMANDS.contains(&cmd_root) {
|
||||||
if output_format == CliOutputFormat::Json {
|
if output_format == CliOutputFormat::Json {
|
||||||
eprintln!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"kind": "unsupported_command",
|
"kind": "unsupported_command",
|
||||||
@@ -3482,7 +3487,7 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
|
|||||||
Ok(Some(command)) => command,
|
Ok(Some(command)) => command,
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
if output_format == CliOutputFormat::Json {
|
if output_format == CliOutputFormat::Json {
|
||||||
eprintln!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"kind": "unsupported_resumed_command",
|
"kind": "unsupported_resumed_command",
|
||||||
@@ -3502,7 +3507,7 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
|
|||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
if output_format == CliOutputFormat::Json {
|
if output_format == CliOutputFormat::Json {
|
||||||
eprintln!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"kind": "cli_parse",
|
"kind": "cli_parse",
|
||||||
@@ -3552,7 +3557,7 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
|
|||||||
// #787: fall back to kind-derived hint when error has no \n delimiter
|
// #787: fall back to kind-derived hint when error has no \n delimiter
|
||||||
let hint = inline_hint
|
let hint = inline_hint
|
||||||
.or_else(|| fallback_hint_for_error_kind(error_kind).map(String::from));
|
.or_else(|| fallback_hint_for_error_kind(error_kind).map(String::from));
|
||||||
eprintln!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"kind": error_kind,
|
"kind": error_kind,
|
||||||
|
|||||||
@@ -266,13 +266,15 @@ fn compact_subcommand_json_help_fails_fast_when_stdin_closed() {
|
|||||||
!output.status.success(),
|
!output.status.success(),
|
||||||
"compact json help should fail non-zero"
|
"compact json help should fail non-zero"
|
||||||
);
|
);
|
||||||
assert!(
|
// #819/#820/#823: JSON abort envelopes route to stdout
|
||||||
output.stdout.is_empty(),
|
|
||||||
"compact json help should not start a prompt/spinner on stdout: {}",
|
|
||||||
String::from_utf8_lossy(&output.stdout)
|
|
||||||
);
|
|
||||||
let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8");
|
let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8");
|
||||||
let parsed: Value = serde_json::from_str(stderr.trim()).expect("stderr should be JSON error");
|
assert!(
|
||||||
|
stderr.trim().is_empty() || !stderr.trim_start().starts_with('{'),
|
||||||
|
"compact json help should not emit JSON envelope to stderr (#819/#820/#823): {stderr}"
|
||||||
|
);
|
||||||
|
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
|
||||||
|
let parsed: Value =
|
||||||
|
serde_json::from_str(stdout.trim()).expect("stdout should be JSON error envelope");
|
||||||
assert_eq!(parsed["status"], "error");
|
assert_eq!(parsed["status"], "error");
|
||||||
assert_eq!(parsed["error_kind"], "interactive_only");
|
assert_eq!(parsed["error_kind"], "interactive_only");
|
||||||
assert_eq!(parsed["action"], "abort");
|
assert_eq!(parsed["action"], "abort");
|
||||||
|
|||||||
@@ -1745,13 +1745,15 @@ fn flag_value_errors_have_error_kind_and_hint_756() {
|
|||||||
!out.status.success(),
|
!out.status.success(),
|
||||||
"invalid reasoning-effort must exit non-zero"
|
"invalid reasoning-effort must exit non-zero"
|
||||||
);
|
);
|
||||||
let raw = String::from_utf8_lossy(&out.stderr)
|
// #819/#820/#823: abort envelopes route to stdout in JSON mode
|
||||||
|
let raw = String::from_utf8_lossy(&out.stdout)
|
||||||
.lines()
|
.lines()
|
||||||
.filter(|l| l.starts_with('{'))
|
.filter(|l| l.starts_with('{'))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("");
|
.join("");
|
||||||
let parsed: serde_json::Value = serde_json::from_str(&raw)
|
let parsed: serde_json::Value = serde_json::from_str(&raw).unwrap_or_else(|_| {
|
||||||
.unwrap_or_else(|_| panic!("invalid --reasoning-effort must emit JSON; got: {raw}"));
|
panic!("invalid --reasoning-effort must emit JSON to stdout; got: {raw}")
|
||||||
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parsed["error_kind"], "invalid_flag_value",
|
parsed["error_kind"], "invalid_flag_value",
|
||||||
"invalid --reasoning-effort must be invalid_flag_value (#756): {parsed}"
|
"invalid --reasoning-effort must be invalid_flag_value (#756): {parsed}"
|
||||||
@@ -1773,13 +1775,13 @@ fn flag_value_errors_have_error_kind_and_hint_756() {
|
|||||||
!out2.status.success(),
|
!out2.status.success(),
|
||||||
"missing --model value must exit non-zero"
|
"missing --model value must exit non-zero"
|
||||||
);
|
);
|
||||||
let raw2 = String::from_utf8_lossy(&out2.stderr)
|
let raw2 = String::from_utf8_lossy(&out2.stdout)
|
||||||
.lines()
|
.lines()
|
||||||
.filter(|l| l.starts_with('{'))
|
.filter(|l| l.starts_with('{'))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("");
|
.join("");
|
||||||
let parsed2: serde_json::Value = serde_json::from_str(&raw2)
|
let parsed2: serde_json::Value = serde_json::from_str(&raw2)
|
||||||
.unwrap_or_else(|_| panic!("missing --model value must emit JSON; got: {raw2}"));
|
.unwrap_or_else(|_| panic!("missing --model value must emit JSON to stdout; got: {raw2}"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parsed2["error_kind"], "missing_flag_value",
|
parsed2["error_kind"], "missing_flag_value",
|
||||||
"missing --model value must be missing_flag_value (#756): {parsed2}"
|
"missing --model value must be missing_flag_value (#756): {parsed2}"
|
||||||
@@ -1816,14 +1818,15 @@ fn short_p_flag_swallows_no_flags_755() {
|
|||||||
!output.status.success(),
|
!output.status.success(),
|
||||||
"claw -p hello --output-format json must exit non-zero (no credentials)"
|
"claw -p hello --output-format json must exit non-zero (no credentials)"
|
||||||
);
|
);
|
||||||
let raw = String::from_utf8_lossy(&output.stderr)
|
// #819/#820/#823: abort envelopes route to stdout in JSON mode
|
||||||
|
let raw = String::from_utf8_lossy(&output.stdout)
|
||||||
.lines()
|
.lines()
|
||||||
.filter(|l| l.starts_with('{'))
|
.filter(|l| l.starts_with('{'))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("");
|
.join("");
|
||||||
// Must be valid JSON (i.e. --output-format json was parsed, not swallowed)
|
// Must be valid JSON (i.e. --output-format json was parsed, not swallowed)
|
||||||
let parsed: serde_json::Value = serde_json::from_str(&raw).unwrap_or_else(|_| {
|
let parsed: serde_json::Value = serde_json::from_str(&raw).unwrap_or_else(|_| {
|
||||||
panic!("--output-format json must be parsed as a flag, not prompt text; stderr: {raw}")
|
panic!("--output-format json must be parsed as a flag, not prompt text; stdout: {raw}")
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parsed["error_kind"], "missing_credentials",
|
parsed["error_kind"], "missing_credentials",
|
||||||
@@ -1836,13 +1839,13 @@ fn short_p_flag_swallows_no_flags_755() {
|
|||||||
.args(["--output-format", "json", "-p", "--model", "sonnet"])
|
.args(["--output-format", "json", "-p", "--model", "sonnet"])
|
||||||
.output()
|
.output()
|
||||||
.expect("claw -p flag-as-prompt should run");
|
.expect("claw -p flag-as-prompt should run");
|
||||||
let raw2 = String::from_utf8_lossy(&output2.stderr)
|
let raw2 = String::from_utf8_lossy(&output2.stdout)
|
||||||
.lines()
|
.lines()
|
||||||
.filter(|l| l.starts_with('{'))
|
.filter(|l| l.starts_with('{'))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("");
|
.join("");
|
||||||
let parsed2: serde_json::Value = serde_json::from_str(&raw2)
|
let parsed2: serde_json::Value = serde_json::from_str(&raw2)
|
||||||
.unwrap_or_else(|_| panic!("claw -p --model must emit JSON; got: {raw2}"));
|
.unwrap_or_else(|_| panic!("claw -p --model must emit JSON to stdout; got: {raw2}"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parsed2["error_kind"], "missing_prompt",
|
parsed2["error_kind"], "missing_prompt",
|
||||||
"flag-like token after -p must be rejected as missing_prompt (#755): {parsed2}"
|
"flag-like token after -p must be rejected as missing_prompt (#755): {parsed2}"
|
||||||
@@ -2038,13 +2041,13 @@ fn export_json_has_kind_702() {
|
|||||||
"export status must be ok or error"
|
"export status must be ok or error"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Error envelope on stderr must be parseable JSON.
|
// #819: Error envelope in JSON mode must be on stdout (not stderr).
|
||||||
assert!(
|
let stdout_json = stdout
|
||||||
!stderr.is_empty(),
|
.lines()
|
||||||
"export failure must emit JSON to stderr"
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
);
|
.expect("export failure must emit JSON to stdout (#819)");
|
||||||
let parsed: serde_json::Value =
|
let parsed: serde_json::Value =
|
||||||
serde_json::from_str(&stderr).expect("export error stderr must be valid JSON");
|
serde_json::from_str(stdout_json).expect("export error stdout must be valid JSON");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parsed["type"], "error",
|
parsed["type"], "error",
|
||||||
"export error envelope must have type:error"
|
"export error envelope must have type:error"
|
||||||
@@ -2069,11 +2072,13 @@ fn config_parse_error_has_typed_error_kind_and_hint_764() {
|
|||||||
!output.status.success(),
|
!output.status.success(),
|
||||||
"malformed settings.json should cause non-zero exit"
|
"malformed settings.json should cause non-zero exit"
|
||||||
);
|
);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let json_line = stderr
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let json_line = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.expect("stderr should contain a JSON error envelope");
|
.expect("stdout should contain a JSON error envelope (#819/#820/#823: abort envelopes route to stdout in JSON mode)");
|
||||||
let parsed: serde_json::Value =
|
let parsed: serde_json::Value =
|
||||||
serde_json::from_str(json_line).expect("error envelope should be valid JSON");
|
serde_json::from_str(json_line).expect("error envelope should be valid JSON");
|
||||||
|
|
||||||
@@ -2102,11 +2107,13 @@ fn login_logout_removed_subcommands_have_error_kind_and_hint_765() {
|
|||||||
!output.status.success(),
|
!output.status.success(),
|
||||||
"claw {subcmd} should exit non-zero"
|
"claw {subcmd} should exit non-zero"
|
||||||
);
|
);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let json_line = stderr
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let json_line = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.unwrap_or_else(|| panic!("claw {subcmd} stderr should contain a JSON envelope"));
|
.unwrap_or_else(|| panic!("claw {subcmd} stdout should contain a JSON envelope (#819/#820/#823: abort envelopes route to stdout in JSON mode)"));
|
||||||
let parsed: serde_json::Value =
|
let parsed: serde_json::Value =
|
||||||
serde_json::from_str(json_line).expect("error envelope should be valid JSON");
|
serde_json::from_str(json_line).expect("error envelope should be valid JSON");
|
||||||
|
|
||||||
@@ -2190,17 +2197,18 @@ fn assert_diff_unexpected_extra_args_json(root: &Path, args: &[&str], label: &st
|
|||||||
String::from_utf8_lossy(&output.stdout),
|
String::from_utf8_lossy(&output.stdout),
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
assert!(
|
// #819/#820/#823: JSON abort envelopes route to stdout
|
||||||
output.stdout.is_empty(),
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
"{label} should not enter the spinner/prompt path; stdout:\n{}",
|
|
||||||
String::from_utf8_lossy(&output.stdout)
|
|
||||||
);
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let json_line = stderr
|
assert!(
|
||||||
|
stderr.lines().all(|l| !l.trim_start().starts_with('{')),
|
||||||
|
"{label} stderr should not contain a JSON envelope in JSON mode (#819/#820/#823); stderr:\n{stderr}"
|
||||||
|
);
|
||||||
|
let json_line = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
panic!("{label} stderr should contain a JSON error envelope; stderr:\n{stderr}")
|
panic!("{label} stdout should contain a JSON error envelope (#819/#820/#823); stdout:\n{stdout}")
|
||||||
});
|
});
|
||||||
let parsed: serde_json::Value =
|
let parsed: serde_json::Value =
|
||||||
serde_json::from_str(json_line).expect("error envelope should be valid JSON");
|
serde_json::from_str(json_line).expect("error envelope should be valid JSON");
|
||||||
@@ -2239,11 +2247,13 @@ fn resume_non_slash_trailing_arg_has_typed_error_kind_and_hint_768() {
|
|||||||
!output.status.success(),
|
!output.status.success(),
|
||||||
"claw --resume latest compact should exit non-zero"
|
"claw --resume latest compact should exit non-zero"
|
||||||
);
|
);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let json_line = stderr
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let json_line = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.expect("stderr should contain a JSON error envelope");
|
.expect("stdout should contain a JSON error envelope (#819/#820/#823: abort envelopes route to stdout in JSON mode)");
|
||||||
let parsed: serde_json::Value =
|
let parsed: serde_json::Value =
|
||||||
serde_json::from_str(json_line).expect("error envelope should be valid JSON");
|
serde_json::from_str(json_line).expect("error envelope should be valid JSON");
|
||||||
|
|
||||||
@@ -2277,8 +2287,10 @@ fn session_with_unknown_subcommand_returns_interactive_only_not_credentials_767(
|
|||||||
!output.status.success(),
|
!output.status.success(),
|
||||||
"claw session {sub} should exit non-zero"
|
"claw session {sub} should exit non-zero"
|
||||||
);
|
);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let json_line = stderr
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let json_line = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.unwrap_or_else(|| panic!("claw session {sub} stderr should contain JSON"));
|
.unwrap_or_else(|| panic!("claw session {sub} stderr should contain JSON"));
|
||||||
@@ -2330,8 +2342,10 @@ fn slash_only_verbs_with_args_return_interactive_only_not_credentials_770() {
|
|||||||
"claw {} should exit non-zero",
|
"claw {} should exit non-zero",
|
||||||
args.join(" ")
|
args.join(" ")
|
||||||
);
|
);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let json_line = stderr
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let json_line = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
@@ -2370,8 +2384,10 @@ fn agents_plugins_mcp_unknown_subcommand_have_hint_774() {
|
|||||||
{
|
{
|
||||||
let output = run_claw(&root, &["--output-format", "json", "agents", "bogus"], &[]);
|
let output = run_claw(&root, &["--output-format", "json", "agents", "bogus"], &[]);
|
||||||
assert!(!output.status.success(), "agents bogus should fail");
|
assert!(!output.status.success(), "agents bogus should fail");
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let json_line = stderr
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let json_line = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.expect("agents bogus should emit JSON error");
|
.expect("agents bogus should emit JSON error");
|
||||||
@@ -2392,8 +2408,10 @@ fn agents_plugins_mcp_unknown_subcommand_have_hint_774() {
|
|||||||
{
|
{
|
||||||
let output = run_claw(&root, &["--output-format", "json", "plugins", "bogus"], &[]);
|
let output = run_claw(&root, &["--output-format", "json", "plugins", "bogus"], &[]);
|
||||||
assert!(!output.status.success(), "plugins bogus should fail");
|
assert!(!output.status.success(), "plugins bogus should fail");
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let json_line = stderr
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let json_line = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.expect("plugins bogus should emit JSON error");
|
.expect("plugins bogus should emit JSON error");
|
||||||
@@ -2412,6 +2430,7 @@ fn agents_plugins_mcp_unknown_subcommand_have_hint_774() {
|
|||||||
assert!(!output.status.success(), "mcp bogus should fail");
|
assert!(!output.status.success(), "mcp bogus should fail");
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let json_str = if stdout.trim().starts_with('{') {
|
let json_str = if stdout.trim().starts_with('{') {
|
||||||
stdout.to_string()
|
stdout.to_string()
|
||||||
} else {
|
} else {
|
||||||
@@ -2470,8 +2489,10 @@ fn interactive_only_guard_batch_769_to_771() {
|
|||||||
"claw {} should exit non-zero",
|
"claw {} should exit non-zero",
|
||||||
args.join(" ")
|
args.join(" ")
|
||||||
);
|
);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let json_line = stderr
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let json_line = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
@@ -2532,12 +2553,14 @@ fn resume_plugin_mutations_are_typed_interactive_only_777() {
|
|||||||
!output.status.success(),
|
!output.status.success(),
|
||||||
"/plugins {mutation} in resume mode should exit non-zero"
|
"/plugins {mutation} in resume mode should exit non-zero"
|
||||||
);
|
);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let json_line = stderr
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let json_line = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
panic!("/plugins {mutation} should emit JSON error, got stderr: {stderr}")
|
panic!("/plugins {mutation} should emit JSON error on stdout, got: {stderr}")
|
||||||
});
|
});
|
||||||
let parsed: serde_json::Value = serde_json::from_str(json_line).unwrap();
|
let parsed: serde_json::Value = serde_json::from_str(json_line).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -2587,12 +2610,14 @@ fn resume_skills_invocation_is_typed_interactive_only_779() {
|
|||||||
!output.status.success(),
|
!output.status.success(),
|
||||||
"/skills <skill> in resume mode should exit non-zero"
|
"/skills <skill> in resume mode should exit non-zero"
|
||||||
);
|
);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let json_line = stderr
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let json_line = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
panic!("/skills invocation should emit JSON error, got stderr: {stderr}")
|
panic!("/skills invocation should emit JSON error on stdout, got: {stderr}")
|
||||||
});
|
});
|
||||||
let parsed: serde_json::Value = serde_json::from_str(json_line).unwrap();
|
let parsed: serde_json::Value = serde_json::from_str(json_line).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -2626,8 +2651,10 @@ fn acp_unsupported_invocation_has_hint_782() {
|
|||||||
|
|
||||||
let output = run_claw(&root, &["--output-format", "json", "acp", "start"], &[]);
|
let output = run_claw(&root, &["--output-format", "json", "acp", "start"], &[]);
|
||||||
assert!(!output.status.success(), "acp start should fail");
|
assert!(!output.status.success(), "acp start should fail");
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let json_line = stderr
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let json_line = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.expect("should emit JSON error");
|
.expect("should emit JSON error");
|
||||||
@@ -2664,6 +2691,7 @@ fn init_json_envelope_has_hint_and_already_initialized_783() {
|
|||||||
assert!(output.status.success(), "init should succeed");
|
assert!(output.status.success(), "init should succeed");
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let raw = if stdout.trim_start().starts_with('{') {
|
let raw = if stdout.trim_start().starts_with('{') {
|
||||||
&*stdout
|
&*stdout
|
||||||
} else {
|
} else {
|
||||||
@@ -2697,6 +2725,7 @@ fn init_json_envelope_has_hint_and_already_initialized_783() {
|
|||||||
assert!(output2.status.success(), "re-init should succeed");
|
assert!(output2.status.success(), "re-init should succeed");
|
||||||
let stdout2 = String::from_utf8_lossy(&output2.stdout);
|
let stdout2 = String::from_utf8_lossy(&output2.stdout);
|
||||||
let stderr2 = String::from_utf8_lossy(&output2.stderr);
|
let stderr2 = String::from_utf8_lossy(&output2.stderr);
|
||||||
|
let stdout2 = String::from_utf8_lossy(&output2.stdout);
|
||||||
let raw2 = if stdout2.trim_start().starts_with('{') {
|
let raw2 = if stdout2.trim_start().starts_with('{') {
|
||||||
&*stdout2
|
&*stdout2
|
||||||
} else {
|
} else {
|
||||||
@@ -2739,7 +2768,8 @@ fn export_arg_errors_have_typed_kind_and_hint_784() {
|
|||||||
);
|
);
|
||||||
assert!(!out1.status.success(), "--output with no value should fail");
|
assert!(!out1.status.success(), "--output with no value should fail");
|
||||||
let stderr1 = String::from_utf8_lossy(&out1.stderr);
|
let stderr1 = String::from_utf8_lossy(&out1.stderr);
|
||||||
let j1: serde_json::Value = stderr1
|
let stdout1 = String::from_utf8_lossy(&out1.stdout);
|
||||||
|
let j1: serde_json::Value = stdout1
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.and_then(|l| serde_json::from_str(l).ok())
|
.and_then(|l| serde_json::from_str(l).ok())
|
||||||
@@ -2765,7 +2795,8 @@ fn export_arg_errors_have_typed_kind_and_hint_784() {
|
|||||||
);
|
);
|
||||||
assert!(!out2.status.success(), "extra positional should fail");
|
assert!(!out2.status.success(), "extra positional should fail");
|
||||||
let stderr2 = String::from_utf8_lossy(&out2.stderr);
|
let stderr2 = String::from_utf8_lossy(&out2.stderr);
|
||||||
let j2: serde_json::Value = stderr2
|
let stdout2 = String::from_utf8_lossy(&out2.stdout);
|
||||||
|
let j2: serde_json::Value = stdout2
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.and_then(|l| serde_json::from_str(l).ok())
|
.and_then(|l| serde_json::from_str(l).ok())
|
||||||
@@ -2801,7 +2832,8 @@ fn unknown_subcommand_returns_typed_kind_785() {
|
|||||||
let output = run_claw(&root, &["--output-format", "json", "dump"], &[]);
|
let output = run_claw(&root, &["--output-format", "json", "dump"], &[]);
|
||||||
assert!(!output.status.success(), "unknown subcommand should fail");
|
assert!(!output.status.success(), "unknown subcommand should fail");
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let j: serde_json::Value = stderr
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let j: serde_json::Value = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.and_then(|l| serde_json::from_str(l).ok())
|
.and_then(|l| serde_json::from_str(l).ok())
|
||||||
@@ -2847,7 +2879,8 @@ fn dump_manifests_missing_dir_has_typed_kind_and_hint_786() {
|
|||||||
);
|
);
|
||||||
assert!(!out1.status.success());
|
assert!(!out1.status.success());
|
||||||
let stderr1 = String::from_utf8_lossy(&out1.stderr);
|
let stderr1 = String::from_utf8_lossy(&out1.stderr);
|
||||||
let j1: serde_json::Value = stderr1
|
let stdout1 = String::from_utf8_lossy(&out1.stdout);
|
||||||
|
let j1: serde_json::Value = stdout1
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.and_then(|l| serde_json::from_str(l).ok())
|
.and_then(|l| serde_json::from_str(l).ok())
|
||||||
@@ -2878,7 +2911,8 @@ fn dump_manifests_missing_dir_has_typed_kind_and_hint_786() {
|
|||||||
);
|
);
|
||||||
assert!(!out2.status.success());
|
assert!(!out2.status.success());
|
||||||
let stderr2 = String::from_utf8_lossy(&out2.stderr);
|
let stderr2 = String::from_utf8_lossy(&out2.stderr);
|
||||||
let j2: serde_json::Value = stderr2
|
let stdout2 = String::from_utf8_lossy(&out2.stdout);
|
||||||
|
let j2: serde_json::Value = stdout2
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.and_then(|l| serde_json::from_str(l).ok())
|
.and_then(|l| serde_json::from_str(l).ok())
|
||||||
@@ -2927,7 +2961,8 @@ fn resume_directory_path_returns_typed_kind_and_hint_787() {
|
|||||||
"resume with directory should fail"
|
"resume with directory should fail"
|
||||||
);
|
);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let j: serde_json::Value = stderr
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let j: serde_json::Value = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.and_then(|l| serde_json::from_str(l).ok())
|
.and_then(|l| serde_json::from_str(l).ok())
|
||||||
@@ -2977,6 +3012,7 @@ fn skills_show_not_found_emits_single_json_object_788() {
|
|||||||
// After fix: stdout has 1 JSON object, stderr has none (no duplicate).
|
// After fix: stdout has 1 JSON object, stderr has none (no duplicate).
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
|
||||||
// Count JSON objects in stdout — must be exactly 1
|
// Count JSON objects in stdout — must be exactly 1
|
||||||
let json_objects: Vec<serde_json::Value> = {
|
let json_objects: Vec<serde_json::Value> = {
|
||||||
@@ -3137,7 +3173,8 @@ fn system_prompt_unknown_option_returns_typed_kind_790() {
|
|||||||
);
|
);
|
||||||
assert!(!out1.status.success());
|
assert!(!out1.status.success());
|
||||||
let stderr1 = String::from_utf8_lossy(&out1.stderr);
|
let stderr1 = String::from_utf8_lossy(&out1.stderr);
|
||||||
let j1: serde_json::Value = stderr1
|
let stdout1 = String::from_utf8_lossy(&out1.stdout);
|
||||||
|
let j1: serde_json::Value = stdout1
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.and_then(|l| serde_json::from_str(l).ok())
|
.and_then(|l| serde_json::from_str(l).ok())
|
||||||
@@ -3163,7 +3200,8 @@ fn system_prompt_unknown_option_returns_typed_kind_790() {
|
|||||||
);
|
);
|
||||||
assert!(!out2.status.success());
|
assert!(!out2.status.success());
|
||||||
let stderr2 = String::from_utf8_lossy(&out2.stderr);
|
let stderr2 = String::from_utf8_lossy(&out2.stderr);
|
||||||
let j2: serde_json::Value = stderr2
|
let stdout2 = String::from_utf8_lossy(&out2.stdout);
|
||||||
|
let j2: serde_json::Value = stdout2
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.and_then(|l| serde_json::from_str(l).ok())
|
.and_then(|l| serde_json::from_str(l).ok())
|
||||||
@@ -3200,7 +3238,8 @@ fn config_extra_args_have_non_null_hint_791() {
|
|||||||
);
|
);
|
||||||
assert!(!out1.status.success());
|
assert!(!out1.status.success());
|
||||||
let stderr1 = String::from_utf8_lossy(&out1.stderr);
|
let stderr1 = String::from_utf8_lossy(&out1.stderr);
|
||||||
let j1: serde_json::Value = stderr1
|
let stdout1 = String::from_utf8_lossy(&out1.stdout);
|
||||||
|
let j1: serde_json::Value = stdout1
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.and_then(|l| serde_json::from_str(l).ok())
|
.and_then(|l| serde_json::from_str(l).ok())
|
||||||
@@ -3233,7 +3272,8 @@ fn config_extra_args_have_non_null_hint_791() {
|
|||||||
);
|
);
|
||||||
assert!(!out2.status.success());
|
assert!(!out2.status.success());
|
||||||
let stderr2 = String::from_utf8_lossy(&out2.stderr);
|
let stderr2 = String::from_utf8_lossy(&out2.stderr);
|
||||||
let j2: serde_json::Value = stderr2
|
let stdout2 = String::from_utf8_lossy(&out2.stdout);
|
||||||
|
let j2: serde_json::Value = stdout2
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.and_then(|l| serde_json::from_str(l).ok())
|
.and_then(|l| serde_json::from_str(l).ok())
|
||||||
@@ -3418,7 +3458,8 @@ fn plugins_uninstall_not_found_has_hint_793() {
|
|||||||
);
|
);
|
||||||
// Error envelope goes to stderr (propagated via ? to main error handler)
|
// Error envelope goes to stderr (propagated via ? to main error handler)
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let j: serde_json::Value = stderr
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let j: serde_json::Value = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.and_then(|l| serde_json::from_str(l).ok())
|
.and_then(|l| serde_json::from_str(l).ok())
|
||||||
@@ -3462,7 +3503,8 @@ fn plugins_install_not_found_path_returns_typed_kind_794() {
|
|||||||
"plugins install not-found-path must exit non-zero (#794)"
|
"plugins install not-found-path must exit non-zero (#794)"
|
||||||
);
|
);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let j: serde_json::Value = stderr
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let j: serde_json::Value = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.and_then(|l| serde_json::from_str(l).ok())
|
.and_then(|l| serde_json::from_str(l).ok())
|
||||||
@@ -3508,7 +3550,8 @@ fn skills_install_not_found_and_unsupported_action_have_hints_795() {
|
|||||||
"skills install not-found must exit non-zero (#795)"
|
"skills install not-found must exit non-zero (#795)"
|
||||||
);
|
);
|
||||||
let stderr1 = String::from_utf8_lossy(&out1.stderr);
|
let stderr1 = String::from_utf8_lossy(&out1.stderr);
|
||||||
let j1: serde_json::Value = stderr1
|
let stdout1 = String::from_utf8_lossy(&out1.stdout);
|
||||||
|
let j1: serde_json::Value = stdout1
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.and_then(|l| serde_json::from_str(l).ok())
|
.and_then(|l| serde_json::from_str(l).ok())
|
||||||
@@ -3543,7 +3586,8 @@ fn skills_install_not_found_and_unsupported_action_have_hints_795() {
|
|||||||
"skills uninstall must exit non-zero (#795)"
|
"skills uninstall must exit non-zero (#795)"
|
||||||
);
|
);
|
||||||
let stderr2 = String::from_utf8_lossy(&out2.stderr);
|
let stderr2 = String::from_utf8_lossy(&out2.stderr);
|
||||||
let j2: serde_json::Value = stderr2
|
let stdout2 = String::from_utf8_lossy(&out2.stdout);
|
||||||
|
let j2: serde_json::Value = stdout2
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.and_then(|l| serde_json::from_str(l).ok())
|
.and_then(|l| serde_json::from_str(l).ok())
|
||||||
@@ -3680,7 +3724,8 @@ fn plugins_extra_args_have_non_null_hint_797() {
|
|||||||
"plugins show with extra arg must exit non-zero (#797)"
|
"plugins show with extra arg must exit non-zero (#797)"
|
||||||
);
|
);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let j: serde_json::Value = stderr
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let j: serde_json::Value = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.and_then(|l| serde_json::from_str(l).ok())
|
.and_then(|l| serde_json::from_str(l).ok())
|
||||||
@@ -3744,6 +3789,7 @@ fn plugins_list_trailing_dash_text_error_stays_on_stderr_817() {
|
|||||||
String::from_utf8_lossy(&output.stdout)
|
String::from_utf8_lossy(&output.stdout)
|
||||||
);
|
);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
assert!(stderr.contains("[error-kind: cli_parse]"), "{stderr}");
|
assert!(stderr.contains("[error-kind: cli_parse]"), "{stderr}");
|
||||||
assert!(
|
assert!(
|
||||||
stderr.contains("unknown option for `claw plugins list`: --"),
|
stderr.contains("unknown option for `claw plugins list`: --"),
|
||||||
@@ -3769,7 +3815,8 @@ fn empty_prompt_has_non_null_hint_798() {
|
|||||||
"empty prompt must exit non-zero (#798)"
|
"empty prompt must exit non-zero (#798)"
|
||||||
);
|
);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let j: serde_json::Value = stderr
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let j: serde_json::Value = stdout
|
||||||
.lines()
|
.lines()
|
||||||
.find(|l| l.trim_start().starts_with('{'))
|
.find(|l| l.trim_start().starts_with('{'))
|
||||||
.and_then(|l| serde_json::from_str(l).ok())
|
.and_then(|l| serde_json::from_str(l).ok())
|
||||||
|
|||||||
@@ -521,8 +521,9 @@ fn resumed_stub_command_emits_not_implemented_json() {
|
|||||||
|
|
||||||
// Stub commands exit with code 2
|
// Stub commands exit with code 2
|
||||||
assert!(!output.status.success());
|
assert!(!output.status.success());
|
||||||
let stderr = String::from_utf8(output.stderr).expect("utf8");
|
// #819/#820/#823: JSON abort envelopes route to stdout
|
||||||
let parsed: Value = serde_json::from_str(stderr.trim()).expect("should be json");
|
let stdout = String::from_utf8(output.stdout).expect("utf8");
|
||||||
|
let parsed: Value = serde_json::from_str(stdout.trim()).expect("should be json");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parsed["status"], "error",
|
parsed["status"], "error",
|
||||||
"stub command should emit status:error"
|
"stub command should emit status:error"
|
||||||
|
|||||||
Reference in New Issue
Block a user