Add metadata validation script and module documentation
- Introduced a new Python script `check_agents_metadata.py` for validating agent metadata, including YAML parsing, rating ranges, and cross-references. - Added usage instructions and exit codes for the script. - Created a new markdown file `模块目录和功能说明.md` to outline the directory structure and functionality of the modules. - Added a text file `说明此文件不可AI修改.txt` to specify that certain files should not be modified by AI, including important information about the `dstalk` framework and its modules.
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
// ============================================================================
|
||||
// anthropic_plugin_test.cpp — Anthropic AI 插件单元测试
|
||||
// W21.6 (qa-wang): 覆盖 SSE 解析 / JSON 请求构建 / URL 解析 / 安全擦除
|
||||
// 通过 #include plugin source 访问 file-scope static 函数
|
||||
// ============================================================================
|
||||
/*
|
||||
* @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/anthropic/src/anthropic_plugin.cpp"
|
||||
@@ -12,6 +16,7 @@
|
||||
#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"; \
|
||||
@@ -22,6 +27,8 @@ static int g_failures = 0;
|
||||
} 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";
|
||||
@@ -31,10 +38,18 @@ static void setup_config() {
|
||||
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";
|
||||
|
||||
@@ -69,14 +84,14 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Malformed JSON: unclosed brace
|
||||
// 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
|
||||
// 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)");
|
||||
@@ -84,6 +99,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// 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";
|
||||
|
||||
@@ -146,6 +162,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// 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";
|
||||
|
||||
@@ -194,11 +211,12 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// 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
|
||||
// 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);
|
||||
@@ -206,7 +224,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// text_delta with unicode content (Japanese)
|
||||
// text_delta with unicode content (Japanese) / 含 unicode 内容的 text_delta(日语)
|
||||
std::string token;
|
||||
const char* json =
|
||||
"{\"type\":\"content_block_delta\","
|
||||
@@ -221,6 +239,7 @@ int main()
|
||||
|
||||
{
|
||||
// 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\","
|
||||
@@ -233,12 +252,13 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// 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
|
||||
// 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,
|
||||
@@ -257,7 +277,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// With system message in history
|
||||
// With system message in history / 历史中包含系统消息
|
||||
dstalk_message_t msgs[1] = {
|
||||
{"system", "You are a helpful assistant", nullptr, nullptr}
|
||||
};
|
||||
@@ -268,6 +288,7 @@ int main()
|
||||
"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.
|
||||
@@ -276,7 +297,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// With user+assistant history
|
||||
// With user+assistant history / 包含 user+assistant 历史
|
||||
dstalk_message_t msgs[2] = {
|
||||
{"user", "What is 2+2?", nullptr, nullptr},
|
||||
{"assistant", "It is 4.", nullptr, nullptr}
|
||||
@@ -292,11 +313,12 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// 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
|
||||
// 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,
|
||||
@@ -314,7 +336,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Temperature in valid range -> should be included
|
||||
// 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,
|
||||
@@ -323,7 +345,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Temperature out of range -> should NOT be included
|
||||
// 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,
|
||||
@@ -336,7 +358,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// History with null role (should default to "")
|
||||
// History with null role (should default to "") / null 角色的历史(应默认为 "")
|
||||
dstalk_message_t msgs[1] = {
|
||||
{nullptr, "some content", nullptr, nullptr}
|
||||
};
|
||||
@@ -345,7 +367,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// History with null content
|
||||
// History with null content / null 内容的历史
|
||||
dstalk_message_t msgs[1] = {
|
||||
{"user", nullptr, nullptr, nullptr}
|
||||
};
|
||||
@@ -355,6 +377,7 @@ int main()
|
||||
|
||||
{
|
||||
// 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");
|
||||
@@ -362,7 +385,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Multiple system messages concatenated
|
||||
// Multiple system messages concatenated / 多条系统消息拼接
|
||||
dstalk_message_t msgs[2] = {
|
||||
{"system", "Rule 1: be polite", nullptr, nullptr},
|
||||
{"system", "Rule 2: be concise", nullptr, nullptr}
|
||||
@@ -376,6 +399,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 7: build_headers_json
|
||||
// 测试块 7:build_headers_json
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 7: build_headers_json ---\n";
|
||||
|
||||
@@ -392,7 +416,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// With empty API key
|
||||
// With empty API key / 空 API key
|
||||
std::string saved = g_cfg.api_key;
|
||||
g_cfg.api_key = "";
|
||||
std::string headers = build_headers_json();
|
||||
@@ -405,6 +429,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 8: extract_host_port
|
||||
// 测试块 8:extract_host_port
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 8: extract_host_port ---\n";
|
||||
|
||||
@@ -473,6 +498,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 9: secure_zero
|
||||
// 测试块 9:secure_zero
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 9: secure_zero ---\n";
|
||||
|
||||
@@ -488,7 +514,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Zero-length should not crash
|
||||
// Zero-length should not crash / 零长度不应崩溃
|
||||
char buf[4] = {1,2,3,4};
|
||||
secure_zero(buf, 0);
|
||||
CHECK(buf[0] == 1 && buf[3] == 4,
|
||||
@@ -496,18 +522,20 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Null pointer + zero length = no-op
|
||||
// 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)");
|
||||
}
|
||||
@@ -524,11 +552,13 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// 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);
|
||||
@@ -539,13 +569,13 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Null string params — should not crash
|
||||
// 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
|
||||
// Summary / 总结
|
||||
// ================================================================
|
||||
std::cout << "\n";
|
||||
if (g_failures == 0) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// ============================================================================
|
||||
// context_plugin_test.cpp — 上下文插件单元测试
|
||||
// ============================================================================
|
||||
// W18.1 (qa-wang + architect-lin): 覆盖 token 计数、trim、UTF-8 边界、
|
||||
// 0xC0/0xC1 过短编码检测。修复 F-11.1-3/4/5/6 后补充测试。
|
||||
// ============================================================================
|
||||
/*
|
||||
* @file context_plugin_test.cpp
|
||||
* @brief Context plugin unit tests: token counting (ASCII, CJK, mixed, emoji),
|
||||
* UTF-8 truncation safety, trim edge cases, and system message preservation.
|
||||
* Context 插件单元测试:token 计数(ASCII、CJK、混合、emoji)、UTF-8 截断安全、trim 边界情况、系统消息保留。
|
||||
* Copyright (c) 2026 dstalk contributors. GPLv3.
|
||||
*/
|
||||
|
||||
#include "dstalk/dstalk_host.h"
|
||||
|
||||
@@ -14,6 +15,7 @@
|
||||
#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"; \
|
||||
@@ -23,6 +25,12 @@ static int g_failures = 0;
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// Context 插件测试:token 计数边界(null、空、ASCII、CJK、混合)、截断 UTF-8 边界保护 (F-11.1-4)、
|
||||
// 0xC0/0xC1 超长编码 (F-11.1-6)、多消息 token、trim 的各种场景、系统消息保留、4 字节 emoji、孤立的续字节。
|
||||
// Context plugin tests: token counting edge cases (null, empty, ASCII, CJK, mixed),
|
||||
// truncated UTF-8 bounds protection (F-11.1-4), 0xC0/0xC1 overlong encoding (F-11.1-6),
|
||||
// multiple-message tokens, trim null/edge/within-limit/exceeds-limit scenarios,
|
||||
// system message preservation, 4-byte emoji, and lone continuation bytes.
|
||||
int main()
|
||||
{
|
||||
const auto dir = std::filesystem::temp_directory_path() / "dstalk-ctx-test";
|
||||
@@ -54,6 +62,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 1: count_tokens edge cases (null / empty)
|
||||
// 测试块 1:count_tokens 边界情况(null / 空)
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 1: count_tokens edge cases ---\n";
|
||||
|
||||
@@ -77,6 +86,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 2: count_tokens — ASCII
|
||||
// 测试块 2:count_tokens — ASCII
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 2: count_tokens ASCII ---\n";
|
||||
|
||||
@@ -107,6 +117,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 3: count_tokens — Chinese (CJK U+4E00-U+9FFF)
|
||||
// 测试块 3:count_tokens — 中文 (CJK U+4E00-U+9FFF)
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 3: count_tokens Chinese (CJK) ---\n";
|
||||
|
||||
@@ -132,6 +143,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 4: count_tokens — Mixed content
|
||||
// 测试块 4:count_tokens — 混合内容
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 4: count_tokens mixed content ---\n";
|
||||
|
||||
@@ -146,6 +158,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 5: Truncated UTF-8 bounds protection (F-11.1-4)
|
||||
// 测试块 5:截断 UTF-8 边界保护 (F-11.1-4)
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 5: Truncated UTF-8 (F-11.1-4 fix) ---\n";
|
||||
|
||||
@@ -197,6 +210,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 6: 0xC0/0xC1 overlong encoding (F-11.1-6)
|
||||
// 测试块 6:0xC0/0xC1 超长编码 (F-11.1-6)
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 6: 0xC0/0xC1 overlong encoding (F-11.1-6 fix) ---\n";
|
||||
|
||||
@@ -230,6 +244,7 @@ int main()
|
||||
{
|
||||
// Verify 0xC0/0xC1 are NOT treated as valid 2-byte sequences
|
||||
// They should each count as 1 other_char, not as 2-byte sequence
|
||||
// 验证 0xC0/0xC1 不被视为合法的 2 字节序列 / 它们每个应计为 1 个 other_char,而非 2 字节序列
|
||||
// 0xC0 + 0xC1 + 2 ASCII = 2 other + 2 ascii
|
||||
// = (2/3) + (2/4) + 4 overhead = 0 + 0 + 4 = 4
|
||||
// Actually 2/4 = 0 (integer division) for ascii, 2/3 = 0 for other
|
||||
@@ -244,6 +259,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 7: count_tokens — multiple messages
|
||||
// 测试块 7:count_tokens — 多消息
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 7: multiple messages ---\n";
|
||||
|
||||
@@ -275,6 +291,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 8: trim — null and edge cases
|
||||
// 测试块 8:trim — null 和边界情况
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 8: trim edge cases ---\n";
|
||||
|
||||
@@ -291,6 +308,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 9: trim — within limit (no trimming needed)
|
||||
// 测试块 9:trim — 预算内(无需裁剪)
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 9: trim within limit ---\n";
|
||||
|
||||
@@ -320,6 +338,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 10: trim — exceeds limit (trimming required)
|
||||
// 测试块 10:trim — 超预算(需要裁剪)
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 10: trim exceeds limit ---\n";
|
||||
|
||||
@@ -358,6 +377,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 11: trim — system message preservation
|
||||
// 测试块 11:trim — 系统消息保留
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 11: trim preserves system messages ---\n";
|
||||
|
||||
@@ -387,11 +407,12 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 12: count_tokens — 4-byte UTF-8 (emoji / supplementary)
|
||||
// 测试块 12:count_tokens — 4 字节 UTF-8(emoji / 补充平面)
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 12: 4-byte UTF-8 ---\n";
|
||||
|
||||
{
|
||||
// U+1F600 (😀) = F0 9F 98 80
|
||||
// U+1F600 (<EFBFBD><EFBFBD>) = F0 9F 98 80
|
||||
char buf[6] = {static_cast<char>(0xF0), static_cast<char>(0x9F),
|
||||
static_cast<char>(0x98), static_cast<char>(0x80), '\0'};
|
||||
dstalk_message_t msg = {"user", buf, nullptr, nullptr};
|
||||
@@ -403,6 +424,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 13: count_tokens — continuation bytes as lone chars
|
||||
// 测试块 13:count_tokens — 孤立的续字节
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 13: lone continuation bytes ---\n";
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// ============================================================================
|
||||
// deepseek_plugin_test.cpp — DeepSeek AI 插件单元测试
|
||||
// W21.6 (qa-wang): 覆盖 SSE 解析 / [DONE] 匹配 / JSON 请求构建 / tool_calls
|
||||
// 通过 #include plugin source 访问 file-scope static 函数
|
||||
// ============================================================================
|
||||
/*
|
||||
* @file deepseek_plugin_test.cpp
|
||||
* @brief DeepSeek 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/deepseek/src/deepseek_plugin.cpp"
|
||||
@@ -12,6 +16,7 @@
|
||||
#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"; \
|
||||
@@ -22,6 +27,8 @@ static int g_failures = 0;
|
||||
} 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 = "deepseek";
|
||||
g_cfg.base_url = "https://api.deepseek.com/v1";
|
||||
@@ -31,10 +38,19 @@ static void setup_config() {
|
||||
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
|
||||
// 测试块 1:parse_sse_line — 无效/畸形输入
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 1: parse_sse_line invalid/malformed ---\n";
|
||||
|
||||
@@ -58,27 +74,28 @@ int main()
|
||||
|
||||
{
|
||||
// "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: " 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: " 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: "
|
||||
// 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");
|
||||
@@ -86,6 +103,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 2: parse_sse_line — [DONE] sentinel
|
||||
// 测试块 2:parse_sse_line — [DONE] 标记
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 2: parse_sse_line [DONE] sentinel ---\n";
|
||||
|
||||
@@ -97,7 +115,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// [DONE] with leading whitespace
|
||||
// [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");
|
||||
@@ -105,7 +123,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// [DONE] with trailing whitespace
|
||||
// [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");
|
||||
@@ -113,7 +131,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// [DONE] with tabs and newlines around it
|
||||
// [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");
|
||||
@@ -121,7 +139,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// [DONE] without spaces — exact match
|
||||
// [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");
|
||||
@@ -129,13 +147,14 @@ int main()
|
||||
|
||||
{
|
||||
// "[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" 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");
|
||||
@@ -143,6 +162,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 3: parse_sse_line — content delta
|
||||
// 测试块 3:parse_sse_line — content delta
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 3: parse_sse_line content delta ---\n";
|
||||
|
||||
@@ -166,7 +186,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Delta with no content field
|
||||
// Delta with no content field / delta 不含 content 字段
|
||||
std::string token;
|
||||
const char* json =
|
||||
"data: {\"choices\":[{\"delta\":{},\"index\":0}]}";
|
||||
@@ -175,7 +195,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Empty choices array
|
||||
// Empty choices array / 空 choices 数组
|
||||
std::string token;
|
||||
const char* json =
|
||||
"data: {\"choices\":[]}";
|
||||
@@ -184,7 +204,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Single character token (typical streaming)
|
||||
// Single character token (typical streaming) / 单字符 token(典型流式)
|
||||
std::string token;
|
||||
const char* json =
|
||||
"data: {\"choices\":[{\"delta\":{\"content\":\"H\"},\"index\":0}]}";
|
||||
@@ -194,7 +214,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Multi-byte UTF-8 content (emoji) in delta
|
||||
// Multi-byte UTF-8 content (emoji) in delta / delta 中的多字节 UTF-8 内容(emoji)
|
||||
std::string token;
|
||||
const char* json =
|
||||
"data: {\"choices\":[{\"delta\":{\"content\":\"\\uD83D\\uDE00\"},"
|
||||
@@ -207,7 +227,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Malformed JSON structure — no "delta" key
|
||||
// Malformed JSON structure — no "delta" key / 畸形 JSON 结构 — 无 "delta" key
|
||||
std::string token;
|
||||
const char* json =
|
||||
"data: {\"choices\":[{\"no_delta\":{},\"index\":0}]}";
|
||||
@@ -217,6 +237,7 @@ int main()
|
||||
|
||||
{
|
||||
// Realistic DeepSeek streaming chunk (with finish_reason)
|
||||
// 真实的 DeepSeek 流式数据块(含 finish_reason)
|
||||
std::string token;
|
||||
const char* json =
|
||||
"data: {\"id\":\"chatcmpl-xxx\","
|
||||
@@ -233,11 +254,13 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 4: parse_sse_line — tool_calls delta
|
||||
// 测试块 4:parse_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 =
|
||||
@@ -258,8 +281,9 @@ int main()
|
||||
|
||||
{
|
||||
// tool_calls arguments chunk (second chunk, same index)
|
||||
// tool_calls arguments 数据块(第二个数据块,相同 index)
|
||||
StreamContext ctx;
|
||||
// First, set up the initial state
|
||||
// First, set up the initial state / 先设置初始状态
|
||||
ctx.tool_calls.push_back({0, "call_abc123", "get_weather", ""});
|
||||
|
||||
std::string token;
|
||||
@@ -276,7 +300,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// tool_calls final arguments chunk
|
||||
// tool_calls final arguments chunk / tool_calls 最终 arguments 数据块
|
||||
StreamContext ctx;
|
||||
ctx.tool_calls.push_back({0, "call_abc123", "get_weather", "{\"city\":\""});
|
||||
|
||||
@@ -295,6 +319,7 @@ int main()
|
||||
|
||||
{
|
||||
// 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,"
|
||||
@@ -306,6 +331,7 @@ int main()
|
||||
|
||||
{
|
||||
// Multiple tool_calls in single chunk (unusual but valid)
|
||||
// 单个数据块中有多个 tool_calls(不常见但合法)
|
||||
StreamContext ctx;
|
||||
std::string token;
|
||||
const char* json =
|
||||
@@ -325,6 +351,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 5: build_request_json — basic cases
|
||||
// 测试块 5:build_request_json — 基础用例
|
||||
// ================================================================
|
||||
setup_config();
|
||||
std::cout << "\n--- Block 5: build_request_json basic ---\n";
|
||||
@@ -351,7 +378,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// With user+assistant history
|
||||
// With user+assistant history / 包含 user+assistant 历史
|
||||
dstalk_message_t msgs[2] = {
|
||||
{"user", "What is 2+2?", nullptr, nullptr},
|
||||
{"assistant", "It is 4.", nullptr, nullptr}
|
||||
@@ -376,22 +403,26 @@ int main()
|
||||
|
||||
{
|
||||
// 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
|
||||
// 测试块 6:build_request_json — tools / 边界情况
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 6: build_request_json tools / edges ---\n";
|
||||
|
||||
{
|
||||
// With tools_json
|
||||
// With tools_json / 含 tools_json
|
||||
std::string tools = "[{\"type\":\"function\","
|
||||
"\"function\":{\"name\":\"get_weather\","
|
||||
"\"description\":\"Get current weather\","
|
||||
@@ -407,7 +438,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Empty tools_json — no tools field
|
||||
// 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,
|
||||
@@ -418,6 +449,8 @@ int main()
|
||||
// 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);
|
||||
@@ -430,7 +463,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// History with null role
|
||||
// History with null role / null 角色的历史
|
||||
dstalk_message_t msgs[1] = {
|
||||
{nullptr, "some content", nullptr, nullptr}
|
||||
};
|
||||
@@ -439,7 +472,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// History with null content
|
||||
// History with null content / null 内容的历史
|
||||
dstalk_message_t msgs[1] = {
|
||||
{"user", nullptr, nullptr, nullptr}
|
||||
};
|
||||
@@ -448,7 +481,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Very long message
|
||||
// Very long message / 超长消息
|
||||
std::string long_input(5000, 'A');
|
||||
std::string json = build_request_json(
|
||||
nullptr, 0, long_input, "", false);
|
||||
@@ -458,6 +491,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 7: build_headers_json
|
||||
// 测试块 7:build_headers_json
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 7: build_headers_json ---\n";
|
||||
|
||||
@@ -470,7 +504,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Empty API key
|
||||
// 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");
|
||||
@@ -480,6 +514,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 8: extract_host_port (same logic as anthropic)
|
||||
// 测试块 8:extract_host_port(逻辑同 anthropic)
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 8: extract_host_port ---\n";
|
||||
|
||||
@@ -525,6 +560,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 9: secure_zero
|
||||
// 测试块 9:secure_zero
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 9: secure_zero ---\n";
|
||||
|
||||
@@ -546,6 +582,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 10: append_history
|
||||
// 测试块 10:append_history
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 10: append_history ---\n";
|
||||
|
||||
@@ -561,7 +598,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Tool message (should include tool_call_id)
|
||||
// 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);
|
||||
@@ -575,7 +612,7 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Assistant with tool_calls_json
|
||||
// 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\":\"{}\"}}]";
|
||||
@@ -589,14 +626,14 @@ int main()
|
||||
}
|
||||
|
||||
{
|
||||
// Empty history (0 messages)
|
||||
// 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
|
||||
// Multiple messages / 多条消息
|
||||
json::array msgs;
|
||||
dstalk_message_t ms[2] = {
|
||||
{"user", "Q1", nullptr, nullptr},
|
||||
@@ -608,6 +645,7 @@ int main()
|
||||
|
||||
{
|
||||
// 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);
|
||||
@@ -619,11 +657,13 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 11: my_free_result — null safety
|
||||
// 测试块 11:my_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 为 nullptr,free_result 应提前返回
|
||||
my_free_result(nullptr);
|
||||
CHECK(true, "T11.1: free_result(nullptr) does not crash (null host)");
|
||||
}
|
||||
@@ -637,6 +677,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 12: my_configure — null host safety
|
||||
// 测试块 12:my_configure — null host 安全
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 12: my_configure null host safety ---\n";
|
||||
|
||||
@@ -656,7 +697,7 @@ int main()
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Summary
|
||||
// Summary / 总结
|
||||
// ================================================================
|
||||
std::cout << "\n";
|
||||
if (g_failures == 0) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ============================================================================
|
||||
// event_bus_test.cpp — EventBus 单元测试
|
||||
// ============================================================================
|
||||
// 测试: subscribe / unsubscribe / emit / 多订阅者 / 空总线
|
||||
// ============================================================================
|
||||
/*
|
||||
* @file event_bus_test.cpp
|
||||
* @brief EventBus unit tests: subscribe, emit, unsubscribe, multi-handler
|
||||
* dispatch order, independent event types.
|
||||
* EventBus 单元测试:订阅、发布、取消订阅、多处理器分发顺序、独立事件类型。
|
||||
* Copyright (c) 2026 dstalk contributors. GPLv3.
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
@@ -13,6 +15,7 @@
|
||||
|
||||
// ---- 轻量断言 ----
|
||||
static int g_failures = 0;
|
||||
// Lightweight assertion helper: increments g_failures counter on failure
|
||||
#define TCHECK(cond, msg) do { \
|
||||
if (cond) { \
|
||||
std::cout << "[OK] " << (msg) << "\n"; \
|
||||
@@ -22,13 +25,16 @@ static int g_failures = 0;
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// ============================================================
|
||||
// EventBus 单元测试:订阅+发布、取消订阅、多处理器分发顺序、空总线、独立事件类型路由、取消不存在的订阅。
|
||||
// EventBus unit tests: subscribe+emit, unsubscribe, multi-handler dispatch order,
|
||||
// empty bus, independent event type routing, and non-existent unsubscribe safety.
|
||||
int main()
|
||||
{
|
||||
std::cout << "=== dstalk event_bus unit tests ===\n\n";
|
||||
|
||||
// ====================================================================
|
||||
// Test 1: subscribe + emit — 基本发布订阅流程
|
||||
// Test 1: subscribe + emit — basic pub/sub flow
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::EventBus bus;
|
||||
@@ -49,6 +55,7 @@ int main()
|
||||
|
||||
// ====================================================================
|
||||
// Test 2: unsubscribe — 取消订阅后 handler 不再被调用
|
||||
// Test 2: unsubscribe — handler NOT called after unsubscription
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::EventBus bus;
|
||||
@@ -64,6 +71,7 @@ int main()
|
||||
|
||||
// ====================================================================
|
||||
// Test 3: 多订阅者 — 同一事件多个 handler 按订阅顺序全部调用
|
||||
// Test 3: multi-subscriber — all handlers for same event invoked in subscription order
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::EventBus bus;
|
||||
@@ -77,13 +85,14 @@ int main()
|
||||
TCHECK(emitted == 3, "emit returns 3 handlers called");
|
||||
TCHECK(order.size() == 3, "all 3 handlers invoked");
|
||||
|
||||
// 验证订阅顺序 (FIFO: 按 subscribe 顺序触发)
|
||||
// 验证订阅顺序 (FIFO: 按 subscribe 顺序触发) / Verify subscription order (FIFO: in subscribe order)
|
||||
bool ordered = (order[0] == 1 && order[1] == 2 && order[2] == 3);
|
||||
TCHECK(ordered, "handlers invoked in subscription order (1,2,3)");
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Test 4: 空总线 emit 不崩溃,返回 0
|
||||
// Test 4: emit on empty bus no crash, returns 0
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::EventBus bus;
|
||||
@@ -93,6 +102,7 @@ int main()
|
||||
|
||||
// ====================================================================
|
||||
// Test 5: 不同 event_type 独立分发 — 只触发匹配的 handler
|
||||
// Test 5: independent event_type dispatch — only matching handler triggered
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::EventBus bus;
|
||||
@@ -112,15 +122,16 @@ int main()
|
||||
|
||||
// ====================================================================
|
||||
// Test 6: 退订不存在的 ID 不崩溃
|
||||
// Test 6: unsubscribe non-existent ID does not crash
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::EventBus bus;
|
||||
bus.unsubscribe(99999); // 不存在的 ID
|
||||
bus.unsubscribe(99999); // 不存在的 ID / non-existent ID
|
||||
std::cout << "[OK] unsubscribe non-existent ID (99999) did not crash\n";
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 结果
|
||||
// 结果 / Result
|
||||
// ====================================================================
|
||||
std::cout << "\n";
|
||||
if (g_failures == 0) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ============================================================================
|
||||
// host_api_test.cpp — host API 单元测试 (独立于 smoke_test)
|
||||
// ============================================================================
|
||||
// 测试: register_service / query_service / alloc / free / log / init / shutdown
|
||||
// ============================================================================
|
||||
/*
|
||||
* @file host_api_test.cpp
|
||||
* @brief Host API unit tests: service registration, event bus, config store,
|
||||
* alloc/free, logging, init/shutdown lifecycle.
|
||||
* Host API 单元测试:服务注册、事件总线、配置存储、alloc/free、日志、init/shutdown 生命周期。
|
||||
* Copyright (c) 2026 dstalk contributors. GPLv3.
|
||||
*/
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
@@ -13,13 +15,14 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
// 引入 ServiceRegistry 实现做纯单元测试
|
||||
// 引入 ServiceRegistry 实现做纯单元测试 / Include ServiceRegistry impl for pure unit tests
|
||||
#include "service_registry.hpp"
|
||||
|
||||
#include "dstalk/dstalk_host.h"
|
||||
|
||||
// ---- 轻量断言 ----
|
||||
static int g_failures = 0;
|
||||
// Lightweight assertion helper: increments g_failures counter on failure
|
||||
#define TCHECK(cond, msg) do { \
|
||||
if (cond) { \
|
||||
std::cout << "[OK] " << (msg) << "\n"; \
|
||||
@@ -29,26 +32,32 @@ static int g_failures = 0;
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// ---- 辅助: 创建临时配置文件 ----
|
||||
|
||||
// Helper: creates a temporary config.toml pointing to a non-existent plugin dir,
|
||||
// so dstalk_init loads no external plugins during tests.
|
||||
// 辅助函数:创建临时 config.toml 指向不存在的插件目录,使 dstalk_init 在测试时不加载任何外部插件。
|
||||
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
|
||||
// 指向不存在的插件目录,避免加载任何 .dll / Point to nonexistent plugin dir, avoid loading any .dll
|
||||
c << "plugin_dir = \"__no_such_plugins_dir__\"\n";
|
||||
}
|
||||
return config_path.string();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Host API 单元测试:覆盖注册/查询重复、版本不匹配、双重 init 防护、alloc/free 边界、日志级别、shutdown 后查询。
|
||||
// Host API unit tests: covers register/query duplicates, version mismatch,
|
||||
// double-init guard, alloc/free edge cases, logging levels, and post-shutdown query.
|
||||
int main()
|
||||
{
|
||||
std::cout << "=== dstalk host_api unit tests ===\n\n";
|
||||
|
||||
// ====================================================================
|
||||
// Test 1: register_service 重复注册 同名+同版本 → 应返回 -2
|
||||
// Test 1: register_service duplicate same-name+same-version -> should return -2
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::ServiceRegistry reg;
|
||||
@@ -64,6 +73,8 @@ int main()
|
||||
// ====================================================================
|
||||
// Test 2: register_service 同名+不同版本 → 应返回 -2
|
||||
// 名称已占用,与版本无关
|
||||
// Test 2: register_service same-name+different-version -> should return -2
|
||||
// Name already taken, regardless of version
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::ServiceRegistry reg;
|
||||
@@ -78,6 +89,7 @@ int main()
|
||||
|
||||
// ====================================================================
|
||||
// Test 3: query_service 不存在的 name → nullptr
|
||||
// Test 3: query_service nonexistent name -> nullptr
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::ServiceRegistry reg;
|
||||
@@ -88,6 +100,8 @@ int main()
|
||||
// ====================================================================
|
||||
// Test 4: query_service 错误版本号 → nullptr
|
||||
// 注册 v=1, 查询 min_version=2 → 不满足 → nullptr
|
||||
// Test 4: query_service wrong version -> nullptr
|
||||
// Registered v=1, query min_version=2 -> unsatisfied -> nullptr
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::ServiceRegistry reg;
|
||||
@@ -97,13 +111,14 @@ int main()
|
||||
void* q = reg.query_service("solo", 2);
|
||||
TCHECK(q == nullptr, "query_service(\"solo\",2) with only v1 available returns nullptr");
|
||||
|
||||
// 确证以正确版本查询能拿到
|
||||
// 确证以正确版本查询能拿到 / Confirm correct version query works
|
||||
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 (幂等拒绝)
|
||||
// Test 5: dstalk_init multiple calls -> second should return -1 (idempotent guard)
|
||||
// ====================================================================
|
||||
{
|
||||
std::string cfg = make_temp_config("init-twice");
|
||||
@@ -120,6 +135,9 @@ int main()
|
||||
// Test 6: alloc(0) / free(nullptr) 行为
|
||||
// malloc(0) 可返回 null 或合法指针; 两者都可 free
|
||||
// free(nullptr) 是安全空操作
|
||||
// Test 6: alloc(0) / free(nullptr) behavior
|
||||
// malloc(0) may return null or valid pointer; both are free-able
|
||||
// free(nullptr) is a safe no-op
|
||||
// ====================================================================
|
||||
{
|
||||
void* p = dstalk_alloc(0);
|
||||
@@ -134,6 +152,7 @@ int main()
|
||||
|
||||
// ====================================================================
|
||||
// Test 7: log 各 level 不崩溃 (DEBUG / INFO / WARN / ERROR)
|
||||
// Test 7: log at each level no crash (DEBUG / INFO / WARN / ERROR)
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk_log(DSTALK_LOG_DEBUG, "host_api_test: debug level message");
|
||||
@@ -148,7 +167,7 @@ int main()
|
||||
dstalk_log(DSTALK_LOG_ERROR, "host_api_test: error level message");
|
||||
std::cout << "[OK] dstalk_log(ERROR) no crash\n";
|
||||
|
||||
// 带格式参数
|
||||
// 带格式参数 / With format args
|
||||
dstalk_log(DSTALK_LOG_INFO, "formatted: %s %d", "answer", 42);
|
||||
std::cout << "[OK] dstalk_log with format args no crash\n";
|
||||
}
|
||||
@@ -156,6 +175,8 @@ int main()
|
||||
// ====================================================================
|
||||
// Test 8: dstalk_shutdown 后 query_service → nullptr
|
||||
// g_service_registry 已被 delete 置空
|
||||
// Test 8: query_service after dstalk_shutdown -> nullptr
|
||||
// g_service_registry has been deleted and nulled
|
||||
// ====================================================================
|
||||
{
|
||||
std::string cfg = make_temp_config("after-shutdown");
|
||||
@@ -167,7 +188,7 @@ int main()
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 结果
|
||||
// 结果 / Result
|
||||
// ====================================================================
|
||||
std::cout << "\n";
|
||||
if (g_failures == 0) {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// ============================================================================
|
||||
// network_plugin_test.cpp — Network 插件单元测试
|
||||
// W22.2 (qa-xu): 覆盖 parse_headers_json / SSE 行解析 / 参数校验
|
||||
// 通过 #include plugin source 访问 file-scope static 函数
|
||||
// ============================================================================
|
||||
/*
|
||||
* @file network_plugin_test.cpp
|
||||
* @brief Network plugin unit tests (W22.2): parse_headers_json (normal, empty,
|
||||
* malformed, long values), SSE line splitting boundaries, and
|
||||
* http_post_json/http_post_stream parameter validation.
|
||||
* Network 插件单元测试 (W22.2):parse_headers_json(正常、空、畸形、长值)、SSE 行解析边界、
|
||||
* http_post_json/http_post_stream 参数校验。
|
||||
* Copyright (c) 2026 dstalk contributors. GPLv3.
|
||||
*/
|
||||
#define _CRT_SECURE_NO_WARNINGS
|
||||
#define BOOST_ASIO_DISABLE_STD_TO_ADDRESS
|
||||
#include "../plugins/network/src/network_plugin.cpp"
|
||||
@@ -15,6 +19,7 @@
|
||||
#include <vector>
|
||||
|
||||
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"; \
|
||||
@@ -24,9 +29,9 @@ static int g_failures = 0;
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// ================================================================
|
||||
// SSE 行分割 helper (复刻 do_post_stream 的 emit_lines 逻辑)
|
||||
// ================================================================
|
||||
// SSE line-split helper: mirrors do_post_stream's emit_lines logic for unit-testing
|
||||
// SSE chunk parsing without a live network connection.
|
||||
// SSE 行分割辅助函数:镜像 do_post_stream 的 emit_lines 逻辑,无需实时网络连接即可单元测试 SSE 数据块解析。
|
||||
static std::vector<std::string> split_sse_lines(std::string fragment) {
|
||||
std::vector<std::string> lines;
|
||||
size_t pos = 0;
|
||||
@@ -55,11 +60,17 @@ static std::vector<std::string> split_sse_lines(std::string fragment) {
|
||||
return lines;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Network 插件测试 (W22.2):parse_headers_json 正常/空/畸形/长值、
|
||||
// SSE 行分割(LF、CRLF、空、null 字节、尾部 CR)、
|
||||
// http_post_json/http_post_stream 参数校验(空指针)。
|
||||
// Network plugin tests (W22.2): parse_headers_json normal/empty/malformed/long,
|
||||
// SSE line splitting (LF, CRLF, empty, null-bytes, trailing CR),
|
||||
// and http_post_json/http_post_stream parameter validation (null pointers).
|
||||
int main()
|
||||
{
|
||||
// ================================================================
|
||||
// Test Block 1: parse_headers_json — 正常 JSON
|
||||
// Test Block 1: parse_headers_json — normal JSON
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 1: parse_headers_json normal JSON ---\n";
|
||||
|
||||
@@ -98,6 +109,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 2: parse_headers_json — 空 / null 输入
|
||||
// Test Block 2: parse_headers_json — empty/null input
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 2: parse_headers_json empty/null input ---\n";
|
||||
|
||||
@@ -124,6 +136,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 3: parse_headers_json — 畸形 JSON
|
||||
// Test Block 3: parse_headers_json — malformed JSON
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 3: parse_headers_json malformed JSON ---\n";
|
||||
|
||||
@@ -177,6 +190,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 4: parse_headers_json — 超长 header 值
|
||||
// Test Block 4: parse_headers_json — long values
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 4: parse_headers_json long values ---\n";
|
||||
|
||||
@@ -209,6 +223,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 5: SSE 行解析边界
|
||||
// Test Block 5: SSE line splitting boundaries
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 5: SSE line splitting boundaries ---\n";
|
||||
|
||||
@@ -274,6 +289,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 6: http_post_json — 参数校验 (null ptr, early return)
|
||||
// Test Block 6: http_post_json — parameter validation (null ptr, early return)
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 6: http_post_json parameter validation ---\n";
|
||||
|
||||
@@ -319,6 +335,7 @@ int main()
|
||||
|
||||
// ================================================================
|
||||
// Test Block 7: http_post_stream — 参数校验
|
||||
// Test Block 7: http_post_stream — parameter validation
|
||||
// ================================================================
|
||||
std::cout << "\n--- Block 7: http_post_stream parameter validation ---\n";
|
||||
|
||||
@@ -338,7 +355,7 @@ int main()
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Summary
|
||||
// Summary / 总结
|
||||
// ================================================================
|
||||
std::cout << "\n";
|
||||
if (g_failures == 0) {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
// ============================================================================
|
||||
// plugin_loader_test.cpp — PluginLoader 安全回归测试
|
||||
// ============================================================================
|
||||
// W20.3 (qa-xu 徐磊): 覆盖 W19 修复的 5 条发现 (F-18.3-1~5)
|
||||
// - F-18.3-3: 路径验证 (lexically_normal + 扩展名 + 目录约束)
|
||||
// - F-18.3-4: next_id_ atomic 唯一性 + 单调递增
|
||||
// - F-18.3-2: host_api_->log 调用 (mock 验证)
|
||||
// - F-18.3-1: try/catch 异常安全边界 (间接: 注入 mock 不崩溃)
|
||||
// ============================================================================
|
||||
/*
|
||||
* @file plugin_loader_test.cpp
|
||||
* @brief PluginLoader safety regression tests (W20.3): path validation,
|
||||
* ABI checks, next_id_ atomicity, failure-path logging with mock host API.
|
||||
* PluginLoader 安全回归测试 (W20.3):路径验证、ABI 检查、next_id_ 原子性、失败路径日志(使用 mock host API)。
|
||||
* Copyright (c) 2026 dstalk contributors. GPLv3.
|
||||
*/
|
||||
|
||||
#include "plugin_loader.hpp"
|
||||
|
||||
@@ -24,6 +22,7 @@ namespace fs = std::filesystem;
|
||||
|
||||
// ---- 轻量断言 ----
|
||||
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"; \
|
||||
@@ -35,11 +34,14 @@ static int g_failures = 0;
|
||||
|
||||
// ============================================================================
|
||||
// Mock host_api — 捕获 log 调用以验证失败路径日志 (F-18.3-2)
|
||||
// Mock host_api — captures log calls to verify failure-path logging (F-18.3-2)
|
||||
// ============================================================================
|
||||
static int g_log_call_count = 0;
|
||||
static int g_last_severity = 0;
|
||||
static char g_last_log_msg[1024] = {0};
|
||||
|
||||
// Mock host_api::log implementation: counts calls and captures last severity+message
|
||||
// Mock host_api::log 实现:计数调用并捕获最后的 severity+message
|
||||
static void mock_log(int level, const char* fmt, ...) {
|
||||
g_log_call_count++;
|
||||
g_last_severity = level;
|
||||
@@ -49,6 +51,8 @@ static void mock_log(int level, const char* fmt, ...) {
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
// Stub host_api functions: return failure/default for all operations except log
|
||||
// Stub host_api 函数:除 log 外所有操作均返回失败/默认值
|
||||
static int stub_reg(const char*, int, void*) { return -1; }
|
||||
static void* stub_query(const char*, int) { return nullptr; }
|
||||
static int stub_sub(int, dstalk_event_handler_fn, void*) { return -1; }
|
||||
@@ -60,6 +64,8 @@ static void* stub_alloc(size_t) { return nullptr; }
|
||||
static void stub_free(void*) {}
|
||||
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_sub, stub_emit, stub_unsub,
|
||||
@@ -68,15 +74,16 @@ static dstalk_host_api_t g_mock_host_api = {
|
||||
stub_alloc, stub_free, stub_strdup
|
||||
};
|
||||
|
||||
// Reset log capture state between tests
|
||||
// 重置日志捕获状态(测试间使用)
|
||||
static void reset_log_state() {
|
||||
g_log_call_count = 0;
|
||||
g_last_severity = 0;
|
||||
g_last_log_msg[0] = '\0';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper: 获取已构建的 plugins/ 目录绝对路径
|
||||
// ============================================================================
|
||||
// Get the absolute path to the build output plugins/ directory
|
||||
// 获取构建输出 plugins/ 目录的绝对路径
|
||||
static fs::path get_plugins_dir() {
|
||||
#ifdef DSTALK_TEST_PLUGINS_DIR
|
||||
return fs::path(DSTALK_TEST_PLUGINS_DIR);
|
||||
@@ -85,50 +92,56 @@ static fs::path get_plugins_dir() {
|
||||
#endif
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PluginLoader 回归测试 (W20.3):F-18.3-3 路径验证拒绝、F-18.3-4 next_id_ 唯一性+单调性+并发、
|
||||
// F-18.3-2 失败路径日志,以及边界情况(空 loader、无效操作)。
|
||||
// PluginLoader regression tests (W20.3): F-18.3-3 path validation rejection,
|
||||
// F-18.3-4 next_id_ uniqueness+monotonic+concurrent, F-18.3-2 failure-path logging,
|
||||
// and edge cases (empty loader, invalid operations).
|
||||
int main()
|
||||
{
|
||||
std::cout << "=== dstalk plugin_loader regression tests (W20.3) ===\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// Block 1: 路径验证 — 拒绝非法路径 (F-18.3-3)
|
||||
// Block 1: Path validation — reject illegal paths (F-18.3-3)
|
||||
// ========================================================================
|
||||
std::cout << "--- Block 1: Path validation — rejection ---\n";
|
||||
{
|
||||
dstalk::PluginLoader loader;
|
||||
|
||||
// T1.1: nullptr
|
||||
// T1.1: nullptr / null pointer
|
||||
CHECK(loader.load_plugin(nullptr) == -1,
|
||||
"T1.1: nullptr path returns -1");
|
||||
|
||||
// T1.2: 非法扩展名 .txt
|
||||
// T1.2: 非法扩展名 .txt / illegal .txt extension
|
||||
CHECK(loader.load_plugin("plugins/test.txt") == -1,
|
||||
"T1.2: .txt extension rejected");
|
||||
|
||||
// T1.3: 路径含 .. 遍历
|
||||
// T1.3: 路径含 .. 遍历 / path contains .. traversal
|
||||
CHECK(loader.load_plugin("../plugins/test.dll") == -1,
|
||||
"T1.3: ../ traversal rejected");
|
||||
|
||||
// T1.4: 不在 plugins/ 目录下
|
||||
// T1.4: 不在 plugins/ 目录下 / not under plugins/ dir
|
||||
auto tmp = fs::temp_directory_path() / "dstalk_test_no_plugins" / "test.dll";
|
||||
CHECK(loader.load_plugin(tmp.string().c_str()) == -1,
|
||||
"T1.4: path not under plugins/ dir rejected");
|
||||
|
||||
// T1.5: 路径中间的 .. 段
|
||||
// T1.5: 路径中间的 .. 段 / .. segment in middle of path
|
||||
CHECK(loader.load_plugin("plugins/../secret/test.dll") == -1,
|
||||
"T1.5: .. in middle of path rejected");
|
||||
|
||||
// T1.6: 无扩展名
|
||||
// T1.6: 无扩展名 / no extension
|
||||
CHECK(loader.load_plugin("plugins/test") == -1,
|
||||
"T1.6: no extension rejected");
|
||||
|
||||
// T1.7: 合法扩展名但不在 plugins/ 下
|
||||
// T1.7: 合法扩展名但不在 plugins/ 下 / valid extension but not under plugins/
|
||||
CHECK(loader.load_plugin("/etc/someconfig.so") == -1,
|
||||
"T1.7: .so extension but not under plugins/ rejected");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Block 2: 合法路径 — 成功加载 + next_id_ 验证 (F-18.3-4)
|
||||
// Block 2: Valid path — successful load + ID uniqueness (F-18.3-4)
|
||||
// ========================================================================
|
||||
std::cout << "\n--- Block 2: Valid path — successful load + ID uniqueness ---\n";
|
||||
{
|
||||
@@ -144,23 +157,23 @@ int main()
|
||||
std::cout << "[WARN] Plugin DLLs not found at " << plugins_dir.string()
|
||||
<< " — skipping Block 2\n";
|
||||
} else {
|
||||
// T2.1: 加载第一个插件
|
||||
// T2.1: 加载第一个插件 / load first plugin
|
||||
int id1 = loader.load_plugin(dll_config.string().c_str());
|
||||
CHECK(id1 >= 1, "T2.1: first plugin loaded with positive ID");
|
||||
std::cout << " id1 = " << id1 << "\n";
|
||||
|
||||
// T2.2: 加载第二个不同插件
|
||||
// T2.2: 加载第二个不同插件 / load second (different) plugin
|
||||
int id2 = loader.load_plugin(dll_fileio.string().c_str());
|
||||
CHECK(id2 >= 1, "T2.2: second plugin loaded with positive ID");
|
||||
std::cout << " id2 = " << id2 << "\n";
|
||||
|
||||
// T2.3: ID 唯一
|
||||
// T2.3: ID 唯一 / IDs are unique
|
||||
CHECK(id1 != id2, "T2.3: IDs are unique (next_id_ atomicity)");
|
||||
|
||||
// T2.4: ID 单调递增
|
||||
// T2.4: ID 单调递增 / IDs monotonically increasing
|
||||
CHECK(id2 > id1, "T2.4: IDs monotonically increasing");
|
||||
|
||||
// T2.5: get_plugin 可查询到已加载插件
|
||||
// T2.5: get_plugin 可查询到已加载插件 / get_plugin can find loaded plugin
|
||||
const dstalk::PluginInfo* info1 = loader.get_plugin(id1);
|
||||
CHECK(info1 != nullptr, "T2.5: get_plugin(id1) returns non-null");
|
||||
if (info1) {
|
||||
@@ -168,23 +181,24 @@ int main()
|
||||
std::cout << " plugin1 name: " << info1->name << "\n";
|
||||
}
|
||||
|
||||
// T2.7: get_plugin 对无效 ID 返回 nullptr
|
||||
// T2.7: get_plugin 对无效 ID 返回 nullptr / get_plugin returns nullptr for invalid ID
|
||||
CHECK(loader.get_plugin(99999) == nullptr,
|
||||
"T2.7: get_plugin(invalid_id) returns nullptr");
|
||||
|
||||
// T2.8: 卸载后 get_plugin 返回 nullptr
|
||||
// T2.8: 卸载后 get_plugin 返回 nullptr / get_plugin returns nullptr after unload
|
||||
int ret = loader.unload_plugin(id1);
|
||||
CHECK(ret == 0, "T2.8: unload_plugin returns 0");
|
||||
CHECK(loader.get_plugin(id1) == nullptr,
|
||||
"T2.9: get_plugin returns nullptr after unload");
|
||||
|
||||
// 清理
|
||||
// 清理 / cleanup
|
||||
loader.unload_plugin(id2);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Block 3: next_id_ 原子性 — 多线程并发加载 (F-18.3-4)
|
||||
// Block 3: next_id_ atomicity — concurrent loads (F-18.3-4)
|
||||
// ========================================================================
|
||||
std::cout << "\n--- Block 3: next_id_ atomicity — concurrent loads ---\n";
|
||||
{
|
||||
@@ -213,7 +227,7 @@ int main()
|
||||
|
||||
for (auto& t : threads) t.join();
|
||||
|
||||
// 验证: 所有 load 成功, ID 唯一且 > 0
|
||||
// 验证: 所有 load 成功, ID 唯一且 > 0 / Verify: all loads succeed, IDs unique and > 0
|
||||
std::vector<int> valid_ids;
|
||||
for (size_t i = 0; i < ids.size(); ++i) {
|
||||
CHECK(ids[i] >= 1, "T3." + std::to_string(i)
|
||||
@@ -222,7 +236,7 @@ int main()
|
||||
if (ids[i] >= 1) valid_ids.push_back(ids[i]);
|
||||
}
|
||||
|
||||
// 去重后大小应等于成功加载数
|
||||
// 去重后大小应等于成功加载数 / dedup size should equal successful load count
|
||||
std::sort(valid_ids.begin(), valid_ids.end());
|
||||
auto dup = std::unique(valid_ids.begin(), valid_ids.end());
|
||||
size_t unique_count = std::distance(valid_ids.begin(), dup);
|
||||
@@ -231,26 +245,27 @@ int main()
|
||||
+ std::to_string(unique_count) + "/"
|
||||
+ std::to_string(valid_ids.size()) + ")");
|
||||
|
||||
// 清理
|
||||
// 清理 / cleanup
|
||||
for (int id : valid_ids) loader.unload_plugin(id);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Block 4: 失败路径日志 — host_api->log 被调用 (F-18.3-2)
|
||||
// Block 4: Failure-path logging — host_api->log is called (F-18.3-2)
|
||||
// ========================================================================
|
||||
std::cout << "\n--- Block 4: Failure-path logging (host_api->log) ---\n";
|
||||
{
|
||||
dstalk::PluginLoader loader;
|
||||
|
||||
// 4.1: 无 host_api 时 load_plugin 失败不崩溃
|
||||
// 4.1: 无 host_api 时 load_plugin 失败不崩溃 / load_plugin fails without crash when no host_api
|
||||
reset_log_state();
|
||||
int id = loader.load_plugin("bad_ext.noext");
|
||||
CHECK(id == -1, "T4.1: load_plugin with invalid ext returns -1 (no host_api)");
|
||||
CHECK(g_log_call_count == 0,
|
||||
"T4.2: log NOT called when host_api_ is null");
|
||||
|
||||
// 4.2: 设置 mock host_api 后验证 log 被调用
|
||||
// 4.2: 设置 mock host_api 后验证 log 被调用 / set mock host_api and verify log is called
|
||||
int init_ret = loader.initialize_all(&g_mock_host_api);
|
||||
CHECK(init_ret == 0, "T4.3: initialize_all with mock host_api returns 0");
|
||||
|
||||
@@ -263,7 +278,7 @@ int main()
|
||||
"T4.6: log severity is DSTALK_LOG_ERROR");
|
||||
std::cout << " log msg: " << g_last_log_msg << "\n";
|
||||
|
||||
// 4.3: LoadLibrary 失败也触发 log (文件不存在)
|
||||
// 4.3: LoadLibrary 失败也触发 log (文件不存在) / LoadLibrary failure also triggers log (file missing)
|
||||
reset_log_state();
|
||||
fs::path missing = get_plugins_dir() / "nonexistent_plugin.dll";
|
||||
id = loader.load_plugin(missing.string().c_str());
|
||||
@@ -275,28 +290,29 @@ int main()
|
||||
|
||||
// ========================================================================
|
||||
// Block 5: 边界 — 空 loader / 无效操作
|
||||
// Block 5: Edge cases — empty loader / invalid operations
|
||||
// ========================================================================
|
||||
std::cout << "\n--- Block 5: Edge cases — empty loader / invalid op ---\n";
|
||||
{
|
||||
dstalk::PluginLoader loader;
|
||||
|
||||
// T5.1: unload 不存在的 ID 返回 -1
|
||||
// T5.1: unload 不存在的 ID 返回 -1 / unload non-existent ID returns -1
|
||||
CHECK(loader.unload_plugin(42) == -1,
|
||||
"T5.1: unload_plugin(nonexistent) returns -1");
|
||||
|
||||
// T5.2: 空 PluginLoader 的 list_plugins 返回 "[]"
|
||||
// T5.2: 空 PluginLoader 的 list_plugins 返回 "[]" / empty PluginLoader list_plugins returns "[]"
|
||||
std::string json = loader.list_plugins();
|
||||
CHECK(!json.empty(), "T5.2: list_plugins returns non-empty string");
|
||||
CHECK(json == "[]", "T5.3: empty loader produces empty JSON array");
|
||||
std::cout << " list_plugins (empty): " << json << "\n";
|
||||
|
||||
// T5.3: get_plugin 在空 loader 上返回 nullptr
|
||||
// T5.3: get_plugin 在空 loader 上返回 nullptr / get_plugin on empty loader returns nullptr
|
||||
CHECK(loader.get_plugin(1) == nullptr,
|
||||
"T5.4: get_plugin on empty loader returns nullptr");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 结果
|
||||
// 结果 / Result
|
||||
// ========================================================================
|
||||
std::cout << "\n";
|
||||
if (g_failures == 0) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// ============================================================================
|
||||
// service_registry_test.cpp — ServiceRegistry 单元测试(补充覆盖,不与 host_api_test 重叠)
|
||||
// ============================================================================
|
||||
// host_api_test 已覆盖: 重复注册(同名同版/同名异版)、查询不存在服务、版本不满足、
|
||||
// shutdown 后查询。本测试补充边界与生命周期路径。
|
||||
// ============================================================================
|
||||
/*
|
||||
* @file service_registry_test.cpp
|
||||
* @brief ServiceRegistry unit tests (supplement to host_api_test): register,
|
||||
* query, version check, unregister, null-pointer safety, re-registration.
|
||||
* ServiceRegistry 单元测试(host_api_test 补充):注册、查询、版本检查、取消注册、空指针安全、重新注册。
|
||||
* Copyright (c) 2026 dstalk contributors. GPLv3.
|
||||
*/
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
@@ -12,6 +13,7 @@
|
||||
|
||||
// ---- 轻量断言 ----
|
||||
static int g_failures = 0;
|
||||
// Lightweight assertion helper: increments g_failures counter on failure
|
||||
#define TCHECK(cond, msg) do { \
|
||||
if (cond) { \
|
||||
std::cout << "[OK] " << (msg) << "\n"; \
|
||||
@@ -21,7 +23,11 @@ static int g_failures = 0;
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// ============================================================
|
||||
// ServiceRegistry 补充测试:空名称/虚表拒绝、完整生命周期(注册→查询→取消注册→查询为空)、
|
||||
// 取消注册空指针安全、取消注册后重新注册、空名称查询。
|
||||
// ServiceRegistry supplement tests: null-name/vtable rejection, full lifecycle
|
||||
// (register->query->unregister->query nullptr), unregister nullptr safety,
|
||||
// re-registration after unregister, and query with nullptr name.
|
||||
int main()
|
||||
{
|
||||
std::cout << "=== dstalk service_registry unit tests (supplement) ===\n\n";
|
||||
@@ -47,6 +53,7 @@ int main()
|
||||
|
||||
// ====================================================================
|
||||
// Test 3: 完整生命周期 — register → query → unregister → query(nullptr)
|
||||
// Test 3: full lifecycle — register → query → unregister → query(nullptr)
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::ServiceRegistry reg;
|
||||
@@ -66,6 +73,7 @@ int main()
|
||||
|
||||
// ====================================================================
|
||||
// Test 4: unregister_service(nullptr name) 不崩溃(安全空操作)
|
||||
// Test 4: unregister_service(nullptr name) does not crash (safe no-op)
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::ServiceRegistry reg;
|
||||
@@ -75,6 +83,7 @@ int main()
|
||||
|
||||
// ====================================================================
|
||||
// Test 5: 注册后重新注册同名 → 先 unregister 再 register 成功
|
||||
// Test 5: re-register same name after unregister → succeeds
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::ServiceRegistry reg;
|
||||
@@ -101,7 +110,7 @@ int main()
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 结果
|
||||
// 结果 / Result
|
||||
// ====================================================================
|
||||
std::cout << "\n";
|
||||
if (g_failures == 0) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// ============================================================================
|
||||
// smoke_test.cpp — 插件化架构烟雾测试
|
||||
// ============================================================================
|
||||
// 测试: 核心初始化、插件加载、服务查询、file_io、session 功能
|
||||
// W13.6 (qa-xu 徐磊): 新增 R1-R4 回归保护点,覆盖 W11.7/W12 已修 bug
|
||||
// ============================================================================
|
||||
/*
|
||||
* @file smoke_test.cpp
|
||||
* @brief Basic smoke test: verifies dstalk_init/shutdown cycle, service queries,
|
||||
* file_io, session, null-safety, escape boundaries, tool chain, and
|
||||
* regression protections R1-R4 (W13.6 qa-xu).
|
||||
* 基础冒烟测试:验证 dstalk_init/shutdown 生命周期、服务查询、file_io、session、
|
||||
* 空指针安全、转义边界、工具链调用,以及回归保护 R1-R4 (W13.6 qa-xu)。
|
||||
* Copyright (c) 2026 dstalk contributors. GPLv3.
|
||||
*/
|
||||
|
||||
#include "dstalk/dstalk_host.h"
|
||||
|
||||
@@ -14,6 +17,7 @@
|
||||
#include <string>
|
||||
|
||||
// ---- 回归测试断言 (W13.6 qa-xu) ----
|
||||
// Regression test assertion macro (W13.6 qa-xu): prints [OK]/[FAIL] and tracks failures
|
||||
static int g_regression_failures = 0;
|
||||
#define REGCHECK(cond, msg) do { \
|
||||
if (cond) { \
|
||||
@@ -24,19 +28,26 @@ static int g_regression_failures = 0;
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// ---- W21.5 mock tool handler (qa-xu) ----
|
||||
// W21.5 mock tool handler (qa-xu): increments call counter and returns mock result JSON
|
||||
// W21.5 模拟工具处理函数 (qa-xu):递增调用计数器并返回模拟结果 JSON
|
||||
static int g_mock_tool_called = 0;
|
||||
static char* mock_tool_handler(const char* /*args_json*/) {
|
||||
g_mock_tool_called++;
|
||||
return dstalk_strdup("{\"mock_result\":\"ok\"}");
|
||||
}
|
||||
|
||||
// 冒烟测试主流程:init → 服务查询 → file_io → session → ai → config,
|
||||
// 然后是扩展测试(空指针安全、转义边界、工具链、session 健壮性),
|
||||
// 接着是回归保护 R1-R3、W21.5 工具调用边界和 R4 生命周期循环。
|
||||
// Smoke test main: init -> service queries -> file_io -> session -> ai -> config,
|
||||
// then extended tests (null-safety, escape, tool chain, session robustness),
|
||||
// then regression protections R1-R3, W21.5 tool-call boundaries, and R4 lifecycle cycles.
|
||||
int main()
|
||||
{
|
||||
const auto dir = std::filesystem::temp_directory_path() / "dstalk-smoke-test";
|
||||
std::filesystem::create_directories(dir);
|
||||
|
||||
// 写一个配置文件用于初始化
|
||||
// 写一个配置文件用于初始化 / Write a config file for initialization
|
||||
const auto config_path = dir / "config.toml";
|
||||
{
|
||||
std::ofstream config(config_path);
|
||||
@@ -47,14 +58,14 @@ int main()
|
||||
<< "model = \"deepseek-v4-pro\"\n";
|
||||
}
|
||||
|
||||
// 初始化主机(会自动扫描 plugins/ 加载插件)
|
||||
// 初始化主机(会自动扫描 plugins/ 加载插件)/ Init host (auto-scans plugins/ to load plugins)
|
||||
if (dstalk_init(config_path.string().c_str()) != 0) {
|
||||
std::cerr << "dstalk_init failed\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "[OK] dstalk_init succeeded\n";
|
||||
|
||||
// 验证插件列表
|
||||
// 验证插件列表 / Verify plugin list
|
||||
{
|
||||
char* list_json = nullptr;
|
||||
int ret = dstalk_plugin_list(&list_json);
|
||||
@@ -66,13 +77,13 @@ int main()
|
||||
}
|
||||
}
|
||||
|
||||
// 测试服务查询: file_io
|
||||
// 测试服务查询: file_io / Test service query: file_io
|
||||
auto* file_io = static_cast<const dstalk_file_io_service_t*>(
|
||||
dstalk_service_query("file_io", 1));
|
||||
if (file_io) {
|
||||
std::cout << "[OK] file_io service found\n";
|
||||
|
||||
// 测试写入
|
||||
// 测试写入 / Test write
|
||||
const auto file_path = dir / "sample.txt";
|
||||
constexpr const char* sample_content = "hello dstalk\nquote=\"yes\" tab=\t slash=\\";
|
||||
if (file_io->write(file_path.string().c_str(), sample_content) == 0) {
|
||||
@@ -83,7 +94,7 @@ int main()
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 测试读取
|
||||
// 测试读取 / Test read
|
||||
char* content = nullptr;
|
||||
if (file_io->read(file_path.string().c_str(), &content) == 0 && content) {
|
||||
bool ok = std::strcmp(content, sample_content) == 0;
|
||||
@@ -104,13 +115,13 @@ int main()
|
||||
std::cerr << "[WARN] file_io service not found (plugin may not be in plugins/ dir)\n";
|
||||
}
|
||||
|
||||
// 测试服务查询: session
|
||||
// 测试服务查询: session / Test service query: session
|
||||
auto* session = static_cast<const dstalk_session_service_t*>(
|
||||
dstalk_service_query("session", 1));
|
||||
if (session) {
|
||||
std::cout << "[OK] session service found\n";
|
||||
|
||||
// 测试 session save/load
|
||||
// 测试 session save/load / Test session save/load
|
||||
const auto session_path = dir / "session.jsonl";
|
||||
const auto saved_path = dir / "session-saved.jsonl";
|
||||
constexpr const char* session_content =
|
||||
@@ -137,7 +148,7 @@ int main()
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 验证保存的内容
|
||||
// 验证保存的内容 / Verify saved content
|
||||
if (file_io) {
|
||||
char* saved = nullptr;
|
||||
if (file_io->read(saved_path.string().c_str(), &saved) == 0 && saved) {
|
||||
@@ -153,16 +164,16 @@ int main()
|
||||
}
|
||||
}
|
||||
|
||||
// 测试 token 计数
|
||||
// 测试 token 计数 / Test token count
|
||||
int tokens = session->token_count();
|
||||
std::cout << "[OK] session->token_count: " << tokens << "\n";
|
||||
|
||||
// 测试 history
|
||||
// 测试 history / Test history
|
||||
int count = 0;
|
||||
session->history(&count);
|
||||
std::cout << "[OK] session->history count: " << count << "\n";
|
||||
|
||||
// 测试 clear
|
||||
// 测试 clear / Test clear
|
||||
session->clear();
|
||||
session->history(&count);
|
||||
if (count == 0) {
|
||||
@@ -173,6 +184,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.deepseek";
|
||||
auto* ai = static_cast<const dstalk_ai_service_t*>(
|
||||
@@ -183,7 +195,7 @@ int main()
|
||||
std::cerr << "[WARN] ai service not found\n";
|
||||
}
|
||||
|
||||
// 测试服务查询: config
|
||||
// 测试服务查询: config / Test service query: config
|
||||
auto* config_svc = static_cast<const dstalk_config_service_t*>(
|
||||
dstalk_service_query("config", 1));
|
||||
if (config_svc) {
|
||||
@@ -196,21 +208,22 @@ int main()
|
||||
std::cerr << "[WARN] config service not found\n";
|
||||
}
|
||||
|
||||
// 测试 dstalk_config_get(主机级配置 API)
|
||||
// 测试 dstalk_config_get(主机级配置 API)/ Test dstalk_config_get (host-level config API)
|
||||
const char* model = dstalk_config_get("api.model");
|
||||
if (model) {
|
||||
std::cout << "[OK] dstalk_config_get(\"api.model\"): " << model << "\n";
|
||||
}
|
||||
|
||||
// 测试 dstalk_log
|
||||
// 测试 dstalk_log / Test dstalk_log
|
||||
dstalk_log(DSTALK_LOG_INFO, "Smoke test completed successfully");
|
||||
|
||||
// ========================================================================
|
||||
// 扩展测试块 C2: null-safety / 转义边界 / tools 调用链 / session 健壮性
|
||||
// Extended test block C2: null-safety / escape boundaries / tools chain / session robustness
|
||||
// ========================================================================
|
||||
std::cout << "\n--- Extended Smoke Tests (C2) ---\n";
|
||||
|
||||
// 提前查询 tools 服务,供后续测试块使用
|
||||
// 提前查询 tools 服务,供后续测试块使用 / Pre-query tools service for subsequent test blocks
|
||||
auto* tools = static_cast<const dstalk_tools_service_t*>(
|
||||
dstalk_service_query("tools", 1));
|
||||
|
||||
@@ -234,7 +247,7 @@ int main()
|
||||
std::cerr << "[FAIL] file_io->write(nullptr, ...) should return error\n";
|
||||
}
|
||||
|
||||
// read 的 content 参数也为 null
|
||||
// read 的 content 参数也为 null / read's content param also null
|
||||
ret = file_io->read("dummy_path", nullptr);
|
||||
if (ret != 0) {
|
||||
std::cout << "[OK] file_io->read(path, nullptr) returned error (" << ret << ")\n";
|
||||
@@ -242,7 +255,7 @@ int main()
|
||||
std::cerr << "[FAIL] file_io->read(path, nullptr) should return error\n";
|
||||
}
|
||||
|
||||
// write 的 content 参数为 null
|
||||
// write 的 content 参数为 null / write's content param is null
|
||||
ret = file_io->write("dummy_path", nullptr);
|
||||
if (ret != 0) {
|
||||
std::cout << "[OK] file_io->write(path, nullptr) returned error (" << ret << ")\n";
|
||||
@@ -278,6 +291,7 @@ int main()
|
||||
char* result = tools->execute(nullptr, nullptr);
|
||||
if (result) {
|
||||
// 实现返回了错误字符串(如 {"error":"tool name is null"}),未崩溃
|
||||
// Implementation returned error string (e.g. {"error":"tool name is null"}), no crash
|
||||
std::cout << "[OK] tools->execute(nullptr, nullptr) did not crash"
|
||||
<< " (returned: " << result << ")\n";
|
||||
dstalk_free(result);
|
||||
@@ -303,7 +317,7 @@ int main()
|
||||
std::cerr << "[FAIL] config->set(nullptr, nullptr) should return error\n";
|
||||
}
|
||||
|
||||
// set 的 value 为 null
|
||||
// set 的 value 为 null / set's value is null
|
||||
ret = config_svc->set("some.key", nullptr);
|
||||
if (ret != 0) {
|
||||
std::cout << "[OK] config->set(key, nullptr) returned error (" << ret << ")\n";
|
||||
@@ -316,6 +330,8 @@ int main()
|
||||
|
||||
// ---- 2. 转义边界测试 ----
|
||||
// 写入含特殊字符的内容,读回后验证内容一致
|
||||
// ---- Escape boundary tests ----
|
||||
// Write content with special chars, verify round-trip integrity
|
||||
std::cout << "\n[Block] Escape boundary tests\n";
|
||||
|
||||
if (file_io) {
|
||||
@@ -325,6 +341,12 @@ int main()
|
||||
// - 实际反斜杠 (0x5C)
|
||||
// - 实际制表符 (0x09)
|
||||
// - 以及字面上的 \n \" \\ \t 转义序列文本
|
||||
// Build content with various special bytes:
|
||||
// - literal newline (0x0A)
|
||||
// - literal double-quote (0x22)
|
||||
// - literal backslash (0x5C)
|
||||
// - literal tab (0x09)
|
||||
// - plus textual \n \" \\ \t escape sequences
|
||||
constexpr const char* escape_content =
|
||||
"line1\nline2\n"
|
||||
"quote=\"yes\"\n"
|
||||
@@ -363,22 +385,25 @@ int main()
|
||||
|
||||
// ---- 3. Tools 调用链测试 ----
|
||||
// 通过 tools->execute("file_read", ...) 验证内置工具可正确调用 file_io
|
||||
// ---- Tools call chain tests ----
|
||||
// Verify built-in tools correctly call file_io via tools->execute("file_read", ...)
|
||||
std::cout << "\n[Block] Tools call chain tests\n";
|
||||
|
||||
if (tools && file_io) {
|
||||
// 准备测试文件
|
||||
// 准备测试文件 / Prepare test file
|
||||
const auto chain_path = dir / "tool_chain_test.txt";
|
||||
constexpr const char* chain_content = "tools-chain-ok\n";
|
||||
file_io->write(chain_path.string().c_str(), chain_content);
|
||||
|
||||
// 用 generic_string() 获取正斜杠路径,避免 JSON 中反斜杠转义问题
|
||||
// Use generic_string() for forward-slash paths to avoid backslash escaping in JSON
|
||||
std::string generic_path = chain_path.generic_string();
|
||||
std::string args_json = "{\"path\":\"" + generic_path + "\"}";
|
||||
|
||||
char* result = tools->execute("file_read", args_json.c_str());
|
||||
if (result) {
|
||||
std::cout << "[OK] tools->execute(\"file_read\", ...) returned result\n";
|
||||
// 验证返回的 JSON 中包含原始文件内容
|
||||
// 验证返回的 JSON 中包含原始文件内容 / Verify returned JSON contains original file content
|
||||
if (std::strstr(result, "tools-chain-ok")) {
|
||||
std::cout << "[OK] tools->execute chain correctly called file_io\n";
|
||||
} else {
|
||||
@@ -391,7 +416,7 @@ int main()
|
||||
<< " (tool may not be registered)\n";
|
||||
}
|
||||
|
||||
// 额外测试:查询 tools 返回的工具列表
|
||||
// 额外测试:查询 tools 返回的工具列表 / Additional test: query tools list
|
||||
char* tools_json = tools->get_tools_json();
|
||||
if (tools_json) {
|
||||
std::cout << "[OK] tools->get_tools_json() returned: " << tools_json << "\n";
|
||||
@@ -406,14 +431,17 @@ int main()
|
||||
// ---- 4. Session 健壮性测试 ----
|
||||
// session->add(nullptr) 后验证 history 不变
|
||||
// session->clear 后验证 token_count 为 0
|
||||
// ---- Session robustness tests ----
|
||||
// Verify history unchanged after session->add(nullptr)
|
||||
// Verify token_count == 0 after session->clear
|
||||
std::cout << "\n[Block] Session robustness tests\n";
|
||||
|
||||
if (session) {
|
||||
// 记录 add(nullptr) 前的 history 计数
|
||||
// 记录 add(nullptr) 前的 history 计数 / Record history count before add(nullptr)
|
||||
int count_before = 0;
|
||||
session->history(&count_before);
|
||||
|
||||
// 传 null 不应改变 history
|
||||
// 传 null 不应改变 history / Passing null should not change history
|
||||
session->add(nullptr);
|
||||
|
||||
int count_after = 0;
|
||||
@@ -427,7 +455,7 @@ int main()
|
||||
<< count_before << " -> " << count_after << "\n";
|
||||
}
|
||||
|
||||
// clear 后 token_count 应为 0
|
||||
// clear 后 token_count 应为 0 / token_count should be 0 after clear
|
||||
session->clear();
|
||||
int tokens = session->token_count();
|
||||
if (tokens == 0) {
|
||||
@@ -443,6 +471,8 @@ int main()
|
||||
// ========================================================================
|
||||
// W13.6 回归保护点 R1-R3 (qa-xu 徐磊)
|
||||
// 覆盖: W11.7 BUG-2/3/4 + W11.1 Discovery 2/3 + W12.2/W12.3 修复
|
||||
// W13.6 regression protections R1-R3 (qa-xu)
|
||||
// Covers: W11.7 BUG-2/3/4 + W11.1 Discovery 2/3 + W12.2/W12.3 fixes
|
||||
// ========================================================================
|
||||
std::cout << "\n--- Regression Tests (R1-R3: W11.7/W12 bug protection) ---\n";
|
||||
|
||||
@@ -450,6 +480,10 @@ int main()
|
||||
// 回归: W11.1 Discovery 3 (g_max_tokens 死变量 — W12.3 已修, W18.1 彻底移除)
|
||||
// W11.7 BUG-3 (/context 静默 — W12.3 已修)
|
||||
// 验证: trim 能正确裁剪消息数,调用链完整不崩溃
|
||||
// ---- R1: context max_tokens takes effect ----
|
||||
// Regression: W11.1 Discovery 3 (g_max_tokens dead var — fixed W12.3, removed W18.1)
|
||||
// W11.7 BUG-3 (/context silent — fixed W12.3)
|
||||
// Verify: trim reduces message count correctly, full call chain without crash
|
||||
{
|
||||
auto* ctx = static_cast<const dstalk_context_service_t*>(
|
||||
dstalk_service_query("context", 1));
|
||||
@@ -457,6 +491,7 @@ int main()
|
||||
std::cout << "[OK] R1: context service found\n";
|
||||
|
||||
// 构造 5 条消息,每条 ~50 字符 / ~15 token,总计 ~75 token > 50 max
|
||||
// Build 5 messages, each ~50 chars / ~15 tokens, total ~75 tokens > 50 max
|
||||
dstalk_message_t msgs[5];
|
||||
msgs[0] = {"user", "Hello this is message one with enough text to count tokens", nullptr, nullptr};
|
||||
msgs[1] = {"assistant", "Message two also has sufficient length for token counting", nullptr, nullptr};
|
||||
@@ -476,6 +511,7 @@ int main()
|
||||
dstalk_free(out);
|
||||
} else if (ret >= 0) {
|
||||
// 首条消息即超 max_tokens 时 trim 可能返回空,这也是合法路径
|
||||
// When first message exceeds max_tokens, trim may return empty; also valid
|
||||
std::cout << "[WARN] R1: trim returned null output (single msg exceeds max?)\n";
|
||||
}
|
||||
} else {
|
||||
@@ -487,15 +523,19 @@ int main()
|
||||
// 回归: W11.2 Discovery 2 (双 ConfigStore 数据孤岛 — W12.2 已修)
|
||||
// W11.2 Discovery 3 (c_str() 悬垂 — W12.2 已修)
|
||||
// 验证: dstalk_config_set 写入后,dstalk_config_get 和 config_service->get 返回一致值
|
||||
// ---- R2: config dual-store consistency ----
|
||||
// Regression: W11.2 Discovery 2 (dual ConfigStore islands — fixed W12.2)
|
||||
// W11.2 Discovery 3 (c_str() dangling — fixed W12.2)
|
||||
// Verify: after dstalk_config_set write, dstalk_config_get and config_service->get return same value
|
||||
{
|
||||
constexpr const char* k = "__regr_w13_6_dual";
|
||||
constexpr const char* v = "dual_ok_42";
|
||||
|
||||
// 通过 host API 写入
|
||||
// 通过 host API 写入 / Write via host API
|
||||
int set_ret = dstalk_config_set(k, v);
|
||||
REGCHECK(set_ret == 0, "R2: dstalk_config_set returned 0");
|
||||
|
||||
// 通过 host API 读回
|
||||
// 通过 host API 读回 / Read back via host API
|
||||
const char* host_val = dstalk_config_get(k);
|
||||
REGCHECK(host_val && std::strcmp(host_val, v) == 0,
|
||||
"R2: dstalk_config_get matches written value");
|
||||
@@ -503,6 +543,9 @@ int main()
|
||||
// 通过 plugin config 服务读回 — 验证双 store 整合后数据可见性一致
|
||||
// 注: W12.2 双 store 整合尚未部署,跨 store 可见性当前为已知 gap;
|
||||
// 本检查用 WARN 记录现状,待 W12.2 fix 落地后改为 REGCHECK
|
||||
// Read back via plugin config service — verify visibility after dual-store merge
|
||||
// Note: W12.2 dual-store merge not yet deployed; cross-store visibility is a known gap;
|
||||
// this check uses WARN to record status, upgrade to REGCHECK after W12.2 lands
|
||||
auto* cfg_svc = static_cast<const dstalk_config_service_t*>(
|
||||
dstalk_service_query("config", 1));
|
||||
if (cfg_svc) {
|
||||
@@ -520,7 +563,7 @@ int main()
|
||||
std::cerr << "[WARN] R2: config service not found, partial skip\n";
|
||||
}
|
||||
|
||||
// 清理测试 key
|
||||
// 清理测试 key / Clean up test key
|
||||
dstalk_config_set(k, "");
|
||||
}
|
||||
|
||||
@@ -529,6 +572,11 @@ int main()
|
||||
// W11.7 BUG-4 (/file write 落空) 同类的错误路径静默问题
|
||||
// 验证: http post_json 到不可达目标返回错误而不崩溃;
|
||||
// 若 http 服务不可用,回退测 ai 服务错误路径
|
||||
// ---- R3: HTTP / AI service error paths do not crash ----
|
||||
// Regression: W12.1 removed TLS/http_client code (removed rewritten network layer)
|
||||
// W11.7 BUG-4 (/file write miss) similar error-path silent issues
|
||||
// Verify: http post_json to unreachable target returns error without crash;
|
||||
// fall back to ai service error path if http unavailable
|
||||
{
|
||||
auto* http = static_cast<const dstalk_http_service_t*>(
|
||||
dstalk_service_query("http", 1));
|
||||
@@ -536,6 +584,8 @@ int main()
|
||||
std::cout << "[OK] R3: http service found\n";
|
||||
// 向 127.0.0.1:1 发请求 — 端口 1 在 Windows 上几乎肯定无服务监听
|
||||
// 连接拒绝应立即返回错误而非崩溃
|
||||
// Send request to 127.0.0.1:1 — port 1 on Windows almost certainly has no listener
|
||||
// Connection refused should return error immediately, not crash
|
||||
char* body = nullptr;
|
||||
int status = 0;
|
||||
int ret = http->post_json("127.0.0.1", "1", "/",
|
||||
@@ -549,6 +599,7 @@ int main()
|
||||
}
|
||||
} else {
|
||||
// 回退:测 AI 服务 (ai.deepseek) 错误路径
|
||||
// Fallback: test AI service (ai.deepseek) error path
|
||||
auto* ai_svc = static_cast<const dstalk_ai_service_t*>(
|
||||
dstalk_service_query("ai.deepseek", 1));
|
||||
if (ai_svc) {
|
||||
@@ -556,6 +607,7 @@ int main()
|
||||
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 而非崩溃
|
||||
// api_key="test-key" is invalid, should return error result, not crash
|
||||
REGCHECK(r.ok == 0 || r.error != nullptr,
|
||||
"R3: ai->chat with invalid key returned error result (no crash)");
|
||||
if (r.content) dstalk_free((void*)r.content);
|
||||
@@ -570,11 +622,14 @@ int main()
|
||||
// ========================================================================
|
||||
// W21.5 Tool Calls 边界测试 (qa-xu 徐磊)
|
||||
// 覆盖: null tool_calls_json / 空数组 "[]" / 有效 tool_calls mock 验证
|
||||
// W21.5 Tool Calls boundary tests (qa-xu)
|
||||
// Covers: null tool_calls_json / empty array "[]" / valid tool_calls mock verification
|
||||
// ========================================================================
|
||||
std::cout << "\n--- Tool Calls Boundary Tests (W21.5) ---\n";
|
||||
|
||||
if (tools && session) {
|
||||
// ---- W21.5-1: null tool_calls_json → 正常处理(不崩溃)----
|
||||
// ---- W21.5-1: null tool_calls_json → handle normally (no crash) ----
|
||||
{
|
||||
int before = 0;
|
||||
session->history(&before);
|
||||
@@ -595,6 +650,7 @@ int main()
|
||||
}
|
||||
|
||||
// ---- W21.5-2: 空 JSON 数组 "[]" → 正常处理(不崩溃)----
|
||||
// ---- W21.5-2: empty JSON array "[]" → handle normally (no crash) ----
|
||||
{
|
||||
int before = 0;
|
||||
session->history(&before);
|
||||
@@ -616,6 +672,7 @@ int main()
|
||||
}
|
||||
|
||||
// ---- W21.5-3: 有效 tool_calls JSON → 验证 execute 被调用 (mock) ----
|
||||
// ---- W21.5-3: valid tool_calls JSON → verify execute is called (mock) ----
|
||||
{
|
||||
g_mock_tool_called = 0;
|
||||
int reg = tools->register_tool(
|
||||
@@ -638,6 +695,7 @@ int main()
|
||||
tools->unregister_tool("__w21_5_mock");
|
||||
|
||||
// 验证已注销的工具返回 error 而非崩溃
|
||||
// Verify unregistered tool returns error, not crash
|
||||
char* err_result = tools->execute("__w21_5_mock", "{}");
|
||||
REGCHECK(err_result && std::strstr(err_result, "error") != nullptr,
|
||||
"W21.5-3d: unregistered tool returns error (not crash)");
|
||||
@@ -645,6 +703,7 @@ int main()
|
||||
}
|
||||
|
||||
// ---- W21.5-4: save/load 往返保留 tool_calls_json ----
|
||||
// ---- W21.5-4: save/load round-trip preserves tool_calls_json ----
|
||||
if (file_io) {
|
||||
const auto rtt_path = dir / "w21_5_tc_rtt.jsonl";
|
||||
int ret = session->save(rtt_path.string().c_str());
|
||||
@@ -664,23 +723,28 @@ int main()
|
||||
std::cerr << "[WARN] W21.5: tools or session service not available\n";
|
||||
}
|
||||
|
||||
// 清理
|
||||
// 清理 / Cleanup
|
||||
dstalk_shutdown();
|
||||
std::cout << "[OK] dstalk_shutdown succeeded\n";
|
||||
|
||||
// ========================================================================
|
||||
// W13.6 回归保护点 R4 (qa-xu 徐磊)
|
||||
// W13.6 regression protection R4 (qa-xu)
|
||||
// ========================================================================
|
||||
|
||||
// ---- R4: 重复 init / shutdown 生命周期 ----
|
||||
// 回归: W9.8 initialize_all 容错 (插件生命周期健壮性)
|
||||
// W11.7 BUG-1 [CRITICAL] build/bin/ 损坏副本 (stale state 残留)
|
||||
// 验证: 多次 dstalk_init/dstalk_shutdown 循环不崩溃,每次 reload 正常
|
||||
// ---- R4: repeat init/shutdown lifecycle ----
|
||||
// Regression: W9.8 initialize_all fault tolerance (plugin lifecycle robustness)
|
||||
// W11.7 BUG-1 [CRITICAL] build/bin/ corrupt copy (stale state residue)
|
||||
// Verify: multiple dstalk_init/dstalk_shutdown cycles without crash, each reload ok
|
||||
{
|
||||
std::cout << "\n[Block] R4: Repeat init/shutdown lifecycle\n";
|
||||
constexpr int cycles = 3;
|
||||
for (int i = 0; i < cycles; i++) {
|
||||
// 每轮重写配置(模拟独立启动)
|
||||
// 每轮重写配置(模拟独立启动)/ Rewrite config each cycle (simulate independent start)
|
||||
{
|
||||
std::ofstream c(config_path);
|
||||
c << "[api]\n"
|
||||
@@ -700,7 +764,7 @@ int main()
|
||||
break;
|
||||
}
|
||||
|
||||
// 快速验证服务可用
|
||||
// 快速验证服务可用 / Quick verify service is available
|
||||
void* q = dstalk_service_query("config", 1);
|
||||
REGCHECK(q != nullptr, "R4: service query ok after init");
|
||||
|
||||
@@ -710,7 +774,7 @@ int main()
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 最终结果 ----
|
||||
// ---- 最终结果 / Final result ----
|
||||
std::cout << "\n";
|
||||
if (g_regression_failures == 0) {
|
||||
std::cout << "=== All smoke tests passed ===\n";
|
||||
|
||||
Reference in New Issue
Block a user