358 lines
13 KiB
Go
358 lines
13 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package helpers
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// clearChannelEnv clears every env var that participates in channel detection
|
|
// (empty == treated as unset), so the test host's own QODER_CLI etc. cannot
|
|
// leak into a case. t.Setenv restores them when the case ends.
|
|
func clearChannelEnv(t *testing.T) {
|
|
for _, k := range []string{
|
|
"DWS_AGENT_CHANNEL", "DINGTALK_AGENT", "OPENCLAW", "OPENCLAW_GATEWAY",
|
|
"HERMES_AGENT", "HERMES", "QODER_CLI", "QODERCLI_INTEGRATION_MODE",
|
|
"DWS_CONNECT_CMD", "DWS_AGENT_CMD",
|
|
"WORKBUDDY_CONFIG_DIR", "WORKBUDDY_APP_NAME", "CLAUDECODE",
|
|
"DWS_AGENT_PERMISSION_MODE", "DWS_AGENT_APPROVAL_MODE",
|
|
"GEMINI_API_KEY", "GOOGLE_API_KEY", "GEMINI_API_BASE_URL",
|
|
"GOOGLE_GEMINI_API_BASE_URL", "GEMINI_MODEL",
|
|
} {
|
|
t.Setenv(k, "")
|
|
}
|
|
}
|
|
|
|
func TestResolveConnectChannel(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
flag string
|
|
env map[string]string
|
|
wantChannel string
|
|
wantDetectedBy string
|
|
}{
|
|
{"explicit flag wins", "openclaw", map[string]string{"DWS_AGENT_CHANNEL": "qoder", "QODER_CLI": "1"}, "openclaw", "flag:--channel"},
|
|
{"env overrides signal", "auto", map[string]string{"DWS_AGENT_CHANNEL": "qoderwork", "QODER_CLI": "1"}, "qoderwork", "env:DWS_AGENT_CHANNEL"},
|
|
{"signal openclaw(DINGTALK_AGENT)", "auto", map[string]string{"DINGTALK_AGENT": "DING_DWS_CLAW"}, "openclaw", "signal:DINGTALK_AGENT"},
|
|
{"signal openclaw(OPENCLAW)", "", map[string]string{"OPENCLAW": "1"}, "openclaw", "signal:OPENCLAW"},
|
|
{"signal qoder family", "", map[string]string{"QODER_CLI": "1"}, "qoder", "signal:QODER_CLI"},
|
|
{"signal qoderwork(INTEGRATION_MODE)", "auto", map[string]string{"QODERCLI_INTEGRATION_MODE": "qoder_work"}, "qoderwork", "signal:QODERCLI_INTEGRATION_MODE"},
|
|
{"qoderwork precedes qoder/claudecode", "auto", map[string]string{"QODERCLI_INTEGRATION_MODE": "qoder_work", "QODER_CLI": "1", "CLAUDECODE": "1"}, "qoderwork", "signal:QODERCLI_INTEGRATION_MODE"},
|
|
{"signal hermes", "auto", map[string]string{"HERMES_AGENT": "1"}, "hermes", "signal:HERMES"},
|
|
{"signal workbuddy(WORKBUDDY_CONFIG_DIR)", "auto", map[string]string{"WORKBUDDY_CONFIG_DIR": "/Users/x/.workbuddy"}, "workbuddy", "signal:WORKBUDDY_CONFIG_DIR"},
|
|
{"signal workbuddy(WORKBUDDY_APP_NAME)", "auto", map[string]string{"WORKBUDDY_APP_NAME": "WorkBuddy"}, "workbuddy", "signal:WORKBUDDY_CONFIG_DIR"},
|
|
{"signal claudecode", "auto", map[string]string{"CLAUDECODE": "1"}, "claudecode", "signal:CLAUDECODE"},
|
|
{"qoder fork precedes claudecode", "auto", map[string]string{"QODER_CLI": "1", "CLAUDECODE": "1"}, "qoder", "signal:QODER_CLI"},
|
|
{"custom via DWS_AGENT_CMD", "auto", map[string]string{"DWS_AGENT_CMD": "lobster -p"}, "custom", "env:DWS_AGENT_CMD"},
|
|
{"explicit channel beats DWS_AGENT_CMD", "claudecode", map[string]string{"DWS_AGENT_CMD": "lobster -p"}, "claudecode", "flag:--channel"},
|
|
{"undetected", "auto", nil, "", "undetected"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
clearChannelEnv(t)
|
|
for k, v := range tc.env {
|
|
t.Setenv(k, v)
|
|
}
|
|
ch, by := resolveConnectChannel(tc.flag)
|
|
if ch != tc.wantChannel || by != tc.wantDetectedBy {
|
|
t.Fatalf("resolveConnectChannel(%q) = (%q,%q), want (%q,%q)", tc.flag, ch, by, tc.wantChannel, tc.wantDetectedBy)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConnectChannelsKnown(t *testing.T) {
|
|
for _, ch := range []string{"openclaw", "qoder", "qoderwork", "hermes", "workbuddy", "claudecode", "codebuddy", "custom"} {
|
|
if _, ok := connectChannels[ch]; !ok {
|
|
t.Errorf("channel %q should be in connectChannels", ch)
|
|
}
|
|
}
|
|
if _, ok := connectChannels["weird"]; ok {
|
|
t.Error("unknown channel should not be in connectChannels")
|
|
}
|
|
}
|
|
|
|
// TestCustomChannel covers the self-built / unsupported-agent escape hatch
|
|
// (issue #37): custom is a stream-bridge channel whose argv comes entirely from
|
|
// DWS_AGENT_CMD (set by --agent-cmd); without a command it errors clearly.
|
|
func TestCustomChannel(t *testing.T) {
|
|
if !isStreamBridgeChannel("custom") {
|
|
t.Fatal("custom should be a stream-bridge channel")
|
|
}
|
|
if got, _ := buildConnectPlan("custom", "cid", "rc")["method"].(string); got != "stream-bridge-custom" {
|
|
t.Errorf("buildConnectPlan(custom).method = %q, want stream-bridge-custom", got)
|
|
}
|
|
t.Run("missing command errors", func(t *testing.T) {
|
|
clearChannelEnv(t)
|
|
if _, _, err := resolveExecAgent("custom"); err == nil {
|
|
t.Error("custom without DWS_AGENT_CMD should error")
|
|
}
|
|
})
|
|
t.Run("command wins", func(t *testing.T) {
|
|
clearChannelEnv(t)
|
|
t.Setenv("DWS_AGENT_CMD", "lobster -p")
|
|
argv, _, err := resolveExecAgent("custom")
|
|
if err != nil {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
if !equalStringSlice(argv, []string{"lobster", "-p"}) {
|
|
t.Errorf("argv = %v, want [lobster -p]", argv)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBuildConnectPlanMethod(t *testing.T) {
|
|
want := map[string]string{
|
|
"openclaw": "openclaw-connector",
|
|
"qoder": "stream-bridge-qoder-stream",
|
|
"qoderwork": "stream-bridge-qoder-stream",
|
|
"hermes": "official-channel",
|
|
"workbuddy": "stream-bridge",
|
|
"claudecode": "stream-bridge",
|
|
"codebuddy": "stream-bridge",
|
|
"codex": "stream-bridge-codex-app-server",
|
|
"weird": "unknown",
|
|
}
|
|
for ch, m := range want {
|
|
got, _ := buildConnectPlan(ch, "cid", "rc")["method"].(string)
|
|
if got != m {
|
|
t.Errorf("buildConnectPlan(%q).method = %q, want %q", ch, got, m)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConnectExternalCommand(t *testing.T) {
|
|
t.Run("DWS_CONNECT_CMD override (applies to all channels)", func(t *testing.T) {
|
|
clearChannelEnv(t)
|
|
t.Setenv("DWS_CONNECT_CMD", "my-bridge --flag x")
|
|
want := []string{"my-bridge", "--flag", "x"}
|
|
for _, ch := range []string{"qoder", "workbuddy", "openclaw", "hermes"} {
|
|
if got := connectExternalCommand(ch); !equalStringSlice(got, want) {
|
|
t.Fatalf("channel %q: got %v, want %v", ch, got, want)
|
|
}
|
|
}
|
|
})
|
|
t.Run("stream-bridge channels go Go-native, no external command", func(t *testing.T) {
|
|
clearChannelEnv(t)
|
|
for _, ch := range []string{"qoder", "qoderwork", "claudecode", "codebuddy", "workbuddy"} {
|
|
if got := connectExternalCommand(ch); got != nil {
|
|
t.Errorf("stream-bridge channel %q should return nil (Go-native), got %v", ch, got)
|
|
}
|
|
if !isStreamBridgeChannel(ch) {
|
|
t.Errorf("channel %q should be recognised as stream-bridge", ch)
|
|
}
|
|
}
|
|
})
|
|
t.Run("openclaw uses external gateway", func(t *testing.T) {
|
|
clearChannelEnv(t)
|
|
if got := connectExternalCommand("openclaw"); len(got) == 0 || got[0] != "openclaw" {
|
|
t.Errorf("openclaw default should be openclaw ..., got %v", got)
|
|
}
|
|
if isStreamBridgeChannel("openclaw") {
|
|
t.Error("openclaw should not be a stream-bridge channel")
|
|
}
|
|
})
|
|
t.Run("hermes has no built-in command", func(t *testing.T) {
|
|
clearChannelEnv(t)
|
|
if got := connectExternalCommand("hermes"); got != nil {
|
|
t.Errorf("hermes with no built-in command should return nil, got %v", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestResolveAgentYoloMode(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
flags map[string]string
|
|
env map[string]string
|
|
want bool
|
|
wantErr bool
|
|
}{
|
|
{"default yolo", nil, nil, true, false},
|
|
{"permission bypass", map[string]string{"agent-permission-mode": "bypass"}, nil, true, false},
|
|
{"permission ask", map[string]string{"agent-permission-mode": "ask", "yolo": "true"}, nil, false, false},
|
|
{"approval yolo", map[string]string{"agent-approval-mode": "yolo"}, nil, true, false},
|
|
{"short yolo", map[string]string{"yolo": "true"}, nil, true, false},
|
|
{"env permission bypass", nil, map[string]string{"DWS_AGENT_PERMISSION_MODE": "bypass"}, true, false},
|
|
{"env approval yolo", nil, map[string]string{"DWS_AGENT_APPROVAL_MODE": "yolo"}, true, false},
|
|
{"invalid permission mode", map[string]string{"agent-permission-mode": "full"}, nil, false, true},
|
|
{"invalid approval mode", map[string]string{"agent-approval-mode": "bypass"}, nil, false, true},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
clearChannelEnv(t)
|
|
cmd := newDevAppRobotConnectCommand(nil)
|
|
for k, v := range tc.env {
|
|
t.Setenv(k, v)
|
|
}
|
|
for k, v := range tc.flags {
|
|
if err := cmd.Flags().Set(k, v); err != nil {
|
|
t.Fatalf("set flag %s=%s: %v", k, v, err)
|
|
}
|
|
}
|
|
got, err := resolveAgentYoloMode(cmd)
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("resolveAgentYoloMode err = nil, want error")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("resolveAgentYoloMode err = %v", err)
|
|
}
|
|
if got != tc.want {
|
|
t.Fatalf("resolveAgentYoloMode = %v, want %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestForwarderForChannel(t *testing.T) {
|
|
clearChannelEnv(t)
|
|
stub := t.TempDir()
|
|
if err := writeExecStub(stub, "codex"); err != nil {
|
|
t.Fatalf("write codex stub: %v", err)
|
|
}
|
|
t.Setenv("PATH", stub)
|
|
// DWS_AGENT_CMD covers ordinary stream-bridge channels so the test does not
|
|
// depend on those binaries being installed. Codex ignores it and stays on
|
|
// app-server.
|
|
t.Setenv("DWS_AGENT_CMD", "fake-cli --flag")
|
|
for ch := range agentSpecs {
|
|
fwd, err := forwarderForChannel(ch, "", connectAgentOptions{})
|
|
if err != nil {
|
|
t.Fatalf("forwarderForChannel(%q) err = %v", ch, err)
|
|
}
|
|
if ch == "codex" {
|
|
if _, ok := fwd.(*codexAppServerForwarder); !ok {
|
|
t.Errorf("codex should ignore DWS_AGENT_CMD and yield app-server, got %T", fwd)
|
|
}
|
|
continue
|
|
}
|
|
ef, ok := fwd.(*execForwarder)
|
|
if !ok {
|
|
t.Errorf("channel %q should yield *execForwarder, got %T", ch, fwd)
|
|
continue
|
|
}
|
|
if !equalStringSlice(ef.argv, []string{"fake-cli", "--flag"}) {
|
|
t.Errorf("channel %q DWS_AGENT_CMD not applied: argv = %v", ch, ef.argv)
|
|
}
|
|
}
|
|
// Non-agent channel has no forwarder.
|
|
if _, err := forwarderForChannel("openclaw", "", connectAgentOptions{}); err == nil {
|
|
t.Error("openclaw should not yield a forwarder")
|
|
}
|
|
}
|
|
|
|
func TestAgentSpecsCoverMainstreamAgents(t *testing.T) {
|
|
// The mainstream agents the bot must support, all present as channels.
|
|
for _, ch := range []string{
|
|
"claudecode", "codex", "gemini", "opencode",
|
|
"qoder", "qoderwork", "codebuddy", "workbuddy",
|
|
} {
|
|
spec, ok := agentSpecs[ch]
|
|
if !ok {
|
|
t.Errorf("agentSpecs missing channel %q", ch)
|
|
continue
|
|
}
|
|
if ch != "gemini" && len(spec.bins) == 0 {
|
|
t.Errorf("channel %q has no bins", ch)
|
|
}
|
|
if spec.hint == "" {
|
|
t.Errorf("channel %q has no install hint", ch)
|
|
}
|
|
if _, ok := connectChannels[ch]; !ok {
|
|
t.Errorf("channel %q not in connectChannels", ch)
|
|
}
|
|
if !isStreamBridgeChannel(ch) {
|
|
t.Errorf("channel %q should be stream-bridge", ch)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMsgDedup(t *testing.T) {
|
|
d := newMsgDedup(3)
|
|
if !d.first("a") {
|
|
t.Fatal("first a should be new")
|
|
}
|
|
if d.first("a") {
|
|
t.Fatal("second a should be a duplicate")
|
|
}
|
|
if !d.first("b") || !d.first("c") {
|
|
t.Fatal("b and c should be new")
|
|
}
|
|
// seen now holds {a,b,c} == limit; the next new id triggers a reset.
|
|
if !d.first("x") {
|
|
t.Fatal("x should be new (and trigger reset at limit)")
|
|
}
|
|
// After the reset, a was evicted, so it is treated as new again.
|
|
if !d.first("a") {
|
|
t.Fatal("a should be new again after reset")
|
|
}
|
|
}
|
|
|
|
func TestLocateBinary(t *testing.T) {
|
|
// On PATH: sh exists on every unix test host.
|
|
if _, ok := locateBinary([]string{"sh"}, nil); !ok {
|
|
t.Error("sh should be found on PATH")
|
|
}
|
|
// Miss: nonexistent name + non-matching glob.
|
|
if _, ok := locateBinary([]string{"definitely-not-a-real-binary-xyz"}, []string{"/no/such/path/*/nope"}); ok {
|
|
t.Error("should not find a nonexistent binary")
|
|
}
|
|
// Glob hit: a real file matched by a wildcard dir.
|
|
dir := t.TempDir()
|
|
sub := filepath.Join(dir, "archX")
|
|
if err := os.MkdirAll(sub, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bin := filepath.Join(sub, "mycli")
|
|
if err := os.WriteFile(bin, []byte("#!/bin/sh\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, ok := locateBinary([]string{"no-such"}, []string{filepath.Join(dir, "*", "mycli")})
|
|
if !ok || got != bin {
|
|
t.Errorf("glob locate = (%q,%v), want (%q,true)", got, ok, bin)
|
|
}
|
|
}
|
|
|
|
func TestResolveExecAgentOverride(t *testing.T) {
|
|
clearChannelEnv(t)
|
|
t.Setenv("DWS_AGENT_CMD", "my-agent --foo bar")
|
|
argv, env, err := resolveExecAgent("qoder")
|
|
if err != nil {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
if !equalStringSlice(argv, []string{"my-agent", "--foo", "bar"}) {
|
|
t.Errorf("override argv = %v", argv)
|
|
}
|
|
if env != nil {
|
|
t.Errorf("override env should be nil, got %v", env)
|
|
}
|
|
}
|
|
|
|
func equalStringSlice(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|