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>
This commit is contained in:
2026-05-31 16:58:25 +08:00
parent ba7382db2a
commit 8faa02c3d5
49 changed files with 1062 additions and 413 deletions

View File

@@ -2,210 +2,215 @@
# tests — 单元测试
# ============================================================
add_executable(dstalk-smoke-test
add_executable(dstalk_smoke_test
smoke_test.cpp
)
target_link_libraries(dstalk-smoke-test
target_link_libraries(dstalk_smoke_test
PRIVATE dstalk
)
add_test(NAME dstalk-smoke-test COMMAND dstalk-smoke-test)
add_test(NAME dstalk_smoke_test COMMAND dstalk_smoke_test)
# ============================================================
# dstalk-host-api-test — host API 单元测试
# dstalk_host_api_test — host API 单元测试
# ============================================================
add_executable(dstalk-host-api-test
add_executable(dstalk_host_api_test
host_api_test.cpp
${CMAKE_SOURCE_DIR}/dstalk_core/src/service_registry.cpp
)
target_include_directories(dstalk-host-api-test
target_include_directories(dstalk_host_api_test
PRIVATE ${CMAKE_SOURCE_DIR}/dstalk_core/src
)
target_compile_features(dstalk-host-api-test
target_compile_features(dstalk_host_api_test
PRIVATE cxx_std_17
)
target_link_libraries(dstalk-host-api-test
target_link_libraries(dstalk_host_api_test
PRIVATE dstalk
)
add_test(NAME dstalk-host-api-test COMMAND dstalk-host-api-test)
add_test(NAME dstalk_host_api_test COMMAND dstalk_host_api_test)
# ============================================================
# dstalk-event-bus-test — EventBus 单元测试
# dstalk_event_bus_test — EventBus 单元测试
# ============================================================
add_executable(dstalk-event-bus-test
add_executable(dstalk_event_bus_test
event_bus_test.cpp
${CMAKE_SOURCE_DIR}/dstalk_core/src/event_bus.cpp
)
target_include_directories(dstalk-event-bus-test
target_include_directories(dstalk_event_bus_test
PRIVATE ${CMAKE_SOURCE_DIR}/dstalk_core/src
)
target_compile_features(dstalk-event-bus-test
target_compile_features(dstalk_event_bus_test
PRIVATE cxx_std_17
)
add_test(NAME dstalk-event-bus-test COMMAND dstalk-event-bus-test)
add_test(NAME dstalk_event_bus_test COMMAND dstalk_event_bus_test)
# ============================================================
# dstalk-service-registry-test — ServiceRegistry 补充单元测试
# dstalk_service_registry_test — ServiceRegistry 补充单元测试
# ============================================================
add_executable(dstalk-service-registry-test
add_executable(dstalk_service_registry_test
service_registry_test.cpp
${CMAKE_SOURCE_DIR}/dstalk_core/src/service_registry.cpp
)
target_include_directories(dstalk-service-registry-test
target_include_directories(dstalk_service_registry_test
PRIVATE ${CMAKE_SOURCE_DIR}/dstalk_core/src
)
target_compile_features(dstalk-service-registry-test
target_compile_features(dstalk_service_registry_test
PRIVATE cxx_std_17
)
add_test(NAME dstalk-service-registry-test COMMAND dstalk-service-registry-test)
add_test(NAME dstalk_service_registry_test COMMAND dstalk_service_registry_test)
# ============================================================
# dstalk-context-plugin-test — Context 插件单元测试
# dstalk_context_plugin_test — Context 插件单元测试
# W18.1 (qa-wang + architect-lin): 覆盖 token 计数/trim/UTF-8 边界
# ============================================================
add_executable(dstalk-context-plugin-test
add_executable(dstalk_context_plugin_test
context_plugin_test.cpp
)
target_link_libraries(dstalk-context-plugin-test
target_link_libraries(dstalk_context_plugin_test
PRIVATE dstalk
)
add_test(NAME dstalk-context-plugin-test COMMAND dstalk-context-plugin-test)
add_test(NAME dstalk_context_plugin_test COMMAND dstalk_context_plugin_test)
# ============================================================
# dstalk-plugin-loader-test — PluginLoader 安全回归测试
# dstalk_plugin_loader_test — PluginLoader 安全回归测试
# W20.3 (qa-xu): 覆盖 W19 F-18.3-1~5 修复验证
# ============================================================
add_executable(dstalk-plugin-loader-test
add_executable(dstalk_plugin_loader_test
plugin_loader_test.cpp
${CMAKE_SOURCE_DIR}/dstalk_core/src/plugin_loader.cpp
${CMAKE_SOURCE_DIR}/dstalk_core/src/service_registry.cpp
${CMAKE_SOURCE_DIR}/dstalk_core/src/boost_json.cpp
)
target_include_directories(dstalk-plugin-loader-test
target_include_directories(dstalk_plugin_loader_test
PRIVATE ${CMAKE_SOURCE_DIR}/dstalk_core/src
)
target_compile_features(dstalk-plugin-loader-test
target_compile_features(dstalk_plugin_loader_test
PRIVATE cxx_std_17
)
find_package(Boost REQUIRED CONFIG)
target_compile_definitions(dstalk-plugin-loader-test
target_compile_definitions(dstalk_plugin_loader_test
PRIVATE
BOOST_JSON_HEADER_ONLY
BOOST_ALL_NO_LIB
DSTALK_TEST_PLUGINS_DIR="${CMAKE_BINARY_DIR}/plugins"
)
target_link_libraries(dstalk-plugin-loader-test
target_link_libraries(dstalk_plugin_loader_test
PRIVATE
dstalk
boost::boost
)
add_test(NAME dstalk-plugin-loader-test COMMAND dstalk-plugin-loader-test)
add_test(NAME dstalk_plugin_loader_test COMMAND dstalk_plugin_loader_test)
# ============================================================
# dstalk-anthropic-plugin-test — Anthropic AI 插件单元测试
# dstalk_anthropic_plugin_test — Anthropic AI 插件单元测试
# W21.6 (qa-wang): 通过 #include source 访问 static 函数
# ============================================================
add_executable(dstalk-anthropic-plugin-test
add_executable(dstalk_anthropic_plugin_test
anthropic_plugin_test.cpp
)
target_include_directories(dstalk-anthropic-plugin-test
target_include_directories(dstalk_anthropic_plugin_test
PRIVATE ${CMAKE_SOURCE_DIR}/dstalk_core/include
PRIVATE ${CMAKE_SOURCE_DIR}/plugins_upper/ai_common/include
)
target_compile_definitions(dstalk-anthropic-plugin-test
target_compile_definitions(dstalk_anthropic_plugin_test
PRIVATE
BOOST_JSON_HEADER_ONLY
BOOST_ALL_NO_LIB
)
target_link_libraries(dstalk-anthropic-plugin-test
target_link_libraries(dstalk_anthropic_plugin_test
PRIVATE
dstalk
ai_common
boost::boost
)
add_test(NAME dstalk-anthropic-plugin-test COMMAND dstalk-anthropic-plugin-test)
add_test(NAME dstalk_anthropic_plugin_test COMMAND dstalk_anthropic_plugin_test)
# ============================================================
# dstalk-openai-plugin-test — OpenAI 兼容 AI 插件单元测试
# dstalk_openai_plugin_test — OpenAI 兼容 AI 插件单元测试
# W21.6 (qa-wang): 通过 #include source 访问 static 函数
# ============================================================
add_executable(dstalk-openai-plugin-test
add_executable(dstalk_openai_plugin_test
openai_plugin_test.cpp
)
target_include_directories(dstalk-openai-plugin-test
target_include_directories(dstalk_openai_plugin_test
PRIVATE ${CMAKE_SOURCE_DIR}/dstalk_core/include
PRIVATE ${CMAKE_SOURCE_DIR}/plugins_upper/ai_common/include
)
target_compile_definitions(dstalk-openai-plugin-test
target_compile_definitions(dstalk_openai_plugin_test
PRIVATE
BOOST_JSON_HEADER_ONLY
BOOST_ALL_NO_LIB
)
target_link_libraries(dstalk-openai-plugin-test
target_link_libraries(dstalk_openai_plugin_test
PRIVATE
dstalk
ai_common
boost::boost
)
add_test(NAME dstalk-openai-plugin-test COMMAND dstalk-openai-plugin-test)
add_test(NAME dstalk_openai_plugin_test COMMAND dstalk_openai_plugin_test)
# ============================================================
# dstalk-network-plugin-test — Network 插件单元测试
# dstalk_network_plugin_test — Network 插件单元测试
# W22.2 (qa-xu): 通过 #include source 访问 static 函数
# ============================================================
find_package(OpenSSL REQUIRED CONFIG)
add_executable(dstalk-network-plugin-test
add_executable(dstalk_network_plugin_test
network_plugin_test.cpp
)
target_include_directories(dstalk-network-plugin-test
target_include_directories(dstalk_network_plugin_test
PRIVATE ${CMAKE_SOURCE_DIR}/dstalk_core/include
)
target_compile_definitions(dstalk-network-plugin-test
target_compile_definitions(dstalk_network_plugin_test
PRIVATE
BOOST_ALL_NO_LIB
)
target_link_libraries(dstalk-network-plugin-test
target_link_libraries(dstalk_network_plugin_test
PRIVATE
dstalk
boost::boost
openssl::openssl
)
add_test(NAME dstalk-network-plugin-test COMMAND dstalk-network-plugin-test)
add_test(NAME dstalk_network_plugin_test COMMAND dstalk_network_plugin_test)
# ============================================================
# coverage — gcovr 覆盖率报告 (HTML + 终端摘要)

View File

@@ -10,6 +10,7 @@
#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>

View File

@@ -10,6 +10,7 @@
#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>

View File

@@ -55,6 +55,7 @@ static void mock_log(int level, const char* fmt, ...) {
// Stub host_api 函数:除 log 外所有操作均返回失败/默认值
static int stub_reg(const char*, int, void*) { return -1; }
static void* stub_query(const char*, int) { return nullptr; }
static void stub_unreg(const char*) {}
static int stub_sub(int, dstalk_event_handler_fn, void*) { return -1; }
static int stub_emit(int, const void*) { return -1; }
static void stub_unsub(int) {}
@@ -67,7 +68,7 @@ static char* stub_strdup(const char*) { return nullptr; }
// Mock host_api vtable: all stubs except mock_log for capturing error-path diagnostics
// Mock host_api 虚表:除 mock_log 外全部 stub用于捕获错误路径诊断
static dstalk_host_api_t g_mock_host_api = {
stub_reg, stub_query,
stub_reg, stub_query, stub_unreg,
stub_sub, stub_emit, stub_unsub,
stub_cget, stub_cset,
mock_log,
@@ -148,8 +149,8 @@ int main()
dstalk::PluginLoader loader;
fs::path plugins_dir = get_plugins_dir();
fs::path dll_config = plugins_dir / "plugin-config.dll";
fs::path dll_fileio = plugins_dir / "plugin-file_io.dll";
fs::path dll_config = plugins_dir / "plugin_config.dll";
fs::path dll_fileio = plugins_dir / "plugin_file_io.dll";
bool have_plugins = fs::exists(dll_config) && fs::exists(dll_fileio);
@@ -206,8 +207,8 @@ int main()
fs::path plugins_dir = get_plugins_dir();
std::vector<fs::path> dlls;
for (auto name : {"plugin-config.dll", "plugin-file_io.dll",
"plugin-context.dll", "plugin-session.dll"}) {
for (auto name : {"plugin_config.dll", "plugin_file_io.dll",
"plugin_context.dll", "plugin_session.dll"}) {
fs::path p = plugins_dir / name;
if (fs::exists(p)) dlls.push_back(p);
}

View File

@@ -186,7 +186,7 @@ int main()
// 测试服务查询: ai可能因为没有真实 API key 而失败,但服务应存在)
// Test service query: ai (may fail without real API key, but service should exist)
const char* ai_provider = dstalk_config_get("ai.provider");
if (!ai_provider) ai_provider = "ai.openai";
if (!ai_provider) ai_provider = "ai_openai";
auto* ai = static_cast<const dstalk_ai_service_t*>(
dstalk_service_query(ai_provider, 1));
if (ai) {
@@ -598,12 +598,12 @@ int main()
std::cout << "[OK] R3: http error path, no response body (connection refused)\n";
}
} else {
// 回退:测 AI 服务 (ai.openai) 错误路径
// Fallback: test AI service (ai.openai) error path
// 回退:测 AI 服务 (ai_openai) 错误路径
// Fallback: test AI service (ai_openai) error path
auto* ai_svc = static_cast<const dstalk_ai_service_t*>(
dstalk_service_query("ai.openai", 1));
dstalk_service_query("ai_openai", 1));
if (ai_svc) {
std::cout << "[OK] R3: ai.openai service found (http fallback)\n";
std::cout << "[OK] R3: ai_openai service found (http fallback)\n";
dstalk_message_t msg = {"user", "hi", nullptr, nullptr};
dstalk_chat_result_t r = ai_svc->chat(&msg, 1, "", nullptr);
// api_key="test-key" 为无效 key应返回 error result 而非崩溃