fix(event): address PR review gates

This commit is contained in:
wxianfeng
2026-07-17 11:57:27 +08:00
parent c7c9a6f926
commit f59be6c19a
19 changed files with 181 additions and 160 deletions
+2 -2
View File
@@ -62,7 +62,7 @@ func TestPersonalEventProjectorUsesRawEnvelopeForDebug(t *testing.T) {
}
ev := transport.Event{
EventID: "raw-event",
Data: `{"payload":{"uid":147867,"bizid":"internal-bizid"}}`,
Data: `{"payload":{"uid":100001,"bizid":"internal-bizid"}}`,
Headers: map[string]string{"TOPIC": "raw"},
}
projected, err := projector(ev)
@@ -143,7 +143,7 @@ func TestEventConsumeRetiredPersonalFlagsAreUnknown(t *testing.T) {
func TestEventConsumeAsAppRejectedBeforePersonalParamSpecFlags(t *testing.T) {
for _, args := range [][]string{
{"--as", "app", "--user", "507971"},
{"--as", "app", "--user", "test-user-001"},
{"--as", "app", "--open-dingtalk-id", "open-user-1"},
{"--as", "app", "--group", "cid"},
{"--as", "app", "--query", "报警"},
+5 -10
View File
@@ -405,7 +405,7 @@ func TestPersonalEventFromUserIsPubliclyAvailable(t *testing.T) {
consumeCmd := newEventConsumeCommand()
consumeCmd.SilenceUsage = true
consumeCmd.SilenceErrors = true
consumeCmd.SetArgs([]string{personal.EventFromUser, "--user", "507971", "--dry-run"})
consumeCmd.SetArgs([]string{personal.EventFromUser, "--user", "test-user-001", "--dry-run"})
if err := consumeCmd.Execute(); err != nil {
t.Fatalf("consume dry-run Execute() error = %v", err)
}
@@ -420,7 +420,7 @@ func TestPersonalEventFromUserIsPubliclyAvailable(t *testing.T) {
conflictingTargetCmd := newEventConsumeCommand()
conflictingTargetCmd.SilenceUsage = true
conflictingTargetCmd.SilenceErrors = true
conflictingTargetCmd.SetArgs([]string{personal.EventFromUser, "--user", "507971", "--open-dingtalk-id", "open-user-1", "--dry-run"})
conflictingTargetCmd.SetArgs([]string{personal.EventFromUser, "--user", "test-user-001", "--open-dingtalk-id", "open-user-1", "--dry-run"})
err := conflictingTargetCmd.Execute()
if err == nil || !strings.Contains(err.Error(), "--user and --open-dingtalk-id are mutually exclusive for "+personal.EventFromUser) {
t.Fatalf("conflicting target identity error = %v", err)
@@ -469,17 +469,13 @@ func TestEventConsumeCobraSchemaIncludesOpenDingTalkID(t *testing.T) {
if _, ok := params["odid"]; ok {
t.Fatalf("schema parameters unexpectedly include odid alias: %#v", params)
}
for name, want := range map[string]string{
"user": "--subscribe-id is absent, event_key uses singleChat or sender, and --open-dingtalk-id is absent",
"open-dingtalk-id": "--subscribe-id is absent, event_key uses singleChat or sender, and --user is absent",
"group": "--subscribe-id is absent and event_key uses group",
} {
for _, name := range []string{"user", "open-dingtalk-id", "group"} {
param, ok := params[name].(map[string]any)
if !ok {
t.Fatalf("schema parameter %s = %#v", name, params[name])
}
if got := param["required_when"]; got != want {
t.Fatalf("schema parameter %s required_when = %#v, want %q", name, got, want)
if got, exists := param["required_when"]; exists {
t.Fatalf("schema parameter %s unexpectedly declares required_when = %#v", name, got)
}
}
constraints, ok := doc["constraints"].(map[string]any)
@@ -511,7 +507,6 @@ func TestEventConsumeCobraSchemaIncludesOpenDingTalkID(t *testing.T) {
t.Fatalf("schema constraint %s = %#v, missing %#v", field, groups, want)
}
assertJSONConstraintGroup("require_one_of", []string{"event_key", "subscribe-id"})
assertJSONConstraintGroup("mutually_exclusive", []string{"user", "open-dingtalk-id", "group"})
}
func TestPersonalEventSchemaRejectsTableFormat(t *testing.T) {
@@ -74,13 +74,9 @@ func TestEventRegistryDeliversOneTypedSchemaPath(t *testing.T) {
if _, exists := consumeParams["odid"]; exists {
t.Error("event.consume exposes unsupported --odid alias")
}
for name, want := range map[string]string{
"user": "--subscribe-id is absent, event_key uses singleChat or sender, and --open-dingtalk-id is absent",
"open-dingtalk-id": "--subscribe-id is absent, event_key uses singleChat or sender, and --user is absent",
"group": "--subscribe-id is absent and event_key uses group",
} {
if got := schemaContractString(consumeParams[name]["required_when"]); got != want {
t.Errorf("event.consume --%s required_when = %q, want %q", name, got, want)
for _, name := range []string{"user", "open-dingtalk-id", "group"} {
if _, exists := consumeParams[name]["required_when"]; exists {
t.Errorf("event.consume --%s unexpectedly declares required_when", name)
}
}
if _, exists := consumeParams["duration"]["default"]; exists {
@@ -91,7 +87,6 @@ func TestEventRegistryDeliversOneTypedSchemaPath(t *testing.T) {
}
assertSchemaContractPositional(t, consume, "event_key", false)
assertSchemaContractConstraintGroup(t, consume, "require_one_of", []string{"event_key", "subscribe-id"})
assertSchemaContractConstraintGroup(t, consume, "mutually_exclusive", []string{"user", "open-dingtalk-id", "group"})
eventSchema := payload.Tools["event.schema"]
assertSchemaContractPositional(t, eventSchema, "event_key", true)
@@ -1,6 +1,6 @@
{
"version": 1,
"source_hash": "sha256:2f24ccd75c2c02fe8c05f3f0b8ca774313232559bec7b8ab3d94f856b5712e07",
"source_hash": "sha256:a65016dce21d32c609d5f422645c02bb28b00e719e459a548829fb513d6b3e5f",
"surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed",
"coverage": {
"surface_products": 22,
@@ -1,6 +1,6 @@
{
"version": 1,
"source_hash": "sha256:2f24ccd75c2c02fe8c05f3f0b8ca774313232559bec7b8ab3d94f856b5712e07",
"source_hash": "sha256:a65016dce21d32c609d5f422645c02bb28b00e719e459a548829fb513d6b3e5f",
"surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed",
"source_files": 150,
"hint_files": 46,
+14 -42
View File
@@ -1,12 +1,12 @@
{
"version": 1,
"source_hash": "sha256:baa16e40767270f9833ebccbdfc3732b03033b2cd4c8a95d96f87da5c73bcf1e",
"source_hash": "sha256:5b8ce46a210299db7bb06a463155410f1a35c10becdcc2e8b23a6c804ee759b8",
"surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed",
"catalog": {
"agent_metadata": {
"products_with_metadata": 22,
"source": "embedded-skill-metadata",
"source_hash": "sha256:2f24ccd75c2c02fe8c05f3f0b8ca774313232559bec7b8ab3d94f856b5712e07",
"source_hash": "sha256:a65016dce21d32c609d5f422645c02bb28b00e719e459a548829fb513d6b3e5f",
"surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed",
"surface_products": 22,
"surface_tools": 572,
@@ -290142,13 +290142,6 @@
"cli_path": "event consume",
"confirmation": "not_required",
"constraints": {
"mutually_exclusive": [
[
"user",
"open-dingtalk-id",
"group"
]
],
"require_one_of": [
[
"event_key",
@@ -291443,23 +291436,17 @@
},
"required_when": {
"candidates": [
{
"precedence": "typed_metadata",
"selected": true,
"source": "typed_parameter_metadata",
"value": "--subscribe-id is absent and event_key uses group"
},
{
"precedence": "default",
"selected": false,
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "typed_metadata",
"precedence": "default",
"resolution": "highest_precedence",
"source": "typed_parameter_metadata",
"value": "--subscribe-id is absent and event_key uses group"
"source": "default",
"value": ""
},
"type": {
"candidates": [
@@ -291478,7 +291465,6 @@
},
"property": "group",
"required": false,
"required_when": "--subscribe-id is absent and event_key uses group",
"type": "string"
},
"max-events": {
@@ -291702,23 +291688,17 @@
},
"required_when": {
"candidates": [
{
"precedence": "typed_metadata",
"selected": true,
"source": "typed_parameter_metadata",
"value": "--subscribe-id is absent, event_key uses singleChat or sender, and --user is absent"
},
{
"precedence": "default",
"selected": false,
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "typed_metadata",
"precedence": "default",
"resolution": "highest_precedence",
"source": "typed_parameter_metadata",
"value": "--subscribe-id is absent, event_key uses singleChat or sender, and --user is absent"
"source": "default",
"value": ""
},
"type": {
"candidates": [
@@ -291737,7 +291717,6 @@
},
"property": "openDingtalkId",
"required": false,
"required_when": "--subscribe-id is absent, event_key uses singleChat or sender, and --user is absent",
"type": "string"
},
"output-dir": {
@@ -292753,23 +292732,17 @@
},
"required_when": {
"candidates": [
{
"precedence": "typed_metadata",
"selected": true,
"source": "typed_parameter_metadata",
"value": "--subscribe-id is absent, event_key uses singleChat or sender, and --open-dingtalk-id is absent"
},
{
"precedence": "default",
"selected": false,
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "typed_metadata",
"precedence": "default",
"resolution": "highest_precedence",
"source": "typed_parameter_metadata",
"value": "--subscribe-id is absent, event_key uses singleChat or sender, and --open-dingtalk-id is absent"
"source": "default",
"value": ""
},
"type": {
"candidates": [
@@ -292788,7 +292761,6 @@
},
"property": "user",
"required": false,
"required_when": "--subscribe-id is absent, event_key uses singleChat or sender, and --open-dingtalk-id is absent",
"type": "string"
}
},
+1 -4
View File
@@ -57,10 +57,7 @@ func init() {
registerRequireOneOf("dev.search_open_platform_docs_rag", "query", "keyword")
registerRequireOneOf("devdoc.search_open_error_code_rag", "query", "request-id", "error-code", "error-message", "context")
registerRequireOneOf("devdoc.search_open_platform_docs_rag", "query", "keyword")
RegisterRuntimeSchemaConstraints("event.consume", RuntimeSchemaConstraints{
MutuallyExclusive: [][]string{{"user", "open-dingtalk-id", "group"}},
RequireOneOf: [][]string{{"event_key", "subscribe-id"}},
})
registerRequireOneOf("event.consume", "event_key", "subscribe-id")
registerExclusiveOneOf("event.stop", "all", "subscribe_id")
registerRequireOneOf("doc.insert_document_block", "text", "heading", "element")
registerExclusiveOneOf("doc.update_document", "content", "content-file")
@@ -110,13 +110,6 @@ func cloneRuntimeSchemaStringMap(source map[string]string) map[string]string {
}
func init() {
RegisterRuntimeSchemaParameterMetadata("event.consume", RuntimeSchemaParameterMetadata{
RequiredWhen: map[string]string{
"user": "--subscribe-id is absent, event_key uses singleChat or sender, and --open-dingtalk-id is absent",
"open-dingtalk-id": "--subscribe-id is absent, event_key uses singleChat or sender, and --user is absent",
"group": "--subscribe-id is absent and event_key uses group",
},
})
RegisterRuntimeSchemaParameterMetadata("aisearch.enterprise_person_search", RuntimeSchemaParameterMetadata{
Required: []string{"keyword"},
})
+4 -4
View File
@@ -151,7 +151,7 @@ func TestCompactFormatter_DispatchesPerEventType(t *testing.T) {
}
}
func TestStructuredFormatsUseProjector(t *testing.T) {
func TestCrossPlatformCoverageStructuredFormatsUseProjector(t *testing.T) {
projector := Projector(func(ev transport.Event) (any, error) {
return map[string]any{"type": ev.EventType, "content": "hello"}, nil
})
@@ -179,7 +179,7 @@ func TestStructuredFormatsUseProjector(t *testing.T) {
}
}
func TestProjectionFailureWarnsAndEmitsFallback(t *testing.T) {
func TestCrossPlatformCoverageProjectionFailureWarnsAndEmitsFallback(t *testing.T) {
var warnings bytes.Buffer
f, err := NewFormatter(
FormatNDJSON,
@@ -210,9 +210,9 @@ func TestProjectionFailureWarnsAndEmitsFallback(t *testing.T) {
}
}
func TestRawFormatBypassesProjector(t *testing.T) {
func TestCrossPlatformCoverageRawFormatBypassesProjector(t *testing.T) {
called := false
raw := `{"payload":{"uid":147867,"clientId":"internal-client","filterSubId":"internal-filter","bizid":"internal-bizid"}}`
raw := `{"payload":{"uid":100001,"clientId":"internal-client","filterSubId":"internal-filter","bizid":"internal-bizid"}}`
f, err := NewFormatter(FormatRaw, WithProjector(func(transport.Event) (any, error) {
called = true
return map[string]any{"projected": true}, nil
+1 -1
View File
@@ -62,7 +62,7 @@ func TestPipeline_OutputDirFallback(t *testing.T) {
}
}
func TestPipeline_StdoutAndOutputDirUseSameProjection(t *testing.T) {
func TestCrossPlatformCoveragePipelineStdoutAndOutputDirUseSameProjection(t *testing.T) {
ev := transport.Event{
EventType: "personal.message",
EventID: "ev1",
+4 -4
View File
@@ -72,7 +72,7 @@ func TestClientCreateSubscriptionDWSRequestAndArrayResponse(t *testing.T) {
RuleType: "singleChat",
Name: "test-o2o",
RuleParam: map[string]any{
"targetUid": "507971",
"targetUid": "test-user-001",
"targetUidType": "staffId",
},
Filter: map[string]any{"field": "payload.body.content", "op": "contains", "value": "P0"},
@@ -94,7 +94,7 @@ func TestClientCreateSubscriptionDWSRequestAndArrayResponse(t *testing.T) {
if err := json.Unmarshal([]byte(gotReq.FilterRule), &filterRule); err != nil {
t.Fatalf("filterRule is not JSON: %q: %v", gotReq.FilterRule, err)
}
if filterRule["targetUid"] != "507971" || filterRule["targetUidType"] != "staffId" {
if filterRule["targetUid"] != "test-user-001" || filterRule["targetUidType"] != "staffId" {
t.Fatalf("filterRule = %#v", filterRule)
}
if gotReq.Ext["ruleType"] != "singleChat" || gotReq.Ext["name"] != "test-o2o" || gotReq.Ext["idempotencyKey"] != "idem-1" {
@@ -218,7 +218,7 @@ func TestClientDebugLogCreateSubscriptionRequestResponse(t *testing.T) {
EventKey: EventSingleChat,
RuleType: "singleChat",
RuleParam: map[string]any{
"targetUid": "507971",
"targetUid": "test-user-001",
"targetUidType": "staffId",
},
IdempotencyKey: "idem-1",
@@ -234,7 +234,7 @@ func TestClientDebugLogCreateSubscriptionRequestResponse(t *testing.T) {
EventSingleChat,
"filterRule",
"targetUid",
"507971",
"test-user-001",
"sub-1",
"req-ok",
} {
+33 -10
View File
@@ -201,10 +201,8 @@ func ProjectOutput(ev transport.Event) (any, error) {
if isMessageReceiveEvent(eventType) {
var payload personalMessagePayload
if len(bytes.TrimSpace(data.Payload)) > 0 && string(bytes.TrimSpace(data.Payload)) != "null" {
if err := json.Unmarshal(data.Payload, &payload); err != nil {
return ev, fmt.Errorf("decode personal message payload: %w", err)
}
if err := decodeRequiredPayload(data.Payload, &payload); err != nil {
return ev, fmt.Errorf("decode personal message payload: %w", err)
}
return MessageEventOutput{
Type: eventType,
@@ -241,7 +239,7 @@ func ProjectOutput(ev transport.Event) (any, error) {
func projectReadEvent(ev transport.Event, base baseEventOutput, raw json.RawMessage) (any, error) {
var payload personalReadPayload
if err := decodeActionPayload(raw, &payload); err != nil {
if err := decodeRequiredPayload(raw, &payload); err != nil {
return ev, fmt.Errorf("decode personal read payload: %w", err)
}
return ReadEventOutput{
@@ -262,7 +260,7 @@ func projectReadEvent(ev transport.Event, base baseEventOutput, raw json.RawMess
func projectRecallEvent(ev transport.Event, base baseEventOutput, raw json.RawMessage) (any, error) {
var payload personalRecallPayload
if err := decodeActionPayload(raw, &payload); err != nil {
if err := decodeRequiredPayload(raw, &payload); err != nil {
return ev, fmt.Errorf("decode personal recall payload: %w", err)
}
return RecallEventOutput{
@@ -283,7 +281,7 @@ func projectRecallEvent(ev transport.Event, base baseEventOutput, raw json.RawMe
func projectReactionEvent(ev transport.Event, base baseEventOutput, raw json.RawMessage) (any, error) {
var payload personalReactionPayload
if err := decodeActionPayload(raw, &payload); err != nil {
if err := decodeRequiredPayload(raw, &payload); err != nil {
return ev, fmt.Errorf("decode personal reaction payload: %w", err)
}
return ReactionEventOutput{
@@ -305,11 +303,36 @@ func projectReactionEvent(ev transport.Event, base baseEventOutput, raw json.Raw
}, nil
}
func decodeActionPayload(raw json.RawMessage, target any) error {
func decodeRequiredPayload(raw json.RawMessage, target any) error {
trimmed := bytes.TrimSpace(raw)
if len(trimmed) == 0 || string(trimmed) == "null" {
return nil
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
return fmt.Errorf("payload is missing")
}
var payloadObject map[string]json.RawMessage
if err := json.Unmarshal(trimmed, &payloadObject); err != nil {
return err
}
if len(payloadObject) == 0 {
return fmt.Errorf("payload is empty")
}
body, ok := payloadObject["body"]
if !ok {
return fmt.Errorf("payload body is missing")
}
body = bytes.TrimSpace(body)
if len(body) == 0 || bytes.Equal(body, []byte("null")) {
return fmt.Errorf("payload body is missing")
}
var bodyObject map[string]json.RawMessage
if err := json.Unmarshal(body, &bodyObject); err != nil {
return fmt.Errorf("decode payload body: %w", err)
}
if len(bodyObject) == 0 {
return fmt.Errorf("payload body is empty")
}
return json.Unmarshal(trimmed, target)
}
+71 -25
View File
@@ -32,7 +32,7 @@ func personalMessageData(eventKey string) string {
"payload":{
"body":{
"createTime":"2026-07-08 12:00:35",
"sender":"昊小淼",
"sender":"测试用户甲",
"openMessageId":"msg-1",
"senderOpenDingTalkId":"open-user-1",
"openConversationId":"cid-1",
@@ -55,16 +55,16 @@ func personalReadData(eventKey, messageID, conversationID string) string {
"msgReadTime":"2026-07-14 13:53:31",
"openConversationId":%q,
"openMessageId":%q,
"reader":"郑御白",
"reader":"测试用户乙",
"readerOpenDingTalkId":"reader-open-id",
"sender":"昊淼",
"sender":"测试用户甲",
"senderOpenDingTalkId":"sender-open-id"
},
"clientId":"internal-client",
"corpid":"internal-corp",
"event_time":1784008411652,
"filterSubId":"internal-filter",
"uid":147867
"uid":100001
}
}`, eventKey, conversationID, messageID)
}
@@ -81,16 +81,16 @@ func personalRecallData(eventKey, messageID, conversationID string) string {
"msgRecallTime":"2026-07-14 13:56:32",
"openConversationId":%q,
"openMessageId":%q,
"recaller":"郑御白",
"recaller":"测试用户乙",
"recallerOpenDingTalkId":"recaller-open-id",
"sender":"郑御白",
"sender":"测试用户乙",
"senderOpenDingTalkId":"sender-open-id"
},
"clientId":"internal-client",
"corpid":"internal-corp",
"event_time":1784008592766,
"filterSubId":"internal-filter",
"uid":147867
"uid":100001
}
}`, eventKey, conversationID, messageID)
}
@@ -108,23 +108,23 @@ func personalReactionData(eventKey, messageID, conversationID string) string {
"emotionText":"微笑",
"openConversationId":%q,
"openSourceMessageId":%q,
"oper":"郑御白",
"oper":"测试用户乙",
"operOpenDingtalkId":"operator-open-id",
"operateTime":"2026-07-14 13:57:59",
"operateType":"add",
"sender":"昊淼",
"sender":"测试用户甲",
"senderOpenDingTalkId":"sender-open-id"
},
"clientId":"internal-client",
"corpid":"internal-corp",
"event_time":1784008679217,
"filterSubId":"internal-filter",
"uid":147867
"uid":100001
}
}`, eventKey, conversationID, messageID)
}
func TestProjectOutputMessageEvents(t *testing.T) {
func TestCrossPlatformCoverageProjectOutputMessageEvents(t *testing.T) {
for _, eventKey := range []string{EventMention, EventSingleChat, EventInChat, EventFromUser} {
t.Run(eventKey, func(t *testing.T) {
projected, err := ProjectOutput(transport.Event{
@@ -149,7 +149,7 @@ func TestProjectOutputMessageEvents(t *testing.T) {
SubscribeID: "outer-sub",
MessageID: "msg-1",
ConversationID: "cid-1",
Sender: "昊小淼",
Sender: "测试用户甲",
SenderOpenDingTalkID: "open-user-1",
Content: "在吗",
CreateTime: "2026-07-08 12:00:35",
@@ -162,7 +162,7 @@ func TestProjectOutputMessageEvents(t *testing.T) {
}
}
func TestProjectOutputDecodesWrappedJSONString(t *testing.T) {
func TestCrossPlatformCoverageProjectOutputDecodesWrappedJSONString(t *testing.T) {
wrapped, err := json.Marshal(personalMessageData(EventSingleChat))
if err != nil {
t.Fatal(err)
@@ -177,7 +177,7 @@ func TestProjectOutputDecodesWrappedJSONString(t *testing.T) {
}
}
func TestProjectOutputFallsBackToTransportFields(t *testing.T) {
func TestCrossPlatformCoverageProjectOutputFallsBackToTransportFields(t *testing.T) {
projected, err := ProjectOutput(transport.Event{
EventID: "outer-event",
EventBornTime: 123,
@@ -194,7 +194,7 @@ func TestProjectOutputFallsBackToTransportFields(t *testing.T) {
}
}
func TestProjectOutputReadEvents(t *testing.T) {
func TestCrossPlatformCoverageProjectOutputReadEvents(t *testing.T) {
for _, eventKey := range []string{EventReadO2O, EventReadGroup} {
t.Run(eventKey, func(t *testing.T) {
projected, err := ProjectOutput(transport.Event{
@@ -211,9 +211,9 @@ func TestProjectOutputReadEvents(t *testing.T) {
SubscribeID: "read-sub",
MessageID: "read-message",
ConversationID: "read-conversation",
Reader: "郑御白",
Reader: "测试用户乙",
ReaderOpenDingTalkID: "reader-open-id",
Sender: "昊淼",
Sender: "测试用户甲",
SenderOpenDingTalkID: "sender-open-id",
ReadTime: "2026-07-14 13:53:31",
EventTime: 1784008411652,
@@ -226,7 +226,7 @@ func TestProjectOutputReadEvents(t *testing.T) {
}
}
func TestProjectOutputRecallEvents(t *testing.T) {
func TestCrossPlatformCoverageProjectOutputRecallEvents(t *testing.T) {
for _, eventKey := range []string{EventRecallO2O, EventRecallGroup} {
t.Run(eventKey, func(t *testing.T) {
projected, err := ProjectOutput(transport.Event{
@@ -243,9 +243,9 @@ func TestProjectOutputRecallEvents(t *testing.T) {
SubscribeID: "recall-sub",
MessageID: "recall-message",
ConversationID: "recall-conversation",
Recaller: "郑御白",
Recaller: "测试用户乙",
RecallerOpenDingTalkID: "recaller-open-id",
Sender: "郑御白",
Sender: "测试用户乙",
SenderOpenDingTalkID: "sender-open-id",
RecallTime: "2026-07-14 13:56:32",
EventTime: 1784008592766,
@@ -258,7 +258,7 @@ func TestProjectOutputRecallEvents(t *testing.T) {
}
}
func TestProjectOutputReactionEvents(t *testing.T) {
func TestCrossPlatformCoverageProjectOutputReactionEvents(t *testing.T) {
for _, eventKey := range []string{EventReactionO2O, EventReactionGroup} {
t.Run(eventKey, func(t *testing.T) {
projected, err := ProjectOutput(transport.Event{
@@ -275,13 +275,13 @@ func TestProjectOutputReactionEvents(t *testing.T) {
SubscribeID: "reaction-sub",
MessageID: "reaction-message",
ConversationID: "reaction-conversation",
Operator: "郑御白",
Operator: "测试用户乙",
OperatorOpenDingTalkID: "operator-open-id",
ReactionName: "微笑",
ReactionText: "微笑",
OperationType: "add",
OperationTime: "2026-07-14 13:57:59",
Sender: "昊淼",
Sender: "测试用户甲",
SenderOpenDingTalkID: "sender-open-id",
EventTime: 1784008679217,
}
@@ -293,7 +293,7 @@ func TestProjectOutputReactionEvents(t *testing.T) {
}
}
func TestProjectOutputReactionRejectsLegacyOperatorOpenIDSpellings(t *testing.T) {
func TestCrossPlatformCoverageProjectOutputReactionRejectsLegacyOperatorOpenIDSpellings(t *testing.T) {
for _, legacyField := range []string{"operOpenDingtlkId", "operOpenDingTalkId"} {
t.Run(legacyField, func(t *testing.T) {
data := strings.Replace(
@@ -327,7 +327,53 @@ func assertNoInternalActionFields(t *testing.T, projected any) {
}
}
func TestProjectOutputMalformedDataReturnsRawEnvelope(t *testing.T) {
func TestCrossPlatformCoverageProjectOutputRejectsEmptyPayloads(t *testing.T) {
eventKeys := []string{
EventMention,
EventSingleChat,
EventInChat,
EventFromUser,
EventReadO2O,
EventReadGroup,
EventRecallO2O,
EventRecallGroup,
EventReactionO2O,
EventReactionGroup,
}
payloads := []struct {
name string
json string
}{
{name: "missing", json: ""},
{name: "null", json: `,"payload":null`},
{name: "empty object", json: `,"payload":{}`},
{name: "missing body", json: `,"payload":{"event_time":1}`},
{name: "null body", json: `,"payload":{"body":null}`},
{name: "empty body", json: `,"payload":{"body":{}}`},
}
for _, eventKey := range eventKeys {
for _, payload := range payloads {
t.Run(eventKey+"/"+payload.name, func(t *testing.T) {
ev := transport.Event{
EventID: "outer-event",
EventType: eventKey,
Data: fmt.Sprintf(`{"eventKey":%q%s}`, eventKey, payload.json),
}
projected, err := ProjectOutput(ev)
if err == nil {
t.Fatal("ProjectOutput() error = nil, want payload validation error")
}
got, ok := projected.(transport.Event)
if !ok || !reflect.DeepEqual(got, ev) {
t.Fatalf("ProjectOutput() fallback = %#v, want %#v", projected, ev)
}
})
}
}
}
func TestCrossPlatformCoverageProjectOutputMalformedDataReturnsRawEnvelope(t *testing.T) {
ev := transport.Event{EventID: "outer-event", Data: "not-json"}
projected, err := ProjectOutput(ev)
if err == nil {
+1 -1
View File
@@ -495,7 +495,7 @@ func TestBuildFilterQueryAndJSON(t *testing.T) {
func TestIdempotencyKeyUsesLocalIdentityKey(t *testing.T) {
left := Identity{LocalSubject: "refresh:left", ClientID: "client-1", SourceID: "open"}
right := Identity{LocalSubject: "refresh:right", ClientID: "client-1", SourceID: "open"}
ruleParam := map[string]any{"targetUid": "507971", "targetUidType": "staffId"}
ruleParam := map[string]any{"targetUid": "test-user-001", "targetUidType": "staffId"}
leftKey := IdempotencyKey(left, EventSingleChat, "singleChat", ruleParam, "")
rightKey := IdempotencyKey(right, EventSingleChat, "singleChat", ruleParam, "")
if leftKey == rightKey {
+10 -10
View File
@@ -43,16 +43,16 @@
| 用户说 | 下一步 |
|---|---|
| "监听有人 @ 我的消息" | `event consume`,事件码 `user_im_message_receive_at`,参数 `-f ndjson` |
| "监听我和 userId 507971 的单聊消息" | `event consume`,事件码 `user_im_message_receive_o2o`,参数 `--user 507971 -f ndjson` |
| "监听我和 userId test-user-001 的单聊消息" | `event consume`,事件码 `user_im_message_receive_o2o`,参数 `--user test-user-001 -f ndjson` |
| "监听我和 openDingtalkId abc 的单聊消息" | `event consume`,事件码 `user_im_message_receive_o2o`,参数 `--open-dingtalk-id abc -f ndjson` |
| "监听 XX 群消息" | 先 `dws chat search --query "XX" --format json`,确认后 consume group |
| "监听 userId 507971 发给我的消息" | `event consume`,事件码 `user_im_message_receive_user`,参数 `--user 507971 -f ndjson` |
| "监听 userId test-user-001 发给我的消息" | `event consume`,事件码 `user_im_message_receive_user`,参数 `--user test-user-001 -f ndjson` |
| "监听 openDingtalkId abc 发给我的消息" | `event consume`,事件码 `user_im_message_receive_user`,参数 `--open-dingtalk-id abc -f ndjson` |
| "监听我发给 userId 507971 的消息是否已读" | `event consume`,事件码 `user_im_message_read_o2o`,参数 `--user 507971 -f ndjson` |
| "监听我发给 userId test-user-001 的消息是否已读" | `event consume`,事件码 `user_im_message_read_o2o`,参数 `--user test-user-001 -f ndjson` |
| "监听 XX 群消息已读" | 先解析群 ID,再 consume `user_im_message_read_group --group <id>` |
| "监听我和 userId 507971 的消息撤回" | `event consume`,事件码 `user_im_message_recall_o2o`,参数 `--user 507971 -f ndjson` |
| "监听我和 userId test-user-001 的消息撤回" | `event consume`,事件码 `user_im_message_recall_o2o`,参数 `--user test-user-001 -f ndjson` |
| "监听 XX 群消息撤回" | 先解析群 ID,再 consume `user_im_message_recall_group --group <id>` |
| "监听我和 userId 507971 的消息贴表情" | `event consume`,事件码 `user_im_message_reaction_o2o`,参数 `--user 507971 -f ndjson` |
| "监听我和 userId test-user-001 的消息贴表情" | `event consume`,事件码 `user_im_message_reaction_o2o`,参数 `--user test-user-001 -f ndjson` |
| "监听 XX 群消息表情回应" | 先解析群 ID,再 consume `user_im_message_reaction_group --group <id>` |
| "监听并自动回复某人的单聊消息" | 先解析对端 userId,再启动 o2o consume;不要写轮询脚本 |
| "查看个人消息事件 schema" | `dws event schema <event_key>` |
@@ -89,16 +89,16 @@ dws event schema user_im_message_reaction_group
```bash
dws event consume user_im_message_receive_at -f ndjson
dws event consume user_im_message_receive_o2o --user 507971 -f ndjson
dws event consume user_im_message_receive_o2o --user test-user-001 -f ndjson
dws event consume user_im_message_receive_o2o --open-dingtalk-id abc -f ndjson
dws event consume user_im_message_receive_group --group <openConversationId> -f ndjson
dws event consume user_im_message_receive_user --user 507971 -f ndjson
dws event consume user_im_message_receive_user --user test-user-001 -f ndjson
dws event consume user_im_message_receive_user --open-dingtalk-id abc -f ndjson
dws event consume user_im_message_read_o2o --user 507971 -f ndjson
dws event consume user_im_message_read_o2o --user test-user-001 -f ndjson
dws event consume user_im_message_read_group --group <openConversationId> -f ndjson
dws event consume user_im_message_recall_o2o --user 507971 -f ndjson
dws event consume user_im_message_recall_o2o --user test-user-001 -f ndjson
dws event consume user_im_message_recall_group --group <openConversationId> -f ndjson
dws event consume user_im_message_reaction_o2o --user 507971 -f ndjson
dws event consume user_im_message_reaction_o2o --user test-user-001 -f ndjson
dws event consume user_im_message_reaction_group --group <openConversationId> -f ndjson
```
+5 -5
View File
@@ -92,7 +92,7 @@ dws event consume user_im_message_receive_at -f ndjson
# 当前用户与指定用户的单聊消息
dws event consume user_im_message_receive_o2o \
--user 507971 \
--user test-user-001 \
-f ndjson
# 使用 openDingtalkId 监听外部联系人、机器人或跨组织身份的单聊消息
@@ -107,7 +107,7 @@ dws event consume user_im_message_receive_group \
# 指定发送人的消息(单聊和群聊)
dws event consume user_im_message_receive_user \
--user 507971 \
--user test-user-001 \
-f ndjson
# 使用 openDingtalkId 监听指定发送人的消息
@@ -117,7 +117,7 @@ dws event consume user_im_message_receive_user \
# 指定单聊消息已读
dws event consume user_im_message_read_o2o \
--user 507971 \
--user test-user-001 \
-f ndjson
# 指定群聊消息撤回
@@ -127,7 +127,7 @@ dws event consume user_im_message_recall_group \
# 指定单聊消息收到表情回应
dws event consume user_im_message_reaction_o2o \
--user 507971 \
--user test-user-001 \
-f ndjson
# 有界自测
@@ -137,7 +137,7 @@ dws event consume user_im_message_receive_at \
# 抓一条样本
dws event consume user_im_message_receive_o2o \
--user 507971 \
--user test-user-001 \
--max-events 1 \
-f json
```
@@ -66,7 +66,7 @@ dws event consume user_im_message_receive_at -f ndjson
# 指定单聊消息
dws event consume user_im_message_receive_o2o \
--user 507971 \
--user test-user-001 \
-f ndjson
# 通过 openDingtalkId 指定单聊对端
@@ -81,7 +81,7 @@ dws event consume user_im_message_receive_group \
# 指定发送人的消息(单聊和群聊)
dws event consume user_im_message_receive_user \
--user 507971 \
--user test-user-001 \
-f ndjson
# 通过 openDingtalkId 指定发送人
@@ -91,7 +91,7 @@ dws event consume user_im_message_receive_user \
# 指定单聊已读事件
dws event consume user_im_message_read_o2o \
--user 507971 \
--user test-user-001 \
-f ndjson
# 指定群聊已读事件
@@ -101,7 +101,7 @@ dws event consume user_im_message_read_group \
# 指定单聊撤回事件
dws event consume user_im_message_recall_o2o \
--user 507971 \
--user test-user-001 \
-f ndjson
# 指定群聊撤回事件
@@ -111,7 +111,7 @@ dws event consume user_im_message_recall_group \
# 指定单聊表情回应事件
dws event consume user_im_message_reaction_o2o \
--user 507971 \
--user test-user-001 \
-f ndjson
# 指定群聊表情回应事件
+18 -18
View File
@@ -1005,9 +1005,9 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然
#### `dws event consume user_im_message_receive_o2o`
**event_event_consume_o2o_001**
- Prompt: 监听我和 userId 507971 的单聊消息
- Expected: `dws event consume user_im_message_receive_o2o --user 507971 -f ndjson`
- Flags: `--user` = `507971`
- Prompt: 监听我和 userId test-user-001 的单聊消息
- Expected: `dws event consume user_im_message_receive_o2o --user test-user-001 -f ndjson`
- Flags: `--user` = `test-user-001`
**event_event_consume_o2o_open_id_001**
- Prompt: 监听我和 openDingtalkId abc 的单聊消息
@@ -1019,9 +1019,9 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然
- Expected: `dws event consume user_im_message_receive_o2o -f ndjson`
**event_event_consume_o2o_auto_reply_001**
- Prompt: 监听我和 userId 507971 的单聊消息,并对方发什么自动回复什么
- Expected: `dws event consume user_im_message_receive_o2o --user 507971 -f ndjson`
- Flags: `--user` = `507971`
- Prompt: 监听我和 userId test-user-001 的单聊消息,并对方发什么自动回复什么
- Expected: `dws event consume user_im_message_receive_o2o --user test-user-001 -f ndjson`
- Flags: `--user` = `test-user-001`
- Contract: 等待 stderr 的 `[event] ready`;从每行 NDJSON 顶层读取 `content` 和 `sender_open_dingtalk_id`,持续读取 stdout,不使用轮询或 output-dir watcher
#### `dws event consume user_im_message_receive_group`
@@ -1035,9 +1035,9 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然
#### `dws event consume user_im_message_receive_user`
**event_event_consume_user_001**
- Prompt: 监听 userId 507971 发给我的消息,包括单聊和群聊
- Expected: `dws event consume user_im_message_receive_user --user 507971 -f ndjson`
- Flags: `--user` = `507971`
- Prompt: 监听 userId test-user-001 发给我的消息,包括单聊和群聊
- Expected: `dws event consume user_im_message_receive_user --user test-user-001 -f ndjson`
- Flags: `--user` = `test-user-001`
**event_event_consume_user_open_id_001**
- Prompt: 监听 openDingtalkId abc 发给我的消息,包括单聊和群聊
@@ -1047,9 +1047,9 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然
#### `dws event consume user_im_message_read_o2o`
**event_event_consume_read_o2o_001**
- Prompt: 监听我发给 userId 507971 的单聊消息是否已读
- Expected: `dws event consume user_im_message_read_o2o --user 507971 -f ndjson`
- Flags: `--user` = `507971`
- Prompt: 监听我发给 userId test-user-001 的单聊消息是否已读
- Expected: `dws event consume user_im_message_read_o2o --user test-user-001 -f ndjson`
- Flags: `--user` = `test-user-001`
- Contract: 从每行 NDJSON 顶层读取 `message_id`、`reader`、`reader_open_dingtalk_id`、`read_time`
#### `dws event consume user_im_message_read_group`
@@ -1063,9 +1063,9 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然
#### `dws event consume user_im_message_recall_o2o`
**event_event_consume_recall_o2o_001**
- Prompt: 监听我和 userId 507971 的单聊消息撤回事件
- Expected: `dws event consume user_im_message_recall_o2o --user 507971 -f ndjson`
- Flags: `--user` = `507971`
- Prompt: 监听我和 userId test-user-001 的单聊消息撤回事件
- Expected: `dws event consume user_im_message_recall_o2o --user test-user-001 -f ndjson`
- Flags: `--user` = `test-user-001`
- Contract: 从每行 NDJSON 顶层读取 `message_id`、`recaller`、`recaller_open_dingtalk_id`、`recall_time`
#### `dws event consume user_im_message_recall_group`
@@ -1079,9 +1079,9 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然
#### `dws event consume user_im_message_reaction_o2o`
**event_event_consume_reaction_o2o_001**
- Prompt: 监听我和 userId 507971 的单聊消息贴表情事件
- Expected: `dws event consume user_im_message_reaction_o2o --user 507971 -f ndjson`
- Flags: `--user` = `507971`
- Prompt: 监听我和 userId test-user-001 的单聊消息贴表情事件
- Expected: `dws event consume user_im_message_reaction_o2o --user test-user-001 -f ndjson`
- Flags: `--user` = `test-user-001`
- Contract: 从每行 NDJSON 顶层读取 `operator`、`operator_open_dingtalk_id`、`reaction_name`、`operation_type`
**event_event_consume_reaction_o2o_open_id_001**
+2 -2
View File
@@ -1424,8 +1424,8 @@
**event_event_consume_o2o_001** ✅ PASS
- Prompt: 监听我和 userId 507971 的单聊消息
- Expected: `dws event consume user_im_message_receive_o2o --user 507971 -f ndjson`
- Prompt: 监听我和 userId test-user-001 的单聊消息
- Expected: `dws event consume user_im_message_receive_o2o --user test-user-001 -f ndjson`
- Skill Reference: references/products/event.md
- Command path: PASS (event consume user_im_message_receive_o2o)
- Flags: PASS (1 flags validated)