diff --git a/ops/ax/smoke-run.sh b/ops/ax/smoke-run.sh new file mode 100755 index 0000000..60babd1 --- /dev/null +++ b/ops/ax/smoke-run.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Render the end-to-end smoke Run with the current runner digest and execute it through the reconciler on ax. +set -euo pipefail +export PATH="$HOME/go/bin:$HOME/.local/bin:$PATH" +SRC="${OUSAST_AX_SRC:-$HOME/.cache/ousast/ax-src}" +RUNNER_IMAGE="$(cat "$SRC/runner-image")"; export RUNNER_IMAGE +KIND_GW="$(docker network inspect kind --format '{{range .IPAM.Config}}{{if .Gateway}}{{.Gateway}} {{end}}{{end}}' | tr ' ' '\n' | grep -m1 '\.')" +export OUSAST_ARTIFACT_HOST="${OUSAST_ARTIFACT_HOST:-$KIND_GW}" # sandboxes reach the host via the kind gateway +envsubst < "$(dirname "$0")/smoke-run.yaml.tmpl" > "$SRC/smoke-run.yaml" +REPO="$(cd "$(dirname "$0")/../.." && pwd)" +"$REPO/.venv/bin/ousast" plane run "$SRC/smoke-run.yaml" "$@" +"$REPO/.venv/bin/ousast" plane status ax-e2e-smoke diff --git a/ops/ax/smoke-run.yaml.tmpl b/ops/ax/smoke-run.yaml.tmpl new file mode 100644 index 0000000..3ca87a0 --- /dev/null +++ b/ops/ax/smoke-run.yaml.tmpl @@ -0,0 +1,39 @@ +# End-to-end smoke of the plane on ax: `ousast plane run` submits one model-free repo-facts Task, resumes it, +# starts it through Substrate's router and receives its artifacts on the host. Rendered by ops/ax/smoke-run.sh. +apiVersion: ax.io/v1alpha1 +kind: Workspace +metadata: + name: ousast-e2e-ws + atespace: default +spec: + files: + - path: "app/views.py" + content: "from app.db import run_query\n\ndef handler(request):\n return run_query(request.args['q'])\n" + - path: "app/db.py" + content: "def run_query(sql):\n return cursor.execute(sql)\n" +--- +apiVersion: ax.io/v1alpha1 +kind: Task +metadata: + name: ousast-e2e-facts + atespace: default +spec: + image: "${RUNNER_IMAGE}" + command: ["repo-facts"] + resources: + requests: {cpu: "250m", memory: "512Mi"} + limits: {cpu: "1", memory: "1Gi"} + workspaces: + - name: ousast-e2e-ws + path: "/workspace/case" +--- +apiVersion: openultrasast.io/v1alpha1 +kind: Run +metadata: + name: ax-e2e-smoke +spec: + tasks: + - name: facts + task: ousast-e2e-facts + outputs: [facts.json, summary.json] + budget: {usd: 0, calls: 0} diff --git a/src/openultrasast/plane/reconciler.py b/src/openultrasast/plane/reconciler.py index bfd23d3..387e370 100644 --- a/src/openultrasast/plane/reconciler.py +++ b/src/openultrasast/plane/reconciler.py @@ -167,7 +167,7 @@ def render_task(run: Run, entry: RunTask, manifests: Manifests, base: Path, arti spec["workspaces"] = bindings if task.debug: spec["debug"] = True - return [_doc("Task", ax_name, spec, task.metadata.atespace), *docs] + return [*docs, _doc("Task", ax_name, spec, task.metadata.atespace)] # ax snapshots bound Workspaces at Task create class Ax: diff --git a/tests/test_plane_increment.py b/tests/test_plane_increment.py index a9b9b6e..95d6db9 100644 --- a/tests/test_plane_increment.py +++ b/tests/test_plane_increment.py @@ -73,8 +73,9 @@ def test_validation_run_resolves_and_renders_every_task(tmp_path: Path) -> None: assert not non_ax_fields(doc["spec"], AX_FIELDS[doc["kind"]], doc["kind"]) assert set(doc["metadata"]) <= {"name", "atespace"} assert len(doc["metadata"]["name"]) <= 63, doc["metadata"]["name"] - assert docs[0]["metadata"]["name"] not in ax_names - ax_names.add(docs[0]["metadata"]["name"]) + assert docs[-1]["kind"] == "Task" + assert docs[-1]["metadata"]["name"] not in ax_names + ax_names.add(docs[-1]["metadata"]["name"]) def test_inputs_carry_the_validation_set() -> None: diff --git a/tests/test_plane_reconciler.py b/tests/test_plane_reconciler.py index baa24c6..ced96e7 100644 --- a/tests/test_plane_reconciler.py +++ b/tests/test_plane_reconciler.py @@ -365,18 +365,20 @@ def test_chain_runs_in_order_with_inputs_env_and_model(fake: Fake, tmp_path: Pat assert env["OUSAST_MODEL"] == "deepseek-flash" assert json.loads(env["OUSAST_MODEL_PARAMS"]) == {"cache_hit_per_m": 0.07, "input_per_m": 0.27, "output_per_m": 1.1} kinds = [(d["kind"], d["metadata"]["name"]) for d in verify["docs"]] - assert kinds == [("Task", "chain-verify"), ("Model", "deepseek-flash"), ("Workspace", "case-pin"), ("Workspace", "chain-verify-inputs")] - inputs_ws = verify["docs"][3] + assert kinds == [("Model", "deepseek-flash"), ("Workspace", "case-pin"), ("Workspace", "chain-verify-inputs"), ("Task", "chain-verify")] + inputs_ws = next(d for d in verify["docs"] if d["metadata"]["name"] == "chain-verify-inputs") assert inputs_ws["spec"]["files"] == [{"path": "repo-facts/facts.json", "content": json.dumps({"a.py": {"functions": ["f"]}})}] - assert verify["docs"][0]["spec"]["workspaces"][-1] == {"name": "chain-verify-inputs", "path": "/workspace/.ousast-in/verify"} - assert "annotations" not in verify["docs"][0]["metadata"], "ax's ObjectMeta has no annotations" + assert verify["docs"][-1]["spec"]["workspaces"][-1] == {"name": "chain-verify-inputs", "path": "/workspace/.ousast-in/verify"} + assert "annotations" not in verify["docs"][-1]["metadata"], "ax's ObjectMeta has no annotations" assert json.loads(env["OUSAST_GIT_PINS"]) == {"case-pin/repo": PIN}, "the Workspace annotation reaches the task as env" - assert verify["docs"][2]["spec"]["git"] == [{"name": "repo", "repo": "https://example.invalid/repo.git", "dir": "src", "depth": 1}] - assert verify["docs"][2]["metadata"] == {"name": "case-pin", "atespace": "default"} + assert next(d for d in verify["docs"] if d["metadata"]["name"] == "case-pin")["spec"]["git"] == [ + {"name": "repo", "repo": "https://example.invalid/repo.git", "dir": "src", "depth": 1} + ] + assert next(d for d in verify["docs"] if d["metadata"]["name"] == "case-pin")["metadata"] == {"name": "case-pin", "atespace": "default"} assert "OUSAST_GIT_PINS" not in fake.applied("chain", "agree")["env"], "no bound workspace, no pins" facts = fake.applied("chain", "repo-facts") assert "OUSAST_MODEL" not in facts["env"] and "OUSAST_BUDGET_USD" not in facts["env"] - assert facts["docs"][0]["spec"]["image"] == "ousast-runner:dev" + assert facts["docs"][-1]["spec"]["image"] == "ousast-runner:dev" delivered = reconciler.run_dir("chain") / "verify" / "agreed.json" assert json.loads(delivered.read_text()) == {"agreed": ["a.py:f"]} assert state_of("chain")["tasks"]["verify"]["usd"] == 0.5 and state_of("chain")["tasks"]["verify"]["model"] == "deepseek-flash" @@ -505,7 +507,7 @@ def test_start_is_sent_once_after_resume_with_the_models_credential(fake: Fake, for s in starts: name = s["body"]["task"] assert accepted[name] < s["at"] < delivered[name.removeprefix("sig-")], f"{name}: resume, then start, then delivery" - verify_model = fake.applied("sig", "verify")["docs"][1] + verify_model = next(d for d in fake.applied("sig", "verify")["docs"] if d["kind"] == "Model") assert verify_model["spec"]["secretKey"] == {"name": "deepseek", "key": "DEEPSEEK_API_KEY"}, "the manifest names the key" assert "DEEPSEEK_API_KEY" not in fake.applied("sig", "verify")["env"], "the rendered Task carries no credential" rendered = str(reconciler.run_dir("sig") / "verify" / "task.yaml") @@ -696,6 +698,18 @@ def test_rendered_documents_carry_only_ax_fields(tmp_path: Path) -> None: assert "openultrasast.io/" not in yaml.safe_dump(rendered["metadata"]), where +def test_the_task_is_applied_after_everything_it_binds(tmp_path: Path) -> None: + """ax copies the bound Workspaces into the task at create time: a Task applied before its Workspace ran with + an empty AX_WORKSPACES_YAML on the live cluster (2026-09-29). The Task is the last document of each apply.""" + (tmp_path / "repo-facts").mkdir() + (tmp_path / "repo-facts" / "facts.json").write_text("{}", encoding="utf-8") + run, manifests = reconciler.load_run(write_run(tmp_path, "order")) + for entry in run.tasks: + docs = reconciler.render_task(run, entry, manifests, tmp_path, "http://h:1/") + assert [d["kind"] for d in docs].count("Task") == 1 + assert docs[-1]["kind"] == "Task", [d["kind"] for d in docs] + + def test_reconciler_is_under_500_lines_and_holds_no_pipeline_logic() -> None: text = RECONCILER.read_text(encoding="utf-8") assert len(text.splitlines()) < 500, "Req 3.4: the reconciler stays under 500 lines" diff --git a/tests/test_plane_runner.py b/tests/test_plane_runner.py index d9e7df4..ca4112d 100644 --- a/tests/test_plane_runner.py +++ b/tests/test_plane_runner.py @@ -584,7 +584,7 @@ def test_git_commits_annotation_round_trips_to_the_checkout( "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") + *rest, task_doc = 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)}