use assert_cmd::Command;
use insta::assert_snapshot;
use predicates::str::contains;
use std::path::{Path, PathBuf};
use tempfile::TempDir;
const TS_FIXTURE: &str = "tests/fixtures/typescript-simple";
fn copy_dir_all(src: &Path, dest: &Path) {
std::fs::create_dir_all(dest).unwrap();
for entry in std::fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
let src_path = entry.path();
let dest_path = dest.join(entry.file_name());
if src_path.is_dir() {
copy_dir_all(&src_path, &dest_path);
} else {
std::fs::copy(&src_path, &dest_path).unwrap();
}
}
}
fn setup_indexed_fixture() -> (TempDir, PathBuf) {
let dir = TempDir::new().unwrap();
let fixture = Path::new(TS_FIXTURE);
copy_dir_all(fixture, dir.path());
Command::cargo_bin("scope")
.unwrap()
.arg("init")
.current_dir(dir.path())
.assert()
.success();
Command::cargo_bin("scope")
.unwrap()
.args(["index", "--full"])
.current_dir(dir.path())
.assert()
.success();
let root = dir.path().to_path_buf();
(dir, root)
}
#[test]
fn test_sketch_class_shows_name_and_kind() {
let (_dir, root) = setup_indexed_fixture();
Command::cargo_bin("scope")
.unwrap()
.args(["sketch", "PaymentService"])
.current_dir(&root)
.assert()
.success()
.stdout(contains("PaymentService"))
.stdout(contains("class"))
.stdout(contains("service.ts"));
}
#[test]
fn test_sketch_method_shows_signature() {
let (_dir, root) = setup_indexed_fixture();
Command::cargo_bin("scope")
.unwrap()
.args(["sketch", "PaymentService.processPayment"])
.current_dir(&root)
.assert()
.success()
.stdout(contains("method"))
.stdout(contains("processPayment"));
}
#[test]
fn test_sketch_qualified_method_lookup() {
let (_dir, root) = setup_indexed_fixture();
Command::cargo_bin("scope")
.unwrap()
.args(["sketch", "PaymentService.refundPayment"])
.current_dir(&root)
.assert()
.success()
.stdout(contains("refundPayment"));
}
#[test]
fn test_sketch_unknown_symbol_fails() {
let (_dir, root) = setup_indexed_fixture();
Command::cargo_bin("scope")
.unwrap()
.args(["sketch", "NonExistentThing"])
.current_dir(&root)
.assert()
.failure()
.stderr(contains("not found"));
}
#[test]
fn test_sketch_file_level() {
let (_dir, root) = setup_indexed_fixture();
Command::cargo_bin("scope")
.unwrap()
.args(["sketch", "src/payments/service.ts"])
.current_dir(&root)
.assert()
.success()
.stdout(contains("PaymentService"));
}
#[test]
fn test_sketch_interface() {
let (_dir, root) = setup_indexed_fixture();
Command::cargo_bin("scope")
.unwrap()
.args(["sketch", "PaymentRequest"])
.current_dir(&root)
.assert()
.success()
.stdout(contains("PaymentRequest"));
}
#[test]
fn test_sketch_json_output_is_valid() {
let (_dir, root) = setup_indexed_fixture();
let output = Command::cargo_bin("scope")
.unwrap()
.args(["sketch", "PaymentService", "--json"])
.current_dir(&root)
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value =
serde_json::from_slice(&output).expect("stdout should be valid JSON");
assert_eq!(
json["command"], "sketch",
"JSON envelope must have command=sketch"
);
assert!(
!json["data"].is_null(),
"JSON envelope must have a non-null data field"
);
}
#[test]
fn test_sketch_class_output_format() {
let (_dir, root) = setup_indexed_fixture();
let raw = Command::cargo_bin("scope")
.unwrap()
.args(["sketch", "PaymentService"])
.current_dir(&root)
.output()
.unwrap();
let stdout = String::from_utf8(raw.stdout).unwrap();
let normalized = normalize_paths(&stdout, &root);
assert_snapshot!("sketch_class_payment_service", normalized);
}
#[test]
fn test_sketch_method_output_format() {
let (_dir, root) = setup_indexed_fixture();
let raw = Command::cargo_bin("scope")
.unwrap()
.args(["sketch", "PaymentService.processPayment"])
.current_dir(&root)
.output()
.unwrap();
let stdout = String::from_utf8(raw.stdout).unwrap();
let normalized = normalize_paths(&stdout, &root);
assert_snapshot!("sketch_method_process_payment", normalized);
}
#[test]
fn test_sketch_file_output_format() {
let (_dir, root) = setup_indexed_fixture();
let raw = Command::cargo_bin("scope")
.unwrap()
.args(["sketch", "src/payments/service.ts"])
.current_dir(&root)
.output()
.unwrap();
let stdout = String::from_utf8(raw.stdout).unwrap();
let normalized = normalize_paths(&stdout, &root);
assert_snapshot!("sketch_file_service_ts", normalized);
}
#[test]
fn test_sketch_enum_shows_variants() {
let (_dir, root) = setup_indexed_fixture();
Command::cargo_bin("scope")
.unwrap()
.args(["sketch", "PaymentMethod"])
.current_dir(&root)
.assert()
.success()
.stdout(contains("enum"))
.stdout(contains("PaymentMethod"))
.stdout(contains("variants:"))
.stdout(contains("CreditCard"))
.stdout(contains("BankTransfer"))
.stdout(contains("Wallet"));
}
#[test]
fn test_sketch_enum_output_format() {
let (_dir, root) = setup_indexed_fixture();
let raw = Command::cargo_bin("scope")
.unwrap()
.args(["sketch", "PaymentMethod"])
.current_dir(&root)
.output()
.unwrap();
let stdout = String::from_utf8(raw.stdout).unwrap();
let normalized = normalize_paths(&stdout, &root);
assert_snapshot!("sketch_enum_payment_method", normalized);
}
#[test]
fn test_sketch_enum_json_includes_variants() {
let (_dir, root) = setup_indexed_fixture();
let output = Command::cargo_bin("scope")
.unwrap()
.args(["sketch", "PaymentMethod", "--json"])
.current_dir(&root)
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value =
serde_json::from_slice(&output).expect("stdout should be valid JSON");
assert_eq!(json["command"], "sketch");
let variants = json["data"]["variants"]
.as_array()
.expect("data.variants should be an array");
assert!(
variants.len() >= 3,
"PaymentMethod should have at least 3 variants"
);
let variant_names: Vec<&str> = variants.iter().filter_map(|v| v["name"].as_str()).collect();
assert!(variant_names.contains(&"CreditCard"));
assert!(variant_names.contains(&"BankTransfer"));
assert!(variant_names.contains(&"Wallet"));
}
#[test]
fn test_sketch_class_json_includes_fields() {
let (_dir, root) = setup_indexed_fixture();
let output = Command::cargo_bin("scope")
.unwrap()
.args(["sketch", "PaymentService", "--json"])
.current_dir(&root)
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value =
serde_json::from_slice(&output).expect("stdout should be valid JSON");
assert_eq!(json["command"], "sketch");
let fields = json["data"]["fields"]
.as_array()
.expect("data.fields should be an array");
assert!(
!fields.is_empty(),
"PaymentService should have at least one field"
);
let field_names: Vec<&str> = fields.iter().filter_map(|f| f["name"].as_str()).collect();
assert!(
field_names.contains(&"logger"),
"PaymentService should have a 'logger' field"
);
}
fn normalize_paths(output: &str, root: &Path) -> String {
let root_str = root.to_string_lossy();
let root_forward = root_str.replace('\\', "/");
let output_forward = output.replace('\\', "/");
output_forward.replace(&*root_forward, "<PROJECT_ROOT>")
}