package launch import ( "errors" "fmt" "os" "path/filepath" "strings" "syscall" "testing" "time" ) type lostWrite struct{} func (lostWrite) Error() string { return "issues.numbers.issue: the key was written by another writer first" } func (lostWrite) ErrorCode() int { return CodeChanged } // A lost compare-and-set is answered to a bundle with its own code, wrapped or not, and the SDK tells it by // that code alone (mesh-sdk's stdio.CodeChanged); every other refusal is the general code, never that one. func TestALostWriteIsAnsweredWithItsOwnCodeAndNothingElseIs(t *testing.T) { if CodeChanged != -32010 { t.Fatalf("the lost-write code is %d; mesh-sdk tells it by -32010", CodeChanged) } for _, err := range []error{lostWrite{}, fmt.Errorf("asked by issues: %w", lostWrite{})} { if got := errorCode(err); got != CodeChanged { t.Errorf("%v is answered %d", err, got) } } if got := errorCode(errors.New("issues keeps no state numbers")); got == CodeChanged || got != -32000 { t.Errorf("a plain refusal is answered %d", got) } } // A bundle that answers the handshake and an empty tool list, and notes each start and its pid in $OUT. const notingBundle = `#!/bin/sh printf 'started %s\n' "$$" >> "$OUT" while IFS= read -r line; do id=$(printf '%s' "$line" | sed -n 's/.*"id":\([0-9]*\)[,}].*/\1/p') case "$line" in *'"method":"initialize"'*) printf '{"jsonrpc":"2.0","id":%s,"result":{}}\n' "$id" ;; *'"method":"tools/list"'*) printf '{"jsonrpc":"2.0","id":%s,"result":{"tools":[]}}\n' "$id" ;; esac done ` // The service manager stops the runtime by signalling its whole group, every bundle with it (issue 1126, ace's // reboot of 2026-10-28: every bundle "exited (SIGTERM); started again in 500ms" for the 15 s the runtime took // to stop, the power bundle among them dialling the system bus again, which restarted the bus during the // shutdown and held it 29 minutes). A bundle that exits while it serves is started again; once the runtime // itself is stopping, none is, by a restart or by a call. func TestABundleIsStartedAgainUnlessTheRuntimeIsStopping(t *testing.T) { saved := RestartFirst RestartFirst = 10 * time.Millisecond t.Cleanup(func() { RestartFirst = saved; shuttingDown.Store(false) }) for _, row := range []struct { name string stopping bool starts int }{ {"a bundle that dies while the runtime serves", false, 2}, {"a bundle signalled with the stopping runtime", true, 1}, } { t.Run(row.name, func(t *testing.T) { shuttingDown.Store(false) dir := t.TempDir() entry, out := filepath.Join(dir, "bundle"), filepath.Join(dir, "out") if err := os.WriteFile(entry, []byte(notingBundle), 0o755); err != nil { t.Fatal(err) } l, err := Start("noting", entry, append(os.Environ(), "OUT="+out), &subscribing{}, t.Logf) if err != nil { t.Fatal(err) } t.Cleanup(l.Stop) said, _ := os.ReadFile(out) var pid int if _, err := fmt.Sscanf(string(said), "started %d", &pid); err != nil { t.Fatalf("the bundle noted %q", said) } if row.stopping { ShutDown() } if err := syscall.Kill(pid, syscall.SIGTERM); err != nil { t.Fatal(err) } // The restart row ends at its second start; the stopping row watches a window many restart pauses long. deadline := time.Now().Add(250 * time.Millisecond) for { said, _ = os.ReadFile(out) n := strings.Count(string(said), "started") if n > row.starts { t.Fatalf("started %d times, want %d", n, row.starts) } if n == row.starts && !row.stopping { break } if time.Now().After(deadline) { if n != row.starts { t.Fatalf("started %d times, want %d", n, row.starts) } break } time.Sleep(5 * time.Millisecond) } if row.stopping { // And a bundle first loaded now (a reload) is not started either. if _, err := Start("late", entry, append(os.Environ(), "OUT="+out), &subscribing{}, t.Logf); err == nil { t.Error("a bundle was started after the runtime began stopping") } } }) } }