diff --git a/pkg/plugins/yaegi/events_test.go b/pkg/plugins/yaegi/events_test.go
new file mode 100644
index 000000000..97ab4fff6
--- /dev/null
+++ b/pkg/plugins/yaegi/events_test.go
@@ -0,0 +1,48 @@
+// Vikunja is a to-do list application to facilitate your life.
+// Copyright 2018-present Vikunja and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package yaegi
+
+import (
+ "testing"
+
+ "code.vikunja.io/api/pkg/log"
+)
+
+func TestPluginEventListener(t *testing.T) {
+ log.InitLogger()
+
+ loaded, err := LoadPluginFull(examplePluginDir)
+ if err != nil {
+ t.Fatalf("LoadPluginFull failed: %v", err)
+ }
+
+ // Call Init() — this registers the TestListener for TaskCreatedEvent.
+ // If the Listener interface boundary is broken, this will panic with a
+ // reflection error when calling events.RegisterListener.
+ err = loaded.Plugin.Init()
+ if err != nil {
+ t.Fatalf("plugin Init failed: %v", err)
+ }
+ t.Log("Init() succeeded — events.RegisterListener accepted the interpreted Listener")
+
+ // Verify Shutdown works too
+ err = loaded.Plugin.Shutdown()
+ if err != nil {
+ t.Fatalf("plugin Shutdown failed: %v", err)
+ }
+ t.Log("Shutdown() succeeded")
+}
diff --git a/pkg/plugins/yaegi/loader_test.go b/pkg/plugins/yaegi/loader_test.go
new file mode 100644
index 000000000..c2bbde1a8
--- /dev/null
+++ b/pkg/plugins/yaegi/loader_test.go
@@ -0,0 +1,70 @@
+// Vikunja is a to-do list application to facilitate your life.
+// Copyright 2018-present Vikunja and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package yaegi
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+const examplePluginDir = "../../../examples/plugins/example"
+
+func TestLoadPlugin(t *testing.T) {
+ pluginDir := examplePluginDir
+
+ mainGo := filepath.Join(pluginDir, "main.go")
+ if _, err := os.Stat(mainGo); err != nil {
+ t.Fatalf("example plugin source not found at %s: %v", mainGo, err)
+ }
+
+ p, err := LoadPlugin(pluginDir)
+ if err != nil {
+ t.Fatalf("LoadPlugin failed: %v", err)
+ }
+
+ if p.Name() != "example" {
+ t.Errorf("expected plugin name 'example', got %q", p.Name())
+ }
+ if p.Version() != "1.0.0" {
+ t.Errorf("expected version '1.0.0', got %q", p.Version())
+ }
+}
+
+func TestLoadPluginFull(t *testing.T) {
+ loaded, err := LoadPluginFull(examplePluginDir)
+ if err != nil {
+ t.Fatalf("LoadPluginFull failed: %v", err)
+ }
+
+ if loaded.Plugin == nil {
+ t.Fatal("Plugin is nil")
+ }
+ if loaded.Plugin.Name() != "example" {
+ t.Errorf("expected plugin name 'example', got %q", loaded.Plugin.Name())
+ }
+
+ if loaded.AuthRouter == nil {
+ t.Fatal("AuthRouter is nil — typed factory NewAuthenticatedRouterPlugin not found")
+ }
+ t.Logf("AuthRouter type: %T, name: %s", loaded.AuthRouter, loaded.AuthRouter.Name())
+
+ if loaded.UnauthRouter == nil {
+ t.Fatal("UnauthRouter is nil — typed factory NewUnauthenticatedRouterPlugin not found")
+ }
+ t.Logf("UnauthRouter type: %T, name: %s", loaded.UnauthRouter, loaded.UnauthRouter.Name())
+}
diff --git a/pkg/plugins/yaegi/routes_test.go b/pkg/plugins/yaegi/routes_test.go
new file mode 100644
index 000000000..d765e95dc
--- /dev/null
+++ b/pkg/plugins/yaegi/routes_test.go
@@ -0,0 +1,63 @@
+// Vikunja is a to-do list application to facilitate your life.
+// Copyright 2018-present Vikunja and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package yaegi
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "code.vikunja.io/api/pkg/log"
+ "github.com/labstack/echo/v5"
+)
+
+func TestPluginRoutesServeHTTP(t *testing.T) {
+ log.InitLogger()
+ loaded, err := LoadPluginFull(examplePluginDir)
+ if err != nil {
+ t.Fatalf("LoadPluginFull failed: %v", err)
+ }
+
+ if loaded.UnauthRouter == nil {
+ t.Fatal("UnauthRouter is nil — cannot test route registration")
+ }
+
+ // Create a real Echo instance and register the plugin's unauthenticated routes
+ e := echo.New()
+ g := e.Group("/plugins")
+ loaded.UnauthRouter.RegisterUnauthenticatedRoutes(g)
+
+ // Make an HTTP request to the plugin's /status endpoint
+ req := httptest.NewRequest(http.MethodGet, "/plugins/status", nil)
+ rec := httptest.NewRecorder()
+ e.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("expected status 200, got %d, body: %s", rec.Code, rec.Body.String())
+ }
+
+ body := rec.Body.String()
+ if !strings.Contains(body, "example") {
+ t.Errorf("response body should contain plugin name 'example', got: %s", body)
+ }
+ if !strings.Contains(body, "ok") {
+ t.Errorf("response body should contain status 'ok', got: %s", body)
+ }
+
+ t.Logf("HTTP response: %s", body)
+}
diff --git a/pkg/yaegi_symbols/stdlib_check_test.go b/pkg/yaegi_symbols/stdlib_check_test.go
new file mode 100644
index 000000000..e6b1cfae5
--- /dev/null
+++ b/pkg/yaegi_symbols/stdlib_check_test.go
@@ -0,0 +1,26 @@
+package yaegi_symbols
+
+import (
+ "testing"
+
+ "github.com/traefik/yaegi/interp"
+ "github.com/traefik/yaegi/stdlib"
+)
+
+func TestYaegiSmoke(t *testing.T) {
+ i := interp.New(interp.Options{})
+ i.Use(stdlib.Symbols)
+
+ _, err := i.Eval(`import "fmt"`)
+ if err != nil {
+ t.Fatalf("import failed: %v", err)
+ }
+
+ v, err := i.Eval(`fmt.Sprintf("hello %s", "yaegi")`)
+ if err != nil {
+ t.Fatalf("eval failed: %v", err)
+ }
+ if v.String() != "hello yaegi" {
+ t.Fatalf("unexpected result: %s", v.String())
+ }
+}