Skip to content

Commit 654b15c

Browse files
malysclaude
andcommitted
fix(diagnostics): keep the guidance artifact inside the export ceiling
The exporter drops an evidence file over 64 KiB and merely lists it as skipped. A full 300-line trace with accented road names lands around 47 KiB, so the one artifact a validation run exists to produce was one long road name away from disappearing quietly into the manifest. toBoundedJson trims oldest trace lines until the artifact fits, well inside the exporter's own limit so a file cannot fail on a boundary. Oldest go first, because the end of a drive is what an arrival forecast is judged on, and trimming marks traceComplete false exactly as the ring overflowing does. Refs: CP-040 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent ea2e855 commit 654b15c

4 files changed

Lines changed: 71 additions & 2 deletions

File tree

‎app/src/testUnstable/java/com/evsuite/chargepilot/NavGuidanceProbeArtifactTest.kt‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,40 @@ class NavGuidanceProbeArtifactTest {
6868
traceComplete = traceComplete,
6969
)
7070
}
71+
72+
class NavGuidanceProbeArtifactBoundTest {
73+
74+
private fun artifact(lines: Int, road: String) = NavGuidanceProbeArtifact.of(
75+
savedAtMs = 1_700_000_000_000L,
76+
firmware = "SWI68",
77+
adapterBound = true,
78+
listenerRegistered = true,
79+
callbacks = lines,
80+
census = mapOf(13 to lines),
81+
censusBeyondCeiling = 0,
82+
trace = List(lines) { "%5ds n=%-4d dist=%-8s road=%s".format(it, it, it * 10, road) },
83+
traceComplete = true,
84+
)
85+
86+
@Test fun `a full trace of accented road names still fits the export ceiling`() {
87+
val json = artifact(300, "Avenue des Carabènes, Saint-Orens-de-Gam")
88+
.toBoundedJson()
89+
assertTrue(
90+
"artifact must stay inside the exporter's per-file limit",
91+
json.toByteArray(Charsets.UTF_8).size <= NavGuidanceProbeArtifact.MAX_ARTIFACT_BYTES,
92+
)
93+
}
94+
95+
@Test fun `a trace that has to be trimmed says so and keeps the end of the drive`() {
96+
val json = artifact(300, "x".repeat(40)).toBoundedJson(maxBytes = 4_096)
97+
assertTrue(json.toByteArray(Charsets.UTF_8).size <= 4_096)
98+
assertTrue("trimming must mark the trace incomplete", json.contains("\"traceComplete\":false"))
99+
// The last line of the drive is the one a forecast is judged on; it must survive.
100+
assertTrue(json.contains(" 299s"))
101+
}
102+
103+
@Test fun `a short trace is left exactly as it was`() {
104+
val full = artifact(5, "A61")
105+
assertEquals(full.toJson(), full.toBoundedJson())
106+
}
107+
}

‎app/src/unstable/java/com/evsuite/chargepilot/EvidenceCaptureHook.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ object EvidenceCaptureHook {
4444
File(context.filesDir, NavGuidanceRecorder.EVIDENCE_DIRECTORY)
4545
)
4646
store.write(
47-
NavGuidanceRecorder.artifact().toJson(),
47+
NavGuidanceRecorder.artifact().toBoundedJson(),
4848
NavGuidanceProbeArtifact.KIND,
4949
firmware,
5050
)

‎app/src/unstable/java/com/evsuite/chargepilot/NavGuidanceProbeArtifact.kt‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,45 @@ internal data class NavGuidanceProbeArtifact(
3232
) {
3333
fun toJson(): String = GSON.toJson(this)
3434

35+
/**
36+
* The artifact, guaranteed to fit the diagnostic export's per-file ceiling.
37+
*
38+
* A file over that ceiling is not truncated by the exporter, it is dropped and merely
39+
* listed as skipped — so the one artifact a validation run exists to produce would go
40+
* missing quietly, and only a careful reader of the manifest would notice. 300 lines of
41+
* trace with accented road names lands around 47 KB against a 64 KB limit, which is
42+
* enough headroom until it is not.
43+
*
44+
* Oldest trace lines go first, because the end of a drive is what a forecast is judged
45+
* on. Dropping any marks [traceComplete] false, exactly as the ring overflowing does.
46+
*/
47+
fun toBoundedJson(maxBytes: Int = MAX_ARTIFACT_BYTES): String {
48+
var candidate = this
49+
var json = candidate.toJson()
50+
while (json.toByteArray(Charsets.UTF_8).size > maxBytes && candidate.trace.isNotEmpty()) {
51+
val keep = (candidate.trace.size * 3) / 4
52+
candidate = candidate.copy(
53+
trace = candidate.trace.takeLast(keep.coerceAtLeast(0)),
54+
traceComplete = false,
55+
)
56+
json = candidate.toJson()
57+
}
58+
return json
59+
}
60+
3561
companion object {
3662
const val SCHEMA_VERSION = 1
3763
const val PROBE = "nav-guidance"
3864

3965
/** File name kind, kept apart from the capture files sharing the evidence folder. */
4066
const val KIND = "navguidance"
4167

68+
/**
69+
* Comfortably inside the exporter's own 64 KiB per-file ceiling, so a file that grew
70+
* between the write and the export still travels.
71+
*/
72+
const val MAX_ARTIFACT_BYTES = 48 * 1024
73+
4274
private val GSON = Gson()
4375

4476
private val NOTES = listOf(

‎app/src/unstable/java/com/evsuite/chargepilot/NavGuidanceRecorder.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ internal object NavGuidanceRecorder {
150150
fun save(context: Context, nowMs: Long = System.currentTimeMillis()): File? {
151151
val firmware = FirmwareInfo.getGeneration().name
152152
return EvidenceCaptureFileStore(File(context.filesDir, EVIDENCE_DIRECTORY))
153-
.write(artifact(nowMs).toJson(), NavGuidanceProbeArtifact.KIND, firmware)
153+
.write(artifact(nowMs).toBoundedJson(), NavGuidanceProbeArtifact.KIND, firmware)
154154
}
155155

156156
private fun Int?.show(): String = this?.toString() ?: DASH

0 commit comments

Comments
 (0)