Skip to content

acc: mutate-based invariant fuzzing for bundle configs - #5686

Open
radakam wants to merge 116 commits into
mainfrom
deco-25361-fuzz-create-payload
Open

acc: mutate-based invariant fuzzing for bundle configs#5686
radakam wants to merge 116 commits into
mainfrom
deco-25361-fuzz-create-payload

Conversation

@radakam

@radakamradakam commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Changes

Reuse the existing invariant targets, but feed them mutated configs instead of curated ones. Everything new lives in acceptance/bundle/fuzz/; the target scripts are untouched — fuzz overrides invariant_render to generate the config, and on PRs swaps the drift check for plan-determinism.

What one seed does:

  1. run_fuzz.py runs seed.sh in a fresh seed-N/ with its own bundle name, so seeds cannot see each other's state.
  2. mutate_fuzz_config.py picks a deploy-verified base from bundle/invariant/configs/ and either injects one optional from a curated INJECT table or applies a few destructive edits (drop a field, swap in a hostile scalar, empty a container).
  3. bundle validate --strict gates the result, so a config the CLI already rejects never reaches deploy.
  4. The real invariant target runs unchanged: no_drift, migrate, delete_idempotent or destroy_idempotent.
  5. run_fuzz.py classifies the seed.

The classifier is the oracle, so it is the main thing to review:

  • deployed — deployed and the invariant held.
  • rejectedvalidate --strict refused the config. Not a finding.
  • gap — the config needed a testserver route we don't model.
  • hang — exceeded FUZZ_SEED_TIMEOUT.
  • bug — panic, internal error, drift, or a deploy that failed after validate passed.

Only hang and bug fail the run, and both write a LOG.repro with a single-seed repro command.

Two ways it runs:

  • Every PR (acceptance/bundle/fuzz): a fixed 25-seed window per target, plan-determinism instead of exact drift (FUZZ_CHECK_DRIFT=0). Expected green, ~35s locally.
  • Nightly (./task test-fuzz, schedule-only job): a day-rotated window, exact no_drift, 15 min budget per target. A red nightly is a finding to triage rather than a broken build, so the job puts each LOG.repro in the step summary and uploads the seed workdirs.

Two things reach outside the new directory:

  • acceptance/cmd/yaml2json is a new test-only binary. The mutator is stdlib-only Python and cannot read YAML, so it shells out to the same yamlloader the bundle uses instead of keeping a second copy of every base.
  • acceptance/bundle/invariant/script.prepare affects the five existing targets: INVARIANT_DIR makes configs/ and data/ reachable from outside the subtree, and invariant_deploy now scans a failed deploy for panics before re-raising its exit code.

acceptance/selftest/mutate_fuzz_config pins the mutator contract: deterministic per seed, every INJECT field settable in the schema, and sample configs golden.

Why

Invariant tests cover curated happy paths. This explores hostile and optional-field configs on the same deploy/plan/destroy scripts to catch reconcile, validate, and migrate bugs without cloud round-trips. Exact drift stays nightly because mutated configs often false-positive on incomplete testserver stubs; PRs keep a stable weaker oracle. Already surfaced real CLI/testserver issues during development, see the table below.

Tests

  • go test ./acceptance -run TestAccept/selftest/mutate_fuzz_config
  • go test ./acceptance -run TestAccept/bundle/fuzz
  • Short local exploration: FUZZ_SEED_COUNT=5 FUZZ_CHECK_DRIFT=1 ENVFILTER=FUZZ_TARGET=no_drift ./task test-fuzz

Bugs found

CLI

BugInvariantMutate?PR
Direct-engine reconcile misclassification (catalogs, registered_models browse_only, serving burst_scaling_enabled + *_plaintext)no_driftNo#5816
Loosely-typed required UC fields not validated (sql_warehouses.*.name, grants[*].principal)no_driftNo#5818
SQL warehouse redeploy hangno_driftNo#5864
registered_models alias driftno_driftNo#5864
UC-managed catalog/schema property defaultsno_driftNo#5877
App deploy-only fields with no active deploymentno_driftNo#5943
jobs.webhook_notifications.on_* reordered after migratemigrateNo#6060
Empty grants node planned as create after migratemigrateYes#6039
grants with empty principal only warned → partial deploy + 400no_driftYes#6041
grants with empty privileges → perpetual updateno_driftYes#6062
pipeline allow_duplicate_names → perpetual updateno_driftYes#6076
Empty resource name stored under empty keyno_driftYes#6157
secret scope permission with no principal only warned → partial deploy + ACL rejectmigrateYes#6215
secret_scopes permission with invalid level only warned → deploy ACL collapse failsno_driftYes#6299

Test server

BugInvariantMutate?PR
Pipeline parameters dropped on read-backno_driftNo#5942
Experiment TraceLocation dropped on read-backno_driftNo#5993
Catalog fields dropped on create/updateno_driftNo#6158

Upstream

BugInvariantMutate?PR
model_serving ai_gateway.invalid_keywords empty-string → provider panicmigrateNoTF provider

@radakam
radakamtemporarily deployed to test-trigger-is June 23, 2026 07:44 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 23, 2026 07:44 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 23, 2026 08:06 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 23, 2026 08:06 — with GitHub Actions Inactive
@eng-dev-ecosystem-bot

eng-dev-ecosystem-bot commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: fb8d79a

Run: 32113153316

Env🟨​KNOWN💚​RECOVERED🙈​SKIP✅​pass🙈​skipTime
🟨​aws linux31428611526:09
🟨​aws windows31428811506:53
🟨​azure linux31428511526:33
🟨​azure windows31428711507:44
💚​gcp linux1528611525:22
💚​gcp windows1528811505:56
8 interesting tests: 4 SKIP, 3 KNOWN, 1 RECOVERED
Test Nameaws linuxaws windowsazure linuxazure windowsgcp linuxgcp windows
💚​TestAccept💚​R💚​R💚​R💚​R💚​R💚​R
🙈​TestAccept/bundle/invariant/no_drift🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/vector_search_endpoints/drift/recreated_same_name🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/vector_search_indexes/recreate/embedding_dimension🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/ssh/connection🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🟨​TestFetchRepositoryInfoAPI_FromRepo🟨​K🟨​K🟨​K🟨​K🙈​S🙈​S
🟨​TestFetchRepositoryInfoAPI_FromRepo/root🟨​K🟨​K🟨​K🟨​K
🟨​TestFetchRepositoryInfoAPI_FromRepo/subdir🟨​K🟨​K🟨​K🟨​K
Top 6 slowest tests (at least 2 minutes):
durationenvtestname
5:58azure windowsTestAccept
5:47gcp windowsTestAccept
5:28aws windowsTestAccept
3:55aws linuxTestAccept
3:51azure linuxTestAccept
3:47gcp linuxTestAccept

@radakam
radakamtemporarily deployed to test-trigger-is June 23, 2026 09:47 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 23, 2026 09:47 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 24, 2026 08:29 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 24, 2026 08:29 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 24, 2026 12:05 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 24, 2026 12:05 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 24, 2026 13:27 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 24, 2026 13:27 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 24, 2026 13:35 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 24, 2026 13:35 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 25, 2026 11:42 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 25, 2026 11:42 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 25, 2026 11:54 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 25, 2026 11:54 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 25, 2026 17:47 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 25, 2026 17:47 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 26, 2026 07:42 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 26, 2026 07:42 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 26, 2026 08:22 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 26, 2026 08:22 — with GitHub Actions Inactive
@radakam
radakam marked this pull request as ready for review June 26, 2026 08:34
@github-actions

github-actionsBot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Approval status: pending

/acceptance/bundle/ - needs approval

8 files changed
Suggested: @denik
Also eligible: @pietern, @janniklasrose, @shreyas-goenka, @andrewnester, @lennartkats-db, @anton-107

General files (require maintainer)

11 files changed
Based on git history:

  • @denik -- recent work in acceptance/bin/, acceptance/, ./

Any maintainer (@andrewnester, @anton-107, @denik, @pietern, @shreyas-goenka, @simonfaltum, @renaudhartert-db, @janniklasrose, @lennartkats-db, @rugpanov, @rclarey) can approve all areas.
See OWNERS for ownership rules.

Comment threadbundle/config/mutator/resourcemutator/cluster_fixups.go Outdated
Comment threadbundle/fuzz/generate_test.go Outdated
@radakam
radakamtemporarily deployed to test-trigger-is June 26, 2026 15:45 — with GitHub Actions Inactive
@radakam
radakamtemporarily deployed to test-trigger-is June 26, 2026 15:45 — with GitHub Actions Inactive
Keep the how; cut glosses that duplicate the targets and "rather than / not a" framing.
Rotate seeds by day-of-epoch so PR runs of push.yml do not open gaps,
raise test-fuzz's package timeout to cover four budgeted variants, restore
set +e around invariant_deploy so a panicking reject is still asserted,
and drop the unused prologue.sh that still called emit_fuzz_config.py.
Additive mutate now picks from a hand-curated INJECT table of deploy-proven
optionals (including the fields behind past drift findings) instead of walking
bundle schema. Drop gen_fuzz_config and the schema dump, and add app and
experiment to MUTATE_BASES so those types stay in reach under mutate-only.
Keep the non-obvious reasons; drop restatements and contrastive framing
across the harness, Taskfile, and nightly job.
Also diversify the selftest sample seeds and gate nightlies through
test-result with a failure summary for triage.
State what the harness does and why; cut "rather than / not a / no X" asides.
Drop the custom YAML dumper for json.dumps (valid YAML 1.2) and check every
INJECT field is a settable schema input, which also caught a nonexistent
experiments.description entry.
Keep inject and destroy on separate seeds, harden the loader contract check,
and keep LOG.repro reachable after a red nightly.
Treat validate type-mismatch warnings as seed rejections so configs
like group_name: [] do not deploy and fail mid-migrate. Point the
nightly triage upload at the same TMPDIR root the summary step uses.
Drop the hand-rolled YAML loader for committed JSON fixtures, reject
schema warnings via validate --strict, and run each seed through seed.sh
instead of export -f.
Sibling layout had to copy curated [[Server]] stubs and source helpers by
hand. As invariant/fuzz/, parent prepare and stubs come from the directory
chain; the leaf only clears INPUT_CONFIG, forces Cloud=false, and adds
TESTSERVER_GAP catch-alls.
The fuzzer kept its own JSON snapshot of each invariant config because the
acceptance Python helpers are stdlib-only and cannot parse YAML. That made the
curated YAML and its snapshot drift apart silently whenever one was edited.
Parse the invariant YAML directly via a new hidden `bundle debug yaml-to-json`,
so there is one copy of every base and it is parsed the way the bundle parses
it. Output is byte-identical to the snapshots this removes.
Also drop the unrelated comment rewording in invariant/script.prepare, leaving
only the INVARIANT_DIR indirection and the panic scan on a rejected deploy.
bundle/debug's help output is a golden, and the other tooling-only subcommands
(terraform, render-template-schema, list-targets) are hidden so they stay out
of it. Hide this one too rather than adding a test helper to the listing.
Fall through to destructive mutate when INJECT has nothing to add,
fail windows with zero deploys unless every seed is a gap, and drop
incomplete INJECT entries that only produced rejects.
gotestsum places custom flags before the package list, so -keeptmp was
rejected before any fuzz seed ran.
Bare `task` is not on PATH in a normal checkout.
The mutator runs in stdlib-only python and cannot read the invariant YAML, so it
went through a hidden `bundle debug yaml-to-json`. That put test-only tooling in
the product CLI for data the CLI has no privileged knowledge of.
Snapshot the configs to fuzz/bases/*.json.tmpl with a generator wired into
generate-check, the way generate-refschema owns its acceptance artifact: the
validate-generated CI job regenerates and diffs, so editing a config without a
regen fails there. The snapshots are byte-identical to the CLI's output, and
load_base is a plain open + json.loads again.
Acceptance runs uv offline, so adding PyYAML at runtime cannot replace the
snapshot generator. Store the 13 shared invariant bases in JSON syntax instead:
JSON is valid YAML 1.2 for the bundle, while the stdlib-only mutator can parse
the same source files with json.loads.
This removes the generated copies, generator task, and drift machinery without
bringing test-only YAML parsing back into the product CLI.
Keep idiomatic YAML fixtures and avoid shipping a test-only command in the
product CLI; stdlib Python shells out to a harness-built helper that uses
the same yamlloader as the bundle.
Remove the "Otherwise …" triage justification and rewrite nearby comments
to state the why without the negative case.
Windows go build treated the flag as an import path and broke every TestAccept run.
INPUT_CONFIG_OK is printed only after a deploy succeeds, so a seed whose config
passed validate --strict and then failed to deploy fell through to rejected and
kept the run green. That hid the validate gaps and deploy failures mutate is
meant to find (empty grant principal, secret scope ACL reject).
Migrate seeds terraform first, so catalog / external_location (direct-only)
and sql_warehouse (known post-migrate drift) cannot succeed there. Mirror
invariant/migrate's EnvMatrixExclude; otherwise the new deploy-fail classifier
flags those seeds as bugs on every PR smoke run.
The nightly job recomputed FUZZ_SEED_COUNT and the day-of-epoch
FUZZ_SEED_START that test-fuzz already defaults to, so the two had to stay
in sync for nightly windows to stay disjoint. Let the task own them.
INVARIANT_DIR needed no export: fuzz/script.prepare sets it before seed.sh
dereferences it, so the export only leaked the variable into every CLI
subprocess of the invariant targets.
Comment fixes: the INJECT schema check fails the run rather than warning,
and the stub check compares Pattern lines, not whole stubs.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@radakam@eng-dev-ecosystem-bot@denik