- 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>
590 lines
24 KiB
C++
590 lines
24 KiB
C++
/*
|
||
* @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
|
||
// 测试块 1:parse_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
|
||
// 测试块 2:parse_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
|
||
// 测试块 3:parse_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
|
||
// 测试块 4:parse_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
|
||
// 测试块 5:build_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
|
||
// 测试块 6:build_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
|
||
// 测试块 7:build_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
|
||
// 测试块 8:extract_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
|
||
// 测试块 9:secure_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
|
||
// 测试块 10:my_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 为 nullptr,free_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
|
||
// 测试块 11:my_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 为 nullptr,configure 仍应返回 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;
|
||
}
|
||
}
|