fix(skills): open reparse points in Windows file ID query and stabilize tunneled test

Add FILE_FLAG_OPEN_REPARSE_POINT to the Windows CreateFile call in
skillPathFileIdentityImpl so symlinks are opened as reparse points
rather than followed to their target. Staged symlinks carry relative
targets computed for the final destination, which may not resolve from
the staging directory; following them caused CreateFile to fail,
yielding an empty file ID that rejected publication and broke canonical
skill layout migration on Windows.

Make skillPathSameFileIdentity a seam variable so the tunneled
replacement test can deterministically simulate the identity change on
Unix. On tmpfs (used by Linux CI runners), os.SameFile can return true
for a recreated file due to inode reuse, making the test flaky. On
Windows the swap is a no-op because skillPathIdentityProven compares
file IDs from GetFileInformationByHandle and ignores
skillPathSameFileIdentity.
This commit is contained in:
玉澜 committed 2026-08-17 14:39:59 +08:00
1 parent e45608bb13
commit 344104268a
6 files changed
+22 -7

No files matched your search

@@ -24,7 +24,7 @@ func skillPathFileIncarnationImpl(info os.FileInfo) string {
// a recreated file from the original after NTFS file tunneling restores the
// creation time. Callers must use skillPathIdentityProven, which compares file
// IDs obtained via GetFileInformationByHandle.
func skillPathSameFileIdentity(_, _ os.FileInfo) bool {
func skillPathSameFileIdentityImpl(_, _ os.FileInfo) bool {
return false
}
@@ -32,6 +32,12 @@ func skillPathSameFileIdentity(_, _ os.FileInfo) bool {
// index via GetFileInformationByHandle. Unlike creation time (which NTFS file
// tunneling can restore for a recreated same-named object), the file index
// uniquely identifies the file on the volume for its lifetime.
//
// FILE_FLAG_OPEN_REPARSE_POINT opens symlinks themselves rather than following
// them to the target. This matters at publish time because a staged symlink
// carries a relative target computed for the final destination, which may not
// resolve from the staging directory. Opening the reparse point yields a file
// ID that is stable across the rename and is the correct identity to compare.
func skillPathFileIdentityImpl(path string) string {
p, err := windows.UTF16PtrFromString(path)
if err != nil {
@@ -40,7 +46,8 @@ func skillPathFileIdentityImpl(path string) string {
handle, err := windows.CreateFile(
p, windows.FILE_READ_ATTRIBUTES,
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
nil, windows.OPEN_EXISTING, windows.FILE_FLAG_BACKUP_SEMANTICS, 0,
nil, windows.OPEN_EXISTING,
windows.FILE_FLAG_BACKUP_SEMANTICS|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0,
)
if err != nil {
return ""