117 KiB
Executable File
title, date, tags
| title | date | tags | |||
|---|---|---|---|---|---|
| CPM Web Extension — единый статус (PR, данные, гипотезы, телеметрия) | 2026-10-15 |
|
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показывает восстановление, а у таких stuckinit_stage=none. - A подтверждается, если сокращается когорта «мид-сессионная первая загрузка» (
context_load_cause). Если нет, основание слабое. - Остаток P1/P3: гипотеза «медленный старт, а не потеря» подтверждается, если
bg_ready_afterу упавших заметно хуже базлайна здоровых загрузок иrecovery_document=failed_navigation.
8. Открытые решения и следующие шаги
- Правки 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» требует ответа ревьюеру без утверждения «хуже не будет» и обязательства перемерить после релиза. - Ответ ревьюеру (публично, без цифр): JA1 (init_failed daily-only; stuck daily+count с одинаковыми параметрами; recovered несут только
recovery_*), JA2 (медленные поля повторяются, быстрые различаются; планируется сократить набор), JA3 (background view = скрытыйWKWebView, в котором WebKit запускает фоновый скрипт расширения; выгружается через около 30 с простоя и пересоздаётся при пробуждении). Обязательство: перемерить различимость строк после первого релиза. - Запушить фикс B (
f1085846e7): красный прогон подтверждён, ждёт зелёного. - Судьба фикса A: слабое основание в данных, не совпадает с названием задачи; решить, сливать ли без зелёного измерения эффекта через
first_load_wait. - P3: довести стабильный
baseURL(ограничить встроенными расширениями безweb_accessible_resources, прогнать harness, перенести на свежийmain), проверить номер сборки ОС с фиксом 318756@main. - Локальные repro для P1–P3 и новые P5/P6 находятся в
alex/cpm-web-extension-remaining-failures; отдельный PR без фиксов ещё не создавался. В этом worktree весь базовый класс gatedCPM_REPRO=1, новые 14 методов дополнительноCPM_REPRO_P5_P6=1;requireLocalRunотносится к выделенному PR-harness. Не путать opt-in и ветки при переносе. - iOS: воспроизведение error 6 на реальном устройстве. Для Mac P5/tab=false найден и проверен путь shared pinned window → didCloseWindow → внутренний didCloseTab WebKit; для P6 проверен условный путь цикла главного flag (§11). Не установлены P5/tab=true, iOS lifecycle и реальность flag-transition в P6-сессиях.
- Блокеры по свежей сверке: #7376/#7378 и JS #3727 ждут ревью; #7384/#7385 стоят на #7376 и имеют failing Danger; #7381 остаётся на красном remote head без локального fix B.
- Найденный P5 tab lifecycle: проверить исправление полного window-close контракта, а не только списка явных didCloseTab. Направления — исключить сохраняемые shared tabs из delegate закрываемого окна либо восстановить регистрацию после window callback. Это варианты для отдельной реализации и проверки, не выполненный фикс. Контроли: закрытие второго/последнего окна, merge, перемещение и новые/существующие документы.
- P5 с исправной регистрацией и P6 attribution: естественная потеря listener metadata и фактический main-flag transition остаются без доказательства. Задержка main actor, длинный документ и один пропуск listener не заменяют такое доказательство; результаты отрицательных проверок сохранены в §10–12.
9. Методика и инструменты
- Доступ:
~/Developer/grafana-data-tools(Grafana → ClickHousemetrics.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 существуют отдельно.
Проверяемые гипотезы:
- P5: потеря регистрации вкладки или runtime listener приводит к отсутствию пробуждения после штатного idle; сравнить content-script send/error, background receive, текущую view и параметры failure-пикселя.
- P5: долгий документ/пауза приложения позволяют background выгрузиться до диагностического снимка; отличить запоздалый снимок от неработающего worker.
- P6: вкладка создана без CPM controller, затем попала в openTabs при загрузке контекста.
- 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=nildealloc старой 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 закрываемого окна и productioneventsListener.didCloseWindow— без явногоdidCloseTab, изменения listeners, ручной выгрузки context или замены manager. Сразу после callbacktab_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 inWebExtensionTabCocoa.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, HEADa0717cf098cd1cbd32d5bc9d19aa7db0fab2399f; новые тесты 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, HEAD99051d5d08cd9f19ec76ce599f7539d070b1ae09. Этот 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 — bucket3_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 producedruntime.sendMessage(): Tab not found, missing page initResp, production timeoutinit_stage=none,tab_in_context=false, matching controller andbg_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.