From bda13be568fe8cce53feebbac57aab61a50ba235 Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Tue, 3 Feb 2026 11:17:34 +0100 Subject: [PATCH 01/12] Add skipped entry model and skip reason enum --- skills/dist/plan-implementation.skill | Bin 0 -> 954 bytes skills/plan-implementation/SKILL.md | 45 ++++++++++++++++++ .../Models/Inventories/SkipReason.cs | 15 ++++++ .../Models/Inventories/SkippedEntry.cs | 18 +++++++ 4 files changed, 78 insertions(+) create mode 100644 skills/dist/plan-implementation.skill create mode 100644 skills/plan-implementation/SKILL.md create mode 100644 src/ByteSync.Client/Models/Inventories/SkipReason.cs create mode 100644 src/ByteSync.Client/Models/Inventories/SkippedEntry.cs diff --git a/skills/dist/plan-implementation.skill b/skills/dist/plan-implementation.skill new file mode 100644 index 0000000000000000000000000000000000000000..d7ba9487c7ce5e031cee579a72c4f494fdb28751 GIT binary patch literal 954 zcmWIWW@Zs#U|`^2I34L6GhN_YqyjSo13w!BgA7o#ASW?TH#4^&Cp9-UuOzV~Ge1v1 z*xS>`M=v)eH{^8QZ3BTlueHTaEM?OX&AsHEmpNO^v2ojr&sno#Zn|g$Jw3eb-@eUL zWYshMC#`t;?#-Js+j4JLn0_$r_4|2^Z}H>C+OV5!_a*$SZUx?3|MJ^`T`6i$UuH(^ z^?Ktw+qs)P(!cWbm$FlTYCp|V>R-5cerM+Xr$6I8WjvSucCnmudEz3&Pv_bu-Eo<- zDav~Gwwz}|nx8#hue6(f;@9#UCpt?~9(<6#6X*W)>$Gck+D|NLKKN0ngZE=%%3R^R z+^%53|7`~!-JYTmeq6%r#ME8OSe4JR)=q6I_kWq8r}yG?-{W(i_GP~N*B_;Lp3|Vi zV&aA+4=Ud|ob1hEk89$8Epdf$otT9CpJMlfunFHUt(aoz^iHeqzT&;_dT*XSZi>@= z5&L0p@8OA>vTk{^@|_gj)O!BsrybbdH|=-dyI)c!^VL2ndi#5xl3TvA+5Pp(dvT{; za4b0aO!(#skyNg`JugfJJjMT<51E&_rjq0Ho_aB>6~C>*?N917Ca(U|7VK`%@IpfC z)zXDacx?Du=A>@dytZNUyvwDRw}jRkF@L#wn>p@oiSKH;qux)ZuU5VHcA=f%f)c)s zbu$)B-m~ClDBFDf@Drt*xx-ARE?)FRL}gY(C6`3_(v8y`?##WuZpLGVAT6JLJlP)i zzg>0z+QDz;*r~tccFAeuMd9o9*GHxL%)a1rJ9@h1INF z|4$#Sxu<@tQs>U+6N)n(E!A_S`Yx<*X?ihRF0@m$W7=e|C;ENc=BEW-X$;W!vr=_F zvU}B)s!i>Fnfaf^CMYWA8nz0*U~CJPKGy2b)1dDC%`w?_hF0C{hpk#-=HE-VDy)dw z6LbBPj<jeuo4<&mqvhq%v&V0rx8GOZ9I2zK-o})8HSb>gsxkrJqt(}% zH2p+*J}p~ZU}v%Mf$yPPtG2HC;mRlUVOxax;iy}i0K4|)aTUviw z)~dvJFV7~aJxDZ+isWFPz3ivdVLmN8zu#MyuIORADZFy|jCW@Hj!z@4{%*$WJ|HG(L7IW54O6__6x7#M-j M3rI%+b0`A?03%tuY5)KL literal 0 HcmV?d00001 diff --git a/skills/plan-implementation/SKILL.md b/skills/plan-implementation/SKILL.md new file mode 100644 index 00000000..be14b55a --- /dev/null +++ b/skills/plan-implementation/SKILL.md @@ -0,0 +1,45 @@ +--- +name: plan-implementation +description: Execute an implementation plan end-to-end in the ByteSync repo. Use when the user asks to implement a previously established plan step by step, requires creating a branch before coding, and expects regular commits during progress. +--- + +# Plan Implementation + +## Overview + +Implement a previously agreed plan in ByteSync, one step at a time, while enforcing branch creation and frequent commits. + +## Workflow + +### 1) Confirm plan scope + +Restate the implementation plan in brief. If the plan is not present in the conversation, ask for it before starting. + +### 2) Create a branch before implementation + +Create a new branch before making any code changes. The branch name must follow ByteSync guidelines: + +- Use one prefix only: `feature/`, `fix/`, `refactor/`, `docs/`, or `test/` +- Avoid extra slashes beyond the prefix +- Include the issue number if an issue exists (e.g., `feature/1234-add-sync-filter`) +- Add a short English description + +### 3) Implement step by step + +Implement the plan in small, ordered steps. After each step: + +- Verify the change fits the plan +- Commit the work with a concise English message +- Keep commits focused and incremental + +### 4) Respect repo conventions + +Follow the repo guidance (AGENTS.md), including: + +- Do not add unnecessary comments +- Use FluentAssertions for tests +- Build and test as separate commands when asked to run them + +### 5) Keep the user informed + +Provide short progress updates after each step and call out any blockers or missing details before proceeding. diff --git a/src/ByteSync.Client/Models/Inventories/SkipReason.cs b/src/ByteSync.Client/Models/Inventories/SkipReason.cs new file mode 100644 index 00000000..d065e5c9 --- /dev/null +++ b/src/ByteSync.Client/Models/Inventories/SkipReason.cs @@ -0,0 +1,15 @@ +namespace ByteSync.Models.Inventories; + +public enum SkipReason +{ + Unknown = 0, + Hidden = 1, + SystemAttribute = 2, + NoiseFile = 3, + Symlink = 4, + SpecialPosixFile = 5, + Offline = 6, + Inaccessible = 7, + NotFound = 8, + IoError = 9 +} diff --git a/src/ByteSync.Client/Models/Inventories/SkippedEntry.cs b/src/ByteSync.Client/Models/Inventories/SkippedEntry.cs new file mode 100644 index 00000000..16de13f4 --- /dev/null +++ b/src/ByteSync.Client/Models/Inventories/SkippedEntry.cs @@ -0,0 +1,18 @@ +using ByteSync.Business.Inventories; + +namespace ByteSync.Models.Inventories; + +public class SkippedEntry +{ + public string FullPath { get; init; } = string.Empty; + + public string RelativePath { get; init; } = string.Empty; + + public string Name { get; init; } = string.Empty; + + public SkipReason Reason { get; init; } + + public FileSystemEntryKind? DetectedKind { get; init; } + + public DateTime SkippedAt { get; init; } = DateTime.UtcNow; +} From a1919b18bdb6ed281813fc3f54c1dffdf074e932 Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Tue, 3 Feb 2026 11:19:41 +0100 Subject: [PATCH 02/12] Track skipped entries in inventory process data --- .../Inventories/InventoryProcessData.cs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs b/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs index ec7735a0..44102652 100644 --- a/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs +++ b/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs @@ -1,4 +1,5 @@ -using System.Reactive.Linq; +using System.Collections.Concurrent; +using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading; using ByteSync.Interfaces.Controls.Inventories; @@ -11,6 +12,7 @@ namespace ByteSync.Business.Inventories; public class InventoryProcessData : ReactiveObject { private readonly object _monitorDataLock = new object(); + private readonly ConcurrentQueue _skippedEntries = new(); public InventoryProcessData() { @@ -101,6 +103,8 @@ public List? Inventories public IObservable InventoryMonitorObservable => InventoryMonitorDataSubject.AsObservable(); + public IReadOnlyCollection SkippedEntries => _skippedEntries.ToArray(); + [Reactive] public DateTimeOffset InventoryStart { get; set; } @@ -131,8 +135,14 @@ public void Reset() LastException = null; InventoryMonitorDataSubject.OnNext(new InventoryMonitorData()); + ClearSkippedEntries(); } + public void RecordSkippedEntry(SkippedEntry entry) + { + _skippedEntries.Enqueue(entry); + } + public void SetError(Exception exception) { LastException = exception; @@ -150,4 +160,12 @@ public void UpdateMonitorData(Action action) InventoryMonitorDataSubject.OnNext(newValue); } } -} \ No newline at end of file + + private void ClearSkippedEntries() + { + while (_skippedEntries.TryDequeue(out _)) + { + } + } +} + From 387069369aacc03b70bd2ccaa2ee2900f331bccd Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Tue, 3 Feb 2026 11:20:34 +0100 Subject: [PATCH 03/12] Split noise and system file detection --- .../Inventories/IFileSystemInspector.cs | 5 +++-- .../Inventories/FileSystemInspector.cs | 18 ++++++++++++++---- .../Services/Inventories/InventoryBuilder.cs | 11 +++++++++-- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/ByteSync.Client/Interfaces/Controls/Inventories/IFileSystemInspector.cs b/src/ByteSync.Client/Interfaces/Controls/Inventories/IFileSystemInspector.cs index fb54148f..fe0a8db8 100644 --- a/src/ByteSync.Client/Interfaces/Controls/Inventories/IFileSystemInspector.cs +++ b/src/ByteSync.Client/Interfaces/Controls/Inventories/IFileSystemInspector.cs @@ -6,9 +6,10 @@ namespace ByteSync.Interfaces.Controls.Inventories; public interface IFileSystemInspector { bool IsHidden(FileSystemInfo fsi, OSPlatforms os); - bool IsSystem(FileInfo fileInfo); + bool IsSystemAttribute(FileInfo fileInfo); + bool IsNoiseFileName(FileInfo fileInfo, OSPlatforms os); bool IsReparsePoint(FileSystemInfo fsi); bool Exists(FileInfo fileInfo); bool IsOffline(FileInfo fileInfo); bool IsRecallOnDataAccess(FileInfo fileInfo); -} \ No newline at end of file +} diff --git a/src/ByteSync.Client/Services/Inventories/FileSystemInspector.cs b/src/ByteSync.Client/Services/Inventories/FileSystemInspector.cs index bd3281e4..ac3675fa 100644 --- a/src/ByteSync.Client/Services/Inventories/FileSystemInspector.cs +++ b/src/ByteSync.Client/Services/Inventories/FileSystemInspector.cs @@ -16,12 +16,22 @@ public bool IsHidden(FileSystemInfo fsi, OSPlatforms os) return isHidden || isDot; } - public bool IsSystem(FileInfo fileInfo) + public bool IsSystemAttribute(FileInfo fileInfo) { - var isCommon = fileInfo.Name.In("desktop.ini", "thumbs.db", ".desktop.ini", ".thumbs.db", ".DS_Store"); var isSystem = (fileInfo.Attributes & FileAttributes.System) == FileAttributes.System; - return isCommon || isSystem; + return isSystem; + } + + public bool IsNoiseFileName(FileInfo fileInfo, OSPlatforms os) + { + var comparison = os == OSPlatforms.Linux ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + + return fileInfo.Name.Equals("desktop.ini", comparison) + || fileInfo.Name.Equals("thumbs.db", comparison) + || fileInfo.Name.Equals(".desktop.ini", comparison) + || fileInfo.Name.Equals(".thumbs.db", comparison) + || fileInfo.Name.Equals(".DS_Store", comparison); } public bool IsReparsePoint(FileSystemInfo fsi) @@ -43,4 +53,4 @@ public bool IsRecallOnDataAccess(FileInfo fileInfo) { return (((int)fileInfo.Attributes) & FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS) == FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS; } -} \ No newline at end of file +} diff --git a/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs b/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs index 46e3511d..823c30a3 100644 --- a/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs +++ b/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs @@ -497,7 +497,14 @@ private bool ShouldIgnoreSystemFile(FileInfo fileInfo) return false; } - if (FileSystemInspector.IsSystem(fileInfo)) + if (FileSystemInspector.IsNoiseFileName(fileInfo, OSPlatform)) + { + _logger.LogInformation("File {File} is ignored because considered as noise", fileInfo.FullName); + + return true; + } + + if (FileSystemInspector.IsSystemAttribute(fileInfo)) { _logger.LogInformation("File {File} is ignored because considered as system", fileInfo.FullName); @@ -628,4 +635,4 @@ private void AddFileSystemDescription(InventoryPart inventoryPart, FileSystemDes } } } -} \ No newline at end of file +} From 94826c76562ace37e501676003a2af1d64aae891 Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Tue, 3 Feb 2026 11:25:24 +0100 Subject: [PATCH 04/12] Record skipped inventory entries with reasons --- .../Services/Inventories/InventoryBuilder.cs | 139 ++++++++++++++++-- 1 file changed, 123 insertions(+), 16 deletions(-) diff --git a/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs b/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs index 823c30a3..948fbe4f 100644 --- a/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs +++ b/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs @@ -270,26 +270,27 @@ private void ProcessSubDirectories(InventoryPart inventoryPart, DirectoryInfo di { if (IsReparsePoint(subDirectory)) { + RecordSkippedEntry(inventoryPart, subDirectory, SkipReason.Symlink, FileSystemEntryKind.Symlink); continue; } } catch (UnauthorizedAccessException ex) { - AddInaccessibleDirectoryAndLog(inventoryPart, subDirectory, ex, + AddInaccessibleDirectoryAndLog(inventoryPart, subDirectory, SkipReason.Inaccessible, ex, "Directory {Directory} is inaccessible and will be skipped"); continue; } catch (DirectoryNotFoundException ex) { - AddInaccessibleDirectoryAndLog(inventoryPart, subDirectory, ex, + AddInaccessibleDirectoryAndLog(inventoryPart, subDirectory, SkipReason.NotFound, ex, "Directory {Directory} not found during enumeration and will be skipped"); continue; } catch (IOException ex) { - AddInaccessibleDirectoryAndLog(inventoryPart, subDirectory, ex, + AddInaccessibleDirectoryAndLog(inventoryPart, subDirectory, SkipReason.IoError, ex, "Directory {Directory} IO error and will be skipped"); continue; @@ -312,12 +313,14 @@ private void ProcessFiles(InventoryPart inventoryPart, DirectoryInfo directoryIn } } - private void AddInaccessibleDirectoryAndLog(InventoryPart inventoryPart, DirectoryInfo directoryInfo, Exception ex, string message) + private void AddInaccessibleDirectoryAndLog(InventoryPart inventoryPart, DirectoryInfo directoryInfo, SkipReason reason, + Exception ex, string message) { inventoryPart.IsIncompleteDueToAccess = true; var subDirectoryDescription = IdentityBuilder.BuildDirectoryDescription(inventoryPart, directoryInfo); subDirectoryDescription.IsAccessible = false; AddFileSystemDescription(inventoryPart, subDirectoryDescription); + RecordSkippedEntry(inventoryPart, directoryInfo, reason); _logger.LogWarning(ex, message, directoryInfo.FullName); } @@ -375,6 +378,13 @@ private bool ShouldIgnoreHiddenDirectory(DirectoryInfo directoryInfo) var isRoot = IsRootPath(inventoryPart, fileInfo); var entryKind = PosixFileTypeClassifier.ClassifyPosixEntry(fileInfo.FullName); + if (entryKind == FileSystemEntryKind.Symlink) + { + RecordSkippedEntry(inventoryPart, fileInfo, SkipReason.Symlink, FileSystemEntryKind.Symlink); + + return; + } + if (IsPosixSpecialFile(entryKind)) { AddPosixSpecialFileAndLog(inventoryPart, fileInfo, entryKind); @@ -384,21 +394,40 @@ private bool ShouldIgnoreHiddenDirectory(DirectoryInfo directoryInfo) if (!isRoot && ShouldIgnoreHiddenFile(fileInfo)) { + RecordSkippedEntry(inventoryPart, fileInfo, SkipReason.Hidden, FileSystemEntryKind.RegularFile); + return; } - if (!isRoot && ShouldIgnoreSystemFile(fileInfo)) + if (!isRoot) { - return; + var systemSkipReason = GetSystemSkipReason(fileInfo); + if (systemSkipReason.HasValue) + { + RecordSkippedEntry(inventoryPart, fileInfo, systemSkipReason.Value, FileSystemEntryKind.RegularFile); + + return; + } } if (IsReparsePoint(fileInfo)) { + RecordSkippedEntry(inventoryPart, fileInfo, SkipReason.Symlink, FileSystemEntryKind.Symlink); + + return; + } + + if (!FileSystemInspector.Exists(fileInfo)) + { + RecordSkippedEntry(inventoryPart, fileInfo, SkipReason.NotFound); + return; } - if (!FileSystemInspector.Exists(fileInfo) || FileSystemInspector.IsOffline(fileInfo) || IsRecallOnDataAccess(fileInfo)) + if (FileSystemInspector.IsOffline(fileInfo) || IsRecallOnDataAccess(fileInfo)) { + RecordSkippedEntry(inventoryPart, fileInfo, SkipReason.Offline, FileSystemEntryKind.RegularFile); + return; } @@ -410,17 +439,17 @@ private bool ShouldIgnoreHiddenDirectory(DirectoryInfo directoryInfo) } catch (UnauthorizedAccessException ex) { - AddInaccessibleFileAndLog(inventoryPart, fileInfo, ex, + AddInaccessibleFileAndLog(inventoryPart, fileInfo, SkipReason.Inaccessible, ex, "File {File} is inaccessible and will be skipped"); } catch (DirectoryNotFoundException ex) { - AddInaccessibleFileAndLog(inventoryPart, fileInfo, ex, + AddInaccessibleFileAndLog(inventoryPart, fileInfo, SkipReason.NotFound, ex, "File {File} parent directory not found and will be skipped"); } catch (IOException ex) { - AddInaccessibleFileAndLog(inventoryPart, fileInfo, ex, + AddInaccessibleFileAndLog(inventoryPart, fileInfo, SkipReason.IoError, ex, "File {File} IO error and will be skipped"); } } @@ -440,8 +469,25 @@ private void DoAnalyze(InventoryPart inventoryPart, DirectoryInfo directoryInfo, return; } + var entryKind = PosixFileTypeClassifier.ClassifyPosixEntry(directoryInfo.FullName); + if (entryKind == FileSystemEntryKind.Symlink) + { + RecordSkippedEntry(inventoryPart, directoryInfo, SkipReason.Symlink, FileSystemEntryKind.Symlink); + + return; + } + + if (IsPosixSpecialFile(entryKind)) + { + RecordSkippedEntry(inventoryPart, directoryInfo, SkipReason.SpecialPosixFile, entryKind); + + return; + } + if (!IsRootPath(inventoryPart, directoryInfo) && ShouldIgnoreHiddenDirectory(directoryInfo)) { + RecordSkippedEntry(inventoryPart, directoryInfo, SkipReason.Hidden, FileSystemEntryKind.Directory); + return; } @@ -459,16 +505,19 @@ private void DoAnalyze(InventoryPart inventoryPart, DirectoryInfo directoryInfo, catch (UnauthorizedAccessException ex) { directoryDescription.IsAccessible = false; + RecordSkippedEntry(inventoryPart, directoryInfo, SkipReason.Inaccessible); _logger.LogWarning(ex, "Directory {Directory} is inaccessible and will be skipped", directoryInfo.FullName); } catch (DirectoryNotFoundException ex) { directoryDescription.IsAccessible = false; + RecordSkippedEntry(inventoryPart, directoryInfo, SkipReason.NotFound); _logger.LogWarning(ex, "Directory {Directory} not found during enumeration and will be skipped", directoryInfo.FullName); } catch (IOException ex) { directoryDescription.IsAccessible = false; + RecordSkippedEntry(inventoryPart, directoryInfo, SkipReason.IoError); _logger.LogWarning(ex, "Directory {Directory} IO error and will be skipped", directoryInfo.FullName); } } @@ -490,28 +539,28 @@ private bool ShouldIgnoreHiddenFile(FileInfo fileInfo) return false; } - private bool ShouldIgnoreSystemFile(FileInfo fileInfo) + private SkipReason? GetSystemSkipReason(FileInfo fileInfo) { if (!IgnoreSystem) { - return false; + return null; } if (FileSystemInspector.IsNoiseFileName(fileInfo, OSPlatform)) { _logger.LogInformation("File {File} is ignored because considered as noise", fileInfo.FullName); - return true; + return SkipReason.NoiseFile; } if (FileSystemInspector.IsSystemAttribute(fileInfo)) { _logger.LogInformation("File {File} is ignored because considered as system", fileInfo.FullName); - return true; + return SkipReason.SystemAttribute; } - return false; + return null; } private bool IsReparsePoint(FileInfo fileInfo) @@ -545,7 +594,8 @@ private bool IsRecallOnDataAccess(FileInfo fileInfo) return FileSystemInspector.IsRecallOnDataAccess(fileInfo); } - private void AddInaccessibleFileAndLog(InventoryPart inventoryPart, FileInfo fileInfo, Exception ex, string message) + private void AddInaccessibleFileAndLog(InventoryPart inventoryPart, FileInfo fileInfo, SkipReason reason, + Exception ex, string message) { inventoryPart.IsIncompleteDueToAccess = true; var relativePath = BuildRelativePath(inventoryPart, fileInfo); @@ -554,6 +604,7 @@ private void AddInaccessibleFileAndLog(InventoryPart inventoryPart, FileInfo fil IsAccessible = false }; AddFileSystemDescription(inventoryPart, fileDescription); + RecordSkippedEntry(inventoryPart, fileInfo, reason); _logger.LogWarning(ex, message, fileInfo.FullName); } @@ -566,6 +617,7 @@ private void AddPosixSpecialFileAndLog(InventoryPart inventoryPart, FileInfo fil IsAccessible = false }; AddFileSystemDescription(inventoryPart, fileDescription); + RecordSkippedEntry(inventoryPart, fileInfo, SkipReason.SpecialPosixFile, entryKind); _logger.LogWarning("File {File} is a POSIX special file ({EntryKind}) and will be skipped", fileInfo.FullName, entryKind); } @@ -578,6 +630,16 @@ FileSystemEntryKind.Fifo or FileSystemEntryKind.Socket; } + private string BuildRelativePath(InventoryPart inventoryPart, FileSystemInfo fileSystemInfo) + { + return fileSystemInfo switch + { + FileInfo fileInfo => BuildRelativePath(inventoryPart, fileInfo), + DirectoryInfo directoryInfo => BuildRelativePath(inventoryPart, directoryInfo), + _ => string.Empty + }; + } + private string BuildRelativePath(InventoryPart inventoryPart, FileInfo fileInfo) { if (inventoryPart.InventoryPartType != FileSystemTypes.Directory) @@ -598,6 +660,51 @@ private string BuildRelativePath(InventoryPart inventoryPart, FileInfo fileInfo) return normalizedPath; } + private string BuildRelativePath(InventoryPart inventoryPart, DirectoryInfo directoryInfo) + { + if (inventoryPart.InventoryPartType != FileSystemTypes.Directory) + { + return "/" + directoryInfo.Name; + } + + var rawRelativePath = IOUtils.ExtractRelativePath(directoryInfo.FullName, inventoryPart.RootPath); + var normalizedPath = OSPlatform == OSPlatforms.Windows + ? rawRelativePath.Replace(Path.DirectorySeparatorChar, IdentityBuilder.GLOBAL_DIRECTORY_SEPARATOR) + : rawRelativePath; + + if (!normalizedPath.StartsWith(IdentityBuilder.GLOBAL_DIRECTORY_SEPARATOR)) + { + normalizedPath = IdentityBuilder.GLOBAL_DIRECTORY_SEPARATOR + normalizedPath; + } + + return normalizedPath; + } + + private void RecordSkippedEntry(InventoryPart inventoryPart, FileSystemInfo fileSystemInfo, SkipReason reason, + FileSystemEntryKind? detectedKind = null) + { + string relativePath; + try + { + relativePath = BuildRelativePath(inventoryPart, fileSystemInfo); + } + catch (Exception) + { + relativePath = string.Empty; + } + + var entry = new SkippedEntry + { + FullPath = fileSystemInfo.FullName, + RelativePath = relativePath, + Name = fileSystemInfo.Name, + Reason = reason, + DetectedKind = detectedKind + }; + + InventoryProcessData.RecordSkippedEntry(entry); + } + private void AddFileSystemDescription(InventoryPart inventoryPart, FileSystemDescription fileSystemDescription) { if (fileSystemDescription.RelativePath.IsNotEmpty() From f2d9dfe261818725b92dc522286e83fe52d9ce9d Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Tue, 3 Feb 2026 11:25:53 +0100 Subject: [PATCH 05/12] Add unit tests for skipped entry defaults --- .../Models/Inventories/SkippedEntryTests.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/ByteSync.Client.UnitTests/Models/Inventories/SkippedEntryTests.cs diff --git a/tests/ByteSync.Client.UnitTests/Models/Inventories/SkippedEntryTests.cs b/tests/ByteSync.Client.UnitTests/Models/Inventories/SkippedEntryTests.cs new file mode 100644 index 00000000..75380073 --- /dev/null +++ b/tests/ByteSync.Client.UnitTests/Models/Inventories/SkippedEntryTests.cs @@ -0,0 +1,56 @@ +using ByteSync.Business.Inventories; +using ByteSync.Models.Inventories; +using FluentAssertions; +using NUnit.Framework; + +namespace ByteSync.Client.UnitTests.Models.Inventories; + +[TestFixture] +public class SkippedEntryTests +{ + [Test] + public void Constructor_ShouldSetDefaults() + { + // Arrange + var before = DateTime.UtcNow; + + // Act + var entry = new SkippedEntry(); + var after = DateTime.UtcNow; + + // Assert + entry.FullPath.Should().BeEmpty(); + entry.RelativePath.Should().BeEmpty(); + entry.Name.Should().BeEmpty(); + entry.Reason.Should().Be(SkipReason.Unknown); + entry.DetectedKind.Should().BeNull(); + entry.SkippedAt.Should().BeOnOrAfter(before); + entry.SkippedAt.Should().BeOnOrBefore(after); + } + + [Test] + public void InitProperties_ShouldSetValues() + { + // Arrange + var skippedAt = new DateTime(2025, 12, 1, 10, 30, 0, DateTimeKind.Utc); + + // Act + var entry = new SkippedEntry + { + FullPath = "/data/test.txt", + RelativePath = "/test.txt", + Name = "test.txt", + Reason = SkipReason.Hidden, + DetectedKind = FileSystemEntryKind.RegularFile, + SkippedAt = skippedAt + }; + + // Assert + entry.FullPath.Should().Be("/data/test.txt"); + entry.RelativePath.Should().Be("/test.txt"); + entry.Name.Should().Be("test.txt"); + entry.Reason.Should().Be(SkipReason.Hidden); + entry.DetectedKind.Should().Be(FileSystemEntryKind.RegularFile); + entry.SkippedAt.Should().Be(skippedAt); + } +} From 7ea295b93e6ae4e1d5f15f2df8d2a1a64cc696e5 Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Tue, 3 Feb 2026 11:32:16 +0100 Subject: [PATCH 06/12] Update inventory builder inspector tests for system detection --- .../InventoryBuilderInspectorTests.cs | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs b/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs index bbf9aee2..1476e2e7 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs @@ -79,7 +79,8 @@ public async Task Hidden_Root_File_Is_Analyzed() { var insp = new Mock(MockBehavior.Strict); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(true); - insp.Setup(i => i.IsSystem(It.IsAny())).Returns(false); + insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(false); insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); insp.Setup(i => i.Exists(It.IsAny())).Returns(true); insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); @@ -102,7 +103,8 @@ public async Task System_Root_File_Is_Analyzed() { var insp = new Mock(MockBehavior.Strict); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); - insp.Setup(i => i.IsSystem(It.IsAny())).Returns(true); + insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(true); insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); insp.Setup(i => i.Exists(It.IsAny())).Returns(true); insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); @@ -126,7 +128,8 @@ public async Task Hidden_Root_Directory_Is_Analyzed() var insp = new Mock(MockBehavior.Strict); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(true); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); - insp.Setup(i => i.IsSystem(It.IsAny())).Returns(false); + insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(false); insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); insp.Setup(i => i.Exists(It.IsAny())).Returns(true); insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); @@ -155,7 +158,8 @@ public async Task Hidden_Child_File_Is_Ignored() .Returns(true); insp.Setup(i => i.IsHidden(It.Is(fi => fi.Name != "hidden.txt"), It.IsAny())) .Returns(false); - insp.Setup(i => i.IsSystem(It.IsAny())).Returns(false); + insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(false); insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); insp.Setup(i => i.Exists(It.IsAny())).Returns(true); insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); @@ -182,8 +186,9 @@ public async Task System_Child_File_Is_Ignored() var insp = new Mock(MockBehavior.Strict); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); - insp.Setup(i => i.IsSystem(It.Is(fi => fi.Name == "system.txt"))).Returns(true); - insp.Setup(i => i.IsSystem(It.Is(fi => fi.Name != "system.txt"))).Returns(false); + insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsSystemAttribute(It.Is(fi => fi.Name == "system.txt"))).Returns(true); + insp.Setup(i => i.IsSystemAttribute(It.Is(fi => fi.Name != "system.txt"))).Returns(false); insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); insp.Setup(i => i.Exists(It.IsAny())).Returns(true); insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); @@ -209,7 +214,8 @@ public async Task Reparse_File_Is_Ignored() { var insp = new Mock(MockBehavior.Strict); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); - insp.Setup(i => i.IsSystem(It.IsAny())).Returns(false); + insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(false); insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(true); var builder = CreateBuilder(insp.Object); @@ -229,7 +235,8 @@ public async Task ExistsFalse_File_Is_Ignored() { var insp = new Mock(MockBehavior.Strict); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); - insp.Setup(i => i.IsSystem(It.IsAny())).Returns(false); + insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(false); insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); insp.Setup(i => i.Exists(It.IsAny())).Returns(false); insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); @@ -258,7 +265,8 @@ public async Task UnauthorizedAccess_Adds_Inaccessible_FileDescription() // File access triggers UnauthorizedAccess inside DoAnalyze(FileInfo) try/catch insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())) .Throws(new UnauthorizedAccessException("denied")); - insp.Setup(i => i.IsSystem(It.IsAny())).Returns(false); + insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(false); insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); insp.Setup(i => i.Exists(It.IsAny())).Returns(true); insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); @@ -287,7 +295,8 @@ public async Task DirectoryNotFound_Adds_Inaccessible_FileDescription() insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())) .Throws(new DirectoryNotFoundException("parent missing")); - insp.Setup(i => i.IsSystem(It.IsAny())).Returns(false); + insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(false); insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); insp.Setup(i => i.Exists(It.IsAny())).Returns(true); insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); @@ -317,7 +326,8 @@ public async Task IOException_Adds_Inaccessible_FileDescription() insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())) .Throws(new IOException("io error")); - insp.Setup(i => i.IsSystem(It.IsAny())).Returns(false); + insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(false); insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); insp.Setup(i => i.Exists(It.IsAny())).Returns(true); insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); @@ -345,7 +355,8 @@ public async Task Directory_IOException_Marked_Inaccessible_And_Skipped() { var insp = new Mock(MockBehavior.Strict); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); - insp.Setup(i => i.IsSystem(It.IsAny())).Returns(false); + insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(false); insp.Setup(i => i.Exists(It.IsAny())).Returns(true); insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); insp.Setup(i => i.IsRecallOnDataAccess(It.IsAny())).Returns(false); @@ -384,7 +395,8 @@ public async Task Directory_ReparsePoint_Is_Skipped() { var insp = new Mock(MockBehavior.Strict); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); - insp.Setup(i => i.IsSystem(It.IsAny())).Returns(false); + insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(false); insp.Setup(i => i.Exists(It.IsAny())).Returns(true); insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); insp.Setup(i => i.IsRecallOnDataAccess(It.IsAny())).Returns(false); @@ -414,3 +426,4 @@ public async Task Directory_ReparsePoint_Is_Skipped() part.FileDescriptions[0].RelativePath.Should().Be("/ok.txt"); } } + From 9f4a8841f34f8e78981c325692852ab5e927d534 Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Tue, 3 Feb 2026 11:56:59 +0100 Subject: [PATCH 07/12] chore: improve skill --- skills/dist/perform-implement-plan.skill | Bin 0 -> 1034 bytes skills/dist/plan-implementation.skill | Bin 954 -> 0 bytes .../SKILL.md | 10 ++++++---- 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 skills/dist/perform-implement-plan.skill delete mode 100644 skills/dist/plan-implementation.skill rename skills/{plan-implementation => perform-implement-plan}/SKILL.md (81%) diff --git a/skills/dist/perform-implement-plan.skill b/skills/dist/perform-implement-plan.skill new file mode 100644 index 0000000000000000000000000000000000000000..e5b551173f4b0eee5a8d0817f7ffbb479e2b30a3 GIT binary patch literal 1034 zcmWIWW@Zs#U|`^2cpvK=Gc)~|P!=--!!kAo26>=pL26N2eo?M&W^O@FYHn&?iEcqo zVxE4mx2KPfUT#Wm$mzb@76N--YfH`W=JAkwvT^!O%abY+EH`*3y?J-l+cheEOD-iZl390t z;v&mW_u3}i>9k1QT4?t6#ugFH&pxw*|9-3ZTD@_i@XHei9&X>UxAW9%*<6X786^fc z-gWjHzn)W&96hJ&#(cHzt(t-B&Kl-?^n2xa_JU)jOm0`{Gc#tUw^KJS6N)Wk7I~fb zV)KH^3N@x}`7h?%9^d=a`MT|&nUkCraZ9f#30$%K!On}pMas83t2E?U$Tl z=*%=Taf@DYjK0cF!vO8KPG?HqZ@*;sC0N@0Z@m7xceU3-?WMm*6-A^4o#XU9EckC~ z0LNabiBlsSGj6VQ`giGsZKTpH-d9uyN-WXi{b8WHss>qnfU(*~5wwhGWwLCj5u3~xB!&|3fzb}+@O1KhX z$YOK&OVZbm|K8`vPoEt5WfGHV(|N6(-xto3-xd6E-d2a6mEy0d$Mx# zmsM3+i^c9vZaOX^DjV)w;`8ELo6fOEGD{zLz5UI$$?NC!MKAxEtXX?TG2!K|^M-FO z+N4j8U8X)&@{-cVs&`$dLJ!-Cc&)p;tI*LtDazxzPr*(D(>YU1UtYRh5t1Bmt@9&u qfHxzP2m|hn3e2luu&oh95y-Xy-mJj<&A`A2guXy}IWWgFFaQ7tJn2aQ literal 0 HcmV?d00001 diff --git a/skills/dist/plan-implementation.skill b/skills/dist/plan-implementation.skill deleted file mode 100644 index d7ba9487c7ce5e031cee579a72c4f494fdb28751..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 954 zcmWIWW@Zs#U|`^2I34L6GhN_YqyjSo13w!BgA7o#ASW?TH#4^&Cp9-UuOzV~Ge1v1 z*xS>`M=v)eH{^8QZ3BTlueHTaEM?OX&AsHEmpNO^v2ojr&sno#Zn|g$Jw3eb-@eUL zWYshMC#`t;?#-Js+j4JLn0_$r_4|2^Z}H>C+OV5!_a*$SZUx?3|MJ^`T`6i$UuH(^ z^?Ktw+qs)P(!cWbm$FlTYCp|V>R-5cerM+Xr$6I8WjvSucCnmudEz3&Pv_bu-Eo<- zDav~Gwwz}|nx8#hue6(f;@9#UCpt?~9(<6#6X*W)>$Gck+D|NLKKN0ngZE=%%3R^R z+^%53|7`~!-JYTmeq6%r#ME8OSe4JR)=q6I_kWq8r}yG?-{W(i_GP~N*B_;Lp3|Vi zV&aA+4=Ud|ob1hEk89$8Epdf$otT9CpJMlfunFHUt(aoz^iHeqzT&;_dT*XSZi>@= z5&L0p@8OA>vTk{^@|_gj)O!BsrybbdH|=-dyI)c!^VL2ndi#5xl3TvA+5Pp(dvT{; za4b0aO!(#skyNg`JugfJJjMT<51E&_rjq0Ho_aB>6~C>*?N917Ca(U|7VK`%@IpfC z)zXDacx?Du=A>@dytZNUyvwDRw}jRkF@L#wn>p@oiSKH;qux)ZuU5VHcA=f%f)c)s zbu$)B-m~ClDBFDf@Drt*xx-ARE?)FRL}gY(C6`3_(v8y`?##WuZpLGVAT6JLJlP)i zzg>0z+QDz;*r~tccFAeuMd9o9*GHxL%)a1rJ9@h1INF z|4$#Sxu<@tQs>U+6N)n(E!A_S`Yx<*X?ihRF0@m$W7=e|C;ENc=BEW-X$;W!vr=_F zvU}B)s!i>Fnfaf^CMYWA8nz0*U~CJPKGy2b)1dDC%`w?_hF0C{hpk#-=HE-VDy)dw z6LbBPj<jeuo4<&mqvhq%v&V0rx8GOZ9I2zK-o})8HSb>gsxkrJqt(}% zH2p+*J}p~ZU}v%Mf$yPPtG2HC;mRlUVOxax;iy}i0K4|)aTUviw z)~dvJFV7~aJxDZ+isWFPz3ivdVLmN8zu#MyuIORADZFy|jCW@Hj!z@4{%*$WJ|HG(L7IW54O6__6x7#M-j M3rI%+b0`A?03%tuY5)KL diff --git a/skills/plan-implementation/SKILL.md b/skills/perform-implement-plan/SKILL.md similarity index 81% rename from skills/plan-implementation/SKILL.md rename to skills/perform-implement-plan/SKILL.md index be14b55a..5881b437 100644 --- a/skills/plan-implementation/SKILL.md +++ b/skills/perform-implement-plan/SKILL.md @@ -1,6 +1,6 @@ --- -name: plan-implementation -description: Execute an implementation plan end-to-end in the ByteSync repo. Use when the user asks to implement a previously established plan step by step, requires creating a branch before coding, and expects regular commits during progress. +name: perform-implement-plan +description: Execute an implementation plan end-to-end in the ByteSync repo. Use when the user asks to implement a previously established plan step by step, requires creating a branch before coding, expects regular commits during progress, and wants build/test validation. --- # Plan Implementation @@ -32,13 +32,15 @@ Implement the plan in small, ordered steps. After each step: - Commit the work with a concise English message - Keep commits focused and incremental -### 4) Respect repo conventions +### 4) Respect repo conventions and validation Follow the repo guidance (AGENTS.md), including: - Do not add unnecessary comments - Use FluentAssertions for tests -- Build and test as separate commands when asked to run them +- Build and test as separate commands +- During intermediate commits, run only tests related to the changes when reasonable +- At the end, run the full solution tests ### 5) Keep the user informed From 0e0ea6e61f778c20573d3116f55424df3c1728be Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Tue, 3 Feb 2026 11:59:28 +0100 Subject: [PATCH 08/12] chore: add skill --- skills/dist/request-pr.skill | Bin 0 -> 678 bytes skills/request-pr/SKILL.md | 44 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 skills/dist/request-pr.skill create mode 100644 skills/request-pr/SKILL.md diff --git a/skills/dist/request-pr.skill b/skills/dist/request-pr.skill new file mode 100644 index 0000000000000000000000000000000000000000..3cd387103f270987d31e6d4c5fe4969a7a2fb836 GIT binary patch literal 678 zcmWIWW@Zs#U|`^2@QQbixsqunBge$RP{zW*APf{ON-Zo+EiTb5DAEu1_Vn@5%S~ww zIe-4L0ngs&>o;s+p7kT@$okWL2g)5h4009tI9{H%D(}6fcF-^USbYCvo#=^bg)>5q zcmADs|MRLwyy5od+^iqRID*^FLM0_R)pD7g3s&y4nK!j0PS0!SS!XU~ z2j>$n-&QdnY>^92YZLEjb+3NGTfTKw%bci~g=@@(bRV4Ne3?=Zl;!&_G0is?zcLue73ca}KsqB(TUCS?BW!-b7TDNC$xxvfeRjJmU^VaP(zdbeR>i)xX zj}}{hu;7_>X8wvmMOpWyd)1dK|Lb_zTd=D_QHr~sTUMrKzFK|p$%HkMCG+1u`O;ga zTvUCQk@fcVjto|R`$IP$``=J2Pt|y?zSQo_H}8V{Vx_veL&c4fhtC_S)j78H3$1cL zt^Bm_`nR_9KL0apva{Z8Qf;|s`?w>#Ws1u^+lTjNyS@LVbny046F2_ABBL8xQxC;7 z?%lj;>)Py=WfFGp^MZa Date: Tue, 3 Feb 2026 12:02:46 +0100 Subject: [PATCH 09/12] chore: improve skill --- skills/dist/request-pr.skill | Bin 678 -> 812 bytes skills/request-pr/SKILL.md | 20 +++++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/skills/dist/request-pr.skill b/skills/dist/request-pr.skill index 3cd387103f270987d31e6d4c5fe4969a7a2fb836..e83a72f7cc7da39d4380078a78085f1cc4edeea6 100644 GIT binary patch delta 783 zcmV+q1MvK&1*`^tP)h>@6aWAK2mnrCLtG2st&*ey005~4000vJ0044jadl;LbS-dl zFH=iNOiV6qWOY0u~Vd)U_{x2-L(V#5}s8+KR*Sh49?s4Z!toYedE zqomkz*Y;pQ9~Sxe-XpUt6Iyn-1c!fmB%fI)Dx}gW>y^=eOIW9KustnZ2qw z22f+1hl6o{cXewH6P#yi9f8g3JSZP7$K=nT>uIF0*fj-Qe!0mbrk}^mVRHtr&*9SO zTDfk_N?v*tVmy5UR}e9#V|w}8g?}8M@Yn0obe-}ZS*Awn9a0WA^|)qYJE=62On0Z( z2GS$gB1?#){CPt7=A5hNDpPW1@)@=`OgQBX#$!x>ItgE?3bgS+7E4tR%%?Y|yNB{gDtlJ#Jm+Oc&$ zU4OU=_o>RatV6IhM(Z4x|LLAEGCr^_H31gU3`-%hhi;hd=boUx>!eeEL!42PP@c5b z85!7reT;mT4YDkGZSl(w7JtKS^m2+iEckkBssR?a>0r6h8RU7s(Hs34hcV;)_$zBg zubiU0>3DG>VpK@5)!FBUqt@biRmSw1exa0OlRY7-V(eV`W zXo=gkMWR!qa9dI?{sB-+0Rj{N6aWAK2mnrCLtG2st&*ey005~4000vJ000000001! NbOI~})dBzj0065iW~u-H delta 619 zcmV-x0+ju%2BrlYP)h>@6aWAK2mnf7LtNBpHXI%T004Fb000w`0~UXURL{@TFbuut zuduWOn)q78ugi|WcR)xmZ6FQ^>B~BIjn*c?PFKcXkCQB0ktT7NRL6<^p5ODTs!~=D zxB|joBXX!bsXcN-#s}l<3O4y1{K%cL2M8TPGZLWa&&BaH$^y_j|uyKXh)29 zSWhOze1UQwYn|YHzch`(0$wiRy|b;Mp>*M(11eQc@8F2~rP_ZoSxHm8LvkX&(Kc=v zOb93nb=n?~YxvlfZ<*4t%Xa!F{!r9|b*d}*nDCZ}o zKFsz@aBp)WetyDpff>WkGA4dOhK~}eN6RDSj@R~vXO2(I1|6F2vL%GxJ>!U1gp@?y zJ>lM)M(_F}!P})YMh{VPGT17W!d!v9v$D0VYlNm4KJPqkQuukSeEHuH51pLZ$2rUR zNLDJKXE9)p%(p3t(YRKB0Z>Z;0u%rg0000807_p&T-0ec93BDy0CohE?*calPXYh{ F006g-7kmH! diff --git a/skills/request-pr/SKILL.md b/skills/request-pr/SKILL.md index c3e7977f..4b91e6a5 100644 --- a/skills/request-pr/SKILL.md +++ b/skills/request-pr/SKILL.md @@ -7,7 +7,7 @@ description: Request pushing the current branch and creating a Pull Request for ## Overview -Provide a short workflow to ask for pushing the current branch and opening a Pull Request with an English title and description. +Provide a short workflow to push the current branch and open a Pull Request with an English title and description using the `gh` CLI. ## Workflow @@ -17,18 +17,28 @@ Confirm the current branch name and whether there are uncommitted changes. If ch ### 2) Push branch -Request the user to push the current branch to the remote. +Push the current branch to the remote. If the user did not specify a tool, default to using the `gh` CLI context and run a normal git push for the current branch. + +Example: +```bash +git push -u origin HEAD +``` ### 3) Create PR -Request creation of a Pull Request with: +Create a Pull Request using `gh pr create` with: - English title - English description summarizing changes and approach +Example: +```bash +gh pr create --title "[type] Short summary" --body "Summary:\n- ...\n\nKey changes:\n- ...\n\nNotes/risks:\n- ..." +``` + ### 4) Provide PR text template -Provide a minimal PR template the user can paste: +Provide a minimal PR template to pass to `gh pr create`: Title: `[type] Short summary` @@ -41,4 +51,4 @@ Ensure the template is in English and matches the repo's PR format when applicab ### 5) Keep it brief -Keep the response focused on the push + PR request, avoiding extra steps unless the user asks. +Keep the response focused on the push + PR actions, avoiding extra steps unless the user asks. From 02d922a17950512566757216dfe506dae6159a91 Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Tue, 3 Feb 2026 12:06:09 +0100 Subject: [PATCH 10/12] chore: improve skill --- skills/{request-pr => create-pr}/SKILL.md | 4 ++-- skills/dist/create-pr.skill | Bin 0 -> 809 bytes skills/dist/request-pr.skill | Bin 812 -> 0 bytes 3 files changed, 2 insertions(+), 2 deletions(-) rename skills/{request-pr => create-pr}/SKILL.md (85%) create mode 100644 skills/dist/create-pr.skill delete mode 100644 skills/dist/request-pr.skill diff --git a/skills/request-pr/SKILL.md b/skills/create-pr/SKILL.md similarity index 85% rename from skills/request-pr/SKILL.md rename to skills/create-pr/SKILL.md index 4b91e6a5..fd28f3b4 100644 --- a/skills/request-pr/SKILL.md +++ b/skills/create-pr/SKILL.md @@ -1,6 +1,6 @@ --- -name: request-pr -description: Request pushing the current branch and creating a Pull Request for ByteSync. Use when the user wants to push a branch and open a PR, and requires the PR title and description in English. +name: create-pr +description: Push the current branch and create a Pull Request for ByteSync. Use when the user wants the agent to push a branch and open a PR, and requires the PR title and description in English. --- # Request PR diff --git a/skills/dist/create-pr.skill b/skills/dist/create-pr.skill new file mode 100644 index 0000000000000000000000000000000000000000..4413b2a8fcbfb08e0cf81c71af3e5cb7eb10c345 GIT binary patch literal 809 zcmWIWW@Zs#U|`^2s7P>*5j*jA&0;17hNY|w3_?KB+?3J~ z-~8JKJbPb<3%pF!2&=wvU3L3f-3>{H#g<5yMX*I|%#2>4o4IJtmET{NF0m-Q_)Xx* z6qipQKTeFC#;IE!dYEp(V&-8zMguax`r+5;C7Cu4`j0Vqt#1qHeI=TkX6P0 zl~u&!{fsA_b7gAeZ145$I`dFm@lEs$1?_pfWRAcK<)(=;t zo+swtiM>1Xui!auQ~v|cMBh*4pC0^4`~vTi65+#QzR?GsZl7(F^sDxSOpkFzHQ#i% z>!-d3hkf$dByjnq@eU{EWGl5<>tib(|7fXi*7Lou{eI%@t6p~%o)!fA-}lMUpPDx5 z<+~@j3YG`OcFvUenee94>Bl>pP9@cJwfKbY!!5z*5^Rnwapw$K#guC^;b_oR<0i7DSI-cMF&yTFxdk@Y5W!o6UXp5MRXzHK|aRHG|Nd!?xR z^c;rAA%`#hEc*LW>&|km`^EB;_B5C1%@3_QbmGT>U55=s0(ra@HyzKKakJR>!iDdb zR{mE!cV$w*ye-!xC2!Pr)lB2vq?4KZ@p!-3|3kS~in^@yepF?z5)|A1GGXUM$tRWj zE=vCuJ+Arq&kyVGUH2?p%a1=k+Bt>)&acCFS`>NCemL=Ak%PbRRqeG-@sDPg$R76+ zYW;t_#W397nb%umo_3RQ{Mm1UpDjcW&;P7+V@`AWGZS8&dkfl?xm-7^3eJ4DRR8wj zq{57|+FTo6E9lG@6aT&_cHhLx-g48lOsqSV6D)Z!A|f+GE3Z%-c|z1)=2 z5a0aU20VLThYP$+)CjA-ab0!$THOsvhsBmimqoBeY+riRaU!^iXQeA7wCC%4UFM3BTFWz8l>h=DBIY;VynJoTj_0pRS zS45}qE#?0%6jpoN?|jzd2ag|oEW41(XV3BQ!h*iQDkpaKm9~xMhIaPzT1}?aKPhbTDjYL+T-zNMIJvv0TR2^K%GFur=^t|Y^VWY*IC5&{-c=TC z0m*aPn+wZJv-yPQq-V)Co_%ejA+LA*$@vAZGnZ_*Vagg)xAUa(viZl)Bo?!+KmYL3 zi>RG>XT0L7M3QW3nZgAOmnB#IywzGSGylW&*OyA>ev*q^=JrZ9h|m1%jAR-ExGxwd?C5c3Y@KeOC? z+1`X*SvR-a+0>p9YrZ7+zShV^KIFo&(oL;%y~KLks(V@r3}*edVd`Yn(Tjy1kZ}{{Q7X10%Z)dZvtG7kRuDH?`-@c>8?bf_<-- z1lK#y37ix#Z_71F$s4sjHPd)E=~U)^Jk2jwe>nF`QJ0m@kE-ldjAGkgChWW@`J{5+ zMd`ni?S_y4{Ls#JT|Z~h?w1vXN>63I>)*`2(BjC`&!aE(PdHAls;KiP249PxvN;8cH9 Date: Tue, 3 Feb 2026 12:10:13 +0100 Subject: [PATCH 11/12] chore: improve skill --- skills/create-pr/SKILL.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/skills/create-pr/SKILL.md b/skills/create-pr/SKILL.md index fd28f3b4..6deda94b 100644 --- a/skills/create-pr/SKILL.md +++ b/skills/create-pr/SKILL.md @@ -31,11 +31,6 @@ Create a Pull Request using `gh pr create` with: - English title - English description summarizing changes and approach -Example: -```bash -gh pr create --title "[type] Short summary" --body "Summary:\n- ...\n\nKey changes:\n- ...\n\nNotes/risks:\n- ..." -``` - ### 4) Provide PR text template Provide a minimal PR template to pass to `gh pr create`: From 8f257bd098b033b3eb0b4f5d263586ad8b1b201b Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Tue, 3 Feb 2026 14:41:51 +0100 Subject: [PATCH 12/12] Add tests for skipped entries and error reporting --- .../Inventories/InventoryProcessDataTests.cs | 28 +++ .../InventoryBuilderInspectorTests.cs | 161 +++++++++++++++++- 2 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 tests/ByteSync.Client.UnitTests/Business/Inventories/InventoryProcessDataTests.cs diff --git a/tests/ByteSync.Client.UnitTests/Business/Inventories/InventoryProcessDataTests.cs b/tests/ByteSync.Client.UnitTests/Business/Inventories/InventoryProcessDataTests.cs new file mode 100644 index 00000000..d0005a5b --- /dev/null +++ b/tests/ByteSync.Client.UnitTests/Business/Inventories/InventoryProcessDataTests.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using ByteSync.Business.Inventories; +using FluentAssertions; +using NUnit.Framework; + +namespace ByteSync.Client.UnitTests.Business.Inventories; + +[TestFixture] +public class InventoryProcessDataTests +{ + [Test] + public void SetError_ShouldUpdateLastException_AndRaiseEvent() + { + // Arrange + var data = new InventoryProcessData(); + var values = new List(); + data.ErrorEvent.Subscribe(values.Add); + var exception = new InvalidOperationException("boom"); + + // Act + data.SetError(exception); + + // Assert + data.LastException.Should().Be(exception); + values.Should().Contain(true); + } +} diff --git a/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs b/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs index 1476e2e7..9a2710b1 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs @@ -6,6 +6,7 @@ using ByteSync.Common.Business.EndPoints; using ByteSync.Common.Business.Misc; using ByteSync.Interfaces.Controls.Inventories; +using ByteSync.Models.Inventories; using ByteSync.Services.Inventories; using ByteSync.TestsCommon; using FluentAssertions; @@ -36,7 +37,8 @@ public void TearDown() _manualResetEvents.Clear(); } - private InventoryBuilder CreateBuilder(IFileSystemInspector inspector) + private InventoryBuilder CreateBuilder(IFileSystemInspector inspector, InventoryProcessData? processData = null, + IPosixFileTypeClassifier? posixFileTypeClassifier = null) { var endpoint = new ByteSyncEndpoint { @@ -59,7 +61,7 @@ private InventoryBuilder CreateBuilder(IFileSystemInspector inspector) var dataNode = new DataNode { Id = Guid.NewGuid().ToString("N"), ClientInstanceId = endpoint.ClientInstanceId, Code = "A", OrderIndex = 0 }; var settings = SessionSettings.BuildDefault(); - var processData = new InventoryProcessData(); + processData ??= new InventoryProcessData(); var logger = new Mock>().Object; var analyzer = new Mock(); @@ -71,7 +73,15 @@ private InventoryBuilder CreateBuilder(IFileSystemInspector inspector) var indexer = new Mock().Object; return new InventoryBuilder(sessionMember, dataNode, settings, processData, endpoint.OSPlatform, - FingerprintModes.Rsync, logger, analyzer.Object, saver, indexer, inspector); + FingerprintModes.Rsync, logger, analyzer.Object, saver, indexer, inspector, posixFileTypeClassifier); + } + + private (InventoryBuilder Builder, InventoryProcessData ProcessData) CreateBuilderWithData(IFileSystemInspector inspector, + IPosixFileTypeClassifier? posixFileTypeClassifier = null) + { + var processData = new InventoryProcessData(); + var builder = CreateBuilder(inspector, processData, posixFileTypeClassifier); + return (builder, processData); } [Test] @@ -208,6 +218,151 @@ public async Task System_Child_File_Is_Ignored() var part = builder.Inventory.InventoryParts.Single(); part.FileDescriptions.Should().ContainSingle(fd => fd.Name == "visible.txt"); } + + [Test] + public async Task Noise_Child_File_Is_Recorded() + { + var insp = new Mock(MockBehavior.Strict); + insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsNoiseFileName(It.Is(fi => fi.Name == "thumbs.db"), It.IsAny())) + .Returns(true); + insp.Setup(i => i.IsNoiseFileName(It.Is(fi => fi.Name != "thumbs.db"), It.IsAny())) + .Returns(false); + insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(false); + insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); + insp.Setup(i => i.Exists(It.IsAny())).Returns(true); + insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); + insp.Setup(i => i.IsRecallOnDataAccess(It.IsAny())).Returns(false); + var (builder, processData) = CreateBuilderWithData(insp.Object); + + var root = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, "root_noise_child")); + var visiblePath = Path.Combine(root.FullName, "visible.txt"); + var noisePath = Path.Combine(root.FullName, "thumbs.db"); + await File.WriteAllTextAsync(visiblePath, "x"); + await File.WriteAllTextAsync(noisePath, "x"); + + builder.AddInventoryPart(root.FullName); + var invPath = Path.Combine(TestDirectory.FullName, "inv_noise_child.zip"); + await builder.BuildBaseInventoryAsync(invPath); + + processData.SkippedEntries.Should() + .ContainSingle(e => e.Name == "thumbs.db" && e.Reason == SkipReason.NoiseFile); + } + + [Test] + public async Task System_Child_File_Is_Recorded() + { + var insp = new Mock(MockBehavior.Strict); + insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsSystemAttribute(It.Is(fi => fi.Name == "system.txt"))).Returns(true); + insp.Setup(i => i.IsSystemAttribute(It.Is(fi => fi.Name != "system.txt"))).Returns(false); + insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); + insp.Setup(i => i.Exists(It.IsAny())).Returns(true); + insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); + insp.Setup(i => i.IsRecallOnDataAccess(It.IsAny())).Returns(false); + var (builder, processData) = CreateBuilderWithData(insp.Object); + + var root = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, "root_system_recorded")); + var visiblePath = Path.Combine(root.FullName, "visible.txt"); + var systemPath = Path.Combine(root.FullName, "system.txt"); + await File.WriteAllTextAsync(visiblePath, "x"); + await File.WriteAllTextAsync(systemPath, "x"); + + builder.AddInventoryPart(root.FullName); + var invPath = Path.Combine(TestDirectory.FullName, "inv_system_recorded.zip"); + await builder.BuildBaseInventoryAsync(invPath); + + processData.SkippedEntries.Should() + .ContainSingle(e => e.Name == "system.txt" && e.Reason == SkipReason.SystemAttribute); + } + + [Test] + public async Task Offline_Root_File_Is_Recorded() + { + var insp = new Mock(MockBehavior.Strict); + insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); + insp.Setup(i => i.Exists(It.IsAny())).Returns(true); + insp.Setup(i => i.IsOffline(It.IsAny())).Returns(true); + insp.Setup(i => i.IsRecallOnDataAccess(It.IsAny())).Returns(false); + var (builder, processData) = CreateBuilderWithData(insp.Object); + + var filePath = Path.Combine(TestDirectory.FullName, "offline.txt"); + await File.WriteAllTextAsync(filePath, "x"); + + builder.AddInventoryPart(filePath); + var invPath = Path.Combine(TestDirectory.FullName, "inv_offline.zip"); + await builder.BuildBaseInventoryAsync(invPath); + + var part = builder.Inventory.InventoryParts.Single(); + part.FileDescriptions.Should().BeEmpty(); + processData.SkippedEntries.Should() + .ContainSingle(e => e.Name == "offline.txt" && e.Reason == SkipReason.Offline); + } + + [Test] + public async Task Posix_Symlink_File_Is_Recorded() + { + var insp = new Mock(MockBehavior.Strict); + var posix = new Mock(MockBehavior.Strict); + posix.Setup(p => p.ClassifyPosixEntry(It.IsAny())).Returns(FileSystemEntryKind.Symlink); + var (builder, processData) = CreateBuilderWithData(insp.Object, posix.Object); + + var filePath = Path.Combine(TestDirectory.FullName, "posix_symlink.txt"); + await File.WriteAllTextAsync(filePath, "x"); + + builder.AddInventoryPart(filePath); + var invPath = Path.Combine(TestDirectory.FullName, "inv_posix_symlink.zip"); + await builder.BuildBaseInventoryAsync(invPath); + + var part = builder.Inventory.InventoryParts.Single(); + part.FileDescriptions.Should().BeEmpty(); + processData.SkippedEntries.Should() + .ContainSingle(e => e.Name == "posix_symlink.txt" && e.Reason == SkipReason.Symlink); + } + + [Test] + public async Task Posix_Special_File_Is_Recorded() + { + var insp = new Mock(MockBehavior.Strict); + var posix = new Mock(MockBehavior.Strict); + posix.Setup(p => p.ClassifyPosixEntry(It.IsAny())).Returns(FileSystemEntryKind.BlockDevice); + var (builder, processData) = CreateBuilderWithData(insp.Object, posix.Object); + + var filePath = Path.Combine(TestDirectory.FullName, "posix_special.txt"); + await File.WriteAllTextAsync(filePath, "x"); + + builder.AddInventoryPart(filePath); + var invPath = Path.Combine(TestDirectory.FullName, "inv_posix_special.zip"); + await builder.BuildBaseInventoryAsync(invPath); + + var part = builder.Inventory.InventoryParts.Single(); + part.FileDescriptions.Should().ContainSingle(fd => !fd.IsAccessible); + processData.SkippedEntries.Should() + .ContainSingle(e => e.Name == "posix_special.txt" && e.Reason == SkipReason.SpecialPosixFile); + } + + [Test] + public async Task Posix_Symlink_Directory_Is_Recorded() + { + var insp = new Mock(MockBehavior.Strict); + var posix = new Mock(MockBehavior.Strict); + posix.Setup(p => p.ClassifyPosixEntry(It.IsAny())).Returns(FileSystemEntryKind.Symlink); + var (builder, processData) = CreateBuilderWithData(insp.Object, posix.Object); + + var root = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, "root_posix_symlink")); + + builder.AddInventoryPart(root.FullName); + var invPath = Path.Combine(TestDirectory.FullName, "inv_posix_symlink_dir.zip"); + await builder.BuildBaseInventoryAsync(invPath); + + var part = builder.Inventory.InventoryParts.Single(); + part.DirectoryDescriptions.Should().BeEmpty(); + processData.SkippedEntries.Should() + .ContainSingle(e => e.Name == "root_posix_symlink" && e.Reason == SkipReason.Symlink); + } [Test] public async Task Reparse_File_Is_Ignored()