From 283f45fd707e00a4816f0f6f2f05bdc89001b780 Mon Sep 17 00:00:00 2001 From: Petr Date: Mon, 1 Jun 2026 23:07:14 +0200 Subject: [PATCH 1/2] fix(storage): correct stale "dev branch only" swap-tables wording Follow-up to #368. The swap-tables semantics correction (0.52.0) left four co-located surfaces still claiming the swap is dev-branch-only / rejected on production -- now false after that fix: - services/storage_service.py: the ConfigError raised on a missing --branch still said "The Storage API rejects this on production" (user-facing at exit code 5, directly contradicting the corrected docstring) - references/commands-reference.md, commands/storage.py docstring (-> SKILL.md via make skill-gen), commands/context.py (AGENT_CONTEXT): "in a dev branch" / "in a development branch" A swap on the default/production branch is supported -- it is how a typed rebuild is applied to prod. branch_id stays mandatory; this is wording only, no behavior change. The dependent test match ("dev branch" -> "requires a branch") and a misleading test docstring were updated accordingly. Found by the kbagent-pr-reviewer third pass on #368 (NB-1/2/3 + NIT-1). --- plugins/kbagent/skills/kbagent/SKILL.md | 2 +- .../kbagent/skills/kbagent/references/commands-reference.md | 2 +- src/keboola_agent_cli/commands/context.py | 2 +- src/keboola_agent_cli/commands/storage.py | 2 +- src/keboola_agent_cli/services/storage_service.py | 6 +++--- tests/test_storage_swap.py | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/kbagent/skills/kbagent/SKILL.md b/plugins/kbagent/skills/kbagent/SKILL.md index 8b1c4055..de76f586 100644 --- a/plugins/kbagent/skills/kbagent/SKILL.md +++ b/plugins/kbagent/skills/kbagent/SKILL.md @@ -193,7 +193,7 @@ When working inside a git repository or project directory, run `kbagent init` (o | Delete one or more storage tables | `kbagent storage delete-table --project PROJECT --table-id TABLE-ID` | | Truncate (delete all rows from) one or more storage tables | `kbagent storage truncate-table --project PROJECT --table-id TABLE-ID` | | Delete one or more columns from a storage table | `kbagent storage delete-column --project PROJECT --table-id TABLE-ID --column COLUMN` | -| Swap two storage tables in a development branch | `kbagent storage swap-tables --project PROJECT --table-id TABLE-ID --target-table-id TARGET-TABLE-ID` | +| Swap two storage tables (any branch, including the default/production branch) | `kbagent storage swap-tables --project PROJECT --table-id TABLE-ID --target-table-id TARGET-TABLE-ID` | | Clone (pull) a production table into a development branch | `kbagent storage clone-table --project PROJECT --table-id TABLE-ID` | | Delete one or more storage buckets | `kbagent storage delete-bucket --project PROJECT --bucket-id BUCKET-ID` | | Set the description on a storage bucket | `kbagent storage describe-bucket --project PROJECT --bucket-id BUCKET-ID` | diff --git a/plugins/kbagent/skills/kbagent/references/commands-reference.md b/plugins/kbagent/skills/kbagent/references/commands-reference.md index bdb633c2..a2be8c82 100644 --- a/plugins/kbagent/skills/kbagent/references/commands-reference.md +++ b/plugins/kbagent/skills/kbagent/references/commands-reference.md @@ -101,7 +101,7 @@ Requires a **super-admin** Manage API token (same kind as `org setup`). Same def - `storage truncate-table --project NAME --table-id ID [--table-id ...] [--dry-run] [--yes] [--branch ID]` (since v0.32.0) -- delete all rows while preserving table schema, primary key, descriptions, sharing edges, and downstream dependents. Batch via repeated `--table-id`. Endpoint is uniformly async-via-job on every branch (returns a queued `tableRowsDelete` job; client polls via `_wait_for_storage_job` before returning). Idempotent (truncating an empty table is a no-op). Use when re-seeding a table without losing the schema contract - `storage delete-column --project NAME --table-id ID --column COL [--column ...] [--force] [--dry-run] [--yes] [--branch ID]` -- delete columns from a table (branch-aware) - `storage delete-bucket --project NAME --bucket-id ID [--bucket-id ...] [--force] [--dry-run] [--yes] [--branch ID]` -- delete buckets (branch-aware) -- `storage swap-tables --project NAME --table-id ID --target-table-id ID --branch ID [--dry-run] [--yes]` (since v0.28.0) -- swap two storage tables in a dev branch (POST `/tables/{id}/swap`). Both tables exchange physical positions; aliases are NOT transferred (they keep pointing at the same physical position and therefore expose the OTHER table's data after the swap). Service refuses without a branch (active branch via `branch use` works too). Use to flip a typed rebuild ("data_change_log") into the original name ("data") without touching downstream config references +- `storage swap-tables --project NAME --table-id ID --target-table-id ID --branch ID [--dry-run] [--yes]` (since v0.28.0) -- swap two storage tables in any branch, including the default/production branch (POST `/tables/{id}/swap`). Both tables exchange physical positions; aliases are NOT transferred (they keep pointing at the same physical position and therefore expose the OTHER table's data after the swap). Service refuses without a branch (active branch via `branch use` works too). Use to flip a typed rebuild ("data_change_log") into the original name ("data") without touching downstream config references - `storage clone-table --project NAME --table-id ID --branch ID [--dry-run]` (since v0.52.0) -- pull (clone) a production table into a dev branch (POST `/tables/{id}/pull`, operationName `devBranchTablePull`). On `storage-branches` projects a dev branch reads prod tables transparently until the first write, so an in-branch schema mutation (`swap-tables`, dropping a column) fails with a misleading "bucket not found" until the table is materialized branch-local; `clone-table` does that. One-way (default -> branch). Service refuses without a branch (active branch via `branch use` works too). Permission class `write` - `storage describe-bucket --project NAME --bucket-id ID [--text STR | --file PATH | --stdin] [--branch ID]` -- set a bucket description (stored as `KBC.description` in bucket metadata, upsert). Provide exactly one of `--text`, `--file`, `--stdin`. Read back via `storage bucket-detail` - `storage describe-table --project NAME --table-id ID [--text STR | --file PATH | --stdin] [--branch ID]` -- set a table description (stored as `KBC.description` in table metadata, upsert). Provide exactly one of `--text`, `--file`, `--stdin`. Read back via `storage table-detail` diff --git a/src/keboola_agent_cli/commands/context.py b/src/keboola_agent_cli/commands/context.py index 16df9e23..b6d88022 100644 --- a/src/keboola_agent_cli/commands/context.py +++ b/src/keboola_agent_cli/commands/context.py @@ -395,7 +395,7 @@ Delete one or more buckets. --force cascade-deletes tables. Linked/shared buckets protected. Branch-aware. kbagent storage swap-tables --project NAME --table-id ID --target-table-id ID --branch ID [--dry-run] [--yes] - Swap two storage tables in a dev branch (POST /tables/{id}/swap). Both tables exchange physical positions; + Swap two storage tables in any branch, including the default/production branch (POST /tables/{id}/swap). Both tables exchange physical positions; aliases are NOT transferred (they keep pointing at the same physical position and therefore expose the OTHER table's data after the swap). Use to promote a typed rebuild back into the original name without touching downstream config references. branch_id is mandatory (--branch or active branch via 'kbagent diff --git a/src/keboola_agent_cli/commands/storage.py b/src/keboola_agent_cli/commands/storage.py index c3aa597a..596e729a 100644 --- a/src/keboola_agent_cli/commands/storage.py +++ b/src/keboola_agent_cli/commands/storage.py @@ -1326,7 +1326,7 @@ def storage_swap_tables( help="Skip confirmation prompt", ), ) -> None: - """Swap two storage tables in a development branch. + """Swap two storage tables (any branch, including the default/production branch). Both tables exchange physical positions. Aliases are NOT transferred -- they keep pointing at the same physical position and therefore expose diff --git a/src/keboola_agent_cli/services/storage_service.py b/src/keboola_agent_cli/services/storage_service.py index a9c91663..69f2d452 100644 --- a/src/keboola_agent_cli/services/storage_service.py +++ b/src/keboola_agent_cli/services/storage_service.py @@ -1375,10 +1375,10 @@ def swap_tables( """ if branch_id is None: raise ConfigError( - "swap-tables requires a dev branch. Set one with " + "swap-tables requires a branch. Set one with " "'kbagent branch use --project

--branch ' or pass " - "--branch directly. The Storage API rejects this on " - "production." + "--branch directly. Any branch works, including the " + "default/production branch." ) if table_id == target_table_id: diff --git a/tests/test_storage_swap.py b/tests/test_storage_swap.py index fe926751..b8d78e5f 100644 --- a/tests/test_storage_swap.py +++ b/tests/test_storage_swap.py @@ -219,12 +219,12 @@ def test_dry_run_skips_client_call(self, tmp_path: Path) -> None: mock_client.swap_tables.assert_not_called() def test_no_branch_raises_config_error(self, tmp_path: Path) -> None: - """Mandatory branch enforcement: production swap is rejected before any HTTP.""" + """Mandatory branch enforcement: swap-tables without --branch or active branch raises ConfigError before any HTTP.""" store = _make_store(tmp_path) mock_client = MagicMock() service = _make_service(store, mock_client) - with pytest.raises(ConfigError, match="dev branch"): + with pytest.raises(ConfigError, match="requires a branch"): service.swap_tables( alias="test", table_id="in.c-foo.a", From 090d2660f45ab4bcc3a3beb19e518e8727d33663 Mon Sep 17 00:00:00 2001 From: Petr Date: Mon, 1 Jun 2026 23:20:32 +0200 Subject: [PATCH 2/2] fix(storage): address #373 review -- remaining stale "dev branch" swap text Two surfaces the first pass of this PR missed, flagged by kbagent-pr-reviewer: - B-1 (blocking): CLI test test_swap_missing_branch_fails_clearly mocked the OLD "swap-tables requires a dev branch" string as its side-effect and asserted on it. The mock short-circuits the real service, so the test was validating phantom text that no longer matches service output. Updated the mock + assertion to "requires a branch". - NB-1: the swap_tables Args docstring still read "branch_id: Dev branch ID"; corrected to "any branch accepted, including the default/production branch". clone-table wording left intact (clone legitimately targets a dev branch; its service message and tests correctly keep "requires a dev branch"). --- src/keboola_agent_cli/services/storage_service.py | 2 +- tests/test_storage_swap.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/keboola_agent_cli/services/storage_service.py b/src/keboola_agent_cli/services/storage_service.py index 69f2d452..ea78b463 100644 --- a/src/keboola_agent_cli/services/storage_service.py +++ b/src/keboola_agent_cli/services/storage_service.py @@ -1362,7 +1362,7 @@ def swap_tables( alias: Project alias. table_id: Full ID of the first table. target_table_id: Full ID of the second table. - branch_id: Dev branch ID (must not be None). + branch_id: Branch ID (must not be None; any branch accepted, including the default/production branch). dry_run: If True, only report what would be swapped. Returns: diff --git a/tests/test_storage_swap.py b/tests/test_storage_swap.py index b8d78e5f..f4d1941c 100644 --- a/tests/test_storage_swap.py +++ b/tests/test_storage_swap.py @@ -433,7 +433,7 @@ def test_swap_missing_branch_fails_clearly(self, tmp_path: Path) -> None: ): MockStore.return_value = store svc = MockSvc.return_value - svc.swap_tables.side_effect = ConfigError("swap-tables requires a dev branch.") + svc.swap_tables.side_effect = ConfigError("swap-tables requires a branch.") result = runner.invoke( app, [ @@ -453,4 +453,4 @@ def test_swap_missing_branch_fails_clearly(self, tmp_path: Path) -> None: assert result.exit_code == 5 payload = json.loads(result.output) assert payload["status"] == "error" - assert "dev branch" in payload["error"]["message"] + assert "requires a branch" in payload["error"]["message"]