* dws event * fix consume fail * test: add stream ticket injection probe * feat: add portal ticket stream mode * user event * fix: allow portal ticket normal without app secret * event * user event * eventType filter * refactor(event): 优化IPC端点路径处理和改进相关测试 - 用dwsevent.IPCEndpoint替代原先根据GOOS判断的路径逻辑 - 新增event包实现Unix socket路径长度限制及长路径fallback机制 - 添加endpoint_test.go覆盖路径短长及唯一性的单元测试 - 修改busctl模块使用统一的IPC端点获取方法,避免重复实现 - transport_unix.go新增checkSocketPath函数检查路径长度,防止EINVAL错误 - 在监听和连接Unix socket时加入路径限制检查,提升错误明晰度 - 去除多个文件中无用的runtime导入,简化代码依赖 * opt * event skill * default value * install script event * fix: remove subscribe id event fanout filter * fix(personal): 修正指定发送人消息描述错误 * more im event * filter subId * fix: align personal event schema with stream payload * fix: avoid duplicate app helper name * feat: simplify personal event schemas * feat: simplify event schema output * docs: refine dingtalk event skill references * feat: align personal event consume flags * fix event stop and status visibility * hide app event public entrypoints * hide incomplete personal sender event * remove external event reference comments * chore(event): prepare official release * fix(event): harden personal stream lifecycle --------- Co-authored-by: 玉澜 <yulan.wqy@alibaba-inc.com> Co-authored-by: zhengyubai <zhengyubai618@gmail.com>
238 lines
7.0 KiB
Go
238 lines
7.0 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 transport
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Each frame type is round-tripped through encoding/json then PeekType
|
|
// to guard against accidental wire-format changes (the JSON field tags
|
|
// double as part of the cross-version protocol contract).
|
|
|
|
func TestFrameType_StableWireValues(t *testing.T) {
|
|
// If any of these strings change we've made a protocol-breaking
|
|
// change. The test value list is duplicated here on purpose so a
|
|
// reviewer renaming a constant is forced to also update the test.
|
|
wants := map[FrameType]string{
|
|
FrameTypeHello: "hello",
|
|
FrameTypeHelloAck: "hello_ack",
|
|
FrameTypeEvent: "event",
|
|
FrameTypeHeartbeat: "heartbeat",
|
|
FrameTypeSourceState: "source_state",
|
|
FrameTypeBye: "bye",
|
|
FrameTypeStatusReq: "status_req",
|
|
FrameTypeStatusResp: "status_resp",
|
|
}
|
|
for ft, want := range wants {
|
|
if string(ft) != want {
|
|
t.Errorf("FrameType wire value drift: %v != %q", ft, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHelloRole_StableWireValues(t *testing.T) {
|
|
wants := map[HelloRole]string{
|
|
HelloRoleConsumer: "",
|
|
HelloRoleStatus: "status",
|
|
HelloRoleStop: "stop",
|
|
}
|
|
for r, want := range wants {
|
|
if string(r) != want {
|
|
t.Errorf("HelloRole wire value drift: %v != %q", r, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func roundTrip(t *testing.T, in any, dst any) {
|
|
t.Helper()
|
|
b, err := json.Marshal(in)
|
|
if err != nil {
|
|
t.Fatalf("marshal %T: %v", in, err)
|
|
}
|
|
if err := json.Unmarshal(b, dst); err != nil {
|
|
t.Fatalf("unmarshal %T: %v", in, err)
|
|
}
|
|
}
|
|
|
|
func TestHello_Roundtrip(t *testing.T) {
|
|
in := Hello{
|
|
Type: FrameTypeHello,
|
|
ConsumerPID: 42,
|
|
EventTypes: []string{"im.*", "approval.task"},
|
|
Filter: `^im\.`,
|
|
Compact: true,
|
|
Role: HelloRoleStatus,
|
|
}
|
|
var out Hello
|
|
roundTrip(t, in, &out)
|
|
if out.Type != in.Type || out.ConsumerPID != in.ConsumerPID || out.Filter != in.Filter ||
|
|
out.Compact != in.Compact || out.Role != in.Role || len(out.EventTypes) != len(in.EventTypes) {
|
|
t.Fatalf("roundtrip mismatch: %+v != %+v", out, in)
|
|
}
|
|
}
|
|
|
|
func TestHello_OmitemptyForDefaults(t *testing.T) {
|
|
// Default values should NOT appear in the wire form so old/new readers
|
|
// stay tolerant of each other (each new field comes in with its
|
|
// zero value by default).
|
|
in := Hello{Type: FrameTypeHello, ConsumerPID: 1}
|
|
b, _ := json.Marshal(in)
|
|
s := string(b)
|
|
for _, k := range []string{`"event_types"`, `"filter"`, `"compact"`, `"role"`} {
|
|
if strings.Contains(s, k) {
|
|
t.Errorf("zero-value field %s leaked into wire form: %s", k, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHelloAck_Roundtrip(t *testing.T) {
|
|
in := HelloAck{
|
|
Type: FrameTypeHelloAck,
|
|
BusPID: 12345,
|
|
SourceState: "connected",
|
|
StateSource: "inferred",
|
|
ClientIDSource: "env",
|
|
ClientSecretSource: "env",
|
|
IdleTimeoutSecs: 300,
|
|
}
|
|
var out HelloAck
|
|
roundTrip(t, in, &out)
|
|
if out != in {
|
|
t.Fatalf("HelloAck roundtrip: %+v != %+v", out, in)
|
|
}
|
|
}
|
|
|
|
func TestEvent_Roundtrip(t *testing.T) {
|
|
in := Event{
|
|
Type: FrameTypeEvent,
|
|
Seq: 7,
|
|
EventID: "ev_x",
|
|
EventBornTime: 1700000000123,
|
|
EventCorpID: "corp_x",
|
|
EventType: "im.message.receive_v1",
|
|
EventUnifiedAppID: "app_y",
|
|
Data: `{"text":"hi"}`,
|
|
Headers: map[string]string{"extra": "v"},
|
|
ReceivedAtUnixMS: 1700000000999,
|
|
}
|
|
var out Event
|
|
roundTrip(t, in, &out)
|
|
if out.EventID != in.EventID || out.Seq != in.Seq || out.Data != in.Data ||
|
|
out.Headers["extra"] != "v" {
|
|
t.Fatalf("Event roundtrip: %+v != %+v", out, in)
|
|
}
|
|
}
|
|
|
|
func TestEvent_OmitsZeroOptionalHeader(t *testing.T) {
|
|
in := Event{Type: FrameTypeEvent, EventID: "ev_1", EventType: "x", Data: "{}"}
|
|
b, _ := json.Marshal(in)
|
|
if strings.Contains(string(b), `"headers"`) {
|
|
t.Errorf("nil Headers should not appear: %s", b)
|
|
}
|
|
}
|
|
|
|
func TestHeartbeat_Roundtrip(t *testing.T) {
|
|
in := Heartbeat{Type: FrameTypeHeartbeat}
|
|
var out Heartbeat
|
|
roundTrip(t, in, &out)
|
|
if out.Type != FrameTypeHeartbeat {
|
|
t.Fatalf("Heartbeat type lost: %v", out)
|
|
}
|
|
}
|
|
|
|
func TestSourceState_Roundtrip(t *testing.T) {
|
|
in := SourceState{
|
|
Type: FrameTypeSourceState,
|
|
State: "reconnecting",
|
|
StateSource: "inferred",
|
|
Attempt: 3,
|
|
}
|
|
var out SourceState
|
|
roundTrip(t, in, &out)
|
|
if out != in {
|
|
t.Fatalf("SourceState mismatch: %+v != %+v", out, in)
|
|
}
|
|
}
|
|
|
|
func TestBye_Roundtrip(t *testing.T) {
|
|
for _, reason := range []string{"client_done", "shutdown", "idle_timeout", "stop_request"} {
|
|
in := Bye{Type: FrameTypeBye, Reason: reason}
|
|
var out Bye
|
|
roundTrip(t, in, &out)
|
|
if out.Reason != reason {
|
|
t.Errorf("Bye reason %q lost: %+v", reason, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStatusResp_Roundtrip(t *testing.T) {
|
|
in := StatusResp{
|
|
Type: FrameTypeStatusResp,
|
|
Bus: StatusBus{
|
|
PID: 99, UptimeSecs: 600, IdleTimeoutSec: 300,
|
|
ClientID: "ding_x", Edition: "open",
|
|
},
|
|
SourceState: StatusSource{
|
|
State: "connected", Source: "inferred", ReconnectCount: 1,
|
|
LastEventAtMS: 1700000000000, LastReconnectMS: 1700000001000,
|
|
},
|
|
Consumers: []StatusConsumer{
|
|
{PID: 12350, EventTypes: []string{"im.*"}, Filter: `^im\.`,
|
|
SubscribedAtMS: 1700000000000, Received: 100, Dropped: 2},
|
|
},
|
|
PerEventTypeCounters: map[string]Counters{
|
|
"im.message.receive_v1": {Received: 100, Dropped: 2},
|
|
},
|
|
}
|
|
var out StatusResp
|
|
roundTrip(t, in, &out)
|
|
if out.Bus.ClientID != in.Bus.ClientID || len(out.Consumers) != 1 ||
|
|
out.PerEventTypeCounters["im.message.receive_v1"].Received != 100 {
|
|
t.Fatalf("StatusResp roundtrip mismatch: %+v", out)
|
|
}
|
|
}
|
|
|
|
// PeekType is used by the daemon to dispatch incoming frames before
|
|
// fully decoding into the typed struct. Behaviour at boundaries matters.
|
|
|
|
func TestPeekType_EachFrameVariant(t *testing.T) {
|
|
cases := []struct {
|
|
v any
|
|
typ FrameType
|
|
}{
|
|
{Hello{Type: FrameTypeHello}, FrameTypeHello},
|
|
{HelloAck{Type: FrameTypeHelloAck}, FrameTypeHelloAck},
|
|
{Event{Type: FrameTypeEvent, EventID: "x"}, FrameTypeEvent},
|
|
{Heartbeat{Type: FrameTypeHeartbeat}, FrameTypeHeartbeat},
|
|
{SourceState{Type: FrameTypeSourceState, State: "connected"}, FrameTypeSourceState},
|
|
{Bye{Type: FrameTypeBye, Reason: "x"}, FrameTypeBye},
|
|
{StatusReq{Type: FrameTypeStatusReq}, FrameTypeStatusReq},
|
|
{StatusResp{Type: FrameTypeStatusResp}, FrameTypeStatusResp},
|
|
}
|
|
for _, c := range cases {
|
|
b, _ := json.Marshal(c.v)
|
|
got, err := PeekType(b)
|
|
if err != nil {
|
|
t.Errorf("PeekType(%T): %v", c.v, err)
|
|
continue
|
|
}
|
|
if got != c.typ {
|
|
t.Errorf("PeekType(%T) = %q, want %q", c.v, got, c.typ)
|
|
}
|
|
}
|
|
}
|