Skip to content

feat(standard-tests): fail on an empty test-scenario set unless acceptance-test-config.yml declares a bypass_reason - #1228

Merged
Anatolii Yatsuk (tolik0) merged 3 commits into
mainfrom
tolik0/cdk/empty-scenarios-fail
Oct 28, 2026
Merged

Anatolii Yatsuk (tolik0) merged 3 commits into
mainfrom
tolik0/cdk/empty-scenarios-fail

Conversation

@tolik0

@tolik0 Anatolii Yatsuk (tolik0) commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Stricter standard tests, no runtime change. On the next CDK release, a Python or manifest-only connector whose acceptance-test-config.yml yields no runnable test scenario, or that has no such file, fails collection instead of passing with every scenario test skipped, unless the file declares a bypass_reason. Connector CI installs airbyte-cdk[dev] unpinned, so this reaches every connector at once. On master fd347841abe (2026-10-27) that is 35 connectors (10 without a bypass, 25 without the file). Monorepo changes fix all of them and must merge first: airbytehq/airbyte#88931 (bypass reasons, 25 connectors), airbytehq/airbyte#88932 (legacy list sections), and airbytehq/airbyte#90137 (bypass reasons for 10 Python destinations whose CI is already red on master, split out of #88931, plus the run module 9 of them were missing). With all three merged, the classifier finds 0 connectors in either group; with only #88931 and #88932, those 10 destinations lack a file.

What

Implements P0.3 of the CAT → FAST coverage epic (airbytehq/airbyte-internal-issues#17131, under airbytehq/airbyte-internal-issues#17129): an empty test-scenario set must fail, not silently skip.

Until now, pytest_generate_tests parametrized scenario with whatever get_scenarios() returned. An empty list made pytest skip every scenario test as got empty parameter set and the run passed. A connector whose configs were never collected, whose file was missing, or whose layout the reader did not understand looked the same as a tested one.

How

  • Zero scenarios is an error. When no scenario can run and no bypass_reason explains why, collection fails with one message that names the file and says why each section and entry produced nothing (e.g. `acceptance_tests.spec.tests[0]`: no `config_path` and no `bypass_reason` (keys: `spec_path`)), followed by the fix. The error is raised with pytrace=False outside the except block, so it prints once, without a traceback.

  • Explicit, machine-readable bypass. A bypass_reason counts in three places:

    • at the top level of the file: the whole connector. A file holding only this key is the opt-out for a connector without test configs. Combined with runnable configs it is an error.
    • on a section (connection: {bypass_reason: ...}), the layout CAT used and that ~285 connectors have today. It is ignored when the section lists a runnable config, as before.
    • on an entry without config_path. An entry with a config_path always runs.

    Only the collected sections count (spec, connection, discovery, basic_read, full_refresh). An incremental bypass or an IAM-role config does not make a connector "bypassed".

  • Bypasses are visible. Each scenario test gets one extra skipped param per distinct reason ([bypassed-connection-discovery-basic_read-full_refresh], [bypassed-connector]) whose skip message is acceptance-test-config.yml bypasses <scope>: <reason>, and collection prints one line per connector, e.g. source-hibob: acceptance-test-config.yml declares 1 bypass(es), reported as skipped tests: the connector: No test config in GSM for this connector. The reasons land in the JUnit XML.

  • Legacy list-format sections (connection: [...]) are read as the section's tests list, through section_entries(), which the spec backward-compatibility waiver reader from feat(standard-tests): check spec backward compatibility against the published version #1220 now shares.

  • Legacy top-level tests: fails with one message naming the file and the migration (rename to acceptance_tests:, nest entries under tests). Converting those 34 connectors is airbytehq/airbyte-internal-issues#16264.

  • A missing file is an error for Python and manifest-only connectors. Java connectors (language:java in metadata.yaml) are reported as bypassed, since their Gradle suites test them.

  • Suites with their own scenarios. A suite that overrides get_scenarios() supplies its scenarios in code, so its acceptance-test-config.yml is not read for bypasses: a missing file, a legacy layout or a top-level bypass_reason changes nothing. If the override returns no scenario, collection fails with <Suite>.get_scenarios() returned no test scenarios.

  • No values in failure messages. An entry the scenario model rejects (e.g. empty_streams as plain strings) is reported by location, field and rule; pydantic's input values are not printed.

Changes

  • airbyte_cdk/test/standard_tests/_acceptance_test_config.py (new, private): loading, layout errors, scenario and bypass collection, and the empty-set message.
  • docker_base.py: acceptance_test_config and get_scenarios() use it. New get_scenario_bypasses() and describe_empty_scenarios(), which skip the file when a subclass overrides get_scenarios(). get_scenarios() raises MissingAcceptanceTestConfigError (a FileNotFoundError) or AcceptanceTestConfigError (a ValueError) instead of warning, so existing except FileNotFoundError / except ValueError callers keep working.
  • pytest_hooks.py: the failure, the bypass params and the collection line (pytest_report_collectionfinish).
  • scenario.py: one field, ConnectorTestScenario.bypass_reason, holding the reason on the placeholder scenario of a skipped bypass test. Kept minimal on purpose (see Rollout).
  • _spec_compatibility.py: reads the spec section through the shared section_entries().
  • cdk-migrations.md: new ## Upgrading to the next minor release (unreleased) section with a before/now table, the three bypass forms and the migration steps.

Fleet impact

Classifier over the 594 non-archived Python and manifest-only connectors on airbyte master (fd347841abe, re-run 2026-10-27; first run on 1e22e3c), and over master plus both fleet PRs:

Group Master count Today After this release, fleet PRs not merged After this release, fleet PRs merged
Runnable scenarios 244 Scenarios run Same scenarios (identical count per connector) Same, 245
Only declared bypasses 286 Every scenario test skipped as an empty parameter set Skipped with the declared reason Same, 321
No runnable config, no bypass 10 Silently skipped Collection fails 0, bypass declared (PR A)
No acceptance-test-config.yml 25 UserWarning, silently skipped Collection fails 0, bypass-only file (PR A)
Legacy list-format sections 3 Sections ignored Configs run: 2 connectors fail (dead or missing GSM configs), 1 config error Converted, runnable configs pass (PR B)
Legacy top-level tests: 28 Collection fails Collection fails, clearer message Unchanged, tracked in airbytehq/airbyte-internal-issues#16264

The first run also found one invalid entry (expect_records without path); the P1.5 cleanup removed it from master.

Certified connectors newly declaring a bypass: destination-pinecone (GSM holds a config no entry lists) and source-granola (no GSM config).

The connectors kept in the private enterprise repository, whose CI runs the same standard tests, are not affected: the same classifier finds every Python and manifest-only connector there runnable.

Rollout

Release gate. This PR stays in draft until all of these are done:

If the fleet PRs cannot land before the next release, split this PR: ship the bypass reading and reporting first, and make the empty set a failure one release later.

Follow-ups

Left for later, from review:

  • The YAML parse error includes PyYAML's source snippet of the offending line. Fix: build the message from the problem and the line and column numbers only.

Test plan

  • unit_tests/test/test_empty_scenarios.py (45 tests, pytester runs of the real hooks): every bypass form and its scope; the empty-set message; section bypass ignored next to runnable entries; blank and non-string reasons; top-level bypass with and without runnable entries; legacy list sections; legacy top-level tests:; unusable layouts; missing file for Python, manifest-only and Java; collection failures print once, without a traceback or chained exception; skipped bypass params, ids and grouping; validation errors without values; a suite overriding get_scenarios() runs its own scenarios with no file, a legacy file or a top-level bypass, and fails when it returns none; the suite create_connector_test_suite(connector_directory=...) builds for airbyte-cdk connector test skips a bypass-only file and fails collection on a zero-scenario file. The no-traceback checks assert that no pytest_hooks.py: frame is printed; switching either pytrace=False to True, or dropping the override check, fails tests.

  • poetry run pytest unit_tests/test: 870 passed, 9 skipped (2026-10-28, head 2e8cc3bf). Earlier, with unit_tests/resources: 834 passed, 20 skipped. The skip count depends on the registry: pokeapi's spec backward-compatibility test skips while the registry publishes a newer version than its fixture.

  • poe lint and ruff format --check clean; poe type-check reports no error in the changed files (the 51 existing errors are unchanged against main); Prettier clean on the new migration section.

  • CLI proofs with this branch's airbyte-cdk, on copies of monorepo connectors with GSM secrets fetched by airbyte-cdk secrets fetch:

    Connector Shape Ref Result
    source-pokeapi runnable master connector test exit 0, 9 passed, 3 skipped
    source-100ms section bypasses only master exit 0, 3 passed, 9 skipped with the declared reasons
    source-hibob no runnable config, no bypass master exit 2, "No test scenarios for connector 'source-hibob'"
    source-clazar no file master exit 2, "Acceptance test config file not found"
    source-activecampaign legacy top-level tests: master exit 2, migration message
    source-hibob, source-clazar top-level bypass PR A exit 0, 3 passed, 9 skipped "bypasses the connector: No test config in GSM for this connector"
    destination-pinecone, source-granola top-level bypass PR A collect: bypass line printed
    source-pardot legacy list, converted PR B connector test exit 0 (5 passed, 16 skipped); image test exit 0
    source-google-directory legacy list, converted PR B image test exit 0, 4 passed (discover kept)
    source-netsuite legacy list, converted PR B image test exit 0, 3 passed
  • Classifier: master has 10 + 25 connectors to fix; master plus #88931, #88932 and the follow-up has 0 and 0; without the follow-up, 0 and 10 (re-run 2026-10-27 on fd347841abe; the 10th no-bypass connector is source-github-g2, added to master on 2026-10-20 and now covered by PR A).

🤖 Generated with Claude Code

…tance-test-config.yml declares a bypass_reason

An acceptance-test-config.yml that yielded no runnable config made pytest
skip every scenario test as an empty parameter set, and the run passed. A
connector whose configs were never collected, whose file was missing, or
whose layout the reader did not understand looked the same as a tested one.

Collection now fails when no scenario can run and no bypass_reason says
why. A bypass is accepted at the top level of the file (the whole
connector, and the opt-out for a connector without test configs), on a
section, or on an entry without config_path. Bypasses are reported as
skipped tests carrying the declared reason, grouped by reason, plus one
line after collection.

The reader also:
- parses legacy list-format sections as the section's tests list;
- fails with a migration message on a legacy top-level `tests:` key;
- treats a missing file as an error for Python and manifest-only
  connectors, and as a bypass for Java connectors;
- wraps invalid entries in an error that names the entry and field but
  never prints the offending values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

👋 Greetings, Airbyte Team Member!

Here are some helpful tips and reminders for your convenience.

💡 Show Tips and Tricks

Testing This CDK Version

You can test this version of the CDK using the following:

# Run the CLI from this branch:
uvx 'git+https://github.com/airbytehq/airbyte-python-cdk.git@tolik0/cdk/empty-scenarios-fail#egg=airbyte-python-cdk[dev]' --help

# Update a connector to use the CDK from this branch ref:
cd airbyte-integrations/connectors/source-example
poe use-cdk-branch tolik0/cdk/empty-scenarios-fail

PR Slash Commands

Airbyte Maintainers can execute the following slash commands on your PR:

  • /autofix - Fixes most formatting and linting issues
  • /poetry-lock - Updates poetry.lock file
  • /test - Runs connector tests with the updated CDK
  • /prerelease - Triggers a prerelease publish with default arguments
  • /poe build - Regenerate git-committed build artifacts, such as the pydantic models which are generated from the manifest JSON schema in YAML.
  • /poe <command> - Runs any poe command in the CDK environment
📚 Show Repo Guidance

Helpful Resources

📝 Edit this welcome message.

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

PyTest Results (Fast)

5 948 tests  +110   5 917 ✅ +110   6m 25s ⏱️ - 1m 44s
    1 suites ±  0      31 💤 ±  0 
    1 files   ±  0       0 ❌ ±  0 

Results for commit 2e8cc3b. ± Comparison against base commit 8b721cd.

This pull request removes 2 and adds 112 tests. Note that renamed tests count towards both.
unit_tests.sources.declarative.test_concurrent_declarative_source ‑ test_api_budget_is_set_before_dynamic_streams_evaluated
unit_tests.test.test_scenario_keys ‑ test_fields_awaiting_a_reader_are_still_unread[bypass_reason]
unit_tests.legacy.sources.declarative.test_manifest_declarative_source ‑ test_building_a_source_does_not_check_the_component_schema[False]
unit_tests.legacy.sources.declarative.test_manifest_declarative_source ‑ test_building_a_source_does_not_check_the_component_schema[True]
unit_tests.legacy.sources.declarative.test_manifest_declarative_source ‑ test_legacy_source_delegates_to_public_manifest_validator
unit_tests.source_declarative_manifest.test_source_declarative_manifest_imports ‑ test_spec_check_discover_do_not_import_pandas_or_numpy
unit_tests.sources.declarative.checks.test_check_dynamic_stream ‑ test_check_dynamic_stream_builds_only_the_first_stream_count_in_order[one-stream]
unit_tests.sources.declarative.checks.test_check_dynamic_stream ‑ test_check_dynamic_stream_builds_only_the_first_stream_count_in_order[three-streams]
unit_tests.sources.declarative.checks.test_check_dynamic_stream ‑ test_check_dynamic_stream_uses_streams_for_legacy_manifest_source
unit_tests.sources.declarative.checks.test_check_dynamic_stream ‑ test_check_dynamic_stream_uses_streams_for_non_positive_count_or_disabled_availability[0-True]
unit_tests.sources.declarative.checks.test_check_dynamic_stream ‑ test_check_dynamic_stream_uses_streams_for_non_positive_count_or_disabled_availability[1-False]
unit_tests.sources.declarative.checks.test_check_dynamic_stream ‑ test_check_dynamic_stream_uses_streams_override_for_concurrent_source
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

PyTest Results (Full)

5 951 tests  +110   5 920 ✅ +110   11m 27s ⏱️ - 5m 14s
    1 suites ±  0      31 💤 ±  0 
    1 files   ±  0       0 ❌ ±  0 

Results for commit 2e8cc3b. ± Comparison against base commit 8b721cd.

This pull request removes 2 and adds 112 tests. Note that renamed tests count towards both.
unit_tests.sources.declarative.test_concurrent_declarative_source ‑ test_api_budget_is_set_before_dynamic_streams_evaluated
unit_tests.test.test_scenario_keys ‑ test_fields_awaiting_a_reader_are_still_unread[bypass_reason]
unit_tests.legacy.sources.declarative.test_manifest_declarative_source ‑ test_building_a_source_does_not_check_the_component_schema[False]
unit_tests.legacy.sources.declarative.test_manifest_declarative_source ‑ test_building_a_source_does_not_check_the_component_schema[True]
unit_tests.legacy.sources.declarative.test_manifest_declarative_source ‑ test_legacy_source_delegates_to_public_manifest_validator
unit_tests.source_declarative_manifest.test_source_declarative_manifest_imports ‑ test_spec_check_discover_do_not_import_pandas_or_numpy
unit_tests.sources.declarative.checks.test_check_dynamic_stream ‑ test_check_dynamic_stream_builds_only_the_first_stream_count_in_order[one-stream]
unit_tests.sources.declarative.checks.test_check_dynamic_stream ‑ test_check_dynamic_stream_builds_only_the_first_stream_count_in_order[three-streams]
unit_tests.sources.declarative.checks.test_check_dynamic_stream ‑ test_check_dynamic_stream_uses_streams_for_legacy_manifest_source
unit_tests.sources.declarative.checks.test_check_dynamic_stream ‑ test_check_dynamic_stream_uses_streams_for_non_positive_count_or_disabled_availability[0-True]
unit_tests.sources.declarative.checks.test_check_dynamic_stream ‑ test_check_dynamic_stream_uses_streams_for_non_positive_count_or_disabled_availability[1-False]
unit_tests.sources.declarative.checks.test_check_dynamic_stream ‑ test_check_dynamic_stream_uses_streams_override_for_concurrent_source
…

♻️ This comment has been updated with latest results.

Resolve the conflicts with #1226, which checks every acceptance-test-config.yml entry for keys
the standard tests do not read:

- Move that check from `get_scenarios()` into `collect_scenarios()`, so the entries of legacy
  list sections are checked too. Invalid values are collected the same way, and both raise one
  `AcceptanceTestConfigError` listing every problem in #1226's format, which pytest reports
  without a traceback.
- Drop `bypass_reason` from `AWAITING_READER`: this change reads it.
- cdk-migrations.md: #1226's section ships as 7.39.0 and is renamed; this change's section drops
  the invalid-entry row, which 7.39.0 already covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for the release ordering only. The code itself looks sound to me.

Done well:

  • the bypass model (top-level, per section, per entry) and the "top-level bypass next to runnable configs" guard;
  • failure messages that name keys and sections, never values;
  • the cdk-migrations.md section;
  • strong tests: 18 of 24 deliberate mutations are caught.

Rollout: this is breaking for connector test suites (not syncs) and ships as a minor, which is fine only if airbytehq/airbyte#88931 and #88932 merge first (inline comment on pytest_hooks.py). I independently verified that the files in both fleet PRs load and collect correctly under this branch and under the released CLI.

Not inline (low, for later): YAML parse errors echo the offending source line; a bypass_reason on a runnable entry is silently dropped; AcceptanceTestConfigError and ScenarioBypass are importable only from a private module; the PR body's fleet counts are stale.

Comment thread unit_tests/test/test_empty_scenarios.py Resolved
Comment thread unit_tests/test/test_scenario_keys.py Resolved
@tolik0
Anatolii Yatsuk (tolik0) marked this pull request as ready for review October 28, 2026 15:10
@tolik0
Anatolii Yatsuk (tolik0) merged commit f7b9cd4 into main Oct 28, 2026
30 of 31 checks passed
@tolik0
Anatolii Yatsuk (tolik0) deleted the tolik0/cdk/empty-scenarios-fail branch October 28, 2026 15:11
Anatolii Yatsuk (tolik0) added a commit that referenced this pull request Oct 29, 2026
…e-test-config.yml

Since #1228, get_scenarios() raises MissingAcceptanceTestConfigError when a
connector has no acceptance-test-config.yml, so the tmp connector of the
test_fail_read_with_invalid_config tests needs one. The suite helper now writes
a file with one basic_read config unless the test wrote its own.
Sign up for free to 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.

2 participants