diff --git a/plugins/kbagent/skills/kbagent/references/gotchas.md b/plugins/kbagent/skills/kbagent/references/gotchas.md index c0d7b08d..2636221a 100644 --- a/plugins/kbagent/skills/kbagent/references/gotchas.md +++ b/plugins/kbagent/skills/kbagent/references/gotchas.md @@ -143,6 +143,12 @@ One project failing does not block others. Check the `errors` array: - **Branch scope**: when active branch is set, MCP tools and config commands automatically scope to that branch. `branch_id` is a **CLI flag** (`--branch`), NOT a tool input parameter -- do not pass it inside `--input`. Config read commands (`config list`, `config detail`, `config search`) also support `--branch`. +- **Storage read commands are the exception**: `storage buckets`, `storage bucket-detail`, + `storage tables`, `storage table-detail`, and `storage files` **ignore the implicit active + dev branch** and query production by default. The Storage API branch-scoped endpoint only + returns resources locally modified in the dev branch (empty for a fresh branch), so + auto-scoping would surprise users with "No tables found". Explicit `--branch ID` still + works. Storage **write** commands (create-*, upload-*, delete-*, file-*) stay branch-aware. - **Schema discovery**: use `kbagent --json tool list` to inspect each tool's `inputSchema` and find accepted parameters. For example, `get_configs` takes `configs` (a list of `{component_id, configuration_id}` objects), not a flat `config_id` string. diff --git a/src/keboola_agent_cli/commands/_helpers.py b/src/keboola_agent_cli/commands/_helpers.py index 700063a8..dc082a0e 100644 --- a/src/keboola_agent_cli/commands/_helpers.py +++ b/src/keboola_agent_cli/commands/_helpers.py @@ -176,6 +176,8 @@ def resolve_branch( formatter: OutputFormatter, project: str | None, branch: int | None, + *, + ignore_active_branch: bool = False, ) -> tuple[str | None, int | None]: """Resolve the effective branch and project. @@ -192,6 +194,14 @@ def resolve_branch( formatter: Output formatter for info messages. project: Explicit --project alias or None. branch: Explicit --branch integer or None. + ignore_active_branch: When True, the implicit active_branch_id from + config is ignored and the production endpoint (branch_id=None) is + used unless --branch was passed explicitly. An info message is + printed so the user can see the active dev branch was skipped. + Intended for storage READ commands -- the Storage API + branch-scoped endpoint returns only locally-modified resources, + which for a freshly created dev branch is an empty set. Explicit + --branch still overrides. Returns: Tuple of (effective_project, effective_branch_id). @@ -202,6 +212,14 @@ def resolve_branch( if project is not None: proj_config = config_store.get_project(project) if proj_config and proj_config.active_branch_id is not None: + if ignore_active_branch: + if not formatter.json_mode: + formatter.err_console.print( + f"[bold blue]Info:[/bold blue] Using production branch for read " + f"(active dev branch '{proj_config.active_branch_id}' ignored; " + f"pass --branch {proj_config.active_branch_id} to override)" + ) + return project, None if not formatter.json_mode: formatter.err_console.print( f"[bold blue]Info:[/bold blue] Using active branch " @@ -217,6 +235,14 @@ def resolve_branch( ] if len(active_projects) == 1: alias, proj = active_projects[0] + if ignore_active_branch: + if not formatter.json_mode: + formatter.err_console.print( + f"[bold blue]Info:[/bold blue] Using production branch for read " + f"(active dev branch '{proj.active_branch_id}' on project '{alias}' " + f"ignored; pass --branch {proj.active_branch_id} to override)" + ) + return alias, None if not formatter.json_mode: formatter.err_console.print( f"[bold blue]Info:[/bold blue] Using active branch " diff --git a/src/keboola_agent_cli/commands/context.py b/src/keboola_agent_cli/commands/context.py index cd32ec30..a41a4a89 100644 --- a/src/keboola_agent_cli/commands/context.py +++ b/src/keboola_agent_cli/commands/context.py @@ -155,17 +155,30 @@ ### Storage +Note on branches: storage READ commands (buckets, bucket-detail, tables, +table-detail, files, file-detail) use the production endpoint by default, +even when a dev branch is active via `branch use`. The Storage API +branch-scoped endpoint returns only resources that were locally modified +in the dev branch, so a freshly-created branch lists nothing. Pass +`--branch ID` explicitly to query dev-branch-local tables/buckets. +Storage WRITE commands (create-*, upload-*, delete-*, file-upload, etc.) +remain branch-aware because modifying a dev branch is the expected intent. + kbagent storage buckets [--project NAME] [--branch ID] - List buckets with sharing/linked info. Shows source project for linked buckets. Branch-aware. + List buckets with sharing/linked info. Shows source project for linked buckets. + Uses production by default; pass --branch to query a dev branch explicitly. kbagent storage bucket-detail --project NAME --bucket-id BUCKET_ID [--branch ID] - Bucket detail with Snowflake direct access paths. Resolves linked bucket source DB. Branch-aware. + Bucket detail with Snowflake direct access paths. Resolves linked bucket source DB. + Uses production by default; pass --branch to query a dev branch explicitly. kbagent storage tables --project NAME [--bucket-id BUCKET_ID] [--branch ID] - List storage tables, optionally filtered by bucket. Branch-aware. + List storage tables, optionally filtered by bucket. + Uses production by default; pass --branch to query a dev branch explicitly. kbagent storage table-detail --project NAME --table-id TABLE_ID [--branch ID] - Show detailed table info: columns (with types if available), primary key, row count, size, last import date. Branch-aware. + Show detailed table info: columns (with types if available), primary key, row count, size, last import date. + Uses production by default; pass --branch to query a dev branch explicitly. kbagent storage create-bucket --project NAME --stage STAGE --name BUCKET_NAME [--description D] [--backend B] [--branch ID] Create a new storage bucket. Stage must be "in" or "out". Branch-aware. @@ -196,7 +209,8 @@ ### Storage Files kbagent storage files --project NAME [--tag TAG ...] [--limit N] [--offset N] [--query Q] [--branch ID] - List Storage Files. --tag filters by tags (AND logic, repeat for multiple). --query for full-text search on name. Branch-aware. + List Storage Files. --tag filters by tags (AND logic, repeat for multiple). --query for full-text search on name. + Uses production by default; pass --branch to query a dev branch explicitly. kbagent storage file-upload --project NAME --file PATH [--name NAME] [--tag TAG ...] [--permanent] [--branch ID] Upload any file to Storage Files. --tag assigns tags (repeatable). --permanent prevents auto-deletion after 15 days. diff --git a/src/keboola_agent_cli/commands/storage.py b/src/keboola_agent_cli/commands/storage.py index 837c9519..adcdc171 100644 --- a/src/keboola_agent_cli/commands/storage.py +++ b/src/keboola_agent_cli/commands/storage.py @@ -53,6 +53,12 @@ def storage_buckets( Shows which buckets are linked from other projects, including the source project ID and name. This information is not available via MCP tools. + + Branch handling: this read command uses the production endpoint by + default, even when a dev branch is active via `branch use`. The + Storage API branch-scoped endpoint only returns locally-modified + buckets, so a fresh dev branch lists nothing. Pass --branch to query + a dev branch explicitly. """ if should_hint(ctx): emit_hint(ctx, "storage.buckets", project=project, branch=branch) @@ -69,10 +75,16 @@ def storage_buckets( ) raise typer.Exit(code=2) - # Resolve active branch for single-project queries + # Resolve active branch for single-project queries. + # Storage read commands ignore the implicit active dev branch: the + # Storage API branch-scoped endpoint returns only locally-modified + # buckets, which for a freshly created dev branch is an empty set. + # Explicit --branch still wins. effective_branch: int | None = branch if branch is None and project and len(project) == 1: - _, effective_branch = resolve_branch(config_store, formatter, project[0], None) + _, effective_branch = resolve_branch( + config_store, formatter, project[0], None, ignore_active_branch=True + ) try: result = service.list_buckets(aliases=project, branch_id=effective_branch) @@ -151,7 +163,10 @@ def storage_bucket_detail( formatter = get_formatter(ctx) service = get_service(ctx, "storage_service") config_store: ConfigStore = ctx.obj["config_store"] - _, effective_branch = resolve_branch(config_store, formatter, project, branch) + # Read command: ignore implicit active dev branch (empty listing trap). + _, effective_branch = resolve_branch( + config_store, formatter, project, branch, ignore_active_branch=True + ) try: result = service.get_bucket_detail( @@ -227,14 +242,24 @@ def storage_tables( help="Dev branch ID (defaults to active branch if set via 'branch use')", ), ) -> None: - """List storage tables from a project.""" + """List storage tables from a project. + + Branch handling: this read command uses the production endpoint by + default, even when a dev branch is active via `branch use`. The + Storage API branch-scoped endpoint only returns tables that were + locally modified in the dev branch, so a fresh dev branch lists + nothing. Pass --branch to query a dev branch explicitly. + """ if should_hint(ctx): emit_hint(ctx, "storage.tables", project=project, bucket_id=bucket_id, branch=branch) formatter = get_formatter(ctx) service = get_service(ctx, "storage_service") config_store: ConfigStore = ctx.obj["config_store"] - _, effective_branch = resolve_branch(config_store, formatter, project, branch) + # Read command: ignore implicit active dev branch (empty listing trap). + _, effective_branch = resolve_branch( + config_store, formatter, project, branch, ignore_active_branch=True + ) try: result = service.list_tables( @@ -306,7 +331,10 @@ def storage_table_detail( formatter = get_formatter(ctx) service = get_service(ctx, "storage_service") config_store: ConfigStore = ctx.obj["config_store"] - _, effective_branch = resolve_branch(config_store, formatter, project, branch) + # Read command: ignore implicit active dev branch (empty listing trap). + _, effective_branch = resolve_branch( + config_store, formatter, project, branch, ignore_active_branch=True + ) try: result = service.get_table_detail( @@ -1152,7 +1180,10 @@ def storage_file_list( formatter = get_formatter(ctx) service = get_service(ctx, "storage_service") config_store: ConfigStore = ctx.obj["config_store"] - _, effective_branch = resolve_branch(config_store, formatter, project, branch) + # Read command: ignore implicit active dev branch (empty listing trap). + _, effective_branch = resolve_branch( + config_store, formatter, project, branch, ignore_active_branch=True + ) try: result = service.list_files( diff --git a/tests/test_helpers.py b/tests/test_helpers.py index f685d54d..d076f9dc 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -227,3 +227,148 @@ def test_resolve_branch_no_branch_returns_none(self, tmp_config_dir) -> None: project, branch_id = resolve_branch(store, formatter, "prod", None) assert project == "prod" assert branch_id is None + + def test_ignore_active_branch_returns_none_when_active_set(self, tmp_config_dir) -> None: + """With ignore_active_branch=True, implicit active_branch_id is skipped. + + Used by storage read commands so users with an active dev branch + still see production tables/buckets by default. + """ + from unittest.mock import MagicMock + + from keboola_agent_cli.commands._helpers import resolve_branch + from keboola_agent_cli.config_store import ConfigStore + from keboola_agent_cli.models import ProjectConfig + + store = ConfigStore(config_dir=tmp_config_dir) + store.add_project( + "prod", + ProjectConfig( + stack_url="https://connection.keboola.com", + token="tok-123", + active_branch_id=15931, + ), + ) + + formatter = MagicMock(json_mode=False) + formatter.err_console = MagicMock() + + project, branch_id = resolve_branch( + store, formatter, "prod", None, ignore_active_branch=True + ) + assert project == "prod" + assert branch_id is None + # User must be told production is being used despite active dev branch. + formatter.err_console.print.assert_called_once() + msg = formatter.err_console.print.call_args.args[0] + assert "production" in msg.lower() + assert "15931" in msg + + def test_ignore_active_branch_does_not_override_explicit_branch(self, tmp_config_dir) -> None: + """Explicit --branch wins even when ignore_active_branch=True.""" + from unittest.mock import MagicMock + + from keboola_agent_cli.commands._helpers import resolve_branch + from keboola_agent_cli.config_store import ConfigStore + from keboola_agent_cli.models import ProjectConfig + + store = ConfigStore(config_dir=tmp_config_dir) + store.add_project( + "prod", + ProjectConfig( + stack_url="https://connection.keboola.com", + token="tok-123", + active_branch_id=15931, + ), + ) + + formatter = MagicMock(json_mode=False) + formatter.err_console = MagicMock() + + project, branch_id = resolve_branch(store, formatter, "prod", 99, ignore_active_branch=True) + assert project == "prod" + assert branch_id == 99 + + def test_ignore_active_branch_no_config_returns_none(self, tmp_config_dir) -> None: + """With ignore_active_branch=True and no active branch, still returns None.""" + from unittest.mock import MagicMock + + from keboola_agent_cli.commands._helpers import resolve_branch + from keboola_agent_cli.config_store import ConfigStore + from keboola_agent_cli.models import ProjectConfig + + store = ConfigStore(config_dir=tmp_config_dir) + store.add_project( + "prod", + ProjectConfig( + stack_url="https://connection.keboola.com", + token="tok-123", + ), + ) + + formatter = MagicMock(json_mode=False) + formatter.err_console = MagicMock() + + project, branch_id = resolve_branch( + store, formatter, "prod", None, ignore_active_branch=True + ) + assert project == "prod" + assert branch_id is None + # No info message needed -- there was no active branch to ignore. + formatter.err_console.print.assert_not_called() + + def test_ignore_active_branch_json_mode_silent(self, tmp_config_dir) -> None: + """In --json mode, ignore_active_branch still works but prints nothing.""" + from unittest.mock import MagicMock + + from keboola_agent_cli.commands._helpers import resolve_branch + from keboola_agent_cli.config_store import ConfigStore + from keboola_agent_cli.models import ProjectConfig + + store = ConfigStore(config_dir=tmp_config_dir) + store.add_project( + "prod", + ProjectConfig( + stack_url="https://connection.keboola.com", + token="tok-123", + active_branch_id=15931, + ), + ) + + formatter = MagicMock(json_mode=True) + formatter.err_console = MagicMock() + + project, branch_id = resolve_branch( + store, formatter, "prod", None, ignore_active_branch=True + ) + assert project == "prod" + assert branch_id is None + formatter.err_console.print.assert_not_called() + + def test_ignore_active_branch_single_project_inferred(self, tmp_config_dir) -> None: + """Without --project, if a single project has an active branch and + ignore_active_branch=True, the project is still returned but branch_id is None. + """ + from unittest.mock import MagicMock + + from keboola_agent_cli.commands._helpers import resolve_branch + from keboola_agent_cli.config_store import ConfigStore + from keboola_agent_cli.models import ProjectConfig + + store = ConfigStore(config_dir=tmp_config_dir) + store.add_project( + "prod", + ProjectConfig( + stack_url="https://connection.keboola.com", + token="tok-123", + active_branch_id=15931, + ), + ) + + formatter = MagicMock(json_mode=False) + formatter.err_console = MagicMock() + + project, branch_id = resolve_branch(store, formatter, None, None, ignore_active_branch=True) + assert project == "prod" + assert branch_id is None + formatter.err_console.print.assert_called_once() diff --git a/tests/test_storage_delete.py b/tests/test_storage_delete.py index d1a04cd2..5b4c2bf0 100644 --- a/tests/test_storage_delete.py +++ b/tests/test_storage_delete.py @@ -957,3 +957,324 @@ def test_cli_branch_flag_json(self, tmp_path: Path) -> None: assert result.exit_code == 0 call_kwargs = svc.delete_columns.call_args.kwargs assert call_kwargs["branch_id"] == 42 + + +def _make_store_with_active_branch(tmp_path: Path, branch_id: int) -> ConfigStore: + """Build a config store with one project that has an active dev branch.""" + store = _make_store(tmp_path) + store.set_project_branch("test", branch_id) + return store + + +class TestStorageReadIgnoresActiveBranch: + """Regression tests for GitHub issue #207. + + Storage READ commands (tables, buckets, bucket-detail, table-detail, + files) must NOT follow the implicit active dev branch -- the Storage + API branch-scoped endpoint returns only locally-modified resources, + so a fresh dev branch lists nothing. Explicit --branch still wins. + Storage WRITE commands stay branch-aware (user intent to modify). + """ + + # Read commands --------------------------------------------------------- + + def test_storage_tables_with_active_branch_uses_production(self, tmp_path: Path) -> None: + """storage tables sends branch_id=None when only an active branch is set.""" + store = _make_store_with_active_branch(tmp_path, 15931) + with ( + patch("keboola_agent_cli.cli.ConfigStore") as MockStore, + patch("keboola_agent_cli.cli.StorageService") as MockSvc, + ): + MockStore.return_value = store + svc = MockSvc.return_value + svc.list_tables.return_value = {"tables": [], "project_alias": "test"} + + result = runner.invoke( + app, + ["--json", "storage", "tables", "--project", "test"], + ) + + assert result.exit_code == 0, result.output + call_kwargs = svc.list_tables.call_args.kwargs + assert call_kwargs["branch_id"] is None + + def test_storage_tables_explicit_branch_overrides_active(self, tmp_path: Path) -> None: + """Explicit --branch wins even when active_branch_id is set.""" + store = _make_store_with_active_branch(tmp_path, 15931) + with ( + patch("keboola_agent_cli.cli.ConfigStore") as MockStore, + patch("keboola_agent_cli.cli.StorageService") as MockSvc, + ): + MockStore.return_value = store + svc = MockSvc.return_value + svc.list_tables.return_value = {"tables": [], "project_alias": "test"} + + result = runner.invoke( + app, + [ + "--json", + "storage", + "tables", + "--project", + "test", + "--branch", + "99", + ], + ) + + assert result.exit_code == 0, result.output + call_kwargs = svc.list_tables.call_args.kwargs + assert call_kwargs["branch_id"] == 99 + + def test_storage_buckets_with_active_branch_uses_production(self, tmp_path: Path) -> None: + """storage buckets ignores active_branch_id (single-project case).""" + store = _make_store_with_active_branch(tmp_path, 15931) + with ( + patch("keboola_agent_cli.cli.ConfigStore") as MockStore, + patch("keboola_agent_cli.cli.StorageService") as MockSvc, + ): + MockStore.return_value = store + svc = MockSvc.return_value + svc.list_buckets.return_value = {"buckets": [], "errors": []} + + result = runner.invoke( + app, + ["--json", "storage", "buckets", "--project", "test"], + ) + + assert result.exit_code == 0, result.output + call_kwargs = svc.list_buckets.call_args.kwargs + assert call_kwargs["branch_id"] is None + + def test_storage_buckets_explicit_branch_overrides_active(self, tmp_path: Path) -> None: + """Explicit --branch wins for storage buckets too.""" + store = _make_store_with_active_branch(tmp_path, 15931) + with ( + patch("keboola_agent_cli.cli.ConfigStore") as MockStore, + patch("keboola_agent_cli.cli.StorageService") as MockSvc, + ): + MockStore.return_value = store + svc = MockSvc.return_value + svc.list_buckets.return_value = {"buckets": [], "errors": []} + + result = runner.invoke( + app, + [ + "--json", + "storage", + "buckets", + "--project", + "test", + "--branch", + "77", + ], + ) + + assert result.exit_code == 0, result.output + call_kwargs = svc.list_buckets.call_args.kwargs + assert call_kwargs["branch_id"] == 77 + + def test_storage_bucket_detail_with_active_branch_uses_production(self, tmp_path: Path) -> None: + """storage bucket-detail sends branch_id=None despite active dev branch.""" + store = _make_store_with_active_branch(tmp_path, 15931) + with ( + patch("keboola_agent_cli.cli.ConfigStore") as MockStore, + patch("keboola_agent_cli.cli.StorageService") as MockSvc, + ): + MockStore.return_value = store + svc = MockSvc.return_value + svc.get_bucket_detail.return_value = { + "project_alias": "test", + "project_id": 1234, + "bucket_id": "in.c-data", + "display_name": "data", + "stage": "in", + "description": "", + "backend": "snowflake", + "is_linked": False, + "source_project_id": None, + "source_project_name": "", + "source_bucket_id": "", + "snowflake_database": "SAPI_1234", + "snowflake_schema": "in.c-data", + "tables": [], + "table_count": 0, + } + + result = runner.invoke( + app, + [ + "--json", + "storage", + "bucket-detail", + "--project", + "test", + "--bucket-id", + "in.c-data", + ], + ) + + assert result.exit_code == 0, result.output + call_kwargs = svc.get_bucket_detail.call_args.kwargs + assert call_kwargs["branch_id"] is None + + def test_storage_table_detail_with_active_branch_uses_production(self, tmp_path: Path) -> None: + """storage table-detail sends branch_id=None despite active dev branch.""" + store = _make_store_with_active_branch(tmp_path, 15931) + with ( + patch("keboola_agent_cli.cli.ConfigStore") as MockStore, + patch("keboola_agent_cli.cli.StorageService") as MockSvc, + ): + MockStore.return_value = store + svc = MockSvc.return_value + svc.get_table_detail.return_value = { + "project_alias": "test", + "project_id": 1234, + "table_id": "in.c-data.users", + "name": "users", + "display_name": "users", + "bucket_id": "in.c-data", + "rows_count": 10, + "data_size_bytes": 1024, + "primary_key": [], + "column_details": [], + "last_import_date": None, + } + + result = runner.invoke( + app, + [ + "--json", + "storage", + "table-detail", + "--project", + "test", + "--table-id", + "in.c-data.users", + ], + ) + + assert result.exit_code == 0, result.output + call_kwargs = svc.get_table_detail.call_args.kwargs + assert call_kwargs["branch_id"] is None + + def test_storage_files_with_active_branch_uses_production(self, tmp_path: Path) -> None: + """storage files sends branch_id=None despite active dev branch.""" + store = _make_store_with_active_branch(tmp_path, 15931) + with ( + patch("keboola_agent_cli.cli.ConfigStore") as MockStore, + patch("keboola_agent_cli.cli.StorageService") as MockSvc, + ): + MockStore.return_value = store + svc = MockSvc.return_value + svc.list_files.return_value = {"files": [], "count": 0} + + result = runner.invoke( + app, + ["--json", "storage", "files", "--project", "test"], + ) + + assert result.exit_code == 0, result.output + call_kwargs = svc.list_files.call_args.kwargs + assert call_kwargs["branch_id"] is None + + def test_storage_files_explicit_branch_overrides_active(self, tmp_path: Path) -> None: + """Explicit --branch wins for storage files.""" + store = _make_store_with_active_branch(tmp_path, 15931) + with ( + patch("keboola_agent_cli.cli.ConfigStore") as MockStore, + patch("keboola_agent_cli.cli.StorageService") as MockSvc, + ): + MockStore.return_value = store + svc = MockSvc.return_value + svc.list_files.return_value = {"files": [], "count": 0} + + result = runner.invoke( + app, + [ + "--json", + "storage", + "files", + "--project", + "test", + "--branch", + "55", + ], + ) + + assert result.exit_code == 0, result.output + call_kwargs = svc.list_files.call_args.kwargs + assert call_kwargs["branch_id"] == 55 + + # Write commands remain branch-aware ----------------------------------- + + def test_storage_create_bucket_still_follows_active_branch(self, tmp_path: Path) -> None: + """Write command create-bucket keeps branch-awareness (user intent).""" + store = _make_store_with_active_branch(tmp_path, 15931) + with ( + patch("keboola_agent_cli.cli.ConfigStore") as MockStore, + patch("keboola_agent_cli.cli.StorageService") as MockSvc, + ): + MockStore.return_value = store + svc = MockSvc.return_value + svc.create_bucket.return_value = { + "id": "in.c-foo", + "stage": "in", + "project_alias": "test", + "display_name": "foo", + "description": "", + "backend": "snowflake", + } + + result = runner.invoke( + app, + [ + "--json", + "storage", + "create-bucket", + "--project", + "test", + "--stage", + "in", + "--name", + "foo", + ], + ) + + assert result.exit_code == 0, result.output + call_kwargs = svc.create_bucket.call_args.kwargs + assert call_kwargs["branch_id"] == 15931 + + def test_storage_delete_table_still_follows_active_branch(self, tmp_path: Path) -> None: + """Destructive delete-table keeps branch-awareness.""" + store = _make_store_with_active_branch(tmp_path, 15931) + with ( + patch("keboola_agent_cli.cli.ConfigStore") as MockStore, + patch("keboola_agent_cli.cli.StorageService") as MockSvc, + ): + MockStore.return_value = store + svc = MockSvc.return_value + svc.delete_tables.return_value = { + "deleted": ["in.c-data.users"], + "failed": [], + "dry_run": False, + "project_alias": "test", + } + + result = runner.invoke( + app, + [ + "--json", + "storage", + "delete-table", + "--project", + "test", + "--table-id", + "in.c-data.users", + "--yes", + ], + ) + + assert result.exit_code == 0, result.output + call_kwargs = svc.delete_tables.call_args.kwargs + assert call_kwargs["branch_id"] == 15931