// ============================================================================ // anthropic_plugin_test.cpp — Anthropic AI 插件单元测试 // W21.6 (qa-wang): 覆盖 SSE 解析 / JSON 请求构建 / URL 解析 / 安全擦除 // 通过 #include plugin source 访问 file-scope static 函数 // ============================================================================ #define BOOST_JSON_HEADER_ONLY #define BOOST_ALL_NO_LIB #include "../plugins/anthropic/src/anthropic_plugin.cpp" #include #include #include static int g_failures = 0; #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 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; } int main() { // ================================================================ // Test Block 1: parse_sse_data — invalid/malformed inputs // ================================================================ 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 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 // ================================================================ 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 // ================================================================ 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 // ================================================================ 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) 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) 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 // ================================================================ setup_config(); std::cout << "\n--- Block 5: build_request_json basic ---\n"; { // Single user input, no history, 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) // 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 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 // ================================================================ 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 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 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 "") 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 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 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 // ================================================================ 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 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 // ================================================================ 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 // ================================================================ 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 // ================================================================ std::cout << "\n--- Block 10: my_free_result null safety ---\n"; { // g_host is nullptr, so free_result should early-return 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 // ================================================================ std::cout << "\n--- Block 11: my_configure null host safety ---\n"; { // g_host is nullptr, configure should still return 0 (log skipped) 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 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; } }