Show manual sync-conflict fix instructions in-app instead of deep-linking to Help

NSWorkspace.shared.open() silently drops #fragment anchors on file://
URLs, so "Fix It Myself" always landed on the Help Book index instead
of the relevant section. Replaced with GitSyncManualFixSheet, an
in-app sheet showing the real conflicting filenames and sync path.

Also indent conversation rows one level deeper than their containing
folder in the sidebar and conversation list, so nesting is visible on
the conversations themselves and not just the folder headers.
This commit is contained in:
2026-08-04 12:21:15 +02:00
parent 125e1698f7
commit e2284aba2b
10 changed files with 556 additions and 14 deletions
+67
View File
@@ -285,4 +285,71 @@ struct GitSyncServiceTests {
#expect(decoded.notes[conversationId]?.filename == "Chat-a3f2.md")
#expect(decoded.notes[conversationId]?.enabled == true)
}
// MARK: - parseUntrackedFileConflict
@Test("Parses a single colliding file out of git's untracked-files error")
func parseUntrackedFileConflictSingleFile() {
let message = "error: The following untracked working tree files would be overwritten by merge:\n\tfolders.json\nPlease move or remove them before you merge.\nAborting"
#expect(GitSyncService.parseUntrackedFileConflict(from: message) == ["folders.json"])
}
@Test("Parses multiple colliding files out of git's untracked-files error")
func parseUntrackedFileConflictMultipleFiles() {
let message = "error: The following untracked working tree files would be overwritten by merge:\n\tfolders.json\n\tnotes.json\nPlease move or remove them before you merge.\nAborting"
#expect(GitSyncService.parseUntrackedFileConflict(from: message) == ["folders.json", "notes.json"])
}
@Test("Wrapped SyncError.gitFailed description still parses correctly")
func parseUntrackedFileConflictWrappedMessage() {
let message = "Git command failed: error: The following untracked working tree files would be overwritten by merge:\n\tnotes/Chat-a3f2.md\nPlease move or remove them before you merge.\nAborting"
#expect(GitSyncService.parseUntrackedFileConflict(from: message) == ["notes/Chat-a3f2.md"])
}
@Test("Unrelated git errors return nil, not an empty or bogus file list")
func parseUntrackedFileConflictUnrelatedErrors() {
#expect(GitSyncService.parseUntrackedFileConflict(from: "fatal: Authentication failed for 'https://gitlab.pm/rune/oai-swift.git/'") == nil)
#expect(GitSyncService.parseUntrackedFileConflict(from: "fatal: unable to access: Could not resolve host") == nil)
#expect(GitSyncService.parseUntrackedFileConflict(from: "error: Your local changes to the following files would be overwritten by merge:\n\tconversations/x.md") == nil)
}
// MARK: - isFileSafeToAutoDelete
@Test("Known sync-manifest files are safe to auto-delete")
func isFileSafeToAutoDeleteAllowsKnownFiles() {
#expect(GitSyncService.isFileSafeToAutoDelete("folders.json"))
#expect(GitSyncService.isFileSafeToAutoDelete("notes.json"))
#expect(GitSyncService.isFileSafeToAutoDelete("conversations/my-chat.md"))
#expect(GitSyncService.isFileSafeToAutoDelete("notes/Chat-a3f2.md"))
}
@Test("Path traversal and absolute paths are never safe to auto-delete")
func isFileSafeToAutoDeleteRejectsTraversal() {
#expect(!GitSyncService.isFileSafeToAutoDelete("../etc/passwd"))
#expect(!GitSyncService.isFileSafeToAutoDelete("/etc/passwd"))
#expect(!GitSyncService.isFileSafeToAutoDelete("conversations/../../../etc/passwd"))
}
@Test("Files outside the known shape are never safe to auto-delete")
func isFileSafeToAutoDeleteRejectsUnknownFiles() {
#expect(!GitSyncService.isFileSafeToAutoDelete("README.md"))
#expect(!GitSyncService.isFileSafeToAutoDelete("conversations/sub/x.md"))
#expect(!GitSyncService.isFileSafeToAutoDelete("notes/sub/x.md"))
#expect(!GitSyncService.isFileSafeToAutoDelete(""))
#expect(!GitSyncService.isFileSafeToAutoDelete("conversations/"))
}
// MARK: - PendingGitConflict.canAutoFix
@Test("canAutoFix is true when every file is safe to auto-delete")
func pendingGitConflictCanAutoFixAllSafe() {
let conflict = GitSyncService.PendingGitConflict(files: ["folders.json", "notes.json"], rawError: "")
#expect(conflict.canAutoFix)
}
@Test("canAutoFix is false when any file isn't safe to auto-delete")
func pendingGitConflictCanAutoFixOneUnsafe() {
let conflict = GitSyncService.PendingGitConflict(files: ["folders.json", "README.md"], rawError: "")
#expect(!conflict.canAutoFix)
}
}