Files
dingtalk-workspace-cli/internal/helpers/connect_qoder_stream_test.go
修雨 d78de010ff Merge remote-tracking branch 'origin/main' into codex/test-coverage-100-v2
# 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
2026-07-15 23:14:16 +08:00

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)
}
}