File size: 6,793 Bytes
3c65377
 
c3449ec
34f7221
3c65377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c3449ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30e135b
c3449ec
 
 
 
30e135b
c3449ec
 
 
 
 
 
 
 
 
 
 
3c65377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34f7221
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21ee86d
34f7221
 
 
 
 
 
 
 
 
 
 
 
 
 
21ee86d
34f7221
 
 
 
 
 
 
4489319
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
from thoughtspot_errors import friendly_mcp_liveboard_error
from tests.e2e_quality import extract_db_schema_from_tml_docs
import liveboard_creator
import thoughtspot_deployer


def test_friendly_mcp_error_classifies_service_outage():
    message, category = friendly_mcp_liveboard_error(
        "MCP liveboard timed out after 360s — MCP endpoint unresponsive at network level"
    )

    assert category == "mcp_service"
    assert "MCP service appears unavailable or timed out" in message
    assert "Raw MCP error:" in message


def test_friendly_mcp_error_classifies_auth_failure():
    message, category = friendly_mcp_liveboard_error(
        "Failed to get trusted auth token. Check THOUGHTSPOT_TRUSTED_AUTH_KEY"
    )

    assert category == "mcp_auth"
    assert "could not authenticate" in message
    assert "trusted auth key" in message.lower()


def test_friendly_mcp_error_classifies_generation_failure():
    message, category = friendly_mcp_liveboard_error(
        "No questions generated. Model may be empty or incompatible."
    )

    assert category == "mcp_generation"
    assert "MCP liveboard generation step" in message
    assert "falling back" in message


def test_friendly_mcp_error_classifies_missing_answer_tokens_as_recoverable():
    message, category = friendly_mcp_liveboard_error(
        "No liveboard-compatible answers with ThoughtSpot session/tokens were retrieved."
    )

    assert category == "mcp_answer_tokens"
    assert "backup liveboard" in message


def test_direct_answer_rejects_empty_token_payload(monkeypatch):
    class FakeResponse:
        status_code = 200
        text = ""

        def json(self):
            return {
                "session_identifier": "session-123",
                "generation_number": -1,
                "message_type": "TSAnswer",
                "visualization_type": "Undefined",
                "tokens": "",
                "display_tokens": "",
            }

    class FakeSession:
        def post(self, *args, **kwargs):
            return FakeResponse()

    monkeypatch.setattr(liveboard_creator, "_get_direct_api_session", lambda *args, **kwargs: (FakeSession(), "token"))

    assert liveboard_creator._get_answer_direct(
        "Total Net Sales",
        "model-guid",
        "testrunner@example.com",
        "secret",
        ts_url="https://example.thoughtspot.cloud",
    ) is None


def test_liveboard_answer_validation_requires_tokens_but_allows_undefined_visualization():
    assert liveboard_creator._is_liveboard_answer_valid(
        {
            "session_identifier": "session-123",
            "tokens": [{"token": "Net Sales"}],
            "visualization_type": "Undefined",
        }
    )
    assert not liveboard_creator._is_liveboard_answer_valid(
        {
            "session_identifier": "session-123",
            "tokens": "",
            "visualization_type": "Undefined",
        }
    )


def test_extract_db_schema_reads_physical_table_tml_not_model_name():
    docs = [
        """
model:
  name: TST_WRONG_01010000_ABC_mdl
""",
        """
table:
  name: SALES
  db: DEMOBUILD
  schema: TST_RIGHT_01010000_DEF_sch
""",
    ]

    assert extract_db_schema_from_tml_docs(docs) == ("DEMOBUILD", "TST_RIGHT_01010000_DEF_sch")


def test_extract_db_schema_does_not_guess_from_model_name():
    docs = [
        """
model:
  name: TST_WRONG_01010000_ABC_mdl
"""
    ]

    assert extract_db_schema_from_tml_docs(docs) == ("", "")


def test_connection_create_reconciles_secure_store_bad_gateway(monkeypatch):
    class FakeResponse:
        status_code = 200
        text = ""

        def json(self):
            return [
                {
                    "response": {
                        "status": {
                            "status_code": "ERROR",
                            "error_message": (
                                "Exception occurred while saving dataSource: "
                                "Error Code: SECURE_STORE_ERROR Error Message: Bad Gateway."
                            ),
                        },
                        "header": {"name": "MBB_VIZ_TEST_conn"},
                    }
                }
            ]

    class FakeSession:
        def __init__(self):
            self.posts = []

        def post(self, url, **kwargs):
            self.posts.append({"url": url, **kwargs})
            return FakeResponse()

    deployer = thoughtspot_deployer.ThoughtSpotDeployer.__new__(
        thoughtspot_deployer.ThoughtSpotDeployer
    )
    deployer.base_url = "https://ts.example"
    deployer.session = FakeSession()
    deployer.headers = {}
    deployer.create_connection_tml = lambda name, database: "connection:\n  name: MBB_VIZ_TEST_conn\n"

    calls = []

    def fake_get_connection_by_name(name):
        calls.append(name)
        if len(calls) == 1:
            return None
        return {"header": {"id_guid": "connection-guid", "name": name}}

    deployer.get_connection_by_name = fake_get_connection_by_name
    monkeypatch.setattr(thoughtspot_deployer.time, "sleep", lambda seconds: None)

    guid, fqn = deployer.create_connection_with_reconcile(
        "MBB_VIZ_TEST_conn",
        "DEMOBUILD_TEST",
        log_progress=lambda message: None,
    )

    assert guid == "connection-guid"
    assert fqn == "connection-guid"
    assert len(deployer.session.posts) == 1
    assert len(calls) == 2


def test_trusted_auth_retries_transient_gateway_error(monkeypatch):
    class FakeResponse:
        def __init__(self, status_code, payload=None, text=""):
            self.status_code = status_code
            self._payload = payload or {}
            self.text = text

        def json(self):
            return self._payload

    class FakeSession:
        def __init__(self):
            self.headers = {}
            self.calls = 0

        def post(self, url, **kwargs):
            self.calls += 1
            if self.calls == 1:
                return FakeResponse(502, text="Bad Gateway")
            return FakeResponse(200, {"token": "token-123"})

    deployer = thoughtspot_deployer.ThoughtSpotDeployer.__new__(
        thoughtspot_deployer.ThoughtSpotDeployer
    )
    deployer.base_url = "https://ts.example"
    deployer.username = "mike.boone@thoughtspot.com"
    deployer.secret_key = "secret-key-value"
    deployer.session = FakeSession()
    deployer.last_auth_status_code = None
    deployer.last_auth_error = ""

    monkeypatch.setenv("TS_AUTH_MAX_ATTEMPTS", "2")
    monkeypatch.setenv("TS_AUTH_RETRY_WAIT_SECONDS", "1")
    monkeypatch.setattr(thoughtspot_deployer.time, "sleep", lambda seconds: None)

    assert deployer.authenticate_trusted() is True
    assert deployer.session.calls == 2
    assert deployer.session.headers["Authorization"] == "Bearer token-123"