Files
kandev/apps/backend/internal/backendapp/adapters_test.go
T
2026-07-21 23:24:22 +01:00

401 lines
12 KiB
Go

package backendapp
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"sync"
"testing"
"github.com/kandev/kandev/internal/common/logger"
githubsvc "github.com/kandev/kandev/internal/github"
"github.com/kandev/kandev/internal/repoclone"
taskrepo "github.com/kandev/kandev/internal/task/repository/sqlite"
taskservice "github.com/kandev/kandev/internal/task/service"
)
func newTestLogger() *logger.Logger {
log, _ := logger.NewLogger(logger.LoggingConfig{
Level: "error",
Format: "json",
})
return log
}
func TestDetectGitDefaultBranchDetachedHEADReturnsEmpty(t *testing.T) {
repoPath := t.TempDir()
gitDir := filepath.Join(repoPath, ".git")
if err := os.MkdirAll(gitDir, 0o755); err != nil {
t.Fatalf("mkdir git dir: %v", err)
}
if err := os.WriteFile(
filepath.Join(gitDir, "HEAD"),
[]byte("3a3f2d3b00000000000000000000000000000000\n"),
0o644,
); err != nil {
t.Fatalf("write detached HEAD: %v", err)
}
if got := detectGitDefaultBranch(repoPath); got != "" {
t.Fatalf("detectGitDefaultBranch = %q, want empty", got)
}
}
func TestResolveReviewBaseBranchRedetectsStoredMasterWhenMainExists(t *testing.T) {
harness := newBootStateTestHarness(t)
ctx := context.Background()
workspace, err := harness.taskSvc.CreateWorkspace(ctx, &taskservice.CreateWorkspaceRequest{
Name: "Workspace",
})
if err != nil {
t.Fatalf("CreateWorkspace: %v", err)
}
repoPath := t.TempDir()
gitDir := filepath.Join(repoPath, ".git")
if err := os.MkdirAll(filepath.Join(gitDir, "refs", "remotes", "origin"), 0o755); err != nil {
t.Fatalf("mkdir origin refs: %v", err)
}
if err := os.WriteFile(filepath.Join(gitDir, "HEAD"), []byte("ref: refs/heads/feature/x\n"), 0o644); err != nil {
t.Fatalf("write HEAD: %v", err)
}
if err := os.WriteFile(
filepath.Join(gitDir, "refs", "remotes", "origin", "HEAD"),
[]byte("ref: refs/remotes/origin/master\n"),
0o644,
); err != nil {
t.Fatalf("write origin HEAD: %v", err)
}
for _, branch := range []string{"main", "master"} {
refPath := filepath.Join(gitDir, "refs", "remotes", "origin", branch)
if err := os.WriteFile(refPath, []byte("0000000\n"), 0o644); err != nil {
t.Fatalf("write %s ref: %v", branch, err)
}
}
repo, err := harness.taskSvc.CreateRepository(ctx, &taskservice.CreateRepositoryRequest{
WorkspaceID: workspace.ID,
Name: "owner/repo",
SourceType: "provider",
LocalPath: repoPath,
Provider: "github",
ProviderOwner: "owner",
ProviderName: "repo",
DefaultBranch: "master",
})
if err != nil {
t.Fatalf("CreateRepository: %v", err)
}
adapter := &repositoryResolverAdapter{
taskSvc: harness.taskSvc,
logger: newTestLogger(),
}
if got := adapter.resolveReviewBaseBranch(ctx, repo, repoPath, ""); got != "main" {
t.Fatalf("resolveReviewBaseBranch = %q, want main", got)
}
stored, err := harness.taskSvc.GetRepository(ctx, repo.ID)
if err != nil {
t.Fatalf("GetRepository: %v", err)
}
if stored.DefaultBranch != "main" {
t.Fatalf("stored default_branch = %q, want main", stored.DefaultBranch)
}
}
func TestResolveReviewBaseBranchSkipsNoopPersistForStoredMaster(t *testing.T) {
harness := newBootStateTestHarness(t)
ctx := context.Background()
workspace, err := harness.taskSvc.CreateWorkspace(ctx, &taskservice.CreateWorkspaceRequest{
Name: "Workspace",
})
if err != nil {
t.Fatalf("CreateWorkspace: %v", err)
}
repoPath := t.TempDir()
gitDir := filepath.Join(repoPath, ".git")
if err := os.MkdirAll(filepath.Join(gitDir, "refs", "remotes", "origin"), 0o755); err != nil {
t.Fatalf("mkdir origin refs: %v", err)
}
if err := os.WriteFile(filepath.Join(gitDir, "HEAD"), []byte("ref: refs/heads/feature/x\n"), 0o644); err != nil {
t.Fatalf("write HEAD: %v", err)
}
if err := os.WriteFile(
filepath.Join(gitDir, "refs", "remotes", "origin", "HEAD"),
[]byte("ref: refs/remotes/origin/master\n"),
0o644,
); err != nil {
t.Fatalf("write origin HEAD: %v", err)
}
refPath := filepath.Join(gitDir, "refs", "remotes", "origin", "master")
if err := os.WriteFile(refPath, []byte("0000000\n"), 0o644); err != nil {
t.Fatalf("write master ref: %v", err)
}
repo, err := harness.taskSvc.CreateRepository(ctx, &taskservice.CreateRepositoryRequest{
WorkspaceID: workspace.ID,
Name: "owner/repo",
SourceType: "provider",
LocalPath: repoPath,
Provider: "github",
ProviderOwner: "owner",
ProviderName: "repo",
DefaultBranch: "master",
})
if err != nil {
t.Fatalf("CreateRepository: %v", err)
}
adapter := &repositoryResolverAdapter{
taskSvc: harness.taskSvc,
logger: newTestLogger(),
}
if got := adapter.resolveReviewBaseBranch(ctx, repo, repoPath, ""); got != "master" {
t.Fatalf("resolveReviewBaseBranch = %q, want master", got)
}
stored, err := harness.taskSvc.GetRepository(ctx, repo.ID)
if err != nil {
t.Fatalf("GetRepository: %v", err)
}
if !stored.UpdatedAt.Equal(repo.UpdatedAt) {
t.Fatalf("stored updated_at = %v, want unchanged %v", stored.UpdatedAt, repo.UpdatedAt)
}
}
func TestResolveReviewBaseBranchKeepsStoredMasterOnHeadFallback(t *testing.T) {
harness := newBootStateTestHarness(t)
ctx := context.Background()
workspace, err := harness.taskSvc.CreateWorkspace(ctx, &taskservice.CreateWorkspaceRequest{
Name: "Workspace",
})
if err != nil {
t.Fatalf("CreateWorkspace: %v", err)
}
repoPath := t.TempDir()
gitDir := filepath.Join(repoPath, ".git")
if err := os.MkdirAll(gitDir, 0o755); err != nil {
t.Fatalf("mkdir git dir: %v", err)
}
if err := os.WriteFile(filepath.Join(gitDir, "HEAD"), []byte("ref: refs/heads/feature/x\n"), 0o644); err != nil {
t.Fatalf("write HEAD: %v", err)
}
repo, err := harness.taskSvc.CreateRepository(ctx, &taskservice.CreateRepositoryRequest{
WorkspaceID: workspace.ID,
Name: "owner/repo",
SourceType: "provider",
LocalPath: repoPath,
Provider: "github",
ProviderOwner: "owner",
ProviderName: "repo",
DefaultBranch: "master",
})
if err != nil {
t.Fatalf("CreateRepository: %v", err)
}
adapter := &repositoryResolverAdapter{
taskSvc: harness.taskSvc,
logger: newTestLogger(),
}
if got := adapter.resolveReviewBaseBranch(ctx, repo, repoPath, ""); got != "master" {
t.Fatalf("resolveReviewBaseBranch = %q, want master", got)
}
stored, err := harness.taskSvc.GetRepository(ctx, repo.ID)
if err != nil {
t.Fatalf("GetRepository: %v", err)
}
if stored.DefaultBranch != "master" {
t.Fatalf("stored default_branch = %q, want master", stored.DefaultBranch)
}
if !stored.UpdatedAt.Equal(repo.UpdatedAt) {
t.Fatalf("stored updated_at = %v, want unchanged %v", stored.UpdatedAt, repo.UpdatedAt)
}
}
func TestResolveForReviewRedetectsStoredMasterAfterClonePath(t *testing.T) {
harness := newBootStateTestHarness(t)
ctx := context.Background()
workspace, err := harness.taskSvc.CreateWorkspace(ctx, &taskservice.CreateWorkspaceRequest{
Name: "Workspace",
})
if err != nil {
t.Fatalf("CreateWorkspace: %v", err)
}
basePath := t.TempDir()
repoPath := filepath.Join(basePath, "owner", "repo")
gitDir := filepath.Join(repoPath, ".git")
if err := os.MkdirAll(filepath.Join(gitDir, "refs", "remotes", "origin"), 0o755); err != nil {
t.Fatalf("mkdir origin refs: %v", err)
}
if err := os.WriteFile(filepath.Join(gitDir, "HEAD"), []byte("ref: refs/heads/feature/x\n"), 0o644); err != nil {
t.Fatalf("write HEAD: %v", err)
}
if err := os.WriteFile(
filepath.Join(gitDir, "refs", "remotes", "origin", "HEAD"),
[]byte("ref: refs/remotes/origin/master\n"),
0o644,
); err != nil {
t.Fatalf("write origin HEAD: %v", err)
}
for _, branch := range []string{"main", "master"} {
refPath := filepath.Join(gitDir, "refs", "remotes", "origin", branch)
if err := os.WriteFile(refPath, []byte("0000000\n"), 0o644); err != nil {
t.Fatalf("write %s ref: %v", branch, err)
}
}
repo, err := harness.taskSvc.CreateRepository(ctx, &taskservice.CreateRepositoryRequest{
WorkspaceID: workspace.ID,
Name: "owner/repo",
SourceType: "provider",
Provider: "github",
ProviderHost: "https://github.com",
ProviderOwner: "owner",
ProviderName: "repo",
DefaultBranch: "master",
})
if err != nil {
t.Fatalf("CreateRepository: %v", err)
}
adapter := &repositoryResolverAdapter{
cloner: repoclone.NewCloner(repoclone.Config{BasePath: basePath}, repoclone.ProtocolHTTPS, "", newTestLogger()),
taskSvc: harness.taskSvc,
logger: newTestLogger(),
}
repoID, baseBranch, err := adapter.ResolveForReview(ctx, workspace.ID, "github", "owner", "repo", "")
if err != nil {
t.Fatalf("ResolveForReview: %v", err)
}
if repoID != repo.ID {
t.Fatalf("repository ID = %q, want %q", repoID, repo.ID)
}
if baseBranch != "main" {
t.Fatalf("base branch = %q, want main", baseBranch)
}
stored, err := harness.taskSvc.GetRepository(ctx, repo.ID)
if err != nil {
t.Fatalf("GetRepository: %v", err)
}
if stored.LocalPath != repoPath {
t.Fatalf("stored local_path = %q, want %q", stored.LocalPath, repoPath)
}
if stored.DefaultBranch != "main" {
t.Fatalf("stored default_branch = %q, want main", stored.DefaultBranch)
}
}
func TestGetSessionModel_Caching(t *testing.T) {
// We can't easily instantiate a full taskservice.Service here without a DB,
// so we test the caching mechanism directly on the adapter struct.
adapter := &messageCreatorAdapter{
svc: nil, // Will cause getSessionModel to return "" on cache miss (nil svc panics)
logger: newTestLogger(),
}
// Pre-populate the cache to avoid calling the nil svc
adapter.sessionModelMu.Lock()
adapter.sessionModelCache = map[string]string{
"session-1": "claude-sonnet-4",
"session-2": "gpt-4",
}
adapter.sessionModelMu.Unlock()
// Test cache hit
model := adapter.getSessionModel(context.Background(), "session-1")
if model != "claude-sonnet-4" {
t.Errorf("expected 'claude-sonnet-4', got %q", model)
}
model = adapter.getSessionModel(context.Background(), "session-2")
if model != "gpt-4" {
t.Errorf("expected 'gpt-4', got %q", model)
}
// Test cache miss for unknown session returns ""
// (svc is nil, so DB lookup would fail gracefully)
// We need a non-nil svc to avoid panic - use a minimal mock approach
// Instead, verify the cache was populated for existing entries
adapter.sessionModelMu.RLock()
if len(adapter.sessionModelCache) != 2 {
t.Errorf("expected 2 cached entries, got %d", len(adapter.sessionModelCache))
}
adapter.sessionModelMu.RUnlock()
}
func TestGetSessionModel_ConcurrentAccess(t *testing.T) {
adapter := &messageCreatorAdapter{
svc: nil,
logger: newTestLogger(),
}
// Pre-populate cache
adapter.sessionModelMu.Lock()
adapter.sessionModelCache = map[string]string{
"session-1": "claude-sonnet-4",
}
adapter.sessionModelMu.Unlock()
// Concurrent reads should not race
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
model := adapter.getSessionModel(context.Background(), "session-1")
if model != "claude-sonnet-4" {
t.Errorf("expected 'claude-sonnet-4', got %q", model)
}
}()
}
wg.Wait()
}
func TestGetSessionModel_LazyInit(t *testing.T) {
// Verify that the cache map is lazily initialized (nil initially)
adapter := &messageCreatorAdapter{
svc: nil,
logger: newTestLogger(),
}
// sessionModelCache should be nil initially
adapter.sessionModelMu.RLock()
if adapter.sessionModelCache != nil {
t.Error("expected sessionModelCache to be nil initially")
}
adapter.sessionModelMu.RUnlock()
}
// Verify the adapter compiles with the taskservice.Service field
func TestMessageCreatorAdapter_StructFields(t *testing.T) {
adapter := &messageCreatorAdapter{
svc: (*taskservice.Service)(nil),
logger: newTestLogger(),
}
if adapter.svc != nil {
t.Error("expected nil svc")
}
}
func TestWrapGitHubTaskIssueStoreError(t *testing.T) {
taskErr := fmt.Errorf("load task: %w", taskrepo.ErrTaskNotFound)
wrapped := wrapGitHubTaskIssueStoreError(taskErr)
if !errors.Is(wrapped, githubsvc.ErrTaskNotFound) {
t.Fatalf("wrapped error should match github ErrTaskNotFound: %v", wrapped)
}
if !errors.Is(wrapped, taskrepo.ErrTaskNotFound) {
t.Fatalf("wrapped error should preserve task repo ErrTaskNotFound: %v", wrapped)
}
otherErr := errors.New("database unavailable")
if got := wrapGitHubTaskIssueStoreError(otherErr); got != otherErr {
t.Fatalf("non-not-found error changed: got %v, want %v", got, otherErr)
}
}
// jiraSecretAdapter Set/Exists branching is tested in
// internal/integrations/secretadapter/secretadapter_test.go now that the
// upsert helper lives there.