Skip to content

Running with Native Go

A Miru Go extension’s single source file can be driven in two stages. This page covers Stage 1 · Native Go (development): write the extension as an ordinary Go package to get full IDE support and unit testing.

Key point: Stage 1 and Stage 2 (Running with Scriggo) use the same source file. Logic you get working locally with native Go is compiled and executed unchanged by Scriggo at runtime, so there is no “works locally but fails once installed” gap. How the host drives it at runtime is on Running with Scriggo.

Terminal window
mkdir my.extension && cd my.extension
go mod init my.extension
go get github.com/miru-project/miru-core/pkg/extension/golang/sdk

my.extension.go (same shape as in Detail Usage, just import sdk directly):

package myextension
import sdk "github.com/miru-project/miru-core/pkg/extension/golang/sdk"
func Search(pkg, kw string, page int, filter string) ([]sdk.ExtensionListItem, error) {
// Debug it like any normal Go function — breakpoints, unit tests, etc.
return []sdk.ExtensionListItem{{Title: kw, URL: "https://example.com/1"}}, nil
}

You can use gopls autocomplete, go build type-checking, and call entry functions like ordinary functions — exactly like any Go project.

Add my.extension_test.go and confirm each entry function returns the right shape with go test:

package myextension
import "testing"
func TestSearch(t *testing.T) {
items, err := Search("my.extension", "test", 1, "")
if err != nil {
t.Fatal(err)
}
if len(items) == 0 {
t.Fatal("want at least one item")
}
}

Stage 1 is a normal Go package, so every standard Go debugging tool works — this is its biggest advantage.

Don’t just eyeball the return value — encode your expectations in tests and use -v to see each case:

Terminal window
go test -v ./...
go test -run TestSearch -v # run a single entry point
go test -run TestDetail -v -count=1 # -count=1 disables caching, forces a rerun

Prefer table-driven tests to cover many inputs (different page / kw / login-required):

func TestSearch(t *testing.T) {
cases := []struct {
name string
kw string
page int
want int // minimum expected item count
}{
{"normal keyword", "naruto", 1, 1},
{"empty result", "zzz_no_such_thing", 1, 0},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
items, err := Search("my.extension", c.kw, c.page, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(items) < c.want {
t.Fatalf("want >= %d items, got %d", c.want, len(items))
}
})
}
}
  • Quick prints: fmt.Printf("DEBUG kw=%q items=%d\n", kw, len(items)); remove or switch to log once confirmed.
  • For timestamps/line numbers: import "log" then log.Printf(...).
  • Your fmt output is not visible at Scriggo runtime (that’s host-side), so verify everything you need to see via go test in Stage 1.

Install Delve, then break into your extension source directly:

Terminal window
dlv test -- -test.run TestSearch # breakpoint-debug inside a test
dlv debug # if you have a main to debug

In VS Code / Zed, configure the Go debugger (built on Delve) for graphical stepping and variable inspection.

  • gopls gives completion, jump-to-definition, and hover docs — type errors surface as you type.
  • Before submitting, run go vet ./... and go build ./... to catch low-level issues (e.g. unused variables) before they reach Scriggo runtime.

The pkg/extension/golang/extensions/example package inside miru-core is exactly this “one source, two stages” reference — it can be imported and called directly by native Go, and also compiled and run by Scriggo.