Skip to content

fix(0.28.0): TestE2EStorageSwapTables must skip without E2E_API_TOKEN (CI fix for PR #249) - #250

Merged
padak merged 1 commit into
feat-0.28.0from
fix/0.28.0-e2e-skip-without-credentials
May 6, 2026
Merged

fix(0.28.0): TestE2EStorageSwapTables must skip without E2E_API_TOKEN (CI fix for PR #249)#250
padak merged 1 commit into
feat-0.28.0from
fix/0.28.0-e2e-skip-without-credentials

Conversation

@padak

@padak padak commented May 6, 2026

Copy link
Copy Markdown
Member

Summary

PR #249 (release rollup) broke CI on the unit-suite job because TestE2EStorageSwapTables (introduced in #247) was decorated only with @pytest.mark.e2e, missing the @skip_without_credentials skipif decorator that all other E2E classes use. CI runs pytest tests/ -v -m 'not integration', which does NOT deselect the e2e marker -- the class collected, the autouse setup fixture ran, and os.environ[ENV_TOKEN] raised KeyError. 3 errors, exit 1.

CI run: https://github.com/padak/keboola_agent_cli/actions/runs/25440815047

Fix is one line: add @skip_without_credentials (the skipif decorator already used by TestE2EStorageNativeTypesAndBranchMaterialize and friends) above the existing @pytest.mark.e2e marker.

Test plan

Verified locally with the exact CI command:

$ unset E2E_API_TOKEN
$ uv run pytest tests/ -v -m 'not integration'
========= 2480 passed, 69 skipped, 7 deselected, 14 warnings in 37.89s =========

Before: 3 errors (KeyError: 'E2E_API_TOKEN' in setup).
After: 3 skips (E2E tests require E2E_API_TOKEN environment variable).

The @pytest.mark.e2e marker is preserved so explicit -m e2e runs (with credentials) still pick the class up. This matches the pattern already established by TestE2EStorageNativeTypesAndBranchMaterialize.

CI `pytest -m 'not integration'` does not deselect the `e2e` marker, so
the class's setup fixture ran in the runner without credentials and
crashed with KeyError: 'E2E_API_TOKEN'. Other E2E classes apply both
`@skip_without_credentials` (skipif decorator on the class) AND
`@pytest.mark.e2e` (marker for explicit `-m e2e` runs). PR #247
shipped only the marker, hence the rollup PR #249 broke CI.

Verified locally: `pytest -m 'not integration'` now skips the 3 swap
tests cleanly (2480 passed, 69 skipped, 0 errors).
@padak
padak merged commit c70e8c2 into feat-0.28.0 May 6, 2026
@padak
padak deleted the fix/0.28.0-e2e-skip-without-credentials branch May 6, 2026 14:37
padak added a commit that referenced this pull request May 6, 2026
…bles (#249)

* fix(0.28.0): config update auto-normalizes script[] string -> array (#245) (#246)

* fix(0.27.1): config update auto-normalizes parameters.blocks[].codes[].script string -> array (#245)

The Storage API silently accepts a string for
parameters.blocks[].codes[].script while the Keboola runtime validator
requires an array (`Invalid type for path "root.parameters.blocks.0.codes.X.script".
Expected "array", but got "string"`). The broken push lands as 200 OK,
the version increments, the UI looks fine -- the crash happens only at
job-run time, often hours later via the scheduler, with no attribution
back to the offending write. Issue #245 was reported after a programmatic
refactor crashed three production Snowflake transformations.

kbagent config update now closes the gap on the write side. Defense-in-depth
in our own command surface; bypassing kbagent (raw Storage API REST,
keboola-mcp-server's update_sql_transformation / create_sql_transformation
as of v1.59.x) does NOT inherit the normalization -- a separate upstream
fix is recommended for the MCP server.

## Surfaces (all v0.27.1)

- Helper: normalize_blocks_codes_script(component_id, config) ->
  (config, normalizations) in src/keboola_agent_cli/sync/code_extraction.py.
  SQL transformations get statement-level split via the existing
  split_statements() state machine (respects '...' / "..." / $$..$$ /
  -- / # / // / /* ... */); Python / R / kds-team.app-custom-python and
  any other component sharing the parameters.blocks[].codes[].script
  shape get a single-element [script] wrap. Already-array script values
  pass through unchanged.

- Service: ConfigService.update_config calls the helper immediately
  after _resolve_configuration, before client.update_config. Both the
  real-write path and the --dry-run preview path normalize, so what
  the operator sees in dry-run matches what would actually land on
  Storage. The result envelope gains a normalizations: [{path, action:
  "sql_split"|"wrap_array", before_type, after_type, after_length}]
  field per write -- empty list when input was already valid.

- CLI: human mode prints a yellow `Auto-normalized N script field(s)
  to array (string -> list). See --json for details.` warning followed
  by a per-element trace, so the silent fix is observable.

## Bonus silent-gap fix

SQL_TRANSFORMATION_COMPONENTS in code_extraction.py was missing
keboola.google-bigquery-transformation and keboola.duckdb-transformation,
so kbagent sync push previously did NOT split semicolons in BigQuery /
DuckDB transformations -- it joined every statement into one script
element. Same failure shape as #119 (closed for Snowflake / Synapse /
Oracle / Redshift), just on different backends. The new fragment-based
is_sql_transformation_component() helper also matches future variants
(*-bigquery-transformation, *-duckdb-transformation, *-exasol-transformation,
*-teradata-transformation, *-transformation-v2) so newer SQL backends
do not require an edit to the exact set.

## Tests

- 35 new unit tests in tests/test_normalize_script.py: registry detection
  (exact + fragment fallback), splitter edge cases (semicolons inside
  block comments and string literals), per-component dispatch (SQL
  split vs Python wrap vs already-array passthrough), ConfigService
  integration (write path, dry-run path, --set path), CLI surfacing in
  both JSON and human modes.

- New E2E test class TestE2EConfigUpdateNormalization in tests/test_e2e.py
  exercising the full path against a real Snowflake transformation in
  a dev branch: dry-run preview -> string-script push -> Storage API
  state assert (script[] is list, not str; literal with embedded ;
  preserved) -> --set path with nested script= value -> job run on the
  normalized config (status success/warning, NOT 'Expected array, got
  string' runtime crash). Live-validated against project 901 (padak)
  during development.

## Sync-map walk

Every silent-drift surface from CONTRIBUTING.md "Plugin synchronization
map" updated:

- pyproject.toml 0.27.0 -> 0.27.1; plugin.json + marketplace.json synced
  via make version-sync.
- changelog.py -- five entries for 0.27.1 (fix description, observability,
  silent gap, plumbing, plugin updates).
- commands/context.py AGENT_CONTEXT -- config update bullet annotated.
- commands-reference.md -- config update bullet annotated.
- keboola-expert.md -- Rule 6 VERSION GATE notes 0.27.1+; tool selection
  matrix gains "Update SQL transformation body (script[])" row; inline
  gotchas gain "script[] string-vs-array runtime crash" entry.
- gotchas.md -- new "config update auto-normalizes script[] from string
  to array (since v0.27.1)" section with full failure mode + bypass
  caveat.
- sql-migration-workflow.md -- new error-table row, new "Auto-normalization
  of script[]" section, new anti-pattern.
- SKILL.md regenerated via make skill-gen (no command-name diff; surface
  is signature-stable, behaviour-only change).

## Verification

make check clean (2467 tests pass, lint + format + skill freshness +
version consistency + changelog + error-codes all green). Live e2e
against project 901 confirmed the full path: string-script push -> array
on Storage API -> Snowflake job run to success.

* chore: renumber 0.27.1 -> 0.28.0 (collection branch retarget)

PR base retargeted from main to feat-0.28.0; this fix lands as part of
the 0.28.0 release alongside other v0.28.0-tagged work, not as a
standalone 0.27.1 patch.

Mechanical rename across every silent-drift surface:
- pyproject.toml 0.27.1 -> 0.28.0; plugin.json + marketplace.json
  re-synced via make version-sync.
- changelog.py block key "0.27.1" -> "0.28.0"; the "(since v0.27.1)"
  inside the plugin entry rewritten to "(since v0.28.0)" for consistency.
- gotchas.md, commands-reference.md, sql-migration-workflow.md,
  keboola-expert.md -- all "(since v0.27.1)" / "0.27.1+" markers
  rewritten to 0.28.0.
- commands/context.py AGENT_CONTEXT auto-normalize bullet retagged.
- tests/test_e2e.py docstring + assertion message reference v0.28.0.

No behaviour change. make check clean (2467 tests pass).

* feat(0.28.0): storage swap-tables -- promote typed table rebuild via Storage API swap (#247)

* feat(0.28.0): storage swap-tables -- promote typed table rebuild via Storage API swap

* fix(swap-tables): poll async storage job to completion (PHP claim of sync was wrong)

* fix(swap-tables): register CommandHint so --hint client/service works (PR #247 review B-1)

* fix(0.28.0): VERSION GATE + test client.close() assertion (PR #247 review NB-1, NB-2) (#248)

- keboola-expert.md Rule 6: enumerate `storage swap-tables needs 0.28.0+`
  so an agent on an older install refuses the task before attempting it
  (carry-over NB-1 from PR #247 self-review).
- tests/test_storage_swap.py: add `mock_client.close.assert_called_once()`
  to TestSwapTablesService.test_success and test_api_error_propagates per
  CONTRIBUTING.md Testing Guidelines line 371. Locks the try/finally
  contract that releases the HTTPX client on both happy + error paths.

Documentation/test gaps only -- no functional code changes.

* fix(0.28.0): TestE2EStorageSwapTables must skip without E2E_API_TOKEN (#250)

CI `pytest -m 'not integration'` does not deselect the `e2e` marker, so
the class's setup fixture ran in the runner without credentials and
crashed with KeyError: 'E2E_API_TOKEN'. Other E2E classes apply both
`@skip_without_credentials` (skipif decorator on the class) AND
`@pytest.mark.e2e` (marker for explicit `-m e2e` runs). PR #247
shipped only the marker, hence the rollup PR #249 broke CI.

Verified locally: `pytest -m 'not integration'` now skips the 3 swap
tests cleanly (2480 passed, 69 skipped, 0 errors).

* docs(0.28.0): add typify-table-workflow.md -- typeless to native-types end-to-end (#251)

The PR #247 storage-types-workflow.md gained a small subsection on
'Promoting a typed rebuild back into the original name', but that section
only covered the swap-tables mechanics. AI agents asked to retypify a
Storage table also need to know HOW to profile the data, HOW to choose
types, and HOW to hand off to the user -- those were missing.

New 8-phase workflow:
  0. Decide-or-skip rubric (when typify is the wrong answer).
  1. Isolate in a dev branch (so production stays on the typeless original).
  2. Profile the typeless table in a workspace -- length / cardinality /
     parse-failure / scale-precision queries with a decision matrix that
     maps profile signals to Snowflake native types.
  3. Build typed sibling via storage create-table + copy data via
     in-workspace INSERT or SQL transformation, with row-count and
     NULL-count verification.
  4. Validate downstream consumers in the dev branch (config search +
     representative transformation run against the typeless source).
  5. Swap (dry-run + actual + verify column types swapped).
  6. Re-run downstream as smoke test against the typed source.
  7. Cleanup the sibling after merge.
  8. Handoff protocol -- structured summary the AI agent gives the user
     with phase-by-phase receipts, the merge URL, and rollback / cleanup
     commands.

Plus: SKILL.md workflow-references table gains the new entry; changelog
0.28.0 entry adds a 'Plugin docs' bullet describing the workflow.
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.

1 participant