# Conflicts: # internal/cli/stdin_test.go # internal/helpers/atomicwrite_test.go # internal/helpers/connect_agent_options_test.go # internal/helpers/connect_codex_appserver_test.go # internal/helpers/connect_daemon_test.go # internal/helpers/connect_lock.go # internal/helpers/doc.go
200 lines
6.7 KiB
Go
200 lines
6.7 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 (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func writeQoderStreamStub(t *testing.T, dir string) (string, string) {
|
|
t.Helper()
|
|
requirePOSIXShell(t)
|
|
logPath := filepath.Join(dir, "qoder-starts.log")
|
|
script := `import json
|
|
import os
|
|
import sys
|
|
|
|
with open(os.environ["DWS_QODER_STUB_LOG"], "a", encoding="utf-8") as f:
|
|
f.write("START " + json.dumps(sys.argv[1:], ensure_ascii=False) + "\n")
|
|
|
|
if "--input-format" in sys.argv:
|
|
for raw in sys.stdin:
|
|
raw = raw.strip()
|
|
if not raw:
|
|
continue
|
|
msg = json.loads(raw)
|
|
if msg.get("type") == "control_request":
|
|
rid = msg.get("request_id", "")
|
|
print(json.dumps({
|
|
"type": "control_response",
|
|
"response": {
|
|
"subtype": "success",
|
|
"request_id": rid,
|
|
"response": {"agents": []},
|
|
},
|
|
}), flush=True)
|
|
continue
|
|
if msg.get("type") == "user":
|
|
content = msg.get("message", {}).get("content", "")
|
|
print(json.dumps({
|
|
"type": "assistant",
|
|
"message": {"content": [{"type": "text", "text": "seen " + content}]},
|
|
}), flush=True)
|
|
print(json.dumps({
|
|
"type": "result",
|
|
"subtype": "success",
|
|
"message": {"content": [{"type": "text", "text": "done " + content}]},
|
|
}), flush=True)
|
|
else:
|
|
prompt = sys.argv[-1] if len(sys.argv) > 1 else ""
|
|
print("one-shot " + prompt)
|
|
`
|
|
pythonPath := filepath.Join(dir, "qoder-stub.py")
|
|
if err := os.WriteFile(pythonPath, []byte(script), 0o600); err != nil {
|
|
t.Fatalf("write qoder stub: %v", err)
|
|
}
|
|
t.Setenv("DWS_QODER_PY_STUB", filepath.ToSlash(pythonPath))
|
|
bin := writeShellExecutable(t, dir, "qodercli", `exec python3 "$DWS_QODER_PY_STUB" "$@"`+"\n")
|
|
return bin, logPath
|
|
}
|
|
|
|
func TestQoderForwarderUsesNativeAttachmentFlag(t *testing.T) {
|
|
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
|
stubDir := t.TempDir()
|
|
bin, logPath := writeQoderStreamStub(t, stubDir)
|
|
t.Setenv("DWS_QODER_STUB_LOG", logPath)
|
|
attachmentPath := filepath.Join(t.TempDir(), "forwarded.mov")
|
|
if err := os.WriteFile(attachmentPath, []byte("video-bytes"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f := &qoderStreamForwarder{
|
|
name: "qoderwork", bin: bin,
|
|
timeout: 5 * time.Second, sessions: newConvSessions(""),
|
|
}
|
|
reply, err := f.forwardWithAttachments(context.Background(), "conv", "分析视频", []connectMediaAttachment{{LocalPath: attachmentPath, FileName: "forwarded.mov", MediaType: "video"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if reply != "one-shot 分析视频" {
|
|
t.Fatalf("reply = %q", reply)
|
|
}
|
|
raw, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
encodedAttachmentPath, err := json.Marshal(attachmentPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(raw), `"--attachment", `+string(encodedAttachmentPath)) || !strings.Contains(string(raw), `"-p", "分析视频"`) {
|
|
t.Fatalf("qoder args missing attachment: %s", raw)
|
|
}
|
|
}
|
|
|
|
func TestQoderForwarderKeepsStreamJSONProcessAlive(t *testing.T) {
|
|
t.Setenv("DWS_CONNECT_NO_INSTALL", "1")
|
|
t.Setenv("DWS_AGENT_CMD", "")
|
|
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
|
t.Setenv("DWS_AGENT_TIMEOUT_MS", "3000")
|
|
stubDir := t.TempDir()
|
|
_, logPath := writeQoderStreamStub(t, stubDir)
|
|
t.Setenv("PATH", stubDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
t.Setenv("DWS_QODER_STUB_LOG", logPath)
|
|
|
|
fwd, err := forwarderForChannel("qoder", "qoder-client", connectAgentOptions{Memory: true})
|
|
if err != nil {
|
|
t.Fatalf("forwarderForChannel(qoder): %v", err)
|
|
}
|
|
if closer, ok := fwd.(forwarderCloser); ok {
|
|
defer closer.close()
|
|
}
|
|
sf, ok := fwd.(streamingForwarder)
|
|
if !ok || !sf.canStream() {
|
|
t.Fatalf("qoder forwarder should support streaming, got %T", fwd)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
got1, err := sf.forwardStream(ctx, "conv-a", "first", func(string) {})
|
|
if err != nil {
|
|
t.Fatalf("first forward: %v", err)
|
|
}
|
|
got2, err := sf.forwardStream(ctx, "conv-a", "second", func(string) {})
|
|
if err != nil {
|
|
t.Fatalf("second forward: %v", err)
|
|
}
|
|
if got1 != "done first" || got2 != "done second" {
|
|
t.Fatalf("replies = %q / %q, want done first / done second", got1, got2)
|
|
}
|
|
|
|
raw, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
t.Fatalf("read qoder stub log: %v", err)
|
|
}
|
|
lines := strings.Split(strings.TrimSpace(string(raw)), "\n")
|
|
if len(lines) != 1 {
|
|
t.Fatalf("qodercli starts = %d, want 1; log:\n%s", len(lines), raw)
|
|
}
|
|
if !strings.Contains(lines[0], "--input-format") || !strings.Contains(lines[0], "stream-json") {
|
|
t.Fatalf("qodercli should start in stream-json input mode, log:\n%s", raw)
|
|
}
|
|
}
|
|
|
|
type qoderTestWriteCloser struct {
|
|
bytes.Buffer
|
|
}
|
|
|
|
func (w *qoderTestWriteCloser) Close() error { return nil }
|
|
|
|
func TestQoderControlRequestReturnsImmediately(t *testing.T) {
|
|
stdin := &qoderTestWriteCloser{}
|
|
f := &qoderStreamForwarder{name: "qoder", stdin: stdin}
|
|
|
|
if !f.handleControlRequestLocked(`{"type":"control_request","request_id":"req-1","request":{"type":"permission","subtype":"permission","tool":"bash"}}`) {
|
|
t.Fatal("control_request was not handled")
|
|
}
|
|
|
|
var got struct {
|
|
Type string `json:"type"`
|
|
Response struct {
|
|
Subtype string `json:"subtype"`
|
|
RequestID string `json:"request_id"`
|
|
Code string `json:"code"`
|
|
} `json:"response"`
|
|
}
|
|
if err := json.Unmarshal(bytes.TrimSpace(stdin.Bytes()), &got); err != nil {
|
|
t.Fatalf("decode response: %v; raw=%q", err, stdin.String())
|
|
}
|
|
if got.Type != "control_response" || got.Response.RequestID != "req-1" {
|
|
t.Fatalf("response identity mismatch: %#v", got)
|
|
}
|
|
if got.Response.Subtype != "error" || got.Response.Code != "unsupported_control_request" {
|
|
t.Fatalf("response should explicitly unblock with unsupported error: %#v", got.Response)
|
|
}
|
|
}
|
|
|
|
func TestParseQoderPersistentLineReadsResultField(t *testing.T) {
|
|
delta, final, done := parseQoderPersistentLine(`{"type":"result","subtype":"success","result":"OK"}`)
|
|
if delta != "" || final != "OK" || !done {
|
|
t.Fatalf("delta=%q final=%q done=%v, want final OK done", delta, final, done)
|
|
}
|
|
}
|