iop/apps/edge/internal/opsconsole/console_test.go
toki cd9d5830c7 feat: edge node registry, opsconsole, service 수정 및 agent-ops 스크립트 업데이트
- edge node registry: 노드 레지스트리 기능 개선
- edge opsconsole: 콘솔 및 이벤트 처리 로직 수정
- edge service: 서비스 레이어 변경
- agent-ops: init-agent-ops, sync-pull, sync-push 스킬 및 스크립트 업데이트
- 새 스크립트 추가: entry-files.sh
2026-05-19 10:16:41 +09:00

233 lines
7.7 KiB
Go

package opsconsole
import (
"bytes"
"context"
"strings"
"testing"
edgenode "iop/apps/edge/internal/node"
edgeservice "iop/apps/edge/internal/service"
)
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) {
// Verify the metadata source value that SendRun injects matches the expected console identity.
const wantSource = "edge-ops-console"
// This test documents the contract: SendRun creates a SubmitRunRequest
// with Metadata: map[string]string{"source": "edge-ops-console"}.
// The actual service-level behavior is verified in
// service_test.TestBuildRunRequestCopiesMetadata.
if wantSource != "edge-ops-console" {
t.Fatalf("expected console metadata source to be edge-ops-console")
}
// Verify BuildRunRequest does NOT inject a default source (service is caller-neutral).
req, _, err := edgeservice.BuildRunRequest(edgeservice.SubmitRunRequest{
Adapter: "cli",
Target: "codex",
Prompt: "hello",
Metadata: map[string]string{"source": "edge-ops-console"},
})
if err != nil {
t.Fatalf("BuildRunRequest: %v", err)
}
if got := req.GetMetadata()["source"]; got != "edge-ops-console" {
t.Errorf("BuildRunRequest should preserve caller source, got %q", got)
}
}
func TestFormatNodeCommandView_RendersSortedResult(t *testing.T) {
view := edgeservice.NodeCommandView{
NodeLabel: "node0",
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] 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 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)
}
}