Skip to main content

lean_ctx/core/context_package/
loader.rs

1use crate::core::knowledge::ProjectKnowledge;
2use crate::core::memory_policy::MemoryPolicy;
3use crate::core::property_graph::{CodeGraph, Edge, EdgeKind, Node, NodeKind};
4
5use super::composition;
6use super::content::{GraphLayer, KnowledgeLayer, PackageContent, PatternsLayer, SessionLayer};
7use super::graph_model::ContextGraph;
8use super::manifest::PackageManifest;
9
10#[derive(Debug, Clone, Default)]
11pub struct LoadReport {
12    pub package_name: String,
13    pub package_version: String,
14    pub knowledge_facts_merged: u32,
15    pub knowledge_facts_skipped: u32,
16    pub knowledge_patterns_merged: u32,
17    pub knowledge_insights_merged: u32,
18    pub graph_nodes_imported: u32,
19    pub graph_edges_imported: u32,
20    pub gotchas_imported: u32,
21    pub patterns_imported: u32,
22    pub session_findings_merged: u32,
23    pub session_decisions_merged: u32,
24    pub v2_nodes_added: u32,
25    pub v2_nodes_updated: u32,
26    pub v2_edges_added: u32,
27    pub v2_edges_merged: u32,
28    pub v2_conflicts: Vec<String>,
29    pub warnings: Vec<String>,
30}
31
32impl std::fmt::Display for LoadReport {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        writeln!(
35            f,
36            "Package: {} v{}",
37            self.package_name, self.package_version
38        )?;
39        if self.knowledge_facts_merged > 0 || self.knowledge_facts_skipped > 0 {
40            writeln!(
41                f,
42                "  Knowledge: {} facts merged, {} skipped (duplicates)",
43                self.knowledge_facts_merged, self.knowledge_facts_skipped
44            )?;
45        }
46        if self.knowledge_patterns_merged > 0 {
47            writeln!(
48                f,
49                "  Patterns:  {} imported",
50                self.knowledge_patterns_merged
51            )?;
52        }
53        if self.knowledge_insights_merged > 0 {
54            writeln!(
55                f,
56                "  Insights:  {} imported",
57                self.knowledge_insights_merged
58            )?;
59        }
60        if self.graph_nodes_imported > 0 || self.graph_edges_imported > 0 {
61            writeln!(
62                f,
63                "  Graph:     {} nodes, {} edges imported",
64                self.graph_nodes_imported, self.graph_edges_imported
65            )?;
66        }
67        if self.patterns_imported > 0 {
68            writeln!(
69                f,
70                "  Patterns:  {} imported (standalone)",
71                self.patterns_imported
72            )?;
73        }
74        if self.gotchas_imported > 0 {
75            writeln!(f, "  Gotchas:   {} imported", self.gotchas_imported)?;
76        }
77        if self.session_findings_merged > 0 || self.session_decisions_merged > 0 {
78            writeln!(
79                f,
80                "  Session:   {} findings, {} decisions imported",
81                self.session_findings_merged, self.session_decisions_merged
82            )?;
83        }
84        if self.v2_nodes_added > 0 || self.v2_nodes_updated > 0 {
85            writeln!(
86                f,
87                "  Graph v2: {} nodes added, {} updated, {} edges added, {} merged",
88                self.v2_nodes_added,
89                self.v2_nodes_updated,
90                self.v2_edges_added,
91                self.v2_edges_merged,
92            )?;
93        }
94        for c in &self.v2_conflicts {
95            writeln!(f, "  CONFLICT: {c}")?;
96        }
97        for w in &self.warnings {
98            writeln!(f, "  WARNING: {w}")?;
99        }
100        Ok(())
101    }
102}
103
104pub fn load_package(
105    manifest: &PackageManifest,
106    content: &PackageContent,
107    project_root: &str,
108) -> Result<LoadReport, String> {
109    let mut report = LoadReport {
110        package_name: manifest.name.clone(),
111        package_version: manifest.version.clone(),
112        ..Default::default()
113    };
114
115    if let Some(ref min_ver) = manifest.compatibility.min_lean_ctx_version {
116        let current = env!("CARGO_PKG_VERSION");
117        if version_lt(current, min_ver) {
118            report.warnings.push(format!(
119                "package requires lean-ctx >= {min_ver}, current is {current}"
120            ));
121        }
122    }
123
124    if !manifest.dependencies.is_empty() {
125        for dep in &manifest.dependencies {
126            if !dep.optional {
127                report.warnings.push(format!(
128                    "unresolved dependency: {} {}",
129                    dep.name, dep.version_req
130                ));
131            }
132        }
133    }
134
135    if let Some(ref kl) = content.knowledge
136        && let Err(e) = merge_knowledge(kl, project_root, manifest, &mut report)
137    {
138        report
139            .warnings
140            .push(format!("knowledge import failed: {e}"));
141    }
142
143    if let Some(ref gl) = content.graph
144        && let Err(e) = import_graph(gl, project_root, &mut report)
145    {
146        report.warnings.push(format!("graph import failed: {e}"));
147    }
148
149    if let Some(ref patterns) = content.patterns
150        && let Err(e) = import_patterns(patterns, project_root, manifest, &mut report)
151    {
152        report.warnings.push(format!("patterns import failed: {e}"));
153    }
154
155    if let Some(ref gotchas) = content.gotchas {
156        import_gotchas(gotchas, project_root, &mut report);
157    }
158
159    if let Some(ref session) = content.session {
160        import_session(session, project_root, manifest, &mut report);
161    }
162
163    if let Some(ref incoming_graph) = content.context_graph {
164        import_v2_graph(incoming_graph, project_root, &mut report);
165    }
166
167    Ok(report)
168}
169
170fn import_v2_graph(incoming: &ContextGraph, project_root: &str, report: &mut LoadReport) {
171    let project_hash = crate::core::project_hash::hash_project_root(project_root);
172    let data_dir = match crate::core::data_dir::lean_ctx_data_dir() {
173        Ok(d) => d,
174        Err(e) => {
175            report
176                .warnings
177                .push(format!("v2 graph: data dir unavailable: {e}"));
178            return;
179        }
180    };
181    let graph_path = data_dir
182        .join("context_graph")
183        .join(format!("{project_hash}.json"));
184
185    let graph_path_str = graph_path.to_string_lossy().to_string();
186    let mut local_graph = if let Ok(data) = std::fs::read_to_string(&graph_path_str) {
187        serde_json::from_str::<ContextGraph>(&data).unwrap_or_default()
188    } else {
189        ContextGraph::default()
190    };
191
192    let merge_report = composition::merge_graphs(&mut local_graph, incoming);
193
194    report.v2_nodes_added = merge_report.nodes_added;
195    report.v2_nodes_updated = merge_report.nodes_updated;
196    report.v2_edges_added = merge_report.edges_added;
197    report.v2_edges_merged = merge_report.edges_merged;
198    report.v2_conflicts = merge_report.conflicts;
199
200    if merge_report.nodes_added > 0
201        || merge_report.nodes_updated > 0
202        || merge_report.edges_added > 0
203    {
204        match serde_json::to_string_pretty(&local_graph) {
205            Ok(json) => {
206                if let Some(parent) = graph_path.parent() {
207                    let _ = std::fs::create_dir_all(parent);
208                }
209                if let Err(e) = std::fs::write(&graph_path_str, json) {
210                    report.warnings.push(format!("v2 graph save failed: {e}"));
211                }
212            }
213            Err(e) => {
214                report
215                    .warnings
216                    .push(format!("v2 graph serialize failed: {e}"));
217            }
218        }
219    }
220}
221
222fn merge_knowledge(
223    layer: &KnowledgeLayer,
224    project_root: &str,
225    manifest: &PackageManifest,
226    report: &mut LoadReport,
227) -> Result<(), String> {
228    let policy = MemoryPolicy::default();
229    let source_tag = format!("{}@{}", manifest.name, manifest.version);
230
231    // #326/#594-A4: merge under the project lock so a package install cannot
232    // clobber a concurrent foreground knowledge write; the closure reloads the
233    // latest store inside the lock before deduping/merging.
234    ProjectKnowledge::mutate_locked(project_root, |knowledge| {
235        for fact in &layer.facts {
236            let exists = knowledge
237                .facts
238                .iter()
239                .any(|f| f.category == fact.category && f.key == fact.key && f.value == fact.value);
240
241            if exists {
242                report.knowledge_facts_skipped += 1;
243                continue;
244            }
245
246            knowledge.remember(
247                &fact.category,
248                &fact.key,
249                &fact.value,
250                &fact.source_session,
251                fact.confidence.min(0.8),
252                &policy,
253            );
254            if let Some(last) = knowledge.facts.last_mut() {
255                last.imported_from = Some(source_tag.clone());
256            }
257            report.knowledge_facts_merged += 1;
258        }
259
260        for pattern in &layer.patterns {
261            let exists = knowledge.patterns.iter().any(|p| {
262                p.pattern_type == pattern.pattern_type && p.description == pattern.description
263            });
264
265            if !exists {
266                knowledge.patterns.push(pattern.clone());
267                report.knowledge_patterns_merged += 1;
268            }
269        }
270
271        for insight in &layer.insights {
272            let exists = knowledge
273                .history
274                .iter()
275                .any(|h| h.summary == insight.summary);
276
277            if !exists {
278                knowledge.history.push(insight.clone());
279                report.knowledge_insights_merged += 1;
280            }
281        }
282    })
283    .map(|_| ())
284}
285
286fn import_graph(
287    layer: &GraphLayer,
288    project_root: &str,
289    report: &mut LoadReport,
290) -> Result<(), String> {
291    let graph = CodeGraph::open(project_root).map_err(|e| format!("graph open: {e}"))?;
292
293    for node_export in &layer.nodes {
294        let node = Node {
295            id: None,
296            kind: NodeKind::parse(&node_export.kind),
297            name: node_export.name.clone(),
298            file_path: node_export.file_path.clone(),
299            line_start: node_export.line_start,
300            line_end: node_export.line_end,
301            metadata: node_export.metadata.clone(),
302        };
303
304        match graph.upsert_node(&node) {
305            Ok(_) => report.graph_nodes_imported += 1,
306            Err(e) => {
307                report
308                    .warnings
309                    .push(format!("node import failed ({}): {e}", node_export.name));
310            }
311        }
312    }
313
314    for edge_export in &layer.edges {
315        let source = find_node_for_edge(&graph, &edge_export.source_path, &edge_export.source_name);
316        let target = find_node_for_edge(&graph, &edge_export.target_path, &edge_export.target_name);
317
318        match (source, target) {
319            (Some(src), Some(tgt)) => {
320                let Some(src_id) = src.id else {
321                    report.warnings.push(format!(
322                        "edge skipped: source node has no id ({}:{})",
323                        edge_export.source_path, edge_export.source_name
324                    ));
325                    continue;
326                };
327                let Some(tgt_id) = tgt.id else {
328                    report.warnings.push(format!(
329                        "edge skipped: target node has no id ({}:{})",
330                        edge_export.target_path, edge_export.target_name
331                    ));
332                    continue;
333                };
334
335                let edge = Edge {
336                    id: None,
337                    source_id: src_id,
338                    target_id: tgt_id,
339                    kind: EdgeKind::parse(&edge_export.kind),
340                    metadata: edge_export.metadata.clone(),
341                };
342
343                match graph.upsert_edge(&edge) {
344                    Ok(()) => report.graph_edges_imported += 1,
345                    Err(e) => {
346                        report.warnings.push(format!(
347                            "edge import failed ({} -> {}): {e}",
348                            edge_export.source_name, edge_export.target_name
349                        ));
350                    }
351                }
352            }
353            _ => {
354                report.warnings.push(format!(
355                    "edge skipped: node not found ({} -> {})",
356                    edge_export.source_name, edge_export.target_name
357                ));
358            }
359        }
360    }
361
362    Ok(())
363}
364
365/// Find a node by symbol name+path first, then fall back to path-only lookup.
366fn find_node_for_edge(graph: &CodeGraph, file_path: &str, name: &str) -> Option<Node> {
367    if let Ok(Some(node)) = graph.get_node_by_symbol(name, file_path) {
368        return Some(node);
369    }
370    if let Ok(Some(node)) = graph.get_node_by_path(file_path) {
371        return Some(node);
372    }
373    None
374}
375
376fn import_patterns(
377    layer: &PatternsLayer,
378    project_root: &str,
379    _manifest: &PackageManifest,
380    report: &mut LoadReport,
381) -> Result<(), String> {
382    // #326/#594-A4: import under the project lock; the closure reloads the latest
383    // store inside the lock so a concurrent write is never clobbered.
384    ProjectKnowledge::mutate_locked(project_root, |knowledge| {
385        for pattern in &layer.patterns {
386            let exists = knowledge.patterns.iter().any(|p| {
387                p.pattern_type == pattern.pattern_type && p.description == pattern.description
388            });
389
390            if !exists {
391                knowledge.patterns.push(pattern.clone());
392                report.patterns_imported += 1;
393            }
394        }
395    })
396    .map(|_| ())
397}
398
399fn import_gotchas(
400    layer: &super::content::GotchasLayer,
401    project_root: &str,
402    report: &mut LoadReport,
403) {
404    use crate::core::gotcha_tracker::{
405        Gotcha, GotchaCategory, GotchaSeverity, GotchaSource, GotchaStore,
406    };
407
408    let mut store = GotchaStore::load(project_root);
409    let before = store.gotchas.len();
410
411    for g in &layer.gotchas {
412        let dup = store.gotchas.iter().any(|e| e.id == g.id);
413        if dup {
414            continue;
415        }
416
417        let category = GotchaCategory::from_str_loose(&g.category);
418        let severity = match g.severity.as_str() {
419            "critical" => GotchaSeverity::Critical,
420            "warning" => GotchaSeverity::Warning,
421            _ => GotchaSeverity::Info,
422        };
423
424        let mut gotcha = Gotcha::new(
425            category,
426            severity,
427            &g.trigger,
428            &g.resolution,
429            GotchaSource::AgentReported {
430                session_id: "package-import".into(),
431            },
432            "package-import",
433        );
434        g.id.clone_into(&mut gotcha.id);
435        g.file_patterns.clone_into(&mut gotcha.file_patterns);
436        gotcha.confidence = g.confidence.min(0.8);
437
438        store.gotchas.push(gotcha);
439    }
440
441    report.gotchas_imported = (store.gotchas.len() - before) as u32;
442    if let Err(e) = store.save(project_root) {
443        report.warnings.push(format!("gotcha save failed: {e}"));
444    }
445}
446
447pub(crate) fn version_lt(current: &str, required: &str) -> bool {
448    let parse = |v: &str| -> Vec<u32> {
449        v.split('.')
450            .map(|s| s.parse::<u32>().unwrap_or(0))
451            .collect()
452    };
453    let c = parse(current);
454    let r = parse(required);
455    for i in 0..c.len().max(r.len()) {
456        let cv = c.get(i).copied().unwrap_or(0);
457        let rv = r.get(i).copied().unwrap_or(0);
458        if cv < rv {
459            return true;
460        }
461        if cv > rv {
462            return false;
463        }
464    }
465    false
466}
467
468fn import_session(
469    layer: &SessionLayer,
470    project_root: &str,
471    manifest: &PackageManifest,
472    report: &mut LoadReport,
473) {
474    let policy = MemoryPolicy::default();
475    let source_tag = format!("{}@{} (session)", manifest.name, manifest.version);
476
477    // #326/#594-A4: merge session findings/decisions under the project lock so a
478    // package install never clobbers a concurrent foreground write.
479    let outcome = ProjectKnowledge::mutate_locked(project_root, |knowledge| {
480        for finding in &layer.findings {
481            let key = finding.file.as_deref().unwrap_or("general");
482            let exists = knowledge
483                .facts
484                .iter()
485                .any(|f| f.category == "session_finding" && f.value == finding.summary);
486            if !exists {
487                knowledge.remember(
488                    "session_finding",
489                    key,
490                    &finding.summary,
491                    &source_tag,
492                    0.6,
493                    &policy,
494                );
495                report.session_findings_merged += 1;
496            }
497        }
498
499        for decision in &layer.decisions {
500            let value = if let Some(ref rationale) = decision.rationale {
501                format!("{} (rationale: {})", decision.summary, rationale)
502            } else {
503                decision.summary.clone()
504            };
505            let exists = knowledge
506                .facts
507                .iter()
508                .any(|f| f.category == "session_decision" && f.value == decision.summary);
509            if !exists {
510                knowledge.remember(
511                    "session_decision",
512                    "decision",
513                    &value,
514                    &source_tag,
515                    0.7,
516                    &policy,
517                );
518                report.session_decisions_merged += 1;
519            }
520        }
521    });
522
523    if let Err(e) = outcome {
524        report
525            .warnings
526            .push(format!("session knowledge save failed: {e}"));
527    }
528}
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533    use crate::core::context_package::content::*;
534    use crate::core::context_package::manifest::*;
535    use chrono::Utc;
536
537    fn test_manifest(layers: Vec<PackageLayer>) -> PackageManifest {
538        PackageManifest {
539            schema_version: 1,
540            conformance_level: None,
541            kind: crate::core::context_package::manifest::PackageKind::default(),
542            name: "test-pkg".into(),
543            version: "1.0.0".into(),
544            description: "test".into(),
545            author: None,
546            scope: None,
547            created_at: Utc::now(),
548            updated_at: None,
549            layers,
550            dependencies: vec![],
551            tags: vec![],
552            visibility: None,
553            integrity: PackageIntegrity {
554                sha256: "a".repeat(64),
555                content_hash: "b".repeat(64),
556                byte_size: 100,
557            },
558            provenance: PackageProvenance {
559                tool: "test".into(),
560                tool_version: "0.0.1".into(),
561                project_hash: None,
562                source_session_id: None,
563            },
564            compatibility: CompatibilitySpec::default(),
565            stats: PackageStats::default(),
566            signature: None,
567            graph_summary: None,
568            marketplace: None,
569        }
570    }
571
572    #[test]
573    fn version_lt_basic_comparisons() {
574        assert!(version_lt("3.5.0", "3.6.0"));
575        assert!(!version_lt("3.6.0", "3.5.0"));
576        assert!(!version_lt("3.6.0", "3.6.0"));
577        assert!(version_lt("3.6.14", "3.6.15"));
578        assert!(version_lt("2.0.0", "3.0.0"));
579    }
580
581    #[test]
582    fn compatibility_warning_when_version_too_low() {
583        let mut manifest = test_manifest(vec![PackageLayer::Knowledge]);
584        manifest.compatibility.min_lean_ctx_version = Some("99.0.0".into());
585
586        let content = PackageContent {
587            knowledge: Some(KnowledgeLayer {
588                facts: vec![],
589                patterns: vec![],
590                insights: vec![],
591                exported_at: Utc::now(),
592            }),
593            ..Default::default()
594        };
595
596        let dir = tempfile::tempdir().unwrap();
597        let report = load_package(&manifest, &content, dir.path().to_str().unwrap()).unwrap();
598        assert!(
599            report
600                .warnings
601                .iter()
602                .any(|w| w.contains("requires lean-ctx >= 99.0.0"))
603        );
604    }
605
606    #[test]
607    fn dependency_warning_for_required_deps() {
608        let mut manifest = test_manifest(vec![PackageLayer::Knowledge]);
609        manifest.dependencies.push(PackageDependency {
610            name: "missing-pkg".into(),
611            version_req: "^1.0".into(),
612            optional: false,
613        });
614
615        let content = PackageContent {
616            knowledge: Some(KnowledgeLayer {
617                facts: vec![],
618                patterns: vec![],
619                insights: vec![],
620                exported_at: Utc::now(),
621            }),
622            ..Default::default()
623        };
624
625        let dir = tempfile::tempdir().unwrap();
626        let report = load_package(&manifest, &content, dir.path().to_str().unwrap()).unwrap();
627        assert!(
628            report
629                .warnings
630                .iter()
631                .any(|w| w.contains("unresolved dependency: missing-pkg"))
632        );
633    }
634
635    #[test]
636    fn optional_dependency_no_warning() {
637        let mut manifest = test_manifest(vec![PackageLayer::Knowledge]);
638        manifest.dependencies.push(PackageDependency {
639            name: "optional-pkg".into(),
640            version_req: "^1.0".into(),
641            optional: true,
642        });
643
644        let content = PackageContent {
645            knowledge: Some(KnowledgeLayer {
646                facts: vec![],
647                patterns: vec![],
648                insights: vec![],
649                exported_at: Utc::now(),
650            }),
651            ..Default::default()
652        };
653
654        let dir = tempfile::tempdir().unwrap();
655        let report = load_package(&manifest, &content, dir.path().to_str().unwrap()).unwrap();
656        assert!(!report.warnings.iter().any(|w| w.contains("optional-pkg")));
657    }
658
659    #[test]
660    fn load_report_display_format() {
661        let report = LoadReport {
662            package_name: "my-pkg".into(),
663            package_version: "1.0.0".into(),
664            knowledge_facts_merged: 5,
665            knowledge_facts_skipped: 2,
666            knowledge_patterns_merged: 3,
667            knowledge_insights_merged: 1,
668            graph_nodes_imported: 10,
669            graph_edges_imported: 8,
670            gotchas_imported: 4,
671            patterns_imported: 2,
672            session_findings_merged: 3,
673            session_decisions_merged: 1,
674            v2_nodes_added: 0,
675            v2_nodes_updated: 0,
676            v2_edges_added: 0,
677            v2_edges_merged: 0,
678            v2_conflicts: vec![],
679            warnings: vec!["test warning".into()],
680        };
681
682        let display = format!("{report}");
683        assert!(display.contains("my-pkg v1.0.0"));
684        assert!(display.contains("5 facts merged"));
685        assert!(display.contains("10 nodes"));
686        assert!(display.contains("2 imported (standalone)"));
687        assert!(display.contains("WARNING: test warning"));
688    }
689
690    #[test]
691    fn v2_graph_import_creates_local_graph() {
692        use crate::core::context_package::graph_model::{ContextEdge, ContextGraph, ContextNode};
693
694        let mut graph = ContextGraph::new();
695        graph.add_node(ContextNode::fact("n1", "test fact", "arch"));
696        graph.add_node(ContextNode::gotcha("n2", "trigger", "resolution"));
697        graph.add_edge(ContextEdge {
698            from: "n1".into(),
699            to: "n2".into(),
700            edge_type: "has_gotcha".into(),
701            weight: 0.9,
702            coactivations: 5,
703            metadata: None,
704        });
705
706        let content = PackageContent {
707            context_graph: Some(graph),
708            ..Default::default()
709        };
710
711        let mut manifest = test_manifest(vec![]);
712        manifest.schema_version = 2;
713        manifest.conformance_level = Some(2);
714
715        let dir = tempfile::tempdir().unwrap();
716        let report = load_package(&manifest, &content, dir.path().to_str().unwrap()).unwrap();
717
718        assert_eq!(report.v2_nodes_added, 2);
719        assert_eq!(report.v2_edges_added, 1);
720    }
721
722    #[test]
723    fn v2_load_report_display_includes_graph() {
724        let report = LoadReport {
725            package_name: "v2-pkg".into(),
726            package_version: "2.0.0".into(),
727            v2_nodes_added: 15,
728            v2_nodes_updated: 3,
729            v2_edges_added: 20,
730            v2_edges_merged: 5,
731            v2_conflicts: vec!["conflict A".into()],
732            ..Default::default()
733        };
734
735        let display = format!("{report}");
736        assert!(display.contains("15 nodes added"));
737        assert!(display.contains("3 updated"));
738        assert!(display.contains("20 edges added"));
739        assert!(display.contains("5 merged"));
740        assert!(display.contains("CONFLICT: conflict A"));
741    }
742
743    #[test]
744    fn v2_graph_import_merges_with_existing() {
745        use crate::core::context_package::graph_model::{ContextGraph, ContextNode};
746
747        let mut first_graph = ContextGraph::new();
748        first_graph.add_node(ContextNode::fact("shared", "original", "cat"));
749
750        let first_content = PackageContent {
751            context_graph: Some(first_graph),
752            ..Default::default()
753        };
754
755        let mut manifest = test_manifest(vec![]);
756        manifest.schema_version = 2;
757
758        let dir = tempfile::tempdir().unwrap();
759        let r1 = load_package(&manifest, &first_content, dir.path().to_str().unwrap()).unwrap();
760        assert_eq!(r1.v2_nodes_added, 1);
761
762        let mut second_graph = ContextGraph::new();
763        let mut node = ContextNode::fact("shared", "updated", "cat");
764        node.activation = 0.9;
765        second_graph.add_node(node);
766        second_graph.add_node(ContextNode::fact("new_node", "new content", "cat"));
767
768        let second_content = PackageContent {
769            context_graph: Some(second_graph),
770            ..Default::default()
771        };
772
773        let r2 = load_package(&manifest, &second_content, dir.path().to_str().unwrap()).unwrap();
774        assert_eq!(r2.v2_nodes_added, 1);
775        assert_eq!(r2.v2_nodes_updated, 1);
776    }
777}