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
16 changes: 11 additions & 5 deletions crates/wright-cli/src/bin/wright-serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,17 @@ fn serve_jsonrpc(service: &mut ToolService<'_>) -> ExitCode {
let params = value.get("params").cloned();
let result = match method {
"request" => {
let request_line = params
.as_ref()
.and_then(|p| p.get("op"))
.map(|op| format!("{{\"op\":{op}}}"))
.unwrap_or_else(|| "{}".to_string());
// The full params object is forwarded verbatim (op plus any
// operation arguments, e.g. mutation sources/targets, M14
// #130) so the JSON-RPC adapter maps the same request shape
// as the stdio adapter; a params object without `op` is a
// malformed request rather than a silently empty one.
let request_line = match params {
Some(params) if params.get("op").is_some() => {
serde_json::to_string(&params).expect("params serialize")
}
_ => "{}".to_string(),
};
dispatch(service, &request_line)
}
"compile" | "check" | "analyze" | "inspect" => {
Expand Down
93 changes: 93 additions & 0 deletions crates/wright-cli/tests/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,96 @@ fn capability_negotiation_is_preserved() {
.starts_with("wright-result/")
);
}

#[test]
fn stdio_transport_serves_mutation_operations() {
// M14 #130: the stdio adapter exposes the shared mutation operations as
// thin mappings — validated edit preview and semantic rename — with the
// same structured all-or-nothing results as in-process consumers.
let input = corpus_opy("synthetic/declarations-rules");
let source = std::fs::read_to_string(&input).unwrap();
let identity = wright_driver::input_identity(&source);
let line_count = source.lines().count().max(1) as u32;
let end_col = source
.lines()
.last()
.map(|line| line.chars().count() as u32 + 1)
.unwrap_or(1);
let request = serde_json::json!({
"op": "validateEditTransaction",
"sources": { input.to_string_lossy().into_owned(): source.clone() },
"transaction": {
"edits": [{
"kind": "rename",
"source": input.to_string_lossy().into_owned(),
"source_identity": identity,
"range": {
"start_line": 1, "start_col": 1,
"end_line": line_count, "end_col": end_col,
},
"new_text": source.replace("score", "total")
}]
}
});
let responses = run_lines(
"stdio",
&input,
&[&serde_json::to_string(&request).unwrap()],
);
assert_eq!(responses[0]["result"]["ok"], true, "{responses:?}");
let previews = responses[0]["result"]["preview"].as_array().unwrap();
assert!(
previews[0]["new_text"]
.as_str()
.unwrap()
.contains("globalvar total = 0"),
"the preview carries the validated edited text: {responses:?}"
);

// Semantic rename through the same transport.
let rename = serde_json::json!({
"op": "semanticRename",
"sources": {
input.to_string_lossy().into_owned():
std::fs::read_to_string(&input).unwrap()
},
"target": { "source": input.to_string_lossy().into_owned(), "line": 1, "col": 11, "to": "total" }
});
let responses = run_lines("stdio", &input, &[&serde_json::to_string(&rename).unwrap()]);
assert_eq!(responses[0]["result"]["ok"], true, "{responses:?}");
assert_eq!(
responses[0]["result"]["transaction"]["edits"][0]["new_text"],
"total"
);
assert!(
responses[0]["result"]["preview"][0]["new_text"]
.as_str()
.unwrap()
.contains("globalvar total")
);
}

#[test]
fn transports_are_equivalent_for_mutation_operations() {
// M14 #130: stdio and JSON-RPC map the same mutation request to the same
// in-process behavior.
let input = corpus_opy("synthetic/declarations-rules");
let rename = serde_json::json!({
"op": "semanticRename",
"sources": {
input.to_string_lossy().into_owned():
std::fs::read_to_string(&input).unwrap()
},
"target": { "source": input.to_string_lossy().into_owned(), "line": 1, "col": 11, "to": "total" }
});
let stdio = run_lines("stdio", &input, &[&serde_json::to_string(&rename).unwrap()]);
let jsonrpc_request = serde_json::json!({
"jsonrpc": "2.0", "id": 1, "method": "request", "params": rename
});
let jsonrpc = run_lines(
"jsonrpc",
&input,
&[&serde_json::to_string(&jsonrpc_request).unwrap()],
);
assert_eq!(stdio[0]["result"], jsonrpc[0]["result"]["result"]);
}
15 changes: 10 additions & 5 deletions crates/wright-consumer/src/workflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ pub fn run_consumer(input: &str) -> Result<(), String> {
}
}

// Safe rename: propose, validate through the pipeline, preview.
// Safe rename: propose, validate through the project transaction
// contract, preview (M14 #128: the shared frontend-neutral contract).
if input.ends_with(".opy") {
if let Some(name) = first_global(&source) {
let identity = wright_driver::input_identity(&source);
Expand All @@ -103,17 +104,20 @@ pub fn run_consumer(input: &str) -> Result<(), String> {
symbol_kind: "globalVariable".to_string(),
from: name.to_string(),
to: "renamed_by_consumer".to_string(),
source: input.to_string(),
source_identity: identity,
},
)
.map_err(|error| error.message)?;
let validation = wright_driver::edit::validate_edit(
&source,
&rename,
let sources = std::collections::BTreeMap::from([(input.to_string(), source.clone())]);
let validation = wright_driver::edit::validate_transaction(
&SessionConfig {
input: InputSpec::Path(input.into()),
..SessionConfig::default()
},
&sources,
&wright_driver::edit::EditTransaction::new(vec![rename])
.map_err(|error| error.message)?,
);
assert!(
validation.ok,
Expand All @@ -125,7 +129,8 @@ pub fn run_consumer(input: &str) -> Result<(), String> {
.preview
.as_ref()
.unwrap()
.contains("renamed_by_consumer")
.iter()
.any(|preview| preview.new_text.contains("renamed_by_consumer"))
);
println!("edit: safe rename validated and previewed");
} else {
Expand Down
Loading
Loading