diff --git a/CONTRIBUTING.en.md b/CONTRIBUTING.en.md index baf5eae..212cbb7 100644 --- a/CONTRIBUTING.en.md +++ b/CONTRIBUTING.en.md @@ -86,7 +86,7 @@ Manually build WebUI to `static/admin/`: go test ./... # End-to-end live tests (real accounts) -./scripts/testsuite/run-live.sh +./tests/scripts/run-live.sh ``` ## Project Structure diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c75d450..3bbd5b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,7 +86,7 @@ docker-compose -f docker-compose.dev.yml up go test ./... # 端到端全链路测试(真实账号) -./scripts/testsuite/run-live.sh +./tests/scripts/run-live.sh ``` ## 项目结构 diff --git a/DEPLOY.en.md b/DEPLOY.en.md index 8a62c98..046df83 100644 --- a/DEPLOY.en.md +++ b/DEPLOY.en.md @@ -518,7 +518,7 @@ curl http://127.0.0.1:5001/v1/chat/completions \ Run the full live testsuite before release (real account tests): ```bash -./scripts/testsuite/run-live.sh +./tests/scripts/run-live.sh ``` With custom flags: diff --git a/DEPLOY.md b/DEPLOY.md index e5b0630..4a1efcb 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -518,7 +518,7 @@ curl http://127.0.0.1:5001/v1/chat/completions \ 建议在发布前执行完整的端到端测试集(使用真实账号): ```bash -./scripts/testsuite/run-live.sh +./tests/scripts/run-live.sh ``` 可自定义参数: diff --git a/README.MD b/README.MD index 261e34a..af24e5b 100644 --- a/README.MD +++ b/README.MD @@ -350,8 +350,10 @@ ds2api/ │ ├── components/ # AccountManager / ApiTester / BatchImport / VercelSync / Login / LandingPage │ └── locales/ # 中英文语言包(zh.json / en.json) ├── scripts/ -│ ├── build-webui.sh # WebUI 手动构建脚本 -│ └── testsuite/ # 测试集运行脚本 +│ └── build-webui.sh # WebUI 手动构建脚本 +├── tests/ +│ ├── compat/ # 兼容性测试夹具与期望输出 +│ └── scripts/ # 统一测试脚本入口(unit/e2e) ├── static/admin/ # WebUI 构建产物(不提交到 Git) ├── .github/ │ ├── workflows/ # GitHub Actions(Release 自动构建) @@ -379,11 +381,11 @@ ds2api/ ## 测试 ```bash -# 单元测试 -go test ./... +# 单元测试(Go + Node) +./tests/scripts/run-unit-all.sh # 一键端到端全链路测试(真实账号,生成完整请求/响应日志) -./scripts/testsuite/run-live.sh +./tests/scripts/run-live.sh # 或自定义参数 go run ./cmd/ds2api-tests \ diff --git a/README.en.md b/README.en.md index 5d2f326..07a5ac8 100644 --- a/README.en.md +++ b/README.en.md @@ -350,8 +350,10 @@ ds2api/ │ ├── components/ # AccountManager / ApiTester / BatchImport / VercelSync / Login / LandingPage │ └── locales/ # Language packs (zh.json / en.json) ├── scripts/ -│ ├── build-webui.sh # Manual WebUI build script -│ └── testsuite/ # Testsuite runner scripts +│ └── build-webui.sh # Manual WebUI build script +├── tests/ +│ ├── compat/ # Compatibility fixtures and expected outputs +│ └── scripts/ # Unified test script entrypoints (unit/e2e) ├── static/admin/ # WebUI build output (not committed to Git) ├── .github/ │ ├── workflows/ # GitHub Actions (Release artifact automation) @@ -379,11 +381,11 @@ ds2api/ ## Testing ```bash -# Unit tests -go test ./... +# Unit tests (Go + Node) +./tests/scripts/run-unit-all.sh # One-command live end-to-end tests (real accounts, full request/response logs) -./scripts/testsuite/run-live.sh +./tests/scripts/run-live.sh # Or with custom flags go run ./cmd/ds2api-tests \ diff --git a/TESTING.md b/TESTING.md index ce349ec..f8e532a 100644 --- a/TESTING.md +++ b/TESTING.md @@ -8,8 +8,10 @@ DS2API 提供两个层级的测试: | 层级 | 命令 | 说明 | | --- | --- | --- | -| 单元测试 | `go test ./...` | 不需要真实账号 | -| 端到端测试 | `./scripts/testsuite/run-live.sh` | 使用真实账号执行全链路测试 | +| 单元测试(Go) | `./tests/scripts/run-unit-go.sh` | 不需要真实账号 | +| 单元测试(Node) | `./tests/scripts/run-unit-node.sh` | 不需要真实账号 | +| 单元测试(全部) | `./tests/scripts/run-unit-all.sh` | 不需要真实账号 | +| 端到端测试 | `./tests/scripts/run-live.sh` | 使用真实账号执行全链路测试 | 端到端测试集会录制完整的请求/响应日志,用于故障排查。 @@ -20,17 +22,19 @@ DS2API 提供两个层级的测试: ### 单元测试 | Unit Tests ```bash -go test ./... +./tests/scripts/run-unit-all.sh ``` ```bash -node --test api/helpers/stream-tool-sieve.test.js api/chat-stream.test.js api/compat/js_compat_test.js +# 或按语言拆分执行 +./tests/scripts/run-unit-go.sh +./tests/scripts/run-unit-node.sh ``` ### 端到端测试 | End-to-End Tests ```bash -./scripts/testsuite/run-live.sh +./tests/scripts/run-live.sh ``` **默认行为**: @@ -179,7 +183,7 @@ go run ./cmd/ds2api-tests \ ```bash # 确保 config.json 存在且包含有效测试账号 -./scripts/testsuite/run-live.sh +./tests/scripts/run-live.sh exit_code=$? if [ $exit_code -ne 0 ]; then echo "Tests failed! Check artifacts for details." diff --git a/internal/format/openai/render.go b/internal/format/openai/render.go index 02c536e..248c7ca 100644 --- a/internal/format/openai/render.go +++ b/internal/format/openai/render.go @@ -47,27 +47,46 @@ func BuildResponseObject(responseID, model, finalPrompt, finalThinking, finalTex // produced a standalone structured payload. This prevents accidental // empty output_text on normal prose that merely contains tool_call-like text. detected := util.ParseStandaloneToolCalls(finalText, toolNames) + toolCallsFromThinking := false + if len(detected) == 0 && strings.TrimSpace(finalThinking) != "" { + detected = util.ParseStandaloneToolCalls(finalThinking, toolNames) + toolCallsFromThinking = len(detected) > 0 + } exposedOutputText := finalText output := make([]any, 0, 2) if len(detected) > 0 { - exposedOutputText = "" + if !toolCallsFromThinking || strings.TrimSpace(finalText) != "" { + exposedOutputText = "" + } else { + exposedOutputText = finalThinking + } + if strings.TrimSpace(finalThinking) != "" { + output = append(output, map[string]any{ + "type": "reasoning", + "text": finalThinking, + }) + } output = append(output, map[string]any{ "type": "tool_calls", "tool_calls": util.FormatOpenAIToolCalls(detected), }) } else { - content := []any{ - map[string]any{ - "type": "output_text", - "text": finalText, - }, - } + content := make([]any, 0, 2) if finalThinking != "" { content = append([]any{map[string]any{ "type": "reasoning", "text": finalThinking, }}, content...) } + if strings.TrimSpace(finalText) != "" { + content = append(content, map[string]any{ + "type": "output_text", + "text": finalText, + }) + } + if strings.TrimSpace(finalText) == "" && strings.TrimSpace(finalThinking) != "" { + exposedOutputText = finalThinking + } output = append(output, map[string]any{ "type": "message", "id": "msg_" + strings.ReplaceAll(uuid.NewString(), "-", ""), diff --git a/internal/format/openai/render_test.go b/internal/format/openai/render_test.go index 50589e0..8fc13e2 100644 --- a/internal/format/openai/render_test.go +++ b/internal/format/openai/render_test.go @@ -87,3 +87,60 @@ func TestBuildResponseObjectKeepsOutputTextForMixedProse(t *testing.T) { t.Fatalf("expected output type message, got %#v", first["type"]) } } + +func TestBuildResponseObjectReasoningOnlyFallsBackToOutputText(t *testing.T) { + obj := BuildResponseObject( + "resp_test", + "gpt-4o", + "prompt", + "internal thinking content", + "", + nil, + ) + + outputText, _ := obj["output_text"].(string) + if outputText == "" { + t.Fatalf("expected output_text fallback from reasoning when final text is empty") + } + + output, _ := obj["output"].([]any) + if len(output) != 1 { + t.Fatalf("expected one output item, got %#v", obj["output"]) + } + first, _ := output[0].(map[string]any) + if first["type"] != "message" { + t.Fatalf("expected output type message, got %#v", first["type"]) + } + content, _ := first["content"].([]any) + if len(content) == 0 { + t.Fatalf("expected reasoning content, got %#v", first["content"]) + } + block0, _ := content[0].(map[string]any) + if block0["type"] != "reasoning" { + t.Fatalf("expected first content block reasoning, got %#v", block0["type"]) + } +} + +func TestBuildResponseObjectDetectsToolCallFromThinkingChannel(t *testing.T) { + obj := BuildResponseObject( + "resp_test", + "gpt-4o", + "prompt", + `{"tool_calls":[{"name":"search","input":{"q":"from-thinking"}}]}`, + "", + []string{"search"}, + ) + + output, _ := obj["output"].([]any) + if len(output) != 2 { + t.Fatalf("expected reasoning + tool_calls outputs, got %#v", obj["output"]) + } + first, _ := output[0].(map[string]any) + if first["type"] != "reasoning" { + t.Fatalf("expected first output reasoning, got %#v", first["type"]) + } + second, _ := output[1].(map[string]any) + if second["type"] != "tool_calls" { + t.Fatalf("expected second output tool_calls, got %#v", second["type"]) + } +} diff --git a/scripts/testsuite/run-live.sh b/tests/scripts/run-live.sh similarity index 100% rename from scripts/testsuite/run-live.sh rename to tests/scripts/run-live.sh diff --git a/tests/scripts/run-unit-all.sh b/tests/scripts/run-unit-all.sh new file mode 100755 index 0000000..59b202c --- /dev/null +++ b/tests/scripts/run-unit-all.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT_DIR" + +./tests/scripts/run-unit-go.sh +./tests/scripts/run-unit-node.sh diff --git a/tests/scripts/run-unit-go.sh b/tests/scripts/run-unit-go.sh new file mode 100755 index 0000000..38a11b8 --- /dev/null +++ b/tests/scripts/run-unit-go.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT_DIR" + +go test ./... "$@" diff --git a/tests/scripts/run-unit-node.sh b/tests/scripts/run-unit-node.sh new file mode 100755 index 0000000..95f11e0 --- /dev/null +++ b/tests/scripts/run-unit-node.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT_DIR" + +node --test api/helpers/stream-tool-sieve.test.js api/chat-stream.test.js api/compat/js_compat_test.js "$@"