feat(dashboard): test a message against the chat filter

GET /api/chat_filter/test?message=&chat=normal|free says whether the
filter would stop a message from a player below GM 2, word by word and
why: blocked or allowed on the staff lists, in chatplus_en_us.txt, an
approved character name, not allowed, in blocklist.dcf, or no
blocklist.dcf (free chat then stops everything). It follows
dChatFilter::IsSentenceOkay: words split at spaces, normalized the same
way. ModerationTools::ExplainMessage, unit tested.

Check: dWebTests ChatFilterWordsTest.Explain*; the API with a word from
each list, in normal and free chat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 00:58:06 -05:00
parent 9a5a9a274f
commit 29edb18298
3 changed files with 133 additions and 0 deletions

View File

@@ -81,3 +81,43 @@ TEST(ChatFilterWordsTest, DcfHashes) {
ASSERT_FALSE(ModerationTools::DcfHashes(bytes.substr(0, bytes.size() - sizeof(size_t) * 2)).has_value());
ASSERT_FALSE(ModerationTools::DcfHashes("DCFB").has_value());
}
namespace {
ModerationTools::WordSources Sources(bool blockFileLoaded = true) {
ModerationTools::WordSources sources;
sources.dashboard = [](const std::string& w) -> std::optional<bool> {
if (w == "darn") return false;
if (w == "brickbuild") return true;
return std::nullopt;
};
sources.allowFile = [](const std::string& w) { return w == "hello" || w == "darn"; };
sources.characterName = [](const std::string& w) { return w == "bob"; };
sources.blockFile = [](const std::string& w) { return w == "rude"; };
sources.blockFileLoaded = blockFileLoaded;
return sources;
}
std::vector<std::string> Reasons(const std::vector<ModerationTools::WordVerdict>& verdicts) {
std::vector<std::string> reasons;
for (const auto& v : verdicts) reasons.push_back((v.stopped ? "x " : "ok ") + v.reason);
return reasons;
}
}
TEST(ChatFilterWordsTest, ExplainNormalChat) {
const auto verdicts = ModerationTools::ExplainMessage("Hello, Bob! brickbuild darn zzz", true, Sources());
ASSERT_EQ(verdicts.size(), 5u);
EXPECT_EQ(verdicts[0].text, "Hello,");
EXPECT_EQ(verdicts[0].word, "hello");
// A blocked word is stopped even though the file allows it
EXPECT_EQ(Reasons(verdicts), (std::vector<std::string>{ "ok allow_file", "ok character_name", "ok allowed_here", "x blocked_here", "x not_allowed" }));
EXPECT_TRUE(ModerationTools::ExplainMessage("", true, Sources()).empty());
}
TEST(ChatFilterWordsTest, ExplainFreeChat) {
EXPECT_EQ(Reasons(ModerationTools::ExplainMessage("rude zzz darn", false, Sources())),
(std::vector<std::string>{ "x block_file", "ok not_in_block_file", "x blocked_here" }));
// Without blocklist.dcf free chat stops every word
EXPECT_EQ(Reasons(ModerationTools::ExplainMessage("zzz darn", false, Sources(false))),
(std::vector<std::string>{ "x no_block_file", "x no_block_file" }));
}