The dispatch method was a single ~240-line switch of 34 cases, each
following the same pattern: decode params into some type P, call a
service method returning (R, error), wrap R in a result struct and
either marshalResultOrError-encode or return a raw rpc.NewError.
Adding a method was a 4-line ceremony per site, and grepping for
"methods banger speaks" meant reading the full switch.
New shape, in internal/daemon/dispatch.go:
- handler is the uniform `func(ctx, d, req) rpc.Response` type
every method dispatches through.
- paramHandler[P, R] is the generic wrapper that absorbs 28 of
the 34 cases (decode, call, marshal). No reflection — P and R
are deduced from the service-call literal, so each map entry
is a one-liner referencing a small adapter func.
- noParamHandler[R] is the decode-free variant for 6 methods
that don't carry params.
- rpcHandlers is the single source of truth for which methods
exist and which adapter they dispatch to.
- Four specials (ping, shutdown, vm.logs, vm.ssh) stay as named
`handler`-typed functions: ping/shutdown encode with raw
rpc.NewResult, vm.logs/vm.ssh need pre-service validation to
emit distinct error codes (not_found, not_running) that the
generic wrapper maps uniformly to operation_failed.
Daemon.dispatch shrinks from a 240-line switch to 11 lines:
version check, test-only handler short-circuit, table lookup,
invoke-or-unknown.
Tests:
- TestRPCHandlersMatchDocumentedMethods — keyset guard. Adding
or removing a method without updating the expected slice is a
red flag the test surfaces.
- TestRPCHandlersAllNonNil — catches nil-function registrations.
All pre-existing dispatch tests (param decode, error codes, etc.)
keep passing unchanged — the handler contract for any given
method is byte-identical from the RPC client's perspective. Smoke
(all 21 scenarios) exercises every code path end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
84 lines
1.9 KiB
Go
84 lines
1.9 KiB
Go
package daemon
|
|
|
|
import (
|
|
"sort"
|
|
"testing"
|
|
)
|
|
|
|
// TestRPCHandlersMatchDocumentedMethods pins the surface of the RPC
|
|
// table: adding or removing a method should be an explicit, reviewable
|
|
// change. If the keyset drifts and this test isn't updated alongside,
|
|
// that's a red flag — either the documented list is stale, or a
|
|
// method sneaked in without being discussed.
|
|
//
|
|
// The expected list is the single source of truth for "methods
|
|
// banger speaks." Any production code consulting it (CLI completions,
|
|
// docs generator) can grep this test.
|
|
func TestRPCHandlersMatchDocumentedMethods(t *testing.T) {
|
|
expected := []string{
|
|
"image.delete",
|
|
"image.list",
|
|
"image.promote",
|
|
"image.pull",
|
|
"image.register",
|
|
"image.show",
|
|
|
|
"kernel.catalog",
|
|
"kernel.delete",
|
|
"kernel.import",
|
|
"kernel.list",
|
|
"kernel.pull",
|
|
"kernel.show",
|
|
|
|
"ping",
|
|
"shutdown",
|
|
|
|
"vm.create",
|
|
"vm.create.begin",
|
|
"vm.create.cancel",
|
|
"vm.create.status",
|
|
"vm.delete",
|
|
"vm.health",
|
|
"vm.kill",
|
|
"vm.list",
|
|
"vm.logs",
|
|
"vm.ping",
|
|
"vm.ports",
|
|
"vm.restart",
|
|
"vm.set",
|
|
"vm.show",
|
|
"vm.ssh",
|
|
"vm.start",
|
|
"vm.stats",
|
|
"vm.stop",
|
|
|
|
"vm.workspace.export",
|
|
"vm.workspace.prepare",
|
|
}
|
|
|
|
got := make([]string, 0, len(rpcHandlers))
|
|
for name := range rpcHandlers {
|
|
got = append(got, name)
|
|
}
|
|
sort.Strings(got)
|
|
sort.Strings(expected)
|
|
|
|
if len(got) != len(expected) {
|
|
t.Fatalf("method count: got %d, want %d\n got: %v\n want: %v", len(got), len(expected), got, expected)
|
|
}
|
|
for i := range expected {
|
|
if got[i] != expected[i] {
|
|
t.Fatalf("method[%d]: got %q, want %q\n full got: %v\n full want: %v", i, got[i], expected[i], got, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRPCHandlersAllNonNil catches a silly-but-possible footgun:
|
|
// registering a method with a nil function literal.
|
|
func TestRPCHandlersAllNonNil(t *testing.T) {
|
|
for name, h := range rpcHandlers {
|
|
if h == nil {
|
|
t.Errorf("rpcHandlers[%q] = nil", name)
|
|
}
|
|
}
|
|
}
|