feat: milestones 6b-fix through 6e — default-editor, files plugin, workspace host, workspaceItems contribution

- Fix PluginCard openProviders display
- Add default-editor plugin (text/markdown/notes-context)
- Add files plugin with workspaceItems placement
- Add workspaceItems contribution point (Go + API + mock + SDK)
- Add WorkspaceHost component for workspace area
- WorkspaceTree dispatches selection event
- Fix default-editor layout to fill container
- Fix PluginCard unsafe .length access
- Add E2E tests: 34/34 pass
- Add bundle execution check to official-plugins check.sh
- Update docs: PLUGIN_RUNTIME, DEV_PLUGINS, MILESTONE_6B/6C/6D plans
This commit is contained in:
2026-06-19 16:42:01 +08:00
parent 6ed6df311a
commit a6412fa070
23 changed files with 1518 additions and 286 deletions
+181
View File
@@ -205,6 +205,187 @@ func TestOpenResourceReturnsNoProviderFallback(t *testing.T) {
}
}
func TestSelectProviderUsesTextPreference(t *testing.T) {
r := NewRouter(Preferences{
DefaultTextEditorProvider: "custom.text-editor",
})
providers := []contribution.ContributionOpenProvider{
provider("official.editor", "builtin.text", 100, "BuiltinText", plugin.OpenProviderSupport{
Kind: "vault-file",
Extensions: []string{".txt"},
Contexts: []string{ContextGenericText},
}),
provider("custom.editor", "custom.text-editor", 10, "CustomText", plugin.OpenProviderSupport{
Kind: "vault-file",
Extensions: []string{".txt"},
Contexts: []string{ContextGenericText},
}),
}
selected, err := r.SelectProvider(OpenResourceRequest{
Kind: "vault-file",
Path: "Docs/todo.txt",
Mode: "view",
}, providers)
if err != nil {
t.Fatalf("SelectProvider: %v", err)
}
if selected.Item.ID != "custom.text-editor" {
t.Fatalf("provider = %q, want custom.text-editor", selected.Item.ID)
}
}
func TestSelectProviderUsesMarkdownPreference(t *testing.T) {
r := NewRouter(Preferences{
DefaultMarkdownEditorProvider: "community.markdown-editor",
})
providers := []contribution.ContributionOpenProvider{
provider("official.editor", "builtin.markdown", 100, "BuiltinMarkdown", plugin.OpenProviderSupport{
Kind: "vault-file",
Extensions: []string{".md"},
Contexts: []string{ContextGenericMarkdown},
}),
provider("community.editor", "community.markdown-editor", 10, "CommunityMarkdown", plugin.OpenProviderSupport{
Kind: "vault-file",
Extensions: []string{".md"},
Contexts: []string{ContextGenericMarkdown},
}),
}
selected, err := r.SelectProvider(OpenResourceRequest{
Kind: "vault-file",
Path: "Docs/readme.md",
Mode: "view",
}, providers)
if err != nil {
t.Fatalf("SelectProvider: %v", err)
}
if selected.Item.ID != "community.markdown-editor" {
t.Fatalf("provider = %q, want community.markdown-editor", selected.Item.ID)
}
}
func TestSelectProviderMatchesMime(t *testing.T) {
r := NewRouter(Preferences{})
providers := []contribution.ContributionOpenProvider{
provider("image.plugin", "image.viewer", 10, "ImageViewer", plugin.OpenProviderSupport{
Kind: "vault-file",
Mime: []string{"image/png", "image/jpeg"},
}),
}
selected, err := r.SelectProvider(OpenResourceRequest{
Kind: "vault-file",
Path: "Photos/screenshot.png",
Extension: ".png",
Mime: "image/png",
Mode: "view",
}, providers)
if err != nil {
t.Fatalf("SelectProvider: %v", err)
}
if selected.Item.ID != "image.viewer" {
t.Fatalf("provider = %q, want image.viewer", selected.Item.ID)
}
}
func TestSelectProviderExtensionCaseInsensitive(t *testing.T) {
r := NewRouter(Preferences{})
providers := []contribution.ContributionOpenProvider{
provider("editor.plugin", "md.editor", 10, "MDEditor", plugin.OpenProviderSupport{
Kind: "vault-file",
Extensions: []string{".md"},
}),
}
tests := []struct {
name string
ext string
path string
}{
{"uppercase .MD", ".MD", "Docs/README.MD"},
{"mixed case .Md", ".Md", "Docs/Notes.Md"},
{"lowercase .md", ".md", "Docs/readme.md"},
{"markdown extension uppercase", ".MD", "Docs/guide.MD"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
selected, err := r.SelectProvider(OpenResourceRequest{
Kind: "vault-file",
Path: tt.path,
Extension: tt.ext,
Mode: "view",
}, providers)
if err != nil {
t.Fatalf("SelectProvider: %v", err)
}
if selected.Item.ID != "md.editor" {
t.Fatalf("provider = %q, want md.editor for ext %s", selected.Item.ID, tt.ext)
}
})
}
}
func TestSelectProviderMultipleSupportsEntries(t *testing.T) {
r := NewRouter(Preferences{})
providers := []contribution.ContributionOpenProvider{
provider("editor.plugin", "multi.editor", 10, "MultiEditor", plugin.OpenProviderSupport{
Kind: "vault-file",
Extensions: []string{".md"},
Contexts: []string{ContextGenericMarkdown},
}, plugin.OpenProviderSupport{
Kind: "vault-file",
Extensions: []string{".txt"},
Contexts: []string{ContextGenericText},
}),
}
t.Run("matches markdown entry", func(t *testing.T) {
selected, err := r.SelectProvider(OpenResourceRequest{
Kind: "vault-file",
Path: "Docs/readme.md",
Mode: "view",
}, providers)
if err != nil {
t.Fatalf("SelectProvider: %v", err)
}
if selected.Item.ID != "multi.editor" {
t.Fatalf("provider = %q, want multi.editor", selected.Item.ID)
}
})
t.Run("matches text entry", func(t *testing.T) {
selected, err := r.SelectProvider(OpenResourceRequest{
Kind: "vault-file",
Path: "Docs/todo.txt",
Mode: "view",
}, providers)
if err != nil {
t.Fatalf("SelectProvider: %v", err)
}
if selected.Item.ID != "multi.editor" {
t.Fatalf("provider = %q, want multi.editor", selected.Item.ID)
}
})
}
func TestSelectProviderKindMismatch(t *testing.T) {
r := NewRouter(Preferences{})
_, err := r.SelectProvider(OpenResourceRequest{
Kind: "http-url",
Path: "https://example.com/file.md",
Mode: "view",
}, []contribution.ContributionOpenProvider{
provider("editor.plugin", "vault.editor", 10, "VaultEditor", plugin.OpenProviderSupport{
Kind: "vault-file",
Extensions: []string{".md"},
}),
})
if err == nil {
t.Fatal("expected no provider for http-url kind with vault-file provider")
}
}
func TestDetermineContextName(t *testing.T) {
tests := []struct {
name string