package doctor import ( "bytes" "context" "errors" "strings" "testing" "github.com/MDietermann/ArduinoArchSetup/internal/config" "github.com/MDietermann/ArduinoArchSetup/internal/module" ) // convergedModule returns an empty Plan — always converged. type convergedModule struct{ id string } func (c convergedModule) Meta() module.Meta { return module.Meta{ID: c.id, Name: c.id} } func (c convergedModule) Detect(context.Context) (module.State, error) { return nil, nil } func (c convergedModule) Plan(context.Context, module.State) (module.Plan, error) { return module.Plan{}, nil } func (c convergedModule) Apply(context.Context, module.Plan, module.Reporter) error { return nil } func (c convergedModule) Verify(context.Context) error { return nil } type pendingModule struct{ id string } func (p pendingModule) Meta() module.Meta { return module.Meta{ID: p.id} } func (p pendingModule) Detect(context.Context) (module.State, error) { return nil, nil } func (p pendingModule) Plan(context.Context, module.State) (module.Plan, error) { return module.Plan{Actions: []module.Action{{Description: "do a thing"}}}, nil } func (p pendingModule) Apply(context.Context, module.Plan, module.Reporter) error { return nil } func (p pendingModule) Verify(context.Context) error { return nil } type detectFailModule struct{ id string } func (d detectFailModule) Meta() module.Meta { return module.Meta{ID: d.id} } func (d detectFailModule) Detect(context.Context) (module.State, error) { return nil, errors.New("boom") } func (d detectFailModule) Plan(context.Context, module.State) (module.Plan, error) { return module.Plan{}, nil } func (d detectFailModule) Apply(context.Context, module.Plan, module.Reporter) error { return nil } func (d detectFailModule) Verify(context.Context) error { return nil } // notFoundRunner simulates "command not found" for version checks. type notFoundRunner struct{} func (notFoundRunner) Run(context.Context, string, ...string) error { return errors.New("not found") } func (notFoundRunner) Output(context.Context, string, ...string) ([]byte, error) { return nil, errors.New("not found") } // fixedRunner returns a preset version string for a matching command. type fixedRunner struct { arduinoOut []byte arduinoErr error wokwiOut []byte wokwiErr error } func (f fixedRunner) Run(context.Context, string, ...string) error { return nil } func (f fixedRunner) Output(_ context.Context, name string, args ...string) ([]byte, error) { switch name { case "arduino-cli": return f.arduinoOut, f.arduinoErr case "wokwi-cli": return f.wokwiOut, f.wokwiErr } return nil, errors.New("no match") } func TestDoctorAllConvergedExitsZero(t *testing.T) { reg := module.NewRegistry() _ = reg.Register(convergedModule{id: "foo"}) var buf bytes.Buffer code := Run(context.Background(), config.Config{}, reg, notFoundRunner{}, &buf) if code != 0 { t.Fatalf("exit %d, want 0; output:\n%s", code, buf.String()) } if !strings.Contains(buf.String(), "[✓] foo") { t.Errorf("missing converged mark: %s", buf.String()) } } func TestDoctorDivergedExitsOne(t *testing.T) { reg := module.NewRegistry() _ = reg.Register(pendingModule{id: "bar"}) var buf bytes.Buffer code := Run(context.Background(), config.Config{}, reg, notFoundRunner{}, &buf) if code != 1 { t.Fatalf("exit %d, want 1", code) } if !strings.Contains(buf.String(), "[✗] bar") { t.Errorf("missing diverged mark: %s", buf.String()) } } func TestDoctorDetectErrorIsReported(t *testing.T) { reg := module.NewRegistry() _ = reg.Register(detectFailModule{id: "baz"}) var buf bytes.Buffer code := Run(context.Background(), config.Config{}, reg, notFoundRunner{}, &buf) if code != 1 { t.Fatalf("exit %d, want 1", code) } if !strings.Contains(buf.String(), "[!] baz") { t.Errorf("missing error mark: %s", buf.String()) } } func TestDoctorArduinoCLIDriftIsReported(t *testing.T) { reg := module.NewRegistry() _ = reg.Register(convergedModule{id: "foo"}) cfg := config.Config{ArduinoCLI: config.ArduinoCLIConfig{Version: "0.35.3"}} runner := fixedRunner{arduinoOut: []byte(`{"VersionString":"0.36.0"}`)} var buf bytes.Buffer code := Run(context.Background(), cfg, reg, runner, &buf) if code != 1 { t.Fatalf("exit %d, want 1 (drift)", code) } if !strings.Contains(buf.String(), "drift") { t.Errorf("missing drift report: %s", buf.String()) } } func TestDoctorMissingBinariesSkipVersionCheck(t *testing.T) { reg := module.NewRegistry() _ = reg.Register(convergedModule{id: "foo"}) cfg := config.Config{ArduinoCLI: config.ArduinoCLIConfig{Version: "0.35.3"}} var buf bytes.Buffer code := Run(context.Background(), cfg, reg, notFoundRunner{}, &buf) if code != 0 { t.Fatalf("exit %d, want 0; output:\n%s", code, buf.String()) } }