use anyhow::Result;
use shiplog_ports::Renderer;
use shiplog_schema::coverage::CoverageManifest;
use shiplog_schema::event::{EventEnvelope, EventKind};
use shiplog_schema::workstream::WorkstreamsFile;
use shiplog_workstreams::WORKSTREAM_RECEIPT_RENDER_LIMIT;
use std::collections::HashMap;
pub mod receipt;
pub use receipt::{format_receipt_markdown, manual_type_emoji};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SectionOrder {
#[default]
Default,
CoverageFirst,
}
pub struct MarkdownRenderer {
pub section_order: SectionOrder,
}
impl Default for MarkdownRenderer {
fn default() -> Self {
Self {
section_order: SectionOrder::Default,
}
}
}
impl MarkdownRenderer {
pub fn new() -> Self {
Self::default()
}
pub fn with_section_order(mut self, order: SectionOrder) -> Self {
self.section_order = order;
self
}
}
impl Renderer for MarkdownRenderer {
fn render_packet_markdown(
&self,
user: &str,
window_label: &str,
events: &[EventEnvelope],
workstreams: &WorkstreamsFile,
coverage: &CoverageManifest,
) -> Result<String> {
let mut out = String::new();
match self.section_order {
SectionOrder::Default => {
render_summary(&mut out, user, window_label, events, workstreams, coverage);
render_workstreams(&mut out, events, workstreams);
render_receipts(&mut out, events, workstreams);
render_coverage(&mut out, coverage);
}
SectionOrder::CoverageFirst => {
render_coverage(&mut out, coverage);
render_summary(&mut out, user, window_label, events, workstreams, coverage);
render_workstreams(&mut out, events, workstreams);
render_receipts(&mut out, events, workstreams);
}
}
render_appendix(&mut out, events, workstreams);
render_file_artifacts(&mut out);
Ok(out)
}
}
fn render_summary(
out: &mut String,
_user: &str,
window_label: &str,
events: &[EventEnvelope],
workstreams: &WorkstreamsFile,
coverage: &CoverageManifest,
) {
out.push_str("# Summary\n\n");
out.push_str(&format!("**Window:** {}\n\n", window_label));
out.push_str(&format!(
"**Workstreams:** {}\n\n",
workstreams.workstreams.len()
));
let pr_count = events
.iter()
.filter(|e| matches!(e.kind, EventKind::PullRequest))
.count();
let review_count = events
.iter()
.filter(|e| matches!(e.kind, EventKind::Review))
.count();
let manual_count = events
.iter()
.filter(|e| matches!(e.kind, EventKind::Manual))
.count();
out.push_str(&format!(
"**Events:** {} PRs, {} reviews, {} manual\n\n",
pr_count, review_count, manual_count
));
out.push_str(&format!("**Coverage:** {:?}\n\n", coverage.completeness));
out.push_str(&format!("**Sources:** {}\n\n", coverage.sources.join(", ")));
if !coverage.warnings.is_empty() {
out.push_str("**Warnings:**\n");
for w in &coverage.warnings {
out.push_str(&format!(" - ⚠️ {}\n", w));
}
out.push('\n');
}
}
fn render_workstreams(out: &mut String, events: &[EventEnvelope], workstreams: &WorkstreamsFile) {
out.push_str("## Workstreams\n\n");
if workstreams.workstreams.is_empty() {
out.push_str("_No workstreams found_\n\n");
return;
}
let _by_id: HashMap<String, &EventEnvelope> =
events.iter().map(|e| (e.id.0.clone(), e)).collect();
for ws in &workstreams.workstreams {
out.push_str(&format!("### {}\n\n", ws.title));
if let Some(s) = &ws.summary {
out.push_str(s);
out.push_str("\n\n");
}
out.push_str("**Claim scaffolds**\n\n");
out.push_str("- Problem: _fill_\n");
out.push_str("- What I shipped: _fill_\n");
out.push_str("- Why it mattered: _fill_\n");
out.push_str("- Result: _fill_\n\n");
out.push_str(&format!(
"_PRs: {}, Reviews: {}, Manual: {}_\n\n",
ws.stats.pull_requests, ws.stats.reviews, ws.stats.manual_events
));
}
}
fn render_receipts(out: &mut String, events: &[EventEnvelope], workstreams: &WorkstreamsFile) {
out.push_str("## Receipts\n\n");
if workstreams.workstreams.is_empty() {
out.push_str("_No workstreams, no receipts_\n\n");
return;
}
let by_id: HashMap<String, &EventEnvelope> =
events.iter().map(|e| (e.id.0.clone(), e)).collect();
let mut shown_receipts: HashMap<String, Vec<String>> = HashMap::new();
for ws in &workstreams.workstreams {
out.push_str(&format!("### Workstream: {}\n\n", ws.title));
let (main_receipts, appendix_receipts): (Vec<_>, Vec<_>) =
if ws.receipts.len() <= WORKSTREAM_RECEIPT_RENDER_LIMIT {
(ws.receipts.clone(), Vec::new())
} else {
let (main, appendix) = ws.receipts.split_at(WORKSTREAM_RECEIPT_RENDER_LIMIT);
(main.to_vec(), appendix.to_vec())
};
shown_receipts.insert(
ws.id.0.clone(),
main_receipts.iter().map(|r| r.0.clone()).collect(),
);
if main_receipts.is_empty() {
out.push_str("- (none)\n\n");
} else {
for id in &main_receipts {
if let Some(ev) = by_id.get(&id.0) {
out.push_str(&format!("{}\n", format_receipt_markdown(ev)));
}
}
if !appendix_receipts.is_empty() {
out.push_str(&format!(
"- *... and {} more in [Appendix](#appendix-receipts)*\n",
appendix_receipts.len()
));
}
out.push('\n');
}
}
}
fn render_coverage(out: &mut String, coverage: &CoverageManifest) {
out.push_str("## Coverage\n\n");
out.push_str(&format!(
"- **Date window:** {} to {}\n",
coverage.window.since, coverage.window.until
));
out.push_str(&format!("- **Mode:** {}\n", coverage.mode));
out.push_str(&format!("- **Sources:** {}\n", coverage.sources.join(", ")));
out.push_str(&format!(
"- **Completeness:** {:?}\n",
coverage.completeness
));
if !coverage.slices.is_empty() {
out.push_str(&format!("- **Query slices:** {}\n", coverage.slices.len()));
let partial_count = coverage
.slices
.iter()
.filter(|s| s.incomplete_results.unwrap_or(false))
.count();
if partial_count > 0 {
out.push_str(&format!(
" - ⚠️ {} slices had incomplete results\n",
partial_count
));
}
let capped_slices: Vec<_> = coverage
.slices
.iter()
.filter(|s| s.total_count > s.fetched)
.collect();
if !capped_slices.is_empty() {
out.push_str(" - **Slicing applied (API caps):**\n");
for slice in capped_slices.iter().take(3) {
let pct = if slice.total_count > 0 {
(slice.fetched as f64 / slice.total_count as f64 * 100.0) as u64
} else {
100
};
out.push_str(&format!(
" - {}: fetched {}/{} ({}%)\n",
slice.query, slice.fetched, slice.total_count, pct
));
}
if capped_slices.len() > 3 {
out.push_str(&format!(" - ... and {} more\n", capped_slices.len() - 3));
}
}
}
out.push('\n');
}
fn render_appendix(out: &mut String, events: &[EventEnvelope], workstreams: &WorkstreamsFile) {
out.push_str("## Appendix: All Receipts\n\n");
if workstreams.workstreams.is_empty() {
return;
}
let by_id: HashMap<String, &EventEnvelope> =
events.iter().map(|e| (e.id.0.clone(), e)).collect();
for ws in &workstreams.workstreams {
if ws.events.is_empty() {
continue;
}
out.push_str(&format!("### {}\n\n", ws.title));
for event_id in &ws.events {
if let Some(ev) = by_id.get(&event_id.0) {
out.push_str(&format!("{}\n", format_receipt_markdown(ev)));
}
}
out.push('\n');
}
out.push_str("---\n\n");
}
fn render_file_artifacts(out: &mut String) {
out.push_str("## File Artifacts\n\n");
out.push_str("- `ledger.events.jsonl` (canonical events)\n");
out.push_str("- `coverage.manifest.json` (completeness + slicing)\n");
out.push_str("- `workstreams.yaml` (editable clustering)\n");
out.push_str("- `manual_events.yaml` (non-GitHub work)\n");
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{NaiveDate, TimeZone, Utc};
use shiplog_ids::{EventId, RunId, WorkstreamId};
use shiplog_schema::coverage::*;
use shiplog_schema::event::*;
use shiplog_schema::workstream::*;
fn create_test_pr(id: &str, number: u64, title: &str) -> EventEnvelope {
EventEnvelope {
id: EventId::from_parts(["pr", id]),
kind: EventKind::PullRequest,
occurred_at: Utc.timestamp_opt(0, 0).unwrap(),
actor: Actor {
login: "octo".into(),
id: None,
},
repo: RepoRef {
full_name: "owner/repo".into(),
html_url: None,
visibility: RepoVisibility::Public,
},
payload: EventPayload::PullRequest(PullRequestEvent {
number,
title: title.into(),
state: PullRequestState::Merged,
created_at: Utc.timestamp_opt(0, 0).unwrap(),
merged_at: Some(Utc.timestamp_opt(0, 0).unwrap()),
additions: Some(10),
deletions: Some(5),
changed_files: Some(2),
touched_paths_hint: vec![],
window: None,
}),
tags: vec![],
links: vec![Link {
label: "pr".into(),
url: format!("https://github.com/owner/repo/pull/{}", number),
}],
source: SourceRef {
system: SourceSystem::Github,
url: None,
opaque_id: Some(id.into()),
},
}
}
fn create_test_manual(id: &str, event_type: ManualEventType, title: &str) -> EventEnvelope {
EventEnvelope {
id: EventId::from_parts(["manual", id]),
kind: EventKind::Manual,
occurred_at: Utc.timestamp_opt(0, 0).unwrap(),
actor: Actor {
login: "user".into(),
id: None,
},
repo: RepoRef {
full_name: "owner/repo".into(),
html_url: None,
visibility: RepoVisibility::Public,
},
payload: EventPayload::Manual(ManualEvent {
event_type: event_type.clone(),
title: title.into(),
description: None,
started_at: None,
ended_at: None,
impact: None,
}),
tags: vec![],
links: vec![],
source: SourceRef {
system: SourceSystem::Manual,
url: None,
opaque_id: Some(id.into()),
},
}
}
#[test]
fn test_snapshot_empty_packet() {
let renderer = MarkdownRenderer::new();
let events: Vec<EventEnvelope> = vec![];
let workstreams = WorkstreamsFile {
version: 1,
generated_at: Utc::now(),
workstreams: vec![],
};
let coverage = CoverageManifest {
run_id: RunId::now("test"),
generated_at: Utc::now(),
user: "test".into(),
window: TimeWindow {
since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
until: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
},
mode: "test".into(),
sources: vec![],
slices: vec![],
warnings: vec![],
completeness: Completeness::Complete,
};
let result = renderer
.render_packet_markdown("test", "2024", &events, &workstreams, &coverage)
.unwrap();
insta::assert_snapshot!(result);
}
#[test]
fn test_snapshot_full_packet() {
let renderer = MarkdownRenderer::new();
let events = vec![
create_test_pr("1", 1, "Fix authentication bug"),
create_test_manual("2", ManualEventType::Incident, "Handle production incident"),
];
let workstreams = WorkstreamsFile {
version: 1,
generated_at: Utc::now(),
workstreams: vec![Workstream {
id: WorkstreamId::from_parts(["ws", "1"]),
title: "Authentication".into(),
summary: Some("Fixed auth bugs and improved security".into()),
tags: vec![],
receipts: vec![EventId::from_parts(["pr", "1"])],
events: vec![EventId::from_parts(["pr", "1"])],
stats: WorkstreamStats {
pull_requests: 1,
reviews: 0,
manual_events: 0,
},
}],
};
let coverage = CoverageManifest {
run_id: RunId::now("test"),
generated_at: Utc::now(),
user: "test".into(),
window: TimeWindow {
since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
until: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
},
mode: "test".into(),
sources: vec!["github".into(), "manual".into()],
slices: vec![],
warnings: vec![],
completeness: Completeness::Complete,
};
let result = renderer
.render_packet_markdown("test", "2024", &events, &workstreams, &coverage)
.unwrap();
insta::assert_snapshot!(result);
}
#[test]
fn test_snapshot_partial_coverage() {
let renderer = MarkdownRenderer::new();
let events = vec![create_test_pr("1", 1, "Add feature")];
let workstreams = WorkstreamsFile {
version: 1,
generated_at: Utc::now(),
workstreams: vec![Workstream {
id: WorkstreamId::from_parts(["ws", "1"]),
title: "Feature".into(),
summary: None,
tags: vec![],
receipts: vec![EventId::from_parts(["pr", "1"])],
events: vec![EventId::from_parts(["pr", "1"])],
stats: WorkstreamStats {
pull_requests: 1,
reviews: 0,
manual_events: 0,
},
}],
};
let coverage = CoverageManifest {
run_id: RunId::now("test"),
generated_at: Utc::now(),
user: "test".into(),
window: TimeWindow {
since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
until: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
},
mode: "test".into(),
sources: vec!["github".into()],
slices: vec![CoverageSlice {
window: TimeWindow {
since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
until: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
},
query: "test".into(),
total_count: 100,
fetched: 50,
incomplete_results: Some(true),
notes: vec![],
}],
warnings: vec!["API rate limit hit".into()],
completeness: Completeness::Partial,
};
let result = renderer
.render_packet_markdown("test", "2024", &events, &workstreams, &coverage)
.unwrap();
insta::assert_snapshot!(result);
}
#[test]
fn test_snapshot_coverage_first_section_order() {
let renderer = MarkdownRenderer::new().with_section_order(SectionOrder::CoverageFirst);
let events = vec![
create_test_pr("1", 1, "Fix bug"),
create_test_pr("2", 2, "Add feature"),
];
let workstreams = WorkstreamsFile {
version: 1,
generated_at: Utc::now(),
workstreams: vec![Workstream {
id: WorkstreamId::from_parts(["ws", "1"]),
title: "Workstream 1".into(),
summary: Some("Summary".into()),
tags: vec![],
receipts: vec![EventId::from_parts(["pr", "1"])],
events: vec![EventId::from_parts(["pr", "1"])],
stats: WorkstreamStats {
pull_requests: 1,
reviews: 0,
manual_events: 0,
},
}],
};
let coverage = CoverageManifest {
run_id: RunId::now("test"),
generated_at: Utc::now(),
user: "test".into(),
window: TimeWindow {
since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
until: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
},
mode: "merged".into(),
sources: vec!["github".into()],
slices: vec![],
warnings: vec![],
completeness: Completeness::Complete,
};
let result = renderer
.render_packet_markdown("testuser", "2024-W01", &events, &workstreams, &coverage)
.unwrap();
assert!(result.starts_with("## Coverage"));
}
#[test]
fn test_snapshot_events_with_reviews() {
let renderer = MarkdownRenderer::new();
let pr_event = create_test_pr("1", 1, "Add feature");
let review_event = EventEnvelope {
id: EventId::from_parts(["review", "1"]),
kind: EventKind::Review,
occurred_at: Utc.timestamp_opt(0, 0).unwrap(),
actor: Actor {
login: "reviewer".into(),
id: None,
},
repo: RepoRef {
full_name: "owner/repo".into(),
html_url: None,
visibility: RepoVisibility::Public,
},
payload: EventPayload::Review(ReviewEvent {
pull_number: 1,
pull_title: "Add feature".into(),
submitted_at: Utc.timestamp_opt(0, 0).unwrap(),
state: "approved".into(),
window: None,
}),
tags: vec![],
links: vec![Link {
label: "pr".into(),
url: "https://github.com/owner/repo/pull/1".into(),
}],
source: SourceRef {
system: SourceSystem::Github,
url: None,
opaque_id: None,
},
};
let events = vec![pr_event, review_event];
let workstreams = WorkstreamsFile {
version: 1,
generated_at: Utc::now(),
workstreams: vec![Workstream {
id: WorkstreamId::from_parts(["ws", "1"]),
title: "Feature Work".into(),
summary: None,
tags: vec![],
receipts: vec![],
events: vec![
EventId::from_parts(["pr", "1"]),
EventId::from_parts(["review", "1"]),
],
stats: WorkstreamStats {
pull_requests: 1,
reviews: 1,
manual_events: 0,
},
}],
};
let coverage = CoverageManifest {
run_id: RunId::now("test"),
generated_at: Utc::now(),
user: "test".into(),
window: TimeWindow {
since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
until: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
},
mode: "test".into(),
sources: vec!["github".into()],
slices: vec![],
warnings: vec![],
completeness: Completeness::Complete,
};
let result = renderer
.render_packet_markdown("test", "2024", &events, &workstreams, &coverage)
.unwrap();
assert!(result.contains("1 PRs, 1 reviews"));
}
#[test]
fn test_snapshot_multiple_workstreams() {
let renderer = MarkdownRenderer::new();
let events = vec![
create_test_pr("1", 1, "Feature A"),
create_test_pr("2", 2, "Feature B"),
create_test_pr("3", 3, "Fix bug"),
];
let workstreams = WorkstreamsFile {
version: 1,
generated_at: Utc::now(),
workstreams: vec![
Workstream {
id: WorkstreamId::from_parts(["ws", "a"]),
title: "Feature A".into(),
summary: Some("Work on feature A".into()),
tags: vec![],
receipts: vec![EventId::from_parts(["pr", "1"])],
events: vec![EventId::from_parts(["pr", "1"])],
stats: WorkstreamStats {
pull_requests: 1,
reviews: 0,
manual_events: 0,
},
},
Workstream {
id: WorkstreamId::from_parts(["ws", "b"]),
title: "Feature B & Bugfix".into(),
summary: Some("Work on feature B and bugfix".into()),
tags: vec![],
receipts: vec![
EventId::from_parts(["pr", "2"]),
EventId::from_parts(["pr", "3"]),
],
events: vec![
EventId::from_parts(["pr", "2"]),
EventId::from_parts(["pr", "3"]),
],
stats: WorkstreamStats {
pull_requests: 2,
reviews: 0,
manual_events: 0,
},
},
],
};
let coverage = CoverageManifest {
run_id: RunId::now("test"),
generated_at: Utc::now(),
user: "test".into(),
window: TimeWindow {
since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
until: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
},
mode: "test".into(),
sources: vec!["github".into()],
slices: vec![],
warnings: vec![],
completeness: Completeness::Complete,
};
let result = renderer
.render_packet_markdown("test", "2024", &events, &workstreams, &coverage)
.unwrap();
assert!(result.contains("**Workstreams:** 2"));
}
fn create_test_review(id: &str, state: &str, with_link: bool) -> EventEnvelope {
let links = if with_link {
vec![Link {
label: "pr".into(),
url: "https://github.com/owner/repo/pull/42".to_string(),
}]
} else {
vec![]
};
EventEnvelope {
id: EventId::from_parts(["review", id]),
kind: EventKind::Review,
occurred_at: Utc.timestamp_opt(0, 0).unwrap(),
actor: Actor {
login: "reviewer".into(),
id: None,
},
repo: RepoRef {
full_name: "owner/repo".into(),
html_url: None,
visibility: RepoVisibility::Public,
},
payload: EventPayload::Review(ReviewEvent {
pull_number: 42,
pull_title: "Some PR".into(),
submitted_at: Utc.timestamp_opt(0, 0).unwrap(),
state: state.into(),
window: None,
}),
tags: vec![],
links,
source: SourceRef {
system: SourceSystem::Github,
url: None,
opaque_id: Some(id.into()),
},
}
}
fn make_coverage(slices: Vec<CoverageSlice>, warnings: Vec<String>) -> CoverageManifest {
CoverageManifest {
run_id: RunId::now("test"),
generated_at: Utc::now(),
user: "test".into(),
window: TimeWindow {
since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
until: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
},
mode: "test".into(),
sources: vec!["github".into()],
slices,
warnings,
completeness: Completeness::Complete,
}
}
#[test]
fn coverage_complete_slices_no_incomplete_message() {
let coverage = make_coverage(
vec![CoverageSlice {
window: TimeWindow {
since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
until: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
},
query: "test".into(),
total_count: 10,
fetched: 10,
incomplete_results: Some(false),
notes: vec![],
}],
vec![],
);
let mut out = String::new();
render_coverage(&mut out, &coverage);
assert!(!out.contains("incomplete results"));
}
#[test]
fn coverage_with_total_equal_fetched_no_slicing_message() {
let coverage = make_coverage(
vec![CoverageSlice {
window: TimeWindow {
since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
until: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
},
query: "test".into(),
total_count: 50,
fetched: 50,
incomplete_results: Some(false),
notes: vec![],
}],
vec![],
);
let mut out = String::new();
render_coverage(&mut out, &coverage);
assert!(!out.contains("Slicing applied"));
}
#[test]
fn coverage_with_capped_slices_shows_slicing_applied() {
let coverage = make_coverage(
vec![CoverageSlice {
window: TimeWindow {
since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
until: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
},
query: "test".into(),
total_count: 100,
fetched: 50,
incomplete_results: Some(false),
notes: vec![],
}],
vec![],
);
let mut out = String::new();
render_coverage(&mut out, &coverage);
assert!(out.contains("Slicing applied"));
assert!(out.contains("fetched 50/100"));
}
#[test]
fn coverage_with_4_plus_capped_slices_shows_and_more() {
let slices: Vec<CoverageSlice> = (0..5)
.map(|i| CoverageSlice {
window: TimeWindow {
since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
until: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
},
query: format!("query-{i}"),
total_count: 100,
fetched: 50,
incomplete_results: Some(false),
notes: vec![],
})
.collect();
let coverage = make_coverage(slices, vec![]);
let mut out = String::new();
render_coverage(&mut out, &coverage);
assert!(out.contains("... and 2 more"));
}
#[test]
fn coverage_with_exactly_3_capped_slices_no_and_more() {
let slices: Vec<CoverageSlice> = (0..3)
.map(|i| CoverageSlice {
window: TimeWindow {
since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
until: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
},
query: format!("query-{i}"),
total_count: 100,
fetched: 50,
incomplete_results: Some(false),
notes: vec![],
})
.collect();
let coverage = make_coverage(slices, vec![]);
let mut out = String::new();
render_coverage(&mut out, &coverage);
assert!(out.contains("Slicing applied"));
assert!(!out.contains("... and"));
}
#[test]
fn coverage_empty_slices_no_incomplete_no_slicing() {
let coverage = make_coverage(vec![], vec![]);
let mut out = String::new();
render_coverage(&mut out, &coverage);
assert!(!out.contains("incomplete results"));
assert!(!out.contains("Slicing applied"));
assert!(!out.contains("Query slices"));
}
#[test]
fn coverage_none_incomplete_results_no_warning() {
let coverage = make_coverage(
vec![CoverageSlice {
window: TimeWindow {
since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
until: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
},
query: "test".into(),
total_count: 10,
fetched: 10,
incomplete_results: None,
notes: vec![],
}],
vec![],
);
let mut out = String::new();
render_coverage(&mut out, &coverage);
assert!(!out.contains("incomplete results"));
}
#[test]
fn review_event_shows_review_tag_and_state() {
let ev = create_test_review("r1", "approved", false);
let formatted = format_receipt_markdown(&ev);
assert!(formatted.contains("[Review]"));
assert!(formatted.contains("approved"));
}
#[test]
fn review_with_pr_link_shows_markdown_link() {
let ev = create_test_review("r2", "changes_requested", true);
let formatted = format_receipt_markdown(&ev);
assert!(formatted.contains("[Review]"));
assert!(formatted.contains("[owner/repo]"));
assert!(formatted.contains("(https://github.com/owner/repo/pull/42)"));
}
#[test]
fn review_without_pr_link_shows_plain_repo() {
let ev = create_test_review("r3", "approved", false);
let formatted = format_receipt_markdown(&ev);
assert!(formatted.contains("owner/repo"));
assert!(!formatted.contains("]("));
}
#[test]
fn test_snapshot_events_with_all_manual_types() {
let renderer = MarkdownRenderer::new();
let events = vec![
create_test_manual("1", ManualEventType::Note, "Take notes"),
create_test_manual("2", ManualEventType::Incident, "Fix outage"),
create_test_manual("3", ManualEventType::Design, "Design review"),
create_test_manual("4", ManualEventType::Mentoring, "Mentor junior"),
create_test_manual("5", ManualEventType::Launch, "Launch feature"),
create_test_manual("6", ManualEventType::Migration, "Migrate data"),
create_test_manual("7", ManualEventType::Review, "Code review"),
create_test_manual("8", ManualEventType::Other, "Other work"),
];
let workstreams = WorkstreamsFile {
version: 1,
generated_at: Utc::now(),
workstreams: vec![Workstream {
id: WorkstreamId::from_parts(["ws", "1"]),
title: "Mixed Work".into(),
summary: None,
tags: vec![],
receipts: vec![],
events: events.iter().map(|e| e.id.clone()).collect(),
stats: WorkstreamStats {
pull_requests: 0,
reviews: 0,
manual_events: 8,
},
}],
};
let coverage = CoverageManifest {
run_id: RunId::now("test"),
generated_at: Utc::now(),
user: "test".into(),
window: TimeWindow {
since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
until: NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
},
mode: "test".into(),
sources: vec!["manual".into()],
slices: vec![],
warnings: vec![],
completeness: Completeness::Complete,
};
let result = renderer
.render_packet_markdown("test", "2024", &events, &workstreams, &coverage)
.unwrap();
assert!(result.contains("0 PRs, 0 reviews, 8 manual"));
assert!(result.contains("📝")); assert!(result.contains("🚨")); assert!(result.contains("🏗️")); assert!(result.contains("👨🏫")); assert!(result.contains("🚀")); assert!(result.contains("🔄")); assert!(result.contains("👀")); assert!(result.contains("📌")); }
}