Files
dstalk/tests/smoke_test.cpp
XiuChengWu e6f24f00f1 Refactor to plugin architecture with B3 CLI UX, C2 smoke tests, C3 CI scripts
Architecture overhaul (Wave 1-4 collaborative work):
- Migrated dstalk-core from monolithic api.cpp to plugin-based design with
  host/service_registry/event_bus/plugin_loader and topological initialization.
- Split public headers into dstalk_host.h / dstalk_services.h /
  dstalk_lsp.h / dstalk_types.h; deleted obsolete dstalk_api.h and inlined
  TLS/file/net code now provided by plugins.
- Added 9 plugins: deepseek, anthropic, network, session, context, tools,
  config, file-io, lsp; AI plugins register as "ai.<provider>" services.

B3 CLI interaction enhancement:
- Prompt now shows current model name (A1).
- /status command prints model/base_url/api_key (sanitized: shown only
  as set/unset)/services readiness (A2).
- SIGINT/Ctrl+C handled on POSIX (signal) and Windows (SetConsoleCtrlHandler);
  /quit no longer std::exit(0) but sets a quit flag so dstalk_shutdown runs
  exactly once via natural control flow (B1+B2).
- Cross-DLL free fixed: print_file uses dstalk_free instead of std::free (B4).
- --batch mode plus isatty auto-detection for piped stdin (C1).
- fgets truncation detection with friendly error and stdin draining (C3).
- Distinct exit codes (init/AI/service-unavailable) (C4).
- /model rejects empty model name (C5).

C2 smoke test extension:
- 4 new test blocks: null-safety (file_io/session/tools/config),
  escape-boundary round-trip, tools->execute call chain, session robustness
  (add(nullptr), clear -> token_count == 0).

C3 CI build scripts:
- scripts/ci-build.sh and scripts/ci-build.bat invoke cmake configure +
  parallel build + ctest, suitable for GitHub Actions.

Build verified: dstalk-cli compiles, smoke test passes via ctest.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-27 05:12:56 +08:00

431 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================================
// smoke_test.cpp — 插件化架构烟雾测试
// ============================================================================
// 测试: 核心初始化、插件加载、服务查询、file_io、session 功能
// ============================================================================
#include "dstalk/dstalk_host.h"
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
int main()
{
const auto dir = std::filesystem::temp_directory_path() / "dstalk-smoke-test";
std::filesystem::create_directories(dir);
// 写一个配置文件用于初始化
const auto config_path = dir / "config.toml";
{
std::ofstream config(config_path);
config << "[api]\n"
<< "provider = \"deepseek\"\n"
<< "base_url = \"https://api.deepseek.com/v1\"\n"
<< "api_key = \"test-key\"\n"
<< "model = \"deepseek-v4-pro\"\n";
}
// 初始化主机(会自动扫描 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";
// 验证插件列表
{
char* list_json = nullptr;
int ret = dstalk_plugin_list(&list_json);
if (ret == 0 && list_json) {
std::cout << "[OK] plugins loaded: " << list_json << "\n";
dstalk_free(list_json);
} else {
std::cerr << "[WARN] dstalk_plugin_list returned: " << ret << "\n";
}
}
// 测试服务查询: 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";
// 测试写入
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) {
std::cout << "[OK] file_io->write succeeded\n";
} else {
std::cerr << "[FAIL] file_io->write failed\n";
dstalk_shutdown();
return 1;
}
// 测试读取
char* content = nullptr;
if (file_io->read(file_path.string().c_str(), &content) == 0 && content) {
bool ok = std::strcmp(content, sample_content) == 0;
std::free(content);
if (ok) {
std::cout << "[OK] file_io->read content matches\n";
} else {
std::cerr << "[FAIL] file_io->read content mismatch\n";
dstalk_shutdown();
return 1;
}
} else {
std::cerr << "[FAIL] file_io->read failed\n";
dstalk_shutdown();
return 1;
}
} else {
std::cerr << "[WARN] file_io service not found (plugin may not be in plugins/ dir)\n";
}
// 测试服务查询: 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
const auto session_path = dir / "session.jsonl";
const auto saved_path = dir / "session-saved.jsonl";
constexpr const char* session_content =
"{\"role\":\"user\",\"content\":\"line\\n\\\"quote\\\"\\\\slash\"}\n"
"{\"role\":\"assistant\",\"content\":\"ok\\tready\"}\n";
if (file_io) {
file_io->write(session_path.string().c_str(), session_content);
}
if (session->load(session_path.string().c_str()) == 0) {
std::cout << "[OK] session->load succeeded\n";
} else {
std::cerr << "[FAIL] session->load failed\n";
dstalk_shutdown();
return 1;
}
if (session->save(saved_path.string().c_str()) == 0) {
std::cout << "[OK] session->save succeeded\n";
} else {
std::cerr << "[FAIL] session->save failed\n";
dstalk_shutdown();
return 1;
}
// 验证保存的内容
if (file_io) {
char* saved = nullptr;
if (file_io->read(saved_path.string().c_str(), &saved) == 0 && saved) {
bool session_ok = std::strcmp(saved, session_content) == 0;
std::free(saved);
if (session_ok) {
std::cout << "[OK] session content matches after save/load\n";
} else {
std::cerr << "[FAIL] session content mismatch after save/load\n";
dstalk_shutdown();
return 1;
}
}
}
// 测试 token 计数
int tokens = session->token_count();
std::cout << "[OK] session->token_count: " << tokens << "\n";
// 测试 history
int count = 0;
session->history(&count);
std::cout << "[OK] session->history count: " << count << "\n";
// 测试 clear
session->clear();
session->history(&count);
if (count == 0) {
std::cout << "[OK] session->clear succeeded\n";
}
} else {
std::cerr << "[WARN] session service not found\n";
}
// 测试服务查询: ai可能因为没有真实 API key 而失败,但服务应存在)
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*>(
dstalk_service_query(ai_provider, 1));
if (ai) {
std::cout << "[OK] ai service found\n";
} else {
std::cerr << "[WARN] ai service not found\n";
}
// 测试服务查询: config
auto* config_svc = static_cast<const dstalk_config_service_t*>(
dstalk_service_query("config", 1));
if (config_svc) {
std::cout << "[OK] config service found\n";
const char* val = config_svc->get("api.model");
if (val) {
std::cout << "[OK] config->get(\"api.model\"): " << val << "\n";
}
} else {
std::cerr << "[WARN] config service not found\n";
}
// 测试 dstalk_config_get主机级配置 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(DSTALK_LOG_INFO, "Smoke test completed successfully");
// ========================================================================
// 扩展测试块 C2: null-safety / 转义边界 / tools 调用链 / session 健壮性
// ========================================================================
std::cout << "\n--- Extended Smoke Tests (C2) ---\n";
// 提前查询 tools 服务,供后续测试块使用
auto* tools = static_cast<const dstalk_tools_service_t*>(
dstalk_service_query("tools", 1));
// ---- 1. Null-safety 测试 ----
// 对所有服务 API 传 null 参数,验证不崩溃且返回错误
std::cout << "\n[Block] Null-safety tests\n";
if (file_io) {
char* dummy = nullptr;
int ret = file_io->read(nullptr, &dummy);
if (ret != 0) {
std::cout << "[OK] file_io->read(nullptr, ...) returned error (" << ret << ")\n";
} else {
std::cerr << "[FAIL] file_io->read(nullptr, ...) should return error\n";
}
ret = file_io->write(nullptr, "test_content");
if (ret != 0) {
std::cout << "[OK] file_io->write(nullptr, ...) returned error (" << ret << ")\n";
} else {
std::cerr << "[FAIL] file_io->write(nullptr, ...) should return error\n";
}
// read 的 content 参数也为 null
ret = file_io->read("dummy_path", nullptr);
if (ret != 0) {
std::cout << "[OK] file_io->read(path, nullptr) returned error (" << ret << ")\n";
} else {
std::cerr << "[FAIL] file_io->read(path, nullptr) should return error\n";
}
// write 的 content 参数为 null
ret = file_io->write("dummy_path", nullptr);
if (ret != 0) {
std::cout << "[OK] file_io->write(path, nullptr) returned error (" << ret << ")\n";
} else {
std::cerr << "[FAIL] file_io->write(path, nullptr) should return error\n";
}
} else {
std::cerr << "[WARN] file_io service not available for null-safety tests\n";
}
if (session) {
session->add(nullptr);
std::cout << "[OK] session->add(nullptr) did not crash\n";
int ret = session->save(nullptr);
if (ret != 0) {
std::cout << "[OK] session->save(nullptr) returned error (" << ret << ")\n";
} else {
std::cerr << "[FAIL] session->save(nullptr) should return error\n";
}
ret = session->load(nullptr);
if (ret != 0) {
std::cout << "[OK] session->load(nullptr) returned error (" << ret << ")\n";
} else {
std::cerr << "[FAIL] session->load(nullptr) should return error\n";
}
} else {
std::cerr << "[WARN] session service not available for null-safety tests\n";
}
if (tools) {
char* result = tools->execute(nullptr, nullptr);
if (result) {
// 实现返回了错误字符串(如 {"error":"tool name is null"}),未崩溃
std::cout << "[OK] tools->execute(nullptr, nullptr) did not crash"
<< " (returned: " << result << ")\n";
dstalk_free(result);
} else {
std::cout << "[OK] tools->execute(nullptr, nullptr) returned null without crash\n";
}
} else {
std::cerr << "[WARN] tools service not available for null-safety tests\n";
}
if (config_svc) {
const char* val = config_svc->get(nullptr);
if (val == nullptr) {
std::cout << "[OK] config->get(nullptr) returned nullptr\n";
} else {
std::cerr << "[FAIL] config->get(nullptr) should return nullptr\n";
}
int ret = config_svc->set(nullptr, nullptr);
if (ret != 0) {
std::cout << "[OK] config->set(nullptr, nullptr) returned error (" << ret << ")\n";
} else {
std::cerr << "[FAIL] config->set(nullptr, nullptr) should return error\n";
}
// set 的 value 为 null
ret = config_svc->set("some.key", nullptr);
if (ret != 0) {
std::cout << "[OK] config->set(key, nullptr) returned error (" << ret << ")\n";
} else {
std::cerr << "[FAIL] config->set(key, nullptr) should return error\n";
}
} else {
std::cerr << "[WARN] config service not available for null-safety tests\n";
}
// ---- 2. 转义边界测试 ----
// 写入含特殊字符的内容,读回后验证内容一致
std::cout << "\n[Block] Escape boundary tests\n";
if (file_io) {
// 构造包含各种特殊字节的内容:
// - 实际换行符 (0x0A)
// - 实际双引号 (0x22)
// - 实际反斜杠 (0x5C)
// - 实际制表符 (0x09)
// - 以及字面上的 \n \" \\ \t 转义序列文本
constexpr const char* escape_content =
"line1\nline2\n"
"quote=\"yes\"\n"
"backslash=\\path\n"
"tab=\there\n"
"literal-escapes: newline=\\n quote=\\\" backslash=\\\\ tab=\\t\n"
"endswithbackslash\\\\\n"
"mixed\\t\\\"quoted\\\"\\\\path\n";
const auto escape_path = dir / "escape_test.txt";
if (file_io->write(escape_path.string().c_str(), escape_content) == 0) {
std::cout << "[OK] escape content write succeeded\n";
char* read_back = nullptr;
if (file_io->read(escape_path.string().c_str(), &read_back) == 0 && read_back) {
bool match = (std::strcmp(read_back, escape_content) == 0);
if (match) {
std::cout << "[OK] escape content round-trip matches"
<< " (length=" << std::strlen(escape_content) << ")\n";
} else {
std::cerr << "[FAIL] escape content round-trip mismatch\n"
<< " expected length: " << std::strlen(escape_content) << "\n"
<< " got length: " << std::strlen(read_back) << "\n";
}
std::free(read_back);
} else {
std::cerr << "[FAIL] escape content read-back failed\n";
}
} else {
std::cerr << "[FAIL] escape content write failed\n";
}
} else {
std::cerr << "[WARN] file_io service not available for escape tests\n";
}
// ---- 3. Tools 调用链测试 ----
// 通过 tools->execute("file_read", ...) 验证内置工具可正确调用 file_io
std::cout << "\n[Block] Tools call chain tests\n";
if (tools && file_io) {
// 准备测试文件
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 中反斜杠转义问题
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 中包含原始文件内容
if (std::strstr(result, "tools-chain-ok")) {
std::cout << "[OK] tools->execute chain correctly called file_io\n";
} else {
std::cout << "[WARN] tools->execute result does not contain expected content: "
<< result << "\n";
}
dstalk_free(result);
} else {
std::cout << "[WARN] tools->execute(\"file_read\", ...) returned null"
<< " (tool may not be registered)\n";
}
// 额外测试:查询 tools 返回的工具列表
char* tools_json = tools->get_tools_json();
if (tools_json) {
std::cout << "[OK] tools->get_tools_json() returned: " << tools_json << "\n";
dstalk_free(tools_json);
} else {
std::cout << "[WARN] tools->get_tools_json() returned null\n";
}
} else {
std::cerr << "[WARN] tools or file_io service not available for chain tests\n";
}
// ---- 4. Session 健壮性测试 ----
// session->add(nullptr) 后验证 history 不变
// session->clear 后验证 token_count 为 0
std::cout << "\n[Block] Session robustness tests\n";
if (session) {
// 记录 add(nullptr) 前的 history 计数
int count_before = 0;
session->history(&count_before);
// 传 null 不应改变 history
session->add(nullptr);
int count_after = 0;
session->history(&count_after);
if (count_before == count_after) {
std::cout << "[OK] session->add(nullptr) did not change history count"
<< " (before=" << count_before << ", after=" << count_after << ")\n";
} else {
std::cerr << "[FAIL] session->add(nullptr) changed history count: "
<< count_before << " -> " << count_after << "\n";
}
// clear 后 token_count 应为 0
session->clear();
int tokens = session->token_count();
if (tokens == 0) {
std::cout << "[OK] session->token_count() == 0 after clear\n";
} else {
std::cerr << "[FAIL] session->token_count() == " << tokens
<< " after clear, expected 0\n";
}
} else {
std::cerr << "[WARN] session service not available for robustness tests\n";
}
// 清理
dstalk_shutdown();
std::cout << "[OK] dstalk_shutdown succeeded\n";
std::cout << "\n=== All smoke tests passed ===\n";
return 0;
}