@@ -378,6 +378,180 @@ func TestImportFromLocalJSONL(t *testing.T) {
378378 })
379379}
380380
381+ func TestImportMergeByTimestamp (t * testing.T ) {
382+ skipIfNoDolt (t )
383+
384+ t .Run ("merge skips update when local record is newer" , func (t * testing.T ) {
385+ tmpDir := t .TempDir ()
386+ dbPath := filepath .Join (tmpDir , "dolt" )
387+ store := newTestStore (t , dbPath )
388+ ctx := context .Background ()
389+
390+ // Step 1: Import an issue that is open, updated at T2.
391+ initial := `{"id":"test-merge1","title":"Original","status":"open","priority":2,"issue_type":"task","created_at":"2025-01-01T00:00:00Z","updated_at":"2025-06-01T00:00:00Z"}
392+ `
393+ jsonlPath := filepath .Join (tmpDir , "issues.jsonl" )
394+ if err := os .WriteFile (jsonlPath , []byte (initial ), 0644 ); err != nil {
395+ t .Fatalf ("write initial JSONL: %v" , err )
396+ }
397+ if _ , err := importFromLocalJSONLFull (ctx , store , jsonlPath , false ); err != nil {
398+ t .Fatalf ("initial import: %v" , err )
399+ }
400+
401+ // Step 2: Simulate local close — re-import with status=closed at a newer T3.
402+ closed := `{"id":"test-merge1","title":"Original","status":"closed","priority":2,"issue_type":"task","created_at":"2025-01-01T00:00:00Z","updated_at":"2025-09-01T00:00:00Z","closed_at":"2025-09-01T00:00:00Z","close_reason":"done"}
403+ `
404+ if err := os .WriteFile (jsonlPath , []byte (closed ), 0644 ); err != nil {
405+ t .Fatalf ("write closed JSONL: %v" , err )
406+ }
407+ if _ , err := importFromLocalJSONLFull (ctx , store , jsonlPath , false ); err != nil {
408+ t .Fatalf ("close import: %v" , err )
409+ }
410+
411+ // Verify issue is closed.
412+ issue , err := store .GetIssue (ctx , "test-merge1" )
413+ if err != nil {
414+ t .Fatalf ("get issue after close: %v" , err )
415+ }
416+ if issue .Status != "closed" {
417+ t .Fatalf ("expected status 'closed', got %q" , issue .Status )
418+ }
419+
420+ // Step 3: Import a STALE snapshot (T2, status=open) with --merge.
421+ // This simulates pulling a backup from another machine that predates
422+ // the local close.
423+ stale := `{"id":"test-merge1","title":"Stale title","status":"open","priority":2,"issue_type":"task","created_at":"2025-01-01T00:00:00Z","updated_at":"2025-06-01T00:00:00Z"}
424+ `
425+ if err := os .WriteFile (jsonlPath , []byte (stale ), 0644 ); err != nil {
426+ t .Fatalf ("write stale JSONL: %v" , err )
427+ }
428+ result , err := importFromLocalJSONLFull (ctx , store , jsonlPath , true )
429+ if err != nil {
430+ t .Fatalf ("merge import: %v" , err )
431+ }
432+ if result .Issues != 1 {
433+ t .Errorf ("expected 1 issue processed, got %d" , result .Issues )
434+ }
435+
436+ // Verify the issue is STILL closed — the stale snapshot was skipped.
437+ issue , err = store .GetIssue (ctx , "test-merge1" )
438+ if err != nil {
439+ t .Fatalf ("get issue after merge: %v" , err )
440+ }
441+ if issue .Status != "closed" {
442+ t .Errorf ("merge allowed stale snapshot to reopen issue: status=%q, want 'closed'" , issue .Status )
443+ }
444+ if issue .Title != "Original" {
445+ t .Errorf ("merge allowed stale snapshot to overwrite title: got %q, want 'Original'" , issue .Title )
446+ }
447+ })
448+
449+ t .Run ("merge allows update when incoming record is newer" , func (t * testing.T ) {
450+ tmpDir := t .TempDir ()
451+ dbPath := filepath .Join (tmpDir , "dolt" )
452+ store := newTestStore (t , dbPath )
453+ ctx := context .Background ()
454+
455+ // Step 1: Import an issue at T1.
456+ initial := `{"id":"test-merge2","title":"Old title","status":"open","priority":2,"issue_type":"task","created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}
457+ `
458+ jsonlPath := filepath .Join (tmpDir , "issues.jsonl" )
459+ if err := os .WriteFile (jsonlPath , []byte (initial ), 0644 ); err != nil {
460+ t .Fatalf ("write initial JSONL: %v" , err )
461+ }
462+ if _ , err := importFromLocalJSONLFull (ctx , store , jsonlPath , false ); err != nil {
463+ t .Fatalf ("initial import: %v" , err )
464+ }
465+
466+ // Step 2: Import a NEWER snapshot with --merge. This should update.
467+ newer := `{"id":"test-merge2","title":"New title","status":"closed","priority":1,"issue_type":"task","created_at":"2025-01-01T00:00:00Z","updated_at":"2025-12-01T00:00:00Z","closed_at":"2025-12-01T00:00:00Z"}
468+ `
469+ if err := os .WriteFile (jsonlPath , []byte (newer ), 0644 ); err != nil {
470+ t .Fatalf ("write newer JSONL: %v" , err )
471+ }
472+ if _ , err := importFromLocalJSONLFull (ctx , store , jsonlPath , true ); err != nil {
473+ t .Fatalf ("merge import: %v" , err )
474+ }
475+
476+ issue , err := store .GetIssue (ctx , "test-merge2" )
477+ if err != nil {
478+ t .Fatalf ("get issue after merge: %v" , err )
479+ }
480+ if issue .Status != "closed" {
481+ t .Errorf ("merge should have applied newer update: status=%q, want 'closed'" , issue .Status )
482+ }
483+ if issue .Title != "New title" {
484+ t .Errorf ("merge should have applied newer title: got %q, want 'New title'" , issue .Title )
485+ }
486+ })
487+
488+ t .Run ("without merge flag stale snapshot overwrites" , func (t * testing.T ) {
489+ // Control test: without --merge, the old behaviour applies.
490+ tmpDir := t .TempDir ()
491+ dbPath := filepath .Join (tmpDir , "dolt" )
492+ store := newTestStore (t , dbPath )
493+ ctx := context .Background ()
494+
495+ // Import at T2 as closed.
496+ closed := `{"id":"test-merge3","title":"Closed issue","status":"closed","priority":2,"issue_type":"task","created_at":"2025-01-01T00:00:00Z","updated_at":"2025-09-01T00:00:00Z","closed_at":"2025-09-01T00:00:00Z"}
497+ `
498+ jsonlPath := filepath .Join (tmpDir , "issues.jsonl" )
499+ if err := os .WriteFile (jsonlPath , []byte (closed ), 0644 ); err != nil {
500+ t .Fatalf ("write JSONL: %v" , err )
501+ }
502+ if _ , err := importFromLocalJSONLFull (ctx , store , jsonlPath , false ); err != nil {
503+ t .Fatalf ("initial import: %v" , err )
504+ }
505+
506+ // Import stale (T1, open) WITHOUT merge — should overwrite.
507+ stale := `{"id":"test-merge3","title":"Stale","status":"open","priority":2,"issue_type":"task","created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}
508+ `
509+ if err := os .WriteFile (jsonlPath , []byte (stale ), 0644 ); err != nil {
510+ t .Fatalf ("write stale JSONL: %v" , err )
511+ }
512+ if _ , err := importFromLocalJSONLFull (ctx , store , jsonlPath , false ); err != nil {
513+ t .Fatalf ("stale import: %v" , err )
514+ }
515+
516+ issue , err := store .GetIssue (ctx , "test-merge3" )
517+ if err != nil {
518+ t .Fatalf ("get issue: %v" , err )
519+ }
520+ if issue .Status != "open" {
521+ t .Errorf ("without merge, stale import should overwrite: status=%q, want 'open'" , issue .Status )
522+ }
523+ })
524+
525+ t .Run ("merge creates new issues normally" , func (t * testing.T ) {
526+ tmpDir := t .TempDir ()
527+ dbPath := filepath .Join (tmpDir , "dolt" )
528+ store := newTestStore (t , dbPath )
529+ ctx := context .Background ()
530+
531+ jsonl := `{"id":"test-merge4","title":"Brand new","status":"open","priority":2,"issue_type":"task","created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}
532+ `
533+ jsonlPath := filepath .Join (tmpDir , "issues.jsonl" )
534+ if err := os .WriteFile (jsonlPath , []byte (jsonl ), 0644 ); err != nil {
535+ t .Fatalf ("write JSONL: %v" , err )
536+ }
537+ result , err := importFromLocalJSONLFull (ctx , store , jsonlPath , true )
538+ if err != nil {
539+ t .Fatalf ("merge import of new issue: %v" , err )
540+ }
541+ if result .Issues != 1 {
542+ t .Errorf ("expected 1 issue, got %d" , result .Issues )
543+ }
544+
545+ issue , err := store .GetIssue (ctx , "test-merge4" )
546+ if err != nil {
547+ t .Fatalf ("get issue: %v" , err )
548+ }
549+ if issue .Title != "Brand new" {
550+ t .Errorf ("expected title 'Brand new', got %q" , issue .Title )
551+ }
552+ })
553+ }
554+
381555func TestImportFromLocalJSONL_LegacyFormats (t * testing.T ) {
382556 skipIfNoDolt (t )
383557
0 commit comments