package diary_test import ( "bytes" "context" "log/slog" "os" "path/filepath" "slices" "strings" "testing" "time" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bonfire" "git.golem15.com/golem15/summercms/modules/compass" "git.golem15.com/golem15/summercms/modules/diary" "git.golem15.com/golem15/summercms/modules/party" ) // The records the boot-logging test plugin writes. const ( bootRegisterInfo = "operators registered" bootBootInfo = "registries collected" bootBootWarn = "registry without operators" ) // bootLogPlugin logs from Register through slog.Default, the way a plugin // written before diary does, and from Boot through a component logger. type bootLogPlugin struct{} func (bootLogPlugin) ID() string { return "acme.bootlog" } func (bootLogPlugin) Requires() []string { return nil } func (bootLogPlugin) Register(*backpack.App) error { slog.Default().Info(bootRegisterInfo, "count", 2) return nil } func (bootLogPlugin) Boot(*backpack.App) error { log := diary.For("bootlog") log.Info(bootBootInfo, "count", 3) log.Warn(bootBootWarn, "registry", "refunds") return nil } func init() { party.Register(bootLogPlugin{}) } // bootLogYAML is a console plus a daily file at info, as in the tracer test. const bootLogYAML = `default: stack level: info channels: stack: driver: stack channels: [console, daily] console: driver: console format: auto daily: driver: daily path: storage/logs/summer.log days: 14 format: json level: info ` // TestBootQuietSmoke runs the generated main's sequence (PeekGlobals, // Bootstrap and Setup with FromGlobals, party.Activate, a root with // OnGlobals(diary.ApplyGlobals), Execute) with a plugin that logs while it // registers and boots: an invocation that only asks for help keeps the boot // Info records off the console, every other one prints them, and the daily // file and the console's warnings are never affected. func TestBootQuietSmoke(t *testing.T) { rows := []struct { name string args []string literal bool // Options as a struct literal, without Args wantInfo bool // the boot Info records reach the console }{ {name: "serve --help", args: []string{"serve", "--help"}}, {name: "serve", args: []string{"serve"}, wantInfo: true}, {name: "literal options", args: []string{"serve", "--help"}, literal: true, wantInfo: true}, {name: "no args", args: []string{}}, {name: "list", args: []string{"list"}}, {name: "help serve", args: []string{"help", "serve"}}, {name: "-h", args: []string{"-h"}}, {name: "-v serve --help", args: []string{"-v", "serve", "--help"}, wantInfo: true}, {name: "queue:work", args: []string{"queue:work"}, wantInfo: true}, } for _, row := range rows { t.Run(row.name, func(t *testing.T) { t.Cleanup(diary.Reset) root := t.TempDir() configDir := filepath.Join(root, "config") if err := os.MkdirAll(configDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(configDir, "log.yaml"), []byte(bootLogYAML), 0o644); err != nil { t.Fatal(err) } var console, stdout, stderr bytes.Buffer now := func() time.Time { return time.Date(2026, 10, 28, 12, 0, 0, 0, time.Local) } g := bonfire.PeekGlobals(row.args) var opts diary.Options if row.literal { opts = diary.Options{Verbosity: g.Verbosity, Quiet: g.Quiet, Color: g.Color} } else { opts = diary.FromGlobals(g) opts.Args = slices.Clone(row.args) if opts.Args == nil { opts.Args = []string{} } } opts.Root, opts.Stderr, opts.Environ, opts.Now = root, &console, []string{}, now diary.Bootstrap(&console, opts) cfg, err := compass.Open(compass.Options{Dir: configDir, Env: g.Env, Environ: []string{}}) if err != nil { t.Fatal(err) } if err := diary.Setup(cfg, opts); err != nil { t.Fatalf("Setup: %v", err) } app := backpack.New(cfg) if _, err := party.Activate(app, []string{"acme.bootlog"}); err != nil { t.Fatalf("Activate: %v", err) } commands := []bonfire.Command{ {Name: "serve", Description: "Start the HTTP server", Run: func(context.Context, bonfire.Input, bonfire.Output) error { return nil }}, {Name: "queue:work", Description: "Run the job workers", Run: func(context.Context, bonfire.Input, bonfire.Output) error { return nil }}, } rootCmd, err := bonfire.NewRootIO("acme", commands, strings.NewReader(""), &stdout, &stderr, bonfire.WithVersion(bonfire.Version{App: "v1.0.0", Framework: "v1.0.0"}), bonfire.OnGlobals(diary.ApplyGlobals)) if err != nil { t.Fatal(err) } if code := bonfire.Execute(context.Background(), rootCmd, row.args); code != bonfire.ExitOK { t.Fatalf("Execute(%q) = %d, want 0; stderr:\n%s", row.args, code, stderr.String()) } if slices.Contains(row.args, "--help") && !strings.Contains(stdout.String(), "serve") { t.Errorf("Execute(%q) printed no help:\n%s", row.args, stdout.String()) } out := console.String() for _, msg := range []string{bootRegisterInfo, bootBootInfo} { if got := strings.Contains(out, msg); got != row.wantInfo { t.Errorf("console holds %q = %v, want %v:\n%s", msg, got, row.wantInfo, out) } } if !strings.Contains(out, bootBootWarn) { t.Errorf("console lacks the boot warning %q:\n%s", bootBootWarn, out) } if err := diary.Close(); err != nil { t.Fatalf("Close: %v", err) } files, err := filepath.Glob(filepath.Join(root, "storage", "logs", "*.log")) if err != nil || len(files) != 1 { t.Fatalf("daily files = %v (%v), want one", files, err) } raw, err := os.ReadFile(files[0]) if err != nil { t.Fatal(err) } for _, msg := range []string{bootRegisterInfo, bootBootInfo, bootBootWarn} { if !bytes.Contains(raw, []byte(msg)) { t.Errorf("daily file lacks %q:\n%s", msg, raw) } } }) } } // TestFromGlobalsArgs: FromGlobals records a copy of the process arguments // without the program name, never nil. func TestFromGlobalsArgs(t *testing.T) { saved := os.Args t.Cleanup(func() { os.Args = saved }) os.Args = []string{"acme", "serve", "--help"} opts := diary.FromGlobals(bonfire.Globals{}) if want := []string{"serve", "--help"}; !slices.Equal(opts.Args, want) { t.Fatalf("Args = %q, want %q", opts.Args, want) } opts.Args[0] = "migrate" if os.Args[1] != "serve" { t.Errorf("changing Args changed os.Args to %q", os.Args) } os.Args = []string{"acme"} if opts := diary.FromGlobals(bonfire.Globals{}); opts.Args == nil || len(opts.Args) != 0 { t.Errorf("Args = %#v, want an empty non-nil slice", opts.Args) } }