清理废弃模块并完善测试基础设施

This commit is contained in:
2026-07-17 23:49:45 +08:00
parent d0d1de4d00
commit 90262aaf6d
102 changed files with 1711 additions and 8175 deletions
+44
View File
@@ -0,0 +1,44 @@
#include "TestHarness.h"
#include <QCoreApplication>
#include <QStandardPaths>
#include <QTextStream>
#include <exception>
int main(int argc, char *argv[])
{
QStandardPaths::setTestModeEnabled(true);
QCoreApplication application(argc, argv);
QCoreApplication::setOrganizationName(QStringLiteral("QtDesktopPetInternalQA"));
QCoreApplication::setApplicationName(QStringLiteral("CoreLogicTests"));
QTextStream output(stdout);
QTextStream errors(stderr);
int failedCount = 0;
for (const Qa::TestCase &test : Qa::registry())
{
try
{
test.function();
output << "[PASS] " << QString::fromStdString(test.name) << Qt::endl;
}
catch (const std::exception &exception)
{
++failedCount;
errors << "[FAIL] " << QString::fromStdString(test.name)
<< ": " << exception.what() << Qt::endl;
}
catch (...)
{
++failedCount;
errors << "[FAIL] " << QString::fromStdString(test.name)
<< ": unknown exception" << Qt::endl;
}
}
output << "Executed " << Qa::registry().size() << " tests; "
<< failedCount << " failed." << Qt::endl;
return failedCount == 0 ? 0 : 1;
}
@@ -0,0 +1,118 @@
#include "TestHarness.h"
#include "src/ai/ConversationStore.h"
#include <QByteArray>
#include <QDate>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QTemporaryDir>
#include <QTime>
namespace
{
ChatMessage message(const QString &role, const QString &content, int minute)
{
return {
role,
content,
QDateTime(QDate(2026, 7, 17), QTime(10, minute)),
QStringLiteral("openai"),
QStringLiteral("test-model"),
};
}
bool writeBytes(const QString &filePath, const QByteArray &bytes)
{
QFile file(filePath);
return file.open(QIODevice::WriteOnly | QIODevice::Truncate)
&& file.write(bytes) == bytes.size();
}
}
QA_TEST(ConversationStore, SavesLoadsAndLimitsEvenHistory)
{
QTemporaryDir directory;
QA_EXPECT(directory.isValid());
const QString filePath = directory.filePath(QStringLiteral("conversation_history.json"));
const ConversationStore store(filePath);
const QVector<ChatMessage> messages = {
message(QStringLiteral("user"), QStringLiteral("one"), 0),
message(QStringLiteral("assistant"), QStringLiteral("two"), 1),
message(QStringLiteral("user"), QStringLiteral("three"), 2),
message(QStringLiteral("assistant"), QStringLiteral("four"), 3),
};
QA_EXPECT(store.save(messages, 4));
const QVector<ChatMessage> loaded = store.load(3);
QA_EXPECT_EQ(loaded.size(), 2);
QA_EXPECT_EQ(loaded.first().content, QStringLiteral("three"));
QA_EXPECT_EQ(loaded.last().model, QStringLiteral("test-model"));
}
QA_TEST(ConversationStore, AvoidsDuplicatingOverlappingMessages)
{
QTemporaryDir directory;
QA_EXPECT(directory.isValid());
const ConversationStore store(directory.filePath(QStringLiteral("history.json")));
const QVector<ChatMessage> firstPair = {
message(QStringLiteral("user"), QStringLiteral("hello"), 0),
message(QStringLiteral("assistant"), QStringLiteral("world"), 1),
};
QA_EXPECT(store.save(firstPair, 10));
QA_EXPECT(store.save(firstPair, 10));
QA_EXPECT_EQ(store.load(10).size(), 2);
}
QA_TEST(ConversationStore, LoadsLegacyCreatedAtField)
{
QTemporaryDir directory;
QA_EXPECT(directory.isValid());
const QString filePath = directory.filePath(QStringLiteral("legacy.json"));
QJsonArray messages;
messages.append(QJsonObject{
{QStringLiteral("role"), QStringLiteral("user")},
{QStringLiteral("content"), QStringLiteral("legacy-user")},
{QStringLiteral("createdAt"), QStringLiteral("2026-07-17T10:00:00")},
});
messages.append(QJsonObject{
{QStringLiteral("role"), QStringLiteral("assistant")},
{QStringLiteral("content"), QStringLiteral("legacy-assistant")},
{QStringLiteral("createdAt"), QStringLiteral("2026-07-17T10:00:01")},
});
QA_EXPECT(writeBytes(filePath, QJsonDocument(QJsonObject{{QStringLiteral("messages"), messages}}).toJson()));
const QVector<ChatMessage> loaded = ConversationStore(filePath).load(10);
QA_EXPECT_EQ(loaded.size(), 2);
QA_EXPECT(loaded.first().timestamp.isValid());
QA_EXPECT(loaded.first().provider.isEmpty());
}
QA_TEST(ConversationStore, BacksUpBrokenJsonAndClearsHistory)
{
QTemporaryDir directory;
QA_EXPECT(directory.isValid());
const QString filePath = directory.filePath(QStringLiteral("conversation_history.json"));
QA_EXPECT(writeBytes(filePath, QByteArrayLiteral("{broken")));
QString errorMessage;
const ConversationStore store(filePath);
QA_EXPECT(store.load(10, &errorMessage).isEmpty());
QA_EXPECT(!errorMessage.isEmpty());
QA_EXPECT(!QFile::exists(filePath));
QA_EXPECT_EQ(
QDir(directory.path()).entryList({QStringLiteral("conversation_history.broken.*.json")}, QDir::Files).size(),
1);
QA_EXPECT(store.save({
message(QStringLiteral("user"), QStringLiteral("hello"), 0),
message(QStringLiteral("assistant"), QStringLiteral("world"), 1),
}, 10));
QA_EXPECT(store.clear());
QA_EXPECT(!QFile::exists(filePath));
}
@@ -0,0 +1,42 @@
#include "TestHarness.h"
#include "src/assistant/CommandDispatcher.h"
#include "src/assistant/IntentRouter.h"
QA_TEST(IntentRouting, TrimsChatText)
{
const UserIntent intent = IntentRouter().route(QStringLiteral(" 你好 "));
QA_EXPECT_EQ(intent.type, UserIntentType::Chat);
QA_EXPECT_EQ(intent.text, QStringLiteral("你好"));
}
QA_TEST(IntentRouting, ReminderHasPriorityOverWeather)
{
const UserIntent intent = IntentRouter().route(QStringLiteral("明天提醒我看天气"));
QA_EXPECT_EQ(intent.type, UserIntentType::Reminder);
}
QA_TEST(IntentRouting, RecognizesWeatherExpressions)
{
const IntentRouter router;
QA_EXPECT_EQ(router.route(QStringLiteral("西安天气怎么样")).type, UserIntentType::Weather);
QA_EXPECT_EQ(router.route(QStringLiteral("明天会不会下雨")).type, UserIntentType::Weather);
QA_EXPECT_EQ(router.route(QStringLiteral("外面冷不冷")).type, UserIntentType::Weather);
}
QA_TEST(IntentRouting, RemovedLocalCapabilitiesRemainChat)
{
const IntentRouter router;
QA_EXPECT_EQ(router.route(QStringLiteral("读取文件")).type, UserIntentType::Chat);
QA_EXPECT_EQ(router.route(QStringLiteral("打开 Codex")).type, UserIntentType::Chat);
QA_EXPECT_EQ(router.route(QStringLiteral("分析这个项目")).type, UserIntentType::Chat);
QA_EXPECT_EQ(router.route(QStringLiteral("创建 test.txt")).type, UserIntentType::Chat);
}
QA_TEST(IntentRouting, DispatcherMapsSupportedActions)
{
const CommandDispatcher dispatcher;
QA_EXPECT_EQ(dispatcher.dispatch(QStringLiteral("提醒我休息")).action, CommandDispatchAction::Reminder);
QA_EXPECT_EQ(dispatcher.dispatch(QStringLiteral("北京天气")).action, CommandDispatchAction::Weather);
QA_EXPECT_EQ(dispatcher.dispatch(QStringLiteral("讲个笑话")).action, CommandDispatchAction::Chat);
}
@@ -0,0 +1,105 @@
#include "TestHarness.h"
#include "src/reminder/ReminderParser.h"
#include "src/reminder/ReminderTypes.h"
#include <QDate>
#include <QDateTime>
#include <QStringList>
#include <QTime>
namespace
{
QDateTime fixedNow()
{
return QDateTime(QDate(2026, 7, 17), QTime(14, 0));
}
}
QA_TEST(ReminderParser, ParsesRelativeMinutes)
{
const ReminderCommand command = ReminderParser().parse(QStringLiteral("10分钟后提醒我喝水"), fixedNow());
QA_EXPECT_EQ(command.type, ReminderCommandType::Create);
QA_EXPECT_EQ(command.remindAt, fixedNow().addSecs(10 * 60));
QA_EXPECT_EQ(command.title, QStringLiteral("喝水"));
}
QA_TEST(ReminderParser, ParsesHalfHours)
{
const ReminderParser parser;
QA_EXPECT_EQ(
parser.parse(QStringLiteral("半小时后提醒我休息"), fixedNow()).remindAt,
fixedNow().addSecs(30 * 60));
QA_EXPECT_EQ(
parser.parse(QStringLiteral("一个半小时后提醒我喝水"), fixedNow()).remindAt,
fixedNow().addSecs(90 * 60));
}
QA_TEST(ReminderParser, ParsesTomorrowClockTime)
{
const ReminderCommand command = ReminderParser().parse(QStringLiteral("明天9点提醒我开会"), fixedNow());
QA_EXPECT_EQ(command.type, ReminderCommandType::Create);
QA_EXPECT_EQ(command.remindAt, QDateTime(QDate(2026, 7, 18), QTime(9, 0)));
}
QA_TEST(ReminderParser, ParsesSupportedRecurrence)
{
const ReminderParser parser;
const ReminderCommand daily = parser.parse(QStringLiteral("每天9点提醒我打卡"), fixedNow());
const ReminderCommand weekly = parser.parse(QStringLiteral("每周一上午10点提醒我周会"), fixedNow());
const ReminderCommand monthly = parser.parse(QStringLiteral("每月31号9点提醒我交报告"), fixedNow());
QA_EXPECT_EQ(daily.recurrence.type, ReminderRecurrenceType::Daily);
QA_EXPECT_EQ(daily.remindAt, QDateTime(QDate(2026, 7, 18), QTime(9, 0)));
QA_EXPECT_EQ(weekly.recurrence.type, ReminderRecurrenceType::Weekly);
QA_EXPECT_EQ(weekly.recurrence.weekday, 1);
QA_EXPECT_EQ(monthly.recurrence.type, ReminderRecurrenceType::Monthly);
QA_EXPECT_EQ(monthly.recurrence.monthDay, 31);
QA_EXPECT_EQ(monthly.remindAt, QDateTime(QDate(2026, 7, 31), QTime(9, 0)));
}
QA_TEST(ReminderParser, RejectsUnsupportedRecurrence)
{
const ReminderParser parser;
const QStringList requests = {
QStringLiteral("工作日9点提醒我打卡"),
QStringLiteral("每两天提醒我喝水"),
QStringLiteral("农历初一提醒我"),
QStringLiteral("每月最后一天9点提醒我结算"),
};
for (const QString &request : requests)
{
const ReminderCommand command = parser.parse(request, fixedNow());
QA_EXPECT_EQ(command.type, ReminderCommandType::Invalid);
QA_EXPECT(!command.errorMessage.isEmpty());
}
}
QA_TEST(ReminderParser, ParsesListAndCancelCommands)
{
const ReminderParser parser;
QA_EXPECT_EQ(parser.parse(QStringLiteral("提醒列表"), fixedNow()).type, ReminderCommandType::List);
const ReminderCommand cancel = parser.parse(QStringLiteral("取消喝水提醒"), fixedNow());
QA_EXPECT_EQ(cancel.type, ReminderCommandType::Cancel);
QA_EXPECT_EQ(cancel.cancelQuery, QStringLiteral("喝水"));
}
QA_TEST(ReminderTypes, ConvertsAndSortsValues)
{
QA_EXPECT_EQ(reminderStatusFromString(QStringLiteral("TRIGGERED")), ReminderStatus::Triggered);
QA_EXPECT_EQ(reminderRecurrenceTypeFromString(QStringLiteral("weekly")), ReminderRecurrenceType::Weekly);
QA_EXPECT_EQ(reminderRecurrenceTypeToString(ReminderRecurrenceType::Monthly), QStringLiteral("monthly"));
ReminderItem later;
later.id = QStringLiteral("later");
later.remindAt = fixedNow().addSecs(60);
ReminderItem earlier;
earlier.id = QStringLiteral("earlier");
earlier.remindAt = fixedNow();
const QVector<ReminderItem> sorted = sortedReminders({later, earlier});
QA_EXPECT_EQ(sorted.first().id, QStringLiteral("earlier"));
QA_EXPECT_EQ(sorted.last().id, QStringLiteral("later"));
}
@@ -0,0 +1,85 @@
#include "TestHarness.h"
#include "src/weather/WeatherParser.h"
#include "src/weather/WeatherSummaryFormatter.h"
QA_TEST(WeatherParser, ExtractsExplicitCitiesAndDates)
{
const WeatherParser parser;
const WeatherQuery current = parser.parse(QStringLiteral("西安天气怎么样"));
const WeatherQuery tomorrow = parser.parse(QStringLiteral("明天西安天气怎么样"));
const WeatherQuery afterTomorrow = parser.parse(QStringLiteral("后天纽约天气"));
QA_EXPECT_EQ(current.cityName, QStringLiteral("西安"));
QA_EXPECT_EQ(current.kind, WeatherQueryKind::Current);
QA_EXPECT_EQ(tomorrow.cityName, QStringLiteral("西安"));
QA_EXPECT_EQ(tomorrow.dateOffset, 1);
QA_EXPECT_EQ(afterTomorrow.cityName, QStringLiteral("纽约"));
QA_EXPECT_EQ(afterTomorrow.dateOffset, 2);
}
QA_TEST(WeatherParser, ParsesRangeAndDefaultCityRequest)
{
const WeatherParser parser;
const WeatherQuery range = parser.parse(QStringLiteral("未来三天北京天气"));
const WeatherQuery defaultCity = parser.parse(QStringLiteral("今天天气怎么样"));
QA_EXPECT_EQ(range.kind, WeatherQueryKind::Range);
QA_EXPECT_EQ(range.forecastDays, 3);
QA_EXPECT_EQ(range.cityName, QStringLiteral("北京"));
QA_EXPECT(defaultCity.cityName.isEmpty());
}
QA_TEST(WeatherParser, RejectsUnsupportedQueries)
{
const WeatherParser parser;
QA_EXPECT(!parser.parse(QStringLiteral("西安空气质量")).unsupportedReason.isEmpty());
QA_EXPECT(!parser.parse(QStringLiteral("北京天气预警")).unsupportedReason.isEmpty());
QA_EXPECT(!parser.parse(QStringLiteral("明天穿什么")).unsupportedReason.isEmpty());
}
QA_TEST(WeatherFormatter, MapsWeatherCodesAndWindDirections)
{
QA_EXPECT_EQ(WeatherSummaryFormatter::weatherCodeText(0), QStringLiteral("晴"));
QA_EXPECT_EQ(WeatherSummaryFormatter::weatherCodeText(95), QStringLiteral("雷暴"));
QA_EXPECT_EQ(WeatherSummaryFormatter::weatherCodeText(-1), QStringLiteral("未知天气"));
QA_EXPECT_EQ(WeatherSummaryFormatter::windDirectionText(0), QStringLiteral("北风"));
QA_EXPECT_EQ(WeatherSummaryFormatter::windDirectionText(90), QStringLiteral("东风"));
QA_EXPECT_EQ(WeatherSummaryFormatter::windDirectionText(225), QStringLiteral("西南风"));
}
QA_TEST(WeatherFormatter, FormatsCurrentWeatherWithSource)
{
WeatherReport report;
report.query.kind = WeatherQueryKind::Current;
report.location.cityName = QStringLiteral("西安");
report.location.countryName = QStringLiteral("中国");
report.location.source = WeatherLocationSource::SettingsDefault;
report.current.valid = true;
report.current.weatherCode = 0;
report.current.hasTemperature = true;
report.current.temperatureC = 28.5;
const QString message = WeatherSummaryFormatter().format(report);
QA_EXPECT(message.contains(QStringLiteral("使用设置页默认城市")));
QA_EXPECT(message.contains(QStringLiteral("西安,中国当前天气:晴")));
QA_EXPECT(message.contains(QStringLiteral("28.5℃")));
}
QA_TEST(WeatherFormatter, FormatsAmbiguousLocationAndMissingData)
{
WeatherReport report;
report.query.kind = WeatherQueryKind::Current;
report.location.cityName = QStringLiteral("Springfield");
report.location.countryName = QStringLiteral("United States");
report.hasLocationAmbiguity = true;
report.locationCandidates = {
{QStringLiteral("Springfield"), QStringLiteral("Illinois"), QStringLiteral("United States"), {}, 0.0, 0.0},
{QStringLiteral("Springfield"), QStringLiteral("Missouri"), QStringLiteral("United States"), {}, 0.0, 0.0},
};
const QString message = WeatherSummaryFormatter().format(report);
QA_EXPECT(message.contains(QStringLiteral("可能存在同名城市")));
QA_EXPECT(message.contains(QStringLiteral("Missouri")));
QA_EXPECT(message.contains(QStringLiteral("缺少当前天气")));
}
@@ -0,0 +1,65 @@
#include "TestHarness.h"
#include "src/config/AIConfig.h"
#include "src/web/WebCapabilityDetector.h"
namespace
{
AIConfig completeConfig(const QString &provider)
{
AIConfig config = defaultAIConfigForProvider(provider);
config.model = QStringLiteral("test-model");
config.apiKey = QStringLiteral("test-key-not-a-real-secret");
return config;
}
}
QA_TEST(WebCapability, RespectsGlobalDisable)
{
WebConfig webConfig;
webConfig.enabled = false;
const WebCapability capability = WebCapabilityDetector::detect(completeConfig(QStringLiteral("openai")), webConfig);
QA_EXPECT(!capability.supported);
QA_EXPECT(capability.statusText.contains(QStringLiteral("已关闭")));
}
QA_TEST(WebCapability, AcceptsOfficialOpenAI)
{
const WebCapability capability = WebCapabilityDetector::detect(completeConfig(QStringLiteral("openai")));
QA_EXPECT(capability.supported);
QA_EXPECT_EQ(capability.kind, WebProviderKind::OpenAIResponses);
}
QA_TEST(WebCapability, RejectsThirdPartyOpenAIBaseUrl)
{
AIConfig config = completeConfig(QStringLiteral("openai"));
config.baseUrl = QStringLiteral("https://relay.example.com/v1");
const WebCapability capability = WebCapabilityDetector::detect(config);
QA_EXPECT(!capability.supported);
QA_EXPECT(capability.userMessage.contains(QStringLiteral("api.openai.com")));
}
QA_TEST(WebCapability, AcceptsGeminiProtocol)
{
const WebCapability capability = WebCapabilityDetector::detect(completeConfig(QStringLiteral("google")));
QA_EXPECT(capability.supported);
QA_EXPECT_EQ(capability.kind, WebProviderKind::GeminiGrounding);
}
QA_TEST(WebCapability, RejectsDeepSeekAndCustomProviders)
{
const WebCapability deepSeek = WebCapabilityDetector::detect(completeConfig(QStringLiteral("deepseek")));
const WebCapability custom = WebCapabilityDetector::detect(completeConfig(QStringLiteral("custom")));
QA_EXPECT(!deepSeek.supported);
QA_EXPECT(deepSeek.statusText.contains(QStringLiteral("DeepSeek")));
QA_EXPECT(!custom.supported);
QA_EXPECT(custom.statusText.contains(QStringLiteral("无法确认")));
}
QA_TEST(WebCapability, RequiresCompleteConfiguration)
{
AIConfig config = defaultAIConfigForProvider(QStringLiteral("openai"));
const WebCapability capability = WebCapabilityDetector::detect(config);
QA_EXPECT(!capability.supported);
QA_EXPECT(capability.statusText.contains(QStringLiteral("未配置完整")));
}