From 277e33da75d5804210bdcfb2b3a332c33c53fa53 Mon Sep 17 00:00:00 2001 From: mrambossek Date: Sat, 1 Aug 2026 10:54:48 +0200 Subject: [PATCH] chore: drop core-measurement/bin from the index too Co-Authored-By: Claude Fable 5 --- .../main/app/echo_lot/measurement/Document.kt | 100 ------------ .../main/app/echo_lot/measurement/Evidence.kt | 85 ---------- .../main/app/echo_lot/measurement/Finding.kt | 68 -------- .../main/app/echo_lot/measurement/Network.kt | 113 ------------- .../main/app/echo_lot/measurement/Summary.kt | 90 ----------- .../bin/main/app/echo_lot/measurement/Test.kt | 151 ------------------ .../echo_lot/measurement/SerializationTest.kt | 71 -------- .../app/echo_lot/measurement/VerdictsTest.kt | 109 ------------- 8 files changed, 787 deletions(-) delete mode 100644 echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Document.kt delete mode 100644 echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Evidence.kt delete mode 100644 echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Finding.kt delete mode 100644 echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Network.kt delete mode 100644 echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Summary.kt delete mode 100644 echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Test.kt delete mode 100644 echolot-app/core-measurement/bin/test/app/echo_lot/measurement/SerializationTest.kt delete mode 100644 echolot-app/core-measurement/bin/test/app/echo_lot/measurement/VerdictsTest.kt diff --git a/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Document.kt b/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Document.kt deleted file mode 100644 index c482607..0000000 --- a/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Document.kt +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Echolot contributors -// SPDX-License-Identifier: GPL-3.0-or-later - -// Package measurement models one measurement run (measurement-schema.md) — the archived, -// diffable, exportable unit. Design rules honored in the types: observation/interpretation -// separated (tests[] vs findings[]), two clocks (wall RFC3339 for humans, *_mono_ns for math), -// units in field names, columnar trains. params/evidence/metrics are per-test-type, so they are -// carried as JsonObject (the probe engine fills them; consumers ignore unknown fields). -package app.echo_lot.measurement - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObject - -@Serializable -data class MeasurementDocument( - val schema: String = "echolot/measurement", - @SerialName("schema_version") val schemaVersion: String = "1.0.0", - val run: Run, - val networks: List = emptyList(), - @SerialName("server_sessions") val serverSessions: List = emptyList(), - val tests: List = emptyList(), - val findings: List = emptyList(), - val summary: Summary? = null, -) - -@Serializable -data class Run( - val id: String, // UUIDv7 - val trigger: Trigger, - @SerialName("started_at") val startedAt: String, // RFC3339 UTC, human correlation only - @SerialName("ended_at") val endedAt: String? = null, - val clock: Clock, - val app: AppInfo, - val device: DeviceInfo, - val tiers: Tiers, - @SerialName("profiles_used") val profilesUsed: List = emptyList(), - val notes: String? = null, -) - -@Serializable -enum class Trigger { - @SerialName("manual") MANUAL, - @SerialName("scheduled") SCHEDULED, - @SerialName("monitor") MONITOR, - @SerialName("peer") PEER, -} - -/** The two-clock anchor: mono_origin_wall maps the monotonic epoch to a wall time for humans; - * all math uses *_mono_ns relative to that monotonic origin. */ -@Serializable -data class Clock( - @SerialName("mono_origin_wall") val monoOriginWall: String, - @SerialName("ntp_offset_ms") val ntpOffsetMs: Double? = null, - @SerialName("ntp_offset_source") val ntpOffsetSource: String? = null, -) - -@Serializable -data class AppInfo( - val version: String, - val build: Int, - val git: String? = null, - val flavor: String? = null, -) - -@Serializable -data class DeviceInfo( - val manufacturer: String, - val model: String, - @SerialName("android_sdk") val androidSdk: Int, - @SerialName("android_release") val androidRelease: String, - @SerialName("security_patch") val securityPatch: String? = null, -) - -/** What each tier was *available*; each test records what it *used*. */ -@Serializable -data class Tiers( - val app: Boolean = true, - val shizuku: Boolean = false, - val root: Boolean = false, -) - -@Serializable -data class ServerSession( - val id: String, - @SerialName("profile_id") val profileId: String? = null, - @SerialName("profile_name") val profileName: String? = null, - @SerialName("control_url") val controlUrl: String, - @SerialName("server_version") val serverVersion: String? = null, - val capabilities: List = emptyList(), - @SerialName("session_id") val sessionId: String, - val target: SessionTarget, -) - -@Serializable -data class SessionTarget( - val ip4: String? = null, - val ip6: String? = null, - @SerialName("udp_port") val udpPort: Int = 0, -) diff --git a/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Evidence.kt b/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Evidence.kt deleted file mode 100644 index 2580453..0000000 --- a/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Evidence.kt +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Echolot contributors -// SPDX-License-Identifier: GPL-3.0-or-later - -package app.echo_lot.measurement - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.encodeToJsonElement - -/** - * Typed builders for the per-test-type evidence shapes the schema fixes (§6.2/§6.3/§6.4). Probe - * code fills these and folds them into [Test.evidence] via [toEvidence]; keeping them typed here - * means the columnar/traceroute/resolver contracts live in one place. - */ - -@PublishedApi -internal val evidenceJson = Json { encodeDefaults = true; explicitNulls = true } - -/** Serialize any typed evidence object into the JsonObject the Test envelope carries. */ -inline fun T.toEvidence(): JsonObject = - evidenceJson.encodeToJsonElement(this) as JsonObject - -/** - * Packet-train evidence (§6.2): columnar parallel arrays, one index per probe packet. Missing - * observations are null at that index — a 10k-packet train stays in the hundreds of kB. Server - * columns use the server session epoch; only differences within one clock are meaningful unless a - * time.server_offset test maps them. - */ -@Serializable -data class TrainEvidence( - @SerialName("epoch_mono_ns") val epochMonoNs: Long, - val seq: List, - @SerialName("t_tx_ns") val tTxNs: List, - @SerialName("t_srv_rx_ns") val tSrvRxNs: List = emptyList(), - @SerialName("t_srv_tx_ns") val tSrvTxNs: List = emptyList(), - @SerialName("t_rx_ns") val tRxNs: List, - @SerialName("size_bytes") val sizeBytes: List, - @SerialName("dscp_sent") val dscpSent: Int? = null, - @SerialName("dscp_seen_by_server") val dscpSeenByServer: List = emptyList(), - @SerialName("ecn_sent") val ecnSent: Int? = null, - @SerialName("ecn_seen_by_server") val ecnSeenByServer: List = emptyList(), - @SerialName("ttl_seen_by_server") val ttlSeenByServer: List = emptyList(), - @SerialName("evidence_truncated") val evidenceTruncated: Boolean = false, -) - -/** Traceroute evidence (§6.3): fixed-tuple flow + per-TTL probe replies. */ -@Serializable -data class TracerouteEvidence(val flow: Flow, val hops: List) - -@Serializable -data class Flow( - @SerialName("src_port") val srcPort: Int, - @SerialName("dst_port") val dstPort: Int, - @SerialName("fixed_tuple") val fixedTuple: Boolean = true, -) - -@Serializable -data class Hop(val ttl: Int, val probes: List) - -@Serializable -data class HopProbe( - @SerialName("reply_from") val replyFrom: String? = null, - @SerialName("rtt_ns") val rttNs: Long? = null, - val icmp: String? = null, - @SerialName("reply_ttl") val replyTtl: Int? = null, -) - -/** Resolver under test (§6.4); every dns.* test carries this in params. */ -@Serializable -data class ResolverSpec( - val source: ResolverSource, - val address: String? = null, - val port: Int = 53, - val transport: String, // do53-udp | do53-tcp | dot | doh - @SerialName("doh_url") val dohUrl: String? = null, -) - -@Serializable -enum class ResolverSource { - @SerialName("system") SYSTEM, - @SerialName("manual") MANUAL, - @SerialName("server-recursive") SERVER_RECURSIVE, -} diff --git a/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Finding.kt b/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Finding.kt deleted file mode 100644 index 5020313..0000000 --- a/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Finding.kt +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Echolot contributors -// SPDX-License-Identifier: GPL-3.0-or-later - -package app.echo_lot.measurement - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** Interpretation with references back to evidence (measurement-schema.md §7.1). A finding with - * no evidence_refs is invalid — every finding must be re-derivable from the evidence alone. */ -@Serializable -data class Finding( - val id: String, // UUIDv7 - val code: String, // stable registry (findings-registry.md), lint-rule style - val category: Category, - val severity: Severity, - val confidence: Confidence, - @SerialName("network_ref") val networkRef: String? = null, - val title: String, - val description: String, - @SerialName("evidence_refs") val evidenceRefs: List, - val recommendation: String? = null, -) { - init { - require(evidenceRefs.isNotEmpty()) { "a finding must reference at least one piece of evidence" } - } -} - -@Serializable -data class EvidenceRef(val test: String, val pointer: String? = null) - -/** Fixed §7.2 categories; each maps to one traffic light. */ -@Serializable -enum class Category { - @SerialName("connectivity") CONNECTIVITY, - @SerialName("dns") DNS, - @SerialName("nat") NAT, - @SerialName("mtu") MTU, - @SerialName("ipv6") IPV6, - @SerialName("security") SECURITY, - @SerialName("performance") PERFORMANCE, - @SerialName("local") LOCAL, - @SerialName("wifi") WIFI, -} - -/** Ordered worst→best via [rank]; drives the §7.3 light mapping. */ -@Serializable -enum class Severity(val rank: Int) { - @SerialName("critical") CRITICAL(4), - @SerialName("high") HIGH(3), - @SerialName("medium") MEDIUM(2), - @SerialName("low") LOW(1), - @SerialName("info") INFO(0); - - /** §7.3: critical|high → red, medium|low → yellow, info → green. */ - fun toLight(): Verdict = when (this) { - CRITICAL, HIGH -> Verdict.RED - MEDIUM, LOW -> Verdict.YELLOW - INFO -> Verdict.GREEN - } -} - -@Serializable -enum class Confidence { - @SerialName("high") HIGH, - @SerialName("medium") MEDIUM, - @SerialName("low") LOW, -} diff --git a/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Network.kt b/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Network.kt deleted file mode 100644 index a432eb0..0000000 --- a/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Network.kt +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Echolot contributors -// SPDX-License-Identifier: GPL-3.0-or-later - -package app.echo_lot.measurement - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObject - -/** One Android Network in play (measurement-schema.md §4). Shizuku-tier fields (route proto, - * lifetimes) are absent at app tier — absence means "not observed", never "not present". */ -@Serializable -data class Network( - val id: String, - val transport: Transport, - @SerialName("interface") val iface: String? = null, - val link: Link, - val wifi: Wifi? = null, - val cellular: Cellular? = null, - val changes: List = emptyList(), -) - -@Serializable -enum class Transport { - @SerialName("wifi") WIFI, - @SerialName("cellular") CELLULAR, - @SerialName("ethernet") ETHERNET, - @SerialName("vpn") VPN, - @SerialName("other") OTHER, -} - -@Serializable -data class Link( - val mtu: Int? = null, - val addresses: List
= emptyList(), - val routes: List = emptyList(), - val dns: DnsConfig? = null, - val dhcp: Dhcp? = null, - @SerialName("captive_portal") val captivePortal: CaptivePortal? = null, -) - -@Serializable -data class Address( - val addr: String, // ip4 | ip6 (logical type, §8) - @SerialName("prefix_len") val prefixLen: Int, - val scope: String? = null, - val flags: List = emptyList(), - @SerialName("valid_lft_s") val validLftS: Long? = null, - @SerialName("pref_lft_s") val prefLftS: Long? = null, -) - -@Serializable -data class Route( - val dst: String, - val gateway: String? = null, - val iface: String? = null, - val proto: RouteProto? = null, // shizuku tier; null = not observed - @SerialName("expires_s") val expiresS: Long? = null, -) - -@Serializable -enum class RouteProto { - @SerialName("dhcp") DHCP, - @SerialName("ra") RA, - @SerialName("static") STATIC, - @SerialName("unknown") UNKNOWN, -} - -@Serializable -data class DnsConfig( - val servers: List = emptyList(), - @SerialName("private_dns_mode") val privateDnsMode: String? = null, - @SerialName("private_dns_hostname") val privateDnsHostname: String? = null, - @SerialName("search_domains") val searchDomains: List = emptyList(), - @SerialName("nat64_prefix") val nat64Prefix: String? = null, -) - -@Serializable -data class Dhcp(val server: String? = null, @SerialName("lease_s") val leaseS: Long? = null) - -@Serializable -data class CaptivePortal( - val detected: Boolean = false, - @SerialName("api_url") val apiUrl: String? = null, - @SerialName("venue_url") val venueUrl: String? = null, -) - -@Serializable -data class Wifi( - val ssid: String? = null, // ssid (logical type) - val bssid: String? = null, // bssid (logical type) - @SerialName("rssi_dbm") val rssiDbm: Int? = null, - @SerialName("link_speed_mbps") val linkSpeedMbps: Int? = null, - @SerialName("frequency_mhz") val frequencyMhz: Int? = null, - @SerialName("channel_width_mhz") val channelWidthMhz: Int? = null, - val standard: String? = null, - val security: String? = null, - @SerialName("mac_randomization") val macRandomization: Boolean? = null, -) - -@Serializable -data class Cellular( - val rat: String? = null, - val operator: String? = null, - val band: String? = null, -) - -@Serializable -data class NetworkChange( - @SerialName("at_mono_ns") val atMonoNs: Long, - val kind: String, // lost | gained | link_changed - val detail: JsonObject? = null, -) diff --git a/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Summary.kt b/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Summary.kt deleted file mode 100644 index 9cf23c3..0000000 --- a/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Summary.kt +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Echolot contributors -// SPDX-License-Identifier: GPL-3.0-or-later - -package app.echo_lot.measurement - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -enum class Verdict { - @SerialName("green") GREEN, - @SerialName("yellow") YELLOW, - @SerialName("red") RED, - @SerialName("inconclusive") INCONCLUSIVE, -} - -@Serializable -data class Summary( - val overall: Verdict, - val categories: Map, -) - -@Serializable -data class CategorySummary( - val verdict: Verdict, - @SerialName("worst_finding") val worstFinding: String? = null, - @SerialName("tests_run") val testsRun: Int, - @SerialName("tests_failed") val testsFailed: Int, -) - -/** - * Deterministic verdict derivation, fixed by measurement-schema.md §7.3: - * - * - A category's verdict = the light of its worst-severity finding - * (critical|high → red, medium|low → yellow, info/none → green). - * - A category is `inconclusive` when > 50% of its tests are failed/unsupported. - * - Overall = the worst category light; `inconclusive` only when ALL categories are. - * - * The mapping test-type → category comes from [TestType.category]. Only categories that have - * findings or tests appear in the summary. - */ -object Verdicts { - - private fun isInconclusiveTest(s: TestStatus) = - s == TestStatus.FAILED || s == TestStatus.UNSUPPORTED - - fun derive(tests: List, findings: List): Summary { - val testsByCat = tests.groupBy { TestType.category(it.type) } - val findingsByCat = findings.groupBy { it.category } - val categories = (testsByCat.keys + findingsByCat.keys) - - val perCat = LinkedHashMap() - for (cat in Category.entries) { - if (cat !in categories) continue - val catTests = testsByCat[cat].orEmpty() - val catFindings = findingsByCat[cat].orEmpty() - - val failed = catTests.count { isInconclusiveTest(it.status) } - val inconclusive = catTests.isNotEmpty() && failed * 2 > catTests.size - - val worst = catFindings.maxByOrNull { it.severity.rank } - val verdict = when { - inconclusive -> Verdict.INCONCLUSIVE - worst == null -> Verdict.GREEN - else -> worst.severity.toLight() - } - perCat[serialName(cat)] = CategorySummary( - verdict = verdict, - worstFinding = worst?.id, - testsRun = catTests.size, - testsFailed = failed, - ) - } - - val overall = deriveOverall(perCat.values) - return Summary(overall = overall, categories = perCat) - } - - /** Overall = worst light; inconclusive only if every category is inconclusive. */ - private fun deriveOverall(cats: Collection): Verdict { - if (cats.isEmpty()) return Verdict.INCONCLUSIVE - if (cats.all { it.verdict == Verdict.INCONCLUSIVE }) return Verdict.INCONCLUSIVE - val rank = mapOf(Verdict.GREEN to 0, Verdict.YELLOW to 1, Verdict.RED to 2) - // Non-inconclusive categories decide the overall light. - return cats.filter { it.verdict != Verdict.INCONCLUSIVE } - .maxByOrNull { rank.getValue(it.verdict) }!!.verdict - } - - private fun serialName(cat: Category): String = cat.name.lowercase() -} diff --git a/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Test.kt b/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Test.kt deleted file mode 100644 index 3393dcd..0000000 --- a/echolot-app/core-measurement/bin/main/app/echo_lot/measurement/Test.kt +++ /dev/null @@ -1,151 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Echolot contributors -// SPDX-License-Identifier: GPL-3.0-or-later - -package app.echo_lot.measurement - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObject - -/** The generic test envelope (measurement-schema.md §6). params must fully reproduce the test; - * evidence is append-only raw truth; metrics must be recomputable from evidence. All three are - * per-test-type JSON, so they are carried as JsonObject. */ -@Serializable -data class Test( - val id: String, // UUIDv7 - val type: String, // TestType registry (§6.1) - @SerialName("network_ref") val networkRef: String? = null, - @SerialName("session_ref") val sessionRef: String? = null, // null for local-only tests - val tier: Tier, - @SerialName("started_mono_ns") val startedMonoNs: Long, - @SerialName("ended_mono_ns") val endedMonoNs: Long, - val status: TestStatus, - val error: TestError? = null, - val params: JsonObject? = null, - val evidence: JsonObject? = null, - val metrics: JsonObject? = null, -) - -@Serializable -enum class Tier { - @SerialName("app") APP, - @SerialName("shizuku") SHIZUKU, - @SerialName("root") ROOT, -} - -@Serializable -enum class TestStatus { - @SerialName("ok") OK, - @SerialName("failed") FAILED, - @SerialName("unsupported") UNSUPPORTED, - @SerialName("skipped") SKIPPED, - @SerialName("partial") PARTIAL, -} - -@Serializable -data class TestError(val code: String, val detail: String? = null) - -/** - * The v1 test-type registry (§6.1). String constants (dotted, family-first) so probe code and the - * server's measurement-schema test-type registry stay aligned. [category] maps a type to one of - * the fixed §7.2 categories for verdict rollup. - */ -object TestType { - // link - const val LINK_SNAPSHOT = "link.snapshot" - const val LINK_DHCP_RENEWAL_WATCH = "link.dhcp_renewal_watch" - const val LINK_IP_MONITOR = "link.ip_monitor" - /** Who advertises IPv6 on this link (+ gateway identity). Registry addition, v1.1. */ - const val LINK_RA_SOURCE = "link.ra_source" - // net — connectivity validation (reproduces Android's NetworkMonitor generate_204 checks) - const val NET_CAPTIVE_PORTAL = "net.captive_portal" - // icmp - const val ICMP_PING4 = "icmp.ping4" - const val ICMP_PING6 = "icmp.ping6" - // trace - const val TRACEROUTE_UDP4 = "traceroute.udp4" - const val TRACEROUTE_UDP6 = "traceroute.udp6" - const val TRACEROUTE_ICMP4 = "traceroute.icmp4" - const val TRACEROUTE_ICMP6 = "traceroute.icmp6" - // train - const val TRAIN_UDP_UPDOWN = "train.udp_updown" - /** Server-to-client train under a §3.4 grant: the direction a round trip cannot separate. */ - const val TRAIN_UDP_DOWNSTREAM = "train.udp_downstream" - // mtu - const val MTU_PMTUD_UP = "mtu.pmtud_up" - const val MTU_PMTUD_DOWN = "mtu.pmtud_down" - const val MTU_BLACKHOLE = "mtu.blackhole" - const val MTU_MSS_OBSERVED = "mtu.mss_observed" - const val MTU_FRAG_DELIVERY = "mtu.frag_delivery" - // nat - const val NAT_STUN_5780 = "nat.stun_5780" - const val NAT_MAPPING_LIFETIME_UDP = "nat.mapping_lifetime_udp" - const val NAT_MAPPING_LIFETIME_TCP = "nat.mapping_lifetime_tcp" - const val NAT_HAIRPIN = "nat.hairpin" - const val NAT_CONNECT_BACK = "nat.connect_back" - const val NAT_CGNAT_DETECT = "nat.cgnat_detect" - // dns - const val DNS_RESOLVER_INVENTORY = "dns.resolver_inventory" - const val DNS_CANARY = "dns.canary" - const val DNS_INTERCEPTION = "dns.interception" - const val DNS_TTL_INTEGRITY = "dns.ttl_integrity" - const val DNS_ANSWER_INTEGRITY = "dns.answer_integrity" - const val DNS_DNSSEC = "dns.dnssec" - const val DNS_NXDOMAIN_WILDCARD = "dns.nxdomain_wildcard" - const val DNS_REBIND_FILTER = "dns.rebind_filter" - const val DNS_AAAA_FILTER = "dns.aaaa_filter" - const val DNS_DNS64 = "dns.dns64" - const val DNS_COMPARE = "dns.compare" - // sec - const val SEC_TLS_REFERENCE = "sec.tls_reference" - const val SEC_CLIENTHELLO_ECHO = "sec.clienthello_echo" - const val SEC_HTTP_ECHO = "sec.http_echo" - const val SEC_SNI_FILTER = "sec.sni_filter" - const val SEC_DSCP_ECN_SURVIVAL = "sec.dscp_ecn_survival" - const val SEC_ARP_WATCH = "sec.arp_watch" - // port - const val PORT_REACH_SWEEP = "port.reach_sweep" - const val PORT_UDP_USABILITY = "port.udp_usability" - // perf - const val PERF_THROUGHPUT_TCP = "perf.throughput_tcp" - const val PERF_THROUGHPUT_UDP = "perf.throughput_udp" - const val PERF_BUFFERBLOAT = "perf.bufferbloat" - const val PERF_RRC_LATENCY = "perf.rrc_latency" - // v6 - const val V6_DUALSTACK_COMPARE = "v6.dualstack_compare" - const val V6_HAPPY_EYEBALLS = "v6.happy_eyeballs" - const val V6_BROKENNESS = "v6.brokenness" - const val V6_NAT64_CLAT = "v6.nat64_clat" - // wifi - const val WIFI_ENVIRONMENT_SCAN = "wifi.environment_scan" - const val WIFI_ROAM_LOG = "wifi.roam_log" - const val WIFI_SIGNAL_LOG = "wifi.signal_log" - // local - const val LOCAL_MDNS_INVENTORY = "local.mdns_inventory" - const val LOCAL_SSDP_INVENTORY = "local.ssdp_inventory" - const val LOCAL_LLMNR_INVENTORY = "local.llmnr_inventory" - const val LOCAL_GATEWAY_SERVICES = "local.gateway_services" - const val LOCAL_NTP = "local.ntp" - // peer - const val PEER_REACHABILITY = "peer.reachability" - const val PEER_ISOLATION = "peer.isolation" - const val PEER_MULTICAST = "peer.multicast" - const val PEER_LAN_TRAIN = "peer.lan_train" - const val PEER_LEASE_DIFF = "peer.lease_diff" - // time - const val TIME_SERVER_OFFSET = "time.server_offset" - - /** Maps a dotted test type to its §7.2 category for verdict rollup. */ - fun category(type: String): Category = when (type.substringBefore('.')) { - "link", "icmp", "trace", "traceroute", "train", "port", "time", "net" -> Category.CONNECTIVITY - "dns" -> Category.DNS - "nat" -> Category.NAT - "mtu" -> Category.MTU - "v6" -> Category.IPV6 - "sec" -> Category.SECURITY - "perf" -> Category.PERFORMANCE - "local", "peer" -> Category.LOCAL - "wifi" -> Category.WIFI - else -> Category.CONNECTIVITY - } -} diff --git a/echolot-app/core-measurement/bin/test/app/echo_lot/measurement/SerializationTest.kt b/echolot-app/core-measurement/bin/test/app/echo_lot/measurement/SerializationTest.kt deleted file mode 100644 index 6a23f0b..0000000 --- a/echolot-app/core-measurement/bin/test/app/echo_lot/measurement/SerializationTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Echolot contributors -// SPDX-License-Identifier: GPL-3.0-or-later - -package app.echo_lot.measurement - -import kotlinx.serialization.json.Json -import kotlin.test.Test as JTest -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class SerializationTest { - - private val json = Json { ignoreUnknownKeys = true; encodeDefaults = true } - - @JTest - fun documentRoundTrips() { - val doc = MeasurementDocument( - run = Run( - id = "0198c5f2-0000-7000-8000-000000000000", - trigger = Trigger.MANUAL, - startedAt = "2026-07-31T14:03:21.114Z", - clock = Clock(monoOriginWall = "2026-07-31T14:03:21.114Z"), - app = AppInfo(version = "0.1.0", build = 1), - device = DeviceInfo("OnePlus", "CPH2747", 36, "16"), - tiers = Tiers(app = true, shizuku = true), - ), - networks = listOf( - Network( - id = "net-1", transport = Transport.WIFI, iface = "wlan0", - link = Link(mtu = 1500, addresses = listOf(Address("192.0.2.23", 24, "global"))), - wifi = Wifi(ssid = "example", rssiDbm = -54), - ), - ), - tests = listOf( - Test( - id = "t-1", type = TestType.ICMP_PING4, networkRef = "net-1", tier = Tier.APP, - startedMonoNs = 0, endedMonoNs = 38_000_000, status = TestStatus.OK, - evidence = TrainEvidence( - epochMonoNs = 0, seq = listOf(0, 1), tTxNs = listOf(0L, 20_000_000L), - tRxNs = listOf(16_500_000L, null), sizeBytes = listOf(64, 64), - ).toEvidence(), - ), - ), - ) - - val encoded = json.encodeToString(MeasurementDocument.serializer(), doc) - val decoded = json.decodeFromString(MeasurementDocument.serializer(), encoded) - assertEquals(doc.run.id, decoded.run.id) - assertEquals(Transport.WIFI, decoded.networks[0].transport) - assertEquals(TestType.ICMP_PING4, decoded.tests[0].type) - // snake_case field names on the wire - assertTrue(encoded.contains("\"schema_version\"")) - assertTrue(encoded.contains("\"mono_origin_wall\"")) - assertTrue(encoded.contains("\"t_tx_ns\"")) - // null preserved at train index 1 - assertTrue(encoded.contains("[16500000,null]")) - } - - @JTest - fun findingRequiresEvidence() { - try { - Finding( - id = "f-1", code = "x", category = Category.DNS, severity = Severity.INFO, - confidence = Confidence.LOW, title = "t", description = "d", evidenceRefs = emptyList(), - ) - throw AssertionError("expected IllegalArgumentException for empty evidence_refs") - } catch (e: IllegalArgumentException) { - // expected — a finding with no evidence is invalid (§7.1) - } - } -} diff --git a/echolot-app/core-measurement/bin/test/app/echo_lot/measurement/VerdictsTest.kt b/echolot-app/core-measurement/bin/test/app/echo_lot/measurement/VerdictsTest.kt deleted file mode 100644 index 9314680..0000000 --- a/echolot-app/core-measurement/bin/test/app/echo_lot/measurement/VerdictsTest.kt +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Echolot contributors -// SPDX-License-Identifier: GPL-3.0-or-later - -package app.echo_lot.measurement - -import kotlin.test.Test as JTest -import kotlin.test.assertEquals - -class VerdictsTest { - - private fun test(type: String, status: TestStatus, id: String = type): Test = - Test(id = id, type = type, tier = Tier.APP, startedMonoNs = 0, endedMonoNs = 1, status = status) - - private fun finding(cat: Category, sev: Severity, id: String = "f-$cat-$sev"): Finding = - Finding( - id = id, code = "x.$cat", category = cat, severity = sev, confidence = Confidence.HIGH, - title = "t", description = "d", evidenceRefs = listOf(EvidenceRef("some-test")), - ) - - @JTest - fun categoryLightFromWorstSeverity() { - val tests = listOf(test(TestType.DNS_CANARY, TestStatus.OK)) - val findings = listOf( - finding(Category.DNS, Severity.LOW), - finding(Category.DNS, Severity.HIGH), // worst → red - finding(Category.DNS, Severity.INFO), - ) - val s = Verdicts.derive(tests, findings) - assertEquals(Verdict.RED, s.categories["dns"]!!.verdict) - assertEquals("f-DNS-HIGH", s.categories["dns"]!!.worstFinding) - assertEquals(Verdict.RED, s.overall) - } - - @JTest - fun noFindingsIsGreen() { - val s = Verdicts.derive(listOf(test(TestType.MTU_BLACKHOLE, TestStatus.OK)), emptyList()) - assertEquals(Verdict.GREEN, s.categories["mtu"]!!.verdict) - assertEquals(Verdict.GREEN, s.overall) - } - - @JTest - fun mediumAndLowAreYellow() { - val s = Verdicts.derive( - listOf(test(TestType.SEC_HTTP_ECHO, TestStatus.OK)), - listOf(finding(Category.SECURITY, Severity.MEDIUM)), - ) - assertEquals(Verdict.YELLOW, s.categories["security"]!!.verdict) - } - - @JTest - fun majorityFailedIsInconclusive() { - // 2 of 3 dns tests failed → > 50% → inconclusive, even with a finding present. - val tests = listOf( - test(TestType.DNS_CANARY, TestStatus.FAILED, "a"), - test(TestType.DNS_TTL_INTEGRITY, TestStatus.UNSUPPORTED, "b"), - test(TestType.DNS_COMPARE, TestStatus.OK, "c"), - ) - val s = Verdicts.derive(tests, listOf(finding(Category.DNS, Severity.HIGH))) - assertEquals(Verdict.INCONCLUSIVE, s.categories["dns"]!!.verdict) - assertEquals(2, s.categories["dns"]!!.testsFailed) - assertEquals(3, s.categories["dns"]!!.testsRun) - } - - @JTest - fun exactlyHalfFailedIsNotInconclusive() { - // 1 of 2 failed → not > 50% → the finding decides. - val tests = listOf( - test(TestType.NAT_HAIRPIN, TestStatus.FAILED, "a"), - test(TestType.NAT_CONNECT_BACK, TestStatus.OK, "b"), - ) - val s = Verdicts.derive(tests, listOf(finding(Category.NAT, Severity.CRITICAL))) - assertEquals(Verdict.RED, s.categories["nat"]!!.verdict) - } - - @JTest - fun overallIsWorstCategory() { - val tests = listOf( - test(TestType.DNS_CANARY, TestStatus.OK, "d"), - test(TestType.MTU_BLACKHOLE, TestStatus.OK, "m"), - ) - val findings = listOf( - finding(Category.DNS, Severity.MEDIUM), // yellow - finding(Category.MTU, Severity.CRITICAL), // red - ) - val s = Verdicts.derive(tests, findings) - assertEquals(Verdict.RED, s.overall) - } - - @JTest - fun overallInconclusiveOnlyWhenAllAre() { - val tests = listOf( - test(TestType.DNS_CANARY, TestStatus.FAILED, "d"), // dns inconclusive - test(TestType.MTU_BLACKHOLE, TestStatus.OK, "m"), // mtu green - ) - val s = Verdicts.derive(tests, emptyList()) - assertEquals(Verdict.INCONCLUSIVE, s.categories["dns"]!!.verdict) - assertEquals(Verdict.GREEN, s.categories["mtu"]!!.verdict) - assertEquals(Verdict.GREEN, s.overall) // not all inconclusive → mtu decides - } - - @JTest - fun categoryMappingCoversFamilies() { - assertEquals(Category.CONNECTIVITY, TestType.category(TestType.TRACEROUTE_UDP4)) - assertEquals(Category.IPV6, TestType.category(TestType.V6_BROKENNESS)) - assertEquals(Category.LOCAL, TestType.category(TestType.PEER_MULTICAST)) - assertEquals(Category.PERFORMANCE, TestType.category(TestType.PERF_BUFFERBLOAT)) - assertEquals(Category.CONNECTIVITY, TestType.category(TestType.TIME_SERVER_OFFSET)) - } -}