* fix(uploads): stop Windows replacement uploads from being skipped by the multi-link guard
NTFS never decrements st_nlink when one of a file's names is removed, so
every upload published through os.link(staging, dest) + unlink keeps
reading st_nlink == 2 on Windows. The non-exclusive replacement guards
then reject that destination forever, and DeerFlowClient.upload_files
turns the UnsafeUploadPathError into a log line - a replacement upload
is silently skipped (Closes#6820).
- Add _replacement_destination_is_hardlinked: POSIX-authoritative
st_nlink > 1, skipped on Windows, used only on the not exclusive
replacement path. The exclusive (first-claim) path keeps the strict
count check, as does the POSIX O_NOFOLLOW branch.
- Decide "published alias" in cleanup_stale_upload_staging_files by
inode identity against siblings (st_dev/st_ino via os.lstat) instead
of the link count; DirEntry.stat reports zeros for both on Windows,
matching the existing _staging_entry_stat note.
- Guard the POSIX-only st_nlink assertions in the affected tests and
mark the three inbound-attachment mode-bit tests POSIX-only, mirroring
the existing skipif in the same class.
* fix(uploads): restore the exclusive post-open link guard and harden the staging sweep
Address the three review findings on 5353715:
- Restore the strict post-open st_nlink != 1 check for exclusive=True
on the no-O_NOFOLLOW (Windows fallback) branch, which the previous
commit dropped there against what the PR description claimed. Only
the non-exclusive replacement path defers to the Windows-aware guard.
- Never match a degenerate st_ino == 0 in the sibling-inode sweep: on
FAT/exFAT or some SMB volumes every entry would look like a published
alias and in-flight uploads would be deleted at any age. Such volumes
fall back to the min_age guard only.
- Build one {name: stat} map per uploads directory before the entry
loop so the sweep scans each directory once instead of rescanning it
for every staging file (O(N) instead of O(N^2) lstat calls).
Tests: 407 passed / 27 skipped across test_uploads_manager.py,
test_uploads_router.py, test_upload_copy_publication.py,
test_upload_windows_staging_aliases.py, test_channel_file_attachments.py
(Windows 11, Python 3.13 venv). The three new regression tests fail on
5353715 and pass with this change.
* style(uploads): apply Ruff formatting
* fix(uploads): report staging directory scan failures
---------
Co-authored-by: sjr666666 <sjr666666@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(client): convert the captured upload source
* test(uploads): cover ACL sources and scope capture failures
* fix(uploads): preserve concurrent uploads from one captured source
Combine the embedded client's source capture with no-replace publication for
first-time filenames and Markdown companions. Hold one private capture across
collision retries, retain original descriptor metadata, and keep replacement
behavior for names present when the call starts.
Port concurrent-upload and staging-publication regressions from PR #6687.
Extend source-edit/removal and real XLSX cases to both publication paths, and
cover edits or removal during two filename collisions in ordinary and event-loop
calls. Document capture, retry, publication, and cleanup contracts.
Validation: related suites 469 passed, 24 skipped; full offline suite 28,788
passed, 488 skipped; blocking-I/O suite 245 passed, 1 skipped; Ruff, guidance,
and diff checks passed. Nine race/collision cases failed on the #6632 baseline.
Co-authored-by: haotianqi666 <250384991+haotianqi666@users.noreply.github.com>
* docs: keep agent guidance within CI budgets
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: haotianqi666 <250384991+haotianqi666@users.noreply.github.com>