diff --git a/docs/DEVICE_VALIDATION_LEDGER.md b/docs/DEVICE_VALIDATION_LEDGER.md index c67f1c01..3603403c 100644 --- a/docs/DEVICE_VALIDATION_LEDGER.md +++ b/docs/DEVICE_VALIDATION_LEDGER.md @@ -17,6 +17,7 @@ | # | Özellik | Nerede | Kabul ölçütü (cihazda ne gözlemlenmeli) | Eklendi | |---|---------|--------|------------------------------------------|---------| +| 55 | **🔴 Device Validation Pending — Worker Source Health Transport (PR-1)** — Worker'ın 1 Hz watchdog'unda **ZATEN hesapladığı** per-kaynak canlılık (`_alive(_canLastSeen, SRC_TIMEOUT_CAN_MS=3s)` · OBD 5s · GPS 5s — `VehicleCompute.worker.ts:92-96, 1010-1013`) ana thread'e taşınır: yeni `SOURCE_HEALTH` mesajı → `VehicleSignalResolver` → `halStatusStore.sourceHealth`. **NEDEN:** sessiz ECU frame kaybında native `canStatus` yalnız TRANSPORT durumunu bildiriyor (`CarLauncherPlugin.java:3748-3756` — port açıkken `connected:true`), dolayısıyla frame kaybını **yalnız worker** görüyor; bu bilgi bugüne dek ana thread'e HİÇ ulaşmıyordu. **DAVRANIŞ:** can/obd/gps **AYRI** tutulur (tek global `halConnected` yetersiz — CAN ölüp GPS yaşarken toptan kapatma yapılamaz); `null` = **BİLİNMİYOR** ve `false` (ölü) ile KARIŞTIRILMAZ; mesaj **YALNIZ DEĞİŞİMDE** postlanır (kenar-tetikli, duplicate durum mesaj üretmez; store de idempotent → gereksiz abone uyanışı yok); payload **yalnız 3 boolean + monotonik ts** (araç verisi/hız/RPM/CAN frame/VIN/PII YOK); pre-allocated envelope (hot-path allocation yok). **YENİ TIMER YOK** (worker'daki `setInterval` sayısı 4'te sabit: speed/fuel/coolant/watchdog — testle kilitli). **BU PR YALNIZ BİLGİ TAŞIR:** hiçbir sinyali unsupported YAPMAZ; HAL · adapter · Event Bus · bridge davranışı DEĞİŞMEZ (adapter hâlâ `halStatusStore` okumaz — kilitli). Fail-closed tüketim **AYRI PR (PR-2)**. ⚠️ **Background/visibility politikası EKLENMEDİ** (risk: uygulama arka plandayken worker timer'ı kısılır veya plugin event teslimi gecikirse yanlış-pozitif "CAN öldü" üretebilir → PR-2'de tüketimden önce ölçülmeli). | `platform/vehicleDataLayer/VehicleCompute.worker.ts` (WorkerOutMessage + kenar-tetikli post), `platform/vehicleDataLayer/VehicleSignalResolver.ts` (SOURCE_HEALTH kanalı), `platform/vehicleDataLayer/halStatusStore.ts` (bounded `sourceHealth` + setter), `__tests__/workerSourceHealthTransport.test.ts` (yeni, 20 test) | Gerçek cihazda/araçta: (1) **geçiş başına TEK mesaj** ve mesaj SPAM'i yok (store yazımı yalnız değişimde); (2) yeni timer/polling YOK; (3) **sahte disconnect YOK** — sinyaller hâlâ supported (Tanı raporu: `halWiring.ingestedSignalCount` düşmüyor, `halBridge.droppedCount=0`); (4) **gerçek araçta CAN frame kesilince `canAlive=false`** (≤3 sn) ve (5) **reconnect'te `canAlive=true`**; (6) OBD/GPS bağımsız kalıyor; (7) Xiaomi 23090RA98I'de boot/CPU/RAM regresyonu YOK; (8) **en az bir low-tier cihazda (K24/Mali-400 veya T507)** doğrulama; (9) background/foreground geçişinde yanlış-pozitif ölçülür. Araç ve low-tier doğrulanmadan 🟢 YAPMA. ⚠️ **KISMİ CİHAZ KANITI (2026-07-12, Xiaomi 23090RA98I — ARAÇ/OBD YOK):** 🟢 **Sahte disconnect YOK:** gerçek "Tanı Gönder" raporunda `halWiring{ingestedSignalCount:3 (değişmedi), activeSubscriptionCount:1, lastErrorCode:null}` ve `halBridge{present:true, started:true, droppedCount:0}` → PR-1 bilgi taşıması hiçbir sinyali düşürmedi, HAL/bridge davranışı DEĞİŞMEDİ. 🟢 Boot crash/ANR YOK (3 cold start: 1287/1027/1058 ms), idle CPU 13-60% / RSS 558-580 MB → önceki build'lerle aynı bantta (regresyon gözlenmedi). **HÂLÂ DOĞRULANMADI (bu yüzden 🔴):** (a) **`canAlive=false` / reconnect `true` — gerçek araç/OBD gerektirir** (CAN frame kesintisi üretilemiyor); (b) **low-tier cihaz (K24/T507) doğrulaması yok**; (c) background/foreground yanlış-pozitif ölçümü yapılmadı. ⚠️ **AÇIK BLOKAJ:** `sourceHealth` alanları TEŞHİS RAPORUNA henüz eklenmedi (diagnosticSections bu PR'ın izinli kapsamında değildi) → `canAlive`'ı cihazda ADB'siz okumak için PR-2'de (veya küçük bir teşhis PR'ında) bounded alan eklenmeli. | 2026-07-12 | | 54 | **🔴 Device Validation Pending — Vehicle HAL → Event Bus Bridge Runtime Wiring (W4C)** — Zincir TAMAMLANDI: `UnifiedVehicleStore → VehicleHalProviderAdapter → vehicleHal → VehicleHalEventBridge → appEventBus`. Bridge foundation'ı (PR #54) gerçek `vehicleHal` singleton'ına ve W3'ün TEK aktif bus'ına bağlandı. **SAHİPLİK:** bridge bu wiring'in → cleanup YALNIZ bridge'i dispose eder; **HAL ve Bus DISPOSE EDİLMEZ**. **TEK INSTANCE:** aktif bridge varken ikinci start YENİ HAL aboneliği AÇMAZ; bayat kayıt serbest; eski cleanup yeni bridge'i düşüremez. **SystemBoot:** Wave 2'de HAL wiring'den SONRA, SystemOrchestrator'dan ÖNCE `_reg` ile → LIFO shutdown: bridge → HAL wiring → VDL → … → Event Bus (en son) ⇒ bridge kapanırken HAL ve Bus HÂLÂ AYAKTA. Bus yoksa wiring sessizce no-op (fail-soft, boot sürer). **EVENT:** `vehicle.signal.changed` (transient, supported sinyal değişimi; W4A sayesinde tek HAL snapshot → değişen sinyal başına 1 event), `vehicle.connection.changed` (retained; W4B unsupported geçişi TÜM destek kalkınca false üretir, kısmi kayıpta üretmez, reconnect'te true), `vehicle.identity.changed` (retained, ham VIN YOK — yalnız fingerprint). **`vehicle.ignition.changed` ÜRETİLMEZ** (kaynak yok → supported=false). TPMS payload bounded (≤8). Duplicate değer → duplicate event YOK (bridge imza-dedupe). **TEŞHİS:** support_snapshot'a bounded `halBridge{present,started,disposed,publishedCount,droppedCount,lastPublishAt}` eklendi — "bridge yok" (present:false, null) ile "0 event" AYRI. **BİLİNÇLİ KARARLAR:** throttle/debounce/coalescing/async-queue **YOK** → publish hızı = gerçek sinyal değişim hızı; **W4D kararı bu PR'ın CİHAZ ÖLÇÜMÜNDEN sonra** verilecek. **RİSK:** `PlatformEventBus.publish()` SENKRON dispatch eder → ileride eklenecek consumer'lar HAL/store hot-path stack'inde çalışır (bu PR'da ABONE YOK → yalnız publish maliyeti). **TİP NOTU:** bridge'in publish sözleşmesi geniş (`string`) domain/source alır, Bus dar birlik tipleri bekler → foundation'a dokunmadan wiring'de daraltıcı publish hedefi kullanıldı (doğrulama zayıflamaz; Bus zaten beyaz liste uygular). **KAPSAM DIŞI:** consumer/abone · Capability bridge/Registry · Deep Scan · Kernel servis kaydı · yeni catalog girdisi · HAL stale sweeper · **native `canStatus` wiring / worker source-health** · throttle (W4D). | `platform/system/platformCoreVehicleHalBridgeWiring.ts` (yeni), `platform/system/SystemBoot.ts` (additive Wave 2), `platform/diagnosticSections.ts` (halBridge bölümü), `__tests__/platformCoreVehicleHalBridgeWiring.test.ts` (yeni, 35 test), `__tests__/platformRuntimeDiagnostics.test.ts` (kilit güncellendi) | Gerçek cihazda/araçta: (1) TEK aktif bridge ve DOĞRU app Event Bus (Tanı raporu: `halBridge.present=true`, `started=true`); (2) HAL sinyal değişimleri doğru topic'lere publish (araçta speed/rpm/coolant/gear/reverse); (3) **publish/sn ölçülür** (park + sürüş); (4) duplicate sinyal duplicate event ÜRETMİYOR; (5) cleanup sonrası publish YOK; (6) bridge HAL/Bus'ı dispose ETMİYOR (restart sonrası bus sayaçları sıfırlanmıyor/çift bus yok); (7) `vehicle.ignition.changed` HİÇ yok; (8) TPMS payload bounded; (9) raporda VIN/ham CAN/koordinat/secret YOK; (10) `droppedCount=0` (veya gerekçeli); (11) CAN/OBD disconnect sonrası mümkün olan sinyaller unsupported + `connection.changed=false` YALNIZ tüm destek kalkınca; (12) reconnect sonrası `connection.changed=true`; (13) Xiaomi'de boot/idle CPU/RSS/FPS regresyonu YOK; (14) **en az bir low-tier cihazda (K24/Mali-400 veya T507)** sayaçlar raporda görünüyor, ANR/donma yok, publish yükü CPU/FPS bütçesini bozmuyor. ⚠️ **AÇIK BLOKAJ (ayrı iş):** ECU sessizce frame kesip store değerlerini null'a çekmezse disconnect TESPİT EDİLEMEZ (native `canStatus` hiçbir store'a yazılmıyor) → `connection.changed=false` o senaryoda üretilemez. ⚠️ **throttle/coalescing W4D'ye bırakıldı** (ölçüm sonrası). Masaüstü/araçsız Xiaomi testleriyle 🟢 YAPMA. ⚠️ **KISMİ CİHAZ KANITI (2026-07-12, Xiaomi 23090RA98I — ARAÇ/OBD YOK; ölçüm PR #68 teşhis yüzeyinden, uygulama koduna HİÇ enstrümantasyon eklenmeden):** 🟢 Bridge CANLI: gerçek "Tanı Gönder" raporunda `halBridge{present:true, started:true, droppedCount:0}`. 🟢 **PUBLISH/SN ÖLÇÜLDÜ (park, yalnız GPS):** iki rapor arası 440 sn — bridge `publishedCount` 33→197 (Δ=164) ⇒ **≈0,37 publish/sn**; bus `publishedCount` 34→198 (aynı Δ ⇒ yalnız bridge yayınlıyor), `droppedCount=0`, **`deliveredCount=0` ve `activeListenerCount=0`** (tasarım gereği abone YOK). 🟢 `halWiring.refreshCount` 56→487 (≈1 store update/sn) ve **`activeSubscriptionCount=1`** (tek HAL aboneliği). 🟢 `historyCount` 6'da SABİT → `vehicle.signal.changed` transient olduğu için history/RAM ŞİŞMİYOR; `retainedEventCount=2` (runtime.started + connection.changed). 🟢 Teşhis bölümü 612-615 bayt, **sızıntı izi YOK**. 🟢 Boot crash/ANR YOK (3 cold start: 1474/1112/1077 ms), idle CPU 17-27% / RSS 545-576 MB (önceki build'lerle aynı bantta → regresyon gözlenmedi). **HÂLÂ DOĞRULANMADI (bu yüzden 🔴): gerçek araç/OBD** — speed/rpm/coolant/gear/reverse event'leri, **sürüşteki publish/sn** (CAN frame hızı kodda yok → W4D throttle kararının girdisi), CAN/OBD disconnect sonrası `connection.changed=false`, reconnect, CPU/FPS bütçesi; ve **low-tier cihaz (K24/Mali-400 veya T507) doğrulaması YAPILMADI.** | 2026-07-12 | | 53 | **🔴 Device Validation Pending — Vehicle HAL Source Disconnect / Fail-Closed (W4B)** — Kaynak kopunca HAL'in eski değerleri sonsuza kadar `supported=true`+`stale=false` tutması KAPATILDI. **KOD GERÇEĞİ:** `VehicleHal.refresh()` (tek stale süpürücü) üretimde HİÇ çağrılmıyor; `canStatus` native olayı hiçbir store'a yazılmıyor (yalnız `console.info` — CanAdapter.ts:57-59); `canPhase` yalnız İLK-FRAME timeout'unu temsil ediyor. **Gerçek kopma kanıtı:** worker kaynak-timeout'unda `null` yayar (SRC_TIMEOUT_CAN/OBD/GPS) ve `resetCanData()` CAN alanlarını null'lar. **SEÇİLEN MODEL (A-refined, timersız, yeni abonelik/DI YOK):** adapter, DAHA ÖNCE aktarılmış bir sinyalin değeri kaybolunca `source:'none'` ingest eder → HAL `supported=false` + `value=null` (eski değer taşınmaz; quality `unknown`, confidence 0 — uydurma YOK; timestamp = GEÇİŞ anı). **unknown ≠ disconnected:** hiç aktarılmamış sinyal için SAHTE disconnect üretilmez. **Toptan kapatma YOK:** yalnız kaynağı kaybolan sinyal düşer; füzyon `speed` GPS/OBD ile canlıysa supported KALIR. **CAN-özel boolean'lar** (door_state/parking_brake) artık CAN canlılığına bağlı → `resetCanData()`'nın `false` artığı "gerçek veri" sayılmaz (reverse füzyon olduğu için genel canlılıkta kalır). **Batch korunur (W4A):** bir geçişte düşen tüm sinyaller TEK batch → TEK HAL emit; aynı disconnected durum TEKRAR ingest edilmez (duplicate emit yok); kaynak dönünce değer normal yoldan yeniden ingest edilir (supported=true + gerçek source). **ignition:** kaynak yok → hâlâ ASLA üretilmez. **KAPSAM DIŞI:** zaman-tabanlı stale sweeper / yeni timer · Event Bus/bridge (W4C) · throttle (W4D) · native CAN state machine refactor. | `platform/vehicleHal/vehicleHalProviderAdapter.ts`, `__tests__/vehicleHalSourceDisconnect.test.ts` (yeni, 25 test) | Gerçek araçta: (1) CAN/OBD bağlıyken ilgili sinyaller supported; (2) adaptör/CAN kesilince **yalnız o kaynağın sinyalleri** `source=none`/`supported=false` oluyor; (3) diğer canlı kaynakların sinyalleri (GPS füzyon hızı) supported KALIYOR; (4) bir kopma geçişi TEK batch/TEK HAL emit üretiyor; (5) aynı disconnected durum duplicate emit ÜRETMİYOR; (6) yeniden bağlanınca supported/source/değer doğru geri dönüyor; (7) `vehicle.ignition` unsupported KALIYOR; (8) yeni timer/polling YOK ve tek subscription korunuyor (Tanı raporu: `halWiring.activeSubscriptionCount=1`, kopma/geri gelme sırasında `ingestedSignalCount` düşüp yükseliyor); (9) Xiaomi'de CPU/RAM/boot regresyonu YOK; (10) **en az bir low-tier cihazda (K24/Mali-400 veya T507)** doğrulama. ⚠️ **BİLİNEN SINIRLAMA (kapsam dışı):** CAN kablosu takılıyken ECU sessizce frame göndermeyi keserse store'daki CAN değerleri null'a DÖNMEZ (native `canStatus` hiçbir store'a yazılmıyor) → bu senaryoda kopma HÂLÂ tespit edilemez; ayrı iş (native canStatus wiring veya worker CAN-sağlık expose). Test edilmeden 🟢 yapma. ⚠️ **KISMİ CİHAZ KANITI (2026-07-12, Xiaomi 23090RA98I — ARAÇ/OBD YOK; ölçüm W4E teşhis yüzeyiyle, uygulama koduna HİÇ enstrümantasyon eklenmeden):** 🟢 Cihazın gerçek "Tanı Gönder" raporunda `halWiring{started:true, **activeSubscriptionCount:1**, refreshCount:169, ingestedSignalCount:3, lastErrorCode:null}` → tek abonelik korunuyor, hata yok. 🟢 **CAN-boolean kapısı cihazda GÖZLEMLENDİ:** aynı cihazda W4B ÖNCESİ build `ingestedSignalCount=5` veriyordu, W4B ile **3** → CAN yokken `door_state`+`parking_brake` artık HAL'e aktarılmıyor (resetCanData'nın `false` artığı "gerçek veri" sayılmıyor). 🟢 Boot/CPU/RAM regresyonu YOK (aynı cihaz durumunda A/B): baseline (main eşdeğeri) cold start 3026/3014/3014 ms · CPU 0-26% · RSS 360-365 MB → W4B 3017/3012/3010 ms · CPU 3-28% · RSS ~332 MB. 🟢 Crash/ANR yok (3 cold start + background/foreground + rapor gönderimi). **HÂLÂ DOĞRULANMADI (bu yüzden 🔴): gerçek CAN/OBD kopar-bağla senaryosu** (supported→unsupported geçişi, fallback, reconnect, tek-emit) — **araç/OBD gerektirir**; ve **low-tier cihaz (K24/Mali-400 veya T507) doğrulaması yapılmadı.** | 2026-07-12 | | 52 | **🔴 Device Validation Pending — Platform Runtime Diagnostics (W4E, kalıcı bounded teşhis yüzeyi)** — Event Bus + Vehicle HAL wiring'in BOUNDED durum sayaçları mevcut kullanıcı-tetikli teşhis raporuna (`triggerSupportSnapshot` → `reportSupportSnapshot` → `_buildSupportSnapshotPayload` → `_deepSanitize` → `pushVehicleEvent('support_snapshot')` → /admin/tani) **kalıcı** olarak eklendi. **AMAÇ:** "tek instance / tek abonelik / event sayacı" invaryantları cihazda **ADB, CDP veya geçici debug expose OLMADAN** okunabilsin (T507 gibi PC-adb'siz cihazların TEK ölçüm kanalı). **KULLANILAN API'ler (yalnız mevcut, salt-okunur):** `getEventBusStatus()` ve `getVehicleHalWiringStatus()`. **WHITELIST — Event Bus:** present · disposed · publishedCount · deliveredCount · droppedCount · listenerErrorCount · duplicateSubscriptionCount · recursionDropCount · activeListenerCount · retainedEventCount · historyCount · lastEventAt · runtimeStartedPublished · runtimeStoppedPublished. **HAL wiring:** started · lastRefreshAt · refreshCount · ingestedSignalCount · activeSubscriptionCount · lastErrorCode (SABİT kod: init_failed/cleanup_failed; serbest metin/stack REDDEDİLİR). **`disposed` HAL wiring status'ünde YOK → uydurulmadı.** **Bridge bölümü ÜRETİLMEZ** (üretimde bağlı değil, status accessor'ı yok → sahte present/started yerine W4C'ye bırakıldı). **PRIVACY:** runtime status nesnesi ASLA spread edilmez (alan-alan whitelist); event payload · history içeriği · topic detayı · correlation/causation · araç sinyal DEĞERLERİ (hız/RPM) · VIN · fingerprint · koordinat · MAC · CAN frame · ham hex · secret · stack trace **GİRMEZ**; çıktı yalnız sayı/bool/null. **"0" ≠ "ölçülemiyor":** wiring yoksa sayaçlar **null** (0 değil). **FAIL-SOFT:** accessor throw ederse bölüm boş döner ve raporun DİĞER bölümleri üretilmeye devam eder (`_safeSection`); NaN/Infinity → null. **YALIN:** yeni timer/polling YOK · yeni publisher/subscriber/bridge/wiring YOK · runtime instance YARATMAZ (teşhis okuma bus/adapter oluşturmaz) · yeni otomatik/yüksek frekanslı gönderim YOK (kullanıcı tetikli akış korunur) · yeni bağımlılık YOK. | `platform/diagnosticSections.ts` (+`buildPlatformRuntimeSnapshot`), `platform/remoteLogService.ts` (payload'a `platform: _safeSection(...)`), `__tests__/platformRuntimeDiagnostics.test.ts` (yeni, 17 test) | Gerçek cihazda: (1) "Tanı Gönder" raporunda **bounded Event Bus sayaçları** görünüyor; (2) raporda **bounded Vehicle HAL wiring sayaçları** görünüyor (`activeSubscriptionCount=1` → tek abonelik ADB'siz doğrulanabiliyor); (3) raporda **hiçbir event payload'ı veya araç sinyal değeri YOK**; (4) VIN/koordinat/CAN/ham hex/secret YOK; (5) wiring yokken rapor fail-soft (sayaçlar null, rapor yine gönderiliyor); (6) teşhis üretimi yeni runtime instance YARATMIYOR; (7) yeni timer/polling YOK; (8) rapor boyutunda anlamlı şişme YOK (önce/sonra ölçülür); (9) Xiaomi 23090RA98I'de ADB/global expose olmadan sayaçlar okunabiliyor; (10) **T507 veya K24/Mali-400'de mevcut teşhis kanalıyla sayaçlar okunabiliyor** ve rapor gönderimi başarılı, UI donması/ANR yok; (11) CPU/RAM/boot regresyonu YOK. Masaüstü testleriyle 🟢 YAPMA; low-tier cihazda gerçek rapor doğrulanmadan 🔴 kalır. ⚠️ **CİHAZ KANITI (2026-07-12, Xiaomi 23090RA98I — UYGULAMA KODUNA HİÇ ENSTRÜMANTASYON EKLENMEDEN):** 🟢 Cihazda **gerçek "Tanı Gönder" butonu** ile rapor tetiklendi; giden `push_vehicle_event` (support_snapshot) isteğinin GÖVDESİ (CDP Network ile, yalnız gözlem) `platform` bölümünü İÇERİYOR: `eventBus{present:true, publishedCount:1, deliveredCount:0, droppedCount:0, listenerErrorCount:0, duplicateSubscriptionCount:0, recursionDropCount:0, activeListenerCount:0, retainedEventCount:1, historyCount:1, runtimeStartedPublished:true, runtimeStoppedPublished:false}` + `halWiring{started:true, **activeSubscriptionCount:1**, refreshCount:289, ingestedSignalCount:5, lastErrorCode:null}` → **tek bus + tek HAL aboneliği + runtime.started bir kez, ADB/global expose OLMADAN raporda okunabiliyor.** 🟢 Gizlilik: platform bölümünde VIN/koordinat/sinyal değeri/correlation/payload/history izi **YOK** (otomatik tarama: sızıntı izi yok). 🟢 Boyut: platform bölümü **484 bayt**, toplam rapor ~14,2 KB (≈%3,4 → anlamlı şişme yok). 🟢 Kararlılık: 3 cold start (1059/1085/1325 ms) + background/foreground + rapor gönderimi sırasında **crash/ANR YOK**, UI donmadı; idle CPU 25-39% / RSS 577-600 MB (önceki build ile aynı bantta → regresyon yok). **HÂLÂ DOĞRULANMADI (bu yüzden 🔴): low-tier cihaz (T507 veya K24/Mali-400) üzerinde rapor gönderimi ve /admin/tani'de platform sayaçlarının görünmesi DOĞRULANMADI** (T507'de PC-adb yok — bu PR'ın asıl hedef senaryosu); ayrıca alınan raporun admin panelinde görüntülenmesi kullanıcı tarafından teyit edilmeli. | 2026-07-12 | diff --git a/src/__tests__/workerSourceHealthTransport.test.ts b/src/__tests__/workerSourceHealthTransport.test.ts new file mode 100644 index 00000000..64a0039f --- /dev/null +++ b/src/__tests__/workerSourceHealthTransport.test.ts @@ -0,0 +1,183 @@ +/** + * workerSourceHealthTransport.test.ts — PR-1: Worker Source Health Transport kilitleri. + * + * AKIŞ: `VehicleCompute.worker` (mevcut 1 Hz watchdog) → `SOURCE_HEALTH` mesajı → + * `VehicleSignalResolver` → `halStatusStore.sourceHealth`. + * + * AMAÇ: worker'ın ZATEN hesapladığı per-kaynak canlılığı (CAN/OBD/GPS) ana thread'e taşımak. + * Bu PR **bilgi taşır**: hiçbir sinyali unsupported YAPMAZ, HAL/adapter/Event Bus/bridge + * davranışını DEĞİŞTİRMEZ, yeni timer/polling AÇMAZ. + * + * Worker bir Web Worker modülüdür (jsdom'da çalıştırılamaz) → worker/resolver tarafı YAPISAL + * kilitlerle (`?raw`), store tarafı DAVRANIŞSAL olarak doğrulanır. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { useHALStatusStore } from '../platform/vehicleDataLayer/halStatusStore'; +import workerSrc from '../platform/vehicleDataLayer/VehicleCompute.worker.ts?raw'; +import resolverSrc from '../platform/vehicleDataLayer/VehicleSignalResolver.ts?raw'; +import adapterSrc from '../platform/vehicleHal/vehicleHalProviderAdapter.ts?raw'; + +/** Sağlık bloğu: kaynak kilitleri yalnız PR-1 bölgesinde çalışsın (yorum/başka kod karışmasın). */ +const HEALTH_BLOCK = workerSrc.slice( + workerSrc.indexOf('_prevCanAlive'), + workerSrc.indexOf('function _postEvent'), +); + +const initial = useHALStatusStore.getState(); + +beforeEach(() => { + useHALStatusStore.setState({ + sourceHealth: { canAlive: null, obdAlive: null, gpsAlive: null, updatedAt: null }, + }); +}); + +/* ── Store davranışı: unknown / geçiş / bağımsızlık / idempotency ──────────── */ + +describe('PR-1 — halStatusStore.sourceHealth davranışı', () => { + it('başlangıç UNKNOWN (null) — "false" (ölü) ile KARIŞMAZ', () => { + const h = useHALStatusStore.getState().sourceHealth; + expect(h.canAlive).toBeNull(); + expect(h.obdAlive).toBeNull(); + expect(h.gpsAlive).toBeNull(); + expect(h.updatedAt).toBeNull(); + }); + + it('alive → store true; dead → store false (geçişler yansır)', () => { + const set = useHALStatusStore.getState().setSourceHealth; + set({ can: true, obd: false, gps: true, ts: 100 }); + let h = useHALStatusStore.getState().sourceHealth; + expect(h.canAlive).toBe(true); + expect(h.obdAlive).toBe(false); + expect(h.gpsAlive).toBe(true); + expect(h.updatedAt).toBe(100); + + set({ can: false, obd: false, gps: true, ts: 200 }); // CAN öldü + h = useHALStatusStore.getState().sourceHealth; + expect(h.canAlive).toBe(false); + expect(h.updatedAt).toBe(200); + }); + + it('dead → alive (reconnect) yansır', () => { + const set = useHALStatusStore.getState().setSourceHealth; + set({ can: false, obd: false, gps: false, ts: 10 }); + set({ can: true, obd: false, gps: false, ts: 20 }); + expect(useHALStatusStore.getState().sourceHealth.canAlive).toBe(true); + }); + + it('CAN / OBD / GPS BAĞIMSIZ (biri düşünce diğerleri etkilenmez)', () => { + const set = useHALStatusStore.getState().setSourceHealth; + set({ can: true, obd: true, gps: true, ts: 1 }); + set({ can: false, obd: true, gps: true, ts: 2 }); + const h = useHALStatusStore.getState().sourceHealth; + expect(h.canAlive).toBe(false); + expect(h.obdAlive).toBe(true); + expect(h.gpsAlive).toBe(true); + }); + + it('AYNI durum tekrar gelirse store DEĞİŞMEZ (idempotent — gereksiz abone uyanışı yok)', () => { + const set = useHALStatusStore.getState().setSourceHealth; + set({ can: true, obd: false, gps: true, ts: 1 }); + const before = useHALStatusStore.getState().sourceHealth; + set({ can: true, obd: false, gps: true, ts: 999 }); // aynı durum, farklı ts + const after = useHALStatusStore.getState().sourceHealth; + expect(after).toBe(before); // REFERANS aynı → set() olmadı + expect(after.updatedAt).toBe(1); + }); + + it('bozuk mesaj YOK SAYILIR (fail-soft; unknown durumu korunur)', () => { + const set = useHALStatusStore.getState().setSourceHealth; + set({ can: 'evet', obd: false, gps: true, ts: 1 } as unknown as { can: boolean; obd: boolean; gps: boolean; ts: number }); + expect(useHALStatusStore.getState().sourceHealth.canAlive).toBeNull(); + }); + + it('geçersiz ts → updatedAt null (uydurma zaman yok)', () => { + useHALStatusStore.getState().setSourceHealth({ can: true, obd: true, gps: true, ts: NaN }); + expect(useHALStatusStore.getState().sourceHealth.updatedAt).toBeNull(); + }); + + it('BOUNDED ve PII\'siz: yalnız 3 boolean|null + 1 sayı', () => { + useHALStatusStore.getState().setSourceHealth({ can: true, obd: false, gps: true, ts: 42 }); + const h = useHALStatusStore.getState().sourceHealth; + expect(new Set(Object.keys(h))).toEqual(new Set(['canAlive', 'obdAlive', 'gpsAlive', 'updatedAt'])); + for (const [k, v] of Object.entries(h)) { + expect(k === 'updatedAt' ? ['number', 'object'] : ['boolean', 'object']).toContain(typeof v); + } + expect(JSON.stringify(h)).not.toMatch(/speed|rpm|vin|lat|lon|frame|[0-9A-F]{17}/i); + }); + + it('mevcut halStatusStore alanları BOZULMADI (canPhase/activeSource/halConnected)', () => { + expect(typeof initial.setCanPhase).toBe('function'); + expect(typeof initial.setActiveSource).toBe('function'); + expect(typeof initial.setHALConnected).toBe('function'); + expect(initial.canPhase).toBeDefined(); + }); +}); + +/* ── Worker: kenar-tetikleme, yeni timer yok, payload bounded ──────────────── */ + +describe('PR-1 — worker SOURCE_HEALTH sözleşmesi (kaynak kilidi)', () => { + it('mesaj tipi yalnız boolean + ts taşır (araç verisi/PII YOK)', () => { + expect(workerSrc).toMatch(/\|\s*\{\s*type:\s*'SOURCE_HEALTH';\s*can:\s*boolean;\s*obd:\s*boolean;\s*gps:\s*boolean;\s*ts:\s*number\s*\}/); + }); + + it('YALNIZ DEĞİŞİMDE postlanır (duplicate durum → erken çıkış)', () => { + expect(HEALTH_BLOCK).toMatch(/if\s*\(can === _prevCanAlive && obd === _prevObdAlive && gps === _prevGpsAlive\)\s*return;/); + }); + + it('önceki durum null ile başlar → unknown ≠ false (ilk tur mutlaka bildirilir)', () => { + expect(HEALTH_BLOCK).toMatch(/_prevCanAlive:\s*boolean \| null = null/); + expect(HEALTH_BLOCK).toMatch(/_prevObdAlive:\s*boolean \| null = null/); + expect(HEALTH_BLOCK).toMatch(/_prevGpsAlive:\s*boolean \| null = null/); + }); + + it('MEVCUT 1 Hz watchdog kullanılır — sağlık bloğunda YENİ timer YOK', () => { + expect(HEALTH_BLOCK).not.toMatch(/setInterval|setTimeout|requestAnimationFrame/); + expect(workerSrc).toMatch(/function _watchdog\(\)[\s\S]{0,400}_postSourceHealthIfChanged\(canAlive, obdAlive, gpsAlive\)/); + }); + + it('worker timer sayısı DEĞİŞMEDİ (yalnız mevcut 4 setInterval)', () => { + const intervals = workerSrc.match(/setInterval\(/g) ?? []; + expect(intervals.length).toBe(4); // speed · fuel · coolant · watchdog + }); + + it('pre-allocated envelope (hot-path allocation yok)', () => { + expect(HEALTH_BLOCK).toMatch(/const _outSourceHealth[\s\S]*self\.postMessage\(_outSourceHealth\)/); + }); +}); + +/* ── Resolver: doğru store alanı, dispose sonrası yazım yok ───────────────── */ + +describe('PR-1 — resolver kanalı (kaynak kilidi)', () => { + it('SOURCE_HEALTH → halStatusStore.setSourceHealth', () => { + expect(resolverSrc).toMatch(/case 'SOURCE_HEALTH':[\s\S]{0,300}setSourceHealth\(/); + }); + + it('dispose sonrası store yazımı YOK: listener terminate\'ten ÖNCE kaldırılır', () => { + const stopBlock = resolverSrc.slice(resolverSrc.indexOf('stop(): void {')); + const iRemove = stopBlock.indexOf('removeEventListener'); + const iTerm = stopBlock.indexOf('terminate()'); + expect(iRemove).toBeGreaterThan(0); + expect(iTerm).toBeGreaterThan(iRemove); // önce listener kalkar → geç mesaj store'a yazamaz + }); +}); + +/* ── Kapsam: HAL / Event Bus / bridge davranışı DEĞİŞMEDİ ─────────────────── */ + +describe('PR-1 — kapsam sınırı', () => { + it('worker ve resolver HAL/Event Bus/bridge\'e DOKUNMAZ', () => { + for (const src of [workerSrc, resolverSrc]) { + expect(src).not.toMatch(/from\s+['"][^'"]*vehicleHal/i); + expect(src).not.toMatch(/from\s+['"][^'"]*eventBus/i); + expect(src).not.toMatch(/from\s+['"][^'"]*bridges?/i); + } + }); + + it('adapter hâlâ halStatusStore OKUMAZ (fail-closed tüketim AYRI PR)', () => { + expect(adapterSrc).not.toMatch(/halStatusStore|sourceHealth/i); + }); + + it('sağlık bloğu hiçbir sinyali unsupported YAPMAZ (source:none üretmez)', () => { + expect(HEALTH_BLOCK).not.toMatch(/'none'|supported/); + }); +}); diff --git a/src/platform/vehicleDataLayer/VehicleCompute.worker.ts b/src/platform/vehicleDataLayer/VehicleCompute.worker.ts index 4214a51c..865cb968 100644 --- a/src/platform/vehicleDataLayer/VehicleCompute.worker.ts +++ b/src/platform/vehicleDataLayer/VehicleCompute.worker.ts @@ -54,7 +54,18 @@ export type WorkerOutMessage = * active=true → arıza başladı, active=false → kalite normale döndü. * SystemHealthMonitor bu olayı dinler (garbage fix'ler beat'i maskeleyemez). */ - | { type: 'GPS_FAILURE'; active: boolean; accuracy: number; ts: number }; + | { type: 'GPS_FAILURE'; active: boolean; accuracy: number; ts: number } + /** + * Kaynak sağlığı (PR-1 "Source Health Transport"): worker'ın ZATEN hesapladığı per-kaynak + * canlılık (`_alive(_xLastSeen, SRC_TIMEOUT_X_MS)`) ana thread'e taşınır. YENİ TIMER YOK — + * mevcut 1 Hz `_watchdog()` kullanılır. YALNIZ DEĞİŞİM ANINDA gönderilir (kenar-tetikli): + * aynı durum tekrar tekrar POSTLANMAZ. Payload YALNIZ boolean + timestamp — araç verisi + * (hız/RPM/CAN frame), VIN veya PII TAŞIMAZ. + * + * ⚠️ Bu mesaj sinyalleri unsupported YAPMAZ ve HAL/adapter/Event Bus/bridge davranışını + * DEĞİŞTİRMEZ; yalnız bilgi taşır (tüketim AYRI PR). + */ + | { type: 'SOURCE_HEALTH'; can: boolean; obd: boolean; gps: boolean; ts: number }; // ── Sabitler ───────────────────────────────────────────────────────────── @@ -404,6 +415,31 @@ function _postPatch(patch: Partial): void { self.postMessage(_outState); } +/* ── Kaynak sağlığı (PR-1) — kenar-tetikli, allocation'sız ──────────────────── + * Önceki durum `null` = BİLİNMİYOR (henüz hiç watchdog turu geçmedi). "unknown" ile + * "false" KARIŞTIRILMAZ: ilk tur mutlaka gönderilir (bilinmiyor → bilinen), sonrasında + * YALNIZ değişimde. Pre-allocated envelope (V8 hidden-class stabilitesi, zero-allocation). */ +let _prevCanAlive: boolean | null = null; +let _prevObdAlive: boolean | null = null; +let _prevGpsAlive: boolean | null = null; + +const _outSourceHealth: Extract = { + type: 'SOURCE_HEALTH', can: false, obd: false, gps: false, ts: 0, +}; + +/** Yalnız DEĞİŞİMDE postlar (duplicate durum mesaj üretmez). Yeni timer AÇMAZ. */ +function _postSourceHealthIfChanged(can: boolean, obd: boolean, gps: boolean): void { + if (can === _prevCanAlive && obd === _prevObdAlive && gps === _prevGpsAlive) return; + _prevCanAlive = can; + _prevObdAlive = obd; + _prevGpsAlive = gps; + _outSourceHealth.can = can; + _outSourceHealth.obd = obd; + _outSourceHealth.gps = gps; + _outSourceHealth.ts = performance.now(); // monotonik; araç verisi/PII YOK + self.postMessage(_outSourceHealth); +} + function _postEvent(ev: VehicleEvent): void { _outEvent.event = ev; self.postMessage(_outEvent); @@ -1012,6 +1048,10 @@ function _watchdog(): void { const obdAlive = _alive(_obdLastSeen, SRC_TIMEOUT_OBD_MS); const gpsAlive = _alive(_gpsLastSeen, SRC_TIMEOUT_GPS_MS); + // PR-1: kaynak sağlığını ana thread'e taşı — MEVCUT 1 Hz watchdog, yeni timer YOK. + // Yalnız geçişte postlanır; sinyalleri unsupported YAPMAZ (tüketim ayrı PR). + _postSourceHealthIfChanged(canAlive, obdAlive, gpsAlive); + // CAN + OBD ikisi de stale → geri vites overlay'ini sıfırla if (!canAlive && !obdAlive) { _clearReverseTimers(); diff --git a/src/platform/vehicleDataLayer/VehicleSignalResolver.ts b/src/platform/vehicleDataLayer/VehicleSignalResolver.ts index 0d233d0f..de5fa940 100644 --- a/src/platform/vehicleDataLayer/VehicleSignalResolver.ts +++ b/src/platform/vehicleDataLayer/VehicleSignalResolver.ts @@ -329,6 +329,13 @@ export class VehicleSignalResolver { // Worker GPS kalite arızası → HealthMonitor bilsin (garbage fix beat'i maskelemesin) healthMonitor.setGpsQuality(!msg.active, msg.accuracy); break; + case 'SOURCE_HEALTH': + // PR-1: worker'ın 1 Hz watchdog'unda hesapladığı per-kaynak canlılık (yalnız GEÇİŞTE gelir) + // → halStatusStore. Bu PR'da hiçbir sinyali unsupported YAPMAZ (tüketim ayrı PR). + useHALStatusStore.getState().setSourceHealth({ + can: msg.can, obd: msg.obd, gps: msg.gps, ts: msg.ts, + }); + break; } } } diff --git a/src/platform/vehicleDataLayer/halStatusStore.ts b/src/platform/vehicleDataLayer/halStatusStore.ts index 5e7c61c7..47a004d2 100644 --- a/src/platform/vehicleDataLayer/halStatusStore.ts +++ b/src/platform/vehicleDataLayer/halStatusStore.ts @@ -26,6 +26,25 @@ const PHASE_TEXT: Record = { FAILED: 'CAN bağlantısı başarısız. OBD/GPS modu aktif.', }; +/** + * Kaynak sağlığı (PR-1 "Source Health Transport") — worker'ın 1 Hz watchdog'unda ZATEN + * hesapladığı per-kaynak canlılık (`SRC_TIMEOUT_CAN/OBD/GPS_MS`) buraya taşınır. + * + * `null` = **BİLİNMİYOR** (worker henüz hiç sağlık bildirmedi) — `false` (kaynak ÖLÜ) ile + * KARIŞTIRILMAZ. Bounded: yalnız 3 boolean|null + 1 monotonik timestamp; araç verisi + * (hız/RPM/CAN frame), VIN veya PII TAŞIMAZ. + * + * ⚠️ Bu alanlar bu PR'da HİÇBİR sinyali unsupported YAPMAZ ve HAL/adapter/Event Bus/bridge + * davranışını DEĞİŞTİRMEZ (fail-closed tüketim AYRI PR). + */ +export interface SourceHealth { + canAlive: boolean | null; + obdAlive: boolean | null; + gpsAlive: boolean | null; + /** Worker monotonik saati (performance.now()) — duvar saati/PII değil. */ + updatedAt: number | null; +} + interface HALStatusState { halConnected: boolean; halConf: number; @@ -33,12 +52,20 @@ interface HALStatusState { canPhase: CanConnectionPhase; canRetryCount: number; canStatusText: string; + /** Per-kaynak canlılık — null=bilinmiyor. Tek global "bağlı mı" YETERSİZ olduğu için ayrı. */ + sourceHealth: SourceHealth; setHALConnected: (connected: boolean) => void; setActiveSource: (src: SignalSource) => void; setCanPhase: (phase: CanConnectionPhase, retryCount?: number) => void; + /** Worker SOURCE_HEALTH mesajından beslenir (yalnız geçişte gelir). Bounded/idempotent. */ + setSourceHealth: (health: { can: boolean; obd: boolean; gps: boolean; ts: number }) => void; resetCan: () => void; } +const _UNKNOWN_HEALTH: SourceHealth = Object.freeze({ + canAlive: null, obdAlive: null, gpsAlive: null, updatedAt: null, +}); + export const useHALStatusStore = create((set) => ({ halConnected: false, halConf: 0, @@ -46,9 +73,26 @@ export const useHALStatusStore = create((set) => ({ canPhase: 'IDLE', canRetryCount: 0, canStatusText: '', + sourceHealth: _UNKNOWN_HEALTH, setHALConnected: (connected) => set({ halConnected: connected, halConf: connected ? 0.98 : 0 }), setActiveSource: (src) => set({ activeSource: src }), + setSourceHealth: (h) => + set((s) => { + // Bozuk/eksik mesaj → yok say (fail-soft; bilinmiyor durumu KORUNUR). + if (!h || typeof h.can !== 'boolean' || typeof h.obd !== 'boolean' || typeof h.gps !== 'boolean') return s; + const cur = s.sourceHealth; + // Worker zaten yalnız geçişte gönderir; burada da idempotent kalınır (gereksiz set yok). + if (cur.canAlive === h.can && cur.obdAlive === h.obd && cur.gpsAlive === h.gps) return s; + return { + sourceHealth: { + canAlive: h.can, + obdAlive: h.obd, + gpsAlive: h.gps, + updatedAt: typeof h.ts === 'number' && Number.isFinite(h.ts) ? h.ts : null, + }, + }; + }), setCanPhase: (phase, retryCount) => set((s) => ({ canPhase: phase,