use fabro_test::{fabro_snapshot, test_context}; use httpmock::MockServer; use insta::assert_snapshot; use serde_json::{Value, json}; use super::support::{ compact_git_inspect, compact_inspect, remote_run_summary_json, run_success, setup_seeded_completed_dry_run, setup_seeded_created_dry_run, setup_seeded_git_backed_changed_run, }; use crate::support::{run_projection_json, unique_run_id}; fn remote_run_summary(run_id: &str, status: &serde_json::Value) -> serde_json::Value { remote_run_summary_json( run_id, "Nightly Build", "nightly-build", "Inspect remote state", status, "2026-04-19T12:00:00Z", ) } #[test] fn help() { let context = test_context!(); let mut cmd = context.command(); cmd.args(["inspect", "--help"]); fabro_snapshot!(context.filters(), cmd, @" success: true exit_code: 0 ----- stdout ----- Show detailed information about a workflow run Usage: fabro inspect [OPTIONS] Arguments: Run ID prefix or workflow name (most recent run) Options: --json Output as JSON [env: FABRO_JSON=] --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=] --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] --quiet Suppress non-essential output [env: FABRO_QUIET=] --verbose Enable verbose output [env: FABRO_VERBOSE=] -h, --help Print help ----- stderr ----- "); } #[test] fn inspect_resolves_selector_via_server_endpoint() { let context = test_context!(); let server = MockServer::start(); let run_id = unique_run_id(); let summary = remote_run_summary( run_id.as_str(), &json!({ "kind": "succeeded", "reason": "completed" }), ); let resolve_run = server.mock(|when, then| { when.method("GET") .path("/api/v1/runs/resolve") .query_param("selector", "nightly-build"); then.status(200) .header("content-type", "application/json") .body(summary.to_string()); }); let run_state = server.mock(|when, then| { when.method("GET") .path(format!("/api/v1/runs/{}/state", run_id.as_str())); then.status(200) .header("content-type", "application/json") .body( run_projection_json( run_id.as_str(), &json!({ "kind": "succeeded", "reason": "completed" }), ) .to_string(), ); }); let mut cmd = context.command(); cmd.args([ "inspect", "--server", &format!("{}/api/v1", server.base_url()), "nightly-build", ]); fabro_snapshot!(context.filters(), cmd, @r#" success: true exit_code: 0 ----- stdout ----- [ { "run_id": "[ULID]", "parent_id": null, "status": { "kind": "succeeded", "reason": "completed" }, "run_spec": { "run_id": "[ULID]", "settings": { "project": { "name": null, "description": null, "metadata": {} }, "workflow": { "name": null, "description": null, "graph": "", "metadata": {} }, "run": { "goal": null, "working_dir": null, "metadata": {}, "inputs": {}, "model": { "provider": null, "name": null, "fallbacks": {}, "controls": { "reasoning_effort": null, "speed": null } }, "git": { "author": null }, "prepare": { "steps": [], "timeout_ms": 300000 }, "execution": { "mode": "normal", "approval": "prompt" }, "checkpoint": { "exclude_globs": [], "skip_git_hooks": false, "commit_timeout_ms": 30000 }, "clone": { "enabled": true, "depth": 100 }, "run_branch": { "enabled": true, "push": true }, "meta_branch": { "enabled": true, "push": true }, "environment": { "id": "default", "provider": "local", "image": { "docker": null, "dockerfile": null }, "resources": { "cpu": null, "memory": null, "disk": null }, "network": { "mode": "allow_all", "allow": [] }, "lifecycle": { "preserve": false, "stop_on_terminal": true, "auto_stop": null }, "labels": {}, "env": {} }, "notifications": {}, "interviews": { "provider": null, "slack": null }, "agent": { "fabro_tools": false, "mcps": {} }, "hooks": [], "scm": { "provider": null, "owner": null, "repository": null, "github": null }, "pull_request": null, "artifacts": { "include": [] }, "integrations": { "github": { "permissions": {} } } } }, "graph": { "name": "Remote Workflow", "nodes": {}, "edges": [], "attrs": {} }, "workflow_slug": "remote-workflow", "source_directory": "/srv/repo", "provenance": { "subject": { "kind": "user", "identity": { "issuer": "fabro:test", "subject": "test-user" }, "login": "test", "auth_method": "dev_token" } } }, "start_record": null, "conclusion": null, "checkpoint": null, "sandbox": null } ] ----- stderr ----- "#); resolve_run.assert(); run_state.assert(); } #[test] fn inspect_includes_parent_id_from_run_projection() { let context = test_context!(); let server = MockServer::start(); let run_id = unique_run_id(); let parent_id = unique_run_id(); let summary = remote_run_summary( run_id.as_str(), &json!({ "kind": "succeeded", "reason": "completed" }), ); let resolve_run = server.mock(|when, then| { when.method("GET") .path("/api/v1/runs/resolve") .query_param("selector", "nightly-build"); then.status(200) .header("content-type", "application/json") .body(summary.to_string()); }); let run_state = server.mock(|when, then| { when.method("GET") .path(format!("/api/v1/runs/{}/state", run_id.as_str())); let mut state = run_projection_json( run_id.as_str(), &json!({ "kind": "succeeded", "reason": "completed" }), ); state["parent_id"] = json!(parent_id); then.status(200) .header("content-type", "application/json") .body(state.to_string()); }); let output = context .command() .args([ "inspect", "--server", &format!("{}/api/v1", server.base_url()), "nightly-build", ]) .output() .expect("inspect should execute"); assert!( output.status.success(), "inspect failed:\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); let items: Value = serde_json::from_slice(&output.stdout).expect("inspect JSON should parse"); assert_eq!(items[0]["run_id"], run_id); assert_eq!(items[0]["parent_id"], parent_id); resolve_run.assert(); run_state.assert(); } #[test] fn inspect_created_run_shows_run_spec_without_start_or_conclusion() { let context = test_context!(); let run = setup_seeded_created_dry_run(&context); let output = run_success(&context, &["inspect", &run.run_id]); assert_snapshot!(serde_json::to_string_pretty(&compact_inspect(&output)).unwrap(), @r#" [ { "run_id": "[ULID]", "status": { "kind": "submitted" }, "run_spec": { "goal": { "type": "inline", "value": "Run tests and report results" }, "workflow_name": "Simple", "workflow_slug": "simple", "sandbox_provider": null, "dry_run": true, "provenance": { "server_version": "[VERSION]", "client_name": "fabro-cli", "client_version": "[VERSION]", "subject_auth_method": "dev_token" } }, "start_record": null, "conclusion": null, "checkpoint": null, "sandbox": { "provider": "docker" } } ] "#); } #[test] fn inspect_completed_run_shows_run_start_conclusion_checkpoint() { let context = test_context!(); let run = setup_seeded_completed_dry_run(&context); let output = run_success(&context, &["inspect", &run.run_id]); assert_snapshot!(serde_json::to_string_pretty(&compact_inspect(&output)).unwrap(), @r#" [ { "run_id": "[ULID]", "status": { "kind": "succeeded", "reason": "completed" }, "run_spec": { "goal": { "type": "inline", "value": "Run tests and report results" }, "workflow_name": "Simple", "workflow_slug": "simple", "sandbox_provider": null, "dry_run": true, "provenance": { "server_version": "[VERSION]", "client_name": "fabro-cli", "client_version": "[VERSION]", "subject_auth_method": "dev_token" } }, "start_record": { "has_start_time": true }, "conclusion": { "status": "succeeded", "timing": "[TIMING]", "stage_count": 3 }, "checkpoint": { "current_node": "report", "completed_nodes": [ "start", "run_tests", "report" ], "next_node_id": "exit" }, "sandbox": { "provider": "local" } } ] "#); } #[test] fn inspect_json_omits_run_dir() { let context = test_context!(); let run = setup_seeded_completed_dry_run(&context); let output = run_success(&context, &["inspect", &run.run_id]); let items: serde_json::Value = serde_json::from_slice(&output.stdout).expect("inspect output should parse"); let first = items .as_array() .and_then(|items| items.first()) .expect("inspect output should contain one item"); assert!( first.get("run_dir").is_none(), "inspect JSON should not expose run_dir" ); } #[test] fn inspect_completed_run_reads_store_without_disk_metadata_files() { let context = test_context!(); let run = setup_seeded_completed_dry_run(&context); let output = run_success(&context, &["inspect", &run.run_id]); assert_snapshot!(serde_json::to_string_pretty(&compact_inspect(&output)).unwrap(), @r#" [ { "run_id": "[ULID]", "status": { "kind": "succeeded", "reason": "completed" }, "run_spec": { "goal": { "type": "inline", "value": "Run tests and report results" }, "workflow_name": "Simple", "workflow_slug": "simple", "sandbox_provider": null, "dry_run": true, "provenance": { "server_version": "[VERSION]", "client_name": "fabro-cli", "client_version": "[VERSION]", "subject_auth_method": "dev_token" } }, "start_record": { "has_start_time": true }, "conclusion": { "status": "succeeded", "timing": "[TIMING]", "stage_count": 3 }, "checkpoint": { "current_node": "report", "completed_nodes": [ "start", "run_tests", "report" ], "next_node_id": "exit" }, "sandbox": { "provider": "local" } } ] "#); } #[test] fn inspect_git_backed_run_exposes_checkpoint_and_sandbox_state() { let context = test_context!(); let setup = setup_seeded_git_backed_changed_run(&context); let output = run_success(&context, &["inspect", &setup.run.run_id]); assert_snapshot!( serde_json::to_string_pretty(&compact_git_inspect(&output)).unwrap(), @r#" [ { "run_id": "[ULID]", "status": { "kind": "succeeded", "reason": "completed" }, "run_spec": { "goal": { "type": "inline", "value": "Edit a tracked file" }, "workflow_name": "Flow", "workflow_slug": "flow", "llm_provider": "openai", "sandbox_provider": null, "provenance": { "server_version": "[VERSION]", "client_name": "fabro-cli", "client_version": "[VERSION]", "subject_auth_method": "dev_token" } }, "start_record": { "has_start_time": true, "run_branch": "fabro/run/[ULID]", "base_sha": "[SHA]" }, "conclusion": { "status": "succeeded", "timing": "[TIMING]", "final_git_commit_sha": "[SHA]", "stage_count": 3 }, "checkpoint": { "current_node": "step_two", "completed_nodes": [ "start", "step_one", "step_two" ], "next_node_id": "exit", "git_commit_sha": "[SHA]" }, "sandbox": { "provider": "local", "working_directory": "[WORKTREE]" } } ] "# ); }