fix: Check patrol role templates per-rig instead of at town level
- PatrolRolesHavePromptsCheck now verifies templates exist in each rig's mayor clone at <rig>/mayor/rig/internal/templates/roles/ - Track missing templates by rig using missingByRig map - Fix copies embedded templates to each rig's location - Add GetAllRoleTemplates helper to templates package - Add tests for no-rigs case and multiple-rigs scenarios
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/steveyegge/gastown/internal/beads"
|
"github.com/steveyegge/gastown/internal/beads"
|
||||||
"github.com/steveyegge/gastown/internal/config"
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
|
"github.com/steveyegge/gastown/internal/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PatrolMoleculesExistCheck verifies that patrol molecules exist for each rig.
|
// PatrolMoleculesExistCheck verifies that patrol molecules exist for each rig.
|
||||||
@@ -392,30 +393,36 @@ func (c *PatrolPluginsAccessibleCheck) Fix(ctx *CheckContext) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PatrolRolesHavePromptsCheck verifies that internal/templates/roles/*.md.tmpl exist for each role.
|
// PatrolRolesHavePromptsCheck verifies that internal/templates/roles/*.md.tmpl exist for each rig.
|
||||||
|
// Checks at <town>/<rig>/mayor/rig/internal/templates/roles/*.md.tmpl
|
||||||
|
// Fix copies embedded templates to missing locations.
|
||||||
type PatrolRolesHavePromptsCheck struct {
|
type PatrolRolesHavePromptsCheck struct {
|
||||||
BaseCheck
|
FixableCheck
|
||||||
|
// missingByRig tracks missing templates per rig: rigName -> []missingFiles
|
||||||
|
missingByRig map[string][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPatrolRolesHavePromptsCheck creates a new patrol roles have prompts check.
|
// NewPatrolRolesHavePromptsCheck creates a new patrol roles have prompts check.
|
||||||
func NewPatrolRolesHavePromptsCheck() *PatrolRolesHavePromptsCheck {
|
func NewPatrolRolesHavePromptsCheck() *PatrolRolesHavePromptsCheck {
|
||||||
return &PatrolRolesHavePromptsCheck{
|
return &PatrolRolesHavePromptsCheck{
|
||||||
BaseCheck: BaseCheck{
|
FixableCheck: FixableCheck{
|
||||||
CheckName: "patrol-roles-have-prompts",
|
BaseCheck: BaseCheck{
|
||||||
CheckDescription: "Check if internal/templates/roles/*.md.tmpl exist for each patrol role",
|
CheckName: "patrol-roles-have-prompts",
|
||||||
|
CheckDescription: "Check if internal/templates/roles/*.md.tmpl exist for each patrol role",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// requiredRolePrompts are the required role prompt template files.
|
|
||||||
var requiredRolePrompts = []string{
|
var requiredRolePrompts = []string{
|
||||||
"deacon.md.tmpl",
|
"deacon.md.tmpl",
|
||||||
"witness.md.tmpl",
|
"witness.md.tmpl",
|
||||||
"refinery.md.tmpl",
|
"refinery.md.tmpl",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run checks if role prompts exist.
|
|
||||||
func (c *PatrolRolesHavePromptsCheck) Run(ctx *CheckContext) *CheckResult {
|
func (c *PatrolRolesHavePromptsCheck) Run(ctx *CheckContext) *CheckResult {
|
||||||
|
c.missingByRig = make(map[string][]string)
|
||||||
|
|
||||||
rigs, err := discoverRigs(ctx.TownRoot)
|
rigs, err := discoverRigs(ctx.TownRoot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &CheckResult{
|
return &CheckResult{
|
||||||
@@ -440,12 +447,17 @@ func (c *PatrolRolesHavePromptsCheck) Run(ctx *CheckContext) *CheckResult {
|
|||||||
mayorRig := filepath.Join(ctx.TownRoot, rigName, "mayor", "rig")
|
mayorRig := filepath.Join(ctx.TownRoot, rigName, "mayor", "rig")
|
||||||
templatesDir := filepath.Join(mayorRig, "internal", "templates", "roles")
|
templatesDir := filepath.Join(mayorRig, "internal", "templates", "roles")
|
||||||
|
|
||||||
|
var rigMissing []string
|
||||||
for _, roleFile := range requiredRolePrompts {
|
for _, roleFile := range requiredRolePrompts {
|
||||||
promptPath := filepath.Join(templatesDir, roleFile)
|
promptPath := filepath.Join(templatesDir, roleFile)
|
||||||
if _, err := os.Stat(promptPath); os.IsNotExist(err) {
|
if _, err := os.Stat(promptPath); os.IsNotExist(err) {
|
||||||
missingPrompts = append(missingPrompts, fmt.Sprintf("%s: %s", rigName, roleFile))
|
missingPrompts = append(missingPrompts, fmt.Sprintf("%s: %s", rigName, roleFile))
|
||||||
|
rigMissing = append(rigMissing, roleFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(rigMissing) > 0 {
|
||||||
|
c.missingByRig[rigName] = rigMissing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(missingPrompts) > 0 {
|
if len(missingPrompts) > 0 {
|
||||||
@@ -454,7 +466,7 @@ func (c *PatrolRolesHavePromptsCheck) Run(ctx *CheckContext) *CheckResult {
|
|||||||
Status: StatusWarning,
|
Status: StatusWarning,
|
||||||
Message: fmt.Sprintf("%d role prompt template(s) missing", len(missingPrompts)),
|
Message: fmt.Sprintf("%d role prompt template(s) missing", len(missingPrompts)),
|
||||||
Details: missingPrompts,
|
Details: missingPrompts,
|
||||||
FixHint: "Role prompt templates should be in the project repository under internal/templates/roles/",
|
FixHint: "Run 'gt doctor --fix' to copy embedded templates to rig repos",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,6 +477,36 @@ func (c *PatrolRolesHavePromptsCheck) Run(ctx *CheckContext) *CheckResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *PatrolRolesHavePromptsCheck) Fix(ctx *CheckContext) error {
|
||||||
|
allTemplates, err := templates.GetAllRoleTemplates()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting embedded templates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for rigName, missingFiles := range c.missingByRig {
|
||||||
|
mayorRig := filepath.Join(ctx.TownRoot, rigName, "mayor", "rig")
|
||||||
|
templatesDir := filepath.Join(mayorRig, "internal", "templates", "roles")
|
||||||
|
|
||||||
|
if err := os.MkdirAll(templatesDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("creating %s: %w", templatesDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, roleFile := range missingFiles {
|
||||||
|
content, ok := allTemplates[roleFile]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
destPath := filepath.Join(templatesDir, roleFile)
|
||||||
|
if err := os.WriteFile(destPath, content, 0644); err != nil {
|
||||||
|
return fmt.Errorf("writing %s in %s: %w", roleFile, rigName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// discoverRigs finds all registered rigs.
|
// discoverRigs finds all registered rigs.
|
||||||
func discoverRigs(townRoot string) ([]string, error) {
|
func discoverRigs(townRoot string) ([]string, error) {
|
||||||
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
|
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
|
||||||
|
|||||||
255
internal/doctor/patrol_check_test.go
Normal file
255
internal/doctor/patrol_check_test.go
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
package doctor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewPatrolRolesHavePromptsCheck(t *testing.T) {
|
||||||
|
check := NewPatrolRolesHavePromptsCheck()
|
||||||
|
if check == nil {
|
||||||
|
t.Fatal("NewPatrolRolesHavePromptsCheck() returned nil")
|
||||||
|
}
|
||||||
|
if check.Name() != "patrol-roles-have-prompts" {
|
||||||
|
t.Errorf("Name() = %q, want %q", check.Name(), "patrol-roles-have-prompts")
|
||||||
|
}
|
||||||
|
if !check.CanFix() {
|
||||||
|
t.Error("CanFix() should return true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupRigConfig(t *testing.T, tmpDir string, rigNames []string) {
|
||||||
|
t.Helper()
|
||||||
|
mayorDir := filepath.Join(tmpDir, "mayor")
|
||||||
|
if err := os.MkdirAll(mayorDir, 0755); err != nil {
|
||||||
|
t.Fatalf("mkdir mayor: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rigsConfig := config.RigsConfig{Rigs: make(map[string]config.RigEntry)}
|
||||||
|
for _, name := range rigNames {
|
||||||
|
rigsConfig.Rigs[name] = config.RigEntry{}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(rigsConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal rigs.json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(filepath.Join(mayorDir, "rigs.json"), data, 0644); err != nil {
|
||||||
|
t.Fatalf("write rigs.json: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupRigTemplatesDir(t *testing.T, tmpDir, rigName string) string {
|
||||||
|
t.Helper()
|
||||||
|
templatesDir := filepath.Join(tmpDir, rigName, "mayor", "rig", "internal", "templates", "roles")
|
||||||
|
if err := os.MkdirAll(templatesDir, 0755); err != nil {
|
||||||
|
t.Fatalf("mkdir templates: %v", err)
|
||||||
|
}
|
||||||
|
return templatesDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatrolRolesHavePromptsCheck_NoRigs(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
check := NewPatrolRolesHavePromptsCheck()
|
||||||
|
ctx := &CheckContext{TownRoot: tmpDir}
|
||||||
|
|
||||||
|
result := check.Run(ctx)
|
||||||
|
|
||||||
|
if result.Status != StatusOK {
|
||||||
|
t.Errorf("Status = %v, want OK (no rigs configured)", result.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatrolRolesHavePromptsCheck_NoTemplatesDir(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setupRigConfig(t, tmpDir, []string{"myproject"})
|
||||||
|
|
||||||
|
check := NewPatrolRolesHavePromptsCheck()
|
||||||
|
ctx := &CheckContext{TownRoot: tmpDir}
|
||||||
|
|
||||||
|
result := check.Run(ctx)
|
||||||
|
|
||||||
|
if result.Status != StatusWarning {
|
||||||
|
t.Errorf("Status = %v, want Warning", result.Status)
|
||||||
|
}
|
||||||
|
if len(check.missingByRig) != 1 {
|
||||||
|
t.Errorf("missingByRig count = %d, want 1", len(check.missingByRig))
|
||||||
|
}
|
||||||
|
if len(check.missingByRig["myproject"]) != 3 {
|
||||||
|
t.Errorf("missing templates for myproject = %d, want 3", len(check.missingByRig["myproject"]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatrolRolesHavePromptsCheck_SomeTemplatesMissing(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setupRigConfig(t, tmpDir, []string{"myproject"})
|
||||||
|
templatesDir := setupRigTemplatesDir(t, tmpDir, "myproject")
|
||||||
|
|
||||||
|
if err := os.WriteFile(filepath.Join(templatesDir, "deacon.md.tmpl"), []byte("test"), 0644); err != nil {
|
||||||
|
t.Fatalf("write deacon.md.tmpl: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
check := NewPatrolRolesHavePromptsCheck()
|
||||||
|
ctx := &CheckContext{TownRoot: tmpDir}
|
||||||
|
|
||||||
|
result := check.Run(ctx)
|
||||||
|
|
||||||
|
if result.Status != StatusWarning {
|
||||||
|
t.Errorf("Status = %v, want Warning", result.Status)
|
||||||
|
}
|
||||||
|
if len(check.missingByRig["myproject"]) != 2 {
|
||||||
|
t.Errorf("missing templates = %d, want 2 (witness, refinery)", len(check.missingByRig["myproject"]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatrolRolesHavePromptsCheck_AllTemplatesExist(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setupRigConfig(t, tmpDir, []string{"myproject"})
|
||||||
|
templatesDir := setupRigTemplatesDir(t, tmpDir, "myproject")
|
||||||
|
|
||||||
|
for _, tmpl := range requiredRolePrompts {
|
||||||
|
if err := os.WriteFile(filepath.Join(templatesDir, tmpl), []byte("test content"), 0644); err != nil {
|
||||||
|
t.Fatalf("write %s: %v", tmpl, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check := NewPatrolRolesHavePromptsCheck()
|
||||||
|
ctx := &CheckContext{TownRoot: tmpDir}
|
||||||
|
|
||||||
|
result := check.Run(ctx)
|
||||||
|
|
||||||
|
if result.Status != StatusOK {
|
||||||
|
t.Errorf("Status = %v, want OK", result.Status)
|
||||||
|
}
|
||||||
|
if len(check.missingByRig) != 0 {
|
||||||
|
t.Errorf("missingByRig count = %d, want 0", len(check.missingByRig))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatrolRolesHavePromptsCheck_Fix(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setupRigConfig(t, tmpDir, []string{"myproject"})
|
||||||
|
|
||||||
|
check := NewPatrolRolesHavePromptsCheck()
|
||||||
|
ctx := &CheckContext{TownRoot: tmpDir}
|
||||||
|
|
||||||
|
result := check.Run(ctx)
|
||||||
|
if result.Status != StatusWarning {
|
||||||
|
t.Fatalf("Initial Status = %v, want Warning", result.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := check.Fix(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Fix() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
templatesDir := filepath.Join(tmpDir, "myproject", "mayor", "rig", "internal", "templates", "roles")
|
||||||
|
for _, tmpl := range requiredRolePrompts {
|
||||||
|
path := filepath.Join(templatesDir, tmpl)
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Fix() did not create %s: %v", tmpl, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if info.Size() == 0 {
|
||||||
|
t.Errorf("Fix() created empty file %s", tmpl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = check.Run(ctx)
|
||||||
|
if result.Status != StatusOK {
|
||||||
|
t.Errorf("After Fix(), Status = %v, want OK", result.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatrolRolesHavePromptsCheck_FixPartial(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setupRigConfig(t, tmpDir, []string{"myproject"})
|
||||||
|
templatesDir := setupRigTemplatesDir(t, tmpDir, "myproject")
|
||||||
|
|
||||||
|
existingContent := []byte("existing custom content")
|
||||||
|
if err := os.WriteFile(filepath.Join(templatesDir, "deacon.md.tmpl"), existingContent, 0644); err != nil {
|
||||||
|
t.Fatalf("write deacon.md.tmpl: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
check := NewPatrolRolesHavePromptsCheck()
|
||||||
|
ctx := &CheckContext{TownRoot: tmpDir}
|
||||||
|
|
||||||
|
result := check.Run(ctx)
|
||||||
|
if result.Status != StatusWarning {
|
||||||
|
t.Fatalf("Initial Status = %v, want Warning", result.Status)
|
||||||
|
}
|
||||||
|
if len(check.missingByRig["myproject"]) != 2 {
|
||||||
|
t.Fatalf("missing = %d, want 2", len(check.missingByRig["myproject"]))
|
||||||
|
}
|
||||||
|
|
||||||
|
err := check.Fix(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Fix() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deaconContent, err := os.ReadFile(filepath.Join(templatesDir, "deacon.md.tmpl"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read deacon.md.tmpl: %v", err)
|
||||||
|
}
|
||||||
|
if string(deaconContent) != string(existingContent) {
|
||||||
|
t.Error("Fix() should not overwrite existing deacon.md.tmpl")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tmpl := range []string{"witness.md.tmpl", "refinery.md.tmpl"} {
|
||||||
|
path := filepath.Join(templatesDir, tmpl)
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
t.Errorf("Fix() did not create %s: %v", tmpl, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatrolRolesHavePromptsCheck_MultipleRigs(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setupRigConfig(t, tmpDir, []string{"project1", "project2"})
|
||||||
|
|
||||||
|
templatesDir1 := setupRigTemplatesDir(t, tmpDir, "project1")
|
||||||
|
for _, tmpl := range requiredRolePrompts {
|
||||||
|
if err := os.WriteFile(filepath.Join(templatesDir1, tmpl), []byte("test"), 0644); err != nil {
|
||||||
|
t.Fatalf("write %s: %v", tmpl, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check := NewPatrolRolesHavePromptsCheck()
|
||||||
|
ctx := &CheckContext{TownRoot: tmpDir}
|
||||||
|
|
||||||
|
result := check.Run(ctx)
|
||||||
|
|
||||||
|
if result.Status != StatusWarning {
|
||||||
|
t.Errorf("Status = %v, want Warning (project2 missing)", result.Status)
|
||||||
|
}
|
||||||
|
if _, ok := check.missingByRig["project1"]; ok {
|
||||||
|
t.Error("project1 should not be in missingByRig")
|
||||||
|
}
|
||||||
|
if len(check.missingByRig["project2"]) != 3 {
|
||||||
|
t.Errorf("project2 missing = %d, want 3", len(check.missingByRig["project2"]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatrolRolesHavePromptsCheck_FixHint(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
setupRigConfig(t, tmpDir, []string{"myproject"})
|
||||||
|
|
||||||
|
check := NewPatrolRolesHavePromptsCheck()
|
||||||
|
ctx := &CheckContext{TownRoot: tmpDir}
|
||||||
|
|
||||||
|
result := check.Run(ctx)
|
||||||
|
|
||||||
|
if result.FixHint == "" {
|
||||||
|
t.Error("FixHint should not be empty for warning status")
|
||||||
|
}
|
||||||
|
if result.FixHint != "Run 'gt doctor --fix' to copy embedded templates to rig repos" {
|
||||||
|
t.Errorf("FixHint = %q, unexpected value", result.FixHint)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,14 +62,14 @@ type EscalationData struct {
|
|||||||
|
|
||||||
// HandoffData contains information for session handoff messages.
|
// HandoffData contains information for session handoff messages.
|
||||||
type HandoffData struct {
|
type HandoffData struct {
|
||||||
Role string
|
Role string
|
||||||
CurrentWork string
|
CurrentWork string
|
||||||
Status string
|
Status string
|
||||||
NextSteps []string
|
NextSteps []string
|
||||||
Notes string
|
Notes string
|
||||||
PendingMail int
|
PendingMail int
|
||||||
GitBranch string
|
GitBranch string
|
||||||
GitDirty bool
|
GitDirty bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Templates instance.
|
// New creates a new Templates instance.
|
||||||
@@ -126,3 +126,25 @@ func (t *Templates) RoleNames() []string {
|
|||||||
func (t *Templates) MessageNames() []string {
|
func (t *Templates) MessageNames() []string {
|
||||||
return []string{"spawn", "nudge", "escalation", "handoff"}
|
return []string{"spawn", "nudge", "escalation", "handoff"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllRoleTemplates returns all role templates as a map of filename to content.
|
||||||
|
func GetAllRoleTemplates() (map[string][]byte, error) {
|
||||||
|
entries, err := templateFS.ReadDir("roles")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading roles directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string][]byte)
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
content, err := templateFS.ReadFile("roles/" + entry.Name())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading %s: %w", entry.Name(), err)
|
||||||
|
}
|
||||||
|
result[entry.Name()] = content
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user