package wizard import ( "context" "strings" "testing" "gitlab.hive-brain.de/MDietermann/Archduino/internal/module" "gitlab.hive-brain.de/MDietermann/Archduino/internal/modules/hello" "gitlab.hive-brain.de/MDietermann/Archduino/internal/tui" ) func TestSyncSelected(t *testing.T) { orig := []tui.ModuleChoice{ {ID: "a", Selected: false}, {ID: "b", Selected: true}, {ID: "c", Selected: true}, } got := syncSelected(orig, []string{"a", "c"}) want := map[string]bool{"a": true, "b": false, "c": true} for _, m := range got { if m.Selected != want[m.ID] { t.Errorf("%s selected=%v, want %v", m.ID, m.Selected, want[m.ID]) } } if orig[0].Selected { t.Error("orig mutated") } } func TestRenderPlanWithHello(t *testing.T) { reg := module.NewRegistry() _ = reg.Register(hello.Module{Path: t.TempDir() + "/marker"}) out := renderPlanCached(context.Background(), reg, []string{"hello"}, newPlanCache()) if !strings.Contains(out, "hello") { t.Fatalf("output missing hello: %q", out) } } func TestRenderPlanEmpty(t *testing.T) { if got := renderPlanCached(context.Background(), module.NewRegistry(), nil, newPlanCache()); got != "No modules selected." { t.Fatalf("got %q", got) } } func TestParseIntClamp(t *testing.T) { cases := map[string]int{ "": 1, " ": 1, "abc": 1, "3abc": 1, // partial match is rejected (strconv.Atoi, not Sscanf) "0": 1, "-5": 1, "3": 3, "12": 12, "99999999999999999999": 1, // overflow → fallback } for in, want := range cases { got := parseIntClamp(in, 1) if got != want { t.Errorf("parseIntClamp(%q) = %d, want %d", in, got, want) } } } func TestRenderPlanCachesDetectAndPlan(t *testing.T) { var detects, plans int m := countingModule{ meta: module.Meta{ID: "count", Name: "Count"}, onDetect: func() { detects++ }, onPlan: func() { plans++ }, } reg := module.NewRegistry() if err := reg.Register(m); err != nil { t.Fatal(err) } cache := newPlanCache() _ = renderPlanCached(context.Background(), reg, []string{"count"}, cache) _ = renderPlanCached(context.Background(), reg, []string{"count"}, cache) _ = renderPlanCached(context.Background(), reg, []string{"count"}, cache) if detects != 1 || plans != 1 { t.Fatalf("want 1 detect and 1 plan, got detects=%d plans=%d", detects, plans) } } func TestRenderPlanInvalidatesOnSelectionChange(t *testing.T) { var detects int m := countingModule{ meta: module.Meta{ID: "count", Name: "Count"}, onDetect: func() { detects++ }, } reg := module.NewRegistry() _ = reg.Register(m) cache := newPlanCache() _ = renderPlanCached(context.Background(), reg, []string{"count"}, cache) _ = renderPlanCached(context.Background(), reg, []string{}, cache) _ = renderPlanCached(context.Background(), reg, []string{"count"}, cache) if detects != 2 { t.Fatalf("deselect+reselect should re-detect; detects=%d (want 2)", detects) } } type countingModule struct { meta module.Meta onDetect func() onPlan func() } func (c countingModule) Meta() module.Meta { return c.meta } func (c countingModule) Detect(context.Context) (module.State, error) { if c.onDetect != nil { c.onDetect() } return nil, nil } func (c countingModule) Plan(context.Context, module.State) (module.Plan, error) { if c.onPlan != nil { c.onPlan() } return module.Plan{}, nil } func (c countingModule) Apply(context.Context, module.Plan, module.Reporter) error { return nil } func (c countingModule) Verify(context.Context) error { return nil } func TestRunCancelledContextReturnsNoConfirmation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() _, confirmed, err := Run(ctx, module.NewRegistry(), tui.WizardState{}) if err != nil && err != context.Canceled { t.Fatalf("unexpected err: %v", err) } if confirmed { t.Fatal("confirmed=true on cancelled context") } }