From 87d166ecd401efcad39bba0704f2ddc1d8af52ca Mon Sep 17 00:00:00 2001 From: Niko Savola Date: Thu, 17 Sep 2026 15:26:38 +0300 Subject: [PATCH 1/5] fix: fail on ambiguous entity names instead of taking the first - a name matching several projects or tasks silently resolved to whichever the api listed first, so writes could hit the wrong entity Co-Authored-By: DeepSeek V4.1 --- CHANGELOG.md | 1 + pkg/cmd/time-entry/util/interactive.go | 8 +- pkg/search/errors.go | 52 ++++++++++ pkg/search/find.go | 49 +++++++-- pkg/search/find_test.go | 133 +++++++++++++++++++++++++ strhlp/strhlp.go | 15 +++ strhlp/strhlp_test.go | 38 +++++++ 7 files changed, 288 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d35f4d4..d5e0bc1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - shell completion no longer crashes with an index-out-of-range panic when pressing tab after the last positional argument of a command +- Commands that accept names for ids (`in`, `out`, `edit`, `delete`, `report`, etc.) now fail with a list of matching candidates when a name matches more than one project, task, client or tag, instead of silently using the first match ## [v0.64.3] - 2026-09-17 diff --git a/pkg/cmd/time-entry/util/interactive.go b/pkg/cmd/time-entry/util/interactive.go index a8603dc6..d6ce6521 100644 --- a/pkg/cmd/time-entry/util/interactive.go +++ b/pkg/cmd/time-entry/util/interactive.go @@ -161,7 +161,9 @@ func getProjectID( if found == -1 { if projectID != "" { - fmt.Printf("Project '%s' informed was not found.\n", projectID) + fmt.Printf( + "Project '%s' informed was not found or is ambiguous, choose one:\n", + projectID) projectID = "" } } else { @@ -224,7 +226,9 @@ func getTaskID( if found == -1 { if taskID != "" { - fmt.Printf("Task '%s' informed was not found.\n", taskID) + fmt.Printf( + "Task '%s' informed was not found or is ambiguous, choose one:\n", + taskID) taskID = "" } } else { diff --git a/pkg/search/errors.go b/pkg/search/errors.go index bd54e36b..a8ab16cc 100644 --- a/pkg/search/errors.go +++ b/pkg/search/errors.go @@ -35,3 +35,55 @@ func (e ErrNotFound) Error() string { return "No " + e.EntityName + " with id or name containing '" + e.Reference + "' was found" + sufix } + +// Candidate is an entity that matched an ambiguous reference +type Candidate struct { + ID string + Name string +} + +// maxCandidatesInError keeps the ambiguous error message readable when the +// reference matches a long list of entities +const maxCandidatesInError = 10 + +// ErrAmbiguous represents a reference that matched more than one entity, the +// caller must be more specific to identify which one it meant +type ErrAmbiguous struct { + EntityName string + Reference string + Candidates []Candidate +} + +func (e ErrAmbiguous) Error() string { + cs := make([]string, len(e.Candidates)) + for i, c := range e.Candidates { + cs[i] = "'" + c.Name + "' (" + c.ID + ")" + } + + return "More than one " + e.EntityName + + " with id or name containing '" + e.Reference + "' was found: " + + strhlp.LimitedListForHumans(cs, maxCandidatesInError) +} + +func newErrAmbiguous(entityName, reference string, matches []named) ErrAmbiguous { + cs := make([]Candidate, len(matches)) + for i, m := range matches { + cs[i] = Candidate{ID: m.GetID(), Name: m.GetName()} + } + + // the api listing order is not stable, sort so the message reads the + // same across runs + sort.Slice(cs, func(i, j int) bool { + if cs[i].Name == cs[j].Name { + return cs[i].ID < cs[j].ID + } + + return cs[i].Name < cs[j].Name + }) + + return ErrAmbiguous{ + EntityName: entityName, + Reference: reference, + Candidates: cs, + } +} diff --git a/pkg/search/find.go b/pkg/search/find.go index bf42adf4..0434cb9e 100644 --- a/pkg/search/find.go +++ b/pkg/search/find.go @@ -26,19 +26,56 @@ func findByName( return r, err } - isSimilar := strhlp.IsSimilar(name) for _, e := range l { if strings.ToLower(e.GetID()) == name { return e.GetID(), nil } + } - if isSimilar(e.GetName()) { - return e.GetID(), nil + var exact, similar []named + isSimilar := strhlp.IsSimilar(name) + for _, e := range l { + switch { + case strhlp.Normalize(e.GetName()) == name: + exact = append(exact, e) + case isSimilar(e.GetName()): + similar = append(similar, e) + } + } + + matches := exact + if len(matches) == 0 { + matches = similar + } + + // the list may repeat an entity, e.g. the client of several projects, + // repeated ids are not ambiguity + matches = uniqueByID(matches) + + switch len(matches) { + case 0: + return r, ErrNotFound{ + EntityName: entityName, + Reference: r, } + case 1: + return matches[0].GetID(), nil + default: + return r, newErrAmbiguous(entityName, r, matches) } +} - return r, ErrNotFound{ - EntityName: entityName, - Reference: r, +func uniqueByID(l []named) []named { + seen := make(map[string]bool, len(l)) + u := make([]named, 0, len(l)) + for _, e := range l { + if seen[e.GetID()] { + continue + } + + seen[e.GetID()] = true + u = append(u, e) } + + return u } diff --git a/pkg/search/find_test.go b/pkg/search/find_test.go index 46dd2acc..03bc13a9 100644 --- a/pkg/search/find_test.go +++ b/pkg/search/find_test.go @@ -1,6 +1,7 @@ package search import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -85,3 +86,135 @@ func TestSearchOnList(t *testing.T) { }) } } + +func TestFindByNameAmbiguous(t *testing.T) { + entities := []named{ + namedStruct{ID: "1", Name: "Web Redesign"}, + namedStruct{ID: "2", Name: "Web Maintenance"}, + namedStruct{ID: "3", Name: "Backend"}, + } + + id, err := findByName("web", "project", func() ([]named, error) { + return entities, nil + }) + + assert.Error(t, err) + assert.Equal(t, "web", id) + + var eAmbiguous ErrAmbiguous + if assert.ErrorAs(t, err, &eAmbiguous) { + assert.Equal(t, "project", eAmbiguous.EntityName) + assert.Equal(t, "web", eAmbiguous.Reference) + if assert.Len(t, eAmbiguous.Candidates, 2) { + assert.Equal(t, Candidate{ID: "2", Name: "Web Maintenance"}, + eAmbiguous.Candidates[0]) + assert.Equal(t, Candidate{ID: "1", Name: "Web Redesign"}, + eAmbiguous.Candidates[1]) + } + } + + assert.Contains(t, err.Error(), "Web Redesign") + assert.Contains(t, err.Error(), "Web Maintenance") +} + +func TestFindByNameAmbiguousCapsCandidatesInMessage(t *testing.T) { + entities := make([]named, 0, 12) + for i := 1; i <= 12; i++ { + entities = append(entities, namedStruct{ + ID: fmt.Sprintf("p%d", i), + Name: fmt.Sprintf("Web %02d", i), + }) + } + + _, err := findByName("web", "project", func() ([]named, error) { + return entities, nil + }) + + if assert.Error(t, err) { + msg := err.Error() + assert.Contains(t, msg, "'Web 10' (p10)") + assert.NotContains(t, msg, "'Web 11'") + assert.Contains(t, msg, ", and 2 more") + assert.NotContains(t, msg, "and and") + } +} + +func TestFindByNameAmbiguousSortsCandidates(t *testing.T) { + redesign := namedStruct{ID: "1", Name: "Web Redesign"} + maintenance := namedStruct{ID: "2", Name: "Web Maintenance"} + design := namedStruct{ID: "3", Name: "Web Design"} + + expected := []Candidate{ + {ID: "3", Name: "Web Design"}, + {ID: "2", Name: "Web Maintenance"}, + {ID: "1", Name: "Web Redesign"}, + } + + for _, entities := range [][]named{ + {redesign, maintenance, design}, + {design, redesign, maintenance}, + {maintenance, design, redesign}, + } { + _, err := findByName("web", "project", func() ([]named, error) { + return entities, nil + }) + + var eAmbiguous ErrAmbiguous + if assert.ErrorAs(t, err, &eAmbiguous) { + assert.Equal(t, expected, eAmbiguous.Candidates) + } + } +} + +func TestFindByNameUnambiguous(t *testing.T) { + entities := []named{ + namedStruct{ID: "1", Name: "Web Redesign"}, + namedStruct{ID: "2", Name: "Web Maintenance"}, + namedStruct{ID: "3", Name: "Backend"}, + namedStruct{ID: "web-4", Name: "Something else"}, + namedStruct{ID: "w", Name: "web"}, + } + + find := func(r string) (string, error) { + return findByName(r, "project", func() ([]named, error) { + return entities, nil + }) + } + + t.Run("exact id", func(t *testing.T) { + id, err := find("web-4") + if assert.NoError(t, err) { + assert.Equal(t, "web-4", id) + } + }) + + t.Run("exact name wins over other fuzzy matches", func(t *testing.T) { + id, err := find("web") + if assert.NoError(t, err) { + assert.Equal(t, "w", id) + } + }) + + t.Run("single fuzzy match", func(t *testing.T) { + id, err := find("maintenance") + if assert.NoError(t, err) { + assert.Equal(t, "2", id) + } + }) + + t.Run("repeated ids are not ambiguous", func(t *testing.T) { + repeated := []named{ + namedStruct{ID: "1", Name: "Web Redesign"}, + namedStruct{ID: "c", Name: "clockify"}, + namedStruct{ID: "2", Name: "Web Maintenance"}, + namedStruct{ID: "c", Name: "clockify"}, + } + + id, err := findByName("clockify", "client", func() ([]named, error) { + return repeated, nil + }) + if assert.NoError(t, err) { + assert.Equal(t, "c", id) + } + }) +} diff --git a/strhlp/strhlp.go b/strhlp/strhlp.go index fd0f9770..99fbcbb2 100644 --- a/strhlp/strhlp.go +++ b/strhlp/strhlp.go @@ -1,6 +1,7 @@ package strhlp import ( + "fmt" "regexp" "strings" "unicode" @@ -90,6 +91,20 @@ func ListForHumans(s []string) string { return strings.Join(s[:len(s)-1], ", ") + " and " + s[len(s)-1] } +// LimitedListForHumans works like ListForHumans, but only lists up to limit +// entries and adds how many were left out +// +// Example: LimitedListForHumans([]string{"one", "two", "three"}, 2) will +// output: "one, two, and 1 more" +func LimitedListForHumans(s []string, limit int) string { + if len(s) <= limit { + return ListForHumans(s) + } + + return ListForHumans(s[:limit]) + + fmt.Sprintf(", and %d more", len(s)-limit) +} + // PadSpace will add spaces to the end of a string until it reaches the size // set at the second parameter func PadSpace(s string, size int) string { diff --git a/strhlp/strhlp_test.go b/strhlp/strhlp_test.go index 41193bf6..48726247 100644 --- a/strhlp/strhlp_test.go +++ b/strhlp/strhlp_test.go @@ -281,6 +281,44 @@ func TestListForHumans(t *testing.T) { } } +func TestLimitedListForHumans(t *testing.T) { + tts := []struct { + args []string + limit int + expected string + }{ + { + args: []string{"uno", "dos", "tres"}, + limit: 3, + expected: "uno, dos and tres", + }, + { + args: []string{"uno", "dos", "tres"}, + limit: 5, + expected: "uno, dos and tres", + }, + { + args: []string{"uno", "dos", "tres"}, + limit: 2, + expected: "uno and dos, and 1 more", + }, + { + args: []string{"uno", "dos", "tres", "cuatro"}, + limit: 1, + expected: "uno, and 3 more", + }, + } + + for _, tt := range tts { + t.Run(tt.expected, func(t *testing.T) { + assert.Equal(t, tt.expected, strhlp.LimitedListForHumans( + tt.args, + tt.limit, + )) + }) + } +} + func TestIsSimilar(t *testing.T) { tts := [][]string{ {"similar", "similar"}, From 46e041217c138e95c17b165df622e54c12941f14 Mon Sep 17 00:00:00 2001 From: Niko Savola Date: Sat, 19 Sep 2026 11:05:50 +0300 Subject: [PATCH 2/5] Apply batched suggestions from Lucas Co-authored-by: Lucas dos Santos Abreu --- CHANGELOG.md | 5 ++++- strhlp/strhlp.go | 6 +++++- strhlp/strhlp_test.go | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5e0bc1e..fedded6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - shell completion no longer crashes with an index-out-of-range panic when pressing tab after the last positional argument of a command -- Commands that accept names for ids (`in`, `out`, `edit`, `delete`, `report`, etc.) now fail with a list of matching candidates when a name matches more than one project, task, client or tag, instead of silently using the first match +- Commands that accept names for ids (`in`, `out`, `edit`, `delete`, + `report`, etc.) now fail with a list of matching candidates when a name + matches more than one project, task, client or tag, instead of silently + using the first match ## [v0.64.3] - 2026-09-17 diff --git a/strhlp/strhlp.go b/strhlp/strhlp.go index 99fbcbb2..7e20998d 100644 --- a/strhlp/strhlp.go +++ b/strhlp/strhlp.go @@ -102,7 +102,11 @@ func LimitedListForHumans(s []string, limit int) string { } return ListForHumans(s[:limit]) + - fmt.Sprintf(", and %d more", len(s)-limit) + return ListForHumans(append( + s[:limit], + fmt.Sprintf("%d more", len(s)-limit), + )) + } // PadSpace will add spaces to the end of a string until it reaches the size diff --git a/strhlp/strhlp_test.go b/strhlp/strhlp_test.go index 48726247..e5068c59 100644 --- a/strhlp/strhlp_test.go +++ b/strhlp/strhlp_test.go @@ -300,12 +300,12 @@ func TestLimitedListForHumans(t *testing.T) { { args: []string{"uno", "dos", "tres"}, limit: 2, - expected: "uno and dos, and 1 more", + expected: "uno, dos and 1 more", }, { args: []string{"uno", "dos", "tres", "cuatro"}, limit: 1, - expected: "uno, and 3 more", + expected: "uno and 3 more", }, } From 246893c1ab2749ceea46a6cfa8bb342146b56de2 Mon Sep 17 00:00:00 2001 From: Lucas dos Santos Abreu Date: Mon, 21 Sep 2026 10:07:23 -0300 Subject: [PATCH 3/5] Apply suggestion from @lucassabreu --- strhlp/strhlp.go | 1 - 1 file changed, 1 deletion(-) diff --git a/strhlp/strhlp.go b/strhlp/strhlp.go index 7e20998d..0b91dfc3 100644 --- a/strhlp/strhlp.go +++ b/strhlp/strhlp.go @@ -101,7 +101,6 @@ func LimitedListForHumans(s []string, limit int) string { return ListForHumans(s) } - return ListForHumans(s[:limit]) + return ListForHumans(append( s[:limit], fmt.Sprintf("%d more", len(s)-limit), From 4bc08bd039e976127c23a37c885d2ee010041925 Mon Sep 17 00:00:00 2001 From: Lucas dos Santos Abreu Date: Mon, 21 Sep 2026 10:12:30 -0300 Subject: [PATCH 4/5] feat: update mockery --- Makefile | 2 +- internal/mocks/mock_Client.go | 87 +++++++++++++++++++--------------- internal/mocks/mock_Config.go | 33 ++++++++----- internal/mocks/mock_Factory.go | 11 ++++- 4 files changed, 80 insertions(+), 53 deletions(-) diff --git a/Makefile b/Makefile index 1bf11f99..aa0871a8 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ go-install: deps-install ## install dev version go install $(MAIN_PKG) go-generate: deps-install ## recreates generate files - go install github.com/vektra/mockery/v3@v3.4.0 + go install github.com/vektra/mockery/v3@v3.8.0 mockery test-install: deps-install go-generate diff --git a/internal/mocks/mock_Client.go b/internal/mocks/mock_Client.go index 4b3047a6..a40616cb 100644 --- a/internal/mocks/mock_Client.go +++ b/internal/mocks/mock_Client.go @@ -16,10 +16,19 @@ func NewMockClient(t interface { mock.TestingT Cleanup(func()) }) *MockClient { + if helper, ok := t.(interface{ Helper() }); ok { + helper.Helper() + } + mock := &MockClient{} mock.Mock.Test(t) - t.Cleanup(func() { mock.AssertExpectations(t) }) + t.Cleanup(func() { + if helper, ok := t.(interface{ Helper() }); ok { + helper.Helper() + } + mock.AssertExpectations(t) + }) return mock } @@ -70,7 +79,7 @@ type MockClient_AddClient_Call struct { // AddClient is a helper method to define mock.On call // - addClientParam api.AddClientParam -func (_e *MockClient_Expecter) AddClient(addClientParam interface{}) *MockClient_AddClient_Call { +func (_e *MockClient_Expecter) AddClient(addClientParam any) *MockClient_AddClient_Call { return &MockClient_AddClient_Call{Call: _e.mock.On("AddClient", addClientParam)} } @@ -130,7 +139,7 @@ type MockClient_AddProject_Call struct { // AddProject is a helper method to define mock.On call // - addProjectParam api.AddProjectParam -func (_e *MockClient_Expecter) AddProject(addProjectParam interface{}) *MockClient_AddProject_Call { +func (_e *MockClient_Expecter) AddProject(addProjectParam any) *MockClient_AddProject_Call { return &MockClient_AddProject_Call{Call: _e.mock.On("AddProject", addProjectParam)} } @@ -190,7 +199,7 @@ type MockClient_AddTask_Call struct { // AddTask is a helper method to define mock.On call // - addTaskParam api.AddTaskParam -func (_e *MockClient_Expecter) AddTask(addTaskParam interface{}) *MockClient_AddTask_Call { +func (_e *MockClient_Expecter) AddTask(addTaskParam any) *MockClient_AddTask_Call { return &MockClient_AddTask_Call{Call: _e.mock.On("AddTask", addTaskParam)} } @@ -241,7 +250,7 @@ type MockClient_ChangeInvoiced_Call struct { // ChangeInvoiced is a helper method to define mock.On call // - changeInvoicedParam api.ChangeInvoicedParam -func (_e *MockClient_Expecter) ChangeInvoiced(changeInvoicedParam interface{}) *MockClient_ChangeInvoiced_Call { +func (_e *MockClient_Expecter) ChangeInvoiced(changeInvoicedParam any) *MockClient_ChangeInvoiced_Call { return &MockClient_ChangeInvoiced_Call{Call: _e.mock.On("ChangeInvoiced", changeInvoicedParam)} } @@ -301,7 +310,7 @@ type MockClient_CreateTimeEntry_Call struct { // CreateTimeEntry is a helper method to define mock.On call // - createTimeEntryParam api.CreateTimeEntryParam -func (_e *MockClient_Expecter) CreateTimeEntry(createTimeEntryParam interface{}) *MockClient_CreateTimeEntry_Call { +func (_e *MockClient_Expecter) CreateTimeEntry(createTimeEntryParam any) *MockClient_CreateTimeEntry_Call { return &MockClient_CreateTimeEntry_Call{Call: _e.mock.On("CreateTimeEntry", createTimeEntryParam)} } @@ -361,7 +370,7 @@ type MockClient_DeleteProject_Call struct { // DeleteProject is a helper method to define mock.On call // - deleteProjectParam api.DeleteProjectParam -func (_e *MockClient_Expecter) DeleteProject(deleteProjectParam interface{}) *MockClient_DeleteProject_Call { +func (_e *MockClient_Expecter) DeleteProject(deleteProjectParam any) *MockClient_DeleteProject_Call { return &MockClient_DeleteProject_Call{Call: _e.mock.On("DeleteProject", deleteProjectParam)} } @@ -421,7 +430,7 @@ type MockClient_DeleteTask_Call struct { // DeleteTask is a helper method to define mock.On call // - deleteTaskParam api.DeleteTaskParam -func (_e *MockClient_Expecter) DeleteTask(deleteTaskParam interface{}) *MockClient_DeleteTask_Call { +func (_e *MockClient_Expecter) DeleteTask(deleteTaskParam any) *MockClient_DeleteTask_Call { return &MockClient_DeleteTask_Call{Call: _e.mock.On("DeleteTask", deleteTaskParam)} } @@ -472,7 +481,7 @@ type MockClient_DeleteTimeEntry_Call struct { // DeleteTimeEntry is a helper method to define mock.On call // - deleteTimeEntryParam api.DeleteTimeEntryParam -func (_e *MockClient_Expecter) DeleteTimeEntry(deleteTimeEntryParam interface{}) *MockClient_DeleteTimeEntry_Call { +func (_e *MockClient_Expecter) DeleteTimeEntry(deleteTimeEntryParam any) *MockClient_DeleteTimeEntry_Call { return &MockClient_DeleteTimeEntry_Call{Call: _e.mock.On("DeleteTimeEntry", deleteTimeEntryParam)} } @@ -534,7 +543,7 @@ type MockClient_GetClients_Call struct { // GetClients is a helper method to define mock.On call // - getClientsParam api.GetClientsParam -func (_e *MockClient_Expecter) GetClients(getClientsParam interface{}) *MockClient_GetClients_Call { +func (_e *MockClient_Expecter) GetClients(getClientsParam any) *MockClient_GetClients_Call { return &MockClient_GetClients_Call{Call: _e.mock.On("GetClients", getClientsParam)} } @@ -596,7 +605,7 @@ type MockClient_GetHydratedTimeEntry_Call struct { // GetHydratedTimeEntry is a helper method to define mock.On call // - getTimeEntryParam api.GetTimeEntryParam -func (_e *MockClient_Expecter) GetHydratedTimeEntry(getTimeEntryParam interface{}) *MockClient_GetHydratedTimeEntry_Call { +func (_e *MockClient_Expecter) GetHydratedTimeEntry(getTimeEntryParam any) *MockClient_GetHydratedTimeEntry_Call { return &MockClient_GetHydratedTimeEntry_Call{Call: _e.mock.On("GetHydratedTimeEntry", getTimeEntryParam)} } @@ -658,7 +667,7 @@ type MockClient_GetHydratedTimeEntryInProgress_Call struct { // GetHydratedTimeEntryInProgress is a helper method to define mock.On call // - getTimeEntryInProgressParam api.GetTimeEntryInProgressParam -func (_e *MockClient_Expecter) GetHydratedTimeEntryInProgress(getTimeEntryInProgressParam interface{}) *MockClient_GetHydratedTimeEntryInProgress_Call { +func (_e *MockClient_Expecter) GetHydratedTimeEntryInProgress(getTimeEntryInProgressParam any) *MockClient_GetHydratedTimeEntryInProgress_Call { return &MockClient_GetHydratedTimeEntryInProgress_Call{Call: _e.mock.On("GetHydratedTimeEntryInProgress", getTimeEntryInProgressParam)} } @@ -773,7 +782,7 @@ type MockClient_GetProject_Call struct { // GetProject is a helper method to define mock.On call // - getProjectParam api.GetProjectParam -func (_e *MockClient_Expecter) GetProject(getProjectParam interface{}) *MockClient_GetProject_Call { +func (_e *MockClient_Expecter) GetProject(getProjectParam any) *MockClient_GetProject_Call { return &MockClient_GetProject_Call{Call: _e.mock.On("GetProject", getProjectParam)} } @@ -835,7 +844,7 @@ type MockClient_GetProjects_Call struct { // GetProjects is a helper method to define mock.On call // - getProjectsParam api.GetProjectsParam -func (_e *MockClient_Expecter) GetProjects(getProjectsParam interface{}) *MockClient_GetProjects_Call { +func (_e *MockClient_Expecter) GetProjects(getProjectsParam any) *MockClient_GetProjects_Call { return &MockClient_GetProjects_Call{Call: _e.mock.On("GetProjects", getProjectsParam)} } @@ -897,7 +906,7 @@ type MockClient_GetTag_Call struct { // GetTag is a helper method to define mock.On call // - getTagParam api.GetTagParam -func (_e *MockClient_Expecter) GetTag(getTagParam interface{}) *MockClient_GetTag_Call { +func (_e *MockClient_Expecter) GetTag(getTagParam any) *MockClient_GetTag_Call { return &MockClient_GetTag_Call{Call: _e.mock.On("GetTag", getTagParam)} } @@ -959,7 +968,7 @@ type MockClient_GetTags_Call struct { // GetTags is a helper method to define mock.On call // - getTagsParam api.GetTagsParam -func (_e *MockClient_Expecter) GetTags(getTagsParam interface{}) *MockClient_GetTags_Call { +func (_e *MockClient_Expecter) GetTags(getTagsParam any) *MockClient_GetTags_Call { return &MockClient_GetTags_Call{Call: _e.mock.On("GetTags", getTagsParam)} } @@ -1019,7 +1028,7 @@ type MockClient_GetTask_Call struct { // GetTask is a helper method to define mock.On call // - getTaskParam api.GetTaskParam -func (_e *MockClient_Expecter) GetTask(getTaskParam interface{}) *MockClient_GetTask_Call { +func (_e *MockClient_Expecter) GetTask(getTaskParam any) *MockClient_GetTask_Call { return &MockClient_GetTask_Call{Call: _e.mock.On("GetTask", getTaskParam)} } @@ -1081,7 +1090,7 @@ type MockClient_GetTasks_Call struct { // GetTasks is a helper method to define mock.On call // - getTasksParam api.GetTasksParam -func (_e *MockClient_Expecter) GetTasks(getTasksParam interface{}) *MockClient_GetTasks_Call { +func (_e *MockClient_Expecter) GetTasks(getTasksParam any) *MockClient_GetTasks_Call { return &MockClient_GetTasks_Call{Call: _e.mock.On("GetTasks", getTasksParam)} } @@ -1143,7 +1152,7 @@ type MockClient_GetTimeEntry_Call struct { // GetTimeEntry is a helper method to define mock.On call // - getTimeEntryParam api.GetTimeEntryParam -func (_e *MockClient_Expecter) GetTimeEntry(getTimeEntryParam interface{}) *MockClient_GetTimeEntry_Call { +func (_e *MockClient_Expecter) GetTimeEntry(getTimeEntryParam any) *MockClient_GetTimeEntry_Call { return &MockClient_GetTimeEntry_Call{Call: _e.mock.On("GetTimeEntry", getTimeEntryParam)} } @@ -1205,7 +1214,7 @@ type MockClient_GetTimeEntryInProgress_Call struct { // GetTimeEntryInProgress is a helper method to define mock.On call // - getTimeEntryInProgressParam api.GetTimeEntryInProgressParam -func (_e *MockClient_Expecter) GetTimeEntryInProgress(getTimeEntryInProgressParam interface{}) *MockClient_GetTimeEntryInProgress_Call { +func (_e *MockClient_Expecter) GetTimeEntryInProgress(getTimeEntryInProgressParam any) *MockClient_GetTimeEntryInProgress_Call { return &MockClient_GetTimeEntryInProgress_Call{Call: _e.mock.On("GetTimeEntryInProgress", getTimeEntryInProgressParam)} } @@ -1265,7 +1274,7 @@ type MockClient_GetUser_Call struct { // GetUser is a helper method to define mock.On call // - getUser api.GetUser -func (_e *MockClient_Expecter) GetUser(getUser interface{}) *MockClient_GetUser_Call { +func (_e *MockClient_Expecter) GetUser(getUser any) *MockClient_GetUser_Call { return &MockClient_GetUser_Call{Call: _e.mock.On("GetUser", getUser)} } @@ -1327,7 +1336,7 @@ type MockClient_GetUserTimeEntries_Call struct { // GetUserTimeEntries is a helper method to define mock.On call // - getUserTimeEntriesParam api.GetUserTimeEntriesParam -func (_e *MockClient_Expecter) GetUserTimeEntries(getUserTimeEntriesParam interface{}) *MockClient_GetUserTimeEntries_Call { +func (_e *MockClient_Expecter) GetUserTimeEntries(getUserTimeEntriesParam any) *MockClient_GetUserTimeEntries_Call { return &MockClient_GetUserTimeEntries_Call{Call: _e.mock.On("GetUserTimeEntries", getUserTimeEntriesParam)} } @@ -1389,7 +1398,7 @@ type MockClient_GetUsersHydratedTimeEntries_Call struct { // GetUsersHydratedTimeEntries is a helper method to define mock.On call // - getUserTimeEntriesParam api.GetUserTimeEntriesParam -func (_e *MockClient_Expecter) GetUsersHydratedTimeEntries(getUserTimeEntriesParam interface{}) *MockClient_GetUsersHydratedTimeEntries_Call { +func (_e *MockClient_Expecter) GetUsersHydratedTimeEntries(getUserTimeEntriesParam any) *MockClient_GetUsersHydratedTimeEntries_Call { return &MockClient_GetUsersHydratedTimeEntries_Call{Call: _e.mock.On("GetUsersHydratedTimeEntries", getUserTimeEntriesParam)} } @@ -1449,7 +1458,7 @@ type MockClient_GetWorkspace_Call struct { // GetWorkspace is a helper method to define mock.On call // - getWorkspace api.GetWorkspace -func (_e *MockClient_Expecter) GetWorkspace(getWorkspace interface{}) *MockClient_GetWorkspace_Call { +func (_e *MockClient_Expecter) GetWorkspace(getWorkspace any) *MockClient_GetWorkspace_Call { return &MockClient_GetWorkspace_Call{Call: _e.mock.On("GetWorkspace", getWorkspace)} } @@ -1511,7 +1520,7 @@ type MockClient_GetWorkspaces_Call struct { // GetWorkspaces is a helper method to define mock.On call // - getWorkspaces api.GetWorkspaces -func (_e *MockClient_Expecter) GetWorkspaces(getWorkspaces interface{}) *MockClient_GetWorkspaces_Call { +func (_e *MockClient_Expecter) GetWorkspaces(getWorkspaces any) *MockClient_GetWorkspaces_Call { return &MockClient_GetWorkspaces_Call{Call: _e.mock.On("GetWorkspaces", getWorkspaces)} } @@ -1573,7 +1582,7 @@ type MockClient_Log_Call struct { // Log is a helper method to define mock.On call // - logParam api.LogParam -func (_e *MockClient_Expecter) Log(logParam interface{}) *MockClient_Log_Call { +func (_e *MockClient_Expecter) Log(logParam any) *MockClient_Log_Call { return &MockClient_Log_Call{Call: _e.mock.On("Log", logParam)} } @@ -1635,7 +1644,7 @@ type MockClient_LogRange_Call struct { // LogRange is a helper method to define mock.On call // - logRangeParam api.LogRangeParam -func (_e *MockClient_Expecter) LogRange(logRangeParam interface{}) *MockClient_LogRange_Call { +func (_e *MockClient_Expecter) LogRange(logRangeParam any) *MockClient_LogRange_Call { return &MockClient_LogRange_Call{Call: _e.mock.On("LogRange", logRangeParam)} } @@ -1686,7 +1695,7 @@ type MockClient_Out_Call struct { // Out is a helper method to define mock.On call // - outParam api.OutParam -func (_e *MockClient_Expecter) Out(outParam interface{}) *MockClient_Out_Call { +func (_e *MockClient_Expecter) Out(outParam any) *MockClient_Out_Call { return &MockClient_Out_Call{Call: _e.mock.On("Out", outParam)} } @@ -1739,7 +1748,7 @@ type MockClient_SetDebugLogger_Call struct { // SetDebugLogger is a helper method to define mock.On call // - logger api.Logger -func (_e *MockClient_Expecter) SetDebugLogger(logger interface{}) *MockClient_SetDebugLogger_Call { +func (_e *MockClient_Expecter) SetDebugLogger(logger any) *MockClient_SetDebugLogger_Call { return &MockClient_SetDebugLogger_Call{Call: _e.mock.On("SetDebugLogger", logger)} } @@ -1792,7 +1801,7 @@ type MockClient_SetInfoLogger_Call struct { // SetInfoLogger is a helper method to define mock.On call // - logger api.Logger -func (_e *MockClient_Expecter) SetInfoLogger(logger interface{}) *MockClient_SetInfoLogger_Call { +func (_e *MockClient_Expecter) SetInfoLogger(logger any) *MockClient_SetInfoLogger_Call { return &MockClient_SetInfoLogger_Call{Call: _e.mock.On("SetInfoLogger", logger)} } @@ -1852,7 +1861,7 @@ type MockClient_UpdateProject_Call struct { // UpdateProject is a helper method to define mock.On call // - updateProjectParam api.UpdateProjectParam -func (_e *MockClient_Expecter) UpdateProject(updateProjectParam interface{}) *MockClient_UpdateProject_Call { +func (_e *MockClient_Expecter) UpdateProject(updateProjectParam any) *MockClient_UpdateProject_Call { return &MockClient_UpdateProject_Call{Call: _e.mock.On("UpdateProject", updateProjectParam)} } @@ -1912,7 +1921,7 @@ type MockClient_UpdateProjectEstimate_Call struct { // UpdateProjectEstimate is a helper method to define mock.On call // - updateProjectEstimateParam api.UpdateProjectEstimateParam -func (_e *MockClient_Expecter) UpdateProjectEstimate(updateProjectEstimateParam interface{}) *MockClient_UpdateProjectEstimate_Call { +func (_e *MockClient_Expecter) UpdateProjectEstimate(updateProjectEstimateParam any) *MockClient_UpdateProjectEstimate_Call { return &MockClient_UpdateProjectEstimate_Call{Call: _e.mock.On("UpdateProjectEstimate", updateProjectEstimateParam)} } @@ -1972,7 +1981,7 @@ type MockClient_UpdateProjectMemberships_Call struct { // UpdateProjectMemberships is a helper method to define mock.On call // - updateProjectMembershipsParam api.UpdateProjectMembershipsParam -func (_e *MockClient_Expecter) UpdateProjectMemberships(updateProjectMembershipsParam interface{}) *MockClient_UpdateProjectMemberships_Call { +func (_e *MockClient_Expecter) UpdateProjectMemberships(updateProjectMembershipsParam any) *MockClient_UpdateProjectMemberships_Call { return &MockClient_UpdateProjectMemberships_Call{Call: _e.mock.On("UpdateProjectMemberships", updateProjectMembershipsParam)} } @@ -2032,7 +2041,7 @@ type MockClient_UpdateProjectTemplate_Call struct { // UpdateProjectTemplate is a helper method to define mock.On call // - updateProjectTemplateParam api.UpdateProjectTemplateParam -func (_e *MockClient_Expecter) UpdateProjectTemplate(updateProjectTemplateParam interface{}) *MockClient_UpdateProjectTemplate_Call { +func (_e *MockClient_Expecter) UpdateProjectTemplate(updateProjectTemplateParam any) *MockClient_UpdateProjectTemplate_Call { return &MockClient_UpdateProjectTemplate_Call{Call: _e.mock.On("UpdateProjectTemplate", updateProjectTemplateParam)} } @@ -2092,7 +2101,7 @@ type MockClient_UpdateProjectUserBillableRate_Call struct { // UpdateProjectUserBillableRate is a helper method to define mock.On call // - updateProjectUserRateParam api.UpdateProjectUserRateParam -func (_e *MockClient_Expecter) UpdateProjectUserBillableRate(updateProjectUserRateParam interface{}) *MockClient_UpdateProjectUserBillableRate_Call { +func (_e *MockClient_Expecter) UpdateProjectUserBillableRate(updateProjectUserRateParam any) *MockClient_UpdateProjectUserBillableRate_Call { return &MockClient_UpdateProjectUserBillableRate_Call{Call: _e.mock.On("UpdateProjectUserBillableRate", updateProjectUserRateParam)} } @@ -2152,7 +2161,7 @@ type MockClient_UpdateProjectUserCostRate_Call struct { // UpdateProjectUserCostRate is a helper method to define mock.On call // - updateProjectUserRateParam api.UpdateProjectUserRateParam -func (_e *MockClient_Expecter) UpdateProjectUserCostRate(updateProjectUserRateParam interface{}) *MockClient_UpdateProjectUserCostRate_Call { +func (_e *MockClient_Expecter) UpdateProjectUserCostRate(updateProjectUserRateParam any) *MockClient_UpdateProjectUserCostRate_Call { return &MockClient_UpdateProjectUserCostRate_Call{Call: _e.mock.On("UpdateProjectUserCostRate", updateProjectUserRateParam)} } @@ -2212,7 +2221,7 @@ type MockClient_UpdateTask_Call struct { // UpdateTask is a helper method to define mock.On call // - updateTaskParam api.UpdateTaskParam -func (_e *MockClient_Expecter) UpdateTask(updateTaskParam interface{}) *MockClient_UpdateTask_Call { +func (_e *MockClient_Expecter) UpdateTask(updateTaskParam any) *MockClient_UpdateTask_Call { return &MockClient_UpdateTask_Call{Call: _e.mock.On("UpdateTask", updateTaskParam)} } @@ -2272,7 +2281,7 @@ type MockClient_UpdateTimeEntry_Call struct { // UpdateTimeEntry is a helper method to define mock.On call // - updateTimeEntryParam api.UpdateTimeEntryParam -func (_e *MockClient_Expecter) UpdateTimeEntry(updateTimeEntryParam interface{}) *MockClient_UpdateTimeEntry_Call { +func (_e *MockClient_Expecter) UpdateTimeEntry(updateTimeEntryParam any) *MockClient_UpdateTimeEntry_Call { return &MockClient_UpdateTimeEntry_Call{Call: _e.mock.On("UpdateTimeEntry", updateTimeEntryParam)} } @@ -2334,7 +2343,7 @@ type MockClient_WorkspaceUsers_Call struct { // WorkspaceUsers is a helper method to define mock.On call // - workspaceUsersParam api.WorkspaceUsersParam -func (_e *MockClient_Expecter) WorkspaceUsers(workspaceUsersParam interface{}) *MockClient_WorkspaceUsers_Call { +func (_e *MockClient_Expecter) WorkspaceUsers(workspaceUsersParam any) *MockClient_WorkspaceUsers_Call { return &MockClient_WorkspaceUsers_Call{Call: _e.mock.On("WorkspaceUsers", workspaceUsersParam)} } diff --git a/internal/mocks/mock_Config.go b/internal/mocks/mock_Config.go index 8435914e..987c5cf6 100644 --- a/internal/mocks/mock_Config.go +++ b/internal/mocks/mock_Config.go @@ -17,10 +17,19 @@ func NewMockConfig(t interface { mock.TestingT Cleanup(func()) }) *MockConfig { + if helper, ok := t.(interface{ Helper() }); ok { + helper.Helper() + } + mock := &MockConfig{} mock.Mock.Test(t) - t.Cleanup(func() { mock.AssertExpectations(t) }) + t.Cleanup(func() { + if helper, ok := t.(interface{ Helper() }); ok { + helper.Helper() + } + mock.AssertExpectations(t) + }) return mock } @@ -110,7 +119,7 @@ type MockConfig_Get_Call struct { // Get is a helper method to define mock.On call // - s string -func (_e *MockConfig_Expecter) Get(s interface{}) *MockConfig_Get_Call { +func (_e *MockConfig_Expecter) Get(s any) *MockConfig_Get_Call { return &MockConfig_Get_Call{Call: _e.mock.On("Get", s)} } @@ -161,7 +170,7 @@ type MockConfig_GetBool_Call struct { // GetBool is a helper method to define mock.On call // - s string -func (_e *MockConfig_Expecter) GetBool(s interface{}) *MockConfig_GetBool_Call { +func (_e *MockConfig_Expecter) GetBool(s any) *MockConfig_GetBool_Call { return &MockConfig_GetBool_Call{Call: _e.mock.On("GetBool", s)} } @@ -212,7 +221,7 @@ type MockConfig_GetInt_Call struct { // GetInt is a helper method to define mock.On call // - s string -func (_e *MockConfig_Expecter) GetInt(s interface{}) *MockConfig_GetInt_Call { +func (_e *MockConfig_Expecter) GetInt(s any) *MockConfig_GetInt_Call { return &MockConfig_GetInt_Call{Call: _e.mock.On("GetInt", s)} } @@ -263,7 +272,7 @@ type MockConfig_GetString_Call struct { // GetString is a helper method to define mock.On call // - s string -func (_e *MockConfig_Expecter) GetString(s interface{}) *MockConfig_GetString_Call { +func (_e *MockConfig_Expecter) GetString(s any) *MockConfig_GetString_Call { return &MockConfig_GetString_Call{Call: _e.mock.On("GetString", s)} } @@ -316,7 +325,7 @@ type MockConfig_GetStringSlice_Call struct { // GetStringSlice is a helper method to define mock.On call // - s string -func (_e *MockConfig_Expecter) GetStringSlice(s interface{}) *MockConfig_GetStringSlice_Call { +func (_e *MockConfig_Expecter) GetStringSlice(s any) *MockConfig_GetStringSlice_Call { return &MockConfig_GetStringSlice_Call{Call: _e.mock.On("GetStringSlice", s)} } @@ -755,7 +764,7 @@ type MockConfig_SetBool_Call struct { // SetBool is a helper method to define mock.On call // - s string // - b bool -func (_e *MockConfig_Expecter) SetBool(s interface{}, b interface{}) *MockConfig_SetBool_Call { +func (_e *MockConfig_Expecter) SetBool(s any, b any) *MockConfig_SetBool_Call { return &MockConfig_SetBool_Call{Call: _e.mock.On("SetBool", s, b)} } @@ -801,7 +810,7 @@ type MockConfig_SetInt_Call struct { // SetInt is a helper method to define mock.On call // - s string // - n int -func (_e *MockConfig_Expecter) SetInt(s interface{}, n interface{}) *MockConfig_SetInt_Call { +func (_e *MockConfig_Expecter) SetInt(s any, n any) *MockConfig_SetInt_Call { return &MockConfig_SetInt_Call{Call: _e.mock.On("SetInt", s, n)} } @@ -846,7 +855,7 @@ type MockConfig_SetLanguage_Call struct { // SetLanguage is a helper method to define mock.On call // - tag language.Tag -func (_e *MockConfig_Expecter) SetLanguage(tag interface{}) *MockConfig_SetLanguage_Call { +func (_e *MockConfig_Expecter) SetLanguage(tag any) *MockConfig_SetLanguage_Call { return &MockConfig_SetLanguage_Call{Call: _e.mock.On("SetLanguage", tag)} } @@ -887,7 +896,7 @@ type MockConfig_SetString_Call struct { // SetString is a helper method to define mock.On call // - s string // - s1 string -func (_e *MockConfig_Expecter) SetString(s interface{}, s1 interface{}) *MockConfig_SetString_Call { +func (_e *MockConfig_Expecter) SetString(s any, s1 any) *MockConfig_SetString_Call { return &MockConfig_SetString_Call{Call: _e.mock.On("SetString", s, s1)} } @@ -933,7 +942,7 @@ type MockConfig_SetStringSlice_Call struct { // SetStringSlice is a helper method to define mock.On call // - s string // - strings []string -func (_e *MockConfig_Expecter) SetStringSlice(s interface{}, strings interface{}) *MockConfig_SetStringSlice_Call { +func (_e *MockConfig_Expecter) SetStringSlice(s any, strings any) *MockConfig_SetStringSlice_Call { return &MockConfig_SetStringSlice_Call{Call: _e.mock.On("SetStringSlice", s, strings)} } @@ -978,7 +987,7 @@ type MockConfig_SetTimeZone_Call struct { // SetTimeZone is a helper method to define mock.On call // - location *time.Location -func (_e *MockConfig_Expecter) SetTimeZone(location interface{}) *MockConfig_SetTimeZone_Call { +func (_e *MockConfig_Expecter) SetTimeZone(location any) *MockConfig_SetTimeZone_Call { return &MockConfig_SetTimeZone_Call{Call: _e.mock.On("SetTimeZone", location)} } diff --git a/internal/mocks/mock_Factory.go b/internal/mocks/mock_Factory.go index 03879a0c..f86cab53 100644 --- a/internal/mocks/mock_Factory.go +++ b/internal/mocks/mock_Factory.go @@ -18,10 +18,19 @@ func NewMockFactory(t interface { mock.TestingT Cleanup(func()) }) *MockFactory { + if helper, ok := t.(interface{ Helper() }); ok { + helper.Helper() + } + mock := &MockFactory{} mock.Mock.Test(t) - t.Cleanup(func() { mock.AssertExpectations(t) }) + t.Cleanup(func() { + if helper, ok := t.(interface{ Helper() }); ok { + helper.Helper() + } + mock.AssertExpectations(t) + }) return mock } From 7f64a86a10ed3299718a067caf629ce8f4546d4b Mon Sep 17 00:00:00 2001 From: Lucas dos Santos Abreu Date: Mon, 21 Sep 2026 10:12:37 -0300 Subject: [PATCH 5/5] fix: unit tests --- pkg/search/find_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/search/find_test.go b/pkg/search/find_test.go index 03bc13a9..c28f6213 100644 --- a/pkg/search/find_test.go +++ b/pkg/search/find_test.go @@ -134,7 +134,7 @@ func TestFindByNameAmbiguousCapsCandidatesInMessage(t *testing.T) { msg := err.Error() assert.Contains(t, msg, "'Web 10' (p10)") assert.NotContains(t, msg, "'Web 11'") - assert.Contains(t, msg, ", and 2 more") + assert.Contains(t, msg, " and 2 more") assert.NotContains(t, msg, "and and") } }