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