mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
The server module named github.com/hanzoai/team/server/v8 — a path that resolves to the retired workspace's repository, not this one — while server/public still published github.com/mattermost/mattermost/server/public. Both are github.com/hanzoteam/server/... now, which is where this code actually lives. That second half is what made the binary unbootable. Upstream's public path stayed claimed by upstream, mattermost-plugin-ai imported it, and the build linked public/model twice; public/plugin's gob.Register then ran twice under one name and panicked at init, before main. Reverting the rename traded the brand for a working binary. Rebranding the plugin alongside it needs no such trade: the agents fork is github.com/hanzoteam/agents/v2 against our public module, so no module named mattermost/mattermost is in the graph at all. Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and it is ours.
1014 lines
34 KiB
Go
1014 lines
34 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
"github.com/hanzoteam/server/server/public/shared/i18n"
|
|
"github.com/hanzoteam/server/server/public/shared/request"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestCreateRecap(t *testing.T) {
|
|
t.Setenv("MM_FEATUREFLAGS_ENABLEAIRECAPS", "true")
|
|
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Enable AI Recaps feature flag
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.EnableAIRecaps = true })
|
|
|
|
t.Run("create recap with valid channels", func(t *testing.T) {
|
|
channel2 := th.CreateChannel(t, th.BasicTeam)
|
|
channelIds := []string{th.BasicChannel.Id, channel2.Id}
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
recap, err := th.App.CreateRecap(ctx, "My Test Recap", channelIds, "test-agent-id")
|
|
require.Nil(t, err)
|
|
require.NotNil(t, recap)
|
|
assert.Equal(t, th.BasicUser.Id, recap.UserId)
|
|
assert.Equal(t, model.RecapStatusPending, recap.Status)
|
|
assert.Equal(t, "My Test Recap", recap.Title)
|
|
})
|
|
|
|
t.Run("create recap with channel user is not member of", func(t *testing.T) {
|
|
// Create a private channel and add only BasicUser2
|
|
privateChannel := th.CreatePrivateChannel(t, th.BasicTeam)
|
|
// Remove BasicUser if they were added automatically
|
|
_ = th.App.RemoveUserFromChannel(th.Context, th.BasicUser.Id, "", privateChannel)
|
|
// Ensure BasicUser2 is a member instead
|
|
th.AddUserToChannel(t, th.BasicUser2, privateChannel)
|
|
|
|
// Try to create recap as BasicUser who is not a member
|
|
channelIds := []string{privateChannel.Id}
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
recap, err := th.App.CreateRecap(ctx, "Test Recap", channelIds, "test-agent-id")
|
|
require.NotNil(t, err)
|
|
assert.Nil(t, recap)
|
|
assert.Equal(t, "app.recap.permission_denied", err.Id)
|
|
})
|
|
|
|
t.Run("cooldown error rounds up remaining minutes", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.AIRecapSettings.EnforceCooldown = model.NewPointer(true)
|
|
cfg.AIRecapSettings.DefaultLimits.CooldownMinutes = model.NewPointer(2)
|
|
})
|
|
|
|
// Place the last recap 1s ago so the remaining cooldown sits near the top of the
|
|
// 2-minute band (~119s). This still exercises ceiling rounding while leaving ~59s of
|
|
// slack, so a slow/loaded CI run can't flip the rounded value down to "1 minute".
|
|
lastCreateAt := model.GetMillis() - int64(1*1000)
|
|
lastManualRecap := &model.Recap{
|
|
Id: model.NewId(),
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Completed Recap",
|
|
CreateAt: lastCreateAt,
|
|
UpdateAt: lastCreateAt,
|
|
DeleteAt: 0,
|
|
ReadAt: 0,
|
|
TotalMessageCount: 1,
|
|
Status: model.RecapStatusCompleted,
|
|
BotID: "test-agent-id",
|
|
}
|
|
_, saveErr := th.App.Srv().Store().Recap().SaveRecap(lastManualRecap)
|
|
require.NoError(t, saveErr)
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
recap, err := th.App.CreateRecap(ctx, "Cooldown Recap", []string{th.BasicChannel.Id}, "test-agent-id")
|
|
require.NotNil(t, err)
|
|
require.Nil(t, recap)
|
|
assert.Equal(t, "app.recap.cooldown_active.app_error", err.Id)
|
|
assert.Contains(t, err.SystemMessage(i18n.GetUserTranslations("en")), "another recap in 2 minutes")
|
|
})
|
|
|
|
t.Run("cooldown still applies after soft deleting last recap", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.AIRecapSettings.EnforceCooldown = model.NewPointer(true)
|
|
cfg.AIRecapSettings.DefaultLimits.CooldownMinutes = model.NewPointer(2)
|
|
})
|
|
|
|
lastCreateAt := model.GetMillis() - int64(30*1000)
|
|
lastManualRecap := &model.Recap{
|
|
Id: model.NewId(),
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Soft Deleted Completed Recap",
|
|
CreateAt: lastCreateAt,
|
|
UpdateAt: lastCreateAt,
|
|
DeleteAt: 0,
|
|
ReadAt: 0,
|
|
TotalMessageCount: 1,
|
|
Status: model.RecapStatusCompleted,
|
|
BotID: "test-agent-id",
|
|
}
|
|
_, saveErr := th.App.Srv().Store().Recap().SaveRecap(lastManualRecap)
|
|
require.NoError(t, saveErr)
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
deleteErr := th.App.DeleteRecap(ctx, lastManualRecap.Id)
|
|
require.Nil(t, deleteErr)
|
|
|
|
recap, err := th.App.CreateRecap(ctx, "Cooldown Recap", []string{th.BasicChannel.Id}, "test-agent-id")
|
|
require.NotNil(t, err)
|
|
require.Nil(t, recap)
|
|
assert.Equal(t, "app.recap.cooldown_active.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("create recap blocked by max channels per recap", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.AIRecapSettings.EnforceChannelsPerRecap = model.NewPointer(true)
|
|
cfg.AIRecapSettings.DefaultLimits.MaxChannelsPerRecap = model.NewPointer(1)
|
|
})
|
|
|
|
channel2 := th.CreateChannel(t, th.BasicTeam)
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
recap, err := th.App.CreateRecap(ctx, "Too Many Channels", []string{th.BasicChannel.Id, channel2.Id}, "test-agent-id")
|
|
require.NotNil(t, err)
|
|
require.Nil(t, recap)
|
|
assert.Equal(t, "app.recap.max_channels_exceeded.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("create recap blocked by max recaps per day", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.AIRecapSettings.EnforceRecapsPerDay = model.NewPointer(true)
|
|
cfg.AIRecapSettings.DefaultLimits.MaxRecapsPerDay = model.NewPointer(1)
|
|
cfg.AIRecapSettings.EnforceCooldown = model.NewPointer(false)
|
|
})
|
|
|
|
existingRecap := &model.Recap{
|
|
Id: model.NewId(),
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Existing Today",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
DeleteAt: 0,
|
|
ReadAt: 0,
|
|
TotalMessageCount: 1,
|
|
Status: model.RecapStatusCompleted,
|
|
BotID: "test-agent-id",
|
|
}
|
|
_, saveErr := th.App.Srv().Store().Recap().SaveRecap(existingRecap)
|
|
require.NoError(t, saveErr)
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
recap, err := th.App.CreateRecap(ctx, "Daily Limit Recap", []string{th.BasicChannel.Id}, "test-agent-id")
|
|
require.NotNil(t, err)
|
|
require.Nil(t, recap)
|
|
assert.Equal(t, "app.recap.max_recaps_reached.app_error", err.Id)
|
|
})
|
|
}
|
|
|
|
func TestCreateRecapMasterToggleDisabledBlocksCreation(t *testing.T) {
|
|
t.Setenv("MM_FEATUREFLAGS_ENABLEAIRECAPS", "true")
|
|
th := Setup(t).InitBasic(t)
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.FeatureFlags.EnableAIRecaps = true
|
|
cfg.AIRecapSettings.Enable = model.NewPointer(false)
|
|
cfg.AIRecapSettings.EnforceRecapsPerDay = model.NewPointer(true)
|
|
cfg.AIRecapSettings.DefaultLimits.MaxRecapsPerDay = model.NewPointer(1)
|
|
cfg.AIRecapSettings.EnforceCooldown = model.NewPointer(true)
|
|
cfg.AIRecapSettings.DefaultLimits.CooldownMinutes = model.NewPointer(60)
|
|
})
|
|
|
|
existingRecap := &model.Recap{
|
|
Id: model.NewId(),
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Existing Today",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
TotalMessageCount: 1,
|
|
Status: model.RecapStatusCompleted,
|
|
BotID: "test-agent-id",
|
|
}
|
|
_, saveErr := th.App.Srv().Store().Recap().SaveRecap(existingRecap)
|
|
require.NoError(t, saveErr)
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
recap, appErr := th.App.CreateRecap(ctx, "Blocked Recap", []string{th.BasicChannel.Id}, "test-agent-id")
|
|
require.NotNil(t, appErr)
|
|
require.Nil(t, recap)
|
|
assert.Equal(t, "api.recap.disabled.app_error", appErr.Id)
|
|
}
|
|
|
|
func TestCreateRecapFeatureFlagDisabledBlocksCreation(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.FeatureFlags.EnableAIRecaps = false
|
|
cfg.AIRecapSettings.Enable = model.NewPointer(true)
|
|
})
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
recap, appErr := th.App.CreateRecap(ctx, "Blocked Recap", []string{th.BasicChannel.Id}, "test-agent-id")
|
|
require.NotNil(t, appErr)
|
|
require.Nil(t, recap)
|
|
assert.Equal(t, "api.recap.disabled.app_error", appErr.Id)
|
|
}
|
|
|
|
func TestGetRecap(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Enable AI Recaps feature flag
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.EnableAIRecaps = true })
|
|
|
|
t.Run("get recap by owner", func(t *testing.T) {
|
|
recap := &model.Recap{
|
|
Id: model.NewId(),
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Test Recap",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
DeleteAt: 0,
|
|
ReadAt: 0,
|
|
TotalMessageCount: 10,
|
|
Status: model.RecapStatusCompleted,
|
|
}
|
|
|
|
_, err := th.App.Srv().Store().Recap().SaveRecap(recap)
|
|
require.NoError(t, err)
|
|
|
|
// Create recap channel
|
|
recapChannel := &model.RecapChannel{
|
|
Id: model.NewId(),
|
|
RecapId: recap.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
ChannelName: th.BasicChannel.DisplayName,
|
|
Highlights: []string{"Test highlight"},
|
|
ActionItems: []string{"Test action"},
|
|
SourcePostIds: []string{model.NewId()},
|
|
CreateAt: model.GetMillis(),
|
|
}
|
|
|
|
err = th.App.Srv().Store().Recap().SaveRecapChannel(recapChannel)
|
|
require.NoError(t, err)
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
retrievedRecap, appErr := th.App.GetRecap(ctx, recap.Id)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, retrievedRecap)
|
|
assert.Equal(t, recap.Id, retrievedRecap.Id)
|
|
assert.Len(t, retrievedRecap.Channels, 1)
|
|
assert.Equal(t, recapChannel.ChannelName, retrievedRecap.Channels[0].ChannelName)
|
|
})
|
|
|
|
t.Run("get recap by non-owner", func(t *testing.T) {
|
|
recap := &model.Recap{
|
|
Id: model.NewId(),
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Test Recap",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
DeleteAt: 0,
|
|
ReadAt: 0,
|
|
TotalMessageCount: 10,
|
|
Status: model.RecapStatusCompleted,
|
|
}
|
|
|
|
_, err := th.App.Srv().Store().Recap().SaveRecap(recap)
|
|
require.NoError(t, err)
|
|
|
|
// Try to get as a different user - create context with BasicUser2's session
|
|
ctx := request.TestContext(t).WithSession(&model.Session{UserId: th.BasicUser2.Id})
|
|
retrievedRecap, appErr := th.App.GetRecap(ctx, recap.Id)
|
|
// Permissions are now checked in API layer, so App layer should return the recap
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, retrievedRecap)
|
|
assert.Equal(t, recap.Id, retrievedRecap.Id)
|
|
})
|
|
}
|
|
|
|
func TestGetRecapsForUser(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Enable AI Recaps feature flag
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.EnableAIRecaps = true })
|
|
|
|
t.Run("get recaps for user", func(t *testing.T) {
|
|
// Create multiple recaps for the user
|
|
for range 5 {
|
|
recap := &model.Recap{
|
|
Id: model.NewId(),
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Test Recap",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
DeleteAt: 0,
|
|
ReadAt: 0,
|
|
TotalMessageCount: 10,
|
|
Status: model.RecapStatusCompleted,
|
|
}
|
|
|
|
_, err := th.App.Srv().Store().Recap().SaveRecap(recap)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
recaps, err := th.App.GetRecapsForUser(ctx, 0, 10)
|
|
require.Nil(t, err)
|
|
assert.Len(t, recaps, 5)
|
|
})
|
|
|
|
t.Run("pagination works correctly", func(t *testing.T) {
|
|
userId := model.NewId()
|
|
|
|
// Create context with the test user's session
|
|
ctx := request.TestContext(t).WithSession(&model.Session{UserId: userId})
|
|
|
|
// Create 15 recaps
|
|
for range 15 {
|
|
recap := &model.Recap{
|
|
Id: model.NewId(),
|
|
UserId: userId,
|
|
Title: "Test Recap",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
DeleteAt: 0,
|
|
ReadAt: 0,
|
|
TotalMessageCount: 10,
|
|
Status: model.RecapStatusCompleted,
|
|
}
|
|
|
|
_, err := th.App.Srv().Store().Recap().SaveRecap(recap)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Get first page
|
|
recapsPage1, err := th.App.GetRecapsForUser(ctx, 0, 10)
|
|
require.Nil(t, err)
|
|
assert.Len(t, recapsPage1, 10)
|
|
|
|
// Get second page
|
|
recapsPage2, err := th.App.GetRecapsForUser(ctx, 1, 10)
|
|
require.Nil(t, err)
|
|
assert.Len(t, recapsPage2, 5)
|
|
})
|
|
}
|
|
|
|
func TestMarkRecapAsRead(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Enable AI Recaps feature flag
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.EnableAIRecaps = true })
|
|
|
|
t.Run("mark recap as read by owner", func(t *testing.T) {
|
|
recap := &model.Recap{
|
|
Id: model.NewId(),
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Test Recap",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
DeleteAt: 0,
|
|
ReadAt: 0,
|
|
TotalMessageCount: 10,
|
|
Status: model.RecapStatusCompleted,
|
|
}
|
|
|
|
savedRecap, err := th.App.Srv().Store().Recap().SaveRecap(recap)
|
|
require.NoError(t, err)
|
|
|
|
// Mark as read
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
updatedRecap, appErr := th.App.MarkRecapAsRead(ctx, savedRecap)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, updatedRecap)
|
|
assert.Greater(t, updatedRecap.ReadAt, int64(0))
|
|
})
|
|
|
|
t.Run("mark recap as read by non-owner", func(t *testing.T) {
|
|
recap := &model.Recap{
|
|
Id: model.NewId(),
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Test Recap",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
DeleteAt: 0,
|
|
ReadAt: 0,
|
|
TotalMessageCount: 10,
|
|
Status: model.RecapStatusCompleted,
|
|
}
|
|
|
|
savedRecap, err := th.App.Srv().Store().Recap().SaveRecap(recap)
|
|
require.NoError(t, err)
|
|
|
|
// Try to mark as read as a different user - create context with BasicUser2's session
|
|
ctx := request.TestContext(t).WithSession(&model.Session{UserId: th.BasicUser2.Id})
|
|
updatedRecap, appErr := th.App.MarkRecapAsRead(ctx, savedRecap)
|
|
// Permissions are now checked in API layer, so App layer should allow it
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, updatedRecap)
|
|
assert.Greater(t, updatedRecap.ReadAt, int64(0))
|
|
})
|
|
}
|
|
|
|
func TestRegenerateRecapLimitEnforcement(t *testing.T) {
|
|
os.Setenv("MM_FEATUREFLAGS_ENABLEAIRECAPS", "true")
|
|
defer os.Unsetenv("MM_FEATUREFLAGS_ENABLEAIRECAPS")
|
|
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
t.Run("regenerate recap blocked by max recaps per day", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.AIRecapSettings.EnforceRecapsPerDay = model.NewPointer(true)
|
|
cfg.AIRecapSettings.DefaultLimits.MaxRecapsPerDay = model.NewPointer(1)
|
|
cfg.AIRecapSettings.EnforceCooldown = model.NewPointer(false)
|
|
})
|
|
|
|
recap := &model.Recap{
|
|
Id: model.NewId(),
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Existing Recap",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
DeleteAt: 0,
|
|
ReadAt: 0,
|
|
TotalMessageCount: 10,
|
|
Status: model.RecapStatusCompleted,
|
|
BotID: "test-agent-id",
|
|
}
|
|
_, err := th.App.Srv().Store().Recap().SaveRecap(recap)
|
|
require.NoError(t, err)
|
|
|
|
recapChannel := &model.RecapChannel{
|
|
Id: model.NewId(),
|
|
RecapId: recap.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
ChannelName: th.BasicChannel.DisplayName,
|
|
Highlights: []string{"highlight"},
|
|
ActionItems: []string{"action"},
|
|
SourcePostIds: []string{model.NewId()},
|
|
CreateAt: model.GetMillis(),
|
|
}
|
|
err = th.App.Srv().Store().Recap().SaveRecapChannel(recapChannel)
|
|
require.NoError(t, err)
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
regenerated, appErr := th.App.RegenerateRecap(ctx, th.BasicUser.Id, recap)
|
|
require.NotNil(t, appErr)
|
|
require.Nil(t, regenerated)
|
|
assert.Equal(t, "app.recap.max_recaps_reached.app_error", appErr.Id)
|
|
})
|
|
|
|
t.Run("regenerate recap blocked by cooldown", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.AIRecapSettings.EnforceRecapsPerDay = model.NewPointer(false)
|
|
cfg.AIRecapSettings.EnforceCooldown = model.NewPointer(true)
|
|
cfg.AIRecapSettings.DefaultLimits.CooldownMinutes = model.NewPointer(60)
|
|
})
|
|
|
|
recap := &model.Recap{
|
|
Id: model.NewId(),
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Cooldown Existing Recap",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
DeleteAt: 0,
|
|
ReadAt: 0,
|
|
TotalMessageCount: 10,
|
|
Status: model.RecapStatusCompleted,
|
|
BotID: "test-agent-id",
|
|
}
|
|
_, err := th.App.Srv().Store().Recap().SaveRecap(recap)
|
|
require.NoError(t, err)
|
|
|
|
recapChannel := &model.RecapChannel{
|
|
Id: model.NewId(),
|
|
RecapId: recap.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
ChannelName: th.BasicChannel.DisplayName,
|
|
Highlights: []string{"highlight"},
|
|
ActionItems: []string{"action"},
|
|
SourcePostIds: []string{model.NewId()},
|
|
CreateAt: model.GetMillis(),
|
|
}
|
|
err = th.App.Srv().Store().Recap().SaveRecapChannel(recapChannel)
|
|
require.NoError(t, err)
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
regenerated, appErr := th.App.RegenerateRecap(ctx, th.BasicUser.Id, recap)
|
|
require.NotNil(t, appErr)
|
|
require.Nil(t, regenerated)
|
|
assert.Equal(t, "app.recap.cooldown_active.app_error", appErr.Id)
|
|
})
|
|
}
|
|
|
|
func TestMarkRecapsAsViewed(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.EnableAIRecaps = true })
|
|
|
|
save := func(userID, status string, viewedAt int64) string {
|
|
r := &model.Recap{
|
|
Id: model.NewId(),
|
|
UserId: userID,
|
|
Title: "T",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
Status: status,
|
|
ViewedAt: viewedAt,
|
|
TotalMessageCount: 1,
|
|
}
|
|
_, err := th.App.Srv().Store().Recap().SaveRecap(r)
|
|
require.NoError(t, err)
|
|
return r.Id
|
|
}
|
|
|
|
t.Run("marks completed and failed and ignores in-flight statuses", func(t *testing.T) {
|
|
userID := model.NewId()
|
|
completed := save(userID, model.RecapStatusCompleted, 0)
|
|
failed := save(userID, model.RecapStatusFailed, 0)
|
|
pending := save(userID, model.RecapStatusPending, 0)
|
|
processing := save(userID, model.RecapStatusProcessing, 0)
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: userID})
|
|
ids, appErr := th.App.MarkRecapsAsViewed(ctx)
|
|
require.Nil(t, appErr)
|
|
assert.ElementsMatch(t, []string{completed, failed}, ids)
|
|
|
|
r1, err := th.App.Srv().Store().Recap().GetRecap(pending)
|
|
require.NoError(t, err)
|
|
assert.Zero(t, r1.ViewedAt)
|
|
r2, err := th.App.Srv().Store().Recap().GetRecap(processing)
|
|
require.NoError(t, err)
|
|
assert.Zero(t, r2.ViewedAt)
|
|
})
|
|
|
|
t.Run("returns empty list when nothing to mark", func(t *testing.T) {
|
|
ctx := th.Context.WithSession(&model.Session{UserId: model.NewId()})
|
|
ids, appErr := th.App.MarkRecapsAsViewed(ctx)
|
|
require.Nil(t, appErr)
|
|
assert.Empty(t, ids)
|
|
})
|
|
|
|
t.Run("publishes a recap_updated websocket event per affected recap", func(t *testing.T) {
|
|
userID := th.BasicUser.Id
|
|
|
|
// Two completed recaps that need to be marked as viewed.
|
|
a := save(userID, model.RecapStatusCompleted, 0)
|
|
b := save(userID, model.RecapStatusCompleted, 0)
|
|
|
|
messages, closeWS := connectFakeWebSocket(t, th, userID, "", []model.WebsocketEventType{model.WebsocketEventRecapUpdated})
|
|
defer closeWS()
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: userID})
|
|
ids, appErr := th.App.MarkRecapsAsViewed(ctx)
|
|
require.Nil(t, appErr)
|
|
assert.ElementsMatch(t, []string{a, b}, ids)
|
|
|
|
seen := make(map[string]bool)
|
|
deadline := time.After(5 * time.Second)
|
|
for len(seen) < 2 {
|
|
select {
|
|
case msg := <-messages:
|
|
recapID, ok := msg.GetData()["recap_id"].(string)
|
|
require.True(t, ok, "recap_updated event missing recap_id")
|
|
seen[recapID] = true
|
|
case <-deadline:
|
|
require.Failf(t, "timed out waiting for recap_updated events", "received %d/2", len(seen))
|
|
}
|
|
}
|
|
assert.True(t, seen[a])
|
|
assert.True(t, seen[b])
|
|
})
|
|
}
|
|
|
|
func TestProcessRecapChannel(t *testing.T) {
|
|
t.Setenv("MM_FEATUREFLAGS_ENABLEAIRECAPS", "true")
|
|
|
|
t.Run("process empty channel", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Enable AI Recaps feature flag
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.EnableAIRecaps = true })
|
|
|
|
// Ensure channel has no posts (it shouldn't in init)
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
// No posts added
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
recapID := model.NewId()
|
|
agentID := "test-agent"
|
|
_, storeErr := th.App.Srv().Store().Recap().SaveRecap(&model.Recap{
|
|
Id: recapID,
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Empty recap",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
Status: model.RecapStatusProcessing,
|
|
BotID: agentID,
|
|
})
|
|
require.NoError(t, storeErr)
|
|
|
|
result, err := th.App.ProcessRecapChannel(ctx, recapID, channel.Id, th.BasicUser.Id, agentID)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, result)
|
|
assert.True(t, result.Success)
|
|
assert.Equal(t, 0, result.MessageCount)
|
|
|
|
recapChannels, storeErr := th.App.Srv().Store().Recap().GetRecapChannelsByRecapId(recapID)
|
|
require.NoError(t, storeErr)
|
|
require.Len(t, recapChannels, 1)
|
|
assert.Equal(t, channel.Id, recapChannels[0].ChannelId)
|
|
assert.Empty(t, recapChannels[0].Highlights)
|
|
assert.Empty(t, recapChannels[0].ActionItems)
|
|
})
|
|
|
|
t.Run("process channel with posts persists recap channel", func(t *testing.T) {
|
|
bridge := &testAgentsBridge{
|
|
completeFn: func(sessionUserID, agentID string, req BridgeCompletionRequest) (string, error) {
|
|
return `{"highlights":["A deterministic highlight"],"action_items":["A deterministic action item"]}`, nil
|
|
},
|
|
}
|
|
|
|
th := Setup(t, WithAgentsBridge(bridge)).InitBasic(t)
|
|
|
|
// Enable AI Recaps feature flag
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.EnableAIRecaps = true })
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
post := th.CreatePost(t, channel)
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
recapID := model.NewId()
|
|
agentID := "test-agent"
|
|
_, storeErr := th.App.Srv().Store().Recap().SaveRecap(&model.Recap{
|
|
Id: recapID,
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Test recap",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
Status: model.RecapStatusProcessing,
|
|
BotID: agentID,
|
|
})
|
|
require.NoError(t, storeErr)
|
|
|
|
result, err := th.App.ProcessRecapChannel(ctx, recapID, channel.Id, th.BasicUser.Id, agentID)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, result)
|
|
assert.True(t, result.Success)
|
|
assert.Equal(t, 1, result.MessageCount)
|
|
require.Len(t, bridge.completeCalls, 1)
|
|
assert.Equal(t, BridgeOperationRecapSummary, bridge.completeCalls[0].request.Operation)
|
|
|
|
recapChannels, storeErr := th.App.Srv().Store().Recap().GetRecapChannelsByRecapId(recapID)
|
|
require.NoError(t, storeErr)
|
|
require.Len(t, recapChannels, 1)
|
|
assert.Equal(t, channel.Id, recapChannels[0].ChannelId)
|
|
assert.Equal(t, []string{"A deterministic highlight"}, recapChannels[0].Highlights)
|
|
assert.Equal(t, []string{"A deterministic action item"}, recapChannels[0].ActionItems)
|
|
assert.Equal(t, []string{post.Id}, recapChannels[0].SourcePostIds)
|
|
})
|
|
|
|
t.Run("malformed completion surfaces parse failure", func(t *testing.T) {
|
|
bridge := &testAgentsBridge{
|
|
completeFn: func(sessionUserID, agentID string, req BridgeCompletionRequest) (string, error) {
|
|
return "{invalid json", nil
|
|
},
|
|
}
|
|
|
|
th := Setup(t, WithAgentsBridge(bridge)).InitBasic(t)
|
|
|
|
// Enable AI Recaps feature flag
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.EnableAIRecaps = true })
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.CreatePost(t, channel)
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
recapID := model.NewId()
|
|
agentID := "test-agent"
|
|
_, storeErr := th.App.Srv().Store().Recap().SaveRecap(&model.Recap{
|
|
Id: recapID,
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Malformed recap",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
Status: model.RecapStatusProcessing,
|
|
BotID: agentID,
|
|
})
|
|
require.NoError(t, storeErr)
|
|
|
|
result, err := th.App.ProcessRecapChannel(ctx, recapID, channel.Id, th.BasicUser.Id, agentID)
|
|
require.NotNil(t, err)
|
|
assert.Equal(t, "app.ai.summarize.parse_failed", err.Id)
|
|
assert.False(t, result.Success)
|
|
|
|
recapChannels, storeErr := th.App.Srv().Store().Recap().GetRecapChannelsByRecapId(recapID)
|
|
require.NoError(t, storeErr)
|
|
require.Len(t, recapChannels, 1)
|
|
assert.Equal(t, channel.Id, recapChannels[0].ChannelId)
|
|
assert.Empty(t, recapChannels[0].Highlights)
|
|
assert.Empty(t, recapChannels[0].ActionItems)
|
|
assert.Len(t, recapChannels[0].SourcePostIds, 1)
|
|
})
|
|
|
|
t.Run("denies channel when user lacks read permission", func(t *testing.T) {
|
|
bridge := &testAgentsBridge{
|
|
completeFn: func(sessionUserID, agentID string, req BridgeCompletionRequest) (string, error) {
|
|
require.Fail(t, "bridge should not be called when user lacks channel access")
|
|
return "", nil
|
|
},
|
|
}
|
|
|
|
th := Setup(t, WithAgentsBridge(bridge)).InitBasic(t)
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.EnableAIRecaps = true })
|
|
|
|
// Private channel owned by another user; BasicUser is not a member.
|
|
channel := th.CreatePrivateChannel(t, th.BasicTeam, func(c *model.Channel) {
|
|
c.CreatorId = th.BasicUser2.Id
|
|
})
|
|
th.CreatePost(t, channel, func(p *model.Post) { p.UserId = th.BasicUser2.Id })
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
recapID := model.NewId()
|
|
agentID := "test-agent"
|
|
_, storeErr := th.App.Srv().Store().Recap().SaveRecap(&model.Recap{
|
|
Id: recapID,
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Unauthorized recap",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
Status: model.RecapStatusProcessing,
|
|
BotID: agentID,
|
|
})
|
|
require.NoError(t, storeErr)
|
|
|
|
result, err := th.App.ProcessRecapChannel(ctx, recapID, channel.Id, th.BasicUser.Id, agentID)
|
|
require.NotNil(t, err)
|
|
assert.Equal(t, "app.recap.permission_denied", err.Id)
|
|
require.NotNil(t, result)
|
|
assert.False(t, result.Success)
|
|
assert.Empty(t, bridge.completeCalls)
|
|
|
|
recapChannels, storeErr := th.App.Srv().Store().Recap().GetRecapChannelsByRecapId(recapID)
|
|
require.NoError(t, storeErr)
|
|
assert.Empty(t, recapChannels)
|
|
})
|
|
|
|
t.Run("max posts per day prevents additional post processing", func(t *testing.T) {
|
|
bridge := &testAgentsBridge{
|
|
completeFn: func(sessionUserID, agentID string, req BridgeCompletionRequest) (string, error) {
|
|
require.Fail(t, "bridge should not be called when post usage is exhausted")
|
|
return "", nil
|
|
},
|
|
}
|
|
|
|
th := Setup(t, WithAgentsBridge(bridge)).InitBasic(t)
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.FeatureFlags.EnableAIRecaps = true
|
|
cfg.AIRecapSettings.EnforcePostsPerDay = model.NewPointer(true)
|
|
cfg.AIRecapSettings.DefaultLimits.MaxPostsPerDay = model.NewPointer(1)
|
|
})
|
|
|
|
existingRecap := &model.Recap{
|
|
Id: model.NewId(),
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Existing usage",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
TotalMessageCount: 1,
|
|
Status: model.RecapStatusCompleted,
|
|
BotID: "test-agent",
|
|
}
|
|
_, storeErr := th.App.Srv().Store().Recap().SaveRecap(existingRecap)
|
|
require.NoError(t, storeErr)
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.CreatePost(t, channel)
|
|
|
|
recapID := model.NewId()
|
|
agentID := "test-agent"
|
|
_, storeErr = th.App.Srv().Store().Recap().SaveRecap(&model.Recap{
|
|
Id: recapID,
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Limited recap",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
Status: model.RecapStatusProcessing,
|
|
BotID: agentID,
|
|
})
|
|
require.NoError(t, storeErr)
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
result, err := th.App.ProcessRecapChannel(ctx, recapID, channel.Id, th.BasicUser.Id, agentID)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, result)
|
|
assert.True(t, result.Success)
|
|
assert.Equal(t, 0, result.MessageCount)
|
|
assert.Empty(t, bridge.completeCalls)
|
|
|
|
recapChannels, storeErr := th.App.Srv().Store().Recap().GetRecapChannelsByRecapId(recapID)
|
|
require.NoError(t, storeErr)
|
|
require.Len(t, recapChannels, 1)
|
|
assert.Empty(t, recapChannels[0].SourcePostIds)
|
|
})
|
|
}
|
|
|
|
func TestProcessRecapChannelTokenLimit(t *testing.T) {
|
|
t.Setenv("MM_FEATUREFLAGS_ENABLEAIRECAPS", "true")
|
|
|
|
// 400 chars => ~100 estimated tokens per post; 5 posts => ~500 tokens.
|
|
longMessage := strings.Repeat("x", 400)
|
|
const postCount = 5
|
|
|
|
tests := []struct {
|
|
name string
|
|
enforceTokens bool
|
|
maxTokens int
|
|
wantMessageCount int
|
|
}{
|
|
{
|
|
name: "token limit reduces posts sent to LLM",
|
|
enforceTokens: true,
|
|
maxTokens: 150, // room for a single ~100-token post
|
|
wantMessageCount: 1,
|
|
},
|
|
{
|
|
name: "no enforcement keeps every post",
|
|
enforceTokens: false,
|
|
maxTokens: 150,
|
|
wantMessageCount: postCount,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
bridge := &testAgentsBridge{
|
|
completeFn: func(sessionUserID, agentID string, req BridgeCompletionRequest) (string, error) {
|
|
return `{"highlights":["h"],"action_items":["a"]}`, nil
|
|
},
|
|
}
|
|
|
|
th := Setup(t, WithAgentsBridge(bridge)).InitBasic(t)
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.FeatureFlags.EnableAIRecaps = true
|
|
cfg.AIRecapSettings.EnforceTokensPerRecap = model.NewPointer(tc.enforceTokens)
|
|
cfg.AIRecapSettings.DefaultLimits.MaxTokensPerRecap = model.NewPointer(tc.maxTokens)
|
|
})
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
for range postCount {
|
|
th.CreateMessagePost(t, channel, longMessage)
|
|
}
|
|
|
|
ctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id})
|
|
recapID := model.NewId()
|
|
agentID := "test-agent"
|
|
_, storeErr := th.App.Srv().Store().Recap().SaveRecap(&model.Recap{
|
|
Id: recapID,
|
|
UserId: th.BasicUser.Id,
|
|
Title: "Token limit recap",
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
Status: model.RecapStatusProcessing,
|
|
BotID: agentID,
|
|
})
|
|
require.NoError(t, storeErr)
|
|
|
|
result, err := th.App.ProcessRecapChannel(ctx, recapID, channel.Id, th.BasicUser.Id, agentID)
|
|
require.Nil(t, err)
|
|
require.True(t, result.Success)
|
|
assert.Equal(t, tc.wantMessageCount, result.MessageCount)
|
|
|
|
recapChannels, storeErr := th.App.Srv().Store().Recap().GetRecapChannelsByRecapId(recapID)
|
|
require.NoError(t, storeErr)
|
|
require.Len(t, recapChannels, 1)
|
|
assert.Len(t, recapChannels[0].SourcePostIds, tc.wantMessageCount)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRecapFetchStartAt(t *testing.T) {
|
|
now := time.Date(2026, time.April, 28, 12, 0, 0, 0, time.UTC)
|
|
lastViewedAt := now.Add(-2 * time.Hour).UnixMilli()
|
|
|
|
startAt, allowFallback := recapFetchStartAt("", lastViewedAt, now)
|
|
assert.Equal(t, lastViewedAt, startAt)
|
|
assert.True(t, allowFallback)
|
|
|
|
startAt, allowFallback = recapFetchStartAt(model.TimePeriodSinceLastRead, lastViewedAt, now)
|
|
assert.Equal(t, lastViewedAt, startAt)
|
|
assert.True(t, allowFallback)
|
|
|
|
startAt, allowFallback = recapFetchStartAt(model.TimePeriodLast24h, lastViewedAt, now)
|
|
assert.Equal(t, now.Add(-24*time.Hour).UnixMilli(), startAt)
|
|
assert.False(t, allowFallback)
|
|
|
|
startAt, allowFallback = recapFetchStartAt(model.TimePeriodLastWeek, lastViewedAt, now)
|
|
assert.Equal(t, now.Add(-7*24*time.Hour).UnixMilli(), startAt)
|
|
assert.False(t, allowFallback)
|
|
}
|
|
|
|
func TestExtractPostIDs(t *testing.T) {
|
|
t.Run("extract post IDs from posts", func(t *testing.T) {
|
|
posts := []*model.Post{
|
|
{Id: "post1", Message: "test1"},
|
|
{Id: "post2", Message: "test2"},
|
|
{Id: "post3", Message: "test3"},
|
|
}
|
|
|
|
ids := extractPostIDs(posts)
|
|
assert.Len(t, ids, 3)
|
|
assert.Equal(t, "post1", ids[0])
|
|
assert.Equal(t, "post2", ids[1])
|
|
assert.Equal(t, "post3", ids[2])
|
|
})
|
|
|
|
t.Run("extract from empty posts", func(t *testing.T) {
|
|
posts := []*model.Post{}
|
|
ids := extractPostIDs(posts)
|
|
assert.Len(t, ids, 0)
|
|
})
|
|
}
|
|
|
|
func TestEstimateTokens(t *testing.T) {
|
|
t.Run("empty string", func(t *testing.T) {
|
|
tokens := estimateTokens("")
|
|
assert.Equal(t, 0, tokens)
|
|
})
|
|
|
|
t.Run("short text", func(t *testing.T) {
|
|
// 4 chars = 1 token (ceiling)
|
|
tokens := estimateTokens("test")
|
|
assert.Equal(t, 1, tokens)
|
|
})
|
|
|
|
t.Run("longer text", func(t *testing.T) {
|
|
// 20 chars -> (20+3)/4 = 5 tokens (ceiling division)
|
|
tokens := estimateTokens("12345678901234567890")
|
|
assert.Equal(t, 5, tokens)
|
|
})
|
|
|
|
t.Run("conservative ceiling division", func(t *testing.T) {
|
|
// 5 chars -> (5+3)/4 = 2 tokens
|
|
tokens := estimateTokens("hello")
|
|
assert.Equal(t, 2, tokens)
|
|
})
|
|
}
|
|
|
|
func TestEstimatePostTokens(t *testing.T) {
|
|
t.Run("estimates tokens from post message", func(t *testing.T) {
|
|
post := &model.Post{Message: "Hello world from Hanzo Team"} // 27 chars
|
|
tokens := estimatePostTokens(post)
|
|
// (27+3)/4 = 7 tokens
|
|
assert.Equal(t, 7, tokens)
|
|
})
|
|
}
|
|
|
|
func TestTrimPostsToTokenLimit(t *testing.T) {
|
|
// 40 chars => (40+3)/4 = 10 tokens per post.
|
|
msg40 := strings.Repeat("x", 40)
|
|
makePosts := func(ids ...string) []*model.Post {
|
|
posts := make([]*model.Post, len(ids))
|
|
for i, id := range ids {
|
|
posts[i] = &model.Post{Id: id, Message: msg40}
|
|
}
|
|
return posts
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
posts []*model.Post
|
|
maxTokens int
|
|
wantIDs []string
|
|
wantTrimmed bool
|
|
}{
|
|
{
|
|
name: "no trim when under limit",
|
|
posts: makePosts("a", "b"),
|
|
maxTokens: 1000,
|
|
wantIDs: []string{"a", "b"},
|
|
wantTrimmed: false,
|
|
},
|
|
{
|
|
name: "keeps newest posts that fit",
|
|
posts: makePosts("newest", "middle", "oldest"),
|
|
maxTokens: 25, // room for 2 posts (20 tokens), not 3 (30 tokens)
|
|
wantIDs: []string{"newest", "middle"},
|
|
wantTrimmed: true,
|
|
},
|
|
{
|
|
name: "drops all when first post exceeds limit",
|
|
posts: makePosts("a", "b"),
|
|
maxTokens: 5, // single post is 10 tokens
|
|
wantIDs: []string{},
|
|
wantTrimmed: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, trimmed := trimPostsToTokenLimit(tc.posts, tc.maxTokens)
|
|
assert.Equal(t, tc.wantTrimmed, trimmed)
|
|
gotIDs := make([]string, len(got))
|
|
for i, p := range got {
|
|
gotIDs[i] = p.Id
|
|
}
|
|
assert.Equal(t, tc.wantIDs, gotIDs)
|
|
})
|
|
}
|
|
}
|