Files
dstalk/tests/openai_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

712 lines
29 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 openai_plugin_test.cpp
* @brief OpenAI-compatible AI plugin unit tests: SSE parsing (parse_sse_line edge cases),
* [DONE] sentinel matching, tool_calls delta extraction, request building,
* append_history, extract_host_port, secure_zero, and null-safety.
* DeepSeek AI 插件单元测试SSE 解析parse_sse_line 边界情况)、[DONE] 标记匹配、
* tool_calls delta 提取、请求构建、append_history、extract_host_port、secure_zero、空指针安全。
* Copyright (c) 2026 dstalk contributors. GPLv3.
*/
#define BOOST_JSON_HEADER_ONLY
#define BOOST_ALL_NO_LIB
#include "../plugins_upper/openai/src/openai_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 deepseek defaults before build_* tests
// 测试辅助函数:为 build_* 测试填充 g_cfg 的有效 deepseek 默认值
static void setup_config() {
g_cfg.provider = "openai";
g_cfg.base_url = "https://api.openai.com/v1";
g_cfg.api_key = "sk-ds-test-key-67890";
g_cfg.model = "gpt-4o";
g_cfg.max_tokens = 4096;
g_cfg.temperature = 0.7;
}
// DeepSeek 插件测试 (W21.6)parse_sse_line 无效/畸形输入、[DONE] 标记及空白变体、
// content delta 提取、tool_calls delta 累积、build_request_json基础、tools、边界
// build_headers_json、extract_host_port、secure_zero、append_history所有消息类型
// my_free_result、my_configure。
// DeepSeek plugin tests (W21.6): parse_sse_line invalid/malformed inputs, [DONE] sentinel
// with whitespace variants, content delta extraction, tool_calls delta accumulation,
// build_request_json (basic, tools, edge cases), build_headers_json, extract_host_port,
// secure_zero, append_history (all message types), my_free_result, and my_configure.
int main()
{
// ================================================================
// Test Block 1: parse_sse_line — invalid/malformed inputs
// 测试块 1parse_sse_line — 无效/畸形输入
// ================================================================
std::cout << "\n--- Block 1: parse_sse_line invalid/malformed ---\n";
{
std::string token;
bool ret = parse_sse_line("", token, nullptr);
CHECK(!ret, "T1.1: empty line returns false");
}
{
std::string token;
bool ret = parse_sse_line("not a data line", token, nullptr);
CHECK(!ret, "T1.2: non-'data:' prefix returns false");
}
{
std::string token;
bool ret = parse_sse_line("event: message", token, nullptr);
CHECK(!ret, "T1.3: 'event:' line returns false");
}
{
// "data:" without space — rfind("data: ", 0) should fail
// "data:" 无空格 — rfind("data: ", 0) 应失败
std::string token;
bool ret = parse_sse_line("data:{\"x\":1}", token, nullptr);
CHECK(!ret, "T1.4: 'data:' without trailing space returns false (rfind mismatch)");
}
{
// "data: " followed by invalid JSON / "data: " 后跟无效 JSON
std::string token;
bool ret = parse_sse_line("data: not valid json!!!", token, nullptr);
CHECK(!ret, "T1.5: 'data: ' + invalid JSON returns false (no crash)");
}
{
// "data: " followed by binary garbage / "data: " 后跟二进制垃圾
std::string token;
bool ret = parse_sse_line("data: \x00\x01\xFF\xFE", token, nullptr);
CHECK(!ret, "T1.6: 'data: ' + binary garbage returns false (no crash)");
}
{
// Empty data after "data: " / "data: " 后数据为空
std::string token;
bool ret = parse_sse_line("data: ", token, nullptr);
CHECK(!ret, "T1.7: 'data: ' with empty payload returns false");
}
// ================================================================
// Test Block 2: parse_sse_line — [DONE] sentinel
// 测试块 2parse_sse_line — [DONE] 标记
// ================================================================
std::cout << "\n--- Block 2: parse_sse_line [DONE] sentinel ---\n";
{
std::string token = "SHOULD_BE_CLEARED";
bool ret = parse_sse_line("data: [DONE]", token, nullptr);
CHECK(ret, "T2.1: 'data: [DONE]' returns true (stream end)");
CHECK(token.empty(), "T2.2: [DONE] clears token");
}
{
// [DONE] with leading whitespace / [DONE] 前导空白
std::string token;
bool ret = parse_sse_line("data: [DONE]", token, nullptr);
CHECK(ret, "T2.3: 'data: [DONE]' (leading spaces) returns true");
CHECK(token.empty(), "T2.4: whitespace-trimmed [DONE] clears token");
}
{
// [DONE] with trailing whitespace / [DONE] 尾部空白
std::string token;
bool ret = parse_sse_line("data: [DONE] ", token, nullptr);
CHECK(ret, "T2.5: 'data: [DONE] ' (trailing spaces) returns true");
CHECK(token.empty(), "T2.6: trailing-whitespace [DONE] clears token");
}
{
// [DONE] with tabs and newlines around it / [DONE] 周围有制表符和换行符
std::string token;
bool ret = parse_sse_line("data: \t [DONE] \t\r\n", token, nullptr);
CHECK(ret, "T2.7: '[DONE]' with mixed whitespace returns true");
CHECK(token.empty(), "T2.8: mixed-whitespace [DONE] clears token");
}
{
// [DONE] without spaces — exact match / [DONE] 精确匹配(无空格)
std::string token;
bool ret = parse_sse_line("data: [DONE]", token, nullptr);
CHECK(ret, "T2.9: '[DONE]' exact match returns true");
}
{
// "[done]" lowercase — should NOT match (case-sensitive)
// "[done]" 小写 — 不应匹配(大小写敏感)
std::string token;
bool ret = parse_sse_line("data: [done]", token, nullptr);
CHECK(!ret, "T2.10: '[done]' lowercase NOT treated as DONE (case-sensitive)");
}
{
// "[DONE" without closing bracket / "[DONE" 缺少闭括号
std::string token;
bool ret = parse_sse_line("data: [DONE", token, nullptr);
CHECK(!ret, "T2.11: '[DONE' (no closing bracket) not treated as DONE");
}
// ================================================================
// Test Block 3: parse_sse_line — content delta
// 测试块 3parse_sse_line — content delta
// ================================================================
std::cout << "\n--- Block 3: parse_sse_line content delta ---\n";
{
std::string token;
const char* json =
"data: {\"choices\":[{\"delta\":{\"content\":\"Hello\"},"
"\"index\":0}]}";
bool ret = parse_sse_line(json, token, nullptr);
CHECK(ret, "T3.1: delta with content 'Hello' returns true");
CHECK(token == "Hello", "T3.2: token equals 'Hello'");
}
{
std::string token;
const char* json =
"data: {\"choices\":[{\"delta\":{\"content\":\"\"},\"index\":0}]}";
bool ret = parse_sse_line(json, token, nullptr);
CHECK(ret, "T3.3: delta with empty content returns true");
CHECK(token.empty(), "T3.4: empty content token is empty");
}
{
// Delta with no content field / delta 不含 content 字段
std::string token;
const char* json =
"data: {\"choices\":[{\"delta\":{},\"index\":0}]}";
bool ret = parse_sse_line(json, token, nullptr);
CHECK(!ret, "T3.5: delta without 'content' field returns false");
}
{
// Empty choices array / 空 choices 数组
std::string token;
const char* json =
"data: {\"choices\":[]}";
bool ret = parse_sse_line(json, token, nullptr);
CHECK(!ret, "T3.6: empty choices array returns false");
}
{
// Single character token (typical streaming) / 单字符 token典型流式
std::string token;
const char* json =
"data: {\"choices\":[{\"delta\":{\"content\":\"H\"},\"index\":0}]}";
bool ret = parse_sse_line(json, token, nullptr);
CHECK(ret, "T3.7: single-char delta returns true");
CHECK(token == "H", "T3.8: single-char token correct");
}
{
// Multi-byte UTF-8 content (emoji) in delta / delta 中的多字节 UTF-8 内容emoji
std::string token;
const char* json =
"data: {\"choices\":[{\"delta\":{\"content\":\"\\uD83D\\uDE00\"},"
"\"index\":0}]}";
bool ret = parse_sse_line(json, token, nullptr);
CHECK(ret, "T3.9: emoji delta returns true");
// U+1F600 in UTF-8: F0 9F 98 80
std::string emoji = "\xF0\x9F\x98\x80";
CHECK(token == emoji, "T3.10: emoji token decoded correctly (U+1F600)");
}
{
// Malformed JSON structure — no "delta" key / 畸形 JSON 结构 — 无 "delta" key
std::string token;
const char* json =
"data: {\"choices\":[{\"no_delta\":{},\"index\":0}]}";
bool ret = parse_sse_line(json, token, nullptr);
CHECK(!ret, "T3.11: choice without 'delta' key returns false");
}
{
// Realistic DeepSeek streaming chunk (with finish_reason)
// 真实的 DeepSeek 流式数据块(含 finish_reason
std::string token;
const char* json =
"data: {\"id\":\"chatcmpl-xxx\","
"\"object\":\"chat.completion.chunk\","
"\"created\":1712345678,"
"\"model\":\"gpt-4o\","
"\"choices\":[{\"index\":0,"
"\"delta\":{\"content\":\" World\"},"
"\"finish_reason\":null}]}";
bool ret = parse_sse_line(json, token, nullptr);
CHECK(ret, "T3.12: realistic DeepSeek chunk returns true");
CHECK(token == " World", "T3.13: realistic chunk token correct");
}
// ================================================================
// Test Block 4: parse_sse_line — tool_calls delta
// 测试块 4parse_sse_line — tool_calls delta
// ================================================================
std::cout << "\n--- Block 4: parse_sse_line tool_calls delta ---\n";
{
// tool_calls chunk with id + function name (first chunk)
// tool_calls 数据块含 id + function name首个数据块
StreamContext ctx = {};
std::string token;
const char* json =
"data: {\"choices\":[{\"index\":0,"
"\"delta\":{\"tool_calls\":[{\"index\":0,"
"\"id\":\"call_abc123\","
"\"type\":\"function\","
"\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}]}}]}";
bool ret = parse_sse_line(json, token, &ctx);
CHECK(!ret, "T4.1: tool_calls first chunk returns false (no content token)");
CHECK(ctx.tool_calls.size() >= 1, "T4.2: tool_calls accumulated in ctx");
if (ctx.tool_calls.size() >= 1) {
CHECK(ctx.tool_calls[0].index == 0, "T4.3: tool_call index=0");
CHECK(ctx.tool_calls[0].id == "call_abc123", "T4.4: tool_call id stored");
CHECK(ctx.tool_calls[0].name == "get_weather", "T4.5: tool_call name stored");
}
}
{
// tool_calls arguments chunk (second chunk, same index)
// tool_calls arguments 数据块(第二个数据块,相同 index
StreamContext ctx;
// First, set up the initial state / 先设置初始状态
ctx.tool_calls.push_back({0, "call_abc123", "get_weather", ""});
std::string token;
const char* json =
"data: {\"choices\":[{\"index\":0,"
"\"delta\":{\"tool_calls\":[{\"index\":0,"
"\"function\":{\"arguments\":\"{\\\"city\\\":\\\"\"}}]}}]}";
bool ret = parse_sse_line(json, token, &ctx);
CHECK(!ret, "T4.6: tool_calls arguments chunk returns false");
if (ctx.tool_calls.size() >= 1) {
CHECK(ctx.tool_calls[0].arguments == "{\"city\":\"",
"T4.7: arguments accumulated (first fragment)");
}
}
{
// tool_calls final arguments chunk / tool_calls 最终 arguments 数据块
StreamContext ctx;
ctx.tool_calls.push_back({0, "call_abc123", "get_weather", "{\"city\":\""});
std::string token;
const char* json =
"data: {\"choices\":[{\"index\":0,"
"\"delta\":{\"tool_calls\":[{\"index\":0,"
"\"function\":{\"arguments\":\"Beijing\\\"}\"}}]}}]}";
bool ret = parse_sse_line(json, token, &ctx);
CHECK(!ret, "T4.8: tool_calls final chunk returns false");
if (ctx.tool_calls.size() >= 1) {
CHECK(ctx.tool_calls[0].arguments == "{\"city\":\"Beijing\"}",
"T4.9: full arguments accumulated correctly");
}
}
{
// tool_calls with null ctx — should skip tool_calls processing
// tool_calls 配合 null ctx — 应跳过 tool_calls 处理
std::string token;
const char* json =
"data: {\"choices\":[{\"index\":0,"
"\"delta\":{\"tool_calls\":[{\"index\":0,"
"\"function\":{\"arguments\":\"{}\"}}]}}]}";
bool ret = parse_sse_line(json, token, nullptr);
CHECK(!ret, "T4.10: tool_calls with null ctx returns false (no crash)");
}
{
// Multiple tool_calls in single chunk (unusual but valid)
// 单个数据块中有多个 tool_calls不常见但合法
StreamContext ctx;
std::string token;
const char* json =
"data: {\"choices\":[{\"index\":0,"
"\"delta\":{\"tool_calls\":["
"{\"index\":0,\"function\":{\"arguments\":\"a\"}},"
"{\"index\":1,\"function\":{\"arguments\":\"b\"}}"
"]}}]}";
bool ret = parse_sse_line(json, token, &ctx);
CHECK(!ret, "T4.11: multi-tool_call chunk returns false");
CHECK(ctx.tool_calls.size() >= 2, "T4.12: both tool_calls accumulated");
if (ctx.tool_calls.size() >= 2) {
CHECK(ctx.tool_calls[0].index == 0, "T4.13: first tool_call index=0");
CHECK(ctx.tool_calls[1].index == 1, "T4.14: second tool_call index=1");
}
}
// ================================================================
// Test Block 5: build_request_json — basic cases
// 测试块 5build_request_json — 基础用例
// ================================================================
setup_config();
std::cout << "\n--- Block 5: build_request_json basic ---\n";
{
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");
CHECK(json.find("\"stream\":false") != std::string::npos,
"T5.5: stream=false present");
CHECK(json.find("\"model\":\"gpt-4o\"")
!= std::string::npos,
"T5.6: model field present");
CHECK(json.find("\"max_tokens\":4096") != std::string::npos,
"T5.7: max_tokens present");
CHECK(json.find("\"temperature\"") != std::string::npos,
"T5.8: temperature field present (always included in DeepSeek)");
}
{
// 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.9: user role present");
CHECK(json.find("\"role\":\"assistant\"") != std::string::npos,
"T5.10: assistant role present");
CHECK(json.find("Thanks!") != std::string::npos,
"T5.11: current user input present");
}
{
// Stream=true
std::string json = build_request_json(
nullptr, 0, "Hi", "", true);
CHECK(json.find("\"stream\":true") != std::string::npos,
"T5.12: stream=true present");
}
{
// Empty user input — no user message appended
// 空用户输入 — 不追加 user 消息
std::string json = build_request_json(
nullptr, 0, "", "", false);
CHECK(!json.empty(), "T5.13: empty user input produces valid JSON");
// DeepSeek's build_request_json checks `if (!user_input.empty())` before adding
// So there should be no user message for empty input
// DeepSeek 的 build_request_json 在添加前检查 `if (!user_input.empty())`
// 因此空输入时不应有 user 消息
CHECK(json.find("\"role\":\"user\"") == std::string::npos,
"T5.14: empty user input NOT added to messages (DeepSeek guard)");
}
// ================================================================
// Test Block 6: build_request_json — tools / edge cases
// 测试块 6build_request_json — tools / 边界情况
// ================================================================
std::cout << "\n--- Block 6: build_request_json tools / edges ---\n";
{
// With tools_json / 含 tools_json
std::string tools = "[{\"type\":\"function\","
"\"function\":{\"name\":\"get_weather\","
"\"description\":\"Get current weather\","
"\"parameters\":{\"type\":\"object\","
"\"properties\":{\"city\":{\"type\":\"string\"}},"
"\"required\":[\"city\"]}}}]";
std::string json = build_request_json(
nullptr, 0, "Weather in Beijing?", tools, false);
CHECK(json.find("\"tools\"") != std::string::npos,
"T6.1: 'tools' field present when tools_json provided");
CHECK(json.find("get_weather") != std::string::npos,
"T6.2: tool name present in serialized JSON");
}
{
// Empty tools_json — no tools field / 空 tools_json — 无 tools 字段
std::string json = build_request_json(
nullptr, 0, "Hello", "", false);
CHECK(json.find("\"tools\"") == std::string::npos,
"T6.3: no 'tools' field when tools_json is empty");
}
{
// Malformed tools_json — build_request_json calls json::parse()
// without try/catch, so it will throw std::exception.
// This test verifies that the exception is thrown (rather than crashing).
// 畸形 tools_json — build_request_json 调用 json::parse() 不含 try/catch
// 因此会抛出 std::exception。本测试验证异常被抛出而非崩溃
bool threw = false;
try {
build_request_json(nullptr, 0, "Hello", "NOT JSON", false);
} catch (const std::exception&) {
threw = true;
} catch (...) {
threw = true;
}
CHECK(threw, "T6.4: malformed tools_json throws (expected, not a crash)");
}
{
// History with null role / 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.5: 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.6: null content produces valid JSON (no crash)");
}
{
// Very long message / 超长消息
std::string long_input(5000, 'A');
std::string json = build_request_json(
nullptr, 0, long_input, "", false);
CHECK(!json.empty(), "T6.7: 5000-char input produces valid JSON");
CHECK(json.length() > 5000, "T6.8: JSON longer than input (wraps content)");
}
// ================================================================
// Test Block 7: build_headers_json
// 测试块 7build_headers_json
// ================================================================
std::cout << "\n--- Block 7: build_headers_json ---\n";
{
std::string headers = build_headers_json("sk-test-123");
CHECK(headers.find("Authorization") != std::string::npos,
"T7.1: contains Authorization header");
CHECK(headers.find("Bearer sk-test-123") != std::string::npos,
"T7.2: contains 'Bearer sk-test-123'");
}
{
// Empty API key / 空 API key
std::string headers = build_headers_json("");
CHECK(headers.find("Authorization") != std::string::npos,
"T7.3: Authorization header present with empty key");
CHECK(headers.find("Bearer ") != std::string::npos,
"T7.4: 'Bearer ' prefix present even with empty key");
}
// ================================================================
// Test Block 8: extract_host_port (same logic as anthropic)
// 测试块 8extract_host_port逻辑同 anthropic
// ================================================================
std::cout << "\n--- Block 8: extract_host_port ---\n";
{
std::string scheme, host, port, target;
bool ret = extract_host_port(
"https://api.openai.com/v1/chat/completions",
scheme, host, port, target);
CHECK(ret, "T8.1: valid HTTPS URL returns true");
CHECK(scheme == "https", "T8.2: scheme is 'https'");
CHECK(host == "api.openai.com", "T8.3: host extracted");
CHECK(port == "443", "T8.4: default HTTPS port 443");
CHECK(target == "/v1/chat/completions", "T8.5: target path extracted");
}
{
std::string scheme, host, port, target;
bool ret = extract_host_port(
"http://localhost:11434/api/generate",
scheme, host, port, target);
CHECK(ret, "T8.6: HTTP URL with port returns true");
CHECK(scheme == "http", "T8.7: scheme is 'http'");
CHECK(host == "localhost", "T8.8: host is 'localhost'");
CHECK(port == "11434", "T8.9: explicit port 11434");
}
{
std::string scheme, host, port, target;
bool ret = extract_host_port(
"no-scheme-url.com",
scheme, host, port, target);
CHECK(!ret, "T8.10: URL without scheme returns false");
}
{
std::string scheme, host, port, target;
bool ret = extract_host_port(
"https://api.openai.com",
scheme, host, port, target);
CHECK(ret, "T8.11: URL without path returns true");
CHECK(target == "/", "T8.12: target defaults to '/'");
}
// ================================================================
// 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");
}
{
secure_zero(nullptr, 0);
CHECK(true, "T9.2: secure_zero(nullptr, 0) does not crash");
}
// ================================================================
// Test Block 10: append_history
// 测试块 10append_history
// ================================================================
std::cout << "\n--- Block 10: append_history ---\n";
{
json::array msgs;
dstalk_message_t m = {"user", "Hello", nullptr, nullptr};
append_history(msgs, &m, 1);
CHECK(msgs.size() == 1, "T10.1: one message appended");
CHECK(msgs[0].as_object()["role"].as_string() == "user",
"T10.2: role preserved");
CHECK(msgs[0].as_object()["content"].as_string() == "Hello",
"T10.3: content preserved");
}
{
// Tool message (should include tool_call_id) / Tool 消息(应包含 tool_call_id
json::array msgs;
dstalk_message_t m = {"tool", "result data", "call_xyz", nullptr};
append_history(msgs, &m, 1);
CHECK(msgs.size() == 1, "T10.4: tool message appended");
auto obj = msgs[0].as_object();
CHECK(obj["role"].as_string() == "tool", "T10.5: tool role preserved");
CHECK(obj["tool_call_id"].as_string() == "call_xyz",
"T10.6: tool_call_id preserved");
CHECK(obj["content"].as_string() == "result data",
"T10.7: tool message content preserved");
}
{
// Assistant with tool_calls_json / Assistant 含 tool_calls_json
json::array msgs;
const char* tc_json = "[{\"id\":\"call_1\",\"type\":\"function\","
"\"function\":{\"name\":\"get_weather\",\"arguments\":\"{}\"}}]";
dstalk_message_t m = {"assistant", "Let me check", nullptr, tc_json};
append_history(msgs, &m, 1);
CHECK(msgs.size() == 1, "T10.8: assistant with tool_calls appended");
auto obj = msgs[0].as_object();
CHECK(obj["role"].as_string() == "assistant", "T10.9: assistant role");
CHECK(obj["content"].as_string() == "Let me check", "T10.10: content");
CHECK(obj.if_contains("tool_calls") != nullptr, "T10.11: tool_calls present");
}
{
// Empty history (0 messages) / 空历史0 条消息)
json::array msgs;
append_history(msgs, nullptr, 0);
CHECK(msgs.size() == 0, "T10.12: empty history produces empty array");
}
{
// Multiple messages / 多条消息
json::array msgs;
dstalk_message_t ms[2] = {
{"user", "Q1", nullptr, nullptr},
{"assistant", "A1", nullptr, nullptr}
};
append_history(msgs, ms, 2);
CHECK(msgs.size() == 2, "T10.13: two messages appended");
}
{
// Null role and null content — default to empty strings
// null 角色与 null 内容 — 默认为空字符串
json::array msgs;
dstalk_message_t m = {nullptr, nullptr, nullptr, nullptr};
append_history(msgs, &m, 1);
CHECK(msgs.size() == 1, "T10.14: null fields produce valid JSON (no crash)");
auto obj = msgs[0].as_object();
CHECK(obj["role"].as_string() == "", "T10.15: null role → empty string");
CHECK(obj["content"].as_string() == "", "T10.16: null content → empty string");
}
// ================================================================
// Test Block 11: my_free_result — null safety
// 测试块 11my_free_result — 空指针安全
// ================================================================
std::cout << "\n--- Block 11: 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, "T11.1: free_result(nullptr) does not crash (null host)");
}
{
dstalk_chat_result_t r = {};
r.ok = 1;
my_free_result(&r);
CHECK(true, "T11.2: free_result with zeroed fields does not crash");
}
// ================================================================
// Test Block 12: my_configure — null host safety
// 测试块 12my_configure — null host 安全
// ================================================================
std::cout << "\n--- Block 12: my_configure null host safety ---\n";
{
int ret = my_configure(
"openai", "https://api.openai.com/v1",
"sk-key", "gpt-4o", 2048, 0.5);
CHECK(ret == 0, "T12.1: my_configure returns 0 with null host");
CHECK(g_cfg.provider == "openai", "T12.2: provider stored");
CHECK(g_cfg.max_tokens == 2048, "T12.3: max_tokens stored");
CHECK(g_cfg.temperature == 0.5, "T12.4: temperature stored");
}
{
int ret = my_configure(nullptr, nullptr, nullptr, nullptr, 4096, 1.0);
CHECK(ret == 0, "T12.5: my_configure with all-null strings returns 0");
}
// ================================================================
// Summary / 总结
// ================================================================
std::cout << "\n";
if (g_failures == 0) {
std::cout << "=== All openai plugin tests passed ===\n";
return 0;
} else {
std::cerr << "=== " << g_failures << " test(s) FAILED ===\n";
return 1;
}
}