跳转至

apps\online\src\test\java\cn\nfarms\online\MergeTest.kt

源码路径:apps\online\src\test\java\cn\nfarms\online\MergeTest.kt · 97 行

package cn.nfarms.online

import cn.nfarms.online.data.Merge
import cn.nfarms.online.data.MergeResult
import cn.nfarms.online.data.Ticket
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * 三路合并。
 *
 * 这四条分支(只有远端改 / 只有本地改 / 都改且一样 / 都改且不同)
 * 就是「冲突合并」的全部逻辑;真机上看到"冲突弹窗"只是它的表现。
 */
class MergeTest {

    private val base = Ticket(
        id = "T-1",
        title = "打印机卡纸",
        body = "二楼东侧",
        status = "open",
        updatedAt = 1_000,
        rev = 3,
    )

    @Test
    fun only_remote_changed_takes_remote() {
        val local = base
        val remote = base.copy(title = "打印机卡纸(已派人)", rev = 4)
        val r = Merge.merge(local, remote, base)
        assertTrue(r is MergeResult.TookRemote)
        assertEquals("打印机卡纸(已派人)", (r as MergeResult.TookRemote).ticket.title)
    }

    @Test
    fun only_local_changed_keeps_local() {
        val local = base.copy(body = "三楼西侧")
        val remote = base
        val r = Merge.merge(local, remote, base)
        assertTrue(r is MergeResult.KeptLocal)
    }

    @Test
    fun both_changed_to_the_same_content_is_not_a_conflict() {
        val local = base.copy(title = "已修好")
        val remote = base.copy(title = "已修好", rev = 5)
        val r = Merge.merge(local, remote, base)
        // 内容一致 → 不是冲突;保留本地内容。
        // 之所以不是 TookRemote:结论虽然一样,但"这次改动是谁的"要留给审计。
        assertTrue(r is MergeResult.KeptLocal)
    }

    @Test
    fun both_changed_differently_is_a_conflict() {
        val local = base.copy(title = "我改的")
        val remote = base.copy(title = "他改的", rev = 6)
        val r = Merge.merge(local, remote, base)
        assertTrue(r is MergeResult.Conflict)
        val c = r as MergeResult.Conflict
        assertEquals("我改的", c.local.title)
        assertEquals("他改的", c.remote.title)
        assertEquals(base.rev, c.base?.rev)
    }

    /** 没有基线时不能瞎猜:内容一致就合并,不一致就必须报冲突。 */
    @Test
    fun missing_base_falls_back_to_conservative_behaviour() {
        val a = base.copy(rev = 0)
        assertEquals(MergeResult.Identical, Merge.merge(a, a, null))
        val b = a.copy(title = "x")
        assertTrue(Merge.merge(a, b, null) is MergeResult.Conflict)
    }

    /** 「用我的」必须把 rev 对齐到远端,否则下次同步又会被判冲突。 */
    @Test
    fun take_mine_aligns_rev_with_remote() {
        val local = base.copy(title = "我改的")
        val remote = base.copy(title = "他改的", rev = 9)
        val mine = Merge.takeMine(local, remote)
        assertEquals(9, mine.rev)
        assertEquals("我改的", mine.title)
    }

    /** 附件也算内容:只有附件变了同样要能识别成"本地改了"。 */
    @Test
    fun attachment_change_counts_as_content_change() {
        val withAttach = base.copy(
            attachments = listOf(
                cn.nfarms.online.data.Attachment("A1", "a.png", 100, "abc"),
            ),
        )
        val r = Merge.merge(withAttach, base, base)
        assertTrue("只有本地加了附件,应当是 KeptLocal", r is MergeResult.KeptLocal)
    }
}