package main // Tests for the sing-box version probe (round-2 item 4): parsing of // `sing-box version` output and the mtime/size-keyed cache that keeps // heartbeats from spawning the binary every round. import ( "os" "path/filepath" "strings" "testing" ) func TestParseSingBoxVersion(t *testing.T) { cases := []struct{ in, want string }{ {"sing-box version 1.13.21\n\nEnvironment: go1.24.6 linux/amd64\n", "1.13.21"}, {"sing-box version 1.13.21", "1.13.21"}, {"sing-box version unknown\n", ""}, {"", ""}, {"garbage output\n", ""}, {"sing-box version\n", ""}, } for _, c := range cases { if got := parseSingBoxVersion(c.in); got != c.want { t.Fatalf("parseSingBoxVersion(%q) = %q, want %q", c.in, got, c.want) } } } func writeFakeSingBox(t *testing.T, dir, version string) (binPath, countPath string) { t.Helper() binPath = filepath.Join(dir, "fake-sing-box") countPath = filepath.Join(dir, "count") script := "#!/bin/sh\necho x >> \"" + countPath + "\"\necho 'sing-box version " + version + "'\n" if err := os.WriteFile(binPath, []byte(script), 0o755); err != nil { t.Fatal(err) } return binPath, countPath } func TestSingBoxVersionProbeCachesByMtime(t *testing.T) { dir := t.TempDir() binPath, countPath := writeFakeSingBox(t, dir, "1.2.3") oldBin := cfg.SingBoxBin cfg.SingBoxBin = binPath defer func() { cfg.SingBoxBin = oldBin }() singBoxVersionCache.probed = false defer func() { singBoxVersionCache.probed = false }() count := func() int { data, _ := os.ReadFile(countPath) return strings.Count(string(data), "x") } if got := singBoxVersion(); got != "1.2.3" { t.Fatalf("first probe = %q, want 1.2.3", got) } if got := singBoxVersion(); got != "1.2.3" { t.Fatalf("cached probe = %q, want 1.2.3", got) } if n := count(); n != 1 { t.Fatalf("binary executed %d times across two heartbeats, want 1 (cached)", n) } // Replacing the binary (engine install/update) must trigger one re-probe. writeFakeSingBox(t, dir, "1.2.4") if got := singBoxVersion(); got != "1.2.4" { t.Fatalf("probe after binary replacement = %q, want 1.2.4", got) } if n := count(); n != 2 { t.Fatalf("binary executed %d times after replacement, want 2", n) } } // Round-3 item 12: when the binary reports "unknown" (the locally built // sing-box has no version stamp), the probe falls back to the install // record written by agent-singbox.sh. func TestSingBoxVersionFallsBackToInstallRecord(t *testing.T) { dir := t.TempDir() binPath, _ := writeFakeSingBox(t, dir, "unknown") recordPath := filepath.Join(dir, "version") if err := os.WriteFile(recordPath, []byte("1.13.21\n"), 0o644); err != nil { t.Fatal(err) } oldBin, oldRecord := cfg.SingBoxBin, cfg.SingBoxVersionFile cfg.SingBoxBin, cfg.SingBoxVersionFile = binPath, recordPath defer func() { cfg.SingBoxBin, cfg.SingBoxVersionFile = oldBin, oldRecord }() singBoxVersionCache.probed = false defer func() { singBoxVersionCache.probed = false }() if got := singBoxVersion(); got != "1.13.21" { t.Fatalf("fallback probe = %q, want 1.13.21", got) } // A stamped binary wins over the record. writeFakeSingBox(t, dir, "1.13.22") if got := singBoxVersion(); got != "1.13.22" { t.Fatalf("stamped probe = %q, want 1.13.22 (record must not shadow it)", got) } // A garbage record is ignored. if err := os.WriteFile(recordPath, []byte("not-a-version\n"), 0o644); err != nil { t.Fatal(err) } writeFakeSingBox(t, dir, "unknown") if got := singBoxVersion(); got != "" { t.Fatalf("garbage record: got %q, want empty", got) } if got := installedSingBoxVersion(); got != "" { t.Fatalf("installedSingBoxVersion(garbage) = %q, want empty", got) } if err := os.WriteFile(recordPath, []byte("v1.13.21\n"), 0o644); err != nil { t.Fatal(err) } if got := installedSingBoxVersion(); got != "1.13.21" { t.Fatalf("installedSingBoxVersion(v-prefixed) = %q, want 1.13.21", got) } } func TestSingBoxVersionMissingBinary(t *testing.T) { oldBin := cfg.SingBoxBin cfg.SingBoxBin = filepath.Join(t.TempDir(), "does-not-exist") defer func() { cfg.SingBoxBin = oldBin }() if got := singBoxVersion(); got != "" { t.Fatalf("missing binary: got %q, want empty", got) } }