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

117 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.

Исправление signing-обхода — 16 октября

После вопроса пользователя проверены исходные настройки: Debug app использует Automatic signing, DEVELOPMENT_TEAM=HKE973VLUW; валидные identities/profile доступны. Unsigned был моим обходом, а не требованием среды. Первую provisioning-ошибку вызвали мои CLI overrides CODE_SIGN_STYLE=Manual, пустой DEVELOPMENT_TEAM/profile и ad-hoc identity. Повтор без этих overrides — build-signed-default.log, TEST BUILD SUCCEEDED, Apple Development: Alexey Martemyanov. Дальнейшие результаты считаются по подписанному приложению; unsigned command убрана из README. Ранние bootstrap-гипотезы о невозможности штатной подписи неверны.

Добавленные test-only изоляции cache/vault не являются CPM fixes; необходимость их сохранения пересматривается после signed проверки. Временная extraction instrumentation теперь подтверждена строкой CPM APP INSTRUMENTED; baseline с native response, но без page mark всё ещё красный, control bridge не подтверждён. Добавлена фиксация DOM marks/native methods. Прямой async импорт WebKit Any-result заменяется callback continuation с String-only границей после runtime abort «freed pointer was not the last allocation» в диагностическом прогоне. Результаты не объявляются зелёными до проверки page receipt и реальных failures/recovery.

Signed baseline и one-navigation — первые подтверждённые результаты

signed-baseline-and-navigation.xcresult / .log: 2 tests, 0 failures, baseline 1.864 с и single-navigation/recovery 5.360 с. На здоровых страницах есть реальные DOM marks initresp (≈13–17 мс) и forwarded native dashboard; при ровно одной потерянной навигации mark отсутствует, есть штатный timeout, следующая навигация отвечает и даёт recovery_document=later_navigation. Это signed DuckDuckGo.app, TeamIdentifier=HKE973VLUW, hardened runtime.

Прежний unsigned vault-обход удалён из SecureVaultFactory.swift и AppMain.swift: эти два файла снова без diff. Последующая signed build без обхода успешна (build-signed-core.log). Recorder в harness теперь использует реальный macOS runtime feature-flag adapter (видимость internal для app-hosted теста), добавлен отдельный test переключения proxy flag с проверкой здоровых навигаций и omission readiness на новом view при off. Всего 19 методов. Запущены все 10 core diagnostic scope/recovery методов. Slow wake/freeze задают busy wait до первой загрузки контекста и ждут исходную готовность, чтобы проверка следующего wake не зависела от чтения изменённого JS из уже закэшированного ресурса.

Core suite завершён: 10/10, signed, штатный Vault

signed-core.xcresult / signed-core.log: 10 tests, 0 failures, все выполнены, без skip. Последовательные методы: M4 11.571 с; dashboard transport 5.195; runtime proxy flag 1.719; healthy baseline 1.149; one navigation/recovery 5.292; all windows + new tab + recovery 23.995; one tab + healthy controls 11.127; several tabs + controls 19.648; slow HTTP 8.872; slow native settings/recovery 8.961. Десять JSON attachments экспортированы в signed-core-evidence/.

Проверенные поля: controlled loss → init_stage=none; native settings delay → received, recovery=failed_navigation, 4s_to_10s; потеря dashboard подтверждения при наличии page initResp → response_sent; scoped loss → actual init_failed/stuck и recovery=later_navigation; M4 recovery идёт after_extension_reload. При proxy off новый view не имеет readiness-параметра, CPM отвечает; при on готовность снова измеряется. Vault-обход удалён, подпись штатная.

Known-failures suite начат с включённым expensive flag. Первый background-SIGKILL автоматически восстановился в том же контексте: actual page initResp + native ответ, timeline содержит died_crash/dealloc/new view. Следующий explicit reload потерял первую навигацию (init_stage=none, ready уже under_1s) — известная startup race A, а не доказательство неустранимого worker death. Recovery-контроль уточнён: после explicit reload дождаться production loadEmbeddedExtensionBackgroundContent, как уже сделано в embedded-only negative control. Исходный красный прогон сохраняется. Embedded-only cycle, first embedded install и first main-enable уже прошли; выполняется реальный WebKit load timeout.

Known paths: P1/P2/P6 и Network подтверждены; P5 требует повторной диагностики

Первый signed-known-failures.xcresult:

  • Embedded-only negative control pass 1.243 с; первая embedded install pass 11.934 с: первая навигация потеряна, view/handler/tab/scripts корректны, readiness=pending, затем CPM работает — controlled A/P4.
  • First main-enable pass 9.130 с и main true→false→true pass 14.616 с: реальные AppDelegate callbacks оставляют nil/старый controller у существующих Tab; новые tabs здоровы — app-level P6.
  • Frozen background pass 68.319 с: настоящая idle-выгрузка, SIGSTOP нового background process, WebKit error 6; bg_load_error_after=25s_to_35s, app_state=inactive, context_errors=WKWebExtensionContextErrorDomain:6, хвост содержит unresponsive/dealloc/error, creations=2. SIGCONT отправлен в defer/teardown.
  • Slow idle wake pass 50.469 с: busy startup 8 с задан до context load; после healthy baseline/idle получены pending + timeout и поздний ответ, затем здоровая навигация.
  • Network SIGKILL дал 4/4 отказавших tabs двух окон, actual init_stage=none, observation=global failure reproduced. Первый recovery-контроль после explicit reload попал в startup race A; добавлен readiness wait, идёт повтор.
  • Background SIGKILL сам восстановился (не утверждается persistent failure); первый post-reload контроль тоже попал в A; уточнён тем же readiness wait.
  • Реальный shared-pinned-window close дошёл до WebKit Tab not found после двух здоровых idle cycles. Однако XCTest host завершился signal INT без финальных assertions/attachment; это пока не green app-level P5. Запущен отдельный повтор со stack sample, причина SIGINT ещё не установлена.

Добавлен 20-й метод P3 testLargePersistentCacheRecordsContextReloadInjectionGap: opt-in CPM_APP_INTEGRATION_CACHE=1, свежие собственные UUID website/controller stores, 20k loopback cache entries, настоящий baseURL-changing reload и проверка actual no-view failure. Сборка signed успешна, strict SwiftLint всех 11 текущих изменённых Swift — 0 violations. P3 ещё не исполнялся; normal suite остаётся nonpersistent. Persistent данные удаляются WebKit API только для UUID данного теста.

Recovery controls исправлены; источник INT в P5 установлен по коду

signed-known-retests.xcresult: background crash/reload control pass 3.328 с, Network global crash/reload control pass 21.972 с после explicit production readiness wait. Никакой browser fix для этих проверок не добавлялся. Повтор P5 опять завершился INT спустя около 4 с после window close, до timeout assertion. Отложенный stack sample не успел: к моменту съёмки test host уже завершился.

Найден штатный DEBUG caller: MainWindowController.windowWillClose вызывает self.ensureObjectDeallocated(after: 4.0, do: .interrupt); NSObjectExtension → breakByRaisingSigInt → raise(SIGINT). Тест держал let controller в async-method scope и закрытое NSWindow в harness.windows; это тестовое удержание, не CPM-сигнатура. Исправлено: pinning в ограниченном autoreleasepool, close удаляет окно из owned collection немедленно, surviving window index скорректирован. Проверка приложения не отключается. Signed build успешна, запущен повтор только P5.

Уточнение P5 после освобождения явной ссылки: первый actual failure уже был получен, но второй вызов helper снова мог выбрать закрытое окно. WindowControllersManager.windowController(for: Tab) сначала возвращает controller из tab.webView.window, даже если тот уже unregister. Поэтому automation могла повторно открыть/удержать closed window вместо пользовательского действия в surviving window и попасть в DEBUG deallocation interrupt. Исправлено только в harness: выбор из registered mainWindowControllers; shared pinned tab заранее выбирается в surviving window через настоящий TabCollectionViewModel.select. Production lifecycle/registration не подделывается, guard приложения остаётся включён. Ещё один повтор P5 необходим, прежний SIGINT не объявляется доказательством app crash/CPM failure.

P5 interrupt: LLDB дал точный объект

Первая попытка attach до idle изменила условия: idle-helper не дождался выгрузки за 45 с; этот debugger run — отдельный infrastructure failure, не результат P5. Attach перенесён только после двух idle cycles и window close.

pinned-lldb-after-idle.log: breakpoint в Common.breakByRaisingSigInt, stack NSObject.DeallocationCheckAction.interrupt → ensureObjectDeallocated, parameter description = MainWindow has not been deallocated, line NSObjectExtension.swift:80. Это уточняет прежнюю гипотезу: controller уже прошёл deinit, сработала следующая 8-секундная проверка его окна. Исправления только harness: создание NSWindow/MainWindowController целиком в отдельном autoreleasepool (как в существующих DeallocationTests), lookup/selection не держит controllers через await, closing window немедленно удаляется из owned list. Guards приложения и production window-close path не отключаются. Нужен повтор без debugger; прежние app-level P5 результаты пока red/incomplete, хотя actual first failure/tab-not-found уже получены.

16 Oct: signed cache/P5 follow-up (intermediate, not a green suite)

  • signed-pinned-autorelease.xcresult: real shared pin + two normal WebKit idle unloads + real window closure again produced runtime.sendMessage(): Tab not found, missing page initResp, production timeout init_stage=none, tab_in_context=false, matching controller and bg_view_creations=2. The host still interrupted before the second probe/attachment. Prior LLDB identifies the DEBUG deallocation guard for MainWindow, not an assertion from the CPM detector. Wrapping creation/selection in autorelease pools alone did not resolve that retention; the test is still red.
  • signed-large-cache.xcresult: fresh persistent UUID store, 20,000 cacheable loopback resources, actual disk-cache records, fill 42.705 s. Real extension reload changed baseURL; immediate next navigation succeeded in 1.058 s, page initResp present, background created once and ready under 1 s. p3NoBackgroundFailure=false: P3 not reproduced in this run. The readiness control also succeeded. The test failed only in teardown (WKWebSiteDataStore: Data store is in use (by network process)), while its own controller/store references were still retained. Cleanup is being corrected before a new run.
  • These runs use the project's normal Apple Development signing. The earlier unsigned CLI workaround was incorrect and removed; no SecureVault bypass remains. Current verified distinct passing methods remain 18/20, with the two red methods above; there is no combined all-green result.

P5: memory-reference evidence and harness correction

pinned-lldb-retainers.log again stops in breakByRaisingSigInt for MainWindow. leaks --noContent --nostacks --traceTree=<closed-window> <owned-test-host> saved pinned-window-retainers.txt: a direct chain is NSApplication._currentEvent → NSEvent._window → closed MainWindow. SDK declares NSApplication.currentEvent strong, and Apple documents it as the last dequeued event. TestRunHelper.loadedViews is weak (confirmed in source), not a strong owner. The graph also contains autorelease-pool and system/weak references; not every reported root proves retention.

The harness now posts and dequeues one neutral application-defined event after window closure, using public AppKit APIs, to advance the event queue while async XCTest is awaiting. It does not disable the DEBUG guard, detach tabs, or send manual extension lifecycle events. Verification is pending. Persistent-store teardown now clears owned website/controller data, releases controllers and the last harness store reference, then retries WebKit's asynchronous removal for at most 10 seconds. The orphan from the first failed cleanup was removed only at its verified fresh UUID 83CBE3E6-4B0E-41F0-B53F-22CD306B8DEE (creation time and extension State.plist matched that test), after the owner process exited.

Signing verification detail

The latest signed build (build-signed-event-loop.log) invokes codesign with Apple Development: Alexey Martemyanov (9SAT8X9S84), team HKE973VLUW, hardened runtime. codesign -dvv with normal keychain access confirms the Apple Development → WWDR → Apple Root CA chain. App debug dylib and Integration Tests.xctest independently pass codesign --verify --strict. codesign --verify --deep --strict on the entire Debug app fails in nested DuckDuckGo VPN.app/Contents/Frameworks/Subscription.framework: the seal lists four missing Modules/Subscription.swiftmodule artifacts (abi.json/swiftdoc/swiftmodule/Project/swiftsourceinfo). This is a separate nested Debug packaging limitation, not an unsigned main app or test bundle. No manual re-signing or production packaging changes were made for it.