Skip to content

Commit a6fa759

Browse files
chubes4Chris Huber
andauthored
fix(runs): route artifact queries to runners (#11913)
AI assistance: OpenAI gpt-5.6-sol via OpenCode was used to implement, review, and verify this change. Chris Huber reviewed and is responsible for every line. Co-authored-by: Chris Huber <chris@chubes.net>
1 parent 99e1739 commit a6fa759

3 files changed

Lines changed: 97 additions & 14 deletions

File tree

crates/homeboy-cli/src/cli_runtime.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1763,6 +1763,7 @@ fn is_interactive_shell() -> bool {
17631763

17641764
fn normalize_runs_runner_options(cli: &mut Cli, normalized_args: &[String]) {
17651765
if is_runs_list_runner_option(normalized_args)
1766+
|| matches!(&cli.command, Commands::Runs(args) if args.is_artifacts())
17661767
|| is_runs_artifact_get_runner_option(normalized_args)
17671768
|| matches!(&cli.command, Commands::Runs(args) if args.is_artifact_get())
17681769
{
@@ -2844,6 +2845,68 @@ mod tests {
28442845
.expect("runs artifact get accepts global runner for command-local fetch");
28452846
}
28462847

2848+
#[test]
2849+
fn runs_artifacts_command_local_runner_is_routable_for_runner_only_and_mirrored_runs() {
2850+
let _env = EnvGuard::remove(crate::core::observation::LAB_OFFLOAD_METADATA_ENV);
2851+
2852+
for run_id in ["runner-only-run", "mirrored-run"] {
2853+
let mut cli = Cli::parse_from([
2854+
"homeboy",
2855+
"runs",
2856+
"artifacts",
2857+
run_id,
2858+
"--runner",
2859+
"homeboy-lab",
2860+
]);
2861+
let argv = vec![
2862+
"homeboy".into(),
2863+
"runs".into(),
2864+
"artifacts".into(),
2865+
run_id.into(),
2866+
"--runner".into(),
2867+
"homeboy-lab".into(),
2868+
];
2869+
2870+
// Clap initially hydrates the global field. Normalize it into the
2871+
// command-local query option before generic Lab routing runs.
2872+
normalize_runs_runner_options(&mut cli, &argv);
2873+
2874+
assert_eq!(
2875+
cli.runner, None,
2876+
"{run_id} must not enter Lab offload routing"
2877+
);
2878+
let Commands::Runs(args) = &cli.command else {
2879+
panic!("expected runs command");
2880+
};
2881+
assert!(args.is_artifacts());
2882+
assert_eq!(args.artifacts_runner(), Some("homeboy-lab"));
2883+
crate::commands::route::route_after_parse(&cli, &argv, None)
2884+
.expect("command-local runner query is accepted");
2885+
}
2886+
}
2887+
2888+
#[test]
2889+
fn runs_artifacts_without_runner_remains_a_local_query() {
2890+
let _env = EnvGuard::remove(crate::core::observation::LAB_OFFLOAD_METADATA_ENV);
2891+
let mut cli = Cli::parse_from(["homeboy", "runs", "artifacts", "local-run"]);
2892+
let argv = vec![
2893+
"homeboy".into(),
2894+
"runs".into(),
2895+
"artifacts".into(),
2896+
"local-run".into(),
2897+
];
2898+
2899+
normalize_runs_runner_options(&mut cli, &argv);
2900+
2901+
assert_eq!(cli.runner, None);
2902+
let Commands::Runs(args) = &cli.command else {
2903+
panic!("expected runs command");
2904+
};
2905+
assert!(args.is_artifacts());
2906+
assert_eq!(args.artifacts_runner(), None);
2907+
assert!(!args.has_command_local_runner_option());
2908+
}
2909+
28472910
#[test]
28482911
fn wrapper_global_runner_preserves_trailing_output_request() {
28492912
let matches = Cli::command_with_scoped_lab_args()

crates/homeboy-cli/src/commands/infra/route.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3124,6 +3124,7 @@ fn is_runs_list_runner_option(args: &[String]) -> bool {
31243124

31253125
fn is_command_local_runner_option(command: &Commands) -> bool {
31263126
match command {
3127+
Commands::Runs(args) if args.has_command_local_runner_option() => true,
31273128
Commands::AgentTask(crate::commands::agent_task::AgentTaskArgs {
31283129
command: crate::commands::agent_task::AgentTaskCommand::Doctor(_),
31293130
}) => true,

crates/homeboy-cli/src/commands/runs/dispatch.rs

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
66
use homeboy::core::Error;
77

8-
use super::types::{RunsArgs, RunsArtifactArgs, RunsArtifactCommand, RunsCommand, RunsOutput};
8+
use super::types::{
9+
RunsArgs, RunsArtifactArgs, RunsArtifactCommand, RunsArtifactGetArgs, RunsArtifactsArgs,
10+
RunsCommand, RunsOutput,
11+
};
912
use super::CmdResult;
1013
use super::{
1114
bench, compare, distribution, dossier, drift, evidence, findings, fuzz_compare, handlers,
@@ -63,6 +66,15 @@ impl RunsArgs {
6366
{
6467
None
6568
}
69+
(RunsCommand::Artifacts(args), Some(runner_id)) if args.runner.is_none() => {
70+
args.runner = Some(runner_id);
71+
None
72+
}
73+
(RunsCommand::Artifacts(args), Some(runner_id))
74+
if args.runner.as_deref() == Some(runner_id.as_str()) =>
75+
{
76+
None
77+
}
6678
(
6779
RunsCommand::Artifact(RunsArtifactArgs {
6880
command: RunsArtifactCommand::Get(args),
@@ -89,6 +101,17 @@ impl RunsArgs {
89101
}
90102
}
91103

104+
pub fn is_artifacts(&self) -> bool {
105+
matches!(self.command, RunsCommand::Artifacts(_))
106+
}
107+
108+
pub fn artifacts_runner(&self) -> Option<&str> {
109+
match &self.command {
110+
RunsCommand::Artifacts(args) => args.runner.as_deref(),
111+
_ => None,
112+
}
113+
}
114+
92115
pub fn is_markdown_mode(&self) -> bool {
93116
matches!(self.command, RunsCommand::Compare(ref compare) if compare::is_table_mode(compare))
94117
}
@@ -118,7 +141,15 @@ impl RunsArgs {
118141
pub fn has_command_local_runner_option(&self) -> bool {
119142
matches!(
120143
self.command,
121-
RunsCommand::Artifact(RunsArtifactArgs {
144+
RunsCommand::Artifacts(RunsArtifactsArgs {
145+
runner: Some(_),
146+
..
147+
}) | RunsCommand::Artifact(RunsArtifactArgs {
148+
command: RunsArtifactCommand::Get(RunsArtifactGetArgs {
149+
runner: Some(_),
150+
..
151+
}),
152+
}) | RunsCommand::Artifact(RunsArtifactArgs {
122153
command: RunsArtifactCommand::Attach(_),
123154
})
124155
)
@@ -150,18 +181,6 @@ impl RunsArgs {
150181
"Use `homeboy runs artifact get <run-id> <artifact-id>` for retrievable runner artifacts recorded in the local observation store.".to_string(),
151182
],
152183
),
153-
RunsCommand::Artifacts(args) => (
154-
format!(
155-
"Lab-offloaded run records are mirrored locally; inspect run `{}` with `homeboy runs show {}` without top-level --runner.",
156-
args.run_id, args.run_id
157-
),
158-
vec![
159-
format!("Run `homeboy runs artifacts {}` to list mirrored artifact records.", args.run_id),
160-
format!("Run `homeboy runs artifacts {} --pull` to retrieve runner/remote artifact bytes to the operator-local artifact root.", args.run_id),
161-
format!("Run `homeboy runs artifacts {} --runner {runner_id}` to query the connected runner daemon directly.", args.run_id),
162-
"Use `homeboy runs artifact get <run-id> <artifact-id> --runner <id>` to pull selected runner-side artifact bytes, or `homeboy --runner <id> runs artifact get <run-id> <artifact-id>` from Lab-oriented workflows.".to_string(),
163-
],
164-
),
165184
RunsCommand::Artifact(_) => (
166185
"Runner artifact commands use the local mirrored observation store; rerun without top-level --runner.".to_string(),
167186
vec![

0 commit comments

Comments
 (0)