完成 0.2.0 预发布收口与中文安装器

This commit is contained in:
2026-09-09 01:20:45 +08:00
parent 90262aaf6d
commit 1f5e26c361
30 changed files with 1826 additions and 121 deletions
+20 -2
View File
@@ -15,14 +15,32 @@ int main(int argc, char *argv[])
QTextStream output(stdout);
QTextStream errors(stderr);
QString filter;
for (int index = 1; index < argc; ++index)
{
const QString argument = QString::fromLocal8Bit(argv[index]);
if (argument.startsWith(QStringLiteral("--filter=")))
{
filter = argument.mid(QStringLiteral("--filter=").size()).trimmed();
}
}
int failedCount = 0;
int executedCount = 0;
for (const Qa::TestCase &test : Qa::registry())
{
const QString testName = QString::fromStdString(test.name);
if (!filter.isEmpty() && !testName.contains(filter, Qt::CaseInsensitive))
{
continue;
}
++executedCount;
try
{
test.function();
output << "[PASS] " << QString::fromStdString(test.name) << Qt::endl;
output << "[PASS] " << testName << Qt::endl;
}
catch (const std::exception &exception)
{
@@ -38,7 +56,7 @@ int main(int argc, char *argv[])
}
}
output << "Executed " << Qa::registry().size() << " tests; "
output << "Executed " << executedCount << " tests; "
<< failedCount << " failed." << Qt::endl;
return failedCount == 0 ? 0 : 1;
}
@@ -0,0 +1,53 @@
#include "TestHarness.h"
#include "src/ai/AIDiagnostics.h"
#include <QByteArray>
#include <QUrl>
QA_TEST(AIDiagnostics, RedactsCredentialsFromContextAndFreeText)
{
AIConfig config;
config.provider = QStringLiteral("custom");
config.protocol = QStringLiteral("openai-compatible");
config.model = QStringLiteral("test-model");
config.apiKey = QStringLiteral("sk-super-secret-value");
const QString context = AIDiagnostics::diagnosticContext(
config,
QUrl(QStringLiteral("https://example.test/v1/chat?api_key=query-secret&trace=visible")));
const QString freeText = AIDiagnostics::safeTextSummary(
QStringLiteral("Authorization: Bearer bearer-secret token=token-secret sk-another-secret-value"));
QA_EXPECT(!context.contains(QStringLiteral("sk-super-secret-value")));
QA_EXPECT(!context.contains(QStringLiteral("query-secret")));
QA_EXPECT(context.contains(QStringLiteral("trace=visible")));
QA_EXPECT(!freeText.contains(QStringLiteral("bearer-secret")));
QA_EXPECT(!freeText.contains(QStringLiteral("token-secret")));
QA_EXPECT(!freeText.contains(QStringLiteral("sk-another-secret-value")));
}
QA_TEST(AIDiagnostics, RedactsConversationAndCredentialFieldsFromJson)
{
const QByteArray body = QByteArrayLiteral(
R"({"safe":"visible","apiKey":"api-secret","authorization":"Bearer header-secret","messages":[{"content":"private prompt"}],"nested":{"token":"token-secret"}})");
const QString summary = AIDiagnostics::responseBodySummary(body);
QA_EXPECT(summary.contains(QStringLiteral("visible")));
QA_EXPECT(!summary.contains(QStringLiteral("api-secret")));
QA_EXPECT(!summary.contains(QStringLiteral("header-secret")));
QA_EXPECT(!summary.contains(QStringLiteral("private prompt")));
QA_EXPECT(!summary.contains(QStringLiteral("token-secret")));
}
QA_TEST(AIDiagnostics, KeepsUsefulErrorMetadataWithoutCredentialLeakage)
{
const QByteArray body = QByteArrayLiteral(
R"({"error":{"message":"API key sk-super-secret-value is invalid","code":"invalid_api_key","type":"authentication_error"},"content":"private response"})");
const QString error = AIDiagnostics::errorMessageFromBody(body);
QA_EXPECT(error.contains(QStringLiteral("invalid")));
QA_EXPECT(error.contains(QStringLiteral("invalid_api_key")));
QA_EXPECT(!error.contains(QStringLiteral("sk-super-secret-value")));
QA_EXPECT(!error.contains(QStringLiteral("private response")));
}
@@ -0,0 +1,153 @@
#include "TestHarness.h"
#include "src/config/ConfigManager.h"
#include "src/reminder/ReminderStore.h"
#include "src/weather/WeatherStore.h"
#include "src/web/WebStore.h"
#include <QByteArray>
#include <QDir>
#include <QFile>
#include <QFileInfo>
namespace
{
bool writeBrokenJson(const QString &filePath)
{
QDir().mkpath(QFileInfo(filePath).absolutePath());
QFile file(filePath);
return file.open(QIODevice::WriteOnly | QIODevice::Truncate)
&& file.write(QByteArrayLiteral("{broken")) == 7;
}
QString qaConfigDirectory()
{
return QFileInfo(ConfigManager().appConfigPath()).absolutePath();
}
bool isQaConfigDirectory(const QString &path)
{
return path.contains(QStringLiteral("QtDesktopPetInternalQA"), Qt::CaseInsensitive)
|| path.contains(QStringLiteral("qttest"), Qt::CaseInsensitive);
}
void clearQaConfigFiles()
{
const QString path = qaConfigDirectory();
if (!isQaConfigDirectory(path))
{
return;
}
QDir directory(path);
const QStringList filters = {
QStringLiteral("app_config.json"),
QStringLiteral("app_config.broken.*.json"),
QStringLiteral("ai_config.json"),
QStringLiteral("ai_config.broken.*.json"),
QStringLiteral("weather_config.json"),
QStringLiteral("weather_config.broken.*.json"),
QStringLiteral("web_config.json"),
QStringLiteral("web_config.broken.*.json"),
QStringLiteral("reminders.json"),
QStringLiteral("reminders.broken.*.json"),
};
for (const QString &fileName : directory.entryList(filters, QDir::Files))
{
directory.remove(fileName);
}
}
int backupCount(const QString &pattern)
{
return QDir(qaConfigDirectory()).entryList({pattern}, QDir::Files).size();
}
class QaConfigFiles
{
public:
QaConfigFiles()
{
QA_EXPECT(isQaConfigDirectory(qaConfigDirectory()));
clearQaConfigFiles();
}
~QaConfigFiles()
{
clearQaConfigFiles();
}
};
}
QA_TEST(ConfigRecovery, AppConfigFallsBackAndBacksUpBrokenJson)
{
QaConfigFiles files;
ConfigManager manager;
QA_EXPECT(writeBrokenJson(manager.appConfigPath()));
const AppConfig config = manager.loadAppConfig();
QA_EXPECT_EQ(config.windowPosition, QPoint(100, 100));
QA_EXPECT(config.alwaysOnTop);
QA_EXPECT(!QFile::exists(manager.appConfigPath()));
QA_EXPECT_EQ(backupCount(QStringLiteral("app_config.broken.*.json")), 1);
}
QA_TEST(ConfigRecovery, AiConfigFallsBackAndBacksUpBrokenJson)
{
QaConfigFiles files;
ConfigManager manager;
QA_EXPECT(writeBrokenJson(manager.aiConfigPath()));
const AIConfigStore store = manager.loadAIConfigStore();
QA_EXPECT_EQ(store.activeProvider, QStringLiteral("custom"));
QA_EXPECT(store.providers.isEmpty());
QA_EXPECT(!QFile::exists(manager.aiConfigPath()));
QA_EXPECT_EQ(backupCount(QStringLiteral("ai_config.broken.*.json")), 1);
}
QA_TEST(ConfigRecovery, WeatherAndWebConfigsFallBackAndBackUpBrokenJson)
{
QaConfigFiles files;
WeatherStore weatherStore;
WebStore webStore;
QA_EXPECT(writeBrokenJson(weatherStore.storePath()));
QA_EXPECT(writeBrokenJson(webStore.storePath()));
QString weatherError;
QString webError;
const WeatherConfig weather = weatherStore.load(&weatherError);
const WebConfig web = webStore.load(&webError);
QA_EXPECT_EQ(weather.provider, QStringLiteral("open-meteo"));
QA_EXPECT(weather.autoLocateWhenNoDefault);
QA_EXPECT(web.enabled);
QA_EXPECT(!web.defaultToggleOn);
QA_EXPECT(!weatherError.isEmpty());
QA_EXPECT(!webError.isEmpty());
QA_EXPECT_EQ(backupCount(QStringLiteral("weather_config.broken.*.json")), 1);
QA_EXPECT_EQ(backupCount(QStringLiteral("web_config.broken.*.json")), 1);
}
QA_TEST(ConfigRecovery, ReminderStoreFallsBackToEmptyAndBacksUpBrokenJson)
{
QaConfigFiles files;
ReminderStore store;
QA_EXPECT(writeBrokenJson(store.storePath()));
QString error;
QA_EXPECT(store.load(&error).isEmpty());
QA_EXPECT(!error.isEmpty());
QA_EXPECT(!QFile::exists(store.storePath()));
QA_EXPECT_EQ(backupCount(QStringLiteral("reminders.broken.*.json")), 1);
}
QA_TEST(ConfigRecovery, SameSecondBackupsNeverOverwriteEarlierFiles)
{
QaConfigFiles files;
WeatherStore store;
QA_EXPECT(writeBrokenJson(store.storePath()));
store.load();
QA_EXPECT(writeBrokenJson(store.storePath()));
store.load();
QA_EXPECT_EQ(backupCount(QStringLiteral("weather_config.broken.*.json")), 2);
}
+140
View File
@@ -1,8 +1,34 @@
#include "TestHarness.h"
#include "src/weather/WeatherParser.h"
#include "src/weather/WeatherLocationResolver.h"
#include "src/weather/WeatherSummaryFormatter.h"
namespace
{
WeatherLocationCandidate candidate(
const QString &city,
const QString &admin,
const QString &country,
const QString &countryCode,
const QString &featureCode,
qint64 population,
double latitude,
double longitude)
{
WeatherLocationCandidate value;
value.cityName = city;
value.adminName = admin;
value.countryName = country;
value.countryCode = countryCode;
value.featureCode = featureCode;
value.population = population;
value.latitude = latitude;
value.longitude = longitude;
return value;
}
}
QA_TEST(WeatherParser, ExtractsExplicitCitiesAndDates)
{
const WeatherParser parser;
@@ -38,6 +64,120 @@ QA_TEST(WeatherParser, RejectsUnsupportedQueries)
QA_EXPECT(!parser.parse(QStringLiteral("明天穿什么")).unsupportedReason.isEmpty());
}
QA_TEST(WeatherLocationResolver, ResolvesChineseInternationalCities)
{
const WeatherLocationSearch newYork = WeatherLocationResolver::resolveSearch(QStringLiteral("纽约"));
const WeatherLocationSearch tokyo = WeatherLocationResolver::resolveSearch(QStringLiteral("东京"));
const WeatherLocationSearch japanTokyo = WeatherLocationResolver::resolveSearch(QStringLiteral("日本东京"));
const WeatherLocationSearch qualifiedTokyo = WeatherLocationResolver::resolveSearch(QStringLiteral("东京,日本"));
QA_EXPECT_EQ(newYork.queryName, QStringLiteral("New York"));
QA_EXPECT_EQ(newYork.countryCode, QStringLiteral("US"));
QA_EXPECT_EQ(tokyo.queryName, QStringLiteral("Tokyo"));
QA_EXPECT_EQ(tokyo.countryCode, QStringLiteral("JP"));
QA_EXPECT_EQ(japanTokyo.queryName, QStringLiteral("Tokyo"));
QA_EXPECT_EQ(japanTokyo.countryCode, QStringLiteral("JP"));
QA_EXPECT_EQ(qualifiedTokyo.queryName, QStringLiteral("Tokyo"));
QA_EXPECT_EQ(qualifiedTokyo.countryCode, QStringLiteral("JP"));
}
QA_TEST(WeatherLocationResolver, PreservesDomesticCityAndCountryQualifier)
{
const WeatherLocationSearch xian = WeatherLocationResolver::resolveSearch(QStringLiteral("西安市"));
const WeatherLocationSearch chinaXian = WeatherLocationResolver::resolveSearch(QStringLiteral("中国西安"));
QA_EXPECT_EQ(xian.queryName, QStringLiteral("西安"));
QA_EXPECT(xian.countryCode.isEmpty());
QA_EXPECT_EQ(chinaXian.queryName, QStringLiteral("西安"));
QA_EXPECT_EQ(chinaXian.countryCode, QStringLiteral("CN"));
}
QA_TEST(WeatherLocationResolver, SuppressesLowValueAlternatives)
{
const QVector<WeatherLocationCandidate> candidates = {
candidate(QStringLiteral("西安"), QStringLiteral("陕西"), QStringLiteral("中国"), QStringLiteral("CN"), QStringLiteral("PPLA"), 6500000, 34.26, 108.93),
candidate(QStringLiteral("西安"), QStringLiteral("湖南"), QStringLiteral("中国"), QStringLiteral("CN"), QStringLiteral("PPL"), 1200, 27.10, 111.20),
candidate(QStringLiteral("西安"), QStringLiteral("广东"), QStringLiteral("中国"), QStringLiteral("CN"), QStringLiteral("PPL"), -1, 23.10, 113.20),
};
const QVector<WeatherLocationCandidate> selected = WeatherLocationResolver::selectRelevantCandidates(
candidates,
WeatherLocationResolver::resolveSearch(QStringLiteral("西安")));
QA_EXPECT_EQ(selected.size(), 1);
QA_EXPECT(!WeatherLocationResolver::hasAmbiguity(selected));
}
QA_TEST(WeatherLocationResolver, KeepsPlausibleAmbiguity)
{
const QVector<WeatherLocationCandidate> candidates = {
candidate(QStringLiteral("Springfield"), QStringLiteral("Missouri"), QStringLiteral("United States"), QStringLiteral("US"), QStringLiteral("PPLA2"), 170000, 37.21, -93.29),
candidate(QStringLiteral("Springfield"), QStringLiteral("Illinois"), QStringLiteral("United States"), QStringLiteral("US"), QStringLiteral("PPLA"), 114000, 39.80, -89.64),
candidate(QStringLiteral("Springfield"), QStringLiteral("Georgia"), QStringLiteral("United States"), QStringLiteral("US"), QStringLiteral("PPL"), 2700, 32.37, -81.31),
};
const QVector<WeatherLocationCandidate> selected = WeatherLocationResolver::selectRelevantCandidates(
candidates,
WeatherLocationResolver::resolveSearch(QStringLiteral("Springfield")));
QA_EXPECT_EQ(selected.size(), 2);
QA_EXPECT(WeatherLocationResolver::hasAmbiguity(selected));
}
QA_TEST(WeatherLocationResolver, AppliesExplicitCountryFilter)
{
const QVector<WeatherLocationCandidate> candidates = {
candidate(QStringLiteral("Tokyo"), QStringLiteral("Tokyo"), QStringLiteral("Japan"), QStringLiteral("JP"), QStringLiteral("PPLC"), 8300000, 35.68, 139.69),
candidate(QStringLiteral("Tokyo"), QStringLiteral("Texas"), QStringLiteral("United States"), QStringLiteral("US"), QStringLiteral("PPL"), 1000, 33.90, -102.50),
};
const QVector<WeatherLocationCandidate> selected = WeatherLocationResolver::selectRelevantCandidates(
candidates,
WeatherLocationResolver::resolveSearch(QStringLiteral("日本东京")));
QA_EXPECT_EQ(selected.size(), 1);
QA_EXPECT_EQ(selected.first().countryCode, QStringLiteral("JP"));
}
QA_TEST(WeatherLocationResolver, ParsesBothIpLocationSchemas)
{
const QByteArray ipApiPayload = QByteArrayLiteral(
R"({"city":"Xi'an","region":"Shaanxi","country_name":"China","latitude":34.26,"longitude":108.94,"timezone":"Asia/Shanghai"})");
const QByteArray ipWhoIsPayload = QByteArrayLiteral(
R"({"success":true,"city":"Tokyo","region":"Tokyo","country":"Japan","latitude":35.68,"longitude":139.69,"timezone":{"id":"Asia/Tokyo"}})");
WeatherLocation ipApiLocation;
WeatherLocation ipWhoIsLocation;
QA_EXPECT(WeatherLocationResolver::parseIpLocationResponse(
ipApiPayload,
WeatherIpLocationProvider::IpApiCo,
&ipApiLocation));
QA_EXPECT(WeatherLocationResolver::parseIpLocationResponse(
ipWhoIsPayload,
WeatherIpLocationProvider::IpWhoIs,
&ipWhoIsLocation));
QA_EXPECT_EQ(ipApiLocation.cityName, QStringLiteral("Xi'an"));
QA_EXPECT_EQ(ipApiLocation.countryName, QStringLiteral("China"));
QA_EXPECT_EQ(ipApiLocation.timezone, QStringLiteral("Asia/Shanghai"));
QA_EXPECT_EQ(ipWhoIsLocation.cityName, QStringLiteral("Tokyo"));
QA_EXPECT_EQ(ipWhoIsLocation.countryName, QStringLiteral("Japan"));
QA_EXPECT_EQ(ipWhoIsLocation.timezone, QStringLiteral("Asia/Tokyo"));
}
QA_TEST(WeatherLocationResolver, RejectsBrokenIpLocationResponses)
{
WeatherLocation location;
QA_EXPECT(!WeatherLocationResolver::parseIpLocationResponse(
QByteArrayLiteral("{broken"),
WeatherIpLocationProvider::IpApiCo,
&location));
QA_EXPECT(!WeatherLocationResolver::parseIpLocationResponse(
QByteArrayLiteral(R"({"success":false,"message":"rate limited"})"),
WeatherIpLocationProvider::IpWhoIs,
&location));
QA_EXPECT(!WeatherLocationResolver::parseIpLocationResponse(
QByteArrayLiteral(R"({"city":"Tokyo","latitude":35.68})"),
WeatherIpLocationProvider::IpWhoIs,
&location));
}
QA_TEST(WeatherFormatter, MapsWeatherCodesAndWindDirections)
{
QA_EXPECT_EQ(WeatherSummaryFormatter::weatherCodeText(0), QStringLiteral("晴"));