Wave 5+6: plugin ABI hardening, build modernization, ABI/security docs
Wave 5 (9 parallel agents): - W1.1 atomic diag callback + DLL handle release on shutdown (lin) - W2.1 unify cross-DLL heap discipline (host->alloc/free/strdup) (chen) - W2.2 secure_zero api_key on shutdown for deepseek/anthropic (cao) - W3 CMake modernization: target-based cxx_std_20, dstalk_boost_config INTERFACE lib, root-level RUNTIME_OUTPUT_DIRECTORY (hu) - W4 GitHub Actions CI with dynamic Linux/Windows matrix (ma) - W5.1 SSE buffer_body to cut peak memory ~67% on 32K streams (zhou) - W6.1 LSP JSON-RPC frame parser hardened against header reordering (sun) - W7 smoke test: copy plugin DLLs post-build + Boost.JSON src.hpp fix for full 9-plugin load coverage (wang) - W8.1 README slimmed 398->92, Diataxis docs/ skeleton (deng) Wave 6 (6 parallel agents): - W9.1 docs/explanation: architecture + plugin-lifecycle (deng) - W9.3 log credential leak audit (0 vulns, audit trail in docs/explanation/security-logging.md) (cao) - W9.4 docs/reference/plugin-abi.md - 7-point ABI contract (lin) - W9.6 CLI /history command + status integration (zhao) - W9.8 plugin_loader fault tolerance: per-plugin failure no longer aborts dstalk_init (huang) - W9.10 host_api unit tests: tests/host_api_test.cpp, 8 cases (liu) CEO oversight (preexisting bugs fixed during Wave 5 verification): - lsp_plugin.cpp:449 forward decl mismatch (int vs void) - tools_plugin.cpp:109 missing forward decl Multi-agent collaboration framework: - agents/WORKFLOW.md: 6-stage protocol, two-tier governance, prompt template, technical constraints registry Build: cmake --build 0 error / 0 warning. Tests: 2/2 100% pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -10,10 +10,27 @@ target_link_libraries(dstalk-smoke-test
|
||||
PRIVATE dstalk
|
||||
)
|
||||
|
||||
add_custom_command(TARGET dstalk-smoke-test POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
$<TARGET_FILE:dstalk>
|
||||
$<TARGET_FILE_DIR:dstalk-smoke-test>
|
||||
add_test(NAME dstalk-smoke-test COMMAND dstalk-smoke-test)
|
||||
|
||||
# ============================================================
|
||||
# dstalk-host-api-test — host API 单元测试
|
||||
# ============================================================
|
||||
|
||||
add_executable(dstalk-host-api-test
|
||||
host_api_test.cpp
|
||||
${CMAKE_SOURCE_DIR}/dstalk-core/src/service_registry.cpp
|
||||
)
|
||||
|
||||
add_test(NAME dstalk-smoke-test COMMAND dstalk-smoke-test)
|
||||
target_include_directories(dstalk-host-api-test
|
||||
PRIVATE ${CMAKE_SOURCE_DIR}/dstalk-core/src
|
||||
)
|
||||
|
||||
target_compile_features(dstalk-host-api-test
|
||||
PRIVATE cxx_std_17
|
||||
)
|
||||
|
||||
target_link_libraries(dstalk-host-api-test
|
||||
PRIVATE dstalk
|
||||
)
|
||||
|
||||
add_test(NAME dstalk-host-api-test COMMAND dstalk-host-api-test)
|
||||
|
||||
180
tests/host_api_test.cpp
Normal file
180
tests/host_api_test.cpp
Normal file
@@ -0,0 +1,180 @@
|
||||
// ============================================================================
|
||||
// host_api_test.cpp — host API 单元测试 (独立于 smoke_test)
|
||||
// ============================================================================
|
||||
// 测试: register_service / query_service / alloc / free / log / init / shutdown
|
||||
// ============================================================================
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
// 引入 ServiceRegistry 实现做纯单元测试
|
||||
#include "service_registry.hpp"
|
||||
|
||||
#include "dstalk/dstalk_host.h"
|
||||
|
||||
// ---- 轻量断言 ----
|
||||
static int g_failures = 0;
|
||||
#define TCHECK(cond, msg) do { \
|
||||
if (cond) { \
|
||||
std::cout << "[OK] " << (msg) << "\n"; \
|
||||
} else { \
|
||||
std::cerr << "[FAIL] " << (msg) << "\n"; \
|
||||
g_failures++; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// ---- 辅助: 创建临时配置文件 ----
|
||||
static std::string make_temp_config(const std::string& tag) {
|
||||
auto dir = std::filesystem::temp_directory_path() / ("dstalk-host-api-" + tag);
|
||||
std::filesystem::create_directories(dir);
|
||||
auto config_path = dir / "config.toml";
|
||||
{
|
||||
std::ofstream c(config_path);
|
||||
// 指向不存在的插件目录,避免加载任何 .dll
|
||||
c << "plugin_dir = \"__no_such_plugins_dir__\"\n";
|
||||
}
|
||||
return config_path.string();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
int main()
|
||||
{
|
||||
std::cout << "=== dstalk host_api unit tests ===\n\n";
|
||||
|
||||
// ====================================================================
|
||||
// Test 1: register_service 重复注册 同名+同版本 → 应返回 -2
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::ServiceRegistry reg;
|
||||
void* dummy_vtable = reinterpret_cast<void*>(0x1);
|
||||
int r1 = reg.register_service("echo", 1, dummy_vtable);
|
||||
TCHECK(r1 == 0, "register_service(\"echo\",1) first call returns 0");
|
||||
|
||||
int r2 = reg.register_service("echo", 1, dummy_vtable);
|
||||
TCHECK(r2 == -2,
|
||||
"register_service(\"echo\",1) duplicate same-version returns -2");
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Test 2: register_service 同名+不同版本 → 应返回 -2
|
||||
// 名称已占用,与版本无关
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::ServiceRegistry reg;
|
||||
void* dummy_vtable = reinterpret_cast<void*>(0x1);
|
||||
int r1 = reg.register_service("calc", 1, dummy_vtable);
|
||||
TCHECK(r1 == 0, "register_service(\"calc\",1) first call returns 0");
|
||||
|
||||
int r2 = reg.register_service("calc", 99, dummy_vtable);
|
||||
TCHECK(r2 == -2,
|
||||
"register_service(\"calc\",99) diff-version duplicate returns -2");
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Test 3: query_service 不存在的 name → nullptr
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::ServiceRegistry reg;
|
||||
void* q = reg.query_service("ghost_service", 1);
|
||||
TCHECK(q == nullptr, "query_service(\"ghost_service\",1) returns nullptr");
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Test 4: query_service 错误版本号 → nullptr
|
||||
// 注册 v=1, 查询 min_version=2 → 不满足 → nullptr
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::ServiceRegistry reg;
|
||||
void* dummy_vtable = reinterpret_cast<void*>(0x2);
|
||||
reg.register_service("solo", 1, dummy_vtable);
|
||||
|
||||
void* q = reg.query_service("solo", 2);
|
||||
TCHECK(q == nullptr, "query_service(\"solo\",2) with only v1 available returns nullptr");
|
||||
|
||||
// 确证以正确版本查询能拿到
|
||||
void* q2 = reg.query_service("solo", 1);
|
||||
TCHECK(q2 == dummy_vtable, "query_service(\"solo\",1) with v1 available returns vtable");
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Test 5: dstalk_init 多次调用 → 第二次应返回 -1 (幂等拒绝)
|
||||
// ====================================================================
|
||||
{
|
||||
std::string cfg = make_temp_config("init-twice");
|
||||
int r1 = dstalk_init(cfg.c_str());
|
||||
TCHECK(r1 == 0, "dstalk_init first call returns 0");
|
||||
|
||||
int r2 = dstalk_init(cfg.c_str());
|
||||
TCHECK(r2 == -1, "dstalk_init second call returns -1 (idempotent guard)");
|
||||
|
||||
dstalk_shutdown();
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Test 6: alloc(0) / free(nullptr) 行为
|
||||
// malloc(0) 可返回 null 或合法指针; 两者都可 free
|
||||
// free(nullptr) 是安全空操作
|
||||
// ====================================================================
|
||||
{
|
||||
void* p = dstalk_alloc(0);
|
||||
std::cout << "[OK] dstalk_alloc(0) returned " << p
|
||||
<< " (null or valid, both acceptable)\n";
|
||||
dstalk_free(p);
|
||||
std::cout << "[OK] dstalk_free(alloc(0)) did not crash\n";
|
||||
|
||||
dstalk_free(nullptr);
|
||||
std::cout << "[OK] dstalk_free(nullptr) did not crash\n";
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Test 7: log 各 level 不崩溃 (DEBUG / INFO / WARN / ERROR)
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk_log(DSTALK_LOG_DEBUG, "host_api_test: debug level message");
|
||||
std::cout << "[OK] dstalk_log(DEBUG) no crash\n";
|
||||
|
||||
dstalk_log(DSTALK_LOG_INFO, "host_api_test: info level message");
|
||||
std::cout << "[OK] dstalk_log(INFO) no crash\n";
|
||||
|
||||
dstalk_log(DSTALK_LOG_WARN, "host_api_test: warn level message");
|
||||
std::cout << "[OK] dstalk_log(WARN) no crash\n";
|
||||
|
||||
dstalk_log(DSTALK_LOG_ERROR, "host_api_test: error level message");
|
||||
std::cout << "[OK] dstalk_log(ERROR) no crash\n";
|
||||
|
||||
// 带格式参数
|
||||
dstalk_log(DSTALK_LOG_INFO, "formatted: %s %d", "answer", 42);
|
||||
std::cout << "[OK] dstalk_log with format args no crash\n";
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Test 8: dstalk_shutdown 后 query_service → nullptr
|
||||
// g_service_registry 已被 delete 置空
|
||||
// ====================================================================
|
||||
{
|
||||
std::string cfg = make_temp_config("after-shutdown");
|
||||
dstalk_init(cfg.c_str());
|
||||
dstalk_shutdown();
|
||||
|
||||
void* q = dstalk_service_query("any_service", 1);
|
||||
TCHECK(q == nullptr, "dstalk_service_query after shutdown returns nullptr");
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 结果
|
||||
// ====================================================================
|
||||
std::cout << "\n";
|
||||
if (g_failures == 0) {
|
||||
std::cout << "=== All host_api tests passed ===\n";
|
||||
return 0;
|
||||
} else {
|
||||
std::cerr << "=== " << g_failures << " host_api test(s) FAILED ===\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ int main()
|
||||
char* content = nullptr;
|
||||
if (file_io->read(file_path.string().c_str(), &content) == 0 && content) {
|
||||
bool ok = std::strcmp(content, sample_content) == 0;
|
||||
std::free(content);
|
||||
dstalk_free(content);
|
||||
if (ok) {
|
||||
std::cout << "[OK] file_io->read content matches\n";
|
||||
} else {
|
||||
@@ -123,7 +123,7 @@ int main()
|
||||
char* saved = nullptr;
|
||||
if (file_io->read(saved_path.string().c_str(), &saved) == 0 && saved) {
|
||||
bool session_ok = std::strcmp(saved, session_content) == 0;
|
||||
std::free(saved);
|
||||
dstalk_free(saved);
|
||||
if (session_ok) {
|
||||
std::cout << "[OK] session content matches after save/load\n";
|
||||
} else {
|
||||
@@ -331,7 +331,7 @@ int main()
|
||||
<< " expected length: " << std::strlen(escape_content) << "\n"
|
||||
<< " got length: " << std::strlen(read_back) << "\n";
|
||||
}
|
||||
std::free(read_back);
|
||||
dstalk_free(read_back);
|
||||
} else {
|
||||
std::cerr << "[FAIL] escape content read-back failed\n";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user