Skip to main content

verifyos_cli/
ci_comment.rs

1use crate::doctor::DoctorReport;
2use crate::report::AgentPack;
3use miette::{IntoDiagnostic, Result};
4use std::path::Path;
5
6const STICKY_MARKER: &str = "<!-- voc-analysis-comment -->";
7
8pub fn render_workflow_pr_comment(
9    output_dir: &Path,
10    scan_exit: i32,
11    doctor_exit: i32,
12    sticky_marker: bool,
13    from_plan: bool,
14    plan_path: Option<&Path>,
15) -> Result<String> {
16    let repair_plan_path = plan_path
17        .map(Path::to_path_buf)
18        .unwrap_or_else(|| output_dir.join("repair-plan.md"));
19    if from_plan && repair_plan_path.exists() {
20        let repair_plan = std::fs::read_to_string(&repair_plan_path).into_diagnostic()?;
21        return Ok(with_marker(repair_plan.trim(), sticky_marker));
22    }
23
24    let comment_path = output_dir.join("pr-comment.md");
25    if comment_path.exists() {
26        let comment = std::fs::read_to_string(&comment_path).into_diagnostic()?;
27        return Ok(with_marker(comment.trim(), sticky_marker));
28    }
29
30    let doctor_path = output_dir.join("doctor.json");
31    let agent_pack_path = output_dir.join(".verifyos-agent").join("agent-pack.json");
32    let findings = load_agent_pack_findings(&agent_pack_path);
33    let doctor_summary = load_doctor_summary(&doctor_path);
34
35    let body = [
36        "## voc analysis",
37        "",
38        &format!("- Findings: **{}**", findings),
39        &format!("- Scan exit code: `{}`", scan_exit),
40        &format!("- Doctor exit code: `{}`", doctor_exit),
41        &format!("- Assets uploaded from: `{}`", output_dir.display()),
42        "- Includes: `report.sarif`, `AGENTS.md`, `fix-prompt.md`, `repair-plan.md`, `pr-brief.md`, `pr-comment.md`, `.verifyos-agent/`",
43        &format!("- Doctor summary: {}", doctor_summary),
44    ]
45    .join("\n");
46
47    Ok(with_marker(&body, sticky_marker))
48}
49
50fn with_marker(body: &str, sticky_marker: bool) -> String {
51    if sticky_marker {
52        format!("{STICKY_MARKER}\n{body}")
53    } else {
54        body.to_string()
55    }
56}
57
58fn load_agent_pack_findings(path: &Path) -> usize {
59    std::fs::read_to_string(path)
60        .ok()
61        .and_then(|raw| serde_json::from_str::<AgentPack>(&raw).ok())
62        .map(|pack| pack.total_findings)
63        .unwrap_or(0)
64}
65
66fn load_doctor_summary(path: &Path) -> String {
67    std::fs::read_to_string(path)
68        .ok()
69        .and_then(|raw| serde_json::from_str::<DoctorReport>(&raw).ok())
70        .map(|report| {
71            report
72                .checks
73                .into_iter()
74                .map(|item| format!("{}: {:?}", item.name, item.status).to_uppercase())
75                .collect::<Vec<_>>()
76                .join(", ")
77        })
78        .filter(|summary| !summary.is_empty())
79        .unwrap_or_else(|| "doctor report missing".to_string())
80}
81
82#[cfg(test)]
83mod tests {
84    use super::render_workflow_pr_comment;
85    use crate::doctor::{DoctorCheck, DoctorReport, DoctorStatus};
86    use crate::report::AgentPack;
87    use tempfile::tempdir;
88
89    #[test]
90    fn workflow_pr_comment_prefers_existing_file() {
91        let dir = tempdir().expect("temp dir");
92        std::fs::write(
93            dir.path().join("pr-comment.md"),
94            "## verifyOS review summary",
95        )
96        .expect("write comment");
97
98        let body = render_workflow_pr_comment(dir.path(), 1, 0, true, false, None)
99            .expect("render comment");
100
101        assert!(body.contains("<!-- voc-analysis-comment -->"));
102        assert!(body.contains("## verifyOS review summary"));
103        assert!(!body.contains("## voc analysis"));
104    }
105
106    #[test]
107    fn workflow_pr_comment_falls_back_to_doctor_and_agent_pack() {
108        let dir = tempdir().expect("temp dir");
109        let agent_dir = dir.path().join(".verifyos-agent");
110        std::fs::create_dir_all(&agent_dir).expect("create agent dir");
111        std::fs::write(
112            agent_dir.join("agent-pack.json"),
113            serde_json::to_string(&AgentPack {
114                generated_at_unix: 1,
115                total_findings: 3,
116                findings: Vec::new(),
117            })
118            .expect("json"),
119        )
120        .expect("write agent pack");
121        std::fs::write(
122            dir.path().join("doctor.json"),
123            serde_json::to_string(&DoctorReport {
124                checks: vec![DoctorCheck {
125                    name: "Config".to_string(),
126                    status: DoctorStatus::Pass,
127                    detail: "ok".to_string(),
128                }],
129                repair_plan: Vec::new(),
130                plan_context: None,
131            })
132            .expect("json"),
133        )
134        .expect("write doctor report");
135
136        let body = render_workflow_pr_comment(dir.path(), 1, 0, false, false, None)
137            .expect("render comment");
138
139        assert!(body.contains("## voc analysis"));
140        assert!(body.contains("Findings: **3**"));
141        assert!(body.contains("Scan exit code: `1`"));
142        assert!(body.contains("Doctor exit code: `0`"));
143        assert!(body.contains("CONFIG: PASS"));
144    }
145
146    #[test]
147    fn workflow_pr_comment_can_reuse_repair_plan() {
148        let dir = tempdir().expect("temp dir");
149        std::fs::write(
150            dir.path().join("repair-plan.md"),
151            "# verifyOS Repair Plan\n\n## Context\n\n- Source: `fresh-scan`\n",
152        )
153        .expect("write repair plan");
154        std::fs::write(
155            dir.path().join("pr-comment.md"),
156            "## verifyOS review summary\n\n- stale\n",
157        )
158        .expect("write comment");
159
160        let body = render_workflow_pr_comment(dir.path(), 1, 0, false, true, None)
161            .expect("render comment");
162
163        assert!(body.contains("# verifyOS Repair Plan"));
164        assert!(!body.contains("## verifyOS review summary"));
165    }
166
167    #[test]
168    fn workflow_pr_comment_from_plan_matches_snapshot() {
169        let dir = tempdir().expect("temp dir");
170        std::fs::write(
171            dir.path().join("repair-plan.md"),
172            "# verifyOS Repair Plan\n\n## Context\n\n- Source: `fresh-scan`\n- Scan artifact: `examples/bad_app.ipa`\n\n## Planned Outputs\n\n- **pr-comment**\n  - Path: `.verifyos/pr-comment.md`\n  - Reason: refresh sticky PR comment draft\n",
173        )
174        .expect("write repair plan");
175
176        let body =
177            render_workflow_pr_comment(dir.path(), 1, 0, true, true, None).expect("render body");
178        let expected = "<!-- voc-analysis-comment -->\n# verifyOS Repair Plan\n\n## Context\n\n- Source: `fresh-scan`\n- Scan artifact: `examples/bad_app.ipa`\n\n## Planned Outputs\n\n- **pr-comment**\n  - Path: `.verifyos/pr-comment.md`\n  - Reason: refresh sticky PR comment draft";
179
180        assert_eq!(body, expected);
181    }
182
183    #[test]
184    fn workflow_pr_comment_can_use_explicit_plan_path() {
185        let dir = tempdir().expect("temp dir");
186        let nested = dir.path().join("plans");
187        std::fs::create_dir_all(&nested).expect("create plan dir");
188        let plan_path = nested.join("custom-plan.md");
189        std::fs::write(
190            &plan_path,
191            "# verifyOS Repair Plan\n\n## Context\n\n- Source: `existing-assets`\n",
192        )
193        .expect("write repair plan");
194
195        let body = render_workflow_pr_comment(dir.path(), 0, 0, false, true, Some(&plan_path))
196            .expect("render comment");
197
198        assert!(body.contains("# verifyOS Repair Plan"));
199        assert!(body.contains("existing-assets"));
200    }
201}