Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,25 @@ concurrency:
cancel-in-progress: true

env:
COVER_MIN: "65"
COVER_MIN: "90"

jobs:
lint:
name: Go lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0

- uses: actions/setup-go@v5
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true

- name: golangci-lint
uses: golangci/golangci-lint-action@v8
uses: golangci/golangci-lint-action@v9
with:
version: latest
only-new-issues: true
Expand All @@ -42,19 +42,28 @@ jobs:
name: Go tests + coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false

- uses: actions/setup-go@v5
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true

# Coverage excludes pure go-plugin host glue packages that cannot be
# meaningfully unit-tested without a full plugin.Serve/GRPC broker:
# - runtimedefault: thin Runtime server embed for plugins
# - runtime: Handshake/Serve/GRPCPlugin wiring + process-singleton Host()
# Capability registration and ServeManifest still have unit tests; those
# packages are run below without contributing to the coverage profile.
- name: Run tests with coverage
env:
GOWORK: off
run: go test $(go list ./pkg/pluginsdk/... | grep -v '/runtimedefault$') -count=1 -covermode=atomic -coverprofile=coverage.out
run: |
pkgs=$(go list ./pkg/pluginsdk/... | grep -vE '/(runtimedefault|runtime)$')
go test $pkgs -count=1 -covermode=atomic -coverprofile=coverage.out
go test ./pkg/pluginsdk/runtime/ -count=1

- name: Enforce coverage floor
run: ./scripts/check-coverage.sh coverage.out
Expand All @@ -66,7 +75,7 @@ jobs:

- name: Upload coverage profile
if: always()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
name: coverage-out
path: coverage.out
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0

Expand Down Expand Up @@ -51,11 +51,11 @@ jobs:
needs: version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
ref: ${{ needs.version.outputs.tag }}

- uses: actions/setup-go@v6
- uses: actions/setup-go@v7
with:
go-version: "1.26"

Expand Down Expand Up @@ -97,10 +97,10 @@ jobs:
needs: [version, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ needs.version.outputs.tag }}
generate_release_notes: true
48 changes: 48 additions & 0 deletions pkg/pluginsdk/config/config_more_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package config_test

import (
"testing"

pluginv1 "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1"
"github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/config"
)

func TestValidateManifestNilAndEmptySchema(t *testing.T) {
t.Parallel()

if err := config.ValidateManifestGlobalValue(nil, "k", nil); err == nil {
t.Fatal("expected nil manifest error")
}
if err := config.ValidateManifestUserValue(nil, "k", nil); err == nil {
t.Fatal("expected nil manifest error")
}

manifest := &pluginv1.PluginManifest{
GlobalConfigSchema: []*pluginv1.ConfigSchema{
{Key: "empty", Title: "Empty", JsonSchema: " "},
{Key: "bad", Title: "Bad", JsonSchema: `{`},
nil,
{Key: "other", Title: "Other"},
},
UserConfigSchema: []*pluginv1.ConfigSchema{
{Key: "prefs", Title: "Prefs", JsonSchema: `{"type":"object"}`},
},
}

if err := config.ValidateManifestGlobalValue(manifest, "empty", nil); err != nil {
t.Fatalf("empty schema with nil value: %v", err)
}
if err := config.ValidateManifestGlobalValue(manifest, "bad", map[string]any{}); err == nil {
t.Fatal("expected invalid schema JSON error")
}
if err := config.ValidateManifestUserValue(manifest, "prefs", map[string]any{"x": 1}); err != nil {
t.Fatalf("user value: %v", err)
}

if got := config.FindSchema(manifest.GetGlobalConfigSchema(), "other"); got == nil || got.GetKey() != "other" {
t.Fatalf("FindSchema = %+v", got)
}
if got := config.FindSchema(manifest.GetGlobalConfigSchema(), "missing"); got != nil {
t.Fatalf("FindSchema(missing) = %+v", got)
}
}
173 changes: 173 additions & 0 deletions pkg/pluginsdk/convert/convert_more_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package convert_test

import (
"testing"

"google.golang.org/protobuf/types/known/structpb"

pluginv1 "github.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1"
"github.com/prairie-server/prairie-plugin-sdk/pkg/pluginsdk/convert"
)

func TestDecodeCapability_FullMetadataAndErrors(t *testing.T) {
t.Parallel()

t.Run("nil metadata", func(t *testing.T) {
got, err := convert.DecodeCapability(convert.CapabilityRecord{Type: "t", ID: "id"})
if err != nil {
t.Fatalf("DecodeCapability: %v", err)
}
if got.GetType() != "t" || got.GetId() != "id" {
t.Fatalf("got %+v", got)
}
})

t.Run("auth modes icon and config schema maps", func(t *testing.T) {
got, err := convert.DecodeCapability(convert.CapabilityRecord{
Type: "auth_provider.v1",
ID: "oauth",
Metadata: map[string]any{
"display_name": "OAuth",
"description": "desc",
"auth_modes": []string{"oauth2", "device"},
"icon_url": "https://example.test/icon.png",
"config_schema": []map[string]any{
{
"key": "connection",
"title": "Connection",
"description": "creds",
"json_schema": `{"type":"object"}`,
"required": true,
},
},
"metadata": map[string]any{"k": "v"},
},
})
if err != nil {
t.Fatalf("DecodeCapability: %v", err)
}
if got.GetIconUrl() == "" || len(got.GetAuthModes()) != 2 {
t.Fatalf("unexpected: %+v", got)
}
if len(got.GetConfigSchema()) != 1 || got.GetConfigSchema()[0].GetKey() != "connection" {
t.Fatalf("config schema: %+v", got.GetConfigSchema())
}
if got.GetMetadata().AsMap()["k"] != "v" {
t.Fatalf("metadata: %+v", got.GetMetadata())
}
})

t.Run("subscriptions any slice", func(t *testing.T) {
got, err := convert.DecodeCapability(convert.CapabilityRecord{
Type: "event_consumer.v1",
ID: "c",
Metadata: map[string]any{
"subscriptions": []any{"a", "b"},
},
})
if err != nil {
t.Fatalf("DecodeCapability: %v", err)
}
if len(got.GetSubscriptions()) != 2 {
t.Fatalf("subscriptions = %v", got.GetSubscriptions())
}
})

t.Run("bad subscriptions type", func(t *testing.T) {
if _, err := convert.DecodeCapability(convert.CapabilityRecord{
Metadata: map[string]any{"subscriptions": "nope"},
}); err == nil {
t.Fatal("expected error")
}
})

t.Run("bad subscription element", func(t *testing.T) {
if _, err := convert.DecodeCapability(convert.CapabilityRecord{
Metadata: map[string]any{"subscriptions": []any{1}},
}); err == nil {
t.Fatal("expected error")
}
})

t.Run("bad auth_modes type", func(t *testing.T) {
if _, err := convert.DecodeCapability(convert.CapabilityRecord{
Metadata: map[string]any{"auth_modes": 42},
}); err == nil {
t.Fatal("expected error")
}
})

t.Run("bad config_schema type", func(t *testing.T) {
if _, err := convert.DecodeCapability(convert.CapabilityRecord{
Metadata: map[string]any{"config_schema": "nope"},
}); err == nil {
t.Fatal("expected error")
}
})

t.Run("bad watch sync provider", func(t *testing.T) {
if _, err := convert.DecodeCapability(convert.CapabilityRecord{
Metadata: map[string]any{
"watch_sync_provider": map[string]any{
"max_batch_size": "not-a-number",
},
},
}); err == nil {
t.Fatal("expected error")
}
})

t.Run("metadata that cannot become struct", func(t *testing.T) {
if _, err := convert.DecodeCapability(convert.CapabilityRecord{
Metadata: map[string]any{
"metadata": map[string]any{"bad": make(chan int)},
},
}); err == nil {
t.Fatal("expected error")
}
})
}

func TestCapabilityRecordsFromManifest_ErrorsAndExtras(t *testing.T) {
t.Parallel()

if _, err := convert.CapabilityRecordsFromManifest(&pluginv1.PluginManifest{
Capabilities: []*pluginv1.CapabilityDescriptor{nil},
}); err == nil {
t.Fatal("expected nil descriptor error")
}

meta, err := structpb.NewStruct(map[string]any{"x": float64(1)})
if err != nil {
t.Fatalf("NewStruct: %v", err)
}
records, err := convert.CapabilityRecordsFromManifest(&pluginv1.PluginManifest{
Capabilities: []*pluginv1.CapabilityDescriptor{
{
Type: "auth_provider.v1",
Id: "a",
DisplayName: "Auth",
Description: "d",
AuthModes: []string{"oauth2"},
IconUrl: "https://example.test/i.png",
Subscriptions: []string{"evt"},
Metadata: meta,
ConfigSchema: []*pluginv1.ConfigSchema{
{Key: "k", Title: "T", Description: "D", JsonSchema: `{}`, Required: true},
},
},
},
})
if err != nil {
t.Fatalf("CapabilityRecordsFromManifest: %v", err)
}
if len(records) != 1 {
t.Fatalf("len=%d", len(records))
}
if records[0].Metadata["icon_url"] != "https://example.test/i.png" {
t.Fatalf("metadata=%v", records[0].Metadata)
}
if _, ok := records[0].Metadata["auth_modes"]; !ok {
t.Fatalf("missing auth_modes: %v", records[0].Metadata)
}
}
30 changes: 30 additions & 0 deletions pkg/pluginsdk/httpclient/httpclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,36 @@ func TestDefaultHTTPClientUsesRaisedTimeout(t *testing.T) {
}
}

func TestStatusErrorEmptyMessageAndNoContent(t *testing.T) {
if got := (&StatusError{StatusCode: 502}).Error(); !strings.Contains(got, "502") {
t.Fatalf("empty body error = %q", got)
}
if got := (&StatusError{StatusCode: 502, Body: "raw"}).Error(); !strings.Contains(got, "raw") {
t.Fatalf("body fallback error = %q", got)
}

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
var dest map[string]any
if err := New(srv.URL, "k", nil).GetJSON(context.Background(), "/x", &dest); err != nil {
t.Fatalf("NoContent with dest: %v", err)
}

// Unencodable body (channel) hits the encode-request path.
if err := New(srv.URL, "k", nil).PostJSON(context.Background(), "/x", make(chan int), nil); err == nil {
t.Fatal("expected encode error")
}

// Canceled context hits the request-failed path.
ctx, cancel := context.WithCancel(context.Background())
cancel()
if err := New(srv.URL, "k", nil).GetJSON(ctx, "/x", nil); err == nil {
t.Fatal("expected canceled request error")
}
}

func TestPostJSONSetsApiKeyAndDecodes(t *testing.T) {
var key, method, ct string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
Loading
Loading