package game // Regression coverage for the shutdown final-save report: runShutdown records // a failed saveAllOnlineCharacters on the Game, and ShutdownSaveError exposes // it so main can exit non-zero instead of losing online state silently. import ( "os" "path/filepath" "strings" "testing" ) // TestShutdownSaveErrorReportsFailedFinalSave makes the character save fail // (the character file is replaced by a directory, so the atomic replace cannot // land) and requires the shutdown sequence to record the failure on the Game. func TestShutdownSaveErrorReportsFailedFinalSave(t *testing.T) { h := newHarness(t) h.seedAccount("alice") h.seedCharacter("alice", "alice") h.connect("alice") charPath := filepath.Join(h.dir, "players", "characters", "alice.yaml") if err := os.Remove(charPath); err != nil { t.Fatalf("remove character file: %v", err) } if err := os.Mkdir(charPath, 0o755); err != nil { t.Fatalf("block character path: %v", err) } h.g.runShutdown() err := h.g.ShutdownSaveError() if err == nil { t.Fatal("a failed final save was not recorded") } if !strings.Contains(err.Error(), "alice") { t.Fatalf("shutdown save error does not name the unsaved character: %v", err) } } // TestShutdownSaveErrorNilWhenFinalSaveSucceeds pins the other direction: // nothing is reported when every online character was persisted, so main does // not exit non-zero on a clean shutdown. func TestShutdownSaveErrorNilWhenFinalSaveSucceeds(t *testing.T) { h := newHarness(t) h.seedAccount("alice") h.seedCharacter("alice", "alice") h.connect("alice") if err := h.g.ShutdownSaveError(); err != nil { t.Fatalf("ShutdownSaveError before any shutdown = %v, want nil", err) } h.g.runShutdown() if err := h.g.ShutdownSaveError(); err != nil { t.Fatalf("clean shutdown recorded a save failure: %v", err) } if _, err := h.g.AccountStore.LoadCharacter("alice"); err != nil { t.Fatalf("online character was not saved: %v", err) } }