mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-07 07:03:34 +08:00
- 关闭时隐藏保活,重开复用现有进程并通过可见性版本消除竞态 - 拆分 AI 与工作台资源,使用启动状态白名单减少序列化开销 - 修复设置中心层级、Windows 前台切换及独立窗口关闭交互 - 补齐状态同步、断线重放与窗口生命周期回归测试
237 lines
6.8 KiB
Go
237 lines
6.8 KiB
Go
package nativewindow
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestBridgeReassemblesFragmentedLargeSSEEventWithoutChangingPayload(t *testing.T) {
|
|
const chunkSize = 256 << 10
|
|
content := strings.Repeat(" value with spaces ", (5<<20)/19)
|
|
payload, err := json.Marshal(bridgeEvent{
|
|
Name: "ai:stream:session-1",
|
|
Args: []any{map[string]any{"content": content}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal large event: %v", err)
|
|
}
|
|
var stream strings.Builder
|
|
stream.WriteString(": connected\n\n")
|
|
for offset := 0; offset < len(payload); offset += chunkSize {
|
|
end := offset + chunkSize
|
|
if end > len(payload) {
|
|
end = len(payload)
|
|
}
|
|
stream.WriteString("data: ")
|
|
stream.Write(payload[offset:end])
|
|
stream.WriteByte('\n')
|
|
}
|
|
stream.WriteByte('\n')
|
|
|
|
bridge := newBridge(ChildOptions{
|
|
ParentURL: "http://127.0.0.1:43119",
|
|
Token: "test-token",
|
|
ID: "ai-chat",
|
|
Kind: "ai-chat",
|
|
})
|
|
bridge.client.Transport = roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
|
status := http.StatusNoContent
|
|
body := ""
|
|
if request.URL.Path == EventsPath {
|
|
status = http.StatusOK
|
|
body = stream.String()
|
|
}
|
|
return &http.Response{
|
|
StatusCode: status,
|
|
Body: io.NopCloser(strings.NewReader(body)),
|
|
Header: make(http.Header),
|
|
}, nil
|
|
})
|
|
|
|
var received string
|
|
bridge.mu.Lock()
|
|
bridge.ctx = context.Background()
|
|
bridge.emitToWails = func(_ context.Context, name string, args ...any) {
|
|
if name != "ai:stream:session-1" || len(args) != 1 {
|
|
t.Fatalf("unexpected event %q %#v", name, args)
|
|
}
|
|
chunk, ok := args[0].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("unexpected event payload: %#v", args[0])
|
|
}
|
|
received, _ = chunk["content"].(string)
|
|
}
|
|
bridge.mu.Unlock()
|
|
|
|
if err := bridge.consumeEventStream(context.Background()); err != nil {
|
|
t.Fatalf("consume fragmented stream: %v", err)
|
|
}
|
|
if received != content {
|
|
t.Fatalf("round-trip content length = %d, want %d", len(received), len(content))
|
|
}
|
|
}
|
|
|
|
func TestBridgeReplaysPendingCloseWhenEventStreamReconnects(t *testing.T) {
|
|
bridge := newBridge(ChildOptions{
|
|
ParentURL: "http://127.0.0.1:43119",
|
|
Token: "test-token",
|
|
ID: "workbench:query-1",
|
|
Kind: "workbench",
|
|
})
|
|
commandPayload, err := json.Marshal(childCommand{
|
|
ID: "workbench:query-1",
|
|
Action: "close",
|
|
Reason: ExitReasonRequested,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal close command: %v", err)
|
|
}
|
|
bridge.client.Transport = roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
|
status := http.StatusNoContent
|
|
body := ""
|
|
switch request.URL.Path {
|
|
case EventsPath:
|
|
status = http.StatusOK
|
|
body = ": connected\n\n"
|
|
case CommandStatePath:
|
|
status = http.StatusOK
|
|
body = string(commandPayload)
|
|
}
|
|
return &http.Response{
|
|
StatusCode: status,
|
|
Body: io.NopCloser(strings.NewReader(body)),
|
|
Header: make(http.Header),
|
|
}, nil
|
|
})
|
|
|
|
var received childCommand
|
|
bridge.mu.Lock()
|
|
bridge.ctx = context.Background()
|
|
bridge.emitToWails = func(_ context.Context, name string, args ...any) {
|
|
if name == CommandEventName && len(args) == 1 {
|
|
received, _ = args[0].(childCommand)
|
|
}
|
|
}
|
|
bridge.mu.Unlock()
|
|
|
|
if err := bridge.consumeEventStream(context.Background()); err != nil {
|
|
t.Fatalf("consume reconnected stream: %v", err)
|
|
}
|
|
if received.ID != "workbench:query-1" || received.Action != "close" || received.Reason != ExitReasonRequested {
|
|
t.Fatalf("replayed command = %#v", received)
|
|
}
|
|
}
|
|
|
|
func TestBridgeReplaysPendingFocusWithoutAcknowledgingBeforeNativeFocus(t *testing.T) {
|
|
bridge := newBridge(ChildOptions{
|
|
ParentURL: "http://127.0.0.1:43119",
|
|
Token: "test-token",
|
|
ID: "ai-chat",
|
|
Kind: "ai-chat",
|
|
})
|
|
commandPayload, err := json.Marshal(childCommand{
|
|
ID: "ai-chat",
|
|
Action: "focus",
|
|
Payload: visibilityCommandPayload{VisibilityRevision: 7},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal focus command: %v", err)
|
|
}
|
|
acknowledgements := 0
|
|
bridge.client.Transport = roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
|
status := http.StatusNoContent
|
|
body := ""
|
|
switch {
|
|
case request.URL.Path == EventsPath:
|
|
status = http.StatusOK
|
|
body = ": connected\n\n"
|
|
case request.URL.Path == CommandStatePath && request.Method == http.MethodGet:
|
|
status = http.StatusOK
|
|
body = string(commandPayload)
|
|
case request.URL.Path == CommandStatePath && request.Method == http.MethodPost:
|
|
acknowledgements++
|
|
status = http.StatusOK
|
|
body = `{"success":true,"id":"ai-chat","visibilityRevision":7}`
|
|
}
|
|
return &http.Response{
|
|
StatusCode: status,
|
|
Body: io.NopCloser(strings.NewReader(body)),
|
|
Header: make(http.Header),
|
|
}, nil
|
|
})
|
|
|
|
var received childCommand
|
|
bridge.mu.Lock()
|
|
bridge.ctx = context.Background()
|
|
bridge.emitToWails = func(_ context.Context, name string, args ...any) {
|
|
if name == CommandEventName && len(args) == 1 {
|
|
received, _ = args[0].(childCommand)
|
|
}
|
|
}
|
|
bridge.mu.Unlock()
|
|
|
|
if err := bridge.consumeEventStream(context.Background()); err != nil {
|
|
t.Fatalf("consume reconnected stream: %v", err)
|
|
}
|
|
if received.Action != "focus" || positiveVisibilityRevision(received.Payload) != 7 {
|
|
t.Fatalf("replayed focus command = %#v", received)
|
|
}
|
|
if acknowledgements != 0 {
|
|
t.Fatalf("focus acknowledgements before native focus = %d, want 0", acknowledgements)
|
|
}
|
|
}
|
|
|
|
func TestBridgeDeliversLiveFocusWithoutAcknowledgingBeforeNativeFocus(t *testing.T) {
|
|
bridge := newBridge(ChildOptions{
|
|
ParentURL: "http://127.0.0.1:43119",
|
|
Token: "test-token",
|
|
ID: "ai-chat",
|
|
Kind: "ai-chat",
|
|
})
|
|
eventPayload, err := json.Marshal(bridgeEvent{
|
|
Name: CommandEventName,
|
|
Args: []any{childCommand{
|
|
ID: "ai-chat",
|
|
Action: "focus",
|
|
Payload: visibilityCommandPayload{VisibilityRevision: 9},
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal focus event: %v", err)
|
|
}
|
|
acknowledgements := 0
|
|
bridge.client.Transport = roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
|
status := http.StatusNoContent
|
|
body := ""
|
|
switch {
|
|
case request.URL.Path == EventsPath:
|
|
status = http.StatusOK
|
|
body = "data: " + string(eventPayload) + "\n\n"
|
|
case request.URL.Path == CommandStatePath && request.Method == http.MethodPost:
|
|
acknowledgements++
|
|
status = http.StatusOK
|
|
body = `{"success":true,"id":"ai-chat","visibilityRevision":9}`
|
|
}
|
|
return &http.Response{
|
|
StatusCode: status,
|
|
Body: io.NopCloser(strings.NewReader(body)),
|
|
Header: make(http.Header),
|
|
}, nil
|
|
})
|
|
bridge.mu.Lock()
|
|
bridge.ctx = context.Background()
|
|
bridge.emitToWails = func(context.Context, string, ...any) {}
|
|
bridge.mu.Unlock()
|
|
|
|
if err := bridge.consumeEventStream(context.Background()); err != nil {
|
|
t.Fatalf("consume live focus stream: %v", err)
|
|
}
|
|
if acknowledgements != 0 {
|
|
t.Fatalf("focus acknowledgements before native focus = %d, want 0", acknowledgements)
|
|
}
|
|
}
|