How can I tell if a video has a variable frame rate?

FFmpeg has a vfrdet filter for this purpose.

ffmpeg -i in -vf vfrdet -an -f null -

In the log, you'll see a readout of the form,

[Parsed_vfrdet_0 @ 0000000003d8fec0] VFR:0.833333 (50/10) min: 23 max: 291)

A non-zero value for VFR indicates a vfr stream. The first value in brackets is the number of frames with a duration different than the expected duration implied by the detected frame rate of the stream. The 2nd value is number of frames having the expected duration. The VFR value is the ratio of the first number to the sum of both.

A couple of caveats: a very low or very high value indicates a few errant frames with non-standard duration, usually the first and/or last frame. These can be considered CFR for most purposes. A value around 0.50 indicates the stream has a frame rate and/or timebase for which the notional frame duration cannot be exactly expressed in the stream's timebase, so the app which generated the file, oscillated the duration e.g. if a stream has a fps of 6 and a timebase of 1/100, then the ideal timestamps would be 0, 16.667, 33.334, 50.000, 66.667, 83.333, 100.000 for the first second of video, but timestamps are integers, so the muxer may store 0, 16, 34, 50, 67, 83, 100. This could show up as a stream with VFR value of 0.5