mod test_helpers;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
struct TestEnv {
temp_dir: TempDir,
binary_path: PathBuf,
venv_path_env: Option<String>,
venv_root: Option<PathBuf>,
}
impl TestEnv {
fn new() -> Self {
let venv_root = test_helpers::setup_execution_venv();
let venv_path_env = test_helpers::setup_venv_environment();
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let binary_path = env!("CARGO_BIN_EXE_nb").into();
Self {
temp_dir,
binary_path,
venv_path_env,
venv_root,
}
}
fn notebook_path(&self, name: &str) -> PathBuf {
self.temp_dir.path().join(name)
}
fn copy_fixture(&self, fixture_name: &str, dest_name: &str) -> PathBuf {
let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join(fixture_name);
let dest_path = self.notebook_path(dest_name);
fs::copy(&fixture_path, &dest_path)
.unwrap_or_else(|_| panic!("Failed to copy fixture {}", fixture_name));
dest_path
}
fn run(&self, args: &[&str]) -> CommandResult {
let mut cmd = Command::new(&self.binary_path);
cmd.args(args)
.current_dir(self.temp_dir.path())
.env("TMPDIR", self.temp_dir.path());
if let Some(path_env) = &self.venv_path_env {
cmd.env("PATH", path_env);
}
if let Some(venv_root) = &self.venv_root {
cmd.env("VIRTUAL_ENV", venv_root);
}
cmd.env_remove("PYTHONHOME");
let output = cmd.output().expect("Failed to execute command");
CommandResult {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
success: output.status.success(),
}
}
}
struct CommandResult {
stdout: String,
stderr: String,
success: bool,
}
impl CommandResult {
fn assert_success(self) -> Self {
if !self.success {
panic!(
"Command failed:\nStderr: {}\nStdout: {}",
self.stderr, self.stdout
);
}
self
}
fn assert_failure(self) -> Self {
if self.success {
panic!(
"Expected command to fail but it succeeded:\nStdout: {}\nStderr: {}",
self.stdout, self.stderr
);
}
self
}
fn json_value(&self) -> serde_json::Value {
serde_json::from_str(&self.stdout).expect("Failed to parse JSON output")
}
}
fn join_source(source: &serde_json::Value) -> String {
source
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap_or(""))
.collect::<Vec<_>>()
.join("")
}
#[test]
fn test_create_default_notebook() {
let env = TestEnv::new();
let nb_path = env.notebook_path("test.ipynb");
let result = env
.run(&["create", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let json = result.json_value();
assert_eq!(json["kernel"], "python3");
assert_eq!(json["cell_count"], 1);
assert!(nb_path.exists());
let read_result = env
.run(&["read", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let read_json = read_result.json_value();
assert_eq!(read_json["cells"][0]["cell_type"], "code");
}
#[test]
fn test_create_with_markdown_flag() {
let env = TestEnv::new();
let nb_path = env.notebook_path("markdown.ipynb");
let result = env
.run(&["create", nb_path.to_str().unwrap(), "--markdown", "--json"])
.assert_success();
let json = result.json_value();
assert_eq!(json["cell_count"], 1);
let read_result = env
.run(&["read", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let read_json = read_result.json_value();
assert_eq!(read_json["cells"][0]["cell_type"], "markdown");
}
#[test]
fn test_create_with_custom_kernel() {
let env = TestEnv::new();
let nb_path = env.notebook_path("custom.ipynb");
let result = env
.run(&[
"create",
nb_path.to_str().unwrap(),
"--kernel",
"python3",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["kernel"], "python3");
}
#[test]
fn test_create_without_ipynb_extension() {
let env = TestEnv::new();
let nb_path = env.notebook_path("test");
env.run(&["create", nb_path.to_str().unwrap()])
.assert_success();
assert!(env.notebook_path("test.ipynb").exists());
}
#[test]
fn test_create_fails_if_exists() {
let env = TestEnv::new();
env.copy_fixture("basic.ipynb", "exists.ipynb");
let nb_path = env.notebook_path("exists.ipynb");
env.run(&["create", nb_path.to_str().unwrap()])
.assert_failure();
}
#[test]
fn test_create_with_force_overwrites() {
let env = TestEnv::new();
env.copy_fixture("with_code.ipynb", "overwrite.ipynb");
let nb_path = env.notebook_path("overwrite.ipynb");
let result = env
.run(&["create", nb_path.to_str().unwrap(), "--force", "--json"])
.assert_success();
let json = result.json_value();
assert_eq!(json["cell_count"], 1); }
#[test]
fn test_create_text_format() {
let env = TestEnv::new();
let nb_path = env.notebook_path("test.ipynb");
let result = env
.run(&["create", nb_path.to_str().unwrap()])
.assert_success();
assert!(result.stdout.contains("Created notebook:"));
assert!(result.stdout.contains("Kernel:"));
assert!(result.stdout.contains("Cells:"));
}
#[test]
fn test_read_empty_notebook() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&["read", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells"].as_array().unwrap().len(), 0);
}
#[test]
fn test_read_notebook_with_cells() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&["read", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells"].as_array().unwrap().len(), 2);
}
#[test]
fn test_read_specific_cell_by_index() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&[
"read",
nb_path.to_str().unwrap(),
"--cell-index",
"1",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cell_type"], "code");
assert!(json["source"]
.as_array()
.unwrap()
.iter()
.any(|s| s.as_str().unwrap().contains("print")));
}
#[test]
fn test_read_last_cell_negative_index() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&[
"read",
nb_path.to_str().unwrap(),
"--cell-index",
"-1",
"--json",
])
.assert_success();
let json = result.json_value();
assert!(json["source"]
.as_array()
.unwrap()
.iter()
.any(|s| s.as_str().unwrap().contains("print")));
}
#[test]
fn test_read_cell_by_id() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&[
"read",
nb_path.to_str().unwrap(),
"--cell",
"cell-1",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["id"], "cell-1");
}
#[test]
fn test_read_with_outputs() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_outputs.ipynb", "test.ipynb");
let result = env
.run(&["read", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let json = result.json_value();
let cells = json["cells"].as_array().unwrap();
assert!(cells[0]["outputs"].as_array().unwrap().len() > 0);
}
#[test]
fn test_read_only_code() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("mixed_cells.ipynb", "test.ipynb");
let result = env
.run(&["read", nb_path.to_str().unwrap(), "--only-code", "--json"])
.assert_success();
let json = result.json_value();
let cells = json["cells"].as_array().unwrap();
assert_eq!(cells.len(), 2); for cell in cells {
assert_eq!(cell["cell_type"], "code");
}
}
#[test]
fn test_read_only_markdown() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("mixed_cells.ipynb", "test.ipynb");
let result = env
.run(&[
"read",
nb_path.to_str().unwrap(),
"--only-markdown",
"--json",
])
.assert_success();
let json = result.json_value();
let cells = json["cells"].as_array().unwrap();
assert_eq!(cells.len(), 2); for cell in cells {
assert_eq!(cell["cell_type"], "markdown");
}
}
#[test]
fn test_read_only_code_and_only_markdown_conflict() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("mixed_cells.ipynb", "test.ipynb");
let result = env.run(&[
"read",
nb_path.to_str().unwrap(),
"--only-code",
"--only-markdown",
]);
assert!(!result.success);
}
#[test]
fn test_read_markdown_format() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&["read", nb_path.to_str().unwrap()])
.assert_success();
let header =
test_helpers::parse_notebook_header(&result.stdout).expect("Should have @@notebook header");
assert_eq!(header.get_str("format"), Some("ai-notebook"));
let cells = test_helpers::parse_cells(&result.stdout);
assert_eq!(cells.len(), 2);
assert_eq!(cells[0].get_str("cell_type"), Some("code"));
assert_eq!(cells[0].get_str("id"), Some("cell-1"));
assert_eq!(cells[0].get_i64("index"), Some(0));
assert_eq!(cells[1].get_str("cell_type"), Some("code"));
assert_eq!(cells[1].get_str("id"), Some("cell-2"));
assert_eq!(cells[1].get_i64("index"), Some(1));
assert!(result.stdout.contains("x = 1 + 1"));
assert!(result.stdout.contains("print"));
}
#[test]
fn test_read_markdown_format_with_outputs() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_outputs.ipynb", "test.ipynb");
let result = env
.run(&["read", nb_path.to_str().unwrap()])
.assert_success();
let header =
test_helpers::parse_notebook_header(&result.stdout).expect("Should have @@notebook header");
assert_eq!(header.get_str("format"), Some("ai-notebook"));
let cells = test_helpers::parse_cells(&result.stdout);
assert!(!cells.is_empty());
let outputs = test_helpers::parse_outputs(&result.stdout);
assert!(!outputs.is_empty(), "Outputs should be included by default");
let first_output = &outputs[0];
assert!(
first_output.get_str("output_type").is_some(),
"Output should have output_type"
);
}
#[test]
fn test_read_markdown_format_no_output() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_outputs.ipynb", "test.ipynb");
let result = env
.run(&["read", nb_path.to_str().unwrap(), "--no-output"])
.assert_success();
let cells = test_helpers::parse_cells(&result.stdout);
assert!(!cells.is_empty());
let outputs = test_helpers::parse_outputs(&result.stdout);
assert!(
outputs.is_empty(),
"Outputs should be excluded with --no-output"
);
}
#[test]
fn test_read_markdown_format_only_code() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("mixed_cells.ipynb", "test.ipynb");
let result = env
.run(&["read", nb_path.to_str().unwrap(), "--only-code"])
.assert_success();
let cells = test_helpers::parse_cells(&result.stdout);
assert_eq!(cells.len(), 2);
for cell in &cells {
assert_eq!(cell.get_str("cell_type"), Some("code"));
}
}
#[test]
fn test_read_markdown_format_only_markdown() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("mixed_cells.ipynb", "test.ipynb");
let result = env
.run(&["read", nb_path.to_str().unwrap(), "--only-markdown"])
.assert_success();
let cells = test_helpers::parse_cells(&result.stdout);
assert_eq!(cells.len(), 2);
for cell in &cells {
assert_eq!(cell.get_str("cell_type"), Some("markdown"));
}
}
#[test]
fn test_read_markdown_format_single_cell() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&["read", nb_path.to_str().unwrap(), "--cell-index", "0"])
.assert_success();
let cells = test_helpers::parse_cells(&result.stdout);
assert_eq!(cells.len(), 1);
assert_eq!(cells[0].get_str("cell_type"), Some("code"));
assert_eq!(cells[0].get_str("id"), Some("cell-1"));
}
#[test]
fn test_read_markdown_format_cell_by_id() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&["read", nb_path.to_str().unwrap(), "--cell", "cell-2"])
.assert_success();
let cells = test_helpers::parse_cells(&result.stdout);
assert_eq!(cells.len(), 1);
assert_eq!(cells[0].get_str("id"), Some("cell-2"));
}
#[test]
fn test_read_markdown_format_empty_notebook() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&["read", nb_path.to_str().unwrap()])
.assert_success();
let header =
test_helpers::parse_notebook_header(&result.stdout).expect("Should have @@notebook header");
assert_eq!(header.get_str("format"), Some("ai-notebook"));
let cells = test_helpers::parse_cells(&result.stdout);
assert_eq!(cells.len(), 0);
}
#[test]
fn test_search_markdown_format() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("mixed_cells.ipynb", "test.ipynb");
let result = env
.run(&["search", nb_path.to_str().unwrap(), "import"])
.assert_success();
let header = test_helpers::parse_notebook_header(&result.stdout)
.expect("Search should output @@notebook header");
assert_eq!(header.get_str("format"), Some("ai-notebook"));
let cells = test_helpers::parse_cells(&result.stdout);
assert!(!cells.is_empty(), "Should find matching cells");
for cell in &cells {
assert_eq!(cell.get_str("cell_type"), Some("code"));
}
assert!(result.stdout.contains("# Found"));
}
#[test]
fn test_read_nonexistent_notebook_fails() {
let env = TestEnv::new();
let nb_path = env.notebook_path("nonexistent.ipynb");
env.run(&["read", nb_path.to_str().unwrap()])
.assert_failure();
}
#[test]
fn test_add_code_cell() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"--source",
"x = 1 + 1",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cell_type"], "code");
assert_eq!(json["index"], 0);
assert_eq!(json["total_cells"], 1);
}
#[test]
fn test_add_markdown_cell() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"--type",
"markdown",
"--source",
"# Hello World",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cell_type"], "markdown");
}
#[test]
fn test_add_raw_cell() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"--type",
"raw",
"--source",
"Raw content",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cell_type"], "raw");
}
#[test]
fn test_add_cell_with_multiline_source() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
env.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"--source",
"def hello():\n print('world')\n\nhello()",
"--json",
])
.assert_success();
let result = env
.run(&["read", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let json = result.json_value();
let cells = json["cells"].as_array().unwrap();
assert_eq!(cells.len(), 1);
}
#[test]
fn test_add_cell_at_beginning() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"--source",
"inserted at start",
"--insert-at",
"0",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["index"], 0);
assert_eq!(json["total_cells"], 3);
}
#[test]
fn test_add_cell_at_end() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"--source",
"appended",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["index"], 2);
}
#[test]
fn test_add_cell_with_negative_index() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"--source",
"before last",
"--insert-at",
"-1",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["index"], 1); }
#[test]
fn test_add_cell_after_cell_id() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"--source",
"after cell-1",
"--after",
"cell-1",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["index"], 1);
}
#[test]
fn test_add_cell_before_cell_id() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"--source",
"before cell-2",
"--before",
"cell-2",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["index"], 1);
}
#[test]
fn test_add_cell_with_custom_id() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"--source",
"test",
"--id",
"my-custom-id",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cell_id"], "my-custom-id");
}
#[test]
fn test_add_cell_duplicate_id_fails() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
env.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"--source",
"duplicate",
"--id",
"cell-1",
])
.assert_failure();
}
#[test]
fn test_add_cell_empty_source() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&["cell", "add", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let json = result.json_value();
assert_eq!(json["index"], 0);
}
#[test]
fn test_add_consecutive_cells_correct_count() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result1 = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"--source",
"a = 10",
"--json",
])
.assert_success();
let json1 = result1.json_value();
assert_eq!(json1["index"], 0);
assert_eq!(json1["total_cells"], 1);
let result2 = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"--source",
"b = 20",
"--json",
])
.assert_success();
let json2 = result2.json_value();
assert_eq!(json2["index"], 1);
assert_eq!(json2["total_cells"], 2);
let result3 = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"--source",
"c = 30",
"--json",
])
.assert_success();
let json3 = result3.json_value();
assert_eq!(json3["index"], 2);
assert_eq!(json3["total_cells"], 3);
}
#[test]
fn test_add_multiple_cells_with_sentinels() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"-s",
"@@code\nx = 1\n@@code\ny = 2\n@@code\nz = 3",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells_added"], 3);
assert_eq!(json["total_cells"], 3);
let cells = json["cells"].as_array().unwrap();
assert_eq!(cells.len(), 3);
assert_eq!(cells[0]["index"], 0);
assert_eq!(cells[1]["index"], 1);
assert_eq!(cells[2]["index"], 2);
assert_eq!(cells[0]["cell_type"], "code");
}
#[test]
fn test_add_multiple_cells_mixed_types() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"-s",
"@@markdown\n# Title\n@@code\nx = 1\n@@raw\nraw stuff",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells_added"], 3);
let cells = json["cells"].as_array().unwrap();
assert_eq!(cells[0]["cell_type"], "markdown");
assert_eq!(cells[1]["cell_type"], "code");
assert_eq!(cells[2]["cell_type"], "raw");
let read_result = env
.run(&["read", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let nb_json = read_result.json_value();
let nb_cells = nb_json["cells"].as_array().unwrap();
assert_eq!(join_source(&nb_cells[0]["source"]), "# Title");
assert_eq!(join_source(&nb_cells[1]["source"]), "x = 1");
assert_eq!(join_source(&nb_cells[2]["source"]), "raw stuff");
}
#[test]
fn test_add_multiple_cells_at_index() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"-s",
"@@code\ninserted_1\n@@code\ninserted_2",
"--insert-at",
"1",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells_added"], 2);
assert_eq!(json["total_cells"], 4);
let cells = json["cells"].as_array().unwrap();
assert_eq!(cells[0]["index"], 1);
assert_eq!(cells[1]["index"], 2);
let read_result = env
.run(&["read", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let nb_json = read_result.json_value();
let nb_cells = nb_json["cells"].as_array().unwrap();
assert_eq!(nb_cells.len(), 4);
assert_eq!(join_source(&nb_cells[1]["source"]), "inserted_1");
assert_eq!(join_source(&nb_cells[2]["source"]), "inserted_2");
}
#[test]
fn test_add_multiple_cells_after_cell_id() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"-s",
"@@code\nafter_1\n@@markdown\nafter_2",
"--after",
"cell-1",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells_added"], 2);
let cells = json["cells"].as_array().unwrap();
assert_eq!(cells[0]["index"], 1);
assert_eq!(cells[1]["index"], 2);
let read_result = env
.run(&["read", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let nb_json = read_result.json_value();
let nb_cells = nb_json["cells"].as_array().unwrap();
assert_eq!(join_source(&nb_cells[1]["source"]), "after_1");
assert_eq!(join_source(&nb_cells[2]["source"]), "after_2");
}
#[test]
fn test_add_multiple_cells_id_flag_rejected() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
env.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"-s",
"@@code\na\n@@code\nb",
"--id",
"my-id",
])
.assert_failure();
}
#[test]
fn test_add_multiple_cells_unique_ids() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"-s",
"@@code\ncell_a\n@@code\ncell_b\n@@code\ncell_c",
"--json",
])
.assert_success();
let json = result.json_value();
let cells = json["cells"].as_array().unwrap();
let ids: Vec<&str> = cells
.iter()
.map(|c| c["cell_id"].as_str().unwrap())
.collect();
let unique_ids: std::collections::HashSet<&str> = ids.iter().cloned().collect();
assert_eq!(ids.len(), unique_ids.len(), "Cell IDs must be unique");
}
#[test]
fn test_add_single_cell_backward_compat() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"-s",
"hello",
"--json",
])
.assert_success();
let json = result.json_value();
assert!(json["cell_id"].is_string());
assert!(json["cell_type"].is_string());
assert!(json["index"].is_number());
assert!(json["total_cells"].is_number());
assert!(json["cells_added"].is_null());
assert!(json["cells"].is_null());
}
#[test]
fn test_add_no_source_backward_compat() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&["cell", "add", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let json = result.json_value();
assert_eq!(json["index"], 0);
assert_eq!(json["total_cells"], 1);
assert!(json["cell_id"].is_string());
}
#[test]
fn test_add_sentinel_multiline_source() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"-s",
"@@code\ndef hello():\n print('world')\n\nhello()\n@@markdown\n# Notes\nThis is a note.",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells_added"], 2);
let read_result = env
.run(&["read", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let nb_json = read_result.json_value();
let nb_cells = nb_json["cells"].as_array().unwrap();
assert_eq!(
join_source(&nb_cells[0]["source"]),
"def hello():\n print('world')\n\nhello()"
);
assert_eq!(
join_source(&nb_cells[1]["source"]),
"# Notes\nThis is a note."
);
}
#[test]
fn test_add_single_sentinel_still_uses_type() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"-s",
"@@markdown\n# Just one cell",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cell_type"], "markdown");
assert!(json["cell_id"].is_string());
}
#[test]
fn test_add_cells_with_cell_json_format() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"-s",
"@@cell {\"cell_type\": \"code\"}\nx = 1\n@@cell {\"cell_type\": \"markdown\"}\n# Title",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells_added"], 2);
let cells = json["cells"].as_array().unwrap();
assert_eq!(cells[0]["cell_type"], "code");
assert_eq!(cells[1]["cell_type"], "markdown");
let read_result = env
.run(&["read", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let nb_json = read_result.json_value();
let nb_cells = nb_json["cells"].as_array().unwrap();
assert_eq!(join_source(&nb_cells[0]["source"]), "x = 1");
assert_eq!(join_source(&nb_cells[1]["source"]), "# Title");
}
#[test]
fn test_add_cell_content_with_sentinel_literal_not_lost() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"-s",
"Use @@code to start a code cell",
"-t",
"markdown",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cell_type"], "markdown");
assert!(json["cell_id"].is_string());
let read_result = env
.run(&["read", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let nb_json = read_result.json_value();
let nb_cells = nb_json["cells"].as_array().unwrap();
assert_eq!(
join_source(&nb_cells[0]["source"]),
"Use @@code to start a code cell"
);
}
#[test]
fn test_add_cell_with_metadata_from_sentinel_json() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"-s",
r#"@@cell {"cell_type": "code", "metadata": {"tags": ["setup", "hidden"]}}
x = 1
@@cell {"cell_type": "markdown", "metadata": {"editable": false}}
# Read-only title
@@code
plain cell"#,
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells_added"], 3);
let read_result = env
.run(&["read", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let nb_json = read_result.json_value();
let nb_cells = nb_json["cells"].as_array().unwrap();
let meta0 = &nb_cells[0]["metadata"];
let tags: Vec<&str> = meta0["tags"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert_eq!(tags, vec!["setup", "hidden"]);
let meta1 = &nb_cells[1]["metadata"];
assert_eq!(meta1["editable"], false);
let meta2 = &nb_cells[2]["metadata"];
assert!(meta2.is_object());
assert!(
meta2.as_object().unwrap().is_empty() || !meta2.as_object().unwrap().contains_key("tags")
);
}
#[test]
fn test_update_cell_source() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
env.run(&[
"cell",
"update",
nb_path.to_str().unwrap(),
"--cell-index",
"0",
"--source",
"y = 2 + 2",
"--json",
])
.assert_success();
let result = env
.run(&[
"read",
nb_path.to_str().unwrap(),
"--cell-index",
"0",
"--json",
])
.assert_success();
let json = result.json_value();
let source = join_source(&json["source"]);
assert!(source.contains("y = 2 + 2"));
}
#[test]
fn test_update_cell_append() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
env.run(&[
"cell",
"update",
nb_path.to_str().unwrap(),
"--cell-index",
"0",
"--append",
"\nprint('appended')",
"--json",
])
.assert_success();
let result = env
.run(&[
"read",
nb_path.to_str().unwrap(),
"--cell-index",
"0",
"--json",
])
.assert_success();
let json = result.json_value();
let source = join_source(&json["source"]);
assert!(source.contains("x = 1 + 1"));
assert!(source.contains("print('appended')"));
}
#[test]
fn test_update_cell_by_id() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
env.run(&[
"cell",
"update",
nb_path.to_str().unwrap(),
"--cell",
"cell-1",
"--source",
"updated via id",
])
.assert_success();
let result = env
.run(&[
"read",
nb_path.to_str().unwrap(),
"--cell",
"cell-1",
"--json",
])
.assert_success();
let json = result.json_value();
let source = join_source(&json["source"]);
assert!(source.contains("updated via id"));
}
#[test]
fn test_update_cell_type() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
env.run(&[
"cell",
"update",
nb_path.to_str().unwrap(),
"--cell-index",
"0",
"--type",
"markdown",
"--json",
])
.assert_success();
let result = env
.run(&[
"read",
nb_path.to_str().unwrap(),
"--cell-index",
"0",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cell_type"], "markdown");
}
#[test]
fn test_update_cell_negative_index() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
env.run(&[
"cell",
"update",
nb_path.to_str().unwrap(),
"--cell-index",
"-1",
"--source",
"updated last cell",
"--json",
])
.assert_success();
let result = env
.run(&[
"read",
nb_path.to_str().unwrap(),
"--cell-index",
"-1",
"--json",
])
.assert_success();
let json = result.json_value();
let source = join_source(&json["source"]);
assert!(source.contains("updated last cell"));
}
#[test]
fn test_delete_cell_by_index() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"delete",
nb_path.to_str().unwrap(),
"--cell-index",
"0",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells_deleted"], 1);
assert_eq!(json["remaining_cells"], 1);
}
#[test]
fn test_delete_cell_by_id() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"delete",
nb_path.to_str().unwrap(),
"--cell",
"cell-1",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells_deleted"], 1);
assert_eq!(json["remaining_cells"], 1);
}
#[test]
fn test_delete_multiple_cells() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("mixed_cells.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"delete",
nb_path.to_str().unwrap(),
"--cell-index",
"0",
"--cell-index",
"2",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells_deleted"], 2);
assert_eq!(json["remaining_cells"], 3);
}
#[test]
fn test_delete_with_negative_index() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&[
"cell",
"delete",
nb_path.to_str().unwrap(),
"--cell-index",
"-1",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["remaining_cells"], 1);
}
#[test]
fn test_delete_all_cells_fails() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
env.run(&[
"cell",
"delete",
nb_path.to_str().unwrap(),
"--cell-index",
"0",
"--cell-index",
"1",
])
.assert_failure();
}
#[test]
fn test_search_finds_pattern() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("mixed_cells.ipynb", "test.ipynb");
let result = env
.run(&["search", nb_path.to_str().unwrap(), "import", "--json"])
.assert_success();
let json = result.json_value();
assert!(json["results"].as_array().unwrap().len() > 0);
assert!(json["total_matches"].as_u64().unwrap() > 0);
}
#[test]
fn test_search_case_insensitive() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("mixed_cells.ipynb", "test.ipynb");
let result = env
.run(&[
"search",
nb_path.to_str().unwrap(),
"PANDAS",
"-i",
"--json",
])
.assert_success();
let json = result.json_value();
assert!(json["results"].as_array().unwrap().len() > 0);
}
#[test]
fn test_search_no_matches() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&[
"search",
nb_path.to_str().unwrap(),
"nonexistent_pattern",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["results"].as_array().unwrap().len(), 0);
assert_eq!(json["total_matches"], 0);
}
#[test]
fn test_search_multiple_matches() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("mixed_cells.ipynb", "test.ipynb");
let result = env
.run(&["search", nb_path.to_str().unwrap(), "import", "--json"])
.assert_success();
let json = result.json_value();
assert!(json["results"].as_array().unwrap().len() > 0);
assert_eq!(json["total_matches"], 2);
}
#[test]
fn test_clear_outputs() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_outputs.ipynb", "test.ipynb");
let result = env
.run(&["output", "clear", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells_cleared"], 2);
let read_result = env
.run(&["read", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let read_json = read_result.json_value();
let cells = read_json["cells"].as_array().unwrap();
for cell in cells {
if cell["cell_type"] == "code" {
assert_eq!(cell["outputs"].as_array().unwrap().len(), 0);
}
}
}
#[test]
fn test_clear_outputs_empty_notebook() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("empty.ipynb", "test.ipynb");
let result = env
.run(&["output", "clear", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells_cleared"], 0);
}
#[test]
fn test_clear_outputs_specific_cell() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_outputs.ipynb", "test.ipynb");
let result = env
.run(&[
"output",
"clear",
nb_path.to_str().unwrap(),
"--cell-index",
"0",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells_cleared"], 1);
}
#[test]
fn test_clear_outputs_negative_index() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_outputs.ipynb", "test.ipynb");
let result = env
.run(&[
"output",
"clear",
nb_path.to_str().unwrap(),
"--cell-index",
"-1",
"--json",
])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells_cleared"], 1);
}
#[test]
fn test_workflow_create_add_read() {
let env = TestEnv::new();
let nb_path = env.notebook_path("workflow.ipynb");
env.run(&["create", nb_path.to_str().unwrap()])
.assert_success();
env.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"--type",
"markdown",
"--source",
"# Workflow Test",
])
.assert_success();
env.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"--source",
"x = 42",
"--json",
])
.assert_success();
let result = env
.run(&["read", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells"].as_array().unwrap().len(), 3);
}
#[test]
fn test_workflow_modify_and_verify() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
env.run(&[
"cell",
"update",
nb_path.to_str().unwrap(),
"--cell-index",
"0",
"--source",
"modified = True",
])
.assert_success();
env.run(&[
"cell",
"delete",
nb_path.to_str().unwrap(),
"--cell-index",
"1",
])
.assert_success();
env.run(&[
"cell",
"add",
nb_path.to_str().unwrap(),
"--source",
"new_cell = 123",
"--json",
])
.assert_success();
let result = env
.run(&["read", nb_path.to_str().unwrap(), "--json"])
.assert_success();
let json = result.json_value();
assert_eq!(json["cells"].as_array().unwrap().len(), 2);
}
#[test]
fn test_read_only_code_preserves_original_indices() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("mixed_cells.ipynb", "test.ipynb");
let result = env
.run(&["read", nb_path.to_str().unwrap(), "--only-code"])
.assert_success();
let cells = test_helpers::parse_cells(&result.stdout);
assert_eq!(cells.len(), 2);
assert_eq!(cells[0].get_i64("index"), Some(1));
assert_eq!(cells[1].get_i64("index"), Some(3));
}
#[test]
fn test_read_only_markdown_preserves_original_indices() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("mixed_cells.ipynb", "test.ipynb");
let result = env
.run(&["read", nb_path.to_str().unwrap(), "--only-markdown"])
.assert_success();
let cells = test_helpers::parse_cells(&result.stdout);
assert_eq!(cells.len(), 2);
assert_eq!(cells[0].get_i64("index"), Some(0));
assert_eq!(cells[1].get_i64("index"), Some(2));
}
#[test]
fn test_read_single_cell_preserves_original_index() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env
.run(&["read", nb_path.to_str().unwrap(), "--cell-index", "1"])
.assert_success();
let cells = test_helpers::parse_cells(&result.stdout);
assert_eq!(cells.len(), 1);
assert_eq!(cells[0].get_i64("index"), Some(1));
}
#[test]
fn test_read_with_output_dir_externalizes_large_output() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_rich_outputs.ipynb", "test.ipynb");
let output_dir = env.temp_dir.path().join("outputs");
let result = env
.run(&[
"read",
nb_path.to_str().unwrap(),
"--output-dir",
output_dir.to_str().unwrap(),
"--limit",
"100", ])
.assert_success();
let outputs = test_helpers::parse_outputs(&result.stdout);
assert!(!outputs.is_empty());
let stream_output = outputs
.iter()
.find(|o| o.get_str("output_type") == Some("stream"));
assert!(stream_output.is_some(), "Should have a stream output");
let path = stream_output.unwrap().get_str("path");
assert!(
path.is_some(),
"Large output should be externalized with a path"
);
let path_str = path.unwrap();
assert!(
std::path::Path::new(path_str).exists(),
"Externalized file should exist at: {}",
path_str
);
}
#[test]
fn test_read_inline_limit_controls_externalization() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_outputs.ipynb", "test.ipynb");
let output_dir = env.temp_dir.path().join("outputs");
let result = env
.run(&[
"read",
nb_path.to_str().unwrap(),
"--output-dir",
output_dir.to_str().unwrap(),
"--limit",
"100000",
])
.assert_success();
let outputs = test_helpers::parse_outputs(&result.stdout);
for o in &outputs {
assert!(
o.get_str("path").is_none(),
"Small outputs should stay inline with high --limit"
);
}
}
#[test]
fn test_read_binary_output_externalized() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_rich_outputs.ipynb", "test.ipynb");
let output_dir = env.temp_dir.path().join("outputs");
let result = env
.run(&[
"read",
nb_path.to_str().unwrap(),
"--output-dir",
output_dir.to_str().unwrap(),
])
.assert_success();
let outputs = test_helpers::parse_outputs(&result.stdout);
let image_output = outputs.iter().find(|o| {
o.get_str("mime")
.map(|m| m.starts_with("image/"))
.unwrap_or(false)
});
assert!(image_output.is_some(), "Should have an image output");
let path = image_output.unwrap().get_str("path");
assert!(
path.is_some(),
"Binary output should always be externalized"
);
assert!(
std::path::Path::new(path.unwrap()).exists(),
"Externalized image file should exist"
);
}
#[test]
fn test_read_error_output_markdown() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_rich_outputs.ipynb", "test.ipynb");
let result = env
.run(&["read", nb_path.to_str().unwrap()])
.assert_success();
let outputs = test_helpers::parse_outputs(&result.stdout);
let error_output = outputs
.iter()
.find(|o| o.get_str("output_type") == Some("error"));
assert!(error_output.is_some(), "Should have an error output");
assert_eq!(error_output.unwrap().get_str("ename"), Some("ValueError"));
assert_eq!(
error_output.unwrap().get_str("evalue"),
Some("invalid literal")
);
assert!(result.stdout.contains("Traceback"));
}
#[test]
fn test_output_clean_command() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_outputs.ipynb", "test.ipynb");
env.run(&["read", nb_path.to_str().unwrap()])
.assert_success();
let result = env.run(&["output", "clean", "--json"]).assert_success();
let json = result.json_value();
assert!(json["cleaned"].is_boolean());
}
#[test]
fn test_output_clean_when_empty() {
let env = TestEnv::new();
let result = env.run(&["output", "clean", "--json"]).assert_success();
let json = result.json_value();
assert!(json["cleaned"].is_boolean());
}
#[test]
fn test_read_default_output_dir_uses_nb_cli_prefix() {
let env = TestEnv::new();
let nb_path = env.copy_fixture("with_rich_outputs.ipynb", "test.ipynb");
let result = env
.run(&["read", nb_path.to_str().unwrap()])
.assert_success();
let outputs = test_helpers::parse_outputs(&result.stdout);
let image_output = outputs.iter().find(|o| {
o.get_str("mime")
.map(|m| m.starts_with("image/"))
.unwrap_or(false)
});
if let Some(img) = image_output {
if let Some(path) = img.get_str("path") {
assert!(
path.contains("nb-cli"),
"Default output dir should use nb-cli prefix, got: {}",
path
);
}
}
}
#[test]
fn test_read_without_extension() {
let env = TestEnv::new();
env.copy_fixture("basic.ipynb", "test.ipynb");
let result_with_ext = env.run(&["read", "test.ipynb", "--json"]).assert_success();
let result_without_ext = env.run(&["read", "test", "--json"]).assert_success();
let json_with = result_with_ext.json_value();
let json_without = result_without_ext.json_value();
assert_eq!(json_with["cell_count"], json_without["cell_count"]);
}
#[test]
fn test_create_without_extension() {
let env = TestEnv::new();
let result = env.run(&["create", "notebook", "--json"]).assert_success();
let json = result.json_value();
assert_eq!(json["file"], "notebook.ipynb");
assert!(env.notebook_path("notebook.ipynb").exists());
}
#[test]
fn test_cell_add_without_extension() {
let env = TestEnv::new();
env.copy_fixture("basic.ipynb", "test.ipynb");
let result = env
.run(&["cell", "add", "test", "-s", "print('hello')", "--json"])
.assert_success();
assert!(result.success);
let json = result.json_value();
assert_eq!(json["cell_type"], "code");
}
#[test]
fn test_cell_update_without_extension() {
let env = TestEnv::new();
env.copy_fixture("basic.ipynb", "test.ipynb");
let result = env
.run(&[
"cell", "update", "test", "-i", "0", "-s", "updated", "--json",
])
.assert_success();
assert!(result.success);
let json = result.json_value();
assert_eq!(json["index"], 0);
}
#[test]
fn test_cell_delete_without_extension() {
let env = TestEnv::new();
env.copy_fixture("mixed_cells.ipynb", "test.ipynb");
let result = env
.run(&["cell", "delete", "test", "-i", "0", "--json"])
.assert_success();
assert!(result.success);
let json = result.json_value();
assert_eq!(json["cells_deleted"], 1);
}
#[test]
fn test_search_without_extension() {
let env = TestEnv::new();
env.copy_fixture("with_code.ipynb", "test.ipynb");
let result = env.run(&["search", "test", "print"]).assert_success();
assert!(result.stdout.contains("match"));
}
#[test]
fn test_output_clear_without_extension() {
let env = TestEnv::new();
env.copy_fixture("with_outputs.ipynb", "test.ipynb");
let result = env
.run(&["output", "clear", "test", "--json"])
.assert_success();
assert!(result.success);
let json = result.json_value();
assert!(json["cells_cleared"].is_number());
}