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.
121 lines
3.6 KiB
Go
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")
|
|
}
|