mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
Requirement 1.1 (Task/Workspace/Model use only fields ax documents) and
Requirement 1.4 (no ax field repurposed) were violated: manifests accepted
metadata.namespace and a Workspace git `commit`, and the reconciler dumped
every git field into what `ax apply` receives.
Facts from ax's source (pkg/apis/v1alpha1/ax.proto, internal/workspace/setup.go):
- ObjectMeta is `name` and `atespace` (plus server-set creation_timestamp);
no `namespace`, no `annotations`.
- GitRepo is `name`, `repo`, `branch`, `dir`, `depth`; no `commit`.
- The API server decodes with protojson.UnmarshalOptions{}: an unknown field
is an error at `ax apply` (confirmed on the live cluster: a rendered
Workspace+Model applied cleanly; the same Workspace with `commit` was
refused with `unknown field "commit"`).
- ValidateModel checks only metadata; any spec.provider string is accepted.
Changes:
- manifests: metadata takes `name`, `atespace`, `annotations` (our extension,
never sent to ax); git entries take exactly ax's five fields (depth int);
`commit` in spec.git is rejected with a pointer to the new Workspace
annotation `openultrasast.io/git-commits: "<git name>=<40-hex>,..."`,
validated for unknown git names and non-hex shas.
- reconciler: rendered docs carry only ax fields; each bound Workspace's pins
reach the Task as env OUSAST_GIT_PINS = {"<workspace>/<git name>": "<sha>"}.
A test walks every rendered doc against ax's field lists.
- runner: pins come from OUSAST_GIT_PINS; git is fetched as setup.go does
(init/remote/fetch with --depth when depth > 0/checkout FETCH_HEAD, default
branch main, cloneDestination's dir and placeholder-name rules), then the
pin is checked out. A pinned entry without a branch fetches the commit.
- workspaces: emits the annotation, `dir: repo` and `depth: 1` instead of
`commit`, so generated case workspaces keep <path>/repo at depth 1.
- design.md: ax field names verbatim; pins and model binding as stripped
openultrasast.io/* annotations carried to the task by env.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
464 lines
20 KiB
Python
464 lines
20 KiB
Python
"""ax runner contract (ai-service-plane task 3, Requirements 4.1-4.4): env contract, readiness, delivery, exit codes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import threading
|
|
import urllib.error
|
|
import urllib.request
|
|
from collections.abc import Iterator
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from openultrasast.plane import runner
|
|
from openultrasast.plane.manifests import GitSource
|
|
from openultrasast.plane.runner import (
|
|
EXIT_BY_STATUS,
|
|
DeliveryError,
|
|
clone_destination,
|
|
deliver,
|
|
find_cached_checkout,
|
|
start_health_server,
|
|
)
|
|
|
|
STUB = Path(__file__).parent / "plane_stub_task.py"
|
|
STUB_MODULE = f"{runner.TASK_PACKAGE}.stub_task"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def stub_task() -> Iterator[None]:
|
|
"""Register the stub as ``openultrasast.plane.tasks.stub_task`` for the runner's import; no src package needed."""
|
|
spec = importlib.util.spec_from_file_location(STUB_MODULE, STUB)
|
|
assert spec is not None and spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[STUB_MODULE] = module
|
|
spec.loader.exec_module(module)
|
|
try:
|
|
yield
|
|
finally:
|
|
sys.modules.pop(STUB_MODULE, None)
|
|
|
|
|
|
@pytest.fixture
|
|
def env(monkeypatch: pytest.MonkeyPatch) -> Iterator[dict[str, str]]:
|
|
"""A private environment for one runner invocation, restored whole afterwards.
|
|
|
|
The runner and the stub read and write ``os.environ`` directly (that is the contract), so monkeypatch alone
|
|
would not undo what the runner exports; the snapshot does.
|
|
"""
|
|
saved = dict(os.environ)
|
|
for key in list(os.environ):
|
|
if key.startswith(("OUSAST_", "AX_", "STUB_")):
|
|
monkeypatch.delenv(key)
|
|
monkeypatch.setenv("AX_RUNNER_HTTP", "0")
|
|
monkeypatch.setenv("OUSAST_DELIVERY_BACKOFF", "0")
|
|
try:
|
|
yield os.environ
|
|
finally:
|
|
os.environ.clear()
|
|
os.environ.update(saved)
|
|
|
|
|
|
def task_yaml(output: Path, workspace: Path, *, command: list[str] | None = None, name: str = "stub") -> str:
|
|
cmd = command or ["stub-task", "--pass", "a"]
|
|
env_lines = "\n".join(
|
|
f" - name: {k}\n value: {json.dumps(v)}"
|
|
for k, v in {
|
|
"OUSAST_OUTPUT_DIR": str(output),
|
|
"OUSAST_BUDGET_USD": "0.5",
|
|
"OUSAST_BUDGET_CALLS": "40",
|
|
"OUSAST_INPUT_CANDIDATES": str(workspace / "candidates.json"),
|
|
}.items()
|
|
)
|
|
return (
|
|
"apiVersion: ax.io/v1alpha1\nkind: Task\nmetadata:\n name: " + name + "\nspec:\n command: " + json.dumps(cmd) + "\n"
|
|
" env:\n" + env_lines + "\n workspaces:\n - name: case\n path: " + json.dumps(str(workspace)) + "\n"
|
|
)
|
|
|
|
|
|
def files_workspace_yaml() -> str:
|
|
return (
|
|
"apiVersion: ax.io/v1alpha1\nkind: Workspace\nmetadata:\n name: case\nspec:\n files:\n"
|
|
' - path: candidates.json\n content: \'{"sinks": ["a"]}\'\n'
|
|
" - path: src/app.py\n content: |\n def handler():\n return 1\n"
|
|
)
|
|
|
|
|
|
def run_main(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, workspace_yaml: str, **extra: str) -> tuple[int, Path]:
|
|
output = tmp_path / "out"
|
|
workspace = tmp_path / "ws"
|
|
monkeypatch.setenv("AX_TASK_YAML", task_yaml(output, workspace))
|
|
monkeypatch.setenv("AX_WORKSPACES_YAML", workspace_yaml)
|
|
for key, value in extra.items():
|
|
monkeypatch.setenv(key, value)
|
|
return runner.main(), output
|
|
|
|
|
|
# --- env contract on a files workspace (Requirement 4.1, 4.3) --------------------------------------------------------
|
|
|
|
|
|
def test_env_contract_on_a_files_workspace(env: dict[str, str], monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
code, output = run_main(monkeypatch, tmp_path, files_workspace_yaml())
|
|
assert code == 0
|
|
facts = json.loads((output / "facts.json").read_text())
|
|
assert facts["argv"] == ["--pass", "a"], "the command's tail is the task's argv"
|
|
assert facts["workspace_files"] == ["candidates.json", "src/app.py"], "spec.files written verbatim at the bound path"
|
|
assert (tmp_path / "ws" / "src" / "app.py").read_text() == "def handler():\n return 1\n"
|
|
contract = facts["env"]
|
|
assert contract["OUSAST_OUTPUT_DIR"] == str(output)
|
|
assert contract["OUSAST_WORKSPACE_DIR"] == str(tmp_path / "ws"), "set by the runner from the first binding"
|
|
assert contract["OUSAST_INPUT_CANDIDATES"] == str(tmp_path / "ws" / "candidates.json")
|
|
assert contract["OUSAST_BUDGET_USD"] == "0.5" and contract["OUSAST_BUDGET_CALLS"] == "40"
|
|
assert json.loads((output / "summary.json").read_text())["status"] == "done"
|
|
|
|
|
|
def test_workspace_dir_from_the_task_env_is_kept(env: dict[str, str], monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
monkeypatch.setenv("OUSAST_WORKSPACE_DIR", str(tmp_path / "ws"))
|
|
code, output = run_main(monkeypatch, tmp_path, files_workspace_yaml())
|
|
assert code == 0
|
|
assert json.loads((output / "facts.json").read_text())["env"]["OUSAST_WORKSPACE_DIR"] == str(tmp_path / "ws")
|
|
|
|
|
|
def test_a_bound_workspace_missing_from_the_stream_fails_before_the_task(
|
|
env: dict[str, str], monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
code, output = run_main(monkeypatch, tmp_path, "")
|
|
assert code == EXIT_BY_STATUS["failed"]
|
|
summary = json.loads((output / "summary.json").read_text())
|
|
assert summary["status"] == "failed" and "case" in summary["reason"]
|
|
assert not (output / "facts.json").exists(), "the task never ran"
|
|
|
|
|
|
# --- readiness (Requirement 4.2) -----------------------------------------------------------------------------------------
|
|
|
|
|
|
def get(url: str) -> int:
|
|
try:
|
|
with urllib.request.urlopen(url, timeout=5) as response:
|
|
return int(response.status)
|
|
except urllib.error.HTTPError as exc:
|
|
return exc.code
|
|
|
|
|
|
def test_readyz_is_503_until_workspaces_are_ready_then_200() -> None:
|
|
ready = threading.Event()
|
|
server = start_health_server(0, ready)
|
|
try:
|
|
base = f"http://127.0.0.1:{server.port}"
|
|
assert get(f"{base}/healthz") == 200
|
|
assert get(f"{base}/readyz") == 503
|
|
assert get(f"{base}/other") == 404
|
|
ready.set()
|
|
assert get(f"{base}/readyz") == 200
|
|
finally:
|
|
server.stop()
|
|
|
|
|
|
def test_main_serves_readiness_on_a_free_port_and_the_task_sees_200(
|
|
env: dict[str, str], monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
code, output = run_main(monkeypatch, tmp_path, files_workspace_yaml(), AX_RUNNER_HTTP="1", AX_RUNNER_PORT="0")
|
|
assert code == 0
|
|
facts = json.loads((output / "facts.json").read_text())
|
|
assert facts["readyz"] == 200, "workspaces were materialised before the task started"
|
|
port = os.environ["AX_RUNNER_BOUND_PORT"]
|
|
with pytest.raises(urllib.error.URLError):
|
|
urllib.request.urlopen(f"http://127.0.0.1:{port}/healthz", timeout=2)
|
|
|
|
|
|
# --- artifact delivery (Requirement 4.4) --------------------------------------------------------------------------------
|
|
|
|
|
|
class Receiver(ThreadingHTTPServer):
|
|
"""Records every POST (headers and body) and answers with ``status``."""
|
|
|
|
def __init__(self, status: int) -> None:
|
|
super().__init__(("127.0.0.1", 0), _ReceiverHandler)
|
|
self.status = status
|
|
self.posts: list[tuple[dict[str, str], bytes]] = []
|
|
self.thread = threading.Thread(target=self.serve_forever, daemon=True)
|
|
self.thread.start()
|
|
|
|
@property
|
|
def url(self) -> str:
|
|
return f"http://127.0.0.1:{self.server_address[1]}/artifacts"
|
|
|
|
def stop(self) -> None:
|
|
self.shutdown()
|
|
self.server_close()
|
|
|
|
|
|
class _ReceiverHandler(BaseHTTPRequestHandler):
|
|
server: Receiver
|
|
|
|
def do_POST(self) -> None: # noqa: N802
|
|
body = self.rfile.read(int(self.headers.get("Content-Length", "0")))
|
|
self.server.posts.append(({k: v for k, v in self.headers.items()}, body))
|
|
self.send_response(self.server.status)
|
|
self.send_header("Content-Length", "0")
|
|
self.end_headers()
|
|
|
|
def log_message(self, format: str, *args: object) -> None: # noqa: A002
|
|
pass
|
|
|
|
|
|
@pytest.fixture
|
|
def receiver() -> Iterator[Receiver]:
|
|
server = Receiver(200)
|
|
try:
|
|
yield server
|
|
finally:
|
|
server.stop()
|
|
|
|
|
|
def test_output_dir_is_delivered_as_a_tar_with_the_headers(
|
|
env: dict[str, str], receiver: Receiver, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
code, output = run_main(monkeypatch, tmp_path, files_workspace_yaml(), OUSAST_ARTIFACT_URL=receiver.url, OUSAST_RUN="validation-46")
|
|
assert code == 0
|
|
assert len(receiver.posts) == 1
|
|
headers, body = receiver.posts[0]
|
|
assert headers["Content-Type"] == "application/x-tar"
|
|
assert headers["X-Ousast-Task"] == "stub"
|
|
assert headers["X-Ousast-Run"] == "validation-46"
|
|
with tarfile.open(fileobj=io.BytesIO(body), mode="r:") as archive:
|
|
names = sorted(archive.getnames())
|
|
summary = archive.extractfile("summary.json")
|
|
assert summary is not None
|
|
assert json.loads(summary.read())["status"] == "done"
|
|
assert names == ["facts.json", "summary.json"]
|
|
|
|
|
|
def test_run_header_is_absent_without_ousast_run(
|
|
env: dict[str, str], receiver: Receiver, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
code, _ = run_main(monkeypatch, tmp_path, files_workspace_yaml(), OUSAST_ARTIFACT_URL=receiver.url)
|
|
assert code == 0
|
|
assert "X-Ousast-Run" not in receiver.posts[0][0]
|
|
|
|
|
|
def test_delivery_failure_is_reported_as_failed_with_exit_2(env: dict[str, str], monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
broken = Receiver(500)
|
|
try:
|
|
code, output = run_main(monkeypatch, tmp_path, files_workspace_yaml(), OUSAST_ARTIFACT_URL=broken.url)
|
|
finally:
|
|
broken.stop()
|
|
assert code == EXIT_BY_STATUS["failed"] == 2
|
|
assert len(broken.posts) == 3, "three attempts"
|
|
summary = json.loads((output / "summary.json").read_text())
|
|
assert summary["status"] == "failed"
|
|
assert summary["reason"].startswith("artifact delivery failed: HTTP 500")
|
|
assert summary["units_done"] == 1 and summary["units_total"] == 1, "the task's other fields are kept"
|
|
|
|
|
|
def test_deliver_retries_on_connection_errors_then_raises() -> None:
|
|
closed = Receiver(200)
|
|
url = closed.url
|
|
closed.stop()
|
|
with pytest.raises(DeliveryError, match="3 attempts"):
|
|
deliver(url, b"x", {"Content-Type": "application/x-tar"}, backoff=0)
|
|
|
|
|
|
# --- exit codes (design: error handling) ------------------------------------------------------------------------------------
|
|
|
|
|
|
def test_a_crash_yields_failed_with_the_traceback(env: dict[str, str], monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
code, output = run_main(monkeypatch, tmp_path, files_workspace_yaml(), STUB_MODE="crash")
|
|
assert code == 2
|
|
summary = json.loads((output / "summary.json").read_text())
|
|
assert summary["status"] == "failed"
|
|
assert "Traceback" in summary["reason"] and "stub exploded" in summary["reason"]
|
|
assert (output / "facts.json").exists(), "artifacts written before the crash stay for resume"
|
|
|
|
|
|
@pytest.mark.parametrize(("mode", "code"), [("done", 0), ("failed", 2), ("unfinished", 3), ("nosummary", 2), ("exit", 2)])
|
|
def test_exit_code_follows_the_summary_status(
|
|
env: dict[str, str], monkeypatch: pytest.MonkeyPatch, tmp_path: Path, mode: str, code: int
|
|
) -> None:
|
|
got, output = run_main(monkeypatch, tmp_path, files_workspace_yaml(), STUB_MODE=mode)
|
|
assert got == code
|
|
summary = json.loads((output / "summary.json").read_text())
|
|
assert summary["status"] == (mode if mode in EXIT_BY_STATUS else "failed")
|
|
|
|
|
|
def test_an_unknown_task_module_is_failed(env: dict[str, str], monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
output, workspace = tmp_path / "out", tmp_path / "ws"
|
|
monkeypatch.setenv("AX_TASK_YAML", task_yaml(output, workspace, command=["no-such-task"]))
|
|
monkeypatch.setenv("AX_WORKSPACES_YAML", files_workspace_yaml())
|
|
assert runner.main() == 2
|
|
assert "no_such_task" in json.loads((output / "summary.json").read_text())["reason"]
|
|
|
|
|
|
# --- git workspaces from the case cache and by shallow clone ----------------------------------------------------------------
|
|
|
|
|
|
def git(*args: str, cwd: Path) -> str:
|
|
return subprocess.run(
|
|
["git", "-c", "user.name=t", "-c", "[email protected]", *args], cwd=cwd, capture_output=True, text=True, check=True
|
|
).stdout.strip()
|
|
|
|
|
|
@pytest.fixture
|
|
def upstream(tmp_path: Path) -> tuple[Path, str]:
|
|
"""A local repository with two commits; returns it and the first commit (the pin)."""
|
|
repo = tmp_path / "upstream"
|
|
repo.mkdir()
|
|
git("init", "-q", "-b", "main", cwd=repo)
|
|
(repo / "hello.py").write_text("print('v1')\n")
|
|
git("add", ".", cwd=repo)
|
|
git("commit", "-q", "-m", "v1", cwd=repo)
|
|
pin = git("rev-parse", "HEAD", cwd=repo)
|
|
(repo / "hello.py").write_text("print('v2')\n")
|
|
git("commit", "-q", "-am", "v2", cwd=repo)
|
|
return repo, pin
|
|
|
|
|
|
def git_workspace_yaml(repo_url: str, extra: str = "") -> str:
|
|
"""ax's Workspace as ax hands it over: no annotations, no commit (the pin travels in ``OUSAST_GIT_PINS``)."""
|
|
return (
|
|
"apiVersion: ax.io/v1alpha1\nkind: Workspace\nmetadata:\n name: case\nspec:\n git:\n"
|
|
f" - name: code\n repo: {repo_url}\n branch: main\n{extra}"
|
|
" files:\n - path: candidates.json\n content: '{\"sinks\": []}'\n"
|
|
)
|
|
|
|
|
|
def pins(pin: str, key: str = "case/code") -> str:
|
|
return json.dumps({key: pin})
|
|
|
|
|
|
def test_git_entry_is_exported_from_the_case_cache_at_the_pin(
|
|
env: dict[str, str], monkeypatch: pytest.MonkeyPatch, tmp_path: Path, upstream: tuple[Path, str]
|
|
) -> None:
|
|
repo, pin = upstream
|
|
url = "https://example.invalid/acme/Repo.git"
|
|
cache = tmp_path / "cache"
|
|
case = cache / "acme-case-sqli"
|
|
subprocess.run(["git", "clone", "-q", "--no-checkout", str(repo), str(case)], check=True)
|
|
git("remote", "set-url", "origin", url, cwd=case)
|
|
assert find_cached_checkout(cache, "https://example.invalid/acme/repo") == case
|
|
assert find_cached_checkout(cache, "https://example.invalid/other/repo.git") is None
|
|
code, output = run_main(monkeypatch, tmp_path, git_workspace_yaml(url), OUSAST_CASE_CACHE=str(cache), OUSAST_GIT_PINS=pins(pin))
|
|
assert code == 0, (output / "summary.json").read_text()
|
|
checkout = tmp_path / "ws" / "code"
|
|
assert (checkout / "hello.py").read_text() == "print('v1')\n", "the pinned commit, not the branch tip"
|
|
assert not (checkout / ".git").exists(), "an export, as evaluate.export makes"
|
|
facts = json.loads((output / "facts.json").read_text())
|
|
assert facts["workspace_files"] == ["candidates.json", "code/hello.py"]
|
|
|
|
|
|
def test_git_entry_without_a_cache_is_shallow_cloned_at_the_pin(
|
|
env: dict[str, str], monkeypatch: pytest.MonkeyPatch, tmp_path: Path, upstream: tuple[Path, str]
|
|
) -> None:
|
|
repo, pin = upstream
|
|
code, output = run_main(
|
|
monkeypatch, tmp_path, git_workspace_yaml(repo.as_uri()), OUSAST_CASE_CACHE=str(tmp_path / "no-cache"), OUSAST_GIT_PINS=pins(pin)
|
|
)
|
|
assert code == 0, (output / "summary.json").read_text()
|
|
checkout = tmp_path / "ws" / "code"
|
|
assert (checkout / "hello.py").read_text() == "print('v1')\n"
|
|
assert git("rev-parse", "HEAD", cwd=checkout) == pin
|
|
|
|
|
|
def test_a_materialised_repo_survives_a_resume(
|
|
env: dict[str, str], monkeypatch: pytest.MonkeyPatch, tmp_path: Path, upstream: tuple[Path, str]
|
|
) -> None:
|
|
repo, pin = upstream
|
|
checkout = tmp_path / "ws" / "code"
|
|
checkout.mkdir(parents=True)
|
|
(checkout / "hello.py").write_text("kept\n")
|
|
code, _ = run_main(monkeypatch, tmp_path, git_workspace_yaml(repo.as_uri()), OUSAST_GIT_PINS=pins(pin))
|
|
assert code == 0
|
|
assert (checkout / "hello.py").read_text() == "kept\n", "/workspace persists across suspend and resume; no re-clone"
|
|
|
|
|
|
# --- ax's GitRepo fields: dir, depth; the commit pin from OUSAST_GIT_PINS ------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("name", "repo", "dir_", "expected"),
|
|
[
|
|
("code", "https://github.com/chalk/chalk.git", None, "/ws/code"),
|
|
("repo", "https://github.com/chalk/chalk.git", None, "/ws/chalk"),
|
|
("origin", "[email protected]:chalk/chalk", None, "/ws/chalk"),
|
|
("code", "https://github.com/chalk/chalk.git", "src", "/ws/src"),
|
|
("code", "https://github.com/chalk/chalk.git", ".", "/ws"),
|
|
("code", "https://github.com/chalk/chalk.git", "/elsewhere", "/elsewhere"),
|
|
],
|
|
)
|
|
def test_clone_destination_follows_ax_setup(name: str, repo: str, dir_: str | None, expected: str) -> None:
|
|
"""ax internal/workspace/setup.go cloneDestination: dir wins; placeholder names derive from the URL."""
|
|
assert clone_destination(GitSource(name, repo, dir=dir_), Path("/ws")) == Path(expected)
|
|
|
|
|
|
def test_git_dir_and_depth_are_honoured(
|
|
env: dict[str, str], monkeypatch: pytest.MonkeyPatch, tmp_path: Path, upstream: tuple[Path, str]
|
|
) -> None:
|
|
repo, _ = upstream
|
|
code, output = run_main(monkeypatch, tmp_path, git_workspace_yaml(repo.as_uri(), " dir: src\n depth: 1\n"))
|
|
assert code == 0, (output / "summary.json").read_text()
|
|
checkout = tmp_path / "ws" / "src"
|
|
assert (checkout / "hello.py").read_text() == "print('v2')\n", "no pin: the branch tip"
|
|
assert git("rev-parse", "--is-shallow-repository", cwd=checkout) == "true", "depth 1 is a shallow fetch"
|
|
assert not (tmp_path / "ws" / "code").exists()
|
|
|
|
|
|
def test_a_pin_naming_no_bound_git_entry_fails_the_task(
|
|
env: dict[str, str], monkeypatch: pytest.MonkeyPatch, tmp_path: Path, upstream: tuple[Path, str]
|
|
) -> None:
|
|
repo, pin = upstream
|
|
code, output = run_main(monkeypatch, tmp_path, git_workspace_yaml(repo.as_uri()), OUSAST_GIT_PINS=pins(pin, "case/other"))
|
|
assert code == EXIT_BY_STATUS["failed"]
|
|
assert "case/other" in json.loads((output / "summary.json").read_text())["reason"]
|
|
|
|
|
|
def test_unreadable_pins_fail_the_task(env: dict[str, str], monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
code, output = run_main(monkeypatch, tmp_path, files_workspace_yaml(), OUSAST_GIT_PINS='{"case/code": "abc"}')
|
|
assert code == EXIT_BY_STATUS["failed"]
|
|
assert "OUSAST_GIT_PINS" in json.loads((output / "summary.json").read_text())["reason"]
|
|
|
|
|
|
def test_git_commits_annotation_round_trips_to_the_checkout(
|
|
env: dict[str, str], monkeypatch: pytest.MonkeyPatch, tmp_path: Path, upstream: tuple[Path, str]
|
|
) -> None:
|
|
"""Workspace annotation -> reconciler -> ``OUSAST_GIT_PINS`` in the Task env -> the runner checks out the pin."""
|
|
import yaml
|
|
|
|
from openultrasast.plane import reconciler
|
|
|
|
repo, pin = upstream
|
|
output, workspace = tmp_path / "out", tmp_path / "ws"
|
|
manifest = tmp_path / "run.yaml"
|
|
manifest.write_text(
|
|
"apiVersion: ax.io/v1alpha1\nkind: Workspace\nmetadata:\n name: case\n"
|
|
f" annotations: {{openultrasast.io/git-commits: 'code={pin}'}}\n"
|
|
f"spec:\n git: [{{name: code, repo: '{repo.as_uri()}', branch: main}}]\n---\n"
|
|
"apiVersion: ax.io/v1alpha1\nkind: Task\nmetadata: {name: stub}\n"
|
|
f"spec: {{command: [stub-task], workspaces: [{{name: case, path: '{workspace}'}}]}}\n---\n"
|
|
"apiVersion: openultrasast.io/v1alpha1\nkind: Run\nmetadata: {name: rt}\nspec: {tasks: [{name: stub, task: stub}]}\n"
|
|
)
|
|
run, manifests = reconciler.load_run(manifest)
|
|
task_doc, *rest = reconciler.render_task(run, run.tasks[0], manifests, tmp_path, "unused")
|
|
assert rest[0]["metadata"] == {"name": "case"}, "annotations are stripped before ax"
|
|
assert rest[0]["spec"]["git"] == [{"name": "code", "repo": repo.as_uri(), "branch": "main"}], "no commit for ax"
|
|
overrides = {"OUSAST_OUTPUT_DIR": str(output)}
|
|
task_doc["spec"]["env"] = [
|
|
{"name": e["name"], "value": overrides.get(e["name"], e["value"])}
|
|
for e in task_doc["spec"]["env"]
|
|
if e["name"] != "OUSAST_ARTIFACT_URL"
|
|
]
|
|
assert json.loads({e["name"]: e["value"] for e in task_doc["spec"]["env"]}["OUSAST_GIT_PINS"]) == {"case/code": pin}
|
|
monkeypatch.setenv("AX_TASK_YAML", yaml.safe_dump(task_doc))
|
|
monkeypatch.setenv("AX_WORKSPACES_YAML", yaml.safe_dump_all([d for d in rest if d["kind"] == "Workspace"]))
|
|
assert runner.main() == 0, (output / "summary.json").read_text()
|
|
checkout = workspace / "code"
|
|
assert git("rev-parse", "HEAD", cwd=checkout) == pin, "the pinned commit, not the branch tip"
|
|
assert (checkout / "hello.py").read_text() == "print('v1')\n"
|