Files
3x-ui-agent/internal/singbox/golden_test.go
T
Muse 750a270955 ITEM6 B0: golden fixtures + fixture validation skeleton for sing-box migration
26 cases (protocol x representative transport): current-shape input,
expected sing-box fragment, and share-link baseline generated by running
current main@8e89960 GetLink. Blocked/partial cases (xhttp, kcp,
fallbacks, finalmask, vlessenc, removed SS ciphers, tunnel) record their
unmapped concepts under _b0 for the Q1 decision instead of fabricating
output. Link-route baselines only: JSON/Clash subscription families are
being removed wholesale per the 2026-10-23 decision, Q2 closed.
2026-10-22 18:14:58 +00:00

121 lines
3.6 KiB
Go

package singbox
import (
"encoding/json"
"os"
"path/filepath"
"sort"
"strings"
"testing"
)
// goldenCase mirrors the fixture envelope shared by input.json and
// expected-singbox.json. The tests in this file are READ-ONLY fixture
// validation: they pin the corpus so B1/B2 can build the converter
// against it. They never write to testdata.
type goldenEnvelope struct {
Case string `json:"case"`
B0 *struct {
Disposition string `json:"disposition"`
Unmapped []json.RawMessage `json:"unmapped"`
} `json:"_b0"`
Inbounds []json.RawMessage `json:"inbounds"`
}
var knownDispositions = map[string]bool{
"direct": true,
"partial-pending-q1": true,
"blocked-pending-q1": true,
"external-runtime": true,
"external": true, // used on the input side
}
func goldenDirs(t *testing.T) []string {
t.Helper()
root := filepath.Join("testdata", "golden")
entries, err := os.ReadDir(root)
if err != nil {
t.Fatalf("read golden root: %v", err)
}
var dirs []string
for _, e := range entries {
if e.IsDir() {
dirs = append(dirs, filepath.Join(root, e.Name()))
}
}
sort.Strings(dirs)
if len(dirs) == 0 {
t.Fatal("no golden fixture cases found")
}
return dirs
}
func readJSON(t *testing.T, path string, v any) {
t.Helper()
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
if err := json.Unmarshal(raw, v); err != nil {
t.Fatalf("parse %s: %v", path, err)
}
}
// TestGoldenFixturesWellFormed pins corpus completeness: every case dir
// carries input.json, expected-singbox.json and expected-link.txt; every
// JSON parses; dispositions are from the known taxonomy; blocked cases
// must not smuggle in a fabricated sing-box output — they carry an empty
// inbounds list plus at least one _b0.unmapped entry awaiting Q1.
func TestGoldenFixturesWellFormed(t *testing.T) {
for _, dir := range goldenDirs(t) {
name := filepath.Base(dir)
var input struct {
Case string `json:"case"`
Disposition string `json:"disposition"`
}
readJSON(t, filepath.Join(dir, "input.json"), &input)
if input.Case != name {
t.Errorf("%s: input case %q != dir name", name, input.Case)
}
if !knownDispositions[input.Disposition] {
t.Errorf("%s: unknown input disposition %q", name, input.Disposition)
}
var expected goldenEnvelope
readJSON(t, filepath.Join(dir, "expected-singbox.json"), &expected)
if expected.B0 == nil {
t.Errorf("%s: expected-singbox.json missing _b0 metadata", name)
continue
}
disp := expected.B0.Disposition
if !knownDispositions[disp] {
t.Errorf("%s: unknown expected disposition %q", name, disp)
}
if strings.HasPrefix(disp, "blocked") {
if len(expected.Inbounds) != 0 {
t.Errorf("%s: blocked case must not fabricate sing-box inbounds", name)
}
if len(expected.B0.Unmapped) == 0 {
t.Errorf("%s: blocked case must record >=1 unmapped concept for Q1", name)
}
}
if strings.HasPrefix(disp, "partial") && len(expected.B0.Unmapped) == 0 {
t.Errorf("%s: partial case must record its unmapped concepts", name)
}
if _, err := os.Stat(filepath.Join(dir, "expected-link.txt")); err != nil {
t.Errorf("%s: missing expected-link.txt: %v", name, err)
}
}
}
// TestGoldenConversion is the B1/B2 hook: once the converter exists, feed
// each input.json through it and compare against expected-singbox.json
// (metadata keys stripped), then regenerate share links from the converted
// model and compare against expected-link.txt byte for byte. Skipped until
// the converter lands.
func TestGoldenConversion(t *testing.T) {
t.Skip("converter lands in ITEM6 B1/B2; fixtures are the baseline it must meet")
}