Files
obsidian-vault/work/projects/cpm-status-2026-10-15.md
T

101 KiB
Executable File
Raw Blame History

title, date, tags
title date tags
CPM Web Extension — единый статус (PR, данные, гипотезы, телеметрия) 2026-10-15
work
cpm
status

CPM Web Extension: единый статус на 15 октября 2026

Внутренний документ. Цифры из ClickHouse, в публичные PR, комментарии и issues не переносить (репозитории duckduckgo/apple-browsers и duckduckgo/duckduckgo-privacy-extension публичные). Заменяет заметку cpm-telemetry-data-requirements-2026-10-15 (теперь ссылка сюда). Исторические срезы с собственными датами и знаменателями: cpm-web-extension-public-attribution-2026-10-05, cpm-web-extension-data-analysis-summary-2026-10-08, cpm-web-extension-repro-and-fix-2026-10-08. Актуальная карта worktree и точек входа добавлена в cpm-web-extension-code-guide-2026-10-09; её прежние разделы сохранены как карта на 9 октября.

Актуализировано 15 октября, 17:26 UTC / 23:26 Бишкек. Разделы 1–9 — сводный статус; §10–11 — сохранённая хронология экспериментов, где «пока», «не найдено» и красные ожидания относятся к моменту записи. Текущий итог всех 14 P5/P6-проверок, границы доказательства и команды — §12. PR/CI сверены read-only через GitHub; результаты локальных тестов сверены с существующими логами и исходниками, в этом документационном проходе тесты и ClickHouse-запросы заново не запускались.

1. Коротко

  • Фикс B (падение Network-процесса) проверен локально и красным прогоном на CI. Основание в данных есть, но неполное: обогащение в stuck есть, а доказать, что строки это именно мёртвый worker, нельзя (не хватает network_replaced_since_view).
  • Фикс A (первая загрузка посреди сессии) реален, но мелкий: около 1% stuck на iOS, на macOS почти не виден, чаще самоизлечивается.
  • Три самые крупные группы сбоев фиксами A и B не закрыты: пробуждение после idle, error 6 (iOS), «нет background view» на запуске (iOS). Механизмы воспроизведены локально на macOS, но проверенного рычага в приложении нет.
  • P5/tab_in_context=false, macOS: найден browser→WebKit путь — закрываемое окно по-прежнему возвращает общий pinned tab, а WebKit внутри didCloseWindow удаляет его регистрацию. На системном WebKit получены точная цепочка, два failure и actual stuck; прямой didCloseTab тест не вызывает. Это продолжает ручное наблюдение 10 сентября из cpm-web-extension-breakage-findings, но теперь подтверждены текущий source contract и автоматический сценарий (§11–12).
  • P6, macOS: подтверждён условный путь через реальный WebExtensionFeatureFlagHandler: true → false → true главного webExtensions создаёт новый manager/controller, существующий WKWebView остаётся со старым controller. Реальность такого переключения в выбранных production-сессиях не доказана; обычное обновление config или только embedded flag не равны этому условию.
  • Проверки P5/P6: все 14 текущих методов имеют pass по последним соответствующим логам; единый финальный прогон всех 14 не выполнялся. Пакет: 478 XCTest, 28 opt-in skipped, 0 failures; strict SwiftLint восьми изменённых Swift-файлов — 0 нарушений. Зелёный characterization-тест подтверждает ожидаемый сбой/контроль, а не исправление CPM.
  • Осталось неизвестным: естественная утрата runtime listener metadata, P5 при tab_in_context=true, реальный iOS suspend/resume и естественный P6 без доказанного main-flag transition. Полный AppKit/browser flow в новых SPM-тестах не выполнялся: app callbacks/delegates реализованы harness по исходникам; production shared code и системный WebKit выполнялись.
  • Телеметрия PR0 пока в виде, вызвавшем вопросы о склейке пикселей. Решение по правкам не принято (раздел 8). #7384/#7385 имеют красный Danger, #7381 — красный Non-Sandbox; текущие heads и ограничения — раздел 2.

2. Цепочки PR (все в duckduckgo/apple-browsers, задачи Asana в телах PR; не читать)

Телеметрия:

Статусы ниже проверены через GitHub 15 октября в 17:26 UTC. Все семь перечисленных PR открыты; это не означает merge или shipped release. «CI зелёный» означает отсутствие failing/pending среди опубликованных checks; skipped jobs отдельно не считаются выполненными. Счётчики — записи GitHub statusCheckRollup, а не число уникальных тестов: имена checks из разных workflows могут повторяться.

PR Ветка База Что Статус
#7376 (PR0) alex/cpm-messaging-readiness-telemetry main параметры готовности и тайминга на failure/recovery пикселях (без новых пикселей) OPEN, head 619869ee84c6; CI: 39 success / 18 skipped, без pending/failure; REVIEW_REQUIRED. JA1–JA3 и неотправленный ответ — прежняя запись сессии; новые review comments в этой сверке не перечитывались
#7384 (0b) alex/cpm-readiness-baseline #7376 ежедневные базлайн-пиксели cpm_background_ready_{context_load,wake}, нагрузка устройства, возраст возобновления OPEN, head 99ecf8b12787; 36 success / 23 skipped / 1 failure: Danger; pending нет. Причина Danger здесь не устанавливалась
#7385 (0c) alex/cpm-init-progress-native #7384 init_stage на failure-пикселях (нужен JS из privacy-extension#3727) OPEN, head 46f25175c7c4; 36 success / 23 skipped / 1 failure: Danger; pending нет. Причина Danger здесь не устанавливалась
privacy-extension #3727 alex/cpm-init-progress main уведомления cpmInitProgress из встроенного расширения OPEN, head b00cc227112d; 32 success, без skipped/failure/pending; REVIEW_REQUIRED

Harness и фиксы (красный коммит, затем зелёный; не ребейзить и не форс-пушить, иначе пропадёт доказательство красного прогона):

PR Ветка База Что Статус
#7378 alex/cpm-repro-harness main harness (только тесты); 4 быстрых теста в CI около 3 с, остальное локально через CPM_REPRO=1 OPEN, head dfb96279196d; 37 success / 18 skipped, без failure/pending; REVIEW_REQUIRED. Предыдущее Danger-предупреждение о 1570 строках сохранено как историческое; в ветке есть коммит другой сессии «Require initResp in the page…»
#7379 alex/cpm-fix-init-loss harness фикс A: ожидание первой загрузки background после установки/апгрейда посреди сессии OPEN, head 7ad771e227bb; 35 success / 20 skipped, без failure/pending; прежние красный и зелёный прогоны сохраняются
#7381 alex/cpm-fix-network-crash фикс A фикс B: перезапуск background при замене Network-процесса и мёртвом worker'е OPEN, remote head 39f94e12eaa4; 28 success / 24 skipped / 6 failure, включая Test (Non-Sandbox) и итоговые отчёты. Прежний красный repro testNetworkProcessCrashKeepsEveryTabWorking с 3 ретраями подтверждён. Исправление f1085846e7ff — единственный локальный commit поверх remote head; в PR его нет, зелёного CI для него нет
(нет PR) alex/cpm-fix-pixels фикс B + PR0 пиксели background_restart и first_load_wait для измерения эффекта держим до слияния PR0, A, B

Локальные проверки фикса A: на коде без фикса оба repro-теста красные (5 из 5 итераций), с фиксом 0 из 10 потерь, 761 тест пакета зелёный. Фикс B: на main красные тесты падения Network-процесса и смерти worker'а, 5 из 5 итераций.

Новые P5/P6 тесты остаются локально в alex/cpm-web-extension-remaining-failures; в PR #7378/#7379/#7381 они не переносились. Весь CPMRepro/ этого worktree untracked, поэтому одного HEAD недостаточно для восстановления тестов. Нужны сами файлы (§12 и актуальная карта кода).

Недоделано в harness и CI: CI гоняет тесты с -test-iterations 3 -retry-tests-on-failure, красный тест должен падать стабильно; тесты WebExtensions идут внутри схем macOS Browser и iOS Browser на osx-tahoe-27. Медленные эксперименты (заморозка 45 с, заполнение кэша около 10 минут, медленный старт worker'а) локальные и не для коммита.

3. Данные (ClickHouse, 12–14 октября, версии iOS 7.240+ и macOS 1.210+)

Единица: daily-пиксель = устройство-день на причину, не пользователь и не эпизод. initialization_failed только .daily; stuck, recovered_*, reload_failed: daily+count. Параметры снимка есть только на init_failed и stuck.

iOS macOS
initialization_failed.daily 3 273 025 119 950
stuck.daily 511 660 40 842
init восстановились без reload 1 135 477 (34.7%) 30 738 (25.6%)
stuck → восстановились после M4 reload 293 557 (57.4%) 21 865 (53.5%)
reload_failed.daily 140 921 (27.5% от stuck) 17 796 (43.6%)

Когорты (по приоритету: error 6 → нет view → вкладка не подключена → Network заменён → пробуждение (view <30 с после dealloc) → недавняя загрузка (load/base_url_changed ≤2 мин) → other). Доля init → stuck:

Когорта iOS macOS Смысл роста или падения
error 6 6.1% → 21.5% 1.0% → 1.9% остаётся сломанной
нет background view 48.1% → 10.9% 3.3% → 1.8% самоизлечивается
вкладка не подключена 0.03% → 0.05% 6.2% → 8.9% остаётся сломанной
Network заменён 16.8% → 39.7% 13.6% → 26.6% остаётся сломанной
пробуждение 7.9% → 9.5% 32.5% → 32.2% без изменений
недавняя загрузка контекста 15.7% → 6.0% 20.0% → 6.4% самоизлечивается
other 5.3% → 12.5% 23.4% → 22.2%

Объёмы stuck.count в разных версиях на порядки выше daily: дашборды сравнивать только по версии и частоте.

4. Основание фиксов

  • B: доля «Network заменён» растёт в 2–2.4× при переходе init → stuck. reload_failed высок: reload расширения мёртвый worker не лечит. Оговорка: network_restarted сравнивает PID с загрузкой контекста, а не с последним view. Если view пересоздан после смены процесса, worker свежий и механизм неприменим. По заметке 8 октября механизм объясняет в лучшем случае 20–45% «здоровых» stuck. Причинность подтвердят network_replaced_since_view (нет ни в одной ветке) и init_stage=none (#7385).
  • A: сигнатура «загрузка контекста ≤2 мин, не при запуске, первый view»: iOS 129k init (3.9%) → 4.6k stuck (0.9%); macOS 505 → 118. Разовая потеря одной навигации (0.23× при переходе в stuck). Сигнатура неточна (захватывает M4-reload). Название связанной Asana-задачи («init loss on background unload») относится скорее к группе пробуждения, чем к фиксу A; несоответствие надо снять с автором задачи.

5. Картины событий (stuck.daily, 14 октября, хвост bg_events без возрастов) и воспроизведение

Картина Хвост iOS / macOS Воспроизведено? Рычаг в приложении
P1. Цикл пробуждения view>proxy_on>dealloc>view>proxy_on 16.0k / 4.3k (wake) + 16.3k / 2.4k (старше 30 с) только с искусственно занятым worker'ом (5 с: init_failed и поздний ответ; 12 с: stuck → M4); органическое пробуждение в тестах проходит. WebKit-баг 324743 (исправлен 22 сентября, нет в 26.7/27.0) гипотезы: отсчёт 4 с от готовности background («фаза 2 детектора»), ожидание готовности при пробуждении, предпрогрев
P2. «Пробуждение → error 6», циклы proxy_on>dealloc>view>proxy_on>error_E:6 (23.6k), error>dealloc>view>proxy_on>error (5.7k), после died_crash (1.5k) iOS; macOS 118 macOS: заморозка нового процесса на ≥30 с даёт error 6, корзина 25–35 с. iPhone: 4 сценария безуспешно. Главный iOS-хвост без unresponsive (в воспроизведении он есть) корень в WebKit (wakeUpBackgroundContentIfNecessary взводит 30-секундный таймер до загрузки worker'а). Гипотеза: перезагрузка расширения при возврате на передний план
P3. Контекст загружен, view нет load / load>base_url_changed 17.1k + 0.9k / 162 + 81 задержка очистки при смене baseURL воспроизведена на macOS при большом кэше (около 10 минут заполнения); iOS-гонка запуска (bg_view_creations=0, восстановление сессии, первые 30 с) не воспроизведена: холодный старт в harness'е отвечает за миллисекунды кандидат: стабильный baseURL (ветка cpm-stable-base-url: не доведён, меняет URL всем расширениям, не прогнан через harness, старая база). Фикс WebKit 318756@main, номер сборки ОС не проверен
P4. Первая загрузка load>view>proxy_on 6.8k + 2.3k / 570 да (фикс A) фикс A
P5. Фон выгружен в момент сбоя proxy_on>dealloc>view>proxy_on>dealloc 1.9k / 494 да: закрытие окна с shared pinned tab через production listener → WebKit сам удаляет сохранённый tab; точная сигнатура с tab=false (§11). Пустые listener metadata пока только test-only (§10) найден browser→WebKit путь для Mac/tab=false; фильтрации явных didCloseTab недостаточно — didCloseWindow закрывает tabs повторно. Для tab=true и iOS естественный вход не найден
P6. macOS: вкладка без расширения load>view>proxy_on>dealloc / load>view>proxy_on; варианты с extension_updated отдельно iOS ≈0 / 627 + 235 да: цикл главного webExtensions flag через production handler создаёт replacement manager, прежний WKWebView сохраняет старый controller и даёт точную сигнатуру (§11); nil controller до/после idle — test-only (§10) условный production-путь найден; фактическое переключение главного flag в выборке не подтверждено. Embedded-only cycle сохраняет controller; reload контекста старую привязку не чинит
Network заменён см. раздел 4 да (краш, worker мёртв при том же view) фикс B
«Здоровый» остаток, other iOS 12.5%, macOS 22% нет нет гипотезы

Сверка определения P5: строка выше сохраняет исходные 494 macOS. Повторная выборка 14 октября по хвосту P5 без error 6, network_restarted=true и scripts=false дала 1953 iOS / 499 macOS (§10). Разница 494/499 не устранена сверкой всех дополнительных фильтров: эти знаменатели не подменяют друг друга. В этом повторном Mac-срезе 68 снимков имеют tab_in_context=false, 374 — все три tab-флага true, 57 — неполную/неразрешённую диагностику вкладки. На iOS 646 снимков имеют все три tab-флага true, у 1299 вкладка не разрешилась, ещё 8 имеют неизвестный tab_in_context. Найденный shared-window механизм применим к сигнатуре Mac/tab=false, но происхождение всех 68 событий не доказано. Неразрешённая вкладка не тождественна tab_in_context=false.

P6 не равен P4 по одному хвосту: load>view>proxy_on при controller/scripts=false — P6, при корректной привязке и потерянном раннем init может быть A/P4. 235 событий P6 — хвост без dealloc, а не группа extension_updated. В 468/627 и 234/235 P6-снимках запуск моложе двух минут. Поэтому main-flag cycle проверяет достижимость, но его частоту и соответствие раннему запуску нужно подтвердить отдельно.

Размер группы не равен возможности её починить: P1 и P3 в основном опаздывающие ответы, а часть «сбоев» это срабатывание детектора («нет ответа за 4 с», опоздавший ответ тоже считается). iOS-когорта «other» на самом деле тот же рисунок P1, но со view старше 30 секунд.

6. Телеметрия

Что делает PR0 (#7376): на 8 failure-пикселей и 2 recovery-пикселя добавлены параметры bg_ready_after, bg_view_age, bg_load_error_after, bg_load_error_app_state, cpm_app_state, cpm_open_tabs, cpm_loading_tabs (failure) и cpm_recovery_document, cpm_recovery_after (recovery). Новых пикселей нет. Recovery-пиксели получают только свои два параметра, extension_reload_failed без параметров.

Замечания ревьюера (JA1–JA3): корреляция между пикселями из-за множества перечислений; сколько пикселей на ошибку; константность cpm_open_tabs/cpm_app_state; что такое background view.

Что верно по пикселям (исправление моей прежней ошибки): initialization_failed только daily (не больше одного на устройство в день на причину); stuck daily+count (daily и count одного события идут с одинаковыми параметрами); snapshot пересобирается при каждой отправке, поэтому быстро меняющиеся поля (bg_view_age, bg_ready_after) между init_failed и stuck различаются, медленные (app_state, open_tabs) повторяются.

Уникальность строк stuck.daily внутри дня (доля уникальных по всем параметрам):

Вариант iOS macOS
сейчас, bg_events из 12 событий 8.3% 30%
информативные токены, последние 4 2.1% 5.2%
скаляры вместо bg_events 0.96% 4.4%
без bg_events 0.25% 2.0%

Верхняя граница прироста от новых полей (синтетическое независимое равновероятное поле): со старым bg_events при десяти значениях iOS 21%, macOS 65%; со скалярами 3.6% / 15.8%. Вывод: «хуже не будет» можно утверждать только со скалярами. Главный идентификатор уже выкачен (bg_events).

bg_events: единственный источник давности загрузки (load, base_url_changed) и причины смерти (died_*). view, dealloc, proxy_on, error_* дублируют bg_view_creations и context_errors; hold/release/proxy_off равны нулю. Скаляры воспроизводят когорты загрузки и пробуждения на 100% строк. Теряется: поиск неожиданных последовательностей в остатке (смягчить удалённым флагом).

Минимум данных по гипотезам:

Поле / пиксель Вопрос Где
bg_ready_after, bg_view_age идёт ли загрузка, пробуждение или давно PR0
bg_load_error_after, bg_load_error_app_state (только при error 6) таймер 30 с, состояние приложения PR0
recovery_document, recovery_after опоздавший ответ или потерянный PR0
app_state, open_tabs (грубо, 1 / 2–5 / >5), только на initialization_failed когорта «нет view» на запуске PR0
новое: context_load_cause, context_load_age (вместо load@ и extension_updated) точная сигнатура A PR0
новое: bg_died_reason, base_url_changed смерть процесса, очистка при смене baseURL PR0
новое: мелкие корзины app_launch_age для первых 30 с (<4 / 4–10 / 10–30) гонка запуска PR0
новое на stuck: stuck_after сколько от первого сбоя до stuck PR0
новое, срочно: network_replaced_since_view доля реальных мёртвых worker'ов в когорте B (рекордер вычисляет, но только логирует; предложено 8 октября, нигде не реализовано) PR0
init_stage дошёл ли init до background #7385 (срочно)
cpm_background_ready_{context_load,wake} базлайн здоровых загрузок и пробуждений #7384
background_restart (причина, исход; убрать из него bg_ready_after, он дублирует соседний failure-пиксель), first_load_wait эффект B и A удержанная ветка
убрать loading_tabs; из таймлайна view, dealloc, proxy_on, error_*, hold/release/proxy_off

Связность стека: #7384 добавляет ещё 5 измерений на те же 8 failure-пикселей (context_load_to_background_view, cpm_low_power_mode, cpm_thermal_state, cpm_lockdown_mode, cpm_resume_age); оценку склейки надо повторять и по нему. background_restart срабатывает вместе с первым initialization_failed: общий ключ bg_ready_after.

7. Критерии успеха и опровержения

  • B подтверждается, если после релиза падают доля «Network заменён» среди stuck и reload_failed, background_restart показывает восстановление, а у таких stuck init_stage=none.
  • A подтверждается, если сокращается когорта «мид-сессионная первая загрузка» (context_load_cause). Если нет, основание слабое.
  • Остаток P1/P3: гипотеза «медленный старт, а не потеря» подтверждается, если bg_ready_after у упавших заметно хуже базлайна здоровых загрузок и recovery_document=failed_navigation.

8. Открытые решения и следующие шаги

  1. Правки PR0 (ждут вашего «да»): скаляры вместо bg_events, context_load_cause, stuck_after, network_replaced_since_view, мелкие корзины app_launch_age, убрать loading_tabs, константы только на initialization_failed, open_tabs грубо, bg_load_error_* только при error 6; после этого ребейз #7384 и #7385. Альтернатива «оставить bg_events» требует ответа ревьюеру без утверждения «хуже не будет» и обязательства перемерить после релиза.
  2. Ответ ревьюеру (публично, без цифр): JA1 (init_failed daily-only; stuck daily+count с одинаковыми параметрами; recovered несут только recovery_*), JA2 (медленные поля повторяются, быстрые различаются; планируется сократить набор), JA3 (background view = скрытый WKWebView, в котором WebKit запускает фоновый скрипт расширения; выгружается через около 30 с простоя и пересоздаётся при пробуждении). Обязательство: перемерить различимость строк после первого релиза.
  3. Запушить фикс B (f1085846e7): красный прогон подтверждён, ждёт зелёного.
  4. Судьба фикса A: слабое основание в данных, не совпадает с названием задачи; решить, сливать ли без зелёного измерения эффекта через first_load_wait.
  5. P3: довести стабильный baseURL (ограничить встроенными расширениями без web_accessible_resources, прогнать harness, перенести на свежий main), проверить номер сборки ОС с фиксом 318756@main.
  6. Локальные repro для P1–P3 и новые P5/P6 находятся в alex/cpm-web-extension-remaining-failures; отдельный PR без фиксов ещё не создавался. В этом worktree весь базовый класс gated CPM_REPRO=1, новые 14 методов дополнительно CPM_REPRO_P5_P6=1; requireLocalRun относится к выделенному PR-harness. Не путать opt-in и ветки при переносе.
  7. iOS: воспроизведение error 6 на реальном устройстве. Для Mac P5/tab=false найден и проверен путь shared pinned window → didCloseWindow → внутренний didCloseTab WebKit; для P6 проверен условный путь цикла главного flag (§11). Не установлены P5/tab=true, iOS lifecycle и реальность flag-transition в P6-сессиях.
  8. Блокеры по свежей сверке: #7376/#7378 и JS #3727 ждут ревью; #7384/#7385 стоят на #7376 и имеют failing Danger; #7381 остаётся на красном remote head без локального fix B.
  9. Найденный P5 tab lifecycle: проверить исправление полного window-close контракта, а не только списка явных didCloseTab. Направления — исключить сохраняемые shared tabs из delegate закрываемого окна либо восстановить регистрацию после window callback. Это варианты для отдельной реализации и проверки, не выполненный фикс. Контроли: закрытие второго/последнего окна, merge, перемещение и новые/существующие документы.
  10. P5 с исправной регистрацией и P6 attribution: естественная потеря listener metadata и фактический main-flag transition остаются без доказательства. Задержка main actor, длинный документ и один пропуск listener не заменяют такое доказательство; результаты отрицательных проверок сохранены в §10–12.

9. Методика и инструменты

  • Доступ: ~/Developer/grafana-data-tools (Grafana → ClickHouse metrics.pixels, read-only; авторизация через grafana-auth). Ad-hoc SQL запускался отдельным раннером через ~/DuckDuckGo/grafana-dashboards/tools/grafana-query/read-dashboard.py. Сканы по pixel LIKE без фильтра по дате, агенту и pixel_id='debug' превышают лимит строк. Временные запросы лежали в папке сессии и могут пропасть; ключевая классификация ниже.
  • Версии с диагностикой: iOS 7.240+, macOS 1.210+. Окно: 12–14 октября (3 полных дня). Ограничения: сигнатуры эвристические; network_restarted только прокси; устройства нельзя проследить от init до stuck, сравниваются только доли; доля не равна пользовательскому эффекту.
-- классификация когорт для init_failed.daily / stuck.daily
multiIf(
  errs LIKE '%ContextErrorDomain:6%', '1_error6',
  creations = '0', '2_no_background_view',
  scripts = 'false', '3_tab_not_connected',
  net = 'true', '4_network_replaced',
  has(ev,'view@30s') AND arrayExists(x -> startsWith(x,'dealloc@'), ev), '5_wake_view_lt30s',
  arrayExists(x -> x IN ('load@30s','load@1m','load@2m','base_url_changed@30s','base_url_changed@1m','base_url_changed@2m'), ev), '6_context_load_recent',
  '7_other')
-- параметры: network_restarted, context_errors, bg_view_creations, tab_has_ext_scripts, bg_events (decodeURLComponent)

10. Журнал воспроизведения P5/P6 — 15 октября 2026

Хронологический журнал первоначальных controlled экспериментов; дополнительные результаты с привязкой к production-путям и текущие ограничения — в §11.

Работа начата по запросу: добавить тесты новых гипотез и попытаться воспроизвести наблюдаемые цепочки. Продакшен-фиксы и публичные PR этим запросом не меняются.

Выбор worktree: /Users/admin/DuckDuckGo/apple-browsers.git/alex/cpm-web-extension-remaining-failures, ветка alex/cpm-web-extension-remaining-failures, HEAD a0717cf098cd1cbd32d5bc9d19aa7db0fab2399f. Здесь общий harness, production recorder, локальные эксперименты пробуждения, error 6 и очистки baseURL (P1–P3), без watchdog/фиксов A/B. Полного единого набора всех сценариев сейчас нет: базовые A/B находятся в cpm-repro-harness/фикс-ветках, init-progress E2E — в cpm-e2e-init. Существующие dirty-изменения сохранены; добавления этой работы ограничиваются тестовым harness.

Среда: macOS 26.7.1 (25G241), Xcode 27.1 (27A9275), системный WebKit. Прогон на macOS проверяет механизм; перенос на iOS требует отдельной проверки.

Уточнение данных перед тестами: read-only перепроверка ClickHouse за 14 октября, stuck.daily, iOS 7.240+/macOS 1.210+:

  • P5: в выборке хвоста proxy_on>dealloc>view>proxy_on>dealloc без error 6/Network replacement/scripts=false — 1953 iOS и 499 macOS, bg_view_alive=false во всех снимках; последняя view везде старше 30 с. Это определение чуть шире строки P5 выше, поэтому 499 не подменяет прежние 494 без сверки дополнительных фильтров. На macOS 68 снимков с tab_in_context=false; на iOS у 1299 не разрешилась вкладка для табличных диагностических полей.
  • P6: load>view>proxy_on>dealloc — 627 macOS; load>view>proxy_on — 235. В обеих подгруппах context_loaded=true, tab_in_context=true, tab_controller_match=false, tab_has_ext_scripts=false. В первых двух минутах запуска — 468/627 и 234/235. 235 — это хвост без dealloc, а не маркер extension_updated; варианты с extension_updated существуют отдельно.

Проверяемые гипотезы:

  1. P5: потеря регистрации вкладки или runtime listener приводит к отсутствию пробуждения после штатного idle; сравнить content-script send/error, background receive, текущую view и параметры failure-пикселя.
  2. P5: долгий документ/пауза приложения позволяют background выгрузиться до диагностического снимка; отличить запоздалый снимок от неработающего worker.
  3. P6: вкладка создана без CPM controller, затем попала в openTabs при загрузке контекста.
  4. P6: существующая вкладка сохранила controller предыдущего менеджера; новый контекст видит вкладку через openTabs, но не внедряет в неё скрипты.

Текущий статус: читаю точки расширения harness; тесты и результаты ещё не получены. Контролируемая инъекция состояния будет явно отделена от естественного воспроизведения.

P5/P6 — тесты добавлены, начат первый прогон

В CPMReproLifecycleHypothesisTests.swift добавлены 7 локальных XCTest-проверок, отдельный opt-in CPM_REPRO_P5_P6=1 поверх CPM_REPRO=1:

  • P6: отсутствующий controller до idle; тот же случай после штатной выгрузки и после reload контекста; перенос существующего WKWebView в replacement manager с сохранением прежнего controller.
  • P5: забытая в WebKit, но оставшаяся открытой вкладка после двух idle-циклов; отсутствие runtime listener после следующего пробуждения; два здоровых документа с реальной задержкой image-subresource на 35 с; те же документы с искусственно потерянными dashboard-уведомлениями.

Harness позволяет отдельно выбирать controller вкладки, переносить существующую view и подавлять конкретный native method только в тестах. CPMReproDelayedResource.swift удерживает реальный subresource через WKURLSchemeHandler; навигационные finish-события не подделываются. Проверки требуют actual stuck от production monitor, полный набор диагностических признаков, content-script initResp для здоровых контролей и восстановление после снятия инъекции.

Первым запущен testTabWithoutControllerMatchesP6BeforeIdleUnload; пока идёт сборка, результата нет. Логи сессии: /private/tmp/cpm-p5-p6-20261015/. Команда сборки: CPM_REPRO=1 CPM_REPRO_P5_P6=1 swift test --disable-automatic-resolution --filter CPMReproLifecycleHypothesisTests/testTabWithoutControllerMatchesP6BeforeIdleUnload.

Первый результат P6 до idle

Сборка прошла. Первый тест получил реальный initialization_failed и затем stuck с load>view>proxy_on, context_loaded=true, tab_in_context=true, tab_controller_match=false, tab_has_ext_scripts=false, bg_view_alive=true, bg_view_creations=1. Content marks пустые: CPM не внедрён. Новая вкладка с current controller получила initResp и вызвала recovery.

Прогон пока красный (1 assertion): дополнительное ожидание network_restarted=false не выполнено — поле true. Падения/замены Network-процесса тест не инъецирует. Проверяю холодный старт Network до контекста: recorder сравнивает PID с context-load, что может смешивать первичный запуск и замену. Результат нельзя объявлять полным совпадением всех признаков до этой проверки. Лог: build-and-p6-before-idle.log. SwiftLint пяти изменённых файлов прошёл с --no-cache, 0 нарушений; обычный режим упёрся в недоступный для записи кэш.

Уточнение harness и первый зелёный P6

Проверенный прогрев default WKWebsiteDataStore перед context-load даёт ненулевой Network PID; в последующем failure network_restarted=false. Это сняло лишнее отличие первого холодного прогона. testTabWithoutControllerMatchesP6BeforeIdleUnload теперь зелёный: совпадают хвост и все проверяемые признаки, новая правильно подключённая вкладка работает. Механизм получен контролируемым отсутствием controller; естественный путь создания такой вкладки в приложении ещё не найден.

Проверка replacement manager сначала не дошла до failure-пикселя: teardown первого harness снимает navigationDelegate, и при переносе view его надо восстановить. Исправлен только тестовый adoptTab; это ошибка постановки эксперимента, не результат о CPM. Начат полный прогон семи тестов с исправлением: p5-p6-full.log.

Дополнительный контроль P6

Добавлен восьмой тест testCurrentControllerBeforeContextLoadRemainsHealthy: создать вкладку до завершения context-load, но сразу с правильным controller, и потребовать initResp, скрипты и отсутствие initialization_failed. Это отделяет обычный ранний startup от отсутствующей привязки controller. Он будет выполнен после текущего запущенного бинарника; результат пока не получен.

Подтверждены два контролируемых механизма

P6, replacement manager — зелёный, 9.186 с. Сначала существующая вкладка получила initResp со старым менеджером. После teardown старого менеджера её тот же WKWebView и прежний controller зарегистрированы в replacement manager. Получены два failure и actual stuck с load>view>proxy_on, context/tab=true, controller/scripts=false, view=true, creations=1, errors=none, network_restarted=false. Новая вкладка replacement manager получила initResp. Это воспроизводит сохранённую привязку; естественный триггер замены менеджера не доказан.

P5, забытая живая вкладка — зелёный, 72.744 с. Два нормальных initResp и две реальные idle-выгрузки, затем test-only didCloseTab без удаления WKWebView. Получен actual stuck с load>view>proxy_on>dealloc>view>proxy_on>dealloc, creations=2, view=false, context=true, tab=false, controller/scripts=true, errors=none, network_restarted=false. Content script отправляет init и получает init-error. После didOpenTab эта же вкладка снова получила initResp. Механизм подходит под P5-подгруппу с tab_in_context=false; его доля в проде и естественное действие приложения, создающее нарушение lifecycle, этим тестом не установлены.

Оба результата находятся в p5-p6-full.log; другие сценарии продолжают выполняться.

Полный первый прогон: положительные и отрицательные результаты

Прогон p5-p6-full.log завершён: 7 тестов, 4 зелёных и 3 красных (11 assertions), 273.8 с. Не все красные результаты означают дефект harness:

  • P6 после idle — зелёный, 38.658 с. Получена цепочка load>view>proxy_on>dealloc с исходной сигнатурой controller/scripts=false, tab/context=true. Reload контекста не исправил существующую вкладку; новая правильно подключённая вкладка получила initResp.
  • Потеря runtime listener только на одном wake не воспроизвела P5. Инъекция действительно потеряла первый init (native dashboard не пришёл), но после восстановления файла и следующего idle новый worker принял следующий init и прислал initResp, stuck=0. Ожидание устойчивого P5 в тесте было неверным; превращаю его в отрицательную characterization-проверку этого результата.
  • Долгий документ: первый image-subresource действительно удерживался 35 с. Background выгрузился примерно на 32 с, но на finish навигации в 35 с WebKit снова создал background; на failure в 39 с view уже жива. При искусственной потере dashboard получен stuck с живой view, а не P5. Здоровый документ получил initResp и не вызвал failure.
  • Ошибка постановки второго долгого документа: одинаковый URL image взялся из кэша, поэтому второй load не был долгим. Исправляю уникальный URL на каждую навигацию и повторю оба контроля.
  • Наблюдение о trace: watcher держал strong reference на view через 50-мс await. При переходе сразу после context._backgroundWebView=nil dealloc старой view иногда попадал после создания новой. Убираю удержание через await и буду ждать одновременно отсутствия WebKit background и dealloc в production recorder, чтобы не менять исследуемую последовательность самим наблюдением.

Все неподтверждённые результаты выше остаются отрицательными/ограниченными; совпадение P5 пока подтверждено только для test-only потери регистрации живой вкладки.

Уточнённые проверки P5 — начат повтор

Теперь 9 тестов. В новом testNoRuntimeListenerMetadataFromFirstStartupMatchesP5 runtime listener не регистрируется с самого первого старта; затем background явно загружается второй раз и оба раза штатно выгружается. Это отдельно проверяет пустые listener metadata, которые нельзя было получить простым пропуском регистрации на одном wake после здорового старта. Условия искусственные; естественная утрата metadata не доказана. После проверки инъекция снимается, выполняются explicit background load и проверка initResp.

Первоначальный listener-тест переименован в testMissingRuntimeListenerOnOneWakeRecoversAfterNextIdle и проверяет реально наблюдённое восстановление, отсутствие stuck и третью view.

Оба 35-секундных документа теперь используют разные URL image. Ожидания изменены по наблюдению первого прогона: здоровый длинный документ — no failure; потеря dashboard — stuck с живой view, не P5. Расширен test-only background trace на все типы autoconsent-сообщений, чтобы установить, какое сообщение повторно будит worker на finish.

Watcher больше не удерживает WKWebView через await, а idle-helper требует и освобождения background в WebKit, и dealloc по recorder. Повтор пяти новых/уточнённых проверок: p5-refined-and-controls.log.

Здоровый startup-контроль подтверждён

testCurrentControllerBeforeContextLoadRemainsHealthy — зелёный, 1.190 с: вкладка с правильным controller проходит стартовый gate, получает initResp и скрипты, initialization_failed=0. Одной ранней загрузки контекста для P6 в этом сценарии недостаточно.

Повтор one-wake listener подтвердил восстановление и отсутствие stuck. Остался только ошибочный assertion формата счётчика: pixel bg_view_creations для трёх view равен 3_to_5, а не строке 3. Проверка заменена на raw backgroundWebViewCreateCount == 3; будет повторена. Расширенный trace действительно показывает дополнительные report/eval после init — проверяю их связь с повторным пробуждением длинного документа.

P5 с исправными tab/controller/scripts — воспроизведён

testNoRuntimeListenerMetadataFromFirstStartupMatchesP5 зелёный, 70.149 с. Пропуск runtime listener с первого старта, две реальные idle-выгрузки (между ними explicit background load), затем две навигации дали actual stuck: load>view>proxy_on>dealloc>view>proxy_on>dealloc, view=false, creations=2, context/tab/controller/scripts=true, errors=none, network_restarted=false. Content script отправляет init; новые view не создаются. После разрешения регистрации listener в установленной тестовой копии скрипта, explicit background load и навигации получен initResp.

Это соответствует P5 с внешне исправной вкладкой. Доказан механизм пустых listener metadata при test-only инъекции; естественная потеря metadata в приложении/WebKit не найдена. В отличие от него, одноразовый пропуск listener после здорового старта восстанавливается на следующем wake.

Два долгих документа с потерянными dashboard после устранения image-cache — зелёная отрицательная проверка, 80.181 с: оба load действительно по 35 с, в обоих документах есть initResp, production monitor получает stuck, но в его snapshot view=true и цепочка заканчивается view>proxy_on. Это воспроизводит сбой диагностического канала, но не P5. Здоровый контроль ещё выполняется. Лог: p5-refined-and-controls.log.

Долгие документы: повтор завершён

Здоровый контроль с двумя разными 35-секундными image — зелёный, 70.683 с: оба документа получили initResp, initialization_failed=0 и stuck=0. На finish background снова просыпается: trace показывает recv report и recv eval примерно на 35.39 с после background none на 32.35 с. Таким образом, в проверенном сценарии длинная загрузка сама по себе не даёт P5.

Повтор пяти тестов завершён: четыре зелёных, один красный исключительно из-за ранее описанного сравнения bucket со строкой «3» (исправлено). Начат финальный прогон оставшихся сценариев и исправленного assertion: p5-p6-final-regressions-and-stall.log.

Добавлен десятый эксперимент testMainActorStallDelaysActualFailureSnapshots: после реального initResp искусственно теряется dashboard, затем test host main actor дважды блокируется на 35 с при ожидающем production timeout. Тест требует actual failures/stuck и восстановления; совпадение P5 отдельно записывается как matchesP5, а не предполагается. Это исследование задержанного выполнения host callbacks, не реальная iOS suspend/resume.

Задержка host main actor: P5 не получен

testMainActorStallDelaysActualFailureSnapshots — зелёный, 71.027 с; результат классификации matchesP5=false. При двух блокировках main actor на 35 с реальные failure-пиксели пришли через 35.42 и 70.68 с, второй дал stuck. В обоих снимках view=true, creations=1, хвост остаётся load>view>proxy_on, context/tab/controller/scripts=true, errors=none, network_restarted=false. InitResp пришёл в документы до пауз; dashboard терялся намеренно. После снятия инъекции новая вкладка работает.

Это подтверждает задержку deadline, но в проверенном порядке callbacks не воспроизводит P5. Настоящие iOS suspend/resume и остановка всего процесса не проверялись; блокировка main actor не подменяет их.

Финальный прогон также повторно подтвердил replacement-manager P6 (9.167 с) и P5 с забытым live tab (72.737 с) после устранения удержания view watcher'ом.

Итог локальных repro-проверок

Финальный прогон шести сценариев — зелёный: 6 тестов, 0 failures, 262.676 с. Исправленный one-wake listener-тест зелёный (62.633 с). P6 до/после idle повторно зелёные (8.569 / 38.543 с).

С учётом четырёх зелёных независимых проверок из предыдущего прогона все 10 актуальных методов добавленного класса имеют зелёный результат. Это проверено несколькими запусками; единого прогона всех десяти после последних добавлений не было. Сводка по каждому методу и исходному логу: /private/tmp/cpm-p5-p6-20261015/results.json, генератор summarize.py.

Что воспроизведено:

  • P6: отсутствующий controller до и после idle; сохранённый controller существующего WKWebView после replacement manager. Reload контекста не исправляет такую вкладку; новая вкладка работает.
  • P5: потеря WebKit-регистрации сохранившейся живой вкладки; отсутствие runtime listener metadata с первого старта. Совпадают хвост, actual stuck и диагностические признаки; после адресного снятия инъекции работа восстанавливается.

Что P5 не дало на macOS 26.7.1: однократная потеря listener на wake после здорового старта, два действительно долгих документа, две задержки main actor на 35 с. Потеря dashboard при дошедшем initResp даёт диагностический stuck с живой view.

Ограничения: все положительные воспроизведения требуют test-only условия; естественные production-триггеры отсутствующего controller/утраты listener metadata не найдены. Настоящая iOS suspension не проверена. Фиксы поведения приложения этим исследованием не добавлялись.

Запущен обычный пакетный прогон swift test --disable-automatic-resolution --skip-build без repro-флагов; результат будет записан ниже. Финальный strict SwiftLint семи изменённых файлов: 0 нарушений (--no-cache). SHA-1 существующего dirty Package.resolved не изменился: 7ccce20acd452656c9bf2d2fa186d044e4a8185f.

Финальная проверка пакета и файлы

Обычный пакетный прогон завершён успешно: swift test --disable-automatic-resolution --skip-build, 474 XCTest, из них 24 opt-in repro пропущены, 0 failures, 11.344 с. Лог: /private/tmp/cpm-p5-p6-20261015/package-tests.log. Отдельный Swift Testing runner: 0 тестов. Длительные эксперименты запускались отдельно с обоими repro-флагами; все 10 текущих методов имеют зелёный результат в сводке.

Изменены только тестовые файлы этой работы внутри выбранного worktree:

  • добавлены CPMRepro/CPMReproLifecycleHypothesisTests.swift (10 тестов) и CPMRepro/CPMReproDelayedResource.swift;
  • расширены CPMReproHarness.swift, CPMReproTabs.swift, CPMReproSupport.swift, CPMReproPrelude.swift, CPMReproTestCase.swift;
  • existing dirty production/telemetry changes не включались в эту работу, Package.resolved сохранил первоначальную контрольную сумму. Commit/PR/push не делались.

Команда полного повторения новых экспериментов из SharedPackages/WebExtensions:

CPM_REPRO=1 CPM_REPRO_P5_P6=1 swift test --disable-automatic-resolution --filter CPMReproLifecycleHypothesisTests

Эта полная команда приведена для повторения: в текущей сессии десять методов проверены раздельными группами, а не одним финальным запуском всех десяти.

Текущий тест Последний результат, с Лог
CurrentControllerBeforeContextLoadRemainsHealthy pass, 1.190 p5-refined-and-controls.log
TabWithoutControllerMatchesP6BeforeIdleUnload pass, 8.569 p5-p6-final-regressions-and-stall.log
TabWithoutControllerMatchesP6AfterIdleUnload pass, 38.543 p5-p6-final-regressions-and-stall.log
ExistingTabKeepsPreviousManagerController pass, 9.167 p5-p6-final-regressions-and-stall.log
ForgottenLiveTabDoesNotWakeBackgroundAndMatchesP5 pass, 72.737 p5-p6-final-regressions-and-stall.log
MissingRuntimeListenerOnOneWakeRecoversAfterNextIdle pass, 62.633 p5-p6-final-regressions-and-stall.log
NoRuntimeListenerMetadataFromFirstStartupMatchesP5 pass, 70.149 p5-refined-and-controls.log
SlowHealthyDocumentsWakeBackgroundAgainAtFinishWithoutFailure pass, 70.683 p5-refined-and-controls.log
SlowDocumentsWithLostDashboardDoNotMatchP5 pass, 80.181 p5-refined-and-controls.log
MainActorStallDelaysActualFailureSnapshots pass, 71.027; matchesP5=false p5-p6-final-regressions-and-stall.log

Зелёный результат characterization-теста означает, что проверенное условие и ожидаемый исход подтверждены. Он не означает исправление CPM или доказательство естественного production-триггера.

11. Привязка P5/P6 к достижимым production-путям

Продолжение после замечания: положительный test-only fault доказывает механизм, но не объясняет его возникновение у пользователя. Дальше нужны конкретные caller → browser state → WebKit side effect.

Найден кандидат P5: закрытие окна с shared pinned tab

Проверено в исходниках текущего repro-worktree и macOS release tag 1.210.0+macos:

  • MainWindowController.windowWillClose отфильтровывает сохранённые общие pinned tabs перед явными didCloseTab, но после windowControllersManager.unregister(self) всё равно вызывает eventsListener.didCloseWindow(self).
  • MainWindowController+WKWebExtensionWindow.tabs(for:) возвращает loadedPinnedTabs + loadedTabs; MainViewController.windowWillClose и WindowControllersManager.unregister эти коллекции не очищают.
  • В локальном WebKit checkout 99051d5d08cd9f19ec76ce599f7539d070b1ae09, WebExtensionContextCocoa.mm:1351–1367, didCloseWindow вызывает didCloseTab для каждого элемента window.tabs(). Фильтрация явных callbacks браузером не предотвращает этот внутренний вызов.
  • didCloseTab удаляет tab из m_tabMap; runtimeSendMessage проверяет tab до пробуждения background и при отсутствии возвращает tab not found. Это возможная связь с P5/tab_in_context=false.

Начат эксперимент на системном WebKit: один и тот же живой tab в двух windows; закрыть одну через production WebExtensionEventsListener.didCloseWindow, сохранить tab в другой, без прямого didCloseTab, изменения listener или выгрузки context. До выполнения это кандидат, не подтверждённое воспроизведение.

Проверка истории также отсекла старые объяснения: исправление Close Other Tabs и фильтр shared pinned tabs уже находятся в 1.210.0+macos; синхронное создание manager до state restoration существует с февраля 2026. Их прежние версии нельзя выдавать за текущий production-триггер.

P5: production callback → WebKit side effect подтверждены

Два новых теста зелёные, 0 failures:

  • testClosingWindowForgetsSharedPinnedTabThroughWebKitAndMatchesP5 — 73.033 с. Один живой tab предоставлен двумя window delegates, как shared pinned tab в macOS. Две успешные инициализации и две штатные idle-выгрузки. Затем только unregister закрываемого окна и production eventsListener.didCloseWindow — без явного didCloseTab, изменения listeners, ручной выгрузки context или замены manager. Сразу после callback tab_in_context меняется true → false, WKWebView остаётся тем же и tab сохраняется в другом окне.
  • Две последующие навигации дали actual initialization failures и stuck: load>view>proxy_on>dealloc>view>proxy_on>dealloc, creations=2, view=false, context=true, tab=false, controller/scripts=true, errors=none, network_restarted=false. Content script получает init-error. Новая вкладка работает; didOpenTab восстанавливает исходную.
  • testClosingUnrelatedWindowPreservesRegisteredTab — 1.205 с: закрытие окна, не возвращающего эту вкладку, сохраняет tab_in_context=true, initResp и 0 failures.

Лог: /private/tmp/cpm-p5-p6-20261015/production-window-close.log. Это source-linked воспроизведение на системном WebKit под macOS 26.7.1 через production listener и неизменённую логику CPM, а не произвольное закрытие живого tab. Harness использует window/tab delegates, повторяющие контракт macOS; сам MainWindowController/AppKit целиком в SPM-тест не загружен. Следовательно, browser call chain подтверждён чтением исходников, внутренний эффект WebKit и P5 — исполнением теста.

Область объяснения: P5/tab_in_context=false (в выбранном Mac-срезе 68 событий). Это не доказательство, что все эти события были caused by shared pinned windows, и не объяснение P5/tab_in_context=true либо iOS.

Тесты WindowsManagerPopupTests в macOS проверяют только список явных didCloseTab и поэтому не проверяют повторное закрытие tabs внутри системного didCloseWindow.

P6: найден условный production-путь, начата проверка

В AppDelegate.setupWebExtensions главная webExtensions flag подключена к production WebExtensionFeatureFlagHandler. Disable вызывает uninstallAllExtensions, затем callback очищает coordinator/manager. Enable вызывает initializeWebExtensions: если manager отсутствует, создаётся новый controller, а существующие WKWebView не пересоздаются. WebKit WebExtensionTab::webView отвергает view, чей controller не равен controller контекста.

Добавлены отдельные source-linked тесты CPMReproProductionFlagTests:

  • явное true → false → true главного flag через реальный handler, с host callbacks по AppDelegate и существующей вкладкой, предоставленной WebKit при загрузке replacement context;
  • negative control: цикл только embedded flag, при неизменном главном, должен сохранить controller и initResp;
  • внутри первого теста есть контроль true → true: повторный одинаковый флаг не должен заменять manager.

Это проверка достижимости механизма через production flag handler, не утверждение, что flag переключался у пользователей. И macOS, и iOS имеют defaultValue enabled для главного flag; подтверждения реального false → true в рассматриваемой выборке нет. Обычная задержка скачивания config сама по себе не доказывает этот путь.

Первый прогон flag-путей: P6 получен, контроль обнаружил известную startup race

production-flag-cycle.log: главный flag-тест зелёный, 9.244 с. Реальный handler по false удалил contexts и host references; по true создан новый manager при сохранённом старом WKWebView/controller. Получены два actual failures и stuck с точным load>view>proxy_on, context/tab/view=true, controller/scripts=false, creations=1, errors=none, network_restarted=false. true → true manager не заменил; новая вкладка replacement manager работает. Это подтверждает условный production-путь P6 при реальном переключении главного flag.

Embedded-контроль красный, 6.119 с, 3 assertions: controller/scripts/tab=true, но первая навигация в 0.704 с ушла до sw activate в 0.747 с и потеряла init. Таким образом, «embedded cycle всегда здоровый сразу после sync» — неверное ожидание: здесь воспроизводится уже известный механизм A / P4, а не P6. Не считаю красный результат ошибкой постановки.

Для изоляции именно controller-binding уточняю контроль: после sync явно дождаться loadEmbeddedExtensionBackgroundContent, затем требовать initResp без failures при прежнем controller. Этот readiness wait — контрольное условие теста; production fix A данной работой не добавляется.

P6: уточнённый прогон завершён

production-flag-cycle-final.log: 2 tests, 0 failures, 10.237 с:

  • testMainFeatureFlagCycleRecreatesManagerAndMatchesP6 — 9.252 с; повторное подтверждение P6 через true → false → true главного flag.
  • testEmbeddedFeatureFlagCycleKeepsControllerAfterBackgroundBecomesReady — 0.985 с; прежний controller и scripts=true, после explicit readiness wait initResp и 0 failures. Первоначальный красный контроль с потерей первой навигации сохранён в предыдущем логе и описан выше как известный A.

Таким образом, добавлены четыре новых source-linked проверки: две window-close и две flag-cycle; все четыре текущих метода зелёные в двух запусках. Они выполняют production listener/flag handler/manager/CPM и системный WebKit; app-side host callbacks и window delegates реализованы harness по исходникам macOS, приложение целиком не запущено.

Оставшиеся неизвестные: происхождение P5 с tab_in_context=true; реальные iOS lifecycle-условия; было ли переключение главного flag в P6-сессиях. Главный flag default enabled, поэтому config fetch с неизвестной задержкой не считается доказанным триггером.

Проверка после source-linked дополнений

  • Обычный пакет: swift test --disable-automatic-resolution --skip-build — 478 XCTest, 28 opt-in skipped, 0 failures, 10.469 с. Лог package-tests-production-paths.log.
  • Strict SwiftLint всех восьми файлов этой работы — 0 violations (--no-cache).
  • summarize.py и results.json обновлены: 14 текущих методов (10 первоначальных + 4 source-linked) имеют pass по последним соответствующим логам. Это несколько групп запусков, не единый прогон всех 14.
  • Изменения этой работы остались в SharedPackages/WebExtensions/Tests/WebExtensionsTests/CPMRepro/: дополнен harness windows/initial-tabs, два P5 window tests в CPMReproLifecycleHypothesisTests.swift, новый CPMReproProductionFlagTests.swift. Новых production-правок нет; первоначальный tracked dirty diff — те же 10 файлов / 164 additions / 8 deletions. SHA-1 Package.resolved прежний 7ccce20acd452656c9bf2d2fa186d044e4a8185f.

Точки для проверки call chain:

  • browser: macOS/DuckDuckGo/MainWindow/MainWindowController.swift:611–640, macOS/DuckDuckGo/WebExtensions/MainWindowController+WKWebExtensionWindow.swift:31, macOS/DuckDuckGo/Windows/View/WindowControllersManager.swift:178;
  • WebKit: Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionContextCocoa.mm:1351–1367 (window closes tabs), :1419 (forgetTab), Cocoa/API/WebExtensionContextAPIRuntimeCocoa.mm:129–141 (tab check before wake);
  • P6 browser: macOS/DuckDuckGo/Application/AppDelegate.swift:2153–2166,2209–2221; shared production handler: Sources/WebExtensions/FeatureFlag/WebExtensionFeatureFlagHandler.swift; WebKit validates controller in WebExtensionTabCocoa.mm:339–360.

Повторить именно четыре новых source-linked теста из alex/cpm-web-extension-remaining-failures/SharedPackages/WebExtensions:

CPM_REPRO=1 CPM_REPRO_P5_P6=1 swift test --disable-automatic-resolution --filter 'CPMReproProductionFlagTests|CPMReproLifecycleHypothesisTests/testClosing'

12. Актуальный итог P5/P6: доказательства, полный реестр и повторение

Границы доказательства и сигнатуры

Механизм Что проверено Чего это не доказывает
P5: закрытие окна с общим pinned tab Browser call chain прочитан в текущем worktree и tag 1.210.0+macos; production listener и системный WebKit исполнены. После window callback tab сохраняется в другом window delegate, но tab_in_context становится false; два failure → stuck с нужным хвостом Полное приложение/AppKit в SPM не запускались; неизвестно, какая доля production P5 связана с этим действием. Не объясняет P5/tab=true или iOS
P5: нет runtime listener metadata При подавлении регистрации с первого старта, двух настоящих idle-выгрузках и explicit load между ними получен P5 с исправными tab/controller/scripts Это test-only исходное условие. Естественное исчезновение metadata и подходящий production caller не найдены
P6: главный flag меняет manager Реальные handler/uninstall/load/context, существующая view при загрузке replacement context; прежний controller сохраняется и воспроизводит P6 Смена состояния flag задаётся тестом; события false→true в production-выборке не установлены. Default главного flag enabled на обеих платформах
P6: nil controller До и после idle контекст знает tab, но scripts отсутствуют; context reload не исправляет конфигурацию созданного WKWebView Это controlled construction. Синхронный manager до state restoration уже есть с февраля 2026; обычный ранний startup сам по себе причиной не оказался
Длинные документы / потерянный dashboard / задержка main actor Требуется отдельно смотреть page initResp, native arrival и actual failure snapshot. Длинные здоровые документы работают; lost dashboard/stall дают stuck при живой view Ни один из этих сценариев не воспроизвёл P5 на проверенной macOS. Host main-actor stall не равен настоящему iOS suspend/resume

Две положительные сигнатуры:

  • P5: load>view>proxy_on>dealloc>view>proxy_on>dealloc; creations=2, view=false, context=true, errors=none, network_restarted=false. Shared-window/прямой didCloseTab: tab=false, controller/scripts=true. Отсутствие listener с первого старта: tab/controller/scripts=true.
  • P6: load>view>proxy_on (view=true) либо load>view>proxy_on>dealloc (view=false); creations=1, context/tab=true, controller/scripts=false, errors=none, network_restarted=false. Main-flag source-linked тест проверяет вариант с живой view; вариант после idle проверен с nil controller.

tab_controller_match=false означает nil или другой controller: recorder требует ненулевую identity-совместимость. tab_in_context проверяет identity tab среди context.openTabs; tab_has_ext_scripts ищет user scripts в content world текущего UUID контекста. Эти поля не являются взаимозаменяемыми и не доказывают page receipt.

Все 14 текущих P5/P6-проверок

Все результаты ниже извлечены из локальных логов; название без суффикса файла — метод XCTest. Первые 12 находятся в CPMReproLifecycleHypothesisTests.swift, последние 2 — в CPMReproProductionFlagTests.swift.

Метод Последний результат, с Наблюдённый смысл pass Исходный лог
testCurrentControllerBeforeContextLoadRemainsHealthy pass, 1.190 Контроль P6: correct controller до context-load → initResp, 0 failures p5-refined-and-controls.log
testTabWithoutControllerMatchesP6BeforeIdleUnload pass, 8.569 Инъекция nil controller → P6, view=true; новая вкладка работает p5-p6-final-regressions-and-stall.log
testTabWithoutControllerMatchesP6AfterIdleUnload pass, 38.543 nil controller → P6 после idle; context reload не чинит эту view p5-p6-final-regressions-and-stall.log
testExistingTabKeepsPreviousManagerController pass, 9.167 Прямой controlled replacement → P6; прежние view/controller сохранены p5-p6-final-regressions-and-stall.log
testClosingWindowForgetsSharedPinnedTabThroughWebKitAndMatchesP5 pass, 73.033 Browser callback → WebKit: shared-window close → P5/tab=false; восстановление и healthy новая вкладка production-window-close.log
testClosingUnrelatedWindowPreservesRegisteredTab pass, 1.205 Контроль window close: без общей вкладки initResp и 0 failures production-window-close.log
testMainActorStallDelaysActualFailureSnapshots pass, 71.027 Две паузы host main actor по 35 с → actual stuck, но matchesP5=false; view жива p5-p6-final-regressions-and-stall.log
testForgottenLiveTabDoesNotWakeBackgroundAndMatchesP5 pass, 72.737 Прямой test-only didCloseTab → P5/tab=false; didOpenTab восстанавливает p5-p6-final-regressions-and-stall.log
testMissingRuntimeListenerOnOneWakeRecoversAfterNextIdle pass, 62.633 Один wake без listener после здорового старта → следующий wake здоровый, stuck=0 p5-p6-final-regressions-and-stall.log
testNoRuntimeListenerMetadataFromFirstStartupMatchesP5 pass, 70.149 Test-only отсутствие listener с первого старта; два idle и explicit load между ними → P5/all tab flags=true p5-refined-and-controls.log
testSlowHealthyDocumentsWakeBackgroundAgainAtFinishWithoutFailure pass, 70.683 Два 35-секундных image → initResp, report/eval снова будят background на finish, 0 failures p5-refined-and-controls.log
testSlowDocumentsWithLostDashboardDoNotMatchP5 pass, 80.181 Два 35-секундных image + потеря dashboard → initResp и stuck с живой view; не P5 p5-refined-and-controls.log
testMainFeatureFlagCycleRecreatesManagerAndMatchesP6 pass, 9.252 Production flag handler true→false→true → P6; true→true controller не меняет production-flag-cycle-final.log
testEmbeddedFeatureFlagCycleKeepsControllerAfterBackgroundBecomesReady pass, 0.985 Embedded-only cycle сохраняет controller; initResp после explicit background-ready wait production-flag-cycle-final.log

Зелёный тест с ожидаемым сбоем означает воспроизведение механизма; зелёный negative control означает отсутствие предполагаемой сигнатуры в проверенных условиях. Исходные неудачные ожидания, ошибки harness и их исправления не удалены из §10–11. Единого итогового запуска всех 14 не было: green подтверждён раздельными группами. 478 XCTest обычного пакета включают 28 пропущенных opt-in; они не заменяют выполненные длительные repro.

Среда, исходники и устройство harness

  • Worktree: /Users/admin/DuckDuckGo/apple-browsers.git/alex/cpm-web-extension-remaining-failures, HEAD a0717cf098cd1cbd32d5bc9d19aa7db0fab2399f; новые тесты untracked, поэтому checkout одного commit их не восстановит.
  • macOS 26.7.1 (25G241), Xcode 27.1 (27A9275), системный WebKit; bundled embedded extension 2026.9.29. Проверка source: /Users/admin/Downloads/WebKit_sparse, HEAD 99051d5d08cd9f19ec76ce599f7539d070b1ae09. Этот checkout не доказывает, что весь его код соответствует shipped binary; нужный эффект отдельно проверен на system WebKit.
  • Harness запускает production extension manager/coordinator, recorder, health monitor и native autoconsent handler, реальные WKWebView и WKWebExtension contexts. Host window/tab delegates и callbacks flag enable/disable собраны по исходникам macOS, не импортированы из целого приложения.
  • База Options: startup navigation gate и M4 включены. В P5/P6-тестах M4 выключен, чтобы actual stuck не был сразу затёрт automatic reload; navigation gate остаётся включён. Перед context load прогревается Network-процесс default data store. Без прогрева PID=0→ненулевой может дать network_restarted=true без crash; это было причиной первого несовпадения.
  • Временная распакованная копия bundled extension содержит test prelude/DOM marks и trace. Production zip не менялся. В source-linked window/flag сценариях listener не подавляется, dashboard не теряется намеренно, background startup не задерживается. Это не byte-identical bundle без instrumentation.
  • Отдельные controlled сценарии используют: nil/старый controller, suppressed runtime listener, suppressed native dashboard method, задержанный image через WKURLSchemeHandler или блокировку main actor. Их нельзя выдавать за найденный естественный production-trigger.
  • Content marks требуют initresp для здоровых контролей; native dashboard arrival отдельно измеряет production detector. Trace не должен удерживать view: watcher исправлен, чтобы strong reference не пересекал 50-мс await. Idle-helper требует одновременно context._backgroundWebView == nil и backgroundWebViewAlive == false в recorder.
  • Долгие image имеют уникальный URL на каждую навигацию, иначе второй документ берётся из cache. adoptTab восстанавливает navigationDelegate после teardown предыдущего harness. Счётчик 3 view в pixel — bucket 3_to_5; raw assertion использует backgroundWebViewCreateCount == 3.
  • Три новых файла: CPMReproLifecycleHypothesisTests.swift, CPMReproDelayedResource.swift, CPMReproProductionFlagTests.swift. Пять дополненных: CPMReproHarness.swift, CPMReproTabs.swift, CPMReproSupport.swift, CPMReproPrelude.swift, CPMReproTestCase.swift. Существующая dirty telemetry/Package.resolved не включалась в эту работу; тестовыми изменениями production поведение не исправлялось.

Артефакты и история красных прогонов

Все перечисленные файлы находились в /private/tmp/cpm-p5-p6-20261015/ на момент документирования. Папка временная: долговечная сводка значимых исходов, причин красных assertions и ограничений сохранена здесь и в §10–11; сами raw logs могут исчезнуть.

Файл Результат и как его читать
build-and-p6-before-idle.log P6 получен, но network_restarted=true из-за холодного PID attribution; не полное совпадение
p6-fast.log 2 теста / 1 failure: перенос старой view упёрся в снятый navigationDelegate; исправлен test adoptTab. P6 до idle уже green
p5-p6-full.log 7 тестов / 11 failing assertions, 273.808 с: one-wake listener сам восстановился; finish длинного документа разбудил background; повторный image был cached. Эти результаты разделены на отрицательные выводы и ошибку постановки, а не скрыты
p5-refined-and-controls.log 5 тестов / 1 failure, 289.130 с: 4 green, оставшийся assertion ожидал строку «3» вместо bucket «3_to_5». Исправлен raw count assertion и повторён
p5-p6-final-regressions-and-stall.log 6 тестов / 0 failures, 262.676 с; задержка main actor даёт matchesP5=false
production-window-close.log 2 теста / 0 failures, 74.238 с; source-linked P5 и unrelated-window control
production-flag-cycle.log 2 теста / 3 assertions, 15.363 с: main flag P6 green; embedded-only first init потерян до worker activate — известный A/P4, не P6
production-flag-cycle-final.log 2 теста / 0 failures, 10.237 с; embedded control проверен после explicit ready wait
package-tests.log Промежуточный пакет: 474 XCTest, 24 skipped, 0 failures, 11.344 с
package-tests-production-paths.log Последний пакет: 478 XCTest, 28 skipped, 0 failures, 10.469 с
summarize.py, results.json Сводка последних результатов 14 текущих методов, имена исходных логов и SHA-256 source файлов; это не лог одного общего прогона

Контрольные суммы актуальных тестов:

  • CPMReproLifecycleHypothesisTests.swift: 60c2f3081c08a40cfc0244e023652013f0abf939151a63c14addc400a40ed413.
  • CPMReproProductionFlagTests.swift: 240c3828e3fac13357c6e80a954f3d07fc5dd26376d36308258ecc984bfd860e.
  • Существующий dirty Package.resolved, SHA-1: 7ccce20acd452656c9bf2d2fa186d044e4a8185f.

Команды повторения и границы следующей работы

Из /Users/admin/DuckDuckGo/apple-browsers.git/alex/cpm-web-extension-remaining-failures/SharedPackages/WebExtensions:

# Все 14 P5/P6 методов; эта общая команда дана для повторения, единым прогоном ещё не выполнялась.
CPM_REPRO=1 CPM_REPRO_P5_P6=1 swift test --disable-automatic-resolution --filter 'CPMReproLifecycleHypothesisTests|CPMReproProductionFlagTests'

# Только четыре новых source-linked метода.
CPM_REPRO=1 CPM_REPRO_P5_P6=1 swift test --disable-automatic-resolution --filter 'CPMReproProductionFlagTests|CPMReproLifecycleHypothesisTests/testClosing'

# Обычные тесты после актуальной сборки: repro без флагов пропускаются.
swift test --disable-automatic-resolution --skip-build

Strict SwiftLint запускался для восьми перечисленных Swift-файлов из корня worktree: mint run swiftlint lint --strict --force-exclude --no-cache <paths>, 0 violations. --no-cache нужен, чтобы не писать во внешний SwiftLint cache; это не пропуск правил.

Карта P1–P3/A/B/init-progress, file names, параметры тяжёлых экспериментов и актуальные worktree — cpm-web-extension-code-guide-2026-10-09. Нельзя предполагать, что один worktree содержит все эксперименты или что HEAD включает локальные untracked файлы.

Эта актуализация — документация. Новые app/WebKit fixes, telemetry/schema changes, commit/push/PR, сообщения ревьюерам, переключения remote flags и новые iOS/device проверки не выполнялись. Старые результаты до исправления ожиданий сохраняются, а causal attribution production-групп не повышена до доказанной.

13. Integration tests в настоящем приложении — работа начата

Запрос: без commit добавить app integration tests, по возможности воспроизвести известные сбои, обязательно разделить slow load, одну потерянную навигацию с recovery, persistent single-tab, несколько tabs одного окна и все окна.

Выбран cpm-e2e-init (HEAD 46f25175c7c42766cab23ea39f447f075c7bdd24): здесь уже PR0/baseline/init_stage и изменённый до этой работы JS bundle для init-progress. Существующие dirty bundle и SPM harness сохраняются. Новый слой — macOS app-hosted Integration Tests, реальный DuckDuckGo.app/AppDelegate/Tab/MainWindowController, production navigation/native/diagnostics. Это отличается от прежнего standalone SPM harness с собственными window delegates.

Планируется отдельная схема без destructive UI-scheme clean-app pre-action. Внешние сервисы/config и pixel transport изолируются, production monitor/navigation callbacks вручную не подделываются. Естественные P5 shared-window / условный P6 flag / process failure и controlled diagnostic scopes будут явно различаться. Начата предварительная сборка в /private/tmp/cpm-app-integration-20261015/; результатов новых integration tests пока нет.

App integration: реализация, 16 октября

В cpm-e2e-init/macOS/IntegrationTests/CPM/ добавлены app-hosted harness и отдельные методы для slow HTTP, slow native settings (received → recovery), одной потерянной навигации, persistent one-tab, нескольких tabs одного окна, всех окон и dashboard-transport negative control (initResp получен, detector timeout). Естественные/условные пути вынесены в CPMAppKnownFailureIntegrationTests: реальные AppDelegate flag callbacks, shared pinned window close, process termination, первая embedded install, slow wake, opt-in 30s WebKit timeout.

Тесты используют реальные Tab, MainWindowController, WindowsManager, macOS provider/native handler/delegate, navigation callbacks и системный WebKit. Monitor events вручную не вызываются. CPM Integration Tests.xcscheme не содержит pre/post actions для очистки Review или killall tests-server; тесты запускаются только при explicit opt-in и не параллельно. Собственные HTTP listener на loopback, временная распакованная extension copy, in-memory installation store, nonpersistent WebKit data store и recorder transport. Существующий dirty ZIP не менялся этой работой.

Минимальные seams: DEBUG-only AppDelegate factory, доступная лишь в .integrationTests при CPM_APP_INTEGRATION=1; необязательная controllerConfiguration у shared manager для изоляции data store (default production прежний). Контролируемые scoped faults меняют доставку настоящих autoconsent messages в extracted test copy; они не являются доказательством естественной причины production-событий.

Xcode bootstrap: исходный frozen lockfile отсутствовал на workspace-level; resolver подготовил зависимости. Попытка ad-hoc build упёрлась в provisioning app/VPN/helpers; начата unsigned build. В sandbox Xcode ошибочно объявлял существующий workspace не-workspace из-за доступа к build services, повтор выполняется с разрешённым доступом. Strict SwiftLint новых 6 Swift + 2 seams: 0 violations (--no-cache). Исполнение новых integration tests ещё не подтверждено; результаты будут дописаны после сборки.

App host bootstrap: проверенные блокеры и границы, 16 октября

Unsigned app/test bundle успешно собран (build-app-integration-2.log), но первый запуск не дошёл до XCTest: stack sample остановился в ConfigurationStore.loadData → NSFileCoordinator → open shared app-group config. Тестовый host остановлен отдельно, без killall и без очистки профиля. Добавлен отдельный directory initializer ConfigurationStore, прежний initializer сохранён; только при DEBUG + .integrationTests + CPM opt-in AppDelegate использует собственный temporary configuration cache и test defaults. Production default не меняется. Промежуточные compile/link ошибки из-за type inference/incremental symbol устранены; build-app-integration-5.log — TEST BUILD SUCCEEDED.

Повтор baseline-2.xcresult дошёл до реального XCTest и штатного cpm_background_ready_context_load (under_1s, ctx_load_to_view under_250ms). Затем создание настоящего Tab упёрлось в независимый сервис: SpecialPagesUserScript → OnboardingActionsManager → SecureVaultFactory.makeVault; main thread ждал lock, background CredentialsDatabaseCleaner ждал SecItemCopyMatching/SecurityServer. Это блокировка unsigned host при доступе к Keychain, не CPM failure и не зелёный baseline. Отдельный test process остановлен; пользовательское запущенное приложение не останавливалось.

Для suite добавлен DEBUG-only SecureVaultFactory.isDisabledForIntegrationTesting; AppMain выставляет его до создания app environment лишь для .integrationTests + CPM opt-in. Autofill/vault этим suite не тестируется и Keychain для него не требуется; monitor, Tab/navigation, native CPM и WebKit остаются production. Выполняется пересборка. Первый и второй запуск пока считаются bootstrap failures, не результатами новых CPM assertions. В коде уже 18 отдельных сценариев, включая M4 recovery и embedded-only negative control; P3 persistent cache / естественная утрата listener / iOS остаются отдельными непроверенными здесь областями.

После DEBUG-only vault isolation baseline-3.xcresult дошёл до реального Tab.willStart для HTTP fixture, где сработал существующий fatalError: обычным integration tests разрешён только WKURLSchemeHandler, настоящая HTTP-навигация запрещена. Добавлено узкое DEBUG-исключение: .integrationTests + CPM_APP_INTEGRATION=1 + host 127.0.0.1. Удалённые hosts и остальные suites сохраняют прежнюю защиту. Это требование инфраструктуры для настоящего HTTP и штатных navigation callbacks; это не CPM fix. Baseline №3 — infrastructure failure, не доказательство сбоя расширения. Пересборка и повтор продолжаются.

Baseline №4: настоящие HTTP navigation callbacks/native dashboard успешно проходят для двух реальных tabs (≈0.40/0.31 с), diagnostics: context/tab/controller/scripts/handler=true, errors=none, network_restarted=false, ready under_1s. Два assertions page receipt были красными: test storage перехватывал copyExtension, но штатная ZIP install использует extractExtension на background task, поэтому DOM bridge вообще не был внедрён. Исправлен override extraction с делегированием production extractor, затем instrumentation только временной копии. Content mark теперь оборачивает существующий runtime listener, не добавляя ещё одного listener для ответов. Этот красный результат сохранён (baseline-4.xcresult) как ошибка harness, не CPM init loss. Выполняется повтор с проверкой control bridge.