package daemon import ( "context" "sort" "strings" "testing" "banger/internal/rpc" ) // 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.cache.prune", "image.delete", "image.list", "image.promote", "image.pull", "image.register", "image.show", "kernel.catalog", "kernel.delete", "kernel.import", "kernel.list", "kernel.pull", "kernel.show", "daemon.operations.list", "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) } } } // TestDispatchStampsOpIDOnError pins the contract that every error // response leaving dispatch carries an op_id, even on the // short-circuit paths (bad_version, unknown_method) that never // reach a handler. Operators rely on this id to correlate a CLI // failure to a daemon log line. func TestDispatchStampsOpIDOnError(t *testing.T) { d := &Daemon{} t.Run("unknown_method", func(t *testing.T) { resp := d.dispatch(context.Background(), rpc.Request{Version: rpc.Version, Method: "no.such.method"}) if resp.OK { t.Fatalf("expected error response, got %+v", resp) } if resp.Error == nil || resp.Error.Code != "unknown_method" { t.Fatalf("error = %+v, want unknown_method", resp.Error) } if !strings.HasPrefix(resp.Error.OpID, "op-") { t.Fatalf("op_id = %q, want op-* prefix", resp.Error.OpID) } }) t.Run("bad_version", func(t *testing.T) { resp := d.dispatch(context.Background(), rpc.Request{Version: rpc.Version + 99, Method: "ping"}) if resp.OK { t.Fatalf("expected error response, got %+v", resp) } if resp.Error == nil || resp.Error.Code != "bad_version" { t.Fatalf("error = %+v, want bad_version", resp.Error) } if !strings.HasPrefix(resp.Error.OpID, "op-") { t.Fatalf("op_id = %q, want op-* prefix", resp.Error.OpID) } }) } // TestDispatchPropagatesOpIDFromContext covers the case where a // handler returns its own rpc.NewError with an empty op_id (most // service errors do); the dispatch wrapper must stamp the // dispatch-generated id on the way out. func TestDispatchPropagatesOpIDFromContext(t *testing.T) { d := &Daemon{ requestHandler: func(_ context.Context, _ rpc.Request) rpc.Response { return rpc.NewError("operation_failed", "deliberate test failure") }, } resp := d.dispatch(context.Background(), rpc.Request{Version: rpc.Version, Method: "anything"}) if resp.OK || resp.Error == nil { t.Fatalf("expected error response, got %+v", resp) } if !strings.HasPrefix(resp.Error.OpID, "op-") { t.Fatalf("dispatch did not stamp op_id: %+v", resp.Error) } }