chore(test,docs): add make test, CI, e2e container, refresh example

- Makefile gains `test` and `test-e2e` targets; `deps` now installs the
  dev extras so pytest is available after `make deps`.
- .github/workflows/test.yml runs unit tests on push and PR to main
  (Python 3.13 on ubuntu-latest, ignores tests/e2e by default).
- tests/e2e/Containerfile + test_dotfiles_e2e.py scaffold a real
  container-based smoke test of `flow dotfiles init` + `link` against
  the example dotfiles repo. Gated by FLOW_RUN_E2E=1 so unit runs
  stay fast; verified locally with podman.
- tests/fakes.FakeRunner uses ordered subsequence matching instead of
  unordered containment -- prevents accidental match between unrelated
  commands that happen to share tokens.
- example/README.md rewritten for the current command surface
  (no more `dotfiles undo`, `dotfiles modules ...`, `--relink`,
  `bootstrap list/show/run --profile`, `bootstrap packages --resolved`).
  Adds an "External modules" section documenting `_module.yaml`.
- example/dotfiles-repo profiles.yaml drops `allow-sudo: true` along
  with the runtime support.
- pyproject.toml adds [tool.coverage] config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 00:02:19 +03:00
parent a71742afee
commit 6a0eb9f6ef
9 changed files with 226 additions and 25 deletions

37
tests/e2e/Containerfile Normal file
View File

@@ -0,0 +1,37 @@
# Containerfile for end-to-end testing of the flow CLI.
#
# Builds a minimal Debian image with python + git, copies the flow source in,
# installs it via pip, and creates a non-root user (flow refuses to run as root).
#
# Build context expected to be the flow-cli repo root. Example:
# podman build -f tests/e2e/Containerfile -t flow-e2e .
#
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git \
python3 \
python3-venv \
python3-pip \
sudo \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Create a non-root user; flow refuses to run as uid 0.
RUN useradd --create-home --shell /bin/bash flowuser \
&& echo "flowuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# Copy the flow source and install it for the non-root user.
WORKDIR /opt/flow-src
COPY --chown=flowuser:flowuser . /opt/flow-src
USER flowuser
ENV PATH="/home/flowuser/.local/bin:${PATH}"
RUN pip3 install --user --break-system-packages -e /opt/flow-src
WORKDIR /home/flowuser
ENTRYPOINT ["/bin/bash"]

1
tests/e2e/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""End-to-end tests gated by FLOW_RUN_E2E=1."""

View File

@@ -0,0 +1,88 @@
"""End-to-end test for `flow dotfiles` against the example repo.
Gated by FLOW_RUN_E2E=1 because it builds and runs a container image.
Tries podman first, falls back to docker. Skips if neither is available.
"""
from __future__ import annotations
import os
import shutil
import subprocess
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
CONTAINERFILE = Path(__file__).parent / "Containerfile"
EXAMPLE_REPO = REPO_ROOT / "example" / "dotfiles-repo"
IMAGE_TAG = "flow-e2e:test"
def _pick_runtime() -> str | None:
for binary in ("podman", "docker"):
if shutil.which(binary):
return binary
return None
@pytest.mark.skipif(
os.environ.get("FLOW_RUN_E2E") != "1",
reason="set FLOW_RUN_E2E=1 to run",
)
def test_dotfiles_init_and_link_in_container():
runtime = _pick_runtime()
if runtime is None:
pytest.skip("neither podman nor docker is available")
build = subprocess.run(
[
runtime, "build",
"-f", str(CONTAINERFILE),
"-t", IMAGE_TAG,
str(REPO_ROOT),
],
capture_output=True,
text=True,
)
if build.returncode != 0:
pytest.fail(f"image build failed:\n{build.stdout}\n{build.stderr}")
try:
# Run flow inside the container against the mounted example repo.
# `flow dotfiles init` clones, so we need a real git remote — turn
# the read-only example mount into a bare-ish working repo first.
script = (
"set -eux; "
"cp -r /example /home/flowuser/dotfiles-src; "
"cd /home/flowuser/dotfiles-src; "
"git init -q -b main; "
"git -c user.email=e2e@example.com -c user.name=e2e add -A; "
"git -c user.email=e2e@example.com -c user.name=e2e commit -q -m initial; "
"cd /home/flowuser; "
"flow dotfiles init --repo /home/flowuser/dotfiles-src; "
"flow dotfiles link --profile linux-auto --dry-run; "
"flow dotfiles status"
)
result = subprocess.run(
[
runtime, "run", "--rm",
"-v", f"{EXAMPLE_REPO}:/example:ro",
IMAGE_TAG,
"-c", script,
],
capture_output=True,
text=True,
)
assert result.returncode == 0, (
f"e2e run failed (rc={result.returncode}):\n"
f"stdout: {result.stdout}\n"
f"stderr: {result.stderr}"
)
finally:
subprocess.run(
[runtime, "rmi", "-f", IMAGE_TAG],
capture_output=True,
text=True,
)