344 lines
10 KiB
Go
344 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
toki "git.toki-labs.com/toki/common-proto-socket/go"
|
|
edgenode "iop/apps/edge/internal/node"
|
|
iop "iop/proto/gen/iop"
|
|
)
|
|
|
|
func TestBuildRunRequest_SessionAndBackground(t *testing.T) {
|
|
req, runID, err := buildRunRequest("cli", "codex", "session-a", true, 30, "hello")
|
|
if err != nil {
|
|
t.Fatalf("buildRunRequest: %v", err)
|
|
}
|
|
if runID == "" {
|
|
t.Fatal("expected non-empty runID")
|
|
}
|
|
if req.GetAdapter() != "cli" {
|
|
t.Errorf("Adapter: got %q want %q", req.GetAdapter(), "cli")
|
|
}
|
|
if req.GetModel() != "codex" {
|
|
t.Errorf("wire model: got %q want %q", req.GetModel(), "codex")
|
|
}
|
|
if req.GetSessionId() != "session-a" {
|
|
t.Errorf("SessionId: got %q want %q", req.GetSessionId(), "session-a")
|
|
}
|
|
if !req.GetBackground() {
|
|
t.Error("Background: expected true")
|
|
}
|
|
if req.GetSessionMode() != iop.RunSessionMode_RUN_SESSION_MODE_CREATE_IF_MISSING {
|
|
t.Errorf("SessionMode: got %v", req.GetSessionMode())
|
|
}
|
|
}
|
|
|
|
func TestBuildRunRequest_EmptySessionNormalized(t *testing.T) {
|
|
req, _, err := buildRunRequest("cli", "codex", "", false, 30, "hello")
|
|
if err != nil {
|
|
t.Fatalf("buildRunRequest: %v", err)
|
|
}
|
|
if req.GetSessionId() != "default" {
|
|
t.Errorf("SessionId: got %q want %q", req.GetSessionId(), "default")
|
|
}
|
|
}
|
|
|
|
func TestBuildRunRequest_DefaultTimeoutFallback(t *testing.T) {
|
|
req, _, err := buildRunRequest("cli", "codex", "s1", false, 0, "hello")
|
|
if err != nil {
|
|
t.Fatalf("buildRunRequest: %v", err)
|
|
}
|
|
if req.GetTimeoutSec() != 30 {
|
|
t.Errorf("TimeoutSec: got %d want 30", req.GetTimeoutSec())
|
|
}
|
|
}
|
|
|
|
func TestNormalizeConsoleSessionID(t *testing.T) {
|
|
if got := normalizeConsoleSessionID(""); got != "default" {
|
|
t.Errorf("empty: got %q want %q", got, "default")
|
|
}
|
|
if got := normalizeConsoleSessionID("my-session"); got != "my-session" {
|
|
t.Errorf("non-empty: got %q want %q", got, "my-session")
|
|
}
|
|
}
|
|
|
|
func TestConsoleEventRouterRoutesRegisteredRun(t *testing.T) {
|
|
var out bytes.Buffer
|
|
router := newConsoleEventRouter(&out, nil, nil)
|
|
events, unregister := router.Register("run-1")
|
|
defer unregister()
|
|
|
|
router.Handle(&iop.RunEvent{RunId: "run-1", Type: "start"})
|
|
|
|
select {
|
|
case event := <-events:
|
|
if event.GetRunId() != "run-1" {
|
|
t.Fatalf("RunId: got %q want run-1", event.GetRunId())
|
|
}
|
|
default:
|
|
t.Fatal("expected registered run event")
|
|
}
|
|
if out.Len() != 0 {
|
|
t.Fatalf("registered foreground event should not be printed asynchronously: %q", out.String())
|
|
}
|
|
}
|
|
|
|
func TestConsoleEventRouterPrintsUnregisteredRun(t *testing.T) {
|
|
var out bytes.Buffer
|
|
router := newConsoleEventRouter(&out, nil, nil)
|
|
|
|
router.Handle(&iop.RunEvent{RunId: "run-bg", Type: "start", SessionId: "s1", Background: true})
|
|
router.Handle(&iop.RunEvent{RunId: "run-bg", Type: "delta", Delta: "hello"})
|
|
if got := out.String(); !strings.Contains(got, "[node-unknown-message] hello") {
|
|
t.Fatalf("expected delta to be printed before completion, got:\n%s", got)
|
|
}
|
|
router.Handle(&iop.RunEvent{RunId: "run-bg", Type: "delta", Delta: " world\n"})
|
|
router.Handle(&iop.RunEvent{RunId: "run-bg", Type: "complete", Message: "done"})
|
|
|
|
got := out.String()
|
|
for _, want := range []string{
|
|
"[node-unknown-event] start run_id=run-bg session=s1 background=true",
|
|
"[node-unknown-event] complete run_id=run-bg detail=\"done\"",
|
|
"[node-unknown-message] hello world",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("expected output to contain %q, got:\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConsoleResponseStreamWritesBeforeFinish(t *testing.T) {
|
|
var out bytes.Buffer
|
|
stream := newConsoleResponseStream(&out, "[node-test-message] ")
|
|
|
|
stream.Write("hello")
|
|
if got := out.String(); got != "[node-test-message] hello" {
|
|
t.Fatalf("Write should print immediately, got %q", got)
|
|
}
|
|
|
|
stream.Write(" world")
|
|
stream.Finish()
|
|
if got := out.String(); got != "[node-test-message] hello world\n" {
|
|
t.Fatalf("Finish should close the line, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildNodeCommandRequest_StatusUsesCurrentTarget(t *testing.T) {
|
|
req, reqID := buildNodeCommandRequest("cli", "codex", "default", 30)
|
|
if reqID == "" {
|
|
t.Fatal("expected non-empty reqID")
|
|
}
|
|
if req.GetType() != iop.NodeCommandType_NODE_COMMAND_TYPE_USAGE_STATUS {
|
|
t.Errorf("Type: got %v want %v", req.GetType(), iop.NodeCommandType_NODE_COMMAND_TYPE_USAGE_STATUS)
|
|
}
|
|
if req.GetAdapter() != "cli" {
|
|
t.Errorf("Adapter: got %q want %q", req.GetAdapter(), "cli")
|
|
}
|
|
if req.GetModel() != "codex" {
|
|
t.Errorf("Model: got %q want %q", req.GetModel(), "codex")
|
|
}
|
|
if req.GetSessionId() != "default" {
|
|
t.Errorf("SessionId: got %q want %q", req.GetSessionId(), "default")
|
|
}
|
|
if req.GetTimeoutSec() != 30 {
|
|
t.Errorf("TimeoutSec: got %d want 30", req.GetTimeoutSec())
|
|
}
|
|
}
|
|
|
|
func TestStatusWaitTimeout_UsesNormalizedRequestTimeout(t *testing.T) {
|
|
// 0 should be normalized to 30, and then +5 added
|
|
req, _ := buildNodeCommandRequest("cli", "codex", "default", 0)
|
|
got := statusWaitTimeout(req)
|
|
want := 35 * time.Second
|
|
if got != want {
|
|
t.Errorf("expected %v, got %v", want, got)
|
|
}
|
|
|
|
// Explicit 60 should result in 65
|
|
req2, _ := buildNodeCommandRequest("cli", "codex", "default", 60)
|
|
got2 := statusWaitTimeout(req2)
|
|
want2 := 65 * time.Second
|
|
if got2 != want2 {
|
|
t.Errorf("expected %v, got %v", want2, got2)
|
|
}
|
|
}
|
|
|
|
func TestFormatUsageStatus_PrintsParsedLimits(t *testing.T) {
|
|
var out bytes.Buffer
|
|
formatUsageStatus(&out, "local-node", "codex", "default", &iop.AgentUsageStatus{
|
|
DailyLimit: "98% left",
|
|
WeeklyLimit: "22% left",
|
|
})
|
|
got := out.String()
|
|
if !strings.Contains(got, "Daily limit: 98% left") {
|
|
t.Errorf("expected Daily limit: 98%% left, got:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "Weekly limit: 22% left") {
|
|
t.Errorf("expected Weekly limit: 22%% left, got:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestFormatUsageStatus_Error(t *testing.T) {
|
|
// The node reports an error string directly inside the command response, which is checked before formatUsageStatus is called.
|
|
// But we should test if raw status block is printed properly if no limits are parsed.
|
|
var out bytes.Buffer
|
|
formatUsageStatus(&out, "local-node", "codex", "default", &iop.AgentUsageStatus{
|
|
RawOutput: "unparsed error text",
|
|
})
|
|
got := out.String()
|
|
if !strings.Contains(got, "raw output did not include parsed limits:") {
|
|
t.Errorf("expected raw output warning, got:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "unparsed error text") {
|
|
t.Errorf("expected unparsed text, got:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveConsoleNode_RequiresExplicitSelectionForMultipleNodes(t *testing.T) {
|
|
reg := edgenode.NewRegistry()
|
|
reg.Register(&edgenode.NodeEntry{NodeID: "node-1"})
|
|
reg.Register(&edgenode.NodeEntry{NodeID: "node-2"})
|
|
|
|
// Empty target should fail for multiple nodes
|
|
if _, err := resolveConsoleNode(reg, ""); err == nil {
|
|
t.Error("expected error for implicit resolve with multiple nodes")
|
|
}
|
|
|
|
// Explicit target should succeed
|
|
if e, err := resolveConsoleNode(reg, "node-1"); err != nil || e.NodeID != "node-1" {
|
|
t.Errorf("failed explicit resolve: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveConsoleNode_AllowsSingleNodeFallback(t *testing.T) {
|
|
reg := edgenode.NewRegistry()
|
|
reg.Register(&edgenode.NodeEntry{NodeID: "node-1"})
|
|
|
|
if e, err := resolveConsoleNode(reg, ""); err != nil || e.NodeID != "node-1" {
|
|
t.Errorf("expected single node fallback, got error: %v", err)
|
|
}
|
|
}
|
|
|
|
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"})
|
|
|
|
var out bytes.Buffer
|
|
printNodes(&out, reg, "alias-1")
|
|
|
|
got := out.String()
|
|
if !strings.Contains(got, "* node-1 (alias-1)") {
|
|
t.Errorf("expected selected node-1 to have marker, got:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, " node-2 (alias-2)") {
|
|
t.Errorf("expected non-selected node-2 to have no marker, got:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestSendTerminateSession_ReturnsNodeLabel(t *testing.T) {
|
|
reg := edgenode.NewRegistry()
|
|
reg.Register(&edgenode.NodeEntry{
|
|
NodeID: "node-1",
|
|
Alias: "alias-1",
|
|
Client: &toki.TcpClient{}, // mock client
|
|
})
|
|
|
|
target := &consoleTarget{
|
|
NodeRef: "alias-1",
|
|
SessionID: "s1",
|
|
}
|
|
|
|
label, _ := sendTerminateSession(context.Background(), reg, target)
|
|
if label != "alias-1" {
|
|
t.Errorf("expected label alias-1, got %q", label)
|
|
}
|
|
|
|
// Test fallback to NodeID
|
|
reg2 := edgenode.NewRegistry()
|
|
reg2.Register(&edgenode.NodeEntry{
|
|
NodeID: "node-2",
|
|
Client: &toki.TcpClient{},
|
|
})
|
|
target.NodeRef = "node-2"
|
|
label2, _ := sendTerminateSession(context.Background(), reg2, target)
|
|
if label2 != "node-2" {
|
|
t.Errorf("expected label node-2, got %q", label2)
|
|
}
|
|
}
|
|
|
|
func TestHandleTerminateSession_OutputFormat(t *testing.T) {
|
|
// Mock sendTerminateSessionFunc to avoid network/toki dependency
|
|
oldFunc := sendTerminateSessionFunc
|
|
defer func() { sendTerminateSessionFunc = oldFunc }()
|
|
|
|
sendTerminateSessionFunc = func(ctx context.Context, registry *edgenode.Registry, target *consoleTarget) (string, error) {
|
|
return "mock-alias", nil
|
|
}
|
|
|
|
reg := edgenode.NewRegistry()
|
|
target := &consoleTarget{
|
|
SessionID: "s-test",
|
|
}
|
|
|
|
var out bytes.Buffer
|
|
handleTerminateSession(context.Background(), reg, &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)
|
|
}
|
|
|
|
// Test fallback to NodeID in output
|
|
sendTerminateSessionFunc = func(ctx context.Context, registry *edgenode.Registry, target *consoleTarget) (string, error) {
|
|
return "node-raw-id", nil
|
|
}
|
|
out.Reset()
|
|
handleTerminateSession(context.Background(), reg, &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)
|
|
}
|
|
}
|
|
|
|
func TestConsoleEventRouterPrintsNodeScopedAsyncRun(t *testing.T) {
|
|
var out bytes.Buffer
|
|
reg := edgenode.NewRegistry()
|
|
reg.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "alias-1"})
|
|
router := newConsoleEventRouter(&out, reg, nil)
|
|
|
|
router.Handle(&iop.RunEvent{
|
|
RunId: "run-1",
|
|
Type: "start",
|
|
NodeId: "node-1",
|
|
Background: true,
|
|
})
|
|
|
|
got := out.String()
|
|
if !strings.Contains(got, "[node-alias-1-event] start run_id=run-1") {
|
|
t.Errorf("expected node-alias-1-event label, got:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestConsoleEventRouterFallsBackToNodeID(t *testing.T) {
|
|
var out bytes.Buffer
|
|
router := newConsoleEventRouter(&out, nil, nil)
|
|
|
|
router.Handle(&iop.RunEvent{
|
|
RunId: "run-1",
|
|
Type: "start",
|
|
NodeId: "some-raw-id",
|
|
Background: true,
|
|
})
|
|
|
|
got := out.String()
|
|
if !strings.Contains(got, "[node-some-raw-id-event] start run_id=run-1") {
|
|
t.Errorf("expected node-some-raw-id-event label, got:\n%s", got)
|
|
}
|
|
}
|