1use crate::{
8 ProgrammaticError,
9 runtime::{
10 AuditProgrammaticOutput, BoundaryViolationsProgrammaticOutput,
11 CircularDependenciesProgrammaticOutput, CombinedProgrammaticOutput,
12 DeadCodeProgrammaticOutput, DecisionSurfaceProgrammaticOutput,
13 DuplicationProgrammaticOutput, FeatureFlagsProgrammaticOutput, HealthJsonReportInput,
14 HealthProgrammaticOutput, TraceCloneProgrammaticOutput, TraceDependencyProgrammaticOutput,
15 TraceErrorProgrammaticOutput, TraceExportProgrammaticOutput, TraceFileProgrammaticOutput,
16 TraceImportPathProgrammaticOutput, serialize_health_report_json,
17 },
18};
19use fallow_output::{
20 AUDIT_SCHEMA_VERSION, CheckOutput, GroupByMode, build_decision_surface_output,
21 serialize_check_json_output, serialize_decision_surface_json_output,
22 serialize_dupes_json_output, serialize_feature_flags_json_output, strip_root_prefix,
23};
24use fallow_types::envelope::{ElapsedMs, SchemaVersion, ToolVersion};
25use fallow_types::workspace::{WorkspaceDiagnostic, merge_workspace_diagnostics};
26use serde::Serialize;
27use std::path::Path;
28use std::time::Duration;
29
30type ProgrammaticResult<T> = Result<T, ProgrammaticError>;
31
32pub fn serialize_combined_programmatic_json(
38 output: CombinedProgrammaticOutput,
39) -> ProgrammaticResult<serde_json::Value> {
40 let CombinedProgrammaticOutput {
41 dead_code,
42 duplication,
43 health,
44 root,
45 elapsed,
46 explain,
47 next_steps,
48 telemetry_analysis_run_id,
49 request_outcomes,
50 } = output;
51 let workspace_diagnostics =
52 combined_workspace_diagnostics(dead_code.as_ref(), health.as_ref(), duplication.as_ref());
53 crate::serialize_combined_json(crate::CombinedJsonOutputInput {
54 package_baselines: crate::first_package_baselines([
55 dead_code
56 .as_ref()
57 .map(|run| run.output.package_baselines.as_slice()),
58 duplication
59 .as_ref()
60 .map(|run| run.output.package_baselines.as_slice()),
61 ]),
62 gate_outcomes: None,
63 request_outcomes,
64 check: dead_code
65 .as_ref()
66 .map(|dead_code| crate::CombinedCheckJsonSection {
67 results: &dead_code.output.results,
68 root: &dead_code.root,
69 elapsed: Duration::from_millis(dead_code.output.elapsed_ms.0),
70 config_fixable: dead_code.config_fixable,
71 extras: crate::CheckJsonExtraOutputs::default(),
72 }),
73 dupes: duplication
74 .as_ref()
75 .map(|duplication| &duplication.output.report),
76 health: health.as_ref().map(|health| &health.report),
77 root: &root,
78 elapsed,
79 explain,
80 type_aware: None,
81 workspace_diagnostics,
82 next_steps,
83 telemetry_analysis_run_id: telemetry_analysis_run_id.as_deref(),
84 })
85 .map_err(|err| {
86 ProgrammaticError::new(format!("failed to serialize combined report: {err}"), 2)
87 .with_code("FALLOW_SERIALIZE_COMBINED_REPORT")
88 .with_context("combined")
89 })
90}
91
92fn combined_workspace_diagnostics(
107 dead_code: Option<&DeadCodeProgrammaticOutput>,
108 health: Option<&HealthProgrammaticOutput>,
109 duplication: Option<&DuplicationProgrammaticOutput>,
110) -> Vec<WorkspaceDiagnostic> {
111 let merged = merge_workspace_diagnostics(
112 dead_code.map_or_else(Vec::new, |dead_code| {
113 dead_code.output.workspace_diagnostics.clone()
114 }),
115 health.map_or_else(Vec::new, |health| health.workspace_diagnostics.clone()),
116 );
117 merge_workspace_diagnostics(
118 merged,
119 duplication.map_or_else(Vec::new, |duplication| {
120 duplication.output.workspace_diagnostics.clone()
121 }),
122 )
123}
124
125pub fn serialize_decision_surface_programmatic_json(
131 output: DecisionSurfaceProgrammaticOutput,
132) -> ProgrammaticResult<serde_json::Value> {
133 let DecisionSurfaceProgrammaticOutput {
134 surface,
135 elapsed: _,
136 telemetry_analysis_run_id,
137 } = output;
138 let payload = build_decision_surface_output(&surface);
139 serialize_decision_surface_json_output(payload, telemetry_analysis_run_id.as_deref()).map_err(
140 |err| {
141 ProgrammaticError::new(format!("failed to serialize decision surface: {err}"), 2)
142 .with_code("FALLOW_SERIALIZE_DECISION_SURFACE")
143 .with_context("decision-surface")
144 },
145 )
146}
147
148pub fn serialize_audit_programmatic_json(
154 output: AuditProgrammaticOutput,
155) -> ProgrammaticResult<serde_json::Value> {
156 let base_snapshot = output.base_snapshot.as_ref();
157 let dead_code = output
158 .dead_code
159 .as_ref()
160 .map(|dead_code| serialize_audit_dead_code(dead_code, base_snapshot))
161 .transpose()?;
162 let duplication = output
163 .duplication
164 .as_ref()
165 .map(|duplication| serialize_audit_duplication(duplication, base_snapshot))
166 .transpose()?;
167 let complexity = output
168 .complexity
169 .as_ref()
170 .map(|complexity| serialize_audit_complexity(complexity, base_snapshot))
171 .transpose()?;
172
173 crate::serialize_audit_json(
174 crate::AuditJsonOutputInput {
175 gate_outcomes: None,
176 header: crate::AuditJsonHeaderInput {
177 schema_version: SchemaVersion(AUDIT_SCHEMA_VERSION),
178 version: ToolVersion(env!("CARGO_PKG_VERSION").to_string()),
179 verdict: output.verdict,
180 changed_files_count: u32::try_from(output.changed_files_count).unwrap_or(u32::MAX),
181 base_ref: output.base_ref,
182 base_description: output.base_description,
183 head_sha: output.head_sha,
184 elapsed_ms: ElapsedMs(
185 u64::try_from(output.elapsed.as_millis()).unwrap_or(u64::MAX),
186 ),
187 base_snapshot_skipped: output.base_snapshot_skipped,
188 summary: output.summary,
189 attribution: output.attribution,
190 },
191 meta: None,
192 dead_code,
193 duplication,
194 complexity,
195 next_steps: output.next_steps,
196 },
197 output.telemetry_analysis_run_id.as_deref(),
198 )
199 .map_err(|err| {
200 ProgrammaticError::new(format!("failed to serialize audit report: {err}"), 2)
201 .with_code("FALLOW_SERIALIZE_AUDIT_REPORT")
202 .with_context("audit")
203 })
204}
205
206fn serialize_audit_dead_code(
217 output: &DeadCodeProgrammaticOutput,
218 base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
219) -> ProgrammaticResult<serde_json::Value> {
220 let mut json = crate::serialize_check_json_payload(crate::CheckJsonPayloadInput {
221 results: &output.output.results,
222 root: &output.root,
223 elapsed: Duration::from_millis(output.output.elapsed_ms.0),
224 config_fixable: output.config_fixable,
225 extras: crate::CheckJsonExtraOutputs::default(),
226 workspace_diagnostics: output.output.workspace_diagnostics.clone(),
227 })
228 .map_err(|err| {
229 ProgrammaticError::new(format!("failed to serialize audit dead-code: {err}"), 2)
230 .with_code("FALLOW_SERIALIZE_AUDIT_DEAD_CODE")
231 .with_context("audit.deadCode")
232 })?;
233 if let Some(base) = base_snapshot {
234 if has_persisted_introduced_flags(&json) {
235 crate::audit_keys::annotate_stale_suppressions_json(
236 &mut json,
237 &output.output.results,
238 &output.root,
239 &base.dead_code,
240 );
241 } else {
242 crate::audit_keys::annotate_dead_code_json(
243 &mut json,
244 &output.output.results,
245 &output.root,
246 &base.dead_code,
247 );
248 }
249 }
250 Ok(json)
251}
252
253fn serialize_audit_duplication(
254 output: &DuplicationProgrammaticOutput,
255 base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
256) -> ProgrammaticResult<serde_json::Value> {
257 let mut json = serde_json::to_value(&output.output.report).map_err(|err| {
258 ProgrammaticError::new(format!("failed to serialize audit duplication: {err}"), 2)
259 .with_code("FALLOW_SERIALIZE_AUDIT_DUPLICATION")
260 .with_context("audit.duplication")
261 })?;
262 let root_prefix = format!("{}/", output.root.display());
263 strip_root_prefix(&mut json, &root_prefix);
264 if let Some(base) = base_snapshot
265 && !has_persisted_introduced_flags(&json)
266 {
267 annotate_audit_duplication_json(&mut json, output, &base.dupes);
268 }
269 Ok(json)
270}
271
272fn serialize_audit_complexity(
273 output: &HealthProgrammaticOutput,
274 base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
275) -> ProgrammaticResult<serde_json::Value> {
276 let mut json = serde_json::to_value(&output.report).map_err(|err| {
277 ProgrammaticError::new(format!("failed to serialize audit complexity: {err}"), 2)
278 .with_code("FALLOW_SERIALIZE_AUDIT_COMPLEXITY")
279 .with_context("audit.complexity")
280 })?;
281 let root_prefix = format!("{}/", output.root.display());
282 strip_root_prefix(&mut json, &root_prefix);
283 if let Some(base) = base_snapshot {
284 crate::audit_keys::annotate_health_json(
285 &mut json,
286 &output.report,
287 &output.root,
288 &base.health,
289 );
290 }
291 Ok(json)
292}
293
294fn has_persisted_introduced_flags(json: &serde_json::Value) -> bool {
295 json.as_object().is_some_and(|object| {
296 object.values().any(|value| {
297 value
298 .as_array()
299 .is_some_and(|items| items.iter().any(|item| item.get("introduced").is_some()))
300 })
301 })
302}
303
304fn annotate_audit_duplication_json(
305 json: &mut serde_json::Value,
306 output: &DuplicationProgrammaticOutput,
307 base: &rustc_hash::FxHashSet<String>,
308) {
309 let Some(items) = json
310 .get_mut("clone_groups")
311 .and_then(serde_json::Value::as_array_mut)
312 else {
313 return;
314 };
315 for (item, group) in items.iter_mut().zip(&output.output.report.clone_groups) {
316 if let serde_json::Value::Object(map) = item {
317 let key = crate::audit_keys::dupe_group_key(&group.group, &output.root);
318 map.insert(
319 "introduced".to_string(),
320 serde_json::json!(!base.contains(&key)),
321 );
322 }
323 }
324}
325
326pub fn serialize_dead_code_programmatic_json(
332 output: DeadCodeProgrammaticOutput,
333) -> ProgrammaticResult<serde_json::Value> {
334 let DeadCodeProgrammaticOutput {
335 output,
336 root,
337 config_fixable: _,
338 telemetry_analysis_run_id,
339 } = output;
340 serialize_check_programmatic_output(
341 output,
342 &root,
343 telemetry_analysis_run_id.as_deref(),
344 "dead-code",
345 "FALLOW_SERIALIZE_DEAD_CODE_REPORT",
346 )
347}
348
349pub fn serialize_circular_dependencies_programmatic_json(
355 output: CircularDependenciesProgrammaticOutput,
356) -> ProgrammaticResult<serde_json::Value> {
357 let CircularDependenciesProgrammaticOutput {
358 output,
359 root,
360 telemetry_analysis_run_id,
361 } = output;
362 serialize_check_programmatic_output(
363 output,
364 &root,
365 telemetry_analysis_run_id.as_deref(),
366 "circular-dependencies",
367 "FALLOW_SERIALIZE_CIRCULAR_DEPENDENCIES_REPORT",
368 )
369}
370
371pub fn serialize_boundary_violations_programmatic_json(
377 output: BoundaryViolationsProgrammaticOutput,
378) -> ProgrammaticResult<serde_json::Value> {
379 let BoundaryViolationsProgrammaticOutput {
380 output,
381 root,
382 telemetry_analysis_run_id,
383 } = output;
384 serialize_check_programmatic_output(
385 output,
386 &root,
387 telemetry_analysis_run_id.as_deref(),
388 "boundary-violations",
389 "FALLOW_SERIALIZE_BOUNDARY_VIOLATIONS_REPORT",
390 )
391}
392
393fn serialize_check_programmatic_output(
394 output: CheckOutput,
395 root: &Path,
396 telemetry_analysis_run_id: Option<&str>,
397 context: &'static str,
398 code: &'static str,
399) -> ProgrammaticResult<serde_json::Value> {
400 let mut json =
401 serialize_check_json_output(output, telemetry_analysis_run_id).map_err(|err| {
402 ProgrammaticError::new(format!("failed to serialize {context} report: {err}"), 2)
403 .with_code(code)
404 .with_context(context)
405 })?;
406 let root_prefix = format!("{}/", root.display());
407 strip_root_prefix(&mut json, &root_prefix);
408 Ok(json)
409}
410
411pub fn serialize_duplication_programmatic_json(
417 output: DuplicationProgrammaticOutput,
418) -> ProgrammaticResult<serde_json::Value> {
419 let DuplicationProgrammaticOutput {
420 output,
421 root,
422 threshold: _,
423 telemetry_analysis_run_id,
424 } = output;
425 let mut json = serialize_dupes_json_output(output, telemetry_analysis_run_id.as_deref())
426 .map_err(|err| {
427 ProgrammaticError::new(format!("failed to serialize duplication report: {err}"), 2)
428 .with_code("FALLOW_SERIALIZE_DUPLICATION_REPORT")
429 .with_context("dupes")
430 })?;
431 let root_prefix = format!("{}/", root.display());
432 strip_root_prefix(&mut json, &root_prefix);
433 Ok(json)
434}
435
436pub fn serialize_feature_flags_programmatic_json(
442 output: FeatureFlagsProgrammaticOutput,
443) -> ProgrammaticResult<serde_json::Value> {
444 serialize_feature_flags_json_output(output.output, output.telemetry_analysis_run_id.as_deref())
445 .map_err(|err| {
446 ProgrammaticError::new(
447 format!("failed to serialize feature flags report: {err}"),
448 2,
449 )
450 .with_code("FALLOW_SERIALIZE_FEATURE_FLAGS_REPORT")
451 .with_context("feature-flags")
452 })
453}
454
455pub fn serialize_trace_export_programmatic_json(
461 output: TraceExportProgrammaticOutput,
462) -> ProgrammaticResult<serde_json::Value> {
463 serialize_trace_programmatic_output(
464 output.output,
465 "export trace",
466 "FALLOW_SERIALIZE_TRACE_EXPORT",
467 "trace_export",
468 )
469}
470
471pub fn serialize_trace_file_programmatic_json(
477 output: TraceFileProgrammaticOutput,
478) -> ProgrammaticResult<serde_json::Value> {
479 serialize_trace_programmatic_output(
480 output.output,
481 "file trace",
482 "FALLOW_SERIALIZE_TRACE_FILE",
483 "trace_file",
484 )
485}
486
487pub fn serialize_trace_import_path_programmatic_json(
493 output: TraceImportPathProgrammaticOutput,
494) -> ProgrammaticResult<serde_json::Value> {
495 serialize_trace_programmatic_output(
496 output.output,
497 "import path trace",
498 "FALLOW_SERIALIZE_TRACE_IMPORT_PATH",
499 "trace_import_path",
500 )
501}
502
503pub fn serialize_trace_dependency_programmatic_json(
509 output: TraceDependencyProgrammaticOutput,
510) -> ProgrammaticResult<serde_json::Value> {
511 serialize_trace_programmatic_output(
512 output.output,
513 "dependency trace",
514 "FALLOW_SERIALIZE_TRACE_DEPENDENCY",
515 "trace_dependency",
516 )
517}
518
519pub fn serialize_trace_error_programmatic_json(
525 output: TraceErrorProgrammaticOutput,
526) -> ProgrammaticResult<serde_json::Value> {
527 serialize_trace_programmatic_output(
528 output.output,
529 "stack-trace resolution",
530 "FALLOW_SERIALIZE_TRACE_ERROR",
531 "trace_error",
532 )
533}
534
535pub fn serialize_trace_clone_programmatic_json(
541 output: TraceCloneProgrammaticOutput,
542) -> ProgrammaticResult<serde_json::Value> {
543 serialize_trace_programmatic_output(
544 output.output,
545 "clone trace",
546 "FALLOW_SERIALIZE_TRACE_CLONE",
547 "trace_clone",
548 )
549}
550
551fn serialize_trace_programmatic_output<T: Serialize>(
552 output: T,
553 context: &'static str,
554 code: &'static str,
555 error_context: &'static str,
556) -> ProgrammaticResult<serde_json::Value> {
557 serde_json::to_value(output).map_err(|err| {
558 ProgrammaticError::new(format!("failed to serialize {context}: {err}"), 2)
559 .with_code(code)
560 .with_context(error_context)
561 })
562}
563
564pub fn serialize_health_programmatic_json(
570 output: HealthProgrammaticOutput,
571) -> ProgrammaticResult<serde_json::Value> {
572 let HealthProgrammaticOutput {
573 report,
574 grouping,
575 root,
576 elapsed,
577 explain,
578 workspace_diagnostics,
579 next_steps,
580 telemetry_analysis_run_id,
581 request_outcomes,
582 } = output;
583 let (grouped_by, groups) = grouping.map_or((None, None), |grouping| {
584 (
585 group_by_mode_from_label(grouping.mode),
586 Some(grouping.groups),
587 )
588 });
589 serialize_health_report_json(HealthJsonReportInput {
590 gate_outcomes: None,
591 request_outcomes,
592 report,
593 root: &root,
594 elapsed,
595 explain,
596 type_aware: None,
597 grouped_by,
598 groups,
599 workspace_diagnostics,
600 next_steps,
601 telemetry_analysis_run_id: telemetry_analysis_run_id.as_deref(),
602 })
603 .map_err(|err| {
604 ProgrammaticError::new(format!("failed to serialize health report: {err}"), 2)
605 .with_code("FALLOW_SERIALIZE_HEALTH_REPORT")
606 .with_context("health")
607 })
608}
609
610fn group_by_mode_from_label(label: &str) -> Option<GroupByMode> {
611 match label {
612 "owner" => Some(GroupByMode::Owner),
613 "directory" => Some(GroupByMode::Directory),
614 "package" => Some(GroupByMode::Package),
615 "section" => Some(GroupByMode::Section),
616 _ => None,
617 }
618}
619
620#[cfg(test)]
621mod tests {
622 use super::{serialize_audit_dead_code, serialize_combined_programmatic_json};
623 use crate::DupesReportPayload;
624 use crate::runtime::{
625 CombinedProgrammaticOutput, DeadCodeProgrammaticOutput, DuplicationProgrammaticOutput,
626 HealthProgrammaticOutput,
627 };
628 use fallow_output::{
629 CHECK_SCHEMA_VERSION, CheckOutputInput, DUPES_SCHEMA_VERSION, DupesOutputInput,
630 HealthReport, build_check_output, build_dupes_output,
631 };
632 use fallow_types::duplicates::DuplicationReport;
633 use fallow_types::results::AnalysisResults;
634 use fallow_types::workspace::{WorkspaceDiagnostic, WorkspaceDiagnosticKind};
635 use std::path::Path;
636 use std::time::Duration;
637
638 fn dead_code_output(
639 root: &Path,
640 workspace_diagnostics: Vec<WorkspaceDiagnostic>,
641 ) -> DeadCodeProgrammaticOutput {
642 DeadCodeProgrammaticOutput {
643 output: build_check_output(CheckOutputInput {
644 schema_version: CHECK_SCHEMA_VERSION,
645 version: "0.0.0-test".to_owned(),
646 elapsed: Duration::ZERO,
647 results: AnalysisResults::default(),
648 config_fixable: false,
649 meta: None,
650 workspace_diagnostics,
651 next_steps: Vec::new(),
652 }),
653 root: root.to_path_buf(),
654 config_fixable: false,
655 telemetry_analysis_run_id: None,
656 }
657 }
658
659 #[test]
663 fn audit_dead_code_section_carries_workspace_diagnostics_root_relative_or_omits_them() {
664 let root = Path::new("/project");
665 let carried = serialize_audit_dead_code(
666 &dead_code_output(
667 root,
668 vec![WorkspaceDiagnostic::new(
669 root,
670 root.join("package.json"),
671 WorkspaceDiagnosticKind::BunLockbOverrideResolutionSkipped,
672 )],
673 ),
674 None,
675 )
676 .expect("audit dead-code JSON");
677 assert_eq!(
678 carried["workspace_diagnostics"][0]["kind"],
679 "bun-lockb-override-resolution-skipped"
680 );
681 assert_eq!(carried["workspace_diagnostics"][0]["path"], "package.json");
682
683 let empty = serialize_audit_dead_code(&dead_code_output(root, Vec::new()), None)
684 .expect("audit dead-code JSON");
685 assert!(
686 empty.get("workspace_diagnostics").is_none(),
687 "an empty list is omitted from the audit dead-code section: {empty}"
688 );
689 }
690
691 fn health_output(
692 root: &Path,
693 workspace_diagnostics: Vec<WorkspaceDiagnostic>,
694 ) -> HealthProgrammaticOutput {
695 HealthProgrammaticOutput {
696 report: HealthReport::default(),
697 grouping: None,
698 root: root.to_path_buf(),
699 elapsed: Duration::ZERO,
700 explain: false,
701 workspace_diagnostics,
702 next_steps: Vec::new(),
703 telemetry_analysis_run_id: None,
704 request_outcomes: None,
705 }
706 }
707
708 fn duplication_output(
709 root: &Path,
710 workspace_diagnostics: Vec<WorkspaceDiagnostic>,
711 ) -> DuplicationProgrammaticOutput {
712 DuplicationProgrammaticOutput {
713 output: build_dupes_output(DupesOutputInput {
714 gate_outcomes: None,
715 request_outcomes: None,
716 baseline_staleness: None,
717 schema_version: DUPES_SCHEMA_VERSION,
718 version: "0.0.0-test".to_owned(),
719 elapsed: Duration::ZERO,
720 report: DupesReportPayload::from_report(&DuplicationReport::default()),
721 clone_groups_shown: 0,
722 clone_groups_omitted: 0,
723 clone_families_shown: 0,
724 clone_families_omitted: 0,
725 grouped_by: None,
726 total_issues: None,
727 groups: None,
728 meta: None,
729 workspace_diagnostics,
730 next_steps: Vec::new(),
731 }),
732 root: root.to_path_buf(),
733 threshold: 0.0,
734 telemetry_analysis_run_id: None,
735 }
736 }
737
738 fn combined_output(
739 root: &Path,
740 dead_code: Option<DeadCodeProgrammaticOutput>,
741 health: Option<HealthProgrammaticOutput>,
742 ) -> CombinedProgrammaticOutput {
743 combined_output_with_duplication(root, dead_code, health, None)
744 }
745
746 fn combined_output_with_duplication(
747 root: &Path,
748 dead_code: Option<DeadCodeProgrammaticOutput>,
749 health: Option<HealthProgrammaticOutput>,
750 duplication: Option<DuplicationProgrammaticOutput>,
751 ) -> CombinedProgrammaticOutput {
752 CombinedProgrammaticOutput {
753 dead_code,
754 duplication,
755 health,
756 root: root.to_path_buf(),
757 elapsed: Duration::ZERO,
758 explain: false,
759 next_steps: Vec::new(),
760 telemetry_analysis_run_id: None,
761 request_outcomes: None,
762 }
763 }
764
765 fn bun_lockb_diagnostic(root: &Path) -> WorkspaceDiagnostic {
766 WorkspaceDiagnostic::new(
767 root,
768 root.join("package.json"),
769 WorkspaceDiagnosticKind::BunLockbOverrideResolutionSkipped,
770 )
771 }
772
773 fn large_file_diagnostic(root: &Path, relative: &str) -> WorkspaceDiagnostic {
774 WorkspaceDiagnostic::new(
775 root,
776 root.join(relative),
777 WorkspaceDiagnosticKind::SkippedLargeFile {
778 size_bytes: 6_000_000,
779 },
780 )
781 }
782
783 fn root_kinds(document: &serde_json::Value) -> Vec<String> {
784 document["workspace_diagnostics"]
785 .as_array()
786 .cloned()
787 .unwrap_or_default()
788 .iter()
789 .map(|diagnostic| diagnostic["kind"].as_str().unwrap_or_default().to_owned())
790 .collect()
791 }
792
793 #[test]
797 fn combined_programmatic_root_carries_workspace_diagnostics_or_omits_them() {
798 let root = Path::new("/project");
799 let carried = serialize_combined_programmatic_json(combined_output(
800 root,
801 Some(dead_code_output(root, vec![bun_lockb_diagnostic(root)])),
802 None,
803 ))
804 .expect("combined JSON");
805 assert_eq!(
806 carried["workspace_diagnostics"][0]["kind"],
807 "bun-lockb-override-resolution-skipped"
808 );
809 assert_eq!(carried["workspace_diagnostics"][0]["path"], "package.json");
810 assert!(
811 carried["check"].is_object(),
812 "the check section is present, so the absence check below is not vacuous: {carried}"
813 );
814 assert!(
815 carried["check"].get("workspace_diagnostics").is_none(),
816 "the check section is not a second carrier: {carried}"
817 );
818
819 let empty = serialize_combined_programmatic_json(combined_output(
820 root,
821 Some(dead_code_output(root, Vec::new())),
822 None,
823 ))
824 .expect("combined JSON");
825 assert!(
826 empty.get("workspace_diagnostics").is_none(),
827 "an empty list is omitted from the combined root: {empty}"
828 );
829 }
830
831 #[test]
835 fn combined_programmatic_root_carries_workspace_diagnostics_without_a_dead_code_section() {
836 let root = Path::new("/project");
837 let carried = serialize_combined_programmatic_json(combined_output(
838 root,
839 None,
840 Some(health_output(root, vec![bun_lockb_diagnostic(root)])),
841 ))
842 .expect("combined JSON");
843 assert!(
844 carried.get("check").is_none(),
845 "this run has no check section: {carried}"
846 );
847 assert_eq!(
848 carried["workspace_diagnostics"][0]["kind"],
849 "bun-lockb-override-resolution-skipped"
850 );
851 assert_eq!(carried["workspace_diagnostics"][0]["path"], "package.json");
852 }
853
854 #[test]
858 fn combined_programmatic_root_carries_workspace_diagnostics_from_a_duplication_only_run() {
859 let root = Path::new("/project");
860 let carried = serialize_combined_programmatic_json(combined_output_with_duplication(
861 root,
862 None,
863 None,
864 Some(duplication_output(
865 root,
866 vec![large_file_diagnostic(root, "src/generated.ts")],
867 )),
868 ))
869 .expect("combined JSON");
870 assert!(
871 carried.get("check").is_none() && carried.get("health").is_none(),
872 "only the dupes section ran: {carried}"
873 );
874 assert_eq!(root_kinds(&carried), ["skipped-large-file"]);
875 assert_eq!(
876 carried["workspace_diagnostics"][0]["path"],
877 "src/generated.ts"
878 );
879 }
880
881 #[test]
889 fn combined_programmatic_root_unions_sections_that_recorded_different_diagnostics() {
890 let root = Path::new("/project");
891 let shared = bun_lockb_diagnostic(root);
892 let carried = serialize_combined_programmatic_json(combined_output_with_duplication(
893 root,
894 Some(dead_code_output(
895 root,
896 vec![shared.clone(), large_file_diagnostic(root, "src/big.ts")],
897 )),
898 Some(health_output(root, vec![shared.clone()])),
899 Some(duplication_output(
900 root,
901 vec![shared, large_file_diagnostic(root, "src/other.ts")],
902 )),
903 ))
904 .expect("combined JSON");
905 assert_eq!(
906 root_kinds(&carried),
907 [
908 "bun-lockb-override-resolution-skipped",
909 "skipped-large-file",
910 "skipped-large-file",
911 ],
912 "the union keeps dead-code order first and drops the repeats: {}",
913 carried["workspace_diagnostics"]
914 );
915 let paths: Vec<&str> = carried["workspace_diagnostics"]
916 .as_array()
917 .expect("array")
918 .iter()
919 .map(|diagnostic| diagnostic["path"].as_str().unwrap_or_default())
920 .collect();
921 assert_eq!(paths, ["package.json", "src/big.ts", "src/other.ts"]);
922 }
923
924 #[test]
929 fn combined_programmatic_root_keeps_diagnostics_an_empty_dead_code_section_missed() {
930 let root = Path::new("/project");
931 let carried = serialize_combined_programmatic_json(combined_output_with_duplication(
932 root,
933 Some(dead_code_output(root, Vec::new())),
934 Some(health_output(
935 root,
936 vec![large_file_diagnostic(root, "src/big.test.ts")],
937 )),
938 None,
939 ))
940 .expect("combined JSON");
941 assert_eq!(root_kinds(&carried), ["skipped-large-file"]);
942 assert_eq!(
943 carried["workspace_diagnostics"][0]["path"],
944 "src/big.test.ts"
945 );
946 }
947}