package main import ( "os" "path/filepath" "testing" "time" ) // fakeServer writes an executable standing in for llama-server: it ignores // SIGTERM the way the real one effectively does — by dying messily rather than // cleanly — and reports a non-zero status. func fakeServer(t *testing.T, body string) string { t.Helper() path := filepath.Join(t.TempDir(), "fake-llama-server") if err := os.WriteFile(path, []byte("#!/bin/sh\n"+body+"\n"), 0o755); err != nil { t.Fatal(err) } return path } // A deliberate stop is a yield, and the log has to say so. // // llama-server aborts inside its own static teardown on SIGTERM, so the exit // status of a routine yield is identical to that of a real crash. Reading the // mavgpud log, the two were indistinguishable (Vikunja #491). func TestStopMarksTheExitAsAYield(t *testing.T) { r := newRunner(fakeServer(t, "while : ; do sleep 1 ; done"), nil, "") if err := r.start(); err != nil { t.Fatalf("start: %v", err) } r.mu.Lock() if r.yielding { t.Error("a freshly started server is already marked as yielding") } r.mu.Unlock() r.stop(2 * time.Second) deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { if !r.running() { return } time.Sleep(10 * time.Millisecond) } t.Fatal("the child outlived stop") } // Stopping when nothing is running must not arm the flag for the next child. // The next exit after that would be a real crash logged as a yield. func TestStopWithNoChildDoesNotArmTheFlag(t *testing.T) { r := newRunner("/nonexistent", nil, "") r.stop(10 * time.Millisecond) r.mu.Lock() defer r.mu.Unlock() if r.yielding { t.Error("stop armed the yield flag with no child running") } }