diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 2484a62..51913ab 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -300,6 +300,12 @@ have exited, rather than only testing a directly built CLI or a warm cache. ## R08: Reuse the existing repository-path semantics for record storage — P2 +**Completed:** R04's shared resolver reuses the established Git root and +metadata-path normalization. Added regressions passed for repository roots and +separate metadata directories ending in whitespace, default/relative read-write +agreement, and rejection of absolute unignored worktree stores. Existing ordinary +and linked-worktree recording tests also passed. + **Relevant code:** `internal/publish/record.go:112`, especially line 117; `internal/publish/attempts.go:74`, especially line 82; the existing `internal/git/source.go:39` and `source.go:50` repository-root helpers. diff --git a/internal/publish/record_test.go b/internal/publish/record_test.go index 31abcde..5d2aadc 100644 --- a/internal/publish/record_test.go +++ b/internal/publish/record_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "os" "os/exec" "path/filepath" @@ -172,6 +173,47 @@ func TestSaveEvidenceAllowsExplicitGitMetadataStores(t *testing.T) { } } +func TestRecordStoragePreservesWhitespaceInRepositoryAndMetadataPaths(t *testing.T) { + for _, separate := range []bool{false, true} { + t.Run(fmt.Sprintf("separate=%t", separate), func(t *testing.T) { + original := recordRepo(t) + repo := original + " " + if err := os.Rename(original, repo); err != nil { + t.Fatal(err) + } + // recordRepo's temporary-directory cleanup owns the old spelling. + t.Cleanup(func() { _ = os.RemoveAll(repo) }) + if separate { + recordGit(t, repo, "init", "--separate-git-dir", filepath.Join(t.TempDir(), "metadata ")) + } + if err := os.WriteFile(filepath.Join(repo, ".gitignore"), []byte("saved/\n"), 0600); err != nil { + t.Fatal(err) + } + recordGit(t, repo, "add", ".gitignore") + recordGit(t, repo, "commit", "-qm", "Ignore saved records") + for _, override := range []string{"", "saved"} { + path, err := SaveEvidence(t.Context(), repo, override, Evidence{RepositoryDir: repo, Tag: "v1.2.3"}) + if err != nil { + t.Fatal(err) + } + listed, err := ListAttempts(t.Context(), repo, override) + if err != nil || len(listed.Attempts) != 1 || listed.Attempts[0].RecordPath != path || listed.Attempts[0].Evidence.RepositoryDir != repo { + t.Fatalf("read/write disagreement for %q: %#v, %v", override, listed, err) + } + if override != "" && filepath.Dir(path) != filepath.Join(repo, override) { + t.Fatalf("changed repository path: %q", path) + } + } + if _, err := SaveEvidence(t.Context(), repo, filepath.Join(repo, "unignored"), Evidence{}); err == nil { + t.Fatal("absolute unignored store was misclassified as external") + } + if status := recordGit(t, repo, "status", "--porcelain"); status != "" { + t.Fatalf("recording changed candidate: %s", status) + } + }) + } +} + func TestSaveEvidenceRejectsUnignoredOverrideInsideWorktree(t *testing.T) { repo := recordRepo(t) _, err := SaveEvidence(t.Context(), repo, "records", Evidence{Tag: "v1.2.3"})