From a21e45d535c6eb3f2797404c79e8a3abc3695494 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Fri, 4 Sep 2026 11:54:18 +0000 Subject: [PATCH 1/6] Add pydabs-acceptance-test skill for authoring resource acceptance tests An AI Agent Skill that guides an agent to author the acceptance test for a newly-onboarded PyDABs resource: the acceptance/bundle/python/-support/ fixture (databricks.yml + resources.py + mutators.py + script + test.toml + generated output.txt). Includes fill-in templates (.tmpl so they stay out of linters). Complements the schema-synthesized unit-test generation; realistic field values are adapted from the resource's invariant config. Co-authored-by: Isaac --- .../skills/pydabs-acceptance-test/SKILL.md | 137 ++++++++++++++++++ .../templates/databricks.yml.tmpl | 15 ++ .../templates/mutators.py.tmpl | 11 ++ .../templates/resources.py.tmpl | 14 ++ .../templates/script.tmpl | 5 + .../templates/test.toml.tmpl | 7 + 6 files changed, 189 insertions(+) create mode 100644 .agents/skills/pydabs-acceptance-test/SKILL.md create mode 100644 .agents/skills/pydabs-acceptance-test/templates/databricks.yml.tmpl create mode 100644 .agents/skills/pydabs-acceptance-test/templates/mutators.py.tmpl create mode 100644 .agents/skills/pydabs-acceptance-test/templates/resources.py.tmpl create mode 100644 .agents/skills/pydabs-acceptance-test/templates/script.tmpl create mode 100644 .agents/skills/pydabs-acceptance-test/templates/test.toml.tmpl diff --git a/.agents/skills/pydabs-acceptance-test/SKILL.md b/.agents/skills/pydabs-acceptance-test/SKILL.md new file mode 100644 index 00000000000..466177d0856 --- /dev/null +++ b/.agents/skills/pydabs-acceptance-test/SKILL.md @@ -0,0 +1,137 @@ +--- +name: pydabs-acceptance-test +description: "Author the acceptance test for a PyDABs (Python DABs) resource. Use when onboarding a new PyDABs resource, when the test_python_support_coverage guard fails for a resource, or when the user says 'add a pydabs acceptance test', 'write the acceptance test for ', or 'the -support test is missing'. Produces the acceptance/bundle/python/-support/ fixture (databricks.yml + resources.py + mutators.py + script + test.toml + generated output.txt)." +user-invocable: true +allowed-tools: Read, Edit, Write, Bash, Glob, Grep, AskUserQuestion +--- + +# Author a PyDABs resource acceptance test + +PyDABs acceptance tests are hand-written, one fixture per resource under +`acceptance/bundle/python/-support/`. The OpenAPI spec has no examples, so +the realistic field values come from the resource's invariant config and your own +judgement — not from a generator. This skill guides you through authoring that +fixture deterministically and verifying it. + +The coverage guard `test_python_support_coverage` +(`python/databricks_tests/core/test_python_support.py`) fails CI for any PyDABs +resource that lacks a `-support` fixture, so every newly-onboarded resource +must get one. This skill is how you close that gap. + +Worked reference: `acceptance/bundle/python/alerts-support/` (a resource with required +nested fields) and `acceptance/bundle/python/catalogs-support/` (a direct-engine-only +resource). Read both before starting — the fixture you write mirrors them. + +## Input + +The resource to cover, as its **plural** name (the `resources:` key in +`databricks.yml`, e.g. `alerts`, `volumes`). If given a singular or a Go/Python +type name, resolve it to the plural first (step 1). + +## Step 1 — Verify the resource is wired in PyDABs + +The fixture cannot work unless the resource's Python surface exists. Confirm all of: + +- The package `python/databricks/bundles//` exists and has a `_models/` + subdirectory (this is what marks it a generated resource package). +- `add_` is a method on `Resources` and `_mutator` is exported + from `databricks.bundles.core`: + + ```sh + grep -rn "def add_\|_mutator" python/databricks/bundles/core/ + ``` + +If any is missing, the resource is not wired yet — stop and onboard it in PyDABs +first (that is a separate task). Note the exact `` and `` names +(the dataclass, e.g. `Alert`) from `python/databricks/bundles//__init__.py`; +you need them for `resources.py` and `mutators.py`. + +## Step 2 — Find the resource's required fields + +The generated dataclass is the source of truth. In +`python/databricks/bundles//_models/.py`, required fields are typed +`VariableOr[...]` (no default); optional fields are `VariableOrOptional[...] = None`. +You must set every required field, including required fields of required nested +objects (recurse into their `_models` files). Optional fields are usually omitted. + +```sh +grep -nE "VariableOr\[|VariableOrOptional\[" python/databricks/bundles//_models/.py +``` + +## Step 3 — Get realistic values (adapt, don't copy) + +The invariant config `acceptance/bundle/invariant/configs/.yml.tmpl` shows +realistic values for the same resource. **Adapt** it — do not copy verbatim: + +- Replace `$UNIQUE_NAME`, `$TEST_DEFAULT_WAREHOUSE_ID`, and every other `$VAR` + interpolation with plain string literals. This test runs locally with no cloud and + no variable substitution. +- Drop cloud-only / IAM blocks the invariant config carries for its real-workspace + run: `permissions`, `grants`, `file_path` pointing at an external asset, etc. Keep + the fixture to the resource's own fields so `bundle validate` is deterministic. + +If no invariant config exists, invent plausible literals that satisfy the field types +(a display name string, an enum's first member, a cron string, etc.). + +## Step 4 — Write the six fixture files + +Copy each `templates/*.tmpl` into `acceptance/bundle/python/-support/`, +dropping the `.tmpl` suffix (e.g. `resources.py.tmpl` → `resources.py`), and fill +them in. The `.tmpl` suffix keeps the placeholder files out of the repo's linters; +the copied fixture files are real Python/YAML/TOML and must pass lint (step 7). +Placeholders: `PLURAL`, `SINGULAR`, `CLASS`, `NAME` (a short stem, e.g. `alert`), +`FIELD` (a required **string** field to mutate). + +1. **`databricks.yml`** — `bundle.name: my_project`, `sync: {paths: []}`, a top-level + `python:` block wiring `resources:load_resources` and `mutators:update_`, + and one YAML-declared resource `resources..my__1` with all required + fields. (`bundle validate` normalizes the `python:` key to `experimental.python` + in the output — that is expected, don't fight it.) +2. **`resources.py`** — `load_resources()` adds a second instance `my__2` via + `resources.add_(...)`, same required fields, slightly different values. +3. **`mutators.py`** — a `@_mutator` that `assert isinstance(...)` on a + required string field and `replace(...)`s it to append `" (updated)"`. The mutator + runs on **both** instances, so the golden shows the transform applied to each. +4. **`script`** — copy `templates/script` verbatim (`bundle validate --output json` + piped through `jq "pick(.experimental.python, .resources)"`). +5. **`test.toml`** — `Cloud = false`. Add `EnvMatrix.PYDAB_VERSION = ["current"]` for + a brand-new resource (it only exists in the current wheel, not the pinned older + one). Add `EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]` only for a direct-only + resource — terraform is deprecated, so never add a `["terraform", "direct"]` + matrix. When unsure, copy the engine convention from the newest existing fixture + (`catalogs-support`), not an old one. +6. **`output.txt`** — do NOT hand-write; generate it in step 5. + +## Step 5 — Generate the golden output + +```sh +go test ./acceptance -run 'TestAccept/bundle/python/-support' -update +``` + +(or `./task test-update` to regenerate the whole suite). This writes `output.txt`. +Inspect it: both `my__1` and `my__2` must appear with the mutated field +showing `" (updated)"`, and only `experimental.python` + `resources` keys are present. + +## Step 6 — Verify it reproduces deterministically + +Re-run **without** `-update`. It must pass against the golden you just generated: + +```sh +go test ./acceptance -run 'TestAccept/bundle/python/-support' -count=1 +``` + +A test that only passes with `-update` is nondeterministic — investigate before +finishing (a `$VAR` left in a value, a non-deterministic field, an EnvMatrix variant +producing different output). Never stop at "golden written". + +## Step 7 — Confirm coverage and format + +```sh +(cd python && uv run --python 3.11 pytest databricks_tests/core/test_python_support.py) +./task fmt && ./task lint-q +``` + +`test_python_support_coverage` should now be green for this resource. If the resource +was previously in the `_LACKING` allowlist +(`python/databricks_tests/core/test_python_support.py`), remove its entry — the list +only shrinks. diff --git a/.agents/skills/pydabs-acceptance-test/templates/databricks.yml.tmpl b/.agents/skills/pydabs-acceptance-test/templates/databricks.yml.tmpl new file mode 100644 index 00000000000..18f303dd816 --- /dev/null +++ b/.agents/skills/pydabs-acceptance-test/templates/databricks.yml.tmpl @@ -0,0 +1,15 @@ +bundle: + name: my_project + +sync: {paths: []} # don't need to copy files + +python: + resources: + - "resources:load_resources" + mutators: + - "mutators:update_SINGULAR" + +resources: + PLURAL: + my_NAME_1: + # required fields, realistic literal values (see SKILL.md steps 2-3) diff --git a/.agents/skills/pydabs-acceptance-test/templates/mutators.py.tmpl b/.agents/skills/pydabs-acceptance-test/templates/mutators.py.tmpl new file mode 100644 index 00000000000..4a2bfb94d89 --- /dev/null +++ b/.agents/skills/pydabs-acceptance-test/templates/mutators.py.tmpl @@ -0,0 +1,11 @@ +from dataclasses import replace + +from databricks.bundles.PLURAL import CLASS +from databricks.bundles.core import SINGULAR_mutator + + +@SINGULAR_mutator +def update_SINGULAR(SINGULAR: CLASS) -> CLASS: + assert isinstance(SINGULAR.FIELD, str) + + return replace(SINGULAR, FIELD=f"{SINGULAR.FIELD} (updated)") diff --git a/.agents/skills/pydabs-acceptance-test/templates/resources.py.tmpl b/.agents/skills/pydabs-acceptance-test/templates/resources.py.tmpl new file mode 100644 index 00000000000..9360bec828a --- /dev/null +++ b/.agents/skills/pydabs-acceptance-test/templates/resources.py.tmpl @@ -0,0 +1,14 @@ +from databricks.bundles.core import Resources + + +def load_resources() -> Resources: + resources = Resources() + + resources.add_SINGULAR( + "my_NAME_2", + { + # same required fields as _1, slightly different values + }, + ) + + return resources diff --git a/.agents/skills/pydabs-acceptance-test/templates/script.tmpl b/.agents/skills/pydabs-acceptance-test/templates/script.tmpl new file mode 100644 index 00000000000..e273fb45a53 --- /dev/null +++ b/.agents/skills/pydabs-acceptance-test/templates/script.tmpl @@ -0,0 +1,5 @@ + +trace uv run $UV_ARGS -q $CLI bundle validate --output json | \ + jq "pick(.experimental.python, .resources)" + +rm -fr .databricks __pycache__ diff --git a/.agents/skills/pydabs-acceptance-test/templates/test.toml.tmpl b/.agents/skills/pydabs-acceptance-test/templates/test.toml.tmpl new file mode 100644 index 00000000000..4935b9b020a --- /dev/null +++ b/.agents/skills/pydabs-acceptance-test/templates/test.toml.tmpl @@ -0,0 +1,7 @@ +Cloud = false # tests don't interact with APIs + +# new resource, only in the current wheel: +# EnvMatrix.PYDAB_VERSION = ["current"] + +# direct-only resource (terraform is deprecated): +# EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] From 687cece88f817d5ce4f17c853dbfba4aa089f6e0 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Fri, 4 Sep 2026 11:54:19 +0000 Subject: [PATCH 2/6] Assert every PyDABs resource has an acceptance test test_python_support_coverage fails until each resource in the _ResourceType registry has an acceptance/bundle/python/-support/ fixture, so coverage cannot silently regress as resources are onboarded. Mirrors the invariant-config coverage guard; shrink-only _LACKING allowlist ({jobs}, whose coverage predates the convention). Lives in the python test suite (runs in CI via pydabs-test) since it checks the filesystem rather than exercising the CLI. Co-authored-by: Isaac --- .../core/test_python_support.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 python/databricks_tests/core/test_python_support.py diff --git a/python/databricks_tests/core/test_python_support.py b/python/databricks_tests/core/test_python_support.py new file mode 100644 index 00000000000..ef8a113d56b --- /dev/null +++ b/python/databricks_tests/core/test_python_support.py @@ -0,0 +1,36 @@ +"""Coverage guard: every PyDABs resource must have an acceptance fixture. + +Asserts each resource in the _ResourceType registry has an +acceptance/bundle/python/-support/ fixture. New resources get one via the +pydabs-acceptance-test skill; this fails CI until it exists. +""" + +from pathlib import Path + +import pytest + +from databricks.bundles.core._resource_type import _ResourceType + +_ACCEPTANCE_DIR = Path(__file__).parents[3] / "acceptance" / "bundle" / "python" + +# Resources knowingly lacking a -support fixture. Shrink-only: the test fails +# if an entry here is actually covered, so gaps can only close. +_LACKING = { + # jobs predates the -support convention; covered across the suite instead. + "jobs", +} + +_PLURALS = sorted(t.plural_name for t in _ResourceType.all()) + + +@pytest.mark.parametrize("plural", _PLURALS) +def test_python_support_coverage(plural: str): + covered = (_ACCEPTANCE_DIR / f"{plural}-support" / "databricks.yml").exists() + + if plural in _LACKING: + assert not covered, f"{plural!r} now has a fixture; remove it from _LACKING" + else: + assert covered, ( + f"no acceptance/bundle/python/{plural}-support/ fixture for {plural!r}; " + "author one with the pydabs-acceptance-test skill or add it to _LACKING" + ) From 8620d03060f238d7bb43d74826fd38db7692b3c3 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Fri, 4 Sep 2026 13:00:03 +0000 Subject: [PATCH 3/6] Replace acceptance-test skill with an auto-loaded rule + README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review feedback (skills aren't reliably loaded, and the examples plus a verbose failure are enough): drop the pydabs-acceptance-test skill in favor of the repo's dresources pattern — a path-scoped .agents/rules/ file (auto-loaded when working under acceptance/bundle/python/**) pointing to a concise acceptance/bundle/python/README.md that leans on the existing fixtures. Retarget the coverage guard's message at the README. Co-authored-by: Isaac --- .agents/rules/pydabs-acceptance-tests.md | 8 + .../skills/pydabs-acceptance-test/SKILL.md | 137 ------------------ .../templates/databricks.yml.tmpl | 15 -- .../templates/mutators.py.tmpl | 11 -- .../templates/resources.py.tmpl | 14 -- .../templates/script.tmpl | 5 - .../templates/test.toml.tmpl | 7 - acceptance/bundle/python/README.md | 48 ++++++ .../core/test_python_support.py | 6 +- 9 files changed, 59 insertions(+), 192 deletions(-) create mode 100644 .agents/rules/pydabs-acceptance-tests.md delete mode 100644 .agents/skills/pydabs-acceptance-test/SKILL.md delete mode 100644 .agents/skills/pydabs-acceptance-test/templates/databricks.yml.tmpl delete mode 100644 .agents/skills/pydabs-acceptance-test/templates/mutators.py.tmpl delete mode 100644 .agents/skills/pydabs-acceptance-test/templates/resources.py.tmpl delete mode 100644 .agents/skills/pydabs-acceptance-test/templates/script.tmpl delete mode 100644 .agents/skills/pydabs-acceptance-test/templates/test.toml.tmpl create mode 100644 acceptance/bundle/python/README.md diff --git a/.agents/rules/pydabs-acceptance-tests.md b/.agents/rules/pydabs-acceptance-tests.md new file mode 100644 index 00000000000..924540845e7 --- /dev/null +++ b/.agents/rules/pydabs-acceptance-tests.md @@ -0,0 +1,8 @@ +--- +description: Rules for authoring PyDABs resource acceptance tests +globs: acceptance/bundle/python/** +paths: + - "acceptance/bundle/python/**" +--- + +**RULE: Before adding a PyDABs resource acceptance test, read `acceptance/bundle/python/README.md`.** It covers the `-support/` fixture layout, how to source and adapt realistic field values, the version/engine `test.toml` knobs, and the determinism re-run. Every PyDABs resource needs one (enforced by `test_python_support_coverage`). diff --git a/.agents/skills/pydabs-acceptance-test/SKILL.md b/.agents/skills/pydabs-acceptance-test/SKILL.md deleted file mode 100644 index 466177d0856..00000000000 --- a/.agents/skills/pydabs-acceptance-test/SKILL.md +++ /dev/null @@ -1,137 +0,0 @@ ---- -name: pydabs-acceptance-test -description: "Author the acceptance test for a PyDABs (Python DABs) resource. Use when onboarding a new PyDABs resource, when the test_python_support_coverage guard fails for a resource, or when the user says 'add a pydabs acceptance test', 'write the acceptance test for ', or 'the -support test is missing'. Produces the acceptance/bundle/python/-support/ fixture (databricks.yml + resources.py + mutators.py + script + test.toml + generated output.txt)." -user-invocable: true -allowed-tools: Read, Edit, Write, Bash, Glob, Grep, AskUserQuestion ---- - -# Author a PyDABs resource acceptance test - -PyDABs acceptance tests are hand-written, one fixture per resource under -`acceptance/bundle/python/-support/`. The OpenAPI spec has no examples, so -the realistic field values come from the resource's invariant config and your own -judgement — not from a generator. This skill guides you through authoring that -fixture deterministically and verifying it. - -The coverage guard `test_python_support_coverage` -(`python/databricks_tests/core/test_python_support.py`) fails CI for any PyDABs -resource that lacks a `-support` fixture, so every newly-onboarded resource -must get one. This skill is how you close that gap. - -Worked reference: `acceptance/bundle/python/alerts-support/` (a resource with required -nested fields) and `acceptance/bundle/python/catalogs-support/` (a direct-engine-only -resource). Read both before starting — the fixture you write mirrors them. - -## Input - -The resource to cover, as its **plural** name (the `resources:` key in -`databricks.yml`, e.g. `alerts`, `volumes`). If given a singular or a Go/Python -type name, resolve it to the plural first (step 1). - -## Step 1 — Verify the resource is wired in PyDABs - -The fixture cannot work unless the resource's Python surface exists. Confirm all of: - -- The package `python/databricks/bundles//` exists and has a `_models/` - subdirectory (this is what marks it a generated resource package). -- `add_` is a method on `Resources` and `_mutator` is exported - from `databricks.bundles.core`: - - ```sh - grep -rn "def add_\|_mutator" python/databricks/bundles/core/ - ``` - -If any is missing, the resource is not wired yet — stop and onboard it in PyDABs -first (that is a separate task). Note the exact `` and `` names -(the dataclass, e.g. `Alert`) from `python/databricks/bundles//__init__.py`; -you need them for `resources.py` and `mutators.py`. - -## Step 2 — Find the resource's required fields - -The generated dataclass is the source of truth. In -`python/databricks/bundles//_models/.py`, required fields are typed -`VariableOr[...]` (no default); optional fields are `VariableOrOptional[...] = None`. -You must set every required field, including required fields of required nested -objects (recurse into their `_models` files). Optional fields are usually omitted. - -```sh -grep -nE "VariableOr\[|VariableOrOptional\[" python/databricks/bundles//_models/.py -``` - -## Step 3 — Get realistic values (adapt, don't copy) - -The invariant config `acceptance/bundle/invariant/configs/.yml.tmpl` shows -realistic values for the same resource. **Adapt** it — do not copy verbatim: - -- Replace `$UNIQUE_NAME`, `$TEST_DEFAULT_WAREHOUSE_ID`, and every other `$VAR` - interpolation with plain string literals. This test runs locally with no cloud and - no variable substitution. -- Drop cloud-only / IAM blocks the invariant config carries for its real-workspace - run: `permissions`, `grants`, `file_path` pointing at an external asset, etc. Keep - the fixture to the resource's own fields so `bundle validate` is deterministic. - -If no invariant config exists, invent plausible literals that satisfy the field types -(a display name string, an enum's first member, a cron string, etc.). - -## Step 4 — Write the six fixture files - -Copy each `templates/*.tmpl` into `acceptance/bundle/python/-support/`, -dropping the `.tmpl` suffix (e.g. `resources.py.tmpl` → `resources.py`), and fill -them in. The `.tmpl` suffix keeps the placeholder files out of the repo's linters; -the copied fixture files are real Python/YAML/TOML and must pass lint (step 7). -Placeholders: `PLURAL`, `SINGULAR`, `CLASS`, `NAME` (a short stem, e.g. `alert`), -`FIELD` (a required **string** field to mutate). - -1. **`databricks.yml`** — `bundle.name: my_project`, `sync: {paths: []}`, a top-level - `python:` block wiring `resources:load_resources` and `mutators:update_`, - and one YAML-declared resource `resources..my__1` with all required - fields. (`bundle validate` normalizes the `python:` key to `experimental.python` - in the output — that is expected, don't fight it.) -2. **`resources.py`** — `load_resources()` adds a second instance `my__2` via - `resources.add_(...)`, same required fields, slightly different values. -3. **`mutators.py`** — a `@_mutator` that `assert isinstance(...)` on a - required string field and `replace(...)`s it to append `" (updated)"`. The mutator - runs on **both** instances, so the golden shows the transform applied to each. -4. **`script`** — copy `templates/script` verbatim (`bundle validate --output json` - piped through `jq "pick(.experimental.python, .resources)"`). -5. **`test.toml`** — `Cloud = false`. Add `EnvMatrix.PYDAB_VERSION = ["current"]` for - a brand-new resource (it only exists in the current wheel, not the pinned older - one). Add `EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]` only for a direct-only - resource — terraform is deprecated, so never add a `["terraform", "direct"]` - matrix. When unsure, copy the engine convention from the newest existing fixture - (`catalogs-support`), not an old one. -6. **`output.txt`** — do NOT hand-write; generate it in step 5. - -## Step 5 — Generate the golden output - -```sh -go test ./acceptance -run 'TestAccept/bundle/python/-support' -update -``` - -(or `./task test-update` to regenerate the whole suite). This writes `output.txt`. -Inspect it: both `my__1` and `my__2` must appear with the mutated field -showing `" (updated)"`, and only `experimental.python` + `resources` keys are present. - -## Step 6 — Verify it reproduces deterministically - -Re-run **without** `-update`. It must pass against the golden you just generated: - -```sh -go test ./acceptance -run 'TestAccept/bundle/python/-support' -count=1 -``` - -A test that only passes with `-update` is nondeterministic — investigate before -finishing (a `$VAR` left in a value, a non-deterministic field, an EnvMatrix variant -producing different output). Never stop at "golden written". - -## Step 7 — Confirm coverage and format - -```sh -(cd python && uv run --python 3.11 pytest databricks_tests/core/test_python_support.py) -./task fmt && ./task lint-q -``` - -`test_python_support_coverage` should now be green for this resource. If the resource -was previously in the `_LACKING` allowlist -(`python/databricks_tests/core/test_python_support.py`), remove its entry — the list -only shrinks. diff --git a/.agents/skills/pydabs-acceptance-test/templates/databricks.yml.tmpl b/.agents/skills/pydabs-acceptance-test/templates/databricks.yml.tmpl deleted file mode 100644 index 18f303dd816..00000000000 --- a/.agents/skills/pydabs-acceptance-test/templates/databricks.yml.tmpl +++ /dev/null @@ -1,15 +0,0 @@ -bundle: - name: my_project - -sync: {paths: []} # don't need to copy files - -python: - resources: - - "resources:load_resources" - mutators: - - "mutators:update_SINGULAR" - -resources: - PLURAL: - my_NAME_1: - # required fields, realistic literal values (see SKILL.md steps 2-3) diff --git a/.agents/skills/pydabs-acceptance-test/templates/mutators.py.tmpl b/.agents/skills/pydabs-acceptance-test/templates/mutators.py.tmpl deleted file mode 100644 index 4a2bfb94d89..00000000000 --- a/.agents/skills/pydabs-acceptance-test/templates/mutators.py.tmpl +++ /dev/null @@ -1,11 +0,0 @@ -from dataclasses import replace - -from databricks.bundles.PLURAL import CLASS -from databricks.bundles.core import SINGULAR_mutator - - -@SINGULAR_mutator -def update_SINGULAR(SINGULAR: CLASS) -> CLASS: - assert isinstance(SINGULAR.FIELD, str) - - return replace(SINGULAR, FIELD=f"{SINGULAR.FIELD} (updated)") diff --git a/.agents/skills/pydabs-acceptance-test/templates/resources.py.tmpl b/.agents/skills/pydabs-acceptance-test/templates/resources.py.tmpl deleted file mode 100644 index 9360bec828a..00000000000 --- a/.agents/skills/pydabs-acceptance-test/templates/resources.py.tmpl +++ /dev/null @@ -1,14 +0,0 @@ -from databricks.bundles.core import Resources - - -def load_resources() -> Resources: - resources = Resources() - - resources.add_SINGULAR( - "my_NAME_2", - { - # same required fields as _1, slightly different values - }, - ) - - return resources diff --git a/.agents/skills/pydabs-acceptance-test/templates/script.tmpl b/.agents/skills/pydabs-acceptance-test/templates/script.tmpl deleted file mode 100644 index e273fb45a53..00000000000 --- a/.agents/skills/pydabs-acceptance-test/templates/script.tmpl +++ /dev/null @@ -1,5 +0,0 @@ - -trace uv run $UV_ARGS -q $CLI bundle validate --output json | \ - jq "pick(.experimental.python, .resources)" - -rm -fr .databricks __pycache__ diff --git a/.agents/skills/pydabs-acceptance-test/templates/test.toml.tmpl b/.agents/skills/pydabs-acceptance-test/templates/test.toml.tmpl deleted file mode 100644 index 4935b9b020a..00000000000 --- a/.agents/skills/pydabs-acceptance-test/templates/test.toml.tmpl +++ /dev/null @@ -1,7 +0,0 @@ -Cloud = false # tests don't interact with APIs - -# new resource, only in the current wheel: -# EnvMatrix.PYDAB_VERSION = ["current"] - -# direct-only resource (terraform is deprecated): -# EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/python/README.md b/acceptance/bundle/python/README.md new file mode 100644 index 00000000000..942af63a6aa --- /dev/null +++ b/acceptance/bundle/python/README.md @@ -0,0 +1,48 @@ +# PyDABs resource acceptance tests + +Each `-support/` directory is the acceptance test for one PyDABs resource. It +checks that the resource loads both from YAML and from Python and that a mutator runs +over it. `test_python_support_coverage` +(`python/databricks_tests/core/test_python_support.py`) requires every PyDABs resource +to have one, so a newly-onboarded resource needs a fixture here. + +Copy an existing one — `alerts-support/` (a resource with required nested fields) or +`catalogs-support/` (direct-engine only) are the canonical examples. A fixture is six +files: + +- `databricks.yml` — `bundle.name: my_project`, `sync: {paths: []}`, a top-level + `python:` block wiring `resources:load_resources` + `mutators:update_`, and + one YAML-declared instance `.my__1`. +- `resources.py` — `load_resources()` adds a second instance `my__2` via + `resources.add_(...)`. +- `mutators.py` — a `@_mutator` that appends `" (updated)"` to a required + string field; it runs over both instances. +- `script` — copy it verbatim (`bundle validate --output json | jq "pick(...)"`). +- `test.toml` — `Cloud = false`. +- `output.txt` — generated, never hand-written. + +## Authoring a new one + +1. Confirm the resource is wired: `python/databricks/bundles//` exists, and + `add_` / `_mutator` are in `databricks.bundles.core`. If not, it + must be onboarded in PyDABs first. +2. Required fields are the `VariableOr[...]` (no default) fields in + `python/databricks/bundles//_models/.py`; set all of them, + recursing into required nested objects. `VariableOrOptional[...] = None` fields are + optional — omit them. +3. Get realistic values from `acceptance/bundle/invariant/configs/.yml.tmpl`, + but **adapt**: replace `$UNIQUE_NAME` / `$TEST_DEFAULT_WAREHOUSE_ID` and other `$VAR`s + with plain literals, and drop cloud-only blocks (`permissions`, `grants`, + `file_path`) — this test is local and deterministic. +4. `test.toml`: add `EnvMatrix.PYDAB_VERSION = ["current"]` for a brand-new resource + (it only exists in the current wheel), and `EnvMatrix.DATABRICKS_BUNDLE_ENGINE = + ["direct"]` for a direct-only resource (terraform is deprecated — never a + `["terraform", "direct"]` matrix). Match the newest fixture when unsure. +5. Generate the golden: + `go test ./acceptance -run 'TestAccept/bundle/python/-support' -update`. +6. **Re-run without `-update`** — it must pass against the golden you just generated. A + test that only passes with `-update` is nondeterministic (usually a `$VAR` or a + volatile field left in); fix it before finishing. + +Note: `bundle validate` normalizes the `python:` key to `experimental.python` in the +output — that's expected. diff --git a/python/databricks_tests/core/test_python_support.py b/python/databricks_tests/core/test_python_support.py index ef8a113d56b..41a68d86943 100644 --- a/python/databricks_tests/core/test_python_support.py +++ b/python/databricks_tests/core/test_python_support.py @@ -1,8 +1,8 @@ """Coverage guard: every PyDABs resource must have an acceptance fixture. Asserts each resource in the _ResourceType registry has an -acceptance/bundle/python/-support/ fixture. New resources get one via the -pydabs-acceptance-test skill; this fails CI until it exists. +acceptance/bundle/python/-support/ fixture (see that directory's README.md for +how to author one); this fails CI until it exists. """ from pathlib import Path @@ -32,5 +32,5 @@ def test_python_support_coverage(plural: str): else: assert covered, ( f"no acceptance/bundle/python/{plural}-support/ fixture for {plural!r}; " - "author one with the pydabs-acceptance-test skill or add it to _LACKING" + "add one (see acceptance/bundle/python/README.md) or add it to _LACKING" ) From 1df03bba2fa6f628f8623ad9c4d8152c062d97ee Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Fri, 4 Sep 2026 13:04:40 +0000 Subject: [PATCH 4/6] update skill --- acceptance/bundle/python/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/acceptance/bundle/python/README.md b/acceptance/bundle/python/README.md index 942af63a6aa..ba49b04fb3e 100644 --- a/acceptance/bundle/python/README.md +++ b/acceptance/bundle/python/README.md @@ -36,8 +36,7 @@ files: `file_path`) — this test is local and deterministic. 4. `test.toml`: add `EnvMatrix.PYDAB_VERSION = ["current"]` for a brand-new resource (it only exists in the current wheel), and `EnvMatrix.DATABRICKS_BUNDLE_ENGINE = - ["direct"]` for a direct-only resource (terraform is deprecated — never a - `["terraform", "direct"]` matrix). Match the newest fixture when unsure. + ["direct"]` for a direct-only resource. 5. Generate the golden: `go test ./acceptance -run 'TestAccept/bundle/python/-support' -update`. 6. **Re-run without `-update`** — it must pass against the golden you just generated. A From 81d29dea7881b95719af4f36ec6c85267dbf2398 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Fri, 4 Sep 2026 13:17:18 +0000 Subject: [PATCH 5/6] Check that .cursor/rules mirror .agents/rules Add tools/validate_cursor_rules.py (wired into `task checks`) so a rule under .agents/rules/ without its .cursor/rules/.mdc symlink fails CI; `--fix` auto-creates missing symlinks and drops stale ones. Also add the symlink for the new pydabs-acceptance-tests rule. Co-authored-by: Isaac --- .cursor/rules/pydabs-acceptance-tests.mdc | 1 + Taskfile.yml | 8 ++- tools/validate_cursor_rules.py | 72 +++++++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 120000 .cursor/rules/pydabs-acceptance-tests.mdc create mode 100755 tools/validate_cursor_rules.py diff --git a/.cursor/rules/pydabs-acceptance-tests.mdc b/.cursor/rules/pydabs-acceptance-tests.mdc new file mode 120000 index 00000000000..ffe41d4bbea --- /dev/null +++ b/.cursor/rules/pydabs-acceptance-tests.mdc @@ -0,0 +1 @@ +../../.agents/rules/pydabs-acceptance-tests.md \ No newline at end of file diff --git a/Taskfile.yml b/Taskfile.yml index 49ae04a5e2d..b8cd6cb3720 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -313,8 +313,13 @@ tasks: cmds: - "! git grep -lF databricks.com -- '*uv.lock' '*.py.lock'" + check-cursor-rules: + desc: Fail if a .agents/rules/*.md lacks its .cursor/rules/*.mdc symlink (--fix adds it) + cmds: + - "./tools/validate_cursor_rules.py" + checks: - desc: Run quick checks (tidy, whitespace, deadcode, changelog, lockfiles) + desc: Run quick checks (tidy, whitespace, deadcode, changelog, lockfiles, cursor-rules) # Sequential: `tidy` rewrites go.mod/go.sum and any future tidy work # touching more paths should not race with the whitespace scanner. cmds: @@ -323,6 +328,7 @@ tasks: - task: deadcode - task: check-changelog - task: check-lockfiles + - task: check-cursor-rules install-pythons: desc: Install Python 3.9-3.13 via uv diff --git a/tools/validate_cursor_rules.py b/tools/validate_cursor_rules.py new file mode 100755 index 00000000000..3ef9ce58953 --- /dev/null +++ b/tools/validate_cursor_rules.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.12" +# /// +"""Keep .cursor/rules/*.mdc symlinks in sync with .agents/rules/*.md. + +The canonical rules live in .agents/rules/.md; Cursor reads them from +.cursor/rules/.mdc, so each rule needs a .mdc symlink pointing back at its +.md. This validates that every rule has a correct symlink and that no symlink is +left dangling. Run with --fix to create missing symlinks and drop stale ones. + +Non-symlink .mdc files (e.g. the standalone 00-agents-context.mdc bootstrap) are +not mirrors of a rule and are left untouched. +""" + +import os +import sys + +AGENTS_RULES = ".agents/rules" +CURSOR_RULES = ".cursor/rules" + + +def link_target(stem): + # The .mdc lives in .cursor/rules/, so ../../ reaches the repo root. + return f"../../{AGENTS_RULES}/{stem}.md" + + +def main(): + fix = "--fix" in sys.argv + + stems = sorted(f[:-3] for f in os.listdir(AGENTS_RULES) if f.endswith(".md")) + problems = [] + + # Every rule must have a .mdc symlink pointing at its .md. + for stem in stems: + mdc = os.path.join(CURSOR_RULES, stem + ".mdc") + want = link_target(stem) + have = os.readlink(mdc) if os.path.islink(mdc) else None + if have == want: + continue + if fix: + if os.path.lexists(mdc): + os.remove(mdc) + os.symlink(want, mdc) + print(f"Linked {mdc} -> {want}") + else: + problems.append(f"{mdc}: missing or wrong symlink, expected -> {want}") + + # No .mdc symlink may point at a rule that no longer exists. + known = {stem + ".mdc" for stem in stems} + for name in sorted(os.listdir(CURSOR_RULES)): + path = os.path.join(CURSOR_RULES, name) + if not os.path.islink(path) or name in known: + continue + if fix: + os.remove(path) + print(f"Removed stale {path}") + else: + problems.append(f"{path}: stale symlink, no matching {AGENTS_RULES}/ rule") + + if problems: + print("\n".join(problems)) + print( + f"\n{len(problems)} problem(s). Run: ./tools/validate_cursor_rules.py --fix", + file=sys.stderr, + ) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 43de1d91fe9a59895bd3a1dd726ac7f1d902e16c Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Fri, 4 Sep 2026 13:34:09 +0000 Subject: [PATCH 6/6] Drop cursor-rules symlink lint (moving to a separate PR) Remove tools/validate_cursor_rules.py and its check-cursor-rules task; the symlink-mirror check is being shipped on its own. Keep the .cursor/rules/pydabs-acceptance-tests.mdc symlink for the rule added here. Co-authored-by: Isaac --- Taskfile.yml | 8 +--- tools/validate_cursor_rules.py | 72 ---------------------------------- 2 files changed, 1 insertion(+), 79 deletions(-) delete mode 100755 tools/validate_cursor_rules.py diff --git a/Taskfile.yml b/Taskfile.yml index b8cd6cb3720..49ae04a5e2d 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -313,13 +313,8 @@ tasks: cmds: - "! git grep -lF databricks.com -- '*uv.lock' '*.py.lock'" - check-cursor-rules: - desc: Fail if a .agents/rules/*.md lacks its .cursor/rules/*.mdc symlink (--fix adds it) - cmds: - - "./tools/validate_cursor_rules.py" - checks: - desc: Run quick checks (tidy, whitespace, deadcode, changelog, lockfiles, cursor-rules) + desc: Run quick checks (tidy, whitespace, deadcode, changelog, lockfiles) # Sequential: `tidy` rewrites go.mod/go.sum and any future tidy work # touching more paths should not race with the whitespace scanner. cmds: @@ -328,7 +323,6 @@ tasks: - task: deadcode - task: check-changelog - task: check-lockfiles - - task: check-cursor-rules install-pythons: desc: Install Python 3.9-3.13 via uv diff --git a/tools/validate_cursor_rules.py b/tools/validate_cursor_rules.py deleted file mode 100755 index 3ef9ce58953..00000000000 --- a/tools/validate_cursor_rules.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -# /// script -# requires-python = ">=3.12" -# /// -"""Keep .cursor/rules/*.mdc symlinks in sync with .agents/rules/*.md. - -The canonical rules live in .agents/rules/.md; Cursor reads them from -.cursor/rules/.mdc, so each rule needs a .mdc symlink pointing back at its -.md. This validates that every rule has a correct symlink and that no symlink is -left dangling. Run with --fix to create missing symlinks and drop stale ones. - -Non-symlink .mdc files (e.g. the standalone 00-agents-context.mdc bootstrap) are -not mirrors of a rule and are left untouched. -""" - -import os -import sys - -AGENTS_RULES = ".agents/rules" -CURSOR_RULES = ".cursor/rules" - - -def link_target(stem): - # The .mdc lives in .cursor/rules/, so ../../ reaches the repo root. - return f"../../{AGENTS_RULES}/{stem}.md" - - -def main(): - fix = "--fix" in sys.argv - - stems = sorted(f[:-3] for f in os.listdir(AGENTS_RULES) if f.endswith(".md")) - problems = [] - - # Every rule must have a .mdc symlink pointing at its .md. - for stem in stems: - mdc = os.path.join(CURSOR_RULES, stem + ".mdc") - want = link_target(stem) - have = os.readlink(mdc) if os.path.islink(mdc) else None - if have == want: - continue - if fix: - if os.path.lexists(mdc): - os.remove(mdc) - os.symlink(want, mdc) - print(f"Linked {mdc} -> {want}") - else: - problems.append(f"{mdc}: missing or wrong symlink, expected -> {want}") - - # No .mdc symlink may point at a rule that no longer exists. - known = {stem + ".mdc" for stem in stems} - for name in sorted(os.listdir(CURSOR_RULES)): - path = os.path.join(CURSOR_RULES, name) - if not os.path.islink(path) or name in known: - continue - if fix: - os.remove(path) - print(f"Removed stale {path}") - else: - problems.append(f"{path}: stale symlink, no matching {AGENTS_RULES}/ rule") - - if problems: - print("\n".join(problems)) - print( - f"\n{len(problems)} problem(s). Run: ./tools/validate_cursor_rules.py --fix", - file=sys.stderr, - ) - return 1 - return 0 - - -if __name__ == "__main__": - sys.exit(main())