Files
fn_registry/cpp/tests/test_llm_anthropic.cpp
T
egutierrez 212875ed0d chore: auto-commit (286 archivos)
- .claude/agents/fn-orquestador/SKILL.md
- .claude/commands/fn_claude.md
- .claude/rules/INDEX.md
- .claude/rules/cpp_apps.md
- .claude/rules/ids_naming.md
- CHANGELOG.md
- apps/dag_engine/README.md
- apps/dag_engine/api.go
- apps/dag_engine/dags_migrated/example.yaml
- apps/dag_engine/dags_migrated/example_lineage_tracking.yaml
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 16:33:22 +02:00

182 lines
6.9 KiB
C++

// 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 <cstdlib>
#include <string>
#include <vector>
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");
}