ZaStoGram_desktop/Telegram/SourceFiles/test/test_history_fixtures.h
John Preston d7a6184776 [ai] Add a regular service-message fixture to the harness
Task: 2026/08/17/inject-regular-service-messages-in-harness
2026-08-18 11:40:55 +04:00

95 lines
4.7 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/basic_types.h"
class History;
class HistoryItem;
class PeerData;
namespace rpl {
class lifetime;
} // namespace rpl
namespace Test {
// Optional injection knobs. The item is always outgoing: an incoming one
// reaches History::newItemAdded's !out() arm, which either requests a dialog
// entry or runs setUnreadCount -> setInboxReadTill and pins _inboxReadBefore
// at the fixture's above-ServerMaxMsgId id. History::setInboxReadTill is
// accumulate_max only, so nothing under test/ can undo that; a scenario that
// needs an incoming service item must bring a way to restore the state.
// |local| reproduces the pre-fixture MessageFlag::Local + client-id shape, so
// a scenario can log a negative control against the same code path.
struct ServiceMessageFields {
PeerData *from = nullptr;
bool local = false;
QString diagnosticTag = u"service"_q;
};
// Injects |action| into |history| as a real history entry and returns the
// item, or null after one logged FIXTURE_UNSUPPORTED FAIL when the shape is
// refused below, so the caller must null-check it. The item is isRegular():
// no MessageFlag::Local, no MessageFlag::FakeHistoryItem, and an id from
// nextNonHistoryEntryId() that IsClientMsgId rejects, so it is never
// registered as a client-side message, and IsServerMsgId rejects too, so the
// server can never delete or edit it.
// It is isService() for every action except messageActionPhoneCall and
// messageActionConferenceCall, which the MTPDmessageService constructor turns
// into Data::MediaCall items with no HistoryServiceData component.
// The injection issues no MTP request, except for actions whose
// applyServiceChanges or newItemAdded arm reaches the network:
// messageActionSetChatWallPaper, messageActionSetChatTheme with a
// plain-emoticon theme, messageActionPaidMessagesPrice on an active broadcast
// peer, and any unique-gift action owned or hosted by the signed-in account,
// which includes the demonstrated star-gift card.
// Both shapes are refused when History::folderKnown() is false, because
// History::newItemAdded would then issue a requestDialogEntry MTP request.
// For the regular shape only, the id sits above ServerMaxMsgId, where
// SparseIdsList::addNew asserts: an existing History::messages() index is
// refused, and both chat-photo actions are refused by action type, which is
// conservative, because a photoEmpty payload builds no media and would not
// have asserted, while messageActionChatDeletePhoto is accepted. The hole
// left there is a dialog-row chat preview opened for the same peer after the
// injection, which can still seed History::messages() from this item.
// The second hole is that the id is not a server id, while
// Histories::readInboxTill(history, tillId) asserts IsServerMsgId(tillId), as
// History::readInboxTillNeedsRequest asserts it again.
// Histories::readInboxTill(item) reaches that assert for any isRegular()
// item, so HistoryInner::checkActivation aborts whenever the injected card is
// the bottom-most fully visible view of a shown chat while
// HistoryWidget::markingMessagesRead() is true, and the two accessibility
// focus paths abort whenever accessibility focus lands on it. Because
// History::setLastMessage makes this item the history's _lastServerMessage,
// Histories::readInbox aborts the same way on a send or a forward in that
// chat and on the dialogs "Mark as read". The paint path's readTill and
// Histories::readInboxOnNewMessage are closed only by the item being
// unconditionally out(), so f_out is load-bearing beyond the _inboxReadBefore
// reason above.
// Destroying |lifetime| destroys the item and removes it from the history; a
// history torn down first, an item already gone, a second destroy and several
// injections into one lifetime are all handled. With fields.local it injects
// the non-regular shape instead.
[[nodiscard]] HistoryItem *InjectServiceMessage(
not_null<History*> history,
const MTPMessageAction &action,
ServiceMessageFields fields,
rpl::lifetime &lifetime);
// Writes one line with the item's id and each menu-gating predicate
// separately, so an empty context menu is diagnosable from the log alone.
// InjectServiceMessage calls it once per injection; a scenario can call it
// again after an interaction to log the same fields in the same format. A
// null |item|, which is what a refused injection returns, logs one line
// naming the null instead of asserting.
void LogServiceMessagePredicates(
HistoryItem *item,
const QString &tag);
} // namespace Test