Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/tracedecay-api/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,8 @@ pub(crate) fn invalid_request_problem(
)
}

pub(crate) fn adapter_problem(
/// A refusal no binding owns, under the adapter-owned problem contract.
pub fn adapter_problem(
request_id: RequestId,
problem: ApplicationProblem,
) -> Result<ApplicationProblemEnvelope, ApplicationContractError> {
Expand Down
6 changes: 3 additions & 3 deletions crates/tracedecay-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ pub use handoff::{
};
pub use http::{
HttpApplicationControls, HttpApplicationInvocationFuture, HttpApplicationOwnerKind,
HttpApplicationOwners, HttpApplicationRequest, HttpRouteDocumentV1, adapter_problem_response,
application_problem_response, application_problem_status, application_router,
configuration_application_router, feedback_application_router,
HttpApplicationOwners, HttpApplicationRequest, HttpRouteDocumentV1, adapter_problem,
adapter_problem_response, application_problem_response, application_problem_status,
application_router, configuration_application_router, feedback_application_router,
http_application_full_route_path, http_application_owner_kind, http_application_route_path,
http_route_documents, is_http_application_operation_exposed,
};
Expand Down
55 changes: 42 additions & 13 deletions crates/tracedecay-cli/src/tool_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ use tracedecay_daemon_protocol::{
use tracedecay_daemon_service::application_surface::observe_surface_argument_rejection;
use tracedecay_domain::UtcMicros;
use tracedecay_domain::errors::{Result, TraceDecayError};
use tracedecay_mcp::tool_errors::{project_route_problem, tool_result_problem};
use tracedecay_mcp::tool_errors::{mark_semantic_tool_error, tool_result_problem};
use tracedecay_mcp::tools::binding::tool_dispatches_registered_project_reader;
use tracedecay_mcp::tools::response_trailers::{
CODE_GRAPH_FRESHNESS_TRAILER_PREFIX, REQUEST_COST_TRAILER_PREFIX,
Expand All @@ -79,7 +79,7 @@ use tracedecay_mcp::{
RESERVED_FLAGS_FOOTER, ToolDefinition, get_tool_definitions, render_tool_cli_help,
short_tool_name,
};
use tracedecay_tool_catalog::ApplicationSurfaceOperation;
use tracedecay_tool_catalog::{ApplicationSurfaceOperation, BindingSurface};

use crate::cli::dispatch::resolve_cli_application_surface;

Expand Down Expand Up @@ -149,13 +149,38 @@ pub(crate) async fn run(
let json_requested = args.iter().any(|arg| arg == "--json");
let tool_name = name.as_deref().map(canonical_tool_name);
let result = run_inner(profile, project, name, args).await;
if json_requested
&& let (Err(error), Some(tool_name)) = (&result, tool_name.as_deref())
&& let Some(problem) = project_route_problem(tool_name, error)
{
println!("{}", serde_json::json!({ "problem": problem }));
match (&result, tool_name.as_deref()) {
(Err(error), Some(tool_name)) if json_requested => {
print_settled_route_refusal(tool_name, error).map_err(|print_error| {
TraceDecayError::Config {
message: format!(
"{error}; the --json refusal could not be rendered: {print_error}"
),
}
})?;
result
}
_ => result,
}
}

/// A `--json` call refused before any owner answered it prints the same
/// tool-result refusal an answered call does.
fn print_settled_route_refusal(tool_name: &str, error: &TraceDecayError) -> Result<()> {
let request_id =
mint_global_request_id(GlobalRequestSurface::Cli).map_err(|_| TraceDecayError::Config {
message: "could not allocate a refusal request id".to_owned(),
})?;
if let Some(rendered) = tracedecay::mcp::tools::render_settled_route_refusal(
BindingSurface::Cli,
tool_name,
request_id,
error,
&serde_json::json!({ "format": "json" }),
) {
print_tool_output(&rendered?.value, true);
}
result
Ok(())
}

fn run_inner(
Expand Down Expand Up @@ -989,11 +1014,15 @@ fn print_cli_application_surface(
&result,
)?;
account_tool_result(project, &mut rendered);
if raw_json {
print!("{}", crate::cli::output::json::json_line(&result.result)?);
print_beside_result_blocks(&rendered.value);
} else {
print_tool_output(&rendered.value, false);
mark_semantic_tool_error(&mut rendered);
match (&result.result, raw_json) {
// A refusal prints as the tool result every route answers.
(Err(_), true) => print_tool_output(&rendered.value, true),
(Ok(_), true) => {
print!("{}", crate::cli::output::json::json_line(&result.result)?);
print_beside_result_blocks(&rendered.value);
}
(_, false) => print_tool_output(&rendered.value, false),
}
if application_problem.is_some() {
std::io::stdout().flush()?;
Expand Down
124 changes: 124 additions & 0 deletions crates/tracedecay-cli/tests/core_cli_suite/tool_cursor_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -627,3 +627,127 @@ fn search_format_json_prints_the_whole_typed_result() {
stdout.len()
);
}

/// Where a `tool --json` stdout document carries its refusal: its members,
/// `isError`, and the members of its structured content.
fn refusal_placement(stdout: &str) -> Value {
let printed: Value = serde_json::from_str(stdout)
.unwrap_or_else(|error| panic!("refusal printed non-JSON ({error}):\n{stdout}"));
let members = |value: &Value| {
value
.as_object()
.map(|object| object.keys().cloned().collect::<Vec<_>>())
};
json!({
"members": members(&printed),
"is_error": printed["isError"],
"structured_members": members(&printed["structuredContent"]),
})
}

fn problem_of(stdout: &str) -> Value {
serde_json::from_str::<Value>(stdout).unwrap()["structuredContent"]["problem"].clone()
}

/// A compatibility tool's refusal, a typed application tool's refusal, and a
/// refusal no daemon was there to answer all print one `--json` shape: the
/// typed problem record at `structuredContent.problem`, beside `isError`.
#[test]
fn every_json_refusal_prints_one_typed_problem_placement() {
let home = TempDir::new().unwrap();
let project = TempDir::new().unwrap();
let home = canonical_existing_path(home.path());
let project = canonical_existing_path(project.path());
committed_git_project(&project, &hub_source());
initialize_tracedecay_cli_project(&home, &project);
hub_node_id(&home, &project);

let search = run_tool(
&home,
&project,
"tracedecay_search",
&json!({"query": "hub", "limit": 2, "cursor": "not-a-cursor", "format": "json"}),
);
let callers = run_tool(
&home,
&project,
"tracedecay_callers",
&json!({"node_id": "symbol.no-such-node"}),
);
let socket = home.join("no-daemon.sock");
let unreachable = tracedecay_command_with_home(&home)
.current_dir(&project)
.env("TRACEDECAY_DAEMON_SOCKET", &socket)
.args(["tool", "tracedecay_status", "--json"])
.output()
.expect("tracedecay tool should run");
stop_managed_daemon(&home);
let unreachable_stdout = String::from_utf8_lossy(&unreachable.stdout).into_owned();

let expected = json!({
"members": ["content", "isError", "structuredContent"],
"is_error": true,
"structured_members": ["problem"],
});
assert_eq!(
[
refusal_placement(&search.stdout),
refusal_placement(&callers.stdout),
refusal_placement(&unreachable_stdout),
],
[expected.clone(), expected.clone(), expected],
"search:\n{}\ncallers:\n{}\nunreachable:\n{unreachable_stdout}",
search.stdout,
callers.stdout,
);
assert_eq!(
(search.success, callers.success, unreachable.status.code()),
(false, false, Some(69))
);

let search = problem_of(&search.stdout);
let callers = problem_of(&callers.stdout);
let unreachable = problem_of(&unreachable_stdout);
assert_eq!(
[
(&search["kind"], &search["code"], &search["retry"]),
(&callers["kind"], &callers["code"], &callers["retry"]),
(
&unreachable["kind"],
&unreachable["diagnostic"]["code"],
&unreachable["retry"]
),
],
[
(
&json!("invalid_request"),
&json!("cursor.invalid"),
&json!("never")
),
(
&json!("not_found_or_not_authorized"),
&json!("not_found_or_not_authorized"),
&json!("never"),
),
(
&json!("unavailable"),
&json!("daemon.unreachable"),
&json!("after_delay")
),
],
"search {search}\ncallers {callers}\nunreachable {unreachable}"
);
assert_eq!(
(&unreachable["detail"], &unreachable["legal_actions"]),
(
&json!({
"kind": "daemon_unreachable",
"socket": socket.display().to_string(),
"named_by": "TRACEDECAY_DAEMON_SOCKET",
"service_unit": {"state": "not_installed"},
}),
&json!(["retry"]),
),
"{unreachable}"
);
}
15 changes: 12 additions & 3 deletions crates/tracedecay-cli/tests/core_cli_suite/tool_daemon_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1880,8 +1880,14 @@ fn configuration_tool_cli_persists_effects_and_fails_on_stale_cas() {
);
let stale_payload: Value =
serde_json::from_slice(&stale.stdout).expect("stale write problem JSON");
assert_eq!(stale_payload["problem"]["kind"], "conflict");
assert_eq!(stale_payload["problem"]["code"], "configuration.conflict");
assert_eq!(
stale_payload["structuredContent"]["problem"]["kind"],
"conflict"
);
assert_eq!(
stale_payload["structuredContent"]["problem"]["code"],
"configuration.conflict"
);
}

#[test]
Expand Down Expand Up @@ -3634,7 +3640,10 @@ fn user_settings_resolve_from_the_profile_outside_any_project() {
let (code, refused) = configuration_get_from(&home, &outside, "index.max_file_size.v1");
assert_eq!(code, Some(1), "{refused}");
assert_eq!(
(&refused["problem"]["kind"], &refused["problem"]["code"]),
(
&refused["structuredContent"]["problem"]["kind"],
&refused["structuredContent"]["problem"]["code"]
),
(&json!("invalid_request"), &json!("project_required")),
"{refused}"
);
Expand Down
Loading
Loading