tests.test_init_project module¶

Tests for bundled configuration templates and repository initialization.

tests.test_init_project.written_file_count(entry)[source]¶

Count the files one registry entry writes: a whole tree, else one.

A tree entry is a skill folder, which can ship references/ pages and other resources beside its SKILL.md. A test that counted the entry itself would fail the day a skill gains one.

Return type:

int

tests.test_init_project.toml_file(tmp_path: Path)[source]¶

Return a factory writing TOML content to a scratch pyproject.toml.

Replaces the NamedTemporaryFile(delete=False) plus manual unlink() dance: pytest removes tmp_path whether or not the test passes, so a failing assertion no longer leaks the file. Each call gets its own directory, so a test may seed several projects.

tests.test_init_project.pin_build(monkeypatch: MonkeyPatch)[source]¶

Return a factory pinning the running build init reports itself as.

run_init and resolve_default_pin read the package’s __version__ and __git_tag_sha__ to decide which upstream ref to write downstream, so almost every pin test has to stage both.

tests.test_init_project.test_all_component_types_handled()[source]¶

Verify every component in the registry has a handled type.

The type-driven dispatch loop in run_init handles these types. If a new component subclass is added without updating the dispatch, this test will catch it.

Return type:

None

tests.test_init_project.test_composite_actions_keep_unmodified()[source]¶

Composite actions under .github/actions/ must set keep_unmodified=True.

GitHub Actions resolves uses: ./.github/actions/X and uses: owner/repo/.github/actions/X@ref directly from the repo path. The action file must remain on disk even when byte-identical to the bundled default; otherwise the autofix workflow deletes it as ā€œredundantā€ and breaks every caller (upstream and downstream).

Return type:

None

tests.test_init_project.test_repo_scope_matches(scope, is_awesome, is_python, is_package, expected)[source]¶

Verify RepoScope.matches returns correct results for all combinations.

Return type:

None

tests.test_init_project.test_init_help_lists_all_components()[source]¶

Verify the init command help text lists every registered component.

Return type:

None

tests.test_init_project.test_supported_config_types()[source]¶

Verify that expected config types are registered as ToolConfigComponent.

Return type:

None

tests.test_init_project.test_config_type_has_required_fields()[source]¶

Verify that each tool config component has all required fields.

Return type:

None

tests.test_init_project.test_returns_non_empty_string(config_type)[source]¶

Verify that export_content returns a non-empty string.

Return type:

None

tests.test_init_project.test_returns_valid_toml(config_type)[source]¶

Verify that the returned content is valid TOML.

Return type:

None

tests.test_init_project.test_native_format_no_tool_prefix(config_type)[source]¶

Verify that native format does not have [tool.X] prefix.

Return type:

None

tests.test_init_project.test_unknown_file_raises_error()[source]¶

Verify that an unknown file raises ValueError.

Return type:

None

tests.test_init_project.test_exportable_file_loadable(filename)[source]¶

Verify every registered data file can be loaded (no dangling symlinks).

Return type:

None

tests.test_init_project.test_native_templates_separate_header(comp)[source]¶

Verify a template’s file-level header ends with a blank line.

The separator is what repomatic.init_project._strip_header_comments keys on to tell a header apart from a comment documenting the first key, the two being otherwise identical: a comment run at the top of the file. Running the header straight into a key hides the key’s own documentation behind the same rule that drops the header, so it never reaches a downstream pyproject.toml.

Return type:

None

tests.test_init_project.test_strip_header_comments_keeps_key_comments(comp)[source]¶

Verify stripping removes the header and every comment below it survives.

The second half is the one that bites: skipping to the first non-comment line also passes the ā€œheader is goneā€ check, while silently taking the first key’s documentation with it.

Return type:

None

tests.test_init_project.test_template_matches_own_pyproject(config_type)[source]¶

Verify bundled template stays in sync with repomatic’s own config.

Template keys (minus intentional exclusions) must match the corresponding [tool.*] section. List keys marked as superset require every template entry to appear in own config, but own config may have extras.

Scope-specific configs (e.g., lychee for awesome repos) are excluded because their templates are intentionally different from repomatic’s own.

Return type:

None

tests.test_init_project.test_template_setting(template, key, expected)[source]¶

A bundled template pins the setting every downstream repo inherits.

Return type:

None

tests.test_init_project.test_template_list_contains(template, key, member)[source]¶

A bundled template carries the entry downstream repos depend on.

Return type:

None

tests.test_init_project.test_pytest_template_registers_once_marker()[source]¶

The once marker the test workflow filters on must be declared.

Return type:

None

tests.test_init_project.test_bumpversion_template_has_required_settings()[source]¶

Bumpversion needs these three keys to run unattended in CI.

Return type:

None

tests.test_init_project.test_bumpversion_template_declares_files()[source]¶

A bumpversion config with no file patterns would bump nothing.

Return type:

None

tests.test_init_project.BARE_PYPROJECT = '[project]\nname = "test"\nversion = "0.1.0"\n'¶

A minimal PEP 621 project, the starting point for every merge test.

tests.test_init_project.test_init_config_merges_template(toml_file, config_type, expected)[source]¶

Merging a template into a bare pyproject keeps its native TOML shape.

Return type:

None

tests.test_init_project.test_init_config_lychee_preserves_other_sections(toml_file)[source]¶

Lychee init merges [tool.lychee] without stripping unrelated sections.

Return type:

None

tests.test_init_project.test_adopted_ongoing_configs(tmp_path, content, expected)[source]¶

A section already on disk re-enters the bare-init set, if it is ONGOING.

EXPLICIT keeps init from pushing a tool config onto a repository that never asked for one; it must not also stop the ongoing sync of a section the repository already carries. BOOTSTRAP templates stay out either way: the repository owns them outright after the first write.

Return type:

None

tests.test_init_project.test_adopted_ongoing_configs_without_pyproject(tmp_path)[source]¶

A repository with no pyproject.toml adopts nothing.

Return type:

None

tests.test_init_project.test_adopted_ongoing_configs_are_explicit_and_ongoing(tmp_path)[source]¶

Whatever the helper returns is an EXPLICIT, ONGOING tool config.

Guards the invariant against a registry edit that flips a component’s init_default or sync_mode without revisiting this selection path.

Return type:

None

tests.test_init_project.test_uv_component_uses_overlay_ongoing()[source]¶

The uv tool config is an ongoing overlay, not a full-section rebuild.

Return type:

None

tests.test_init_project.test_init_config_uv_overlay_noop_when_pins_match(toml_file)[source]¶

A [tool.uv] already carrying the canonical pins is a no-op.

The overlay updates owned keys in place, so a section whose pins already match the template returns no change. A regression to the rebuild-and-graft path would reorder the owned keys ahead of the project’s keys and return a diff, failing this fixpoint guard.

Return type:

None

tests.test_init_project.test_init_config_uv_overlay_updates_stale_pins_in_place(toml_file)[source]¶

Stale uv pins are updated in place, preserving order and local keys.

Return type:

None

tests.test_init_project.test_init_config_uv_overlay_appends_missing_pins(toml_file)[source]¶

A [tool.uv] lacking the pins gets both appended, local keys intact.

Return type:

None

tests.test_init_project.test_full_ruff_init(toml_file)[source]¶

Verify full ruff config initialization.

Return type:

None

tests.test_init_project.test_adds_config_to_empty_pyproject(toml_file)[source]¶

Verify that config is added to a pyproject.toml without the section.

Return type:

None

tests.test_init_project.test_adds_bumpversion_with_array_sections(toml_file)[source]¶

Verify that bumpversion [[files]] sections are transformed.

Return type:

None

tests.test_init_project.test_returns_none_if_section_exists(toml_file)[source]¶

Verify that None is returned if the section already exists.

Return type:

None

tests.test_init_project.test_preserves_existing_content(toml_file)[source]¶

Verify that existing content is preserved when adding config.

Return type:

None

tests.test_init_project.test_unknown_config_type_raises_error()[source]¶

Verify that an unknown config type raises TypeError.

Return type:

None

tests.test_init_project.test_default_version_pin()[source]¶

Verify version pin is derived correctly.

tests.test_init_project.test_select_cooldown_pin(base, expected)[source]¶
tests.test_init_project.test_select_cooldown_pin_no_aged_predecessor()[source]¶

A fresh base with no cleared predecessor has nothing to step back to.

tests.test_init_project.test_resolve_default_pin_aged_version_is_noop(monkeypatch, pin_build)[source]¶

An aged running version pins itself with its build SHA, no step-back.

Locks in the CI sync-repomatic invariant: init at the already-pinned version never moves the pin and never resolves a replacement SHA.

tests.test_init_project.test_resolve_default_pin_steps_back_when_fresh(pin_build)[source]¶

A fresh running version steps the pin back, re-resolves the SHA, and warns.

The step-back note is appended to the caller-supplied warnings list so run_init can surface it in the init summary, not only in the log.

tests.test_init_project.test_resolve_default_pin_aged_version_leaves_warnings_empty(pin_build)[source]¶

The no-op path appends no warning.

tests.test_init_project.test_resolve_default_pin_dev_version_skips_datasource(monkeypatch, pin_build)[source]¶

An unreleased dev cut pins its base version without any network call.

tests.test_init_project.test_resolve_default_pin_zero_window_skips_datasource(monkeypatch, pin_build)[source]¶

A disabled cooldown (0 days) pins the running version without a lookup.

tests.test_init_project.test_resolve_default_pin_fails_open_when_datasource_empty(monkeypatch, pin_build)[source]¶

An unreachable datasource falls back to the running version.

tests.test_init_project.test_highest_upstream_pin_none_without_refs(tmp_path)[source]¶

A repository carrying no upstream ref has no floor.

tests.test_init_project.test_highest_upstream_pin_converges_stragglers(tmp_path)[source]¶

The repository-wide maximum wins, so a lagging file cannot drag the pin down.

tests.test_init_project.test_highest_upstream_pin_prefers_a_hardened_ref(tmp_path)[source]¶

At equal versions the SHA-pinned ref wins, so the floor never unpins one.

tests.test_init_project.test_highest_upstream_pin_reads_composite_actions(tmp_path)[source]¶

Composite-action refs count too: release.yaml pins one alongside the lanes.

tests.test_init_project.test_resolve_default_pin_lifecycle(label, base, floor, expected, monkeypatch)[source]¶

The cooldown gates an adoption and never regresses a committed pin.

The steady-state rows are the ones that matter in CI: sync-repomatic runs init at the pinned version on every push, so a cooldown verdict recomputed there would downgrade the repository for a full window after each bump.

tests.test_init_project.test_resolve_default_pin_matching_floor_skips_datasource(monkeypatch, pin_build)[source]¶

A pin equal to the running version is answered without a network call.

Every CI sync-repomatic run takes this path, so it must not depend on the GitHub releases datasource being reachable.

tests.test_init_project.test_resolve_default_pin_matching_floor_keeps_on_disk_sha(pin_build)[source]¶

A build with no tag SHA of its own falls back to the pin already on disk.

Without the fallback, regenerating from a source checkout would rewrite a hardened @<sha> # vX.Y.Z ref into a bare tag pin, and nothing downstream re-hardens an upstream ref.

tests.test_init_project.test_resolve_default_pin_held_at_floor_warns(pin_build)[source]¶

Declining to adopt a fresh release is reported, naming the kept version.

tests.test_init_project.test_run_init_held_at_floor_leaves_workflows_untouched(tmp_path, pin_build)[source]¶

Declining the pin declines the caller content with it.

init renders workflow bodies from the running wheel and substitutes only the uses: ref, so regenerating them under a held-back pin ships the new release’s triggers, concurrency groups and env: against the old release’s reusable-workflow surface. Half an adoption is worse than none: the caller half lands with nothing pinned to serve it.

tests.test_init_project.test_run_init_first_adoption_writes_workflows_despite_step_back(tmp_path, pin_build)[source]¶

A repository with no pin yet still gets workflows when the cooldown bites.

The counterpart carve-out: holding the content back needs somewhere to stand, and a first-time adoption has no prior tree to keep. Skipping here would leave the repository with no workflows at all, so the pin/content skew stands as the lesser outcome.

tests.test_init_project.test_run_init_keeps_a_fresh_pin_already_on_disk(tmp_path, monkeypatch, pin_build)[source]¶

Re-running init at the pinned version regenerates without downgrading.

End-to-end shape of the CI lifecycle: a maintainer adopts a release on its publication day, then sync-repomatic re-runs init at that very version on every push. The pin must survive untouched.

tests.test_init_project.test_run_init_records_the_pin_move(tmp_path, pin_build)[source]¶

The result carries both halves of a pin move, and only a forward move.

A first adoption has no previous release, a regeneration at the pinned version moves nothing, and a rollback is deliberate: none of the three is an upgrade to review, which is what the closing hint keys on.

tests.test_init_project.test_run_init_reads_no_pin_without_a_workflow(tmp_path, pin_build)[source]¶

A config-only run neither reads nor writes an upstream pin.

tests.test_init_project.test_init_cli_prints_the_upgrade_launcher(tmp_path, pin_build)[source]¶

A moved pin ends the run with the command launching the upgrade review.

The launcher names both versions so the skill needs no discovery, and the audit follows it because the two answer different questions: what the release offers, then what the repository drifted from.

tests.test_init_project.test_init_cli_skips_the_install_hint_once_the_skill_is_on_disk(tmp_path, pin_build)[source]¶

The install line is for repositories that never installed the skill.

tests.test_init_project.test_init_cli_prints_no_launcher_without_a_pin_move(tmp_path, pin_build)[source]¶

A first adoption has no previous release to review against.

tests.test_init_project.test_run_init_no_cooldown_skips_datasource(tmp_path, monkeypatch, pin_build)[source]¶

–no-cooldown pins the running version and never consults the datasource.

tests.test_init_project.test_resolve_upgrade_target(tmp_path, monkeypatch, pin_build, floor, version, released, previous, held_back)[source]¶
tests.test_init_project.test_resolve_upgrade_target_opt_outs(tmp_path, monkeypatch, pin_build, config)[source]¶

Both opt-outs skip the upgrade before any datasource lookup.

tests.test_init_project.test_resolve_upgrade_target_skips_development_builds(tmp_path, monkeypatch, pin_build)[source]¶

A development build has no published release to compare against.

tests.test_init_project.test_resolve_upgrade_target_without_a_cleared_release(tmp_path, monkeypatch, pin_build, candidates)[source]¶

No release past the window, or no release data, leaves the pin alone.

tests.test_init_project.test_upgrade_target_in_process(pin_build)[source]¶

Only a target naming the running release skips the hand-off.

tests.test_init_project.test_upgrade_command()[source]¶

The child runs the target with the parent’s verdict and the window.

tests.test_init_project.test_upgrade_command_without_a_window()[source]¶

A disabled window passes no cutoff, and the exemption keeps the target.

tests.test_init_project.test_upgrade_report(tmp_path, monkeypatch)[source]¶

The report reads top-down: the move, what breaks, the run, the notes.

tests.test_init_project.test_upgrade_report_without_a_move(tmp_path, monkeypatch, previous, adopted)[source]¶

A run that moved no pin forward reports nothing, and fetches nothing.

tests.test_init_project.test_upgrade_report_without_breaking_entries(tmp_path, monkeypatch, notes, expected)[source]¶
tests.test_init_project.test_init_upgrade_takes_the_whole_tree(extra)[source]¶
tests.test_init_project.test_init_upgrade_hands_off_to_the_newest_cleared_release(tmp_path, monkeypatch, pin_build)[source]¶

A newer release runs as a child, and the report names both ends.

tests.test_init_project.test_init_upgrade_fails_with_the_child(tmp_path, monkeypatch, pin_build)[source]¶

A failed child fails the command, and no report claims an upgrade.

tests.test_init_project.test_init_upgrade_runs_in_process_on_the_target(tmp_path, monkeypatch, pin_build)[source]¶

On the target already: no child, and no second cooldown verdict.

tests.test_init_project.test_run_init_preserves_downstream_release_needs(tmp_path, pin_build)[source]¶

repomatic init carries a consumer’s own needs: edge across a sync.

Asserted on the entry point sync-repomatic actually runs, which is what a second renderer once made necessary: two copies of the same logic rendered the caller, only one was wired up, and the edge survived in the copy nothing called.

tests.test_init_project.test_run_init_is_idempotent(tmp_path, pin_build)[source]¶

A second init over its own output rewrites nothing.

sync-repomatic re-runs init on every push to main, so any non- convergent render becomes an unbounded drift that opens a fresh PR forever. Checked with downstream extras in the tree, which is where the separator between the managed lanes and those extras is re-derived.

tests.test_init_project.test_init_syncs_header_and_keeps_downstream_jobs(tmp_path, monkeypatch)[source]¶

A non-reusable workflow gets the canonical header, keeping its own jobs.

tests.yaml has no workflow_call trigger, so it is never rendered as a caller: only everything above its jobs: line is replaced.

tests.test_init_project.test_init_skips_a_non_reusable_workflow_absent_downstream(tmp_path, monkeypatch)[source]¶

Header-only sync never creates a file, it only updates one already there.

The downstream repo owns the jobs of a non-reusable workflow, so there is nothing to write when it has not adopted the workflow at all.

tests.test_init_project.test_init_realigns_an_inline_pin_in_downstream_jobs(tmp_path, monkeypatch, pinned)[source]¶

The inline pin follows the uses: refs init writes, in either direction.

The literal sits in a job body header-only sync never rewrites, so nothing else in this pass would touch it and the lint would go red until the next scheduled sync-workflow-pins run.

tests.test_init_project.test_init_adds_a_missing_cooldown_exemption(tmp_path, monkeypatch, pinned)[source]¶

A bare self-pin gets the exemption, or the command cannot resolve at all.

Every workflow exports a UV_EXCLUDE_NEWER, and the pin init writes moves in lockstep with the uses: refs regardless of the cooldown, so it routinely names a release younger than that window. A bare uvx 'repomatic==X.Y.Z' left behind here fails to install the very version just written.

tests.test_init_project.test_init_warns_about_a_metadata_key_this_version_dropped(tmp_path, monkeypatch)[source]¶

A workflow asking for a retired key is reported before the commit.

An unknown key is a hard UsageError, so the mismatch takes down the metadata job and every job hanging off it on the first push.

tests.test_init_project.test_init_skips_the_metadata_key_check_behind_a_cooldown(tmp_path, monkeypatch)[source]¶

A pin held back to an older release is judged by no key set at all.

The older release’s keys cannot be read from this process, and this version’s would blame a workflow that works.

tests.test_init_project.test_init_leaves_an_unrelated_pypi_pin_alone(tmp_path, monkeypatch)[source]¶

Only the upstream toolkit’s own literal is realigned, not every == pin.

tests.test_init_project.test_init_leaves_a_headerless_workflow_untouched(tmp_path, monkeypatch)[source]¶

Without a jobs: line there is no boundary to splice the header onto.

tests.test_init_project.test_init_header_sync_honors_paths_spec(tmp_path, monkeypatch)[source]¶

The synced header carries the repository’s own paths: filters.

tests.test_init_project.test_run_init_cooldown_steps_back_in_generated_pin(tmp_path, monkeypatch, pin_build)[source]¶

A fresh running version lands the stepped-back tag in the thin caller.

tests.test_init_project.test_init_default_components()[source]¶

Verify default selection includes expected components.

tests.test_init_project.test_init_creates_all_default_files(tmp_path, monkeypatch)[source]¶

Verify all default component files are created (with no exclusions).

tests.test_init_project.test_init_creates_changelog(tmp_path)[source]¶

Verify changelog is created with expected content.

tests.test_init_project.test_init_creates_parent_dirs(tmp_path)[source]¶

Verify .github/workflows/ is created automatically.

tests.test_init_project.test_init_workflow_paths_config_flows_to_generated_files(tmp_path)[source]¶

[tool.repomatic.workflow] paths knobs reach the generated workflows.

End-to-end check that a Config carrying extra_paths, ignore_paths, and per-workflow paths overrides is honored by _init_workflows when rendering thin callers.

tests.test_init_project.test_init_workflow_sync_disabled_leaves_workflows_untouched(tmp_path)[source]¶

[tool.repomatic.workflow] sync = false skips workflow generation.

Like the other *.sync toggles, the opt-out writes nothing and leaves any existing workflow file untouched rather than overwriting or deleting it. Applies even when workflows is named explicitly.

tests.test_init_project.test_init_existing_changelog_skipped(tmp_path)[source]¶

Verify existing changelog is not overwritten.

tests.test_init_project.test_init_idempotent(tmp_path)[source]¶

Verify a second run with no on-disk changes produces no updates.

Re-running repomatic init against an unchanged tree must be a no-op: files identical to what init would write are not reported as updated.

tests.test_init_project.test_init_per_component_idempotent(tmp_path, component)[source]¶

Verify re-running a single-component init produces no spurious updates.

Locks in the per-component no-op behavior: when bundled content matches what is already on disk, the second run must not report any file as updated. Regression guard for skills/workflows wrongly flagged as updated when their content was unchanged.

tests.test_init_project.test_init_only_labels(tmp_path)[source]¶

Verify only label files are created.

tests.test_init_project.test_init_only_skills(tmp_path)[source]¶

Verify only skill files are created.

Scope exclusions are bypassed when components are explicitly requested, so every skill is created, awesome-only ones included. The count is read off the registry rather than written here: a literal makes adding a skill fail a test that has nothing to say about it.

tests.test_init_project.test_skills_consistency()[source]¶

Verify all .claude/skills/ directories have matching entries in code.

Guards against adding a skill definition to .claude/skills/ without registering it in the component registry, SKILL_PHASES, and the repomatic/data/ symlinks.

tests.test_init_project.test_every_skill_phase_is_orderable()[source]¶

list-skills orders its rows by SKILL_PHASE_ORDER.index().

The two constants are declared apart: one names the phase of each registry entry, the other the lifecycle order they render in. A phase in the first and not the second used to drop its skill from the listing without a word, and now raises instead, so the roster can no longer go quietly short.

tests.test_init_project.test_init_only_workflows(tmp_path)[source]¶

Verify only workflow files are created.

Pinned to a default Config rather than the ambient one: config discovery walks up from the working directory, so this repository’s own [tool.repomatic] would otherwise decide which opt-in workflows the assertions below expect, and enabling one here would fail the test.

tests.test_init_project.test_init_always_overwrites_managed_files(tmp_path)[source]¶

Verify managed files are always replaced on re-run.

tests.test_init_project.test_init_changelog_never_overwritten(tmp_path)[source]¶

Verify an existing changelog.md is never overwritten.

tests.test_init_project.test_init_tool_configs_no_pyproject(tmp_path)[source]¶

Verify warning when pyproject.toml is missing.

tests.test_init_project.test_init_version_pinned(tmp_path)[source]¶

Verify generated workflows contain the specified version pin.

tests.test_init_project.test_init_with_specific_tool_configs(tmp_path)[source]¶

Verify multiple tool configs are merged into pyproject.toml.

tests.test_init_project.test_init_with_single_tool_config(tmp_path)[source]¶

Verify only requested tool config is merged.

tests.test_init_project.test_syncs_bumpversion_template_keys(tmp_path)[source]¶

Verify template keys are synced into existing bumpversion config.

Return type:

None

tests.test_init_project.test_replaces_bumpversion_files_from_template(tmp_path)[source]¶

Verify [[tool.bumpversion.files]] entries come from the template.

Return type:

None

tests.test_init_project.test_preserves_other_pyproject_sections(tmp_path)[source]¶

Verify [project] and other sections are unchanged.

Return type:

None

tests.test_init_project.test_skips_already_up_to_date(tmp_path, toml_file)[source]¶

Verify config matching the template returns None (no changes needed).

Return type:

None

tests.test_init_project.test_bumpversion_template_is_pyproject_fmt_fixed_point(tmp_path)[source]¶

sync-bumpversion output must already be a fixed point of pyproject-fmt.

sync-bumpversion rewrites [tool.bumpversion] from the bundled template, while format-pyproject reorders the same keys with pyproject-fmt. When the template’s key order diverges from pyproject-fmt’s, the two autofix jobs ping-pong: one PR imposes the template order, the next reverts it.

The in-tree pyproject.toml has been processed by format-pyproject, so its [tool.bumpversion] block is the canonical pyproject-fmt output. Asserting the freshly merged template equals it textually (modulo current_version, the only preserved key) locks the order. Unlike test_template_matches_own_pyproject, which compares parsed dicts, this compares text, so it catches key-order drift.

Return type:

None

tests.test_init_project.test_ongoing_sync_template_survives_pyproject_fmt(config_type, tmp_path)[source]¶

Templates re-synced into pyproject.toml must be pyproject-fmt fixed points.

A config that repomatic init re-merges on every push (sync_mode=ONGOING, via the sync-bumpversion job or a bare repomatic init in sync-repomatic) shares pyproject.toml with format-pyproject. If its bundled template is not already in pyproject-fmt’s canonical form, the two autofix jobs ping-pong: one rewrites the section, the next reformats it.

This merges each template into a throwaway pyproject.toml, runs the pinned pyproject-fmt over it, and asserts the [tool.X] block is unchanged. It is the category guard for the bumpversion ↔ format-pyproject loop, and also covers scope-specific templates (lychee, opted into a Python project) that have no in-tree section to compare against.

Marked once: it shells out to pyproject-fmt via uvx, so a single runner suffices. Skipped when pyproject-fmt cannot be fetched (offline local dev); CI is the enforcement point.

Return type:

None

tests.test_init_project.test_ongoing_resync_preserves_local_string_style(config_type, tmp_path)[source]¶

Re-syncing an adopted config must not restyle what the project wrote.

The sibling test_ongoing_sync_template_survives_pyproject_fmt merges each template into an empty file, so it only ever sees the template’s own text. The ping-pong this guards needs a local addition: iterating a tomlrt array yields decoded values rather than the nodes carrying their source lexeme, so rebuilding the array re-emits every item in tomlrt’s style. A local 'base32 "[0-9a-z]{52}"', which pyproject-fmt writes as a literal string precisely because it contains a quote, came back as an escaped "base32 \"[0-9a-z]{52}\"". Same content, so format-pyproject rewrote it straight back, and the two unattended jobs opened pull requests undoing each other indefinitely.

Offline by construction: it asserts the invariant that makes the file a fixed point rather than shelling out to pyproject-fmt to observe one.

Return type:

None

tests.test_init_project.test_replaces_old_changelog_pattern(tmp_path)[source]¶

Verify old changelog pattern without backticks is replaced by template.

Return type:

None

tests.test_init_project.test_bumpversion_update_idempotent(tmp_path)[source]¶

Verify running update twice produces the same result.

Return type:

None

tests.test_init_project.test_bumpversion_update_valid_toml(tmp_path)[source]¶

Verify updated pyproject.toml is still valid TOML.

Return type:

None

tests.test_init_project.test_preserves_local_array_entries(tmp_path)[source]¶

Verify local [[tool.bumpversion.files]] entries survive ongoing sync.

Return type:

None

tests.test_init_project.test_ongoing_sync_idempotent_with_local_entries(tmp_path)[source]¶

Verify ongoing sync with local entries is idempotent after first run.

Return type:

None

tests.test_init_project.test_ongoing_sync_no_duplicate_template_entries(tmp_path)[source]¶

Verify template entries are not duplicated when also present locally.

Return type:

None

tests.test_init_project.test_evolved_canonical_entry_superseded_not_duplicated(tmp_path)[source]¶

Verify a stale copy of a canonical entry is superseded, not duplicated.

The [project] version entry gained a regex anchor (so it cannot bleed into [tool.nuitka]’s file-/product-version keys). A downstream repo still carrying the old unanchored form must converge on the single canonical anchored entry rather than ending up with both, which would break the bump.

Return type:

None

tests.test_init_project.test_local_entries_preserved_via_update(tmp_path)[source]¶

Verify local array entries survive ongoing sync via dict comparison.

Return type:

None

tests.test_init_project.test_local_entry_comments_preserved(tmp_path)[source]¶

Verify comments between local [[files]] entries survive ongoing sync.

Regression test for the sync-bumpversion job stripping comments from downstream repos (e.g., click-extra#1595).

Note

tomlkit stores comments preceding the first local entry in the parent table body, not in the AoT. These are lost during section replacement. Comments between local entries survive because tomlkit attaches them to the preceding entry’s trivia.

Return type:

None

tests.test_init_project.test_syncs_typos_identifiers_into_existing_section(tmp_path)[source]¶

Canonical identifiers merge into a pre-existing typos section.

Regression test for the BOOTSTRAP footgun: a downstream repo carrying its own [tool.typos] never received the bundled proper-noun identifiers, because a BOOTSTRAP insert skipped the existing section and typos reads [tool.typos] natively (no bundled fallback at runtime).

Return type:

None

tests.test_init_project.test_typos_preserves_local_inline_table_keys(tmp_path)[source]¶

Local keys inside a shared inline table survive alongside canonical ones.

Return type:

None

tests.test_init_project.test_typos_appends_local_excludes_after_template_ones(tmp_path)[source]¶

A local files.extend-exclude entry survives after the canonical ones.

Return type:

None

tests.test_init_project.test_typos_canonical_value_wins_on_conflict(tmp_path)[source]¶

Canonical capitalization overrides a wrong local target on shared keys.

Return type:

None

tests.test_init_project.test_typos_update_idempotent(tmp_path)[source]¶

The typos sync is a no-op on the second run.

Return type:

None

tests.test_init_project.LOCAL_ARRAY_ITEMS_WITH_COMMENTS = '[tool.typos]\ndefault.extend-ignore-re = [\n  # Quoted from a storm forecast.\n  "hail-warning",\n  "rainbow",  # Seen after the rain.\n]\n'¶

A local array lacking the canonical item, so the sync rebuilds it.

tests.test_init_project.test_typos_carries_comments_on_local_array_items(tmp_path)[source]¶

A local extend-ignore-re item keeps its comments through a rebuild.

A local array lacking a canonical item is rebuilt from the template, and each local item is appended to it as a decoded value: the comment block above the item and the comment after it have to be copied across.

Return type:

None

tests.test_init_project.test_typos_rebuilt_array_uses_pyproject_fmt_indent(tmp_path)[source]¶

A rebuilt multi-line array is indented the way pyproject-fmt writes it.

The rebuild starts from the template’s single-line array. tomlrt breaks it into rows once a carried comment needs a line of its own, but at four spaces, which the format-pyproject job re-indents to two in a pull request of its own.

Return type:

None

tests.test_init_project.LOCAL_KEYS_WITH_COMMENTS = '[tool.typos]\n# Spelled the way the Lisbon office writes.\ndefault.locale = "en-gb"  # Not the default "en".\n# Kept out of the forecast pages.\ncheck-filename = false  # Station codes read as typos.\n'¶

Local-only keys whose comments live in the layout of their parent table.

tests.test_init_project.test_typos_carries_comments_on_local_keys(tmp_path)[source]¶

A local key keeps the comment above it and the comment after it.

Both belong to the parent table’s layout rather than to the value, so a key grafted onto the rebuilt section arrives without them unless they are copied across. That holds for a key inside a table the template also defines (default) and for one on the section itself.

Return type:

None

tests.test_init_project.test_typos_sync_ignores_the_spaces_before_a_comment(tmp_path)[source]¶

A synced section stays a fixpoint once pyproject-fmt respaces it.

A carried end-of-line comment is written one space after its value, and pyproject-fmt moves it to two. The next sync must not read that as a change, or sync-repomatic and format-pyproject undo each other forever.

Return type:

None

tests.test_init_project.test_typos_merged_inline_tables_use_pyproject_fmt_spacing(tmp_path)[source]¶

Merged inline tables render { ... }, matching pyproject-fmt.

A compact {...} would be re-spaced by the format-pyproject autofix job, producing churn on every run.

Return type:

None

tests.test_init_project.test_typos_skips_svg_content(tmp_path, monkeypatch)[source]¶

A word split across two <text> runs survives a real fix-typos pass.

A terminal capture renders each run of same-styled characters as one <text> element, so a word cut at a run boundary leaves a fragment the checker reads as a misspelling. Rewriting it in place doubles a letter and corrupts the picture, which type.svg.check-file prevents.

The plain-text control proves the dictionary still carries the fragment, so the SVG half cannot pass for the wrong reason once typos drops the entry.

tests.test_init_project.test_init_default_excludes(tmp_path, monkeypatch)[source]¶

Verify default exclude skips agents, labels, and skills.

tests.test_init_project.test_init_respects_exclude_components(tmp_path, monkeypatch)[source]¶

Verify exclude config skips listed components.

tests.test_init_project.test_init_respects_exclude_workflow_files(tmp_path, monkeypatch)[source]¶

Verify exclude config with workflow file entries skips those workflows.

tests.test_init_project.test_init_respects_exclude_skill_files(tmp_path, monkeypatch)[source]¶

Verify exclude config with skill file entries skips those skills.

tests.test_init_project.test_init_changelog_excluded_for_awesome_repo(tmp_path, monkeypatch)[source]¶

Verify changelog.md is not created for awesome-* repos.

tests.test_init_project.test_init_changelog_excluded_existing_for_awesome_repo(tmp_path, monkeypatch)[source]¶

Verify existing changelog.md is flagged as excluded for awesome-* repos.

tests.test_init_project.test_init_scoped_component_excluded_existing_for_awesome_repo(tmp_path, monkeypatch)[source]¶

An existing out-of-scope component file is flagged as excluded, not created.

tests.test_init_project.test_include_config_includes_all_skill_files(tmp_path, monkeypatch, repo_slug)[source]¶

Config include = ["skills"] produces all skills regardless of repo type.

tests.test_init_project.test_explicit_component_bypasses_scope_exclusion(tmp_path, monkeypatch)[source]¶

Explicit component request overrides scope exclusion.

repomatic init publish-pypi-action in an awesome repo should produce .github/actions/publish-pypi/action.yaml even though the component has scope=PACKAGE_ONLY and awesome repos build no distributable. Scope exclusions only apply during bare repomatic init (no explicit components).

tests.test_init_project.test_include_config_bypasses_scope_exclusion(tmp_path, monkeypatch)[source]¶

Config include bypasses scope exclusions, like CLI explicit naming.

publish-pypi-action is a BundledComponent with files, scoped PACKAGE_ONLY and init_default=INCLUDE. In an awesome repo, which builds no distributable, it would normally be scope-excluded. With include, the scope bypass falls through to file-level checks (the file has scope=ALL, so nothing is excluded).

tests.test_init_project.test_include_bypasses_scope_for_tool_config(tmp_path, monkeypatch)[source]¶

Include bypasses scope for a ToolConfigComponent.

Lychee is AWESOME_ONLY. In a non-awesome repo it would normally be scope-excluded. With include, the scope bypass falls through to config-key and file-level checks, then the tool config is merged into pyproject.toml.

tests.test_init_project.test_bare_init_applies_scope_exclusion(tmp_path, monkeypatch)[source]¶

Bare init without include or CLI components applies scope exclusions.

tests.test_init_project.test_virtual_project_init_drops_exactly_the_release_lane(tmp_path, monkeypatch)[source]¶

A uv virtual project loses the release lane and keeps everything else.

Conformance test for the PYTHON_ONLY/PACKAGE_ONLY split. Rather than naming the affected files (which would go stale the moment an entry moves between the two scopes), it inits the same repository twice, once as a distributable package and once as a virtual project, and asserts the difference is exactly the registry’s PACKAGE_ONLY population.

That pins both directions at once: nothing from the release lane survives into a repo that can never publish, and no PYTHON_ONLY entry gets dragged out with it. A virtual project still locks dependencies and runs tests, so over-narrowing is as much a bug as under-narrowing.

tests.test_init_project.test_file_level_include_bypasses_scope(tmp_path, monkeypatch)[source]¶

File-level include entry bypasses scope for that specific file only.

tests.test_init_project.test_file_level_include_implies_parent_component(tmp_path, monkeypatch)[source]¶

File-level include implicitly selects the parent component.

include = ["skills/awesome-triage"] should select the skills component (which is default-excluded) and bypass scope for that specific file, without bypassing scope for other AWESOME_ONLY files.

tests.test_init_project.test_exclude_overrides_include_scope_bypass(tmp_path, monkeypatch)[source]¶

Explicit exclude entries take precedence over include scope bypass.

tests.test_init_project.test_publish_pypi_action_excluded_in_non_python_repo(tmp_path, monkeypatch)[source]¶

publish-pypi-action is skipped when the repo is not a Python project.

Reproduces the scenario where a non-Python repository (e.g., a dotfiles project) carries a pyproject.toml solely for [tool.*] configuration. The composite action targets PyPI publishing, so it has no purpose there.

tests.test_init_project.test_publish_pypi_action_included_in_python_repo(tmp_path, monkeypatch)[source]¶

publish-pypi-action is installed when the repo is a Python project.

tests.test_init_project.test_explicit_component_bypasses_python_only_scope(tmp_path, monkeypatch)[source]¶

Explicit CLI naming bypasses RepoScope.PYTHON_ONLY.

When the caller explicitly asks for publish-pypi-action, the component is materialized regardless of repo type, matching the existing scope-bypass semantics for awesome-only components.

tests.test_init_project.test_include_config_bypasses_python_only_scope(tmp_path, monkeypatch)[source]¶

[tool.repomatic] include bypasses RepoScope.PYTHON_ONLY.

tests.test_init_project.test_init_respects_exclude_label_files(tmp_path, monkeypatch)[source]¶

Verify exclude config with label file entries skips those label files.

tests.test_init_project.test_init_mixed_exclude(tmp_path, monkeypatch)[source]¶

Verify exclude with both component and file entries.

tests.test_init_project.test_init_detects_excluded_component_files(tmp_path, monkeypatch)[source]¶

Verify init reports excluded component files that still exist on disk.

tests.test_init_project.test_init_detects_excluded_skill_file(tmp_path, monkeypatch)[source]¶

Verify init reports a file-level excluded skill that still exists on disk.

tests.test_init_project.test_init_detects_auto_excluded_awesome_triage(tmp_path, monkeypatch)[source]¶

Verify awesome-triage is detected as excluded for non-awesome repos.

When include does not cover skills, scope exclusions still apply and stale awesome-only files are flagged in excluded_existing.

tests.test_init_project.test_resolve_target(component, field, default_dir, leaf, location, expected_dir)[source]¶

A declared target is rebased onto the configured location.

removeprefix (not strip) normalizes the ./ the config default carries, so a location that is itself a hidden directory keeps its leading dot.

tests.test_init_project.test_resolve_target_leaves_foreign_paths_alone(component)[source]¶

A target outside the component’s default location passes through.

tests.test_init_project.test_resolve_target_without_config_is_noop(component)[source]¶

With no config there is nothing to rebase onto.

tests.test_init_project.test_resolve_target_ignores_components_with_fixed_location()[source]¶

A component with no configurable location never rebases.

.github/workflows/ is GitHub’s path, not one the user may move, so workflows declares no location_field and its targets are literal.

tests.test_init_project.test_init_skills_custom_location(tmp_path, monkeypatch)[source]¶

Verify skills are written to a custom location when configured.

tests.test_init_project.test_init_agents_custom_location(tmp_path, monkeypatch)[source]¶

Verify agents are written to a custom location when configured.

tests.test_init_project.test_init_detects_excluded_agent_custom_location(tmp_path, monkeypatch)[source]¶

Verify excluded agent detection works at custom locations.

tests.test_init_project.test_init_detects_excluded_skill_custom_location(tmp_path, monkeypatch)[source]¶

Verify excluded skill detection works at custom locations.

tests.test_init_project.test_init_detects_disabled_opt_in_workflow(tmp_path, monkeypatch)[source]¶

Verify disabled opt-in workflows on disk are detected as excluded.

tests.test_init_project.test_init_cli_delete_excluded(tmp_path, monkeypatch)[source]¶

Verify –delete-excluded deletes excluded files and cleans empty dirs.

tests.test_init_project.test_init_cli_no_delete_excluded_warns(tmp_path, monkeypatch)[source]¶

Verify excluded files are reported but not deleted without –delete-excluded.

tests.test_init_project.test_removed_assets_sorted()[source]¶

REMOVED_ASSETS is ordered by (component, target).

Return type:

None

tests.test_init_project.test_removed_assets_no_collision_with_live_registry()[source]¶

No tombstone may point at a path a live component still ships.

Guards against re-adding an asset without dropping its tombstone, which would make init prune a file it just wrote.

Return type:

None

tests.test_init_project.test_removed_asset_metadata_valid(asset)[source]¶

Each tombstone has a valid gate, a bare version, and a known component.

Return type:

None

tests.test_init_project.test_detect_removed_assets_classifies_by_content(tmp_path, monkeypatch)[source]¶

_detect_removed_assets sorts orphans into prunable vs review by hash.

Return type:

None

tests.test_init_project.test_detect_removed_assets_prunes_an_empty_owned_folder(tmp_path, monkeypatch)[source]¶

A skill folder outliving its SKILL.md is an orphan nothing else sees.

Return type:

None

tests.test_init_project.test_init_cli_prunes_unmodified_removed_asset(tmp_path, monkeypatch)[source]¶

Bare init deletes an unmodified orphan and its emptied parent dir.

Return type:

None

tests.test_init_project.test_init_cli_keeps_modified_removed_asset(tmp_path, monkeypatch)[source]¶

A locally modified orphan is reported for review, never auto-deleted.

Return type:

None

tests.test_init_project.test_init_cli_keep_removed_preserves_unmodified_orphan(tmp_path, monkeypatch)[source]¶

--keep-removed reports an unmodified orphan but does not delete it.

Return type:

None

tests.test_init_project.test_detect_removed_assets_matches_any_shipped_hash(tmp_path, monkeypatch)[source]¶

A copy matching any released revision (not just the last) is prunable.

Return type:

None

tests.test_init_project.test_init_cli_delete_removed_modified_forces_deletion(tmp_path, monkeypatch)[source]¶

--delete-removed-modified deletes a locally modified orphan.

Return type:

None

tests.test_init_project.test_init_cli_keep_removed_and_force_are_exclusive(tmp_path, monkeypatch)[source]¶

--keep-removed and --delete-removed-modified cannot combine.

Return type:

None

tests.test_init_project.test_detect_prunes_thin_caller_any_slug(tmp_path, slug)[source]¶

A pure thin-caller for a removed workflow is prunable, whatever the era slug.

Return type:

None

tests.test_init_project.test_detect_reviews_customized_thin_caller(tmp_path)[source]¶

A thin-caller the user extended with extra jobs is reported, never pruned.

Return type:

None

tests.test_init_project.test_detect_skips_unrelated_workflow(tmp_path)[source]¶

A user’s own workflow that merely shares the name is left untouched.

Return type:

None

tests.test_init_project.test_init_cli_prunes_orphaned_workflow(tmp_path, monkeypatch)[source]¶

Bare init deletes a pure thin-caller for a removed reusable workflow.

Return type:

None

tests.test_init_project.test_removed_reusable_workflows_are_tombstoned()[source]¶

A reusable workflow dropped since the last release must be tombstoned.

A workflow with a workflow_call trigger generates downstream thin-callers; removing it without a RemovedAsset leaves orphaned thin-callers. Skips when git history or release tags are unavailable.

Return type:

None

tests.test_init_project.test_removed_data_assets_are_tombstoned()[source]¶

A skill/agent data file dropped since the last release must be tombstoned.

Compares the bundled skill/agent data files at the most recent release tag against the current tree. A file present then but gone now is a removed asset that must have a matching RemovedAsset entry, or init would leave orphans in downstream repos. Skips when git history or release tags are unavailable (shallow clone, no tags).

Return type:

None

tests.test_init_project.test_shrinking_component_file_lists_are_tombstoned()[source]¶

A target a component stopped shipping must carry a tombstone.

The sibling tests above compare sources under repomatic/data/, which only catches a bundled file being deleted. A component can orphan a downstream file without deleting anything: dropping a FileEntry from its files tuple stops shipping that target while the source lives on (or goes, independently). labels did exactly that, shedding the two .github/labeller-*.yaml entries while labels.toml stayed.

Nothing catches that on its own, because _detect_removed_assets walks REMOVED_ASSETS alone: it never diffs a component’s current tuple against what earlier versions wrote, so a shrunk tuple leaves a downstream orphan no later init will ever look at.

Comparing target sets across the previous release closes it. Skips when git history or release tags are unavailable (shallow clone, no tags).

Note

One-sided by construction: the previous registry is read as text, so the ā€œshipped thenā€ set covers only FileEntry literals carrying an explicit target, while ā€œshipped nowā€ is the fully built registry. A component whose entries are generated by a helper is therefore invisible on the old side. That direction is the safe one — it can only miss a drop, never invent one — but it does mean a green run here is not proof that no component shrank, only that no literal-target one did.

Return type:

None

tests.test_init_project.test_init_explicit_components_bypass_exclude(tmp_path, monkeypatch)[source]¶

Verify explicit CLI components override exclusion.

tests.test_init_project.test_init_exclude_unknown_component_raises(tmp_path, monkeypatch)[source]¶

Verify unknown component name in exclude raises ValueError.

tests.test_init_project.test_init_exclude_unknown_file_raises(tmp_path, monkeypatch)[source]¶

Verify unknown file identifier in exclude raises ValueError.

tests.test_init_project.test_init_include_unknown_component_raises(tmp_path, monkeypatch)[source]¶

Verify unknown component name in include raises ValueError.

tests.test_init_project.test_init_include_overrides_default_exclusions(tmp_path, monkeypatch)[source]¶

Verify include overrides default exclusions additively.

tests.test_init_project.test_init_include_cannot_materialize_ephemeral(tmp_path, monkeypatch, caplog)[source]¶

Verify include cannot opt a bare init into an ephemeral component.

tests.test_init_project.test_init_explicit_selection_materializes_ephemeral(tmp_path)[source]¶

Verify naming an ephemeral component explicitly still writes its files.

tests.test_init_project.test_init_exclude_additive_to_defaults(tmp_path, monkeypatch)[source]¶

Verify user exclude is additive to default exclusions.

tests.test_init_project.test_all_data_files_registered_in_exportable_files()[source]¶

Every non-infrastructure file in data/ must appear in EXPORTABLE_FILES.

Return type:

None

tests.test_init_project.test_every_data_file_maps_to_a_component()[source]¶

Every file in EXPORTABLE_FILES belongs to a registry component, tool runner default, or registered runtime fragment.

Return type:

None

tests.test_init_project.bundled_configurations()[source]¶

File names of every tool configuration the package ships.

The templates repomatic init merges into a repository, and the defaults the tool runner reads when a repository declares no configuration of its own. Sorted, because pytest-xdist aborts a run whose workers disagree on the order of the test IDs.

Return type:

list[str]

tests.test_init_project.test_bundled_configuration_reads_true_downstream(filename)[source]¶

A bundled tool configuration cites nothing a downstream repository lacks.

Return type:

None

tests.test_init_project.test_no_data_file_claimed_by_multiple_components()[source]¶

Each data filename must belong to at most one component.

Return type:

None

tests.test_init_project.test_valid_file_ids_cover_all_multi_file_components()[source]¶

Components with multiple files must report valid file identifiers.

Return type:

None

tests.test_init_project.test_workflow_file_ids_match_all_workflow_files()[source]¶

valid_file_ids(ā€˜workflows’) must match ALL_WORKFLOW_FILES.

Return type:

None

tests.test_init_project.test_component_file_ids_match_registry()[source]¶

valid_file_ids must return identifiers matching registry entries.

Return type:

None

tests.test_init_project.test_tool_components_have_no_file_ids()[source]¶

Tool components (ruff, pytest, etc.) do not support file-level exclusion.

Return type:

None

tests.test_init_project.test_workflow_files_target_workflow_dir()[source]¶

All workflow file entries must target .github/workflows/.

Return type:

None

tests.test_init_project.test_workflow_sources_are_yaml()[source]¶

All workflow source files must be .yaml.

Return type:

None

tests.test_init_project.test_skill_files_target_skill_dir()[source]¶

All skill entries must target the .claude/skills/{id} folder itself.

Return type:

None

tests.test_init_project.test_skill_sources_follow_naming_convention()[source]¶

Skill sources must be the bundled skills/{id} folder.

Return type:

None

tests.test_init_project.test_skill_file_id_matches_target_dir()[source]¶

Skill file_id must match the directory name in the target path.

Return type:

None

tests.test_init_project.test_no_target_prefix_mixing_within_component()[source]¶

Files within a component must not mix unrelated target directories.

Return type:

None

tests.test_init_project.test_tool_config_rejects_missing_source_file()[source]¶

ToolConfigComponent raises ValueError without source_file.

Return type:

None

tests.test_init_project.test_tool_config_rejects_missing_tool_section()[source]¶

ToolConfigComponent raises ValueError without tool_section.

Return type:

None

tests.test_init_project.test_tool_config_rejects_files()[source]¶

ToolConfigComponent raises ValueError with file entries.

Return type:

None

tests.test_init_project.test_file_ids_unique_within_component()[source]¶

File IDs must be unique within each component.

Return type:

None

tests.test_init_project.test_component_names_unique()[source]¶

Component names must be unique across the registry.

Return type:

None

tests.test_init_project.test_config_key_has_config_default()[source]¶

Entries with config_key must have an intentional config_default.

File entries default to False (opt-in). Component entries default to True (opt-out). This test verifies the pairing exists — not the specific default value.

Return type:

None

tests.test_init_project.test_every_config_key_gate_actually_reaches_its_field()[source]¶

Every declared config_key must move its entry when the user flips it.

A gate naming a key Config cannot resolve does not fail: it silently falls through to config_default, which reads as a feature switched off rather than as a gate wired wrong. metrics.sync shipped that way, because the resolver flattened every dotted key to one attribute name and could not reach a key living on a nested schema.

Written against the whole registry rather than against that one entry: the fault is a property of how a gate is spelled, so any future entry naming a nested key would repeat it.

Return type:

None

tests.test_init_project.test_tools_with_bundled_defaults_not_init_components()[source]¶

Tools with a bundled default_config must not also be init components.

The tool runner already falls back to the bundled config at runtime when no native config exists (Level 3 in resolve_config). Copying the same file into downstream repos via init would be redundant pollution.

Return type:

None

tests.test_init_project.test_init_reports_unmodified_configs(tmp_path, monkeypatch)[source]¶

Init reports native config files that match bundled defaults.

tests.test_init_project.test_init_no_unmodified_when_different(tmp_path, monkeypatch)[source]¶

Init does not flag modified config files as unmodified.

tests.test_init_project.test_parse_component_entries_accepts_valid_qualified_entries(entry)[source]¶

Qualified component/file entries are accepted by parse_component_entries.

Return type:

None

tests.test_init_project.test_parse_component_entries_accepts_all_bare_components(component)[source]¶

Every component name is accepted as a bare exclude entry.

Return type:

None

tests.test_init_project.test_parse_component_entries_bare_filename_without_component_fails()[source]¶

Bare filenames like ā€˜debug.yaml’ must fail — qualified form required.

Return type:

None

tests.test_init_project.test_parse_component_entries_bare_skill_name_without_component_fails()[source]¶

Bare skill identifiers like ā€˜repomatic-audit’ must fail.

Return type:

None

tests.test_init_project.test_parse_component_entries_rejects_invalid_entries(entry, match)[source]¶

Invalid entries produce hard ValueError failures.

Return type:

None

tests.test_init_project.test_init_single_skill(tmp_path)[source]¶

Qualified entry creates only the specified skill.

tests.test_init_project.test_init_multiple_qualified_same_component(tmp_path)[source]¶

Multiple qualified entries for same component creates all specified.

tests.test_init_project.test_init_bare_overrides_qualified(tmp_path)[source]¶

Bare component name includes all files, ignoring qualified filter.

tests.test_init_project.test_init_mixed_bare_and_qualified(tmp_path)[source]¶

Bare + qualified for different components work independently.

tests.test_init_project.test_init_qualified_workflow(tmp_path)[source]¶

Single workflow file selection.

tests.test_init_project.test_init_qualified_scope_bypassed_when_explicit(tmp_path)[source]¶

Explicit selection bypasses scope: awesome-triage created in non-awesome repo.

tests.test_init_project.test_init_qualified_selection_context_in_error()[source]¶

Qualified selection uses ā€˜selection’ context in error messages.

tests.test_init_project.test_init_awesome_template_not_auto_included_with_explicit_components(tmp_path)[source]¶

awesome-template is not auto-included when explicit components are given.

tests.test_init_project.test_update_tool_config_output_contract(comp)[source]¶

Syncing any tool-config component yields well-formed, well-spaced TOML.

Three invariants the production path has no other guard for, checked on one sync per component:

  • Non-empty output. tomlrt 1.7.3, 1.7.4, and the main-branch SHA briefly pinned in pyproject.toml each shipped fixes for a shape where tomlrt.dumps returned an empty string with no exception.

  • Section spacing. The previous tomlkit implementation enforced this with a four-step regex normalization on the dumped string; the tomlrt rewrite dropped that stack and trusts the library, so a regression reintroducing ]\n[ collisions or triple-blank gaps must fail here.

  • A terminal newline. When the template’s prefix-strip drops the source’s trailing newline, the last KV slot loses its EOL trivia and every regenerated PR diff grows a \ No newline at end of file.

Return type:

None

tests.test_init_project.test_tomlrt_aot_only_section_overwrite_invariant()[source]¶

A populate-then-overwrite of an AoT-only section preserves the document.

Mirrors the _graft_local_additions + cross-doc assign path from _update_tool_config: parse a pyproject with a [project] preamble followed by an AoT-only target section, build a fresh detached section with Table.section() from an AoT-only template, populate it with the existing AoT entries, then assign the new section back into the document.

Earlier tomlrt releases silently dropped the entire document body in this configuration: tomlrt.dumps(doc) returned an empty string with no exception. The 1.7.5 changelog entry ā€œOverwriting a key with a body-less section (one whose only child is an array-of-tables) no longer wipes the whole document on dumpā€ fixed it; this test pins the contract so a future tomlrt bump that regresses it fails CI instead of silently corrupting downstream pyproject.toml files. No bundled ToolConfigComponent ships an AoT-only template today, so a regression would not otherwise surface through the sync-* autofix jobs until a user defined one.

Return type:

None