Local Testing
A module is a normal Go program, so most testing is plain go test plus running the binary from source against a development cluster.
Running Locally
The module's own run command connects to whatever cluster your kubeconfig points at:
cd my-module
go run ./cmd run --name my-module --version 0.0.1 \
--kubeconfig ~/.kube/config --namespace tinysystems
Notes:
--nameand--versionare required; the version must not start withv.- The gRPC server binds
:0(a random free port) by default and logs the address it picked. The operator chart sets:8483in-cluster; pass--grpc-server-bind-address :8483if you want the same port locally. - Metrics and probes also default to
:0; pin them the same way if you need stable ports. - Set
DEBUG=truefor debug logging (the template'smain.goreads it via viper). - Set
OTLP_DSNto see traces; leave it unset and telemetry is simply disabled.
There is no watch/reload mode — restart the process after code changes (or use a tool like air if you want one).
With Minikube or Kind
minikube start # or: kind create cluster
tiny up # provision CRDs, broker, collector, core modules
go run ./cmd run --name my-module --version 0.0.1 --namespace tinysystems
The locally running process registers a TinyModule CR in the namespace and reconciles TinyNodes there, exactly like the in-cluster deployment would.
Port Forwarding
To reach a module already running in the cluster, forward its gRPC port (the chart binds :8483):
kubectl port-forward deploy/<module-release> 8483:8483 -n tinysystems
Unit Testing Components
Handle takes a module.Handler and returns module.Result, so components test as plain functions:
func TestEcho_Handle(t *testing.T) {
component := &Component{}
var got OutMessage
handler := func(ctx context.Context, port string, data any) module.Result {
if port == OutPort {
got = data.(OutMessage)
}
return module.Ok(nil)
}
res := component.Handle(context.Background(), handler, InPort, InMessage{Context: "test"})
if err := res.Err(); err != nil {
t.Fatalf("Handle returned error: %v", err)
}
if got.Context != "test" {
t.Fatalf("expected passthrough, got %v", got.Context)
}
}
Run with the usual tooling:
go test ./...
go test -race -cover ./components/echo/...
To simulate leader-only paths, put leadership on the context:
ctx := utils.WithLeader(context.Background(), true)
Integration Testing Against a Cluster
Create a TinyNode and assert on its status. Note the status shape: there is no .Status.State field — a TinyNode reports status.status (free-form string), status.error (bool), plus the introspected module, component, and ports:
//go:build integration
node := &v1alpha1.TinyNode{
ObjectMeta: metav1.ObjectMeta{
Name: "test-node",
Namespace: "tinysystems",
},
Spec: v1alpha1.TinyNodeSpec{
Module: "my-module",
Component: "echo",
},
}
require.NoError(t, k8sClient.Create(ctx, node))
// ... wait for reconcile ...
require.NoError(t, k8sClient.Get(ctx, client.ObjectKeyFromObject(node), node))
require.False(t, node.Status.Error)
require.NotEmpty(t, node.Status.Ports) // ports introspected from the running module
go test -tags=integration ./...
Triggering Flows Manually
There is no TinySignal CR — it was removed. To push a test message into a running flow, use the MCP send_signal tool exposed by the tiny dev server (any MCP client, e.g. Claude Code, can call it):
tiny # starts the dev server + MCP endpoint
Then, from your MCP client: send_signal with the node, port, and payload. Execution traces for the run land in the telemetry panel (or via the get_traces tool), which is the fastest way to see where a message stopped.
For HTTP-serving components, plain curl against the exposed port also works:
kubectl port-forward svc/<service> 8080:8080 -n tinysystems
curl -X POST http://localhost:8080/api/test -d '{"message": "test"}'
Debugging
- Logs: the module logs with zerolog; locally they go to your terminal, in-cluster use
kubectl logs. - Delve:
dlv debug ./cmd -- run --name my-module --version 0.0.1works like any Go program. - Introspection:
go run ./cmd tools components-infoconfirms what the binary registers, and warns on non-conformant error ports. - RBAC: a component that 403s in-cluster but works locally usually has an RBAC gap — run
go run ./cmd tools rbac-check.
CI
Standard Go CI applies:
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.25'
- name: Run tests
run: go test -v -race ./...
- name: RBAC coverage gate
run: go run ./cmd tools rbac-check --strict
Next Steps
- Development Commands - Command reference
- Building Modules - Production builds