Skip to content

Pin down the symlink + restart-required setup path in __init__.py #1262

@frenckatron

Description

@frenckatron

Why This Matters

link_sub_integrations (util.py:287-305) writes filesystem symlinks into <config_dir>/custom_components/ based on what's bundled inside Spook, and the consequence in __init__.py:42-79 is one of three outcomes: restart HA immediately, listen for EVENT_HOMEASSISTANT_START/STARTED to restart later, or raise a repair issue. That is the single most operationally dangerous code path in the integration — it both mutates the user's HA config directory and forces a process restart. It is also the path most likely to break across HA versions and across host filesystems (containers, symlink-hostile FS, read-only configs). Testing it in PHACC's tmp_path config dir is straightforward and removes a class of "works on my machine" bugs.

Approach

Add tests/test_setup.py. Use PHACC's hass.config.config_dir (already a tmp dir under the hood). Pre-populate it with custom_components/spook/integrations/<name>/manifest.json fixtures (or symlink to the real repo paths). Run async_setup_entry with MockConfigEntry and assert: (a) symlinks are created on first run, (b) a second run is a no-op (link_sub_integrations returns False), (c) unlink_sub_integrations removes them on async_remove_entry, (d) parametrize over CoreState.not_running, CoreState.starting, CoreState.running and the "Boo!" sentinel, asserting the right branch fires. Patch hass.async_stop to a recording mock so the test can assert RESTART_EXIT_CODE was requested without actually shutting HA down.

Acceptance Criteria

  • Symlink creation, idempotency, and removal are all asserted
  • All four CoreState × sentinel branches in __init__.py:53-79 are covered
  • The restart_required repair issue is asserted when no auto-restart fires
  • unsubscribe_ghost_busters swallows a ValueError if called twice (regression test for the explicit try/except at __init__.py:103-109)

Risks & Caveats

The dest.symlink_to(src) call (util.py:303) targets an absolute path that may not exist inside the test's tmp config dir — you'll need to either create the source tree under tmp or stub the source path. Symlinks on Windows runners are nontrivial; if CI ever expands beyond ubuntu-latest, this test will need pytest.mark.skipif(sys.platform == "win32"). The restart-listener path is the trickiest to assert; consider using async_fire_time_changed + bus event simulation.

Scores

  • Impact: ████████░░ 8/10
  • Difficulty: ████████░░ 8/10
  • Short-Term ROI: ████░░░░░░ 4/10
  • Long-Term Value: █████████░ 9/10

Priority

Research Further

Dependencies

#1257, #1260

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions