"""Tests for agnes explore command.""" import json import pytest from unittest.mock import patch, MagicMock from typer.testing import CliRunner from cli.main import app runner = CliRunner() @pytest.fixture(autouse=True) def tmp_config(tmp_path, monkeypatch): monkeypatch.setenv("AGNES_CONFIG_DIR", str(tmp_path / "config")) monkeypatch.setenv("AGNES_LOCAL_DIR", str(tmp_path / "local")) (tmp_path / "config").mkdir() (tmp_path / "local").mkdir() yield tmp_path def _resp(status_code=200, json_data=None, text=""): r = MagicMock() r.status_code = status_code r.json.return_value = json_data if json_data is not None else {} r.text = text return r def _make_local_db(tmp_config): """Create a local DuckDB with a sample table for exploration tests.""" import duckdb db_dir = tmp_config / "local" / "user" / "duckdb" db_dir.mkdir(parents=True) conn = duckdb.connect(str(db_dir / "analytics.duckdb")) conn.execute("CREATE TABLE orders (id INTEGER, amount DOUBLE, status VARCHAR)") conn.executemany("INSERT INTO orders VALUES (?, ?, ?)", [ (1, 99.5, "shipped"), (2, 200.0, "pending"), (3, 50.0, "shipped"), ]) conn.close() return db_dir / "analytics.duckdb" class TestExploreLocal: def test_explore_existing_table(self, tmp_config): """Exploring an existing local table shows row count and columns.""" _make_local_db(tmp_config) result = runner.invoke(app, ["explore", "orders"]) assert result.exit_code == 0 assert "orders" in result.output assert "3" in result.output # row count def test_explore_no_db(self, tmp_config): """Exploring without local DB exits with guidance.""" result = runner.invoke(app, ["explore", "orders"]) assert result.exit_code == 1 assert "not found" in result.output.lower() or "sync" in result.output.lower() def test_explore_missing_table(self, tmp_config): """Exploring a non-existent table exits with error.""" _make_local_db(tmp_config) result = runner.invoke(app, ["explore", "nonexistent_xyz"]) assert result.exit_code == 1 assert "not found" in result.output.lower() or "nonexistent" in result.output.lower() def test_explore_json_flag(self, tmp_config): """--json flag produces valid JSON with table info. Note: with explore's callback pattern, options must precede positional args. """ _make_local_db(tmp_config) result = runner.invoke(app, ["explore", "--json", "orders"]) assert result.exit_code == 0 data = json.loads(result.output) assert data["table"] == "orders" assert data["row_count"] == 3 assert len(data["columns"]) == 3 assert len(data["sample_rows"]) <= 5 class TestExploreRemote: def test_explore_remote_success(self): """--remote flag fetches catalog profile from server. api_get is imported inside _explore_remote so mock the source module. """ profile = { "table": "orders", "row_count": 1000, "columns": [{"name": "id"}, {"name": "amount"}], } with patch("cli.client.api_get", return_value=_resp(200, profile)): result = runner.invoke(app, ["explore", "--remote", "orders"]) assert result.exit_code == 0 assert "orders" in result.output def test_explore_remote_not_found(self): """--remote with unknown table exits with error.""" with patch("cli.client.api_get", return_value=_resp(404, {"detail": "Not found"}, "Not found")): result = runner.invoke(app, ["explore", "--remote", "unknown_table"]) assert result.exit_code == 1 assert "not found" in result.output.lower() or "Profile not found" in result.output def test_explore_remote_json_flag(self): """--remote --json outputs raw API JSON.""" profile = {"table": "orders", "row_count": 500} with patch("cli.client.api_get", return_value=_resp(200, profile)): result = runner.invoke(app, ["explore", "--remote", "--json", "orders"]) assert result.exit_code == 0 data = json.loads(result.output) assert data["table"] == "orders"