tedi 0.16.3

Personal productivity CLI for task tracking, time management, and GitHub issue integration
Documentation
//! Integration tests for issue content preservation through edit/sync cycles.
//!
//! Tests that nested issues, blockers, and other content survive the
//! parse -> edit -> serialize -> sync cycle intact.

use tedi::CloseState;
use v_fixtures::FixtureRenderer;

use crate::{
	common::{
		FixtureIssuesExt, TestContext,
		are_you_sure::{UnsafePathExt, read_issue_file},
		parse_virtual,
	},
	render_fixture,
};

#[tokio::test]
async fn test_comments_with_ids_sync_correctly() {
	let ctx = TestContext::build_with_preexisting_state_unsafe("");

	// Issue with a comment that has an ID
	let vi = parse_virtual(
		r#"- [ ] a <!-- @mock_user https://github.com/o/r/issues/1 -->

  body text

  <!-- @mock_user https://github.com/o/r/issues/1#issuecomment-12345 -->
  This is my comment
"#,
	);

	let issue = ctx.consensus(&vi, None).await;
	ctx.remote(&vi, None);

	let out = ctx.open_issue(&issue).args(&["--force"]).run();
	eprintln!("stdout: {}", out.stdout);
	eprintln!("stderr: {}", out.stderr);

	// This should NOT fail with "comment X not found in consensus"
	assert!(out.status.success(), "sync failed: {}", out.stderr);
}

#[tokio::test]
async fn test_nested_issues_preserved_through_sync() {
	let ctx = TestContext::build_with_preexisting_state_unsafe("");

	let vi = parse_virtual(
		r#"- [ ] a <!-- @mock_user https://github.com/o/r/issues/1 -->

  lorem ipsum

  - [ ] b <!-- @mock_user https://github.com/o/r/issues/2 -->

    nested body b

  - [ ] c <!-- @mock_user https://github.com/o/r/issues/3 -->

    nested body c
"#,
	);

	let issue = ctx.consensus(&vi, None).await;
	ctx.remote(&vi, None);

	let out = ctx.open_issue(&issue).run();
	eprintln!("stdout: {}", out.stdout);
	eprintln!("stderr: {}", out.stderr);

	assert!(out.status.success(), "stderr: {}", out.stderr);

	// With the new model, children are stored in separate files in the parent's directory
	let path = ctx.resolve_issue_path(&issue);
	let parent_dir = path.parent().unwrap();
	let child_b_path = parent_dir.join("2_-_b.md");
	let child_c_path = parent_dir.join("3_-_c.md");

	let child_b_content = read_issue_file(&child_b_path);
	let child_c_content = read_issue_file(&child_c_path);

	assert!(child_b_content.contains("nested body b"), "nested issue b body lost");
	assert!(child_c_content.contains("nested body c"), "nested issue c body lost");
}

#[tokio::test]
async fn test_blockers_preserved_through_sync() {
	let ctx = TestContext::build_with_preexisting_state_unsafe("");

	let vi = parse_virtual(
		r#"- [ ] a <!-- @mock_user https://github.com/o/r/issues/1 -->

  lorem ipsum

  # Blockers
  - first blocker
  - second blocker
"#,
	);

	let issue = ctx.consensus(&vi, None).await;
	ctx.remote(&vi, None);

	let out = ctx.open_issue(&issue).run();
	eprintln!("stdout: {}", out.stdout);
	eprintln!("stderr: {}", out.stderr);

	assert!(out.status.success(), "stderr: {}", out.stderr);

	let path = ctx.resolve_issue_path(&issue);
	let final_content = read_issue_file(&path);
	assert!(final_content.contains("# Blockers"), "blockers section lost");
	assert!(final_content.contains("first blocker"), "first blocker lost");
	assert!(final_content.contains("second blocker"), "second blocker lost");
}

#[tokio::test]
async fn test_blockers_added_during_edit_preserved() {
	let ctx = TestContext::build_with_preexisting_state_unsafe("");

	// Initial state: no blockers
	let initial_vi = parse_virtual(
		r#"- [ ] a <!-- @mock_user https://github.com/o/r/issues/1 -->

  lorem ipsum
"#,
	);

	let initial_issue = ctx.consensus(&initial_vi, None).await;
	ctx.remote(&initial_vi, None);

	// User adds blockers during edit
	let edited_issue = parse_virtual(
		r#"- [ ] a <!-- @mock_user https://github.com/o/r/issues/1 -->

  lorem ipsum

  # Blockers
  - new blocker added
"#,
	);

	let out = ctx.open_issue(&initial_issue).edit(&edited_issue).run();
	eprintln!("stdout: {}", out.stdout);
	eprintln!("stderr: {}", out.stderr);

	assert!(out.status.success(), "stderr: {}", out.stderr);

	let path = ctx.resolve_issue_path(&initial_issue);
	let final_content = read_issue_file(&path);
	assert!(final_content.contains("# Blockers"), "blockers section not preserved");
	assert!(final_content.contains("new blocker added"), "added blocker lost");
}

#[tokio::test]
async fn test_blockers_with_nesting_preserved() {
	let ctx = TestContext::build_with_preexisting_state_unsafe("");

	let vi = parse_virtual(
		r#"- [ ] a <!-- @mock_user https://github.com/o/r/issues/1 -->

  lorem ipsum

  # Blockers
  - phase 1
    - task alpha
    - task beta
  - phase 2
    - task gamma
"#,
	);

	let issue = ctx.consensus(&vi, None).await;
	ctx.remote(&vi, None);

	let out = ctx.open_issue(&issue).run();
	eprintln!("stdout: {}", out.stdout);
	eprintln!("stderr: {}", out.stderr);

	assert!(out.status.success(), "stderr: {}", out.stderr);

	let path = ctx.resolve_issue_path(&issue);
	let final_content = read_issue_file(&path);
	assert!(final_content.contains("phase 1"), "phase 1 lost");
	assert!(final_content.contains("phase 2"), "phase 2 lost");
	assert!(final_content.contains("task alpha"), "task alpha lost");
	assert!(final_content.contains("task gamma"), "task gamma lost");
}

#[tokio::test]
async fn test_closing_nested_issue_creates_bak_file() {
	let ctx = TestContext::build();

	// Start with open nested issue
	let initial_vi = parse_virtual(
		r#"- [ ] a <!-- @mock_user https://github.com/o/r/issues/1 -->

  lorem ipsum

  - [ ] b <!-- @mock_user https://github.com/o/r/issues/2 -->

    nested body content
"#,
	);

	let initial_issue = ctx.consensus(&initial_vi, None).await;
	ctx.remote(&initial_vi, None);

	// User closes nested issue during edit
	let edited_issue = {
		let mut initial = initial_issue.clone();
		let (_, child) = initial.children.iter_mut().next().unwrap();
		child.contents.state = CloseState::Closed;
		initial
	};

	let out = ctx.open_issue(&initial_issue).edit(&edited_issue.into()).run();

	assert!(out.status.success(), "stderr: {}", out.stderr);

	insta::assert_snapshot!(render_fixture(FixtureRenderer::try_new(&ctx).unwrap().skip_meta(), &out), @"
	//- /o/r/1_-_a/2_-_b.md.bak
	- [x] b <!-- @mock_user https://github.com/o/r/issues/2 -->
	  nested body content
	//- /o/r/1_-_a/__main__.md
	- [ ] a <!-- @mock_user https://github.com/o/r/issues/1 -->
	  lorem ipsum
	");

	// With the new model, closed child is in a separate .bak file
	let path = ctx.resolve_issue_path(&initial_issue);
	let closed_child_path = path.parent().unwrap().join("2_-_b.md.bak");
	assert!(closed_child_path.exists(), "closed nested issue should have .bak file");

	let child_content = read_issue_file(&closed_child_path);
	assert!(child_content.contains("- [x] b"), "nested issue not marked closed");
	assert!(child_content.contains("nested body content"), "child body should be preserved");
}