From 17470079fe094868c9cb50ca0ff54761a65225c6 Mon Sep 17 00:00:00 2001 From: JonghunYu Date: Thu, 20 Nov 2025 15:14:10 +0900 Subject: [PATCH] Close issue #014 --- .issues/.counter | 2 +- .../closed/014-create-new-issue-when-close.md | 53 +++++++++++++++++++ cmd/close_test.go | 43 +++++++++++++++ pkg/storage.go | 18 +++++-- pkg/storage_test.go | 45 ++++++++++++++++ 5 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 .issues/closed/014-create-new-issue-when-close.md diff --git a/.issues/.counter b/.issues/.counter index 8351c19..60d3b2f 100644 --- a/.issues/.counter +++ b/.issues/.counter @@ -1 +1 @@ -14 +15 diff --git a/.issues/closed/014-create-new-issue-when-close.md b/.issues/closed/014-create-new-issue-when-close.md new file mode 100644 index 0000000..20cb6b9 --- /dev/null +++ b/.issues/closed/014-create-new-issue-when-close.md @@ -0,0 +1,53 @@ +--- +id: "014" +assignee: "" +labels: + - bug +created: 2025-11-20T14:57:43.364376+09:00 +updated: 2025-11-20T15:14:10.062556+09:00 +--- + +# Create new issue when close + +## Problem + +Closing an issue with `gi close 004 -c` is creating a brand new issue file in `./.issues/open/` instead of only moving the existing one to `./.issues/closed/`. + +Observed console output from `~/work/cargo-note-backend`: + +``` +gi close 004 -c +✓ Closed issue #004 +[feat/balance-check 9901acb] Close issue #004 + 8 files changed, 630 insertions(+), 1 deletion(-) + rename .issues/{open => closed}/004-add-transaction-history-endpoint.md (100%) + create mode 100644 .issues/open/004-save-transaction-history-to-database.md + ... +``` + +## Expected Behavior + +- The close command should move the existing issue file from `./.issues/open/` to `./.issues/closed/` and stop there. +- No new issue files should be created during close, regardless of the issue title. + +## Actual Behavior + +- The existing file was moved to `./.issues/closed/004-add-transaction-history-endpoint.md`. +- A new file `./.issues/open/004-save-transaction-history-to-database.md` was created alongside the move, leaving the issue appearing open again with a new slug. + +## Steps to Reproduce + +1. In a repo with `.issues/open/004-add-transaction-history-endpoint.md`, run `gi close 004 -c`. +2. Inspect `.issues/open/` and `.issues/closed/`. + +## Requirements + +- Ensure `gi close` only relocates the targeted issue file; it must not create any new `.issues/open/*.md` file as part of the operation. +- Preserve the original filename/slug when closing, even if the issue title has changed. +- Add a regression test that covers closing an issue after its title or slug has been modified. + +## Success Criteria + +- [ ] Running `gi close 004 -c` results in exactly one file in `./.issues/closed/` for ID 004 and zero files in `./.issues/open/` with ID 004. +- [ ] Closing and reopening flows handle title changes without creating duplicate files. +- [ ] New test(s) fail on current main and pass after the fix. diff --git a/cmd/close_test.go b/cmd/close_test.go index 1a834ae..ef4f8d5 100644 --- a/cmd/close_test.go +++ b/cmd/close_test.go @@ -179,6 +179,49 @@ func TestRunClosePreservesFilenameWithKoreanTitle(t *testing.T) { } } +func TestRunCloseAfterEditDoesNotCreateNewIssueFile(t *testing.T) { + _, cleanup := setupCommandTestRepo(t) + defer cleanup() + + // Create issue + if err := runCreate(nil, []string{"Original Title"}); err != nil { + t.Fatalf("runCreate() failed: %v", err) + } + + // Simulate editing the issue to change the title (mimics `gi edit`) + issue, dir, err := pkg.LoadIssue("001") + if err != nil { + t.Fatalf("failed to load issue: %v", err) + } + issue.Title = "Edited Title That Changes The Slug" + if err := pkg.SaveIssue(issue, dir); err != nil { + t.Fatalf("failed to save edited issue: %v", err) + } + + // Close the issue + if err := runClose(nil, []string{"001"}); err != nil { + t.Fatalf("runClose() failed: %v", err) + } + + // Verify no new files were created in open/ + openFiles, _ := pkg.ListIssues(pkg.OpenDir) + if len(openFiles) != 0 { + t.Fatalf("open directory should be empty after closing, found %d issues", len(openFiles)) + } + + // Verify closed file uses the original filename + closedPath, dirAfter, err := pkg.FindIssueFile("001") + if err != nil { + t.Fatalf("failed to find closed issue: %v", err) + } + if dirAfter != pkg.ClosedDir { + t.Fatalf("issue should be in closed dir, got %s", dirAfter) + } + if !strings.Contains(closedPath, "original-title") { + t.Fatalf("expected filename to retain original slug, got %s", closedPath) + } +} + func TestRunClosePreservesFilenameWhenTitleModified(t *testing.T) { _, cleanup := setupCommandTestRepo(t) defer cleanup() diff --git a/pkg/storage.go b/pkg/storage.go index f67b486..2fdcde7 100644 --- a/pkg/storage.go +++ b/pkg/storage.go @@ -120,10 +120,20 @@ func GetNextID() (int, error) { // SaveIssue writes an issue to the specified directory (open or closed) func SaveIssue(issue *Issue, dir string) error { - // Generate filename - slug := GenerateSlug(issue.Title) - filename := fmt.Sprintf("%s-%s.md", issue.ID, slug) - path := filepath.Join(IssuesDir, dir, filename) + var path string + + // If the issue already exists in the target directory, preserve its existing filename + if existingPath, existingDir, err := FindIssueFile(issue.ID); err == nil { + if existingDir != dir { + return fmt.Errorf("issue %s exists in %s directory, cannot save to %s", issue.ID, existingDir, dir) + } + path = existingPath + } else { + // Generate a new filename only when the issue doesn't exist yet + slug := GenerateSlug(issue.Title) + filename := fmt.Sprintf("%s-%s.md", issue.ID, slug) + path = filepath.Join(IssuesDir, dir, filename) + } // Serialize issue content, err := SerializeIssue(issue) diff --git a/pkg/storage_test.go b/pkg/storage_test.go index 9f1a7f0..7010e62 100644 --- a/pkg/storage_test.go +++ b/pkg/storage_test.go @@ -3,6 +3,7 @@ package pkg import ( "os" "path/filepath" + "strings" "testing" "time" ) @@ -162,6 +163,50 @@ func TestSaveAndLoadIssue(t *testing.T) { } } +func TestSaveIssuePreservesExistingFilename(t *testing.T) { + cleanup := setupTestRepo(t) + defer cleanup() + + if err := InitializeRepo(); err != nil { + t.Fatal(err) + } + + // Create initial issue + issue := NewIssue(1, "Original Title", "bob", []string{}) + if err := SaveIssue(issue, OpenDir); err != nil { + t.Fatalf("SaveIssue() initial save error = %v", err) + } + + // Change the title to something that would generate a different slug + issue.Title = "Completely Different Title After Edit" + if err := SaveIssue(issue, OpenDir); err != nil { + t.Fatalf("SaveIssue() second save error = %v", err) + } + + // Ensure only one issue file exists and the filename was preserved + openDir := filepath.Join(IssuesDir, OpenDir) + entries, err := os.ReadDir(openDir) + if err != nil { + t.Fatalf("failed to read open dir: %v", err) + } + + var issueFiles []string + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") { + continue + } + issueFiles = append(issueFiles, entry.Name()) + } + + if len(issueFiles) != 1 { + t.Fatalf("expected exactly 1 issue file, found %d: %v", len(issueFiles), issueFiles) + } + + if !strings.Contains(issueFiles[0], "original-title") { + t.Fatalf("expected filename to contain original slug, got %s", issueFiles[0]) + } +} + func TestMoveIssue(t *testing.T) { cleanup := setupTestRepo(t) defer cleanup()