package backendapp import ( "context" "errors" "fmt" "os" "path/filepath" "reflect" "sync" "testing" runtimeapi "github.com/kandev/kandev/internal/agent/runtime" "github.com/kandev/kandev/internal/agent/runtime/lifecycle" "github.com/kandev/kandev/internal/common/logger" githubsvc "github.com/kandev/kandev/internal/github" orchestratorexecutor "github.com/kandev/kandev/internal/orchestrator/executor" "github.com/kandev/kandev/internal/repoclone" taskmodels "github.com/kandev/kandev/internal/task/models" 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 TestBuildLifecycleLaunchRequestForwardsRemoteContributions(t *testing.T) { binding := &taskmodels.RemoteContribution{CanonicalURL: "https://github.com/acme/widget/pull/7"} req := &orchestratorexecutor.LaunchAgentRequest{ RemoteContribution: binding, Repositories: []orchestratorexecutor.RepoSpec{{RemoteContribution: binding}}, } launchReq := buildLifecycleLaunchRequest(req, "/workspace", "office-profile") if launchReq.RemoteContribution != binding { t.Fatalf("top-level remote contribution was not forwarded") } if len(launchReq.Repositories) != 1 || launchReq.Repositories[0].RemoteContribution != binding { t.Fatalf("per-repository remote contribution was not forwarded: %#v", launchReq.Repositories) } } // distinctFiller populates struct fields with non-zero values that are unique // per field, so a mapper that drops a field (or crosswires two of the same // type) produces an observable difference rather than two matching zero // values. Bool is the documented exception — see fill. type distinctFiller struct{ n int } // fillStruct populates every exported field of the struct pointed at by ptr. func (f *distinctFiller) fillStruct(t *testing.T, ptr any) { t.Helper() v := reflect.ValueOf(ptr).Elem() typ := v.Type() for i := 0; i < typ.NumField(); i++ { field := typ.Field(i) if !v.Field(i).CanSet() { t.Fatalf("distinctFiller: %s.%s is unexported and cannot be covered", typ, field.Name) } f.fill(t, v.Field(i), field.Name) } } func (f *distinctFiller) fill(t *testing.T, v reflect.Value, name string) { t.Helper() f.n++ switch v.Kind() { case reflect.String: v.SetString(fmt.Sprintf("%s-%d", name, f.n)) case reflect.Bool: // Always true, never counter-derived: bool has only two values and one // of them is the zero value, so distinctness across several bool fields // and drop-detection are mutually exclusive. Drop-detection wins — // a field left unmapped reads back false and fails. The cost is that // two crosswired bool fields would still compare equal. v.SetBool(true) case reflect.Int, reflect.Int64: v.SetInt(int64(f.n)) case reflect.Pointer: // Deliberately shallow: a zero-value allocation is enough to catch a // dropped pointer field (nil != non-nil). It does not catch a mapper // that substitutes a freshly allocated zero value of the same type, // since reflect.DeepEqual compares pointees rather than addresses. v.Set(reflect.New(v.Type().Elem())) case reflect.Slice: slice := reflect.MakeSlice(v.Type(), 1, 1) f.fill(t, slice.Index(0), name+"-elem") v.Set(slice) case reflect.Map: m := reflect.MakeMap(v.Type()) key := reflect.New(v.Type().Key()).Elem() f.fill(t, key, name+"-key") value := reflect.New(v.Type().Elem()).Elem() f.fill(t, value, name+"-value") m.SetMapIndex(key, value) v.Set(m) default: t.Fatalf("distinctFiller: unsupported kind %s for field %s — extend the helper", v.Kind(), name) } } // assertSameNamedFieldsEqual fails unless src and dst expose the same number of // exported fields and every same-named pair holds a DeepEqual value. Mapper // tests use it so that dropping a field — or adding one to both sides of a // mirrored type pair and forgetting to wire it — fails instead of passing // silently the way a hand-written subset of field assertions would. func assertSameNamedFieldsEqual(t *testing.T, label string, src, dst any) { t.Helper() srcVal, dstVal := reflect.ValueOf(src), reflect.ValueOf(dst) srcType, dstType := srcVal.Type(), dstVal.Type() if srcType.NumField() != dstType.NumField() { t.Fatalf("%s: %s has %d fields but %s has %d — the mirrored types drifted", label, srcType, srcType.NumField(), dstType, dstType.NumField()) } for i := 0; i < srcType.NumField(); i++ { name := srcType.Field(i).Name dstField := dstVal.FieldByName(name) if !dstField.IsValid() { t.Errorf("%s: %s has no field %s", label, dstType, name) continue } want := srcVal.Field(i).Interface() if got := dstField.Interface(); !reflect.DeepEqual(got, want) { t.Errorf("%s: field %s = %#v, want %#v", label, name, got, want) } } } // TestLifecycleWorkspaceFoldersMapsEveryField covers the launch path's folder // projection. Every agent launch flows through it and a dropped field is // silent — no panic, no error, just missing agent configuration. func TestLifecycleWorkspaceFoldersMapsEveryField(t *testing.T) { if got := lifecycleWorkspaceFolders(nil); got != nil { t.Errorf("lifecycleWorkspaceFolders(nil) = %#v, want nil", got) } if got := lifecycleWorkspaceFolders([]orchestratorexecutor.WorkspaceFolderSpec{}); got != nil { t.Errorf("lifecycleWorkspaceFolders(empty) = %#v, want nil", got) } filler := &distinctFiller{} folders := make([]orchestratorexecutor.WorkspaceFolderSpec, 2) for i := range folders { filler.fillStruct(t, &folders[i]) } got := lifecycleWorkspaceFolders(folders) if len(got) != len(folders) { t.Fatalf("lifecycleWorkspaceFolders returned %d folders, want %d", len(got), len(folders)) } for i := range folders { assertSameNamedFieldsEqual(t, fmt.Sprintf("workspace folder %d", i), folders[i], got[i]) } } // TestLifecycleRouteOverrideMapsEveryField pins the same all-fields contract on // the sibling route-override mapper. func TestLifecycleRouteOverrideMapsEveryField(t *testing.T) { if got := lifecycleRouteOverride(nil); got != nil { t.Errorf("lifecycleRouteOverride(nil) = %#v, want nil", got) } override := &orchestratorexecutor.RouteOverride{} (&distinctFiller{}).fillStruct(t, override) got := lifecycleRouteOverride(override) if got == nil { t.Fatal("lifecycleRouteOverride returned nil for a non-nil override") } assertSameNamedFieldsEqual(t, "route override", *override, *got) } // TestLifecycleRepoLaunchSpecsMapsEveryField pins the all-fields contract on // the multi-repo launch spec mapper — the widest of the three (19 fields). func TestLifecycleRepoLaunchSpecsMapsEveryField(t *testing.T) { if got := lifecycleRepoLaunchSpecs(nil); got != nil { t.Errorf("lifecycleRepoLaunchSpecs(nil) = %#v, want nil", got) } if got := lifecycleRepoLaunchSpecs([]orchestratorexecutor.RepoSpec{}); got != nil { t.Errorf("lifecycleRepoLaunchSpecs(empty) = %#v, want nil", got) } filler := &distinctFiller{} repos := make([]orchestratorexecutor.RepoSpec, 2) for i := range repos { filler.fillStruct(t, &repos[i]) } got := lifecycleRepoLaunchSpecs(repos) if len(got) != len(repos) { t.Fatalf("lifecycleRepoLaunchSpecs returned %d specs, want %d", len(got), len(repos)) } for i := range repos { assertSameNamedFieldsEqual(t, fmt.Sprintf("repo spec %d", i), repos[i], got[i]) } } // TestBuildLifecycleLaunchRequestWiresMappedCollections asserts the launch // request actually carries the three mapper outputs. A mapper can be correct // while the wiring in buildLifecycleLaunchRequest drops it. func TestBuildLifecycleLaunchRequestWiresMappedCollections(t *testing.T) { filler := &distinctFiller{} folders := make([]orchestratorexecutor.WorkspaceFolderSpec, 2) for i := range folders { filler.fillStruct(t, &folders[i]) } repos := make([]orchestratorexecutor.RepoSpec, 2) for i := range repos { filler.fillStruct(t, &repos[i]) } override := &orchestratorexecutor.RouteOverride{} filler.fillStruct(t, override) launchReq := buildLifecycleLaunchRequest(&orchestratorexecutor.LaunchAgentRequest{ WorkspaceFolders: folders, RouteOverride: override, Repositories: repos, }, "/workspace", "office-profile") if len(launchReq.WorkspaceFolders) != len(folders) { t.Fatalf("WorkspaceFolders length = %d, want %d", len(launchReq.WorkspaceFolders), len(folders)) } for i := range folders { assertSameNamedFieldsEqual(t, fmt.Sprintf("launch workspace folder %d", i), folders[i], launchReq.WorkspaceFolders[i]) } if launchReq.RouteOverride == nil { t.Fatal("RouteOverride was dropped from the launch request") } assertSameNamedFieldsEqual(t, "launch route override", *override, *launchReq.RouteOverride) if len(launchReq.Repositories) != len(repos) { t.Fatalf("Repositories length = %d, want %d", len(launchReq.Repositories), len(repos)) } for i := range repos { assertSameNamedFieldsEqual(t, fmt.Sprintf("launch repo spec %d", i), repos[i], launchReq.Repositories[i]) } } func TestBuildLifecycleLaunchRequestCarriesMCPProviders(t *testing.T) { want := []string{"github", "gitlab"} got := buildLifecycleLaunchRequest(&orchestratorexecutor.LaunchAgentRequest{ McpProviders: want, }, "", "") if len(got.McpProviders) != len(want) || got.McpProviders[0] != want[0] || got.McpProviders[1] != want[1] { t.Fatalf("McpProviders = %#v, want %#v", got.McpProviders, want) } } 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 := canonicalTempDir(t) cloner := repoclone.NewCloner( repoclone.Config{BasePath: basePath}, repoclone.ProtocolHTTPS, "", newTestLogger(), ) cloner.SetGitCredentialProvider(staticRepoCloneCredential{}) repoPath, err := cloner.WorkspaceRepoPath(workspace.ID, "github", "owner", "repo") if err != nil { t.Fatalf("WorkspaceRepoPath: %v", err) } 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: cloner, 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) } } type staticRepoCloneCredential struct{} func (staticRepoCloneCredential) ResolveGitCredential( context.Context, repoclone.GitCredentialRequest, ) (string, string, error) { return "x-access-token", "test-token", nil } 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) } } // TestNormalizeRuntimeStopError asserts the lifecycle adapter maps the backend // lifecycle not-found sentinel onto the public runtime not-found sentinel, so // orchestrator/reconciliation can depend only on runtimeapi.ErrNotFound and // never has to import runtime/lifecycle. Unrelated errors pass through unchanged. func TestNormalizeRuntimeStopError(t *testing.T) { if got := normalizeRuntimeStopError(nil); got != nil { t.Fatalf("normalizeRuntimeStopError(nil) = %v, want nil", got) } wrapped := fmt.Errorf("stop agent: %w", lifecycle.ErrExecutionNotFound) got := normalizeRuntimeStopError(wrapped) if !errors.Is(got, runtimeapi.ErrNotFound) { t.Fatalf("lifecycle not-found must normalize to runtimeapi.ErrNotFound; got %v", got) } if !errors.Is(got, lifecycle.ErrExecutionNotFound) { t.Fatalf("original lifecycle sentinel must be preserved for diagnostics; got %v", got) } other := errors.New("agentctl unreachable") if got := normalizeRuntimeStopError(other); got != other { t.Fatalf("unrelated error must pass through unchanged; got %v, want %v", got, other) } } // jiraSecretAdapter Set/Exists branching is tested in // internal/integrations/secretadapter/secretadapter_test.go now that the // upsert helper lives there.