mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* allow workflow_dispatch trigger for Server CI (for plugins CI) * [MM-68402] MBE Phase 2: declare four generic plugin hooks (#36291) * new hooks-only phase 2 * remove ChannelWillBeMoved * remove RecapWillBeProcessed and MessageWillBeRewrittenByAI Drop the AI/recap hooks from the new-hook surface; AI-LLM paths remain uncovered in tech preview and are documented as residuals. * [MM-68403] MBE Phase 3: ChannelGuards primitive (storage + cache + plugin API) (#36365) * phase 3 * phase 3: register ChannelGuard mock in test setup helper NewChannels' startup-time call to reloadGuardCache invokes s.ChannelGuard().GetAll(); without an expectation on the mock store, every test that sets up the server with GetMockStoreForSetupFunctions panics during init. * phase 3: register ChannelGuard mock in retrylayer test retrylayer.New walks every store getter to wrap it; without the mock expectation on ChannelGuard, TestRetry panics during layer construction. * use rctx properly in the store methods * phase 3: match rctx arg in testlib ChannelGuard mock GetAll now takes request.CTX, so the testify expectation must include mock.Anything; otherwise the call panics under the mocked store. * phase 3: set api.ctx in TestChannelGuardLowercaseNormalization The test constructs PluginAPI directly without a ctx, which used to work when App.RegisterChannelGuard built its own EmptyContext. Now that the App methods take rctx from the caller, the nil ctx panics inside RequestContextWithMaster. * [MM-68404] MBE Phase 4: App-layer plugin hook wiring (#36407) * phase 4 * Fix nil rctx in TestChannelGuardLowercaseNormalization The PluginAPI struct literal was missing ctx: rctx after a refactor moved the rctx declaration below the struct construction, leaving api.ctx as nil. This caused a nil pointer dereference in reloadGuardCache when RegisterChannelGuard called store.RequestContextWithMaster(nil). * Remove ChannelWillBeMoved hook call from MoveChannel (phase 4) The hook and its ID were removed from mbe-phase-2 but the call site in MoveChannel and its i18n string were not cleaned up during the rebase. * remove channel will be moved test * Remove RecapWillBeProcessed and MessageWillBeRewrittenByAI hook calls (phase 4) The hooks and their IDs were removed from mbe-phase-2 but the call sites in ProcessRecapChannel and RewriteMessage, their i18n strings, and their tests were not cleaned up during the rebase. * Revert channel_id plumbing on rewrite endpoint (phase 4) The channel_id field on RewriteRequest was added in phase 4 to feed the synthetic post passed to MessageWillBeRewrittenByAI. With that hook removed from mbe-phase-2, channel_id has no consumer; revert the field, the api4 validation, the app.RewriteMessage parameter, and the corresponding webapp client + hook plumbing. * [MM-68555] MBE Phase 5: Channel-guard enforcement + two-phase dispatch (#36473) * phase 5 * Bake plugin counter-file paths into source instead of env vars t.Setenv panics when an ancestor test calls t.Parallel, so the two channel-guard tests broke under ENABLE_FULLY_PARALLEL_TESTS in CI. Build each plugin source per-subtest with its temp file path embedded as a Go literal — same pattern as TestPluginUploadsAPI. * Remove guarded helpers and tests for dropped hooks (phase 5) The runGuardedRecapWillBeProcessed and runGuardedMessageWillBeRewrittenByAI helpers were never wired (their app-layer call sites were already removed in the phase-4 cleanup), and the corresponding sub-tests across panic / allow / reject / partial plugins reference hooks that no longer exist. * [MM-68405] MBE Phase 6: fire MessagesWillBeConsumed on the edit path (#36475) --------- --------- --------- --------- * rebase onto master --------- Co-authored-by: Hanzo Dev <[email protected]>
145 lines
4.4 KiB
Go
145 lines
4.4 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package plugin
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/plugin/utils"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
)
|
|
|
|
func TestRunMultiPluginHookWithRPCErr(t *testing.T) {
|
|
pluginDir, err := os.MkdirTemp("", "mm-rpcerr-plugin")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { os.RemoveAll(pluginDir) })
|
|
webappPluginDir, err := os.MkdirTemp("", "mm-rpcerr-webapp")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { os.RemoveAll(webappPluginDir) })
|
|
|
|
pluginID1 := "test-rpc-err-plugin"
|
|
pluginID2 := "test-rpc-err-plugin-2"
|
|
require.NoError(t, os.MkdirAll(filepath.Join(pluginDir, pluginID1), 0700))
|
|
require.NoError(t, os.MkdirAll(filepath.Join(pluginDir, pluginID2), 0700))
|
|
backend1 := filepath.Join(pluginDir, pluginID1, "backend.exe")
|
|
backend2 := filepath.Join(pluginDir, pluginID2, "backend.exe")
|
|
|
|
utils.CompileGo(t, `
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) MessageHasBeenPosted(c *plugin.Context, post *model.Post) {}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`, backend1)
|
|
copyExecutable(t, backend1, backend2)
|
|
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(pluginDir, pluginID1, "plugin.json"),
|
|
[]byte(`{"id":"`+pluginID1+`","server":{"executable":"backend.exe"}}`),
|
|
0600,
|
|
))
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(pluginDir, pluginID2, "plugin.json"),
|
|
[]byte(`{"id":"`+pluginID2+`","server":{"executable":"backend.exe"}}`),
|
|
0600,
|
|
))
|
|
|
|
logger := mlog.CreateConsoleTestLogger(t)
|
|
apiImpl := func(*model.Manifest) API { return nil }
|
|
env, err := NewEnvironment(apiImpl, nil, pluginDir, webappPluginDir, logger, nil)
|
|
require.NoError(t, err)
|
|
t.Cleanup(env.Shutdown)
|
|
|
|
_, _, err = env.Activate(pluginID1)
|
|
require.NoError(t, err)
|
|
require.True(t, env.IsActive(pluginID1))
|
|
_, _, err = env.Activate(pluginID2)
|
|
require.NoError(t, err)
|
|
require.True(t, env.IsActive(pluginID2))
|
|
|
|
t.Run("both plugins healthy - closure invoked once per plugin", func(t *testing.T) {
|
|
seen := map[string]int{}
|
|
runErr := env.RunMultiPluginHookWithRPCErr(func(hooks HooksWithRPCErr, manifest *model.Manifest) (bool, error) {
|
|
seen[manifest.Id]++
|
|
require.NoError(t, hooks.MessageHasBeenPostedWithRPCErr(&Context{}, &model.Post{}))
|
|
return true, nil
|
|
}, MessageHasBeenPostedID)
|
|
require.NoError(t, runErr)
|
|
require.Equal(t, map[string]int{pluginID1: 1, pluginID2: 1}, seen)
|
|
})
|
|
|
|
t.Run("closure error propagates and stops iteration", func(t *testing.T) {
|
|
sentinel := errors.New("from closure")
|
|
var calls int
|
|
runErr := env.RunMultiPluginHookWithRPCErr(func(_ HooksWithRPCErr, _ *model.Manifest) (bool, error) {
|
|
calls++
|
|
return true, sentinel
|
|
}, MessageHasBeenPostedID)
|
|
require.ErrorIs(t, runErr, sentinel)
|
|
require.Equal(t, 1, calls)
|
|
})
|
|
|
|
t.Run("hook id not implemented - closure never invoked", func(t *testing.T) {
|
|
var calls int
|
|
runErr := env.RunMultiPluginHookWithRPCErr(func(_ HooksWithRPCErr, _ *model.Manifest) (bool, error) {
|
|
calls++
|
|
return true, nil
|
|
}, MessageHasBeenUpdatedID)
|
|
require.NoError(t, runErr)
|
|
require.Equal(t, 0, calls)
|
|
})
|
|
|
|
t.Run("rpc transport error surfaces after plugin process dies", func(t *testing.T) {
|
|
rp, ok := env.registeredPlugins.Load(pluginID1)
|
|
require.True(t, ok)
|
|
sup := rp.(registeredPlugin).supervisor
|
|
require.NotNil(t, sup)
|
|
sup.client.Kill()
|
|
|
|
// Give the rpc client a moment to notice the dead connection.
|
|
require.Eventually(t, func() bool {
|
|
var rpcErr error
|
|
_ = env.RunMultiPluginHookWithRPCErr(func(hooks HooksWithRPCErr, manifest *model.Manifest) (bool, error) {
|
|
if manifest.Id != pluginID1 {
|
|
return true, nil
|
|
}
|
|
rpcErr = hooks.MessageHasBeenPostedWithRPCErr(&Context{}, &model.Post{})
|
|
return true, nil
|
|
}, MessageHasBeenPostedID)
|
|
return rpcErr != nil
|
|
}, 2*time.Second, 50*time.Millisecond)
|
|
})
|
|
}
|
|
|
|
func copyExecutable(t *testing.T, src, dst string) {
|
|
t.Helper()
|
|
in, err := os.Open(src)
|
|
require.NoError(t, err)
|
|
defer in.Close()
|
|
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700)
|
|
require.NoError(t, err)
|
|
defer out.Close()
|
|
_, err = io.Copy(out, in)
|
|
require.NoError(t, err)
|
|
}
|