ZaStoGram_desktop/Telegram/SourceFiles/test
Repository files (latest commit first)
Filename Latest commit message Latest commit date
loop-uh 0fd5d76a67 Merge upstream v7.3.1
Слияние с tdesktop от 64ca5475f2 до тега v7.3.1 (7.2.10 beta, 7.3, 7.3.1).

- Версия 7.3.1 (7003001), BetaChannel 0, комментарий ZaStoGram сохранён.
- Обновлятор: оригинал убрал пакеты v1 (RSA), а наш Packer и Forgejo
  выпускают именно их. v1 оставлен как UnpackUpdateV1 рядом с v2 и
  использует новые проверенные распаковку и извлечение файлов.
- mtproto: исправления импорта авторизации из оригинала перенесены в нашу
  структуру RequestRegistry; пути include новых файлов кошелька
  приведены к нашей раскладке mtproto/.
- prepare.py: стадии оригинала (ucrt64, ffmpeg 8, openssl 3.5, libheif,
  Rust, wallet-engine) с нашими skip-debug и patches_extra.
- lib_ui, lib_lottie: форки слиты с версиями из v7.3.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:43:47 +03:00
..
README.md [ai] Correct the refusal and App Nap claims in harness docs 2026-10-10 11:45:28 +04:00
test_activation.cpp [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_activation.h [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_agent.cpp [ai] Hold an App Nap opt-out for the whole macOS test run 2026-10-09 23:29:29 +04:00
test_agent.h [ai] Hold an App Nap opt-out for the whole macOS test run 2026-10-09 23:29:29 +04:00
test_animation_clock.cpp [ai] Share the harness console-lock reading with macOS 2026-10-09 23:29:29 +04:00
test_animation_clock.h [ai] Share the harness console-lock reading with macOS 2026-10-09 23:29:29 +04:00
test_box_button.cpp [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_box_button.h [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_capture.cpp [ai] Require full visibility for mapped captures 2026-09-24 22:54:54 +04:00
test_capture.h [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_clipboard.cpp [ai] Name a locked Windows console in harness self-tests 2026-10-08 22:24:50 +04:00
test_clipboard.h [ai] Name a locked Windows console in harness self-tests 2026-10-08 22:24:50 +04:00
test_console_lock.cpp [ai] Share the harness console-lock reading with macOS 2026-10-09 23:29:29 +04:00
test_console_lock.h [ai] Share the harness console-lock reading with macOS 2026-10-09 23:29:29 +04:00
test_corner_patch.cpp [ai] Add corner and animation helpers and media traps 2026-10-08 22:24:50 +04:00
test_corner_patch.h [ai] Add corner and animation helpers and media traps 2026-10-08 22:24:50 +04:00
test_custom_emoji.cpp [ai] Give the test harness an always-ready emoji stub 2026-08-21 15:34:29 +04:00
test_custom_emoji.h [ai] Give the test harness an always-ready emoji stub 2026-08-21 15:34:29 +04:00
test_gated_stage.cpp [ai] Record MTP retries, space-class reads and N/A rows 2026-09-03 22:26:26 +04:00
test_gated_stage.h [ai] Record MTP retries, space-class reads and N/A rows 2026-09-03 22:26:26 +04:00
test_gram_reconcile.cpp [ai] Add wallet fixture reconciliation and secrecy scan helpers 2026-10-08 22:23:59 +04:00
test_gram_reconcile.h [ai] Add wallet fixture reconciliation and secrecy scan helpers 2026-10-08 22:23:59 +04:00
test_history_fixtures.cpp [ai] Add a regular service-message fixture to the harness 2026-08-18 11:40:55 +04:00
test_history_fixtures.h [ai] Add a regular service-message fixture to the harness 2026-08-18 11:40:55 +04:00
test_hover.cpp [ai] Clear the hover a synthesized click left behind 2026-08-28 13:11:19 +04:00
test_hover.h [ai] Clear the hover a synthesized click left behind 2026-08-28 13:11:19 +04:00
test_ink.cpp [ai] Read a text pen from solid glyph cores 2026-09-24 22:54:55 +04:00
test_ink.h [ai] Read a text pen from solid glyph cores 2026-09-24 22:54:55 +04:00
test_lang_pack.cpp [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_lang_pack.h [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_launch_fuse.cpp [ai] Print a passing check's details in the harness 2026-08-28 10:41:01 +04:00
test_launch_fuse.h [ai] Declare expected launches, refuse unpainted grabs 2026-08-10 18:33:29 +04:00
test_layer_root.cpp [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_layer_root.h [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_log.cpp Merge upstream v7.3.1 2026-10-15 21:43:47 +03:00
test_log.h [ai] Print helper numbers in the telemetry format on request 2026-10-09 20:43:49 +04:00
test_log_lines.cpp [ai] Keep every harness log record on one physical line 2026-09-03 22:26:26 +04:00
test_log_lines.h [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_mapped_target.cpp [ai] Scroll mapped-target fixture by exclusive till 2026-09-24 22:54:54 +04:00
test_mapped_target.h [ai] Require full visibility for mapped captures 2026-09-24 22:54:54 +04:00
test_marking_read.cpp [ai] Share the harness console-lock reading with macOS 2026-10-09 23:29:29 +04:00
test_marking_read.h [ai] Share the harness console-lock reading with macOS 2026-10-09 23:29:29 +04:00
test_menu.cpp [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_menu.h [ai] Share the harness console-lock reading with macOS 2026-10-09 23:29:29 +04:00
test_messages.cpp [ai] Remove test code from release. 2026-07-31 22:53:32 +04:00
test_messages.h Add sent message watcher to test harness 2026-07-31 19:11:28 +04:00
test_notify_override.cpp Merge upstream v7.3.1 2026-10-15 21:43:47 +03:00
test_notify_override.h [ai] Add a local restored notify override to the harness 2026-10-08 22:24:49 +04:00
test_open_handoff.cpp Got rid of cast-to-void result discarding. 2026-08-21 14:38:14 +03:00
test_open_handoff.h [ai] Fuse real launches and assert document opens 2026-07-29 13:01:53 +04:00
test_panel.cpp [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_panel.h [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_post_paint.cpp [ai] Print helper numbers in the telemetry format on request 2026-10-09 20:43:49 +04:00
test_post_paint.h [ai] Print helper numbers in the telemetry format on request 2026-10-09 20:43:49 +04:00
test_probe.cpp [ai] Print helper numbers in the telemetry format on request 2026-10-09 20:43:49 +04:00
test_probe.h [ai] Print helper numbers in the telemetry format on request 2026-10-09 20:43:49 +04:00
test_rpc_fixture.cpp Поправить пути mtproto в тестовой заглушке RPC из апстрима 2026-09-25 15:29:23 +03:00
test_rpc_fixture.h Поправить пути mtproto в тестовой заглушке RPC из апстрима 2026-09-25 15:29:23 +03:00
test_rpc_retry.cpp Merge upstream dev through 64ca5475f2 2026-09-25 14:38:13 +03:00
test_rpc_retry.h [ai] Record MTP retries, space-class reads and N/A rows 2026-09-03 22:26:26 +04:00
test_runner.cpp [ai] Name a locked Windows console in harness self-tests 2026-10-08 22:24:50 +04:00
test_runner.h [ai] Name a locked Windows console in harness self-tests 2026-10-08 22:24:50 +04:00
test_scenario.cpp [ai] Improve task test harness reliability and guidance. 2026-08-16 22:17:49 +04:00
test_secrecy_scan.cpp [ai] Print helper numbers in the telemetry format on request 2026-10-09 20:43:49 +04:00
test_secrecy_scan.h [ai] Print helper numbers in the telemetry format on request 2026-10-09 20:43:49 +04:00
test_style.cpp [ai] Settle chat style before harness colour reads 2026-08-14 17:28:43 +04:00
test_style.h [ai] Settle chat style before harness colour reads 2026-08-14 17:28:43 +04:00
test_text_drop.cpp [ai] Add a clipboard-free text drop helper to the harness 2026-10-08 22:24:47 +04:00
test_text_drop.h [ai] Add a clipboard-free text drop helper to the harness 2026-10-08 22:24:47 +04:00
test_text_reads.cpp [ai] Record MTP retries, space-class reads and N/A rows 2026-09-03 22:26:26 +04:00
test_text_reads.h [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_toast_capture.cpp [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_toast_capture.h [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_transfer.cpp Fix build for Release. 2026-08-28 11:14:03 +04:00
test_transfer.h Add document transfer observation test helper 2026-08-10 18:42:49 +04:00
test_via_window.cpp [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_via_window.h [ai] Cite code by symbol, not line number, in the harness 2026-10-08 22:24:50 +04:00
test_wallet_intercept.cpp Merge upstream v7.3.1 2026-10-15 21:43:47 +03:00
test_wallet_intercept.h Merge upstream v7.3.1 2026-10-15 21:43:47 +03:00
test_wallet_ready.cpp [ai] Correct the wallet refresh helper's stale notes 2026-10-08 22:23:54 +04:00
test_wallet_ready.h Fix key update, add more logging. 2026-10-08 22:23:58 +04:00
test_web_cache.cpp [ai] Add a harness helper to clear web document cache 2026-10-08 22:24:47 +04:00
test_web_cache.h [ai] Add a harness helper to clear web document cache 2026-10-08 22:24:47 +04:00
test_wheel.cpp [ai] Deliver synthetic wheels up the parent ladder 2026-09-24 22:54:00 +04:00
test_wheel.h [ai] Deliver synthetic wheels up the parent ladder 2026-09-24 22:54:00 +04:00
test_widgets.cpp [ai] Add a clipboard-free text drop helper to the harness 2026-10-08 22:24:47 +04:00
test_widgets.h [ai] Launch the test-run client without activating it 2026-10-09 20:43:18 +04:00
test_window_exposure.cpp [ai] Share the harness console-lock reading with macOS 2026-10-09 23:29:29 +04:00
test_window_exposure.h [ai] Share the harness console-lock reading with macOS 2026-10-09 23:29:29 +04:00

Telegram Desktop task-test harness

This directory contains the permanent Debug-only infrastructure used by perform-task overlays. Read this catalog before designing or recovering a scenario. The overlay should add only task-specific fixtures, production entry points, checks, and captures; do not recreate facilities already here.

The default is one packed scenario and one app launch. Put all compatible checks, themes, values, interactions, and captures into that execution. Plan another launch only when you can name an incompatible process-lifetime requirement, such as a startup-only setting or persisted state that the first flow necessarily contaminates. “The scenario is large” and “a smoke run is simpler” are not reasons to split it. After a TEST_FLAW, repair every flaw visible in the run together and rerun the packed scenario.

Cite code by symbol, never by line number, here and in harness comments: Runner::finish() in test_runner.cpp, not test_runner.cpp:NNN or :NNN-MMM. Line numbers drift with every edit, including the next harness task's, and nothing updates them. A failure-diagnosis row names the functions, constants and macros on its causal path, in product, submodule and Qt sources alike. Quoted log or crash text stays verbatim.

The stage contract

Runner executes each Stage as three distinct operations:

Member Contract Must not do
run One action whose prerequisites were established by an earlier stage. Dereference an object that this stage is supposed to wait for.
until A pure, repeatable readiness observation. Click, mutate state, emit a PASS/FAIL, or encode the expected product result. The one documented exception is the Test::ForceWindowActive re-assertion in the activation row under Failure diagnosis.
then Assertions and the next action after readiness succeeded. Resolve an unguarded replacement for the object that readiness accepted.
timeoutDetails Return the latest observed values and identities. Repeat only “not ready”; omit the values needed to diagnose why.
skipReason Return why this stage does not apply, or an empty string when it does. A non-empty reason writes TEST_RESULT: N/A: <stage> - <reason>, skips run, until and then, and moves on in the same turn. Decide the product outcome. A gate that reads a measurement instead of a precondition turns a would-be FAIL into a silent N/A.

A stage timeout is a harness failure unless the readiness condition itself is the behavior under test. Keep expected geometry, text, pixels, counts, and other falsifiable outcomes in then or captureAndInspect, not in until. Otherwise a real product mismatch is mislabeled as an opaque timeout.

Use Runner::actOnWidget when an action depends on a widget that may appear asynchronously. It polls the resolver and optional readiness predicate, keeps a QPointer to the exact accepted object, and invokes the action once. This removes both the eager-.run crash and the “resolve again in .then” race. The same rule is built into the harness's widget-carrying readings: Test::BoxShellButtons, Test::WindowMappedCapture, Test::PaintingLayerRootResult, Test::ToastSubtreeReading and Test::ContextMenuReading hold QPointer<QWidget>, so a reading kept across a stage boundary answers matched() / resolved() false (a context-menu reading's menu reads null) once its subject is destroyed and still prints its counts, labels, identity and refusal; format a WidgetDescription of the pointer while the reading is still matched, because there is nothing to format afterwards.

The generic startup waits are opt-in:

  • waitEvent("launch_finished") is the normal first gate.
  • waitForSessionReady() is needed only when the scenario consumes a session.
  • waitForChatsLoaded() is needed only when the tested setup consumes the chats list; it is non-fatal and records whether loading completed.
  • waitForChatsLoadedStrict() is appropriate only when chats loading is part of the required fixture or result.

Do not put all three waits in every scenario preamble. An injected box, helper oracle, or direct production seam often needs none of the account or chats gates.

Exact objects and actions

Telegram custom widgets generally do not declare Q_OBJECT. Never call findChildren<Ui::CustomWidget*>(); Qt may return unrelated widgets blindly cast to that type. FindAll<T>, FindFirst<T>, and FindVisible<T> enumerate real QWidget descendants and apply C++ RTTI safely.

FindVisible means only QWidget::isVisible(). It does not prove that the widget belongs to the current layer, is unobscured, has non-empty mapped geometry, or is the latest instance. Add those task-specific conditions, or publish the exact object from its construction seam.

For layer-owned objects, repeated boxes, async replacements, and footer callbacks with no returned pointer, use the live publication helpers:

// In an inventoried Debug overlay hunk at the real construction seam.
Test::PublishLiveWidget(u"gift.composer"_q, composer);
Test::PublishLiveAction(
	u"gift.confirm"_q,
	box,
	[=] { confirm(); });

// In test_scenario.cpp.
runner->actOnWidget(
	u"open current composer"_q,
	[] { return Test::ReadLiveWidget(u"gift.composer"_q).widget; },
	[](QWidget *widget) { Test::Click(widget); },
	[](QWidget *widget) {
		return widget->isVisible() && widget->isEnabled();
	},
	Test::kDefaultStageTimeout,
	[](QWidget *widget) {
		return u"visible=%1 enabled=%2 size=%3x%4"_q
			.arg(widget->isVisible())
			.arg(widget->isEnabled())
			.arg(widget->width())
			.arg(widget->height());
	});

Each publication advances a generation. A destroyed context makes its widget or action unavailable. Actions are one-shot by default; pass true only for a production callback that is intentionally retryable. Use ReadLiveAction() to wait for a specific generation and invocation count, then InvokeLiveAction() in an action/assertion stage. This is preferable to guessing which RoundButton belongs to a presentation wrapper.

Resolving a chat's message list

A chat's message list is not necessarily HistoryWidget / HistoryInner. The legacy main-chat stack is HistoryWidget (history/history_widget.h) plus HistoryInner (history/history_inner_widget.h); the modern stack is HistoryView::ChatWidget (history/view/history_view_chat_section.h) plus HistoryView::ListWidget (history/view/history_view_list_widget.h); and the admin log is a third stack again, AdminLog::InnerWidget, which implements HistoryView::ElementDelegate itself and constructs no ListWidget. ChatWidget already backs replies threads, forum topics and both monoforum and Saved Messages sublists (both are Data::SavedSublist); scheduled messages, pinned messages, welcome messages and the chat preview popup each construct their own ListWidget.

A scenario that assumes one named widget renders every chat fails opaquely. Publishing a live widget from HistoryWidget's construction seam and then walking the window logged RENDER_ROUTES: published=0 walked=0 chain=[none] against 403 root descendants — the HistoryInner was never constructed, and the walk found none either — while the window grab plainly showed the chat painting the row.

The rule is therefore about resolution, not about a name, and stays true as the migration proceeds. Resolve the list from the section that constructs it and prove the candidate owns the message:

  • publish the exact live object at the statement that constructs the list — _inner = _scroll->setOwnedWidget(object_ptr<ListWidget>( in history/view/history_view_chat_section.cpp — through Test::PublishLiveWidget;
  • keep Test::FindVisible<HistoryView::ListWidget> as an independent cross-check and log both answers;
  • accept a candidate only when its own lookup answers for the exact message: list->viewByPosition(item->position()) must return an Element whose data() is that item and whose delegate() is that same list;
  • scroll with the product's own entry points, showThread and then the list's showAtPosition; decide where the row sits with the list's public elementIntersectsRange(view, from, till), and frame it with Test::CaptureMappedRect.

item->mainView() is not the Element the visible section owns, so it is neither a readiness gate nor a geometry oracle. For a row a ListWidget was painting it answered view=1 with viewHeight=0 and itemTop=-3; use the list's own viewByPosition for both jobs instead.

The parent chain a resolved ListWidget produced, as the shape to expect, was ListWidget < Ui::ElasticScroll < ChatWidget < MainWidget < Ui::RpWidget < Ui::RpWidget < MainWindow.

Input helpers

All input helpers synchronously dispatch real Qt events and drain Ui::PostponeCall work after each event. They lifetime-guard the target and stop if it is destroyed. AppendClipboardRoundTrip sits beside DropText below because the two answer the clipboard question from opposite sides, but it is a stage, not an event helper: it yields to the event loop between its attempts.

On macOS a default workspace.py test-run launch of an app bundle's main executable does not activate the client: it launches the bundle through LaunchServices in the background, with Qt's own launch activation switched off (.agents/shared/test-loop.md, Launch activation; the report reads launch_method: "background"). The client starts as an inactive application and its windows open behind the frontmost application's. A window shown already covered reads isExposed() true until the window server first reports a change for it, so exposed=1 is no proof that it is on screen; after such a change a covered window reads unexposed and gets no paints. Drive focus-routed actions with ForceWindowActive, keep a window a stage needs painted exposed with KeepWindowExposed either way, and use test-run --activate when genuine OS activation is the subject. Product flows that raise or activate a top-level window - Ui::PopupMenu, Ui::SeparatePanel::showAndActivate(), Window::Controller::activate(), DropText into a shown field - still request activation of the client on macOS, and so do the not-marking-read, separate-panel-walk and popup-menu self-tests. After a background launch the window server may grant or refuse that request: on this host it granted it about 11 s after launch in two default runs and refused it about 20 s after launch in a third, under a condition those runs did not establish (2026/10/08/launch-the-testagent-client-without-activating-it, Runs 4, 8 and 9). A granted request takes the key window from the frontmost application. After a refused one the client stays inactive and the frontmost application keeps its key window, but the client's window may still be ordered in front of the frontmost application's window by the request's own window ordering: in Run 9 above the primary window stayed covered after its restore, while with an overlay that made the client's own application-activation calls no-ops, reproducing that refusal under another stage sequence, the inactive primary window was in front for about 4 s after the not-marking-read self-test's restore through Window::Controller::activate(), until a later request put it behind again (2026/10/09/keep-background-testagent-runs-on-macos-unthrottled-and-painting, Runs 0 and 3). So the scenario's own activation is no reliable route to OS activation under the default. A client the launch did not activate is a background application, which macOS may nap and whose timers it may coalesce: in background runs on this host without a hold, a sampler's heartbeat gaps grew to 100-290 ms after about 15 s of normal cadence (2026/10/01/animate-gram-card-sending-and-settle-effects, Run 1), and a 4 ms precise sampler ticked every 50-1000 ms from about the 80th second (2026/10/02/show-the-input-method-composition-in-the-gram-send-amount, Run 1). On a locked console macOS napped such a client without a hold, from about 30 s after process start (2026/10/09/keep-background-testagent-runs-on-macos-unthrottled-and-painting, Runs 0 and 4), but another application's window covering the client did not guarantee it: on an unlocked console, a client without a hold that the frontmost application's window covered kept an exact 16 ms timer cadence over 60 s and over 300 s (Runs 1 and 2). Those runs did not establish what made macOS nap the client. So on macOS the harness holds one user-initiated, latency-critical NSProcessInfo activity (NSActivityUserInitiatedAllowingIdleSystemSleep | NSActivityLatencyCritical; idle system sleep stays allowed) for the whole -testagent run: Test::ApplyStartupOverrides() (test_agent.cpp) begins it early in Application::run(), before Test::Start(), and never ends it, so it lasts until the process exits, a watchdog or stage-timeout ending included. The log says once which case a run is in: a held run reads NOTE: TDESKTOP_TEST_APP_NAP=[] applied: hold=active ..., and a run without the hold reads hold=off with the reason. An overlay no longer holds an activity of its own: the per-overlay BeginLatencyActivity of the 2026/10/07/animate-the-sending-row-for-a-collectible-transfer overlay is history, and a second activity is harmless but redundant. A campaign whose subject is background scheduling itself (App Nap, timer coalescing, throttled-timer behaviour) launches with test-run --env TDESKTOP_TEST_APP_NAP=allow, and its log then reads hold=off source=environment; any other non-empty value is logged as rejected and the hold is kept. Off macOS nothing reads TDESKTOP_TEST_APP_NAP and nothing is held.

Helper Use
Click Left press and release at the center or a supplied local point, then a QEvent::Leave to that same widget, so Ui::AbstractButton::isOver() is false on it and a RoundButton repaints its normal textBg. Send it the widget that accepts the press — an ignored press propagates to the ancestor that takes it and the leave does not, so a click aimed at a non-accepting child leaves that ancestor hovered.
TypeText Key press/release per Unicode grapheme; surrogate pairs and joined emoji stay intact. Send it to the widget that owns the key route: for a Ui::InputField that is rawTextEdit(), never the wrapper.
CommitText One QInputMethodEvent commit when insertion, custom emoji, or IME semantics matter more than physical keys. Same target rule as TypeText, and the fallback when key events left the field empty.
DropText Hand a text to a Ui::InputField the way a paste does, with no system clipboard: a QMimeData carried by a QDragEnterEvent and then a QDropEvent, both sent to rawTextEdit()->viewport(), which ends in canInsertFromMimeDataInner / insertFromMimeDataInner and so in the field's setMimeDataHook (or its own insertion when the hook leaves it), the insertion Ctrl+V ends in. The drop is sent only when the field's own editor accepted the enter. An ignored enter climbs to the first enabled ancestor that accepts drops, such as a chat's file-drop area, so for the pair an application filter withholds every drag event Qt would hand any other widget, and the returned TextDrop lists them in shielded. It settles once after the pair, not after each event. A miss is a named refusal: viewport-refuses-drops, enter-not-accepted or drop-not-accepted. The text goes in at the caret and does not replace a selection. It does not exercise the clipboard read of a Paste accessory or of Ctrl+V. InputField::dropEventInner raises and activates the field's window afterwards (on macOS this asks for the app to come forward, which the window server may grant or refuse after a default background test-run launch; a never-shown top level is unaffected). TextDropDetails never prints the text. Confirm with test_text_drop.h.
AppendClipboardRoundTrip A check whose subject is the system clipboard itself - a copy entry's result, or a Paste accessory's or Ctrl+V's own read - first needs a host whose clipboard reads back. One stage (test_clipboard.h) writes a sentinel carrying the caller's label (one token, no spaces) and reads it back, up to 10 times 300 ms apart across event-loop turns, logging every attempt as a CLIPBOARD_ROUND_TRIP_ATTEMPT row; read the returned ClipboardRoundTrip from a later stage: succeededAt, or a named refusal saying what the last read held (read-empty, read-stale-sentinel, read-foreign). Judge a product copy afterwards with ReadClipboard(expected), one classified reading (expected / empty / harness-sentinel / foreign) with its owns reading, never a wait. Text the harness did not write is never printed, measured or hashed, and the owner's clipboard is overwritten and never saved or restored. For what a field does with pasted text, DropText stays the route. Confirm with AppendClipboardRoundTripSelfTest.
PressKey Escape, Return, arrows, shortcuts, and other key behavior. Send to the widget that owns the event route, often the raw editor (Ui::InputField::rawTextEdit()) or the top Ui::BoxLayerWidget, not a wrapper.
Drag Left-button press, interpolated moves with buttons()==LeftButton, and release, then the same QEvent::Leave as Click.
Wheel A real wheel event at the named widget's local point (QPoint(0, -120) is one conventional downward step). A stack-built QWheelEvent is not spontaneous, so Qt 6 does not propagate it; the helper re-sends up parentWidget() until a widget handles and accepts — the scroll area's viewport, never the QAbstractScrollArea itself — honouring isWindow() and Qt::WA_NoMousePropagation. Returns WheelDelivery; a miss is a named refusal, not a silent no-op. Confirm with test_wheel.h.
OpenContextMenu A context menu (test_menu.h): deliver a QContextMenuEvent (Mouse by default, Keyboard allowed) at a point in the receiving widget's coordinates and, in the same call, read the Ui::PopupMenu it built - its ordered non-separator action texts and its lifecycle at build and after the postponed calls settled - as a ContextMenuReading, or a named refusal: no-menu, several-menus, receiver-gone. The snapshot spans the whole application, so a menu the handler parents to another widget is found, and the reading holds the menu as a QPointer, so it stays printable after the menu dies. The helper does not hover: hover first when the product's handler reads the hovered link. TriggerContextMenuAction(reading, text) triggers the one entry with exactly that text and delivers its queued Ui::Menu::CreateAction callback before it returns, without hiding the menu; it refuses menu-gone, no-such-action, several-actions, action-disabled and submenu-entry. Neither grabs pixels: a frame stays with CapturePopupMenu while the menu is still open. Never poll for a menu on later turns, and count a no-menu reading only beside a positive control the same helper built in the same widget (Failure diagnosis). Confirm with AppendContextMenuReaderSelfTest.
Settle Wrap programmatic mutations such as InputField::setText; drains postponed fixups after the action.
ForceWindowActive Inject window activation through the QPA seam before any focus-routed action, and re-assert it on every poll of a bounded wait. On a locked console, or on macOS after a default test-run launch until something activates the client, the window is never active in Qt's sense, so setFocus() never reaches QApplication::focusWidget() and every hasFocus()/isActiveWindow() branch silently reads false.
KeepMainWindowNotMarkingRead Minimize the primary window and poll until MainWindow::markingAsRead() is false with the window minimized and not hidden, or until kNotMarkingReadBound returns a named refusal. Call it again after Window::Controller::activate() or any showHistory that is not anim::activation::background, which clear the minimized bit. A control that cannot read true within kNotMarkingReadBound - a still-unexposed window, or a locked console that keeps it false - must not be reported as success; the self-test exposes the primary window itself with KeepWindowExposed from its control until its restore stage. Confirm with test_marking_read.h.
KeepWindowExposed Stack a covered top-level window above ordinary windows - on macOS Qt::WindowStaysOnTopHint on its QWindow, never QWidget::setWindowFlag - without activating the app, changing the focus window or taking focus, so a window another application covers is exposed and paints again. It never waits: wait for ReadWindowExposure(...).exposed (isExposed()) in a pure until. Undo with RestoreWindowExposure before teardown. It refuses a minimized window by name (stacking cannot expose it), so it cannot undo KeepMainWindowNotMarkingRead; off macOS it is a no-op that still returns its reading. Confirm with test_window_exposure.h.
ClickBoxButton Click a box's shell footer button by label. addButton re-parents the button onto the Ui::BoxLayerWidget, so a content-rooted FindAll<Ui::RoundButton> never sees it; this roots the search at Test::PaintingLayerRoot(box), keeps only its direct children, and refuses by name when nothing matches.

The harness has no pointer. No helper produces a hover, and every measurement is taken as if no cursor exists — assert a hovered state by setting it deliberately, not by relying on a click's residue. A press also starts a RippleButton ripple that decays over its style's showDuration + hideDuration; finish it with finishAnimating() or wait it out before measuring a fill.

Timers, queued invokes, animations, network callbacks, and reactive streams still require condition waits. Settle is not a replacement for readiness.

Capture and visual assertions

Choose the narrowest helper that captures the real paint owner:

Need Helper
Static visible widget or local rect CaptureWidget / CaptureRect
Rect expressed in a child or offscreen content widget's coordinates CaptureMappedRect
Complete mapped target visible in painted owner and scroll viewport before that capture ReadMappedTarget / MappedTargetReady / PreparedWidgetCapture::prepare(owner, origin, rect) / CaptureMappedTarget
Full box, layer owner, animation, or asynchronously populated surface Runner::captureWidget
Animating owner whose product paints can run back to back (several animations at once, especially in Debug), sampled right after each of them instead of on a timer PostPaintSampler (test_post_paint.h)
Short Ui::Animations-driven product animation (a slide transition, a reveal) captured at chosen elapsed times after one synchronous action or click handler, whether or not the target receives compositor paints (a minimized window, a never-shown widget) RunClockedFrames (test_animation_clock.h)
Whole box inside a layer (title + footer shell) PaintingLayerRoot + Runner::captureWidget, or CaptureBoxLayer
Box content cropped inside a layer CaptureInLayerRoot
Open Ui::PopupMenu CapturePopupMenu, a supplementary frame taken only while the menu is still open; its action texts and lifecycle come from OpenContextMenu (test_menu.h), which reads the menu in the turn that built it and grabs no pixels
Toast whose frame must not compose the product surface painted beneath it CaptureToastSubtree
Other widget that paints no opaque background of its own: a fade-in wrapper that is a child widget, such as Ui::ImportantTooltip CaptureViaWindow
A Ui::Tooltip, which is RpWidget(nullptr) and so its own top-level window that CaptureViaWindow refuses Find it in QApplication::topLevelWidgets() (dynamic_cast<Ui::Tooltip*>, visible only), read its text from accessibilityName(), capture it with CaptureWidget
Exact accepted frame plus numeric/raster assertions Runner::captureAndInspect
Small target comparison Crop, Zoom, ContactSheet
Foreground colour, contrast, or painted-band measurement MeasurePaintedInk and the helpers in test_ink.h
Rounded corner of a media or bubble against a twin or reference: the background beyond the corner, arc coverage across twins whose media colours differ, containment, chrome inside the patch ReadCornerBackground / CompareCornerCoverage / ReadCornerContainment / GateCornerChrome (test_corner_patch.h)

CaptureInLayerRoot crops to the box content's own rect. That is not the whole box: Ui::BoxLayerWidget::setTitle creates _title parented to the shell, and addButton re-parents each footer button onto the shell, so both live outside box->rect(). CaptureBoxLayer grabs that shell.

captureWidget and captureAndInspect reject missing, hidden, empty, near-uniform, and invalid transparent-root frames. Their readiness predicate should identify current content: model id, generation, title, child count, paint generation, or settled animation state. It must not assert that the result has the expected height or colour.

captureAndInspect saves the accepted image before it runs assertions, so a failure still has decisive evidence:

runner->captureAndInspect(
	u"gift_composer_150"_q,
	[] { return Test::ReadLiveWidget(u"gift.composer"_q).widget; },
	[](QWidget *widget) {
		return widget->property("contentGeneration").toInt() > 0;
	},
	[](QWidget *widget, const QImage &image) {
		Test::Note(u"composer image=%1x%2"_q
			.arg(image.width())
			.arg(image.height()));
		const auto content = widget->childrenRect();
		Test::Check(
			widget->rect().contains(content),
			u"composer contains its child geometry"_q,
			u"owner=%1,%2 %3x%4 children=%5,%6 %7x%8"_q
				.arg(widget->rect().x())
				.arg(widget->rect().y())
				.arg(widget->rect().width())
				.arg(widget->rect().height())
				.arg(content.x())
				.arg(content.y())
				.arg(content.width())
				.arg(content.height()));
	});

If a child grab exposes the harness background, capture the ancestor that paints the background. If a row is clipped by a scroll viewport, grab the content widget and map the row rect with CaptureMappedRect; do not treat a partial screenshot as product evidence.

For a capture whose subject is the currently visible mapping — a card, row, or child rectangle that must appear whole in the painted owner — the caller supplies that origin widget, the painted owner, and the exact origin-local rectangle to ReadMappedTarget / MappedTargetReady. Readiness is whole-rectangle containment in the owner and the relevant viewport (Ui::ElasticScroll itself, because its viewport() is Dummy and returns the inner content; QAbstractScrollArea::viewport() otherwise). It never intersects, clips, or reframes the requested rect. Origin-local containment alone is not visibility: a HistoryInner-local card rect can sit fully inside the inner widget while a negative scroll offset leaves part of it outside HistoryWidget. CaptureMappedTarget refuses that unready reading and then composes CaptureMappedRect, which still independently refuses a rect that is not fully inside the grabbed widget. A nonpainting owner whose geometry fits is still not capture-ready (PreparedWidgetCapture::prepare). PaintingLayerRoot resolves boxes inside layers specifically; it is not a history/list paint-owner resolver. Callers still own semantic navigation and exact item identity.

Sample an animating owner whose product paints can run back to back — several animations at once, especially in Debug — with PostPaintSampler (test_post_paint.h) instead of a timer-driven sampler. In Attempt 1 Run 1 of 2026/10/05/play-gram-card-read-animations-one-after-another a one-shot 16 ms base::Timer sampler of a chat list had a 52 ms gap, from grab 25 (hi 44159) to grab 26 (lo 44211), while three product paints ran back to back (frames 44159, 44173 and 44193, about 16-20 ms each in Debug), and the product's own precise timer reported arrive due=44159 late=52; the bound vstart(B2).hi - turn <= 50 spanned that gap, read 75 and stayed undecided. Hand the sampler a name for its rows, the owner whose own paintEvent paints the measured surface (a child's paints are not seen), one action — normally one Test::GrabRect of an owner-local rect — and sampleDuring(from, till) windows in crl::now() milliseconds; outside every window it is idle. A product paint received inside a window posts at most one queued sample, which runs after that paint returned; a paint arriving while one is pending shares it. Each sample is one row and one log line, NOTE: <name>: kind=post seq=<n> paint=<ms> last=<ms> covered=<k> lo=<ms> hi=<ms>: paint and last are the receipt times of the first and the last product paint it covers, covered counts them, and lo / hi are crl::now() taken by the sampler itself right before and right after the action. Read rows through mark(), taken immediately before the action under test, and samplesSince(mark), and key images by the seq the action receives, never by position. A sample's lo never precedes the end of the paint it follows, so a bound from that paint's start shorter than the paint plus one action cannot be decided with it.

grabFreeDuring(from, till) declares a window in which no sample runs. Its lead is the longest action so far, at least 40 ms: a paint received from from minus the lead until till posts nothing and joins no pending sample, a queued sample that would start there is dropped without a row and counted in dropped=, and sampling resumes with the first product paint received at or after till. The recursion guard ignores every paint received during the action: a grab delivers QEvent::Paint to the owner through QWidget::render, so a sample never posts another, while an owner that requests a repaint from every paint keeps being sampled because it keeps painting. Any other grab of the owner or of an ancestor taken inside a sampling window looks like a product paint: route it through the action or take it outside the windows. There is no timer fallback, so nothing is sampled without a product paint; a frame of an idle surface is a plain grab taken outside the windows. The owner is held weakly. When it is destroyed, a pending sample does nothing and the sampler logs one kind=stop reason=owner-destroyed row with its pending=, samples=, paints=, ignored= and dropped= counts; when the sampler is destroyed first, its queued call does nothing, and its destructor logs nothing, so read the rows before releasing it. The stop row is NOTE: <name>: kind=stop reason=owner-destroyed seq=<n> pending=<0|1> samples=<n> paints=<n> ignored=<n> dropped=<n> (PostPaintStopText). Both rows print ordinary integers until Test::RequestTelemetryNumbers() (test_log.h); a row formatted after it prints every one of its numbers as a TelemetryNumber instead, for example kind=post seq=4821p paint=37645p last=37693p covered=59p lo=61482p hi=61537p and kind=stop reason=owner-destroyed seq=4821p pending=1p samples=3764p .... A judge reads a value of either grammar with float(v.replace('p', '.')). PostPaintSampleText and PostPaintStopText also take an explicit NumberFormat, so a self-test reads both formats. details() and the empty-window note (NOTE: <name>: ignored an empty sampling|grab-free window from=<ms> till=<ms>) keep ordinary integers either way. Confirm with AppendPostPaintSamplerSelfTest.

Capture a short Ui::Animations-driven animation — a slide transition, a reveal, a button or card animation of a few hundred milliseconds — at chosen elapsed times with Test::RunClockedFrames (test_animation_clock.h), not at whatever runner tick comes next. Its value changes only on an animation-manager tick: in Run 2 of 2026/10/06/round-rich-message-edge-media-like-ordinary-media twelve 200 ms transitions sampled at the next 50 ms runner tick were already over. A ClockedRequest names its rows (name) and carries exactly one of an action or a handler with its context, strictly increasing elapsed times in milliseconds after the action, an optional animationEnd and a synchronous render. The helper starts a probe Ui::Animations::Basic of its own, runs the action inside Test::Settle with crl::now() stamped right before the action and right after its postponed calls were drained, and calls a handler's onClick(context) directly, never through ActivateClickHandler(), whose crl::on_main call would run only after the stage returned. For each time it sleeps on the main thread until then, drains the postponed calls — Manager::update() skips while the manager's own schedule() callback is pending, and the drain runs it — calls Core::App().animationManager().update() once, its only manager call, and renders. The probe proves each tick and supplies its time: a tick that did not reach it is refused as manager-did-not-tick, a render that did as render-advanced-the-manager, each a TEST_RESULT: FAIL: fixture gate: <name>: frame <k> at <ms> ms row, and that frame is not taken. An animation started inside the action or in a postponed call it posted stands, at a tick T, somewhere in the reached bounds [T - actionFinished, T - actionStarted], so judge a painted value against the transition at both bounds. A time at or after the declared end, or a frame whose upper bound reached it, is a first-class TEST_RESULT: N/A: <name>: frame <k> at <ms> ms - <reason> gate with nothing rendered, never a FAIL; a malformed request is one TEST_RESULT: FAIL: fixture gate: <name>: clocked frames refused row, and nothing runs. Each taken frame is one row, NOTE: <name>: kind=frame index=<k> requested=<ms> reached=[<lo>,<hi>] tick=<ms> waited=<ms> probeTicks=<n> renderTicks=<n> (ClockedFrameText). Every number in these rows is an ordinary integer until Test::RequestTelemetryNumbers() (test_log.h) and a TelemetryNumber after it, read once per call, so one call's rows share a format: the frame row (index=37p requested=4821p reached=[3764p,4829p] tick=37645p ...), the frame 37p at 4821p ms subject, the gate and refusal reasons (requested 4821p ms is at or after the declared animation end 3764p ms, the tick reached [5937p,6148p] ms, probe calls 59p before and after it, 7293p ms after the action started, waited 8352p ms, called 41p time(s) during the render) and the request refusal's details (exactly one of action and handler must be set (action=1p handler=1p), elapsed times must be non-negative and strictly increasing, read 4821p,3764p). A judge reads a value of either grammar with float(v.replace('p', '.')). ClockedFrameText, ClockedFrameSubject, ClockedEndGateText, ClockedReachedGateText, ClockedNoTickText, ClockedRenderTickText and ClockedRequestRefusal also take an explicit NumberFormat; a gate or refusal text kept in a ClockedFrame or ClockedRun keeps the format it was formatted in. The caller's elapsed times and animationEnd are among those numbers, so under the request they print in a format that hides digits from the secrecy scan: they must never be a fixture secret or a number read from one. Render with Test::GrabWidget / Test::GrabRect, which are QWidget::render, never with repaint() or processEvents(); keep the render cheap and save images after the run: Run 4 of the same campaign took a ~140 ms pair capture per frame, and its +100 / +160 ms ticks landed at 181 / 324 ms. Gate anim::Disabled(), which makes the first tick jump to the end, and scale times by anim::SlowMultiplier(). Choose by the subject: when the product's own paint sequence is measured — back-to-back paints, a bound from a paint — use PostPaintSampler, which samples only after real product paints and so needs a shown, exposed, painting window (Test::KeepWindowExposed keeps one that another application covers exposed without activating anything); when a Ui::Animations value at a known elapsed time is measured, use the clock, which awaits no paint and so runs the same in a minimized window or on a never-shown widget. Neither advances a streamed video player, which paces itself with its own base::Timer (see the failure row for an inline video or GIF that stays on one frame). Confirm with AppendAnimationClockSelfTest, which clocks a 200 ms linear bar started from a click handler on a never-shown child of the primary window.

Compare a rounded corner of a media or bubble with a twin or a reference through test_corner_patch.h, not through pixels read by hand. Each reading takes a CornerPatch: a device-pixel QImage cropped at a named PatchCorner inside the media's bounding box, about Ui::BubbleRadiusLarge() + 2 logical px square so that its corner-most pixel lies beyond the arc and its opposite corner inside the media, and a media colour sampled inside the media away from the arc and from any chrome; a coverage map assumes that colour is locally uniform near the arc. ReadCornerBackground reads the background at the patch's own corner-most pixel (CornerMostPixel) and keeps only the 4-connected component of pixels exactly that colour, so a background-coloured glyph the media encloses is never background. ReadCornerCoverage maps each pixel's media share on the channel where media and background contrast most. CompareCornerCoverage compares two patches of one corner by those maps, both read against the reference's background, so capture both twins over one base: twins whose media colours differ but whose arcs match compare equal, while another radius, a square corner or a one-pixel arc shift does not, and the worst pixel, its delta and the count over the tolerance are printed on both verdicts. Pass kCoverageSameKind (0.03) within one media kind and kCoverageCrossKind (0.06) across kinds. ReadCornerContainment checks that every background pixel of the reference stays background in the subject; a second reference with another media colour widens the mask by the zero-coverage pixels both share that stay connected to the corner, such as a shadow. Chrome — a corner-status badge, an enlarge badge — is left out only through GateCornerChrome, gated on a twin or reference that does not draw it: the gate refuses a region touching an arc or background pixel there, and the compare and the containment refuse a chrome the gate did not accept instead of applying it. A reading that cannot be made is refused by name, its predicate is false and every value it did not measure prints none: ReadCornerBackground refuses empty-patch, media-missing, media-at-corner and background-reaches-inner-corner; ReadCornerCoverage empty-patch, background-missing, media-missing and contrast-too-low; GateCornerChrome region-empty, region-outside-patch, gate-background, gate-coverage, chrome-touches-arc and chrome-touches-background; CompareCornerCoverage corner-mismatch, size-mismatch, reference, chrome-refused, chrome-mismatch, subject and subject-background-differs; ReadCornerContainment size-mismatch, second-reference-size-mismatch, reference, chrome-refused, chrome-mismatch and empty-mask. The hand-written readings it replaces produced false FAILs in Runs 1 and 5 of 2026/10/06/round-rich-message-edge-media-like-ordinary-media (the three corner-patch rows under Failure diagnosis). Confirm with AppendCornerPatchSelfTest, over synthetic patches rounded with the product's own Images::CornersMask at both bubble radii.

Measurements cross three coordinate systems, and each has a fixed contract. CaptureWidget / CaptureRect / CaptureMappedRect take widget LOGICAL rects; the image they produce — like every Ui::GrabWidgetToImage grab — holds logical-size × style::DevicePixelRatio() DEVICE pixels, so Crop and any direct QImage sampling index device space. Multiply logical values by the image's devicePixelRatio() exactly once, at the sampling boundary. TDESKTOP_TEST_SCALE is a third, independent multiplier: it scales the logical style metrics themselves, so derive expected geometry from the live scaled tokens, never from literals recorded at 100%. Rects from different widgets never share an origin by accident — map both through one declared frame (Ui::MapFrom, mapToGlobal) before comparing, cropping, or clicking.

Helper catalog

Module Facilities
test_agent.h Runtime gate, startup scale override, the macOS App Nap hold (ApplyStartupOverrides() begins one NSActivityUserInitiatedAllowingIdleSystemSleep | NSActivityLatencyCritical NSProcessInfo activity early in Application::run(), before Test::Start(), and holds it until the process exits, logging one NOTE: TDESKTOP_TEST_APP_NAP=[<value>] applied: hold=<active|off> source=<default|environment> - <reason> row; TDESKTOP_TEST_APP_NAP=allow leaves it off and any other value is rejected; nothing off macOS), sticky named events, scenario start, account fixture secrets (TwoStepPassword() reads 2svpassword.txt, GramAccount() reads test_gram_account.txt, RewriteGramAccountWords() rewrites the live and golden copies at the rotated wallet's address after a confirmed rotation, StageGramAccountLiveWords() stages a proper prefix of the live copy's own words for a self-test and RestoreGramAccountLiveWords() lengthens such a staged copy back to the words it was cut from, neither touching the golden sibling; GramAccountLivePath() / GramAccountGoldenPath() name the two copies for metadata-only checks).
test_runner.h Stages, bounded waits, exact-widget actions, prepared capture/inspection (captureAndInspect takes an optional trailing skipReason for its stage), first-class gated skips (skipReason), onFinish release hook (and its finish-release self-test), watchdog (TDESKTOP_TEST_WATCHDOG in seconds) and termination.
test_gated_stage.h The first-class gated skip's own self-test: a stage whose skipReason returns a reason, writing one TEST_RESULT: N/A: row and skipping run, until and then without waiting - its never-ready until under a one-second timeout is the falsifier - beside a stage whose gate returns an empty string and runs normally in the tick that begins it.
test_log.h Absolute flushed logs, steps, notes, checks whose details are printed on the passing verdict as well as the failing one, tolerances, geometry, completion markers, N/A rows for stages that did not apply, and their count. One LogRaw call always writes exactly one physical line, whatever it is handed: every character Python's str.splitlines() breaks on - U+000A, U+000B, U+000C, U+000D, U+001C, U+001D, U+001E, U+0085, U+2028, U+2029, and so a CRLF pair as its two code points - is written as a visible \uXXXX escape, so a record carrying a break stays one row the external readers' line grammar reads whole and cannot mistake for a completion, while text with no separator is passed through byte for byte and the escape adds no trailing whitespace. LogGeometry(name, rect) writes GEOMETRY: <name>: x=<x> y=<y> w=<w> h=<h> - so do CaptureWidget, CaptureRect, CaptureViaWindow, CaptureToastSubtree and a saved PreparedWidgetCapture - and CheckNear(actual, expected, tolerance, what) writes TEST_RESULT: PASS: <what> (actual <a>, expected <e> ±<t>), or the same FAIL row with - out of tolerance appended. Their numbers are ordinary integers (x=-4821 y=3764 w=5937 h=6148, actual 4826, expected 4821 ±37), and TelemetryNumbers in a row formatted after RequestTelemetryNumbers() (x=-4821p y=3764p w=5937p h=6148p, actual 4826p, expected 4821p ±37p); a judge reads a value of either format with float(v.replace('p', '.')). GeometryText (the whole GEOMETRY: row) and CheckNearText (the check text without its verdict prefix) are the pure formatters, taking an explicit NumberFormat. CheckNear's actual, expected and tolerance are the caller's numbers and print in the requested format too, which hides digits from the secrecy scan, so they must never be a fixture secret or a number read from one.
test_log.h (TelemetryNumber, RequestTelemetryNumbers) The number format for telemetry printed into a scanned log, from 2026/10/08/give-overlay-telemetry-a-number-format-the-secrecy-scan-cannot-count, and the one request that makes the shared helpers print their own numbers in it. TelemetryNumber(value, decimals) prints - for a value below zero, the integer digits, p and exactly decimals fraction digits (decimals defaults to 0, and a negative one counts as 0), rounding as QString::number(value, 'f', decimals) does, and nan, inf or -inf for a non-finite value: 42p, 16p31, -4821p75. Every digit run touches the p, so under the scan's bounded rule no digit run of a formatted number is ever counted. The grammar is an optional -, digits, p and optional digits, or nan, inf or -inf; a judge script reads a value with float(v.replace('p', '.')), and a judge that matches a value with a regular expression takes the p and the fraction digits too (-?\d+(p\d*)?), since -?\d+ alone silently stops at the p. Format only numbers an overlay, or a converted helper under the request, measured or computed, never a fixture secret or a number read from one, and choose decimals per quantity, never from a secret. A TelemetryNumber is a word to the phrase matcher - it ends a word run where a bare number is neutral - so never print one as a list index or between words copied from product or fixture text; keep such text out of telemetry rows. RequestTelemetryNumbers() is one explicit, process-wide, one-way and idempotent ask that the converted shared helpers print the numbers they themselves measure, count or time as TelemetryNumbers. It takes no argument and reads nothing, so it cannot depend on a secret: a campaign calls it unconditionally at its first stage, before any row it may scan, never under a condition and never from code that read a fixture secret. The first call logs exactly TELEMETRY_NUMBERS: requested; a later call does nothing and logs nothing, and there is no undo. HelperNumberFormat() answers NumberFormat::Ordinary until then and NumberFormat::Telemetry after it. A converted helper formats each row through a pure formatter taking a NumberFormat, which its logging path calls with HelperNumberFormat(), and prints each number through HelperNumber(value, format) (QString::number(value) or TelemetryNumber(double(value))) or, in an .arg chain that already substituted caller text, ArgNumber(text, value, format), which under Ordinary is the same integer QString::arg call as before, so a row printed without the request stays byte-identical for any caller text. Every converted quantity is an integer - pixels, counts, ordinals, 0/1 flags, crl::now() milliseconds - and prints with decimals 0: 4821p, -12p, 0p. The converted rows: GEOMETRY: (LogGeometry); the CheckNear verdict; the PostPaintSampler kind=post and kind=stop rows; the RunClockedFrames kind=frame row, its frame <k> at <ms> ms subject, its gate and refusal reasons and its request refusal; the Probe window bounds, checkCountSince's counts and the round-trip tallies and times; and the DiscriminatingScan examined= tally row and its refusal, the secrecy scan's SECRECY: <class> reports among them. Their grammars in both formats are in the rows of test_log.h, test_probe.h, test_post_paint.h and test_animation_clock.h and under Capture and visual assertions. Not converted, ordinary in both modes: caller text and detail lists (a probe row, a check's what, a scan's subject or control name and its details, a self-test's own numbers), PostPaintSampler::details(), the sampler's empty-window note, the scan's SECRECY_* rows and its three check rows, test_runner's rows, and every other module, such as test_capture's own readings, test_ink, test_corner_patch or test_wallet_intercept. In every converted row a helper-written word sits on at least one side of each converted number, so a number never splits a word run made only of caller text. A judge splits test_log.txt at the first line equal to TELEMETRY_NUMBERS: requested: a converted row before it holds ordinary integers and one after it TelemetryNumbers, and a log without that line never asked. A text a helper formatted before the ask and a caller prints after it, such as a kept RoundTrip::observation or ClockedRun::refusal, stays ordinary; asking at the first stage avoids that. Its proofs are two stages of AppendSecrecyScanSelfTest: secrecy_self_telemetry for the format, and secrecy_self_helper_numbers for the converted rows, which reads each of them through the helper's own formatter for fixed synthetic values - without the request it prints today's text, and under it the same text with only a p after each number; the ordinary text's digit runs, as synthetic short secrets, fail the scan at that row, while the requested row holds none bounded and scans clean; and a synthetic phrase in its caller text gives the same word runs in both formats - and never asks the request itself.
test_log_lines.h The one-physical-line guarantee's own self-test: one LogRaw-family call driven with each of the eleven separator forms in turn, with one payload mixing them all, with a trailing separator and with a separator-only payload, each read back out of test_log.txt by byte offset and asserted to have added exactly one physical line under an independent transcription of str.splitlines() - beside a separator-free control that must read one line under any writer, and a last stage whose payload's middle line would be byte-equal to TEST_COMPLETE and which asserts no produced line is.
test_text_reads.h Space-class-normalizing text comparison: Test::NormalizeSpaces maps U+00A0 and U+202F to U+0020 and changes nothing else, Test::CheckTextReads compares a read-back against an expectation through it and prints both raw strings and their whitespace code points on either verdict; with its own self-test, in which a real Ui::FlatLabel's narrow-no-break-space read-back is accepted beside two deliberate FAIL negative controls - a different minute and a different day - that keep the check from becoming permissive.
test_lang_pack.h Install synthetic key/value overrides into the RUNNING language pack and take them back down symmetrically: Test::InstallLangPack freezes the pack identity, the serialize() snapshot and one LangFrozenValue per key before applying one Instance::applyDifference, whose two Expects it holds by construction, and LangPackFixture::remove() restores through switchToId + fillFromSerialized and then one EMPTY difference purely to deliver Lang::Updated(), so a key that was default before the install comes back and an already-painted reactive label re-reads with no interaction. frozen(key) is the only value-returning read, so an expectation taken after the install — the one that passes vacuously — is not expressible through it; checkRestored compares live against frozen on both the value and the non-default axis and prints every reading on the passing verdict. checkInstalled certifies the caller's requested override: with no placeholder the expectation is that text and the reading is getValue; with a placeholder the expectation is the QString Instance::ParseStrings returns for that requested text — the encoding applyValue stores — and the reading is still getValue, so getNonDefaultValue stays only the non-empty axis. A placeholder ParseStrings omits (unknown, not accepted by that key, repeated, or malformed) is not certified: checkInstalled FAILs by name and the verdict quotes the raw non-default value and whether getValue stayed at the frozen reading. A #custom pack, an empty override list, a key GetKeyIndex answers kKeysCount for (a plural phrase's base name included), an empty override value, a frozen() miss, an oracle on a fixture that never installed and an out-of-order remove() are seven named refusals rather than silences. With its own arm-selectable self-test, whose two LangRestoreFault arms deliberately leave one key installed or suppress the notification, and whose placeholder arm on lng_dlg_search_from shows a {user} install PASS and a deliberate {amount} install FAIL.
test_probe.h Append-only observation records read only through a declared window, each carrying the time it was recorded; keyed issue/answer rows correlated into one round trip by key rather than by list position, refusing every reading it cannot positively pair; and scans that must match a control before a zero counts as absence. record(row) logs NOTE: <name>: <row> and recordIssue / recordAnswer log NOTE: <name>: <row> role=issue|answer key=<key>: caller text only, holding no helper number. Every check's details are probe=<name> window=[<from>,<till>) rows=<rows> (ProbeWindowText: the window's rows joined by ; , or <none>); checkCountSince's are expected=<n> actual=<n> <window> (ProbeCountText); and checkRoundTripSince prints the round trip's observation, key=<key> state=<state> issues=<n> answers=<n> dropped=<n> preIssues=<n> preAnswers=<n> issueAtMs=<ms> answerAtMs=<ms> roundTripMs=<ms> discarded=<rows> <window> (RoundTripText, which roundTripSince builds into RoundTrip::observation). DiscriminatingScan::report() writes NOTE: <name>: examined=<n> subject(<subjectWhat>)=<n> control(<controlWhat>)=<n> (tallyText), then the caller's details as NOTE: <name> subjects: ... and NOTE: <name> controls: ..., then the check <name> discriminates, whose FAIL details are the walk matched no <controlWhat>, so its <subjectWhat> count of <n> over <n> examined items cannot tell absence from an enumeration that never reaches the subject (refusalText). The numbers the helper itself counts or times there - window bounds, counts, tallies and times - are ordinary integers (window=[4821,5937), expected=6148 actual=6148, issues=41 ... roundTripMs=48, examined=4821), and TelemetryNumbers in a text formatted after Test::RequestTelemetryNumbers() (window=[4821p,5937p), expected=6148p actual=6148p, issues=41p ... roundTripMs=48p, examined=4821p); a judge reads a value of either format with float(v.replace('p', '.')). ProbeWindowText, ProbeCountText, RoundTripText, tallyText and refusalText are pure and take an explicit NumberFormat. checkCountSince's expected is the caller's number and prints in the requested format too, so it must never be a fixture secret or a number read from one. Caller text - a row, a key, a probe or scan name, a subject or control name, a detail - prints exactly as given, and a timedRowsSince time or a RoundTrip field that a caller prints itself is the caller's number to format.
test_widgets.h Safe typed discovery, live object/action publication, input, postponed-call settlement, QPA-injected window activation, wheel delivery along Qt's parent ladder with a named WheelDelivery refusal, and clipboard-free text drop into a Ui::InputField with a named TextDrop refusal.
test_wheel.h The wheel helper's own self-test: a covering Ui::AbstractButton inside a Ui::ScrollArea whose subject wheel moves the area and names the viewport, beside the same wheel aimed at the viewport (today's working target), a wheel aimed at the QAbstractScrollArea itself that is not certified delivered, and a parentless-window button that refuses by name.
test_text_drop.h The text-drop helper's own self-test: a real Ui::InputField inside a parentless, never-shown container that accepts drops and counts every drag event, where the field's mime hook taking the drop receives exactly the text and leaving it makes the field hold exactly the text, beside three refusal controls - acceptDrops off and a disabled field (viewport-refuses-drops), a read-only editor under the container (enter-not-accepted, preceded by a raw-route premise showing the same enter and drop landing in the container) and an editor made read-only on the drop (drop-not-accepted) - each leg reading the container's counters, QDragManager's current target by pointer and the QClipboard::changed count within the call, never the clipboard itself.
test_clipboard.h The retried clipboard round trip: AppendClipboardRoundTrip(runner, { label, accessor, delay, attempts }) appends one stage, clipboard round trip: <label>, that writes a sentinel harness-clipboard:<label>:<serial>:a<k> and reads it back, up to kClipboardRoundTripAttempts (10) times kClipboardRoundTripDelay (300 ms) apart: attempt 1 in the stage's run, every later one from a Qt::PreciseTimer base::Timer across event-loop turns, never a main-thread sleep, and a pure until on finished. Each attempt is one CLIPBOARD_ROUND_TRIP_ATTEMPT: label=<label> ... row (read=sentinel, empty, stale-sentinel:<held> or foreign, then owns, yielded, atMs, writeMs), so keep the label one token without spaces, and the stage's then Notes the CLIPBOARD_ROUND_TRIP summary. The returned ClipboardRoundTrip, read from a later stage, carries succeededAt or a refusal naming what the last read held - read-empty, read-stale-sentinel, read-foreign - or invalid-bound (fewer than one attempt or a negative delay; nothing written). A read-* refusal's reason also ends with the console-lock reading taken at refusal, ; console at refusal: CONSOLE_LOCK: ... (Test::ReadConsoleLock(), test_console_lock.h) - the host console's state, printed for every accessor, fakes included - so a campaign's own refusal names a locked Windows console with no wiring; a success and invalid-bound carry none. A read is only classified, inside a scope its text never leaves: every sentinel is registered process-wide before it is written and only those are ever printed, foreign text is never stored, printed, measured or hashed, a sentinel from an earlier process counts as foreign, and the owner's clipboard is overwritten and never saved or restored. ReadClipboard(expected, accessor) is a one-shot reading for a product copy, deliberately not a wait: expected, empty, harness-sentinel (naming it) or foreign, with owns (ownsClipboard(), meaningful on Windows, macOS and X11) and the platform name; ClipboardReadingText prints neither the expected nor the read text. On Windows a read while the process owns the clipboard is served in process, so a success there proves the set took, not an OS-level read. ClipboardAccessor (write, read, optional owns) is the injection seam, and SystemClipboardAccessor() the default. Its own self-test, AppendClipboardRoundTripSelfTest, counts rows only from a mark it takes in the test log: an accessor failing three times reads back at attempt 4 with exactly 4 rows; an always-empty one with the shipped defaults is refused read-empty after exactly 10 logged attempts; a stale sentinel is refused read-stale-sentinel naming it, and an injected foreign text read-foreign; ReadClipboard classifies the same fakes; the always-empty refusal carries the console reading and the three-failure success none; one live round trip runs against the system clipboard, and is TEST_RESULT: N/A by name - the lock, the probe row, the refusal and its attempts - instead of failing when Test::ReadConsoleLock() reads a locked Windows console as its judging stage begins and it did not read back, while a judging stage that runs Notes the CONSOLE_LOCK row its gate read; and a Test::DiscriminatingScan finds the foreign text in no line the self-test appended, with the foreign arm's own attempt rows as its control.
test_console_lock.h Whether the console session this process runs in is locked, read at the moment of the call and never cached, on Windows and macOS: Test::ReadConsoleLock(). On Windows it reads the SessionFlags of WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, <own session>, WTSSessionInfoEx), the own session taken from ProcessIdToSessionId - the reading Qt's own clipboard takes before it stops retrying OleSetClipboard (QWindowsContext::isSessionLocked(), which reads the active console session instead). WTS_SESSIONSTATE_LOCK reads locked and WTS_SESSIONSTATE_UNLOCK unlocked; any other value, a failed call, a short answer, a level other than 1, and Windows 7 / Server 2008 R2, which report the two flags reversed, read unknown. On macOS it reads a fresh CGSessionCopyCurrentDictionary() per call, released before it returns, and two booleans in it: the undocumented CGSSessionScreenIsLocked - true while the session's lock screen is up, measured absent on an unlocked console on macOS 27 - and kCGSessionOnConsoleKey (the key kCGSSessionOnConsoleKey). In this order: a null dictionary (no window-server session) reads unknown; a CGSSessionScreenIsLocked that is true reads locked, and one that is not a boolean unknown; kCGSessionOnConsoleKey true, with the lock key absent or false, reads unlocked; anything else - a session off the console (fast user switching), whose lock bit does not say whether its screen is shown - reads unknown. So it also sees a console locked before the launch; test-run's screen_locked reads the same two keys out of process through ioreg's IOConsoleUsers, the independent control. Elsewhere it reads not-applicable with source none, and locked is reachable only in the Windows and macOS branches. ConsoleLockText prints the one CONSOLE_LOCK: state=<locked|unlocked|unknown|not-applicable> source=<call> <raw> - <reason> row, naming that source (CGSessionCopyCurrentDictionary().CGSSessionScreenIsLocked on macOS) and its raw values - on Windows session, console session, bytes, level, reported session, session state, flags in hex; on macOS kCGSSessionOnConsoleKey=<1|0|absent|not-boolean> CGSSessionScreenIsLocked=<1|0|absent|not-boolean>, none for a null dictionary - never the user, domain or station name the Windows answer carries, nor the user's short and long names, UID, GID, session UUID or audit and security session ids the macOS dictionary carries. ConsoleLockGate answers locked console: <row> for a locked reading in a Windows build only (Platform::IsWindows() && reading.locked()) and an empty string for every other reading and on every other platform, so it can be (part of) a stage's skipReason; unknown and not-applicable never gate. So the gates keyed on it stay Windows-only - AppendClipboardRoundTripSelfTest's live arm, AppendPopupMenuCaptureSelfTest's across-turn stages and every campaign leg gated the same way - because those are the legs a locked Windows console breaks: no process on its desktop can open the clipboard, and Qt closes a shown popup on ApplicationDeactivate. A locked macOS console did not close the popup: a packed launch with CGSSessionScreenIsLocked true ran AppendPopupMenuCaptureSelfTest with 0 FAIL (2026/10/06/add-a-post-paint-capture-sampler-to-the-harness, work/test.md, Test 2). On macOS the reading is printed in those places and gates nothing; a check that a locked macOS console does break gates on locked() itself, as AppendKeepWindowExposedSelfTest does. Not Core::App().screenIsLocked(), which is not consulted on any platform: a product flag set only by notifications that arrive after the launch (WM_WTSSESSION_CHANGE, the com.apple.screenIsLocked / com.apple.screenIsUnlocked observers, the xdg-desktop-portal Inhibit monitor's screensaver-active), so it reads false on a console already locked at launch; not the input desktop's name (OpenInputDesktop named it Default on a locked Windows 11 console); not an OpenClipboard attempt (the measurement a clipboard check makes). Read by AppendClipboardRoundTrip's read-* refusal, by the gates of AppendClipboardRoundTripSelfTest's live arm and AppendPopupMenuCaptureSelfTest's across-turn stages, by AppendKeepWindowExposedSelfTest's lock N/A and its control's screenLocked=, and into the printed screenLocked of ReadMainWindowMarking and of the animation-clock window reading (test_animation_clock.h); a campaign gates its own clipboard or popup-capture legs on it the same way (its judging stage's skipReason, CapturePopupMenu's skipReason), Windows-only through the same gate. It has no self-test of its own: on Windows those two self-tests' gated and ungated rows, and on macOS the window-exposure lock N/A and the marking-read and animation-clock screenLocked= rows, each beside an independent reading of the console, confirm it.
test_capture.h In-process grabs, paint-root validation, mapped rects, complete-target mapped readiness (ReadMappedTarget / MappedTargetReady / CaptureMappedTarget / PreparedWidgetCapture::prepare(owner, origin, rect)), blank detection, painting-layer-root resolution for boxes inside a layer, CaptureBoxLayer for the whole Ui::BoxLayerWidget (title + addButton footer row, which setTitle / addButton parent onto the shell) versus CaptureInLayerRoot for a frame cropped to the box content, crops, zoom, contact sheets, window-mapped capture for widgets that paint no opaque background of their own; a toast goes to test_toast_capture.h instead, whose frame cannot compose the surface beneath it.
test_mapped_target.h Complete-target mapped readiness measuring itself: a painted owner taller than its Ui::ElasticScroll, a child rectangle that fits the owner while remaining partly outside the scroll, then the same origin/owner/local rect capturable after ordinary scrollToY, plus a sibling Ui::ScrollArea whose clipper is viewport(), overlap-only / empty / hidden refusals, a nonpainting root that geometry would otherwise accept, and PaintingLayerRoot unresolved on the fixture.
test_post_paint.h PostPaintSampler: samples one painted owner right after each of its product paints instead of on a timer. A base::install_event_filter on the owner sees each QEvent::Paint; a product paint inside a sampleDuring window posts at most one queued sample that runs after the paint returned, and later paints share a pending one (covered); a recursion guard ignores the paints the sampler's own action causes; grabFreeDuring windows, with a lead of the longest action so far and at least 40 ms, take no sample and count the queued ones they drop (dropped=); the owner is held weakly and its destruction logs one kind=stop reason=owner-destroyed row; rows are read through mark() / samplesSince() and printed by PostPaintSampleText. Both rows follow Test::RequestTelemetryNumbers() (test_log.h): PostPaintSampleText and PostPaintStopText (over a PostPaintStop) also take an explicit NumberFormat, and a row formatted after the ask prints every number as a TelemetryNumber, while details() and the empty-window note keep ordinary integers. With its own self-test on a harness-owned opaque Ui::RpWidget whose every paint costs 18 ms, needing only a shown, non-minimized primary window, which its fixture stage keeps exposed with Test::KeepWindowExposed until its last stage (a minimized or hidden window still fails the gate once and every later stage is N/A): a timer-only control at a 16 ms cadence over 1500 ms printing its largest gap, missed paints, lateness, the paints started while a tick was overdue and the lateness of ticks with no paint inside, as context; a 1500 ms stressed window of at least 20 back-to-back paints in which every product paint is covered by a sample taken after it returned, within one product paint plus one grab; one product paint yielding exactly one sample and none over an idle window of at least 500 ms although a grab painted the widget; a grab-free window [from + 400, from + 700) holding at least 3 product paints and no sample, with the first paint after it sampled; and the owner destroyed with a sample pending, after which no action runs, the stop is logged and the next stage runs.
test_animation_clock.h RunClockedFrames: captures a short Ui::Animations-driven animation at chosen elapsed times after one synchronous action, or a ClickHandler invoked directly with its context, never through ActivateClickHandler(). The action runs inside Test::Settle with crl::now() stamped right before the action and right after its postponed calls were drained (actionStarted / actionFinished); then, for each strictly increasing requested time, the main thread sleeps in 1 ms steps until that many milliseconds after actionStarted, the postponed calls are drained, Core::App().animationManager().update() runs once - the helper's only manager call - proven by a probe Ui::Animations::Basic of the helper's own, whose received time is the tick, and a synchronous caller render follows. Each frame reports its reached elapsed bounds [tick - actionFinished, tick - actionStarted]. A requested time at or after the declared animationEnd, or a tick whose upper bound reached it, is a first-class TEST_RESULT: N/A gate with nothing rendered; a malformed request (clocked frames refused), a tick that did not call the probe (manager-did-not-tick) and a render that did (render-advanced-the-manager) are named fixture gate: FAIL rows, and the loop goes on. ClockedRequest, ClockedFrame (taken()) and ClockedRun carry the request, each frame and the run; ClockedFrameText prints a frame, and every taken frame is one NOTE: <name>: kind=frame ... row. Its frame, gate and refusal rows follow Test::RequestTelemetryNumbers() (test_log.h), read once per call, through ClockedFrameText, ClockedFrameSubject, ClockedEndGateText, ClockedReachedGateText, ClockedNoTickText, ClockedRenderTickText and ClockedRequestRefusal, each also taking an explicit NumberFormat, and the caller's elapsed and animationEnd print among them, so they must never be a fixture secret or a number read from one. With its own self-test needing only the primary window, as the parent of a never-shown opaque bar widget, and no session, chats or account: a 200 ms anim::linear Ui::Animations::Simple (scaled by anim::SlowMultiplier()) started by a LambdaClickHandler is clocked at 25, 50, 75 and 125 % of its duration in one synchronous stage; the handler runs once inside the action window, each in-progress value read from the rendered pixels lies within the transition at its reached bounds (within 1.5 device pixels), the three values strictly increase, the fourth time is the helper's N/A row with no image, and the bar stays hidden and not visible with no paint outside the renders and one render paint per taken frame, with the bar's and the window's state (minimized, hidden, exposed, and screenLocked from Test::ReadConsoleLock().locked()) printed beside the frames; the window itself is read, never changed. Two stages, eight checks plus that N/A row, and an animation_clock_frames contact sheet. Its Simple lives in the bar's lifetime(), so it needs no Runner::onFinish.
test_corner_patch.h Rounded-corner parity readings over device-pixel corner patches, each a CornerPatch (the image, its PatchCorner, and a media colour sampled away from the arc and chrome). ReadCornerBackground reads the background at the patch's own corner-most pixel (CornerMostPixel), restricted to the 4-connected component of that exact colour, and refuses a corner-most pixel within kCoverageMinContrast (24) of the media (media-at-corner) or a component that reaches the opposite corner (background-reaches-inner-corner); ReadCornerCoverage maps each pixel's media share on the max-contrast channel, unclamped, and refuses contrast-too-low; CompareCornerCoverage compares a subject with a reference of the same corner, both against the reference's background, at a caller tolerance (kCoverageSameKind 0.03, kCoverageCrossKind 0.06), classifies the subject's corner-most pixel as background or media, and prints maxDelta with its pixel and both map values, over/total and excluded on both verdicts; ReadCornerContainment counts the reference-background pixels that changed in the subject, its mask widened by an optional second reference's shared zero-coverage pixels connected to the corner; GateCornerChrome accepts a chrome region only when every pixel of it is full media on a gate patch without the chrome (chrome-touches-arc, chrome-touches-background otherwise), and the compare and containment refuse a chrome that was not accepted (chrome-refused) or was gated on another corner or size (chrome-mismatch). Every reading has a named refusal set and a formatter (CornerBackgroundText, CornerCoverageText, CornerCoverageCompareText, CornerContainmentText, CornerChromeText) that prints none for a value it did not measure. Own self-test over synthetic patches painted with Images::Round and Images::CornersMask at Ui::BubbleRadiusLarge() and Ui::BubbleRadiusSmall(), needing no window, session or account: a Large and a Small arc (N/A when the use-small-msg-bubble-radius option makes the radii equal), a square and a rounded corner, and an arc shifted by one device pixel told apart; colour twins equal by coverage with their worst pixel printed; all four corners read at their own corner-most pixel; media-at-corner and contrast-too-low refusals; an enclosed background-coloured glyph kept out of the background and ignored by containment, which still catches a media pixel beyond the arc; a chrome region touching the arc refused by its gate, a clear one accepted and left out of a compare that differs without it; and a second reference widening the mask by a shared shadow to catch a changed shadow pixel. Nine stages, eighteen checks, a corner_patch_self_test contact sheet.
test_marking_read.h Keep the primary window from marking messages read on an unlocked console. ReadMainWindowMarking / MarkingReadDetails carry markingAsRead, isActive, isMinimized, isHidden, exposed and screenLocked - Test::ReadConsoleLock().locked() (test_console_lock.h), read at each call on every platform, so a console locked before the launch reads 1 on Windows and macOS and every other platform reads 0 - plus the WindowActivation reading, and a refusal. KeepMainWindowNotMarkingRead sets Qt::WindowMinimized (not Controller::minimize(), which tray-hides in WorkMode::TrayOnly, and not QWidget::hide(), which makes CaptureWidget refuse the window) and refuses by name if that lever is not holding within kNotMarkingReadBound. A null controller refuses without searching for another window. The self-test covers a true control, the helper, Controller::activate() undoing the lever, a second call, a non-blank PreparedWidgetCapture of a main-window widget while the lever holds, the null refusal, and showing the window again. From its control until its restore stage it keeps the primary window exposed with Test::KeepWindowExposed, so a window another application covers - the usual state after a default test-run launch - still reads exposed, and its Controller::activate() stages request activation of the client on macOS, which the window server may grant or refuse after a background test-run launch; the self-test passes either way (2026/10/08/launch-the-testagent-client-without-activating-it, Runs 4, 8 and 9). The lock never ends the control's wait on its own: the control waits for markingAsRead true, and only a control still false at kNotMarkingReadBound on an unexposed window or on a console Test::ReadConsoleLock() reads locked ends it, which makes the deciding half a named N/A whose reason names the lock first (the console is locked) - never a pass; an exposed window on an unlocked console that never reads it true runs to the stage timeout. A locked console whose control reads true runs and decides the deciding half: two packed launches on a locked macOS console read 8 PASS (2026/10/04/add-a-local-restored-notify-settings-override-to-the-harness and 2026/10/06/add-a-post-paint-capture-sampler-to-the-harness, work/test.md).
test_window_exposure.h Keep a top-level window that another application covers exposed, without activating anything. ReadWindowExposure(widget) is the pure reading of widget->window()'s QWindow, legal in an until: the target's identity and frame, exposed (isExposed()), active (isActive()), stayOnTop (Qt::WindowStaysOnTopHint in its flags), applied, the application state, the focus window, and a refusal. KeepWindowExposed(widget) returns that reading taken before its call - so exposed, active and stayOnTop are the values before it - and on macOS then sets Qt::WindowStaysOnTopHint on the QWindow (NSModalPanelWindowLevel; no ordering, key-window or activation call, the same call Calls::Window::setPinnedOnTop makes), with applied=1 only when this call set the hint, so a window that already carries it gets nothing applied. It never waits: call it, wait for isExposed() in a pure until, then ask for the repaint you need - a re-exposed window with nothing dirty is only flushed, not painted. Named refusals, returned and never logged, in order: null-widget, minimized (stacking cannot expose a minimized window, so the helper and KeepMainWindowNotMarkingRead exclude each other) and hidden (no QWindow handle - never retargeted at another window -, isHidden(), or a QWindow that is not visible, such as under Qt::WA_DontShowOnScreen). RestoreWindowExposure(reading) clears the hint only when that reading applied it, so it leaves a product's own hint alone; it is idempotent and safe after the window is gone. WindowExposureText prints one window exposure: target=... exposed= active= stayOnTop= applied= stacking= appState= focusWindow= row, with - <refusal> when refused. Off macOS the helper is a documented no-op that still returns its reading (applied=0 stacking=none): Windows and X11 never turn a covered window unexposed and the hint would restyle or recreate the native frame there, and Wayland gives clients no stacking control. Exposing the primary window can make it mark messages read (see the failure row). Own self-test, AppendKeepWindowExposedSelfTest, needing no session, network or account, over two legs - an ordinary parentless top-level window and a Ui::SeparatePanel with the hint cleared before show - each shown without activation (Qt::WA_ShowWithoutActivating before the native window exists, never raise() or showAndActivate()) under a harness-owned opaque titled cover: showing them changes neither the focus window nor the application state; a settle stage puts the cover up only once the target has been shown for 2000 ms, because a freshly shown window gets one more layer display about half a second after it is shown and Qt counts any layer display as an expose, so a target covered sooner reads isExposed() true while the window server still reports it occluded; the control reads the target unexposed under the cover within 3000 ms or writes a named N/A, never a pass, and the leg's deciding stages are N/A with it; the premise shows the covered target, which nothing asks to repaint, staying unexposed with no paint for 500 ms; the helper exposes it within 3000 ms with no repaint requested, isActive(), the focus window and the application state unchanged, and the widget still shown with its own flags unchanged; only then does the self-test ask for one update(), which must paint within 3000 ms; the undo - a second call applies nothing and its undo leaves the hint set, the first reading's undo clears it and activates nothing; the teardown destroys the target and the cover (before=2 after=0), and Runner::onFinish releases them on every path; and a refusal stage that returns null-widget, hidden and minimized without a logged failure. Off macOS, or on a console Test::ReadConsoleLock() reads locked - on macOS the session's CGSSessionScreenIsLocked, so a console locked before the launch too, where a window shown behind the lock screen keeps a stale isExposed() - every leg stage is a named N/A; the lock's N/A ends with the CONSOLE_LOCK row it read (the console is locked, so occlusion and exposure cannot be decided: CONSOLE_LOCK: state=locked ...), and the control's screenLocked= is the same reading. After a default background test-run launch a target that another application's window covers from the moment it is shown cannot be turned unexposed by the harness cover (see the exposure failure row), so that leg is the named N/A, never a pass.
test_layer_root.h The painting-layer-root resolver's own self-test: a plain Ui::GenericBox accepted as its own render root beside one that cleared Qt::WA_OpaquePaintEvent, refused and then captured through its Ui::BoxLayerWidget; CaptureBoxLayer saves the whole shell beside CaptureInLayerRoot's content crop, and MisframedDetails is quoted on a rect outside the layer.
test_via_window.h The window-mapped capture's own self-test: a real Ui::Toast whose bare prepared grab is refused in the turn it is created, beside the window-cropped frame of the same rect that the harness accepts, and a flat fixture region proving a blank frame is a Note and never a FAIL; a sixth stage builds an offscreen Qt::WA_DontShowOnScreen top level, reads it, destroys it inside the same .run, and shows the retained reading answering resolved() false with its identity, mapped rect and refusal intact.
test_toast_capture.h The toast capture whose frame cannot compose the product surface beneath it: the live-toast walk over the top level widgets (Ui::Toast::internal::Manager keeps _toastByWidget private and enumerates nothing), a grab rooted at the toast subtree that Ui::GrabWidgetToImage fills with st::windowBg wherever the toast does not cover, a settled-show readiness term measured against st::toastBg's own blend over that base because _shownLevel is private, named refusals for a root that is not the live toast and for a rect outside the toast's geometry that quote both rects and never reframe, the joined-label text oracle read through Test::CheckTextReads, and its own self-test over a synthetic sentinel surface it paints, which counts sentinel pixels in both saved frames and then hides that surface to show no pixel of the subtree frame depending on it, and which drives both documented ambiguity branches of the resolver — a second live toast shown and taken down inside one stage, and the empty walk its own teardown arranges — showing FindLiveToast() answering nullptr for each, and which measures a default-duration toast live on ToastSubtreeReady inside its lifetime and gone from the walk after it, quoting the live count, the texts and the elapsed time.
test_hover.h The input helpers' own self-test: a clicked-then-dragged and a never-touched Ui::RoundButton measured against the two fills their style names, proving a completed synthetic click and drag leave no hover.
test_activation.h The window-activation helper's own self-test: the application deliberately de-activated and re-activated through the same QPA seam inside one stage, the wrapper-versus-inner hasFocus() contrast and the wrapper-versus-rawTextEdit() typing contrast measured on one real Ui::InputField.
test_box_button.h Click a box's shell footer button by label, rooted at Test::PaintingLayerRoot, with its own self-test: a content-rooted Ui::RoundButton search that reaches none of the footer buttons beside the rooted search that finds and clicks one, a named refusal for a label no shell button carries, a disabled or hidden footer readable as present+disabled/hidden with matched() still false (BoxButtonDisabled / BoxButtonHidden beside BoxButtonReady), box_button_shell saved through CaptureBoxLayer so the frame contains the footer row, and a same-launch hidden-then-ready shell-button reading while the layer show animation runs. ClickBoxButton still Fails on an unusable button.
test_ink.h Derived paint bands, colour separation, ink scans, counts, and contrast reports. DeriveBand names the case where the requested fill is the image's background outside the candidate, distinct from no-rows; MeasurePaintedInk carries that reason in report, and can no longer answer with an empty report on any path. A scan that cannot classify a frame refuses by name as well: InkScanState answers outside-image, no-rows-in-band, candidates-collide, background-collinear, no-ink or classified beside a reason that is never empty on a returned reading, while ok keeps its one meaning — this band was scanned — so only the two states that looked at no pixel are false and every frame-level number on the other five is a real measurement. background-collinear is a candidate within kInkMargin of the segment from the scan's measured modal background to another candidate: the reading is not classified, and countAt refuses every count as count=none instead of returning the zero that candidate can never earn. It loses to candidates-collide and to no-ink, which keep their texts. candidates and counts are filled before either refusal returns, so the two are always the same length, and one candidate's count is read through InkScan::countAt(i), which names the candidate and refuses an index the reading does not have, a reading that classified nothing and a candidate pair that does not separate, instead of indexing past the end of a vector the scan never filled; InkCountDetails prints that refusal as count=none, never as a zero. FormatInkScan starts with FormatInkReport's unchanged text and then names the candidate colours, their counts, CollisionDump's collision and all five thresholds on the passing verdict as well as the failing one. Own self-test over synthetic images.
test_ink.h (ReadChromaticRaster) Where a coloured glyph clusters in a captured band. Given a pen and a fill to exclude, it keeps pixels whose channel spread clears kChromaticFloor and that match neither excluded colour within kSameTolerance, slides a square window across their x range, and reports the densest window's box, matched count, total chromatic count, density and the mean colour of the pixels it matched — never a colour the caller passed in. no-chromatic (the band was scanned and no pixel cleared the floor) and below-density (chromatic pixels exist but no window reaches kChromaticDensityFloor) are named refusals; readBox, readMean and readDensity refuse on both, so an empty box is never a measurement. A band that misses the image is raster-outside-band, distinct from both. FormatChromaticRaster prints the box, both counts, the density and the measured mean on a found reading and names the refusal on the others. Own self-test over synthetic images, registered from AppendDeriveBandSelfTest.
test_ink.h (ReadGlyphCore) Which pen painted a text run, read from its solid glyph cores rather than from nearest-candidate attribution. The band's background is its modal colour. Ink is a pixel at least kInkDelta from that background. A solid core is an ink pixel within kGlyphCoreTolerance (8) of the per-channel extreme away from the background, which is full coverage rather than a fringe or a partial blend. The reported modal colour is the mode of those cores, with its share and the core count; a named pen is only a count of solid cores within the same tolerance and is never copied into the modal. no-paint is a band with no ink. no-solid-core is a band that has ink but no pixel within the tolerance of the extreme, so a fringe colour is not returned as the pen. readModal and solidAt refuse both, and core-outside-band is a band that misses the image. Distinct from background-collinear, which is an attribution refusal, and from ReadChromaticRaster, which is a mark-density reading and not a pen reading. Own self-test over synthetic images, registered from AppendDeriveBandSelfTest.
test_style.h Wait for palette/style samples to stabilize and assert a recorded baseline still holds.
test_panel.h Distinguish a live Ui::SeparatePanel from its faded/squeezed show-animation cache, and answer which top-level Ui::SeparatePanels exist. Test::WalkPanels(&scan, query) is the one walk eight disposable overlays each wrote by hand: it enumerates them with a caller-supplied exclude of panels the caller already holds — never a type — and a caller-chosen Test::PanelLiveness, where Settled is decided by this module's own ReadPanelShowState and not by isVisible(), so a panel still painting its show cache is not counted as one that opened, while Shown is kept beside it as that reading's control. A mark is simply a previous answer: Test::PanelList is std::vector<QPointer<QWidget>>, so it keeps nothing alive and a panel at a reused address cannot read back as one the mark held, and query.before — or Test::PanelsAdded, the same implementation — reads the difference back as pointers and never as a count. A walk that reached no Ui::SeparatePanel of any kind refuses by name instead of answering a zero, certified through Test::DiscriminatingScan whose control is every panel the walk reached, so "panels reached, none settled" is a certified zero while "no panel reached" is not; and Test::PickPanel refuses an ambiguous identity ask, naming every candidate it saw, in three refusal sentences no two of which read alike. PanelWalkText / PanelListText are the pure, pollable readings and PANEL_WALK the one reporting row. Identifying a panel from its labels is FindAll<Ui::FlatLabel> + CheckTextReads composed at the call site; no label-join idiom is promoted here. Own self-test over a synthetic top-level panel it creates, shows and takes down itself.
test_menu.h Deterministic Ui::PopupMenu capture whose readiness is content identity only, its showingContent reading, and its own self-test. CapturePopupMenu takes an optional trailing skipReason that gates both stages it appends, the opener and the capture; left empty the helper is unchanged. The self-test reads Test::ReadConsoleLock() (test_console_lock.h) right before it pops its menu and when each stage that needs the menu open across turns begins, keeping the first locked reading, so on a locked Windows console the popup_menu_open capture, close, and the popup_menu_reopened opener and capture are TEST_RESULT: N/A naming the lock while open, refusal text and the teardown run and decide. The context-menu reader beside it: OpenContextMenu(receiver, point, reason) snapshots every live Ui::PopupMenu in the application (QApplication::allWidgets(), held as QPointers, so a menu at a reused address never reads as an old one), sends the QContextMenuEvent inside Settle and, in that same call, takes the one menu that was not alive before - a submenu, whose parent is a Ui::PopupMenu, is not counted - into a ContextMenuReading: the ordered non-separator texts (an entry with no text kept in place as an empty string), the action and separator counts, the parent and parentIsReceiver, and a ContextMenuLifecycle at build, inside the settle action right after sendEvent, and again after the postponed calls settled (alive, visible, hidden, activePopupWidget() as self / other / none, geometry, actions, PopupMenuReady, application state, active window, focus window), with the receiver, the menu and its parent held as QPointer<QWidget> and everything else recorded by value. No event-loop turn runs inside the call, though on Windows PopupMenu::showPrepared's synchronous repaint can drain crl::on_main work there. A menu that exists but is hidden or empty is reported, not refused; the refusals are receiver-gone, no-menu (with reshown counting a kept menu shown again) and several-menus (naming every candidate). ReadContextMenuLifecycle is the pure reading a later poll takes, and ContextMenuDetails prints recorded values only. TriggerContextMenuAction(reading, text) triggers the one entry with exactly that text on the still-live menu and, inside Settle, delivers its queued Ui::Menu::CreateAction callback with QCoreApplication::sendPostedEvents(action, QEvent::MetaCall) - that action's calls and no other receiver's - before returning, without hiding the menu; its refusals are menu-gone, no-such-action (listing the menu's texts), several-actions, action-disabled (Qt 5 would emit triggered for a disabled entry, Qt 6 would not, a user cannot) and submenu-entry (decided by action->menu(), so an entry given a submenu by a bare ensureSubmenu is triggered like an ordinary one), printed by MenuTriggerDetails. Neither grabs pixels, Notes or Fails. Its own self-test, AppendContextMenuReaderSelfTest, runs over hidden harness widgets on the primary window (a missing one is its named fixture gate): a menu built in the receiving widget, the Keyboard reason and a reading that outlives its menu, a menu parented to another widget that a receiver-rooted search misses, no-menu beside those positive controls, the reader and trigger refusals, and an entry's callback run when the trigger returns while a marker queued just before it has not, beside a raw QAction::trigger() premise whose callback stays queued.
test_messages.h Lifetime-owned watcher for a matching newly sent server message.
test_history_fixtures.h Inject a caller-supplied service action into a real history as a regular or (negative-control) local item, with a caller-owned lifetime that removes it, and log the menu-gating predicates.
test_notify_override.h A local, restored override of one peer's notify settings, for a campaign that must see a notification delivered on a chat the golden account serves muted: its default user settings are mute_until=2147483647, which 777000 and every private chat without a mute value of its own inherit, so System::computeSkipState answers Skip. Test::OverrideNotify(runner, peer, values) first captures the peer's whole PeerNotifyState (ReadPeerNotifyState / PeerNotifyStateText) - known, value presence, mute_until, sound, silent, show_previews and stories_muted, each with its presence, plus muteUnknown, isMuted, History::muted() and exception membership; value presence is read through an explicit-instantiation accessor confined to the module's .cpp, because public API cannot tell an all-unset value object from none - and then applies the capture with values laid over it through Data::NotifySettings::apply(MTPNotifyPeer, MTPPeerNotifySettings), the exact local call the updateNotifySettings handler makes, which forms no request. The default UnmutedSilentNotify() is mute_until=0 with sound none, so the notification is delivered silent (no sound, no dock bounce); MutedSilentNotify() is its inverse. Two named gates apply nothing and still return the never-null handle: settings-unknown (an apply would make unknown settings known, and no apply can make them unknown again) and already-overridden (an applied, unrestored override of the same peer in the same live session). NotifyOverride::restore() applies the capture back once and then only re-reads and compares on every later call, so it is safe from the window's end, teardown and Runner::onFinish alike; a served writer that replaced the override inside the window (an updateNotifySettings push, History::applyDialog, users.getFullUser, ApiWrap::requestNotifySettings) reads intact=0, and the capture is restored anyway. CheckNotifyRestored prints applied, intact and both whole states on either verdict, and DefaultNotifyStateText reads the three defaults for a campaign's "unchanged" checks. The module registers one Runner::onFinish on the first override, which restores or verifies every handle LIFO. A handle holds a base::weak_ptr to the session and a PeerId only; notify settings are not persisted, so an override also ends with the process. Forum topics and the community fallback are not covered. Test::InterceptNotifyRequest is the request observer the overlay pastes, quoted in the header, as the first statement of MTP::details::Session::sendPrepared: it records every request formed and, under Test::Active(), suppresses every account.updateNotifySettings. Test::AppendNotifyLocalityCheck waits more than 2 s (over kNotifySettingSaveTimeout, 1 s) after the last restore, sends one raw read-only account.getNotifySettings control with no handlers and decides through Test::DiscriminatingScan - subject any account.updateNotifySettings formed from the first override's request mark, control the record carrying the control's own request id and constructor - and is N/A by name without the hook. Own session-backed self-test: AppendNotifyOverrideSelfTest (inverts 777000's, or Saved Messages', served isMuted; both refusals; the restore, a second restore that applies nothing, and the locality check).
test_custom_emoji.h Supply an always-ready Ui::Text::CustomEmoji that fills the large-emoji box with worst-case ink, handed out only for document ids the scenario itself registered.
test_launch_fuse.h Declare and verify operating-system launches while refusing every real launch in test-agent mode.
test_open_handoff.h Inspect and assert the document-open branch without handing anything to the OS.
test_transfer.h Observe document save/failure transitions and assert duplicate or failed transfer behavior.
test_web_cache.h Remove a web document's persistent cache entry before a leg that must reach the network. Data::WebDocumentCacheKey is the SHA-256 of the URL alone - never the access hash - and the marked live portable folder is reused from run to run with its caches, so a URL an earlier run loaded is a local hit that FileLoader finishes with no upload.getWebFile at all. Test::RemoveWebDocumentCache(session, url) removes that key from session->data().cache() - where web documents land - and from cacheBigFile(), which no current writer uses for them and which the removal guards against a future one, and returns a WebCacheRemoval reading written on main only: poll finished() (true only after both databases answered) from a pure until, and judge ok() in then with WebCacheRemovalText as details. It holds only a base::weak_ptr to the session: once the session is gone nothing completes and the text says session=gone. Removing an absent key completes ok(), so a campaign may remove unconditionally. It clears the persistent caches only, not a media object that already holds the bytes in this process. Own self-test over fabricated .example.invalid URLs with synthetic bytes and no network, beside an unrelated key that must survive: AppendWebDocumentCacheSelfTest.
test_rpc_retry.h The permanent MTP resend seam: Test::RecordRpcRetry records one rpc retry code=<code> type=<type> request=<constructor> row for every code-500 or negative-code answer the transport auto-resends without calling the request's fail handler, read through the Test::RpcRetryProbe() accessor; with its own self-test for the recorded 500, the non-500 that reaches .fail() instead, and the answer for a request id this process never sent. Its synthesized rpc_error answers now go through test_rpc_fixture.h rather than a hand-rolled processCallback.
test_rpc_fixture.h Type-safe controlled RPC replies for a registered request. Successful answers take the generated request's boxed ResponseType (tl::boxed), which writes the constructor word; a bare factory such as MTP_messages_messages(...) omits that word and must not be .write()'d into the reply. DiagnoseControlledRpcResult requires a complete decode with no leftover primes (truncation and trailing words included) before MTP::Instance::processCallback. DeliverControlledRpcSuccess / DeliverControlledRpcPrepared also require hasCallback and refuse delivery on a diagnosis so the parser stays registered. DeliverControlledRpcError is the explicit boxed rpc_error route (registered or not). DeliverControlledRpcStale is the explicit canceled/unknown-id success route: it still calls processCallback after a valid boxed body. Own self-test over messages.getMessages (empty and nonempty boxed messages.Messages, bare/truncated/trailing/incompatible rejects, code-400 error, cancel-then-stale).
test_wallet_ready.h Judge one reading of the wallet's freshness stamp — the one applyState() writes when the server's wallet state reaches this client — refuse a stamp that was never written, wait for a stamped state under a bounded deadline that names what it observed, and re-run the instrument's own positive/negative/recovery self-test.
test_wallet_intercept.h The controlled Gram wallet fixture's shared request intercept, so a wallet campaign calls it instead of copying GramIntercept overlay code. The overlay pastes one hook, quoted in the header, as the first statement of MTP::details::Session::sendPrepared: Test::InterceptWalletRequest(_instance, request), returning when it answers true. That is the single main-thread funnel into a session's send map, every Instance::Private resend included, and the module itself adds no production seam. ControlledWalletIntercept() is the process-global WalletIntercept: arm(&session->mtp()) suppresses and records every request of that instance; seal() suppresses wallet.sendTransfer and a provider POST /api/v2/jsonRPC whose JSON method is sendBoc (the product's IsSendBocRequest, here IsWalletTransferRequest) from any instance for the rest of the process; disarm() lets every other constructor pass. Held toncenter.performApiRequest provider calls are released through their real registered callbacks with a controlled rpc_error 400 TIMEOUT, which Wallet::Api::IsTimeoutError classifies as the host's own expiry (kTimeout), from the first drain through every box and press; a campaign turns releasing off only for a deliberate hold and then answers those calls itself, posting from onRegistered. AppendWalletEngineDrain releases everything held, leaves the release on and waits for the pure readiness(): the engine worker idle by the overlay's own counters (an absent worker is an idle reading, a missing reading refuses), no provider call held, and none registered, released or answered for 1500 ms, longer than the 1000 ms comment-recipient retry delay. CheckNoEarlierProviderCallHeld gates the press. A refusal of either is a fixture gate (TEST_FLAW), never a product FAIL. teardown(quiesce) runs seal, stop releasing, release the held provider calls, cancel the other suppressed requests, the caller's quiesce, disarm - the seal before the disarm, because the source run disarmed first and its queued fixture send's routed wallet.sendTransfer reached test DC 2 - and is idempotent from Runner::onFinish. The overlay fixes the fixture route when the send is queued (Test::GramOwns(this) captured before _engine->run), so a fixture send that enters the engine after disarm never takes the real engine path. Own self-test on private intercepts, with no network, no account, no session and no MTP::Instance: AppendWalletInterceptSelfTest.
test_gram_reconcile.h The README's P0 fixture reconciliation ("Account fixture secrets"): ReconcileGramAccount / the one-stage AppendGramReconcile reconcile the golden test_gram_account.txt with the marked live copy's own custody through the product's local reveal and restore only - NoOp, case (b) RewroteFromReveal, case (a) RestoredFromFile, or a refusal by a named GramReconcileGate that writes nothing - and print one public GRAM_RECONCILE: row; GramReconcileResult::phrases and GramCustodySecretTokens hand the secrets to the secrecy scan in memory; GramReconcileDrive is the self-test seam. Its own acceptance on the marked live copy is AppendGramReconcileSelfTest.
test_secrecy_scan.h Prove that no fixture secret reached the logs: CheckSecrecy takes phrases, short secrets and tokens once, selects the test log, the app log (Logs::full()) and this launch's DebugLogs parts by identity (the day index in the first line, never a last-write time), counts only what the client wrote (plain lines and Send dump entries; Recv hits are reported with their schema site), reports every client-written short-secret hit in a plain line at its <file>|<head>|<field> site (the message head and the name of the field whose whole value the hit is), lets a campaign declare computed fields (SecrecyScanArgs::computedFields, message head and field name) whose exact-value hits are reported in SECRECY_COMPUTED and never decide, refuses to certify a zero without a planted nonce, named controls and passing canaries, and prints counts and public identifiers only (SECRECY_SCAN, SECRECY_WINDOW, SECRECY_CLASS, SECRECY_RECEIVED, SECRECY_COMPUTED). Its SECRECY: <class> reports are DiscriminatingScan rows, so they follow the number-format request of test_log.h like every converted helper row, while its SECRECY_* rows and its three checks keep ordinary integers in both modes. Its session-free self-test over synthetic log trees is AppendSecrecyScanSelfTest, which also covers the number format of overlay telemetry and the shared helpers' numbers in both formats.
test_scenario.cpp The only permanent overlay slot; the repository version remains a no-op.

Test::Check's third argument is an observation, not a failure excuse. It is printed on both verdicts — TEST_RESULT: PASS: <what> - <details> and TEST_RESULT: FAIL: <what> - <details> — so a green log says what each check was made against and a passing run can be audited without re-running it. Pass the values the check judged: the measured geometry, the observed identity, the window and the rows behind the verdict. Text that is only true after a failure stays conditional at the call site — ok ? QString() : u"out of tolerance"_q — which leaves the passing line exactly TEST_RESULT: PASS: <what>, the same line an empty details produces. Do not emit a Note beside a check only to print a reading that check's own details could carry; that duplication is what this argument replaces. Where the failure text cannot double as a true passing observation, the details stay conditional and an adjacent Note remains the carrier — CheckBlockedLaunchesExactly suppresses its mismatch text on a pass and keeps its Test::Note(u"blocked launch record: [...]"), which is what test_launch_fuse.h promises. None of this loosens a refusal: failure text still has to name what it judged, and an undecidable reading is still refused rather than printed as a Note a reader would take as a measurement.

Read the selected module's header before using it; the contracts there are more precise than the summary above. Search this directory before writing a new local helper: an overlay that reimplements a shared facility is a test flaw like any other, because the local copy carries none of the refusals the shared one accumulated. One overlay rebuilt grab-check-save by hand after CaptureWidget had already refused blank and hidden targets for months.

Building one reliable packed scenario

  1. Derive every falsifiable check from the task, visual contract, and retained diff before writing overlay code.
  2. Choose the most direct fixture that still executes the changed production code. Publish exact objects/actions at construction and callback seams when unrelated navigation would be the fragile part.
  3. Arrange nondestructive states first and destructive/closing actions last. Reuse one live fixture through its empty, rich, long, error, retry, light, and dark states when those transitions are production behavior.
  4. Put readiness and generation checks in until; put actual-versus-expected values in then or captureAndInspect.
  5. Log every value needed to diagnose a failure in the first run. A timeout should name the last object generation, visibility, geometry, state, and callback count instead of forcing a speculative second overlay edit.
  6. Save decisive tight captures as the scenario reaches each state. Do not postpone all visuals to the end, after later actions have replaced them.
  7. Finish with guards: no undeclared OS launch, no real payment/network call when mocked, expected callbacks exactly once, and no leftover expectation.

Account fixture secrets

The golden test_TelegramForcePortable folder may carry secrets beside tdata. Today those are 2svpassword.txt and test_gram_account.txt. 2svpassword.txt is the test account's two-step verification (cloud) password, which the server demands on wallet.exportSecretPhrase, wallet.replaceWallet and the password route of wallet.disableBackup (PASSWORD_MISSING without it, SRP_ID_INVALID for a fabricated proof). wallet.enableBackup, which has no password field, and the proof route of wallet.disableBackup prove key ownership instead of sending the password. A scenario reads it at runtime through Test::TwoStepPassword() — whitespace-trimmed, std::nullopt when the file is absent or blank, always std::nullopt outside test-agent mode — and the generic Test::FixtureSecret(name) reads any sibling file the same way. Both look in the live portable folder first and then in the golden sibling, so a file added to the golden folder after a marked live copy was made is still found.

Type the value into the product's own PasscodeBox so the real SRP path computes the proof; never assemble an InputCheckPasswordSRP by hand. When a selected check needs the password, the file's presence is gated before the scenario is authored and its absence blocks the task (pipeline.md, Phase 3 step 5); a scenario that still reaches a missing file at runtime refuses with a named fixture gate (fixture gate: 2svpassword.txt absent), never a product FAIL, and never substitutes a driven answer for the leg. The value never leaves the process: no overlay literal, Note, check details, capture, evidence file, work/ artifact, receipt, prompt, or environment variable recorded anywhere may carry it. The overlay is published into the task repository with the result, so a literal there is a leak, not a convenience.

test_gram_account.txt is the owner's funded golden wallet: the test account's server-created wallet, recorded by hand and funded at its address. The format is the word lines until the first empty line, then the address as the app shows it; never assume a fixed word count, because a confirmed key rotation lengthens it. A scenario reads it at runtime through Test::GramAccount() — the live copy first, then the golden sibling — which returns the words, the address line and addressRaw, the address normalized through Wallet::CanonicalAddress so either form compares; std::nullopt when the file is absent, has no word line or no address line, and always outside test-agent mode. The golden sibling is consulted only when the live copy is absent or blank — a present but malformed live copy answers std::nullopt rather than masking a different golden file, because after a rotation the live copy is the authority. The words are under the same rule as the password: typed or compared in process only, never in overlay code, a Note, check details, a capture, an evidence file, a work/ artifact, a receipt or a prompt. Only the COUNT of the words and the address may be recorded.

A campaign that declares it needs the funded wallet is gated on it at campaign start, before anything is spent: the file must be present; GramAccount()->addressRaw must equal *wallet.address() once WalletRefreshSettled accepted the served state — a mismatch FAILS the run and is never repaired by /wallet_reset, wallet.replaceWallet or a minted wallet; and the balance must cover every planned live leg plus a margin — a shortfall refuses with a named fixture gate (fixture gate: golden wallet balance short) and the performer publishes the task-local Block naming the address and the amount required, never a driven workaround. Campaigns on the golden wallet never reset it and spend only what a leg needs; state the count of live legs the run issued. A live leg also satisfies the effective wallet_transfer_min_nanos floor in force for that account, in addition to the conservative 0.3 GRAM evidence guidance, which the configured floor does not replace. No campaign spends a sub-floor amount to observe how the backend answers: the client's own amount refusal is established from controlled configuration and local send fixtures, and an at-the-floor control may stop at fee preview.

After a confirmed key rotation the scenario calls Test::RewriteGramAccountWords(newWords, addressRaw) from the stage that observed the confirmation, before any later leg can cut the run off. It rewrites the word lines of every existing copy — the marked live copy AND the golden sibling — through QSaveFile, keeping the empty line and the address line; each copy is parsed on its own and rewritten only when its own address is the rotated wallet's (the scenario passes the served raw address); a copy naming another wallet is left alone and reported false. It returns which copies were written; the stage checks both and re-reads Test::GramAccount() to compare the count and the words in process. The golden write is the one owner-decided exception to the read-only golden folder; nothing else in the harness or in a campaign ever writes there.

The golden tdata is never modified while the file is, so a later campaign's P0 reconciles the two before anything else — in process, after the address gate has already passed. Do not hand-roll it: call Test::AppendGramReconcile (one stage) or Test::ReconcileGramAccount (the flow) from test_gram_reconcile.h right after the address gate. The two cases remain the definition: (a) the local custody has FEWER words than the file: the golden tdata predates a confirmed rotation, so restore custody from the file's words through the product's own import (Wallet::Session::restoreFromPhrase) before any other stage; (b) the local reveal differs from the file with an equal or greater count: the live copy was promoted after a chain-confirmed rotation but the rewrite was cut off, so rewrite the file from the local reveal.

A phrase qualifies when it signs for the served key (its signing key — words 13-24 of a 24-word phrase, the anchor of a 12-word one — is Wallet::Session::publicKey()) and derives the served address (its anchor, words 1-12, derives *wallet.address()). The helper decides in this order, and each refusal is a named gate (GramReconcileGate) that writes nothing:

  1. Preconditions: NotActive; ServedUnknown (no served address or no 32-byte served key); FixtureAbsent; FixtureOtherWallet; CustodyUnreadable; CustodyBusy (custodyBusy(), a pending rotation, or a record still awaiting its server key — such a record "signs with" any served key, so it cannot certify the file).
  2. NoOp: the file qualifies and a readable current record exists. Nothing is written.
  3. Case (a), RestoredFromFile: the file qualifies, no current record exists, and the local count is below the file's. The local count is 0 when no record of the served wallet is held — the fresh golden copy — and otherwise implied by the held obsolete record's keys: 12 words when its signing key is its anchor, else 24. It is never read by a local reveal: under the rotation-following custody model an obsolete record is not the current record, so revealPhrase would fall through to the server's wallet.exportSecretPhrase. The helper restores with the silent Open grant (no chooser opens), then requires a local reveal equal to the file (RestoreFailed / ConfirmMismatch otherwise).
  4. The file qualifies but (a) does not apply: NoReadableCurrentRecord, the detail naming why — the current record is unreadable, the held obsolete record is not shorter than the file, or a pre-v4 unresolved record whose count is unknown (never revealParked, whose reveal writes custody).
  5. The file does not qualify: NoReadableCurrentRecord unless revealsLocally(); NoOpenGrant without a silent grant; otherwise the local reveal (RevealFailed <PHRASE_*> when it fails), then RevealedNotServedKey, RevealedNotServedAddress or RevealedShorterThanFile. Otherwise case (b), RewroteFromReveal: RewriteGramAccountWords(revealed, served address), then GramAccount() is re-read and compared in process (RewriteFailed when the live copy was not written, RereadMismatch when the re-read differs).

The stage writes one public GRAM_RECONCILE: row (case, gate, counts, booleans, public keys and addresses), reports NoOp, (a) and (b) as PASS, and a refusal as TEST_RESULT: N/A with fixture gate: P0 <gate> - <detail>; a campaign's funded-wallet legs gate on GramReconcileResult::reconciled(). A fresh golden copy whose keyring is not Open refuses NoOpenGrant (the detail names the keyring kind) until the protection chooser has been driven once on that copy. The helper never calls wallet.replaceWallet, wallet.exportSecretPhrase or any other server method that mutates or reveals the wallet, never resets and never mints. Its GramReconcileDrive seam fakes only the served identity and the record facts it names, never writes a fixture file (DrivenWriteSuppressed), and restores only when the undriven facts show a same-phrase re-import. Its own acceptance is Test::AppendGramReconcileSelfTest, on the marked live copy. Every stage of it that reaches a reveal or a restore is a fixture gate, NoOpenGrant keyringKind=<N>, without the silent Open grant, and a live copy staged to 12 words that case (b) did not rewrite is put back to its earlier words through RestoreGramAccountLiveWords (the gram_reconcile_restore_staged stage, and again when the scenario finishes).

Never delete or reset the marked live copy while a campaign may have left a rotation in flight or unrewritten: SETUP keeps a marked live copy, and only a manual wipe or test-account-reset discards it — and with it the only copy of a not-yet-rewritten signing half.

Proving no fixture secret reached the logs. A funded-wallet campaign proves that no recovery word, 2SV password, custody record id or secret ref reached the test log, the app log or DebugLogs by calling Test::CheckSecrecy(args, what) (test_secrecy_scan.h) once, from a late stage's then, instead of grepping by hand. Hand it, in SecrecyScanArgs::secrets:

  • phrases: GramReconcileResult::phrases (the file's words and every revealed phrase), plus any rotated phrase the run produced later;
  • shortSecrets: *Test::TwoStepPassword() when the campaign used it;
  • tokens: Test::GramCustodySecretTokens(session) taken at P0 AND again at the scan (a restore or rotation mints a new record id), which lists every record id, secret ref and secret/ storage key of the custody store.

Declare computed fields in SecrecyScanArgs::computedFields only as described under Declared computed fields below.

A campaign that calls CheckSecrecy also calls Test::RequestTelemetryNumbers() (test_log.h) unconditionally at its first stage, before any row the scan may read, never under a condition and never from code that read a fixture secret. From then on the shared helpers print the numbers they measure, count or time - the GEOMETRY:, CheckNear, post-paint, clocked-frame, probe and scan rows - as TelemetryNumbers, whose digit runs the scan never counts, so no number such a helper printed can fail the scan by coincidence (the shared-helper row under Failure diagnosis); the caller text in those rows stays the caller's to format. The call logs TELEMETRY_NUMBERS: requested once, and a row formatted before it keeps ordinary integers.

The values stay in memory: the helper prints counts, public file names, TL schema identifiers and the heads and field names of plain-line sites only, never a secret, a field's value, a length or a whole scanned line.

  • Classes and identity. It reads five classes: TestLog (test_log.txt), AppLog (the main log through Logs::full(), the text the logger holds), DebugLog and MtpLog (every DebugLogs/log_HH_MM.txt / mtp_HH_MM.txt part named for a quarter hour in [process start, scan]), and EarlierParts (only parts named in earlierParts). A part is accepted only when its first line is its own day index (yyyyMMdd); otherwise it is Foreign, Missing or Unreadable. A window part is scanned from its last NEW LOGGING INSTANCE STARTED!!! banner, so an earlier same-day launch's lines are not charged to this one. No last-write time takes part anywhere.
  • Matching. A phrase counts as any ordered adjacent pair of its words (case-insensitive). Words are letter-and-digit runs: whitespace, punctuation (commas, quotes, brackets), a dump string's \n, \t, \\ and \" escapes, the dump's type tags ([STRING]) and the test log's \uXXXX escapes all separate them, and a run carries across the lines of one file. A phrase one word per line, comma-joined, escaped in a dump string or dumped as a one-element-per-line vector therefore still counts; a number is neutral, and any other word or an entry header ends the run. A short secret counts only when bounded — no letter or digit right before or after it; embedded occurrences are reported and never count. A token counts on every substring occurrence; a token shorter than 16 characters is refused as "not a token; hand it as a short secret".
  • Direction. MTP dump entries are classed by the transport's own Send: / Recv: header. Only what the client wrote — plain lines and Send entries — decides. Recv hits are server-sent content (a received value that holds a short secret by coincidence, or chat text that happens to hold two adjacent BIP-39 words) and are reported with their <top>/<inner>.<field> schema site in SECRECY_RECEIVED, never deciding.
  • Plain-line sites. Every bounded short-secret hit in a plain line is reported at its <file>|<head>|<field> site, in its class's SECRECY_CLASS row (plainSites) and in that class's SECRECY: <class> subjects. The head is the message text after the app log's [yyyy.MM.dd hh:mm:ss] , a DebugLogs entry's [hh:mm:ss.zzz tid-index] or an mtp_ entry's (dc:<dc>) (a test-log row has no prefix, so its head keeps NOTE: ), up to the first name= field: an ASCII identifier at the message start or after a space, outside double quotes. A value runs to the next space, less a trailing ;, ,, . or : and one enclosing pair of quotes; <field> is the name of the field whose whole value the hit is, or -. The whole site is withheld as <file>|?|?, for every reason alike, when the line has no field, the hit lies in the head, the head is longer than 64 characters or holds anything but ASCII letters, digits, spaces and _.:-/, or the head (with the field name) holds any given secret. No value, length, offset or line is printed.
  • Declared computed fields. The fourth trap: a value the product computes and formats into its own diagnostic line can equal a short secret, and a count alone cannot tell that from a leak. Declare such a field in SecrecyScanArgs::computedFields as { .head, .field }, spelled exactly as its site prints them, only when the product source shows that the field is formatted from a value the product computes, never from fixture input; name that formatting site (file and function) in the campaign's test design, and decide from the site row and the source, never from the matched line. A hit that is exactly that field's value on a line with that head is reported in SECRECY_COMPUTED and never decides. The same value in any other field, in free text or in part of the value, a withheld site, and every word run, token and Send-entry hit still decide; a declaration that matches no printed site changes nothing. A number an overlay prints into its own telemetry row is never such a field: print it with Test::TelemetryNumber instead (the overlay telemetry row under Failure diagnosis). Nor is a number a shared helper prints into its own row - a GEOMETRY: row, a CheckNear verdict, a post-paint, clocked-frame, probe or scan row: ask with Test::RequestTelemetryNumbers() at the first stage, or convert the helper (the shared-helper row under Failure diagnosis).
  • Controls and canaries. A zero is certified only when it can be told from absence. The call plants a fresh public nonce through Note, LOG and MTP_LOG and requires it in every non-earlier class; a named control (testLogControl, appLogControl, debugLogControl, mtpLogControl, for example the served key hex or wallet_getState) must also be hit. A class is undecided with no accepted file, an unreadable part, no planted control, an absent named control, or an mtp_ part without headers of both directions. In-memory canaries run every given secret through the same matcher and dump walk on every call; a failing canary leaves the scan undecided.
  • Output. One SECRECY: <class> DiscriminatingScan report per class, then the rows SECRECY_SCAN, SECRECY_WINDOW (every window candidate's identity), one SECRECY_CLASS per class (with its plainSites), SECRECY_RECEIVED and SECRECY_COMPUTED, then three checks: <what>: every class decided, <what>: no client-written fixture secret, and <what>: the scan's own rows carry no fixture secret (the bytes it appended to test_log.txt, re-read). It returns decided && clean. The SECRECY: <class> reports are DiscriminatingScan rows and follow Test::RequestTelemetryNumbers() like any other: after the ask their examined=, subject(...)= and control(...)= counts, and a refusal's counts, print as TelemetryNumbers. The SECRECY_SCAN, SECRECY_WINDOW, SECRECY_CLASS, SECRECY_RECEIVED and SECRECY_COMPUTED rows and the three checks keep ordinary integers in both modes: they also print identifiers whose digit runs no number format removes (window times, DebugLogs part names), and judges read their counts as integers. The own-rows check re-reads exactly the bytes the call appended, whatever their format.

DebugLogs must be enabled (tdata/withdebug = 1), or the DebugLog and MtpLog classes are undecided — MTP_LOG writes nothing without it. The scan covers logs only: the post-run grep of promoted evidence, work/ artifacts and receipts for 64-hex and UUID tokens stays the performer's job. The matcher's own acceptance is Test::AppendSecrecyScanSelfTest, session-free, over synthetic log trees and synthetic secrets.

Media fixtures and fixture gates

A media document is a fixture only after the run has watched it play. Metadata does not decide this: an undecodable upload left on the shared test account can report song=1 audioFile=1 sharedMusic=1 with a plausible duration, so no predicate over flags, title, performer, filename, or membership in some list can tell it apart from real music. “The first song not already in profile Saved Music” is exactly such a predicate, and it accepts an undecodable upload. Observe playback instead:

  1. Order the candidates the account already exposes by playability signals — already downloaded, then a longer duration, then a non-empty title or performer — and push known-synthetic names behind every other candidate. The order is a preference; nothing on it is trusted yet.
  2. Really play each candidate in turn, through the ordinary production path (Window::SessionController::openDocument() with that candidate's own message as its MessageContext), and sample Media::Player::instance()->getState(AudioMsgId::Type::Song) around a bounded wait. Accept the first candidate whose TrackState::position strictly advances while length > 0 and the playing id and context are still that candidate's own document and message.
  3. Stop the probe with Media::Player::instance()->stop(AudioMsgId::Type::Song) after every attempt and assert the player is stopped, so no probe state leaks into the measurement that follows.
  4. Feed the one validated document to every stage that needs it — the fixture, any injection, and the negative control — instead of re-deriving it per stage.

The reported length corroborates a probe's verdict but never decides it. Until the stream reports its own duration, Media::Streaming::Player::prepareLegacyState() substitutes the document's declared duration, so a length equal to the declared duration means only that no stream duration has arrived yet — a healthy candidate sampled early reads the same way. It is a bad sign only together with a position that never advances, while a document that really opened reports a stream-measured length instead (len=272910 against a declared 272000). Keep acceptance anchored on the strict position advance.

When a scenario's subject needs a streaming fixture, a stage immediately before the measured action must prove the track is already advancing while the subject still exists. Classify that stage as a fixture gate, not a check: reverting the diff under test cannot change its reading, so its failure makes the acceptance criteria N/A and the run a test flaw — never a FAIL. Without that gate, a frozen reading taken after the action cannot be told apart from the product failing.

Scenario teardown before quit

A scenario that deliberately leaks its State so it outlives a stage owns the release of everything that State holds: every rpl::lifetime, every watcher, every raw cross-stage pointer, and every base::Timer. Register that release with Runner::onFinish. finish() runs those callbacks on every path that reaches it — stage timeout, the scenario watchdog, skip-to-end, and normal completion — exactly once, after the ticker and watchdog are cancelled, before the kFinishDrainDelay fuse drain, and before Complete() / Core::Quit(). A callback registered after finish() has already run executes immediately and is never silently dropped.

A final teardown stage is still useful for work that must happen while the session still exists and the runner is still stepping, but it is not the release point. The stage is not guaranteed to run: a stage that times out, and the scenario watchdog, make Runner finish immediately and skip every stage after it, while a stage whose assertions FAIL does not — that run still reaches its teardown. onFinish still runs in those abort paths, and it also runs when a teardown stage already ran, so the callback must be safe to call after teardown. A main-window geometry change belongs in such a stage, one that waits for the change to settle, and never in onFinish: it releases nothing and starts an asynchronous chat-background re-cache that must not still be painting at quit (see the d->allThreads.isEmpty() row under “Failure diagnosis” below).

Skipping the hook costs a whole run. A leaked State whose two rpl::lifetimes were never released kept observers subscribed to Storage::Facade's and Data::Session's streams while ~Main::Session tore those streams and their items down. The run reached TEST_COMPLETE after a clean sweep and then died with Caught signal 11 (SIGSEGV), no assertion line anywhere in the run's logs, and a 0-byte minidump because the runner had to kill the process. With the release registered, three consecutive runs exited 0, wrote no crash report and left no minidump. After a timeout the same death can follow TEST_COMPLETE, so read it as this signature rather than as a second product fault, and keep the leaked State's session-observing subscriptions no wider than the stages that need them.

Failure diagnosis

Symptom Likely harness cause Repair
Run exits with no TEST_COMPLETE and no SCENARIO_RESULT TDESKTOP_TEST_WATCHDOG is seconds; a millisecond-shaped value (for example 600000) used to arm a multi-day timer that test-run --deadline always outruns, so the watchdog never wrote the markers. Override is seconds in 1..600; implausible values fall back to 120s and the armed duration is logged next to SCENARIO_START. Use a value in that range, or omit the variable.
Timeout before the task fixture exists Generic account/chats preamble or unrelated navigation. Remove unused startup gates; inject the fixture or publish the production object at its real seam.
Assertion/crash in a stage action .run dereferenced an async object or a raw pointer outlived its owner. Use actOnWidget, QPointer, or live publication.
Wrong custom widget/button found Unsafe Qt typed search or ambiguous descendant order. Use the RTTI finders; for repeated/layer-owned controls publish the exact object/action.
Expected mismatch reported as timeout Product outcome was put in until. Wait only for propagation/generation; assert and log the outcome in then/captureAndInspect.
A stage times out at exactly the length of a product deadline it was waiting through - stage timed out: ... - waited 60000 ms on a stage whose until awaits an outcome the product bounds with its own timer - while the product's own fail row, which that until already accepts, would have arrived a moment later The stage timeout coincides with the product's deadline, so the harness ends the stage before the product decides and the run records an instrument timeout where a decided negative existed. The row above is the neighbouring fault: there the outcome was put in until; here it is in then already, but the wait was not given room to reach it. Make a stage that waits through a product timer outlast it by a clear margin - 75 s over a 60 s deadline - accept the product's own fail row as the decided negative, and keep the outcome assertion in then.
A request never answers although the server did: no .done(), no .fail(), no product row, the stage waits until its timeout, while the application Debug log repeats RPC Info: error received, code 500, type <T> for the same request Instance::Private::onErrorDefault auto-resends every code-500 and negative-code rpc_error after a doubling delay and never calls a product callback for it, so nothing a scenario can read ever changes. Read the rpc retry code=<code> type=<type> request=<constructor> rows the permanent seam records into Test::RpcRetryProbe() (test_rpc_retry.h), through a mark() taken immediately before the action, and decide from those rows plus the product's own fail row - never from the stage timeout. <constructor> is the boxed body constructor id in hex, compared against the mtpc_* constant the scenario cares about. The Debug log's lines stay the fallback for a reader without the harness log.
A controlled success fixture never reaches .done(), or a later valid answer looks like a stale/missing callback, while the application Debug log shows RESPONSE_PARSE_FAILED / Response parse failed A bare TL constructor factory or an incomplete buffer was written into MTP::Response::reply and handed to processCallback. MakeDoneHandler calls senderRequestHandled before Result::read, returns false on a missing constructor word or truncated body, and processCallback then unregisters the request, so the fixture mistake is consumed as a local parse failure. Deliver through test_rpc_fixture.h: typed success takes the boxed Request::ResponseType, DiagnoseControlledRpcResult refuses empty/truncated/trailing/wrong-constructor buffers before processCallback, and explicit error/stale routes are separate. Confirm with AppendControlledRpcSelfTest.
Blank or partial screenshot Wrong paint owner, animation cache, or viewport clipping. Use prepared capture, PanelShowSettled, the owning ancestor, or CaptureMappedRect; for a box inside a layer, Test::PaintingLayerRoot; for a Ui::PopupMenu, Test::CapturePopupMenu. Gate a visible mapped card/row on Test::MappedTargetReady / prepare(owner, origin, rect) so origin-local containment cannot pass a target that is still clipped.
A mapped capture is refused after readiness passed, or a HistoryInner-local (or other origin-local) rectangle was treated as visible while part of it sat outside the painted owner or scroll Readiness checked the origin widget's own rect, which still contains a child that a negative scroll offset has moved outside HistoryWidget / Ui::ElasticScroll. ElasticScroll::viewport() returns the inner content, so using it as the clipper recreates the same pass. PaintingLayerRoot does not name a chat paint owner. Use Test::ReadMappedTarget(owner, origin, localRect): whole-rect containment in the painted owner and the relevant clipper (ElasticScroll itself, or QAbstractScrollArea::viewport()). Do not intersect or reframe. CaptureMappedTarget refuses an unready reading; CaptureMappedRect still refuses a rect that is not fully inside the grabbed widget. Confirm with test_mapped_target.h.
Old palette/colour sampled Style had not settled or moved between reference and target. Use StyleSettled and StyleBaseline.
A colour check on a widget inside the Wallet window fails on pixels that are correct: the measured colour matches none of the expected st:: values but is the Wallet palette's value of that very key - a focused import field's line measured #32aaff where the check expected #6396cb - while a colour read beside it, such as the error red, passes; unlike the settling row above, the reading is stable and repeats run after run Core::Sandbox::notify (core/sandbox.cpp 760-769) wraps every QEvent::Paint of a widget in style::main_palette::Override(Ui::WindowPaletteFor(widget)) while any window palette is registered (Ui::HasWindowPalettes), and Ui::WindowPaletteFor (ui/window_palette.cpp) answers the palette of the nearest window registered with Ui::SetWindowPalette. The registry is generic; the Wallet window is its only registrant today, through Wallet::UseWindowPalette (wallet/wallet_palette.cpp, called from Wallet::ShowWallet in wallet/wallet_panel.cpp). Under an embedded theme everything in that window paints with Wallet::WindowPalette(), while a scenario that reads an st:: colour outside a paint event gets the main palette's value of the same key; under a non-embedded theme Wallet::WindowPalette() is null and the two agree. Observed in 2026/09/29/keep-the-focused-word-field-in-view-when-importing-a-wallet (work/test.md, Run 1, Test 12, the T11 control): in the night theme the oracle demanded equality with st::walletImportField.borderFgActive read in the scenario, the main palette's activeLineFg #6396cb, while the live line was #32aaff, the Wallet palette's active line, so a correct non-error line failed; activeLineFgError #ef5959 is the same in both palettes, which is why the RED rows beside it passed. Resolve every expected st:: colour inside Wallet::WindowPaletteScope(widget) - the product's own expression, generically style::main_palette::Override(Ui::WindowPaletteFor(widget)) - before comparing it or handing it to a harness colour reader, and print both palettes' values in the details so a mismatch names the palette the pixels match. Copy each expected value as a QColor (->c) while the scope is open: style::main_palette::Override (lib_ui/ui/style/style_core_palette.cpp) swaps colours into the main palette's slots in place and its destructor swaps them back, so a style::color held past the scope reads the main palette again. Where the check is "not X", deciding by distance from the painted X rather than by equality with one other colour also keeps it palette-proof. The source task's Run 2 (work/test.md, Test 12 rerun of the control) repaired its control this way and passed, with details paint(windowPalette=1): error=#ef5959 active=#32aaff ... main(windowPalette=0): error=#ef5959 active=#6396cb.
A clicked button's measured fill matches no style constant, or DeriveBand returns ok=0 with no rows for a widget plainly on screen The reading was taken while the widget was still hovered by an earlier synthetic click, so it painted textBgOver where the check named textBg; before the input helpers delivered a leave this latched for the whole process. Take the reading through helpers that leave the target pointerless (Click/Drag deliver a QEvent::Leave), and when a hovered reading is what is wanted, set the hover deliberately and name the fill the state actually implies. Confirm the instrument with the test_hover.h self-test.
Every in-flight band reading reports derivedOk=0 inkPixels=0 on a footer button while a restored or control band still measures st::defaultBoxButton derives from defaultLightButton, whose textBg is lightButtonBg, and lightButtonBg: windowBg (ui/colors.palette) — the box's own background. DeriveBand's premise is a row whose modal is the requested fill, so on that style the fill cannot be separated from the image outside the candidate. The old answer was the same empty ok=0 as the hovered-fill row above, or as "no row of the recovered box has the pill fill as its own background". Read DeriveBand.reason: when the fill is the surroundings it now says the requested fill is the image's background outside the candidate, so no band can be derived, distinct from the no-rows refusal. MeasurePaintedInk.report carries the same reason instead of a bare zero. Confirm with AppendDeriveBandSelfTest. Do not repair this as latched hover.
Emoji split or custom entity absent UTF-16 code units or the wrong editor event route were synthesized. Use grapheme-safe TypeText or one CommitText on the raw editor.
Drag/wheel/cancel has no effect Wrapper received an event owned by a child, presentation, or viewport. For drag and keys, target the real event owner. For a wheel, Test::Wheel now climbs to the consuming ancestor (the viewport, not the QAbstractScrollArea); if it still does nothing, read WheelDelivery.refusal instead of retargeting by hand. Confirm a wheel with test_wheel.h.
A wheel-delta oracle reads identical values before and after, on a widget plainly inside a scroll area Test::Wheel sent a non-spontaneous QWheelEvent that Qt 6 does not propagate; a covering button ignores it and the scroll area never sees it. Delivering to the QAbstractScrollArea itself is the neighbouring trap: event() returns false without ignoring, so an acceptance check certifies a no-op. Use the repaired Test::Wheel, which replays Qt's parent ladder or refuses by name. Do not treat isAccepted() on a scroll area as success. Confirm with the test_wheel.h self-test.
Test reaches a real external action Missing expectation/fuse or mock seam. Declare the exact blocked launch, mock the transport/payment boundary, and assert zero real calls.
Pixel probe misses only on Retina or at 125/150% Logical rect indexed into the device-pixel grab, or a 100% literal reused at another interface scale. Multiply by the image devicePixelRatio() once at the sampling boundary; derive expectations from the live scaled tokens.
Geometry oracle fails on plausible-looking rects Rects from different widgets compared without a shared origin. Map both through one declared frame (Ui::MapFrom, mapToGlobal) and log the mapped values in the failure details.
Process dies after TEST_COMPLETE, with no assertion line and often a 0-byte dump Overlay teardown, not the product: a leaked scenario State still holds rpl subscriptions to session-owned streams while ~Main::Session destroys them. Destroy the scenario's lifetimes, release its watchers and null every raw cross-stage pointer from Runner::onFinish — a teardown stage is not enough, because a timed-out stage or the watchdog skips every stage after it.
Telegram finished, result: 0 then QObject::~QObject: Timers cannot be stopped from another thread and a fresh dump Overlay teardown, not the product: a scenario-owned base::Timer in static or leaked State was still armed when QApplication died. Cancel the timer (and destroy lifetimes / null cross-stage pointers) from Runner::onFinish; do not rely on a teardown stage, which timeout and the watchdog skip.
SCENARIO_RESULT and TEST_COMPLETE are written, then the process dies: test-run reports died-after-complete (exit -11), the crash report reads Assertion: Unexpected: Qt FATAL message was generated!, QtFatal: ASSERT: "d->allThreads.isEmpty()" in file .../qthreadpool.cpp, line 462 and Caught signal 11 (SIGSEGV), and the Crashpad dump has the main thread in QThreadPool::~QThreadPool under QGuiApplication::~QGuiApplication while a crl thread waits in QPainter::drawImage under Ui::CacheBackgroundByRequest A main-window geometry change made just before quit - from Runner::onFinish, or from the last stage, after whose then Runner::completeStage() calls finish() at once (test_runner.cpp) - on a chat background that is not a plain colour. The next chat paint at the new size hands that area to Ui::ChatTheme::backgroundState() (Window::SectionWidget::PaintBackground in section_widget.cpp), which schedules a re-cache 200 ms later (kCacheBackgroundFastTimeout in ui/chat/chat_theme.cpp); cacheBackgroundAsync() runs CacheBackgroundByRequest() on a crl::async thread, one QPainter::drawImage per tile (its drawTile lambda), and Qt 6.11.2's QT_THREAD_PARALLEL_FILLS (qdrawhelper.cpp) splits each large blend from a thread outside Qt Gui's thread pool into tasks on that pool, waiting on a QLatch. Runner::finish() quits kFinishDrainDelay (500 ms, test_runner.cpp) after its onFinish callbacks. ~QGuiApplication waits for and deletes the pool (qguiapplication.cpp); ~QThreadPool waits again and asserts d->allThreads.isEmpty() (qthreadpool.cpp), but waitForDone() waits only until the pool was idle once (Qt's comment there: “New jobs might have started during reset”), and the painting thread's next blend refills it. Q_ASSERT is compiled out under QT_NO_DEBUG (qassert.h), so it is the Debug build's Qt that asserts. The two rows above and the “Scenario teardown before quit” rule are about releases, which onFinish must own; a geometry change releases nothing and starts asynchronous painting, which onFinish must not start, because finish() quits only kFinishDrainDelay later. Unlike the first of those rows, this death names an assertion and leaves a real dump. The row names the scenario's late geometry change as the trigger and neither claims nor rules out a product quit-time race. Observed in 2026/10/06/fit-the-rich-article-width-and-last-line-to-the-bubble (work/test.md, the A-only launches of Runs 1 and 2; stack in evidence/run-1-2-aonly-shutdown-stack.txt). Make the change in a teardown stage while the runner is still stepping: run sets the geometry (in the source run, back to the start geometry) and until waits until the window has it plus 1.5 s; keep releases in onFinish, as the two rows above require. Like every teardown stage it is skipped after a stage timeout or the watchdog, and that path was not exercised. Repaired that way from Run 2's B-only launch of the same campaign: without it 2 of 5 launches died this way (both A-only launches, not Run 1's FINAL and B-only or Run 2's FINAL), and with it both launches exited 0 (Run 2's B-only and Run 3's FINAL); 1.5 s is what held there, not a measured bound. The stack, not the launch count, ties the death to the late geometry change.
Media reading frozen at position 0, with the length equal to the document's declared duration Undecodable fixture document — often a synthetic upload left on the shared test account — accepted on metadata alone; the app debug log shows Streaming Error: Error in avformat_open_input. Select the fixture with the playability probe: play each candidate and accept only one whose position strictly advances, then reuse that document everywhere.
A premise fails against a row its own fixture had to create, or a check passes without ever reaching its subject The oracle read the probe's whole history, or bracketed a slice by wall time, so rows from an earlier stage or a slow neighbouring surface answered it. Record through Test::Probe, take mark() immediately before the action, and query only ...Since(mark); there is no whole-history accessor to fall back to.
A sweep reports a confident found=0 that no repair ever changes The enumeration structurally cannot reach the subject, so the zero was guaranteed before the run started and measures nothing. Count through Test::DiscriminatingScan and feed it a known-present control; report() refuses to certify a zero the walk cannot tell from absence.
A wallet settle that "completed" while nothing was refreshed, read later as a stamp age equal to the observation's own timestamp Wallet::Session::refreshHistory(done)'s callback says nothing about the server's wallet state: it runs at once when the presence is not Ready or the lane is paged, and otherwise queues on _historyDone, which finishHistoryWaiters() drains when the wallet.getTransactions it issued answers or fails — a transaction-history round trip that stamps _historyRefreshedAt, never _stateRefreshedAt, which only the state lane writes — applyState(), and applyEngineUpdate(), which cannot run while the engine holds no client. The queued callback is not even guaranteed a round trip of its own: requestTransactions() issues nothing while one is already in flight, so the drain then comes from that other request, and setPresence() drains the same queue with no round trip at all on any transition away from Ready. So a completion certifies no wallet-state freshness stamp; an unwritten stamp then reads crl::now() - 0. Gate on the state, not the callback: Test::WalletRefreshSettled over Test::ReadWalletRefresh, which reports a never-written stamp as unstamped and never as ready. A refusal there is a fixture gate — a harness or environment TEST_FLAW, never a product FAIL — unless the diff under test touches the wallet state lane itself, which is exactly refreshState() / requestState() / applyState() / applyUpdate() / clearNetworkState() and nothing wider, in which case the readiness condition is the behavior under test and the refusal is a product FAIL.
A funded-wallet leg gated off by signingIsServed=0 (the fixture's phrase does not sign for the served key) while the device's current custody record signs for the served key and the address gate passed The golden test_gram_account.txt predates later rotations of the golden wallet: a campaign rotated the key and was cut off before RewriteGramAccountWords, or the file was restored from an older copy. The fixture is stale, not the product. Run the P0 helper right after the address gate - Test::AppendGramReconcile / Test::ReconcileGramAccount (test_gram_reconcile.h) - and gate the funded legs on GramReconcileResult::reconciled(); it rewrites the file from the local reveal (case (b)) or refuses by a named gate. Never hand-roll the reveal.
A Money-window controlled send's press stage times out with send-job-queued and no send-job-entered, the engine worker reading started > finished (worker=[queued=4 started=11 finished=10]), while the Debug log shows a registered toncenter.performApiRequest that is never answered and is cancelled exactly when its 15 s timeout expires; or, at teardown, wallet.sendTransfer data_normal followed by adding request to toSendMap, answered 400 WALLET_TRANSFER_SEND_FAILED Suppressed, unanswered provider calls each hold the single engine worker in StatuslessHost::execute_statusless until their 15 s timeout - the send box's comment-recipient resolution and its three retries 1 s apart, the fee preview, and the fixture's first collectibles poll (refresh_nfts) - so a later press queues behind all of them. The teardown case is a fixture that disarmed before the queued send's routed wallet.sendTransfer was issued on main, so nothing intercepted it. Observed in 2026/10/02/keep-the-sending-row-one-stable-row-until-the-final-row (work/test.md, Run 1), after four gram-wallet campaigns had each repaired the stall by hand, differently. Use test_wallet_intercept.h: AppendWalletEngineDrain before every send box (it leaves the release on through the box and the press), CheckNoEarlierProviderCallHeld at the press, teardown() (seal first) from the teardown stage and again from Runner::onFinish, and the fixture route fixed when the send is queued. Never wait the provider timeouts out; that is the forbidden technique. Confirm the instrument with AppendWalletInterceptSelfTest.
A hand-rolled secrecy scan reports a confident zero while it read no app log and no DebugLogs part, or reports "leaks" of short secrets or phrase words that the client never wrote; or CheckSecrecy fails no client-written fixture secret on a plain-line short-secret hit that is a value the product computed and logged itself Four traps: (1) selecting logs "modified since the scenario started" picked no app log or DebugLogs part on Windows, because a file the running process holds open keeps its old last-write time; (2) matching a short secret as a plain substring over the very large number of lines in a campaign's MTP dumps gives coincidental hits inside longer, unrelated values; (3) even a bounded match inside a dump can be content that the server sent (Recv), not anything the client wrote; (4) a value the product computes and formats into its own diagnostic line can equal the short secret, which a bounded count alone cannot tell from a leak. Test::CheckSecrecy and its checks for (1)-(3) come from 2026/09/25/add-fixture-reconciliation-and-secrecy-scan-helpers-to-the-harness, and its plain-line site report and declared computed fields for (4) come from 2026/09/27/report-the-site-of-short-secret-hits-in-the-secrecy-scan. Call Test::CheckSecrecy (test_secrecy_scan.h): it selects by identity (the part's day index, Logs::full()), counts only bounded occurrences of a short secret, decides on plain lines and Send entries only and reports Recv hits by schema site and every plain-line short-secret hit at its <file>|<head>|<field> site, and refuses an undecided class. For (4), confirm from the site row and the product source that the field is formatted from a value the product computes, never from fixture input, and declare that head and field in SecrecyScanArgs::computedFields: the hit moves to SECRECY_COMPUTED and no longer decides. Never settle it by reading the matched line. Confirm the instrument with Test::AppendSecrecyScanSelfTest.
<what>: no client-written fixture secret fails with a plainSites entry in the test log - SECRECY_CLASS: TestLog ... plainSites=[test_log.txt|NOTE: <head>|- x<n>] - on the overlay's own row head, with field - (or that field's name when the number is a field's whole value; a row with no name= field shows the withheld test_log.txt|?|? instead) A bounded digit run inside a number the overlay formatted equals a short secret by coincidence. An ordinary decimal leaves every digit run bounded by =, -, . or a blank - in value=-4821.75 both 4821 and 75 are bounded - so any of them can equal a short secret, and the more values a run prints, the likelier that is. The hit is part of a value, so its field is -. A declared computed field cannot exempt it: overlay telemetry is not a value the product computes, and a hit in part of a value still decides (Declared computed fields). Print every number the overlay measured or computed into a scanned log with Test::TelemetryNumber(value, decimals) (test_log.h), the rule from 2026/10/08/give-overlay-telemetry-a-number-format-the-secrecy-scan-cannot-count: a p for the point, so every digit run touches a letter - -4821.75 prints -4821p75 and 42 prints 42p - and no digit run is ever bounded. Choose decimals per quantity, never from a secret; a judge script reads a value with float(v.replace('p', '.')). A TelemetryNumber is a word to the phrase matcher - it ends a word run where a bare number is neutral - so never print one as a list index or between words copied from product or fixture text; keep such text out of telemetry rows. Within those limits, changing only how measured numbers are formatted cannot remove a real short-secret leak at that site, so a scan that passes after only the format changed, with its word-run and token counts unchanged, has told a coincidence from a leak. Never exempt the hit (a computedFields declaration for the overlay's own row, or a filter), move telemetry out of the scanned logs, choose a format from a secret, pass a fixture secret or a number read from one through TelemetryNumber, or settle the hit by reading the matched line. Confirm the rule with the secrecy_self_telemetry stage of Test::AppendSecrecyScanSelfTest.
<what>: no client-written fixture secret fails with a plainSites entry in the test log on a shared helper's row rather than on the overlay's own: a GEOMETRY: <name>: head (from LogGeometry, a capture helper or a saved PreparedWidgetCapture), a NOTE: <name>: head of a kind=post, kind=stop, kind=frame or examined= row, a probe check's details, a clocked gate or refusal row, or the withheld test_log.txt|?|? of a row with no name= field, such as a CheckNear verdict A bounded digit run inside a number the helper measured, counted or timed equals a short secret by coincidence, exactly as in overlay telemetry: the helper printed ordinary decimals because the run did not ask for the telemetry format or because the helper is not converted. The overlay cannot reformat the row without reimplementing the helper, which the reuse rule forbids (an overlay that reimplements a shared facility is a test flaw), and a declared computed field cannot exempt it: a helper's number is not a value the product computes, and a hit in part of a value still decides (Declared computed fields). Converted helper, run did not ask - the row is one the test_log.h (TelemetryNumber, RequestTelemetryNumbers) catalog row lists, and no TELEMETRY_NUMBERS: requested line precedes it: call Test::RequestTelemetryNumbers() (test_log.h) unconditionally at the campaign's first stage and re-run. Changing only the number format cannot remove a real short-secret leak at that site, so a scan that passes after only that change, with its word-run and token counts unchanged, has told a coincidence from a leak. When the run asked and the hit is still on a converted row, it lies in caller text (a probe row, a check's what, a scan detail), which the caller formats like its own telemetry, or in a text the helper formatted before the ask, such as a kept RoundTrip::observation; ask at the first stage. Helper not converted - for example PostPaintSampler::details(), test_capture's own readings, test_runner's rows or any other module's rows: convert that helper the same way, as harness work - a pure formatter taking a NumberFormat that its logging path calls with HelperNumberFormat(), its case in the secrecy_self_helper_numbers stage of Test::AppendSecrecyScanSelfTest, and its README grammar in both formats. A local copy of the helper is never the repair. Forbidden: everything the overlay-telemetry row forbids - exempting the hit (a computedFields declaration for the helper's row, or a filter), moving telemetry out of the scanned logs, choosing a format from a secret, passing a fixture secret or a number read from one through TelemetryNumber, and settling the hit by reading the matched line - plus a local re-implementation of the helper, and asking for the format under a condition or from code that read a fixture secret. Caller numbers: CheckNear's actual, expected and tolerance, Probe::checkCountSince's expected and RunClockedFrames' elapsed and animationEnd print in the requested format too, which hides digits from the scan, so they must never be a fixture secret or a number read from one. Confirm the route with the secrecy_self_helper_numbers stage of Test::AppendSecrecyScanSelfTest.
A green log that does not say what its checks were made against, so a passing run cannot be audited after the fact The reading was handed to Test::Check as details back when details was written only on the failing branch, or worked around by folding it into what or by emitting a Note beside the check that a reader then has to re-correlate by position. Pass the reading as Check's third argument: it is printed on the passing verdict too, as TEST_RESULT: PASS: <what> - <details>. Keep only failure-only text behind ok ? QString() : ..., which still prints the bare passing line.
A round trip reported as a negative or otherwise impossible number, or a pair count that does not match the issue count Two lists were related by position - an issue list against an answer list, or a row list against a parallel crl::time vector indexed at mark + i - so one extra or missing element on either side paired a row with another row's time, and the reading was emitted as a Note that failed nothing. Record both sides into one Test::Probe with recordIssue/recordAnswer and read them through checkRoundTripSince(mark, key): it pairs by key, discards and names every answer not strictly later than its issue, and refuses as a FAIL carrying the tallies rather than reporting an interval it did not positively pair. Read a bare time through timedRowsSince, which carries each plain row's own time.
A capture or captureWidget stage times out and its details carry render root paints no background of its own: ... - grab N...BoxLayerWidget... instead (unpainted 0/1000) The render root was the box, and that box had cleared Qt::WA_OpaquePaintEvent. Ui::BoxContent's constructor sets that attribute, so a plain box paints its own background and is accepted - but setNoContentMargin(true), which 53 product call sites use, clears it again, and then the box paints no background of its own and PreparedWidgetCapture::prepare() refuses every frame it is offered; the poll can only end in a timeout. Resolve the root with Test::PaintingLayerRoot(box) and capture the Ui::BoxLayerWidget it answers — Test::CaptureBoxLayer(box, name) is that whole-shell grab, including _title and the addButton footer row parented onto the shell — or use Test::CaptureInLayerRoot(box, name) for a frame cropped to the box content (it composes CaptureMappedRect, so a box that maps outside its layer is still a named FAIL). The refusal is naming the right widget - do not widen it.
A menu capture times out with showingContent=0 in its last state, or the popup vanishes mid-stage; or, on a locked Windows console, capture painted widget: <name> times out with last state: target is not visible: class Ui::PopupMenu ... right after the stage that opened the menu passed The readiness waited on the inner Ui::Menu's visibility. Ui::PopupMenu::startShowAnimation() calls hideChildren(), and the children come back only from the final paintEvent's Ui::PostponeCall - a side effect no -testagent run is guaranteed to reach - so the wait can last until the popup dies of a focus-out. A one-shot grab in the turn that opened the menu is the opposite failure: it lands on a show-animation frame with nothing drawn. A locked Windows console is a diagnosed cause of its own: the app is deactivated, so Qt closes a shown Ui::PopupMenu before the next turn - QApplication::notify closes every open popup on QEvent::ApplicationDeactivate (Qt 5.15.19 closeAllPopups() in qapplication.cpp) - and CapturePopupMenu, which needs the menu open across turns, times out on target is not visible; Test::OpenContextMenu still reads the menu in the turn that built it. Observed in 2026/10/07/add-a-same-turn-context-menu-reader-and-a-retried-clipboard-round-trip-to-the-test-harness (work/test.md, Attempt 1): in Run 1 popup menu self-test: open passed 6/6 and capture painted widget: popup_menu_open timed out, which ended the scenario before the self-test's five later stages, while the reader's positive arm read appState=active at build and visible=0 hidden=1 activePopup=none ... appState=inactive one turn later and the reader's self-test passed 22/22; Run 2, the popup-capture self-test alone on the same locked console, reproduced the timeout; Run 3, unlocked, passed both frames. Capture with Test::CapturePopupMenu: its readiness carries content identity only - the menu exists, is visible, has non-empty geometry and carries actions - and the prepared capture's own blank-frame refusal decides when the frame is good. showingContent belongs in the details, never in the gate. Do not use Test::PaintingLayerRoot on a popup: it sets Qt::WA_NoSystemBackground, so the blank-root refusal never applies, and it is its own window. On a locked Windows console, read the console with Test::ReadConsoleLock() (test_console_lock.h) and gate the stages that need the menu open across turns through CapturePopupMenu's skipReason ([] { return Test::ConsoleLockGate(Test::ReadConsoleLock()); }). AppendPopupMenuCaptureSelfTest does exactly that: its open stage Notes the CONSOLE_LOCK row it read right before it popped the menu; the popup_menu_open capture, close, and the popup_menu_reopened opener and capture are TEST_RESULT: N/A naming the lock and printing the reading, read when each begins and kept once locked, because a menu popped or held while the console was locked is already closed; and open (decided from the opening turn), refusal text and the teardown run and decide, so the scenario continues past it. The rule: for these arms a locked console is a scheduling condition - rerun them on an unlocked console. This applies to Windows: on macOS a packed launch with CGSSessionScreenIsLocked true ran AppendPopupMenuCaptureSelfTest with 0 FAIL (2026/10/06/add-a-post-paint-capture-sampler-to-the-harness, work/test.md, Test 2) - a locked macOS console does not close the popup - so Test::ConsoleLockGate answers only in a Windows build: on macOS the probe reads the lock and the open stage prints it, and no gate fires there. The gated rows are skipReason N/A rows rather than a hard fixture gate: Check like this self-test's translucency premise (the AppendSeparatePanelWalkSelfTest row below): a host without translucent windows can never certify that premise, so a skip there would withdraw a certification the host can never give, while a locked console is a scheduling condition on a host that certifies the capture once unlocked (Run 3); the harness already skips AppendMainWindowNotMarkingReadSelfTest's deciding half when its control cannot read markingAsRead true on a locked console the same way; a Check could not stop the next stage from timing out; and an N/A row never counts as a pass, is counted in SCENARIO_RESULT's skipped clause and names the lock and the probe reading.
A context menu is never observed: an overlay delivers a QContextMenuEvent and polls on later event-loop turns for a visible, ready Ui::PopupMenu, and finds none for any target, the controls included - popup=0 actions=[] on HistoryInner, HistoryView::ListWidget and Iv::Markdown::MarkdownDocumentWidget alike, elapsedMs~3020 per chat-list leg, for an ordinary keyboard URL button and a plain paragraph as well Observed in 2026/10/07/show-url-tooltips-and-copy-link-on-rich-message-url-buttons (work/test.md, Attempt 1 Run 1, Tests 2-4); its app log showed the main window re-activated once per menu leg. Each product handler builds its menu synchronously, inside the event, as a child of the widget that received it - HistoryInner::showContextMenu (base::make_unique_q<Ui::PopupMenu>(this, ...)), ListWidget::showContextMenu through FillContextMenu(this, ...) (base::make_unique_q<Ui::PopupMenu>(list, ...)) and MarkdownDocumentWidget::contextMenuEvent (_contextMenu = base::make_unique_q<Ui::PopupMenu>(this, ...)) - so the menu exists in the turn of the event; on that unattended console it was gone before the first poll, and the run did not diagnose why. Nothing guarantees that a popup outlives its turn: Ui::PopupMenu::focusOutEvent hides it once focus leaves its focus chain, QApplication::notify closes every open popup on QEvent::ApplicationDeactivate, and a menu left at deleteOnHide(true) then deleteLater()s from hideEvent - candidate routes, not a diagnosis. Read the menu in the turn that built it with Test::OpenContextMenu(receiver, point) (test_menu.h): it snapshots every live Ui::PopupMenu in the application, sends the event inside Test::Settle and, in that same call, takes the one new menu - its ordered action texts and its lifecycle at build and after the postponed calls settled - or refuses by name (no-menu, several-menus, receiver-gone); trigger an entry with Test::TriggerContextMenuAction, which delivers its queued Ui::Menu::CreateAction callback before it returns. Run 2 of the same campaign decided every menu leg on four surfaces by reading in the building turn, built=1 fresh=1 and ready=1 at build. A no-menu reading counts as a product result only beside a positive control that the same helper built in the same widget - the source run's ordinary URL button and plain paragraph on the chat lists, and its Docs button in the window and the box (a window / box no-menu reading is N/A by name unless the Docs control was built in the same widget). A frame stays with Test::CapturePopupMenu, a supplementary leg taken only while the menu is still open. Confirm the instrument with AppendContextMenuReaderSelfTest.
A stage times out although its own details show every precondition met and the action already fired - for example box=1 toggle=1 popups=0 after a click on a control in a freshly shown layer One synthesized click on a control in a layer that is still settling intermittently does not land, and nothing observes that until the stage's 10-second timeout ends the whole scenario. The input helpers are not at fault: Test::Click and Test::Drag deliver press, release and a QEvent::Leave synchronously and drain every Ui::PostponeCall before returning. Make the click self-correcting in the scenario, not in the helper: read the effect, click the same QPointer-guarded widget once more if it did not appear, and Note which attempt worked so the flake rate stays visible. Establish first, from the production callback, that a repeat is provably a no-op - the details-menu toggle's first statement is if (*menu) { return; }.
A focus-routed affordance produces nothing at all — a submit that formed no request, a field that never took focus, focused=0 / isActiveWindow=0 — with no error and no event, especially on a locked console or on macOS after a default test-run launch, which does not activate the client there The platform window is not active - which is what makes QWidget::isActiveWindow() false, because it falls back to QPlatformWindow::isActive() (qwidget.cpp) - so QWidget::setFocus() never promotes the target to QApplication::focusWidget(); clearing only the QPA focus window does not reproduce it where the OS window is still active. Window::Controller::activate() asks the window manager, which on a locked console does not comply. Assert it with Test::ForceWindowActive() before the action. A one-shot activation expected to survive across polled turns is a forbidden technique, because the platform de-activates asynchronously between event-loop turns: re-assert it on every poll of the bounded wait, where the helper caps its note to about every tenth call and reports the attempt count at pass time. This is the one documented exception to the until purity rule above — it mutates only which window Qt considers focused, is idempotent, and encodes no expected product result. Confirm the instrument with the test_activation.h self-test. The other side of this fact is a window that is active and exposed and marks messages read: that is the KeepMainWindowNotMarkingRead row below, not another use of ForceWindowActive.
An injected or fixture message is read the moment its chat is shown, an injected incoming item's notification never appears, or HistoryInner::checkActivation aborts on IsServerMsgId, next to markingAsRead=1, with screenLocked=0 on an unlocked console or screenLocked=1 on a locked one The main window is exposed and either active in Qt's sense - Test::ForceWindowActive, the scenario's Window::Controller::activate() or another product flow, a test-run --activate launch, or the owner's click - or under auto-scroll-inactive-chat, so MainWindow::markingAsRead() is true. It reads no lock state, so a locked console marks read the same way while those terms hold, and a window shown behind the lock screen keeps a stale isExposed(); screenLocked is Test::ReadConsoleLock()'s reading of the console itself (on macOS the session's CGSSessionScreenIsLocked, which also sees a lock taken before the launch, unlike Core::App().screenIsLocked()), so screenLocked=0 no longer hides a console locked before the launch. On macOS a default test-run launch no longer activates it. Clearing the QPA focus window does not help while auto-scroll-inactive-chat is on, and QWidget::hide() makes CaptureWidget refuse the window. This is the other side of the activation row above: there the window is not active, here it is and it marks messages read. Call Test::KeepMainWindowNotMarkingRead, and call it again after Window::Controller::activate() or any non-background showHistory. Confirm with AppendMainWindowNotMarkingReadSelfTest.
A window reads isExposed() false (exposed=0) and gets no product paints - a Test::PostPaintSampler with no rows, a paint-stamped product timer that never starts, a frame that never advances - while another application is frontmost (test-run's frontmost_app), with or without input during the run (input_during_run), the screen not locked On macOS test-run's default background launch does not activate the client (launch_activation: "suppressed", launch_method: "background"), so its windows open behind the frontmost application's. A window covered from the moment it is shown keeps reading isExposed() true until the window server first reports an occlusion change for it - for example when RestoreWindowExposure drops it back behind, or when the owner rearranges windows - so exposed=1 is no proof that it is on screen. Once the window server reports a covered window occluded, Qt's cocoa plugin delivers an empty expose (QCocoaWindow::windowDidChangeOcclusionState calls handleExposeEvent(QRegion()), qtbase/src/plugins/platforms/cocoa/qcocoawindow.mm), so isExposed() turns false and the widgets' repaints are held - their dirty regions kept, not painted - until the window is exposed again. Observed, before that default, in 2026/10/07/animate-the-sending-row-for-a-collectible-transfer (work/test.md, Attempt 1). Run 1 launched without --wait-idle while the console was in use (input_during_run=true, idle 0.19 s before the launch and 0.01 s after, frontmost cmux both times, screen not locked): its fixture gate the Money window shows the controlled head page in an exposed window read pages=1 exposed=0, and every later stage was N/A. Run 2 launched with --wait-idle 20 --wait-idle-max 120, which was not met after 120 s, and input arrived again (input_during_run=true); its overlay set Qt::WindowStaysOnTopHint on the panel's QWindow right after showing it, activating nothing, and waited for isExposed() in a pure until: the gate read exposed=1 and the leg ran end to end (50 in-process PASS). Call Test::KeepWindowExposed on the window right after it is shown, then wait for isExposed() (ReadWindowExposure(...).exposed) in a pure until; call it whatever the window reads before it, since a window covered from the moment it is shown can read exposed=1 there. Undo with RestoreWindowExposure before teardown and before reading anything the product derives from the hint, such as the call window's pin (Calls::Window::pinnedOnTop() reads its QWindow's flags); while applied the window floats above every application's ordinary windows, so keep it no longer than the paints are needed. Never activate the app or take focus (Window::Controller::activate(), activateWindow(), raise(), requestActivate(), Ui::SeparatePanel::showAndActivate()) - the owner's keystrokes would go into the test client - and never QWidget::setWindowFlag on a shown widget, which re-parents and hides it. Launch with test-run --activate instead only when OS activation is itself the subject. The --wait-idle gate does not change the occlusion. A minimized window is refused by name, since stacking cannot expose it. Marking read: isExposed() is one term of MainWindow::markingAsRead() (see test_marking_read.h), so on an unlocked console exposing the primary window with auto-scroll-inactive-chat on can make it mark messages read. Confirm with AppendKeepWindowExposedSelfTest.
A preview-dependent check measures a message with no link preview: a fixture gate: or premise row reads the injected item's media as not displayed (media=0), the bubble is only as large as its text, items drawn as outgoing fail like incoming ones, and the same item may show its preview later in the launch The item's out() is false and its chat's bar settings are not known yet. HistoryItem::hideLinks() is !out() && history()->peer->hideLinks() (history_item.cpp); PeerData::hideLinks() holds while barSettings() is unknown or has ReportSpam (data_peer.cpp). MediaWebPage::createView() then makes no view and calls setHasHiddenLinks(true) (data_media_types.cpp), and HistoryItem::translatedTextWithLocalEntities() drops the text's Mention, Hashtag, Cashtag, Url and CustomUrl entities (its isUrl filter in history_item.cpp), so a link hit test on the text misses too. It keys on the flag, not on the bubble's side: NewMessageFlags() leaves MessageFlag::Outgoing off for a self peer (history_item_helpers.cpp), while Message::hasOutLayout() draws every non-forwarded Saved Messages item as outgoing (history_view_message.cpp), so there every item injected with NewMessageFlags() loses its preview while the settings are unknown. When the settings arrive, PeerData::setBarSettings() queues History::refreshHiddenLinksItems() (data_peer.cpp, history.cpp), which rebuilds the marked items' views, so a reading can flip mid-run. A user's settings come from ApiWrap::requestPeerSettings(), the full-user answer or updatePeerSettings; ApiWrap::requestFullPeer() requests them separately only for a non-user (apiwrap.cpp). Unlike the markingAsRead=1 row above, where the active window marks an injected incoming item read, no window state is involved here: the item keeps its text but loses its preview and its text links. Observed in 2026/10/06/fit-the-rich-article-width-and-last-line-to-the-bubble (work/test.md, Run 1, the B-only launch): both preview gates read rich media=0 plain media=0, and the same twins showed their previews in later stages. Before injecting, request the settings when barSettings() is unknown (ApiWrap::requestPeerSettings, a read) and wait for them in until, bounded at 30 s, in a stage whose timeout is above that bound (the source run used 50 s; the default kDefaultStageTimeout of 10 s would end the run as a stage timeout before the gate is written); add a fixture gate: row that they are known and the peer's hideLinks() is false; and make every preview-dependent row first assert that its media is displayed, writing it N/A through Test::Skipped otherwise (see “Media fixtures and fixture gates”). Repaired that way from Run 2 of the same campaign: the gate passed in all four later launches (barSettings known=1 hideLinks=0 after 66-101 ms), but the chats list loaded within 4 s in each, so none re-created Run 1's condition and the request was not measured without a loaded chats list. The gate is what keeps an oracle off a text-only bubble: opening a chat in HistoryWidget already requests its settings (ContactStatus, PaysStatus), and requestPeerSettings keeps one request per peer in flight.
A notification check finds nothing delivered although the item was injected or received - no toast, no native row, NativeManager::doShowNotification never runs - the notification system skips the item (System::schedule returns at its skipNotification reading, SkipState::Skip), and the chat reads muted (isMuted=1, history->muted()=1) with no mute_until of its own Not the product and not the window: the golden test account's default user notify settings are served mute_until=2147483647, which 777000 and every private chat without a mute value of its own inherit, so Data::NotifySettings::isMuted reads true and System::computeSkipState answers Skip. Observed in 2026/10/04/show-hidden-sender-gram-transfers-in-the-service-chat (work/test.md: Run 1 recorded its delivered-notification leg N/A by fixture gate G-notify: 777000 muted; Run 2 reached it through a hand-written local unmute in its overlay). Distinct from the markingAsRead row above, where the window reads the item the moment its chat is shown; here the chat itself reads muted. Override the chat locally with Test::OverrideNotify(runner, peer) (test_notify_override.h; the default UnmutedSilentNotify is unmuted with no sound), restore at the window's end and again in teardown (the module's own Runner::onFinish restores or verifies once more), treat a settings-unknown refusal as a fixture gate, paste the InterceptNotifyRequest hook into the overlay and prove locality with Test::AppendNotifyLocalityCheck. Never unmute through Data::NotifySettings::update, resetToDefault, defaultUpdate or clearExceptions: each reaches ApiWrap::updateNotifySettingsDelayed, which sends account.updateNotifySettings after kNotifySettingSaveTimeout (1 s) and changes the shared test account's served settings for every later run on every checkout. Confirm the instrument with AppendNotifyOverrideSelfTest.
A stage times out while the button it aims at is plainly on screen, and FindAll<Ui::RoundButton>(<box content>) reports zero buttons Ui::BoxLayerWidget::addButton re-parents the button onto the shell (box_layer_widget.cpp), so a footer button is a direct child of the shell and not a descendant of the published box content. Click it with Test::ClickBoxButton(box, label), which roots the search at Test::PaintingLayerRoot(box): gate on Test::BoxButtonReady in until and click in then, and pair it with the self-correcting repeat of the settling-layer row above. A label no shell button carries is a named refusal listing every label seen, never a silent no-op.
ReadBoxButtons / BoxButtonReady / ClickBoxButton in the turn the box was resolved names every shell button (hidden), or BoxButtonReady is false, while the box is plainly being shown and a later stage reads and clicks the same button LayerStackWidget::prepareForAnimation() calls layer->hide() on the current layer (ui/layers/layer_widget.cpp) and animationDone() calls layer->show() again. The stack's BackgroundWidget paints the animation from cached pixmaps while that hide holds, so every descendant of the Ui::BoxLayerWidget — the _title label, the content, and the re-parented footer buttons — answers QWidget::isVisible() false. ReadBoxButtons records that as (hidden) (a merely disabled footer included, because !visible wins over isDisabled()), and BoxButtonReady / ClickBoxButton are correct to refuse: a click then really would be the silent no-op those helpers exist to remove. anim::type::instant skips this hide; the product default anim::type::normal does not. Take the judgement on a bounded readiness wait in until (Test::BoxButtonReady) rather than as an assertion in the turn the box is resolved. Do not relax BoxButtonReady's visible-and-enabled requirement. Confirm with the test_box_button.h self-test. Distinguish this whole-layer hide from a product-hidden or disabled footer (BoxButtonHidden / BoxButtonDisabled after the layer has shown).
Every busy reading reports no matched button (matched=0 / buttonFound=false) while the footer button is plainly on screen and other clicks in the same run work ReadBoxButtons refuses to match a disabled or hidden shell button on purpose — a click on one is the silent no-op the helper exists to remove — so matched() is false and ClickBoxButton Fails. The only previous answer was a formatted labels entry "Save (disabled)" / "Save (hidden)", which a caller either string-matched or read as "the button is not there". Read present / disabled / hidden on the same BoxShellButtons reading, or BoxButtonDisabled / BoxButtonHidden beside BoxButtonReady. matched() stays false and ClickBoxButton still Fails. Confirm with the test_box_button.h self-test. Do not parse labels for a boolean and do not treat matched()==false as absent.
A capture of a visible widget is blank or refused at perfectly sane geometry — target grab still looks blank or render root paints no background of its own — and the target is a toast or another fade-in wrapper Ui::Toast::internal::Widget::paintEvent (toast_widget.cpp) draws its whole frame into a transparent proxy at the fade-in opacity, so the grab holds the harness base and nothing else. For a toast, capture with Test::CaptureToastSubtree(toast, name) (test_toast_capture.h) gated on Test::ToastSubtreeReady — its settled-show term is this module's own answer to the same blankness, and its frame cannot compose the surface beneath; never Test::CaptureViaWindow, which would (see the row below). For another fade-in wrapper that is a child widget - a Ui::ImportantTooltip is one: RpWidget(parent), painting its cached frame at the animation opacity - capture with Test::CaptureViaWindow(widget, name), which grabs the widget's window cropped to the widget's rect mapped into it. A blank mid-fade frame is a Note and never a FAIL, because the decisive oracle is the joined accessibilityName of the toast's Ui::FlatLabels and the capture is corroboration. Second fact for the same target: only inside a synchronously drained input helper, which starves hideAnimated(), a toast's fade-out never completes, so toasts persist far past Ui::Toast::kDefaultDuration (1500 ms) — a harness-environment effect of that drained helper, never a product defect to “fix”. Across the runner's real event-loop ticks the manager's own hide timer still fires and takes an ordinary toast down; see the live-toast walk row below. A Ui::Tooltip is not a fade-in wrapper, and CaptureViaWindow refuses it: it is constructed as RpWidget(nullptr) (Telegram/lib_ui/ui/widgets/tooltip.cpp), its own top-level Qt::ToolTip window, so ReadViaWindow answers target is its own window, so there is no opaque window behind it to grab. Find it in QApplication::topLevelWidgets() with dynamic_cast<Ui::Tooltip*>, keeping only a visible one (Tooltip::Hide() hides the instance and deletes it from a queued deleteLater(), so a hidden one can linger in the walk), with the main window as the walk's known-present control; read its text from the public accessibilityName() (_text.toString(), the full text Tooltip::popup() sets, which it paints elided); and capture it with Test::CaptureWidget: it sets Qt::WA_NoSystemBackground, which exempts it from the blank-root refusal, and paints its own textBg background and its text with no fade animation. Observed in 2026/10/07/show-url-tooltips-and-copy-link-on-rich-message-url-buttons (work/test.md, Attempt 1 Run 1), whose test log read that refusal for class Ui::Tooltip 878,549 703x60 and saved the tooltip through CaptureWidget.
A toast capture is perfectly legible and shows the sheet, box or row underneath it: words, a hint or a row the toast is not about, readable through the toast, and its four corners holding whatever was behind them Test::CaptureViaWindow grabs the toast's window cropped to the toast's rect, and st::toastBg is #2c3033e5 (ui/colors.palette) — alpha 0xE5, about 90% opaque — so about a tenth of every pixel the product painted underneath composes into the frame. The corners are worse: the rounded background is _roundRect.paint(p, rect()) (Widget::paintBackground → Ui::DrawRoundedRect → Images::PrepareCornersMask), whose corner mask is transparent outside the arc, so the rect's four corners are not covered at all and the surface beneath shows there unblended. Capture with Test::CaptureToastSubtree(toast, name) (test_toast_capture.h), which grabs the toast subtree itself — Ui::GrabWidgetToImage fills every pixel the toast leaves uncovered with st::windowBg->c, so the frame holds the harness base and never the product surface. Gate on Test::ToastSubtreeReady and read the phrase through Test::CheckToastReads. A root that is not the live toast, or a rect outside its geometry, is a named FAIL quoting both rects — never a silent reframe and never a quiet no-op. Resolve the toast with Test::FindLiveToast(); Ui::Toast::internal::Manager enumerates nothing.
A check read Test::FindLiveToast() as null and concluded no toast was raised, or a reading taken after a longer settle found an empty walk and treated that as a confident absence Test::FindLiveToast() is return (all.size() == 1) ? all.front() : nullptr (test_toast_capture.cpp), so one nullptr answers both zero live toasts and more than one — a null-versus-non-null assertion cannot tell "no toast was raised" from "two toasts were". Separately, the persist fact in the fade-in row above holds only inside a synchronously drained helper; across the runner's real event-loop ticks Ui::Toast::internal::Manager still arms _hideTimer for _hideAt, hideByTimer calls Instance::hideAnimated(), and the completed fade reaches Instance::hide(), so a reading taken on a longer settle finds an empty walk and reads as a confident absence of a toast that was raised correctly. Record Test::FindLiveToasts().size() and every live toast's text (Test::ReadToastText) in the check's own details, which print on the passing verdict too. Take the reading inside the toast's lifetime on Test::ToastSubtreeReady rather than on a longer settle. Do not treat FindLiveToast()'s nullptr as "no toast" without that count and those texts.
focused=0 for a field that is plainly focused, often beside focusWindowSet=1 focusWidget=QTextEdit Ui::InputField declares its own non-virtual bool hasFocus() const returning _inner->hasFocus() (input_field.cpp), and QWidget::hasFocus() is non-virtual too, so a read through the QWidget* a generic finder returns answers for the wrapper, not for the inner editor that holds the focus. dynamic_cast<Ui::InputField*> the resolved widget and call its own hasFocus(), or test QApplication::focusWidget() for descendancy of the field. Ui::PasswordInput is a QLineEdit subclass with no wrapper, so a direct read there is already right.
A passcode / cloud-password box accepted Enter and produced no request: no SRP computed, no API call, no error PasscodeBox::submit() (passcode_box.cpp) is five per-field hasFocus() branches and nothing else; in a Qt-inactive window every branch reads false, Enter falls through and save() never runs. Submit through the box's own shell button with Test::ClickBoxButton — lng_passcode_submit when a caller set customSubmitButton, otherwise lng_settings_save or lng_passcode_remove_button — whose handler calls save() with no focus dependency; assert activation first, per the activation row above. Two of those branches are irreversible on a live account: lng_passcode_remove_button is the turning-off submit that disables the cloud password (PasscodeBox::prepare), and a customSubmitButton is tr::lng_theme_delete() on the account self-destruction flow (AddDeleteAccount in self_destruction_box.cpp). Place that click last in the scenario, per step 3 of Building one reliable packed scenario above. PressKey(Qt::Key_Enter) is a last resort only, and log which path submitted (submittedVia=) so a fallback stays visible.
descriptionRead=0 for a PasscodeBox whose description the product plainly shows, while the same run's title read-back answers titleLabelRead=1 PasscodeBox renders CloudFields::customDescription (passcode_box.h) into Ui::Text::String _about (passcode_box.h, set in PasscodeBox::prepare) and draws it from the box's own paintEvent (_about.drawLeft(...)). No FlatLabel exists for it, so every label and accessibility read-back returns nothing on a correct product. The title is different: BoxContent::setTitle reaches Ui::BoxLayerWidget, which creates an object_ptr<Ui::FlatLabel> _title (box_layer_widget.h, Ui::BoxLayerWidget::setTitle), so a title read-back does work. Decide it in process from the box's own construction seam: publish the comparison of fields.customDescription against the expected tr:: phrase through Test::CheckTextReads and record the boolean as a row (descriptionIsRestore=1 descriptionIsPhrase=0), keeping the label read-back as a detail only. The row is about the description alone; a title read-back needs no such repair.
Text was typed and nothing was inserted: the field's own text stays empty and changes() never fires, even with focused=1 focusWidget=QTextEdit The events were aimed at the Ui::InputField wrapper, which does not override keyPressEvent (input_field.h), and Qt propagates an ignored key event up the parent chain and never down into a child - the QEvent::KeyPress case of QApplication::notify re-delivers it to w->parentWidget() until it is accepted or a window is reached - so the keys reach the wrapper's ancestors right up to the primary window and never the inner QTextEdit that owns the text. Type into dynamic_cast<Ui::InputField*>(widget)->rawTextEdit(), fall back to Test::CommitText on that same editor when the text stays empty, and prove insertion by reading the field's own getLastText() afterwards.
A text oracle fails on a date, time or number whose two sides read identically in the details The formatter's U+202F or U+00A0 against the read-back's U+0020. Ui::Text::String's block parser maps every space-class character except U+00A0 to U+0020 (text_block_parser.cpp, replaceWithSpace), so a Ui::FlatLabel fed langDateTime()'s narrow no-break space before AM/PM reads back a plain space through accessibilityName(), while a U+00A0 survives the parse. Compare through Test::CheckTextReads / Test::NormalizeSpaces (test_text_reads.h): they map U+202F and U+00A0 to U+0020 on both sides and print both raw strings' distinct U+XXXX whitespace tokens on the passing verdict as well as the failing one. Never loosen digits, month or AM/PM.
A check is missing from the log with no PASS and no FAIL The stage's gate read false and it ran as a hand-rolled no-op - run and then skipped, until answered true - so it wrote nothing at all. Distinct from the stages a timed-out stage or the watchdog skips (see “Scenario teardown before quit” above), which leave the timeout's own FAIL behind them. Give the stage a skipReason (test_runner.h): a non-empty return emits TEST_RESULT: N/A: <stage> - <reason> in the turn that begins it, counts in SCENARIO_RESULT's skipped clause and in the test-run report's skipped list, and never waits or times out. Confirm the instrument with the test_gated_stage.h self-test.
A language-restore check reports PASS against the wrong language: after the “restore” the fixture's synthetic text is still live through the product's own accessor, or an already-painted reactive label still shows it, and the row written to observe the restore passed anyway Two independent halves. Lang::Instance::fillFromSerialized (lang_instance.cpp) never clears _values or _nonDefaultValues and re-applies only the pairs that were non-default in the serialized snapshot (its closing applyValue loop), so a key that was default before the install is absent from that set and keeps the fixture's value. And neither it (which fires _idChanges only) nor switchToId on an ordinary id (its _updated.fire sits inside the #TEST_X / #TEST_0 branch) fires Lang::Updated(), the only signal Lang::details::Value (lang_instance.cpp) re-reads on and the producer every tr:: phrase funnels into (Producer<>::Combine in lang_values.h), so an already-painted Ui::FlatLabel keeps the fixture text. The vacuous PASS is the consequence rather than a second fault: an expectation rebuilt from the live producers at read time matches a restore that did nothing, so the check reports PASS against the wrong language — three rows did exactly that in run 27 of 2026/09/07/prepare-gasless-gram-transfers-for-server-selection, and only an unrelated post-teardown control caught it. Install and remove through Test::InstallLangPack / LangPackFixture::remove() (test_lang_pack.h), whose removal is switchToId + fillFromSerialized plus one empty MTP_langPackDifference for the instance's own lang code and version, which applies and resets nothing and exists only to fire the _updated notification at the end of Lang::Instance::applyDifferenceToMe. Freeze the expectation before the install through frozen(key) and never rebuild it from the live producers afterwards; checkRestored compares against those frozen bytes and against the key's frozen non-default state, so “was default before, is default again” is proved rather than “reads the same string”. Confirm the instrument with AppendLangPackSelfTest, whose LangRestoreFault::LeaveOneInstalled and ::SuppressNotification arms fail exactly the restore row and the read-back rows.
A language-pack scenario or self-test writes nothing but TEST_RESULT: N/A rows, every stage gated with “no candidate language key resolves in the generated table” and every candidate quoted as already overridden (nonDefault="Cancel" beside value="Cancel"), and widening the candidate list changes nothing The cached cloud language pack overrides every key the generated table knows, so a currently-default key does not exist to be found. The client's own log states the count: Lang Info: Loaded cached, keys: 10993, which Lang::Instance::fillFromSerialized writes from nonDefaultValuesCount (lang_instance.cpp), against kKeysCount = 10948 (lang_auto_counts.h) — two counts over different sets, since applyValue writes _nonDefaultValues unconditionally and so counts cloud keys the generated table does not know. That holds on any client that has ever downloaded a cloud pack, which is every real client and every -testagent run against a real account. The property is universal over the key table rather than specific to the candidates tried, which is why a longer candidate list cannot help and why the gate is permanent rather than flaky. Arrange the precondition instead of searching for it. Freeze the live pack FIRST — an outer Test::InstallLangPack fixture does exactly that, and its remove() restores it — then call Lang::Instance::switchToId with the identity read from the instance itself: reset (lang_instance.cpp) rewrites every _values[i] from GetOriginalValue(i), clears _nonDefaultValues, zeroes _nonDefaultSet and sets _version to 0, so every key is default by construction, and on an ordinary id it fires _idChanges only and never _updated. Assert that premise with a check of its own — Test::AppendLangPackSelfTest's first stage is that whole arrangement, and its both keys are default after the live pack was held and reset row FAILs if the reset did not take. One ordering trap for anyone reproducing the leftover-value half by hand: the snapshot to restore to must be taken after that reset, or fillFromSerialized re-applies the cloud override and the half does not reproduce.
A scenario dies inside its own ink scan: SIGABRT with no TEST_COMPLETE after a long run of PASS rows and saved screenshots, the dump pointing at std::vector::operator[]; or, with no crash at all, a candidate count read back as a plausible 0 from a reading that classified nothing ScanInk’s refusing paths returned before result.candidates = std::move(candidates) and result.counts.assign(...), so a refused reading carried ok=0 with both vectors empty and no reason of any kind. scan.counts[i], front() or back() on that reading is a read past the end the hardened Debug std::vector aborts, and the hand-rolled counts.empty() ? 0 : counts[0] guard three campaigns wrote instead of crashing turns the same refused reading into a number a log reader cannot tell from a measurement. MeasurePaintedInk compounded it: when the derivation succeeded and the scan then refused, neither branch of its report chain ran and report stayed the empty string. Read a count only through Test::InkScan::countAt(i), which names the candidate and answers a named refusal for an index the reading does not have, for a reading that classified nothing and for candidates that do not separate; branch on InkScan::state and reason, never on an empty vector. Print the reading through Test::FormatInkScan, which carries the candidate colours, the counts, the collision and the five thresholds on the passing verdict too, so a palette whose references do not separate is readable without re-running the campaign. The refusing paths now fill both vectors, so they are always the same length, and MeasurePaintedInk writes a non-empty report on every path. Confirm the instrument with AppendDeriveBandSelfTest. Do not read this as the latched-hover row or the light-footer-fill row above: those are DeriveBand refusing to derive a band at all, this is a scan that ran and could not classify what it scanned.
A Classified reading with reason=none, every candidate count zero and ambiguous equal to inkPixels, on pixels that are demonstrably painted The candidates separate from each other and from the fill by the shipped thresholds, but one candidate lies on the segment from the measured background to another, so the classifier's margin never lets a pixel of it win. The app's dim text is this triple: windowSubTextFg #999999 on the segment from windowBgOver #f1f1f1 to windowFg #000000 (ui/colors.palette). Observed in 2026/09/21/ui-show-the-recipient-in-the-gram-send-box, whose day-theme arms failed on correct pixels. The scan answers background-collinear instead of classified, and its reason names the unreachable candidate, the candidate whose segment swallows it, and the measured background. countAt refuses with count=none. Confirm with AppendDeriveBandSelfTest. candidates-collide and no-ink keep their own texts.
A "this surface paints a coloured glyph" claim decided by counting pixels that are neither the pen colour nor the fill, passing and failing on totals only about 2% apart Subpixel text rendering puts chromatic fringes on ordinary glyphs, so that count cannot separate a coloured mark from plain text. The diagnosing run saw 794 such pixels (mean #888988) for an ASCII name against 813 (mean #c94840) for an emoji. Density does separate them: the emoji filled about 70% of a near-square window, the fringes about 26% of a wide smear. Read ReadChromaticRaster (test_ink.h). It reports the densest chromatic window and the measured mean of the pixels it matched, and refuses by name as no-chromatic or below-density instead of handing back an empty box. Confirm with the chromatic arm of AppendDeriveBandSelfTest.
A "text is not link-coloured" (or "text is pen X") check fails on correct neutral text because ScanInk answers classified with a non-zero countAt for a chromatic candidate, in both themes Subpixel antialiasing puts chromatic fringes on the glyph. ScanInk attributes each ink pixel to the candidate segment nearest it, so those fringes go to the link pen even when the solid cores are the neutral pen. A zero on countAt(chromatic) is not reachable, and no threshold on the non-zero count is principled. ReadChromaticRaster does not repair it either: it is a mark-density reading, not a pen reading, and its below-density refusal is no proof that the text is not a chromatic pen. Read ReadGlyphCore (test_ink.h). It reports the modal colour of the solid cores and each named pen's solid-core count, and refuses by name as no-paint or no-solid-core instead of handing back a fringe colour. Confirm with the glyph-core arm of AppendDeriveBandSelfTest.
A panel reading that changed by one and was attributed to the panel the scenario expected, or a zero recorded as "no panel opened" by a walk that was never shown to reach a panel it could see A count is not a difference: one panel closing while another opens reads as no change, and a count cannot name which pointer is new — and an isVisible() / !isHidden() filter counts a Ui::SeparatePanel that is still painting its show-animation cache, so the panel such a reading names may be the wrong one. A bare zero from a walk with no control cannot be told from an enumeration that structurally could not see a panel. Distinct from the Test::DiscriminatingScan row above, which is a sweep whose enumeration cannot reach its subject at all: here the walk reached the right widgets and read the wrong statistic out of them. Take a Test::PanelList mark before the action and read Test::WalkPanels(&scan, { .before = mark })'s added — pointers, not a count — under the default Settled liveness, which ReadPanelShowState decides; certify the zero with scan.report(), and read PanelWalkText / PanelWalk::refusal where a pure reading safe to poll from until is wanted; refuse an ambiguous identity ask with Test::PickPanel instead of taking the front of the list. Confirm the instrument with Test::AppendSeparatePanelWalkSelfTest.
Exactly two FAIL rows in the first stage of Test::AppendSeparatePanelWalkSelfTest — the fixture gate: premise row reading state=live translucentWindows=0 where show_cache was expected, and the settled reading holding the same panel it was expected to exclude — while every other row in the run passes, including the !isHidden() control in that same stage, the panel_walk_show_cache discriminates scan report, and all of the settle, exclusion and teardown stages Not a harness fault but a host precondition. Ui::SeparatePanel builds its show-animation cache only inside if (_useTransparency) (SeparatePanel::toggleOpacityAnimation in ui/widgets/separate_panel.cpp), and _useTransparency is Ui::Platform::TranslucentWindowsSupported() latched once in initGeometry — an inline return true on macOS and Windows (ui/platform/mac/ui_utility_mac.h, ui/platform/win/ui_utility_win.h) and the display server's answer everywhere else (ui/platform/linux/ui_utility_linux.cpp: true on Wayland, the _NET_WM_CM_S0 selection owner on X11, false otherwise). With no cache hideChildren() never runs, so the panel's children are never all hidden at once, ReadPanelShowState answers live in the turn the panel was shown, and the Settled walk keeps it. Ui::PopupMenu carries the same precondition on the same predicate and states it in test_menu.h. Tell it from an instrument fault in one glance: the failing premise row names itself fixture gate:, and an instrument fault takes the controls down with it — the scan's discriminates row and the !isHidden() control would fail too, and so would the settle and difference stages; here both controls pass and only the two cache rows fail. Nothing in the harness is to be repaired: read the pair as the host precondition, keep the rest of the run's verdict, and prove the cache-versus-settled distinction on a host whose display server supports translucent windows. Do not convert it to a skipReason: the popup menu self-test: open stage of Test::AppendPopupMenuCaptureSelfTest (test_menu.cpp) answers this same predicate with a named hard Check (its translucency fixture gate:) rather than a skip, and test_panel.h's doc block records why this module follows it — a skip would withdraw the cache-versus-settled certification on exactly the hosts that cannot perform it, instead of reporting it. The same self-test's locked-console gate is a skip, for the reason in the menu-capture row above: a lock passes, a host's missing translucency does not. This is a host property the run cannot arrange, so it is not a fixture gate in the “Media fixtures and fixture gates” sense above either.
A click check reads no effect in the very tick that clicked - for example detailCount=0 before=0 id= via=handler guard=[] after a tap that plainly reached a handler - and the stages that depend on that click read their skipReason in the same tick and skip as N/A, so whether the click worked is never read ActivateClickHandler (Telegram/lib_ui/ui/click_handler.cpp) does not call onClick itself: it posts strong->onClick(context) through crl::on_main(guard, ...), so the handler runs in a later main-queue turn. The production HistoryInner release path (Telegram/SourceFiles/history/history_inner_widget.cpp, the mouseActionCancel() followed by ActivateClickHandler(window(), activated, prepareClickContext(...))) and a direct call from a scenario defer alike. This differs from the settling-layer row above: there the synthesized click intermittently does not land at all; here the click landed and the handler ran, one tick later than the reading. Observed in 2026/09/17/open-the-chat-after-sending-grams-from-a-profile (work/test.md, Run 3). Click, then wait a bounded tick for the effect in until (the details count grew, or 2.5 s), and only then assert in then; a skipReason that depends on a click's effect is read after that wait, never in the tick that clicked. Repaired that way from Run 4 of the same campaign.
A tap on a service message that also displays a media activates the wrong link: the guard rows name a registered handler that is not the media's (guard=[registered id=... constructor=...], no media guard row) and an unrelated request such as payments.getSavedStarGifts registers right after it - the recipient's profile opened instead of the media's own action HistoryView::Service::textState (Telegram/SourceFiles/history/view/history_view_service_message.cpp) lays the service text above a displayed media: with the media displayed it shrinks the text geometry by st::msgServiceMargin.top() + media->height(), computes mediaTop below that geometry, and hands the point to media->textState only in its mediaDisplayed && point.y() >= mediaTop branch. A link scan that walks the element from its top therefore meets the text's own links first - for a transfer card that is the recipient-name peer link - and never reaches the media's link. Observed in 2026/09/17/open-the-chat-after-sending-grams-from-a-profile (work/test.md, Run 4). Scan only the media rows: from the element's bottom up to height - media->height(), and print mediaTop and mediaHeight in the details so a miss names the geometry it searched. Repaired that way from Run 5 of the same campaign, which found the link at mediaTop=47 mediaHeight=196.
A readiness gate on a served one-page history times out with the list resting at its bottom - scrollTop=5055 scrollTopMax=5055, the card resolved and mapped whole inside its owner - and every later stage is skipped The legacy HistoryWidget requests its older page only from HistoryWidget::preloadHistoryByScroll (Telegram/SourceFiles/history/history_widget.cpp), which calls loadMessages() only while scrollTop <= kPreloadHeightsCount * scrollHeight (kPreloadHeightsCount is 3 screens). A served history shown at its bottom never issues that request, so historyLoadedAtTop() stays false while historyLoadedAtBottom() is already true, and a gate that requires both edges before the first scroll can never turn true. Observed in 2026/09/17/open-the-chat-after-sending-grams-from-a-profile (work/test.md, Run 1). Gate an at-bottom reading on the bottom edge only (historyLoadedAtBottom()), which is the edge the claim is about, and print loadedTop, loadedBottom and the scroll movement in the readout so a timeout names its failing term. Repaired that way from Run 2 of the same campaign.
A "the box stays open after the refusal" check reads isVisible() false on a box that plainly exists - Ui::GenericBox 0,48 364x176, later closed by closeBox - and the same tick's Info layer readout says (visible 0) where a passing leg of the same check says (visible 1) The sibling of the layer-hide row above: LayerStackWidget::prepareForAnimation() (Telegram/lib_ui/ui/layers/layer_widget.cpp) hides the current layer while a show or hide animation paints from cached pixmaps, so a refusal that lands inside the box's own show animation is read while the whole layer, the box included, answers QWidget::isVisible() false. That row covers the shell buttons reading (hidden) in the turn the box was resolved; this one covers a synchronous isVisible() read on the box itself, taken in the tick a controlled refusal arrived, on a box that is being shown rather than resolved. Observed in 2026/09/17/open-the-chat-after-sending-grams-from-a-profile (work/test.md, Run 4, C5b and C5c). Read the box after a bounded settle (visible, or 1.5 s) and print visible, hidden and layerShown so the row shows which term answered. Repaired that way from Run 5 of the same campaign, whose settles measured 152 ms and 37 ms.
A web-document leg that expected an upload.getWebFile finds no request - the held or counted request never appears, or its canned answer names an unregistered request id (request is not registered) - while the loaded entry reports its bytes anyway (state=5 bytes=227) and paints The document was served from the persistent local cache, not the network. Data::WebDocumentCacheKey (data/data_types.cpp) is the SHA-256 of the URL alone, not of its access_hash, and every -testagent run reuses the marked live portable folder with its caches. So a canned or fabricated URL an earlier run (or an earlier leg of this one) loaded is found by FileLoader::loadLocal in session->data().cache() and finished locally, and no request is issued. The same trap fakes a pass: a "no request was issued" check, or a zero on a network counter, reads true on a cache hit. Observed in 2026/09/29/load-collectible-media-only-through-upload-get-web-file (work/test.md, Run 2, Test C6). Remove the key before the leg that must reach the network: Test::RemoveWebDocumentCache(session, url) (test_web_cache.h) in a stage's run, finished() in its bounded until, Check(ok()) in then. It clears both cache() and cacheBigFile() and reports finished only after both answered. Repaired that way in Run 3 of the same campaign. A "no request" or zero-count check on such a document is not certified until its key has been removed in that run. Confirm the instrument with AppendWebDocumentCacheSelfTest.
A paste check never pastes: the clipboard reads back empty right after QGuiApplication::clipboard()->setText (formats=0, text length 0, often already empty at the start), so the Paste accessory is never clicked or Ctrl+V inserts nothing, and every paste row stays undecided; or a single round trip fails on a transient lock - the app log reads OleSetClipboard: Failed to set mime data (text/plain) on clipboard: COM error 0x800401d0 (OpenClipboard Failed), then Qt's own Retrying to obtain clipboard. / Unable to obtain clipboard. for about 0.9 s - and the clipboard reads back again within about a second The agent-launched -testagent process could not read the system clipboard back on that host. Observed in 2026/09/29/keep-the-focused-word-field-in-view-when-importing-a-wallet (work/test.md, Run 1, Tests 9 and 10) on a Windows host: CLIPBOARD saved formats=0, clipboardTextLength=0 right after setText, eight paste rows undecided, TEST_FLAW. The cause is host-level and not diagnosed: earlier campaigns on other hosts, Windows included, wrote and read the clipboard. A save and restore does not protect the owner either: restoring a clipboard that read back empty writes the empty reading back, which clears it. The transient lock is a distinct case, observed in 2026/10/07/show-url-tooltips-and-copy-link-on-rich-message-url-buttons (work/test.md, Attempt 1 Run 1, Test 0): OpenClipboard failed at that moment, the run did not identify what held the clipboard, a later Get-Clipboard read fine and GetOpenClipboardWindow() was 0, and that run's single, un-retried round trip left every clipboard oracle undecided. Qt's own retries are short and sleep the main thread: OleSetClipboard three times 100 ms apart on CLIPBRD_E_CANT_OPEN, OleGetClipboard three times 50 ms apart (qwindowsclipboard.cpp). A locked Windows console is a diagnosed cause of the persistent shape: no process on the console's desktop can open the clipboard while it is locked, so every write fails OpenClipboard and the round trip ends in read-empty after the whole bound. Qt does not retry OleSetClipboard while QWindowsContext::isSessionLocked() - the same WTSSessionInfoEx SessionFlags reading Test::ReadConsoleLock() takes, but of the active console session - answers locked (QWindowsClipboard::setMimeData in qwindowsclipboard.cpp), so a locked console's writes return at once (writeMs 0-26 in Run 1 below), while a retried write on an unlocked console sleeps 100 ms per retry. Observed in 2026/10/07/add-a-same-turn-context-menu-reader-and-a-retried-clipboard-round-trip-to-the-test-harness (work/test.md, Attempt 1 Runs 1 and 2): Run 1's live arm logged 10 attempts, each read=empty owns=0, and ended in read-empty, beside ten OleSetClipboard ... 0x800401d0 (OpenClipboard Failed) lines in the app log, and an independent PowerShell reading right after it found LogonUI.exe in session 1, GetForegroundWindow() 0 and OpenClipboard(NULL) failing with GetLastError 5 while Telegram was not running; Run 2, the popup-capture self-test alone, launched with LogonUI.exe still present; Run 3, unlocked, read the sentinel back at attempt 1. The 2026-09-29 run did not record whether its console was locked, so a locked console is a candidate cause for that case, not its diagnosis. For checks about what a field does with pasted text, use Test::DropText(field, text) (test_widgets.h), which never reads or writes the clipboard, and judge its TextDrop (delivered, or the named refusal) before the product result. Run 2 of the same campaign repaired it this way in its overlay. Do not save or restore the clipboard around it. The helper does not exercise the Paste accessory's or Ctrl+V's own clipboard read: a check whose subject is that read still needs a host where the clipboard reads back, and it refuses by name rather than FAILing when it does not. Confirm the instrument with AppendTextDropSelfTest. Tell the transient lock from the persistent case with Test::AppendClipboardRoundTrip (test_clipboard.h): a caller-labelled sentinel written and read back up to 10 times 300 ms apart, yielding to the event loop between attempts instead of sleeping, each attempt logged as a CLIPBOARD_ROUND_TRIP_ATTEMPT row. A transient lock reads its sentinel back at a later attempt; the persistent case ends in the named refusal read-empty, read-stale-sentinel or read-foreign, which never prints text the harness did not write. Run 2 of 2026/10/07/show-url-tooltips-and-copy-link-on-rich-message-url-buttons decided every clipboard leg behind a bounded retry of the same size, written by hand with a main-thread sleep between attempts, and its premise read ok:1 attempts:1. Judge a product copy with Test::ReadClipboard. Confirm with AppendClipboardRoundTripSelfTest. On a locked Windows console, read the console with Test::ReadConsoleLock() (test_console_lock.h): a refused round trip's reason already ends with it (; console at refusal: CONSOLE_LOCK: state=...), so a campaign's own refusal names a locked console with no wiring, and AppendClipboardRoundTripSelfTest's live arm is TEST_RESULT: N/A naming the lock - the probe row, the refusal and its attempts - instead of failing read-empty, only when the probe reads locked as its judging stage begins and the round trip did not read back, so a round trip that read back is never withdrawn; its injected-accessor arms and its foreign-text scan still run and decide. A campaign gates its own clipboard leg the same way, through its judging stage's skipReason. The rule: for these arms a locked console is a scheduling condition - rerun them on an unlocked console. This applies to Windows; Test::ConsoleLockGate answers only in a Windows build, so no gate fires elsewhere, while a refusal on macOS still prints the session's lock reading (the macOS observation is in the menu-capture row above).
A check that samples an effect through a driven sequence reads it unchanged at every sample although the drive itself acted - for example E6_SWIPE build=before samples=58 preShown=15.0000 maxTurn=0.0000 with shownMoves=0 centreMoves=0 beside minShift=-40 and a passing "the swipe moved the card on screen" control - while the product's own pending flag, wherever a sample reads it, stays set. The build or leg that must show the effect reads it unchanged as well, so a "nothing changed" check on the other build passes vacuously The product applies the effect in a callback it posted with crl::on_main, and the sampler never returned to the event loop between samples, so that callback could not run until the sequence was over. crl::on_main never runs its callback inline: Telegram/lib_crl/crl/crl_on_main.h picks crl/dispatch/crl_dispatch_on_main.h when CRL_USE_DISPATCH is defined without CRL_FORCE_COMMON_QUEUE (Telegram/lib_crl/CMakeLists.txt; that is macOS outside the Mac Store build), whose on_main is a dispatch_async_f onto dispatch_get_main_queue() (crl/dispatch/crl_dispatch_async.cpp), and crl/common/crl_common_on_main.h otherwise, whose on_main pushes onto a crl::queue (crl/common/crl_common_queue.h) that wakes PushToMainQueueGeneric in Telegram/lib_ui/ui/main_queue_processor.cpp, which only posts a QEvent to Ui::MainQueueProcessor. The queued callback then runs on a later event-loop turn, or when a QEvent::UpdateRequest reaches Core::Sandbox::notify (Telegram/SourceFiles/core/sandbox.cpp), whose widgetUpdateRequests() stream makes Ui::MainQueueProcessor drain the queue (Platform::DrainMainQueue() or DrainMainQueueGeneric()): QWidget::repaint() can send one synchronously, while QWidget::update() only schedules one and an offscreen frame through Ui::GrabWidgetToImage or Test::ForceWindowActive, which flushes only window-system events, sends none. The observed sampler was the Script sequencer of the run's saved overlay (work/test-overlay.patch of the task below): Script::advance() is a while loop that stamps _lastRun before each step's action and arms its timer only for a step not yet due, so a step whose action outlasts the next step's delay runs that next step inline. The campaign's saved CSVs (evidence/swipe-before.csv from Run 3, evidence/swipe-after.csv from Run 4, whose build never pumped) put consecutive samples' at 22-40 ms apart against a 16 ms delay, so each next step was already due and all 58 samples ran inside one advance() call. Observed in 2026/10/02/turn-gram-cards-only-by-visible-angle-steps (work/test.md, Run 2): the card-angle tracker's follow, posted through crl::on_main, never ran during the 58-sample drive of about 1.5 s, so the build before the change showed no turn and the after build's earlier pass decided nothing. This differs from the ActivateClickHandler deferral row above: there the click landed and the handler ran one main-queue turn after a reading taken in the clicking tick, and a bounded until wait repairs it; here the sampler does not return to the event loop between samples, so the posted callback does not run during the sequence at all, and no wait after the sequence repairs samples already taken. After each driven frame, pump QCoreApplication::processEvents() with the window re-activated before each pass (Test::ForceWindowActive), bounded to at most 5 passes, until the product's own pending flag clears; record the pass count and the flag after the pump in the sample's row, and only then read the effect. Never judge an effect from samples taken before the posted callback could run. Keep a control build or leg that must show the effect, so a callback that never ran fails it instead of passing silently. Repaired that way from Run 3 of the same campaign: the posted follow ran within 2 passes on each of the 20 samples that had scheduled one, and the before build turned 4.8204 deg and sprang back.
A decisive bracket spans a sample gap far above the sampler's cadence while the measured surface paints back to back, and the product's own timers report similar lateness - for example a 16 ms timer sampler whose grab 25 (hi 44159) and grab 26 (lo 44211) are 52 ms apart across product frames 44159, 44173 and 44193, beside the product's arrive due=44159 late=52, so vstart(B2).hi - turn read 75 against 50 and stayed undecided A timer-driven sampler is not synchronized with the paints it measures: whatever runs between two of its ticks falls into one gap, and the tick itself can fire late. Observed in 2026/10/05/play-gram-card-read-animations-one-after-another (work/test.md, Attempt 1 Run 1, Test 2), which read that lateness as back-to-back product paints starving the timer queue without measuring it. Four launches of AppendPostPaintSamplerSelfTest (task 2026/10/06/add-a-post-paint-capture-sampler-to-the-harness: a calibration launch, one with every self-test, and two whose mutated sampler left the control unchanged) measured it on its 18 ms synthetic owner, whose every paint requests the next. In every one the event loop started product paints while the 16 ms control's tick was already overdue (overdueStartsTotal 11, 22, 11 and 8 in that order), and each launch's latest tick had such paints in its overdue interval (lateMax 87, 34, 107 and 74 ms, with overdueStarts 1-2 and 36-54 ms of overlapping paint, less than the lateness in all but the launch with every self-test). Ticks with no fixture paint in their overdue interval were at most 2 and 4 ms late in the two mutated launches, did not occur in the launch with every self-test, and reached 36 ms in the calibration launch (lateNoPaintMax); the median tick was 2-32 ms late (lateMedian). So the measured lateness is consistent with back-to-back product paints holding an overdue timer, with something besides fixture paints adding to it, and the product's own timers can wait the same way. In the launch with every self-test the control's largest gap was gapMax=50, with missed=22/45 product paints that no sample followed, while the post-paint sampler on the same owner read gapMax=20 and missed=0/41, its largest delay from a paint's return to the covering sample delayMax=2 within bound=36 (one product paint plus one grab). That is a measurement of these launches on one host, not a general law. Sample the owner with Test::PostPaintSampler (test_post_paint.h) over sampling windows around the transition, with grab-free windows wherever the product's own paint must not be preceded by a grab, and gate each decisive bracket on its distance to the previous sample, recording a bracket that still spans a gap as undecided. Repaired that way from Run 2 of the same campaign: 9 post-paint grabs in [turn - 100, turn + 300], the bracket 20 ms after the previous sample (its G-sample gate, bound 34 ms), and the bound read 46, decided. Confirm with AppendPostPaintSamplerSelfTest.
A containment or compare row on a top-right or bottom corner patch names a media colour as the background, or counts every "background" pixel changed while the capture shows the arc intact - for example right background #e0189c: 1046 of 1046 background pixels changed beside left background #ffffff: 0 of 191 The background was read at patch pixel (0, 0) for every patch. That pixel lies beyond the arc only in a top-left patch; in a top-right or bottom patch it is a media pixel, so the "background" mask was the media, and any slide, spoiler or frame change inside it counted as a changed background pixel. Observed in 2026/10/06/round-rich-message-edge-media-like-ordinary-media (work/test.md, Run 1, failed assumption 9: 14 FAIL rows). Read the background with Test::ReadCornerBackground (test_corner_patch.h) and the patch's PatchCorner: it reads the patch's own corner-most pixel (CornerMostPixel), keeps only the component of that colour connected to it, and refuses media-at-corner instead of returning a media colour; ReadCornerContainment, CompareCornerCoverage and GateCornerChrome take their background from it. Run 2 of the same campaign repaired its rows by reading each patch's own corner. Confirm with AppendCornerPatchSelfTest.
A pixel-identity or "within N of the media colour" corner compare fails between twins whose media colours legitimately differ - letterbox fill, map tiles, another media kind - with the differences only at the arc's ends, or a tiny channel delta on nearly every pixel: shape sets 1048 vs 1046 pixels, only-a=2 ... a(31,0) a(0,31), 1273/1296 maxChannelDelta=3 An arc pixel is a blend of the media colour over the background, so its colour moves with the media colour: a device-exact compare differs wherever the media differs at all, and a fixed-colour set such as "within 2 of the media colour" gains or loses the arc-end pixels with the contrast. The source run's recomputation of those patches by coverage found the same arcs (largest deltas 0.0034-0.047). This differs from the (0, 0) background row above: there the wrong pixel was taken for the background; here the background is right and the colour model is wrong. Observed in 2026/10/06/round-rich-message-edge-media-like-ordinary-media (work/test.md, Run 1, failed assumption 3: 3 FAIL rows). Compare by coverage with Test::CompareCornerCoverage (test_corner_patch.h): each pixel's media share over the reference background, on the channel where they contrast most, within kCoverageSameKind (0.03) for one media kind or kCoverageCrossKind (0.06) across kinds, with each twin's media colour sampled inside its media away from the arc and from chrome. Colour twins with the same arc compare equal, while another radius, a square corner or a one-pixel arc shift moves pixels by 0.3-1.0. Repaired that way from Run 2 of the same campaign. Confirm with AppendCornerPatchSelfTest.
A corner compare reports hundreds of differing pixels, all inside one compact shape within the media and none outside it - mismatching device pixels=742/1296 - or one side's containment mask holds a few more "background" pixels than the other, and they change in every frame (right corner TR background #ffffff: 6 of 197 beside left corner TL background #ffffff: 0 of 191); or a media colour sampled at a corner inset reads a badge's text or fill (#e1edf0) Chrome is painted inside the patch, over the media. A video or GIF draws its corner-status badge at the top left (Gif::drawCornerStatus() -> PaintVideoCornerStatus(), st::msgDateImgDelta in from the media's corner). HistoryView::Photo draws its enlarge badge at Photo::enlargeRect(), st::historyPageEnlargeSkip in from the top right, when _showEnlarge is set: Photo::countCurrentSize() sets it for a photo that is not its message's own media, on a message that is not sponsored and whose media has a web page that suggestEnlargePhoto() (a site name, a title or a description), with the photo at least 2 * st::historyPageEnlargeSkip + st::historyPageEnlargeSize in both dimensions, and Photo::draw() paints it only once the photo is loaded. Its white glyph equals a white background exactly, so a mask of "every pixel of the background colour" takes it in. This differs from the colour-twin row above: there the arc pixels move with the media colour; here the arc matches and the difference is a shape inside the media that the twin does not draw. Observed in 2026/10/06/round-rich-message-edge-media-like-ordinary-media (work/test.md, Run 5: all 8 FAIL rows, from the enlarge badge in the top-right patch; Run 1: 5 FAIL rows from colours sampled inside the corner-status badge). Exclude the chrome's exact shape, built from the product geometry and grown by a device pixel, only through Test::GateCornerChrome (test_corner_patch.h) on a twin or reference that does not draw it - it refuses a region touching an arc or background pixel there (chrome-touches-arc, chrome-touches-background) - and pass the accepted chrome to CompareCornerCoverage / ReadCornerContainment, which refuse one the gate did not accept. ReadCornerBackground's corner-connected mask keeps an enclosed glyph out of the background on its own. Sample media colours away from badges. Repaired that way in Run 6 (the badge's shape excluded behind a twin gate) and Run 2 (badge-free colour sampling) of the same campaign. Confirm with AppendCornerPatchSelfTest.
A transition or reveal of a few hundred milliseconds is always captured already finished - only the destination slide or the fully revealed state, Cm1=0 Cs2=72364 - or a clocked stage's frames all read the state from before a clicked effect: media centre #9f116f (hidden #9f116f, previous frame #9f116f, ...); one animation-manager tick 41 ms after the click, and the same at 100 and 160 ms A Ui::Animations value changes only on an animation-manager tick (Simple::value() returns what the last tick stored), and the manager ticks from the event loop; the runner reads a stage every 50 ms, so a 200 ms animation is often over by the next reading, and a minimized window gives no paint to wait for. A stage that ticks the manager itself still reads nothing if the click went through ActivateClickHandler() (Telegram/lib_ui/ui/click_handler.cpp): it posts onClick(context) with crl::on_main, and Test::Settle drains only Ui::PostponeCall, so the handler runs only after the synchronous stage has returned, after every frame. This differs from the ActivateClickHandler row above: there the handler ran one main-queue turn after a reading taken in the clicking tick, and a bounded until wait repairs it; here the clocked stage never returns to the event loop before its frames, so no wait inside it helps. It differs from the crl::on_main sampler row above as well: there the product's own posted callback needs pumping between samples; here the harness's own click must not be posted at all. Observed in 2026/10/06/round-rich-message-edge-media-like-ordinary-media (work/test.md, Run 2, failed assumption 2: 12 transition premises; Run 3, failed assumption 1: three reveal premises). Clock the frames with Test::RunClockedFrames (test_animation_clock.h): it calls the handler's onClick(context) inside Test::Settle, drains the postponed calls, waits on the main thread until each requested elapsed time, ticks Core::App().animationManager().update() once with a probe proving the tick, renders synchronously, prints each frame's reached elapsed bounds and reports a time past the declared end as N/A. Keep each render cheap: a grab, no save. Repaired that way in Runs 3-5 of the same campaign: Run 3 decided every clocked transition, Run 4's direct onClick decided the reveal, and Run 5, grabbing only the subject's strip between ticks, took all three reveal frames at 41, 100 and 160 ms. Confirm with AppendAnimationClockSelfTest.
A tail-presence or other edge-shape reading of an incoming bubble in the default theme reads nothing on every twin, including the control that must show a tail - tail present - ... 0 of 240 device pixels differ - so every "no tail on either twin" row beside it passes vacuously, and the saved list capture shows white bubbles on white with only their one-pixel shadow line In the default palette msgInBg is windowBg (#ffffff, Telegram/lib_ui/ui/colors.palette), and Test::GrabWidget / Test::GrabRect hand st::windowBg to Ui::GrabWidgetToImage, which fills the image of a target without Qt::WA_OpaquePaintEvent with it before rendering, so every pixel beyond the bubble reads #ffffff whether a white tail covers it or not. This differs from the lightButtonBg: windowBg row above: there the requested fill is the box's own background and DeriveBand names that refusal; here the subject's own colour equals the capture base, nothing refuses, and a present and an absent tail read the same. Observed in 2026/10/06/round-rich-message-edge-media-like-ordinary-media (work/test.md, Run 2, failed assumption 1: 2 FAIL rows, and five "no tail" rows vacuous in Runs 1 and 2). Render the tail area directly - Element::draw() with the list's own paint context (HistoryInner::preparePaintContext() for the legacy history, ListDelegate::listPreparePaintContext() for a HistoryView::ListWidget) - into an image filled first with a mid grey (#808080), for both twins, and keep a twin that must show the tail as the positive control in the same render. Repaired that way from Run 3 of the same campaign: the tail-present controls read 105 of 240 and every no-tail row 0 of 240 on both twins.
A twin row on a message drawn without a bubble - a map or a photo with no caption - compares the wrong pixels because its media rect, built from mediaTopLeft(), lies outside the element's bubble geometry: twin media x 0..320 bubble 132..452, mediaTop-G=-6 HistoryView::Message overrides Element::mediaTopLeft(), which answers innerGeometry().topLeft(), with _lastMediaPosition, and the only writer of that member is the paintMedia lambda inside the if (bubble) branch of Message::draw() (history/view/history_view_message.cpp). A media-only message has Message::drawBubble() false - no visible text, and media that does not needsBubble(), such as a map whose Location::needsBubble() finds no title, description, comments, reply, forward, sender name, via bot or topic button - so the bubble-less else if (media && media->isDisplayed()) branch draws it and never writes the member: the accessor answers its default (0, 0), the element's own top-left, or a position an earlier bubble paint of that element stored. This differs from the geometry-oracle row above: there rects of different widgets were compared without a shared origin; here one element's own accessor answers a position from another paint, or none. Observed in 2026/10/06/round-rich-message-edge-media-like-ordinary-media (work/test.md, Run 1, failed assumption 2: 3 FAIL rows). For a message with no visible text (hasVisibleText() false) whose mediaTopLeft() falls outside its bubble geometry, take the media rect from that geometry - left innerGeometry().x(), top the element's top plus marginTop() - with the media's own width and height, and print which source the rect came from. Repaired that way from Run 2 of the same campaign.
A synthesized text-selection drag over the legacy chat history selects nothing - drag 439,36695 -> 78,36407 selected text "" - although its press, moves and release all reached HistoryInner The press (mouseActionStart()), every move and the release (mouseActionFinish()) go through HistoryInner::mouseActionUpdate(), which maps the pointer and passes it through HistoryWidget::clampMousePosition(): x is clamped into [0, scroll width) and y into [scrollTop, scrollTop + scroll height). A drag whose points lie in list coordinates outside the visible area therefore lands on the viewport's edge row and selects nothing there. This differs from the drag/wheel/cancel row above: there a wrapper received an event that its child, presentation or viewport owns; here the event owner received every event and clamped its point. Observed in 2026/10/06/round-rich-message-edge-media-like-ordinary-media (work/test.md, Run 2, failed assumption 5; Run 1 had blamed a press past the glyphs). Scroll both drag points into the viewport first, and gate the drag on a premise row that prints both points, scrollTop and the viewport height. Repaired that way from Run 3 of the same campaign: drag rows [36407,36695] scrollTop=36294 viewport=514, then the same drag selected the text.
A paint-cost bound written as a ratio of Debug timings fails by about 2x on a frame whose paint is one or two full-frame blits - median ms edge=31.28 control=16.31 (bound: <= 2x and <= +4 ms) - or a timing delta between two different views lies far above the added work's own cost (delta 53.87 against 0.98 ms) This macOS checkout's Debug build links Qt's Debug-configuration static library: Qt6GuiTargets-debug.cmake imports libQt6Gui_debug.a as IMPORTED_LOCATION_DEBUG, and the generated Xcode project links the Telegram target's Debug configuration with it and its other configurations with libQt6Gui.a (on another platform, read its own link line). In the source run's standalone probe, built against the same Qt prefix, one 792x594 SourceOver blit cost about 14-15 ms in libQt6Gui_debug.a against about 0.14 ms in libQt6Gui.a, so a Debug paint timing is mostly unoptimised Qt raster time: one more full-frame composite doubles it whatever the code around it costs, and two different views also differ in unrelated work. Observed in 2026/10/06/round-rich-message-edge-media-like-ordinary-media (work/test.md, Run 1, failed assumption 13; Run 2, failed assumption 8: a cross-view delta against a transparent-layer replica). Decompose in one binary: time one view in both arms (Run 3 switched only the arm under test, on the same view) beside the added raster operations timed alone on an opaque replica, bound delta <= 1.25 x replica + 1 ms, and add a narrow-band case whose delta must stay below the full frame's; take the absolute cost from a standalone probe built against the optimised Qt (evidence/p1-probe/ of the source task: +0.166 ms per full frame). Repaired that way from Run 3 of the same campaign; Run 2's cross-view delta is not a repair.
An inline video or GIF in a chat stays on one frame across samples seconds apart - fixture gate: autoplay advancing (...) - media centre colour per sample: [#a88615, #a88615, #a88615], a GIF moving only from its thumbnail to its first decoded frame - and a check that needs its progress line reads inside the frame 0, outside 0, while the window is minimized or not exposed (isMinimized=1 ... exposed=0) Gif::startStreamedPlayer() starts the inline player with waitForMarkAsShown = true, so Media::Streaming::Player::renderFrame() holds each next frame until Player::markFrameShown(), and in the view only a paint calls that: Gif::draw() and Gif::drawGrouped() mark the drawn frame shown unless the paint is paused, and a paint context is paused while SessionController::isGifPausedAtLeastFor() holds, which includes an inactive window. A window that gets no compositor paint never presents the next frame: its GIFs stop after the first decoded frame and a video never leaves its first. Read the AutoPlayVideo limit before blaming it: Run 1 did, and Run 2 read it as already 50 MB (AutoPlayVideo limit 67108864 (was 52428800)). This differs from the undecodable-fixture row above: there the document cannot be decoded; here it decodes and its inline player exists, but no frame after the first is presented. Test::KeepMainWindowNotMarkingRead minimizes the primary window, which is exactly what stops such players: a scenario that uses it gets no inline playback without a pump. It differs from the clocked-animation row above too: Test::RunClockedFrames ticks Ui::Animations::Manager, which a streamed player does not use (it paces frames with its own base::Timer), so a clocked stage does not advance it, minimized or not. Observed in 2026/10/06/round-rich-message-edge-media-like-ordinary-media (work/test.md, Run 2, failed assumptions 3 and 4: 4 video gates and 3 progress-line rows). A frame pump: render the target media rect directly every 30 ms (Element::draw() with the list's own paint context, the paint a compositor paint would run), re-asserting Test::ForceWindowActive on each pass so the context is not paused, until each twin's frame visibly advanced, and log the playback facts (canBePlayed, loaded, inappPlaybackFailed, ShouldAutoPlay, window exposure, isGifPausedAtLeastFor) at every gate. Repaired that way from Run 3 of the same campaign: 8 pump gates decided, at three playback positions per twin. Its proven scope is "the frame advanced": reading a playing video's progress line through the pump stalled in Runs 3 and 4, and the campaign retired "pump until the video plays" (work/test.md, Run 4 "Forbidden repeated techniques"); for a line, see the next row.
A check that needs an inline video's progress line has nothing to read: no line appears until the playback position passes 200 ms, and pumped playback stalls in the player's waiting state - pump ticks=46 in 15227 ms; ... moved=0 line=0 (max 0), or eight stalled activations with FFmpeg decode errors on the fixture in the app's stderr Gif::paintTimestampMark() returns while _videoTimestamp <= 0 and _videoPosition is below 200 ms; otherwise it calls PaintVideoTimestampMark() at _videoPosition, or at _videoTimestamp * 1000 while the position is 0 - on the static frame too, with no player at all - and PaintVideoTimestampMark() draws nothing for a position past the video's duration. Every HistoryView::Gif reads _videoTimestamp once, in its constructor, from Media::View::ExtractVideoTimestamp(): the media's own videoTimestamp(), else its web page's extractVideoTimestamp(), which parses the t= parameter of the link preview's URL. With video autoplay allowed, such a video still gets a player on an item that isSending(), as the source run's injected local items were: Gif::draw() stops a timestamped video only while underCursor() is false, and underCursor() compares ClickHandler::getActive() with currentVideoLink(), which is null because currentAction() is Action::None for a sending item, so with no active click handler the two nulls are equal and playAnimation() runs whenever autoplay is eligible. Observed in 2026/10/06/round-rich-message-edge-media-like-ordinary-media (work/test.md, Runs 3 and 4, and Run 4's failed assumption 4: the line does not need playback). A fixture whose link preview URL carries ?t=<seconds> within the video's duration, with the in-memory AutoPlayVideo limit set to 0 for the leg (Data::AutoDownload::Full::setBytesLimit(source, Type::AutoPlayVideo, 0) on the session's autoDownload() settings), restored after it and again in teardown, and a premise row reading Data::AutoDownload::ShouldAutoPlay() false for the document (a per-peer ForceAllow override falls back to the default limit when the limit is 0), so that no inline player is created; every paint then draws the line at the timestamp. Repaired that way in Runs 5 and 6 of the same campaign (ExtractVideoTimestamp(item)=7 s ... AutoPlayVideo limit 0, ShouldAutoPlay=0). This differs from the inline-player row above: there the pump makes frames advance; here no playback is needed when only the line is read. A link preview with a site name, title or description also turns on the enlarge badge of the message's other photos: see the corner-chrome row above.

Classify a sound assertion against changed behavior as an implementation bug, not a test flaw. Classify a wrong fixture, target, readiness model, event route, capture owner, or oracle as a test flaw. Preserve everything a failed run proved, repair all visible harness faults together, and rerun the packed scenario rather than fragmenting it into isolated launches.

When a flaw's cause is the instrument idiom rather than this task's fixture, repair it here as well as in the overlay: add the missing helper, tighten an existing contract, or add its row to the table above, in the same run that diagnosed it. A diagnosis that stays in one task's notes is rediscovered by the next task, which is how the Test::Probe window row and the Test::DiscriminatingScan row above cost four runs each before they were written down.