948743600e
Tier 1 of the post-review cleanup. Each item is independently safe; bundling because they're all small, mechanical, and lint-passing. - fix(clone): unify injectTokenIntoURL across Docker and Sprites — Sprites used to ignore GH_TOKEN entirely (a user with only GH_TOKEN set got 401 on Sprites clone but worked on Docker). Promote one `injectGitHubTokenIntoCloneURL` helper, accept both env vars (GITHUB_TOKEN wins, matching Docker), and use the documented `https://x-access-token:T@github.com/` form on both surfaces. - fix(executor-popover): swap the <span tabIndex role="button"> trigger for <Button variant="ghost"> so Enter/Space activation, focus rings on Safari, and disabled semantics come for free. - test(docker): rewrite TestDockerStopInstance_PreservesSessionDirOnPlainStop to drive the real DockerExecutor.StopInstance path with ContainerID="" (skips the docker daemon, exercises the kandev-side cleanup branch). Previous version only iterated shouldRunExecutorCleanup strings — would pass even if StopInstance always cleaned up. Now table-driven over preserve+destructive reasons with on-disk asserts. - refactor(orchestrator): drop dead `_ = arg` parameters from persist* helpers (persistResumeState, persistModelSwitchState, persistLaunchState). Brings signatures from 9→6 args, under the funlen lint cap. - refactor(orchestrator): collapse byte-identical isContainerizedExecutor ≡ isCloneBasedExecutorType predicate into one. - refactor(lifecycle): drop dead getMetadataBool, FileUploader.RunCommand, expandMountSource {home} arm (no production agent ships {home} in Mounts; codex_acp_test asserts it), dockerTaskBranch one-line wrapper.
624 lines
18 KiB
Go
624 lines
18 KiB
Go
package lifecycle
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/kandev/kandev/internal/agent/agents"
|
|
"github.com/kandev/kandev/internal/agent/docker"
|
|
"github.com/kandev/kandev/internal/agent/executor"
|
|
"github.com/kandev/kandev/internal/common/config"
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
)
|
|
|
|
func newTestDockerLogger() *logger.Logger {
|
|
log, _ := logger.NewLogger(logger.LoggingConfig{Level: "error", Format: "json"})
|
|
return log
|
|
}
|
|
|
|
// failingClientFactory returns a factory that always fails.
|
|
func failingClientFactory(errMsg string) func(config.DockerConfig, *logger.Logger) (*docker.Client, error) {
|
|
return func(_ config.DockerConfig, _ *logger.Logger) (*docker.Client, error) {
|
|
return nil, fmt.Errorf("%s", errMsg)
|
|
}
|
|
}
|
|
|
|
func TestNewDockerExecutor(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
exec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
|
|
if exec == nil {
|
|
t.Fatal("expected non-nil executor")
|
|
}
|
|
if exec.initialized {
|
|
t.Error("expected initialized to be false")
|
|
}
|
|
if exec.docker != nil {
|
|
t.Error("expected docker client to be nil before first use")
|
|
}
|
|
if exec.containerMgr != nil {
|
|
t.Error("expected container manager to be nil before first use")
|
|
}
|
|
if exec.newClientFunc == nil {
|
|
t.Error("expected newClientFunc to be set")
|
|
}
|
|
}
|
|
|
|
func TestDockerExecutor_Name(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
exec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
|
|
if exec.Name() != executor.NameDocker {
|
|
t.Errorf("expected name %q, got %q", executor.NameDocker, exec.Name())
|
|
}
|
|
}
|
|
|
|
func TestDockerExecutor_HealthCheck(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
exec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
|
|
if err := exec.HealthCheck(context.Background()); err != nil {
|
|
t.Errorf("expected nil error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDockerExecutor_RecoverInstances(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
exec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
|
|
instances, err := exec.RecoverInstances(context.Background())
|
|
if err != nil {
|
|
t.Errorf("expected nil error, got: %v", err)
|
|
}
|
|
if instances != nil {
|
|
t.Errorf("expected nil instances, got: %v", instances)
|
|
}
|
|
}
|
|
|
|
func TestDockerExecutor_GetInteractiveRunner(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
exec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
|
|
if runner := exec.GetInteractiveRunner(); runner != nil {
|
|
t.Error("expected nil interactive runner for docker executor")
|
|
}
|
|
}
|
|
|
|
func TestDockerExecutor_EnsureClient_Success(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
exec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
// Default factory uses docker.NewClient which succeeds even without Docker running
|
|
|
|
cli, cm, err := exec.ensureClient()
|
|
if err != nil {
|
|
t.Fatalf("expected nil error, got: %v", err)
|
|
}
|
|
if cli == nil {
|
|
t.Error("expected non-nil client")
|
|
}
|
|
if cm == nil {
|
|
t.Error("expected non-nil container manager")
|
|
}
|
|
if !exec.initialized {
|
|
t.Error("expected initialized to be true after success")
|
|
}
|
|
|
|
// Second call should return cached values
|
|
cli2, cm2, err2 := exec.ensureClient()
|
|
if err2 != nil {
|
|
t.Fatalf("expected nil error on second call, got: %v", err2)
|
|
}
|
|
if cli2 != cli {
|
|
t.Error("expected same client on second call")
|
|
}
|
|
if cm2 != cm {
|
|
t.Error("expected same container manager on second call")
|
|
}
|
|
|
|
// Clean up
|
|
_ = exec.Close()
|
|
}
|
|
|
|
func TestDockerExecutor_EnsureClient_RetriesOnFailure(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
exec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
|
|
var callCount atomic.Int32
|
|
exec.newClientFunc = func(_ config.DockerConfig, _ *logger.Logger) (*docker.Client, error) {
|
|
callCount.Add(1)
|
|
return nil, fmt.Errorf("docker daemon not running")
|
|
}
|
|
|
|
// First call should fail
|
|
cli, cm, err := exec.ensureClient()
|
|
if err == nil {
|
|
t.Fatal("expected error on first call")
|
|
}
|
|
if cli != nil || cm != nil {
|
|
t.Error("expected nil client and container manager on failure")
|
|
}
|
|
if exec.initialized {
|
|
t.Error("expected initialized to be false after failure")
|
|
}
|
|
|
|
// Second call should retry (not return a cached error like sync.Once would)
|
|
_, _, err2 := exec.ensureClient()
|
|
if err2 == nil {
|
|
t.Fatal("expected error on second call")
|
|
}
|
|
if callCount.Load() != 2 {
|
|
t.Errorf("expected factory to be called twice (retry), got %d calls", callCount.Load())
|
|
}
|
|
}
|
|
|
|
func TestDockerExecutor_EnsureClient_RecoversAfterTransientFailure(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
exec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
|
|
var callCount atomic.Int32
|
|
exec.newClientFunc = func(cfg config.DockerConfig, l *logger.Logger) (*docker.Client, error) {
|
|
n := callCount.Add(1)
|
|
if n == 1 {
|
|
return nil, fmt.Errorf("transient error")
|
|
}
|
|
// Succeed on second call using real factory
|
|
return docker.NewClient(cfg, l)
|
|
}
|
|
|
|
// First call fails
|
|
_, _, err := exec.ensureClient()
|
|
if err == nil {
|
|
t.Fatal("expected error on first call")
|
|
}
|
|
|
|
// Second call succeeds
|
|
cli, cm, err := exec.ensureClient()
|
|
if err != nil {
|
|
t.Fatalf("expected nil error after recovery, got: %v", err)
|
|
}
|
|
if cli == nil || cm == nil {
|
|
t.Error("expected non-nil client and container manager after recovery")
|
|
}
|
|
if !exec.initialized {
|
|
t.Error("expected initialized to be true after recovery")
|
|
}
|
|
|
|
_ = exec.Close()
|
|
}
|
|
|
|
func TestDockerExecutor_Client_ReturnsNilOnFailure(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
exec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
exec.newClientFunc = failingClientFactory("docker unavailable")
|
|
|
|
if cli := exec.Client(); cli != nil {
|
|
t.Error("expected nil client when Docker is unavailable")
|
|
}
|
|
}
|
|
|
|
func TestDockerExecutor_ContainerMgr_ReturnsNilOnFailure(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
exec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
exec.newClientFunc = failingClientFactory("docker unavailable")
|
|
|
|
if cm := exec.ContainerMgr(); cm != nil {
|
|
t.Error("expected nil container manager when Docker is unavailable")
|
|
}
|
|
}
|
|
|
|
func TestDockerExecutor_Client_ReturnsClientOnSuccess(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
exec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
|
|
cli := exec.Client()
|
|
if cli == nil {
|
|
t.Error("expected non-nil client with default config")
|
|
}
|
|
|
|
_ = exec.Close()
|
|
}
|
|
|
|
func TestDockerExecutor_Close_BeforeInit(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
exec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
|
|
if err := exec.Close(); err != nil {
|
|
t.Errorf("expected nil error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDockerExecutor_Close_AfterInit(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
exec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
|
|
// Initialize the client
|
|
_, _, _ = exec.ensureClient()
|
|
if !exec.initialized {
|
|
t.Fatal("expected initialized to be true")
|
|
}
|
|
|
|
// Close should succeed and reset state
|
|
if err := exec.Close(); err != nil {
|
|
t.Errorf("expected nil error, got: %v", err)
|
|
}
|
|
if exec.initialized {
|
|
t.Error("expected initialized to be false after close")
|
|
}
|
|
if exec.docker != nil {
|
|
t.Error("expected docker to be nil after close")
|
|
}
|
|
if exec.containerMgr != nil {
|
|
t.Error("expected containerMgr to be nil after close")
|
|
}
|
|
}
|
|
|
|
func TestDockerExecutor_Close_AfterFailedInit(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
exec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
exec.newClientFunc = failingClientFactory("docker unavailable")
|
|
|
|
// Trigger failed init
|
|
_, _, _ = exec.ensureClient()
|
|
|
|
// Close after failed init should be a no-op
|
|
if err := exec.Close(); err != nil {
|
|
t.Errorf("expected nil error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDockerClientProvider_NilRegistry(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
mgr := NewManager(nil, nil, nil, nil, nil, nil, ExecutorFallbackWarn, "", log)
|
|
|
|
provider := mgr.DockerClientProvider()
|
|
if provider == nil {
|
|
t.Fatal("expected non-nil provider function")
|
|
}
|
|
if client := provider(); client != nil {
|
|
t.Error("expected nil client from provider with nil registry")
|
|
}
|
|
}
|
|
|
|
func TestDockerClientProvider_NoDockerExecutor(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
registry := NewExecutorRegistry(log)
|
|
registry.Register(&MockExecutor{name: executor.NameStandalone})
|
|
mgr := NewManager(nil, nil, registry, nil, nil, nil, ExecutorFallbackWarn, "", log)
|
|
|
|
provider := mgr.DockerClientProvider()
|
|
if client := provider(); client != nil {
|
|
t.Error("expected nil client when no Docker executor is registered")
|
|
}
|
|
}
|
|
|
|
func TestDockerClientProvider_WithDockerExecutor(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
registry := NewExecutorRegistry(log)
|
|
dockerExec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
dockerExec.newClientFunc = failingClientFactory("docker unavailable")
|
|
registry.Register(dockerExec)
|
|
mgr := NewManager(nil, nil, registry, nil, nil, nil, ExecutorFallbackWarn, "", log)
|
|
|
|
provider := mgr.DockerClientProvider()
|
|
if client := provider(); client != nil {
|
|
t.Error("expected nil client from provider with unavailable Docker")
|
|
}
|
|
}
|
|
|
|
func TestDockerClientProvider_WithWorkingDocker(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
registry := NewExecutorRegistry(log)
|
|
dockerExec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
registry.Register(dockerExec)
|
|
mgr := NewManager(nil, nil, registry, nil, nil, nil, ExecutorFallbackWarn, "", log)
|
|
|
|
provider := mgr.DockerClientProvider()
|
|
client := provider()
|
|
if client == nil {
|
|
t.Error("expected non-nil client from provider with working Docker executor")
|
|
}
|
|
|
|
_ = dockerExec.Close()
|
|
}
|
|
|
|
func TestExecutorRegistry_CloseAll(t *testing.T) {
|
|
t.Run("closes closeable backends", func(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
registry := NewExecutorRegistry(log)
|
|
|
|
dockerExec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
registry.Register(dockerExec)
|
|
registry.Register(&MockExecutor{name: executor.NameStandalone})
|
|
|
|
// Should not panic
|
|
registry.CloseAll()
|
|
})
|
|
|
|
t.Run("empty registry", func(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
registry := NewExecutorRegistry(log)
|
|
|
|
// Should not panic
|
|
registry.CloseAll()
|
|
})
|
|
}
|
|
|
|
func TestInjectTokenIntoURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
env map[string]string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "HTTPS URL with GITHUB_TOKEN",
|
|
url: "https://github.com/org/repo.git",
|
|
env: map[string]string{"GITHUB_TOKEN": "ghp_test123"},
|
|
expected: "https://x-access-token:ghp_test123@github.com/org/repo.git",
|
|
},
|
|
{
|
|
name: "HTTPS URL with GH_TOKEN fallback",
|
|
url: "https://github.com/org/repo.git",
|
|
env: map[string]string{"GH_TOKEN": "ghp_ghtoken"},
|
|
expected: "https://x-access-token:ghp_ghtoken@github.com/org/repo.git",
|
|
},
|
|
{
|
|
name: "GITHUB_TOKEN takes priority over GH_TOKEN",
|
|
url: "https://github.com/org/repo.git",
|
|
env: map[string]string{"GITHUB_TOKEN": "ghp_primary", "GH_TOKEN": "ghp_secondary"},
|
|
expected: "https://x-access-token:ghp_primary@github.com/org/repo.git",
|
|
},
|
|
{
|
|
name: "SSH URL converted to authenticated HTTPS",
|
|
url: "git@github.com:org/repo.git",
|
|
env: map[string]string{"GITHUB_TOKEN": "ghp_test123"},
|
|
expected: "https://x-access-token:ghp_test123@github.com/org/repo.git",
|
|
},
|
|
{
|
|
name: "no token returns URL unchanged",
|
|
url: "https://github.com/org/repo.git",
|
|
env: map[string]string{},
|
|
expected: "https://github.com/org/repo.git",
|
|
},
|
|
{
|
|
name: "nil env returns URL unchanged",
|
|
url: "https://github.com/org/repo.git",
|
|
env: nil,
|
|
expected: "https://github.com/org/repo.git",
|
|
},
|
|
{
|
|
name: "non-GitHub URL unchanged",
|
|
url: "https://gitlab.com/org/repo.git",
|
|
env: map[string]string{"GITHUB_TOKEN": "ghp_test123"},
|
|
expected: "https://gitlab.com/org/repo.git",
|
|
},
|
|
{
|
|
name: "SSH URL without token unchanged",
|
|
url: "git@github.com:org/repo.git",
|
|
env: map[string]string{},
|
|
expected: "git@github.com:org/repo.git",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := injectGitHubTokenIntoCloneURL(tt.url, tt.env)
|
|
if got != tt.expected {
|
|
t.Errorf("injectTokenIntoURL(%q) = %q, want %q", tt.url, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReconnectToContainer_ValidationErrors(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
exec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
|
|
t.Run("short execution ID rejected", func(t *testing.T) {
|
|
req := &ExecutorCreateRequest{
|
|
PreviousExecutionID: "short",
|
|
}
|
|
_, err := exec.reconnectToContainer(context.Background(), nil, req)
|
|
if err == nil {
|
|
t.Fatal("expected error for short execution ID")
|
|
}
|
|
if !strings.Contains(err.Error(), "too short") {
|
|
t.Errorf("expected 'too short' error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("empty execution ID rejected", func(t *testing.T) {
|
|
req := &ExecutorCreateRequest{
|
|
PreviousExecutionID: "",
|
|
}
|
|
_, err := exec.reconnectToContainer(context.Background(), nil, req)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty execution ID")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestResolveReconnectContainerRef(t *testing.T) {
|
|
t.Run("uses stored container id before derived execution name", func(t *testing.T) {
|
|
req := &ExecutorCreateRequest{
|
|
PreviousExecutionID: "exec-previous",
|
|
Metadata: map[string]interface{}{
|
|
MetadataKeyContainerID: "container-real",
|
|
},
|
|
}
|
|
got, err := resolveReconnectContainerRef(req)
|
|
if err != nil {
|
|
t.Fatalf("resolveReconnectContainerRef returned error: %v", err)
|
|
}
|
|
if got != "container-real" {
|
|
t.Fatalf("expected stored container id, got %q", got)
|
|
}
|
|
})
|
|
|
|
t.Run("falls back to legacy name from previous execution id", func(t *testing.T) {
|
|
req := &ExecutorCreateRequest{PreviousExecutionID: "12345678-abcdef"}
|
|
got, err := resolveReconnectContainerRef(req)
|
|
if err != nil {
|
|
t.Fatalf("resolveReconnectContainerRef returned error: %v", err)
|
|
}
|
|
if got != "kandev-agent-12345678" {
|
|
t.Fatalf("expected derived container name, got %q", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestReconnectInstanceID(t *testing.T) {
|
|
t.Run("uses requested instance for new session in existing container", func(t *testing.T) {
|
|
req := &ExecutorCreateRequest{InstanceID: "exec-new"}
|
|
got := reconnectInstanceID(req, "exec-old")
|
|
if got != "exec-new" {
|
|
t.Fatalf("reconnectInstanceID = %q, want exec-new", got)
|
|
}
|
|
})
|
|
|
|
t.Run("falls back to previous execution for legacy callers", func(t *testing.T) {
|
|
got := reconnectInstanceID(&ExecutorCreateRequest{}, "exec-old")
|
|
if got != "exec-old" {
|
|
t.Fatalf("reconnectInstanceID = %q, want exec-old", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestShouldStartExistingDockerContainer(t *testing.T) {
|
|
tests := []struct {
|
|
state string
|
|
want bool
|
|
}{
|
|
{state: "created", want: true},
|
|
{state: "exited", want: true},
|
|
{state: "running", want: false},
|
|
{state: "paused", want: false},
|
|
{state: "dead", want: false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.state, func(t *testing.T) {
|
|
if got := shouldStartExistingDockerContainer(tt.state); got != tt.want {
|
|
t.Fatalf("shouldStartExistingDockerContainer(%q) = %v, want %v", tt.state, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildReconnectCreateInstanceRequest(t *testing.T) {
|
|
req := &ExecutorCreateRequest{
|
|
TaskID: "task-1",
|
|
SessionID: "session-1",
|
|
AgentConfig: &testAgent{
|
|
id: "codex",
|
|
enabled: true,
|
|
runtimeConfig: &agents.RuntimeConfig{
|
|
AssumeMcpSse: true,
|
|
},
|
|
},
|
|
Env: map[string]string{
|
|
"OPENAI_API_KEY": "token",
|
|
},
|
|
McpServers: []McpServerConfig{{Name: "test-mcp"}},
|
|
McpMode: "config",
|
|
}
|
|
|
|
got := buildReconnectCreateInstanceRequest(req, "previous-exec")
|
|
if got.ID != "previous-exec" {
|
|
t.Fatalf("ID = %q, want previous-exec", got.ID)
|
|
}
|
|
if got.WorkspacePath != dockerWorkspacePath {
|
|
t.Fatalf("WorkspacePath = %q, want %q", got.WorkspacePath, dockerWorkspacePath)
|
|
}
|
|
if got.AgentType != "codex" {
|
|
t.Fatalf("AgentType = %q, want codex", got.AgentType)
|
|
}
|
|
if got.SessionID != "session-1" || got.TaskID != "task-1" {
|
|
t.Fatalf("task/session not propagated: task=%q session=%q", got.TaskID, got.SessionID)
|
|
}
|
|
if got.Env["OPENAI_API_KEY"] != "token" {
|
|
t.Fatalf("env not propagated: %v", got.Env)
|
|
}
|
|
if len(got.McpServers) != 1 || got.McpServers[0].Name != "test-mcp" {
|
|
t.Fatalf("mcp servers not propagated: %v", got.McpServers)
|
|
}
|
|
if !got.AssumeMcpSse {
|
|
t.Fatal("expected AssumeMcpSse to be propagated")
|
|
}
|
|
if got.McpMode != "config" {
|
|
t.Fatalf("McpMode = %q, want config", got.McpMode)
|
|
}
|
|
}
|
|
|
|
func TestResolvePrepareScript(t *testing.T) {
|
|
log := newTestDockerLogger()
|
|
exec := NewDockerExecutor(config.DockerConfig{}, "", log)
|
|
|
|
t.Run("uses default docker script when no metadata", func(t *testing.T) {
|
|
req := &ExecutorCreateRequest{
|
|
Metadata: map[string]interface{}{
|
|
"repository_path": "/tmp/repo",
|
|
"base_branch": "main",
|
|
"repository_clone_url": "https://github.com/org/repo.git",
|
|
"worktree_branch": "feature/task-abc",
|
|
},
|
|
Env: map[string]string{"GH_TOKEN": "ghp_test"},
|
|
}
|
|
script := exec.resolvePrepareScript(req)
|
|
if script == "" {
|
|
t.Fatal("expected non-empty script")
|
|
}
|
|
// Should contain clone command
|
|
if !strings.Contains(script, "git clone") {
|
|
t.Error("expected git clone in script")
|
|
}
|
|
if !strings.Contains(script, "git config --global --add safe.directory '*'") {
|
|
t.Error("expected default Docker prepare script to trust mounted local repositories")
|
|
}
|
|
// Should contain token stripping
|
|
if !strings.Contains(script, "git remote set-url") {
|
|
t.Error("expected token stripping after clone")
|
|
}
|
|
if !strings.Contains(script, "git checkout -b \"feature/task-abc\"") {
|
|
t.Fatalf("expected Docker prepare script to create task branch, got:\n%s", script)
|
|
}
|
|
})
|
|
|
|
t.Run("empty metadata uses defaults", func(t *testing.T) {
|
|
req := &ExecutorCreateRequest{
|
|
Metadata: map[string]interface{}{},
|
|
Env: map[string]string{},
|
|
}
|
|
script := exec.resolvePrepareScript(req)
|
|
// Should still produce a script (default docker script)
|
|
if script == "" {
|
|
t.Fatal("expected non-empty default script")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLocalPathFromCloneURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
raw string
|
|
want string
|
|
}{
|
|
{name: "file URL", raw: "file:///tmp/e2e-docker-remote.git", want: "/tmp/e2e-docker-remote.git"},
|
|
{name: "escaped file URL", raw: "file:///tmp/repo%20remote.git", want: "/tmp/repo remote.git"},
|
|
{name: "localhost file URL", raw: "file://localhost/tmp/repo.git", want: "/tmp/repo.git"},
|
|
{name: "plain absolute path", raw: "/tmp/repo.git", want: "/tmp/repo.git"},
|
|
{name: "https URL", raw: "https://github.com/org/repo.git", want: ""},
|
|
{name: "remote file host", raw: "file://server/tmp/repo.git", want: ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := localPathFromCloneURL(tt.raw); got != tt.want {
|
|
t.Fatalf("localPathFromCloneURL(%q) = %q, want %q", tt.raw, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|