- Add status_provider_test.go for queue status coverage - Update model_queue.go with queue simplification - Update run_dispatch.go with dispatch logic improvements - Update status_provider.go with status tracking - Update server tests for a2a, openai, and opsconsole - Remove archived PLAN and CODE_REVIEW documents for 02+01 surface snapshot contract - Archive 02+01_surface_snapshot_contract to 2026/06
361 lines
12 KiB
Go
361 lines
12 KiB
Go
package opsconsole
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
toki "git.toki-labs.com/toki/proto-socket/go"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
edgeevents "iop/apps/edge/internal/events"
|
|
edgenode "iop/apps/edge/internal/node"
|
|
edgeservice "iop/apps/edge/internal/service"
|
|
"iop/packages/go/config"
|
|
iop "iop/proto/gen/iop"
|
|
)
|
|
|
|
func TestParseCommand(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
input string
|
|
want Command
|
|
}{
|
|
{"empty", "", Command{Kind: CommandEmpty}},
|
|
{"whitespace", " ", Command{Kind: CommandEmpty}},
|
|
{"slash exit", "/exit", Command{Kind: CommandExit}},
|
|
{"slash quit", "/quit", Command{Kind: CommandExit}},
|
|
{"bare exit", "exit", Command{Kind: CommandExit}},
|
|
{"bare quit upper", "QUIT", Command{Kind: CommandExit}},
|
|
{"nodes", "/nodes", Command{Kind: CommandNodes}},
|
|
{"terminate", "/terminate-session", Command{Kind: CommandTerminateSession}},
|
|
{"status", "/status", Command{Kind: CommandStatus}},
|
|
{"capabilities", "/capabilities", Command{Kind: CommandCapabilities}},
|
|
{"sessions", "/sessions", Command{Kind: CommandSessionList}},
|
|
{"transport", "/transport", Command{Kind: CommandTransportStatus}},
|
|
{"node with arg", "/node alias-1", Command{Kind: CommandSetNode, Arg: "alias-1"}},
|
|
{"node missing arg", "/node ", Command{Kind: CommandSetNode, UsageErr: "usage: /node <id|alias>"}},
|
|
{"session with arg", "/session work-1", Command{Kind: CommandSetSession, Arg: "work-1"}},
|
|
{"session missing arg", "/session ", Command{Kind: CommandSetSession, UsageErr: "usage: /session <id>"}},
|
|
{"background on", "/background on", Command{Kind: CommandSetBackground, Arg: "on"}},
|
|
{"background off", "/background OFF", Command{Kind: CommandSetBackground, Arg: "off"}},
|
|
{"background invalid", "/background maybe", Command{Kind: CommandSetBackground, UsageErr: "usage: /background on|off"}},
|
|
{"background missing", "/background ", Command{Kind: CommandSetBackground, UsageErr: "usage: /background on|off"}},
|
|
{"prompt", "hello world", Command{Kind: CommandPrompt, Arg: "hello world"}},
|
|
{"prompt slash unknown is prompt? no it stays prompt", "/unknown stuff", Command{Kind: CommandPrompt, Arg: "/unknown stuff"}},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := ParseCommand(tc.input)
|
|
if got != tc.want {
|
|
t.Fatalf("ParseCommand(%q) = %+v, want %+v", tc.input, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConsoleStateSessionBackgroundNode(t *testing.T) {
|
|
state := &TargetState{
|
|
Adapter: "cli",
|
|
Target: "codex",
|
|
SessionID: "default",
|
|
Background: false,
|
|
TimeoutSec: 30,
|
|
}
|
|
|
|
apply := func(line string) {
|
|
t.Helper()
|
|
cmd := ParseCommand(line)
|
|
switch cmd.Kind {
|
|
case CommandSetNode:
|
|
if cmd.UsageErr != "" {
|
|
t.Fatalf("unexpected usage error for %q: %s", line, cmd.UsageErr)
|
|
}
|
|
state.NodeRef = cmd.Arg
|
|
case CommandSetSession:
|
|
if cmd.UsageErr != "" {
|
|
t.Fatalf("unexpected usage error for %q: %s", line, cmd.UsageErr)
|
|
}
|
|
state.SessionID = cmd.Arg
|
|
case CommandSetBackground:
|
|
if cmd.UsageErr != "" {
|
|
t.Fatalf("unexpected usage error for %q: %s", line, cmd.UsageErr)
|
|
}
|
|
state.Background = cmd.Arg == "on"
|
|
default:
|
|
t.Fatalf("unexpected command kind %v for %q", cmd.Kind, line)
|
|
}
|
|
}
|
|
|
|
apply("/node node-7")
|
|
apply("/session worker-1")
|
|
apply("/background on")
|
|
|
|
if state.NodeRef != "node-7" {
|
|
t.Errorf("NodeRef: got %q want node-7", state.NodeRef)
|
|
}
|
|
if state.SessionID != "worker-1" {
|
|
t.Errorf("SessionID: got %q want worker-1", state.SessionID)
|
|
}
|
|
if !state.Background {
|
|
t.Error("Background: expected true")
|
|
}
|
|
|
|
apply("/background off")
|
|
if state.Background {
|
|
t.Error("Background: expected false after /background off")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeSessionID(t *testing.T) {
|
|
if got := NormalizeSessionID(""); got != "default" {
|
|
t.Errorf("empty: got %q want %q", got, "default")
|
|
}
|
|
if got := NormalizeSessionID("my-session"); got != "my-session" {
|
|
t.Errorf("non-empty: got %q want %q", got, "my-session")
|
|
}
|
|
}
|
|
|
|
func TestPrintNodes_ShowsSelectedNode(t *testing.T) {
|
|
reg := edgenode.NewRegistry()
|
|
reg.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "alias-1"})
|
|
reg.Register(&edgenode.NodeEntry{NodeID: "node-2", Alias: "alias-2"})
|
|
svc := edgeservice.New(reg, nil)
|
|
|
|
var out bytes.Buffer
|
|
PrintNodes(&out, svc.ListNodeSnapshots(), "alias-1")
|
|
|
|
got := out.String()
|
|
if !strings.Contains(got, "* node0 = node-1 (alias-1)") {
|
|
t.Errorf("expected selected node-1 to have marker, got:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, " node1 = node-2 (alias-2)") {
|
|
t.Errorf("expected non-selected node-2 to have no marker, got:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestPrintNodes_AcceptsSnapshotDTO(t *testing.T) {
|
|
var out bytes.Buffer
|
|
PrintNodes(&out, []edgeservice.NodeSnapshot{
|
|
{NodeID: "node-a", Alias: "alpha", Label: "node0"},
|
|
}, "alpha")
|
|
|
|
got := out.String()
|
|
if !strings.Contains(got, "* node0 = node-a (alpha)") {
|
|
t.Errorf("expected DTO render to mark selected, got:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestSendRun_SubmitRunRequest_MetadataSource(t *testing.T) {
|
|
// Create net.Pipe for client-server communication
|
|
edgeConn, nodeConn := net.Pipe()
|
|
defer edgeConn.Close()
|
|
defer nodeConn.Close()
|
|
|
|
parserMap := toki.ParserMap{
|
|
toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) {
|
|
m := &iop.RunRequest{}
|
|
return m, proto.Unmarshal(b, m)
|
|
},
|
|
}
|
|
|
|
edgeClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap)
|
|
nodeClient := toki.NewTcpClient(nodeConn, 0, 0, parserMap)
|
|
|
|
reqReceived := make(chan *iop.RunRequest, 1)
|
|
|
|
toki.AddListenerTyped[*iop.RunRequest](&nodeClient.Communicator, func(req *iop.RunRequest) {
|
|
reqReceived <- req
|
|
})
|
|
|
|
reg := edgenode.NewRegistry()
|
|
reg.Register(&edgenode.NodeEntry{
|
|
NodeID: "node-console-test",
|
|
Alias: "alpha",
|
|
AgentKind: "node",
|
|
Client: edgeClient,
|
|
})
|
|
|
|
// Enable queue manager in service by passing a new Bus
|
|
bus := edgeevents.NewBus()
|
|
svc := edgeservice.New(reg, bus)
|
|
|
|
// Configure NodeStore to fail for queued paths by setting CLI.Enabled = false
|
|
// while presenting the "codex" profile. This will cause resolveAdapterForNode
|
|
// to return supported = false if the queued path (which resolves candidate nodes)
|
|
// is evaluated.
|
|
store := edgenode.NewNodeStore()
|
|
store.Add(&edgenode.NodeRecord{
|
|
ID: "node-console-test",
|
|
Adapters: config.AdaptersConf{
|
|
CLI: config.CLIConf{
|
|
Enabled: false,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"codex": {},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
svc.SetNodeStore(store)
|
|
|
|
target := &TargetState{
|
|
NodeRef: "alpha",
|
|
Adapter: "cli",
|
|
Target: "codex",
|
|
SessionID: "sess-1",
|
|
Background: true,
|
|
TimeoutSec: 10,
|
|
}
|
|
|
|
var out bytes.Buffer
|
|
err := SendRun(context.Background(), svc, nil, &out, target, "test prompt")
|
|
if err != nil {
|
|
t.Fatalf("SendRun failed: %v", err)
|
|
}
|
|
|
|
select {
|
|
case req := <-reqReceived:
|
|
if req.GetAdapter() != "cli" || req.GetTarget() != "codex" {
|
|
t.Errorf("unexpected run request: %+v", req)
|
|
}
|
|
if req.GetMetadata()["source"] != "edge-ops-console" {
|
|
t.Errorf("expected source edge-ops-console, got %q", req.GetMetadata()["source"])
|
|
}
|
|
// If SendRun had set a non-empty ModelGroupKey, it would have routed through the queue.
|
|
// Since we configured CLI.Enabled = false, resolveQueueCandidates would return an error
|
|
// ("node ... does not support adapter ..."), causing SendRun to fail.
|
|
// Successful delivery confirms that SendRun maintains ModelGroupKey == "" (direct path).
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("timeout waiting for RunRequest at node")
|
|
}
|
|
}
|
|
|
|
func TestFormatNodeCommandView_RendersSortedResult(t *testing.T) {
|
|
view := edgeservice.NodeCommandView{
|
|
NodeLabel: "node0",
|
|
Adapter: "cli@local",
|
|
Target: "codex",
|
|
SessionID: "default",
|
|
Result: map[string]string{
|
|
"targets": "v1,v2",
|
|
"version": "1.0",
|
|
},
|
|
}
|
|
var out bytes.Buffer
|
|
FormatNodeCommandView(&out, "capabilities", view)
|
|
got := out.String()
|
|
if !strings.Contains(got, "[node0-capabilities] adapter=cli@local target=codex session=default") {
|
|
t.Errorf("missing header: %q", got)
|
|
}
|
|
// keys must appear in sorted order: targets before version
|
|
tIdx := strings.Index(got, "targets = v1,v2")
|
|
vIdx := strings.Index(got, "version = 1.0")
|
|
if tIdx < 0 || vIdx < 0 || tIdx > vIdx {
|
|
t.Errorf("expected sorted keys, got: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestFormatNodeCommandView_EmptyResult(t *testing.T) {
|
|
view := edgeservice.NodeCommandView{NodeLabel: "alpha", Target: "codex", SessionID: "default"}
|
|
var out bytes.Buffer
|
|
FormatNodeCommandView(&out, "sessions", view)
|
|
if !strings.Contains(out.String(), "no sessions payload returned") {
|
|
t.Errorf("expected empty-payload notice, got: %q", out.String())
|
|
}
|
|
}
|
|
|
|
func TestFormatNodeCommandView_RendersStructuredSessions(t *testing.T) {
|
|
view := edgeservice.NodeCommandView{
|
|
NodeLabel: "node0",
|
|
Adapter: "cli@local",
|
|
Target: "claude",
|
|
SessionID: "default",
|
|
Result: map[string]string{
|
|
"count": "2",
|
|
"sessions": "persistent:claude/default,opencode-sse:opencode/work",
|
|
"session.0.mode": "persistent",
|
|
"session.0.target": "claude",
|
|
"session.0.session_id": "default",
|
|
"session.1.mode": "opencode-sse",
|
|
"session.1.target": "opencode",
|
|
"session.1.session_id": "work",
|
|
},
|
|
}
|
|
var out bytes.Buffer
|
|
FormatNodeCommandView(&out, "sessions", view)
|
|
got := out.String()
|
|
if !strings.Contains(got, "[node0-sessions] adapter=cli@local target=claude session=default") {
|
|
t.Errorf("missing header: %q", got)
|
|
}
|
|
if !strings.Contains(got, "sessions: 2") {
|
|
t.Errorf("expected count line, got: %q", got)
|
|
}
|
|
if !strings.Contains(got, "[0] mode=persistent target=claude session=default") {
|
|
t.Errorf("expected session 0 group, got: %q", got)
|
|
}
|
|
if !strings.Contains(got, "[1] mode=opencode-sse target=opencode session=work") {
|
|
t.Errorf("expected session 1 group, got: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestFormatNodeCommandView_RendersTransportStatusKeys(t *testing.T) {
|
|
view := edgeservice.NodeCommandView{
|
|
NodeLabel: "node0",
|
|
Adapter: "cli@local",
|
|
Target: "claude",
|
|
SessionID: "default",
|
|
Result: map[string]string{
|
|
"connected": "true",
|
|
"state": "connected",
|
|
"node_id": "test-node",
|
|
"session_id": "default",
|
|
"adapter": "cli",
|
|
"target": "claude",
|
|
},
|
|
}
|
|
var out bytes.Buffer
|
|
FormatNodeCommandView(&out, "transport", view)
|
|
got := out.String()
|
|
if !strings.Contains(got, "[node0-transport] adapter=cli@local target=claude session=default") {
|
|
t.Errorf("missing header: %q", got)
|
|
}
|
|
for _, want := range []string{"connected = true", "state = connected", "node_id = test-node", "session_id = default"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("expected %q in transport output, got:\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandleTerminateSession_OutputFormat(t *testing.T) {
|
|
oldFunc := SendTerminateSessionFunc
|
|
defer func() { SendTerminateSessionFunc = oldFunc }()
|
|
|
|
SendTerminateSessionFunc = func(ctx context.Context, edgeSvc *edgeservice.Service, target *TargetState) (string, error) {
|
|
return "mock-alias", nil
|
|
}
|
|
|
|
target := &TargetState{SessionID: "s-test"}
|
|
|
|
var out bytes.Buffer
|
|
HandleTerminateSession(context.Background(), nil, &out, target)
|
|
|
|
got := out.String()
|
|
want := "terminated session s-test node=mock-alias\n"
|
|
if got != want {
|
|
t.Errorf("OutputFormat (alias): got %q, want %q", got, want)
|
|
}
|
|
|
|
SendTerminateSessionFunc = func(ctx context.Context, edgeSvc *edgeservice.Service, target *TargetState) (string, error) {
|
|
return "node-raw-id", nil
|
|
}
|
|
out.Reset()
|
|
HandleTerminateSession(context.Background(), nil, &out, target)
|
|
got2 := out.String()
|
|
want2 := "terminated session s-test node=node-raw-id\n"
|
|
if got2 != want2 {
|
|
t.Errorf("OutputFormat (node_id): got %q, want %q", got2, want2)
|
|
}
|
|
}
|