use chrono::{TimeZone, Utc};
use shiplog::ids::EventId;
use shiplog::schema::event::EventEnvelope;
use shiplog::schema::workstream::{Workstream, WorkstreamStats, WorkstreamsFile};
use shiplog::workstreams::RepoClusterer;
use shiplog::workstreams::{
CURATED_FILENAME, SUGGESTED_FILENAME, WorkstreamManager, load_or_cluster, write_workstreams,
};
use tempfile::tempdir;
fn make_event(repo_name: &str, id: &str) -> EventEnvelope {
EventEnvelope {
id: EventId::from_parts(["github", id]),
kind: shiplog::schema::event::EventKind::PullRequest,
occurred_at: Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).single().unwrap(),
actor: shiplog::schema::event::Actor {
login: "tester".into(),
id: None,
},
repo: shiplog::schema::event::RepoRef {
full_name: repo_name.into(),
html_url: Some(format!("https://example.com/{repo_name}")),
visibility: shiplog::schema::event::RepoVisibility::Unknown,
},
payload: shiplog::schema::event::EventPayload::PullRequest(
shiplog::schema::event::PullRequestEvent {
number: 1,
title: "Test PR".into(),
state: shiplog::schema::event::PullRequestState::Merged,
created_at: Utc::now(),
merged_at: Some(Utc::now()),
additions: Some(10),
deletions: Some(2),
changed_files: Some(1),
touched_paths_hint: vec![],
window: None,
},
),
tags: vec![],
links: vec![],
source: shiplog::schema::event::SourceRef {
system: shiplog::schema::event::SourceSystem::Github,
url: None,
opaque_id: None,
},
}
}
fn make_file(title: &str) -> WorkstreamsFile {
WorkstreamsFile {
version: 1,
generated_at: Utc::now(),
workstreams: vec![Workstream {
id: shiplog::ids::WorkstreamId::from_parts(["repo", title]),
title: title.to_string(),
summary: None,
tags: vec!["repo".into()],
stats: WorkstreamStats::zero(),
events: vec![EventId::from_parts(["event", title])],
receipts: vec![],
}],
}
}
#[test]
fn integration_prefers_curated_workstreams() {
let temp_dir = tempdir().unwrap();
let curated = temp_dir.path().join(CURATED_FILENAME);
let suggested = temp_dir.path().join(SUGGESTED_FILENAME);
write_workstreams(&curated, &make_file("curated")).unwrap();
write_workstreams(&suggested, &make_file("suggested")).unwrap();
let loaded = WorkstreamManager::try_load(temp_dir.path())
.unwrap()
.unwrap();
assert_eq!(loaded.workstreams[0].title, "curated");
}
#[test]
fn integration_falls_back_to_suggested_when_curated_missing() {
let temp_dir = tempdir().unwrap();
let suggested = temp_dir.path().join(SUGGESTED_FILENAME);
write_workstreams(&suggested, &make_file("suggested")).unwrap();
let loaded = WorkstreamManager::try_load(temp_dir.path())
.unwrap()
.unwrap();
assert_eq!(loaded.workstreams[0].title, "suggested");
}
#[test]
fn integration_generates_when_no_files_exist() {
let temp_dir = tempdir().unwrap();
let events = [make_event("acme/app", "1"), make_event("acme/app", "2")];
let loaded =
WorkstreamManager::load_effective(temp_dir.path(), &RepoClusterer, &events).unwrap();
assert_eq!(loaded.workstreams.len(), 1);
assert_eq!(loaded.workstreams[0].title, "acme/app");
assert_eq!(
WorkstreamManager::suggested_path(temp_dir.path())
.file_name()
.unwrap(),
SUGGESTED_FILENAME
);
assert!(WorkstreamManager::suggested_path(temp_dir.path()).exists());
}
#[test]
fn integration_load_or_cluster_with_real_clusterer() {
let events = [make_event("acme/app", "1"), make_event("acme/lib", "2")];
let loaded = load_or_cluster(None, &RepoClusterer, &events).unwrap();
assert_eq!(loaded.workstreams.len(), 2);
}
#[test]
fn integration_write_suggested_then_load_effective_reads_it() {
let temp_dir = tempdir().unwrap();
let ws = make_file("auto-suggested");
WorkstreamManager::write_suggested(temp_dir.path(), &ws).unwrap();
let loaded = WorkstreamManager::load_effective(temp_dir.path(), &RepoClusterer, &[]).unwrap();
assert_eq!(loaded.workstreams[0].title, "auto-suggested");
}
#[test]
fn integration_curated_overrides_suggested_on_load_effective() {
let temp_dir = tempdir().unwrap();
WorkstreamManager::write_suggested(temp_dir.path(), &make_file("old-suggested")).unwrap();
write_workstreams(
&WorkstreamManager::curated_path(temp_dir.path()),
&make_file("user-curated"),
)
.unwrap();
let loaded = WorkstreamManager::load_effective(temp_dir.path(), &RepoClusterer, &[]).unwrap();
assert_eq!(loaded.workstreams[0].title, "user-curated");
}
#[test]
fn integration_multiple_workstreams_roundtrip() {
let temp_dir = tempdir().unwrap();
let ws = WorkstreamsFile {
version: 1,
generated_at: Utc::now(),
workstreams: vec![
Workstream {
id: shiplog::ids::WorkstreamId::from_parts(["repo", "a"]),
title: "repo-a".into(),
summary: Some("First".into()),
tags: vec!["repo".into()],
stats: WorkstreamStats {
pull_requests: 3,
reviews: 1,
manual_events: 0,
},
events: vec![EventId::from_parts(["e", "1"])],
receipts: vec![EventId::from_parts(["e", "1"])],
},
Workstream {
id: shiplog::ids::WorkstreamId::from_parts(["repo", "b"]),
title: "repo-b".into(),
summary: None,
tags: vec!["repo".into()],
stats: WorkstreamStats::zero(),
events: vec![],
receipts: vec![],
},
],
};
let path = temp_dir.path().join(CURATED_FILENAME);
write_workstreams(&path, &ws).unwrap();
let loaded = WorkstreamManager::try_load(temp_dir.path())
.unwrap()
.unwrap();
assert_eq!(loaded.workstreams.len(), 2);
assert_eq!(loaded.workstreams[0].title, "repo-a");
assert_eq!(loaded.workstreams[1].title, "repo-b");
}