Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/DEVICE_VALIDATION_LEDGER.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

| # | Özellik | Nerede | Kabul ölçütü (cihazda ne gözlemlenmeli) | Eklendi |
|---|---------|--------|------------------------------------------|---------|
| 25 | **🔴 Device Validation Pending — Learning Integration Layer (P2-5)** — P2-1→P2-4 öğrenme katmanları (Evidence Store · Decay · Pattern Engine) uygulamanın mevcut ekran/tanı akışlarına SALT-OKUNUR bağlandı. `vehicleLearningIntegrationService` Evidence Store'dan okur → decay uygular → pattern üretir → (a) Discovery Dashboard'a WEAK/CANDIDATE/STRONG/MANUAL_REVIEW/STALE/CONFLICT rozetleri + confidence/decay/araç/gözlem/ECU alanları + Strong/Candidate/Manual/Conflict filtreleri ekler, (b) Expert Mode'a "Araç Öğrenme" özet bölümü (toplam/weak/candidate/strong/manual/stale/prune + güçlü PID/DID adayları + marka cluster + son öğrenme) ekler, (c) Diagnostic Insight için `enrichInsight` ile öğrenme bağlamı üretir. Hiçbir registry/profile/store'a OTOMATİK YAZMAZ; safety/severity/driveSafe DEĞİŞMEZ; memoized + bounded(512) + fail-soft + zero-leak. BASIC_JS(low) tier yalnız özet istatistik (ağır pattern/conflict detayı kapalı). | `vehicleLearningIntegrationService.ts` (yeni), `discoveryDashboardModel.ts` (öğrenme filtreleri + AnnotationResolver), `DiscoveryDashboard.tsx` (rozet/alan/filtre), `settings/expert/VehicleLearningInspector.tsx` (yeni), `ExpertModePanel.tsx` (bölüm wiring) | Gerçek araçta: (1) yeni PID/DID discovery oluşur → Evidence Store kaydı oluşur → Dashboard'da doğru WEAK/CANDIDATE/STRONG rozeti görünür; (2) duplicate gözlem vehicleCount'i şişirmez (distinct fingerprint); (3) Expert "Araç Öğrenme" özeti doğru sayıları gösterir; (4) Diagnostic Insight öğrenme bağlamını (learnedEvidenceCount/relatedStrongPids/requiresManualReview) gösterir; (5) düşük cihaz tier'ında (BASIC_JS) FPS/CPU regresyonu oluşmaz (ağır pattern detayı kapalı). ⚠️ Salt-okunur/additive → alt katman davranışı değişmez (tsc+test+build yeşil). | 2026-07-10 |
| 24 | **🔴 Device Validation Pending — P0-Güvenlik #4: production log hijyeni** (commit 049e290). terser `drop_console:true` (error+warn dahil TÜM console'u siliyor → üretimde hata gözlemlenebilirliği yoktu + logGate ölü kod) yerine dev-aware kesim: release'te yalnız `log/info/debug/trace` çıkarılır, `error`+`warn` KORUNUR (logGate runtime'da adaptif susturur). `NODE_ENV=development` build hiçbir console'ı çıkarmaz. Build-analiz kanıtı: release bundle'da `console.error(`/`warn(` var, `console.log(`/`info(`/`debug(` **çağrı = 0**; dev bundle'da log/info çağrıları korunur. | `vite.config.ts` (`_isDevBuild` + `drop_console: _isDevBuild ? false : ['log','info','debug','trace']`) | **Release APK** cihaz-üstü (debug build ile Chrome inspector): bir hata durumunda `console.error` GÖRÜNÜR (üretim gözlemlenebilirliği geri geldi); debug seviye log GÖRÜNMEZ. Debug APK'da tüm console akar (on-device debug korundu). Hot-path'te debug log kaynaklı ek CPU/IO gözlenmez. | 2026-07-08 |
| 23 | **🔴 Device Validation Pending — P0-Güvenlik #2: cleartext trafik kısıtlama** (commit 77e5292). Global `android:usesCleartextTraffic="true"` kaldırıldı; `network_security_config` base-config ile tüm cleartext reddedilir, yalnız loopback (`localhost`/`127.0.0.1`) whitelist'te. Ölü dev IP `192.168.1.12` çıkarıldı. Ham `ServerSocket`'ler (passenger 8765 / diag 8899) bu politikadan etkilenmez. | `AndroidManifest.xml`, `res/xml/network_security_config.xml` | **Release APK**'da dış **https** API'ler (hava/Nominatim/Supabase/AI) çalışmaya devam eder; kasıtlı bir `http://` dış çağrı NSC ile REDDEDİLİR; cihaz-içi OSRM (`http://localhost:5000`) offline routing + yolcu paneli (ayrı telefonun tarayıcısı) çalışır. | 2026-07-08 |
| 22 | **🔴 Device Validation Pending — P0-Güvenlik #1: teşhis HTTP sunucusu + ADB uçları release'te kapalı** (commit 8786764). `startDiagServer` release'te güvenli no-op; `/enable-adb` (root ADB) + `/root-check` uçları `BuildConfig.DEBUG` guard'ı altında → release'te `DEBUG=false` → R8 blokları **ve** yalnız onlarda kullanılan `runRootShell`'i APK'dan siler. DTCPanel çağrısı `import.meta.env.DEV` ile ikinci katman. BuildConfig üretimi build.gradle'de açıldı (debug=true / release=false doğrulandı). | `build.gradle` (buildConfig), `CarLauncherPlugin.java` (no-op + 2 endpoint guard), `DTCPanel.tsx` (DEV guard) | **Release APK**'da DTC paneli açıkken PC'den `http://<ip>:8899/` → **bağlantı reddi** (soket açılmaz); `strings`/apktool ile `enable-adb` / `service.adb.tcp.port` literal'i APK'da **bulunamaz**. **Debug APK**'da aynı uçlar ÇALIŞIR (saha teşhis workflow'u korundu). | 2026-07-08 |
Expand Down
277 changes: 277 additions & 0 deletions src/__tests__/vehicleLearningIntegrationService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
/**
* vehicleLearningIntegrationService.test.ts — Öğrenme Entegrasyon Katmanı (P2-5).
*
* Kilitler: diagnostic learning context ekleniyor · veri yoksa insight korunuyor ·
* safety/severity/driveSafe DEĞİŞMİYOR · strong PID/DID doğru · manual-review/conflict doğru ·
* dashboard rozet/filtre doğru · Expert summary doğru · stale decay sonrası doğru · BASIC_JS
* ağır detay üretmiyor · BALANCED/HIGH tam detay · memoization · girdi mutate edilmiyor ·
* boş/bozuk fail-soft · dispose.
*/
import { describe, it, expect } from 'vitest';
import {
VehicleLearningIntegrationService,
learningBadgesFor,
type LearningDiscoveryAnnotation,
} from '../platform/vehicleLearningIntegrationService';
import { buildPatterns } from '../platform/vehicleLearningPatternEngine';
import { type LearningEvidence } from '../platform/vehicleLearningEngine';
import { type DiagnosticInsight } from '../platform/diagnosticKnowledgeEngine';
import {
filterObservations,
selectVisible,
isLearningFilter,
type DiscoveryFilter,
} from '../components/discovery/discoveryDashboardModel';

const DAY = 86_400_000;
const NOW = 100 * DAY;

/** Kanıt fabrikası. */
function ev(over: Partial<LearningEvidence> = {}): LearningEvidence {
const manufacturer = over.manufacturer ?? 'Renault';
const protocol = over.protocol ?? '6';
const source = over.discoverySource ?? 'PID';
const pidOrDid = over.pidOrDid ?? 'A5';
const mode = over.mode ?? (source === 'DID' ? '22' : '01');
const hashes = over.supportingVehicleHashes ?? ['v1'];
return {
evidenceId: over.evidenceId ?? `${manufacturer}|${protocol}|${source}|${pidOrDid}|${mode}`,
manufacturer, profileHint: over.profileHint ?? manufacturer, protocol,
discoverySource: source, pidOrDid, mode,
ecuAddresses: over.ecuAddresses ?? ['7E8'],
supportingVehicleHashes: hashes,
vehicleCount: over.vehicleCount ?? hashes.length,
observationCount: over.observationCount ?? 6,
firstSeen: over.firstSeen ?? (NOW - 5 * DAY),
lastSeen: over.lastSeen ?? NOW,
confidence: over.confidence ?? 0.8,
status: over.status ?? 'candidate',
createdAt: over.createdAt ?? (NOW - 5 * DAY),
updatedAt: over.updatedAt ?? NOW,
};
}

function svc(list: LearningEvidence[], tier: 'low' | 'mid' | 'high' = 'high', now = NOW): VehicleLearningIntegrationService {
return new VehicleLearningIntegrationService(() => list, () => now, () => tier);
}

/** Diagnostic Insight fixture (yalnız gerekli alanlar; gerçek şekle uygun). */
function insight(over: Partial<DiagnosticInsight> = {}): DiagnosticInsight {
return {
dtc: 'P0401', description: 'EGR', severity: 'warning', confidence: 0.6,
manufacturer: 'Renault', profileHint: 'Renault',
likelySystems: ['EGR'], relatedPids: over.relatedPids ?? ['A5'], relatedDids: over.relatedDids ?? [],
discoveredOnVehicle: over.discoveredOnVehicle ?? false, manufacturerSeenCount: 0, vehicleSeenCount: 0,
possibleCauses: [], recommendedChecks: [], driveSafe: over.driveSafe ?? true,
firstSeen: 0, lastSeen: 0,
...over,
};
}

/* ── Diagnostic entegrasyonu ─────────────────────────────────────────────── */
describe('diagnostic learning context', () => {
it('learning context insight’a EKLENİYOR (eşleşen evidence var)', () => {
const s = svc([ev({ pidOrDid: 'A5', supportingVehicleHashes: ['v1', 'v2'] })]);
const out = s.enrichInsight(insight({ relatedPids: ['A5'] }));
expect(out.learning).toBeTruthy();
expect(out.learning!.learnedEvidenceCount).toBe(1);
expect(out.learning!.learnedOnManufacturer).toBe(true);
});

it('learning verisi YOKSA insight AYNEN korunuyor (learning undefined)', () => {
const s = svc([ev({ pidOrDid: 'ZZ' })]); // ilgisiz sinyal
const base = insight({ relatedPids: ['A5'] });
const out = s.enrichInsight(base);
expect(out.learning).toBeUndefined();
expect(out).toBe(base); // aynı referans → değişmedi
});

it('safety/severity/driveSafe/confidence learning nedeniyle DEĞİŞMİYOR', () => {
const s = svc([ev({ pidOrDid: 'A5', supportingVehicleHashes: ['v1', 'v2', 'v3'] })]);
const base = insight({ relatedPids: ['A5'], severity: 'critical', driveSafe: false, confidence: 0.42 });
const out = s.enrichInsight(base);
expect(out.severity).toBe('critical');
expect(out.driveSafe).toBe(false);
expect(out.confidence).toBe(0.42);
expect(out.learning).toBeTruthy(); // ek bağlam eklendi ama karar aynı
});

it('strong PID/DID alanları DOĞRU', () => {
const s = svc([
ev({ evidenceId: 'p', discoverySource: 'PID', pidOrDid: 'A5', supportingVehicleHashes: ['v1', 'v2', 'v3'], firstSeen: NOW - 5 * DAY, lastSeen: NOW }),
ev({ evidenceId: 'd', discoverySource: 'DID', pidOrDid: 'F190', supportingVehicleHashes: ['v1', 'v2', 'v3'], firstSeen: NOW - 5 * DAY, lastSeen: NOW }),
]);
const ctx = s.buildDiagnosticLearningContext(insight({ relatedPids: ['A5'], relatedDids: ['F190'] }));
expect(ctx!.relatedStrongPids).toContain('A5');
expect(ctx!.relatedStrongDids).toContain('F190');
});

it('manual-review / conflict uyarıları DOĞRU (aynı PID iki markada)', () => {
const s = svc([
ev({ evidenceId: 'r', manufacturer: 'Renault', pidOrDid: 'A5', supportingVehicleHashes: ['v1', 'v2'] }),
ev({ evidenceId: 't', manufacturer: 'Toyota', pidOrDid: 'A5', supportingVehicleHashes: ['w1', 'w2'] }),
]);
const ctx = s.buildDiagnosticLearningContext(insight({ relatedPids: ['A5'] }));
expect(ctx!.requiresManualReview).toBe(true);
expect(ctx!.learningWarnings.length).toBeGreaterThan(0);
});
});

/* ── Dashboard rozet / filtre ────────────────────────────────────────────── */
describe('dashboard annotation + badges + filters', () => {
it('annotateDiscovery doğru statü/decay döndürüyor', () => {
const s = svc([ev({ pidOrDid: 'A5', supportingVehicleHashes: ['v1', 'v2', 'v3'], status: 'strong' })]);
const ann = s.annotateDiscovery('PID', 'A5');
expect(ann).toBeTruthy();
expect(ann!.evidenceStatus).toBe('strong');
expect(ann!.decayedConfidence).toBeGreaterThan(0);
});

it('learningBadgesFor doğru rozetleri üretiyor', () => {
const ann: LearningDiscoveryAnnotation = {
pidOrDid: 'A5', discoverySource: 'PID', evidenceStatus: 'strong', patternStatus: 'strong',
confidence: 0.8, decayedConfidence: 0.75, vehicleCount: 3, observationCount: 6, ecuCount: 1,
firstSeen: 0, lastSeen: NOW, stale: false, requiresManualReview: true,
conflictReasons: ['CROSS_MANUFACTURER_MEANING'],
};
const badges = learningBadgesFor(ann);
expect(badges).toContain('STRONG');
expect(badges).toContain('CONFLICT');
expect(badges).toContain('MANUAL_REVIEW');
expect(learningBadgesFor(null)).toEqual([]);
});

it('dashboard öğrenme filtreleri annotation ile DOĞRU süzüyor', () => {
// Sahte gözlemler (yalnız record.discoverySource + pidOrDid önemli)
const obs = [
{ record: { discoverySource: 'PID', pidOrDid: 'A5' }, status: 'new', seenCount: 1, firstAt: 0, lastAt: 0 },
{ record: { discoverySource: 'PID', pidOrDid: 'B2' }, status: 'new', seenCount: 1, firstAt: 0, lastAt: 0 },
] as unknown as Parameters<typeof filterObservations>[0];
const annMap = new Map<string, LearningDiscoveryAnnotation>([
['PID:A5', { pidOrDid: 'A5', discoverySource: 'PID', evidenceStatus: 'strong', patternStatus: 'strong', confidence: 0.8, decayedConfidence: 0.7, vehicleCount: 3, observationCount: 6, ecuCount: 1, firstSeen: 0, lastSeen: 0, stale: false, requiresManualReview: false, conflictReasons: [] }],
['PID:B2', { pidOrDid: 'B2', discoverySource: 'PID', evidenceStatus: 'weak', patternStatus: 'weak', confidence: 0.3, decayedConfidence: 0.3, vehicleCount: 1, observationCount: 1, ecuCount: 1, firstSeen: 0, lastSeen: 0, stale: false, requiresManualReview: true, conflictReasons: ['CROSS_MANUFACTURER_MEANING'] }],
]);
const resolver = (o: { record: { discoverySource: string; pidOrDid: string } }) => annMap.get(`${o.record.discoverySource}:${o.record.pidOrDid}`) ?? null;

expect(filterObservations(obs, 'strong', resolver)).toHaveLength(1);
expect(filterObservations(obs, 'manual', resolver)).toHaveLength(1);
expect(filterObservations(obs, 'conflict', resolver)).toHaveLength(1);
// resolver YOKSA fail-soft tümü (mevcut davranış korunur)
expect(filterObservations(obs, 'strong')).toHaveLength(2);
// mevcut filtreler bozulmadı
expect(filterObservations(obs, 'pid')).toHaveLength(2);
expect(selectVisible(obs, 'strong', '', resolver)).toHaveLength(1);
expect(isLearningFilter('strong')).toBe(true);
expect(isLearningFilter('pid' as DiscoveryFilter)).toBe(false);
});
});

/* ── Expert summary ──────────────────────────────────────────────────────── */
describe('expert summary', () => {
it('sayımlar DOĞRU (weak/candidate/strong + son öğrenme)', () => {
const s = svc([
ev({ evidenceId: 'a', status: 'weak', pidOrDid: 'A1' }),
ev({ evidenceId: 'b', status: 'candidate', pidOrDid: 'A2' }),
ev({ evidenceId: 'c', status: 'strong', pidOrDid: 'A3', supportingVehicleHashes: ['v1', 'v2', 'v3'] }),
]);
const sum = s.getExpertSummary();
expect(sum.totalEvidence).toBe(3);
expect(sum.weakCount).toBe(1);
expect(sum.candidateCount).toBe(1);
expect(sum.strongCount).toBe(1);
expect(sum.lastLearnedAt).toBe(NOW);
expect(sum.manufacturerClusters.length).toBeGreaterThanOrEqual(1);
});

it('decay sonrası STALE durumu doğru (çok eski kanıt)', () => {
const s = svc([ev({ status: 'weak', confidence: 0.3, lastSeen: 0, firstSeen: 0 })], 'high', 3000 * DAY);
const sum = s.getExpertSummary();
expect(sum.staleCount).toBe(1);
expect(sum.pruneCandidateCount).toBe(1);
});
});

/* ── DeviceTier ──────────────────────────────────────────────────────────── */
describe('DeviceTier davranışı', () => {
const conflicting = () => [
ev({ evidenceId: 'r', manufacturer: 'Renault', pidOrDid: 'A5', supportingVehicleHashes: ['v1', 'v2', 'v3'] }),
ev({ evidenceId: 't', manufacturer: 'Toyota', pidOrDid: 'A5', supportingVehicleHashes: ['w1', 'w2', 'w3'] }),
];

it('BASIC_JS (low) AĞIR detay ÜRETMİYOR (manual/conflict=0, patternDetail kapalı)', () => {
const sum = svc(conflicting(), 'low').getExpertSummary();
expect(sum.patternDetailEnabled).toBe(false);
expect(sum.manualReviewCount).toBe(0);
expect(sum.conflictCount).toBe(0);
// annotation'da conflict/manual detayı da yok
const ann = svc(conflicting(), 'low').annotateDiscovery('PID', 'A5');
expect(ann!.conflictReasons).toEqual([]);
expect(ann!.requiresManualReview).toBe(false);
});

it('BALANCED/HIGH TAM detay üretiyor (conflict işaretli)', () => {
const sum = svc(conflicting(), 'high').getExpertSummary();
expect(sum.patternDetailEnabled).toBe(true);
expect(sum.conflictCount).toBeGreaterThan(0);
const ann = svc(conflicting(), 'high').annotateDiscovery('PID', 'A5');
expect(ann!.conflictReasons.length).toBeGreaterThan(0);
expect(ann!.requiresManualReview).toBe(true);
});
});

/* ── Memoization / immutability / fail-soft / dispose ────────────────────── */
describe('memoization + immutability + fail-soft + dispose', () => {
it('memoization: aynı veri → ağır pattern hesabı TEK KEZ (buildPatterns memoize)', () => {
let builds = 0;
const list = [ev({ pidOrDid: 'A5', supportingVehicleHashes: ['v1', 'v2'] })];
const s = new VehicleLearningIntegrationService(
() => list, () => NOW, () => 'high',
(evList, opts) => { builds++; return buildPatterns(evList, opts); },
);
s.getExpertSummary();
s.getExpertSummary();
s.annotateDiscovery('PID', 'A5');
expect(builds).toBe(1); // memo → ağır adım tek kez
s.invalidate();
s.getExpertSummary();
expect(builds).toBe(2); // invalidate sonrası taze hesap
});

it('girdi evidence MUTATE EDİLMİYOR', () => {
const list = [ev({ pidOrDid: 'A5', supportingVehicleHashes: ['v1', 'v2'], ecuAddresses: ['7E8'] })];
const snap = JSON.parse(JSON.stringify(list));
const s = svc(list);
s.getExpertSummary();
s.annotateDiscovery('PID', 'A5');
s.buildDiagnosticLearningContext(insight({ relatedPids: ['A5'] }));
expect(list).toEqual(snap);
});

it('boş / bozuk veri FAIL-SOFT', () => {
const empty = svc([]);
expect(empty.getExpertSummary().totalEvidence).toBe(0);
expect(empty.annotateDiscovery('PID', 'A5')).toBeNull();
expect(empty.buildDiagnosticLearningContext(insight())).toBeNull();

const broken = new VehicleLearningIntegrationService(
() => ([null, { evidenceId: '' }, ev({ pidOrDid: 'A5' })] as unknown as LearningEvidence[]),
() => NOW, () => 'high',
);
expect(() => broken.getExpertSummary()).not.toThrow();
expect(broken.getExpertSummary().totalEvidence).toBe(3); // bozuklar sayıda ama fail-soft

// reader throw → fail-soft boş
const throwing = new VehicleLearningIntegrationService(() => { throw new Error('x'); }, () => NOW, () => 'high');
expect(throwing.getExpertSummary().totalEvidence).toBe(0);
expect(throwing.annotateDiscovery('PID', 'A5')).toBeNull();
});

it('enrichInsight null insight’ta çökmüyor + dispose temizliyor', () => {
const s = svc([ev({ pidOrDid: 'A5', supportingVehicleHashes: ['v1', 'v2'] })]);
expect(() => s.enrichInsight(null as unknown as DiagnosticInsight)).not.toThrow();
s.getExpertSummary();
expect(() => s.dispose()).not.toThrow();
// dispose sonrası tekrar çalışır (fail-soft)
expect(s.getExpertSummary().totalEvidence).toBe(1);
});
});
Loading
Loading