feat: add bootstrap domain (models, setup modules, planning)
- Profile, BootstrapAction, BootstrapPlan models - Setup modules: hostname, locale, shell, ssh-keygen, runcmd - Bootstrap planning with ordered phases and env validation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
73
tests/test_domain_bootstrap_modules.py
Normal file
73
tests/test_domain_bootstrap_modules.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Tests for bootstrap setup modules."""
|
||||
|
||||
from flow.domain.bootstrap.modules import (
|
||||
HostnameModule,
|
||||
LocaleModule,
|
||||
RuncmdModule,
|
||||
ShellModule,
|
||||
SSHKeygenModule,
|
||||
)
|
||||
|
||||
|
||||
class TestHostnameModule:
|
||||
def test_describe(self):
|
||||
m = HostnameModule(hostname="my-host")
|
||||
assert isinstance(m.describe(), str)
|
||||
assert "my-host" in m.describe()
|
||||
|
||||
def test_plan(self):
|
||||
m = HostnameModule(hostname="my-host")
|
||||
cmds = m.plan()
|
||||
assert len(cmds) > 0
|
||||
assert any("my-host" in c for c in cmds)
|
||||
|
||||
|
||||
class TestLocaleModule:
|
||||
def test_describe(self):
|
||||
m = LocaleModule(locale="en_US.UTF-8")
|
||||
assert isinstance(m.describe(), str)
|
||||
assert "en_US.UTF-8" in m.describe()
|
||||
|
||||
def test_plan(self):
|
||||
m = LocaleModule(locale="en_US.UTF-8")
|
||||
cmds = m.plan()
|
||||
assert len(cmds) > 0
|
||||
|
||||
|
||||
class TestShellModule:
|
||||
def test_describe(self):
|
||||
m = ShellModule(shell="zsh")
|
||||
assert isinstance(m.describe(), str)
|
||||
assert "zsh" in m.describe()
|
||||
|
||||
def test_plan(self):
|
||||
m = ShellModule(shell="zsh")
|
||||
cmds = m.plan()
|
||||
assert len(cmds) > 0
|
||||
|
||||
|
||||
class TestSSHKeygenModule:
|
||||
def test_describe(self):
|
||||
m = SSHKeygenModule(keys=[{"path": "~/.ssh/id_ed25519"}])
|
||||
assert isinstance(m.describe(), str)
|
||||
assert "1" in m.describe()
|
||||
|
||||
def test_plan(self):
|
||||
m = SSHKeygenModule(keys=[
|
||||
{"path": "~/.ssh/id_ed25519", "type": "ed25519", "comment": "me@host"},
|
||||
])
|
||||
cmds = m.plan()
|
||||
assert len(cmds) == 1
|
||||
assert "ssh-keygen" in cmds[0]
|
||||
|
||||
|
||||
class TestRuncmdModule:
|
||||
def test_describe(self):
|
||||
m = RuncmdModule(commands=["echo a", "echo b", "echo c"])
|
||||
assert isinstance(m.describe(), str)
|
||||
assert "3" in m.describe()
|
||||
|
||||
def test_plan(self):
|
||||
m = RuncmdModule(commands=["echo hello"])
|
||||
cmds = m.plan()
|
||||
assert cmds == ["echo hello"]
|
||||
85
tests/test_domain_bootstrap_planning.py
Normal file
85
tests/test_domain_bootstrap_planning.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Tests for bootstrap planning."""
|
||||
|
||||
import pytest
|
||||
|
||||
from flow.core.errors import ConfigError
|
||||
from flow.domain.bootstrap.models import BootstrapAction, Profile
|
||||
from flow.domain.bootstrap.planning import parse_profile, plan_bootstrap
|
||||
|
||||
|
||||
class TestParseProfile:
|
||||
def test_basic(self):
|
||||
raw = {
|
||||
"os": "linux",
|
||||
"hostname": "dev-box",
|
||||
"shell": "zsh",
|
||||
"packages": ["fd", "ripgrep"],
|
||||
}
|
||||
profile = parse_profile("work", raw)
|
||||
assert profile.name == "work"
|
||||
assert profile.os == "linux"
|
||||
assert profile.hostname == "dev-box"
|
||||
assert profile.shell == "zsh"
|
||||
assert len(profile.packages) == 2
|
||||
|
||||
def test_defaults(self):
|
||||
profile = parse_profile("minimal", {})
|
||||
assert profile.os == "linux"
|
||||
assert profile.hostname is None
|
||||
assert profile.packages == []
|
||||
|
||||
def test_ssh_keys(self):
|
||||
raw = {"ssh-keys": [{"path": "~/.ssh/id_ed25519", "type": "ed25519"}]}
|
||||
profile = parse_profile("test", raw)
|
||||
assert len(profile.ssh_keys) == 1
|
||||
|
||||
|
||||
class TestPlanBootstrap:
|
||||
def test_basic_plan(self):
|
||||
profile = Profile(
|
||||
name="test", os="linux", arch=None,
|
||||
hostname="my-host", locale="en_US.UTF-8",
|
||||
shell="zsh", ssh_keys=[], runcmd=[],
|
||||
packages=["fd"], env_required=[],
|
||||
)
|
||||
manifest = {"packages": [{"name": "fd", "type": "pkg"}]}
|
||||
plan = plan_bootstrap(profile, manifest)
|
||||
assert plan.profile == "test"
|
||||
assert plan.total_steps > 0
|
||||
phases = [a.phase for a in plan.actions]
|
||||
assert "setup" in phases
|
||||
assert "packages" in phases
|
||||
assert "dotfiles" in phases
|
||||
|
||||
def test_missing_env_raises(self, monkeypatch):
|
||||
monkeypatch.delenv("REQUIRED_VAR", raising=False)
|
||||
profile = Profile(
|
||||
name="test", os="linux", arch=None,
|
||||
hostname=None, locale=None, shell=None,
|
||||
ssh_keys=[], runcmd=[], packages=[],
|
||||
env_required=["REQUIRED_VAR"],
|
||||
)
|
||||
with pytest.raises(ConfigError, match="REQUIRED_VAR"):
|
||||
plan_bootstrap(profile, {})
|
||||
|
||||
def test_runcmd_produces_action(self):
|
||||
profile = Profile(
|
||||
name="test", os="linux", arch=None,
|
||||
hostname=None, locale=None, shell=None,
|
||||
ssh_keys=[], runcmd=["echo hello", "echo world"],
|
||||
packages=[], env_required=[],
|
||||
)
|
||||
plan = plan_bootstrap(profile, {})
|
||||
runcmd_actions = [a for a in plan.actions if "custom command" in a.description.lower()]
|
||||
assert len(runcmd_actions) == 1
|
||||
|
||||
def test_ssh_keys_action(self):
|
||||
profile = Profile(
|
||||
name="test", os="linux", arch=None,
|
||||
hostname=None, locale=None, shell=None,
|
||||
ssh_keys=[{"path": "~/.ssh/id", "type": "ed25519"}],
|
||||
runcmd=[], packages=[], env_required=[],
|
||||
)
|
||||
plan = plan_bootstrap(profile, {})
|
||||
ssh_actions = [a for a in plan.actions if "SSH" in a.description]
|
||||
assert len(ssh_actions) == 1
|
||||
Reference in New Issue
Block a user