a03675113a
- .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>
182 lines
6.9 KiB
C++
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");
|
|
}
|