package cli import ( "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "testing" "time" "iop/packages/go/config" ) func TestParseOpencodeRunArgs(t *testing.T) { args := []string{ "--attach", "http://127.0.0.1:9876", "--model", "ollama-dgx/qwen3.6:35b-a3b-bf16", "--agent", "build", "--variant", "fast", "--title", "demo", "--dir", "/work", "--dangerously-skip-permissions", } opts := parseOpencodeRunArgs(args) if opts.AttachURL != "http://127.0.0.1:9876" { t.Errorf("attach: got %q", opts.AttachURL) } if opts.Model != "ollama-dgx/qwen3.6:35b-a3b-bf16" { t.Errorf("model: got %q", opts.Model) } if opts.Agent != "build" { t.Errorf("agent: got %q", opts.Agent) } if opts.Variant != "fast" { t.Errorf("variant: got %q", opts.Variant) } if opts.Title != "demo" { t.Errorf("title: got %q", opts.Title) } if opts.Dir != "/work" { t.Errorf("dir: got %q", opts.Dir) } if !opts.DangerouslySkipPermissions { t.Error("dangerously-skip-permissions should be true") } } func TestParseOpencodeRunArgs_NoAttach(t *testing.T) { args := []string{"--title", "untitled", "--model", "m"} opts := parseOpencodeRunArgs(args) if opts.AttachURL != "" { t.Errorf("expected empty attach, got %q", opts.AttachURL) } if opts.Title != "untitled" || opts.Model != "m" { t.Errorf("unexpected: %+v", opts) } } func TestOpencodeModelPayload(t *testing.T) { cases := []struct { in string wantProv string wantMod string wantErr bool }{ {"ollama-dgx/qwen3.6:35b-a3b-bf16", "ollama-dgx", "qwen3.6:35b-a3b-bf16", false}, {"anthropic/claude-sonnet-4-6", "anthropic", "claude-sonnet-4-6", false}, {"openrouter/google/gemini-2.5-pro", "openrouter", "google/gemini-2.5-pro", false}, {"missing-slash", "", "", true}, {"/missing-provider", "", "", true}, {"missing-model/", "", "", true}, } for _, tc := range cases { got, err := opencodeModelPayload(tc.in) if tc.wantErr { if err == nil { t.Errorf("opencodeModelPayload(%q): expected error, got nil", tc.in) } continue } if err != nil { t.Errorf("opencodeModelPayload(%q): unexpected error %v", tc.in, err) continue } if got["providerID"] != tc.wantProv { t.Errorf("opencodeModelPayload(%q) providerID = %v, want %q", tc.in, got["providerID"], tc.wantProv) } if got["modelID"] != tc.wantMod { t.Errorf("opencodeModelPayload(%q) modelID = %v, want %q", tc.in, got["modelID"], tc.wantMod) } } } func TestOpencodeStatusIdle(t *testing.T) { cases := []struct { name string props map[string]any want bool }{ {"string idle", map[string]any{"status": "idle"}, true}, {"object idle", map[string]any{"status": map[string]any{"type": "idle"}}, true}, {"object busy", map[string]any{"status": map[string]any{"type": "busy"}}, false}, {"string busy", map[string]any{"status": "busy"}, false}, {"missing", map[string]any{}, false}, {"non-map number", map[string]any{"status": 1.0}, false}, {"nil props", nil, false}, } for _, tc := range cases { if got := opencodeStatusIdle(tc.props); got != tc.want { t.Errorf("opencodeStatusIdle(%s) = %v, want %v", tc.name, got, tc.want) } } } func TestOpencodeEventEnvelope(t *testing.T) { cases := []struct { name string in string want string ok bool }{ { name: "plain event", in: `{"id":"evt_1","type":"session.idle","properties":{"sessionID":"ses_1"}}`, want: "session.idle", ok: true, }, { name: "global payload event", in: `{"directory":"/work","payload":{"id":"evt_1","type":"message.part.delta","properties":{"sessionID":"ses_1"}}}`, want: "message.part.delta", ok: true, }, { name: "sync event ignored", in: `{"directory":"/work","payload":{"type":"sync","syncEvent":{"type":"message.part.delta.1"}}}`, ok: false, }, } for _, tc := range cases { got, ok := opencodeEventEnvelope([]byte(tc.in)) if ok != tc.ok { t.Fatalf("%s: ok=%v, want %v", tc.name, ok, tc.ok) } if ok && got.Type != tc.want { t.Fatalf("%s: type=%q, want %q", tc.name, got.Type, tc.want) } } } func TestOpencodeMessagePartDeltaText(t *testing.T) { partTypes := map[string]string{ "prt_text": "text", "prt_reasoning": "reasoning", } cases := []struct { name string props map[string]any want string }{ {"text part", map[string]any{"partID": "prt_text", "field": "text", "delta": "OK"}, "OK"}, {"reasoning part ignored", map[string]any{"partID": "prt_reasoning", "field": "text", "delta": "thinking"}, ""}, // Unknown part: message.part.updated may have been missed on a new SSE // connection; allow the delta rather than silently dropping it. {"unknown part allowed when field=text", map[string]any{"partID": "prt_missing", "field": "text", "delta": "allowed"}, "allowed"}, {"non text field ignored", map[string]any{"partID": "prt_text", "field": "metadata", "delta": "lost"}, ""}, {"empty partID field=text allowed", map[string]any{"partID": "", "field": "text", "delta": "bare"}, "bare"}, } for _, tc := range cases { if got := opencodeMessagePartDeltaText(tc.props, partTypes); got != tc.want { t.Fatalf("%s: got %q, want %q", tc.name, got, tc.want) } } } func TestOpencodeStepTokensFromPart(t *testing.T) { var props map[string]any if err := json.Unmarshal([]byte(`{"part":{"type":"step-finish","tokens":{"input":7,"output":1}}}`), &props); err != nil { t.Fatal(err) } in, out, ok := opencodeStepTokens(props) if !ok || in != 7 || out != 1 { t.Fatalf("tokens: got %d/%d ok=%v, want 7/1 true", in, out, ok) } } func TestDecodeSSEDataLine(t *testing.T) { cases := []struct { in string want string shouldGet bool }{ {"data: {\"a\":1}", `{"a":1}`, true}, {"data:{\"a\":1}", `{"a":1}`, true}, {"event: ping", "", false}, {": comment", "", false}, {"", "", false}, {"data:", "", false}, {"data: ", "", false}, } for _, tc := range cases { got, ok := decodeSSEDataLine([]byte(tc.in)) if ok != tc.shouldGet { t.Errorf("decodeSSEDataLine(%q) ok=%v, want %v", tc.in, ok, tc.shouldGet) continue } if ok && string(got) != tc.want { t.Errorf("decodeSSEDataLine(%q) = %q, want %q", tc.in, string(got), tc.want) } } } func TestOpencodeLocalServerCwd_Workspace(t *testing.T) { tmpDir := t.TempDir() workspace := t.TempDir() scriptPath := filepath.Join(tmpDir, "fake-opencode-server.sh") scriptContent := fmt.Sprintf(`#!/bin/sh pwd > "%s/cwd.txt" echo "opencode server listening on http://127.0.0.1:12345" `, workspace) if err := os.WriteFile(scriptPath, []byte(scriptContent), 0755); err != nil { t.Fatalf("write fake script: %v", err) } profile := config.CLIProfileConf{ Command: scriptPath, } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() url, cmd, err := startOpencodeLocalServer(ctx, profile, opencodeRunOpts{}, workspace, nil) if err != nil { t.Fatalf("startOpencodeLocalServer failed: %v", err) } if cmd != nil && cmd.Process != nil { _ = cmd.Process.Kill() } if url != "http://127.0.0.1:12345" { t.Errorf("url got %q, want http://127.0.0.1:12345", url) } time.Sleep(100 * time.Millisecond) markerPath := filepath.Join(workspace, "cwd.txt") cwdBytes, err := os.ReadFile(markerPath) if err != nil { t.Fatalf("failed to read marker file: %v", err) } cwd := strings.TrimSpace(string(cwdBytes)) resolvedWorkspace, err := os.Readlink(workspace) if err != nil { resolvedWorkspace = workspace } if cwd != workspace && cwd != resolvedWorkspace { t.Errorf("expected process cwd to be %q or %q, got %q", workspace, resolvedWorkspace, cwd) } } func TestOpencodeLocalServerCwd_DirOverride(t *testing.T) { tmpDir := t.TempDir() workspace := t.TempDir() overrideDir := t.TempDir() scriptPath := filepath.Join(tmpDir, "fake-opencode-server.sh") scriptContent := fmt.Sprintf(`#!/bin/sh pwd > "%s/cwd.txt" echo "opencode server listening on http://127.0.0.1:12345" `, overrideDir) if err := os.WriteFile(scriptPath, []byte(scriptContent), 0755); err != nil { t.Fatalf("write fake script: %v", err) } profile := config.CLIProfileConf{ Command: scriptPath, } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() opts := opencodeRunOpts{Dir: overrideDir} url, cmd, err := startOpencodeLocalServer(ctx, profile, opts, workspace, nil) if err != nil { t.Fatalf("startOpencodeLocalServer failed: %v", err) } if cmd != nil && cmd.Process != nil { _ = cmd.Process.Kill() } if url != "http://127.0.0.1:12345" { t.Errorf("url got %q, want http://127.0.0.1:12345", url) } time.Sleep(100 * time.Millisecond) markerPath := filepath.Join(overrideDir, "cwd.txt") cwdBytes, err := os.ReadFile(markerPath) if err != nil { t.Fatalf("failed to read marker file: %v", err) } cwd := strings.TrimSpace(string(cwdBytes)) resolvedOverrideDir, err := os.Readlink(overrideDir) if err != nil { resolvedOverrideDir = overrideDir } if cwd != overrideDir && cwd != resolvedOverrideDir { t.Errorf("expected process cwd to be %q or %q, got %q", overrideDir, resolvedOverrideDir, cwd) } }