- add console_events.go for edge console event handling - add codex_exec.go for CLI codex execution - add codex_exec_test.go for persistent CLI tests - update console.go and oneshot.go with new features - update config and edge.yaml for new settings - update README files for edge and node apps
247 lines
7 KiB
Go
247 lines
7 KiB
Go
package oneshot_test
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
clipkg "iop/apps/node/internal/adapters/cli"
|
|
"iop/apps/node/internal/adapters/cli/internal/testutil"
|
|
noderuntime "iop/apps/node/internal/runtime"
|
|
"iop/packages/config"
|
|
)
|
|
|
|
func TestCLIExecuteOneShotPassesPromptAsArg(t *testing.T) {
|
|
testutil.RequireUnixShell(t)
|
|
|
|
cfg := config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"echo": {
|
|
Command: "sh",
|
|
Args: []string{"-c", `printf "reply:%s\n" "$1"`, "sh"},
|
|
},
|
|
},
|
|
}
|
|
c := clipkg.New(cfg, zap.NewNop())
|
|
sink := &testutil.FakeSink{}
|
|
|
|
err := c.Execute(context.Background(), noderuntime.ExecutionSpec{
|
|
RunID: "run-1",
|
|
Model: "echo",
|
|
Input: map[string]any{"prompt": "hello"},
|
|
}, sink)
|
|
if err != nil {
|
|
t.Fatalf("execute: %v", err)
|
|
}
|
|
|
|
events := sink.Events()
|
|
var types []string
|
|
for _, e := range events {
|
|
types = append(types, string(e.Type))
|
|
}
|
|
if len(events) < 3 {
|
|
t.Fatalf("expected at least 3 events (start/delta/complete), got %v", types)
|
|
}
|
|
if events[0].Type != noderuntime.EventTypeStart {
|
|
t.Fatalf("first event should be start, got %q", events[0].Type)
|
|
}
|
|
if events[len(events)-1].Type != noderuntime.EventTypeComplete {
|
|
t.Fatalf("last event should be complete, got %q", events[len(events)-1].Type)
|
|
}
|
|
if combined := testutil.CollectDeltas(events); !strings.Contains(combined, "reply:hello") {
|
|
t.Fatalf("expected reply:hello in deltas, got %q", combined)
|
|
}
|
|
}
|
|
|
|
func TestCLIExecuteOneShotStreamJSONParsesAssistantContent(t *testing.T) {
|
|
testutil.RequireUnixShell(t)
|
|
|
|
script := `cat <<'EOF'
|
|
{"type":"init","timestamp":"2026-05-03T00:00:00.000Z","session_id":"abc"}
|
|
{"type":"message","role":"user","content":"hi"}
|
|
{"type":"message","role":"assistant","content":"Hello","delta":true}
|
|
{"type":"message","role":"assistant","content":" world","delta":true}
|
|
{"type":"result","status":"success"}
|
|
EOF`
|
|
cfg := config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"streamy": {
|
|
Command: "sh",
|
|
Args: []string{"-c", script, "sh"},
|
|
OutputFormat: "stream-json",
|
|
},
|
|
},
|
|
}
|
|
c := clipkg.New(cfg, zap.NewNop())
|
|
sink := &testutil.FakeSink{}
|
|
|
|
if err := c.Execute(context.Background(), noderuntime.ExecutionSpec{
|
|
RunID: "run-sj",
|
|
Model: "streamy",
|
|
Input: map[string]any{"prompt": "hi"},
|
|
}, sink); err != nil {
|
|
t.Fatalf("execute: %v", err)
|
|
}
|
|
combined := testutil.CollectDeltas(sink.Events())
|
|
if combined != "Hello world" {
|
|
t.Fatalf("expected stream-json deltas to concat to %q, got %q", "Hello world", combined)
|
|
}
|
|
}
|
|
|
|
func TestCLIExecuteOneShotCodexJSONParsesAgentMessage(t *testing.T) {
|
|
testutil.RequireUnixShell(t)
|
|
|
|
script := `cat <<'EOF'
|
|
{"type":"thread.started","thread_id":"019d-aaa"}
|
|
{"type":"turn.started"}
|
|
{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"Hi there."}}
|
|
{"type":"turn.completed","usage":{"input_tokens":10,"output_tokens":3}}
|
|
EOF`
|
|
cfg := config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"codexy": {
|
|
Command: "sh",
|
|
Args: []string{"-c", script, "sh"},
|
|
OutputFormat: "codex-json",
|
|
},
|
|
},
|
|
}
|
|
c := clipkg.New(cfg, zap.NewNop())
|
|
sink := &testutil.FakeSink{}
|
|
|
|
if err := c.Execute(context.Background(), noderuntime.ExecutionSpec{
|
|
RunID: "run-cj",
|
|
Model: "codexy",
|
|
Input: map[string]any{"prompt": "hi"},
|
|
}, sink); err != nil {
|
|
t.Fatalf("execute: %v", err)
|
|
}
|
|
combined := testutil.CollectDeltas(sink.Events())
|
|
if combined != "Hi there." {
|
|
t.Fatalf("expected codex-json deltas to be %q, got %q", "Hi there.", combined)
|
|
}
|
|
}
|
|
|
|
func TestCLIExecuteOneShotClaudeJSONParsesContentBlockDeltas(t *testing.T) {
|
|
testutil.RequireUnixShell(t)
|
|
|
|
script := `cat <<'EOF'
|
|
{"type":"system","subtype":"init","session_id":"abc"}
|
|
{"type":"stream_event","event":{"type":"message_start","message":{"id":"msg_1"}}}
|
|
{"type":"stream_event","event":{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}}
|
|
{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}}
|
|
{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" world"}}}
|
|
{"type":"assistant","message":{"id":"msg_1","content":[{"type":"text","text":"Hello world"}]}}
|
|
{"type":"stream_event","event":{"type":"content_block_stop","index":0}}
|
|
{"type":"stream_event","event":{"type":"message_stop"}}
|
|
{"type":"result","subtype":"success","is_error":false,"result":"Hello world"}
|
|
EOF`
|
|
cfg := config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"claudish": {
|
|
Command: "sh",
|
|
Args: []string{"-c", script, "sh"},
|
|
OutputFormat: "claude-json",
|
|
},
|
|
},
|
|
}
|
|
c := clipkg.New(cfg, zap.NewNop())
|
|
sink := &testutil.FakeSink{}
|
|
|
|
if err := c.Execute(context.Background(), noderuntime.ExecutionSpec{
|
|
RunID: "run-cl",
|
|
Model: "claudish",
|
|
Input: map[string]any{"prompt": "hi"},
|
|
}, sink); err != nil {
|
|
t.Fatalf("execute: %v", err)
|
|
}
|
|
combined := testutil.CollectDeltas(sink.Events())
|
|
if combined != "Hello world" {
|
|
t.Fatalf("expected claude-json deltas to be %q, got %q", "Hello world", combined)
|
|
}
|
|
}
|
|
|
|
func TestCLIExecuteOneShotDrainsStderrConcurrently(t *testing.T) {
|
|
testutil.RequireUnixShell(t)
|
|
|
|
cfg := config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"noisy": {
|
|
Command: "sh",
|
|
Args: []string{
|
|
"-c",
|
|
`i=0; while [ "$i" -lt 20000 ]; do echo "warning line $i" >&2; i=$((i+1)); done; printf "reply:%s\n" "$1"`,
|
|
"sh",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
c := clipkg.New(cfg, zap.NewNop())
|
|
sink := &testutil.FakeSink{}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
|
|
err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
|
RunID: "run-noisy",
|
|
Model: "noisy",
|
|
Input: map[string]any{"prompt": "hello"},
|
|
}, sink)
|
|
if err != nil {
|
|
t.Fatalf("execute: %v", err)
|
|
}
|
|
|
|
if combined := testutil.CollectDeltas(sink.Events()); !strings.Contains(combined, "reply:hello") {
|
|
t.Fatalf("expected reply:hello in deltas, got %q", combined)
|
|
}
|
|
}
|
|
|
|
func TestCLIExecuteOneShotStreamsStdoutChunks(t *testing.T) {
|
|
testutil.RequireUnixShell(t)
|
|
|
|
cfg := config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"chunky": {
|
|
Command: "sh",
|
|
Args: []string{
|
|
"-c",
|
|
`printf "first"; sleep 0.2; printf "second"`,
|
|
"sh",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
c := clipkg.New(cfg, zap.NewNop())
|
|
sink := &testutil.FakeSink{}
|
|
|
|
err := c.Execute(context.Background(), noderuntime.ExecutionSpec{
|
|
RunID: "run-chunky",
|
|
Model: "chunky",
|
|
Input: map[string]any{"prompt": "unused"},
|
|
}, sink)
|
|
if err != nil {
|
|
t.Fatalf("execute: %v", err)
|
|
}
|
|
|
|
var deltas []string
|
|
for _, event := range sink.Events() {
|
|
if event.Type == noderuntime.EventTypeDelta {
|
|
deltas = append(deltas, event.Delta)
|
|
}
|
|
}
|
|
if len(deltas) < 2 {
|
|
t.Fatalf("expected stdout chunks to be emitted separately, got %v", deltas)
|
|
}
|
|
if got := strings.Join(deltas, ""); got != "firstsecond" {
|
|
t.Fatalf("combined deltas: got %q want firstsecond", got)
|
|
}
|
|
}
|