Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 47 additions & 28 deletions .github/workflows/ci-v2.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,51 @@
name: CI v2

# NO paths filter (the adversarial review's high finding): a PR touching only
# corvus-sdk or the Makefile got ZERO CI — the blind spot. The suite is fast
# (the test ~16s, the smoke ~5s): every PR and main push runs everything.
# corvus-sdk or the Makefile got ZERO CI — the blind spot. The suite is fast:
# every PR and main push runs everything.
# workflow_dispatch stays as the manual re-run lever for any sha.
#
# EVERY component runs its own harness. The live three (v2, sdk, cli) run a real
# build > lint > test cycle; the legacy/config three run their guards. The
# interpreter is pinned to 3.12 here and in the Makefiles — a mismatch changes the
# coverage number and used to hide behind a 7-point tolerance.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:

jobs:
test:
harness:
name: harness (${{ matrix.component }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
component:
- corvus-v2
- corvus-sdk
- corvus-cli
- corvus-server
- corvus-hooks
- corvus-splunk
defaults:
run:
working-directory: ${{ matrix.component }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Setup (pinned venv + deps)
run: make setup
- name: Build > Lint > Test
run: make cycle

phase-gates:
# The P1 capability gate, actually gated: BUILD-PLAN.md requires 'the
# conformance suite runs green in CI with the mutation evidence'. At HEAD
# neither ran in CI — they were manual targets. They are manual no longer.
runs-on: ubuntu-latest
defaults:
run:
Expand All @@ -21,32 +55,17 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install
run: |
python -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
pip install pytest pytest-cov httpx ruff==0.17.1
- name: Lint
run: |
. .venv/bin/activate
ruff check corvus/ tests/
- name: Tests with coverage
run: |
. .venv/bin/activate
pytest tests/ -v --cov=corvus --cov-report=term --cov-report=json:coverage.json --junitxml=junit.xml
- name: The metrics drift guard
run: |
. .venv/bin/activate
python scripts/gen_metrics.py --coverage coverage.json --junit junit.xml \
--out /tmp/metrics-check.json --source "$GITHUB_SHA"
python scripts/check_metrics.py --expected ../docs/canonical-path/dev-metrics.json \
--actual /tmp/metrics-check.json
- name: Coverage summary
run: |
. .venv/bin/activate
python -c 'import json; c = json.load(open("coverage.json")); t = c["totals"]; print("coverage: %.1f%% (%d/%d lines)" % (t["percent_covered"], t["covered_lines"], t["num_statements"]))'
- name: Setup
run: make setup
- name: Mutation evidence
run: make mutation
- name: Conformance run
run: make conformance

smoke:
# Kept from the original gate: the components install and import cleanly.
# Each component's build step now proves this too, but this job stays as the
# independent cross-component check that a packaging change cannot silently break.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
Expand Down
148 changes: 82 additions & 66 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,66 +1,82 @@
# The Corvus dev harness — the Build > Test > Fix cycle, one command per step.
# `make cycle` is the full loop; every step is idempotent and fresh-clone safe.

VENV := corvus-v2/.venv
PY := $(VENV)/bin/python
PIP := $(VENV)/bin/pip
METRICS := docs/canonical-path/dev-metrics.json

.PHONY: setup lint test metrics run check cycle clean

setup: ## Bootstrap the venv + the deps (fresh-clone safe)
python3 -m venv $(VENV)
$(PIP) install --quiet --upgrade pip
$(PIP) install --quiet -r corvus-v2/requirements.txt
$(PIP) install --quiet pytest pytest-cov ruff httpx
@echo "setup: ok"

lint: ## Ruff over the v2 package and the tests
$(PY) -m ruff check corvus-v2/corvus/ corvus-v2/tests/
@echo "lint: ok"

test: ## The tests with the coverage measurement
cd corvus-v2 && .venv/bin/python -m pytest tests/ -q \
--cov=corvus --cov-report=term --cov-report=json:coverage.json \
--junitxml=junit.xml
@echo "test: ok"

metrics: ## Regenerate the dev-metrics artifact from the last test run
$(PY) corvus-v2/scripts/gen_metrics.py \
--coverage corvus-v2/coverage.json --junit corvus-v2/junit.xml --out $(METRICS) \
--source "$$(git rev-parse --short HEAD)" --duration "$$(cd corvus-v2 && .venv/bin/python -c "import json;print(json.load(open('coverage.json')).get('duration',0) if False else 0)")"
@echo "metrics: regenerated"

metrics-check: ## The drift guard: the committed metrics must match the fresh run
$(PY) corvus-v2/scripts/gen_metrics.py \
--coverage corvus-v2/coverage.json --junit corvus-v2/junit.xml --out /tmp/metrics-check.json \
--source "$$(git rev-parse --short HEAD)"
$(PY) corvus-v2/scripts/check_metrics.py --expected $(METRICS) --actual /tmp/metrics-check.json

run: ## The server up + the live health check (the dev loop's "build" step)
cd corvus-v2 && .venv/bin/python -c "\
import sys, threading, time, json, urllib.request;\
sys.path.insert(0, '.');\
from corvus.app import create_app;\
from corvus.config import FleetConfig;\
cfg = FleetConfig(name='dev-fleet', node_id='dev-1', api_key='dev-local-key');\
app = create_app(config=cfg, db_path=':memory:');\
t = threading.Thread(target=lambda: __import__('uvicorn').run(app, host='127.0.0.1', port=8932, log_level='error'), daemon=True);\
t.start(); time.sleep(1.5);\
r = json.loads(urllib.request.urlopen(urllib.request.Request('http://127.0.0.1:8932/health', headers={'X-Corvus-Key': 'dev-key'}), timeout=5).read());\
assert r['status'] == 'ok', r;\
print('run: ok —', r['fleet'])"

check: lint test metrics-check ## The gates the CI runs (identical locally)

cycle: check run ## THE Build > Test > Fix loop, one command

clean: ## The artifacts
rm -rf $(VENV) corvus-v2/.pytest_cache corvus-v2/coverage.json /tmp/metrics-check.json

# WP14: the mutation evidence + the conformance run (the phase-gate targets)
mutation:
cd corvus-v2 && .venv/bin/python -c "import sys; sys.path.insert(0, '.'); from corvus.conformance import run_mutation_suite; import json; r = run_mutation_suite(); print(json.dumps(r, indent=1)); exit(0 if r['all_mutants_caught'] else 1)"

conformance:
cd corvus-v2 && .venv/bin/python -m corvus.engine_shim --port 8933 & sleep 2; .venv/bin/python -c "import sys; sys.path.insert(0, '.'); from corvus.conformance import conformance_run; import json; r = conformance_run('http://127.0.0.1:8933'); print(json.dumps(r, indent=1)); exit(0 if r['failed'] == 0 else 1)"; pkill -f "engine_shim --port 8933" || true
# The Corvus dev harness — the Build > Test > Fix cycle for EVERY component.
# Each component owns its cycle in <component>/Makefile; this file is the umbrella
# that runs them all, so "does the repo work?" has one answer.
#
# Live components (real harnesses): corvus-v2, corvus-sdk, corvus-cli
# Legacy/config guards: corvus-server, corvus-hooks, corvus-splunk
# v1 is archived at branch v1-archive and is NOT deployed; its guards exist only
# so archived code cannot silently rot. No tests are resurrected for it.
#
# The component lists below are NOT cosmetic: the legacy three do not declare
# build/test (nothing to build or test), and corvus-splunk has no lint/fix (it is
# config, not Python). Looping a target over a component that lacks it aborts the
# whole umbrella with "No rule to make target", so each target runs over exactly
# the components that define it.
#
# The interpreter is PINNED to python3.12 (CI uses 3.12). Bare `python3` on this
# host is 3.15, and running the suite on it changes the coverage number enough to
# matter — that mismatch is what the pin removes.

PYTHON ?= python3.12

# Live three: the full harness.
LIVE := corvus-v2 corvus-sdk corvus-cli
# Everything: setup, cycle and clean exist everywhere.
ALL := $(LIVE) corvus-server corvus-hooks corvus-splunk
# Python components: they lint and autofix. corvus-splunk is config (validate only).
PY := $(LIVE) corvus-server corvus-hooks

V2 := corvus-v2

export PYTHON

.PHONY: help setup build lint test fix cycle check clean metrics metrics-check run mutation conformance

help: ## This list
@grep -E '^[a-z-]+:.*##' $(MAKEFILE_LIST) | sed 's/:.*##/ —/'

setup: ## Bootstrap every component's venv + deps (fresh-clone safe)
@for c in $(ALL); do printf '\n== %s ==\n' "$$c"; $(MAKE) -C $$c setup || exit 1; done
@echo "\nsetup: all components ok"

build: ## The live three's build proof (the legacy guards have nothing to build)
@for c in $(LIVE); do printf '\n== %s ==\n' "$$c"; $(MAKE) -C $$c build || exit 1; done
@echo "\nbuild: all live components ok"

lint: ## Ruff over every Python component
@for c in $(PY); do printf '\n== %s ==\n' "$$c"; $(MAKE) -C $$c lint || exit 1; done
@echo "\nlint: all Python components ok"

test: ## The live three's tests (the legacy guards have no tests by design)
@for c in $(LIVE); do printf '\n== %s ==\n' "$$c"; $(MAKE) -C $$c test || exit 1; done
@echo "\ntest: all live components ok"

fix: ## The autofix step across every Python component
@for c in $(PY); do printf '\n== %s ==\n' "$$c"; $(MAKE) -C $$c fix || exit 1; done
@echo "\nfix: all Python components ok — run `make lint` to confirm"

cycle: setup ## THE loop: setup, then every component's build > lint > test
@for c in $(ALL); do printf '\n===== %s =====\n' "$$c"; $(MAKE) -C $$c cycle || exit 1; done
@echo "\ncycle: ALL COMPONENTS OK"

check: cycle ## The full gate set (what CI runs) — identical to cycle

clean: ## Remove every component's regenerable state
@for c in $(ALL); do $(MAKE) -C $$c clean; done
@echo "clean: ok — run `make setup` to rebuild"

# ── v2-specific targets, kept at the root for continuity ────────────────────
metrics: ## Regenerate the committed dev-metrics artifact
$(MAKE) -C $(V2) metrics

metrics-check: ## The metrics drift guard
$(MAKE) -C $(V2) metrics-check

run: ## The engine up + the live proof
$(MAKE) -C $(V2) build

mutation: ## The mutation evidence (the phase-gate target)
$(MAKE) -C $(V2) mutation

conformance: ## The conformance run (the phase-gate target)
$(MAKE) -C $(V2) conformance
9 changes: 9 additions & 0 deletions corvus-cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# corvus-cli harness artifacts (component-local: the root .gitignore is not ours to edit)
.venv/
.pytest_cache/
.ruff_cache/
.coverage
coverage.json
junit.xml
__pycache__/
*.egg-info/
47 changes: 47 additions & 0 deletions corvus-cli/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# corvus-cli dev harness — the Build > Test > Fix cycle, one command per step.
# Every target is idempotent and fresh-clone safe; run from this directory (make -C corvus-cli ...).

PYTHON ?= python3.12
VENV := .venv
PY := $(VENV)/bin/python
PIP := $(VENV)/bin/pip
RUFF := $(VENV)/bin/ruff

.PHONY: setup build lint test fix cycle clean

setup: ## Create the 3.12 venv, install the deps and the pinned tooling
@test -d $(VENV) || $(PYTHON) -m venv $(VENV)
$(PIP) install --quiet --upgrade pip
$(PIP) install --quiet -e .
$(PIP) install --quiet ruff==0.17.1 pytest pytest-cov
@echo "setup: ok"

build: setup ## Editable install + proof the entrypoint dispatches
$(PIP) install --quiet -e .
$(PIP) check # the resolved dependency set must be internally consistent
$(PY) -c "import corvus_cli.main as m; assert m.app is not None; print('import: ok')"
$(VENV)/bin/corvus --help > /dev/null
$(VENV)/bin/corvus check-target --help > /dev/null
@echo "build: ok"

lint: ## Ruff over the package and the tests
$(RUFF) check src/ tests/
@echo "lint: ok"

test: ## The tests with coverage + junit evidence
$(PY) -m pytest tests/ -q --cov=corvus_cli --cov-report=term \
--cov-report=json:coverage.json --junitxml=junit.xml
@echo "test: ok"

fix: ## Apply the ruff autofixes (no `ruff format` — the repo does not use it)
$(RUFF) check --fix src/ tests/
@echo "fix: ok"

cycle: build lint test ## The one command: build, lint, test

# clean removes only artifacts this harness generates (venv, caches, reports); nothing tracked
# or hand-written is in that list. PREFLIGHT:rollback-written — the rollback is `make setup && make cycle`.
clean: ## Drop the venv and the test artifacts
rm -rf $(VENV) .pytest_cache .ruff_cache src/corvus_cli/__pycache__ tests/__pycache__
rm -f .coverage coverage.json junit.xml
@echo "clean: ok"
4 changes: 3 additions & 1 deletion corvus-cli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ name = "corvus-cli"
version = "0.1.0"
requires-python = ">=3.11"
description = "CLI for the Corvus operational governance API"
dependencies = ["httpx>=0.27", "typer>=0.15", "rich>=13"]
# Floors are the versions the harness tested (python3.12), capped at the next
# MAJOR so a breaking release fails at install time rather than silently.
dependencies = ["httpx>=0.28,<1.0", "typer>=0.27,<1.0", "rich>=15,<16"]

[project.scripts]
corvus = "corvus_cli.main:app"
Expand Down
82 changes: 82 additions & 0 deletions corvus-cli/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Fixtures for the CLI suite: a hermetic environment and an in-memory HTTP transport.

No test ever opens a socket - every httpx.Client the CLI builds gets a MockTransport.
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from typing import Any

import httpx
import pytest

from corvus_cli import main


@pytest.fixture(autouse=True)
def isolated_env(monkeypatch, tmp_path):
"""Hermetic env: no real ~/.corvus.json and no inherited CORVUS_* variables."""
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.delenv("CORVUS_URL", raising=False)
monkeypatch.delenv("CORVUS_TOKEN", raising=False)
return tmp_path


@dataclass
class Call:
"""One request the CLI actually sent."""

method: str
url: str
path: str
headers: dict
params: dict
body: Any = None


@dataclass
class FakeAPI:
"""A route table plus a record of what was requested."""

routes: dict = field(default_factory=dict)
calls: list = field(default_factory=list)

def on(self, method: str, path: str, payload: Any, status: int = 200) -> FakeAPI:
self.routes[(method.upper(), path)] = (status, payload)
return self

def handler(self, request: httpx.Request) -> httpx.Response:
self.calls.append(
Call(
method=request.method,
url=str(request.url),
path=request.url.path,
headers=dict(request.headers),
params=dict(request.url.params),
body=json.loads(request.content) if request.content else None,
)
)
status, payload = self.routes.get((request.method, request.url.path), (404, {"detail": "no route"}))
return httpx.Response(status, json=payload)

def call(self, method: str, path: str) -> Call:
"""The one recorded call to (method, path)."""
matches = [c for c in self.calls if c.method == method.upper() and c.path == path]
assert len(matches) == 1, f"expected exactly one {method} {path}, recorded {self.calls}"
return matches[0]


@pytest.fixture
def api(monkeypatch) -> FakeAPI:
"""Route every httpx.Client through an in-memory transport (never a live server)."""
fake = FakeAPI()
real_client = httpx.Client

def factory(*args, **kwargs):
kwargs["transport"] = httpx.MockTransport(fake.handler)
return real_client(*args, **kwargs)

monkeypatch.setattr(main.httpx, "Client", factory)
return fake
Loading
Loading