// test_llm_anthropic.cpp — Catch2 tests for llm_anthropic pure helpers. // Issue 0081. Does NOT make real HTTP calls. Uses FN_LLM_MOCK_RESPONSE for // call_api injection. Real roundtrip is a manual_test (requires API key). #include "catch_amalgamated.hpp" #include "core/llm_anthropic.h" #include #include #include using namespace llm_anthropic; using namespace data_table; // ============================================================================ // build_request_body (pure) // ============================================================================ TEST_CASE("build_request_body: contains model field", "[llm_anthropic]") { AskInput in; in.question = "show me total sales by region"; in.col_names = {"region", "amount"}; in.col_types = {ColumnType::String, ColumnType::Float}; in.mode = OutputMode::TQL; std::string body = build_request_body(in); REQUIRE(body.find("\"model\"") != std::string::npos); REQUIRE(body.find("claude") != std::string::npos); } TEST_CASE("build_request_body: contains messages array", "[llm_anthropic]") { AskInput in; in.question = "top 10 by revenue"; std::string body = build_request_body(in); REQUIRE(body.find("\"messages\"") != std::string::npos); REQUIRE(body.find("\"user\"") != std::string::npos); } TEST_CASE("build_request_body: question appears in user content", "[llm_anthropic]") { AskInput in; in.question = "unique_question_marker_xyz"; std::string body = build_request_body(in); // The question is JSON-escaped inside "content", so the marker must appear. REQUIRE(body.find("unique_question_marker_xyz") != std::string::npos); } TEST_CASE("build_request_body: col_names appear in schema block", "[llm_anthropic]") { AskInput in; in.question = "q"; in.col_names = {"customer_id", "purchase_date", "total_amount"}; in.col_types = {ColumnType::Int, ColumnType::Date, ColumnType::Float}; std::string body = build_request_body(in); REQUIRE(body.find("customer_id") != std::string::npos); REQUIRE(body.find("purchase_date") != std::string::npos); REQUIRE(body.find("total_amount") != std::string::npos); } TEST_CASE("build_request_body: custom model overrides default", "[llm_anthropic]") { AskInput in; in.question = "q"; in.model = "claude-opus-4-5"; std::string body = build_request_body(in); REQUIRE(body.find("claude-opus-4-5") != std::string::npos); } TEST_CASE("build_request_body: SQL mode produces SQL system prompt", "[llm_anthropic]") { AskInput in; in.question = "q"; in.mode = OutputMode::SQL; std::string body = build_request_body(in); REQUIRE(body.find("DuckDB") != std::string::npos); } TEST_CASE("build_request_body: current TQL appears in body", "[llm_anthropic]") { AskInput in; in.question = "q"; in.tql_current = "return { version=1, stages={} }"; std::string body = build_request_body(in); REQUIRE(body.find("version=1") != std::string::npos); } TEST_CASE("build_request_body: joinable tables appear in schema", "[llm_anthropic]") { AskInput in; in.question = "q"; in.joinable_names = {"products_table", "customers_table"}; std::string body = build_request_body(in); REQUIRE(body.find("products_table") != std::string::npos); REQUIRE(body.find("customers_table") != std::string::npos); } // ============================================================================ // extract_code_block (pure) // ============================================================================ TEST_CASE("extract_code_block: extracts lua fence", "[llm_anthropic]") { std::string raw = "Here is the TQL:\n```lua\nreturn { version=1 }\n```\nDone."; std::string code = extract_code_block(raw, "lua"); REQUIRE(code == "return { version=1 }"); } TEST_CASE("extract_code_block: extracts sql fence", "[llm_anthropic]") { std::string raw = "```sql\nSELECT * FROM t;\n```"; std::string code = extract_code_block(raw, "sql"); REQUIRE(code == "SELECT * FROM t;"); } TEST_CASE("extract_code_block: fallback to plain fence without lang", "[llm_anthropic]") { std::string raw = "```\nSOME CODE\n```"; std::string code = extract_code_block(raw, "lua"); REQUIRE(code.find("SOME CODE") != std::string::npos); } TEST_CASE("extract_code_block: no fence returns stripped raw", "[llm_anthropic]") { std::string raw = " return { version=1 } "; std::string code = extract_code_block(raw, "lua"); REQUIRE(code == "return { version=1 }"); } // ============================================================================ // parse_response_text (pure) // ============================================================================ TEST_CASE("parse_response_text: extracts text from Anthropic response JSON", "[llm_anthropic]") { std::string json = R"({"id":"msg_01","content":[{"type":"text","text":"Hello world"}],"model":"claude-sonnet-4-6"})"; std::string text = parse_response_text(json); REQUIRE(text == "Hello world"); } TEST_CASE("parse_response_text: handles escaped newline in text", "[llm_anthropic]") { std::string json = R"({"content":[{"type":"text","text":"line1\nline2"}]})"; std::string text = parse_response_text(json); REQUIRE(text.find("line1") != std::string::npos); REQUIRE(text.find("line2") != std::string::npos); } TEST_CASE("parse_response_text: empty text on missing field", "[llm_anthropic]") { std::string json = R"({"error":"no_key"})"; std::string text = parse_response_text(json); REQUIRE(text.empty()); } // ============================================================================ // call_api: mock injection via FN_LLM_MOCK_RESPONSE // ============================================================================ TEST_CASE("call_api: mock response injection via env var", "[llm_anthropic]") { // Set mock response so no real HTTP call is made. setenv("FN_LLM_MOCK_RESPONSE", R"({"content":[{"type":"text","text":"```lua\nreturn {}\n```"}]})", 1); std::string err; std::string resp = call_api("{}", "", err); REQUIRE(err.empty()); REQUIRE(resp.find("content") != std::string::npos); unsetenv("FN_LLM_MOCK_RESPONSE"); } // ============================================================================ // ask: end-to-end with mock (no real HTTP) // ============================================================================ TEST_CASE("ask: mock roundtrip produces code block", "[llm_anthropic]") { setenv("FN_LLM_MOCK_RESPONSE", R"({"content":[{"type":"text","text":"```lua\nreturn { version=1 }\n```"}]})", 1); AskInput in; in.question = "show all rows"; in.col_names = {"name", "value"}; in.col_types = {ColumnType::String, ColumnType::Float}; in.mode = OutputMode::TQL; AskResult r = ask(in); REQUIRE(r.error.empty()); REQUIRE(r.code.find("version=1") != std::string::npos); unsetenv("FN_LLM_MOCK_RESPONSE"); }