mod common;
use common::TestFixture;
use lazyspec::engine::config::{Config, StoreBackend};
use lazyspec::engine::store::Store;
fn config_with_git_ref_iteration() -> Config {
let mut config = Config::default();
for t in &mut config.documents.types {
if t.name == "iteration" {
t.store = StoreBackend::GitRef;
}
}
config
}
fn setup_cross_backend(implements_target: &str) -> (TestFixture, Store) {
let fixture = TestFixture::new();
fixture.write_doc(
"docs/stories/STORY-001-feature.md",
"---\ntitle: \"Feature Story\"\ntype: story\nstatus: draft\nauthor: \"test\"\ndate: 2026-04-01\ntags: []\nrelated: []\n---\n\nStory body.\n",
);
let cache_dir = fixture.root().join(".lazyspec/cache/iteration");
std::fs::create_dir_all(&cache_dir).unwrap();
std::fs::write(
cache_dir.join("ITERATION-001-impl.md"),
format!(
"---\ntitle: \"Impl Iteration\"\ntype: iteration\nstatus: draft\nauthor: \"agent\"\ndate: 2026-04-01\ntags: []\nrelated:\n- implements: {}\n---\n\nIteration body.\n",
implements_target
),
)
.unwrap();
let config = config_with_git_ref_iteration();
let store = Store::load(fixture.root(), &config).unwrap();
(fixture, store)
}
#[test]
fn context_resolves_chain_across_fs_story_and_git_ref_iteration() {
let (_fixture, store) = setup_cross_backend("docs/stories/STORY-001-feature.md");
let resolved = lazyspec::cli::context::resolve_chain(&store, "ITERATION-001").unwrap();
assert_eq!(
resolved.chain.len(),
2,
"chain should contain story + iteration; got: {:?}",
resolved.chain.iter().map(|d| &d.title).collect::<Vec<_>>()
);
assert_eq!(resolved.chain[0].title, "Feature Story");
assert_eq!(resolved.chain[1].title, "Impl Iteration");
assert_eq!(resolved.target_index, 1);
let json_output = lazyspec::cli::context::run_json(&store, "ITERATION-001").unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json_output).unwrap();
let chain = parsed["chain"].as_array().unwrap();
assert_eq!(chain.len(), 2);
assert_eq!(chain[0]["type"], "story");
assert_eq!(chain[1]["type"], "iteration");
}
#[test]
fn context_reverse_links_show_git_ref_iteration_from_fs_story() {
let (_fixture, store) = setup_cross_backend("docs/stories/STORY-001-feature.md");
let resolved = lazyspec::cli::context::resolve_chain(&store, "STORY-001").unwrap();
assert_eq!(resolved.chain.len(), 1);
assert_eq!(resolved.chain[0].title, "Feature Story");
let forward_titles: Vec<&str> = resolved.forward.iter().map(|d| d.title.as_str()).collect();
assert!(
forward_titles.contains(&"Impl Iteration"),
"forward deps should include git-ref iteration; got: {:?}",
forward_titles
);
}
#[test]
fn context_with_id_based_target() {
let (_fixture, store) = setup_cross_backend("STORY-001");
let resolved = lazyspec::cli::context::resolve_chain(&store, "ITERATION-001").unwrap();
assert_eq!(
resolved.chain.len(),
1,
"ID-based target breaks upward chain walk; chain should only contain the iteration itself; got: {:?}",
resolved.chain.iter().map(|d| &d.title).collect::<Vec<_>>()
);
assert_eq!(resolved.chain[0].title, "Impl Iteration");
let story_resolved = lazyspec::cli::context::resolve_chain(&store, "STORY-001").unwrap();
let forward_titles: Vec<&str> = story_resolved
.forward
.iter()
.map(|d| d.title.as_str())
.collect();
assert!(
forward_titles.contains(&"Impl Iteration"),
"reverse links should still resolve ID-based targets; got: {:?}",
forward_titles
);
}
#[test]
fn fs_doc_links_to_git_ref_doc_via_related_to() {
let fixture = TestFixture::new();
fixture.write_doc(
"docs/stories/STORY-001-feature.md",
"---\ntitle: \"Feature Story\"\ntype: story\nstatus: draft\nauthor: \"test\"\ndate: 2026-04-01\ntags: []\nrelated:\n- related-to: ITERATION-001\n---\n\nStory body.\n",
);
let cache_dir = fixture.root().join(".lazyspec/cache/iteration");
std::fs::create_dir_all(&cache_dir).unwrap();
std::fs::write(
cache_dir.join("ITERATION-001-impl.md"),
"---\ntitle: \"Impl Iteration\"\ntype: iteration\nstatus: draft\nauthor: \"agent\"\ndate: 2026-04-01\ntags: []\nrelated: []\n---\n\nIteration body.\n",
)
.unwrap();
let config = config_with_git_ref_iteration();
let store = Store::load(fixture.root(), &config).unwrap();
let story_path = std::path::PathBuf::from("docs/stories/STORY-001-feature.md");
let forward = store.forward_links_for(&story_path);
let targets: Vec<&std::path::Path> = forward.iter().map(|(_, p)| p.as_path()).collect();
let has_iteration = targets
.iter()
.any(|p| p.to_string_lossy().contains("ITERATION-001"));
assert!(
has_iteration,
"FS story's forward links should include the git-ref iteration; got: {:?}",
forward
);
let iteration_path = forward
.iter()
.find(|(_, p)| p.to_string_lossy().contains("ITERATION-001"))
.map(|(_, p)| p.clone())
.unwrap();
let reverse = store.reverse_links_for(&iteration_path);
let reverse_sources: Vec<&std::path::Path> = reverse.iter().map(|(_, p)| p.as_path()).collect();
assert!(
reverse_sources
.iter()
.any(|p| p.to_string_lossy().contains("STORY-001")),
"git-ref iteration's reverse links should include the FS story; got: {:?}",
reverse
);
}
#[test]
fn git_ref_doc_links_to_fs_doc_via_implements() {
let (_fixture, store) = setup_cross_backend("STORY-001");
let iteration_doc = store
.all_docs()
.into_iter()
.find(|d| d.id == "ITERATION-001")
.expect("iteration should be in the store");
let forward = store.forward_links_for(&iteration_doc.path);
let has_story = forward
.iter()
.any(|(_, p)| p.to_string_lossy().contains("STORY-001"));
assert!(
has_story,
"git-ref iteration's forward links should include the FS story (via ID resolution in build_links); got: {:?}",
forward
);
let story_path = forward
.iter()
.find(|(_, p)| p.to_string_lossy().contains("STORY-001"))
.map(|(_, p)| p.clone())
.unwrap();
let reverse = store.reverse_links_for(&story_path);
let has_iteration = reverse
.iter()
.any(|(_, p)| p.to_string_lossy().contains("ITERATION-001"));
assert!(
has_iteration,
"FS story's reverse links should include the git-ref iteration; got: {:?}",
reverse
);
}