fix(plane): apply a Task after the Workspaces it binds; end-to-end smoke Run

On the live cluster ax created the Task before its Workspace in the same apply
and the runner saw an empty AX_WORKSPACES_YAML. Tests now find rendered
documents by kind and name, not position. ops/ax/smoke-run.sh renders a
model-free repo-facts Run with the current runner digest and executes it
through the reconciler.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-29 20:26:30 +02:00
co-authored by Claude Opus 5.5
parent 14ea48b1ea
commit 30e4778b11
6 changed files with 78 additions and 12 deletions
+12
View File
@@ -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
+39
View File
@@ -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}
+1 -1
View File
@@ -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:
+3 -2
View File
@@ -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:
+22 -8
View File
@@ -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"
+1 -1
View File
@@ -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)}