test(worktree): reject stat-cache-hidden source overwrites

Problem: A snapshot-only regression does not establish that the shared tree
reader also protects the source workspace before Apply publishes a candidate.

Root cause: The old cached index can hide a user's intervening edit from the
whole-tree drift check, allowing Apply to return success and overwrite it.

Fix: Exercise actual snapshot/candidate/Apply assembly on disposable real Git
repositories. Require ErrWorkspaceMoved and unchanged user bytes. Document the
existing Git driver confinement and the additional rehashing cost explicitly.

Verification: A compiling overlay of the original production file fails for
both wrong success and actual overwrite; the repaired owner passes. Full final
worktree race passes (48.247s). Broader root and static qualification records
are retained separately; production code is unchanged by this test follow-up.

Refs #14025
Refs #14133
This commit is contained in:
SivanCola committed 2026-10-24 22:48:54 +08:00
1 parent 0f943c7f7a
commit fd8729692b
2 files changed
+64

No files matched your search

+7
View File
@@ -29,6 +29,10 @@ the transaction-library PR #14368. It is not independent security approval.
各路径来自现有宿主解析的仓库身份或新建的私有临时目录。新命令仍经现有 Git
驱动执行;参数不作为 shell 脚本拼接。真实索引和 refs 不参与私有索引重建。
The existing driver disables Git hooks/fsmonitor and submodule recursion for
`read-tree`. The extra invocations inherit that policy. Index rebuilding and
rehashing add work; this slice makes no capture-latency improvement claim.
## Rules and failure / 规则与失败
1. The copied index MUST first preserve its staged tree, including explicitly
@@ -60,6 +64,9 @@ the transaction-library PR #14368. It is not independent security approval.
3. The complete worktree race suite covers neighbouring creation, cancellation,
filtering, relative roots, application and rollback behavior. Platform and
broader assembly results are reported separately in the PR.
4. `TestApplyRejectsStatCacheHiddenSourceEdit` reproduces an actual stale
candidate overwriting a hidden user edit with the old code. The repair
returns `ErrWorkspaceMoved` and keeps the user's bytes.
回归先证明旧实现在真实 Git 仓库中捕获旧内容,再验证修复后内容、忽略文件、
索引及 HEAD 均符合预期。其他平台和装配层结论按各自实际测试结果记录。
@@ -0,0 +1,57 @@
package worktree
import (
"context"
"errors"
"os"
"path/filepath"
"testing"
"time"
)
func TestApplyRejectsStatCacheHiddenSourceEdit(t *testing.T) {
requireGit(t)
root := t.TempDir()
gitIn(t, root, "init", "-q")
gitIn(t, root, "config", "core.trustctime", "false")
gitIn(t, root, "config", "core.checkstat", "minimal")
writeRepoFile(t, root, "input", "old\n")
path := filepath.Join(root, "input")
stamp := time.Now().Add(-time.Hour).Truncate(time.Second)
if err := os.Chtimes(path, stamp, stamp); err != nil {
t.Fatal(err)
}
gitIn(t, root, "add", "-A")
gitIn(t, root, "commit", "-qm", "base")
snapshot, err := TakeSnapshot(t.Context(), opened(t, root))
if err != nil {
t.Fatal(err)
}
candidate, err := CreateCandidate(t.Context(), snapshot, t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := RemoveCandidate(context.WithoutCancel(t.Context()), snapshot, candidate); err != nil {
t.Error(err)
}
})
writeRepoFile(t, candidate.WorkspaceRoot, "input", "new\n")
tree, changes, err := CandidateTree(t.Context(), snapshot, candidate)
if err != nil || len(changes) != 1 {
t.Fatalf("candidate fixture: %v %v", changes, err)
}
writeRepoFile(t, root, "input", "usr\n")
if err := os.Chtimes(path, stamp, stamp); err != nil {
t.Fatal(err)
}
if diff := gitIn(t, root, "diff", "--", "input"); diff != "" {
t.Fatalf("fixture did not hide the user's edit: %s", diff)
}
if err := Apply(t.Context(), snapshot, tree, changes); !errors.Is(err, ErrWorkspaceMoved) {
t.Errorf("stale candidate was not refused: %v", err)
}
if got := readRepoFile(t, root, "input"); got != "usr\n" {
t.Fatalf("candidate overwrote the user's edit: %q", got)
}
}