-
Notifications
You must be signed in to change notification settings - Fork 0
Description
The Idea
Instead of having "process tasks" as the primary abstraction with mocks/fakes for testing, flip it: goroutines are the fundamental task abstraction, and process management is just one implementation.
┌─────────────────────────────────────────────────────────────────┐
│ TaskFunc │
│ func(ctx context.Context, stdin io.Reader, │
│ stdout, stderr io.Writer) int │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ Pure Go function │ │ Process-bridging │
│ (for tests) │ │ function (production) │
│ │ │ │
│ Does computation │ │ Starts systemd unit │
│ in-process │ │ Bridges I/O to process │
│ │ │ Returns exit code │
└──────────────────────────┘ └──────────────────────────┘
Why
-
Testability without mocks — Tests use real goroutines doing real I/O through channels/pipes. No fake responses, no recording calls — actual concurrent behavior.
-
testing/synctestcompatibility — Go 1.25's synctest package virtualizes time and providesWait()for deterministic concurrency testing. Goroutine tasks work perfectly with this; process spawning doesn't. -
No zombie risk in tests — Process-based tests can leave zombies if something crashes. Goroutines just get garbage collected.
-
Cleaner architecture — The core domain logic (session management, output capture, journal integration) doesn't care whether the "task" is a goroutine or a process. The process is an implementation detail.
What It Would Look Like
// The universal task signature
type TaskFunc func(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) int
// For tests — pure Go
spec := TaskSpec{
Func: func(ctx context.Context, in io.Reader, out, err io.Writer) int {
fmt.Fprintln(out, "hello from goroutine")
return 0
},
}
// For production — wraps a process
spec := TaskSpec{
Func: ProcessTask(systemd, TransientSpec{
Command: []string{"bash", "-c", userCommand},
// ...
}),
}The ProcessTask function returns a TaskFunc — it's a goroutine whose job is to:
- Start the systemd transient unit
- Bridge stdin/stdout/stderr between Go I/O and the process
- Wait for exit
- Return the exit code
Current State
The groundwork is in place:
Runtimestruct with injectableSystemd,Journal,SessionClientJournal.Followreturnsiter.Seq[JournalEntry]for natural iteration- Lifecycle events (
SWASH_EVENT=started/exited) decouple from systemd internals - Go 1.25.5 with
testing/synctestavailable
Implementation Steps
- Define
TaskFuncandTaskSpectypes - Create
TaskRunnerinterface withStart,List,Stopmethods - Implement
GoroutineRunner— manages goroutine tasks with channels for I/O - Implement
ProcessTask— aTaskFuncthat bridges to systemd - Refactor
Runtimeto useTaskRunnerinstead of direct systemd calls - Write tests using pure goroutine tasks +
synctest
Related
- Recent commit
97b21e5adds Runtime abstraction and lifecycle events testing/synctestdocs: https://pkg.go.dev/testing/synctest