2026-07-21 14:58:25 +08:00
package projects
import (
"encoding/json"
"fmt"
2026-07-21 15:07:54 +08:00
"os"
2026-07-21 14:58:25 +08:00
"testing"
"time"
"github.com/stretchr/testify/require"
"senlinai-agent/backend/internal/models"
)
func TestWorkspaceUsesIdentitiesUTCTimesAndProjectScopedTags ( t * testing . T ) {
database := newTestDB ( t )
service := NewService ( )
require . NoError ( t , database . Create ( & models . SenlinAgentUser { Email : "david@example.com" , DisplayName : "David" , PasswordHash : "hash" } ) . Error )
project , err := service . CreateProjectWithInput ( 1 , CreateProjectRequest {
Name : "Project A1" , Identifier : "A1" , Icon : "folder" , Background : "#165DFF" , Description : "Client strategy workspace" ,
} )
require . NoError ( t , err )
other , err := service . CreateProject ( 1 , "Project A2" , "" )
require . NoError ( t , err )
projectTag := models . SenlinAgentTag { ProjectID : project . ID , Name : "UI" }
otherTag := models . SenlinAgentTag { ProjectID : other . ID , Name : "UI" }
require . NoError ( t , database . Create ( & projectTag ) . Error )
require . NoError ( t , database . Create ( & otherTag ) . Error )
localZone := time . FixedZone ( "UTC+8" , 8 * 60 * 60 )
createdAt := time . Date ( 2026 , 7 , 20 , 10 , 30 , 0 , 0 , localZone )
updatedAt := createdAt . Add ( 90 * time . Minute )
nextRun := time . Date ( 2026 , 7 , 21 , 9 , 0 , 0 , 0 , localZone )
inbox := models . SenlinAgentInboxItem { ProjectID : project . ID , CreatedBy : 1 , SourceType : "Manual" , Title : "Collect notes" , Body : "Turn research into tasks." , Status : "open" , CreatedAt : createdAt , UpdatedAt : updatedAt }
task := models . SenlinAgentTask { ProjectID : project . ID , CreatedBy : 1 , TagID : & projectTag . ID , Title : "Confirm IA" , Description : "Review channel layout." , Status : "done" , DueAt : & nextRun , CreatedAt : createdAt , UpdatedAt : updatedAt }
session := models . SenlinAgentAISession { ProjectID : project . ID , CreatedBy : 1 , Title : "UI critique" , Context : "Discuss channel behavior." , CreatedAt : createdAt , UpdatedAt : updatedAt }
note := models . SenlinAgentNote { ProjectID : project . ID , CreatedBy : 1 , Title : "Workbench principles" , Markdown : "# Notes" , CreatedAt : createdAt , UpdatedAt : updatedAt }
source := models . SenlinAgentSource { ProjectID : project . ID , CreatedBy : 1 , Kind : "link" , Title : "Reference board" , URL : "https://example.com" , CreatedAt : createdAt , UpdatedAt : updatedAt }
channel := models . SenlinAgentProjectChannel { ProjectID : project . ID , Title : "Roadmap Board" , Icon : "link" , URL : "https://example.com/roadmap" , SortOrder : 7 , CreatedAt : createdAt , UpdatedAt : updatedAt }
plan := models . SenlinAgentCronPlan { ProjectID : project . ID , CreatedBy : 1 , Title : "Weekly review" , Schedule : "0 9 * * 1" , NextRunAt : & nextRun , Enabled : true , CreatedAt : createdAt , UpdatedAt : updatedAt }
for _ , record := range [ ] any { & inbox , & task , & session , & note , & source , & channel , & plan } {
require . NoError ( t , database . Create ( record ) . Error )
}
workspace , err := service . Workspace ( 1 , project . Identity )
require . NoError ( t , err )
require . Equal ( t , project . Identity , workspace . Project . ID )
require . Equal ( t , [ ] WorkspaceTagDTO { { ID : projectTag . Identity , Name : "UI" } } , workspace . Tags )
require . Len ( t , workspace . Channels , 7 )
require . Equal ( t , project . Identity , workspace . Channels [ 0 ] . ProjectID )
require . Equal ( t , channel . Identity , workspace . Channels [ 6 ] . ID )
require . Equal ( t , project . Identity , workspace . Channels [ 6 ] . ProjectID )
require . Len ( t , workspace . Inbox , 1 )
require . Equal ( t , inbox . Identity , workspace . Inbox [ 0 ] . ID )
require . Equal ( t , project . Identity , workspace . Inbox [ 0 ] . ProjectID )
require . Equal ( t , updatedAt . UTC ( ) , workspace . Inbox [ 0 ] . Time )
require . Len ( t , workspace . Tasks , 1 )
require . Equal ( t , task . Identity , workspace . Tasks [ 0 ] . ID )
require . Equal ( t , project . Identity , workspace . Tasks [ 0 ] . ProjectID )
require . Equal ( t , projectTag . Identity , * workspace . Tasks [ 0 ] . TagID )
require . Equal ( t , "UI" , workspace . Tasks [ 0 ] . Tag )
require . Equal ( t , nextRun . UTC ( ) , * workspace . Tasks [ 0 ] . Due )
require . Equal ( t , createdAt . UTC ( ) , workspace . Tasks [ 0 ] . CreatedAt )
2026-07-21 15:07:54 +08:00
require . Nil ( t , workspace . Tasks [ 0 ] . CompletedAt )
2026-07-21 14:58:25 +08:00
require . Len ( t , workspace . AISessions , 1 )
require . Equal ( t , session . Identity , workspace . AISessions [ 0 ] . ID )
require . Equal ( t , project . Identity , workspace . AISessions [ 0 ] . ProjectID )
require . Equal ( t , updatedAt . UTC ( ) , workspace . AISessions [ 0 ] . UpdatedAt )
require . Equal ( t , session . Identity , workspace . RecentSessions [ 0 ] . ID )
require . Len ( t , workspace . NotesSources , 2 )
require . Equal ( t , note . Identity , workspace . NotesSources [ 0 ] . ID )
require . Equal ( t , source . Identity , workspace . NotesSources [ 1 ] . ID )
for _ , item := range workspace . NotesSources {
require . Equal ( t , project . Identity , item . ProjectID )
require . Equal ( t , updatedAt . UTC ( ) , item . UpdatedAt )
}
require . Len ( t , workspace . CronPlans , 1 )
require . Equal ( t , plan . Identity , workspace . CronPlans [ 0 ] . ID )
require . Equal ( t , project . Identity , workspace . CronPlans [ 0 ] . ProjectID )
require . Equal ( t , nextRun . UTC ( ) , * workspace . CronPlans [ 0 ] . NextRun )
payload , err := json . Marshal ( workspace )
require . NoError ( t , err )
require . Contains ( t , string ( payload ) , ` "createdAt":"2026-07-20T02:30:00Z" ` )
require . Contains ( t , string ( payload ) , ` "updatedAt":"2026-07-20T04:00:00Z" ` )
require . Contains ( t , string ( payload ) , ` "nextRun":"2026-07-21T01:00:00Z" ` )
require . NotContains ( t , string ( payload ) , fmt . Sprintf ( ` "id":%d ` , task . ID ) )
require . NotContains ( t , string ( payload ) , "duration" )
}
2026-07-21 15:07:54 +08:00
func TestWorkspaceDoneTaskDoesNotInventCompletedAt ( t * testing . T ) {
database := newTestDB ( t )
service := NewService ( )
require . NoError ( t , database . Create ( & models . SenlinAgentUser { Email : "owner@example.com" , DisplayName : "Owner" , PasswordHash : "hash" } ) . Error )
project , err := service . CreateProject ( 1 , "Alpha" , "" )
require . NoError ( t , err )
createdAt := time . Date ( 2026 , 7 , 20 , 1 , 0 , 0 , 0 , time . UTC )
updatedAt := createdAt . Add ( 2 * time . Hour )
require . NoError ( t , database . Create ( & models . SenlinAgentTask {
ProjectID : project . ID , CreatedBy : 1 , Title : "Done" , Status : "done" , CreatedAt : createdAt , UpdatedAt : updatedAt ,
} ) . Error )
workspace , err := service . Workspace ( 1 , project . Identity )
require . NoError ( t , err )
require . Len ( t , workspace . Tasks , 1 )
require . Nil ( t , workspace . Tasks [ 0 ] . CompletedAt )
payload , err := json . Marshal ( workspace . Tasks [ 0 ] )
require . NoError ( t , err )
require . Contains ( t , string ( payload ) , ` "completedAt":null ` )
}
func TestWorkspaceFileDoesNotOwnDashboard ( t * testing . T ) {
source , err := os . ReadFile ( "workspace.go" )
require . NoError ( t , err )
require . NotContains ( t , string ( source ) , "func (s *Service) Dashboard" )
}
2026-07-21 14:58:25 +08:00
func TestWorkspaceRejectsProjectOwnedByAnotherUserByIdentity ( t * testing . T ) {
newTestDB ( t )
service := NewService ( )
project , err := service . CreateProject ( 2 , "Beta" , "" )
require . NoError ( t , err )
_ , err = service . Workspace ( 1 , project . Identity )
require . Error ( t , err )
}