harness

package
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 23, 2026 License: MIT Imports: 42 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// DefaultReadyTimeout is the default timeout for waiting for container readiness.
	DefaultReadyTimeout = 60 * time.Second

	// E2EReadyTimeout is the timeout for E2E tests which may take longer.
	E2EReadyTimeout = 120 * time.Second

	// CIReadyTimeout is the timeout for CI environments which may be slower.
	CIReadyTimeout = 180 * time.Second

	// BypassCommandTimeout is the timeout for entrypoint bypass commands.
	BypassCommandTimeout = 10 * time.Second
)

Timeout constants for different test scenarios.

View Source
const (
	// ReadyFilePath is the path to the ready signal file inside containers.
	ReadyFilePath = "/var/run/clawker/ready"

	// ReadyLogPrefix is the prefix for the ready signal log line.
	ReadyLogPrefix = "[clawker] ready"

	// ErrorLogPrefix is the prefix for error signal log lines.
	ErrorLogPrefix = "[clawker] error"
)

Ready signal constants matching what the entrypoint emits.

View Source
const (
	// NamePrefix is the standard prefix for clawker resources
	NamePrefix = "clawker"
)
View Source
const TestChownImage = "clawker-test-chown:latest"

TestChownImage is the stable tag for the test chown image used by CopyToVolume.

Variables

View Source
var (
	// TestLabel is the label used to identify test resources.
	TestLabel = _blankCfg.LabelTest()
	// TestLabelValue is the value for the test label.
	TestLabelValue = _blankCfg.ManagedLabelValue()
	// ClawkerManagedLabel is the standard clawker managed label.
	ClawkerManagedLabel = _blankCfg.LabelManaged()
	// LabelTestName is the label key for the originating test function name.
	LabelTestName = _blankCfg.LabelTestName()
)

Functions

func AddClawkerLabels

func AddClawkerLabels(labels map[string]string, project, agent, testName string) map[string]string

AddClawkerLabels adds both clawker and test labels to a map. This creates resources that are managed by clawker AND marked for test cleanup. The testName parameter sets the test name label for leak tracing.

func AddTestLabels

func AddTestLabels(labels map[string]string) map[string]string

AddTestLabels adds the test label to a label map for resource identification. Returns a new map with test labels added.

func BuildLightImage

func BuildLightImage(t *testing.T, dc *docker.Client, _ ...string) string

BuildLightImage builds a lightweight Alpine test image with all *.sh scripts from internal/bundler/assets/ and internal/hostproxy/internals/ baked in at /usr/local/bin/. The image is content-addressed and cached across tests in the same run, producing a single shared image regardless of which scripts individual tests use.

The scripts parameter is accepted for backward compatibility but ignored.

The image includes bash, iptables, ipset, iproute2, curl, openssh-client, sudo. A "claude" user is created matching production containers.

Cleanup is handled by CleanupTestResources (label-based) — call it from TestMain to guarantee removal even after killed runs.

func BuildRemoteSocketsEnv

func BuildRemoteSocketsEnv(cfg SocketBridgeConfig) string

BuildRemoteSocketsEnv builds the CLAWKER_REMOTE_SOCKETS env var value.

func BuildSimpleTestImage

func BuildSimpleTestImage(t *testing.T, dockerfile string, opts BuildSimpleTestImageOptions) string

BuildSimpleTestImage builds a simple test image from a Dockerfile string. This is useful for tests that don't need the full clawker infrastructure (Claude Code, etc.). The image is tagged uniquely and automatically cleaned up when the test completes. Uses docker.Client (wrapping whail.Engine) so the image is built through clawker's label management system and is visible to managed image queries.

func BuildTestChownImage

func BuildTestChownImage(t *testing.T)

BuildTestChownImage builds a minimal busybox-based image with test labels for CopyToVolume operations. Uses docker.Client (wrapping whail.Engine) so the image is built through clawker's label management system — managed labels are auto-injected and the image is visible to managed image queries.

The image uses a stable tag (TestChownImage) and is only built if it doesn't already exist, making it safe to call multiple times per test run. The image is NOT cleaned up per-test since it's shared; CleanupTestResources removes it by label.

func BuildTestImage

func BuildTestImage(t *testing.T, h *Harness, opts BuildTestImageOptions) string

BuildTestImage builds a clawker image for e2e testing using the harness configuration. The image is tagged uniquely for the test and automatically cleaned up when the test completes. Returns the image tag that can be used to run containers.

func CheckForErrorPattern

func CheckForErrorPattern(logs string) (bool, string)

CheckForErrorPattern checks container logs for error patterns. Returns (true, error message) if an error pattern is found, (false, "") otherwise.

func CleanupProjectResources

func CleanupProjectResources(ctx context.Context, c *docker.Client, project string) error

CleanupProjectResources removes all Docker resources associated with a project. This cleans up containers, volumes, networks, and images with the project label.

func CleanupTestResources

func CleanupTestResources(ctx context.Context, cli *client.Client) error

CleanupTestResources removes all Docker resources marked with the test label. Use this for cleanup after tests that create resources outside of projects.

func ComputeTemplateHash

func ComputeTemplateHash() (string, error)

ComputeTemplateHash generates a content-addressed hash of Dockerfile templates and their related type definitions. Used for CI cache invalidation - rebuild test images only when templates change.

The hash includes:

  • All files in internal/bundler/assets/ (Dockerfile.tmpl, entrypoint.sh, etc.)
  • All files in internal/hostproxy/internals/ (host-open.sh, git-credential-clawker.sh, Go sources)
  • The dockerfile.go file containing DockerfileContext struct definition

Returns a stable SHA256 hex digest (64 characters).

Usage in CI:

hash, err := testutil.ComputeTemplateHash()
if err != nil {
    log.Fatal(err)
}
// Use hash as cache key: "clawker-test-image-" + hash[:12]

func ComputeTemplateHashFromDir

func ComputeTemplateHashFromDir(rootDir string) (string, error)

ComputeTemplateHashFromDir computes the template hash using an explicit project root. This is useful for testing or when the project root cannot be auto-detected.

func ContainerExists

func ContainerExists(ctx context.Context, cli *docker.Client, name string) bool

ContainerExists checks if a container exists by name.

func ContainerIsRunning

func ContainerIsRunning(ctx context.Context, cli *docker.Client, name string) bool

ContainerIsRunning checks if a container is running by name.

func DefaultGPGSocketPath

func DefaultGPGSocketPath() string

DefaultGPGSocketPath returns the default GPG socket path for the claude user.

func DefaultSSHSocketPath

func DefaultSSHSocketPath() string

DefaultSSHSocketPath returns the default SSH socket path for the claude user.

func FindProjectRoot

func FindProjectRoot() (string, error)

FindProjectRoot locates the project root by looking for go.mod file. Starts from the current file's location and walks up.

func GetContainerLogs

func GetContainerLogs(ctx context.Context, cli *docker.Client, containerID string) (string, error)

GetContainerLogs retrieves all logs from a container.

func GetReadyTimeout

func GetReadyTimeout() time.Duration

GetReadyTimeout returns the appropriate timeout for waiting on ready signals. It checks the CLAWKER_READY_TIMEOUT environment variable first.

func NetworkExists

func NetworkExists(ctx context.Context, cli *docker.Client, name string) bool

NetworkExists checks if a network exists by name.

func NewRawDockerClient deprecated

func NewRawDockerClient(t *testing.T) *client.Client

Deprecated: NewRawDockerClient creates a raw Docker SDK client for testing. Prefer NewTestClient which returns *docker.Client with automatic test label injection. This function remains for infrastructure code (e.g., BuildLightImage prune, RunTestMain cleanup) that needs raw SDK access for operations whail overrides with different signatures.

func NewTestClient

func NewTestClient(t *testing.T) *docker.Client

NewTestClient creates a clawker Docker client for testing. The client is automatically closed when the test completes.

func NewTestFactory

func NewTestFactory(t *testing.T, h *Harness) (*cmdutil.Factory, *iostreamstest.TestIOStreams)

NewTestFactory returns a fully-wired Factory suitable for integration tests that execute real Cobra commands (run, create, start, exec). The returned factory uses the harness config (h.Config) with test-safe defaults applied, provides real Docker client construction, and host proxy with t.Cleanup teardown.

The harness config carries the test's project name and settings. applyTestDefaults fills in only fields the test hasn't explicitly set:

  • ClaudeCode strategy "fresh" (skips CopyToVolume — no temp busybox containers)
  • UseHostAuth false (skips second CopyToVolume for host auth)
  • Firewall disabled, host proxy disabled (no daemon processes)

The factory's Docker client uses ChownImage set to TestChownImage so that any CopyToVolume calls use the locally-built labeled image instead of pulling busybox:latest from DockerHub.

Tests that explicitly need copy mode (e.g., test/internals/containerfs_test.go) set their own ClaudeCodeConfig directly and don't use NewTestFactory.

func ParseYAML

func ParseYAML[T any](yamlStr string) (T, error)

ParseYAML unmarshals a YAML string into a value of type T. Generic utility for tests that need to parse YAML config snippets.

func RequireDocker

func RequireDocker(t *testing.T)

RequireDocker skips the test if Docker is not available. Use this at the start of internals tests.

func RunTestMain

func RunTestMain(m *testing.M) int

RunTestMain wraps testing.M.Run with cleanup of test-labeled Docker resources and host-proxy daemons. It acquires an exclusive file lock to prevent concurrent integration test runs from piling up containers and processes. Stale resources from previous (possibly killed) runs are cleaned before tests start, and again after tests complete — including on SIGINT/SIGTERM (e.g. Ctrl+C). Use from TestMain:

func TestMain(m *testing.M) { os.Exit(harness.RunTestMain(m)) }

func SkipIfNoDocker

func SkipIfNoDocker(t *testing.T)

SkipIfNoDocker is an alias for RequireDocker for clearer semantics.

func StartSocketBridge

func StartSocketBridge(t *testing.T, containerID string, cfg SocketBridgeConfig) (stop func(), err error)

StartSocketBridge starts a socket bridge for the given container. It launches the socket-forwarder via docker exec and handles the muxrpc protocol. The bridge runs in a goroutine and should be stopped via the returned stop function. Returns a stop function and any error from starting the bridge.

func StripDockerStreamHeaders

func StripDockerStreamHeaders(data string) string

StripDockerStreamHeaders removes Docker's multiplexed stream headers. Docker prefixes each frame with an 8-byte header: [stream_type][padding][size].

func TemplateHashShort

func TemplateHashShort() (string, error)

TemplateHashShort returns a shortened hash suitable for use as a cache key suffix. Returns the first 12 characters of the full hash.

func UniqueAgentName

func UniqueAgentName(t *testing.T) string

UniqueAgentName generates a unique agent name suitable for parallel test isolation. Format: "test-<short-test-name>-<timestamp>-<random>"

func UniqueContainerName

func UniqueContainerName(t *testing.T) string

UniqueContainerName generates a unique test container name. Returns: "clawker-test-<short-test-name>-<timestamp>-<random>"

func VerifyClaudeCodeRunning

func VerifyClaudeCodeRunning(ctx context.Context, cli *docker.Client, containerID string) error

VerifyClaudeCodeRunning is a convenience wrapper to verify Claude Code is running. Searches for a process with "claude" in the command line.

func VerifyProcessRunning

func VerifyProcessRunning(ctx context.Context, cli *docker.Client, containerID, pattern string) error

VerifyProcessRunning checks if a process matching the pattern is running in the container. Uses pgrep -f to search for the pattern in the full command line. Returns nil if process is found, error otherwise.

func VolumeExists

func VolumeExists(ctx context.Context, cli *docker.Client, name string) bool

VolumeExists checks if a volume exists by name.

func WaitForContainerCompletion

func WaitForContainerCompletion(ctx context.Context, cli *docker.Client, containerID string) error

WaitForContainerCompletion waits for a short-lived container to complete. For containers that exit quickly (like echo hello), this function: 1. Checks if container is still running - if so, waits for ready file 2. If container already exited with code 0, verifies ready signal in logs This is the appropriate wait function for entrypoint bypass commands.

func WaitForContainerExit

func WaitForContainerExit(ctx context.Context, cli *docker.Client, containerID string) error

WaitForContainerExit waits for a container to exit with code 0. This is a simpler wait function for vanilla containers that don't emit ready signals. Use this for internals tests with alpine:latest or other non-clawker images.

func WaitForContainerExitAny added in v0.1.2

func WaitForContainerExitAny(ctx context.Context, cli *docker.Client, containerID string) (int, error)

WaitForContainerExitAny waits for a container to stop running and returns the exit code. Unlike WaitForContainerExit, non-zero exit codes are not treated as errors — the caller decides how to handle them.

func WaitForContainerRunning

func WaitForContainerRunning(ctx context.Context, cli *docker.Client, name string) error

WaitForContainerRunning waits for a container to exist and be in running state. Polls every 500ms until the context is cancelled or the container is running. Fails fast with exit code information if the container enters a terminal state (exited or dead) instead of timing out silently.

func WaitForHealthy

func WaitForHealthy(ctx context.Context, cli *docker.Client, containerID string) error

WaitForHealthy waits for the container to be healthy using Docker's HEALTHCHECK. Returns nil when healthy, or an error if timeout is reached.

func WaitForLogPattern

func WaitForLogPattern(ctx context.Context, cli *docker.Client, containerID, pattern string) error

WaitForLogPattern waits for a specific pattern to appear in container logs. Returns nil when the pattern is found, or an error if timeout is reached.

func WaitForReadyFile

func WaitForReadyFile(ctx context.Context, cli *docker.Client, containerID string) error

WaitForReadyFile waits for the ready signal file to exist in the container. Returns nil when the file exists, or an error if timeout is reached or exec fails.

func WaitForReadyLog

func WaitForReadyLog(ctx context.Context, cli *docker.Client, containerID string) error

WaitForReadyLog waits for the ready signal log line. This is a convenience wrapper around WaitForLogPattern.

Types

type BuildSimpleTestImageOptions

type BuildSimpleTestImageOptions struct {
	// SuppressOutput suppresses build output (default: true for cleaner test output)
	SuppressOutput bool
	// NoCache disables Docker build cache (default: false)
	NoCache bool
	// Project is the project name for labeling (optional)
	Project string
}

BuildSimpleTestImageOptions configures simple image building for tests.

type BuildTestImageOptions

type BuildTestImageOptions struct {
	// SuppressOutput suppresses build output (default: true for cleaner test output)
	SuppressOutput bool
	// NoCache disables Docker build cache (default: false)
	NoCache bool
}

BuildTestImageOptions configures image building for tests.

type ContainerExitDiagnostics

type ContainerExitDiagnostics struct {
	ExitCode        int
	OOMKilled       bool
	Error           string // Docker's state error field
	Logs            string // Last N lines of logs
	LogError        error  // Error retrieving logs, if any
	StartedAt       string // ISO 8601 timestamp
	FinishedAt      string // ISO 8601 timestamp
	HasClawkerError bool
	ClawkerErrorMsg string
	FirewallFailed  bool
}

ContainerExitDiagnostics contains comprehensive exit information for debugging.

func GetContainerExitDiagnostics

func GetContainerExitDiagnostics(ctx context.Context, cli *docker.Client, containerID string, logTailLines int) (*ContainerExitDiagnostics, error)

GetContainerExitDiagnostics retrieves detailed exit information for a stopped container. logTailLines specifies how many lines from the end of logs to include (0 = all logs).

type ContainerOpt

type ContainerOpt func(*containerConfig)

ContainerOpt configures a test container.

func WithCapAdd

func WithCapAdd(caps ...string) ContainerOpt

WithCapAdd adds Linux capabilities to the container (e.g., "NET_ADMIN", "NET_RAW").

func WithCmd

func WithCmd(cmd ...string) ContainerOpt

WithCmd sets the command to run in the container.

func WithConfigVolume

func WithConfigVolume(t *testing.T, dc *docker.Client, project, agent string) ContainerOpt

WithConfigVolume creates a named config volume and returns a ContainerOpt that mounts it at /home/claude/.claude. The volume is cleaned up via t.Cleanup.

Use this when you want one-step volume creation + mount. If you create the volume separately (e.g., via EnsureVolume + InitContainerConfig), use WithVolumeMount instead to avoid duplicate creation/cleanup.

func WithEntrypoint added in v0.1.2

func WithEntrypoint(entrypoint ...string) ContainerOpt

WithEntrypoint sets the entrypoint for the container.

func WithEnv

func WithEnv(env ...string) ContainerOpt

WithEnv adds environment variables (KEY=VALUE format).

func WithExtraHost

func WithExtraHost(hosts ...string) ContainerOpt

WithExtraHost adds extra host entries (e.g., "host.docker.internal:host-gateway").

func WithMounts

func WithMounts(mounts ...mount.Mount) ContainerOpt

WithMounts adds bind or volume mounts to the container.

func WithSocketForwarding

func WithSocketForwarding(cfg SocketBridgeConfig) ContainerOpt

WithSocketForwarding configures socket forwarding for a container. Returns a ContainerOpt that sets the CLAWKER_REMOTE_SOCKETS env var.

func WithUser

func WithUser(user string) ContainerOpt

WithUser sets the user to run as inside the container.

func WithVolumeMount

func WithVolumeMount(volumeName, target string) ContainerOpt

WithVolumeMount returns a ContainerOpt that mounts an existing volume at the given target path. Unlike WithConfigVolume, it does not create the volume or register cleanup — use when the volume is managed separately (e.g., via EnsureVolume + InitContainerConfig before container start).

type ExecResult

type ExecResult struct {
	ExitCode int
	Stdout   string
	Stderr   string
}

ExecResult holds the result of executing a command in a container.

func (*ExecResult) CleanOutput

func (r *ExecResult) CleanOutput() string

CleanOutput returns the Stdout with Docker stream headers stripped.

type Harness

type Harness struct {
	T           *testing.T
	ProjectDir  string            // Temp dir with clawker.yaml
	ConfigDir   string            // Isolated ~/.config/clawker/
	OriginalEnv map[string]string // For restoration
	OriginalDir string            // Original working directory
	Config      *config.Project   // The test config
	Project     string            // Project name
	// contains filtered or unexported fields
}

Harness provides an isolated test environment for clawker tests. It manages: - Temporary project directory with clawker.yaml - Isolated config directory (~/.config/clawker/) - Environment variable backup and restoration - Automatic cleanup via t.Cleanup()

func NewHarness

func NewHarness(t *testing.T, opts ...HarnessOption) *Harness

NewHarness creates a new test harness with isolation. The harness automatically cleans up all resources when the test completes.

func (*Harness) Chdir

func (h *Harness) Chdir()

Chdir changes to the project directory and registers restoration for cleanup.

func (*Harness) ConfigPath

func (h *Harness) ConfigPath() string

ConfigPath returns the path to the clawker.yaml file.

func (*Harness) ContainerName

func (h *Harness) ContainerName(agent string) string

ContainerName generates the expected container name for an agent. Format: clawker.<project>.<agent>

func (*Harness) FileExists

func (h *Harness) FileExists(relPath string) bool

FileExists checks if a file exists in the project directory.

func (*Harness) ImageName

func (h *Harness) ImageName() string

ImageName returns the expected image name. Returns Build.Image if set, otherwise clawker-<project>:latest

func (*Harness) NetworkName

func (h *Harness) NetworkName() string

NetworkName returns the clawker network name.

func (*Harness) NewRootCmd added in v0.1.4

func (h *Harness) NewRootCmd(f *cmdutil.Factory) (*cobra.Command, error)

NewRootCmd creates a root command for integration tests using the provided factory. This allows tests to invoke subcommands through the full command hierarchy, inheriting settings like SilenceUsage from root.

func (*Harness) ReadFile

func (h *Harness) ReadFile(relPath string) string

ReadFile reads a file from the project directory.

func (*Harness) SetEnv

func (h *Harness) SetEnv(key, value string)

SetEnv sets an environment variable and registers it for cleanup. The original value is restored when the test completes.

func (*Harness) UnsetEnv

func (h *Harness) UnsetEnv(key string)

UnsetEnv unsets an environment variable and registers it for cleanup.

func (*Harness) UpdateConfig

func (h *Harness) UpdateConfig(fn func(*config.Project))

UpdateConfig updates the config and rewrites clawker.yaml.

func (*Harness) VolumeName

func (h *Harness) VolumeName(agent, purpose string) string

VolumeName generates the expected volume name for an agent and purpose. Format: clawker.<project>.<agent>-<purpose>

func (*Harness) WriteFile

func (h *Harness) WriteFile(relPath, content string)

WriteFile writes a file to the project directory.

type HarnessOption

type HarnessOption func(*Harness)

HarnessOption configures a Harness.

func WithConfig

func WithConfig(cfg *config.Project) HarnessOption

WithConfig sets the config directly. Note: Project name is no longer stored in config.Project. Use WithProject() to set the project name.

func WithConfigBuilder

func WithConfigBuilder(cb *builders.ConfigBuilder) HarnessOption

WithConfigBuilder sets the config from a ConfigBuilder. Note: Project name is no longer stored in config.Project. Use WithProject() to set the project name.

func WithProject

func WithProject(name string) HarnessOption

WithProject sets the project name. Note: This should be used before WithConfig/WithConfigBuilder, or the config's project name will override this setting.

type ReadyFileContent

type ReadyFileContent struct {
	Timestamp int64
	PID       int
}

ReadyFileContent represents the parsed content of the ready signal file.

func ParseReadyFile

func ParseReadyFile(content string) (*ReadyFileContent, error)

ParseReadyFile parses the content of a ready signal file. Format: ts=<timestamp> pid=<pid>

type RunningContainer

type RunningContainer struct {
	ID   string
	Name string
}

RunningContainer represents a container started by RunContainer.

func RunContainer

func RunContainer(t *testing.T, dc *docker.Client, image string, opts ...ContainerOpt) *RunningContainer

RunContainer creates and starts a container from the given image, returning a RunningContainer with automatic cleanup registered via t.Cleanup.

If GPG is available on the host, socket forwarding is automatically configured by setting CLAWKER_REMOTE_SOCKETS and starting the socket bridge. This allows TDD tests to validate socket forwarding without explicit configuration.

For production code, socket forwarding config comes from clawker.yaml via RuntimeEnv.

func (*RunningContainer) DirExists

func (c *RunningContainer) DirExists(ctx context.Context, dc *docker.Client, path string) bool

DirExists checks if a directory exists at path inside the container.

func (*RunningContainer) Exec

func (c *RunningContainer) Exec(ctx context.Context, dc *docker.Client, cmd ...string) (*ExecResult, error)

Exec executes a command in the container and returns the result.

func (*RunningContainer) ExecAsUser added in v0.1.2

func (c *RunningContainer) ExecAsUser(ctx context.Context, dc *docker.Client, user string, cmd ...string) (*ExecResult, error)

ExecAsUser executes a command in the container as a specific user and returns the result.

func (*RunningContainer) FileExists

func (c *RunningContainer) FileExists(ctx context.Context, dc *docker.Client, path string) bool

FileExists checks if a file exists at path inside the container.

func (*RunningContainer) GetLogs

func (c *RunningContainer) GetLogs(ctx context.Context, rawCli *client.Client) (string, error)

GetLogs retrieves container logs.

func (*RunningContainer) ReadFile

func (c *RunningContainer) ReadFile(ctx context.Context, dc *docker.Client, path string) (string, error)

ReadFile reads the content of a file inside the container. Returns the trimmed content and any error from exec or non-zero exit code.

func (*RunningContainer) WaitForFile

func (c *RunningContainer) WaitForFile(ctx context.Context, dc *docker.Client, path string, timeout time.Duration) error

WaitForFile waits for a file to exist in the container.

type SocketBridgeConfig

type SocketBridgeConfig struct {
	GPGEnabled bool // Forward GPG agent
	SSHEnabled bool // Forward SSH agent
}

SocketBridgeConfig defines which sockets to forward.

Directories

Path Synopsis
Package builders provides shared test utilities for clawker tests.
Package builders provides shared test utilities for clawker tests.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL