559 lines
106 KiB
Markdown
Executable File
559 lines
106 KiB
Markdown
Executable File
---
|
||
title: 'CPM Web Extension — единый статус (PR, данные, гипотезы, телеметрия)'
|
||
date: '2026-10-15'
|
||
tags:
|
||
- work
|
||
- cpm
|
||
- status
|
||
---
|
||
|
||
# CPM Web Extension: единый статус на 15 октября 2026
|
||
|
||
Внутренний документ. **Цифры из ClickHouse, в публичные PR, комментарии и issues не переносить** (репозитории `duckduckgo/apple-browsers` и `duckduckgo/duckduckgo-privacy-extension` публичные). Заменяет заметку `cpm-telemetry-data-requirements-2026-10-15` (теперь ссылка сюда). Исторические срезы с собственными датами и знаменателями: [[cpm-web-extension-public-attribution-2026-10-05]], [[cpm-web-extension-data-analysis-summary-2026-10-08]], [[cpm-web-extension-repro-and-fix-2026-10-08]]. Актуальная карта worktree и точек входа добавлена в [[cpm-web-extension-code-guide-2026-10-09]]; её прежние разделы сохранены как карта на 9 октября.
|
||
|
||
**Актуализировано 15 октября, 17:26 UTC / 23:26 Бишкек.** Разделы 1–9 — сводный статус; §10–11 — сохранённая хронология экспериментов, где «пока», «не найдено» и красные ожидания относятся к моменту записи. Текущий итог всех 14 P5/P6-проверок, границы доказательства и команды — §12. PR/CI сверены read-only через GitHub; результаты локальных тестов сверены с существующими логами и исходниками, в этом документационном проходе тесты и ClickHouse-запросы заново не запускались.
|
||
|
||
## 1. Коротко
|
||
|
||
- Фикс **B** (падение Network-процесса) проверен локально и красным прогоном на CI. **Основание в данных есть, но неполное:** обогащение в stuck есть, а доказать, что строки это именно мёртвый worker, нельзя (не хватает `network_replaced_since_view`).
|
||
- Фикс **A** (первая загрузка посреди сессии) реален, но мелкий: около 1% stuck на iOS, на macOS почти не виден, чаще самоизлечивается.
|
||
- Три самые крупные группы сбоев фиксами A и B **не закрыты**: пробуждение после idle, error 6 (iOS), «нет background view» на запуске (iOS). Механизмы воспроизведены локально на macOS, но проверенного рычага в приложении нет.
|
||
- **P5/tab_in_context=false, macOS:** найден browser→WebKit путь — закрываемое окно по-прежнему возвращает общий pinned tab, а WebKit внутри `didCloseWindow` удаляет его регистрацию. На системном WebKit получены точная цепочка, два failure и actual stuck; прямой `didCloseTab` тест не вызывает. Это продолжает ручное наблюдение 10 сентября из [[cpm-web-extension-breakage-findings]], но теперь подтверждены текущий source contract и автоматический сценарий (§11–12).
|
||
- **P6, macOS:** подтверждён условный путь через реальный `WebExtensionFeatureFlagHandler`: true → false → true главного `webExtensions` создаёт новый manager/controller, существующий WKWebView остаётся со старым controller. Реальность такого переключения в выбранных production-сессиях не доказана; обычное обновление config или только embedded flag не равны этому условию.
|
||
- **Проверки P5/P6:** все 14 текущих методов имеют pass по последним соответствующим логам; единый финальный прогон всех 14 не выполнялся. Пакет: 478 XCTest, 28 opt-in skipped, 0 failures; strict SwiftLint восьми изменённых Swift-файлов — 0 нарушений. Зелёный characterization-тест подтверждает ожидаемый сбой/контроль, а не исправление CPM.
|
||
- **Осталось неизвестным:** естественная утрата runtime listener metadata, P5 при tab_in_context=true, реальный iOS suspend/resume и естественный P6 без доказанного main-flag transition. Полный AppKit/browser flow в новых SPM-тестах не выполнялся: app callbacks/delegates реализованы harness по исходникам; production shared code и системный WebKit выполнялись.
|
||
- Телеметрия PR0 пока в виде, вызвавшем вопросы о склейке пикселей. Решение по правкам не принято (раздел 8). #7384/#7385 имеют красный Danger, #7381 — красный Non-Sandbox; текущие heads и ограничения — раздел 2.
|
||
|
||
## 2. Цепочки PR (все в `duckduckgo/apple-browsers`, задачи Asana в телах PR; не читать)
|
||
|
||
**Телеметрия:**
|
||
|
||
Статусы ниже проверены через GitHub 15 октября в 17:26 UTC. Все семь перечисленных PR открыты; это не означает merge или shipped release. «CI зелёный» означает отсутствие failing/pending среди опубликованных checks; skipped jobs отдельно не считаются выполненными. Счётчики — записи GitHub statusCheckRollup, а не число уникальных тестов: имена checks из разных workflows могут повторяться.
|
||
|
||
| PR | Ветка | База | Что | Статус |
|
||
|---|---|---|---|---|
|
||
| [#7376 (PR0)](https://github.com/duckduckgo/apple-browsers/pull/7376) | `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)](https://github.com/duckduckgo/apple-browsers/pull/7384) | `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)](https://github.com/duckduckgo/apple-browsers/pull/7385) | `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](https://github.com/duckduckgo/duckduckgo-privacy-extension/pull/3727) | `alex/cpm-init-progress` | main | уведомления `cpmInitProgress` из встроенного расширения | OPEN, head `b00cc227112d`; 32 success, без skipped/failure/pending; REVIEW_REQUIRED |
|
||
|
||
**Harness и фиксы** (красный коммит, затем зелёный; не ребейзить и не форс-пушить, иначе пропадёт доказательство красного прогона):
|
||
|
||
| PR | Ветка | База | Что | Статус |
|
||
|---|---|---|---|---|
|
||
| [#7378](https://github.com/duckduckgo/apple-browsers/pull/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](https://github.com/duckduckgo/apple-browsers/pull/7379) | `alex/cpm-fix-init-loss` | harness | фикс A: ожидание первой загрузки background после установки/апгрейда посреди сессии | OPEN, head `7ad771e227bb`; 35 success / 20 skipped, без failure/pending; прежние красный и зелёный прогоны сохраняются |
|
||
| [#7381](https://github.com/duckduckgo/apple-browsers/pull/7381) | `alex/cpm-fix-network-crash` | фикс A | фикс B: перезапуск background при замене Network-процесса и мёртвом worker'е | OPEN, remote head `39f94e12eaa4`; 28 success / 24 skipped / 6 failure, включая **Test (Non-Sandbox)** и итоговые отчёты. Прежний красный repro `testNetworkProcessCrashKeepsEveryTabWorking` с 3 ретраями подтверждён. Исправление `f1085846e7ff` — единственный локальный commit поверх remote head; в PR его нет, зелёного CI для него нет |
|
||
| (нет PR) | `alex/cpm-fix-pixels` | фикс B + PR0 | пиксели `background_restart` и `first_load_wait` для измерения эффекта | держим до слияния PR0, A, B |
|
||
|
||
Локальные проверки фикса A: на коде без фикса оба repro-теста красные (5 из 5 итераций), с фиксом 0 из 10 потерь, 761 тест пакета зелёный. Фикс B: на `main` красные тесты падения Network-процесса и смерти worker'а, 5 из 5 итераций.
|
||
|
||
**Новые P5/P6 тесты остаются локально** в `alex/cpm-web-extension-remaining-failures`; в PR #7378/#7379/#7381 они не переносились. Весь `CPMRepro/` этого worktree untracked, поэтому одного HEAD недостаточно для восстановления тестов. Нужны сами файлы (§12 и актуальная карта кода).
|
||
|
||
Недоделано в harness и CI: CI гоняет тесты с `-test-iterations 3 -retry-tests-on-failure`, красный тест должен падать стабильно; тесты WebExtensions идут внутри схем `macOS Browser` и `iOS Browser` на `osx-tahoe-27`. Медленные эксперименты (заморозка 45 с, заполнение кэша около 10 минут, медленный старт worker'а) локальные и не для коммита.
|
||
|
||
## 3. Данные (ClickHouse, 12–14 октября, версии iOS 7.240+ и macOS 1.210+)
|
||
|
||
Единица: `daily`-пиксель = устройство-день на причину, не пользователь и не эпизод. `initialization_failed` только `.daily`; `stuck`, `recovered_*`, `reload_failed`: `daily`+`count`. Параметры снимка есть только на init_failed и stuck.
|
||
|
||
| | iOS | macOS |
|
||
|---|---|---|
|
||
| `initialization_failed.daily` | 3 273 025 | 119 950 |
|
||
| `stuck.daily` | 511 660 | 40 842 |
|
||
| init восстановились без reload | 1 135 477 (34.7%) | 30 738 (25.6%) |
|
||
| stuck → восстановились после M4 reload | 293 557 (57.4%) | 21 865 (53.5%) |
|
||
| `reload_failed.daily` | 140 921 (27.5% от stuck) | 17 796 (43.6%) |
|
||
|
||
**Когорты** (по приоритету: error 6 → нет view → вкладка не подключена → Network заменён → пробуждение (view <30 с после `dealloc`) → недавняя загрузка (`load`/`base_url_changed` ≤2 мин) → other). Доля init → stuck:
|
||
|
||
| Когорта | iOS | macOS | Смысл роста или падения |
|
||
|---|---|---|---|
|
||
| error 6 | 6.1% → 21.5% | 1.0% → 1.9% | остаётся сломанной |
|
||
| нет background view | 48.1% → 10.9% | 3.3% → 1.8% | самоизлечивается |
|
||
| вкладка не подключена | 0.03% → 0.05% | 6.2% → 8.9% | остаётся сломанной |
|
||
| Network заменён | 16.8% → 39.7% | 13.6% → 26.6% | остаётся сломанной |
|
||
| пробуждение | 7.9% → 9.5% | 32.5% → 32.2% | без изменений |
|
||
| недавняя загрузка контекста | 15.7% → 6.0% | 20.0% → 6.4% | самоизлечивается |
|
||
| other | 5.3% → 12.5% | 23.4% → 22.2% | |
|
||
|
||
Объёмы `stuck.count` в разных версиях на порядки выше `daily`: дашборды сравнивать только по версии и частоте.
|
||
|
||
## 4. Основание фиксов
|
||
|
||
- **B:** доля «Network заменён» растёт в 2–2.4× при переходе init → stuck. `reload_failed` высок: reload расширения мёртвый worker не лечит. **Оговорка:** `network_restarted` сравнивает PID с загрузкой *контекста*, а не с последним view. Если view пересоздан после смены процесса, worker свежий и механизм неприменим. По заметке 8 октября механизм объясняет в лучшем случае 20–45% «здоровых» stuck. Причинность подтвердят `network_replaced_since_view` (нет ни в одной ветке) и `init_stage=none` (#7385).
|
||
- **A:** сигнатура «загрузка контекста ≤2 мин, не при запуске, первый view»: iOS 129k init (3.9%) → 4.6k stuck (0.9%); macOS 505 → 118. Разовая потеря одной навигации (0.23× при переходе в stuck). Сигнатура неточна (захватывает M4-reload). Название связанной Asana-задачи («init loss on background unload») относится скорее к группе пробуждения, чем к фиксу A; несоответствие надо снять с автором задачи.
|
||
|
||
## 5. Картины событий (stuck.daily, 14 октября, хвост `bg_events` без возрастов) и воспроизведение
|
||
|
||
| Картина | Хвост | iOS / macOS | Воспроизведено? | Рычаг в приложении |
|
||
|---|---|---|---|---|
|
||
| **P1. Цикл пробуждения** | `view>proxy_on>dealloc>view>proxy_on` | 16.0k / 4.3k (wake) + 16.3k / 2.4k (старше 30 с) | только с искусственно занятым worker'ом (5 с: init_failed и поздний ответ; 12 с: stuck → M4); органическое пробуждение в тестах проходит. WebKit-баг 324743 (исправлен 22 сентября, нет в 26.7/27.0) | гипотезы: отсчёт 4 с от готовности background («фаза 2 детектора»), ожидание готовности при пробуждении, предпрогрев |
|
||
| **P2. «Пробуждение → error 6»**, циклы | `proxy_on>dealloc>view>proxy_on>error_E:6` (23.6k), `error>dealloc>view>proxy_on>error` (5.7k), после `died_crash` (1.5k) | iOS; macOS 118 | macOS: заморозка нового процесса на ≥30 с даёт error 6, корзина 25–35 с. iPhone: 4 сценария безуспешно. Главный iOS-хвост **без** `unresponsive` (в воспроизведении он есть) | корень в WebKit (`wakeUpBackgroundContentIfNecessary` взводит 30-секундный таймер до загрузки worker'а). Гипотеза: перезагрузка расширения при возврате на передний план |
|
||
| **P3. Контекст загружен, view нет** | `load` / `load>base_url_changed` | 17.1k + 0.9k / 162 + 81 | задержка очистки при смене `baseURL` воспроизведена на macOS при большом кэше (около 10 минут заполнения); iOS-гонка запуска (`bg_view_creations=0`, восстановление сессии, первые 30 с) не воспроизведена: холодный старт в harness'е отвечает за миллисекунды | кандидат: стабильный `baseURL` (ветка `cpm-stable-base-url`: не доведён, меняет URL всем расширениям, не прогнан через harness, старая база). Фикс WebKit 318756@main, номер сборки ОС не проверен |
|
||
| **P4. Первая загрузка** | `load>view>proxy_on` | 6.8k + 2.3k / 570 | да (фикс A) | фикс A |
|
||
| **P5. Фон выгружен в момент сбоя** | `proxy_on>dealloc>view>proxy_on>dealloc` | 1.9k / 494 | да: закрытие окна с shared pinned tab через production listener → WebKit сам удаляет сохранённый tab; точная сигнатура с tab=false (§11). Пустые listener metadata пока только test-only (§10) | найден browser→WebKit путь для Mac/tab=false; фильтрации явных didCloseTab недостаточно — didCloseWindow закрывает tabs повторно. Для tab=true и iOS естественный вход не найден |
|
||
| **P6. macOS: вкладка без расширения** | `load>view>proxy_on>dealloc` / `load>view>proxy_on`; варианты с `extension_updated` отдельно | iOS ≈0 / 627 + 235 | да: цикл главного webExtensions flag через production handler создаёт replacement manager, прежний WKWebView сохраняет старый controller и даёт точную сигнатуру (§11); nil controller до/после idle — test-only (§10) | условный production-путь найден; фактическое переключение главного flag в выборке не подтверждено. Embedded-only cycle сохраняет controller; reload контекста старую привязку не чинит |
|
||
| **Network заменён** | | см. раздел 4 | да (краш, worker мёртв при том же view) | фикс B |
|
||
| **«Здоровый» остаток, other** | | iOS 12.5%, macOS 22% | нет | нет гипотезы |
|
||
|
||
**Сверка определения P5:** строка выше сохраняет исходные 494 macOS. Повторная выборка 14 октября по хвосту P5 без error 6, network_restarted=true и scripts=false дала 1953 iOS / 499 macOS (§10). Разница 494/499 не устранена сверкой всех дополнительных фильтров: эти знаменатели не подменяют друг друга. В этом повторном Mac-срезе 68 снимков имеют tab_in_context=false, 374 — все три tab-флага true, 57 — неполную/неразрешённую диагностику вкладки. На iOS 646 снимков имеют все три tab-флага true, у 1299 вкладка не разрешилась, ещё 8 имеют неизвестный tab_in_context. Найденный shared-window механизм применим к сигнатуре Mac/tab=false, но происхождение всех 68 событий не доказано. Неразрешённая вкладка не тождественна tab_in_context=false.
|
||
|
||
**P6 не равен P4 по одному хвосту:** `load>view>proxy_on` при controller/scripts=false — P6, при корректной привязке и потерянном раннем init может быть A/P4. 235 событий P6 — хвост без dealloc, а не группа extension_updated. В 468/627 и 234/235 P6-снимках запуск моложе двух минут. Поэтому main-flag cycle проверяет достижимость, но его частоту и соответствие раннему запуску нужно подтвердить отдельно.
|
||
|
||
Размер группы не равен возможности её починить: P1 и P3 в основном опаздывающие ответы, а часть «сбоев» это срабатывание детектора («нет ответа за 4 с», опоздавший ответ тоже считается). iOS-когорта «other» на самом деле тот же рисунок P1, но со view старше 30 секунд.
|
||
|
||
## 6. Телеметрия
|
||
|
||
**Что делает PR0 (#7376):** на 8 failure-пикселей и 2 recovery-пикселя добавлены параметры `bg_ready_after`, `bg_view_age`, `bg_load_error_after`, `bg_load_error_app_state`, `cpm_app_state`, `cpm_open_tabs`, `cpm_loading_tabs` (failure) и `cpm_recovery_document`, `cpm_recovery_after` (recovery). Новых пикселей нет. Recovery-пиксели получают только свои два параметра, `extension_reload_failed` без параметров.
|
||
|
||
**Замечания ревьюера (JA1–JA3):** корреляция между пикселями из-за множества перечислений; сколько пикселей на ошибку; константность `cpm_open_tabs`/`cpm_app_state`; что такое background view.
|
||
|
||
**Что верно по пикселям (исправление моей прежней ошибки):** `initialization_failed` только daily (не больше одного на устройство в день на причину); `stuck` daily+count (daily и count одного события идут с одинаковыми параметрами); snapshot пересобирается при каждой отправке, поэтому быстро меняющиеся поля (`bg_view_age`, `bg_ready_after`) между init_failed и stuck различаются, медленные (`app_state`, `open_tabs`) повторяются.
|
||
|
||
**Уникальность строк `stuck.daily` внутри дня** (доля уникальных по всем параметрам):
|
||
|
||
| Вариант | iOS | macOS |
|
||
|---|---|---|
|
||
| сейчас, `bg_events` из 12 событий | 8.3% | 30% |
|
||
| информативные токены, последние 4 | 2.1% | 5.2% |
|
||
| скаляры вместо `bg_events` | 0.96% | 4.4% |
|
||
| без `bg_events` | 0.25% | 2.0% |
|
||
|
||
Верхняя граница прироста от новых полей (синтетическое независимое равновероятное поле): со старым `bg_events` при десяти значениях iOS 21%, macOS 65%; со скалярами 3.6% / 15.8%. **Вывод:** «хуже не будет» можно утверждать только со скалярами. Главный идентификатор уже выкачен (`bg_events`).
|
||
|
||
**`bg_events`:** единственный источник давности загрузки (`load`, `base_url_changed`) и причины смерти (`died_*`). `view`, `dealloc`, `proxy_on`, `error_*` дублируют `bg_view_creations` и `context_errors`; `hold/release/proxy_off` равны нулю. Скаляры воспроизводят когорты загрузки и пробуждения на 100% строк. Теряется: поиск неожиданных последовательностей в остатке (смягчить удалённым флагом).
|
||
|
||
**Минимум данных по гипотезам:**
|
||
|
||
| Поле / пиксель | Вопрос | Где |
|
||
|---|---|---|
|
||
| `bg_ready_after`, `bg_view_age` | идёт ли загрузка, пробуждение или давно | PR0 |
|
||
| `bg_load_error_after`, `bg_load_error_app_state` (только при error 6) | таймер 30 с, состояние приложения | PR0 |
|
||
| `recovery_document`, `recovery_after` | опоздавший ответ или потерянный | PR0 |
|
||
| `app_state`, `open_tabs` (грубо, 1 / 2–5 / >5), только на `initialization_failed` | когорта «нет view» на запуске | PR0 |
|
||
| **новое:** `context_load_cause`, `context_load_age` (вместо `load@` и `extension_updated`) | точная сигнатура A | PR0 |
|
||
| **новое:** `bg_died_reason`, `base_url_changed` | смерть процесса, очистка при смене baseURL | PR0 |
|
||
| **новое:** мелкие корзины `app_launch_age` для первых 30 с (<4 / 4–10 / 10–30) | гонка запуска | PR0 |
|
||
| **новое на stuck:** `stuck_after` | сколько от первого сбоя до stuck | PR0 |
|
||
| **новое, срочно:** `network_replaced_since_view` | доля реальных мёртвых worker'ов в когорте B (рекордер вычисляет, но только логирует; предложено 8 октября, **нигде не реализовано**) | PR0 |
|
||
| `init_stage` | дошёл ли init до background | #7385 (срочно) |
|
||
| `cpm_background_ready_{context_load,wake}` | базлайн здоровых загрузок и пробуждений | #7384 |
|
||
| `background_restart` (причина, исход; **убрать из него `bg_ready_after`**, он дублирует соседний failure-пиксель), `first_load_wait` | эффект B и A | удержанная ветка |
|
||
| убрать | `loading_tabs`; из таймлайна `view`, `dealloc`, `proxy_on`, `error_*`, `hold/release/proxy_off` | |
|
||
|
||
Связность стека: #7384 добавляет ещё 5 измерений на те же 8 failure-пикселей (`context_load_to_background_view`, `cpm_low_power_mode`, `cpm_thermal_state`, `cpm_lockdown_mode`, `cpm_resume_age`); оценку склейки надо повторять и по нему. `background_restart` срабатывает вместе с первым `initialization_failed`: общий ключ `bg_ready_after`.
|
||
|
||
## 7. Критерии успеха и опровержения
|
||
|
||
- **B** подтверждается, если после релиза падают доля «Network заменён» среди stuck и `reload_failed`, `background_restart` показывает восстановление, а у таких stuck `init_stage=none`.
|
||
- **A** подтверждается, если сокращается когорта «мид-сессионная первая загрузка» (`context_load_cause`). Если нет, основание слабое.
|
||
- Остаток P1/P3: гипотеза «медленный старт, а не потеря» подтверждается, если `bg_ready_after` у упавших заметно хуже базлайна здоровых загрузок и `recovery_document=failed_navigation`.
|
||
|
||
## 8. Открытые решения и следующие шаги
|
||
|
||
1. **Правки PR0** (ждут вашего «да»): скаляры вместо `bg_events`, `context_load_cause`, `stuck_after`, `network_replaced_since_view`, мелкие корзины `app_launch_age`, убрать `loading_tabs`, константы только на `initialization_failed`, `open_tabs` грубо, `bg_load_error_*` только при error 6; после этого ребейз #7384 и #7385. Альтернатива «оставить `bg_events`» требует ответа ревьюеру без утверждения «хуже не будет» и обязательства перемерить после релиза.
|
||
2. **Ответ ревьюеру** (публично, **без цифр**): JA1 (init_failed daily-only; stuck daily+count с одинаковыми параметрами; recovered несут только `recovery_*`), JA2 (медленные поля повторяются, быстрые различаются; планируется сократить набор), JA3 (background view = скрытый `WKWebView`, в котором WebKit запускает фоновый скрипт расширения; выгружается через около 30 с простоя и пересоздаётся при пробуждении). Обязательство: перемерить различимость строк после первого релиза.
|
||
3. **Запушить фикс B** (`f1085846e7`): красный прогон подтверждён, ждёт зелёного.
|
||
4. **Судьба фикса A:** слабое основание в данных, не совпадает с названием задачи; решить, сливать ли без зелёного измерения эффекта через `first_load_wait`.
|
||
5. **P3:** довести стабильный `baseURL` (ограничить встроенными расширениями без `web_accessible_resources`, прогнать harness, перенести на свежий `main`), проверить номер сборки ОС с фиксом 318756@main.
|
||
6. **Локальные repro для P1–P3 и новые P5/P6** находятся в `alex/cpm-web-extension-remaining-failures`; отдельный PR без фиксов ещё не создавался. В этом worktree весь базовый класс gated `CPM_REPRO=1`, новые 14 методов дополнительно `CPM_REPRO_P5_P6=1`; `requireLocalRun` относится к выделенному PR-harness. Не путать opt-in и ветки при переносе.
|
||
7. **iOS:** воспроизведение error 6 на реальном устройстве. Для Mac P5/tab=false найден и проверен путь shared pinned window → didCloseWindow → внутренний didCloseTab WebKit; для P6 проверен условный путь цикла главного flag (§11). Не установлены P5/tab=true, iOS lifecycle и реальность flag-transition в P6-сессиях.
|
||
8. Блокеры по свежей сверке: #7376/#7378 и JS #3727 ждут ревью; #7384/#7385 стоят на #7376 и имеют failing Danger; #7381 остаётся на красном remote head без локального fix B.
|
||
9. **Найденный P5 tab lifecycle:** проверить исправление полного window-close контракта, а не только списка явных didCloseTab. Направления — исключить сохраняемые shared tabs из delegate закрываемого окна либо восстановить регистрацию после window callback. Это варианты для отдельной реализации и проверки, не выполненный фикс. Контроли: закрытие второго/последнего окна, merge, перемещение и новые/существующие документы.
|
||
10. **P5 с исправной регистрацией и P6 attribution:** естественная потеря listener metadata и фактический main-flag transition остаются без доказательства. Задержка main actor, длинный документ и один пропуск listener не заменяют такое доказательство; результаты отрицательных проверок сохранены в §10–12.
|
||
|
||
## 9. Методика и инструменты
|
||
|
||
- Доступ: `~/Developer/grafana-data-tools` (Grafana → ClickHouse `metrics.pixels`, read-only; авторизация через `grafana-auth`). Ad-hoc SQL запускался отдельным раннером через `~/DuckDuckGo/grafana-dashboards/tools/grafana-query/read-dashboard.py`. Сканы по `pixel LIKE` без фильтра по дате, агенту и `pixel_id='debug'` превышают лимит строк. Временные запросы лежали в папке сессии и могут пропасть; ключевая классификация ниже.
|
||
- Версии с диагностикой: iOS 7.240+, macOS 1.210+. Окно: 12–14 октября (3 полных дня). Ограничения: сигнатуры эвристические; `network_restarted` только прокси; устройства нельзя проследить от init до stuck, сравниваются только доли; доля не равна пользовательскому эффекту.
|
||
|
||
```sql
|
||
-- классификация когорт для init_failed.daily / stuck.daily
|
||
multiIf(
|
||
errs LIKE '%ContextErrorDomain:6%', '1_error6',
|
||
creations = '0', '2_no_background_view',
|
||
scripts = 'false', '3_tab_not_connected',
|
||
net = 'true', '4_network_replaced',
|
||
has(ev,'view@30s') AND arrayExists(x -> startsWith(x,'dealloc@'), ev), '5_wake_view_lt30s',
|
||
arrayExists(x -> x IN ('load@30s','load@1m','load@2m','base_url_changed@30s','base_url_changed@1m','base_url_changed@2m'), ev), '6_context_load_recent',
|
||
'7_other')
|
||
-- параметры: network_restarted, context_errors, bg_view_creations, tab_has_ext_scripts, bg_events (decodeURLComponent)
|
||
```
|
||
|
||
## 10. Журнал воспроизведения P5/P6 — 15 октября 2026
|
||
|
||
Хронологический журнал первоначальных controlled экспериментов; дополнительные результаты с привязкой к production-путям и текущие ограничения — в §11.
|
||
|
||
Работа начата по запросу: добавить тесты новых гипотез и попытаться воспроизвести наблюдаемые цепочки. Продакшен-фиксы и публичные PR этим запросом не меняются.
|
||
|
||
**Выбор worktree:** `/Users/admin/DuckDuckGo/apple-browsers.git/alex/cpm-web-extension-remaining-failures`, ветка `alex/cpm-web-extension-remaining-failures`, HEAD `a0717cf098cd1cbd32d5bc9d19aa7db0fab2399f`. Здесь общий harness, production recorder, локальные эксперименты пробуждения, error 6 и очистки baseURL (P1–P3), без watchdog/фиксов A/B. Полного единого набора всех сценариев сейчас нет: базовые A/B находятся в `cpm-repro-harness`/фикс-ветках, init-progress E2E — в `cpm-e2e-init`. Существующие dirty-изменения сохранены; добавления этой работы ограничиваются тестовым harness.
|
||
|
||
**Среда:** macOS 26.7.1 (25G241), Xcode 27.1 (27A9275), системный WebKit. Прогон на macOS проверяет механизм; перенос на iOS требует отдельной проверки.
|
||
|
||
**Уточнение данных перед тестами:** read-only перепроверка ClickHouse за 14 октября, `stuck.daily`, iOS 7.240+/macOS 1.210+:
|
||
- P5: в выборке хвоста `proxy_on>dealloc>view>proxy_on>dealloc` без error 6/Network replacement/scripts=false — 1953 iOS и 499 macOS, `bg_view_alive=false` во всех снимках; последняя `view` везде старше 30 с. Это определение чуть шире строки P5 выше, поэтому 499 не подменяет прежние 494 без сверки дополнительных фильтров. На macOS 68 снимков с `tab_in_context=false`; на iOS у 1299 не разрешилась вкладка для табличных диагностических полей.
|
||
- P6: `load>view>proxy_on>dealloc` — 627 macOS; `load>view>proxy_on` — 235. В обеих подгруппах `context_loaded=true`, `tab_in_context=true`, `tab_controller_match=false`, `tab_has_ext_scripts=false`. В первых двух минутах запуска — 468/627 и 234/235. **235 — это хвост без dealloc, а не маркер extension_updated**; варианты с extension_updated существуют отдельно.
|
||
|
||
**Проверяемые гипотезы:**
|
||
1. P5: потеря регистрации вкладки или runtime listener приводит к отсутствию пробуждения после штатного idle; сравнить content-script send/error, background receive, текущую view и параметры failure-пикселя.
|
||
2. P5: долгий документ/пауза приложения позволяют background выгрузиться до диагностического снимка; отличить запоздалый снимок от неработающего worker.
|
||
3. P6: вкладка создана без CPM controller, затем попала в openTabs при загрузке контекста.
|
||
4. P6: существующая вкладка сохранила controller предыдущего менеджера; новый контекст видит вкладку через openTabs, но не внедряет в неё скрипты.
|
||
|
||
**Текущий статус:** читаю точки расширения harness; тесты и результаты ещё не получены. Контролируемая инъекция состояния будет явно отделена от естественного воспроизведения.
|
||
|
||
### P5/P6 — тесты добавлены, начат первый прогон
|
||
|
||
В `CPMReproLifecycleHypothesisTests.swift` добавлены 7 локальных XCTest-проверок, отдельный opt-in `CPM_REPRO_P5_P6=1` поверх `CPM_REPRO=1`:
|
||
- P6: отсутствующий controller до idle; тот же случай после штатной выгрузки и после reload контекста; перенос существующего WKWebView в replacement manager с сохранением прежнего controller.
|
||
- P5: забытая в WebKit, но оставшаяся открытой вкладка после двух idle-циклов; отсутствие runtime listener после следующего пробуждения; два здоровых документа с реальной задержкой image-subresource на 35 с; те же документы с искусственно потерянными dashboard-уведомлениями.
|
||
|
||
Harness позволяет отдельно выбирать controller вкладки, переносить существующую view и подавлять конкретный native method только в тестах. `CPMReproDelayedResource.swift` удерживает реальный subresource через WKURLSchemeHandler; навигационные finish-события не подделываются. Проверки требуют actual `stuck` от production monitor, полный набор диагностических признаков, content-script `initResp` для здоровых контролей и восстановление после снятия инъекции.
|
||
|
||
Первым запущен `testTabWithoutControllerMatchesP6BeforeIdleUnload`; пока идёт сборка, результата нет. Логи сессии: `/private/tmp/cpm-p5-p6-20261015/`. Команда сборки: `CPM_REPRO=1 CPM_REPRO_P5_P6=1 swift test --disable-automatic-resolution --filter CPMReproLifecycleHypothesisTests/testTabWithoutControllerMatchesP6BeforeIdleUnload`.
|
||
|
||
### Первый результат P6 до idle
|
||
|
||
Сборка прошла. Первый тест получил реальный `initialization_failed` и затем `stuck` с `load>view>proxy_on`, `context_loaded=true`, `tab_in_context=true`, `tab_controller_match=false`, `tab_has_ext_scripts=false`, `bg_view_alive=true`, `bg_view_creations=1`. Content marks пустые: CPM не внедрён. Новая вкладка с current controller получила `initResp` и вызвала recovery.
|
||
|
||
**Прогон пока красный (1 assertion):** дополнительное ожидание `network_restarted=false` не выполнено — поле true. Падения/замены Network-процесса тест не инъецирует. Проверяю холодный старт Network до контекста: recorder сравнивает PID с context-load, что может смешивать первичный запуск и замену. Результат нельзя объявлять полным совпадением всех признаков до этой проверки. Лог: `build-and-p6-before-idle.log`. SwiftLint пяти изменённых файлов прошёл с `--no-cache`, 0 нарушений; обычный режим упёрся в недоступный для записи кэш.
|
||
|
||
### Уточнение harness и первый зелёный P6
|
||
|
||
Проверенный прогрев default WKWebsiteDataStore перед context-load даёт ненулевой Network PID; в последующем failure `network_restarted=false`. Это сняло лишнее отличие первого холодного прогона. **`testTabWithoutControllerMatchesP6BeforeIdleUnload` теперь зелёный:** совпадают хвост и все проверяемые признаки, новая правильно подключённая вкладка работает. Механизм получен контролируемым отсутствием controller; естественный путь создания такой вкладки в приложении ещё не найден.
|
||
|
||
Проверка replacement manager сначала не дошла до failure-пикселя: teardown первого harness снимает navigationDelegate, и при переносе view его надо восстановить. Исправлен только тестовый `adoptTab`; это ошибка постановки эксперимента, не результат о CPM. Начат полный прогон семи тестов с исправлением: `p5-p6-full.log`.
|
||
|
||
### Дополнительный контроль P6
|
||
|
||
Добавлен восьмой тест `testCurrentControllerBeforeContextLoadRemainsHealthy`: создать вкладку до завершения context-load, но сразу с правильным controller, и потребовать `initResp`, скрипты и отсутствие initialization_failed. Это отделяет обычный ранний startup от отсутствующей привязки controller. Он будет выполнен после текущего запущенного бинарника; результат пока не получен.
|
||
|
||
### Подтверждены два контролируемых механизма
|
||
|
||
**P6, replacement manager — зелёный, 9.186 с.** Сначала существующая вкладка получила initResp со старым менеджером. После teardown старого менеджера её тот же WKWebView и прежний controller зарегистрированы в replacement manager. Получены два failure и actual stuck с `load>view>proxy_on`, context/tab=true, controller/scripts=false, view=true, creations=1, errors=none, network_restarted=false. Новая вкладка replacement manager получила initResp. Это воспроизводит сохранённую привязку; естественный триггер замены менеджера не доказан.
|
||
|
||
**P5, забытая живая вкладка — зелёный, 72.744 с.** Два нормальных initResp и две реальные idle-выгрузки, затем test-only `didCloseTab` без удаления WKWebView. Получен actual stuck с `load>view>proxy_on>dealloc>view>proxy_on>dealloc`, creations=2, view=false, context=true, tab=false, controller/scripts=true, errors=none, network_restarted=false. Content script отправляет init и получает init-error. После `didOpenTab` эта же вкладка снова получила initResp. Механизм подходит под P5-подгруппу с tab_in_context=false; его доля в проде и естественное действие приложения, создающее нарушение lifecycle, этим тестом не установлены.
|
||
|
||
Оба результата находятся в `p5-p6-full.log`; другие сценарии продолжают выполняться.
|
||
|
||
### Полный первый прогон: положительные и отрицательные результаты
|
||
|
||
Прогон `p5-p6-full.log` завершён: 7 тестов, 4 зелёных и 3 красных (11 assertions), 273.8 с. Не все красные результаты означают дефект harness:
|
||
|
||
- **P6 после idle — зелёный, 38.658 с.** Получена цепочка `load>view>proxy_on>dealloc` с исходной сигнатурой controller/scripts=false, tab/context=true. Reload контекста не исправил существующую вкладку; новая правильно подключённая вкладка получила initResp.
|
||
- **Потеря runtime listener только на одном wake не воспроизвела P5.** Инъекция действительно потеряла первый init (native dashboard не пришёл), но после восстановления файла и следующего idle новый worker принял следующий init и прислал initResp, stuck=0. Ожидание устойчивого P5 в тесте было неверным; превращаю его в отрицательную characterization-проверку этого результата.
|
||
- **Долгий документ:** первый image-subresource действительно удерживался 35 с. Background выгрузился примерно на 32 с, но на finish навигации в 35 с WebKit снова создал background; на failure в 39 с view уже жива. При искусственной потере dashboard получен stuck с живой view, а не P5. Здоровый документ получил initResp и не вызвал failure.
|
||
- **Ошибка постановки второго долгого документа:** одинаковый URL image взялся из кэша, поэтому второй load не был долгим. Исправляю уникальный URL на каждую навигацию и повторю оба контроля.
|
||
- **Наблюдение о trace:** watcher держал strong reference на view через 50-мс await. При переходе сразу после `context._backgroundWebView=nil` dealloc старой view иногда попадал после создания новой. Убираю удержание через await и буду ждать одновременно отсутствия WebKit background и dealloc в production recorder, чтобы не менять исследуемую последовательность самим наблюдением.
|
||
|
||
Все неподтверждённые результаты выше остаются отрицательными/ограниченными; совпадение P5 пока подтверждено только для test-only потери регистрации живой вкладки.
|
||
|
||
### Уточнённые проверки P5 — начат повтор
|
||
|
||
Теперь 9 тестов. В новом `testNoRuntimeListenerMetadataFromFirstStartupMatchesP5` runtime listener не регистрируется с самого первого старта; затем background явно загружается второй раз и оба раза штатно выгружается. Это отдельно проверяет пустые listener metadata, которые нельзя было получить простым пропуском регистрации на одном wake после здорового старта. Условия искусственные; естественная утрата metadata не доказана. После проверки инъекция снимается, выполняются explicit background load и проверка initResp.
|
||
|
||
Первоначальный listener-тест переименован в `testMissingRuntimeListenerOnOneWakeRecoversAfterNextIdle` и проверяет реально наблюдённое восстановление, отсутствие stuck и третью view.
|
||
|
||
Оба 35-секундных документа теперь используют разные URL image. Ожидания изменены по наблюдению первого прогона: здоровый длинный документ — no failure; потеря dashboard — stuck с живой view, не P5. Расширен test-only background trace на все типы autoconsent-сообщений, чтобы установить, какое сообщение повторно будит worker на finish.
|
||
|
||
Watcher больше не удерживает WKWebView через await, а idle-helper требует и освобождения background в WebKit, и dealloc по recorder. Повтор пяти новых/уточнённых проверок: `p5-refined-and-controls.log`.
|
||
|
||
### Здоровый startup-контроль подтверждён
|
||
|
||
`testCurrentControllerBeforeContextLoadRemainsHealthy` — зелёный, 1.190 с: вкладка с правильным controller проходит стартовый gate, получает initResp и скрипты, initialization_failed=0. Одной ранней загрузки контекста для P6 в этом сценарии недостаточно.
|
||
|
||
Повтор one-wake listener подтвердил восстановление и отсутствие stuck. Остался только ошибочный assertion формата счётчика: pixel `bg_view_creations` для трёх view равен `3_to_5`, а не строке `3`. Проверка заменена на raw `backgroundWebViewCreateCount == 3`; будет повторена. Расширенный trace действительно показывает дополнительные `report`/`eval` после init — проверяю их связь с повторным пробуждением длинного документа.
|
||
|
||
### P5 с исправными tab/controller/scripts — воспроизведён
|
||
|
||
**`testNoRuntimeListenerMetadataFromFirstStartupMatchesP5` зелёный, 70.149 с.** Пропуск runtime listener с первого старта, две реальные idle-выгрузки (между ними explicit background load), затем две навигации дали actual stuck:
|
||
`load>view>proxy_on>dealloc>view>proxy_on>dealloc`, view=false, creations=2, context/tab/controller/scripts=true, errors=none, network_restarted=false. Content script отправляет init; новые view не создаются. После разрешения регистрации listener в установленной тестовой копии скрипта, explicit background load и навигации получен initResp.
|
||
|
||
Это соответствует P5 с внешне исправной вкладкой. **Доказан механизм пустых listener metadata при test-only инъекции; естественная потеря metadata в приложении/WebKit не найдена.** В отличие от него, одноразовый пропуск listener после здорового старта восстанавливается на следующем wake.
|
||
|
||
**Два долгих документа с потерянными dashboard** после устранения image-cache — зелёная отрицательная проверка, 80.181 с: оба load действительно по 35 с, в обоих документах есть initResp, production monitor получает stuck, но в его snapshot view=true и цепочка заканчивается `view>proxy_on`. Это воспроизводит сбой диагностического канала, но **не P5**. Здоровый контроль ещё выполняется. Лог: `p5-refined-and-controls.log`.
|
||
|
||
### Долгие документы: повтор завершён
|
||
|
||
Здоровый контроль с двумя разными 35-секундными image — зелёный, 70.683 с: оба документа получили initResp, initialization_failed=0 и stuck=0. На finish background снова просыпается: trace показывает `recv report` и `recv eval` примерно на 35.39 с после `background none` на 32.35 с. Таким образом, в проверенном сценарии длинная загрузка сама по себе не даёт P5.
|
||
|
||
Повтор пяти тестов завершён: четыре зелёных, один красный исключительно из-за ранее описанного сравнения bucket со строкой «3» (исправлено). Начат финальный прогон оставшихся сценариев и исправленного assertion: `p5-p6-final-regressions-and-stall.log`.
|
||
|
||
Добавлен десятый эксперимент `testMainActorStallDelaysActualFailureSnapshots`: после реального initResp искусственно теряется dashboard, затем test host main actor дважды блокируется на 35 с при ожидающем production timeout. Тест требует actual failures/stuck и восстановления; совпадение P5 отдельно записывается как `matchesP5`, а не предполагается. Это исследование задержанного выполнения host callbacks, **не реальная iOS suspend/resume**.
|
||
|
||
### Задержка host main actor: P5 не получен
|
||
|
||
`testMainActorStallDelaysActualFailureSnapshots` — зелёный, 71.027 с; результат классификации `matchesP5=false`. При двух блокировках main actor на 35 с реальные failure-пиксели пришли через 35.42 и 70.68 с, второй дал stuck. В обоих снимках view=true, creations=1, хвост остаётся `load>view>proxy_on`, context/tab/controller/scripts=true, errors=none, network_restarted=false. InitResp пришёл в документы до пауз; dashboard терялся намеренно. После снятия инъекции новая вкладка работает.
|
||
|
||
Это подтверждает задержку deadline, но в проверенном порядке callbacks **не воспроизводит P5**. Настоящие iOS suspend/resume и остановка всего процесса не проверялись; блокировка main actor не подменяет их.
|
||
|
||
Финальный прогон также повторно подтвердил replacement-manager P6 (9.167 с) и P5 с забытым live tab (72.737 с) после устранения удержания view watcher'ом.
|
||
|
||
### Итог локальных repro-проверок
|
||
|
||
Финальный прогон шести сценариев — **зелёный: 6 тестов, 0 failures, 262.676 с**. Исправленный one-wake listener-тест зелёный (62.633 с). P6 до/после idle повторно зелёные (8.569 / 38.543 с).
|
||
|
||
С учётом четырёх зелёных независимых проверок из предыдущего прогона **все 10 актуальных методов добавленного класса имеют зелёный результат**. Это проверено несколькими запусками; единого прогона всех десяти после последних добавлений не было. Сводка по каждому методу и исходному логу: `/private/tmp/cpm-p5-p6-20261015/results.json`, генератор `summarize.py`.
|
||
|
||
**Что воспроизведено:**
|
||
- P6: отсутствующий controller до и после idle; сохранённый controller существующего WKWebView после replacement manager. Reload контекста не исправляет такую вкладку; новая вкладка работает.
|
||
- P5: потеря WebKit-регистрации сохранившейся живой вкладки; отсутствие runtime listener metadata с первого старта. Совпадают хвост, actual stuck и диагностические признаки; после адресного снятия инъекции работа восстанавливается.
|
||
|
||
**Что P5 не дало на macOS 26.7.1:** однократная потеря listener на wake после здорового старта, два действительно долгих документа, две задержки main actor на 35 с. Потеря dashboard при дошедшем initResp даёт диагностический stuck с живой view.
|
||
|
||
**Ограничения:** все положительные воспроизведения требуют test-only условия; естественные production-триггеры отсутствующего controller/утраты listener metadata не найдены. Настоящая iOS suspension не проверена. Фиксы поведения приложения этим исследованием не добавлялись.
|
||
|
||
Запущен обычный пакетный прогон `swift test --disable-automatic-resolution --skip-build` без repro-флагов; результат будет записан ниже. Финальный strict SwiftLint семи изменённых файлов: 0 нарушений (`--no-cache`). SHA-1 существующего dirty Package.resolved не изменился: `7ccce20acd452656c9bf2d2fa186d044e4a8185f`.
|
||
|
||
### Финальная проверка пакета и файлы
|
||
|
||
**Обычный пакетный прогон завершён успешно:** `swift test --disable-automatic-resolution --skip-build`, 474 XCTest, из них 24 opt-in repro пропущены, **0 failures**, 11.344 с. Лог: `/private/tmp/cpm-p5-p6-20261015/package-tests.log`. Отдельный Swift Testing runner: 0 тестов. Длительные эксперименты запускались отдельно с обоими repro-флагами; все 10 текущих методов имеют зелёный результат в сводке.
|
||
|
||
**Изменены только тестовые файлы этой работы** внутри выбранного worktree:
|
||
- добавлены `CPMRepro/CPMReproLifecycleHypothesisTests.swift` (10 тестов) и `CPMRepro/CPMReproDelayedResource.swift`;
|
||
- расширены `CPMReproHarness.swift`, `CPMReproTabs.swift`, `CPMReproSupport.swift`, `CPMReproPrelude.swift`, `CPMReproTestCase.swift`;
|
||
- existing dirty production/telemetry changes не включались в эту работу, Package.resolved сохранил первоначальную контрольную сумму. Commit/PR/push не делались.
|
||
|
||
Команда полного повторения новых экспериментов из `SharedPackages/WebExtensions`:
|
||
```sh
|
||
CPM_REPRO=1 CPM_REPRO_P5_P6=1 swift test --disable-automatic-resolution --filter CPMReproLifecycleHypothesisTests
|
||
```
|
||
Эта полная команда приведена для повторения: в текущей сессии десять методов проверены раздельными группами, а не одним финальным запуском всех десяти.
|
||
|
||
| Текущий тест | Последний результат, с | Лог |
|
||
|---|---:|---|
|
||
| CurrentControllerBeforeContextLoadRemainsHealthy | pass, 1.190 | p5-refined-and-controls.log |
|
||
| TabWithoutControllerMatchesP6BeforeIdleUnload | pass, 8.569 | p5-p6-final-regressions-and-stall.log |
|
||
| TabWithoutControllerMatchesP6AfterIdleUnload | pass, 38.543 | p5-p6-final-regressions-and-stall.log |
|
||
| ExistingTabKeepsPreviousManagerController | pass, 9.167 | p5-p6-final-regressions-and-stall.log |
|
||
| ForgottenLiveTabDoesNotWakeBackgroundAndMatchesP5 | pass, 72.737 | p5-p6-final-regressions-and-stall.log |
|
||
| MissingRuntimeListenerOnOneWakeRecoversAfterNextIdle | pass, 62.633 | p5-p6-final-regressions-and-stall.log |
|
||
| NoRuntimeListenerMetadataFromFirstStartupMatchesP5 | pass, 70.149 | p5-refined-and-controls.log |
|
||
| SlowHealthyDocumentsWakeBackgroundAgainAtFinishWithoutFailure | pass, 70.683 | p5-refined-and-controls.log |
|
||
| SlowDocumentsWithLostDashboardDoNotMatchP5 | pass, 80.181 | p5-refined-and-controls.log |
|
||
| MainActorStallDelaysActualFailureSnapshots | pass, 71.027; matchesP5=false | p5-p6-final-regressions-and-stall.log |
|
||
|
||
Зелёный результат characterization-теста означает, что проверенное условие и ожидаемый исход подтверждены. Он не означает исправление CPM или доказательство естественного production-триггера.
|
||
|
||
## 11. Привязка P5/P6 к достижимым production-путям
|
||
|
||
Продолжение после замечания: положительный test-only fault доказывает механизм, но не объясняет его возникновение у пользователя. Дальше нужны конкретные caller → browser state → WebKit side effect.
|
||
|
||
### Найден кандидат P5: закрытие окна с shared pinned tab
|
||
|
||
Проверено в исходниках текущего repro-worktree и macOS release tag `1.210.0+macos`:
|
||
- `MainWindowController.windowWillClose` отфильтровывает сохранённые общие pinned tabs перед явными `didCloseTab`, но после `windowControllersManager.unregister(self)` всё равно вызывает `eventsListener.didCloseWindow(self)`.
|
||
- `MainWindowController+WKWebExtensionWindow.tabs(for:)` возвращает `loadedPinnedTabs + loadedTabs`; `MainViewController.windowWillClose` и `WindowControllersManager.unregister` эти коллекции не очищают.
|
||
- В локальном WebKit checkout `99051d5d08cd9f19ec76ce599f7539d070b1ae09`, `WebExtensionContextCocoa.mm:1351–1367`, `didCloseWindow` вызывает `didCloseTab` для **каждого** элемента `window.tabs()`. Фильтрация явных callbacks браузером не предотвращает этот внутренний вызов.
|
||
- `didCloseTab` удаляет tab из `m_tabMap`; `runtimeSendMessage` проверяет tab до пробуждения background и при отсутствии возвращает `tab not found`. Это возможная связь с P5/tab_in_context=false.
|
||
|
||
Начат эксперимент на системном WebKit: один и тот же живой tab в двух windows; закрыть одну через production `WebExtensionEventsListener.didCloseWindow`, сохранить tab в другой, без прямого `didCloseTab`, изменения listener или выгрузки context. До выполнения это **кандидат**, не подтверждённое воспроизведение.
|
||
|
||
Проверка истории также отсекла старые объяснения: исправление `Close Other Tabs` и фильтр shared pinned tabs уже находятся в `1.210.0+macos`; синхронное создание manager до state restoration существует с февраля 2026. Их прежние версии нельзя выдавать за текущий production-триггер.
|
||
|
||
### P5: production callback → WebKit side effect подтверждены
|
||
|
||
Два новых теста зелёные, 0 failures:
|
||
- `testClosingWindowForgetsSharedPinnedTabThroughWebKitAndMatchesP5` — **73.033 с**. Один живой tab предоставлен двумя window delegates, как shared pinned tab в macOS. Две успешные инициализации и две штатные idle-выгрузки. Затем только unregister закрываемого окна и production `eventsListener.didCloseWindow` — без явного `didCloseTab`, изменения listeners, ручной выгрузки context или замены manager. Сразу после callback `tab_in_context` меняется true → false, WKWebView остаётся тем же и tab сохраняется в другом окне.
|
||
- Две последующие навигации дали actual initialization failures и stuck: `load>view>proxy_on>dealloc>view>proxy_on>dealloc`, creations=2, view=false, context=true, tab=false, controller/scripts=true, errors=none, network_restarted=false. Content script получает init-error. Новая вкладка работает; `didOpenTab` восстанавливает исходную.
|
||
- `testClosingUnrelatedWindowPreservesRegisteredTab` — **1.205 с**: закрытие окна, не возвращающего эту вкладку, сохраняет tab_in_context=true, initResp и 0 failures.
|
||
|
||
Лог: `/private/tmp/cpm-p5-p6-20261015/production-window-close.log`. Это **source-linked воспроизведение на системном WebKit под macOS 26.7.1** через production listener и неизменённую логику CPM, а не произвольное закрытие живого tab. Harness использует window/tab delegates, повторяющие контракт macOS; сам MainWindowController/AppKit целиком в SPM-тест не загружен. Следовательно, browser call chain подтверждён чтением исходников, внутренний эффект WebKit и P5 — исполнением теста.
|
||
|
||
Область объяснения: **P5/tab_in_context=false** (в выбранном Mac-срезе 68 событий). Это не доказательство, что все эти события были caused by shared pinned windows, и не объяснение P5/tab_in_context=true либо iOS.
|
||
|
||
Тесты WindowsManagerPopupTests в macOS проверяют только список явных didCloseTab и поэтому не проверяют повторное закрытие tabs внутри системного didCloseWindow.
|
||
|
||
### P6: найден условный production-путь, начата проверка
|
||
|
||
В `AppDelegate.setupWebExtensions` главная `webExtensions` flag подключена к production `WebExtensionFeatureFlagHandler`. Disable вызывает `uninstallAllExtensions`, затем callback очищает coordinator/manager. Enable вызывает `initializeWebExtensions`: если manager отсутствует, создаётся новый controller, а существующие WKWebView не пересоздаются. WebKit `WebExtensionTab::webView` отвергает view, чей controller не равен controller контекста.
|
||
|
||
Добавлены отдельные source-linked тесты `CPMReproProductionFlagTests`:
|
||
- явное true → false → true главного flag через **реальный handler**, с host callbacks по AppDelegate и существующей вкладкой, предоставленной WebKit при загрузке replacement context;
|
||
- negative control: цикл только embedded flag, при неизменном главном, должен сохранить controller и initResp;
|
||
- внутри первого теста есть контроль true → true: повторный одинаковый флаг не должен заменять manager.
|
||
|
||
Это проверка достижимости механизма через production flag handler, **не утверждение, что flag переключался у пользователей**. И macOS, и iOS имеют defaultValue enabled для главного flag; подтверждения реального false → true в рассматриваемой выборке нет. Обычная задержка скачивания config сама по себе не доказывает этот путь.
|
||
|
||
### Первый прогон flag-путей: P6 получен, контроль обнаружил известную startup race
|
||
|
||
`production-flag-cycle.log`: главный flag-тест **зелёный, 9.244 с**. Реальный handler по false удалил contexts и host references; по true создан новый manager при сохранённом старом WKWebView/controller. Получены два actual failures и stuck с точным `load>view>proxy_on`, context/tab/view=true, controller/scripts=false, creations=1, errors=none, network_restarted=false. true → true manager не заменил; новая вкладка replacement manager работает. Это подтверждает **условный production-путь P6 при реальном переключении главного flag**.
|
||
|
||
Embedded-контроль **красный, 6.119 с, 3 assertions**: controller/scripts/tab=true, но первая навигация в 0.704 с ушла до sw activate в 0.747 с и потеряла init. Таким образом, «embedded cycle всегда здоровый сразу после sync» — неверное ожидание: здесь воспроизводится уже известный механизм **A / P4**, а не P6. Не считаю красный результат ошибкой постановки.
|
||
|
||
Для изоляции именно controller-binding уточняю контроль: после sync явно дождаться `loadEmbeddedExtensionBackgroundContent`, затем требовать initResp без failures при прежнем controller. Этот readiness wait — контрольное условие теста; production fix A данной работой не добавляется.
|
||
|
||
### P6: уточнённый прогон завершён
|
||
|
||
`production-flag-cycle-final.log`: **2 tests, 0 failures, 10.237 с**:
|
||
- `testMainFeatureFlagCycleRecreatesManagerAndMatchesP6` — **9.252 с**; повторное подтверждение P6 через true → false → true главного flag.
|
||
- `testEmbeddedFeatureFlagCycleKeepsControllerAfterBackgroundBecomesReady` — **0.985 с**; прежний controller и scripts=true, после explicit readiness wait initResp и 0 failures. Первоначальный красный контроль с потерей первой навигации сохранён в предыдущем логе и описан выше как известный A.
|
||
|
||
Таким образом, добавлены **четыре новых source-linked проверки**: две window-close и две flag-cycle; все четыре текущих метода зелёные в двух запусках. Они выполняют production listener/flag handler/manager/CPM и системный WebKit; app-side host callbacks и window delegates реализованы harness по исходникам macOS, приложение целиком не запущено.
|
||
|
||
Оставшиеся неизвестные: происхождение P5 с tab_in_context=true; реальные iOS lifecycle-условия; было ли переключение главного flag в P6-сессиях. Главный flag default enabled, поэтому config fetch с неизвестной задержкой не считается доказанным триггером.
|
||
|
||
### Проверка после source-linked дополнений
|
||
|
||
- Обычный пакет: `swift test --disable-automatic-resolution --skip-build` — **478 XCTest, 28 opt-in skipped, 0 failures, 10.469 с**. Лог `package-tests-production-paths.log`.
|
||
- Strict SwiftLint всех восьми файлов этой работы — **0 violations** (`--no-cache`).
|
||
- `summarize.py` и `results.json` обновлены: 14 текущих методов (10 первоначальных + 4 source-linked) имеют pass по последним соответствующим логам. Это несколько групп запусков, не единый прогон всех 14.
|
||
- Изменения этой работы остались в `SharedPackages/WebExtensions/Tests/WebExtensionsTests/CPMRepro/`: дополнен harness windows/initial-tabs, два P5 window tests в `CPMReproLifecycleHypothesisTests.swift`, новый `CPMReproProductionFlagTests.swift`. Новых production-правок нет; первоначальный tracked dirty diff — те же 10 файлов / 164 additions / 8 deletions. SHA-1 Package.resolved прежний `7ccce20acd452656c9bf2d2fa186d044e4a8185f`.
|
||
|
||
Точки для проверки call chain:
|
||
- browser: `macOS/DuckDuckGo/MainWindow/MainWindowController.swift:611–640`, `macOS/DuckDuckGo/WebExtensions/MainWindowController+WKWebExtensionWindow.swift:31`, `macOS/DuckDuckGo/Windows/View/WindowControllersManager.swift:178`;
|
||
- WebKit: `Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionContextCocoa.mm:1351–1367` (window closes tabs), `:1419` (forgetTab), `Cocoa/API/WebExtensionContextAPIRuntimeCocoa.mm:129–141` (tab check before wake);
|
||
- P6 browser: `macOS/DuckDuckGo/Application/AppDelegate.swift:2153–2166,2209–2221`; shared production handler: `Sources/WebExtensions/FeatureFlag/WebExtensionFeatureFlagHandler.swift`; WebKit validates controller in `WebExtensionTabCocoa.mm:339–360`.
|
||
|
||
Повторить именно четыре новых source-linked теста из `alex/cpm-web-extension-remaining-failures/SharedPackages/WebExtensions`:
|
||
```sh
|
||
CPM_REPRO=1 CPM_REPRO_P5_P6=1 swift test --disable-automatic-resolution --filter 'CPMReproProductionFlagTests|CPMReproLifecycleHypothesisTests/testClosing'
|
||
```
|
||
|
||
## 12. Актуальный итог P5/P6: доказательства, полный реестр и повторение
|
||
|
||
### Границы доказательства и сигнатуры
|
||
|
||
| Механизм | Что проверено | Чего это не доказывает |
|
||
|---|---|---|
|
||
| P5: закрытие окна с общим pinned tab | Browser call chain прочитан в текущем worktree и tag `1.210.0+macos`; production listener и системный WebKit исполнены. После window callback tab сохраняется в другом window delegate, но tab_in_context становится false; два failure → stuck с нужным хвостом | Полное приложение/AppKit в SPM не запускались; неизвестно, какая доля production P5 связана с этим действием. Не объясняет P5/tab=true или iOS |
|
||
| P5: нет runtime listener metadata | При подавлении регистрации с первого старта, двух настоящих idle-выгрузках и explicit load между ними получен P5 с исправными tab/controller/scripts | Это test-only исходное условие. Естественное исчезновение metadata и подходящий production caller не найдены |
|
||
| P6: главный flag меняет manager | Реальные handler/uninstall/load/context, существующая view при загрузке replacement context; прежний controller сохраняется и воспроизводит P6 | Смена состояния flag задаётся тестом; события false→true в production-выборке не установлены. Default главного flag enabled на обеих платформах |
|
||
| P6: nil controller | До и после idle контекст знает tab, но scripts отсутствуют; context reload не исправляет конфигурацию созданного WKWebView | Это controlled construction. Синхронный manager до state restoration уже есть с февраля 2026; обычный ранний startup сам по себе причиной не оказался |
|
||
| Длинные документы / потерянный dashboard / задержка main actor | Требуется отдельно смотреть page initResp, native arrival и actual failure snapshot. Длинные здоровые документы работают; lost dashboard/stall дают stuck при живой view | Ни один из этих сценариев не воспроизвёл P5 на проверенной macOS. Host main-actor stall не равен настоящему iOS suspend/resume |
|
||
|
||
Две положительные сигнатуры:
|
||
- P5: `load>view>proxy_on>dealloc>view>proxy_on>dealloc`; creations=2, view=false, context=true, errors=none, network_restarted=false. Shared-window/прямой didCloseTab: tab=false, controller/scripts=true. Отсутствие listener с первого старта: tab/controller/scripts=true.
|
||
- P6: `load>view>proxy_on` (view=true) либо `load>view>proxy_on>dealloc` (view=false); creations=1, context/tab=true, controller/scripts=false, errors=none, network_restarted=false. Main-flag source-linked тест проверяет вариант с живой view; вариант после idle проверен с nil controller.
|
||
|
||
`tab_controller_match=false` означает nil **или** другой controller: recorder требует ненулевую identity-совместимость. `tab_in_context` проверяет identity tab среди `context.openTabs`; `tab_has_ext_scripts` ищет user scripts в content world текущего UUID контекста. Эти поля не являются взаимозаменяемыми и не доказывают page receipt.
|
||
|
||
### Все 14 текущих P5/P6-проверок
|
||
|
||
Все результаты ниже извлечены из локальных логов; название без суффикса файла — метод XCTest. Первые 12 находятся в `CPMReproLifecycleHypothesisTests.swift`, последние 2 — в `CPMReproProductionFlagTests.swift`.
|
||
|
||
| Метод | Последний результат, с | Наблюдённый смысл pass | Исходный лог |
|
||
|---|---:|---|---|
|
||
| `testCurrentControllerBeforeContextLoadRemainsHealthy` | pass, 1.190 | Контроль P6: correct controller до context-load → initResp, 0 failures | `p5-refined-and-controls.log` |
|
||
| `testTabWithoutControllerMatchesP6BeforeIdleUnload` | pass, 8.569 | Инъекция nil controller → P6, view=true; новая вкладка работает | `p5-p6-final-regressions-and-stall.log` |
|
||
| `testTabWithoutControllerMatchesP6AfterIdleUnload` | pass, 38.543 | nil controller → P6 после idle; context reload не чинит эту view | `p5-p6-final-regressions-and-stall.log` |
|
||
| `testExistingTabKeepsPreviousManagerController` | pass, 9.167 | Прямой controlled replacement → P6; прежние view/controller сохранены | `p5-p6-final-regressions-and-stall.log` |
|
||
| `testClosingWindowForgetsSharedPinnedTabThroughWebKitAndMatchesP5` | pass, 73.033 | Browser callback → WebKit: shared-window close → P5/tab=false; восстановление и healthy новая вкладка | `production-window-close.log` |
|
||
| `testClosingUnrelatedWindowPreservesRegisteredTab` | pass, 1.205 | Контроль window close: без общей вкладки initResp и 0 failures | `production-window-close.log` |
|
||
| `testMainActorStallDelaysActualFailureSnapshots` | pass, 71.027 | Две паузы host main actor по 35 с → actual stuck, но matchesP5=false; view жива | `p5-p6-final-regressions-and-stall.log` |
|
||
| `testForgottenLiveTabDoesNotWakeBackgroundAndMatchesP5` | pass, 72.737 | Прямой test-only didCloseTab → P5/tab=false; didOpenTab восстанавливает | `p5-p6-final-regressions-and-stall.log` |
|
||
| `testMissingRuntimeListenerOnOneWakeRecoversAfterNextIdle` | pass, 62.633 | Один wake без listener после здорового старта → следующий wake здоровый, stuck=0 | `p5-p6-final-regressions-and-stall.log` |
|
||
| `testNoRuntimeListenerMetadataFromFirstStartupMatchesP5` | pass, 70.149 | Test-only отсутствие listener с первого старта; два idle и explicit load между ними → P5/all tab flags=true | `p5-refined-and-controls.log` |
|
||
| `testSlowHealthyDocumentsWakeBackgroundAgainAtFinishWithoutFailure` | pass, 70.683 | Два 35-секундных image → initResp, report/eval снова будят background на finish, 0 failures | `p5-refined-and-controls.log` |
|
||
| `testSlowDocumentsWithLostDashboardDoNotMatchP5` | pass, 80.181 | Два 35-секундных image + потеря dashboard → initResp и stuck с живой view; не P5 | `p5-refined-and-controls.log` |
|
||
| `testMainFeatureFlagCycleRecreatesManagerAndMatchesP6` | pass, 9.252 | Production flag handler true→false→true → P6; true→true controller не меняет | `production-flag-cycle-final.log` |
|
||
| `testEmbeddedFeatureFlagCycleKeepsControllerAfterBackgroundBecomesReady` | pass, 0.985 | Embedded-only cycle сохраняет controller; initResp после explicit background-ready wait | `production-flag-cycle-final.log` |
|
||
|
||
Зелёный тест с ожидаемым сбоем означает воспроизведение механизма; зелёный negative control означает отсутствие предполагаемой сигнатуры в проверенных условиях. Исходные неудачные ожидания, ошибки harness и их исправления не удалены из §10–11. Единого итогового запуска всех 14 не было: green подтверждён раздельными группами. 478 XCTest обычного пакета включают **28 пропущенных opt-in**; они не заменяют выполненные длительные repro.
|
||
|
||
### Среда, исходники и устройство harness
|
||
|
||
- Worktree: `/Users/admin/DuckDuckGo/apple-browsers.git/alex/cpm-web-extension-remaining-failures`, HEAD `a0717cf098cd1cbd32d5bc9d19aa7db0fab2399f`; новые тесты untracked, поэтому checkout одного commit их не восстановит.
|
||
- macOS **26.7.1 (25G241)**, Xcode **27.1 (27A9275)**, системный WebKit; bundled embedded extension **2026.9.29**. Проверка source: `/Users/admin/Downloads/WebKit_sparse`, HEAD `99051d5d08cd9f19ec76ce599f7539d070b1ae09`. Этот checkout не доказывает, что весь его код соответствует shipped binary; нужный эффект отдельно проверен на system WebKit.
|
||
- Harness запускает production extension manager/coordinator, recorder, health monitor и native autoconsent handler, реальные WKWebView и WKWebExtension contexts. Host window/tab delegates и callbacks flag enable/disable собраны по исходникам macOS, не импортированы из целого приложения.
|
||
- База Options: startup navigation gate и M4 включены. **В P5/P6-тестах M4 выключен**, чтобы actual stuck не был сразу затёрт automatic reload; navigation gate остаётся включён. Перед context load прогревается Network-процесс default data store. Без прогрева PID=0→ненулевой может дать network_restarted=true без crash; это было причиной первого несовпадения.
|
||
- Временная распакованная копия bundled extension содержит test prelude/DOM marks и trace. Production zip не менялся. В source-linked window/flag сценариях listener не подавляется, dashboard не теряется намеренно, background startup не задерживается. Это не byte-identical bundle без instrumentation.
|
||
- Отдельные controlled сценарии используют: nil/старый controller, suppressed runtime listener, suppressed native dashboard method, задержанный image через WKURLSchemeHandler или блокировку main actor. Их нельзя выдавать за найденный естественный production-trigger.
|
||
- Content marks требуют `initresp` для здоровых контролей; native dashboard arrival отдельно измеряет production detector. Trace не должен удерживать view: watcher исправлен, чтобы strong reference не пересекал 50-мс await. Idle-helper требует одновременно `context._backgroundWebView == nil` и `backgroundWebViewAlive == false` в recorder.
|
||
- Долгие image имеют уникальный URL на каждую навигацию, иначе второй документ берётся из cache. `adoptTab` восстанавливает navigationDelegate после teardown предыдущего harness. Счётчик 3 view в pixel — bucket `3_to_5`; raw assertion использует `backgroundWebViewCreateCount == 3`.
|
||
- Три новых файла: `CPMReproLifecycleHypothesisTests.swift`, `CPMReproDelayedResource.swift`, `CPMReproProductionFlagTests.swift`. Пять дополненных: `CPMReproHarness.swift`, `CPMReproTabs.swift`, `CPMReproSupport.swift`, `CPMReproPrelude.swift`, `CPMReproTestCase.swift`. Существующая dirty telemetry/Package.resolved не включалась в эту работу; тестовыми изменениями production поведение не исправлялось.
|
||
|
||
### Артефакты и история красных прогонов
|
||
|
||
Все перечисленные файлы находились в `/private/tmp/cpm-p5-p6-20261015/` на момент документирования. Папка временная: долговечная сводка значимых исходов, причин красных assertions и ограничений сохранена здесь и в §10–11; сами raw logs могут исчезнуть.
|
||
|
||
| Файл | Результат и как его читать |
|
||
|---|---|
|
||
| `build-and-p6-before-idle.log` | P6 получен, но network_restarted=true из-за холодного PID attribution; не полное совпадение |
|
||
| `p6-fast.log` | 2 теста / 1 failure: перенос старой view упёрся в снятый navigationDelegate; исправлен test adoptTab. P6 до idle уже green |
|
||
| `p5-p6-full.log` | 7 тестов / 11 failing assertions, 273.808 с: one-wake listener сам восстановился; finish длинного документа разбудил background; повторный image был cached. Эти результаты разделены на отрицательные выводы и ошибку постановки, а не скрыты |
|
||
| `p5-refined-and-controls.log` | 5 тестов / 1 failure, 289.130 с: 4 green, оставшийся assertion ожидал строку «3» вместо bucket «3_to_5». Исправлен raw count assertion и повторён |
|
||
| `p5-p6-final-regressions-and-stall.log` | 6 тестов / 0 failures, 262.676 с; задержка main actor даёт matchesP5=false |
|
||
| `production-window-close.log` | 2 теста / 0 failures, 74.238 с; source-linked P5 и unrelated-window control |
|
||
| `production-flag-cycle.log` | 2 теста / 3 assertions, 15.363 с: main flag P6 green; embedded-only first init потерян до worker activate — известный A/P4, не P6 |
|
||
| `production-flag-cycle-final.log` | 2 теста / 0 failures, 10.237 с; embedded control проверен после explicit ready wait |
|
||
| `package-tests.log` | Промежуточный пакет: 474 XCTest, 24 skipped, 0 failures, 11.344 с |
|
||
| `package-tests-production-paths.log` | Последний пакет: 478 XCTest, 28 skipped, 0 failures, 10.469 с |
|
||
| `summarize.py`, `results.json` | Сводка последних результатов 14 текущих методов, имена исходных логов и SHA-256 source файлов; это не лог одного общего прогона |
|
||
|
||
Контрольные суммы актуальных тестов:
|
||
- `CPMReproLifecycleHypothesisTests.swift`: `60c2f3081c08a40cfc0244e023652013f0abf939151a63c14addc400a40ed413`.
|
||
- `CPMReproProductionFlagTests.swift`: `240c3828e3fac13357c6e80a954f3d07fc5dd26376d36308258ecc984bfd860e`.
|
||
- Существующий dirty `Package.resolved`, SHA-1: `7ccce20acd452656c9bf2d2fa186d044e4a8185f`.
|
||
|
||
### Команды повторения и границы следующей работы
|
||
|
||
Из `/Users/admin/DuckDuckGo/apple-browsers.git/alex/cpm-web-extension-remaining-failures/SharedPackages/WebExtensions`:
|
||
|
||
```sh
|
||
# Все 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.
|