2.5.0 #10
@@ -68,6 +68,11 @@ class GitSyncService {
|
|||||||
_ = try await runGit(["clone", url, localPath])
|
_ = try await runGit(["clone", url, localPath])
|
||||||
syncStatus.isCloned = true
|
syncStatus.isCloned = true
|
||||||
|
|
||||||
|
// Import immediately so this machine's DB is never left empty after a clone —
|
||||||
|
// an empty DB is what makes the next export think every existing conversation
|
||||||
|
// was deleted (see exportAllConversations's orphan-cleanup guard).
|
||||||
|
_ = try? await importAllConversations()
|
||||||
|
|
||||||
await updateStatus()
|
await updateStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +207,15 @@ class GitSyncService {
|
|||||||
currentIds: Set<String>,
|
currentIds: Set<String>,
|
||||||
files: [(filename: String, markdown: String)]
|
files: [(filename: String, markdown: String)]
|
||||||
) -> [String] {
|
) -> [String] {
|
||||||
files.compactMap { file in
|
// A locally-empty conversation list is indistinguishable here from "nothing has been
|
||||||
|
// imported into this machine's DB yet" (e.g. right after a fresh clone). Treating it as
|
||||||
|
// "every existing file was deleted" wiped a user's entire sync repo in production: clone
|
||||||
|
// completed, an auto-sync fired before the post-clone import finished, every synced
|
||||||
|
// conversation looked orphaned, and the deletion got committed and pushed. Skipping
|
||||||
|
// cleanup here means a genuine last-conversation deletion won't propagate until another
|
||||||
|
// conversation exists locally — a far smaller cost than mass data loss.
|
||||||
|
guard !currentIds.isEmpty else { return [] }
|
||||||
|
return files.compactMap { file in
|
||||||
guard let export = try? ConversationExport.fromMarkdown(file.markdown) else { return nil }
|
guard let export = try? ConversationExport.fromMarkdown(file.markdown) else { return nil }
|
||||||
return currentIds.contains(export.id) ? nil : file.filename
|
return currentIds.contains(export.id) ? nil : file.filename
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,4 +151,18 @@ struct GitSyncServiceTests {
|
|||||||
func emptyFileListProducesNoOrphans() {
|
func emptyFileListProducesNoOrphans() {
|
||||||
#expect(GitSyncService.orphanedExportFilenames(currentIds: [UUID().uuidString], files: []).isEmpty)
|
#expect(GitSyncService.orphanedExportFilenames(currentIds: [UUID().uuidString], files: []).isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test("Empty local conversation list never flags files as orphaned, even when files exist")
|
||||||
|
func emptyCurrentIdsNeverOrphansExistingFiles() {
|
||||||
|
// Regression test: this is the exact shape of the production bug. A fresh clone (or any
|
||||||
|
// moment where the local DB hasn't caught up yet) has zero local conversations, while the
|
||||||
|
// sync repo still has every previously-synced file on disk. Without the guard, all of them
|
||||||
|
// used to be flagged orphaned and deleted+pushed, wiping the whole sync repo.
|
||||||
|
let files = [
|
||||||
|
(filename: "a.md", markdown: makeExportMarkdown(id: UUID().uuidString)),
|
||||||
|
(filename: "b.md", markdown: makeExportMarkdown(id: UUID().uuidString)),
|
||||||
|
]
|
||||||
|
let orphans = GitSyncService.orphanedExportFilenames(currentIds: [], files: files)
|
||||||
|
#expect(orphans.isEmpty)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user