Skip to main content

changeset_operations/operations/
init.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use changeset_manifest::{InitConfig, MetadataSection};
5use changeset_project::{CargoProject, ProjectKind, RootChangesetConfig};
6
7use crate::Result;
8use crate::traits::{
9    ChangelogSettingsInput, GitSettingsInput, InitInteractionProvider, ManifestWriter,
10    ProjectContext, ProjectProvider, VersionSettingsInput,
11};
12
13/// Input for the init operation.
14///
15/// Configuration sources have the following precedence (highest to lowest):
16/// 1. `defaults: true` - Uses all default values, ignores other fields
17/// 2. Explicit `git_config`, `changelog_config`, `version_config` fields
18/// 3. Interactive prompts via `InitInteractionProvider` (only if no explicit config)
19#[derive(Debug, Default)]
20pub struct InitInput {
21    pub defaults: bool,
22    pub git_config: Option<GitSettingsInput>,
23    pub changelog_config: Option<ChangelogSettingsInput>,
24    pub version_config: Option<VersionSettingsInput>,
25}
26
27/// A preview of what the init operation will do, without performing any changes.
28#[derive(Debug)]
29pub struct InitPlan {
30    pub changeset_dir: PathBuf,
31    pub dir_exists: bool,
32    pub gitkeep_exists: bool,
33    pub metadata_section: MetadataSection,
34    pub config: InitConfig,
35}
36
37#[derive(Debug)]
38#[must_use]
39pub struct InitOutput {
40    pub changeset_dir: PathBuf,
41    pub created_dir: bool,
42    pub created_gitkeep: bool,
43    pub wrote_config: bool,
44    pub config_location: Option<MetadataSection>,
45}
46
47pub struct InitOperation<P, M = (), I = ()> {
48    project_provider: P,
49    manifest_writer: Option<M>,
50    interaction_provider: Option<I>,
51}
52
53impl<P> InitOperation<P, (), ()>
54where
55    P: ProjectProvider,
56{
57    pub fn new(project_provider: P) -> Self {
58        Self {
59            project_provider,
60            manifest_writer: None,
61            interaction_provider: None,
62        }
63    }
64}
65
66impl<P, M, I> InitOperation<P, M, I>
67where
68    P: ProjectProvider,
69{
70    #[must_use]
71    pub fn with_manifest_writer<M2>(self, writer: M2) -> InitOperation<P, M2, I> {
72        InitOperation {
73            project_provider: self.project_provider,
74            manifest_writer: Some(writer),
75            interaction_provider: self.interaction_provider,
76        }
77    }
78
79    #[must_use]
80    pub fn with_interaction_provider<I2>(self, provider: I2) -> InitOperation<P, M, I2> {
81        InitOperation {
82            project_provider: self.project_provider,
83            manifest_writer: self.manifest_writer,
84            interaction_provider: Some(provider),
85        }
86    }
87}
88
89impl<P, M, I> InitOperation<P, M, I>
90where
91    P: ProjectProvider,
92    M: ManifestWriter,
93    I: InitInteractionProvider,
94{
95    /// Prepares an initialization plan by collecting all configuration without
96    /// performing any file system operations.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if the project cannot be discovered or configuration
101    /// cannot be built (e.g., interactive prompts fail).
102    pub fn prepare(&self, start_path: &Path, input: &InitInput) -> Result<InitPlan> {
103        let project = self.project_provider.discover_project(start_path)?;
104        let (root_config, _) = self.project_provider.load_configs(&project)?;
105
106        let context = ProjectContext {
107            is_single_package: project.kind == ProjectKind::SinglePackage,
108        };
109        let config = self.build_config(input, context)?;
110
111        Ok(build_init_plan(&project, &root_config, config))
112    }
113
114    /// Executes the init operation using a pre-built plan.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if the changeset directory cannot be created or
119    /// configuration cannot be written.
120    pub fn execute_plan(&self, start_path: &Path, plan: &InitPlan) -> Result<InitOutput> {
121        let project = self.project_provider.discover_project(start_path)?;
122        let (root_config, _) = self.project_provider.load_configs(&project)?;
123
124        let changeset_dir = self
125            .project_provider
126            .ensure_changeset_dir(&project, &root_config)?;
127
128        let gitkeep_path = changeset_dir.join(".gitkeep");
129        if !plan.gitkeep_exists {
130            fs::write(&gitkeep_path, "")?;
131        }
132
133        let wrote_config = if let Some(ref writer) = self.manifest_writer {
134            if plan.config.is_empty() {
135                false
136            } else {
137                let manifest_path = project.root.join("Cargo.toml");
138                writer.write_metadata(&manifest_path, plan.metadata_section, &plan.config)?;
139                true
140            }
141        } else {
142            false
143        };
144
145        Ok(InitOutput {
146            changeset_dir,
147            created_dir: !plan.dir_exists,
148            created_gitkeep: !plan.gitkeep_exists,
149            wrote_config,
150            config_location: if wrote_config {
151                Some(plan.metadata_section)
152            } else {
153                None
154            },
155        })
156    }
157
158    /// Executes the full init operation (prepare + execute).
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if the project cannot be discovered, the changeset
163    /// directory cannot be created, or configuration cannot be written.
164    pub fn execute(&self, start_path: &Path, input: &InitInput) -> Result<InitOutput> {
165        let plan = self.prepare(start_path, input)?;
166        self.execute_plan(start_path, &plan)
167    }
168
169    fn build_config(&self, input: &InitInput, context: ProjectContext) -> Result<InitConfig> {
170        if input.defaults {
171            return Ok(build_default_config(context));
172        }
173
174        let mut config = InitConfig::default();
175
176        if let Some(ref git) = input.git_config {
177            config.commit = Some(git.commit);
178            config.tags = Some(git.tags);
179            config.keep_changesets = Some(git.keep_changesets);
180            config.tag_format = Some(git.tag_format);
181        }
182
183        if let Some(ref changelog) = input.changelog_config {
184            config.changelog = Some(changelog.changelog);
185            config.comparison_links = Some(changelog.comparison_links);
186        }
187
188        if let Some(ref version) = input.version_config {
189            config.zero_version_behavior = Some(version.zero_version_behavior);
190        }
191
192        if config.is_empty() {
193            if let Some(ref provider) = self.interaction_provider {
194                if let Some(git) = provider.configure_git_settings(context)? {
195                    config.commit = Some(git.commit);
196                    config.tags = Some(git.tags);
197                    config.keep_changesets = Some(git.keep_changesets);
198                    config.tag_format = Some(git.tag_format);
199                }
200
201                if let Some(changelog) = provider.configure_changelog_settings(context)? {
202                    config.changelog = Some(changelog.changelog);
203                    config.comparison_links = Some(changelog.comparison_links);
204                }
205
206                if let Some(version) = provider.configure_version_settings()? {
207                    config.zero_version_behavior = Some(version.zero_version_behavior);
208                }
209            }
210        }
211
212        Ok(config)
213    }
214}
215
216/// Builds an `InitPlan` from project information and configuration.
217fn build_init_plan(
218    project: &CargoProject,
219    root_config: &RootChangesetConfig,
220    config: InitConfig,
221) -> InitPlan {
222    let changeset_dir_path = root_config.changeset_dir();
223    let full_changeset_dir = project.root.join(changeset_dir_path);
224    let dir_exists = full_changeset_dir.exists();
225    let gitkeep_exists = full_changeset_dir.join(".gitkeep").exists();
226
227    let metadata_section = match project.kind {
228        ProjectKind::VirtualWorkspace | ProjectKind::WorkspaceWithRoot => {
229            MetadataSection::Workspace
230        }
231        ProjectKind::SinglePackage => MetadataSection::Package,
232    };
233
234    InitPlan {
235        changeset_dir: full_changeset_dir,
236        dir_exists,
237        gitkeep_exists,
238        metadata_section,
239        config,
240    }
241}
242
243/// Builds the default configuration with all options set to their defaults.
244///
245/// The tag format default varies by project type:
246/// - Single package: `version-only` (e.g., `v1.0.0`)
247/// - Workspace: `crate-prefixed` (e.g., `crate-name@1.0.0`)
248#[must_use]
249pub fn build_default_config(context: ProjectContext) -> InitConfig {
250    let tag_format = if context.is_single_package {
251        changeset_manifest::TagFormat::VersionOnly
252    } else {
253        changeset_manifest::TagFormat::CratePrefixed
254    };
255
256    InitConfig {
257        commit: Some(true),
258        tags: Some(true),
259        keep_changesets: Some(false),
260        tag_format: Some(tag_format),
261        changelog: Some(changeset_manifest::ChangelogLocation::default()),
262        comparison_links: Some(changeset_manifest::ComparisonLinks::default()),
263        zero_version_behavior: Some(changeset_manifest::ZeroVersionBehavior::default()),
264    }
265}
266
267/// Builds an `InitConfig` from the provided input settings.
268#[must_use]
269pub fn build_config_from_input(input: &InitInput, context: ProjectContext) -> InitConfig {
270    if input.defaults {
271        return build_default_config(context);
272    }
273
274    let mut config = InitConfig::default();
275
276    if let Some(ref git) = input.git_config {
277        config.commit = Some(git.commit);
278        config.tags = Some(git.tags);
279        config.keep_changesets = Some(git.keep_changesets);
280        config.tag_format = Some(git.tag_format);
281    }
282
283    if let Some(ref changelog) = input.changelog_config {
284        config.changelog = Some(changelog.changelog);
285        config.comparison_links = Some(changelog.comparison_links);
286    }
287
288    if let Some(ref version) = input.version_config {
289        config.zero_version_behavior = Some(version.zero_version_behavior);
290    }
291
292    config
293}
294
295impl<P> InitOperation<P, (), ()>
296where
297    P: ProjectProvider,
298{
299    /// Prepares a simple initialization plan without configuration.
300    ///
301    /// # Errors
302    ///
303    /// Returns an error if the project cannot be discovered.
304    pub fn prepare_simple(&self, start_path: &Path) -> Result<InitPlan> {
305        let project = self.project_provider.discover_project(start_path)?;
306        let (root_config, _) = self.project_provider.load_configs(&project)?;
307
308        Ok(build_init_plan(
309            &project,
310            &root_config,
311            InitConfig::default(),
312        ))
313    }
314
315    /// Executes the simple init operation using a pre-built plan.
316    ///
317    /// # Errors
318    ///
319    /// Returns an error if the changeset directory cannot be created.
320    pub fn execute_simple_plan(&self, start_path: &Path, plan: &InitPlan) -> Result<InitOutput> {
321        let project = self.project_provider.discover_project(start_path)?;
322        let (root_config, _) = self.project_provider.load_configs(&project)?;
323
324        let changeset_dir = self
325            .project_provider
326            .ensure_changeset_dir(&project, &root_config)?;
327
328        let gitkeep_path = changeset_dir.join(".gitkeep");
329        if !plan.gitkeep_exists {
330            fs::write(&gitkeep_path, "")?;
331        }
332
333        Ok(InitOutput {
334            changeset_dir,
335            created_dir: !plan.dir_exists,
336            created_gitkeep: !plan.gitkeep_exists,
337            wrote_config: false,
338            config_location: None,
339        })
340    }
341
342    /// Simple execute method for backward compatibility when no manifest writer
343    /// or interaction provider is configured.
344    ///
345    /// # Errors
346    ///
347    /// Returns an error if the project cannot be discovered or the changeset
348    /// directory cannot be created.
349    pub fn execute_simple(&self, start_path: &Path) -> Result<InitOutput> {
350        let plan = self.prepare_simple(start_path)?;
351        self.execute_simple_plan(start_path, &plan)
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use std::sync::Arc;
358
359    use changeset_manifest::{ChangelogLocation, ComparisonLinks, TagFormat, ZeroVersionBehavior};
360
361    use super::*;
362    use crate::mocks::{MockInitInteractionProvider, MockManifestWriter, MockProjectProvider};
363
364    #[test]
365    fn returns_changeset_dir_path() {
366        let dir = tempfile::tempdir().expect("create temp dir");
367        let changeset_dir = dir.path().join(".changeset");
368        std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
369
370        let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
371            .with_changeset_dir(changeset_dir.clone());
372
373        let operation = InitOperation::new(project_provider);
374
375        let result = operation
376            .execute_simple(Path::new("/any"))
377            .expect("InitOperation failed for single-package project");
378
379        assert_eq!(result.changeset_dir, changeset_dir);
380    }
381
382    #[test]
383    fn works_with_workspace_projects() {
384        let dir = tempfile::tempdir().expect("create temp dir");
385        let changeset_dir = dir.path().join(".changeset");
386        std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
387
388        let project_provider =
389            MockProjectProvider::workspace(vec![("crate-a", "1.0.0"), ("crate-b", "2.0.0")])
390                .with_changeset_dir(changeset_dir.clone());
391
392        let operation = InitOperation::new(project_provider);
393
394        let result = operation
395            .execute_simple(Path::new("/any"))
396            .expect("InitOperation failed for workspace project");
397
398        assert!(
399            result
400                .changeset_dir
401                .to_string_lossy()
402                .contains(".changeset")
403        );
404    }
405
406    #[test]
407    fn creates_gitkeep_file() {
408        let dir = tempfile::tempdir().expect("create temp dir");
409        let changeset_dir = dir.path().join(".changeset");
410        std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
411
412        let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
413            .with_changeset_dir(changeset_dir.clone());
414
415        let operation = InitOperation::new(project_provider);
416
417        let result = operation
418            .execute_simple(Path::new("/any"))
419            .expect("InitOperation failed");
420
421        assert!(result.created_gitkeep);
422        assert!(changeset_dir.join(".gitkeep").exists());
423    }
424
425    #[test]
426    fn creates_gitkeep_even_when_dir_exists() {
427        let dir = tempfile::tempdir().expect("create temp dir");
428        let changeset_dir = dir.path().join(".changeset");
429        std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
430
431        let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
432            .with_changeset_dir(changeset_dir.clone());
433
434        let operation = InitOperation::new(project_provider);
435        let result = operation
436            .execute_simple(Path::new("/any"))
437            .expect("InitOperation failed");
438
439        assert!(!result.created_dir);
440        assert!(result.created_gitkeep);
441        assert!(changeset_dir.join(".gitkeep").exists());
442    }
443
444    #[test]
445    fn writes_config_with_defaults() {
446        let dir = tempfile::tempdir().expect("create temp dir");
447        let changeset_dir = dir.path().join(".changeset");
448        std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
449
450        let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
451            .with_changeset_dir(changeset_dir.clone());
452        let manifest_writer = Arc::new(MockManifestWriter::new());
453        let interaction_provider = Arc::new(MockInitInteractionProvider::new());
454
455        let operation = InitOperation::new(project_provider)
456            .with_manifest_writer(Arc::clone(&manifest_writer))
457            .with_interaction_provider(Arc::clone(&interaction_provider));
458
459        let input = InitInput {
460            defaults: true,
461            ..Default::default()
462        };
463
464        let result = operation
465            .execute(Path::new("/any"), &input)
466            .expect("InitOperation failed");
467
468        assert!(result.wrote_config);
469        assert_eq!(result.config_location, Some(MetadataSection::Package));
470
471        let written = manifest_writer.written_metadata();
472        assert_eq!(written.len(), 1);
473        let (_, section, config) = &written[0];
474        assert_eq!(*section, MetadataSection::Package);
475        assert_eq!(config.commit, Some(true));
476        assert_eq!(config.tags, Some(true));
477        assert_eq!(config.keep_changesets, Some(false));
478    }
479
480    #[test]
481    fn writes_config_from_input() {
482        let dir = tempfile::tempdir().expect("create temp dir");
483        let changeset_dir = dir.path().join(".changeset");
484        std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
485
486        let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
487            .with_changeset_dir(changeset_dir.clone());
488        let manifest_writer = Arc::new(MockManifestWriter::new());
489        let interaction_provider = Arc::new(MockInitInteractionProvider::new());
490
491        let operation = InitOperation::new(project_provider)
492            .with_manifest_writer(Arc::clone(&manifest_writer))
493            .with_interaction_provider(Arc::clone(&interaction_provider));
494
495        let input = InitInput {
496            defaults: false,
497            git_config: Some(GitSettingsInput {
498                commit: false,
499                tags: true,
500                keep_changesets: true,
501                tag_format: TagFormat::CratePrefixed,
502            }),
503            changelog_config: Some(ChangelogSettingsInput {
504                changelog: ChangelogLocation::PerPackage,
505                comparison_links: ComparisonLinks::Enabled,
506            }),
507            version_config: Some(VersionSettingsInput {
508                zero_version_behavior: ZeroVersionBehavior::AutoPromoteOnMajor,
509            }),
510        };
511
512        let result = operation
513            .execute(Path::new("/any"), &input)
514            .expect("InitOperation failed");
515
516        assert!(result.wrote_config);
517
518        let written = manifest_writer.written_metadata();
519        assert_eq!(written.len(), 1);
520        let (_, _, config) = &written[0];
521        assert_eq!(config.commit, Some(false));
522        assert_eq!(config.tags, Some(true));
523        assert_eq!(config.keep_changesets, Some(true));
524        assert_eq!(config.tag_format, Some(TagFormat::CratePrefixed));
525        assert_eq!(config.changelog, Some(ChangelogLocation::PerPackage));
526        assert_eq!(config.comparison_links, Some(ComparisonLinks::Enabled));
527        assert_eq!(
528            config.zero_version_behavior,
529            Some(ZeroVersionBehavior::AutoPromoteOnMajor)
530        );
531    }
532
533    #[test]
534    fn writes_partial_config() {
535        let dir = tempfile::tempdir().expect("create temp dir");
536        let changeset_dir = dir.path().join(".changeset");
537        std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
538
539        let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
540            .with_changeset_dir(changeset_dir.clone());
541        let manifest_writer = Arc::new(MockManifestWriter::new());
542        let interaction_provider = Arc::new(MockInitInteractionProvider::new());
543
544        let operation = InitOperation::new(project_provider)
545            .with_manifest_writer(Arc::clone(&manifest_writer))
546            .with_interaction_provider(Arc::clone(&interaction_provider));
547
548        let input = InitInput {
549            defaults: false,
550            git_config: Some(GitSettingsInput {
551                commit: true,
552                tags: false,
553                keep_changesets: false,
554                tag_format: TagFormat::VersionOnly,
555            }),
556            changelog_config: None,
557            version_config: None,
558        };
559
560        let result = operation
561            .execute(Path::new("/any"), &input)
562            .expect("InitOperation failed");
563
564        assert!(result.wrote_config);
565
566        let written = manifest_writer.written_metadata();
567        assert_eq!(written.len(), 1);
568        let (_, _, config) = &written[0];
569        assert_eq!(config.commit, Some(true));
570        assert_eq!(config.tags, Some(false));
571        assert!(config.changelog.is_none());
572        assert!(config.zero_version_behavior.is_none());
573    }
574
575    #[test]
576    fn interactive_mode_collects_all_groups() {
577        let dir = tempfile::tempdir().expect("create temp dir");
578        let changeset_dir = dir.path().join(".changeset");
579        std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
580
581        let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
582            .with_changeset_dir(changeset_dir.clone());
583        let manifest_writer = Arc::new(MockManifestWriter::new());
584        let interaction_provider = Arc::new(MockInitInteractionProvider::all_defaults());
585
586        let operation = InitOperation::new(project_provider)
587            .with_manifest_writer(Arc::clone(&manifest_writer))
588            .with_interaction_provider(Arc::clone(&interaction_provider));
589
590        let input = InitInput::default();
591
592        let result = operation
593            .execute(Path::new("/any"), &input)
594            .expect("InitOperation failed");
595
596        assert!(result.wrote_config);
597
598        let written = manifest_writer.written_metadata();
599        assert_eq!(written.len(), 1);
600        let (_, _, config) = &written[0];
601        assert!(config.commit.is_some());
602        assert!(config.tags.is_some());
603        assert!(config.changelog.is_some());
604        assert!(config.zero_version_behavior.is_some());
605    }
606
607    #[test]
608    fn interactive_mode_skips_declined_groups() {
609        let dir = tempfile::tempdir().expect("create temp dir");
610        let changeset_dir = dir.path().join(".changeset");
611        std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
612
613        let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
614            .with_changeset_dir(changeset_dir.clone());
615        let manifest_writer = Arc::new(MockManifestWriter::new());
616        let interaction_provider = Arc::new(
617            MockInitInteractionProvider::new()
618                .with_git_settings(Some(GitSettingsInput::default()))
619                .with_changelog_settings(None)
620                .with_version_settings(None),
621        );
622
623        let operation = InitOperation::new(project_provider)
624            .with_manifest_writer(Arc::clone(&manifest_writer))
625            .with_interaction_provider(Arc::clone(&interaction_provider));
626
627        let input = InitInput::default();
628
629        let result = operation
630            .execute(Path::new("/any"), &input)
631            .expect("InitOperation failed");
632
633        assert!(result.wrote_config);
634
635        let written = manifest_writer.written_metadata();
636        assert_eq!(written.len(), 1);
637        let (_, _, config) = &written[0];
638        assert!(config.commit.is_some());
639        assert!(config.changelog.is_none());
640        assert!(config.zero_version_behavior.is_none());
641    }
642
643    #[test]
644    fn skips_config_write_when_empty() {
645        let dir = tempfile::tempdir().expect("create temp dir");
646        let changeset_dir = dir.path().join(".changeset");
647        std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
648
649        let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
650            .with_changeset_dir(changeset_dir.clone());
651        let manifest_writer = Arc::new(MockManifestWriter::new());
652        let interaction_provider = Arc::new(MockInitInteractionProvider::all_skipped());
653
654        let operation = InitOperation::new(project_provider)
655            .with_manifest_writer(Arc::clone(&manifest_writer))
656            .with_interaction_provider(Arc::clone(&interaction_provider));
657
658        let input = InitInput::default();
659
660        let result = operation
661            .execute(Path::new("/any"), &input)
662            .expect("InitOperation failed");
663
664        assert!(!result.wrote_config);
665        assert!(result.config_location.is_none());
666
667        let written = manifest_writer.written_metadata();
668        assert!(written.is_empty());
669    }
670
671    #[test]
672    fn workspace_uses_workspace_metadata() {
673        let dir = tempfile::tempdir().expect("create temp dir");
674        let changeset_dir = dir.path().join(".changeset");
675        std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
676
677        let project_provider =
678            MockProjectProvider::workspace(vec![("crate-a", "1.0.0"), ("crate-b", "2.0.0")])
679                .with_changeset_dir(changeset_dir.clone());
680        let manifest_writer = Arc::new(MockManifestWriter::new());
681        let interaction_provider = Arc::new(MockInitInteractionProvider::new());
682
683        let operation = InitOperation::new(project_provider)
684            .with_manifest_writer(Arc::clone(&manifest_writer))
685            .with_interaction_provider(Arc::clone(&interaction_provider));
686
687        let input = InitInput {
688            defaults: true,
689            ..Default::default()
690        };
691
692        let result = operation
693            .execute(Path::new("/any"), &input)
694            .expect("InitOperation failed");
695
696        assert!(result.wrote_config);
697        assert_eq!(result.config_location, Some(MetadataSection::Workspace));
698
699        let written = manifest_writer.written_metadata();
700        assert_eq!(written.len(), 1);
701        let (_, section, _) = &written[0];
702        assert_eq!(*section, MetadataSection::Workspace);
703    }
704
705    #[test]
706    fn single_package_uses_package_metadata() {
707        let dir = tempfile::tempdir().expect("create temp dir");
708        let changeset_dir = dir.path().join(".changeset");
709        std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
710
711        let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
712            .with_changeset_dir(changeset_dir.clone());
713        let manifest_writer = Arc::new(MockManifestWriter::new());
714        let interaction_provider = Arc::new(MockInitInteractionProvider::new());
715
716        let operation = InitOperation::new(project_provider)
717            .with_manifest_writer(Arc::clone(&manifest_writer))
718            .with_interaction_provider(Arc::clone(&interaction_provider));
719
720        let input = InitInput {
721            defaults: true,
722            ..Default::default()
723        };
724
725        let result = operation
726            .execute(Path::new("/any"), &input)
727            .expect("InitOperation failed");
728
729        assert!(result.wrote_config);
730        assert_eq!(result.config_location, Some(MetadataSection::Package));
731
732        let written = manifest_writer.written_metadata();
733        assert_eq!(written.len(), 1);
734        let (_, section, _) = &written[0];
735        assert_eq!(*section, MetadataSection::Package);
736    }
737}