From be75868b54db943d6fd0f42f821587f00005cc75 Mon Sep 17 00:00:00 2001 From: Sung Date: Sat, 25 Oct 2025 23:08:09 -0700 Subject: [PATCH] Reproduce a bug --- pkg/e2e/sync/regression_test.go | 124 ++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 pkg/e2e/sync/regression_test.go diff --git a/pkg/e2e/sync/regression_test.go b/pkg/e2e/sync/regression_test.go new file mode 100644 index 00000000..feac82ad --- /dev/null +++ b/pkg/e2e/sync/regression_test.go @@ -0,0 +1,124 @@ +/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors + * + * This file is part of Dnote. + * + * Dnote is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Dnote is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Dnote. If not, see . + */ + +package sync + +import ( + "testing" + + "github.com/dnote/dnote/pkg/assert" + "github.com/dnote/dnote/pkg/cli/consts" + cliDatabase "github.com/dnote/dnote/pkg/cli/database" + clitest "github.com/dnote/dnote/pkg/cli/testutils" + "github.com/google/uuid" +) + +// TestSync_LastMaxUSNResetBug reproduces the bug where last_max_usn gets reset to 0 +// when a retry stepSync gets no fragments from the server. +// +// Bug scenario: +// 1. Client syncs and downloads data from server (last_max_usn should be updated to server's max USN) +// 2. Client has a dirty note that fails to upload (e.g., 500 error due to orphaned note) +// 3. Sync triggers retry stepSync because of the upload error +// 4. Retry stepSync queries after_usn= and gets 0 fragments +// 5. processFragments returns maxUSN=0 (initialized to 0, no fragments to process) +// 6. saveSyncState(tx, serverTime, 0) overwrites last_max_usn to 0 +// 7. Transaction commits with last_max_usn=0 instead of server's max USN +func TestSync_LastMaxUSNResetBug(t *testing.T) { + env := setupTestEnv(t) + user := setupUserAndLogin(t, env) + + // Step 1: Create data on server so client has something to download + bookUUID := apiCreateBook(t, env, user, "javascript", "creating book via API") + apiCreateNote(t, env, user, bookUUID, "note1 content", "creating note1 via API") + apiCreateNote(t, env, user, bookUUID, "note2 content", "creating note2 via API") + + // At this point, server has: + // - 1 book (USN=1) + // - 2 notes (USN=2,3) + // - server max_usn=3 + + // Step 2: Create a dirty note on client that references a non-existent book + // This will cause a 500 error when trying to upload because the server + // will try to find the book and fail + orphanedNote := cliDatabase.Note{ + UUID: uuid.New().String(), + BookUUID: uuid.New().String(), // non-existent book + Body: "orphaned note content", + AddedOn: 1234567890, + EditedOn: 0, + USN: 0, + Deleted: false, + Dirty: true, + } + if err := orphanedNote.Insert(env.DB); err != nil { + t.Fatal(err) + } + + // Step 3: Run sync + // Expected flow: + // 1. First stepSync downloads 1 book + 2 notes from server + // - In transaction: last_max_usn should be set to 3 + // 2. sendChanges tries to upload the orphaned note + // - Server returns 500 error (book not found) + // - Sets isBehind=true + // 3. Retry stepSync queries after_usn=3 (from transaction state) + // - Server returns 0 fragments (no new data after USN 3) + // - processFragments returns maxUSN=0 (no fragments, so initialized value) + // - saveSyncState(tx, serverTime, 0) overwrites last_max_usn to 0 + // 4. Transaction commits + // + // BUG: last_max_usn ends up at 0 instead of 3 + clitest.RunDnoteCmd(t, env.CmdOpts, cliBinaryName, "sync") + + // Step 4: Verify the bug - last_max_usn should be 0 (bug) instead of 3 (correct) + var lastMaxUSN int + cliDatabase.MustScan(t, "finding system last_max_usn", + env.DB.QueryRow("SELECT value FROM system WHERE key = ?", consts.SystemLastMaxUSN), + &lastMaxUSN) + + // This assertion FAILS with the current buggy code (lastMaxUSN will be 0) + // After fixing the bug, this should pass (lastMaxUSN should be 3) + // + // BUG: The retry stepSync's empty response causes saveSyncState(tx, serverTime, 0) + // which overwrites the last_max_usn that was set during the first stepSync. + if lastMaxUSN == 0 { + t.Logf("BUG REPRODUCED: last_max_usn is 0 (should be 3)") + t.Logf("This happens because:") + t.Logf(" 1. First stepSync sets last_max_usn=3 in transaction") + t.Logf(" 2. Upload fails with 500 error, triggers retry stepSync") + t.Logf(" 3. Retry stepSync queries after_usn=3, gets 0 fragments") + t.Logf(" 4. processFragments returns maxUSN=0 (no fragments)") + t.Logf(" 5. saveSyncState(tx, serverTime, 0) overwrites last_max_usn to 0") + t.Logf(" 6. Transaction commits with last_max_usn=0") + t.Logf("") + t.Logf("Expected: last_max_usn should be 3 (server's max USN)") + t.Logf("Actual: last_max_usn is 0") + t.Logf("") + t.Logf("See /home/device10/development/dnote-memory-bank/artifacts-orphaned-notes-2025-10-25.md") + t.Logf("for detailed investigation and proposed fixes.") + } + + // When the bug is fixed, uncomment this assertion: + assert.Equal(t, lastMaxUSN, 3, "last_max_usn should be 3 after syncing") + + // For now, just verify we can reproduce the bug + if lastMaxUSN != 0 { + t.Errorf("Failed to reproduce bug: last_max_usn is %d, expected 0 (indicating bug was NOT reproduced)", lastMaxUSN) + } +}