#![allow(clippy::unwrap_used)]
use assert_cmd::Command;
use predicates::prelude::*;
use std::path::Path;
fn schema_cmd() -> Command {
Command::cargo_bin("schema").unwrap()
}
fn init_repo(dir: &Path) {
schema_cmd()
.args(["init", dir.to_str().unwrap()])
.current_dir(dir)
.assert()
.success();
}
fn write_schema(dir: &Path, name: &str, vertices: &[(&str, &str)]) {
let mut verts = serde_json::Map::new();
for (id, kind) in vertices {
verts.insert(
id.to_string(),
serde_json::json!({
"id": id, "kind": kind, "nsid": null
}),
);
}
let schema = serde_json::json!({
"protocol": "test",
"vertices": verts,
"edges": [],
"hyper_edges": {},
"constraints": {},
"required": {},
"nsids": {},
"variants": {},
"orderings": [],
"recursion_points": {},
"spans": {},
"usage_modes": [],
"nominal": {},
"outgoing": {},
"incoming": {},
"between": []
});
let path = dir.join(name);
std::fs::write(&path, serde_json::to_string_pretty(&schema).unwrap()).unwrap();
}
fn add_and_commit(dir: &Path, schema_file: &str, message: &str) {
schema_cmd()
.args(["add", schema_file])
.current_dir(dir)
.assert()
.success();
schema_cmd()
.args(["commit", "-m", message])
.current_dir(dir)
.assert()
.success();
}
fn stdout_of(cmd: &mut Command) -> String {
let output = cmd.output().unwrap();
String::from_utf8(output.stdout).unwrap()
}
#[test]
fn cli_init_success() {
let tmp = tempfile::tempdir().unwrap();
schema_cmd()
.args(["init", tmp.path().to_str().unwrap()])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Initialized"));
}
#[test]
fn cli_init_with_initial_branch() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "seed");
schema_cmd()
.args(["branch", "main", "-m", "develop"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["status"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("develop"));
}
#[test]
fn cli_status_no_commits() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
schema_cmd()
.args(["status"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("no commits yet"));
}
#[test]
fn cli_status_short() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
schema_cmd()
.args(["status", "-s", "-b"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("## main"));
}
#[test]
fn cli_status_porcelain() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
schema_cmd()
.args(["status", "--porcelain"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("## main"));
}
#[test]
fn cli_add_commit_log() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object"), ("b", "string")]);
add_and_commit(tmp.path(), "v1.json", "initial schema");
schema_cmd()
.args(["log"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(
predicate::str::contains("initial schema")
.and(predicate::str::contains("Author:"))
.and(predicate::str::contains("Date:")),
);
}
#[test]
fn cli_add_dry_run() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
schema_cmd()
.args(["add", "--dry-run", "v1.json"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Would stage"));
schema_cmd()
.args(["commit", "-m", "should fail"])
.current_dir(tmp.path())
.assert()
.failure();
}
#[test]
fn cli_commit_amend() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "original message");
schema_cmd()
.args(["commit", "--amend", "-m", "amended message"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("amended"));
let log_out = stdout_of(
schema_cmd()
.args(["log", "--oneline"])
.current_dir(tmp.path()),
);
assert!(log_out.contains("amended message"));
assert_eq!(log_out.trim().lines().count(), 1);
}
#[test]
fn cli_commit_no_staged_fails() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
schema_cmd()
.args(["commit", "-m", "nothing staged"])
.current_dir(tmp.path())
.assert()
.failure();
}
#[test]
fn cli_add_unchanged_fails() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "first");
schema_cmd()
.args(["add", "v1.json"])
.current_dir(tmp.path())
.assert()
.failure();
}
#[test]
fn cli_log_default() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "first commit");
schema_cmd()
.args(["log"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(
predicate::str::contains("Author:")
.and(predicate::str::contains("Date:"))
.and(predicate::str::contains("first commit")),
);
}
#[test]
fn cli_log_oneline() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "one-liner");
let out = stdout_of(
schema_cmd()
.args(["log", "--oneline"])
.current_dir(tmp.path()),
);
let lines: Vec<&str> = out.trim().lines().collect();
assert_eq!(lines.len(), 1);
assert!(lines[0].contains("one-liner"));
}
#[test]
fn cli_log_limit() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "first");
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
add_and_commit(tmp.path(), "v2.json", "second");
let out = stdout_of(
schema_cmd()
.args(["log", "--oneline", "-n", "1"])
.current_dir(tmp.path()),
);
let lines: Vec<&str> = out.trim().lines().collect();
assert_eq!(lines.len(), 1);
assert!(lines[0].contains("second"));
}
#[test]
fn cli_log_format() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "formatted");
let out = stdout_of(
schema_cmd()
.args(["log", "--format", "%h %s"])
.current_dir(tmp.path()),
);
let line = out.trim();
assert!(line.contains("formatted"));
let parts: Vec<&str> = line.splitn(2, ' ').collect();
assert_eq!(parts.len(), 2);
assert_eq!(parts[0].len(), 7); }
#[test]
fn cli_log_grep() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "first commit");
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
add_and_commit(tmp.path(), "v2.json", "second fix");
let out = stdout_of(
schema_cmd()
.args(["log", "--oneline", "--grep", "second"])
.current_dir(tmp.path()),
);
let lines: Vec<&str> = out.trim().lines().collect();
assert_eq!(lines.len(), 1);
assert!(lines[0].contains("second fix"));
}
#[test]
fn cli_diff_two_files() {
let tmp = tempfile::tempdir().unwrap();
write_schema(tmp.path(), "old.json", &[("a", "object")]);
write_schema(tmp.path(), "new.json", &[("a", "object"), ("c", "string")]);
schema_cmd()
.args(["diff", "old.json", "new.json"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("change(s) detected"));
}
#[test]
fn cli_diff_stat() {
let tmp = tempfile::tempdir().unwrap();
write_schema(tmp.path(), "old.json", &[("a", "object")]);
write_schema(tmp.path(), "new.json", &[("a", "object"), ("c", "string")]);
schema_cmd()
.args(["diff", "--stat", "old.json", "new.json"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("addition(s)"));
}
#[test]
fn cli_diff_name_only() {
let tmp = tempfile::tempdir().unwrap();
write_schema(tmp.path(), "old.json", &[("a", "object")]);
write_schema(tmp.path(), "new.json", &[("a", "object"), ("c", "string")]);
schema_cmd()
.args(["diff", "--name-only", "old.json", "new.json"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("c"));
}
#[test]
fn cli_diff_staged() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
schema_cmd()
.args(["add", "v2.json"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["diff", "--staged"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("change(s) detected"));
}
#[test]
fn cli_branch_create_list() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
schema_cmd()
.args(["branch", "feature"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Created branch feature"));
schema_cmd()
.args(["branch"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("feature").and(predicate::str::contains("main")));
}
#[test]
fn cli_branch_delete() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
schema_cmd()
.args(["branch", "feature"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["branch", "-d", "feature"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Deleted branch feature"));
}
#[test]
fn cli_branch_force_delete() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
schema_cmd()
.args(["branch", "feature"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["branch", "-D", "feature"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Deleted branch feature"));
}
#[test]
fn cli_branch_rename() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
schema_cmd()
.args(["branch", "old-name"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["branch", "old-name", "-m", "new-name"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains(
"Renamed branch old-name -> new-name",
));
let out = stdout_of(schema_cmd().args(["branch"]).current_dir(tmp.path()));
assert!(out.contains("new-name"));
assert!(!out.contains("old-name"));
}
#[test]
fn cli_tag_annotated() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
schema_cmd()
.args(["tag", "-a", "v1.0", "-m", "release"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Tagged").and(predicate::str::contains("v1.0")));
schema_cmd()
.args(["tag"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("v1.0"));
}
#[test]
fn cli_checkout_branch() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
schema_cmd()
.args(["branch", "feature"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["checkout", "feature"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Switched to branch 'feature'"));
schema_cmd()
.args(["status"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("feature"));
}
#[test]
fn cli_checkout_create() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
schema_cmd()
.args(["checkout", "-b", "new-feature"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains(
"Switched to a new branch 'new-feature'",
));
schema_cmd()
.args(["status"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("new-feature"));
}
#[test]
fn cli_checkout_detached() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "for detach");
let log_out = stdout_of(
schema_cmd()
.args(["log", "--format", "%H"])
.current_dir(tmp.path()),
);
let full_hash = log_out.trim();
schema_cmd()
.args(["checkout", "--detach", full_hash])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("HEAD is now at"));
schema_cmd()
.args(["status"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("detached"));
}
#[test]
fn cli_merge_fast_forward() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial on main");
schema_cmd()
.args(["checkout", "-b", "feature"])
.current_dir(tmp.path())
.assert()
.success();
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
add_and_commit(tmp.path(), "v2.json", "feature work");
schema_cmd()
.args(["checkout", "main"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["merge", "feature"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Merge successful"));
}
#[test]
fn cli_merge_ff_only_fails() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
schema_cmd()
.args(["checkout", "-b", "feature"])
.current_dir(tmp.path())
.assert()
.success();
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
add_and_commit(tmp.path(), "v2.json", "feature");
schema_cmd()
.args(["checkout", "main"])
.current_dir(tmp.path())
.assert()
.success();
write_schema(tmp.path(), "v3.json", &[("a", "object"), ("c", "integer")]);
add_and_commit(tmp.path(), "v3.json", "main diverge");
schema_cmd()
.args(["merge", "--ff-only", "feature"])
.current_dir(tmp.path())
.assert()
.failure();
}
#[test]
fn cli_stash_push_list_pop() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
schema_cmd()
.args(["add", "v2.json"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["stash", "push", "-m", "wip changes"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Saved working state"));
schema_cmd()
.args(["stash", "list"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("stash@{0}"));
schema_cmd()
.args(["stash", "pop"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Restored stash"));
}
#[test]
fn cli_stash_apply() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
schema_cmd()
.args(["add", "v2.json"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["stash", "push", "-m", "save"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["stash", "apply"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Applied stash@{0}"));
schema_cmd()
.args(["stash", "list"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("stash@{0}"));
}
#[test]
fn cli_stash_clear() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
schema_cmd()
.args(["add", "v2.json"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["stash", "push", "-m", "will clear"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["stash", "clear"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Cleared all stash entries"));
}
#[test]
fn cli_gc_dry_run() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
schema_cmd()
.args(["gc", "--dry-run"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(
predicate::str::contains("Reachable objects:")
.and(predicate::str::contains("Would delete:")),
);
}
#[test]
fn cli_blame_vertex() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object"), ("b", "string")]);
add_and_commit(tmp.path(), "v1.json", "added vertices");
schema_cmd()
.args(["blame", "--element-type", "vertex", "a"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("added vertices").and(predicate::str::contains("Date:")));
}
#[test]
fn cli_remote_stubs_complete() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
schema_cmd()
.args(["remote", "list"])
.current_dir(tmp.path())
.assert()
.failure()
.stderr(predicate::str::contains("not yet implemented"));
}
#[test]
fn cli_push_requires_url() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
schema_cmd()
.args(["push"])
.current_dir(tmp.path())
.assert()
.failure()
.stderr(predicate::str::contains("remote URL required"));
}
#[test]
fn cli_clone_requires_cospan_url() {
let tmp = tempfile::tempdir().unwrap();
schema_cmd()
.args(["clone", "https://example.com/repo"])
.current_dir(tmp.path())
.assert()
.failure()
.stderr(predicate::str::contains("cospan://"));
}
fn write_protocol_schema(dir: &Path, name: &str, protocol: &str, vertices: &[(&str, &str)]) {
let mut verts = serde_json::Map::new();
for (id, kind) in vertices {
verts.insert(
id.to_string(),
serde_json::json!({
"id": id, "kind": kind, "nsid": null
}),
);
}
let schema = serde_json::json!({
"protocol": protocol,
"vertices": verts,
"edges": [],
"hyper_edges": {},
"constraints": {},
"required": {},
"nsids": {},
"variants": {},
"orderings": [],
"recursion_points": {},
"spans": {},
"usage_modes": [],
"nominal": {},
"outgoing": {},
"incoming": {},
"between": []
});
let path = dir.join(name);
std::fs::write(&path, serde_json::to_string_pretty(&schema).unwrap()).unwrap();
}
fn write_migration(dir: &Path, name: &str, vertex_map: &[(&str, &str)]) {
let vmap: serde_json::Map<String, serde_json::Value> = vertex_map
.iter()
.map(|(k, v)| (k.to_string(), serde_json::json!(v)))
.collect();
let mig = serde_json::json!({
"vertex_map": vmap,
"edge_map": [],
"hyper_edge_map": {},
"label_map": [],
"resolver": [],
"hyper_resolver": []
});
let path = dir.join(name);
std::fs::write(&path, serde_json::to_string_pretty(&mig).unwrap()).unwrap();
}
#[test]
fn cli_validate_valid_schema() {
let tmp = tempfile::tempdir().unwrap();
write_protocol_schema(
tmp.path(),
"schema.json",
"atproto",
&[("root", "object"), ("root.name", "string")],
);
schema_cmd()
.args(["validate", "--protocol", "atproto", "schema.json"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Schema is valid."));
}
#[test]
fn cli_validate_invalid_schema() {
let tmp = tempfile::tempdir().unwrap();
write_protocol_schema(tmp.path(), "bad.json", "atproto", &[("root", "bogus_kind")]);
schema_cmd()
.args(["validate", "--protocol", "atproto", "bad.json"])
.current_dir(tmp.path())
.assert()
.failure()
.stdout(predicate::str::contains("error"));
}
#[test]
fn cli_check_valid_migration() {
let tmp = tempfile::tempdir().unwrap();
write_protocol_schema(
tmp.path(),
"src.json",
"atproto",
&[("a", "object"), ("b", "string")],
);
write_protocol_schema(
tmp.path(),
"tgt.json",
"atproto",
&[("a", "object"), ("b", "string"), ("c", "integer")],
);
write_migration(tmp.path(), "mig.json", &[("a", "a"), ("b", "b")]);
schema_cmd()
.args([
"check",
"--src",
"src.json",
"--tgt",
"tgt.json",
"--mapping",
"mig.json",
])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Migration is valid."));
}
#[test]
fn cli_lift_identity() {
let tmp = tempfile::tempdir().unwrap();
write_protocol_schema(tmp.path(), "src.json", "atproto", &[("root", "string")]);
write_protocol_schema(tmp.path(), "tgt.json", "atproto", &[("root", "string")]);
write_migration(tmp.path(), "mig.json", &[("root", "root")]);
let record = serde_json::json!("Alice");
std::fs::write(
tmp.path().join("record.json"),
serde_json::to_string_pretty(&record).unwrap(),
)
.unwrap();
let output = schema_cmd()
.args([
"lift",
"--migration",
"mig.json",
"--src-schema",
"src.json",
"--tgt-schema",
"tgt.json",
"record.json",
])
.current_dir(tmp.path())
.output()
.unwrap();
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
output.status.success(),
"lift command failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
stdout.contains("Alice"),
"expected 'Alice' in output: {stdout}"
);
}
#[test]
fn cli_diff_name_status() {
let tmp = tempfile::tempdir().unwrap();
write_schema(tmp.path(), "old.json", &[("a", "object")]);
write_schema(tmp.path(), "new.json", &[("a", "object"), ("c", "string")]);
let output = schema_cmd()
.args(["diff", "--name-status", "old.json", "new.json"])
.current_dir(tmp.path())
.output()
.unwrap();
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(output.status.success());
assert!(
stdout.contains("A\t"),
"expected 'A\\t' marker in name-status output: {stdout}"
);
}
#[test]
fn cli_show_commit() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial commit");
schema_cmd()
.args(["show", "HEAD"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(
predicate::str::contains("commit")
.and(predicate::str::contains("Schema:"))
.and(predicate::str::contains("Author:")),
);
}
#[test]
fn cli_show_with_stat() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "first");
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
add_and_commit(tmp.path(), "v2.json", "second");
schema_cmd()
.args(["show", "HEAD", "--stat"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("addition"));
}
#[test]
fn cli_rebase_success() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "seed on main");
schema_cmd()
.args(["checkout", "-b", "feature"])
.current_dir(tmp.path())
.assert()
.success();
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
add_and_commit(tmp.path(), "v2.json", "feature work");
schema_cmd()
.args(["checkout", "main"])
.current_dir(tmp.path())
.assert()
.success();
write_schema(tmp.path(), "v3.json", &[("a", "object"), ("c", "integer")]);
add_and_commit(tmp.path(), "v3.json", "main advance");
schema_cmd()
.args(["checkout", "feature"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["rebase", "main"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Rebased onto"));
}
#[test]
fn cli_cherry_pick_success() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial on main");
schema_cmd()
.args(["checkout", "-b", "feature"])
.current_dir(tmp.path())
.assert()
.success();
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
add_and_commit(tmp.path(), "v2.json", "feature commit");
let log_out = stdout_of(
schema_cmd()
.args(["log", "--format", "%H"])
.current_dir(tmp.path()),
);
let feature_hash = log_out.trim().lines().next().unwrap().trim();
schema_cmd()
.args(["checkout", "main"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["cherry-pick", feature_hash])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Cherry-picked"));
}
#[test]
fn cli_cherry_pick_with_x() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
schema_cmd()
.args(["checkout", "-b", "feature"])
.current_dir(tmp.path())
.assert()
.success();
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
add_and_commit(tmp.path(), "v2.json", "feature work");
let log_out = stdout_of(
schema_cmd()
.args(["log", "--format", "%H"])
.current_dir(tmp.path()),
);
let feature_hash = log_out.trim().lines().next().unwrap().trim();
schema_cmd()
.args(["checkout", "main"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["cherry-pick", "-x", feature_hash])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Cherry-picked"));
}
#[test]
fn cli_reset_soft_output() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "first");
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
add_and_commit(tmp.path(), "v2.json", "second");
let log_out = stdout_of(
schema_cmd()
.args(["log", "--format", "%H"])
.current_dir(tmp.path()),
);
let first_hash = log_out.trim().lines().last().unwrap().trim();
schema_cmd()
.args(["reset", "--soft", first_hash])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("HEAD is now at").and(predicate::str::contains("soft")));
}
#[test]
fn cli_reset_hard_output() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "first");
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
add_and_commit(tmp.path(), "v2.json", "second");
let log_out = stdout_of(
schema_cmd()
.args(["log", "--format", "%H"])
.current_dir(tmp.path()),
);
let first_hash = log_out.trim().lines().last().unwrap().trim();
schema_cmd()
.args(["reset", "--hard", first_hash])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("HEAD is now at").and(predicate::str::contains("hard")));
}
#[test]
fn cli_bisect_output() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "commit one");
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
add_and_commit(tmp.path(), "v2.json", "commit two");
write_schema(
tmp.path(),
"v3.json",
&[("a", "object"), ("b", "string"), ("c", "integer")],
);
add_and_commit(tmp.path(), "v3.json", "commit three");
let log_out = stdout_of(
schema_cmd()
.args(["log", "--format", "%H"])
.current_dir(tmp.path()),
);
let hashes: Vec<&str> = log_out.trim().lines().map(str::trim).collect();
let last = hashes.first().unwrap();
let first = hashes.last().unwrap();
let output = schema_cmd()
.args(["bisect", first, last])
.current_dir(tmp.path())
.output()
.unwrap();
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
output.status.success(),
"bisect failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
stdout.contains("Breaking commit") || stdout.contains("Test commit"),
"expected bisect output, got: {stdout}"
);
}
#[test]
fn cli_reflog_shows_history() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "first");
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
add_and_commit(tmp.path(), "v2.json", "second");
let output = schema_cmd()
.args(["reflog", "HEAD"])
.current_dir(tmp.path())
.output()
.unwrap();
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(output.status.success());
assert!(
stdout.contains("HEAD@{0}") || stdout.contains("->"),
"expected reflog entries, got: {stdout}"
);
}
#[test]
fn cli_reflog_with_all() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
schema_cmd()
.args(["reflog", "--all"])
.current_dir(tmp.path())
.assert()
.success();
}
#[test]
fn cli_log_author_filter() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
schema_cmd()
.args(["add", "v1.json"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["commit", "-m", "by alice", "--author", "alice"])
.current_dir(tmp.path())
.assert()
.success();
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
schema_cmd()
.args(["add", "v2.json"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["commit", "-m", "by bob", "--author", "bob"])
.current_dir(tmp.path())
.assert()
.success();
let out = stdout_of(
schema_cmd()
.args(["log", "--oneline", "--author", "alice"])
.current_dir(tmp.path()),
);
assert!(out.contains("by alice"), "expected alice's commit: {out}");
assert!(
!out.contains("by bob"),
"should not contain bob's commit: {out}"
);
}
#[test]
fn cli_branch_verbose() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
schema_cmd()
.args(["branch", "feature"])
.current_dir(tmp.path())
.assert()
.success();
let out = stdout_of(schema_cmd().args(["branch", "-v"]).current_dir(tmp.path()));
assert!(
out.contains("main"),
"expected 'main' in branch list: {out}"
);
assert!(
out.contains("feature"),
"expected 'feature' in branch list: {out}"
);
assert!(
out.contains("initial"),
"expected commit message in verbose output: {out}"
);
}
#[test]
fn cli_merge_no_commit() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial on main");
schema_cmd()
.args(["checkout", "-b", "feature"])
.current_dir(tmp.path())
.assert()
.success();
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
add_and_commit(tmp.path(), "v2.json", "feature work");
schema_cmd()
.args(["checkout", "main"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["merge", "--no-commit", "feature"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Merge successful"));
}
#[test]
fn cli_merge_abort() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
schema_cmd()
.args(["merge", "--abort"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Merge aborted"));
}
#[test]
fn cli_commit_allow_empty() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
let output = schema_cmd()
.args(["commit", "--allow-empty", "-m", "empty commit"])
.current_dir(tmp.path())
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success()
|| stderr.contains("failed to commit")
|| stderr.contains("nothing"),
"expected graceful behavior, got stderr: {stderr}"
);
}
#[test]
fn cli_stash_show() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
schema_cmd()
.args(["add", "v2.json"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["stash", "push", "-m", "wip"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["stash", "show"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("stash@{0}"));
}
#[test]
fn cli_stash_drop() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
write_schema(tmp.path(), "v1.json", &[("a", "object")]);
add_and_commit(tmp.path(), "v1.json", "initial");
write_schema(tmp.path(), "v2.json", &[("a", "object"), ("b", "string")]);
schema_cmd()
.args(["add", "v2.json"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["stash", "push", "-m", "will drop"])
.current_dir(tmp.path())
.assert()
.success();
schema_cmd()
.args(["stash", "drop"])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Dropped stash@{0}"));
}
#[test]
fn cli_pull_fetch_require_url() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
schema_cmd()
.args(["pull"])
.current_dir(tmp.path())
.assert()
.failure()
.stderr(predicate::str::contains("remote URL required"));
schema_cmd()
.args(["fetch"])
.current_dir(tmp.path())
.assert()
.failure()
.stderr(predicate::str::contains("remote URL required"));
}
#[test]
fn cli_lift_string_identity() {
let tmp = tempfile::tempdir().unwrap();
write_protocol_schema(tmp.path(), "src.json", "atproto", &[("root", "string")]);
write_protocol_schema(tmp.path(), "tgt.json", "atproto", &[("root", "string")]);
write_migration(tmp.path(), "mig.json", &[("root", "root")]);
let record = serde_json::json!("Hello, world!");
std::fs::write(
tmp.path().join("record.json"),
serde_json::to_string_pretty(&record).unwrap(),
)
.unwrap();
schema_cmd()
.args([
"lift",
"--migration",
"mig.json",
"--src-schema",
"src.json",
"--tgt-schema",
"tgt.json",
"record.json",
])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("Hello, world!"));
}
#[test]
fn cli_lift_integer_identity() {
let tmp = tempfile::tempdir().unwrap();
write_protocol_schema(tmp.path(), "src.json", "atproto", &[("root", "integer")]);
write_protocol_schema(tmp.path(), "tgt.json", "atproto", &[("root", "integer")]);
write_migration(tmp.path(), "mig.json", &[("root", "root")]);
std::fs::write(tmp.path().join("record.json"), "42").unwrap();
schema_cmd()
.args([
"lift",
"--migration",
"mig.json",
"--src-schema",
"src.json",
"--tgt-schema",
"tgt.json",
"record.json",
])
.current_dir(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("42"));
}
#[test]
fn cli_lift_verbose() {
let tmp = tempfile::tempdir().unwrap();
write_protocol_schema(tmp.path(), "src.json", "atproto", &[("root", "string")]);
write_protocol_schema(tmp.path(), "tgt.json", "atproto", &[("root", "string")]);
write_migration(tmp.path(), "mig.json", &[("root", "root")]);
std::fs::write(
tmp.path().join("record.json"),
serde_json::to_string_pretty(&serde_json::json!("Alice")).unwrap(),
)
.unwrap();
let output = schema_cmd()
.args([
"--verbose",
"lift",
"--migration",
"mig.json",
"--src-schema",
"src.json",
"--tgt-schema",
"tgt.json",
"record.json",
])
.current_dir(tmp.path())
.output()
.unwrap();
assert!(output.status.success(), "verbose lift should succeed");
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(
stderr.contains("vertex mappings"),
"stderr should mention vertex mappings, got: {stderr}"
);
assert!(
stderr.contains("nodes") && stderr.contains("arcs"),
"stderr should mention node and arc counts, got: {stderr}"
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("Alice"));
}
#[test]
fn cli_lift_bad_migration_fails() {
let tmp = tempfile::tempdir().unwrap();
write_protocol_schema(tmp.path(), "src.json", "atproto", &[("root", "string")]);
write_protocol_schema(tmp.path(), "tgt.json", "atproto", &[("root", "string")]);
write_migration(tmp.path(), "mig.json", &[("root", "nonexistent")]);
std::fs::write(tmp.path().join("record.json"), "\"test\"").unwrap();
schema_cmd()
.args([
"lift",
"--migration",
"mig.json",
"--src-schema",
"src.json",
"--tgt-schema",
"tgt.json",
"record.json",
])
.current_dir(tmp.path())
.assert()
.failure();
}
use panproto_core::gat::Name;
use panproto_core::inst;
use panproto_core::mig;
use panproto_core::schema::{Edge, Schema, Vertex};
use smallvec::SmallVec;
use std::collections::HashMap;
fn make_lift_schema(
vertices: &[(&str, &str)],
edges: &[(&str, &str, &str, &str)], ) -> Schema {
let mut vert_map = HashMap::new();
for (id, kind) in vertices {
vert_map.insert(
Name::from(*id),
Vertex {
id: Name::from(*id),
kind: Name::from(*kind),
nsid: None,
},
);
}
let mut edge_map = HashMap::new();
let mut outgoing: HashMap<Name, SmallVec<Edge, 4>> = HashMap::new();
let mut incoming: HashMap<Name, SmallVec<Edge, 4>> = HashMap::new();
let mut between: HashMap<(Name, Name), SmallVec<Edge, 2>> = HashMap::new();
for (src, tgt, kind, name) in edges {
let edge = Edge {
src: (*src).into(),
tgt: (*tgt).into(),
kind: (*kind).into(),
name: Some((*name).into()),
};
edge_map.insert(edge.clone(), Name::from(*kind));
outgoing
.entry(Name::from(*src))
.or_default()
.push(edge.clone());
incoming
.entry(Name::from(*tgt))
.or_default()
.push(edge.clone());
between
.entry((Name::from(*src), Name::from(*tgt)))
.or_default()
.push(edge);
}
Schema {
protocol: "test".into(),
vertices: vert_map,
edges: edge_map,
hyper_edges: HashMap::new(),
constraints: HashMap::new(),
required: HashMap::new(),
nsids: HashMap::new(),
entries: Vec::new(),
variants: HashMap::new(),
orderings: HashMap::new(),
recursion_points: HashMap::new(),
spans: HashMap::new(),
usage_modes: HashMap::new(),
nominal: HashMap::new(),
coercions: HashMap::new(),
mergers: HashMap::new(),
defaults: HashMap::new(),
policies: HashMap::new(),
outgoing,
incoming,
between,
}
}
fn make_migration(
vertex_map: &[(&str, &str)],
edge_map_entries: &[(Edge, Edge)],
) -> mig::Migration {
mig::Migration {
vertex_map: vertex_map
.iter()
.map(|(k, v)| (Name::from(*k), Name::from(*v)))
.collect(),
edge_map: edge_map_entries.iter().cloned().collect(),
hyper_edge_map: HashMap::new(),
label_map: HashMap::new(),
resolver: HashMap::new(),
hyper_resolver: HashMap::new(),
expr_resolvers: HashMap::new(),
}
}
#[test]
fn lift_api_add_field() {
let src_schema = make_lift_schema(
&[("root", "object"), ("root.name", "string")],
&[("root", "root.name", "prop", "name")],
);
let tgt_schema = make_lift_schema(
&[
("root", "object"),
("root.name", "string"),
("root.email", "string"),
],
&[
("root", "root.name", "prop", "name"),
("root", "root.email", "prop", "email"),
],
);
let name_edge = Edge {
src: "root".into(),
tgt: "root.name".into(),
kind: "prop".into(),
name: Some("name".into()),
};
let migration = make_migration(
&[("root", "root"), ("root.name", "root.name")],
&[(name_edge.clone(), name_edge)],
);
let compiled = mig::compile(&src_schema, &tgt_schema, &migration).unwrap();
let record = serde_json::json!({"name": "Alice"});
let instance = inst::parse_json(&src_schema, "root", &record).unwrap();
let lifted = mig::lift_wtype(&compiled, &src_schema, &tgt_schema, &instance).unwrap();
let output = inst::to_json(&tgt_schema, &lifted);
assert_eq!(output["name"], "Alice", "name should be preserved");
assert!(
output.get("email").is_none() || output["email"].is_null(),
"email should be absent or null in lifted output"
);
}
#[test]
fn lift_api_drop_field() {
let src_schema = make_lift_schema(
&[
("root", "object"),
("root.name", "string"),
("root.age", "integer"),
],
&[
("root", "root.name", "prop", "name"),
("root", "root.age", "prop", "age"),
],
);
let tgt_schema = make_lift_schema(
&[("root", "object"), ("root.name", "string")],
&[("root", "root.name", "prop", "name")],
);
let name_edge = Edge {
src: "root".into(),
tgt: "root.name".into(),
kind: "prop".into(),
name: Some("name".into()),
};
let migration = make_migration(
&[("root", "root"), ("root.name", "root.name")],
&[(name_edge.clone(), name_edge)],
);
let compiled = mig::compile(&src_schema, &tgt_schema, &migration).unwrap();
let record = serde_json::json!({"name": "Bob", "age": 30});
let instance = inst::parse_json(&src_schema, "root", &record).unwrap();
let lifted = mig::lift_wtype(&compiled, &src_schema, &tgt_schema, &instance).unwrap();
let output = inst::to_json(&tgt_schema, &lifted);
assert_eq!(output["name"], "Bob", "name should be preserved");
assert!(
output.get("age").is_none(),
"age should be absent from lifted output, got: {output}"
);
}
#[test]
fn lift_api_identity_all_fields_survive() {
let schema = make_lift_schema(
&[
("root", "object"),
("root.name", "string"),
("root.email", "string"),
],
&[
("root", "root.name", "prop", "name"),
("root", "root.email", "prop", "email"),
],
);
let name_edge = Edge {
src: "root".into(),
tgt: "root.name".into(),
kind: "prop".into(),
name: Some("name".into()),
};
let email_edge = Edge {
src: "root".into(),
tgt: "root.email".into(),
kind: "prop".into(),
name: Some("email".into()),
};
let migration = make_migration(
&[
("root", "root"),
("root.name", "root.name"),
("root.email", "root.email"),
],
&[
(name_edge.clone(), name_edge),
(email_edge.clone(), email_edge),
],
);
let compiled = mig::compile(&schema, &schema, &migration).unwrap();
let record = serde_json::json!({"name": "Eve", "email": "eve@example.com"});
let instance = inst::parse_json(&schema, "root", &record).unwrap();
let lifted = mig::lift_wtype(&compiled, &schema, &schema, &instance).unwrap();
let output = inst::to_json(&schema, &lifted);
assert_eq!(output["name"], "Eve");
assert_eq!(output["email"], "eve@example.com");
}
#[test]
fn lift_api_multi_field_projection() {
let src_schema = make_lift_schema(
&[
("root", "object"),
("root.name", "string"),
("root.email", "string"),
("root.age", "integer"),
],
&[
("root", "root.name", "prop", "name"),
("root", "root.email", "prop", "email"),
("root", "root.age", "prop", "age"),
],
);
let tgt_schema = make_lift_schema(
&[
("root", "object"),
("root.name", "string"),
("root.email", "string"),
],
&[
("root", "root.name", "prop", "name"),
("root", "root.email", "prop", "email"),
],
);
let name_edge = Edge {
src: "root".into(),
tgt: "root.name".into(),
kind: "prop".into(),
name: Some("name".into()),
};
let email_edge = Edge {
src: "root".into(),
tgt: "root.email".into(),
kind: "prop".into(),
name: Some("email".into()),
};
let migration = make_migration(
&[
("root", "root"),
("root.name", "root.name"),
("root.email", "root.email"),
],
&[
(name_edge.clone(), name_edge),
(email_edge.clone(), email_edge),
],
);
let compiled = mig::compile(&src_schema, &tgt_schema, &migration).unwrap();
let record = serde_json::json!({"name": "Dana", "email": "dana@example.com", "age": 25});
let instance = inst::parse_json(&src_schema, "root", &record).unwrap();
let lifted = mig::lift_wtype(&compiled, &src_schema, &tgt_schema, &instance).unwrap();
let output = inst::to_json(&tgt_schema, &lifted);
assert_eq!(output["name"], "Dana");
assert_eq!(output["email"], "dana@example.com");
assert!(
output.get("age").is_none(),
"age should be absent from lifted output, got: {output}"
);
}
#[test]
fn lift_api_preserves_value_types() {
let src_schema = make_lift_schema(
&[
("root", "object"),
("root.active", "boolean"),
("root.name", "string"),
],
&[
("root", "root.active", "prop", "active"),
("root", "root.name", "prop", "name"),
],
);
let tgt_schema = make_lift_schema(
&[("root", "object"), ("root.active", "boolean")],
&[("root", "root.active", "prop", "active")],
);
let active_edge = Edge {
src: "root".into(),
tgt: "root.active".into(),
kind: "prop".into(),
name: Some("active".into()),
};
let migration = make_migration(
&[("root", "root"), ("root.active", "root.active")],
&[(active_edge.clone(), active_edge)],
);
let compiled = mig::compile(&src_schema, &tgt_schema, &migration).unwrap();
let record = serde_json::json!({"active": true, "name": "test"});
let instance = inst::parse_json(&src_schema, "root", &record).unwrap();
let lifted = mig::lift_wtype(&compiled, &src_schema, &tgt_schema, &instance).unwrap();
let output = inst::to_json(&tgt_schema, &lifted);
assert_eq!(output["active"], true, "boolean value should be preserved");
assert!(
output.get("name").is_none(),
"dropped field should be absent, got: {output}"
);
}