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]>
278 lines
7.5 KiB
Go
278 lines
7.5 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/format"
|
|
"go/parser"
|
|
"go/token"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"slices"
|
|
"strings"
|
|
"text/template"
|
|
)
|
|
|
|
const (
|
|
ErrorType = "error"
|
|
)
|
|
|
|
func isError(typeName string) bool {
|
|
return strings.Contains(typeName, ErrorType)
|
|
}
|
|
|
|
func main() {
|
|
if err := buildTimerLayer(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if err := buildRetryLayer(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func buildRetryLayer() error {
|
|
code, err := generateLayer("RetryLayer", "retry_layer.go.tmpl")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
formatedCode, err := format.Source(code)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(path.Join("retrylayer/retrylayer.go"), formatedCode, 0644)
|
|
}
|
|
|
|
func buildTimerLayer() error {
|
|
code, err := generateLayer("TimerLayer", "timer_layer.go.tmpl")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
formatedCode, err := format.Source(code)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(path.Join("timerlayer", "timerlayer.go"), formatedCode, 0644)
|
|
}
|
|
|
|
type methodParam struct {
|
|
Name string
|
|
Type string
|
|
}
|
|
|
|
type methodData struct {
|
|
Params []methodParam
|
|
Results []string
|
|
}
|
|
|
|
type subStore struct {
|
|
Methods map[string]methodData
|
|
}
|
|
|
|
type storeMetadata struct {
|
|
Name string
|
|
SubStores map[string]subStore
|
|
Methods map[string]methodData
|
|
}
|
|
|
|
func extractMethodMetadata(method *ast.Field, src []byte) methodData {
|
|
params := []methodParam{}
|
|
results := []string{}
|
|
ast.Inspect(method.Type, func(expr ast.Node) bool {
|
|
switch e := expr.(type) {
|
|
case *ast.FuncType:
|
|
if e.Params != nil {
|
|
for _, param := range e.Params.List {
|
|
for _, paramName := range param.Names {
|
|
params = append(params, methodParam{Name: paramName.Name, Type: string(src[param.Type.Pos()-1 : param.Type.End()-1])})
|
|
}
|
|
}
|
|
}
|
|
if e.Results != nil {
|
|
for _, result := range e.Results.List {
|
|
results = append(results, string(src[result.Type.Pos()-1:result.Type.End()-1]))
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
return methodData{Params: params, Results: results}
|
|
}
|
|
|
|
func extractStoreMetadata() (*storeMetadata, error) {
|
|
// Create the AST by parsing src.
|
|
fset := token.NewFileSet() // positions are relative to fset
|
|
|
|
file, err := os.Open("store.go")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to open store/store.go file: %w", err)
|
|
}
|
|
src, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
file.Close()
|
|
f, err := parser.ParseFile(fset, "", src, parser.AllErrors|parser.ParseComments)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
topLevelFunctions := map[string]bool{
|
|
"MarkSystemRanUnitTests": false,
|
|
"Close": false,
|
|
"LockToMaster": false,
|
|
"UnlockFromMaster": false,
|
|
"DropAllTables": false,
|
|
"TotalMasterDbConnections": true,
|
|
"TotalReadDbConnections": true,
|
|
"SetContext": true,
|
|
"TotalSearchDbConnections": true,
|
|
"GetCurrentSchemaVersion": true,
|
|
}
|
|
|
|
metadata := storeMetadata{Methods: map[string]methodData{}, SubStores: map[string]subStore{}}
|
|
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
switch x := n.(type) {
|
|
case *ast.TypeSpec:
|
|
if x.Name.Name == "Store" {
|
|
for _, method := range x.Type.(*ast.InterfaceType).Methods.List {
|
|
methodName := method.Names[0].Name
|
|
if _, ok := topLevelFunctions[methodName]; ok {
|
|
metadata.Methods[methodName] = extractMethodMetadata(method, src)
|
|
}
|
|
}
|
|
} else if subStoreName, ok := strings.CutSuffix(x.Name.Name, "Store"); ok {
|
|
metadata.SubStores[subStoreName] = subStore{Methods: map[string]methodData{}}
|
|
for _, method := range x.Type.(*ast.InterfaceType).Methods.List {
|
|
methodName := method.Names[0].Name
|
|
metadata.SubStores[subStoreName].Methods[methodName] = extractMethodMetadata(method, src)
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
return &metadata, nil
|
|
}
|
|
|
|
func generateLayer(name, templateFile string) ([]byte, error) {
|
|
out := bytes.NewBufferString("")
|
|
metadata, err := extractStoreMetadata()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
metadata.Name = name
|
|
|
|
myFuncs := template.FuncMap{
|
|
"joinResults": func(results []string) string {
|
|
return strings.Join(results, ", ")
|
|
},
|
|
"joinResultsForSignature": func(results []string) string {
|
|
if len(results) == 0 {
|
|
return ""
|
|
}
|
|
returns := []string{}
|
|
for _, result := range results {
|
|
switch result {
|
|
case "*PostReminderMetadata":
|
|
returns = append(returns, fmt.Sprintf("*store.%s", strings.TrimPrefix(result, "*")))
|
|
case "[]*ChannelGuard":
|
|
returns = append(returns, fmt.Sprintf("[]*store.%s", strings.TrimPrefix(result, "[]*")))
|
|
default:
|
|
returns = append(returns, result)
|
|
}
|
|
}
|
|
|
|
if len(returns) == 1 {
|
|
return strings.Join(returns, ", ")
|
|
}
|
|
return fmt.Sprintf("(%s)", strings.Join(returns, ", "))
|
|
},
|
|
"genResultsVars": func(results []string, withNilError bool) string {
|
|
vars := []string{}
|
|
for i, typeName := range results {
|
|
if isError(typeName) {
|
|
if withNilError {
|
|
vars = append(vars, "nil")
|
|
} else {
|
|
vars = append(vars, "err")
|
|
}
|
|
} else if i == 0 {
|
|
vars = append(vars, "result")
|
|
} else {
|
|
vars = append(vars, fmt.Sprintf("resultVar%d", i))
|
|
}
|
|
}
|
|
return strings.Join(vars, ", ")
|
|
},
|
|
"errorToBoolean": func(results []string) string {
|
|
if slices.ContainsFunc(results, isError) {
|
|
return "err == nil"
|
|
}
|
|
return "true"
|
|
},
|
|
"errorPresent": func(results []string) bool {
|
|
return slices.ContainsFunc(results, isError)
|
|
},
|
|
"errorVar": func(results []string) string {
|
|
if slices.ContainsFunc(results, isError) {
|
|
return "err"
|
|
}
|
|
return ""
|
|
},
|
|
"joinParams": func(params []methodParam) string {
|
|
paramsNames := make([]string, 0, len(params))
|
|
for _, param := range params {
|
|
tParams := ""
|
|
if strings.HasPrefix(param.Type, "...") {
|
|
tParams = "..."
|
|
}
|
|
paramsNames = append(paramsNames, param.Name+tParams)
|
|
}
|
|
return strings.Join(paramsNames, ", ")
|
|
},
|
|
"joinParamsWithType": func(params []methodParam) string {
|
|
paramsWithType := []string{}
|
|
for _, param := range params {
|
|
switch param.Type {
|
|
case "ChannelSearchOpts", "UserGetByIdsOpts", "ThreadMembershipOpts", "GetPolicyOptions":
|
|
paramsWithType = append(paramsWithType, fmt.Sprintf("%s store.%s", param.Name, param.Type))
|
|
case "*UserGetByIdsOpts", "*SidebarCategorySearchOpts", "*ChannelGuard":
|
|
paramsWithType = append(paramsWithType, fmt.Sprintf("%s *store.%s", param.Name, strings.TrimPrefix(param.Type, "*")))
|
|
default:
|
|
paramsWithType = append(paramsWithType, fmt.Sprintf("%s %s", param.Name, param.Type))
|
|
}
|
|
}
|
|
return strings.Join(paramsWithType, ", ")
|
|
},
|
|
"joinParamsWithTypeOutsideStore": func(params []methodParam) string {
|
|
paramsWithType := []string{}
|
|
for _, param := range params {
|
|
switch param.Type {
|
|
case "ChannelSearchOpts", "UserGetByIdsOpts", "ThreadMembershipOpts", "GetPolicyOptions":
|
|
paramsWithType = append(paramsWithType, fmt.Sprintf("%s store.%s", param.Name, param.Type))
|
|
case "*UserGetByIdsOpts", "*SidebarCategorySearchOpts", "*ChannelGuard":
|
|
paramsWithType = append(paramsWithType, fmt.Sprintf("%s *store.%s", param.Name, strings.TrimPrefix(param.Type, "*")))
|
|
default:
|
|
paramsWithType = append(paramsWithType, fmt.Sprintf("%s %s", param.Name, param.Type))
|
|
}
|
|
}
|
|
return strings.Join(paramsWithType, ", ")
|
|
},
|
|
}
|
|
|
|
t := template.Must(template.New(templateFile).Funcs(myFuncs).ParseFiles("layer_generators/" + templateFile))
|
|
if err = t.Execute(out, metadata); err != nil {
|
|
return nil, err
|
|
}
|
|
return out.Bytes(), nil
|
|
}
|