// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package remoteinput import ( "context" "errors" "io" "os" "os/exec" "path/filepath" "strconv" "syscall" "testing" "time" ) type supervisorTestTarget struct{ mode string } func (target supervisorTestTarget) Apply(string, bool) error { switch target.mode { case "exit": os.Exit(0) case "blocked": // A timer models a native call that remains blocked without causing // Go's non-race runtime to terminate the synthetic child as deadlocked. time.Sleep(time.Hour) } return nil } func (target supervisorTestTarget) Close() error { if target.mode == "cleanup-failure" { return ErrCleanup } return nil } func TestSupervisorProcess(t *testing.T) { mode := os.Getenv("CTHULHU_SUPERVISOR_TEST") if mode == "" { return } epoch, _ := strconv.ParseUint(os.Getenv("CTHULHU_SUPERVISOR_EPOCH"), 10, 64) control, err := AdoptPipe(os.Stdin, false) if err != nil { os.Exit(2) } output, err := AdoptPipe(os.Stdout, true) if err != nil { os.Exit(2) } life, err := AdoptPipe(os.NewFile(3, "lifetime"), false) if err != nil { os.Exit(2) } if mode == "no-ready" || mode == "bad-ready" { if mode == "bad-ready" { frame, _ := EncodeAck(Ack{Kind: Ready, Epoch: epoch + 1}) writeAll(output, frame) } io.Copy(io.Discard, life) frame, _ := EncodeAck(Ack{Kind: Closed, Reason: ReasonEOF, Epoch: epoch}) writeAll(output, frame) os.Exit(0) } if mode == "wrong-epoch" || mode == "wrong-sequence" || mode == "duplicate-ready" || mode == "withheld" || mode == "bad-exit" { ready, _ := EncodeAck(Ack{Kind: Ready, Epoch: epoch}) writeAll(output, ready) var frame [OperationSize]byte io.ReadFull(control, frame[:]) operation, _ := DecodeOperation(frame[:]) ack := Ack{Kind: Accepted, Epoch: epoch, Sequence: operation.Sequence} switch mode { case "wrong-epoch": ack.Epoch++ case "wrong-sequence": ack.Sequence++ case "duplicate-ready": ack = Ack{Kind: Ready, Epoch: epoch} } if mode == "withheld" { if os.WriteFile(os.Getenv("CTHULHU_SUPERVISOR_MARKER"), []byte("pending"), 0600) != nil { os.Exit(2) } } else { encoded, _ := EncodeAck(ack) writeAll(output, encoded) } io.Copy(io.Discard, life) closed, _ := EncodeAck(Ack{Kind: Closed, Reason: ReasonEOF, Epoch: epoch, Sequence: operation.Sequence}) writeAll(output, closed) if mode == "bad-exit" { os.Exit(1) } os.Exit(0) } err = Run(context.Background(), epoch, control, life, output, supervisorTestTarget{mode}) if err != nil { os.Exit(1) } os.Exit(0) } func testSupervisor(t *testing.T, mode string) (*Supervisor, func() *exec.Cmd) { t.Helper() var command *exec.Cmd supervisor := &Supervisor{command: func(epoch uint64) *exec.Cmd { cmd := exec.Command(os.Args[0], "-test.run=^TestSupervisorProcess$") cmd.Env = append(os.Environ(), "CTHULHU_SUPERVISOR_TEST="+mode, "CTHULHU_SUPERVISOR_EPOCH="+strconv.FormatUint(epoch, 10)) t.Cleanup(func() { if cmd.Process != nil { cmd.Process.Kill() } }) command = cmd return cmd }} return supervisor, func() *exec.Cmd { return command } } func startSupervisor(t *testing.T, supervisor *Supervisor, epoch uint64) *Client { t.Helper() ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) client, err := supervisor.Start(ctx, epoch) if err != nil { t.Fatal(err) } t.Cleanup(func() { client.Cancel(); client.Wait() }) return client } func TestSupervisorRetirementAndReplacement(t *testing.T) { supervisor, command := testSupervisor(t, "normal") client := startSupervisor(t, supervisor, 41) group, err := syscall.Getpgid(command().Process.Pid) if err != nil || group != command().Process.Pid || group == syscall.Getpgrp() { t.Fatal("helper shares its writer's process group", err) } if _, err := supervisor.Start(context.Background(), 42); !errors.Is(err, ErrUnavailable) { t.Fatal("overlapping helper admitted", err) } for _, pressed := range []bool{true, true, false} { if err := client.Apply(t.Context(), "Shift_L", pressed); err != nil { t.Fatal(err) } } // Heartbeats must retain an otherwise idle helper beyond its watchdog. select { case <-client.Done(): t.Fatal("idle helper retired", client.Wait()) case <-time.After(Watchdog + 100*time.Millisecond): } client.Cancel() client.Cancel() if err := client.Wait(); err != nil { t.Fatal(err) } if err := client.Apply(t.Context(), "a", true); !errors.Is(err, ErrUnavailable) { t.Fatal("retired helper accepted a key", err) } replacement := startSupervisor(t, supervisor, 42) client.Cancel() if err := replacement.Apply(t.Context(), "Control_R", true); err != nil { t.Fatal("old cleanup affected replacement", err) } replacement.Cancel() if err := replacement.Wait(); err != nil { t.Fatal(err) } } func TestSupervisorUncertainRetirementBlocksReplacement(t *testing.T) { for _, mode := range []string{"cleanup-failure", "exit", "wrong-epoch", "wrong-sequence", "duplicate-ready", "blocked", "bad-exit"} { t.Run(mode, func(t *testing.T) { supervisor, command := testSupervisor(t, mode) client := startSupervisor(t, supervisor, 1) _ = client.Apply(t.Context(), "Shift_L", true) client.Cancel() if err := client.Wait(); !errors.Is(err, ErrUncertain) { t.Fatal("missing cleanup barrier", err) } if _, err := supervisor.Start(t.Context(), 2); !errors.Is(err, ErrUnavailable) { t.Fatal("uncertain input automatically replaced", err) } if mode == "blocked" && command().Process.Signal(syscall.Signal(0)) != nil { t.Fatal("supervisor killed helper before native cleanup") } }) } } func TestSupervisorCancelledAdmittedCall(t *testing.T) { marker := filepath.Join(t.TempDir(), "pending") t.Setenv("CTHULHU_SUPERVISOR_MARKER", marker) supervisor, _ := testSupervisor(t, "withheld") client := startSupervisor(t, supervisor, 31) ctx, cancel := context.WithCancel(context.Background()) defer cancel() result := make(chan error, 1) go func() { result <- client.Apply(ctx, "Shift_L", true) }() deadline := time.NewTimer(operationTimeout) defer deadline.Stop() tick := time.NewTicker(5 * time.Millisecond) defer tick.Stop() pending: for { select { case <-tick.C: if _, err := os.Stat(marker); err == nil { break pending } case <-deadline.C: t.Fatal("helper did not receive pending key") } } if _, err := supervisor.Start(t.Context(), 32); !errors.Is(err, ErrUnavailable) { t.Fatal("pending key owner replaced", err) } cancel() if err := <-result; !errors.Is(err, ErrUnavailable) { t.Fatal("cancelled uncertain key acknowledged", err) } if err := client.Wait(); err != nil { t.Fatal("independent lifeline failed to retire pending call", err) } } func TestSupervisorLaunchValidationAndFailure(t *testing.T) { if _, err := NewSupervisor("relative-helper"); !errors.Is(err, ErrUnavailable) { t.Fatal("relative executable accepted", err) } supervisor, _ := NewSupervisor(filepath.Join(t.TempDir(), "absent-helper")) for _, epoch := range []uint64{0, 1, 2} { if _, err := supervisor.Start(t.Context(), epoch); !errors.Is(err, ErrUnavailable) { t.Fatal("launch failure not private", err) } } if supervisor.active || supervisor.uncertain { t.Fatal("failed exec poisoned unused target") } } func TestSupervisorStartupBarrier(t *testing.T) { for _, mode := range []string{"no-ready", "bad-ready"} { t.Run(mode, func(t *testing.T) { supervisor, _ := testSupervisor(t, mode) _, err := supervisor.Start(t.Context(), 8) if mode == "no-ready" { if !errors.Is(err, ErrUnavailable) || supervisor.uncertain { t.Fatal("confirmed startup retirement poisoned replacement", err) } } else if !errors.Is(err, ErrUncertain) || !supervisor.uncertain { t.Fatal("invalid readiness bypassed retirement barrier", err) } }) } } func TestSupervisorContextAndRepresentation(t *testing.T) { supervisor, _ := testSupervisor(t, "normal") ctx, cancel := context.WithCancel(context.Background()) client, err := supervisor.Start(ctx, 11) if err != nil { t.Fatal(err) } t.Cleanup(func() { cancel(); client.Wait() }) for _, name := range []string{"", "private unsupported name", "A"} { if err := client.Apply(t.Context(), name, true); !errors.Is(err, ErrUnavailable) { t.Fatal("invalid key admitted", err) } } if err := client.Apply(t.Context(), "a", true); err != nil { t.Fatal(err) } cancel() if err := client.Wait(); err != nil { t.Fatal("context cancellation prevented clean EOF retirement", err) } }