//go:build integration package main // Issue 1165: these tests make a real unix socket and watch its directory, so they run in the integration level. import ( "context" "net" "os" "path/filepath" "strings" "testing" "time" ) // Issue 1165: a file whose write failed because the node-engine's socket was gone is written when the socket is made // again, with no timer; a file that was placed is not asked again by it. func TestNatsAFillThatFailedForWantOfTheSocketIsTriedWhenTheSocketReturns(t *testing.T) { b := startBus(t) heard := b.listen(t) e := newEngine(t, map[string]string{dunstrc: "owned"}) b.config(t, "dunst", "font-size", "13") b.assign(t, "dunst", dunst(t)) aClient(t, b, e, t.TempDir()) eventually(t, "the failure is said", func() bool { r, _ := heard.last("dunst") return r.Status == StatusFailed && strings.Contains(r.Reason, "fill.sock") }) if got := e.read(t, dunstrc); got != "" { t.Fatalf("written without a socket: %q", got) } e.listen(t) eventually(t, "dunstrc is written once the socket is back", func() bool { return strings.Contains(e.read(t, dunstrc), "Monospace 13") }) eventually(t, "and the report says applied", func() bool { r, _ := heard.last("dunst") return r.Status == StatusApplied }) } // A returning socket fills every module again, a placed one too: the node-engine that made it may have applied a // composition that left a filled file as the composition had it (issue 1945, live on 2026-10-25: release 6's node-engines // wrote claude-code's, node-env's and power's filled files empty, and nothing filled them again). func TestNatsAReturningSocketFillsAPlacedModuleAgain(t *testing.T) { b := startBus(t) e := newEngine(t, map[string]string{dunstrc: "owned"}) stop := e.listen(t) b.config(t, "dunst", "font-size", "13") b.assign(t, "dunst", dunst(t)) aClient(t, b, e, t.TempDir()) eventually(t, "dunstrc is filled", func() bool { return strings.Contains(e.read(t, dunstrc), "Monospace 13") }) // The node-engine replaces itself and, before it returns, the file is emptied. stop() if err := os.WriteFile(e.at(dunstrc), nil, 0o644); err != nil { t.Fatal(err) } e.listen(t) eventually(t, "dunstrc is filled again", func() bool { return strings.Contains(e.read(t, dunstrc), "Monospace 13") }) } func TestWatchSocketWakesWhenTheSocketIsMadeAndWhenItsDirectoryReturns(t *testing.T) { base, err := os.MkdirTemp("/tmp", "ws-") if err != nil { t.Fatal(err) } t.Cleanup(func() { os.RemoveAll(base) }) dir := filepath.Join(base, "engine") if err := os.Mkdir(dir, 0o755); err != nil { t.Fatal(err) } socket := filepath.Join(dir, "fill.sock") ctx, cancel := context.WithCancel(context.Background()) defer cancel() up, err := watchSocket(ctx, socket) if err != nil { t.Fatal(err) } heard := func(what string) { t.Helper() select { case <-up: case <-time.After(5 * time.Second): t.Fatalf("no wake for %s", what) } } l, err := net.Listen("unix", socket) if err != nil { t.Fatal(err) } heard("the socket made") l.Close() // The whole directory goes, as with a runtime directory removed on stop, and returns with the socket. if err := os.RemoveAll(dir); err != nil { t.Fatal(err) } if err := os.Mkdir(dir, 0o755); err != nil { t.Fatal(err) } if l, err = net.Listen("unix", socket); err != nil { t.Fatal(err) } defer l.Close() heard("the directory and socket back") } // Issue 1873: a fill the node-engine refused for a reason of its own (on ace, "read …/superuser.secret: is a // directory", what a container runtime had left there) is tried again when a node-engine makes its socket again, as a // newer engine may write what the older could not. Before, only a fill that failed for want of the socket was, so // ace's postgres stayed unplaced after the engine that cures it was delivered, and its old failure was said every 30 s. func TestNatsAFillTheEngineFailedIsTriedWhenTheSocketReturns(t *testing.T) { b := startBus(t) heard := b.listen(t) e := newEngine(t, map[string]string{dunstrc: "owned"}) if err := os.MkdirAll(e.at(dunstrc), 0o755); err != nil { t.Fatal(err) } stop := e.listen(t) b.config(t, "dunst", "font-size", "13") b.assign(t, "dunst", dunst(t)) aClient(t, b, e, t.TempDir()) eventually(t, "the engine's failure is said", func() bool { r, _ := heard.last("dunst") return r.Status == StatusFailed && strings.Contains(r.Reason, "is a directory") }) // The node-engine replaces itself with one that clears what the runtime left. stop() if err := os.Remove(e.at(dunstrc)); err != nil { t.Fatal(err) } e.listen(t) eventually(t, "dunstrc is written by the new engine", func() bool { return strings.Contains(e.read(t, dunstrc), "Monospace 13") }) eventually(t, "and the report says applied", func() bool { r, _ := heard.last("dunst") return r.Status == StatusApplied }) }