use serde_json::Value;
use std::path::Path;
use std::process::Command;
use tempfile::tempdir;
fn run_cli(args: &[&str]) -> std::process::Output {
Command::new(assert_cmd::cargo::cargo_bin!("agent-spreadsheet"))
.args(args)
.output()
.expect("run agent-spreadsheet")
}
fn parse_stdout_json(output: &std::process::Output) -> Value {
let stdout = String::from_utf8(output.stdout.clone()).expect("stdout utf8");
serde_json::from_str(&stdout).unwrap_or_else(|e| {
panic!(
"invalid json in stdout: {}\nstdout: {}\nstderr: {}",
e,
stdout,
String::from_utf8_lossy(&output.stderr)
)
})
}
fn assert_success(output: &std::process::Output) {
assert!(
output.status.success(),
"command failed.\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
fn write_fixture(path: &Path) {
let mut workbook = umya_spreadsheet::new_file();
{
let sheet = workbook
.get_sheet_by_name_mut("Sheet1")
.expect("default sheet");
sheet.get_cell_mut("A1").set_value("Name");
sheet.get_cell_mut("B1").set_value("Amount");
sheet.get_cell_mut("C1").set_value("Total");
sheet.get_cell_mut("A2").set_value("Alice");
sheet.get_cell_mut("B2").set_value_number(10.0);
sheet.get_cell_mut("C2").set_formula("B2*2");
sheet.get_cell_mut("A3").set_value("Bob");
sheet.get_cell_mut("B3").set_value_number(20.0);
sheet.get_cell_mut("C3").set_formula("B3*2");
}
workbook.new_sheet("Summary").expect("add summary sheet");
{
let s = workbook.get_sheet_by_name_mut("Summary").expect("summary");
s.get_cell_mut("A1").set_value("Flag");
s.get_cell_mut("B1").set_value("Ready");
}
umya_spreadsheet::writer::xlsx::write(&workbook, path).expect("write fixture");
}
fn write_ops_json(path: &Path) {
let payload = serde_json::json!({
"kind": "transform.write_matrix",
"sheet_name": "Sheet1",
"anchor": "A2",
"rows": [[{"v": "Eve"}]],
"overwrite_formulas": false,
});
std::fs::write(path, serde_json::to_string_pretty(&payload).unwrap()).unwrap();
}
fn write_second_ops_json(path: &Path) {
let payload = serde_json::json!({
"kind": "transform.write_matrix",
"sheet_name": "Sheet1",
"anchor": "B2",
"rows": [[{"v": 99.0}]],
"overwrite_formulas": false,
});
std::fs::write(path, serde_json::to_string_pretty(&payload).unwrap()).unwrap();
}
#[test]
fn session_full_workflow_start_stage_apply_materialize() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let base_path = workspace.join("base.xlsx");
write_fixture(&base_path);
let base_str = base_path.to_str().unwrap();
let ws_str = workspace.to_str().unwrap();
let start = run_cli(&[
"session",
"start",
"--base",
base_str,
"--label",
"e2e test session",
"--workspace",
ws_str,
]);
assert_success(&start);
let start_json = parse_stdout_json(&start);
let session_id = start_json["session_id"].as_str().expect("session_id");
assert_eq!(start_json["label"], "e2e test session");
let ops_path = workspace.join("ops.json");
write_ops_json(&ops_path);
let stage = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops_path.display()),
"--workspace",
ws_str,
]);
assert_success(&stage);
let stage_json = parse_stdout_json(&stage);
let staged_id = stage_json["staged_id"].as_str().expect("staged_id");
assert!(staged_id.starts_with("stg_"));
assert!(stage_json["head_at_stage"].is_null());
let apply = run_cli(&[
"session",
"apply",
"--session",
session_id,
staged_id,
"--workspace",
ws_str,
]);
assert_success(&apply);
let apply_json = parse_stdout_json(&apply);
assert_eq!(apply_json["applied"], true);
let op_id = apply_json["op_id"].as_str().expect("op_id");
assert_eq!(apply_json["head"], op_id);
let log = run_cli(&[
"session",
"log",
"--session",
session_id,
"--workspace",
ws_str,
]);
assert_success(&log);
let log_json = parse_stdout_json(&log);
assert_eq!(log_json["event_count"], 1);
assert_eq!(log_json["head"], op_id);
assert_eq!(log_json["events"][0]["op_id"], op_id);
let output_path = workspace.join("result.xlsx");
let materialize = run_cli(&[
"session",
"materialize",
"--session",
session_id,
"--output",
output_path.to_str().unwrap(),
"--workspace",
ws_str,
]);
assert_success(&materialize);
let mat_json = parse_stdout_json(&materialize);
assert_eq!(mat_json["events_replayed"], 1);
assert!(mat_json["output_size_bytes"].as_u64().unwrap() > 0);
assert!(output_path.exists());
let diff = run_cli(&[
"diff",
base_str,
output_path.to_str().unwrap(),
"--details",
"--limit",
"50",
]);
assert_success(&diff);
let diff_json = parse_stdout_json(&diff);
assert!(
diff_json["change_count"].as_u64().unwrap() >= 1,
"expected at least 1 change, got: {}",
diff_json
);
}
#[test]
fn session_multi_op_undo_redo() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let base_path = workspace.join("undo_base.xlsx");
write_fixture(&base_path);
let ws_str = workspace.to_str().unwrap();
let base_str = base_path.to_str().unwrap();
let start_json = {
let out = run_cli(&[
"session",
"start",
"--base",
base_str,
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let session_id = start_json["session_id"].as_str().unwrap();
let ops1 = workspace.join("ops1.json");
write_ops_json(&ops1);
let stg1 = {
let out = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops1.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let stg1_id = stg1["staged_id"].as_str().unwrap();
let apply1 = run_cli(&[
"session",
"apply",
"--session",
session_id,
stg1_id,
"--workspace",
ws_str,
]);
assert_success(&apply1);
let op1_id = parse_stdout_json(&apply1)["op_id"]
.as_str()
.unwrap()
.to_string();
let ops2 = workspace.join("ops2.json");
write_second_ops_json(&ops2);
let stg2 = {
let out = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops2.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let stg2_id = stg2["staged_id"].as_str().unwrap();
let apply2 = run_cli(&[
"session",
"apply",
"--session",
session_id,
stg2_id,
"--workspace",
ws_str,
]);
assert_success(&apply2);
let op2_id = parse_stdout_json(&apply2)["op_id"]
.as_str()
.unwrap()
.to_string();
let log = run_cli(&[
"session",
"log",
"--session",
session_id,
"--workspace",
ws_str,
]);
assert_success(&log);
assert_eq!(parse_stdout_json(&log)["event_count"], 2);
let undo = run_cli(&[
"session",
"undo",
"--session",
session_id,
"--workspace",
ws_str,
]);
assert_success(&undo);
let undo_json = parse_stdout_json(&undo);
assert_eq!(undo_json["undone"], true);
assert_eq!(undo_json["head"].as_str().unwrap(), op1_id);
let out_undo = workspace.join("undo_result.xlsx");
let mat_undo = run_cli(&[
"session",
"materialize",
"--session",
session_id,
"--output",
out_undo.to_str().unwrap(),
"--workspace",
ws_str,
]);
assert_success(&mat_undo);
let read_a2 = run_cli(&[
"range-values",
out_undo.to_str().unwrap(),
"Sheet1",
"A2:A2",
]);
assert_success(&read_a2);
let read_json = parse_stdout_json(&read_a2);
let values = &read_json["values"][0];
let dense = values.get("dense").or(values.get("rows_keyed"));
assert!(
serde_json::to_string(dense.unwrap_or(&Value::Null))
.unwrap()
.contains("Eve"),
"expected Eve in A2 after undo to op1, got: {}",
read_json
);
let redo = run_cli(&[
"session",
"redo",
"--session",
session_id,
"--workspace",
ws_str,
]);
assert_success(&redo);
let redo_json = parse_stdout_json(&redo);
assert_eq!(redo_json["redone"], true);
assert_eq!(redo_json["head"].as_str().unwrap(), op2_id);
}
#[test]
fn session_branching_fork_and_switch() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let base_path = workspace.join("branch_base.xlsx");
write_fixture(&base_path);
let ws_str = workspace.to_str().unwrap();
let base_str = base_path.to_str().unwrap();
let start = run_cli(&[
"session",
"start",
"--base",
base_str,
"--workspace",
ws_str,
]);
assert_success(&start);
let session_id = parse_stdout_json(&start)["session_id"]
.as_str()
.unwrap()
.to_string();
let ops = workspace.join("fork_ops.json");
write_ops_json(&ops);
let stg = {
let out = run_cli(&[
"session",
"op",
"--session",
&session_id,
"--ops",
&format!("@{}", ops.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let stg_id = stg["staged_id"].as_str().unwrap();
let apply = run_cli(&[
"session",
"apply",
"--session",
&session_id,
stg_id,
"--workspace",
ws_str,
]);
assert_success(&apply);
let _op1_id = parse_stdout_json(&apply)["op_id"]
.as_str()
.unwrap()
.to_string();
let fork = run_cli(&[
"session",
"fork",
"--session",
&session_id,
"--label",
"alternative approach",
"alt-branch",
"--workspace",
ws_str,
]);
assert_success(&fork);
let fork_json = parse_stdout_json(&fork);
assert_eq!(fork_json["branch"], "alt-branch");
let branches = run_cli(&[
"session",
"branches",
"--session",
&session_id,
"--workspace",
ws_str,
]);
assert_success(&branches);
let branches_json = parse_stdout_json(&branches);
let branch_list = branches_json["branches"]
.as_array()
.expect("branches array");
assert_eq!(branch_list.len(), 2, "expected main + alt-branch");
let branch_names: Vec<&str> = branch_list
.iter()
.map(|b| b["name"].as_str().unwrap())
.collect();
assert!(branch_names.contains(&"main"));
assert!(branch_names.contains(&"alt-branch"));
let switch = run_cli(&[
"session",
"switch",
"--session",
&session_id,
"--branch",
"alt-branch",
"--workspace",
ws_str,
]);
assert_success(&switch);
let switch_json = parse_stdout_json(&switch);
assert_eq!(switch_json["branch"], "alt-branch");
let switch_main = run_cli(&[
"session",
"switch",
"--session",
&session_id,
"--branch",
"main",
"--workspace",
ws_str,
]);
assert_success(&switch_main);
assert_eq!(parse_stdout_json(&switch_main)["branch"], "main");
}
#[test]
fn session_checkout_specific_event() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let base_path = workspace.join("checkout_base.xlsx");
write_fixture(&base_path);
let ws_str = workspace.to_str().unwrap();
let base_str = base_path.to_str().unwrap();
let start_json = {
let out = run_cli(&[
"session",
"start",
"--base",
base_str,
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let session_id = start_json["session_id"].as_str().unwrap();
let ops1 = workspace.join("co_ops1.json");
write_ops_json(&ops1);
let stg1 = {
let out = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops1.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let apply1 = run_cli(&[
"session",
"apply",
"--session",
session_id,
stg1["staged_id"].as_str().unwrap(),
"--workspace",
ws_str,
]);
assert_success(&apply1);
let op1_id = parse_stdout_json(&apply1)["op_id"]
.as_str()
.unwrap()
.to_string();
let ops2 = workspace.join("co_ops2.json");
write_second_ops_json(&ops2);
let stg2 = {
let out = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops2.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let apply2 = run_cli(&[
"session",
"apply",
"--session",
session_id,
stg2["staged_id"].as_str().unwrap(),
"--workspace",
ws_str,
]);
assert_success(&apply2);
let checkout = run_cli(&[
"session",
"checkout",
"--session",
session_id,
&op1_id,
"--workspace",
ws_str,
]);
assert_success(&checkout);
let co_json = parse_stdout_json(&checkout);
assert_eq!(co_json["head"], op1_id);
let mat_path = workspace.join("checkout_result.xlsx");
let mat = run_cli(&[
"session",
"materialize",
"--session",
session_id,
"--output",
mat_path.to_str().unwrap(),
"--workspace",
ws_str,
]);
assert_success(&mat);
let mat_json = parse_stdout_json(&mat);
assert_eq!(
mat_json["events_replayed"], 1,
"should replay only 1 event at op1"
);
}
#[test]
fn session_cas_conflict_rejects_stale_stage() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let base_path = workspace.join("cas_base.xlsx");
write_fixture(&base_path);
let ws_str = workspace.to_str().unwrap();
let base_str = base_path.to_str().unwrap();
let start_json = {
let out = run_cli(&[
"session",
"start",
"--base",
base_str,
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let session_id = start_json["session_id"].as_str().unwrap();
let ops1 = workspace.join("cas_ops1.json");
write_ops_json(&ops1);
let stg1 = {
let out = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops1.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let stg1_id = stg1["staged_id"].as_str().unwrap();
let ops2 = workspace.join("cas_ops2.json");
write_second_ops_json(&ops2);
let stg2 = {
let out = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops2.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let apply2 = run_cli(&[
"session",
"apply",
"--session",
session_id,
stg2["staged_id"].as_str().unwrap(),
"--workspace",
ws_str,
]);
assert_success(&apply2);
let apply1 = run_cli(&[
"session",
"apply",
"--session",
session_id,
stg1_id,
"--workspace",
ws_str,
]);
assert!(
!apply1.status.success(),
"expected CAS conflict, but command succeeded"
);
let stderr = String::from_utf8_lossy(&apply1.stderr);
assert!(
stderr.contains("CAS conflict") || stderr.contains("HEAD has advanced"),
"expected CAS conflict error, got stderr: {}",
stderr
);
}
#[test]
fn session_log_filters_by_kind() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let base_path = workspace.join("logfilter_base.xlsx");
write_fixture(&base_path);
let ws_str = workspace.to_str().unwrap();
let base_str = base_path.to_str().unwrap();
let start_json = {
let out = run_cli(&[
"session",
"start",
"--base",
base_str,
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let session_id = start_json["session_id"].as_str().unwrap();
let ops = workspace.join("logfilter_ops.json");
write_ops_json(&ops);
let stg = {
let out = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let apply = run_cli(&[
"session",
"apply",
"--session",
session_id,
stg["staged_id"].as_str().unwrap(),
"--workspace",
ws_str,
]);
assert_success(&apply);
let log = run_cli(&[
"session",
"log",
"--session",
session_id,
"--kind",
"transform",
"--workspace",
ws_str,
]);
assert_success(&log);
let log_json = parse_stdout_json(&log);
assert_eq!(log_json["event_count"], 1);
assert!(
log_json["events"][0]["kind"]
.as_str()
.unwrap()
.starts_with("transform"),
);
let log_empty = run_cli(&[
"session",
"log",
"--session",
session_id,
"--kind",
"structure",
"--workspace",
ws_str,
]);
assert_success(&log_empty);
assert_eq!(parse_stdout_json(&log_empty)["event_count"], 0);
}
#[test]
fn session_materialize_refuses_overwrite_without_force() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let base_path = workspace.join("force_base.xlsx");
write_fixture(&base_path);
let ws_str = workspace.to_str().unwrap();
let base_str = base_path.to_str().unwrap();
let start_json = {
let out = run_cli(&[
"session",
"start",
"--base",
base_str,
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let session_id = start_json["session_id"].as_str().unwrap();
let output_path = workspace.join("overwrite_target.xlsx");
let mat1 = run_cli(&[
"session",
"materialize",
"--session",
session_id,
"--output",
output_path.to_str().unwrap(),
"--workspace",
ws_str,
]);
assert_success(&mat1);
let mat2 = run_cli(&[
"session",
"materialize",
"--session",
session_id,
"--output",
output_path.to_str().unwrap(),
"--workspace",
ws_str,
]);
assert!(!mat2.status.success(), "expected overwrite refusal");
let stderr = String::from_utf8_lossy(&mat2.stderr);
assert!(
stderr.contains("already exists") || stderr.contains("--force"),
"expected overwrite error, got: {}",
stderr
);
let mat3 = run_cli(&[
"session",
"materialize",
"--session",
session_id,
"--output",
output_path.to_str().unwrap(),
"--force",
"--workspace",
ws_str,
]);
assert_success(&mat3);
}
#[test]
fn session_start_rejects_missing_base() {
let tmp = tempdir().expect("tempdir");
let ws_str = tmp.path().to_str().unwrap();
let start = run_cli(&[
"session",
"start",
"--base",
"/nonexistent/path/workbook.xlsx",
"--workspace",
ws_str,
]);
assert!(
!start.status.success(),
"expected failure for missing base file"
);
let stderr = String::from_utf8_lossy(&start.stderr);
assert!(
stderr.contains("not found"),
"expected 'not found' error, got: {}",
stderr
);
}
#[test]
fn session_op_stage_rejects_missing_kind() {
let workspace = tempdir().expect("workspace");
let base_path = workspace.path().join("missing_kind_base.xlsx");
write_fixture(&base_path);
let ws_str = workspace.path().to_str().unwrap();
let base_str = base_path.to_str().unwrap();
let start = run_cli(&[
"session",
"start",
"--base",
base_str,
"--workspace",
ws_str,
]);
assert_success(&start);
let session_id = parse_stdout_json(&start)["session_id"]
.as_str()
.unwrap()
.to_string();
let ops_path = workspace.path().join("missing_kind_ops.json");
let payload = serde_json::json!({
"sheet_name": "Sheet1",
"anchor": "A2",
"rows": [[{"v": "Eve"}]],
"overwrite_formulas": false,
});
std::fs::write(&ops_path, serde_json::to_string_pretty(&payload).unwrap()).unwrap();
let out = run_cli(&[
"session",
"op",
"--session",
&session_id,
"--ops",
&format!("@{}", ops_path.display()),
"--workspace",
ws_str,
]);
assert!(
!out.status.success(),
"expected session op to reject payload without kind"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("top-level string 'kind'"),
"stderr: {stderr}"
);
assert!(
stderr.contains("transform.write_matrix"),
"stderr: {stderr}"
);
}
#[test]
fn session_apply_rejects_unknown_staged_id() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let base_path = workspace.join("unknown_stg_base.xlsx");
write_fixture(&base_path);
let ws_str = workspace.to_str().unwrap();
let base_str = base_path.to_str().unwrap();
let start_json = {
let out = run_cli(&[
"session",
"start",
"--base",
base_str,
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let session_id = start_json["session_id"].as_str().unwrap();
let apply = run_cli(&[
"session",
"apply",
"--session",
session_id,
"stg_nonexistent_12345678",
"--workspace",
ws_str,
]);
assert!(
!apply.status.success(),
"expected failure for unknown staged_id"
);
let stderr = String::from_utf8_lossy(&apply.stderr);
assert!(
stderr.contains("not found"),
"expected staged op 'not found' error, got: {}",
stderr
);
}
#[test]
fn session_undo_at_base_is_graceful() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let base_path = workspace.join("undo_base_edge.xlsx");
write_fixture(&base_path);
let ws_str = workspace.to_str().unwrap();
let base_str = base_path.to_str().unwrap();
let start_json = {
let out = run_cli(&[
"session",
"start",
"--base",
base_str,
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let session_id = start_json["session_id"].as_str().unwrap();
let undo = run_cli(&[
"session",
"undo",
"--session",
session_id,
"--workspace",
ws_str,
]);
let _stderr = String::from_utf8_lossy(&undo.stderr);
}
#[test]
fn session_structure_op_roundtrip() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let base_path = workspace.join("struct_base.xlsx");
write_fixture(&base_path);
let ws_str = workspace.to_str().unwrap();
let base_str = base_path.to_str().unwrap();
let start_json = {
let out = run_cli(&[
"session",
"start",
"--base",
base_str,
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let session_id = start_json["session_id"].as_str().unwrap();
let ops_path = workspace.join("struct_ops.json");
let payload = serde_json::json!({
"kind": "structure.insert_rows",
"ops": [{
"kind": "insert_rows",
"sheet_name": "Sheet1",
"at_row": 3,
"count": 2
}]
});
std::fs::write(&ops_path, serde_json::to_string_pretty(&payload).unwrap()).unwrap();
let stg = {
let out = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops_path.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let stg_id = stg["staged_id"].as_str().unwrap();
let apply = run_cli(&[
"session",
"apply",
"--session",
session_id,
stg_id,
"--workspace",
ws_str,
]);
assert_success(&apply);
let output_path = workspace.join("struct_result.xlsx");
let mat = run_cli(&[
"session",
"materialize",
"--session",
session_id,
"--output",
output_path.to_str().unwrap(),
"--workspace",
ws_str,
]);
assert_success(&mat);
assert_eq!(parse_stdout_json(&mat)["events_replayed"], 1);
let read = run_cli(&[
"range-values",
output_path.to_str().unwrap(),
"Sheet1",
"A5:A5",
]);
assert_success(&read);
let read_json = parse_stdout_json(&read);
let read_str = serde_json::to_string(&read_json).unwrap();
assert!(
read_str.contains("Bob"),
"expected Bob at row 5 after insert_rows at row 3, got: {}",
read_str
);
}
#[test]
fn session_style_op_roundtrip() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let base_path = workspace.join("style_base.xlsx");
write_fixture(&base_path);
let ws_str = workspace.to_str().unwrap();
let base_str = base_path.to_str().unwrap();
let start_json = {
let out = run_cli(&[
"session",
"start",
"--base",
base_str,
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let session_id = start_json["session_id"].as_str().unwrap();
let ops_path = workspace.join("style_ops.json");
let payload = serde_json::json!({
"kind": "style.apply",
"ops": [{
"sheet_name": "Sheet1",
"target": {"kind": "range", "range": "A1:A1"},
"patch": {"font": {"bold": true}}
}]
});
std::fs::write(&ops_path, serde_json::to_string_pretty(&payload).unwrap()).unwrap();
let stg = {
let out = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops_path.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let stg_id = stg["staged_id"].as_str().unwrap();
let apply = run_cli(&[
"session",
"apply",
"--session",
session_id,
stg_id,
"--workspace",
ws_str,
]);
assert_success(&apply);
let output_path = workspace.join("style_result.xlsx");
let mat = run_cli(&[
"session",
"materialize",
"--session",
session_id,
"--output",
output_path.to_str().unwrap(),
"--workspace",
ws_str,
]);
assert_success(&mat);
assert_eq!(parse_stdout_json(&mat)["events_replayed"], 1);
let read = run_cli(&[
"range-values",
output_path.to_str().unwrap(),
"Sheet1",
"A1:A1",
]);
assert_success(&read);
let read_str = serde_json::to_string(&parse_stdout_json(&read)).unwrap();
assert!(
read_str.contains("Name"),
"expected Name in A1 after style op (data preserved), got: {}",
read_str
);
let layout = run_cli(&[
"layout-page",
output_path.to_str().unwrap(),
"Sheet1",
"--range",
"A1:A1",
"--render",
"json",
]);
assert_success(&layout);
let layout_str = serde_json::to_string(&parse_stdout_json(&layout)).unwrap();
assert!(
layout_str.contains("bold") || layout_str.contains("B") || layout_str.contains("Name"),
"expected style or content in layout-page output, got: {}",
layout_str
);
}
#[test]
fn session_name_define_roundtrip() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let base_path = workspace.join("name_base.xlsx");
write_fixture(&base_path);
let ws_str = workspace.to_str().unwrap();
let base_str = base_path.to_str().unwrap();
let start_json = {
let out = run_cli(&[
"session",
"start",
"--base",
base_str,
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let session_id = start_json["session_id"].as_str().unwrap();
let ops_path = workspace.join("name_ops.json");
let payload = serde_json::json!({
"kind": "name.define",
"name": "TestRange",
"refers_to": "Sheet1!$A$1:$C$3"
});
std::fs::write(&ops_path, serde_json::to_string_pretty(&payload).unwrap()).unwrap();
let stg = {
let out = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops_path.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let stg_id = stg["staged_id"].as_str().unwrap();
let apply = run_cli(&[
"session",
"apply",
"--session",
session_id,
stg_id,
"--workspace",
ws_str,
]);
assert_success(&apply);
let output_path = workspace.join("name_result.xlsx");
let mat = run_cli(&[
"session",
"materialize",
"--session",
session_id,
"--output",
output_path.to_str().unwrap(),
"--workspace",
ws_str,
]);
assert_success(&mat);
let nr = run_cli(&["named-ranges", output_path.to_str().unwrap()]);
assert_success(&nr);
let nr_json = parse_stdout_json(&nr);
let nr_str = serde_json::to_string(&nr_json).unwrap();
assert!(
nr_str.contains("TestRange"),
"expected TestRange in named-ranges output, got: {}",
nr_str
);
}
#[test]
fn session_read_with_session_flag() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let base_path = workspace.join("read_session_base.xlsx");
write_fixture(&base_path);
let ws_str = workspace.to_str().unwrap();
let base_str = base_path.to_str().unwrap();
let start_json = {
let out = run_cli(&[
"session",
"start",
"--base",
base_str,
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let session_id = start_json["session_id"].as_str().unwrap();
let ops_path = workspace.join("read_sess_ops.json");
write_ops_json(&ops_path);
let stg = {
let out = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops_path.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let apply = run_cli(&[
"session",
"apply",
"--session",
session_id,
stg["staged_id"].as_str().unwrap(),
"--workspace",
ws_str,
]);
assert_success(&apply);
let read = run_cli(&[
"range-values",
base_str,
"Sheet1",
"A2:A2",
"--session",
session_id,
"--session-workspace",
ws_str,
]);
assert_success(&read);
let read_json = parse_stdout_json(&read);
let read_str = serde_json::to_string(&read_json).unwrap();
assert!(
read_str.contains("Eve"),
"expected Eve in session-read A2, got: {}",
read_str
);
let read_base = run_cli(&["range-values", base_str, "Sheet1", "A2:A2"]);
assert_success(&read_base);
let base_str_out = serde_json::to_string(&parse_stdout_json(&read_base)).unwrap();
assert!(
base_str_out.contains("Alice"),
"expected Alice in base file A2, got: {}",
base_str_out
);
}
#[test]
fn session_dry_run_impact_on_stage() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let base_path = workspace.join("impact_base.xlsx");
write_fixture(&base_path);
let ws_str = workspace.to_str().unwrap();
let base_str = base_path.to_str().unwrap();
let start_json = {
let out = run_cli(&[
"session",
"start",
"--base",
base_str,
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let session_id = start_json["session_id"].as_str().unwrap();
let ops_path = workspace.join("impact_ops.json");
write_ops_json(&ops_path);
let stg = {
let out = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops_path.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
assert!(
!stg["dry_run_impact"].is_null(),
"expected dry_run_impact in stage response, got: {}",
stg
);
assert!(
stg["dry_run_impact"]["cells_changed"].as_u64().unwrap() >= 1,
"expected cells_changed >= 1, got: {}",
stg["dry_run_impact"]
);
}
#[test]
fn session_precondition_cell_match_blocks_apply() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let base_path = workspace.join("precond_base.xlsx");
write_fixture(&base_path);
let ws_str = workspace.to_str().unwrap();
let base_str = base_path.to_str().unwrap();
let start_json = {
let out = run_cli(&[
"session",
"start",
"--base",
base_str,
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let session_id = start_json["session_id"].as_str().unwrap();
let ops_path = workspace.join("precond_ops.json");
let payload = serde_json::json!({
"kind": "transform.write_matrix",
"sheet_name": "Sheet1",
"anchor": "A2",
"rows": [[{"v": "Eve"}]],
"overwrite_formulas": false,
});
std::fs::write(&ops_path, serde_json::to_string_pretty(&payload).unwrap()).unwrap();
let stg = {
let out = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops_path.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let stg_id = stg["staged_id"].as_str().unwrap();
let staged_path = workspace
.join(".asp/sessions")
.join(session_id)
.join("staged")
.join(format!("{}.json", stg_id));
let staged_content: Value =
serde_json::from_str(&std::fs::read_to_string(&staged_path).unwrap()).unwrap();
let mut modified = staged_content.clone();
modified["preconditions"] = serde_json::json!({
"cell_matches": [
{"address": "Sheet1!A1", "value": "WRONG_VALUE"}
]
});
std::fs::write(
&staged_path,
serde_json::to_string_pretty(&modified).unwrap(),
)
.unwrap();
let apply = run_cli(&[
"session",
"apply",
"--session",
session_id,
stg_id,
"--workspace",
ws_str,
]);
assert!(
!apply.status.success(),
"expected precondition failure but command succeeded"
);
let stderr = String::from_utf8_lossy(&apply.stderr);
assert!(
stderr.contains("precondition") || stderr.contains("cell_match"),
"expected precondition error, got stderr: {}",
stderr
);
}
#[test]
fn session_precondition_cell_match_passes() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let base_path = workspace.join("precond_pass_base.xlsx");
write_fixture(&base_path);
let ws_str = workspace.to_str().unwrap();
let base_str = base_path.to_str().unwrap();
let start_json = {
let out = run_cli(&[
"session",
"start",
"--base",
base_str,
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let session_id = start_json["session_id"].as_str().unwrap();
let ops_path = workspace.join("precond_pass_ops.json");
let payload = serde_json::json!({
"kind": "transform.write_matrix",
"sheet_name": "Sheet1",
"anchor": "A2",
"rows": [[{"v": "Eve"}]],
"overwrite_formulas": false,
});
std::fs::write(&ops_path, serde_json::to_string_pretty(&payload).unwrap()).unwrap();
let stg = {
let out = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops_path.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let stg_id = stg["staged_id"].as_str().unwrap();
let staged_path = workspace
.join(".asp/sessions")
.join(session_id)
.join("staged")
.join(format!("{}.json", stg_id));
let staged_content: Value =
serde_json::from_str(&std::fs::read_to_string(&staged_path).unwrap()).unwrap();
let mut modified = staged_content.clone();
modified["preconditions"] = serde_json::json!({
"cell_matches": [
{"address": "Sheet1!A1", "value": "Name"}
]
});
std::fs::write(
&staged_path,
serde_json::to_string_pretty(&modified).unwrap(),
)
.unwrap();
let apply = run_cli(&[
"session",
"apply",
"--session",
session_id,
stg_id,
"--workspace",
ws_str,
]);
assert_success(&apply);
let apply_json = parse_stdout_json(&apply);
assert_eq!(apply_json["applied"], true);
}
#[test]
fn session_mixed_ops_replay() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let base_path = workspace.join("mixed_base.xlsx");
write_fixture(&base_path);
let ws_str = workspace.to_str().unwrap();
let base_str = base_path.to_str().unwrap();
let start_json = {
let out = run_cli(&[
"session",
"start",
"--base",
base_str,
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let session_id = start_json["session_id"].as_str().unwrap();
let ops1 = workspace.join("mixed_ops1.json");
write_ops_json(&ops1);
let stg1 = {
let out = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops1.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let apply1 = run_cli(&[
"session",
"apply",
"--session",
session_id,
stg1["staged_id"].as_str().unwrap(),
"--workspace",
ws_str,
]);
assert_success(&apply1);
let ops2 = workspace.join("mixed_ops2.json");
let payload2 = serde_json::json!({
"kind": "structure.insert_rows",
"ops": [{
"kind": "insert_rows",
"sheet_name": "Sheet1",
"at_row": 2,
"count": 1
}]
});
std::fs::write(&ops2, serde_json::to_string_pretty(&payload2).unwrap()).unwrap();
let stg2 = {
let out = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops2.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let apply2 = run_cli(&[
"session",
"apply",
"--session",
session_id,
stg2["staged_id"].as_str().unwrap(),
"--workspace",
ws_str,
]);
assert_success(&apply2);
let ops3 = workspace.join("mixed_ops3.json");
let payload3 = serde_json::json!({
"kind": "style.apply",
"ops": [{
"sheet_name": "Sheet1",
"target": {"kind": "range", "range": "A1:A1"},
"patch": {"font": {"bold": true}}
}]
});
std::fs::write(&ops3, serde_json::to_string_pretty(&payload3).unwrap()).unwrap();
let stg3 = {
let out = run_cli(&[
"session",
"op",
"--session",
session_id,
"--ops",
&format!("@{}", ops3.display()),
"--workspace",
ws_str,
]);
assert_success(&out);
parse_stdout_json(&out)
};
let apply3 = run_cli(&[
"session",
"apply",
"--session",
session_id,
stg3["staged_id"].as_str().unwrap(),
"--workspace",
ws_str,
]);
assert_success(&apply3);
let output_path = workspace.join("mixed_result.xlsx");
let mat = run_cli(&[
"session",
"materialize",
"--session",
session_id,
"--output",
output_path.to_str().unwrap(),
"--workspace",
ws_str,
]);
assert_success(&mat);
let mat_json = parse_stdout_json(&mat);
assert_eq!(mat_json["events_replayed"], 3);
let read = run_cli(&[
"range-values",
output_path.to_str().unwrap(),
"Sheet1",
"A3:A3",
]);
assert_success(&read);
let read_str = serde_json::to_string(&parse_stdout_json(&read)).unwrap();
assert!(
read_str.contains("Eve"),
"expected Eve at row 3 after write + insert, got: {}",
read_str
);
}