Files
dstalk/tests/anthropic_plugin_test.cpp
XiuChengWu 8faa02c3d5 W17: extract ai_common shared module + fix anthropic data race + brace bugs
- New plugins_upper/ai_common/ static library: shared PluginConfig, ToolCallAccum,
  StreamContext, secure_zero, extract_host_port, serialize_tool_calls, free_chat_result
- Refactored openai/anthropic plugins to use dstalk_ai:: namespace from ai_common
- Fixed anthropic g_config raw pointer → std::atomic (data race)
- Added SSE parse error counter with threshold abort (kMaxSseParseErrors=5)
- Fixed missing closing brace in both plugins' error-body catch block
- Updated test targets: ai_common include path + link, using namespace dstalk_ai
- plugin_loader_test: added stub_unreg + service_registry.cpp for unregister_service
- Includes pre-existing uncommitted changes from prior waves

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-31 16:58:25 +08:00

590 lines
24 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* @file anthropic_plugin_test.cpp
* @brief Anthropic AI plugin unit tests: SSE parsing (parse_sse_data edge cases),
* request building (build_request_json), header construction, URL parsing
* (extract_host_port), secure_zero, and null-safety for free_result/configure.
* Anthropic AI 插件单元测试SSE 解析parse_sse_data 边界情况、请求构建build_request_json
* 头部构造、URL 解析extract_host_port、secure_zero、free_result/configure 空指针安全。
* Copyright (c) 2026 dstalk contributors. GPLv3.
*/
#define BOOST_JSON_HEADER_ONLY
#define BOOST_ALL_NO_LIB
#include "../plugins_upper/anthropic/src/anthropic_plugin.cpp"
using namespace dstalk_ai;
#include <cstring>
#include <iostream>
#include <string>
static int g_failures = 0;
// Lightweight assertion macro: increments g_failures counter on failure
#define CHECK(cond, msg) do { \
if (cond) { \
std::cout << "[OK] " << (msg) << "\n"; \
} else { \
std::cerr << "[FAIL] " << (msg) << "\n"; \
g_failures++; \
} \
} while (0)
// Test helper: populate g_cfg for build functions
// Test helper: populate g_cfg with valid anthropic defaults before build_* tests
// 测试辅助函数:为 build_* 测试填充 g_cfg 的有效 anthropic 默认值
static void setup_config() {
g_cfg.provider = "anthropic";
g_cfg.base_url = "https://api.anthropic.com";
g_cfg.api_key = "sk-ant-test-key-12345";
g_cfg.model = "claude-sonnet-4-20250514";
g_cfg.max_tokens = 4096;
g_cfg.temperature = 0.7;
}
// Anthropic 插件测试 (W21.6)parse_sse_data 畸形/无效 JSON、content_block_delta 文本提取、
// message_stop/忽略类型、深层/边界结构、build_request_json 基础+边界、build_headers_json、
// extract_host_port、secure_zero、my_free_result 空指针安全、my_configure 空指针安全。
// Anthropic plugin tests (W21.6): parse_sse_data for malformed/invalid JSON,
// content_block_delta text extraction, message_stop/ignored types, deep/edge structures,
// build_request_json basics+edges, build_headers_json, extract_host_port,
// secure_zero, my_free_result null-safety, and my_configure null-safety.
int main()
{
// ================================================================
// Test Block 1: parse_sse_data — invalid/malformed inputs
// 测试块 1parse_sse_data — 无效/畸形输入
// ================================================================
std::cout << "\n--- Block 1: parse_sse_data invalid/malformed ---\n";
{
std::string token;
bool ret = parse_sse_data("", token, nullptr);
CHECK(!ret, "T1.1: empty string returns false");
}
{
std::string token;
bool ret = parse_sse_data("not json at all", token, nullptr);
CHECK(!ret, "T1.2: non-JSON string returns false (no crash)");
}
{
std::string token;
bool ret = parse_sse_data("{}", token, nullptr);
CHECK(!ret, "T1.3: empty JSON object returns false");
}
{
std::string token;
bool ret = parse_sse_data("{\"notype\":\"x\"}", token, nullptr);
CHECK(!ret, "T1.4: JSON without 'type' field returns false");
}
{
std::string token;
bool ret = parse_sse_data("{\"type\":123}", token, nullptr);
CHECK(!ret, "T1.5: 'type' is number (not string) returns false");
}
{
// Malformed JSON: unclosed brace / 畸形 JSON未闭合的花括号
std::string token;
bool ret = parse_sse_data("{\"type\":\"ping\"", token, nullptr);
CHECK(!ret, "T1.6: malformed JSON (unclosed brace) returns false (no crash)");
}
{
// Random garbage bytes / 随机垃圾字节
std::string token;
bool ret = parse_sse_data("\x00\x01\xFF\xFE", token, nullptr);
CHECK(!ret, "T1.7: binary garbage returns false (no crash)");
}
// ================================================================
// Test Block 2: parse_sse_data — content_block_delta
// 测试块 2parse_sse_data — content_block_delta
// ================================================================
std::cout << "\n--- Block 2: parse_sse_data content_block_delta ---\n";
{
std::string token;
const char* json =
"{\"type\":\"content_block_delta\","
"\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello\"}}";
bool ret = parse_sse_data(json, token, nullptr);
CHECK(ret, "T2.1: text_delta with 'Hello' returns true");
CHECK(token == "Hello", "T2.2: token equals 'Hello'");
}
{
std::string token;
const char* json =
"{\"type\":\"content_block_delta\","
"\"delta\":{\"type\":\"text_delta\",\"text\":\"\"}}";
bool ret = parse_sse_data(json, token, nullptr);
CHECK(ret, "T2.3: text_delta with empty text returns true");
CHECK(token.empty(), "T2.4: token is empty string");
}
{
std::string token;
const char* json =
"{\"type\":\"content_block_delta\","
"\"delta\":{\"type\":\"text_delta\"}}";
bool ret = parse_sse_data(json, token, nullptr);
CHECK(!ret, "T2.5: text_delta missing 'text' field returns false");
}
{
std::string token;
const char* json =
"{\"type\":\"content_block_delta\","
"\"delta\":{\"type\":\"input_json_delta\","
"\"partial_json\":\"{\\\"foo\\\":\"}}";
bool ret = parse_sse_data(json, token, nullptr);
CHECK(!ret, "T2.6: input_json_delta (non-text) returns false");
}
{
std::string token;
const char* json =
"{\"type\":\"content_block_delta\","
"\"delta\":\"not_an_object\"}";
bool ret = parse_sse_data(json, token, nullptr);
CHECK(!ret, "T2.7: delta is string (not object) returns false");
}
{
std::string token;
const char* json =
"{\"type\":\"content_block_delta\","
"\"delta\":{\"no_type_here\":\"x\"}}";
bool ret = parse_sse_data(json, token, nullptr);
CHECK(!ret, "T2.8: delta without 'type' field returns false");
}
// ================================================================
// Test Block 3: parse_sse_data — message_stop / ignored types
// 测试块 3parse_sse_data — message_stop / 忽略的类型
// ================================================================
std::cout << "\n--- Block 3: parse_sse_data message_stop / ignored types ---\n";
{
std::string token = "SHOULD_BE_CLEARED";
const char* json = "{\"type\":\"message_stop\"}";
bool ret = parse_sse_data(json, token, nullptr);
CHECK(ret, "T3.1: message_stop returns true (stream end)");
CHECK(token.empty(), "T3.2: message_stop clears token");
}
{
std::string token;
const char* json = "{\"type\":\"message_start\",\"message\":{}}";
bool ret = parse_sse_data(json, token, nullptr);
CHECK(!ret, "T3.3: message_start returns false (ignored)");
}
{
std::string token;
const char* json = "{\"type\":\"content_block_start\"}";
bool ret = parse_sse_data(json, token, nullptr);
CHECK(!ret, "T3.4: content_block_start returns false (ignored)");
}
{
std::string token;
const char* json = "{\"type\":\"content_block_stop\"}";
bool ret = parse_sse_data(json, token, nullptr);
CHECK(!ret, "T3.5: content_block_stop returns false (ignored)");
}
{
std::string token;
const char* json = "{\"type\":\"ping\"}";
bool ret = parse_sse_data(json, token, nullptr);
CHECK(!ret, "T3.6: ping returns false (ignored)");
}
{
std::string token;
const char* json = "{\"type\":\"message_delta\"}";
bool ret = parse_sse_data(json, token, nullptr);
CHECK(!ret, "T3.7: message_delta returns false (ignored)");
}
// ================================================================
// Test Block 4: parse_sse_data — deeply nested / edge structures
// 测试块 4parse_sse_data — 深层嵌套 / 边界结构
// ================================================================
std::cout << "\n--- Block 4: parse_sse_data deep/edge structures ---\n";
{
// Unrecognized event type should just be ignored / 未识别的事件类型应被忽略
std::string token;
const char* json = "{\"type\":\"some_unknown_future_type\"}";
bool ret = parse_sse_data(json, token, nullptr);
CHECK(!ret, "T4.1: unknown type returns false (ignored)");
}
{
// text_delta with unicode content (Japanese) / 含 unicode 内容的 text_delta日语
std::string token;
const char* json =
"{\"type\":\"content_block_delta\","
"\"delta\":{\"type\":\"text_delta\","
"\"text\":\"\\u3053\\u3093\\u306b\\u3061\\u306f\"}}";
bool ret = parse_sse_data(json, token, nullptr);
CHECK(ret, "T4.2: unicode text_delta returns true");
CHECK(token == "\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab"
"\xe3\x81\xa1\xe3\x81\xaf",
"T4.3: unicode token decoded correctly"); // こんにちは
}
{
// Realistic Anthropic SSE chunk (content_block_delta + text_delta)
// 真实的 Anthropic SSE 数据块content_block_delta + text_delta
std::string token;
const char* json =
"{\"type\":\"content_block_delta\","
"\"index\":0,"
"\"delta\":{\"type\":\"text_delta\",\"text\":\" I'm\"}}";
bool ret = parse_sse_data(json, token, nullptr);
CHECK(ret, "T4.4: realistic SSE chunk returns true");
CHECK(token == " I'm", "T4.5: token ' I'm' correct");
}
// ================================================================
// Test Block 5: build_request_json — basic cases
// 测试块 5build_request_json — 基础用例
// ================================================================
setup_config();
std::cout << "\n--- Block 5: build_request_json basic ---\n";
{
// Single user input, no history, stream=false / 单一用户输入无历史stream=false
std::string json = build_request_json(nullptr, 0, "Hello", "", false);
CHECK(!json.empty(), "T5.1: non-empty JSON produced");
CHECK(json.find("\"messages\"") != std::string::npos,
"T5.2: contains 'messages' array");
CHECK(json.find("\"user\"") != std::string::npos,
"T5.3: contains 'user' role");
CHECK(json.find("\"Hello\"") != std::string::npos,
"T5.4: contains user input text");
CHECK(json.find("\"stream\":false") != std::string::npos,
"T5.5: stream=false present");
CHECK(json.find("\"model\":\"claude-sonnet-4-20250514\"")
!= std::string::npos,
"T5.6: model field present");
CHECK(json.find("\"max_tokens\":4096") != std::string::npos,
"T5.7: max_tokens field present");
}
{
// With system message in history / 历史中包含系统消息
dstalk_message_t msgs[1] = {
{"system", "You are a helpful assistant", nullptr, nullptr}
};
std::string json = build_request_json(msgs, 1, "Hello", "", false);
CHECK(json.find("\"system\"") != std::string::npos,
"T5.8: system prompt extracted to 'system' field");
CHECK(json.find("You are a helpful assistant") != std::string::npos,
"T5.9: system prompt content present");
// messages should NOT contain the system role
// (since system messages are stripped from messages[] and put in system field)
// messages 不应包含 system 角色(系统消息从 messages[] 中提取出来,放入 system 字段)
// Actually, the code puts non-system into msgs. Let me check if system is in messages...
// The loop skips system: `if (m.role && strcmp(m.role, "system")==0) { ... continue; }`
// So system should NOT be in the messages array.
CHECK(json.find("\"role\":\"system\"") == std::string::npos,
"T5.10: system role NOT in messages array");
}
{
// With user+assistant history / 包含 user+assistant 历史
dstalk_message_t msgs[2] = {
{"user", "What is 2+2?", nullptr, nullptr},
{"assistant", "It is 4.", nullptr, nullptr}
};
std::string json = build_request_json(msgs, 2, "Thanks!", "", false);
CHECK(json.find("\"role\":\"user\"") != std::string::npos,
"T5.11: user role present in messages");
CHECK(json.find("\"role\":\"assistant\"") != std::string::npos,
"T5.12: assistant role present in messages");
CHECK(json.find("Thanks!") != std::string::npos,
"T5.13: current user input present");
}
// ================================================================
// Test Block 6: build_request_json — edge cases
// 测试块 6build_request_json — 边界情况
// ================================================================
std::cout << "\n--- Block 6: build_request_json edge cases ---\n";
{
// Empty user input / 空用户输入
std::string json = build_request_json(nullptr, 0, "", "", false);
CHECK(!json.empty(), "T6.1: empty user input produces valid JSON");
CHECK(json.find("\"role\":\"user\"") != std::string::npos,
"T6.2: user role still present with empty content");
CHECK(json.find("\"content\":\"\"") != std::string::npos,
"T6.3: empty content string present");
// The user input IS added even if empty (line 109-112: no check for empty)
}
{
// Stream=true
std::string json = build_request_json(nullptr, 0, "Hi", "", true);
CHECK(json.find("\"stream\":true") != std::string::npos,
"T6.4: stream=true present");
}
{
// Temperature in valid range -> should be included / 有效范围内的 temperature -> 应包含
g_cfg.temperature = 1.0;
std::string json = build_request_json(nullptr, 0, "Hi", "", false);
CHECK(json.find("\"temperature\"") != std::string::npos,
"T6.5: temperature=1.0 included (boundary valid)");
g_cfg.temperature = 0.7; // reset
}
{
// Temperature out of range -> should NOT be included / 超出范围的 temperature -> 不应包含
g_cfg.temperature = 1.5;
std::string json = build_request_json(nullptr, 0, "Hi", "", false);
CHECK(json.find("\"temperature\"") == std::string::npos,
"T6.6: temperature=1.5 (>1.0) omitted");
g_cfg.temperature = -0.5;
json = build_request_json(nullptr, 0, "Hi", "", false);
CHECK(json.find("\"temperature\"") == std::string::npos,
"T6.7: temperature=-0.5 (<0.0) omitted");
g_cfg.temperature = 0.7; // reset
}
{
// History with null role (should default to "") / null 角色的历史(应默认为 ""
dstalk_message_t msgs[1] = {
{nullptr, "some content", nullptr, nullptr}
};
std::string json = build_request_json(msgs, 1, "Hi", "", false);
CHECK(!json.empty(), "T6.8: null role produces valid JSON (no crash)");
}
{
// History with null content / null 内容的历史
dstalk_message_t msgs[1] = {
{"user", nullptr, nullptr, nullptr}
};
std::string json = build_request_json(msgs, 1, "Hi", "", false);
CHECK(!json.empty(), "T6.9: null content produces valid JSON (no crash)");
}
{
// Very long message (>2000 chars) — validate no truncation / crash
// 超长消息 (>2000 字符) — 验证无截断/崩溃
std::string long_input(5000, 'A');
std::string json = build_request_json(nullptr, 0, long_input, "", false);
CHECK(!json.empty(), "T6.10: 5000-char input produces valid JSON");
CHECK(json.length() > 5000, "T6.11: JSON longer than input (wraps content)");
}
{
// Multiple system messages concatenated / 多条系统消息拼接
dstalk_message_t msgs[2] = {
{"system", "Rule 1: be polite", nullptr, nullptr},
{"system", "Rule 2: be concise", nullptr, nullptr}
};
std::string json = build_request_json(msgs, 2, "Hello", "", false);
CHECK(json.find("be polite") != std::string::npos,
"T6.12: first system prompt present");
CHECK(json.find("be concise") != std::string::npos,
"T6.13: second system prompt present (concatenated)");
}
// ================================================================
// Test Block 7: build_headers_json
// 测试块 7build_headers_json
// ================================================================
std::cout << "\n--- Block 7: build_headers_json ---\n";
{
std::string headers = build_headers_json();
CHECK(headers.find("x-api-key") != std::string::npos,
"T7.1: contains x-api-key header");
CHECK(headers.find(g_cfg.api_key) != std::string::npos,
"T7.2: contains correct API key value");
CHECK(headers.find("anthropic-version") != std::string::npos,
"T7.3: contains anthropic-version header");
CHECK(headers.find("2023-06-01") != std::string::npos,
"T7.4: anthropic-version is 2023-06-01");
}
{
// With empty API key / 空 API key
std::string saved = g_cfg.api_key;
g_cfg.api_key = "";
std::string headers = build_headers_json();
CHECK(headers.find("x-api-key") != std::string::npos,
"T7.5: x-api-key present even with empty key");
CHECK(headers.find("\"x-api-key\":\"\"") != std::string::npos,
"T7.6: empty API key serialized as empty string");
g_cfg.api_key = saved;
}
// ================================================================
// Test Block 8: extract_host_port
// 测试块 8extract_host_port
// ================================================================
std::cout << "\n--- Block 8: extract_host_port ---\n";
{
std::string scheme, host, port, target;
bool ret = extract_host_port(
"https://api.anthropic.com/v1/messages",
scheme, host, port, target);
CHECK(ret, "T8.1: valid HTTPS URL returns true");
CHECK(scheme == "https", "T8.2: scheme is 'https'");
CHECK(host == "api.anthropic.com", "T8.3: host extracted correctly");
CHECK(port == "443", "T8.4: default HTTPS port 443");
CHECK(target == "/v1/messages", "T8.5: target path extracted");
}
{
std::string scheme, host, port, target;
bool ret = extract_host_port(
"http://localhost:8080/chat",
scheme, host, port, target);
CHECK(ret, "T8.6: HTTP URL with explicit port returns true");
CHECK(scheme == "http", "T8.7: scheme is 'http'");
CHECK(host == "localhost", "T8.8: host is 'localhost'");
CHECK(port == "8080", "T8.9: explicit port 8080");
CHECK(target == "/chat", "T8.10: target path extracted");
}
{
std::string scheme, host, port, target;
bool ret = extract_host_port(
"https://example.com",
scheme, host, port, target);
CHECK(ret, "T8.11: URL without path returns true");
CHECK(scheme == "https", "T8.12: scheme is 'https'");
CHECK(host == "example.com", "T8.13: host extracted");
CHECK(port == "443", "T8.14: default HTTPS port");
CHECK(target == "/", "T8.15: target defaults to '/'");
}
{
std::string scheme, host, port, target;
bool ret = extract_host_port(
"http://localhost",
scheme, host, port, target);
CHECK(ret, "T8.16: HTTP URL returns true");
CHECK(port == "80", "T8.17: default HTTP port 80");
}
{
std::string scheme, host, port, target;
bool ret = extract_host_port(
"no-scheme-url.com/path",
scheme, host, port, target);
CHECK(!ret, "T8.18: URL without scheme returns false");
}
{
std::string scheme, host, port, target;
bool ret = extract_host_port(
"https://127.0.0.1:9090/v1/chat/completions",
scheme, host, port, target);
CHECK(ret, "T8.19: IP address with port returns true");
CHECK(host == "127.0.0.1", "T8.20: IP host extracted");
CHECK(port == "9090", "T8.21: port 9090 extracted");
}
// ================================================================
// Test Block 9: secure_zero
// 测试块 9secure_zero
// ================================================================
std::cout << "\n--- Block 9: secure_zero ---\n";
{
char buf[16];
memset(buf, 0xFF, sizeof(buf));
secure_zero(buf, sizeof(buf));
bool all_zero = true;
for (int i = 0; i < 16; ++i) {
if (buf[i] != 0) { all_zero = false; break; }
}
CHECK(all_zero, "T9.1: secure_zero zeros entire buffer");
}
{
// Zero-length should not crash / 零长度不应崩溃
char buf[4] = {1,2,3,4};
secure_zero(buf, 0);
CHECK(buf[0] == 1 && buf[3] == 4,
"T9.2: secure_zero(0) is a no-op (buffer unchanged)");
}
{
// Null pointer + zero length = no-op / 空指针 + 零长度 = 空操作
secure_zero(nullptr, 0);
CHECK(true, "T9.3: secure_zero(nullptr, 0) does not crash");
}
// ================================================================
// Test Block 10: my_free_result — null safety
// 测试块 10my_free_result — 空指针安全
// ================================================================
std::cout << "\n--- Block 10: my_free_result null safety ---\n";
{
// g_host is nullptr, so free_result should early-return
// g_host 为 nullptrfree_result 应提前返回
my_free_result(nullptr);
CHECK(true, "T10.1: free_result(nullptr) does not crash (null host)");
}
{
dstalk_chat_result_t r = {};
r.ok = 1;
r.content = nullptr;
r.error = nullptr;
r.tool_calls_json = nullptr;
my_free_result(&r);
CHECK(true, "T10.2: free_result with all-null fields does not crash");
}
// ================================================================
// Test Block 11: my_configure — null host safety
// 测试块 11my_configure — null host 安全
// ================================================================
std::cout << "\n--- Block 11: my_configure null host safety ---\n";
{
// g_host is nullptr, configure should still return 0 (log skipped)
// g_host 为 nullptrconfigure 仍应返回 0跳过日志
int ret = my_configure(
"anthropic", "https://api.anthropic.com",
"sk-key", "claude-sonnet", 2048, 0.5);
CHECK(ret == 0, "T11.1: my_configure returns 0 with null host");
CHECK(g_cfg.provider == "anthropic", "T11.2: provider stored");
CHECK(g_cfg.max_tokens == 2048, "T11.3: max_tokens stored");
CHECK(g_cfg.temperature == 0.5, "T11.4: temperature stored");
}
{
// Null string params — should not crash / null 字符串参数 — 不应崩溃
int ret = my_configure(nullptr, nullptr, nullptr, nullptr, 4096, 0.7);
CHECK(ret == 0, "T11.5: my_configure with all-null strings returns 0");
}
// ================================================================
// Summary / 总结
// ================================================================
std::cout << "\n";
if (g_failures == 0) {
std::cout << "=== All anthropic plugin tests passed ===\n";
return 0;
} else {
std::cerr << "=== " << g_failures << " test(s) FAILED ===\n";
return 1;
}
}