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:

  • --name and --version are required; the version must not start with v.
  • The gRPC server binds :0 (a random free port) by default and logs the address it picked. The operator chart sets :8483 in-cluster; pass --grpc-server-bind-address :8483 if 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=true for debug logging (the template's main.go reads it via viper).
  • Set OTLP_DSN to 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.1 works like any Go program.
  • Introspection: go run ./cmd tools components-info confirms 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