mod support;
use std::process::Command;
use loopflow::ops::task::{task_snapshot, task_status};
use loopflow::ops::task_actions::TaskAction;
use loopflow::store::PmSnapshotRow;
use loopflow::work::task::TaskEventKind;
use loopflow_test_support::TestRepo;
use support::{register_unrun_task, EnvGuard};
#[test]
fn initializing_worktree_keeps_status_wait_and_roadmap_readable() {
let home = tempfile::tempdir().expect("Task home");
let _env = EnvGuard::with_lf_home(&[], home.path());
let repo = TestRepo::new();
let base = repo.head_sha();
let branch = "jack/initializing-task";
repo.create_branch(branch);
let mut task = register_unrun_task(home.path(), repo.path(), branch, &base);
let missing_worktree = home.path().join("not-yet-created-worktree");
task.task.worktree = missing_worktree.clone();
let runtime = tokio::runtime::Runtime::new().expect("initialization fixture runtime");
runtime
.block_on(task.store.update_task(&task.task))
.expect("publish declared Task worktree");
runtime
.block_on(task.store.append_task_event(
&task.task.id,
&TaskEventKind::WorktreeInitializing {
pr_id: task.pr.id.clone(),
sequence: task.pr.sequence,
branch: task.pr.branch.clone(),
path: missing_worktree.display().to_string(),
base_commit: task.pr.base_commit.clone(),
},
))
.expect("publish initialization marker");
std::fs::create_dir_all(&missing_worktree)
.expect("simulate a partially created worktree directory");
let project = runtime
.block_on(task.store.get_project(&task.task.project_id))
.expect("read owning Project")
.expect("owning Project exists");
let payload = serde_json::json!({
"projects": [{
"id": project.plan.id.as_str(),
"slug": project.plan.slug,
"name": project.plan.name,
"summary": project.plan.prompt_context,
"definition": project.plan.prompt_context,
"flows": {"first": null, "loop": null, "finally": null},
"krs": [],
"initiative_ids": ["initialization-initiative"],
"team_ids": ["initialization-team"]
}],
"items": [{
"id": task.task.plan.id.as_str(),
"identifier": task.task.plan.identifier,
"url": null,
"name": task.task.plan.title,
"description": task.task.plan.description,
"rank": 1,
"completed": false,
"project_id": project.plan.id.as_str(),
"project": project.plan.slug,
"team_id": "initialization-team",
"assignee": null
}]
});
runtime
.block_on(task.store.put_pm_snapshot(PmSnapshotRow {
wave_id: task.task.wave_id.clone(),
provider: "linear".to_string(),
initiative: "initialization-initiative".to_string(),
synced_at: time::OffsetDateTime::now_utc().unix_timestamp(),
payload: serde_json::to_string(&payload).expect("serialize PM snapshot"),
}))
.expect("seed roadmap planning");
let run_lf = |args: &[&str]| {
Command::new(env!("CARGO_BIN_EXE_lf"))
.args(args)
.env("LF_DB_PATH", home.path().join("loopflow.db"))
.env_remove("LF_WAVE_ID")
.current_dir(repo.path())
.output()
.expect("run lf read surface")
};
let status = run_lf(&["task", "status", "INF-123", "--json"]);
assert!(
status.status.success(),
"status stays readable: {}",
String::from_utf8_lossy(&status.stderr)
);
let status: serde_json::Value = serde_json::from_slice(&status.stdout).expect("status JSON");
assert_eq!(status["actions"]["recommended"], "no_action");
assert!(status["actions"]["reason"]
.as_str()
.expect("status action reason")
.contains("is initializing worktree"));
let wait = run_lf(&["task", "wait", "INF-123", "--timeout", "0s", "--json"]);
assert!(
wait.status.success(),
"wait stays readable: {}",
String::from_utf8_lossy(&wait.stderr)
);
let wait: serde_json::Value = serde_json::from_slice(&wait.stdout).expect("wait JSON");
assert_eq!(wait["actions"], status["actions"]);
let roadmap = run_lf(&["roadmap", "--wave", "task-pr-tests", "--json"]);
assert!(
roadmap.status.success(),
"roadmap stays readable: {}",
String::from_utf8_lossy(&roadmap.stderr)
);
let roadmap: serde_json::Value = serde_json::from_slice(&roadmap.stdout).expect("roadmap JSON");
let wave = &roadmap["waves"][0];
assert_eq!(wave["projects"]["state"], "ok", "roadmap wave: {wave:#}");
let roadmap_task = &wave["projects"]["items"][0]["tasks"][0];
assert_eq!(roadmap_task["task"]["identifier"], "INF-123");
assert_eq!(roadmap_task["actions"]["recommended"], "no_action");
assert!(roadmap_task["condition"]["reason"]
.as_str()
.expect("roadmap condition reason")
.contains("is initializing worktree"));
let projected =
task_snapshot(&task_status("INF-123").expect("read Task")).expect("project Task status");
assert_eq!(projected.actions.recommended, Some(TaskAction::NoAction));
rusqlite::Connection::open(home.path().join("loopflow.db"))
.expect("open stale initialization fixture")
.execute(
"UPDATE task_events SET created_at=?2 WHERE task_id=?1",
rusqlite::params![
task.task.id.as_str(),
time::OffsetDateTime::now_utc().unix_timestamp() - 301,
],
)
.expect("age the initialization marker");
let stale = run_lf(&["task", "status", "INF-123", "--json"]);
assert!(
stale.status.success(),
"stale initialization stays readable"
);
let stale: serde_json::Value =
serde_json::from_slice(&stale.stdout).expect("stale status JSON");
assert_eq!(stale["actions"]["recommended"], "no_action");
assert!(stale["actions"]["reason"]
.as_str()
.expect("stale action reason")
.contains("initialization did not complete"));
let stale_roadmap = run_lf(&["roadmap", "--wave", "task-pr-tests", "--json"]);
assert!(
stale_roadmap.status.success(),
"stale roadmap stays readable"
);
let stale_roadmap: serde_json::Value =
serde_json::from_slice(&stale_roadmap.stdout).expect("stale roadmap JSON");
let stale_condition =
&stale_roadmap["waves"][0]["projects"]["items"][0]["tasks"][0]["condition"];
assert_eq!(stale_condition["state"], "blocked");
assert!(stale_condition["reason"]
.as_str()
.expect("stale roadmap reason")
.contains("initialization did not complete"));
}
#[test]
fn missing_worktree_status_is_actionable_and_read_only() {
let home = tempfile::tempdir().expect("Task home");
let _env = EnvGuard::with_lf_home(&[], home.path());
let (task, missing_path, branch) = {
let repo = TestRepo::new();
let base = repo.head_sha();
let branch = "jack/missing-worktree";
repo.create_branch(branch);
let task = register_unrun_task(home.path(), repo.path(), branch, &base);
(task, repo.path().to_path_buf(), branch.to_string())
};
assert!(!missing_path.exists(), "fixture worktree is absent");
let runtime = tokio::runtime::Runtime::new().expect("missing Task runtime");
let before_task = runtime
.block_on(task.store.get_task(&task.task.id))
.expect("read Task before status")
.expect("Task exists before status");
let before_prs = runtime
.block_on(task.store.task_prs(&task.task.id))
.expect("read PRs before status");
let status = task_status("INF-123").expect("status survives the absent worktree");
let snapshot = task_snapshot(&status).expect("project missing-worktree status");
assert_eq!(snapshot.actions.recommended, Some(TaskAction::NoAction));
assert!(snapshot
.actions
.reason
.contains(&missing_path.display().to_string()));
assert!(snapshot.actions.reason.contains(&branch));
assert!(snapshot.actions.reason.contains("lf task resume INF-123"));
assert!(snapshot
.actions
.reason
.contains("identity and PR history are unchanged"));
assert_eq!(
runtime
.block_on(task.store.get_task(&task.task.id))
.expect("reread Task after status")
.expect("Task remains registered"),
before_task
);
assert_eq!(
runtime
.block_on(task.store.task_prs(&task.task.id))
.expect("reread PRs after status"),
before_prs
);
}