diff --git a/ops/ax/up.sh b/ops/ax/up.sh index 29fda05..ef41785 100755 --- a/ops/ax/up.sh +++ b/ops/ax/up.sh @@ -66,7 +66,7 @@ step "smoke task" ax delete task ousast-smoke >/dev/null 2>&1 || true envsubst < "$(dirname "$0")/smoke-task.yaml.tmpl" > "$SRC/smoke-task.yaml" ax apply -f "$SRC/smoke-task.yaml" -ax resume task ousast-smoke || true +for i in 1 2 3 4 5 6; do ax resume task ousast-smoke && break; sleep 20; done # first resume may time out while the golden snapshot builds ax get tasks step "footprint" diff --git a/src/openultrasast/plane/runner.py b/src/openultrasast/plane/runner.py index f3c47c5..f81eb8a 100644 --- a/src/openultrasast/plane/runner.py +++ b/src/openultrasast/plane/runner.py @@ -119,6 +119,21 @@ class DeliveryError(RuntimeError): # --- manifests --------------------------------------------------------------------------------------------------- +# Fields ax's API server sets on what it hands the runner (ax.proto: ObjectMeta.creation_timestamp, Task.status). +# They are not authoring fields, so they are dropped before the strict schema sees the document; any other +# unknown field still fails. +_SERVER_META = ("creationTimestamp", "creation_timestamp") + + +def _without_server_fields(document: object) -> object: + if not isinstance(document, dict): + return document + cleaned = {k: v for k, v in document.items() if k != "status"} + meta = cleaned.get("metadata") + if isinstance(meta, dict): + cleaned["metadata"] = {k: v for k, v in meta.items() if k not in _SERVER_META} + return cleaned + def load_task(text: str) -> Task: """Parse ``AX_TASK_YAML``; raise :class:`RunnerError` unless it is exactly one Task.""" @@ -129,7 +144,7 @@ def load_task(text: str) -> Task: if len(documents) != 1: raise RunnerError(f"AX_TASK_YAML must hold exactly one Task document, found {len(documents)}") try: - manifest = parse_manifest(documents[0], source="AX_TASK_YAML") + manifest = parse_manifest(_without_server_fields(documents[0]), source="AX_TASK_YAML") except ManifestError as exc: raise RunnerError(str(exc)) from exc if not isinstance(manifest, Task): @@ -146,7 +161,7 @@ def load_workspaces(text: str) -> dict[str, Workspace]: workspaces: dict[str, Workspace] = {} for index, document in enumerate(documents): try: - manifest = parse_manifest(document, source=f"AX_WORKSPACES_YAML[{index}]") + manifest = parse_manifest(_without_server_fields(document), source=f"AX_WORKSPACES_YAML[{index}]") except ManifestError as exc: raise RunnerError(str(exc)) from exc if not isinstance(manifest, Workspace): diff --git a/tests/test_plane_runner.py b/tests/test_plane_runner.py index 0e2b015..b4ca7cf 100644 --- a/tests/test_plane_runner.py +++ b/tests/test_plane_runner.py @@ -590,3 +590,22 @@ def test_git_commits_annotation_round_trips_to_the_checkout( 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" + + +def test_the_runner_accepts_what_ax_hands_it() -> None: + """ax's server adds metadata.creationTimestamp and status (live cluster, 2026-09-29); other fields stay strict.""" + from openultrasast.plane.runner import RunnerError, load_task, load_workspaces + + task = load_task( + "apiVersion: ax.io/v1alpha1\nkind: Task\nmetadata:\n name: t\n atespace: default\n" + " creationTimestamp: '2026-09-29T17:11:58Z'\nspec:\n command: [repo-facts]\n" + "status:\n phase: Running\n id: task-t-1\n" + ) + assert task.metadata.name == "t" + spaces = load_workspaces( + "apiVersion: ax.io/v1alpha1\nkind: Workspace\nmetadata:\n name: w\n" + " creationTimestamp: '2026-09-29T17:11:58Z'\nspec:\n files:\n - path: a.py\n content: x\n" + ) + assert list(spaces) == ["w"] + with pytest.raises(RunnerError, match="uid"): + load_task("apiVersion: ax.io/v1alpha1\nkind: Task\nmetadata:\n name: t\n uid: x\nspec: {}\n")