109 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 данного теста.