package tasks import ( "fmt" "sync" "testing" "github.com/glebarez/sqlite" "github.com/stretchr/testify/require" "gorm.io/gorm" "senlinai-agent/backend/internal/models" ) func TestUpdateLocksTaskBeforeProjectValidation(t *testing.T) { database, owner, project, task := newTaskLockFixture(t) queries := captureTaskQueryOrder(t, database) _, err := NewService(database).Update(owner.ID, project.Identity, task.Identity, UpdateTaskInput{Title: task.Title}) require.NoError(t, err) require.NotEmpty(t, *queries) require.Equal(t, "task:locked-in-tx", (*queries)[0]) } func TestShareObjectLocksTaskBeforeObjectValidation(t *testing.T) { database, _, project, task := newTaskLockFixture(t) note := models.SenlinAgentNote{ProjectID: project.ID, CreatedBy: task.CreatedBy, Title: "Context", Markdown: "Private"} require.NoError(t, database.Create(¬e).Error) queries := captureTaskQueryOrder(t, database) err := NewService(database).ShareObject(task.ID, "note", note.ID) require.NoError(t, err) require.NotEmpty(t, *queries) require.Equal(t, "task:locked-in-tx", (*queries)[0]) } func TestAssigneeOnlySeesExplicitlySharedObjects(t *testing.T) { database := newTestDB(t) assigneeID := uint(2) task := models.SenlinAgentTask{ProjectID: 1, CreatedBy: 1, AssigneeID: &assigneeID, Title: "处理合同"} note := models.SenlinAgentNote{ProjectID: 1, CreatedBy: 1, Title: "合同背景", Markdown: "只在共享后可见"} require.NoError(t, database.Create(&task).Error) require.NoError(t, database.Create(¬e).Error) service := NewService() before, err := service.VisibleLinkedObjects(task.ID, assigneeID) require.NoError(t, err) require.Empty(t, before) require.NoError(t, service.ShareObject(task.ID, "note", note.ID)) after, err := service.VisibleLinkedObjects(task.ID, assigneeID) require.NoError(t, err) require.Len(t, after, 1) require.Equal(t, "note", after[0].ObjectType) require.Equal(t, note.Identity, after[0].ObjectID) } func TestShareObjectRejectsUnsupportedType(t *testing.T) { newTestDB(t) service := NewService() err := service.ShareObject(1, "ai_session", 9) require.ErrorContains(t, err, "unsupported shared object type") } func TestShareObjectRejectsObjectFromAnotherProject(t *testing.T) { database := newTestDB(t) task := models.SenlinAgentTask{ProjectID: 1, CreatedBy: 1, Title: "Review"} note := models.SenlinAgentNote{ProjectID: 2, CreatedBy: 1, Title: "Other project", Markdown: "Private context"} require.NoError(t, database.Create(&task).Error) require.NoError(t, database.Create(¬e).Error) service := NewService() err := service.ShareObject(task.ID, "note", note.ID) require.ErrorContains(t, err, "shared object not found in task project") } func TestAssignResolvesIdentityAndTaskDTOReflectsReassignment(t *testing.T) { database := newTestDB(t) owner := models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"} first := models.SenlinAgentUser{Email: "first@example.com", DisplayName: "First", PasswordHash: "hash"} second := models.SenlinAgentUser{Email: "second@example.com", DisplayName: "Second", PasswordHash: "hash"} require.NoError(t, database.Create(&owner).Error) require.NoError(t, database.Create(&first).Error) require.NoError(t, database.Create(&second).Error) project := models.SenlinAgentProject{OwnerID: owner.ID, Name: "Alpha", Identifier: "ALPHA"} require.NoError(t, database.Create(&project).Error) task := models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "安排评审", Status: "open"} require.NoError(t, database.Create(&task).Error) service := NewService(database) require.NoError(t, service.Assign(task.ID, first.Identity)) assigned, err := service.Update(owner.ID, project.Identity, task.Identity, UpdateTaskInput{Title: task.Title}) require.NoError(t, err) require.Equal(t, first.Identity, *assigned.AssigneeID) require.NoError(t, service.Assign(task.ID, second.Identity)) reassigned, err := service.Update(owner.ID, project.Identity, task.Identity, UpdateTaskInput{Title: task.Title}) require.NoError(t, err) require.Equal(t, second.Identity, *reassigned.AssigneeID) var event models.SenlinAgentProjectEvent require.NoError(t, database.Where("project_id = ? AND entity_type = ? AND entity_id = ?", project.ID, "task", task.ID).Order("id desc").First(&event).Error) require.Equal(t, "task_assigned", event.EventType) } func newTestDB(t *testing.T) *gorm.DB { t.Helper() database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{}) require.NoError(t, err) require.NoError(t, models.AutoMigrate(database)) models.DBService = database return database } func newTaskLockFixture(t *testing.T) (*gorm.DB, models.SenlinAgentUser, models.SenlinAgentProject, models.SenlinAgentTask) { t.Helper() database := newTestDB(t) owner := models.SenlinAgentUser{Email: "lock-owner@example.com", DisplayName: "Owner", PasswordHash: "hash"} require.NoError(t, database.Create(&owner).Error) project := models.SenlinAgentProject{OwnerID: owner.ID, Name: "Lock", Identifier: "LOCK"} require.NoError(t, database.Create(&project).Error) task := models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "Lock me", Status: "open"} require.NoError(t, database.Create(&task).Error) return database, owner, project, task } func captureTaskQueryOrder(t *testing.T, database *gorm.DB) *[]string { t.Helper() var mutex sync.Mutex queries := make([]string, 0, 4) callbackName := "test:capture_task_lock_" + t.Name() require.NoError(t, database.Callback().Query().Before("gorm:query").Register(callbackName, func(tx *gorm.DB) { table := tx.Statement.Table if tx.Statement.Schema != nil { table = tx.Statement.Schema.Table } if table != (models.SenlinAgentTask{}).TableName() && table != (models.SenlinAgentProject{}).TableName() { return } entry := "project" if table == (models.SenlinAgentTask{}).TableName() { entry = "task" if _, ok := tx.Statement.Clauses["FOR"]; ok { entry += ":locked" if _, ok := tx.Statement.ConnPool.(gorm.TxCommitter); ok { entry += "-in-tx" } } } mutex.Lock() queries = append(queries, entry) mutex.Unlock() })) return &queries }