跳到正文
DEV Community· Jeff·· 3 小時前AI 評分59

智能體保留合併衝突雙方,程式碼通過編譯卻留下無法執行的分支

The agent resolved the merge conflict by keeping both sides, and the code compiled into a dead branch

AI 導讀

智能體處理 Git 合併衝突時保留了雙方程式碼,合併檔案雖通過編譯,卻令一方邏輯落入無法執行的分支。型別檢查和現有測試可能未覆蓋被取代的舊路徑,因此 CI 通過不代表衝突取捨正確。文章建議逐個衝突區塊對照兩個父分支、檢查重複輔助函式和舊條件的可達性,並記錄每個區塊採納哪一方及原因。

正文

Two branches land on top of each other. Branch A tightened a broad allow into a narrow allowlist inside one function. Branch B extended that same function with a second condition for a new role. Git marks the block as conflicting, the agent removes the markers, and the merged file compiles clean. What it actually wrote is both versions stacked: A's narrow guard followed by B's separate condition, both imports kept, both helpers present. CI goes green, the reviewer sees a clean diff, and one side of the original conflict shipped as code that can never run.

Keeping both sides compiles because nothing duplicated exactly

A compiler errors on a duplicate declaration when two symbols occupy the same name with the same shape. When the two branches solved the conflicted block differently, they rarely match that description: one renamed the helper, the other added an if, one changed the condition, the other split the logic into a second function. Stacking them produces two sequential blocks that the parser accepts with no name collision. Typecheck reads each block independently; it never asks which one actually executes. Lint checks style, not reachability. The tests that run cover the new path B added, so the legacy guard A replaced stays green without being exercised.

The merged result looks finished because the markers are gone. Removing <<<<<<<, =======, and >>>>>>> is mechanical; choosing which version wins is the real work, and the version that compiles and passes the current suite can hide the choice.

Four leftovers that drift after the merge

  • An unreachable legacy branch still ships. The guard A replaced sits below the new condition. Under the normal input it cannot fire, but an edge case the agent did not model can reach it, and behavior then differs from what the merge intended.
  • A near-duplicate helper drifts. Two functions with the same first lines and slightly different bodies accumulate separate fixes. A future patch lands on one copy; the other keeps the old behavior and nobody notices until a bug appears in the path that reads it.
  • Two imports for the same concept linger. When both helpers survive, their imports survive too, and the file carries redundant dependencies that look intentional at a glance.
  • The decision is not recorded. The PR body says "resolved conflicts" without naming which side won each hunk, so the reviewer cannot audit the choice and a later archaeologist cannot tell why both copies exist.

Why the agent defaults to stacking

Choosing a side means taking a position: which feature's behavior wins, which condition should apply, whether A's tightening should survive B's addition. Stacking both avoids that judgment and still satisfies the visible success criteria: markers gone, file parses, checks green. The optimizer that minimizes error tends to pick the path that removes the conflict rather than the path that resolves the disagreement. Human contributors do the same thing under time pressure, usually with one anchor: they know which feature is meant to win. A fresh session often does not, so it preserves both.

Diff against both parents, not the merged result alone

The merged diff against main hides the choice because it compares the final file to the branch point. The check that exposes stacking is to compare each conflicted hunk to both parents, not just read the merged file:

  • Compare the final hunk to ours and theirs. If the merged block contains statements from both versions of the same line where only one should survive, the choice was deferred, not made.
  • Hunt the duplicate pair. In the conflicted region, look for two imports for the same concept and two helpers that start the same way. Their presence is the signature of keeping both sides.
  • Trace reachability of the leftover condition. Ask which input can actually enter the older branch after the new condition sits above it. If the answer is none under realistic data, the branch should be deleted rather than preserved.
  • Log the decision per hunk. The PR note should state, for every conflicted block, which side won and why. A generic "resolved" forces the reviewer to redo the comparison from scratch.

Green after a conflict proves the markers are gone

A green typecheck after a merge conflict confirms the file parses and the current tests pass. It does not confirm the right side survived. Resolving a conflict means choosing and deleting the loser; stacking both is deferring the choice into unreachable code and a duplicate copy that drifts later. Compare the merged hunk to both parents before approving, and require the winner to be written down.

來源:DEV Community · dev.to