fix(program): preserve V1 digest during Android V2 upgrade
This commit is contained in:
parent
c4df2c3b3f
commit
df41300353
2 changed files with 54 additions and 12 deletions
|
|
@ -11829,6 +11829,34 @@ class TrainlogRepository(
|
|||
|
||||
fun applyProgramsV2Json(json: String): ProgramsImportResult = applyProgramsJson(json, 2)
|
||||
|
||||
private fun legacyProgramDigest(program: JSONObject): String {
|
||||
// WHY: serializing and reparsing JSONObject changes integral doubles (for
|
||||
// example 52.0 to 52), which changes a V1 revision's causal digest.
|
||||
// CONTRACT: discard only V2 fields while retaining the parsed V1 values.
|
||||
val legacy = JSONObject()
|
||||
program.keys().forEach { key ->
|
||||
when (key) {
|
||||
"revision_sequence" -> Unit
|
||||
"sessions" -> {
|
||||
val rows = JSONArray()
|
||||
val sessions = program.getJSONArray("sessions")
|
||||
for (index in 0 until sessions.length()) {
|
||||
val source = sessions.getJSONObject(index)
|
||||
val row = JSONObject()
|
||||
source.keys().forEach { sessionKey ->
|
||||
if (sessionKey !in setOf("position", "current_for", "planning_state"))
|
||||
row.put(sessionKey, source.get(sessionKey))
|
||||
}
|
||||
rows.put(row)
|
||||
}
|
||||
legacy.put(key, rows)
|
||||
}
|
||||
else -> legacy.put(key, program.get(key))
|
||||
}
|
||||
}
|
||||
return causalDigest(legacy)
|
||||
}
|
||||
|
||||
private fun applyProgramsJson(json: String, documentVersion: Int): ProgramsImportResult {
|
||||
if (json.toByteArray(StandardCharsets.UTF_8).size > 8 * 1024 * 1024 ||
|
||||
!jsonHasUniqueObjectKeys(json)) return ProgramsImportResult.Invalid("Invalid Programs JSON.")
|
||||
|
|
@ -12015,18 +12043,8 @@ class TrainlogRepository(
|
|||
if (existing?.revisionId == program.getString("revision_id")) {
|
||||
if (existing.digest == digest) { skipped++; continue }
|
||||
if (documentVersion != 2 || existing.version != 1 ||
|
||||
existing.digest != causalDigest(JSONObject(program.toString()).apply {
|
||||
remove("revision_sequence")
|
||||
getJSONArray("sessions").let { rows ->
|
||||
for (sessionIndex in 0 until rows.length()) {
|
||||
rows.getJSONObject(sessionIndex).apply {
|
||||
remove("position")
|
||||
remove("current_for")
|
||||
remove("planning_state")
|
||||
}
|
||||
}
|
||||
}
|
||||
})) throw ProgramsImportInvalid("Program revision identity conflict.")
|
||||
existing.digest != legacyProgramDigest(program))
|
||||
throw ProgramsImportInvalid("Program revision identity conflict.")
|
||||
}
|
||||
if (documentVersion == 2) {
|
||||
val activeProgramSession = db.rawQuery(
|
||||
|
|
|
|||
|
|
@ -131,6 +131,30 @@ class ProgramsProjectionRepositoryTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun v2UpgradeKeepsV1DigestForIntegralDecimalWeights() {
|
||||
val name = "program-decimal-upgrade-${UUID.randomUUID()}.db"
|
||||
val repository = TrainlogRepository(context, name)
|
||||
try {
|
||||
// The desktop JSON exporter writes SQLite REAL values as 40.0.
|
||||
// JSONObject's own serialization writes the same value as 40.
|
||||
val v1 = artifact(JSONArray().put(program()))
|
||||
.replace("\"target_weight_kg\":40", "\"target_weight_kg\":40.0")
|
||||
val v2 = artifactV2(program())
|
||||
.replace("\"target_weight_kg\":40", "\"target_weight_kg\":40.0")
|
||||
assertTrue(v1.contains("\"target_weight_kg\":40.0"))
|
||||
assertTrue(v2.contains("\"target_weight_kg\":40.0"))
|
||||
assertEquals(ProgramsImportResult.Applied(1, 0, 0), repository.applyProgramsV1Json(v1))
|
||||
assertEquals(ProgramsImportResult.Applied(1, 0, 0), repository.applyProgramsV2Json(v2))
|
||||
assertEquals("2026-10-01", repository.getSyncedProgram(program().getString("program_id"))!!
|
||||
.sessions.single().currentFor)
|
||||
assertEquals(ProgramsImportResult.Applied(0, 0, 1), repository.applyProgramsV2Json(v2))
|
||||
} finally {
|
||||
repository.close()
|
||||
context.deleteDatabase(name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun preparationArtifact(
|
||||
includeProgramProvenance: Boolean = true,
|
||||
deliveryState: String = "pending",
|
||||
|
|
|
|||
Loading…
Reference in a new issue