package tui import ( "fmt" "strings" "testing" "time" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/jobhistory" ) // loadSandboxedHistory loads the persisted job history from the sandboxed // mpv config dir. func loadSandboxedHistory(t *testing.T) []jobhistory.Record { t.Helper() path, err := jobHistoryFilePath() if err != nil { t.Fatalf("failed to resolve job history path: %v", err) } records, err := loadJobHistoryRecords(path) if err != nil { t.Fatalf("failed to load job history: %v", err) } return records } // newRecordingModel sandboxes the user config and returns a model with a // started install operation. func newRecordingModel(t *testing.T) *Model { t.Helper() sandboxConfigHome(t) if _, err := config.Reload(); err != nil { t.Fatalf("failed to reload config: %v", err) } m := NewModel() m.state = StateInstalling m.installing = true m.installStartedAt = time.Now().Add(-time.Minute) m.installOutput = []string{"downloading", "installing"} return m } func TestCmdDoneRecordsSuccessfulInstall(t *testing.T) { m := newRecordingModel(t) m.selectedMethodID = "mpv-binary" m.selectedMethod = "mpv" m.installOperationType = OpTypeInstall m = sendCmdDone(t, m, nil) records := loadSandboxedHistory(t) if len(records) != 1 { t.Fatalf("expected 1 record, got %d", len(records)) } rec := records[0] if rec.Type != "install" || rec.MethodID != "mpv-binary" { t.Errorf("unexpected record identity: %+v", rec) } if rec.AppName != getClientName("mpv-binary", "mpv") { t.Errorf("unexpected app name: %q", rec.AppName) } if rec.Status != "complete" || rec.Progress != 100 { t.Errorf("expected complete/100, got %s/%d", rec.Status, rec.Progress) } if rec.ID == "" { t.Error("expected a generated record ID") } if rec.CompletedAt == nil { t.Error("expected CompletedAt to be set") } if rec.StartedAt.Unix() != m.installStartedAt.Unix() { t.Errorf("expected StartedAt from operation start, got %v", rec.StartedAt) } if len(rec.Output) != 2 || rec.Output[0] != "downloading" { t.Errorf("expected captured output, got %v", rec.Output) } } func TestCmdDoneRecordsFailedUninstall(t *testing.T) { m := newRecordingModel(t) m.selectedUninstall = &config.InstalledApp{ ID: "app-1", AppName: "MPV", InstallMethod: "mpv-package", } _ = sendCmdDone(t, m, fmt.Errorf("boom")) records := loadSandboxedHistory(t) if len(records) != 1 { t.Fatalf("expected 1 record, got %d", len(records)) } rec := records[0] if rec.Type != "uninstall" || rec.MethodID != "mpv-package" || rec.AppName != "MPV" { t.Errorf("unexpected record identity: %+v", rec) } if rec.Status != "error" || rec.Error != "boom" { t.Errorf("expected error record, got status=%q error=%q", rec.Status, rec.Error) } } func TestCmdDoneRecordsSuccessfulUpdate(t *testing.T) { m := newRecordingModel(t) m.selectedUpdateItem = &updateItem{ AppName: "MPV", MethodID: "mpv-flatpak", UpdateType: "package-update", } m.installOperationType = OpTypeUpdate _ = sendCmdDone(t, m, nil) records := loadSandboxedHistory(t) if len(records) != 1 { t.Fatalf("expected 1 record, got %d", len(records)) } rec := records[0] if rec.Type != "update" || rec.MethodID != "mpv-flatpak" || rec.AppName != "MPV" { t.Errorf("unexpected record identity: %+v", rec) } if rec.Status != "complete" { t.Errorf("expected complete, got %q", rec.Status) } } func TestRecordJobHistoryPersistsPartialOutcome(t *testing.T) { m := newRecordingModel(t) m.selectedMethodID = "mpv-binary" m.selectedMethod = "mpv" m.installOperationType = OpTypeInstall m.installProgress = 82 m.recordJobHistory(&partialOperationError{ operation: "Installation", cause: fmt.Errorf("config is read-only"), guidance: "Fix config permissions and adopt the installation.", }) records := loadSandboxedHistory(t) if len(records) != 1 { t.Fatalf("expected 1 record, got %d", len(records)) } rec := records[0] if rec.Status != "partial" || rec.Progress != 100 { t.Fatalf("expected partial/100, got %s/%d", rec.Status, rec.Progress) } if !strings.Contains(rec.Error, "authoritative state persistence failed") { t.Errorf("expected authoritative-state error, got %q", rec.Error) } if rec.ErrorDetails != "Fix config permissions and adopt the installation." { t.Errorf("unexpected reconciliation guidance: %q", rec.ErrorDetails) } } func TestCmdDoneSkipsNonJobOperations(t *testing.T) { // Config operations are not app jobs. m := newRecordingModel(t) m.installOperationType = OpTypeConfig _ = sendCmdDone(t, m, nil) if records := loadSandboxedHistory(t); len(records) != 0 { t.Errorf("expected no record for config operation, got %+v", records) } // PATH add/remove is not an app job. m = newRecordingModel(t) m.selectedMethodID = "install-to-path" _ = sendCmdDone(t, m, nil) if records := loadSandboxedHistory(t); len(records) != 0 { t.Errorf("expected no record for PATH operation, got %+v", records) } // No operation type (e.g. manager self-update) is not an app job. m = newRecordingModel(t) _ = sendCmdDone(t, m, nil) if records := loadSandboxedHistory(t); len(records) != 0 { t.Errorf("expected no record for untyped operation, got %+v", records) } } func TestTUIWrittenRecordsLoadInHistoryView(t *testing.T) { m := newRecordingModel(t) m.selectedMethodID = "mpv-binary" m.selectedMethod = "mpv" m.installOperationType = OpTypeInstall m = sendCmdDone(t, m, nil) // The history view must show TUI-written records. m.setupJobHistoryList() if m.jobHistoryLoadErr != "" { t.Fatalf("unexpected load error: %s", m.jobHistoryLoadErr) } items := m.jobHistoryList.Items() if len(items) != 1 { t.Fatalf("expected 1 history item, got %d", len(items)) } } // sendCmdDone delivers a cmdDoneMsg to the model and returns the updated model. func sendCmdDone(t *testing.T, m *Model, err error) *Model { t.Helper() updated, _ := m.Update(cmdDoneMsg{err: err}) um, ok := updated.(*Model) if !ok { t.Fatalf("expected *Model, got %T", updated) } return um }