go-agent/jobrunner/coverage_boost_test.go
Snider f1e55b370f
Some checks failed
Security Scan / security (push) Failing after 10s
Test / test (push) Successful in 4m55s
fix: skip permission tests when running as root (CI/Docker)
Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-23 07:43:48 +00:00

395 lines
9.3 KiB
Go

package jobrunner
import (
"context"
"fmt"
"os"
"path/filepath"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- Journal: NewJournal error path ---
func TestNewJournal_Bad_EmptyBaseDir(t *testing.T) {
_, err := NewJournal("")
require.Error(t, err)
assert.Contains(t, err.Error(), "base directory is required")
}
func TestNewJournal_Good(t *testing.T) {
dir := t.TempDir()
j, err := NewJournal(dir)
require.NoError(t, err)
assert.NotNil(t, j)
}
// --- Journal: sanitizePathComponent additional cases ---
func TestSanitizePathComponent_Good_ValidNames(t *testing.T) {
tests := []struct {
input string
want string
}{
{"host-uk", "host-uk"},
{"core", "core"},
{"my_repo", "my_repo"},
{"repo.v2", "repo.v2"},
{"A123", "A123"},
}
for _, tc := range tests {
got, err := sanitizePathComponent(tc.input)
require.NoError(t, err, "input: %q", tc.input)
assert.Equal(t, tc.want, got)
}
}
func TestSanitizePathComponent_Bad_Invalid(t *testing.T) {
tests := []struct {
name string
input string
}{
{"empty", ""},
{"spaces", " "},
{"dotdot", ".."},
{"dot", "."},
{"slash", "foo/bar"},
{"backslash", `foo\bar`},
{"special", "org$bad"},
{"leading-dot", ".hidden"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
_, err := sanitizePathComponent(tc.input)
assert.Error(t, err, "input: %q", tc.input)
})
}
}
// --- Journal: Append with readonly directory ---
func TestJournal_Append_Bad_ReadonlyDir(t *testing.T) {
if os.Getuid() == 0 {
t.Skip("chmod does not restrict root")
}
// Create a dir that we then make readonly (only works as non-root).
dir := t.TempDir()
readonlyDir := filepath.Join(dir, "readonly")
require.NoError(t, os.MkdirAll(readonlyDir, 0o755))
require.NoError(t, os.Chmod(readonlyDir, 0o444))
t.Cleanup(func() { _ = os.Chmod(readonlyDir, 0o755) })
j, err := NewJournal(readonlyDir)
require.NoError(t, err)
signal := &PipelineSignal{
RepoOwner: "test-owner",
RepoName: "test-repo",
}
result := &ActionResult{
Action: "test",
Timestamp: time.Now(),
}
err = j.Append(signal, result)
// Should fail because MkdirAll cannot create subdirectories in readonly dir.
assert.Error(t, err)
}
// --- Poller: error-returning source ---
type errorSource struct {
name string
}
func (e *errorSource) Name() string { return e.name }
func (e *errorSource) Poll(_ context.Context) ([]*PipelineSignal, error) {
return nil, fmt.Errorf("poll error")
}
func (e *errorSource) Report(_ context.Context, _ *ActionResult) error { return nil }
func TestPoller_RunOnce_Good_SourceError(t *testing.T) {
src := &errorSource{name: "broken-source"}
handler := &mockHandler{name: "test"}
p := NewPoller(PollerConfig{
Sources: []JobSource{src},
Handlers: []JobHandler{handler},
})
err := p.RunOnce(context.Background())
require.NoError(t, err) // Poll errors are logged, not returned
handler.mu.Lock()
defer handler.mu.Unlock()
assert.Empty(t, handler.executed, "handler should not be called when poll fails")
}
// --- Poller: error-returning handler ---
type errorHandler struct {
name string
}
func (e *errorHandler) Name() string { return e.name }
func (e *errorHandler) Match(_ *PipelineSignal) bool { return true }
func (e *errorHandler) Execute(_ context.Context, _ *PipelineSignal) (*ActionResult, error) {
return nil, fmt.Errorf("handler error")
}
func TestPoller_RunOnce_Good_HandlerError(t *testing.T) {
sig := &PipelineSignal{
EpicNumber: 1,
ChildNumber: 1,
PRNumber: 1,
RepoOwner: "test",
RepoName: "repo",
}
src := &mockSource{
name: "test-source",
signals: []*PipelineSignal{sig},
}
handler := &errorHandler{name: "broken-handler"}
p := NewPoller(PollerConfig{
Sources: []JobSource{src},
Handlers: []JobHandler{handler},
})
err := p.RunOnce(context.Background())
require.NoError(t, err) // Handler errors are logged, not returned
// Source should not have received a report (handler errored out).
src.mu.Lock()
defer src.mu.Unlock()
assert.Empty(t, src.reports)
}
// --- Poller: with Journal integration ---
func TestPoller_RunOnce_Good_WithJournal(t *testing.T) {
dir := t.TempDir()
journal, err := NewJournal(dir)
require.NoError(t, err)
sig := &PipelineSignal{
EpicNumber: 10,
ChildNumber: 3,
PRNumber: 55,
RepoOwner: "host-uk",
RepoName: "core",
PRState: "OPEN",
CheckStatus: "SUCCESS",
Mergeable: "MERGEABLE",
}
src := &mockSource{
name: "test-source",
signals: []*PipelineSignal{sig},
}
handler := &mockHandler{
name: "test-handler",
matchFn: func(s *PipelineSignal) bool {
return true
},
}
p := NewPoller(PollerConfig{
Sources: []JobSource{src},
Handlers: []JobHandler{handler},
Journal: journal,
})
err = p.RunOnce(context.Background())
require.NoError(t, err)
handler.mu.Lock()
require.Len(t, handler.executed, 1)
handler.mu.Unlock()
// Verify the journal file was written.
date := time.Now().UTC().Format("2006-01-02")
journalPath := filepath.Join(dir, "host-uk", "core", date+".jsonl")
_, statErr := os.Stat(journalPath)
assert.NoError(t, statErr, "journal file should exist at %s", journalPath)
}
// --- Poller: error-returning Report ---
type reportErrorSource struct {
name string
signals []*PipelineSignal
mu sync.Mutex
}
func (r *reportErrorSource) Name() string { return r.name }
func (r *reportErrorSource) Poll(_ context.Context) ([]*PipelineSignal, error) {
r.mu.Lock()
defer r.mu.Unlock()
return r.signals, nil
}
func (r *reportErrorSource) Report(_ context.Context, _ *ActionResult) error {
return fmt.Errorf("report error")
}
func TestPoller_RunOnce_Good_ReportError(t *testing.T) {
sig := &PipelineSignal{
EpicNumber: 1,
ChildNumber: 1,
PRNumber: 1,
RepoOwner: "test",
RepoName: "repo",
}
src := &reportErrorSource{
name: "report-fail-source",
signals: []*PipelineSignal{sig},
}
handler := &mockHandler{
name: "test-handler",
matchFn: func(s *PipelineSignal) bool { return true },
}
p := NewPoller(PollerConfig{
Sources: []JobSource{src},
Handlers: []JobHandler{handler},
})
err := p.RunOnce(context.Background())
require.NoError(t, err) // Report errors are logged, not returned
handler.mu.Lock()
defer handler.mu.Unlock()
assert.Len(t, handler.executed, 1, "handler should still execute even though report fails")
}
// --- Poller: multiple sources and handlers ---
func TestPoller_RunOnce_Good_MultipleSources(t *testing.T) {
sig1 := &PipelineSignal{
EpicNumber: 1, ChildNumber: 1, PRNumber: 1,
RepoOwner: "org1", RepoName: "repo1",
}
sig2 := &PipelineSignal{
EpicNumber: 2, ChildNumber: 2, PRNumber: 2,
RepoOwner: "org2", RepoName: "repo2",
}
src1 := &mockSource{name: "source-1", signals: []*PipelineSignal{sig1}}
src2 := &mockSource{name: "source-2", signals: []*PipelineSignal{sig2}}
handler := &mockHandler{
name: "catch-all",
matchFn: func(s *PipelineSignal) bool { return true },
}
p := NewPoller(PollerConfig{
Sources: []JobSource{src1, src2},
Handlers: []JobHandler{handler},
})
err := p.RunOnce(context.Background())
require.NoError(t, err)
handler.mu.Lock()
defer handler.mu.Unlock()
assert.Len(t, handler.executed, 2)
}
// --- Poller: Run with immediate cancellation ---
func TestPoller_Run_Good_ImmediateCancel(t *testing.T) {
src := &mockSource{name: "source", signals: nil}
p := NewPoller(PollerConfig{
Sources: []JobSource{src},
PollInterval: 1 * time.Hour, // long interval
})
ctx, cancel := context.WithCancel(context.Background())
// Cancel after the first RunOnce completes.
go func() {
time.Sleep(50 * time.Millisecond)
cancel()
}()
err := p.Run(ctx)
assert.ErrorIs(t, err, context.Canceled)
assert.Equal(t, 1, p.Cycle()) // One cycle from the initial RunOnce
}
// --- Journal: Append with journal error logging ---
func TestPoller_RunOnce_Good_JournalAppendError(t *testing.T) {
if os.Getuid() == 0 {
t.Skip("chmod does not restrict root")
}
// Use a directory that will cause journal writes to fail.
dir := t.TempDir()
journal, err := NewJournal(dir)
require.NoError(t, err)
// Make the journal directory read-only to trigger append errors.
require.NoError(t, os.Chmod(dir, 0o444))
t.Cleanup(func() { _ = os.Chmod(dir, 0o755) })
sig := &PipelineSignal{
EpicNumber: 1,
ChildNumber: 1,
PRNumber: 1,
RepoOwner: "test",
RepoName: "repo",
}
src := &mockSource{
name: "test-source",
signals: []*PipelineSignal{sig},
}
handler := &mockHandler{
name: "test-handler",
matchFn: func(s *PipelineSignal) bool { return true },
}
p := NewPoller(PollerConfig{
Sources: []JobSource{src},
Handlers: []JobHandler{handler},
Journal: journal,
})
err = p.RunOnce(context.Background())
// Journal errors are logged, not returned.
require.NoError(t, err)
handler.mu.Lock()
defer handler.mu.Unlock()
assert.Len(t, handler.executed, 1, "handler should still execute even when journal fails")
}
// --- Poller: Cycle counter increments ---
func TestPoller_Cycle_Good_Increments(t *testing.T) {
src := &mockSource{name: "source", signals: nil}
p := NewPoller(PollerConfig{
Sources: []JobSource{src},
})
assert.Equal(t, 0, p.Cycle())
_ = p.RunOnce(context.Background())
assert.Equal(t, 1, p.Cycle())
_ = p.RunOnce(context.Background())
assert.Equal(t, 2, p.Cycle())
}