feat(lifecycle): add AppMode type + DetectMode()
Some checks failed
Security Scan / security (push) Has been cancelled
Test / test (push) Has been cancelled

New pkg/lifecycle/mode.go introduces AppMode (Manager | Worker) and
DetectMode() that returns:
- Manager by default (explicit or empty env)
- Worker when CORE_APP_MODE=worker, or when empty env + CI=true signals
  a headless/compute context

Prerequisite for the BugSETI compute worker pattern (core/gui RFC.md §9.1) —
manager GUI runs the full app UI, workers run headless compute with only an
RPC surface.

Tests follow the core/go {Good,Bad,Ugly} naming convention; go test
./pkg/lifecycle/... passes.

Co-authored-by: Codex <noreply@openai.com>

Closes tasks.lthn.sh/view.php?id=32
This commit is contained in:
Snider 2026-04-23 13:12:11 +01:00
parent 49198acd3a
commit 6e62d4a20c
2 changed files with 109 additions and 0 deletions

52
pkg/lifecycle/mode.go Normal file
View file

@ -0,0 +1,52 @@
package lifecycle
import (
"os"
"strings"
)
type AppMode string
const (
ModeManager AppMode = "manager"
ModeWorker AppMode = "worker"
)
const (
appModeEnv = "CORE_APP_MODE"
ciEnv = "CI"
)
// DetectMode returns ModeWorker when CORE_APP_MODE=worker.
//
// mode := DetectMode()
func DetectMode() AppMode {
if value, ok := os.LookupEnv(appModeEnv); ok {
if mode, valid := parseAppMode(value); valid {
return mode
}
}
if value, ok := os.LookupEnv(ciEnv); ok && isTrue(value) {
return ModeWorker
}
return ModeManager
}
func parseAppMode(value string) (AppMode, bool) {
switch strings.ToLower(strings.TrimSpace(value)) {
case string(ModeManager):
return ModeManager, true
case string(ModeWorker):
return ModeWorker, true
case "":
return "", false
default:
return ModeManager, true
}
}
func isTrue(value string) bool {
return strings.EqualFold(strings.TrimSpace(value), "true")
}

View file

@ -0,0 +1,57 @@
package lifecycle
import (
"os"
"testing"
)
func TestMode_DetectMode_Good(t *testing.T) {
unsetEnv(t, appModeEnv)
t.Setenv(ciEnv, "")
mode := DetectMode()
if mode != ModeManager {
t.Fatalf("expected manager mode, got %q", mode)
}
}
func TestMode_DetectMode_Bad(t *testing.T) {
t.Setenv(appModeEnv, "bogus")
t.Setenv(ciEnv, "")
mode := DetectMode()
if mode != ModeManager {
t.Fatalf("expected manager mode after invalid env value, got %q", mode)
}
}
func TestMode_DetectMode_Ugly(t *testing.T) {
t.Setenv(appModeEnv, "")
t.Setenv(ciEnv, "true")
mode := DetectMode()
if mode != ModeWorker {
t.Fatalf("expected worker mode in CI headless context, got %q", mode)
}
}
func unsetEnv(t *testing.T, key string) {
t.Helper()
value, ok := os.LookupEnv(key)
if err := os.Unsetenv(key); err != nil {
t.Fatalf("unset %s: %v", key, err)
}
t.Cleanup(func() {
if ok {
if err := os.Setenv(key, value); err != nil {
t.Fatalf("restore %s: %v", key, err)
}
return
}
if err := os.Unsetenv(key); err != nil {
t.Fatalf("restore unset %s: %v", key, err)
}
})
}