mirror of
https://github.com/hanzoai/team.git
synced 2026-10-30 14:49: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.
259 lines
8.7 KiB
Go
259 lines
8.7 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package extract_content
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
"github.com/hanzoteam/server/server/public/shared/mlog"
|
|
"github.com/hanzoteam/server/server/public/shared/request"
|
|
"github.com/hanzoteam/server/server/v8/channels/jobs"
|
|
"github.com/hanzoteam/server/server/v8/channels/store/storetest"
|
|
"github.com/hanzoteam/server/server/v8/channels/utils/testutils"
|
|
)
|
|
|
|
type trackingApp struct {
|
|
errOn map[string]error
|
|
calls []string
|
|
}
|
|
|
|
func (a *trackingApp) ExtractContentFromFileInfo(_ request.CTX, fileInfo *model.FileInfo) error {
|
|
a.calls = append(a.calls, fileInfo.Id)
|
|
if err, ok := a.errOn[fileInfo.Id]; ok {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func makeTestJobServer(t *testing.T) (*jobs.JobServer, *storetest.Store) {
|
|
t.Helper()
|
|
|
|
mockStore := &storetest.Store{}
|
|
t.Cleanup(func() {
|
|
mockStore.AssertExpectations(t)
|
|
})
|
|
|
|
jobServer := jobs.NewJobServer(
|
|
&testutils.StaticConfigService{},
|
|
mockStore,
|
|
nil,
|
|
mlog.CreateConsoleTestLogger(t),
|
|
nil,
|
|
)
|
|
|
|
return jobServer, mockStore
|
|
}
|
|
|
|
func expectJobDataUpdate(mockStore *storetest.Store) {
|
|
mockStore.JobStore.On("UpdateOptimistically", mock.AnythingOfType("*model.Job"), model.JobStatusInProgress).Return(nil, nil)
|
|
}
|
|
|
|
func expectWorkerJobCompletion(mockStore *storetest.Store, job *model.Job) {
|
|
claimed := *job
|
|
claimed.Status = model.JobStatusInProgress
|
|
mockStore.JobStore.On("UpdateStatusOptimistically", job.Id, model.JobStatusPending, model.JobStatusInProgress).Return(&claimed, nil)
|
|
mockStore.JobStore.On("UpdateStatus", job.Id, model.JobStatusSuccess).Return(&claimed, nil)
|
|
}
|
|
|
|
func makeFileInfo(id string, createAt int64, ext string) *model.FileInfo {
|
|
return &model.FileInfo{
|
|
Id: id,
|
|
CreateAt: createAt,
|
|
Extension: ext,
|
|
Name: "file." + ext,
|
|
Path: "path/" + id,
|
|
}
|
|
}
|
|
|
|
func makeFileInfoBatch(n int, startCreateAt int64) []*model.FileInfo {
|
|
batch := make([]*model.FileInfo, n)
|
|
for i := range n {
|
|
batch[i] = makeFileInfo(model.NewId(), startCreateAt+int64(i), "pdf")
|
|
}
|
|
return batch
|
|
}
|
|
|
|
func TestRunCatchupExtraction(t *testing.T) {
|
|
logger := mlog.CreateConsoleTestLogger(t)
|
|
|
|
t.Run("extracts non-ignored files and passes OnlyEmptyContent filter", func(t *testing.T) {
|
|
jobServer, mockStore := makeTestJobServer(t)
|
|
expectJobDataUpdate(mockStore)
|
|
|
|
pdf := makeFileInfo("pdf1", 1000, "pdf")
|
|
png := makeFileInfo("png1", 1001, "png")
|
|
docx := makeFileInfo("docx1", 1002, "docx")
|
|
|
|
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.MatchedBy(func(opt *model.GetFileInfosOptions) bool {
|
|
return opt.OnlyEmptyContent && !opt.IncludeDeleted && opt.SortBy == model.FileinfoSortByCreated
|
|
})).Return([]*model.FileInfo{pdf, png, docx}, nil).Once()
|
|
|
|
app := &trackingApp{}
|
|
job := &model.Job{Data: map[string]string{}}
|
|
|
|
err := runCatchupExtraction(logger, job, jobServer, app, mockStore)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, []string{"pdf1", "docx1"}, app.calls)
|
|
require.Equal(t, "2", job.Data["processed"])
|
|
require.Equal(t, "0", job.Data["errors"])
|
|
})
|
|
|
|
t.Run("empty result is a no-op", func(t *testing.T) {
|
|
jobServer, mockStore := makeTestJobServer(t)
|
|
expectJobDataUpdate(mockStore)
|
|
|
|
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.Anything).Return(nil, nil).Once()
|
|
|
|
job := &model.Job{Data: map[string]string{}}
|
|
err := runCatchupExtraction(logger, job, jobServer, &trackingApp{}, mockStore)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "0", job.Data["processed"])
|
|
require.Equal(t, "0", job.Data["errors"])
|
|
})
|
|
|
|
t.Run("full batch advances cursor and fetches the next page", func(t *testing.T) {
|
|
jobServer, mockStore := makeTestJobServer(t)
|
|
expectJobDataUpdate(mockStore)
|
|
|
|
first := makeFileInfoBatch(catchupBatchSize, 1000)
|
|
second := []*model.FileInfo{makeFileInfo("tail1", 2000, "pdf"), makeFileInfo("tail2", 2001, "pdf")}
|
|
|
|
var firstPageOpts *model.GetFileInfosOptions
|
|
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.MatchedBy(func(opt *model.GetFileInfosOptions) bool {
|
|
return opt.OnlyEmptyContent
|
|
})).Run(func(args mock.Arguments) {
|
|
opt := args.Get(2).(*model.GetFileInfosOptions)
|
|
copied := *opt
|
|
firstPageOpts = &copied
|
|
}).Return(first, nil).Once()
|
|
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.MatchedBy(func(opt *model.GetFileInfosOptions) bool {
|
|
return opt.Since == 1999+1
|
|
})).Return(second, nil).Once()
|
|
|
|
sinceLower := model.GetMillis() - catchupLookback.Milliseconds()
|
|
app := &trackingApp{}
|
|
job := &model.Job{Data: map[string]string{}}
|
|
|
|
err := runCatchupExtraction(logger, job, jobServer, app, mockStore)
|
|
sinceUpper := model.GetMillis() - catchupLookback.Milliseconds()
|
|
require.NoError(t, err)
|
|
|
|
require.NotNil(t, firstPageOpts)
|
|
require.GreaterOrEqual(t, firstPageOpts.Since, sinceLower)
|
|
require.LessOrEqual(t, firstPageOpts.Since, sinceUpper)
|
|
require.Len(t, app.calls, catchupBatchSize+2)
|
|
require.Equal(t, "1002", job.Data["processed"])
|
|
})
|
|
|
|
t.Run("partial batch terminates after one fetch", func(t *testing.T) {
|
|
jobServer, mockStore := makeTestJobServer(t)
|
|
expectJobDataUpdate(mockStore)
|
|
|
|
batch := []*model.FileInfo{
|
|
makeFileInfo("f1", 1000, "pdf"),
|
|
makeFileInfo("f2", 1001, "pdf"),
|
|
makeFileInfo("f3", 1002, "pdf"),
|
|
}
|
|
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.Anything).Return(batch, nil).Once()
|
|
|
|
job := &model.Job{Data: map[string]string{}}
|
|
err := runCatchupExtraction(logger, job, jobServer, &trackingApp{}, mockStore)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "3", job.Data["processed"])
|
|
})
|
|
|
|
t.Run("accumulates extraction errors in job data", func(t *testing.T) {
|
|
jobServer, mockStore := makeTestJobServer(t)
|
|
expectJobDataUpdate(mockStore)
|
|
|
|
okFile := makeFileInfo("ok", 1000, "pdf")
|
|
failFile := makeFileInfo("fail", 1001, "pdf")
|
|
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.Anything).
|
|
Return([]*model.FileInfo{okFile, failFile}, nil).Once()
|
|
|
|
app := &trackingApp{errOn: map[string]error{"fail": errors.New("extract failed")}}
|
|
job := &model.Job{Data: map[string]string{}}
|
|
|
|
err := runCatchupExtraction(logger, job, jobServer, app, mockStore)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "2", job.Data["processed"])
|
|
require.Equal(t, "1", job.Data["errors"])
|
|
})
|
|
|
|
t.Run("store error propagates", func(t *testing.T) {
|
|
jobServer, mockStore := makeTestJobServer(t)
|
|
|
|
wantErr := errors.New("store failed")
|
|
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.Anything).Return(nil, wantErr).Once()
|
|
|
|
job := &model.Job{Data: map[string]string{}}
|
|
err := runCatchupExtraction(logger, job, jobServer, &trackingApp{}, mockStore)
|
|
require.ErrorIs(t, err, wantErr)
|
|
})
|
|
}
|
|
|
|
func TestWorkerDispatch(t *testing.T) {
|
|
t.Run("catchup job uses OnlyEmptyContent filter", func(t *testing.T) {
|
|
jobServer, mockStore := makeTestJobServer(t)
|
|
expectJobDataUpdate(mockStore)
|
|
|
|
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.MatchedBy(func(opt *model.GetFileInfosOptions) bool {
|
|
return opt.OnlyEmptyContent
|
|
})).Return(nil, nil).Once()
|
|
|
|
worker := MakeWorker(jobServer, &trackingApp{}, mockStore)
|
|
job := &model.Job{Id: model.NewId(), Data: map[string]string{catchupJobDataKey: "true"}}
|
|
expectWorkerJobCompletion(mockStore, job)
|
|
|
|
worker.DoJob(job)
|
|
})
|
|
|
|
t.Run("range job does not use OnlyEmptyContent filter", func(t *testing.T) {
|
|
jobServer, mockStore := makeTestJobServer(t)
|
|
expectJobDataUpdate(mockStore)
|
|
|
|
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.MatchedBy(func(opt *model.GetFileInfosOptions) bool {
|
|
return !opt.OnlyEmptyContent
|
|
})).Return(nil, nil).Once()
|
|
|
|
worker := MakeWorker(jobServer, &trackingApp{}, mockStore)
|
|
job := &model.Job{Id: model.NewId(), Data: map[string]string{}}
|
|
expectWorkerJobCompletion(mockStore, job)
|
|
|
|
worker.DoJob(job)
|
|
})
|
|
}
|
|
|
|
func TestSchedulerScheduleJob(t *testing.T) {
|
|
logger := mlog.CreateConsoleTestLogger(t)
|
|
jobServer, mockStore := makeTestJobServer(t)
|
|
|
|
app := &trackingApp{}
|
|
jobServer.RegisterJobType(model.JobTypeExtractContent, MakeWorker(jobServer, app, mockStore), nil)
|
|
|
|
savedJob := &model.Job{
|
|
Id: model.NewId(),
|
|
Type: model.JobTypeExtractContent,
|
|
Data: map[string]string{catchupJobDataKey: "true"},
|
|
}
|
|
mockStore.JobStore.On("Save", mock.MatchedBy(func(job *model.Job) bool {
|
|
return job.Type == model.JobTypeExtractContent && job.Data[catchupJobDataKey] == "true"
|
|
})).Return(savedJob, nil)
|
|
|
|
scheduler := MakeScheduler(jobServer)
|
|
job, appErr := scheduler.ScheduleJob(request.EmptyContext(logger), &model.Config{}, false, nil)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, job)
|
|
require.Equal(t, "true", job.Data[catchupJobDataKey])
|
|
}
|