// tera-serial-check performs a read-only end-to-end check of tera-seriald. package main import ( "context" "encoding/json" "flag" "fmt" "io" "os" "path/filepath" "strings" "time" "tera-clone/services/tera-api/internal/serial" ) const ( defaultHardwareProfile = "fet3568j-c" defaultMappingVersion = "fet3568j-c-20260721-v1" ) type finding struct { Channel string `json:"channel,omitempty"` Check string `json:"check"` Expected string `json:"expected,omitempty"` Actual string `json:"actual,omitempty"` Message string `json:"message,omitempty"` } type checkReport struct { Kind string `json:"kind"` Status string `json:"status"` CheckedAt string `json:"checked_at"` HardwareProfile string `json:"hardware_profile"` MappingVersion string `json:"mapping_version"` ExpectedChannels int `json:"expected_channels"` ObservedChannels int `json:"observed_channels"` AvailableChannels int `json:"available_channels"` Findings []finding `json:"findings,omitempty"` States []serial.State `json:"states,omitempty"` } func main() { os.Exit(run(os.Args[1:], os.Stdout, os.Stderr, time.Now)) } func run(args []string, stdout, stderr io.Writer, now func() time.Time) int { flags := flag.NewFlagSet("tera-serial-check", flag.ContinueOnError) flags.SetOutput(stderr) socketPath := flags.String("socket", serial.DefaultSocketPath, "tera-seriald Unix socket") hardwareProfile := flags.String("hardware-profile", defaultHardwareProfile, "expected hardware profile") mappingVersion := flags.String("mapping-version", defaultMappingVersion, "expected mapping version") timeout := flags.Duration("timeout", 10*time.Second, "overall read-only check timeout") if err := flags.Parse(args); err != nil { return 2 } if flags.NArg() != 0 || !filepath.IsAbs(*socketPath) || strings.TrimSpace(*hardwareProfile) == "" || strings.TrimSpace(*mappingVersion) == "" || *timeout <= 0 { fmt.Fprintln(stderr, "invalid arguments: require an absolute socket path, profile, mapping version, and positive timeout") return 2 } ctx, cancel := context.WithTimeout(context.Background(), *timeout) defer cancel() report := inspect(ctx, serial.NewSocketClient(*socketPath), *hardwareProfile, *mappingVersion, now().UTC()) encoder := json.NewEncoder(stdout) encoder.SetIndent("", " ") if err := encoder.Encode(report); err != nil { fmt.Fprintln(stderr, "write serial check report:", err) return 1 } if report.Status != "pass" { return 1 } return 0 } func inspect(ctx context.Context, controller serial.Controller, expectedProfile, expectedVersion string, checkedAt time.Time) checkReport { report := checkReport{ Kind: "tera-serial-readonly-check", Status: "fail", CheckedAt: checkedAt.UTC().Format(time.RFC3339Nano), HardwareProfile: expectedProfile, MappingVersion: expectedVersion, ExpectedChannels: serial.ChannelCount, } states, err := controller.States(ctx) if err != nil { report.Findings = append(report.Findings, finding{Check: "seriald_states", Message: err.Error()}) return report } report.States = states report.ObservedChannels = len(states) if len(states) != serial.ChannelCount { report.Findings = append(report.Findings, finding{ Check: "channel_count", Expected: fmt.Sprint(serial.ChannelCount), Actual: fmt.Sprint(len(states)), }) } seenChannels := make(map[string]struct{}, len(states)) seenDevices := make(map[string]struct{}, len(states)) for index, state := range states { channelNumber := index + 1 expectedChannel := fmt.Sprintf("CH%02d", channelNumber) ttyNumber := channelNumber + 3 expectedBDF := "0000:01:00.0" if channelNumber > 24 { ttyNumber = channelNumber + 7 expectedBDF = "0002:21:00.0" } expectedDevice := fmt.Sprintf("/dev/ttyWCH%d", ttyNumber) if _, duplicate := seenChannels[state.Channel]; duplicate { report.Findings = append(report.Findings, finding{Channel: state.Channel, Check: "unique_channel", Message: "duplicate channel"}) } seenChannels[state.Channel] = struct{}{} if _, duplicate := seenDevices[state.Device]; duplicate { report.Findings = append(report.Findings, finding{Channel: state.Channel, Check: "unique_device", Actual: state.Device, Message: "duplicate device"}) } seenDevices[state.Device] = struct{}{} checkEqual := func(check, expected, actual string) { if actual != expected { report.Findings = append(report.Findings, finding{Channel: expectedChannel, Check: check, Expected: expected, Actual: actual}) } } checkEqual("channel_order", expectedChannel, state.Channel) checkEqual("device_mapping", expectedDevice, state.Device) checkEqual("pci_ancestry", expectedBDF, state.PCIBDF) checkEqual("hardware_profile", expectedProfile, state.HardwareProfile) checkEqual("mapping_version", expectedVersion, state.MappingVersion) if state.Available { report.AvailableChannels++ } else { report.Findings = append(report.Findings, finding{Channel: expectedChannel, Check: "availability", Expected: "available", Actual: "unavailable", Message: state.Message}) } } if len(report.Findings) == 0 && report.AvailableChannels == serial.ChannelCount { report.Status = "pass" } return report }