mirror of
https://github.com/boolean-maybe/tiki
synced 2026-04-21 13:37:20 +00:00
consolidate plugin files
This commit is contained in:
parent
aa94c897d6
commit
24917ecd84
22 changed files with 448 additions and 988 deletions
115
config/loader.go
115
config/loader.go
|
|
@ -3,6 +3,7 @@ package config
|
|||
// Viper configuration loader: reads config.yaml from the binary's directory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
|
@ -12,6 +13,7 @@ import (
|
|||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config holds all application configuration loaded from config.yaml
|
||||
|
|
@ -161,50 +163,93 @@ func GetInt(key string) int {
|
|||
return viper.GetInt(key)
|
||||
}
|
||||
|
||||
// SaveBoardViewMode saves the board view mode to config.yaml
|
||||
// Deprecated: Use SavePluginViewMode("Board", -1, viewMode) instead
|
||||
func SaveBoardViewMode(viewMode string) error {
|
||||
viper.Set("board.view", viewMode)
|
||||
return saveConfig()
|
||||
// workflowFileData represents the YAML structure of workflow.yaml for read-modify-write.
|
||||
// kept in config package to avoid import cycle with plugin package.
|
||||
type workflowFileData struct {
|
||||
Plugins []map[string]interface{} `yaml:"plugins"`
|
||||
}
|
||||
|
||||
// GetBoardViewMode loads the board view mode from config
|
||||
// Priority: plugins array entry with name "Board", then default
|
||||
// readWorkflowFile reads and unmarshals workflow.yaml from the given path.
|
||||
func readWorkflowFile(path string) (*workflowFileData, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading workflow.yaml: %w", err)
|
||||
}
|
||||
var wf workflowFileData
|
||||
if err := yaml.Unmarshal(data, &wf); err != nil {
|
||||
return nil, fmt.Errorf("parsing workflow.yaml: %w", err)
|
||||
}
|
||||
return &wf, nil
|
||||
}
|
||||
|
||||
// writeWorkflowFile marshals and writes workflow.yaml to the given path.
|
||||
func writeWorkflowFile(path string, wf *workflowFileData) error {
|
||||
data, err := yaml.Marshal(wf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling workflow.yaml: %w", err)
|
||||
}
|
||||
//nolint:gosec // G306: 0644 is appropriate for config file
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
return fmt.Errorf("writing workflow.yaml: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBoardViewMode loads the board view mode from workflow.yaml.
|
||||
// Returns "expanded" as default if not found.
|
||||
func GetBoardViewMode() string {
|
||||
// Check plugins array
|
||||
var currentPlugins []map[string]interface{}
|
||||
if err := viper.UnmarshalKey("plugins", ¤tPlugins); err == nil {
|
||||
for _, p := range currentPlugins {
|
||||
if name, ok := p["name"].(string); ok && name == "Board" {
|
||||
if view, ok := p["view"].(string); ok && view != "" {
|
||||
return view
|
||||
}
|
||||
return getPluginViewModeFromWorkflow("Board", "expanded")
|
||||
}
|
||||
|
||||
// getPluginViewModeFromWorkflow reads a plugin's view mode from workflow.yaml by name.
|
||||
func getPluginViewModeFromWorkflow(pluginName string, defaultValue string) string {
|
||||
path := FindWorkflowFile()
|
||||
if path == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
wf, err := readWorkflowFile(path)
|
||||
if err != nil {
|
||||
slog.Debug("failed to read workflow.yaml for view mode", "error", err)
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
for _, p := range wf.Plugins {
|
||||
if name, ok := p["name"].(string); ok && name == pluginName {
|
||||
if view, ok := p["view"].(string); ok && view != "" {
|
||||
return view
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default
|
||||
return "expanded"
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// SavePluginViewMode saves a plugin's view mode to config.yaml
|
||||
// This function updates or creates the plugin entry in the plugins array
|
||||
// configIndex: index in config array (-1 to create new entry by name)
|
||||
// SavePluginViewMode saves a plugin's view mode to workflow.yaml.
|
||||
// configIndex: index in workflow.yaml plugins array (-1 to find/create by name)
|
||||
func SavePluginViewMode(pluginName string, configIndex int, viewMode string) error {
|
||||
// Get current plugins configuration
|
||||
var currentPlugins []map[string]interface{}
|
||||
if err := viper.UnmarshalKey("plugins", ¤tPlugins); err != nil {
|
||||
// If no plugins exist or unmarshal fails, start with empty array
|
||||
currentPlugins = []map[string]interface{}{}
|
||||
path := FindWorkflowFile()
|
||||
if path == "" {
|
||||
// create workflow.yaml in project config dir
|
||||
path = DefaultWorkflowFilePath()
|
||||
}
|
||||
|
||||
if configIndex >= 0 && configIndex < len(currentPlugins) {
|
||||
// Update existing config entry (works for inline, file-based, or hybrid)
|
||||
currentPlugins[configIndex]["view"] = viewMode
|
||||
var wf *workflowFileData
|
||||
|
||||
// try to read existing file
|
||||
if existing, err := readWorkflowFile(path); err == nil {
|
||||
wf = existing
|
||||
} else {
|
||||
// Embedded plugin or missing entry - check if name-based entry already exists
|
||||
wf = &workflowFileData{}
|
||||
}
|
||||
|
||||
if configIndex >= 0 && configIndex < len(wf.Plugins) {
|
||||
// update existing entry by index
|
||||
wf.Plugins[configIndex]["view"] = viewMode
|
||||
} else {
|
||||
// find by name or create new entry
|
||||
existingIndex := -1
|
||||
for i, p := range currentPlugins {
|
||||
for i, p := range wf.Plugins {
|
||||
if name, ok := p["name"].(string); ok && name == pluginName {
|
||||
existingIndex = i
|
||||
break
|
||||
|
|
@ -212,21 +257,17 @@ func SavePluginViewMode(pluginName string, configIndex int, viewMode string) err
|
|||
}
|
||||
|
||||
if existingIndex >= 0 {
|
||||
// Update existing name-based entry
|
||||
currentPlugins[existingIndex]["view"] = viewMode
|
||||
wf.Plugins[existingIndex]["view"] = viewMode
|
||||
} else {
|
||||
// Create new name-based entry
|
||||
newEntry := map[string]interface{}{
|
||||
"name": pluginName,
|
||||
"view": viewMode,
|
||||
}
|
||||
currentPlugins = append(currentPlugins, newEntry)
|
||||
wf.Plugins = append(wf.Plugins, newEntry)
|
||||
}
|
||||
}
|
||||
|
||||
// Save back to viper
|
||||
viper.Set("plugins", currentPlugins)
|
||||
return saveConfig()
|
||||
return writeWorkflowFile(path, wf)
|
||||
}
|
||||
|
||||
// SaveHeaderVisible saves the header visibility setting to config.yaml
|
||||
|
|
|
|||
|
|
@ -302,6 +302,36 @@ func GetPluginSearchPaths() []string {
|
|||
return mustGetPathManager().PluginSearchPaths()
|
||||
}
|
||||
|
||||
// defaultWorkflowFilename is the default name for the workflow configuration file
|
||||
const defaultWorkflowFilename = "workflow.yaml"
|
||||
|
||||
// FindWorkflowFile searches for workflow.yaml in config search paths.
|
||||
// Search order: project config dir → user config dir → current directory (cwd).
|
||||
// Returns the first found path or empty string if not found.
|
||||
func FindWorkflowFile() string {
|
||||
searchPaths := GetPluginSearchPaths()
|
||||
|
||||
var paths []string
|
||||
for _, dir := range searchPaths {
|
||||
paths = append(paths, filepath.Join(dir, defaultWorkflowFilename))
|
||||
}
|
||||
paths = append(paths, defaultWorkflowFilename) // relative to cwd
|
||||
|
||||
for _, path := range paths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// DefaultWorkflowFilePath returns the default path for creating a new workflow.yaml
|
||||
// (in the project config dir, i.e. .doc/tiki/)
|
||||
func DefaultWorkflowFilePath() string {
|
||||
return filepath.Join(mustGetPathManager().TaskDir(), defaultWorkflowFilename)
|
||||
}
|
||||
|
||||
// GetTemplateFile returns the path to the user's custom new.md template
|
||||
func GetTemplateFile() string {
|
||||
return mustGetPathManager().TemplateFile()
|
||||
|
|
|
|||
|
|
@ -128,6 +128,31 @@ func BootstrapSystem() error {
|
|||
}
|
||||
createdFiles = append(createdFiles, linkedPath)
|
||||
|
||||
// Write default config.yaml
|
||||
defaultConfig := `logging:
|
||||
level: error
|
||||
header:
|
||||
visible: true
|
||||
tiki:
|
||||
maxPoints: 10
|
||||
appearance:
|
||||
theme: auto
|
||||
gradientThreshold: 256
|
||||
`
|
||||
configPath := GetProjectConfigFile()
|
||||
if err := os.WriteFile(configPath, []byte(defaultConfig), 0644); err != nil {
|
||||
return fmt.Errorf("write default config.yaml: %w", err)
|
||||
}
|
||||
createdFiles = append(createdFiles, configPath)
|
||||
|
||||
// Write default workflow.yaml
|
||||
defaultWorkflow := "plugins: []\n"
|
||||
workflowPath := DefaultWorkflowFilePath()
|
||||
if err := os.WriteFile(workflowPath, []byte(defaultWorkflow), 0644); err != nil {
|
||||
return fmt.Errorf("write default workflow.yaml: %w", err)
|
||||
}
|
||||
createdFiles = append(createdFiles, workflowPath)
|
||||
|
||||
// Git add all created files
|
||||
gitArgs := append([]string{"add"}, createdFiles...)
|
||||
//nolint:gosec // G204: git command with controlled file paths
|
||||
|
|
|
|||
|
|
@ -1,41 +1,47 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/boolean-maybe/tiki/model"
|
||||
"github.com/boolean-maybe/tiki/plugin"
|
||||
"github.com/boolean-maybe/tiki/task"
|
||||
"github.com/boolean-maybe/tiki/testutil"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestPluginView_MoveTaskAppliesLaneAction(t *testing.T) {
|
||||
originalPlugins := viper.Get("plugins")
|
||||
viper.Set("plugins", []plugin.PluginRef{
|
||||
{
|
||||
Name: "ActionTest",
|
||||
Key: "F4",
|
||||
Lanes: []plugin.PluginLaneConfig{
|
||||
{
|
||||
Name: "Backlog",
|
||||
Columns: 1,
|
||||
Filter: "status = 'backlog'",
|
||||
Action: "status=backlog, tags-=[moved]",
|
||||
},
|
||||
{
|
||||
Name: "Done",
|
||||
Columns: 1,
|
||||
Filter: "status = 'done'",
|
||||
Action: "status=done, tags+=[moved]",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
// create a temp workflow.yaml with the test plugin
|
||||
tmpDir := t.TempDir()
|
||||
workflowContent := `plugins:
|
||||
- name: ActionTest
|
||||
key: "F4"
|
||||
lanes:
|
||||
- name: Backlog
|
||||
columns: 1
|
||||
filter: status = 'backlog'
|
||||
action: status=backlog, tags-=[moved]
|
||||
- name: Done
|
||||
columns: 1
|
||||
filter: status = 'done'
|
||||
action: status=done, tags+=[moved]
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "workflow.yaml"), []byte(workflowContent), 0644); err != nil {
|
||||
t.Fatalf("failed to write workflow.yaml: %v", err)
|
||||
}
|
||||
|
||||
// chdir so FindWorkflowFile() picks it up
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get cwd: %v", err)
|
||||
}
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("failed to chdir: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
viper.Set("plugins", originalPlugins)
|
||||
_ = os.Chdir(origDir)
|
||||
})
|
||||
|
||||
ta := testutil.NewTestApp(t)
|
||||
|
|
|
|||
|
|
@ -54,18 +54,8 @@ func Bootstrap(tikiSkillContent, dokiSkillContent string) (*Result, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Phase 2: Configuration and logging
|
||||
cfg, err := LoadConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logLevel := InitLogging(cfg)
|
||||
|
||||
// Phase 2.5: System information collection and gradient support initialization
|
||||
// Collect early (before app creation) using terminfo lookup for future visual adjustments
|
||||
systemInfo := InitColorAndGradientSupport(cfg)
|
||||
|
||||
// Phase 3: Project initialization
|
||||
// Phase 2: Project initialization (creates dirs, seeds files, writes default config/workflow)
|
||||
// runs before LoadConfig so that config.yaml and workflow.yaml exist on first launch
|
||||
proceed, err := EnsureProjectInitialized(tikiSkillContent, dokiSkillContent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -74,6 +64,17 @@ func Bootstrap(tikiSkillContent, dokiSkillContent string) (*Result, error) {
|
|||
return nil, nil // User chose not to proceed
|
||||
}
|
||||
|
||||
// Phase 3: Configuration and logging
|
||||
cfg, err := LoadConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logLevel := InitLogging(cfg)
|
||||
|
||||
// Phase 3.5: System information collection and gradient support initialization
|
||||
// Collect early (before app creation) using terminfo lookup for future visual adjustments
|
||||
systemInfo := InitColorAndGradientSupport(cfg)
|
||||
|
||||
// Phase 4: Store initialization
|
||||
tikiStore, taskStore, err := InitStores()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -104,24 +104,3 @@ type TikiLane struct {
|
|||
Filter filter.FilterExpr
|
||||
Action LaneAction
|
||||
}
|
||||
|
||||
// PluginRef is the entry in config.yaml that references a plugin file or defines it inline
|
||||
type PluginRef struct {
|
||||
// File reference (for file-based and hybrid modes)
|
||||
File string `mapstructure:"file"`
|
||||
|
||||
// Inline definition fields (for inline and hybrid modes)
|
||||
Name string `mapstructure:"name"`
|
||||
Foreground string `mapstructure:"foreground"`
|
||||
Background string `mapstructure:"background"`
|
||||
Key string `mapstructure:"key"`
|
||||
Filter string `mapstructure:"filter"`
|
||||
Sort string `mapstructure:"sort"`
|
||||
View string `mapstructure:"view"`
|
||||
Type string `mapstructure:"type"`
|
||||
Fetcher string `mapstructure:"fetcher"`
|
||||
Text string `mapstructure:"text"`
|
||||
URL string `mapstructure:"url"`
|
||||
Lanes []PluginLaneConfig `mapstructure:"lanes"`
|
||||
Actions []PluginActionConfig `mapstructure:"actions"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
name: Backlog
|
||||
foreground: "#5fff87"
|
||||
background: "#0b3d2e"
|
||||
key: "F3"
|
||||
lanes:
|
||||
- name: Backlog
|
||||
columns: 4
|
||||
filter: status = 'backlog' and type != 'epic'
|
||||
actions:
|
||||
- key: "b"
|
||||
label: "Add to board"
|
||||
action: status = 'ready'
|
||||
sort: Priority, ID
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
name: Docs
|
||||
type: doki
|
||||
fetcher: file
|
||||
url: "index.md"
|
||||
foreground: "#ff9966"
|
||||
background: "#2b3a42"
|
||||
key: "F2"
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
name: Help
|
||||
type: doki
|
||||
fetcher: internal
|
||||
text: "Help"
|
||||
foreground: "#bcbcbc"
|
||||
background: "#003399"
|
||||
key: "?"
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
name: Kanban
|
||||
foreground: "#87ceeb"
|
||||
background: "#25496a"
|
||||
key: "F1"
|
||||
lanes:
|
||||
- name: Ready
|
||||
filter: status = 'ready' and type != 'epic'
|
||||
action: status = 'ready'
|
||||
- name: In Progress
|
||||
filter: status = 'in_progress' and type != 'epic'
|
||||
action: status = 'in_progress'
|
||||
- name: Review
|
||||
filter: status = 'review' and type != 'epic'
|
||||
action: status = 'review'
|
||||
- name: Done
|
||||
filter: status = 'done' and type != 'epic'
|
||||
action: status = 'done'
|
||||
sort: Priority, CreatedAt
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
name: Recent
|
||||
foreground: "#f4d6a6"
|
||||
background: "#5a3d1b"
|
||||
key: Ctrl-R
|
||||
lanes:
|
||||
- name: Recent
|
||||
columns: 4
|
||||
filter: NOW - UpdatedAt < 24hours
|
||||
sort: UpdatedAt DESC
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
name: Roadmap
|
||||
foreground: "#e2e8f0"
|
||||
background: "#2a5f5a"
|
||||
key: "F4"
|
||||
lanes:
|
||||
- name: Now
|
||||
columns: 1
|
||||
filter: type = 'epic' AND status = 'ready'
|
||||
action: status = 'ready'
|
||||
- name: Next
|
||||
columns: 1
|
||||
filter: type = 'epic' AND status = 'backlog' AND priority = 1
|
||||
action: status = 'backlog', priority = 1
|
||||
- name: Later
|
||||
columns: 2
|
||||
filter: type = 'epic' AND status = 'backlog' AND priority > 1
|
||||
action: status = 'backlog', priority = 2
|
||||
sort: Priority, Points DESC
|
||||
view: expanded
|
||||
74
plugin/embed/workflow.yaml
Normal file
74
plugin/embed/workflow.yaml
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
plugins:
|
||||
- name: Kanban
|
||||
foreground: "#87ceeb"
|
||||
background: "#25496a"
|
||||
key: "F1"
|
||||
lanes:
|
||||
- name: Ready
|
||||
filter: status = 'ready' and type != 'epic'
|
||||
action: status = 'ready'
|
||||
- name: In Progress
|
||||
filter: status = 'in_progress' and type != 'epic'
|
||||
action: status = 'in_progress'
|
||||
- name: Review
|
||||
filter: status = 'review' and type != 'epic'
|
||||
action: status = 'review'
|
||||
- name: Done
|
||||
filter: status = 'done' and type != 'epic'
|
||||
action: status = 'done'
|
||||
sort: Priority, CreatedAt
|
||||
- name: Backlog
|
||||
foreground: "#5fff87"
|
||||
background: "#0b3d2e"
|
||||
key: "F3"
|
||||
lanes:
|
||||
- name: Backlog
|
||||
columns: 4
|
||||
filter: status = 'backlog' and type != 'epic'
|
||||
actions:
|
||||
- key: "b"
|
||||
label: "Add to board"
|
||||
action: status = 'ready'
|
||||
sort: Priority, ID
|
||||
- name: Recent
|
||||
foreground: "#f4d6a6"
|
||||
background: "#5a3d1b"
|
||||
key: Ctrl-R
|
||||
lanes:
|
||||
- name: Recent
|
||||
columns: 4
|
||||
filter: NOW - UpdatedAt < 24hours
|
||||
sort: UpdatedAt DESC
|
||||
- name: Roadmap
|
||||
foreground: "#e2e8f0"
|
||||
background: "#2a5f5a"
|
||||
key: "F4"
|
||||
lanes:
|
||||
- name: Now
|
||||
columns: 1
|
||||
filter: type = 'epic' AND status = 'ready'
|
||||
action: status = 'ready'
|
||||
- name: Next
|
||||
columns: 1
|
||||
filter: type = 'epic' AND status = 'backlog' AND priority = 1
|
||||
action: status = 'backlog', priority = 1
|
||||
- name: Later
|
||||
columns: 2
|
||||
filter: type = 'epic' AND status = 'backlog' AND priority > 1
|
||||
action: status = 'backlog', priority = 2
|
||||
sort: Priority, Points DESC
|
||||
view: expanded
|
||||
- name: Help
|
||||
type: doki
|
||||
fetcher: internal
|
||||
text: "Help"
|
||||
foreground: "#bcbcbc"
|
||||
background: "#003399"
|
||||
key: "?"
|
||||
- name: Docs
|
||||
type: doki
|
||||
fetcher: file
|
||||
url: "index.md"
|
||||
foreground: "#ff9966"
|
||||
background: "#2b3a42"
|
||||
key: "F2"
|
||||
|
|
@ -3,68 +3,43 @@ package plugin
|
|||
import (
|
||||
_ "embed"
|
||||
"log/slog"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
//go:embed embed/kanban.yaml
|
||||
var kanbanYAML string
|
||||
//go:embed embed/workflow.yaml
|
||||
var embeddedWorkflowYAML string
|
||||
|
||||
//go:embed embed/recent.yaml
|
||||
var recentYAML string
|
||||
|
||||
//go:embed embed/roadmap.yaml
|
||||
var roadmapYAML string
|
||||
|
||||
//go:embed embed/backlog.yaml
|
||||
var backlogYAML string
|
||||
|
||||
//go:embed embed/help.yaml
|
||||
var helpYAML string
|
||||
|
||||
//go:embed embed/documentation.yaml
|
||||
var documentationYAML string
|
||||
|
||||
// loadEmbeddedPlugin parses a single embedded plugin and sets its ConfigIndex to -1
|
||||
func loadEmbeddedPlugin(yamlContent string, sourceName string) Plugin {
|
||||
p, err := parsePluginYAML([]byte(yamlContent), sourceName)
|
||||
if err != nil {
|
||||
slog.Error("failed to parse embedded plugin", "source", sourceName, "error", err)
|
||||
// loadEmbeddedPlugins loads the built-in default plugins from the embedded workflow.yaml
|
||||
func loadEmbeddedPlugins() []Plugin {
|
||||
var wf WorkflowFile
|
||||
if err := yaml.Unmarshal([]byte(embeddedWorkflowYAML), &wf); err != nil {
|
||||
slog.Error("failed to parse embedded workflow.yaml", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set ConfigIndex = -1 for both TikiPlugin and DokiPlugin
|
||||
switch plugin := p.(type) {
|
||||
case *TikiPlugin:
|
||||
plugin.ConfigIndex = -1
|
||||
case *DokiPlugin:
|
||||
plugin.ConfigIndex = -1
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// loadEmbeddedPlugins loads the built-in default plugins (Kanban, Backlog, Recent, Roadmap, Help, and Documentation)
|
||||
func loadEmbeddedPlugins() []Plugin {
|
||||
var plugins []Plugin
|
||||
|
||||
// Define embedded plugins with their YAML content and source names
|
||||
// Kanban is first so it becomes the default view
|
||||
embeddedPlugins := []struct {
|
||||
yaml string
|
||||
source string
|
||||
}{
|
||||
{kanbanYAML, "embedded:kanban"},
|
||||
{backlogYAML, "embedded:backlog"},
|
||||
{recentYAML, "embedded:recent"},
|
||||
{roadmapYAML, "embedded:roadmap"},
|
||||
{helpYAML, "embedded:help"},
|
||||
{documentationYAML, "embedded:documentation"},
|
||||
}
|
||||
|
||||
// Load each embedded plugin
|
||||
for _, ep := range embeddedPlugins {
|
||||
if p := loadEmbeddedPlugin(ep.yaml, ep.source); p != nil {
|
||||
plugins = append(plugins, p)
|
||||
for _, cfg := range wf.Plugins {
|
||||
if cfg.Name == "" {
|
||||
slog.Warn("skipping embedded plugin with no name")
|
||||
continue
|
||||
}
|
||||
|
||||
p, err := parsePluginConfig(cfg, "embedded:"+cfg.Name)
|
||||
if err != nil {
|
||||
slog.Error("failed to parse embedded plugin", "name", cfg.Name, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// mark as embedded (not from user config)
|
||||
switch plugin := p.(type) {
|
||||
case *TikiPlugin:
|
||||
plugin.ConfigIndex = -1
|
||||
case *DokiPlugin:
|
||||
plugin.ConfigIndex = -1
|
||||
}
|
||||
|
||||
plugins = append(plugins, p)
|
||||
}
|
||||
|
||||
return plugins
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/boolean-maybe/tiki/config"
|
||||
)
|
||||
|
||||
// findPluginFile searches for the plugin file in various locations
|
||||
// Search order: absolute path → project config dir → user config dir
|
||||
func findPluginFile(filename string) string {
|
||||
// If filename is absolute, try it directly
|
||||
if filepath.IsAbs(filename) {
|
||||
if _, err := os.Stat(filename); err == nil {
|
||||
return filename
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Get search paths from PathManager
|
||||
// Search order: project config dir → user config dir
|
||||
searchPaths := config.GetPluginSearchPaths()
|
||||
|
||||
// Build full list of paths to check
|
||||
var paths []string
|
||||
paths = append(paths, filename) // Try as-is first (relative to cwd)
|
||||
|
||||
for _, dir := range searchPaths {
|
||||
paths = append(paths, filepath.Join(dir, filename))
|
||||
}
|
||||
|
||||
// Search for the file
|
||||
for _, path := range paths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFindPluginFile(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create test files in different locations
|
||||
currentDir := tmpDir
|
||||
testFile := "test-plugin.yaml"
|
||||
testFilePath := filepath.Join(currentDir, testFile)
|
||||
|
||||
// Create the test file
|
||||
if err := os.WriteFile(testFilePath, []byte("name: test"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create test file: %v", err)
|
||||
}
|
||||
|
||||
// Change to temp directory for testing
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get current directory: %v", err)
|
||||
}
|
||||
defer func() { _ = os.Chdir(origDir) }()
|
||||
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to change to temp directory: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
filename string
|
||||
wantPath string
|
||||
wantFound bool
|
||||
}{
|
||||
{
|
||||
name: "absolute path",
|
||||
filename: testFilePath,
|
||||
wantPath: testFilePath,
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
name: "relative path in current dir",
|
||||
filename: testFile,
|
||||
wantPath: testFile,
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
name: "non-existent file",
|
||||
filename: "nonexistent.yaml",
|
||||
wantPath: "",
|
||||
wantFound: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := findPluginFile(tt.filename)
|
||||
|
||||
if tt.wantFound {
|
||||
if got == "" {
|
||||
t.Errorf("findPluginFile(%q) = empty, want non-empty path",
|
||||
tt.filename)
|
||||
}
|
||||
// Verify the file exists at the returned path
|
||||
if _, err := os.Stat(got); err != nil {
|
||||
t.Errorf("findPluginFile returned path %q that doesn't exist: %v",
|
||||
got, err)
|
||||
}
|
||||
} else {
|
||||
if got != "" {
|
||||
t.Errorf("findPluginFile(%q) = %q, want empty string",
|
||||
tt.filename, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindPluginFile_SearchOrder(t *testing.T) {
|
||||
// Create temporary directories
|
||||
tmpDir := t.TempDir()
|
||||
subDir := filepath.Join(tmpDir, "subdir")
|
||||
//nolint:gosec // G301: test directory permissions
|
||||
if err := os.MkdirAll(subDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create subdirectory: %v", err)
|
||||
}
|
||||
|
||||
// Create test files in different locations with same name
|
||||
testFile := "plugin.yaml"
|
||||
currentFile := filepath.Join(tmpDir, testFile)
|
||||
subFile := filepath.Join(subDir, testFile)
|
||||
|
||||
// Create files
|
||||
if err := os.WriteFile(currentFile, []byte("current"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create current file: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(subFile, []byte("sub"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create sub file: %v", err)
|
||||
}
|
||||
|
||||
// Change to temp directory
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get current directory: %v", err)
|
||||
}
|
||||
defer func() { _ = os.Chdir(origDir) }()
|
||||
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to change to temp directory: %v", err)
|
||||
}
|
||||
|
||||
// Test that current directory is preferred in search order
|
||||
got := findPluginFile(testFile)
|
||||
if got == "" {
|
||||
t.Fatal("findPluginFile returned empty path")
|
||||
}
|
||||
|
||||
// Read the file to verify which one was found
|
||||
content, err := os.ReadFile(got)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read found file: %v", err)
|
||||
}
|
||||
|
||||
// Should find the one in current directory first
|
||||
if string(content) != "current" {
|
||||
t.Errorf("findPluginFile found wrong file: got content %q, want %q",
|
||||
string(content), "current")
|
||||
}
|
||||
}
|
||||
136
plugin/loader.go
136
plugin/loader.go
|
|
@ -5,58 +5,72 @@ import (
|
|||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/boolean-maybe/tiki/config"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// loadConfiguredPlugins loads plugins defined in config.yaml
|
||||
// WorkflowFile represents the YAML structure of a workflow.yaml file
|
||||
type WorkflowFile struct {
|
||||
Plugins []pluginFileConfig `yaml:"plugins"`
|
||||
}
|
||||
|
||||
// loadConfiguredPlugins loads plugins defined in workflow.yaml
|
||||
func loadConfiguredPlugins() []Plugin {
|
||||
// Get plugin refs from config.yaml
|
||||
var refs []PluginRef
|
||||
if err := viper.UnmarshalKey("plugins", &refs); err != nil {
|
||||
// Not an error if plugins key doesn't exist
|
||||
slog.Debug("no plugins configured or failed to parse", "error", err)
|
||||
workflowPath := config.FindWorkflowFile()
|
||||
if workflowPath == "" {
|
||||
slog.Debug("no workflow.yaml found")
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(refs) == 0 {
|
||||
return nil // no plugins configured
|
||||
data, err := os.ReadFile(workflowPath)
|
||||
if err != nil {
|
||||
slog.Warn("failed to read workflow.yaml", "path", workflowPath, "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var wf WorkflowFile
|
||||
if err := yaml.Unmarshal(data, &wf); err != nil {
|
||||
slog.Warn("failed to parse workflow.yaml", "path", workflowPath, "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(wf.Plugins) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var plugins []Plugin
|
||||
|
||||
for i, ref := range refs {
|
||||
// Validate before loading
|
||||
if err := validatePluginRef(ref); err != nil {
|
||||
slog.Warn("invalid plugin configuration", "error", err)
|
||||
for i, cfg := range wf.Plugins {
|
||||
if cfg.Name == "" {
|
||||
slog.Warn("skipping plugin with no name in workflow.yaml", "index", i)
|
||||
continue
|
||||
}
|
||||
|
||||
plugin, err := loadPluginFromRef(ref)
|
||||
source := fmt.Sprintf("%s:%s", workflowPath, cfg.Name)
|
||||
p, err := parsePluginConfig(cfg, source)
|
||||
if err != nil {
|
||||
slog.Warn("failed to load plugin", "name", ref.Name, "file", ref.File, "error", err)
|
||||
continue // Skip failed plugins, continue with others
|
||||
slog.Warn("failed to load plugin from workflow.yaml", "name", cfg.Name, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Set config index (need type assertion or helper)
|
||||
if p, ok := plugin.(*TikiPlugin); ok {
|
||||
p.ConfigIndex = i
|
||||
} else if p, ok := plugin.(*DokiPlugin); ok {
|
||||
p.ConfigIndex = i
|
||||
// set config index to position in workflow.yaml
|
||||
if tp, ok := p.(*TikiPlugin); ok {
|
||||
tp.ConfigIndex = i
|
||||
} else if dp, ok := p.(*DokiPlugin); ok {
|
||||
dp.ConfigIndex = i
|
||||
}
|
||||
|
||||
plugins = append(plugins, plugin)
|
||||
pk, pr, pm := plugin.GetActivationKey()
|
||||
slog.Info("loaded plugin", "name", plugin.GetName(), "key", keyName(pk, pr), "modifier", pm)
|
||||
plugins = append(plugins, p)
|
||||
pk, pr, pm := p.GetActivationKey()
|
||||
slog.Info("loaded plugin", "name", p.GetName(), "key", keyName(pk, pr), "modifier", pm)
|
||||
}
|
||||
|
||||
return plugins
|
||||
}
|
||||
|
||||
// LoadPlugins loads all plugins: embedded defaults (Recent, Roadmap) plus configured plugins from config.yaml
|
||||
// Configured plugins with the same name as embedded plugins will be merged (configured fields override embedded)
|
||||
// LoadPlugins loads all plugins: embedded defaults plus configured plugins from workflow.yaml.
|
||||
// Configured plugins with the same name as embedded plugins will be merged (configured fields override embedded).
|
||||
func LoadPlugins() ([]Plugin, error) {
|
||||
// Load embedded default plugins first (maintains order)
|
||||
// load embedded default plugins first (maintains order)
|
||||
embedded := loadEmbeddedPlugins()
|
||||
embeddedByName := make(map[string]Plugin)
|
||||
for _, p := range embedded {
|
||||
|
|
@ -65,29 +79,28 @@ func LoadPlugins() ([]Plugin, error) {
|
|||
slog.Info("loaded embedded plugin", "name", p.GetName(), "key", keyName(pk, pr), "modifier", pm)
|
||||
}
|
||||
|
||||
// Load configured plugins (may override embedded ones)
|
||||
// load configured plugins (may override embedded ones)
|
||||
configured := loadConfiguredPlugins()
|
||||
|
||||
// Track which embedded plugins were overridden and merge them
|
||||
// track which embedded plugins were overridden and merge them
|
||||
overridden := make(map[string]bool)
|
||||
mergedConfigured := make([]Plugin, 0, len(configured))
|
||||
|
||||
for _, configPlugin := range configured {
|
||||
if embeddedPlugin, ok := embeddedByName[configPlugin.GetName()]; ok {
|
||||
// Merge: embedded plugin fields + configured overrides
|
||||
// merge: embedded plugin fields + configured overrides
|
||||
merged := mergePluginDefinitions(embeddedPlugin, configPlugin)
|
||||
mergedConfigured = append(mergedConfigured, merged)
|
||||
overridden[configPlugin.GetName()] = true
|
||||
slog.Info("plugin override (merged)", "name", configPlugin.GetName(),
|
||||
"from", embeddedPlugin.GetFilePath(), "to", configPlugin.GetFilePath())
|
||||
} else {
|
||||
// New plugin (not an override)
|
||||
// new plugin (not an override)
|
||||
mergedConfigured = append(mergedConfigured, configPlugin)
|
||||
}
|
||||
}
|
||||
|
||||
// Build final list: non-overridden embedded plugins + merged configured plugins
|
||||
// This preserves order: embedded plugins first (in their original order), then configured
|
||||
// build final list: non-overridden embedded plugins + merged configured plugins
|
||||
var plugins []Plugin
|
||||
for _, p := range embedded {
|
||||
if !overridden[p.GetName()] {
|
||||
|
|
@ -98,58 +111,3 @@ func LoadPlugins() ([]Plugin, error) {
|
|||
|
||||
return plugins, nil
|
||||
}
|
||||
|
||||
// loadPluginFromRef loads a single plugin from a PluginRef, handling three modes:
|
||||
// 1. Fully inline (no file): all fields in config.yaml
|
||||
// 2. File-based (file only): reference external YAML
|
||||
// 3. Hybrid (file + overrides): file provides base, inline overrides
|
||||
func loadPluginFromRef(ref PluginRef) (Plugin, error) {
|
||||
var cfg pluginFileConfig
|
||||
var source string
|
||||
|
||||
if ref.File != "" {
|
||||
// File-based or hybrid mode
|
||||
pluginPath := findPluginFile(ref.File)
|
||||
if pluginPath == "" {
|
||||
return nil, fmt.Errorf("plugin file not found: %s", ref.File)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(pluginPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading file: %w", err)
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parsing yaml: %w", err)
|
||||
}
|
||||
|
||||
source = pluginPath
|
||||
|
||||
// Apply inline overrides
|
||||
cfg = mergePluginConfigs(cfg, ref)
|
||||
} else {
|
||||
// Fully inline mode
|
||||
cfg = pluginFileConfig{
|
||||
Name: ref.Name,
|
||||
Foreground: ref.Foreground,
|
||||
Background: ref.Background,
|
||||
Key: ref.Key,
|
||||
Filter: ref.Filter,
|
||||
Sort: ref.Sort,
|
||||
View: ref.View,
|
||||
Type: ref.Type,
|
||||
Fetcher: ref.Fetcher,
|
||||
Text: ref.Text,
|
||||
URL: ref.URL,
|
||||
Lanes: ref.Lanes,
|
||||
}
|
||||
source = "inline:" + ref.Name
|
||||
}
|
||||
|
||||
// Validate: must have name
|
||||
if cfg.Name == "" {
|
||||
return nil, fmt.Errorf("plugin must have a name")
|
||||
}
|
||||
|
||||
return parsePluginConfig(cfg, source)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import (
|
|||
taskpkg "github.com/boolean-maybe/tiki/task"
|
||||
)
|
||||
|
||||
func TestLoadPluginFromRef_FullyInline(t *testing.T) {
|
||||
ref := PluginRef{
|
||||
func TestParsePluginConfig_FullyInline(t *testing.T) {
|
||||
cfg := pluginFileConfig{
|
||||
Name: "Inline Test",
|
||||
Foreground: "#ffffff",
|
||||
Background: "#000000",
|
||||
|
|
@ -22,7 +22,7 @@ func TestLoadPluginFromRef_FullyInline(t *testing.T) {
|
|||
View: "expanded",
|
||||
}
|
||||
|
||||
def, err := loadPluginFromRef(ref)
|
||||
def, err := parsePluginConfig(cfg, "test")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ func TestLoadPluginFromRef_FullyInline(t *testing.T) {
|
|||
t.Errorf("Expected sort 'Priority DESC', got %+v", tp.Sort)
|
||||
}
|
||||
|
||||
// Test filter evaluation
|
||||
// test filter evaluation
|
||||
task := &taskpkg.Task{
|
||||
ID: "TIKI-1",
|
||||
Status: taskpkg.StatusReady,
|
||||
|
|
@ -63,15 +63,15 @@ func TestLoadPluginFromRef_FullyInline(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoadPluginFromRef_InlineMinimal(t *testing.T) {
|
||||
ref := PluginRef{
|
||||
func TestParsePluginConfig_Minimal(t *testing.T) {
|
||||
cfg := pluginFileConfig{
|
||||
Name: "Minimal",
|
||||
Lanes: []PluginLaneConfig{
|
||||
{Name: "Bugs", Filter: "type = 'bug'"},
|
||||
},
|
||||
}
|
||||
|
||||
def, err := loadPluginFromRef(ref)
|
||||
def, err := parsePluginConfig(cfg, "test")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
|
@ -90,220 +90,29 @@ func TestLoadPluginFromRef_InlineMinimal(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoadPluginFromRef_FileBased(t *testing.T) {
|
||||
// Create temp plugin file
|
||||
tmpDir := t.TempDir()
|
||||
pluginFile := filepath.Join(tmpDir, "test-plugin.yaml")
|
||||
content := `name: Test Plugin
|
||||
foreground: "#ff0000"
|
||||
background: "#0000ff"
|
||||
key: T
|
||||
lanes:
|
||||
- name: In Progress
|
||||
filter: status = 'in_progress'
|
||||
sort: Priority, UpdatedAt DESC
|
||||
view: compact
|
||||
`
|
||||
if err := os.WriteFile(pluginFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write plugin file: %v", err)
|
||||
}
|
||||
|
||||
ref := PluginRef{
|
||||
File: pluginFile, // Use absolute path
|
||||
}
|
||||
|
||||
def, err := loadPluginFromRef(ref)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
tp, ok := def.(*TikiPlugin)
|
||||
if !ok {
|
||||
t.Fatalf("Expected TikiPlugin, got %T", def)
|
||||
}
|
||||
|
||||
if tp.Name != "Test Plugin" {
|
||||
t.Errorf("Expected name 'Test Plugin', got '%s'", tp.Name)
|
||||
}
|
||||
|
||||
if tp.Rune != 'T' {
|
||||
t.Errorf("Expected rune 'T', got '%c'", tp.Rune)
|
||||
}
|
||||
|
||||
if tp.ViewMode != "compact" {
|
||||
t.Errorf("Expected view mode 'compact', got '%s'", tp.ViewMode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPluginFromRef_Hybrid(t *testing.T) {
|
||||
// Create temp plugin file with base config
|
||||
tmpDir := t.TempDir()
|
||||
pluginFile := filepath.Join(tmpDir, "base-plugin.yaml")
|
||||
content := `name: Base Plugin
|
||||
foreground: "#ff0000"
|
||||
background: "#0000ff"
|
||||
key: L
|
||||
lanes:
|
||||
- name: Todo
|
||||
filter: status = 'ready'
|
||||
sort: Priority
|
||||
view: compact
|
||||
`
|
||||
if err := os.WriteFile(pluginFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write plugin file: %v", err)
|
||||
}
|
||||
|
||||
// Override view and key
|
||||
ref := PluginRef{
|
||||
File: pluginFile, // Use absolute path
|
||||
View: "expanded",
|
||||
Key: "H",
|
||||
}
|
||||
|
||||
def, err := loadPluginFromRef(ref)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
tp, ok := def.(*TikiPlugin)
|
||||
if !ok {
|
||||
t.Fatalf("Expected TikiPlugin, got %T", def)
|
||||
}
|
||||
|
||||
// Base fields should be from file
|
||||
if tp.Name != "Base Plugin" {
|
||||
t.Errorf("Expected name 'Base Plugin', got '%s'", tp.Name)
|
||||
}
|
||||
|
||||
if len(tp.Lanes) != 1 || tp.Lanes[0].Filter == nil {
|
||||
t.Error("Expected lane filter from file")
|
||||
}
|
||||
|
||||
// Overridden fields should be from inline
|
||||
if tp.Rune != 'H' {
|
||||
t.Errorf("Expected rune 'H' (overridden), got '%c'", tp.Rune)
|
||||
}
|
||||
|
||||
if tp.ViewMode != "expanded" {
|
||||
t.Errorf("Expected view mode 'expanded' (overridden), got '%s'", tp.ViewMode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPluginFromRef_HybridMultipleOverrides(t *testing.T) {
|
||||
// Create temp plugin file
|
||||
tmpDir := t.TempDir()
|
||||
pluginFile := filepath.Join(tmpDir, "multi-plugin.yaml")
|
||||
content := `name: Multi Plugin
|
||||
foreground: "#ffffff"
|
||||
background: "#000000"
|
||||
key: M
|
||||
lanes:
|
||||
- name: Todo
|
||||
filter: status = 'ready'
|
||||
sort: Priority
|
||||
view: compact
|
||||
`
|
||||
if err := os.WriteFile(pluginFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write plugin file: %v", err)
|
||||
}
|
||||
|
||||
// Override multiple fields
|
||||
ref := PluginRef{
|
||||
File: pluginFile, // Use absolute path
|
||||
Key: "X",
|
||||
Lanes: []PluginLaneConfig{
|
||||
{Name: "In Progress", Filter: "status = 'in_progress'"},
|
||||
},
|
||||
Sort: "UpdatedAt DESC",
|
||||
View: "expanded",
|
||||
Foreground: "#00ff00",
|
||||
}
|
||||
|
||||
def, err := loadPluginFromRef(ref)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
tp, ok := def.(*TikiPlugin)
|
||||
if !ok {
|
||||
t.Fatalf("Expected TikiPlugin, got %T", def)
|
||||
}
|
||||
|
||||
// Check overridden values
|
||||
if tp.Rune != 'X' {
|
||||
t.Errorf("Expected rune 'X', got '%c'", tp.Rune)
|
||||
}
|
||||
|
||||
if tp.ViewMode != "expanded" {
|
||||
t.Errorf("Expected view 'expanded', got '%s'", tp.ViewMode)
|
||||
}
|
||||
|
||||
// Verify filter override
|
||||
task := &taskpkg.Task{
|
||||
ID: "TIKI-1",
|
||||
Status: taskpkg.StatusInProgress,
|
||||
}
|
||||
if len(tp.Lanes) != 1 || tp.Lanes[0].Filter == nil {
|
||||
t.Fatal("Expected overridden lane filter")
|
||||
}
|
||||
if !tp.Lanes[0].Filter.Evaluate(task, time.Now(), "testuser") {
|
||||
t.Error("Expected overridden filter to match in_progress task")
|
||||
}
|
||||
|
||||
todoTask := &taskpkg.Task{
|
||||
ID: "TIKI-2",
|
||||
Status: taskpkg.StatusReady,
|
||||
}
|
||||
if tp.Lanes[0].Filter.Evaluate(todoTask, time.Now(), "testuser") {
|
||||
t.Error("Expected overridden filter to NOT match todo task")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPluginFromRef_MissingFile(t *testing.T) {
|
||||
ref := PluginRef{
|
||||
File: "nonexistent.yaml",
|
||||
}
|
||||
|
||||
_, err := loadPluginFromRef(ref)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for missing file")
|
||||
}
|
||||
|
||||
if err.Error() != "plugin file not found: nonexistent.yaml" {
|
||||
t.Errorf("Expected 'file not found' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPluginFromRef_NoName(t *testing.T) {
|
||||
// Inline plugin without name
|
||||
ref := PluginRef{
|
||||
func TestParsePluginConfig_NoName(t *testing.T) {
|
||||
cfg := pluginFileConfig{
|
||||
Lanes: []PluginLaneConfig{
|
||||
{Name: "Todo", Filter: "status = 'ready'"},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := loadPluginFromRef(ref)
|
||||
_, err := parsePluginConfig(cfg, "test")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for plugin without name")
|
||||
}
|
||||
|
||||
if err.Error() != "plugin must have a name" {
|
||||
t.Errorf("Expected 'must have a name' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for merger functions moved to merger_test.go
|
||||
|
||||
func TestPluginTypeExplicit(t *testing.T) {
|
||||
// 1. Inline plugin with type doki
|
||||
ref := PluginRef{
|
||||
// inline plugin with type doki
|
||||
cfg := pluginFileConfig{
|
||||
Name: "Type Doki Test",
|
||||
Type: "doki",
|
||||
Fetcher: "internal",
|
||||
Text: "some text",
|
||||
}
|
||||
|
||||
def, err := loadPluginFromRef(ref)
|
||||
def, err := parsePluginConfig(cfg, "test")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
|
@ -315,70 +124,113 @@ func TestPluginTypeExplicit(t *testing.T) {
|
|||
if _, ok := def.(*DokiPlugin); !ok {
|
||||
t.Errorf("Expected DokiPlugin type assertion to succeed")
|
||||
}
|
||||
}
|
||||
|
||||
// 2. File-based plugin with type doki
|
||||
func TestLoadConfiguredPlugins_WorkflowFile(t *testing.T) {
|
||||
// create a temp directory with a workflow.yaml
|
||||
tmpDir := t.TempDir()
|
||||
pluginFile := filepath.Join(tmpDir, "type-doki.yaml")
|
||||
content := `name: File Type Doki
|
||||
type: doki
|
||||
fetcher: file
|
||||
url: http://example.com/resource
|
||||
workflowContent := `plugins:
|
||||
- name: TestBoard
|
||||
key: "F5"
|
||||
lanes:
|
||||
- name: Ready
|
||||
filter: status = 'ready'
|
||||
sort: Priority
|
||||
- name: TestDocs
|
||||
type: doki
|
||||
fetcher: internal
|
||||
text: "hello"
|
||||
key: "D"
|
||||
`
|
||||
if err := os.WriteFile(pluginFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write plugin file: %v", err)
|
||||
workflowPath := filepath.Join(tmpDir, "workflow.yaml")
|
||||
if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to write workflow.yaml: %v", err)
|
||||
}
|
||||
|
||||
refFile := PluginRef{
|
||||
File: pluginFile, // Use absolute path
|
||||
}
|
||||
|
||||
defFile, err := loadPluginFromRef(refFile)
|
||||
// change to temp dir so FindWorkflowFile() finds it in cwd
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
t.Fatalf("Failed to get cwd: %v", err)
|
||||
}
|
||||
defer func() { _ = os.Chdir(origDir) }()
|
||||
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to chdir: %v", err)
|
||||
}
|
||||
|
||||
if defFile.GetType() != "doki" {
|
||||
t.Errorf("Expected type 'doki' for file plugin, got '%s'", defFile.GetType())
|
||||
plugins := loadConfiguredPlugins()
|
||||
if len(plugins) != 2 {
|
||||
t.Fatalf("Expected 2 plugins, got %d", len(plugins))
|
||||
}
|
||||
|
||||
if plugins[0].GetName() != "TestBoard" {
|
||||
t.Errorf("Expected first plugin 'TestBoard', got '%s'", plugins[0].GetName())
|
||||
}
|
||||
if plugins[1].GetName() != "TestDocs" {
|
||||
t.Errorf("Expected second plugin 'TestDocs', got '%s'", plugins[1].GetName())
|
||||
}
|
||||
|
||||
// verify config indices
|
||||
if plugins[0].GetConfigIndex() != 0 {
|
||||
t.Errorf("Expected config index 0, got %d", plugins[0].GetConfigIndex())
|
||||
}
|
||||
if plugins[1].GetConfigIndex() != 1 {
|
||||
t.Errorf("Expected config index 1, got %d", plugins[1].GetConfigIndex())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginTypeOverride(t *testing.T) {
|
||||
// File specifies tiki, override specifies doki
|
||||
// This scenario tests if we can override an embedded/file plugin type.
|
||||
// Current mergePluginDefinitions only merges Tiki->Tiki.
|
||||
// If types mismatch, it returns the override.
|
||||
|
||||
func TestLoadConfiguredPlugins_NoWorkflowFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
pluginFile := filepath.Join(tmpDir, "type-override.yaml")
|
||||
content := `name: Type Override
|
||||
type: tiki
|
||||
`
|
||||
if err := os.WriteFile(pluginFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write plugin file: %v", err)
|
||||
}
|
||||
|
||||
ref := PluginRef{
|
||||
File: pluginFile, // Use absolute path
|
||||
Type: "doki",
|
||||
Fetcher: "internal",
|
||||
Text: "override text",
|
||||
}
|
||||
|
||||
// loadPluginFromRef calls mergePluginConfigs but NOT mergePluginDefinitions.
|
||||
// mergePluginConfigs updates the config struct.
|
||||
// parsePluginConfig then creates the struct.
|
||||
// So this test checks mergePluginConfigs logic + parsing logic.
|
||||
|
||||
def, err := loadPluginFromRef(ref)
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
t.Fatalf("Failed to get cwd: %v", err)
|
||||
}
|
||||
defer func() { _ = os.Chdir(origDir) }()
|
||||
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to chdir: %v", err)
|
||||
}
|
||||
|
||||
if def.GetType() != "doki" {
|
||||
t.Errorf("Expected type 'doki' (overridden), got '%s'", def.GetType())
|
||||
}
|
||||
|
||||
if _, ok := def.(*DokiPlugin); !ok {
|
||||
t.Errorf("Expected DokiPlugin type assertion to succeed")
|
||||
plugins := loadConfiguredPlugins()
|
||||
if plugins != nil {
|
||||
t.Errorf("Expected nil plugins when no workflow.yaml, got %d", len(plugins))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfiguredPlugins_InvalidPlugin(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
workflowContent := `plugins:
|
||||
- name: Valid
|
||||
key: "V"
|
||||
lanes:
|
||||
- name: Todo
|
||||
filter: status = 'ready'
|
||||
- name: Invalid
|
||||
type: unknown
|
||||
`
|
||||
workflowPath := filepath.Join(tmpDir, "workflow.yaml")
|
||||
if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to write workflow.yaml: %v", err)
|
||||
}
|
||||
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get cwd: %v", err)
|
||||
}
|
||||
defer func() { _ = os.Chdir(origDir) }()
|
||||
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to chdir: %v", err)
|
||||
}
|
||||
|
||||
// should load valid plugin and skip invalid one
|
||||
plugins := loadConfiguredPlugins()
|
||||
if len(plugins) != 1 {
|
||||
t.Fatalf("Expected 1 valid plugin (invalid skipped), got %d", len(plugins))
|
||||
}
|
||||
|
||||
if plugins[0].GetName() != "Valid" {
|
||||
t.Errorf("Expected plugin 'Valid', got '%s'", plugins[0].GetName())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
|
|
@ -23,54 +21,6 @@ type pluginFileConfig struct {
|
|||
Actions []PluginActionConfig `yaml:"actions"`
|
||||
}
|
||||
|
||||
// mergePluginConfigs merges file-based config (base) with inline overrides
|
||||
// Inline values override file values for any non-empty field
|
||||
func mergePluginConfigs(base pluginFileConfig, overrides PluginRef) pluginFileConfig {
|
||||
result := base
|
||||
|
||||
if overrides.Name != "" {
|
||||
result.Name = overrides.Name
|
||||
}
|
||||
if overrides.Foreground != "" {
|
||||
result.Foreground = overrides.Foreground
|
||||
}
|
||||
if overrides.Background != "" {
|
||||
result.Background = overrides.Background
|
||||
}
|
||||
if overrides.Key != "" {
|
||||
result.Key = overrides.Key
|
||||
}
|
||||
if overrides.Filter != "" {
|
||||
result.Filter = overrides.Filter
|
||||
}
|
||||
if overrides.Sort != "" {
|
||||
result.Sort = overrides.Sort
|
||||
}
|
||||
if overrides.View != "" {
|
||||
result.View = overrides.View
|
||||
}
|
||||
if overrides.Type != "" {
|
||||
result.Type = overrides.Type
|
||||
}
|
||||
if overrides.Fetcher != "" {
|
||||
result.Fetcher = overrides.Fetcher
|
||||
}
|
||||
if overrides.Text != "" {
|
||||
result.Text = overrides.Text
|
||||
}
|
||||
if overrides.URL != "" {
|
||||
result.URL = overrides.URL
|
||||
}
|
||||
if len(overrides.Lanes) > 0 {
|
||||
result.Lanes = overrides.Lanes
|
||||
}
|
||||
if len(overrides.Actions) > 0 {
|
||||
result.Actions = overrides.Actions
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// mergePluginDefinitions merges an embedded plugin (base) with a configured override
|
||||
// Override fields replace base fields only if they are non-zero/non-empty
|
||||
func mergePluginDefinitions(base Plugin, override Plugin) Plugin {
|
||||
|
|
@ -130,29 +80,3 @@ func mergePluginDefinitions(base Plugin, override Plugin) Plugin {
|
|||
// just return the override.
|
||||
return override
|
||||
}
|
||||
|
||||
// validatePluginRef validates a PluginRef before loading
|
||||
func validatePluginRef(ref PluginRef) error {
|
||||
if ref.File != "" {
|
||||
// File-based or hybrid - name is optional (can come from file)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fully inline - must have name
|
||||
if ref.Name == "" {
|
||||
return fmt.Errorf("inline plugin must specify 'name' field")
|
||||
}
|
||||
|
||||
// Should have at least one configuration field
|
||||
hasContent := ref.Key != "" || ref.Filter != "" ||
|
||||
ref.Sort != "" || ref.Foreground != "" ||
|
||||
ref.Background != "" || ref.View != "" || ref.Type != "" ||
|
||||
ref.Fetcher != "" || ref.Text != "" || ref.URL != "" ||
|
||||
len(ref.Lanes) > 0 || len(ref.Actions) > 0
|
||||
|
||||
if !hasContent {
|
||||
return fmt.Errorf("inline plugin '%s' has no configuration fields", ref.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,167 +8,6 @@ import (
|
|||
"github.com/boolean-maybe/tiki/plugin/filter"
|
||||
)
|
||||
|
||||
func TestMergePluginConfigs(t *testing.T) {
|
||||
base := pluginFileConfig{
|
||||
Name: "Base",
|
||||
Foreground: "#ff0000",
|
||||
Background: "#0000ff",
|
||||
Key: "L",
|
||||
Lanes: []PluginLaneConfig{
|
||||
{Name: "Todo", Filter: "status = 'ready'"},
|
||||
},
|
||||
Sort: "Priority",
|
||||
View: "compact",
|
||||
}
|
||||
|
||||
overrides := PluginRef{
|
||||
View: "expanded",
|
||||
Key: "O",
|
||||
}
|
||||
|
||||
result := mergePluginConfigs(base, overrides)
|
||||
|
||||
// Base fields should remain
|
||||
if result.Name != "Base" {
|
||||
t.Errorf("Expected name 'Base', got '%s'", result.Name)
|
||||
}
|
||||
if len(result.Lanes) != 1 || result.Lanes[0].Filter != "status = 'ready'" {
|
||||
t.Errorf("Expected lanes from base, got %+v", result.Lanes)
|
||||
}
|
||||
if result.Foreground != "#ff0000" {
|
||||
t.Errorf("Expected foreground from base, got '%s'", result.Foreground)
|
||||
}
|
||||
|
||||
// Overridden fields
|
||||
if result.View != "expanded" {
|
||||
t.Errorf("Expected view 'expanded', got '%s'", result.View)
|
||||
}
|
||||
if result.Key != "O" {
|
||||
t.Errorf("Expected key 'O', got '%s'", result.Key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergePluginConfigs_AllOverrides(t *testing.T) {
|
||||
base := pluginFileConfig{
|
||||
Name: "Base",
|
||||
Foreground: "#ff0000",
|
||||
Background: "#0000ff",
|
||||
Key: "L",
|
||||
Lanes: []PluginLaneConfig{
|
||||
{Name: "Todo", Filter: "status = 'ready'"},
|
||||
},
|
||||
Sort: "Priority",
|
||||
View: "compact",
|
||||
}
|
||||
|
||||
overrides := PluginRef{
|
||||
Name: "Overridden",
|
||||
Foreground: "#00ff00",
|
||||
Background: "#000000",
|
||||
Key: "O",
|
||||
Lanes: []PluginLaneConfig{
|
||||
{Name: "Done", Filter: "status = 'done'"},
|
||||
},
|
||||
Sort: "UpdatedAt DESC",
|
||||
View: "expanded",
|
||||
}
|
||||
|
||||
result := mergePluginConfigs(base, overrides)
|
||||
|
||||
// All fields should be overridden
|
||||
if result.Name != "Overridden" {
|
||||
t.Errorf("Expected name 'Overridden', got '%s'", result.Name)
|
||||
}
|
||||
if result.Foreground != "#00ff00" {
|
||||
t.Errorf("Expected foreground '#00ff00', got '%s'", result.Foreground)
|
||||
}
|
||||
if result.Background != "#000000" {
|
||||
t.Errorf("Expected background '#000000', got '%s'", result.Background)
|
||||
}
|
||||
if result.Key != "O" {
|
||||
t.Errorf("Expected key 'O', got '%s'", result.Key)
|
||||
}
|
||||
if len(result.Lanes) != 1 || result.Lanes[0].Filter != "status = 'done'" {
|
||||
t.Errorf("Expected lane filter 'status = 'done'', got %+v", result.Lanes)
|
||||
}
|
||||
if result.Sort != "UpdatedAt DESC" {
|
||||
t.Errorf("Expected sort 'UpdatedAt DESC', got '%s'", result.Sort)
|
||||
}
|
||||
if result.View != "expanded" {
|
||||
t.Errorf("Expected view 'expanded', got '%s'", result.View)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePluginRef_FileBased(t *testing.T) {
|
||||
ref := PluginRef{
|
||||
File: "plugin.yaml",
|
||||
}
|
||||
|
||||
err := validatePluginRef(ref)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for file-based plugin, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePluginRef_Hybrid(t *testing.T) {
|
||||
ref := PluginRef{
|
||||
File: "plugin.yaml",
|
||||
View: "expanded",
|
||||
}
|
||||
|
||||
err := validatePluginRef(ref)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for hybrid plugin, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePluginRef_InlineValid(t *testing.T) {
|
||||
ref := PluginRef{
|
||||
Name: "Test",
|
||||
Lanes: []PluginLaneConfig{
|
||||
{Name: "Todo", Filter: "status = 'ready'"},
|
||||
},
|
||||
}
|
||||
|
||||
err := validatePluginRef(ref)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for valid inline plugin, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePluginRef_InlineNoName(t *testing.T) {
|
||||
ref := PluginRef{
|
||||
Lanes: []PluginLaneConfig{
|
||||
{Name: "Todo", Filter: "status = 'ready'"},
|
||||
},
|
||||
}
|
||||
|
||||
err := validatePluginRef(ref)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for inline plugin without name")
|
||||
}
|
||||
|
||||
if err.Error() != "inline plugin must specify 'name' field" {
|
||||
t.Errorf("Expected 'must specify name' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePluginRef_InlineNoContent(t *testing.T) {
|
||||
ref := PluginRef{
|
||||
Name: "Empty",
|
||||
}
|
||||
|
||||
err := validatePluginRef(ref)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for inline plugin with no content")
|
||||
}
|
||||
|
||||
expected := "inline plugin 'Empty' has no configuration fields"
|
||||
if err.Error() != expected {
|
||||
t.Errorf("Expected '%s', got: %v", expected, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergePluginDefinitions_TikiToTiki(t *testing.T) {
|
||||
baseFilter, _ := filter.ParseFilter("status = 'ready'")
|
||||
baseSort, _ := ParseSort("Priority")
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ import (
|
|||
|
||||
// parsePluginConfig parses a pluginFileConfig into a Plugin
|
||||
func parsePluginConfig(cfg pluginFileConfig, source string) (Plugin, error) {
|
||||
if cfg.Name == "" {
|
||||
return nil, fmt.Errorf("plugin must have a name (%s)", source)
|
||||
}
|
||||
|
||||
// Common fields
|
||||
// Use ColorDefault as sentinel so views can detect "not specified" and use theme-appropriate colors
|
||||
fg := parseColor(cfg.Foreground, tcell.ColorDefault)
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ import (
|
|||
func TestDokiValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ref PluginRef
|
||||
cfg pluginFileConfig
|
||||
wantError string
|
||||
}{
|
||||
{
|
||||
name: "Missing Fetcher",
|
||||
ref: PluginRef{
|
||||
cfg: pluginFileConfig{
|
||||
Name: "Invalid Doki",
|
||||
Type: "doki",
|
||||
},
|
||||
|
|
@ -21,7 +21,7 @@ func TestDokiValidation(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "Invalid Fetcher",
|
||||
ref: PluginRef{
|
||||
cfg: pluginFileConfig{
|
||||
Name: "Invalid Fetcher",
|
||||
Type: "doki",
|
||||
Fetcher: "http",
|
||||
|
|
@ -30,7 +30,7 @@ func TestDokiValidation(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "File Fetcher Missing URL",
|
||||
ref: PluginRef{
|
||||
cfg: pluginFileConfig{
|
||||
Name: "File No URL",
|
||||
Type: "doki",
|
||||
Fetcher: "file",
|
||||
|
|
@ -39,7 +39,7 @@ func TestDokiValidation(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "Internal Fetcher Missing Text",
|
||||
ref: PluginRef{
|
||||
cfg: pluginFileConfig{
|
||||
Name: "Internal No Text",
|
||||
Type: "doki",
|
||||
Fetcher: "internal",
|
||||
|
|
@ -48,7 +48,7 @@ func TestDokiValidation(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "Doki with Tiki fields",
|
||||
ref: PluginRef{
|
||||
cfg: pluginFileConfig{
|
||||
Name: "Doki with Filter",
|
||||
Type: "doki",
|
||||
Fetcher: "internal",
|
||||
|
|
@ -59,7 +59,7 @@ func TestDokiValidation(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "Valid File Fetcher",
|
||||
ref: PluginRef{
|
||||
cfg: pluginFileConfig{
|
||||
Name: "Valid File",
|
||||
Type: "doki",
|
||||
Fetcher: "file",
|
||||
|
|
@ -69,7 +69,7 @@ func TestDokiValidation(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "Valid Internal Fetcher",
|
||||
ref: PluginRef{
|
||||
cfg: pluginFileConfig{
|
||||
Name: "Valid Internal",
|
||||
Type: "doki",
|
||||
Fetcher: "internal",
|
||||
|
|
@ -81,7 +81,7 @@ func TestDokiValidation(t *testing.T) {
|
|||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := loadPluginFromRef(tc.ref)
|
||||
_, err := parsePluginConfig(tc.cfg, "test")
|
||||
if tc.wantError != "" {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error containing '%s', got nil", tc.wantError)
|
||||
|
|
@ -100,12 +100,12 @@ func TestDokiValidation(t *testing.T) {
|
|||
func TestTikiValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ref PluginRef
|
||||
cfg pluginFileConfig
|
||||
wantError string
|
||||
}{
|
||||
{
|
||||
name: "Tiki with Doki fields (Fetcher)",
|
||||
ref: PluginRef{
|
||||
cfg: pluginFileConfig{
|
||||
Name: "Tiki with Fetcher",
|
||||
Type: "tiki",
|
||||
Filter: "status='ready'",
|
||||
|
|
@ -115,7 +115,7 @@ func TestTikiValidation(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "Tiki with Doki fields (Text)",
|
||||
ref: PluginRef{
|
||||
cfg: pluginFileConfig{
|
||||
Name: "Tiki with Text",
|
||||
Type: "tiki",
|
||||
Filter: "status='ready'",
|
||||
|
|
@ -127,7 +127,7 @@ func TestTikiValidation(t *testing.T) {
|
|||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := loadPluginFromRef(tc.ref)
|
||||
_, err := parsePluginConfig(tc.cfg, "test")
|
||||
if tc.wantError != "" {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error containing '%s', got nil", tc.wantError)
|
||||
|
|
|
|||
Loading…
Reference in a new issue