mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-10-21 14:19:40 +00:00
Some checks failed
CI / test (push) Waiting to run
CI / windows (push) Waiting to run
CI / cross-compile (amd64, darwin) (push) Waiting to run
CI / cross-compile (amd64, windows) (push) Waiting to run
CI / cross-compile (arm64, darwin) (push) Waiting to run
CI / cross-compile (arm64, linux) (push) Waiting to run
CI / cross-compile (arm64, windows) (push) Waiting to run
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Pages / build (push) Waiting to run
Deploy Pages / deploy (push) Blocked by required conditions
plugin-contract / plugin-contract (push) Waiting to run
Action Contract / action-contract (push) Has been cancelled
Sessions accumulate in ~/.opencodereview/sessions/ with no way to remove one short of deleting files by hand. The Viewer stays read-only by design, so deletion belongs in the CLI, where the caller already owns the files. The session id is enough on its own: the command looks for it under every repository, so it works from anywhere. That a session is stored under a directory derived from its repository is an implementation detail, and making the caller stand in the right repository to delete by id pushes that detail onto them. --repo narrows the search rather than enabling it. An id saved for more than one repository lists the candidates and deletes nothing. encodeRepoPath replaces separators with "-", so two repository paths can share one sessions directory and one id can name two different files; choosing between them would be a coin flip on a file that cannot be recovered. The command prints the session's repository, branch, start time, file count and comment count in the same format `ocr session list` uses, then asks to confirm. A non-interactive stdin answers no, so `--yes` has to be passed deliberately from a pipeline or CI. Reports an unreadable session rather than folding it into "corrupt" -- a session whose metadata cannot be parsed is still deletable, since that is the record most likely to need removing. Documented in all five locales of the CLI reference. Co-authored-by: Qiyuanqiii <267806965+Qiyuanqiii@users.noreply.github.com> Co-authored-by: wu21-web <wu2196674@icloud.com>
536 lines
17 KiB
Go
536 lines
17 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright 2026 alibaba/open-code-review Contributors
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/alibaba/open-code-review/internal/session"
|
|
)
|
|
|
|
func seedRmSession(t *testing.T, _ /*home*/, repoDir, sessionID string) string {
|
|
t.Helper()
|
|
dir, err := session.SessionsDir(repoDir)
|
|
if err != nil {
|
|
t.Fatalf("SessionsDir: %v", err)
|
|
}
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
t.Fatalf("MkdirAll: %v", err)
|
|
}
|
|
path := filepath.Join(dir, sessionID+".jsonl")
|
|
if err := os.WriteFile(path, []byte("{}\n"), 0o600); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestSessionRm_DeclinedConfirmationKeepsTheSession(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
repoDir := t.TempDir()
|
|
path := seedRmSessionRecorded(t, repoDir, "20250601-100000-aaaaaa", "main")
|
|
|
|
sessionRmRepoDir = repoDir
|
|
sessionRmYes = false
|
|
t.Cleanup(func() { sessionRmRepoDir, sessionRmYes = "", false })
|
|
|
|
out := &bytes.Buffer{}
|
|
sessionRmCmd.SetOut(out)
|
|
sessionRmCmd.SetIn(strings.NewReader("n\n"))
|
|
|
|
if err := runSessionRm(sessionRmCmd, "20250601-100000-aaaaaa"); err != nil {
|
|
t.Fatalf("runSessionRm: %v", err)
|
|
}
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Errorf("session was deleted despite a declined confirmation: %v", err)
|
|
}
|
|
if !strings.Contains(out.String(), "Cancelled") {
|
|
t.Errorf("expected the command to say it cancelled, got %q", out.String())
|
|
}
|
|
}
|
|
|
|
func TestSessionRm_NonInteractiveStdinDoesNotDelete(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
repoDir := t.TempDir()
|
|
path := seedRmSessionRecorded(t, repoDir, "20250601-100000-aaaaaa", "main")
|
|
|
|
sessionRmRepoDir = repoDir
|
|
sessionRmYes = false
|
|
t.Cleanup(func() { sessionRmRepoDir, sessionRmYes = "", false })
|
|
|
|
sessionRmCmd.SetOut(&bytes.Buffer{})
|
|
sessionRmCmd.SetIn(strings.NewReader(""))
|
|
|
|
if err := runSessionRm(sessionRmCmd, "20250601-100000-aaaaaa"); err != nil {
|
|
t.Fatalf("runSessionRm: %v", err)
|
|
}
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Errorf("session was deleted on an empty stdin: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSessionRm_YesFlagDeletesWithoutPrompting(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
repoDir := t.TempDir()
|
|
path := seedRmSessionRecorded(t, repoDir, "20250601-100000-aaaaaa", "main")
|
|
|
|
sessionRmRepoDir = repoDir
|
|
sessionRmYes = true
|
|
t.Cleanup(func() { sessionRmRepoDir, sessionRmYes = "", false })
|
|
|
|
out := &bytes.Buffer{}
|
|
sessionRmCmd.SetOut(out)
|
|
sessionRmCmd.SetIn(strings.NewReader(""))
|
|
|
|
if err := runSessionRm(sessionRmCmd, "20250601-100000-aaaaaa"); err != nil {
|
|
t.Fatalf("runSessionRm: %v", err)
|
|
}
|
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
t.Errorf("session still present after --yes: %v", err)
|
|
}
|
|
if !strings.Contains(out.String(), "Deleted session") {
|
|
t.Errorf("expected a confirmation line, got %q", out.String())
|
|
}
|
|
}
|
|
|
|
func TestSessionRm_UnknownIDExplainsHowToList(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
repoDir := t.TempDir()
|
|
|
|
sessionRmRepoDir = repoDir
|
|
sessionRmYes = true
|
|
t.Cleanup(func() { sessionRmRepoDir, sessionRmYes = "", false })
|
|
|
|
sessionRmCmd.SetOut(&bytes.Buffer{})
|
|
sessionRmCmd.SetIn(strings.NewReader(""))
|
|
|
|
err := runSessionRm(sessionRmCmd, "20250601-999999-zzzzzz")
|
|
if err == nil {
|
|
t.Fatal("expected an error for an unknown session id")
|
|
}
|
|
if !strings.Contains(err.Error(), "ocr session list") {
|
|
t.Errorf("error should point at 'ocr session list', got %q", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestSessionRm_RejectsTraversalIDsBeforeTouchingTheFilesystem(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
repoDir := t.TempDir()
|
|
|
|
sessionRmRepoDir = repoDir
|
|
sessionRmYes = true
|
|
t.Cleanup(func() { sessionRmRepoDir, sessionRmYes = "", false })
|
|
|
|
out := &bytes.Buffer{}
|
|
sessionRmCmd.SetOut(out)
|
|
sessionRmCmd.SetIn(strings.NewReader(""))
|
|
|
|
for _, id := range []string{"../escape", "../../escape", "a/b", "..", "."} {
|
|
t.Run(id, func(t *testing.T) {
|
|
err := runSessionRm(sessionRmCmd, id)
|
|
if err == nil {
|
|
t.Fatalf("runSessionRm(%q) returned no error", id)
|
|
}
|
|
if strings.Contains(err.Error(), "ocr session list") {
|
|
t.Errorf("runSessionRm(%q) reported a missing session; an invalid id must be refused as invalid", id)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSessionRm_CorruptSessionIsDeletableAndDescribedHonestly(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
repoDir := t.TempDir()
|
|
|
|
dir, err := session.SessionsDir(repoDir)
|
|
if err != nil {
|
|
t.Fatalf("SessionsDir: %v", err)
|
|
}
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
t.Fatalf("MkdirAll: %v", err)
|
|
}
|
|
path := filepath.Join(dir, "20250601-100000-corrupt.jsonl")
|
|
if err := os.WriteFile(path, []byte("this is not json\n{\"also\": not json\n"), 0o600); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
|
|
sessionRmRepoDir = ""
|
|
sessionRmYes = false
|
|
t.Cleanup(func() { sessionRmRepoDir, sessionRmYes = "", false })
|
|
|
|
out := &bytes.Buffer{}
|
|
sessionRmCmd.SetOut(out)
|
|
sessionRmCmd.SetIn(strings.NewReader("y\n"))
|
|
|
|
if err := runSessionRm(sessionRmCmd, "20250601-100000-corrupt"); err != nil {
|
|
t.Fatalf("runSessionRm: %v", err)
|
|
}
|
|
if !strings.Contains(out.String(), "could not be read") {
|
|
t.Errorf("a corrupt session should be described as unreadable, got %q", out.String())
|
|
}
|
|
if strings.Contains(out.String(), "started: 0001-01-01") {
|
|
t.Errorf("zero-value metadata was printed as if real: %q", out.String())
|
|
}
|
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
t.Errorf("corrupt session was not deleted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSessionRm_OtherRepoRefusedBeforePrompting(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
|
|
base := t.TempDir()
|
|
repoA := filepath.Join(base, "a-b", "c")
|
|
repoB := filepath.Join(base, "a", "b-c")
|
|
for _, d := range []string{repoA, repoB} {
|
|
if err := os.MkdirAll(d, 0o755); err != nil {
|
|
t.Fatalf("MkdirAll: %v", err)
|
|
}
|
|
}
|
|
dirA, _ := session.SessionsDir(repoA)
|
|
dirB, _ := session.SessionsDir(repoB)
|
|
if dirA != dirB {
|
|
t.Skip("paths no longer collide; the encoding may have been fixed")
|
|
}
|
|
if err := os.MkdirAll(dirB, 0o700); err != nil {
|
|
t.Fatalf("MkdirAll: %v", err)
|
|
}
|
|
path := filepath.Join(dirB, "20250601-100000-bbbbbb.jsonl")
|
|
rec := `{"type":"session_start","cwd":"` + strings.ReplaceAll(repoB, `\`, `\\`) + `","sessionId":"20250601-100000-bbbbbb","timestamp":"2025-06-01T10:00:00Z"}` + "\n"
|
|
if err := os.WriteFile(path, []byte(rec), 0o600); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
|
|
sessionRmRepoDir = repoA
|
|
sessionRmYes = false
|
|
t.Cleanup(func() { sessionRmRepoDir, sessionRmYes = "", false })
|
|
|
|
out := &bytes.Buffer{}
|
|
sessionRmCmd.SetOut(out)
|
|
sessionRmCmd.SetIn(strings.NewReader("y\n"))
|
|
|
|
err := runSessionRm(sessionRmCmd, "20250601-100000-bbbbbb")
|
|
if err == nil {
|
|
t.Fatal("deleting another repository's session must fail")
|
|
}
|
|
if !strings.Contains(err.Error(), "different repository") {
|
|
t.Errorf("error should name the cause, got %q", err.Error())
|
|
}
|
|
if strings.Contains(out.String(), "Delete session") {
|
|
t.Errorf("the user was prompted before the refusal: %q", out.String())
|
|
}
|
|
if _, statErr := os.Stat(path); statErr != nil {
|
|
t.Errorf("the other repository's session was deleted: %v", statErr)
|
|
}
|
|
}
|
|
|
|
func makeUnreadable(t *testing.T, path string) {
|
|
t.Helper()
|
|
if err := os.Chmod(path, 0o000); err != nil {
|
|
t.Fatalf("Chmod: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = os.Chmod(path, 0o600) })
|
|
if _, err := os.ReadFile(path); err == nil {
|
|
t.Skipf("this platform does not enforce the file mode: %s is still readable after chmod 000", path)
|
|
}
|
|
}
|
|
|
|
func TestSessionRm_UnreadableSessionIsReportedNotTreatedAsCorrupt(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
repoDir := t.TempDir()
|
|
path := seedRmSession(t, home, repoDir, "20250601-100000-locked")
|
|
makeUnreadable(t, path)
|
|
|
|
sessionRmRepoDir = repoDir
|
|
sessionRmYes = true
|
|
t.Cleanup(func() { sessionRmRepoDir, sessionRmYes = "", false })
|
|
sessionRmCmd.SetOut(&bytes.Buffer{})
|
|
sessionRmCmd.SetIn(strings.NewReader(""))
|
|
|
|
err := runSessionRm(sessionRmCmd, "20250601-100000-locked")
|
|
if err == nil {
|
|
t.Fatal("an unreadable session must be reported, not deleted")
|
|
}
|
|
if !strings.Contains(err.Error(), "cannot read session") {
|
|
t.Errorf("error should say the file could not be read, got %q", err.Error())
|
|
}
|
|
if _, statErr := os.Stat(path); statErr != nil {
|
|
t.Errorf("an unreadable session was deleted anyway: %v", statErr)
|
|
}
|
|
}
|
|
|
|
func TestSessionRm_PromptMatchesTheListFormatting(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
repoDir := t.TempDir()
|
|
|
|
dir, err := session.SessionsDir(repoDir)
|
|
if err != nil {
|
|
t.Fatalf("SessionsDir: %v", err)
|
|
}
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
t.Fatalf("MkdirAll: %v", err)
|
|
}
|
|
path := filepath.Join(dir, "20250601-100000-fmt.jsonl")
|
|
rec := `{"type":"session_start","cwd":"` + strings.ReplaceAll(repoDir, `\`, `\\`) +
|
|
`","sessionId":"20250601-100000-fmt","timestamp":"2025-06-01T10:00:00Z","gitBranch":"feature-x"}` + "\n"
|
|
if err := os.WriteFile(path, []byte(rec), 0o600); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
|
|
sessionRmRepoDir = repoDir
|
|
sessionRmYes = false
|
|
t.Cleanup(func() { sessionRmRepoDir, sessionRmYes = "", false })
|
|
out := &bytes.Buffer{}
|
|
sessionRmCmd.SetOut(out)
|
|
sessionRmCmd.SetIn(strings.NewReader("n\n"))
|
|
|
|
if err := runSessionRm(sessionRmCmd, "20250601-100000-fmt"); err != nil {
|
|
t.Fatalf("runSessionRm: %v", err)
|
|
}
|
|
got := out.String()
|
|
if strings.Contains(got, "T10:00:00Z") {
|
|
t.Errorf("prompt used RFC3339; 'ocr session list' shows a local timestamp: %q", got)
|
|
}
|
|
for _, want := range []string{"repo:", "branch:", "started:", "files:", "comments:"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("prompt is missing %q: %q", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func seedRmSessionRecorded(t *testing.T, repoDir, sessionID, branch string) string {
|
|
t.Helper()
|
|
path := seedRmSession(t, "", repoDir, sessionID)
|
|
rec := `{"type":"session_start","cwd":"` + strings.ReplaceAll(repoDir, `\`, `\\`) +
|
|
`","sessionId":"` + sessionID + `","timestamp":"2025-06-01T10:00:00Z","gitBranch":"` + branch + `"}` + "\n"
|
|
if err := os.WriteFile(path, []byte(rec), 0o600); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func runSessionRmForTest(t *testing.T, repoFlag, sessionID, stdin string, yes bool) (string, error) {
|
|
t.Helper()
|
|
sessionRmRepoDir = repoFlag
|
|
sessionRmYes = yes
|
|
t.Cleanup(func() { sessionRmRepoDir, sessionRmYes = "", false })
|
|
var out bytes.Buffer
|
|
sessionRmCmd.SetOut(&out)
|
|
sessionRmCmd.SetIn(strings.NewReader(stdin))
|
|
err := runSessionRm(sessionRmCmd, sessionID)
|
|
return out.String(), err
|
|
}
|
|
|
|
func TestSessionRm_FindsTheSessionWithoutBeingToldTheRepository(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
repoDir := t.TempDir()
|
|
path := seedRmSessionRecorded(t, repoDir, "20250601-100000-alone", "main")
|
|
|
|
out, err := runSessionRmForTest(t, "", "20250601-100000-alone", "y\n", false)
|
|
if err != nil {
|
|
t.Fatalf("runSessionRm: %v", err)
|
|
}
|
|
if !strings.Contains(out, repoDir) {
|
|
t.Errorf("the prompt must name the repository the session was found in, got %q", out)
|
|
}
|
|
if _, statErr := os.Stat(path); !os.IsNotExist(statErr) {
|
|
t.Errorf("session was not deleted: %v", statErr)
|
|
}
|
|
}
|
|
|
|
func TestSessionRm_ListsCandidatesRatherThanGuessing(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
repoA := t.TempDir()
|
|
repoB := t.TempDir()
|
|
pathA := seedRmSessionRecorded(t, repoA, "20250601-100000-shared", "main")
|
|
pathB := seedRmSessionRecorded(t, repoB, "20250601-100000-shared", "feature")
|
|
|
|
_, err := runSessionRmForTest(t, "", "20250601-100000-shared", "y\n", true)
|
|
if err == nil {
|
|
t.Fatal("an id saved for two repositories must not be deleted on a guess")
|
|
}
|
|
for _, want := range []string{repoA, repoB, "--repo"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("error should mention %q so the caller can disambiguate, got %q", want, err.Error())
|
|
}
|
|
}
|
|
for _, path := range []string{pathA, pathB} {
|
|
if _, statErr := os.Stat(path); statErr != nil {
|
|
t.Errorf("an ambiguous id deleted %s anyway: %v", path, statErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSessionRm_RepoFlagNarrowsAnAmbiguousID(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
repoA := t.TempDir()
|
|
repoB := t.TempDir()
|
|
pathA := seedRmSessionRecorded(t, repoA, "20250601-100000-shared", "main")
|
|
pathB := seedRmSessionRecorded(t, repoB, "20250601-100000-shared", "feature")
|
|
|
|
if _, err := runSessionRmForTest(t, repoB, "20250601-100000-shared", "", true); err != nil {
|
|
t.Fatalf("runSessionRm --repo: %v", err)
|
|
}
|
|
if _, statErr := os.Stat(pathB); !os.IsNotExist(statErr) {
|
|
t.Errorf("the named repository's session was not deleted: %v", statErr)
|
|
}
|
|
if _, statErr := os.Stat(pathA); statErr != nil {
|
|
t.Errorf("the other repository's session was deleted too: %v", statErr)
|
|
}
|
|
}
|
|
|
|
func TestSessionRm_UnknownIDAnywhereSaysSoWithoutPrompting(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
seedRmSessionRecorded(t, t.TempDir(), "20250601-100000-present", "main")
|
|
|
|
out, err := runSessionRmForTest(t, "", "20250601-100000-missing", "y\n", false)
|
|
if err == nil {
|
|
t.Fatal("an id that is saved nowhere must be an error")
|
|
}
|
|
if !strings.Contains(err.Error(), "ocr session list") {
|
|
t.Errorf("error should point at the listing command, got %q", err.Error())
|
|
}
|
|
if strings.Contains(out, "Delete session") {
|
|
t.Errorf("the caller must not be asked to confirm deleting something that is not there, got %q", out)
|
|
}
|
|
}
|
|
|
|
func TestSessionRm_TraversalIDIsRefusedBeforeSearchingEveryRepository(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
|
|
for _, id := range []string{"../escape", "sub/../../escape", `..\escape`, ".", ".."} {
|
|
if _, err := session.FindSessionsByID(id); err == nil {
|
|
t.Errorf("FindSessionsByID(%q) must be refused", id)
|
|
}
|
|
if _, err := runSessionRmForTest(t, "", id, "", true); err == nil {
|
|
t.Errorf("ocr session rm %q must be refused", id)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSessionRm_RepoFlagRefusesAnUnverifiableSession(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
repoDir := t.TempDir()
|
|
path := seedRmSession(t, home, repoDir, "20250601-100000-nocwd")
|
|
|
|
out, err := runSessionRmForTest(t, repoDir, "20250601-100000-nocwd", "y\n", true)
|
|
if err == nil {
|
|
t.Fatal("a session that records no repository must not be deleted on the strength of --repo")
|
|
}
|
|
if !errors.Is(err, session.ErrSessionUnverifiable) {
|
|
t.Errorf("error should be ErrSessionUnverifiable, got %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "without --repo") {
|
|
t.Errorf("error should say how to delete it instead, got %q", err.Error())
|
|
}
|
|
if strings.Contains(out, "Delete session") {
|
|
t.Errorf("the caller must not be prompted before the refusal, got %q", out)
|
|
}
|
|
if _, statErr := os.Stat(path); statErr != nil {
|
|
t.Errorf("the session was deleted anyway: %v", statErr)
|
|
}
|
|
}
|
|
|
|
func TestSessionRm_UnverifiableSessionIsStillDeletableByIDAlone(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
repoDir := t.TempDir()
|
|
path := seedRmSession(t, home, repoDir, "20250601-100000-nocwd")
|
|
|
|
if _, err := runSessionRmForTest(t, "", "20250601-100000-nocwd", "", true); err != nil {
|
|
t.Fatalf("runSessionRm without --repo: %v", err)
|
|
}
|
|
if _, statErr := os.Stat(path); !os.IsNotExist(statErr) {
|
|
t.Errorf("the session was not deleted: %v", statErr)
|
|
}
|
|
}
|
|
|
|
func TestSessionRm_CompletionOffersSessionsFromEveryRepository(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
here := t.TempDir()
|
|
elsewhere := t.TempDir()
|
|
seedRmSessionRecorded(t, here, "20250601-100000-here", "main")
|
|
seedRmSessionRecorded(t, elsewhere, "20250601-100000-away", "main")
|
|
|
|
sessionRmRepoDir = ""
|
|
t.Cleanup(func() { sessionRmRepoDir = "" })
|
|
if err := sessionRmCmd.Flags().Set("repo", ""); err != nil {
|
|
t.Fatalf("clear --repo: %v", err)
|
|
}
|
|
|
|
got, _ := completeSessionIDsAnywhere(sessionRmCmd, nil, "20250601")
|
|
joined := strings.Join(got, "\n")
|
|
for _, want := range []string{"20250601-100000-here", "20250601-100000-away"} {
|
|
if !strings.Contains(joined, want) {
|
|
t.Errorf("completion is missing %q; got %q", want, joined)
|
|
}
|
|
}
|
|
if !strings.Contains(joined, elsewhere) {
|
|
t.Errorf("completion should name the repository each id belongs to; got %q", joined)
|
|
}
|
|
}
|
|
|
|
func TestSessionRm_CompletionNarrowsWithRepoFlag(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
here := t.TempDir()
|
|
elsewhere := t.TempDir()
|
|
seedRmSessionRecorded(t, here, "20250601-100000-here", "main")
|
|
seedRmSessionRecorded(t, elsewhere, "20250601-100000-away", "main")
|
|
|
|
if err := sessionRmCmd.Flags().Set("repo", here); err != nil {
|
|
t.Fatalf("set --repo: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = sessionRmCmd.Flags().Set("repo", ""); sessionRmRepoDir = "" })
|
|
|
|
joined := strings.Join(firstOf(completeSessionIDsAnywhere(sessionRmCmd, nil, "20250601")), "\n")
|
|
if !strings.Contains(joined, "20250601-100000-here") {
|
|
t.Errorf("completion lost this repository's session; got %q", joined)
|
|
}
|
|
if strings.Contains(joined, "20250601-100000-away") {
|
|
t.Errorf("--repo should scope the completion; got %q", joined)
|
|
}
|
|
}
|
|
|
|
func firstOf(completions []string, _ cobra.ShellCompDirective) []string { return completions }
|