* feat(codex): 添加独立上游 WebSocket 会话封装 * fix(codex): 放开 SOCKS5 代理并修正超时分类 * fix(codex): 修正 WS 终态并脱敏断连日志 * fix(codex): 保留失败终态的上游错误码 * fix(codex): 提前注册 WebSocket 日志脱敏
87 lines
2.7 KiB
Go
87 lines
2.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"maps"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"gpt-load/internal/platform/redact"
|
|
"gpt-load/internal/subscription/providers/codex"
|
|
)
|
|
|
|
type runtimeLogCapture struct {
|
|
mu sync.Mutex
|
|
buf bytes.Buffer
|
|
}
|
|
|
|
func (capture *runtimeLogCapture) Write(body []byte) (int, error) {
|
|
capture.mu.Lock()
|
|
defer capture.mu.Unlock()
|
|
return capture.buf.Write(body)
|
|
}
|
|
|
|
func (capture *runtimeLogCapture) String() string {
|
|
capture.mu.Lock()
|
|
defer capture.mu.Unlock()
|
|
return capture.buf.String()
|
|
}
|
|
|
|
func (*runtimeLogCapture) Levels() []logrus.Level { return logrus.AllLevels }
|
|
|
|
func (capture *runtimeLogCapture) Fire(entry *logrus.Entry) error {
|
|
// 与 Windows Event Log 一样,在 Hook 内格式化并写入,而非等待最终输出。
|
|
formatted, err := entry.Logger.Formatter.Format(entry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = capture.Write(formatted)
|
|
return err
|
|
}
|
|
|
|
func TestCodexWSRedactionPrecedesRuntimeLogHooks(t *testing.T) {
|
|
// 非并行测试保留包初始化安装的 Hook,只临时追加运行时日志链。
|
|
logger := logrus.StandardLogger()
|
|
originalOutput, originalHooks := logger.Out, maps.Clone(logger.Hooks)
|
|
t.Cleanup(func() {
|
|
logger.SetOutput(originalOutput)
|
|
logger.ReplaceHooks(originalHooks)
|
|
})
|
|
var output, sink runtimeLogCapture
|
|
logger.SetOutput(&output)
|
|
logger.AddHook(redact.NewHook(redact.New()))
|
|
logger.AddHook(&sink)
|
|
|
|
// 复现服务启动后才首次创建 Session 的顺序;创建句柄不会建立连接。
|
|
session, err := codex.NewWSSession(codex.WSSessionOptions{
|
|
CredentialID: "test", Credential: codex.Credential{
|
|
Type: codex.Provider, AccessToken: "test-access", RefreshToken: "test-refresh", AccountID: "test-account",
|
|
}, ProxyURL: "direct",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := session.Close(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
const marker = "private-close-reason-marker"
|
|
// 使用固定 SDK 的断连日志格式,验证桥接封装与项目日志链的组合。
|
|
logger.Infof("codex websockets: upstream disconnected session=gptload-codex-ws-log-order-test auth=test url=wss://example.test/responses reason=read_error err=websocket: close 1008 (policy violation): %s", marker)
|
|
for name, capture := range map[string]*runtimeLogCapture{"output": &output, "sink": &sink} {
|
|
message := capture.String()
|
|
if strings.Contains(message, marker) {
|
|
t.Errorf("close reason leaked into %s", name)
|
|
}
|
|
if !strings.Contains(message, "session=[REDACTED]") ||
|
|
!strings.Contains(message, "ws_close_code=1008") ||
|
|
!strings.Contains(message, "error_class=websocket_closed") {
|
|
t.Errorf("missing redacted session or safe close diagnostics in %s", name)
|
|
}
|
|
}
|
|
}
|