Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cmd/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,9 @@ func copyFileIntoZip(path string, w io.Writer) error {
return err
}
_, err = io.Copy(w, src)
src.Close()
if closeErr := src.Close(); err == nil {
err = closeErr
}
return err
}

Expand Down
148 changes: 139 additions & 9 deletions internal/analyze/zip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,28 @@ package analyze
import (
"archive/zip"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)

func TestIsGitRepo_WithDotGit(t *testing.T) {
dir := t.TempDir()
// Simulate .git via git init
if err := os.MkdirAll(filepath.Join(dir, ".git"), 0750); err != nil {
t.Fatal(err)
}
// isGitRepo uses `git rev-parse --git-dir` which needs an actual git repo;
// fall back to checking directory creation only — the factory version
// (os.Stat) is simpler, but here we just ensure non-git dir returns false.
func TestIsGitRepo_NonGitDir(t *testing.T) {
// isGitRepo uses `git rev-parse --git-dir`; an empty temp dir is not a git repo.
if isGitRepo(t.TempDir()) {
t.Error("empty temp dir should not be a git repo")
}
}

// ── isWorktreeClean ───────────────────────────────────────────────────────────

func TestIsWorktreeClean_NonGitDir(t *testing.T) {
// git status on a non-repo exits non-zero → returns false
if isWorktreeClean(t.TempDir()) {
t.Error("non-git dir should not be considered clean")
}
}

func TestWalkZip_IncludesFiles(t *testing.T) {
src := t.TempDir()
if err := os.WriteFile(filepath.Join(src, "main.go"), []byte("package main"), 0600); err != nil {
Expand Down Expand Up @@ -85,6 +88,30 @@ func TestWalkZip_SkipsSkipDirs(t *testing.T) {
}
}

func TestWalkZip_SkipsLargeFiles(t *testing.T) {
src := t.TempDir()
// Create a file just over 10 MB
bigFile := filepath.Join(src, "huge.dat")
if err := os.WriteFile(bigFile, make([]byte, 10<<20+1), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(src, "small.go"), []byte("x"), 0600); err != nil {
t.Fatal(err)
}

dest := filepath.Join(t.TempDir(), "out.zip")
if err := walkZip(src, dest); err != nil {
t.Fatal(err)
}
entries := readZipEntries(t, dest)
if entries["huge.dat"] {
t.Error("file over 10 MB should be excluded from zip")
}
if !entries["small.go"] {
t.Error("small file should be included in zip")
}
}

func TestCreateZip_NonGitDir(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main"), 0600); err != nil {
Expand All @@ -100,6 +127,109 @@ func TestCreateZip_NonGitDir(t *testing.T) {
}
}

func TestWalkZip_CreateDestError(t *testing.T) {
src := t.TempDir()
dest := filepath.Join(t.TempDir(), "nonexistent-subdir", "out.zip")
if err := walkZip(src, dest); err == nil {
t.Error("walkZip should fail when dest directory does not exist")
}
}

func TestWalkZip_WalkError(t *testing.T) {
dest := filepath.Join(t.TempDir(), "out.zip")
if err := walkZip("/nonexistent-dir-xyzzy-analyze", dest); err == nil {
t.Error("walkZip should fail when source directory does not exist")
}
}

func TestWalkZip_OpenFileError(t *testing.T) {
if os.Getenv("CI") != "" {
t.Skip("skipping chmod-based test in CI")
}
src := t.TempDir()
secret := filepath.Join(src, "secret.go")
if err := os.WriteFile(secret, []byte("package main"), 0600); err != nil {
t.Fatal(err)
}
if err := os.Chmod(secret, 0000); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { os.Chmod(secret, 0600) }) //nolint:errcheck
dest := filepath.Join(t.TempDir(), "out.zip")
if err := walkZip(src, dest); err == nil {
t.Error("walkZip should fail when a source file cannot be opened")
}
}

func TestCreateZip_CreateTempError(t *testing.T) {
t.Setenv("TMPDIR", filepath.Join(t.TempDir(), "nonexistent-tmp"))
_, err := createZip(t.TempDir())
if err == nil {
t.Error("createZip should fail when os.CreateTemp fails")
}
}

func TestCreateZip_NonExistentDir(t *testing.T) {
_, err := createZip("/nonexistent-dir-analyze-createzip-xyz")
if err == nil {
t.Error("createZip should fail when directory does not exist")
}
}

func initCleanAnalyzeGitRepo(t *testing.T) string {
t.Helper()
dir := t.TempDir()
run := func(args ...string) {
t.Helper()
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = dir
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git setup %v: %v\n%s", args, err, out)
}
}
run("git", "init")
run("git", "config", "user.email", "ci@test.local")
run("git", "config", "user.name", "CI")
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main"), 0600); err != nil {
t.Fatal(err)
}
run("git", "add", ".")
run("git", "commit", "-m", "init")
return dir
}

func TestGitArchive_CleanRepo(t *testing.T) {
dir := initCleanAnalyzeGitRepo(t)
dest := filepath.Join(t.TempDir(), "out.zip")
if err := gitArchive(dir, dest); err != nil {
t.Fatalf("gitArchive: %v", err)
}
entries := readZipEntries(t, dest)
if !entries["main.go"] {
t.Error("git archive should contain main.go")
}
}

func TestIsWorktreeClean_CleanRepo(t *testing.T) {
dir := initCleanAnalyzeGitRepo(t)
if !isWorktreeClean(dir) {
t.Error("freshly committed repo should be considered clean")
}
}

func TestCreateZip_CleanGitRepo(t *testing.T) {
dir := initCleanAnalyzeGitRepo(t)
path, err := createZip(dir)
if err != nil {
t.Fatalf("createZip on clean git repo: %v", err)
}
defer os.Remove(path)
entries := readZipEntries(t, path)
if !entries["main.go"] {
t.Error("zip should contain main.go from git archive")
}
}

func readZipEntries(t *testing.T, path string) map[string]bool {
t.Helper()
r, err := zip.OpenReader(path)
Expand Down
4 changes: 2 additions & 2 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,8 @@ func (c *Client) request(ctx context.Context, method, path, contentType string,
return &apiErr
}
snippet := string(respBody)
if len(snippet) > 300 {
snippet = snippet[:300] + "..."
if runes := []rune(snippet); len(runes) > 300 {
snippet = string(runes[:300]) + "..."
}
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, snippet)
}
Expand Down
111 changes: 111 additions & 0 deletions internal/api/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,117 @@ func TestError_Error_WithoutCode(t *testing.T) {
}
}

func TestError_Error_FallsBackToStatus(t *testing.T) {
// When StatusCode is 0, Error() should use the Status field.
e := &Error{StatusCode: 0, Status: 404, Message: "not found"}
got := e.Error()
if !containsStr(got, "404") {
t.Errorf("Error() = %q, should contain '404' (from Status field)", got)
}
}

// ── GraphFromShardIR ──────────────────────────────────────────────────────────

func TestGraphFromShardIR_NodesAndRels(t *testing.T) {
ir := &ShardIR{
Repo: "myorg/myrepo",
Graph: ShardGraph{
Nodes: []Node{
{ID: "n1", Labels: []string{"File"}, Properties: map[string]any{"filePath": "src/a.go"}},
{ID: "n2", Labels: []string{"Function"}, Properties: map[string]any{"name": "doThing"}},
},
Relationships: []Relationship{
{ID: "r1", Type: "defines_function", StartNode: "n1", EndNode: "n2"},
},
},
}
g := GraphFromShardIR(ir)

if len(g.Nodes) != 2 {
t.Errorf("nodes: got %d, want 2", len(g.Nodes))
}
if len(g.Relationships) != 1 {
t.Errorf("relationships: got %d, want 1", len(g.Relationships))
}
if g.Nodes[0].ID != "n1" {
t.Errorf("first node ID: got %q", g.Nodes[0].ID)
}
}

func TestGraphFromShardIR_RepoID(t *testing.T) {
ir := &ShardIR{Repo: "acme/backend"}
g := GraphFromShardIR(ir)
if got := g.RepoID(); got != "acme/backend" {
t.Errorf("RepoID: got %q, want 'acme/backend'", got)
}
}

func TestGraphFromShardIR_RelsViaRels(t *testing.T) {
// Rels() should return the Relationships slice (not Edges)
ir := &ShardIR{
Graph: ShardGraph{
Relationships: []Relationship{
{ID: "r1", Type: "imports"},
{ID: "r2", Type: "calls"},
},
},
}
g := GraphFromShardIR(ir)
rels := g.Rels()
if len(rels) != 2 {
t.Errorf("Rels(): got %d, want 2", len(rels))
}
}

func TestGraphFromShardIR_Empty(t *testing.T) {
ir := &ShardIR{}
g := GraphFromShardIR(ir)
if g == nil {
t.Fatal("GraphFromShardIR returned nil")
}
if len(g.Nodes) != 0 {
t.Errorf("empty IR: expected 0 nodes, got %d", len(g.Nodes))
}
if g.RepoID() != "" {
t.Errorf("empty IR: expected empty repoId, got %q", g.RepoID())
}
}

func TestGraphFromShardIR_NodeByID(t *testing.T) {
ir := &ShardIR{
Graph: ShardGraph{
Nodes: []Node{
{ID: "fn1", Labels: []string{"Function"}, Properties: map[string]any{"name": "myFunc"}},
},
},
}
g := GraphFromShardIR(ir)
n, ok := g.NodeByID("fn1")
if !ok {
t.Fatal("NodeByID('fn1') returned false")
}
if n.Prop("name") != "myFunc" {
t.Errorf("name prop: got %q", n.Prop("name"))
}
}

func TestGraphFromShardIR_NodesByLabel(t *testing.T) {
ir := &ShardIR{
Graph: ShardGraph{
Nodes: []Node{
{ID: "f1", Labels: []string{"File"}},
{ID: "fn1", Labels: []string{"Function"}},
{ID: "f2", Labels: []string{"File"}},
},
},
}
g := GraphFromShardIR(ir)
files := g.NodesByLabel("File")
if len(files) != 2 {
t.Errorf("NodesByLabel('File'): got %d, want 2", len(files))
}
}

func containsStr(s, sub string) bool {
return len(s) >= len(sub) && (s == sub ||
func() bool {
Expand Down
Loading
Loading