package journal_test import ( "context" "os" "path/filepath" "slices" "strings" "testing" journal "git.golem15.com/golem15/sm-journal-plugin" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/compass" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/party" "git.golem15.com/golem15/summercms/modules/surf" ) // TestJournalMCPSmoke pins the Journal's MCP declaration (P-17, D-15, D-16): // exactly the seven post, category and tag tools, the four settings and // content-fix tools (33-02) and the draft_post prompt, each with Guard // backend and golem15.journal.access_posts (manage_settings for the // settings and fix_headings tools), delete and set_image_alt marked // destructive, and every tool bound to a route the plugin registers (the // same method and pattern in a router built from the booted plugins). // Runs under -short (no database). func TestJournalMCPSmoke(t *testing.T) { var _ pact.HasMCP = (*journal.Plugin)(nil) decl := (&journal.Plugin{}).MCP() wantTools := []string{"list_posts", "show_post", "list_categories", "list_tags", "create_post", "update_post", "delete_post", "get_settings", "update_settings", "fix_headings", "set_image_alt"} // The settings and heading tools need manage_settings (33-02, D-10). manageSettings := map[string]bool{"get_settings": true, "update_settings": true, "fix_headings": true} destructive := map[string]bool{"delete_post": true, "set_image_alt": true} var names []string for _, tool := range decl.Tools { names = append(names, tool.Name) want := []string{"golem15.journal.access_posts"} if manageSettings[tool.Name] { want = []string{"golem15.journal.manage_settings"} } if tool.Guard != "backend" || !slices.Equal(tool.Permissions, want) { t.Errorf("tool %s guard %q permissions %v, want backend and %v", tool.Name, tool.Guard, tool.Permissions, want) } if tool.Title == "" || tool.Description == "" || tool.Input == nil { t.Errorf("tool %s lacks a title, description or input", tool.Name) } if (tool.Destructive != nil && *tool.Destructive) != destructive[tool.Name] { t.Errorf("tool %s destructive = %v", tool.Name, tool.Destructive) } } if !slices.Equal(names, wantTools) { t.Fatalf("tools %v, want %v", names, wantTools) } if len(decl.Prompts) != 1 || decl.Prompts[0].Name != "draft_post" || decl.Prompts[0].Guard != "backend" || !slices.Equal(decl.Prompts[0].Permissions, []string{"golem15.journal.access_posts"}) { t.Fatalf("prompts %+v, want draft_post (backend, access_posts)", decl.Prompts) } prompt := decl.Prompts[0] if len(prompt.Arguments) != 2 || prompt.Arguments[0].Name != "topic" || !prompt.Arguments[0].Required || prompt.Arguments[1].Name != "tone" || prompt.Arguments[1].Required { t.Fatalf("draft_post arguments %+v, want topic (required) and tone", prompt.Arguments) } msgs, err := prompt.Render(context.Background(), map[string]string{"topic": "Release notes", "tone": "formal"}) if err != nil || len(msgs) != 1 || msgs[0].Role != "user" || !strings.Contains(msgs[0].Text, "Release notes") || !strings.Contains(msgs[0].Text, "formal") || !strings.Contains(msgs[0].Text, "journal_create_post") || !strings.Contains(msgs[0].Text, "published set to false") { t.Fatalf("draft_post render = %+v, %v", msgs, err) } // Every tool's method and path is a route the plugin registers. dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: journal-mcp\nlocale: en\nfallback_locale: en\nurl: https://app.example.test\n"), 0o644); err != nil { t.Fatal(err) } cfg, err := compass.Open(compass.Options{Dir: dir, Environ: []string{"SUMMER_ENV=development", "SUMMER_ADMIN__JWT__SECRET=" + adminJWTSecret}}) if err != nil { t.Fatal(err) } for key, value := range map[string]any{"http.body_limits.default_bytes": 1048576, "http.body_limits.upload_bytes": 8388608} { if err := cfg.Set(key, value); err != nil { t.Fatal(err) } } app := backpack.New(cfg) plugins, err := party.Activate(app, []string{"golem15.translate", "golem15.journal"}) if err != nil { t.Fatal(err) } rt, err := surf.BuildRouter(app, plugins) if err != nil { t.Fatalf("surf.BuildRouter: %v", err) } routes := map[string]string{} for _, r := range rt.Routes() { routes[r.Method+" "+r.Pattern] = r.PluginID } for _, tool := range decl.Tools { if owner, ok := routes[tool.Method+" "+tool.Path]; !ok || owner != "golem15.journal" { t.Errorf("tool %s targets %s %s, which golem15.journal does not register (owner %q)", tool.Name, tool.Method, tool.Path, owner) } } }