fix(lotw): 呼号冲突批量全阻时也进预览,呼号不符改为显示记录≠证书

整批记录都被呼号冲突挡住(0 条可传)时,统计闸门直接弹「N QSO(s) can't be uploaded」
消息并返回,带【改写为…】【换一张证书】两个动作的预览弹窗根本走不到,那批记录无从
处理。现在只要原因里有呼号冲突就放行到预览:0 条可传也出预览(显示 0 QSO(s) + 冲突
提示 + 两个动作),上传键置灰——以前点确认会返回“预览已过期”。其它原因的 0 条可传
批次保持原消息弹窗不变。

「MY callsign does not match the certificate」的示例值由某条记录的对方呼号改为
「记录呼号 ≠ 证书呼号」,把需要协调的两个值都打出来(旧文案读起来像证书呼号)。

空预览不再显示空的日期区间。

新增 LoTWUploadMessagesTest 3 例:冲突批量必须进预览、无冲突的 0 条可传批次保持消息、
有待传记录时不走冲突兜底;仓库层用例新增:全阻批量仍返回带冲突清单的预览、且无载荷
时确认安全地按“过期”失败。
This commit is contained in:
atsunatsu committed 2026-10-07 00:19:27 +08:00
1 parent b2e9454ee4
commit d7d5918f0a
8 files changed
+151 -8

No files matched your search

@@ -37,7 +37,12 @@ internal class LoTWSigner(private val config: LoTWConfig) {
val call = record.theirCallsign.trim().uppercase(Locale.US)
if (record.status != QsoStatus.COMPLETE || !call.matches(Regex("[A-Z0-9]+(/[A-Z0-9]+)*")) ||
!call.any(Char::isLetter) || !call.any(Char::isDigit)) fail(LoTWProblem.INVALID_CONTACT, call)
if (!record.myCallsign.trim().equals(key.info.callsign, true)) fail(LoTWProblem.CALLSIGN_MISMATCH, call)
// Report the two callsigns the operator has to reconcile (record vs certificate). A generic
// sample — the opposite station's call — says nothing about which side is wrong, which is
// exactly what a "MY callsign does not match the certificate" message must tell.
if (!record.myCallsign.trim().equals(key.info.callsign, true)) {
fail(LoTWProblem.CALLSIGN_MISMATCH, "${record.myCallsign.trim()} ≠ ${key.info.callsign}")
}
val date = utc(record.startUtcMillis, "yyyy-MM-dd")
// A record dated after "now" gets its own message: it happens when the operator
// logged against a pass that had not started yet (out-of-window flow), and "fix the
@@ -22,6 +22,7 @@ import com.rtbishop.look4sat.core.domain.logbook.QsoStatus
import com.rtbishop.look4sat.core.domain.repository.LoTWOperationException
import com.rtbishop.look4sat.core.domain.repository.LoTWProblem
import com.rtbishop.look4sat.core.domain.repository.LoTWStation
import com.rtbishop.look4sat.core.domain.repository.LoTWUploadResult
import java.io.File
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
@@ -120,6 +121,55 @@ class LoTWUploadRepositoryPreviewTest {
}
}
/** Nothing signable at all: the preview still comes back (count 0) with the conflicts listed,
* so the UI can offer the rewrite / use-another-certificate actions instead of a dead end. */
@Test
fun `fully blocked batch still returns a preview with the conflicts`() {
runBlocking {
val storage = MemStorage()
val repo = LoTWUploadRepository(
storage,
{ LoTWConfig(File("src/main/assets/lotw/config.tq6").inputStream()) },
now = { System.currentTimeMillis() }
)
repo.importCertificate(fixture("test_tqsl_empty.p12"), charArrayOf())
repo.saveStation(LoTWStation(grid = "OL62TI"))
val start = 1_787_000_000_000L
val preview = repo.prepare(
listOf(
sampleRecord(start, "BH6RJD", "XX0YY"),
sampleRecord(start + 60_000L, "BG7QBL", "XX0ZZ")
),
resubmit = false
)
assertEquals(0, preview.count)
assertEquals(2, preview.unavailableSkipped)
assertEquals(listOf(start, start + 60_000L), preview.callsignConflicts)
assertEquals(2, preview.unavailableReasons[LoTWProblem.CALLSIGN_MISMATCH])
assertEquals(0, preview.missingCallsign)
// No payload was prepared: confirming this preview must fail as expired, not crash.
assertTrue(repo.upload(preview.id) is LoTWUploadResult.ExpiredPreview)
}
}
/** The mismatch reason names both callsigns (record vs certificate). The old detail showed the
* opposite station's call, which told the operator nothing about which side to fix. */
@Test
fun `callsign mismatch detail names the record and the certificate callsign`() {
runBlocking {
val storage = MemStorage()
val repo = LoTWUploadRepository(
storage,
{ LoTWConfig(File("src/main/assets/lotw/config.tq6").inputStream()) },
now = { System.currentTimeMillis() }
)
repo.importCertificate(fixture("test_tqsl_empty.p12"), charArrayOf())
repo.saveStation(LoTWStation(grid = "OL62TI"))
val audit = repo.audit(listOf(sampleRecord(1_787_000_000_000L, "BH6RJD", "XX0YY")))
assertEquals("XX0YY ≠ BA7OPF", audit.details[LoTWProblem.CALLSIGN_MISMATCH])
}
}
private fun sampleRecord(start: Long, call: String, myCallsign: String) = QsoRecord(
id = start,
startUtcMillis = start,