package cli import ( "bytes" "context" "os" "os/exec" "path/filepath" "strings" "testing" "banger/internal/daemon/workspace" ) // seedRepoWithSubdir creates a git repo with one tracked file, and an // untracked non-ignored file at the repo root (not under the subdir). // Returns the repo root and the subdir path. func seedRepoWithSubdir(t *testing.T) (repoRoot, subDir string) { t.Helper() if _, err := exec.LookPath("git"); err != nil { t.Skipf("git not on PATH: %v", err) } repoRoot = t.TempDir() run := func(args ...string) { t.Helper() cmd := exec.Command(args[0], args[1:]...) cmd.Dir = repoRoot cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t", "GIT_CONFIG_GLOBAL=/dev/null", ) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("%v: %v\n%s", args, err, out) } } writeFile := func(relPath, content string) { t.Helper() full := filepath.Join(repoRoot, relPath) if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(full, []byte(content), 0o644); err != nil { t.Fatal(err) } } run("git", "init", "-q", "-b", "main") run("git", "config", "commit.gpgsign", "false") writeFile("tracked.md", "hello\n") writeFile("sub/kept.txt", "kept\n") run("git", "add", ".") run("git", "commit", "-q", "-m", "init") // Untracked non-ignored file at the ROOT — not under sub/. This is // what the pre-fix noteUntrackedSkipped would miss when the user // passed sub/ as the workspace source. writeFile("ROOT-SECRET.env", "TOKEN=abc\n") subDir = filepath.Join(repoRoot, "sub") return repoRoot, subDir } // TestNoteUntrackedSkippedCountsRepoWideEvenFromSubdir pins the bug // fix: when the user passes a subdirectory of a repo as the workspace // source, the untracked-files notice must still reflect what will // actually be skipped at the guest-shipping layer — which is a // repo-wide concern. Before the fix the helper ran `git -C // ls-files --others --exclude-standard`, which only sees files under // the subdir, silently underreporting the real skip count. func TestNoteUntrackedSkippedCountsRepoWideEvenFromSubdir(t *testing.T) { repoRoot, subDir := seedRepoWithSubdir(t) d := defaultDeps() d.repoInspector = workspace.NewInspector() var out bytes.Buffer d.noteUntrackedSkipped(context.Background(), &out, subDir) got := out.String() if !strings.Contains(got, "1 untracked") { t.Fatalf("note = %q, want mention of 1 untracked file (the root-level SECRET.env)", got) } _ = repoRoot } // TestNoteUntrackedSkippedSilentOutsideRepo verifies the best-effort // contract: when sourcePath is not inside any git repo, the helper // prints nothing and does not error. Callers rely on this so a user // who points vm run at an ad-hoc directory (or an export tarball // that's been unpacked) doesn't get the whole operation aborted // over a courtesy notice. func TestNoteUntrackedSkippedSilentOutsideRepo(t *testing.T) { d := defaultDeps() d.repoInspector = workspace.NewInspector() nonRepo := t.TempDir() var out bytes.Buffer d.noteUntrackedSkipped(context.Background(), &out, nonRepo) if got := out.String(); got != "" { t.Fatalf("note = %q, want no output outside a git repo", got) } } // TestNoteUntrackedSkippedSwallowsInspectorErrors verifies that a // runner that errors on every call produces no output and no panic. // This is the other half of best-effort: even if git-the-binary is // somehow broken or missing, the live flow keeps running. func TestNoteUntrackedSkippedSwallowsInspectorErrors(t *testing.T) { d := defaultDeps() d.repoInspector = &workspace.Inspector{ Runner: func(context.Context, string, ...string) ([]byte, error) { return nil, &exec.Error{Name: "git", Err: exec.ErrNotFound} }, } var out bytes.Buffer d.noteUntrackedSkipped(context.Background(), &out, t.TempDir()) if got := out.String(); got != "" { t.Fatalf("note = %q, want silence when inspector runner errors", got) } }