test: cover init install flows end to end

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
JerrettDavis 2026-04-21 20:15:11 -05:00
parent c5d795c2af
commit a278a7b0ba
7 changed files with 463 additions and 69 deletions

View file

@ -1,63 +1,73 @@
# VCS
.git
.github
.gitignore
# Python artifacts
__pycache__
*.pyc
*.pyo
*.egg-info
dist/
build/
# Dev/test tooling
.pytest_cache
.coverage
.mypy_cache
.ruff_cache
.pre-commit-config.yaml
.venv
venv
# Tests & docs (not needed in image)
tests/
docs/
mkdocs.yml
CHANGELOG.md
LICENSE
NOTICE
# JS/TS artifacts (dashboard, SDK — not part of proxy image)
apps/
sdk/
!sdk/
!sdk/typescript/
!sdk/typescript/**
plugins/
!plugins/
!plugins/openclaw/
!plugins/openclaw/**
node_modules/
*.tgz
# Secrets & local config
.env
.env.*
*.log
# IDE
.vscode
.idea
*.swp
# Docker
Dockerfile
docker-compose*.yml
.dockerignore
# Misc
.pi-lens/
.superpowers/
examples/
node-compile-cache/
# VCS
.git
.github
.github/*
!.github/plugin/
!.github/plugin/**
.gitignore
# Python artifacts
__pycache__
*.pyc
*.pyo
*.egg-info
dist/
build/
# Dev/test tooling
.pytest_cache
.coverage
.mypy_cache
.ruff_cache
.pre-commit-config.yaml
.venv
venv
# Tests & docs (not needed in image)
tests/
docs/
mkdocs.yml
CHANGELOG.md
LICENSE
NOTICE
# JS/TS artifacts (dashboard, SDK — not part of proxy image)
apps/
sdk/
!sdk/
!sdk/typescript/
!sdk/typescript/**
plugins/
!plugins/
!plugins/openclaw/
!plugins/openclaw/**
!plugins/headroom-agent-hooks/
!plugins/headroom-agent-hooks/**
node_modules/
*.tgz
# Secrets & local config
.env
.env.*
*.log
# IDE
.vscode
.idea
*.swp
# Docker
Dockerfile
docker-compose*.yml
.dockerignore
# Misc
.pi-lens/
.superpowers/
examples/
node-compile-cache/
!e2e/
!e2e/init/
!e2e/init/**
!.claude-plugin/
!.claude-plugin/**

View file

@ -49,12 +49,12 @@ jobs:
- name: Run tests
run: |
pytest -v --tb=short
pytest -v --tb=short tests scripts/tests
- name: Run tests with coverage
if: matrix.python-version == '3.11'
run: |
pytest --cov=headroom --cov-report=xml --cov-report=term-missing
pytest tests scripts/tests --cov=headroom --cov-report=xml --cov-report=term-missing
- name: Upload coverage to Codecov
if: matrix.python-version == '3.11'
@ -146,6 +146,11 @@ jobs:
docker build -f e2e/wrap/Dockerfile -t headroom-wrap-e2e .
docker run --rm headroom-wrap-e2e
- name: Run Docker-native init e2e
run: |
docker build -f e2e/init/Dockerfile -t headroom-init-e2e .
docker run --rm headroom-init-e2e
windows-native-wrapper:
runs-on: windows-latest
steps:
@ -215,10 +220,10 @@ jobs:
name: dist
path: dist/
commitlint:
if: github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'Merge pull request ')
runs-on: ubuntu-latest
steps:
commitlint:
if: github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'Merge pull request ')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

22
.github/workflows/init-e2e.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: Init E2E
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
jobs:
docker-init-e2e:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- name: Build init e2e image
run: docker build -f e2e/init/Dockerfile -t headroom-init-e2e .
- name: Run init e2e container
run: docker run --rm headroom-init-e2e

33
e2e/init/Dockerfile Normal file
View file

@ -0,0 +1,33 @@
FROM node:22-bookworm
ENV DEBIAN_FRONTEND=noninteractive \
PATH="/opt/headroom-venv/bin:${PATH}" \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
git \
python3 \
python3-pip \
python3-venv && \
ln -sf /usr/bin/python3 /usr/local/bin/python && \
rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
COPY pyproject.toml README.md uv.lock ./
COPY headroom ./headroom
COPY .claude-plugin ./.claude-plugin
COPY .github/plugin ./.github/plugin
COPY plugins/headroom-agent-hooks ./plugins/headroom-agent-hooks
COPY e2e/init ./e2e/init
RUN python -m venv /opt/headroom-venv && \
/opt/headroom-venv/bin/python -m pip install --upgrade pip && \
/opt/headroom-venv/bin/python -m pip install -e ".[proxy]"
CMD ["python", "e2e/init/run.py"]

236
e2e/init/run.py Normal file
View file

@ -0,0 +1,236 @@
from __future__ import annotations
import json
import os
import stat
import subprocess
import sys
import tempfile
import textwrap
from pathlib import Path
from headroom.cli import init as init_cli
REPO_ROOT = Path("/workspace")
HEADROOM = "headroom"
def log(message: str) -> None:
print(f"[init-e2e] {message}", flush=True)
def run(
cmd: list[str],
*,
env: dict[str, str],
cwd: Path,
timeout: int = 180,
) -> subprocess.CompletedProcess[str]:
log(f"$ {' '.join(cmd)}")
result = subprocess.run(
cmd,
env=env,
cwd=str(cwd),
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=timeout,
)
if result.stdout.strip():
print(result.stdout.rstrip(), flush=True)
if result.stderr.strip():
print(result.stderr.rstrip(), file=sys.stderr, flush=True)
if result.returncode != 0:
raise RuntimeError(f"Command failed with exit code {result.returncode}: {' '.join(cmd)}")
return result
def assert_true(condition: bool, message: str) -> None:
if not condition:
raise AssertionError(message)
def write_executable(path: Path, content: str) -> None:
path.write_text(content, encoding="utf-8")
path.chmod(path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
def read_jsonl(path: Path) -> list[dict[str, object]]:
if not path.exists():
return []
return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line]
def create_agent_shims(shim_dir: Path, log_path: Path) -> None:
shim = textwrap.dedent(
"""\
#!/usr/bin/env python3
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
record = {
"tool": Path(sys.argv[0]).name,
"argv": sys.argv[1:],
"cwd": os.getcwd(),
}
log_path = Path(os.environ["HEADROOM_INIT_E2E_LOG"])
log_path.parent.mkdir(parents=True, exist_ok=True)
with log_path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(record) + "\\n")
print(f"{record['tool']} shim executed")
raise SystemExit(0)
"""
)
shim_dir.mkdir(parents=True, exist_ok=True)
for name in ("claude", "copilot"):
write_executable(shim_dir / name, shim)
def expect_hook_command(command: str, profile: str) -> None:
assert_true("init hook ensure" in command, f"missing init hook ensure in: {command}")
assert_true(f"--profile {profile}" in command, f"missing profile {profile} in: {command}")
def read_manifest(home_dir: Path, profile: str) -> dict[str, object]:
path = home_dir / ".headroom" / "deploy" / profile / "manifest.json"
assert_true(path.exists(), f"Expected manifest at {path}")
return json.loads(path.read_text(encoding="utf-8"))
def verify_claude_local(home_dir: Path, project_dir: Path, shim_log: Path) -> None:
settings = json.loads(
(project_dir / ".claude" / "settings.local.json").read_text(encoding="utf-8")
)
assert_true(
settings["env"]["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:9011",
"Claude local settings should point at the requested proxy port",
)
session_start = settings["hooks"]["SessionStart"][0]["hooks"][0]["command"]
pre_tool = settings["hooks"]["PreToolUse"][0]["hooks"][0]["command"]
profile = init_cli._local_profile(project_dir)
expect_hook_command(session_start, profile)
expect_hook_command(pre_tool, profile)
manifest = read_manifest(home_dir, profile)
assert_true("claude" in manifest["targets"], "Claude init should register the claude target")
claude_calls = [record["argv"] for record in read_jsonl(shim_log) if record["tool"] == "claude"]
assert_true(
claude_calls
== [
["plugin", "marketplace", "add", str(REPO_ROOT)],
["plugin", "install", "headroom@headroom-marketplace", "--scope", "local"],
],
f"Unexpected Claude install commands: {claude_calls}",
)
def verify_copilot_global(home_dir: Path, shim_log: Path) -> None:
config = json.loads((home_dir / ".copilot" / "config.json").read_text(encoding="utf-8"))
assert_true(
"SessionStart" in config["hooks"], "Copilot config should include SessionStart hooks"
)
assert_true("PreToolUse" in config["hooks"], "Copilot config should include PreToolUse hooks")
session_start = config["hooks"]["SessionStart"][0]["command"]
expect_hook_command(session_start, "init-user")
for shell_file in (home_dir / ".bashrc", home_dir / ".zshrc", home_dir / ".profile"):
content = shell_file.read_text(encoding="utf-8")
assert_true(
'export COPILOT_PROVIDER_TYPE="openai"' in content,
f"{shell_file.name} should contain the Copilot provider type",
)
assert_true(
'export COPILOT_PROVIDER_BASE_URL="http://127.0.0.1:9005/v1"' in content,
f"{shell_file.name} should contain the Copilot provider base URL",
)
assert_true(
'export COPILOT_PROVIDER_WIRE_API="completions"' in content,
f"{shell_file.name} should contain the Copilot wire API",
)
copilot_calls = [
record["argv"] for record in read_jsonl(shim_log) if record["tool"] == "copilot"
]
assert_true(
copilot_calls
== [
["plugin", "marketplace", "add", str(REPO_ROOT)],
["plugin", "install", "headroom@headroom-marketplace"],
],
f"Unexpected Copilot install commands: {copilot_calls}",
)
def verify_codex_local(home_dir: Path, project_dir: Path) -> None:
config_path = project_dir / ".codex" / "config.toml"
hooks_path = project_dir / ".codex" / "hooks.json"
config = config_path.read_text(encoding="utf-8")
hooks = json.loads(hooks_path.read_text(encoding="utf-8"))
profile = init_cli._local_profile(project_dir)
assert_true(
'base_url = "http://127.0.0.1:9012/v1"' in config,
"Codex config should point at the requested proxy port",
)
assert_true(
config.count("[features]") == 1, "Codex config should keep a single [features] table"
)
assert_true("codex_hooks = true" in config, "Codex config should enable codex_hooks")
command = hooks["hooks"]["SessionStart"][0]["hooks"][0]["command"]
expect_hook_command(command, profile)
manifest = read_manifest(home_dir, profile)
targets = manifest["targets"]
assert_true(set(targets) == {"claude", "codex"}, f"Unexpected merged targets: {targets}")
def main() -> None:
with tempfile.TemporaryDirectory(prefix="headroom-init-e2e-") as temp_root_raw:
temp_root = Path(temp_root_raw)
home_dir = temp_root / "home"
project_dir = temp_root / "project"
shim_dir = temp_root / "bin"
shim_log = temp_root / "shim-log.jsonl"
home_dir.mkdir(parents=True)
project_dir.mkdir(parents=True)
create_agent_shims(shim_dir, shim_log)
env = os.environ.copy()
env["HOME"] = str(home_dir)
env["USERPROFILE"] = str(home_dir)
env["HEADROOM_INIT_E2E_LOG"] = str(shim_log)
env["PATH"] = f"{shim_dir}:{env['PATH']}"
run([HEADROOM, "init", "--port", "9011", "claude"], env=env, cwd=project_dir)
verify_claude_local(home_dir, project_dir, shim_log)
run(
[
HEADROOM,
"init",
"-g",
"--port",
"9005",
"--backend",
"openai",
"copilot",
],
env=env,
cwd=project_dir,
)
verify_copilot_global(home_dir, shim_log)
run([HEADROOM, "init", "--port", "9012", "codex"], env=env, cwd=project_dir)
verify_codex_local(home_dir, project_dir)
log("Init e2e completed successfully")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,68 @@
"""Tests for sync-plugin-versions.py."""
from __future__ import annotations
import importlib.util
from pathlib import Path
def _load_module():
script = Path(__file__).parent.parent / "sync-plugin-versions.py"
spec = importlib.util.spec_from_file_location("sync_plugin_versions", script)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_compute_repo_semver_uses_release_helpers(monkeypatch) -> None:
module = _load_module()
calls: dict[str, object] = {}
monkeypatch.setattr(module, "list_release_tags", lambda root: ["v0.9.0"])
monkeypatch.setattr(module, "find_latest_release_tag", lambda tags: "v0.9.0")
monkeypatch.setattr(module, "list_release_commits", lambda root, tag: ["feat: add init"])
monkeypatch.setattr(module, "determine_bump_level", lambda commits: "minor")
monkeypatch.setattr(module, "get_canonical_version", lambda root: "0.5.25")
def fake_compute_release_version(*, canonical_version: str, level: str, tags: list[str]):
calls["canonical_version"] = canonical_version
calls["level"] = level
calls["tags"] = tags
return type("Info", (), {"npm_version": "0.10.0"})()
monkeypatch.setattr(module, "compute_release_version", fake_compute_release_version)
assert module.compute_repo_semver(Path("repo")) == "0.10.0"
assert calls == {
"canonical_version": "0.5.25",
"level": "minor",
"tags": ["v0.9.0"],
}
def test_main_runs_plugin_only_version_sync(monkeypatch) -> None:
module = _load_module()
commands: list[list[str]] = []
monkeypatch.setattr(module, "compute_repo_semver", lambda root: "0.10.0")
monkeypatch.setattr(
module.subprocess,
"run",
lambda command, cwd, check: commands.append(command),
)
module.main()
assert commands == [
[
module.sys.executable,
str(module.ROOT / "scripts" / "version-sync.py"),
"--root",
str(module.ROOT),
"--version",
"0.10.0",
"--plugin-manifests-only",
]
]

View file

@ -200,3 +200,23 @@ def test_detect_init_targets_respects_scope(monkeypatch) -> None:
assert init_cli.detect_init_targets(False) == ["claude", "codex"]
assert init_cli.detect_init_targets(True) == ["claude", "copilot", "codex", "openclaw"]
def test_marketplace_source_prefers_env_override(monkeypatch) -> None:
init_cli, _ = _load_init_module(monkeypatch)
monkeypatch.setenv("HEADROOM_MARKETPLACE_SOURCE", "custom/source")
assert init_cli._marketplace_source() == "custom/source"
def test_run_checked_treats_existing_install_as_success(monkeypatch) -> None:
init_cli, _ = _load_init_module(monkeypatch)
class _Result:
returncode = 1
stderr = "plugin already exists"
stdout = ""
monkeypatch.setattr(init_cli.subprocess, "run", lambda *args, **kwargs: _Result())
init_cli._run_checked(["claude", "plugin", "install"], action="claude plugin install")