zenops 0.20.0

Declarative system configuration management for shell config and dotfiles.
# Cross-platform integration tests

Docker-driven integration tests for the `zenops` binary. Each combination of
`(distro, shell, package_manager)` runs in its own container with a real
shell, a real package manager, and a real `cargo install` — no env-var
fakery, no mocked filesystem.

## Requirements

- Docker (any recent version).
- ~2 GB free disk for the cached images (one per distro).
- Internet access (rustup, distro packages, crates.io).
- Python 3.10+ on the host. Stdlib only — no `pip install` needed.

## Quick start

From the repo root:

```bash
python tests/cross-platform/run.py --only ubuntu --shell bash
```

Tier-1 matrix (the CI gate — latest LTS / latest stable / Arch):

```bash
python tests/cross-platform/run.py
```

Extended matrix (also runs the opt-in tier-2 rows — older / interim
Fedora and Ubuntu releases that should work but aren't covered by CI):

```bash
python tests/cross-platform/run.py --tier all
```

Install from crates.io instead of the local source tree:

```bash
python tests/cross-platform/run.py --install registry
```

Other useful flags:

```bash
--tier {1,2,all}    # which matrix tiers to run (default: 1)
--scenario <name>   # which scenario to run (default: basic)
--keep              # leave the container around for `docker exec` debugging
--pull              # docker build --pull (refresh base images)
--no-build          # assume images already exist; skip build step
```

## What gets tested (scenario `basic`)

A fresh container, four steps in order. The runner stops at the first
failing step.

1. **`01_install`**`cargo install --path /src --locked` (or `cargo install
   zenops` from crates.io).
2. **`02_bootstrap`** — drive `zenops init` over a pseudo-TTY, answer the
   rustyline prompts (shell / name / email), verify `config.toml` and the
   initial commit.
3. **`03_add_apply`** — append a `[pkg.demo]` block to `config.toml`, commit
   it in the zenops repo, then `zenops apply --yes`. Verify the symlink
   landed under `~/.config/demo/` and the shell's generated rc files exist.
4. **`04_import`** — create a real config at `~/.config/widget/`, run
   `zenops import --yes`, verify the original is now a symlink into the
   zenops repo and `config.toml` has gained a `[pkg.widget]` block.

## Layout

```
tests/cross-platform/
  run.py                          host driver
  matrix.py                       Entry rows: (distro, version, shell, pm, tier)
  docker/
    ubuntu.Dockerfile
    fedora.Dockerfile
    archlinux.Dockerfile
  container/
    runner.py                     in-container entrypoint
    common.py                     shared helpers (paths, pexpect, asserts)
    scenarios/
      basic.py                    STEPS = [...]
    steps/
      01_install.py
      02_bootstrap.py
      03_add_apply.py
      04_import.py
```

## Extending

**Adding a shell.** Append to `MATRIX` in `matrix.py` and add an entry to
`EXPECTED_FILES` in `container/common.py`. If the bootstrap prompt response
isn't the shell's plain name, override that in `02_bootstrap.py`.

**Adding a distro version.** Append an `Entry` to `MATRIX` with the new
`version` and the desired `tier`. The Dockerfile is shared across
versions via the `VERSION` build arg; nothing else to change. Tier-2
rows by convention run bash only — the shell dimension is exercised in
tier 1.

**Adding a distro family.** Drop a Dockerfile under
`docker/<distro>.Dockerfile` that installs `git`, `bash`, `zsh`, `curl`,
build tools, Python 3 with `pexpect`, a non-root `tester` user, and
rustup. Then add matrix entries. No test code changes.

**Adding a scenario.** Copy `container/scenarios/basic.py` and edit the
`STEPS` list. New step scripts go under `container/steps/` and are loaded by
file path, so the `NN_<name>.py` numbering controls order. Each step is a
standalone script (runnable directly during debugging) that uses helpers
from `container/common.py`.

## Debugging

`ZENOPS_TEST_DEBUG=1` in the container's environment makes the pexpect
wrapper echo the raw PTY stream — useful when a prompt match fails.

Inspect a failed run's state:

```bash
python tests/cross-platform/run.py --only ubuntu --shell bash --keep
# ... (failure) ...
docker exec -it zenops-test-ubuntu-24.04-bash bash   # container name = "zenops-test-<distro>[-<version>]-<shell>"
```

Remove cached images when something's stuck:

```bash
docker image rm $(docker image ls 'zenops-test:*' -q)
```