fix(api): harden response contracts

This commit is contained in:
2026-07-21 16:44:16 +08:00
parent c56ada3708
commit 2275d388a7
11 changed files with 191 additions and 26 deletions

View File

@@ -81,8 +81,8 @@ type WorkspaceChannelDTO struct {
Type string `json:"type"`
Title string `json:"title"`
Icon string `json:"icon"`
Count *int64 `json:"count,omitempty"`
URL string `json:"url,omitempty"`
Count int64 `json:"count"`
URL string `json:"url"`
SortOrder int `json:"sortOrder"`
}

View File

@@ -171,7 +171,7 @@ func TestWorkspaceMatchesFrontendContract(t *testing.T) {
require.Len(t, workspace.Channels, 7)
require.Equal(t, "overview", workspace.Channels[0].Type)
require.Equal(t, "inbox", workspace.Channels[1].Type)
require.Equal(t, int64(1), *workspace.Channels[1].Count)
require.Equal(t, int64(1), workspace.Channels[1].Count)
require.Equal(t, "custom_link", workspace.Channels[6].Type)
require.Equal(t, "Roadmap Board", workspace.Channels[6].Title)
require.Equal(t, "https://example.com/roadmap-a1", workspace.Channels[6].URL)

View File

@@ -120,12 +120,12 @@ func (s *Service) workspaceTags(projectID uint) ([]WorkspaceTagDTO, error) {
func (s *Service) workspaceChannels(projectID uint, projectIdentity string, counts workspaceCounts) ([]WorkspaceChannelDTO, error) {
total := counts.inbox + counts.tasks + counts.aiSessions + counts.notesSources + counts.cronPlans
channels := []WorkspaceChannelDTO{
{ID: projectIdentity + ":overview", ProjectID: projectIdentity, Type: "overview", Title: "Overview", Icon: "home", Count: &total, SortOrder: 1},
{ID: projectIdentity + ":inbox", ProjectID: projectIdentity, Type: "inbox", Title: "Message Flow", Icon: "mail", Count: &counts.inbox, SortOrder: 2},
{ID: projectIdentity + ":tasks", ProjectID: projectIdentity, Type: "tasks", Title: "Work Plan", Icon: "list", Count: &counts.tasks, SortOrder: 3},
{ID: projectIdentity + ":ai", ProjectID: projectIdentity, Type: "ai_sessions", Title: "AI Sessions", Icon: "sparkles", Count: &counts.aiSessions, SortOrder: 4},
{ID: projectIdentity + ":notes", ProjectID: projectIdentity, Type: "notes_sources", Title: "Notes & Sources", Icon: "file", Count: &counts.notesSources, SortOrder: 5},
{ID: projectIdentity + ":cron", ProjectID: projectIdentity, Type: "cron", Title: "Cron Plans", Icon: "clock", Count: &counts.cronPlans, SortOrder: 6},
{ID: projectIdentity + ":overview", ProjectID: projectIdentity, Type: "overview", Title: "Overview", Icon: "home", Count: total, URL: "", SortOrder: 1},
{ID: projectIdentity + ":inbox", ProjectID: projectIdentity, Type: "inbox", Title: "Message Flow", Icon: "mail", Count: counts.inbox, URL: "", SortOrder: 2},
{ID: projectIdentity + ":tasks", ProjectID: projectIdentity, Type: "tasks", Title: "Work Plan", Icon: "list", Count: counts.tasks, URL: "", SortOrder: 3},
{ID: projectIdentity + ":ai", ProjectID: projectIdentity, Type: "ai_sessions", Title: "AI Sessions", Icon: "sparkles", Count: counts.aiSessions, URL: "", SortOrder: 4},
{ID: projectIdentity + ":notes", ProjectID: projectIdentity, Type: "notes_sources", Title: "Notes & Sources", Icon: "file", Count: counts.notesSources, URL: "", SortOrder: 5},
{ID: projectIdentity + ":cron", ProjectID: projectIdentity, Type: "cron", Title: "Cron Plans", Icon: "clock", Count: counts.cronPlans, URL: "", SortOrder: 6},
}
var customChannels []models.SenlinAgentProjectChannel
if err := models.DBService.Where("project_id = ?", projectID).Order("sort_order asc, id asc").Find(&customChannels).Error; err != nil {
@@ -134,7 +134,7 @@ func (s *Service) workspaceChannels(projectID uint, projectIdentity string, coun
for _, channel := range customChannels {
channels = append(channels, WorkspaceChannelDTO{
ID: channel.Identity, ProjectID: projectIdentity, Type: "custom_link", Title: channel.Title,
Icon: channel.Icon, URL: channel.URL, SortOrder: channel.SortOrder,
Icon: channel.Icon, Count: 0, URL: channel.URL, SortOrder: channel.SortOrder,
})
}
return channels, nil

View File

@@ -94,6 +94,13 @@ func TestWorkspaceUsesIdentitiesUTCTimesAndProjectScopedTags(t *testing.T) {
require.NotContains(t, string(payload), "duration")
}
func TestWorkspaceChannelDTOSerializesRequiredCountAndURL(t *testing.T) {
payload, err := json.Marshal(WorkspaceChannelDTO{})
require.NoError(t, err)
require.Contains(t, string(payload), `"count":0`)
require.Contains(t, string(payload), `"url":""`)
}
func TestWorkspaceDoneTaskDoesNotInventCompletedAt(t *testing.T) {
database := newTestDB(t)
service := NewService()