From f59be6c19a14d2fa9e9fc20caaa4a910b8341feb Mon Sep 17 00:00:00 2001 From: wxianfeng Date: Fri, 17 Jul 2026 11:57:27 +0800 Subject: [PATCH] fix(event): address PR review gates --- internal/app/event_personal_debug_test.go | 4 +- internal/app/event_personal_output_test.go | 15 +-- .../schema_local_registry_contract_test.go | 11 +-- internal/cli/schema_agent_metadata/index.json | 2 +- internal/cli/schema_agent_metadata_audit.json | 2 +- internal/cli/schema_catalog.json | 56 +++-------- internal/cli/schema_constraints_runtime.go | 5 +- internal/cli/schema_parameters_runtime.go | 7 -- internal/event/consume/formatter_test.go | 8 +- internal/event/consume/pipeline_test.go | 2 +- internal/event/personal/client_test.go | 8 +- internal/event/personal/output.go | 43 +++++++-- internal/event/personal/output_test.go | 96 ++++++++++++++----- internal/event/personal/registry_test.go | 2 +- skills/mono/references/products/event.md | 20 ++-- skills/multi/dingtalk-event/SKILL.md | 10 +- .../dingtalk-event/references/event-im.md | 10 +- test/skill_tests.md | 36 +++---- test/skill_tests_results.md | 4 +- 19 files changed, 181 insertions(+), 160 deletions(-) diff --git a/internal/app/event_personal_debug_test.go b/internal/app/event_personal_debug_test.go index abbcb772..8ccf7b12 100644 --- a/internal/app/event_personal_debug_test.go +++ b/internal/app/event_personal_debug_test.go @@ -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", "报警"}, diff --git a/internal/app/event_personal_output_test.go b/internal/app/event_personal_output_test.go index f654ea63..f1d7af51 100644 --- a/internal/app/event_personal_output_test.go +++ b/internal/app/event_personal_output_test.go @@ -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) { diff --git a/internal/app/schema_local_registry_contract_test.go b/internal/app/schema_local_registry_contract_test.go index a4599d4a..b648bd22 100644 --- a/internal/app/schema_local_registry_contract_test.go +++ b/internal/app/schema_local_registry_contract_test.go @@ -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) diff --git a/internal/cli/schema_agent_metadata/index.json b/internal/cli/schema_agent_metadata/index.json index f6ddafef..11b7d0d3 100644 --- a/internal/cli/schema_agent_metadata/index.json +++ b/internal/cli/schema_agent_metadata/index.json @@ -1,6 +1,6 @@ { "version": 1, - "source_hash": "sha256:2f24ccd75c2c02fe8c05f3f0b8ca774313232559bec7b8ab3d94f856b5712e07", + "source_hash": "sha256:a65016dce21d32c609d5f422645c02bb28b00e719e459a548829fb513d6b3e5f", "surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed", "coverage": { "surface_products": 22, diff --git a/internal/cli/schema_agent_metadata_audit.json b/internal/cli/schema_agent_metadata_audit.json index e2463550..da78579e 100644 --- a/internal/cli/schema_agent_metadata_audit.json +++ b/internal/cli/schema_agent_metadata_audit.json @@ -1,6 +1,6 @@ { "version": 1, - "source_hash": "sha256:2f24ccd75c2c02fe8c05f3f0b8ca774313232559bec7b8ab3d94f856b5712e07", + "source_hash": "sha256:a65016dce21d32c609d5f422645c02bb28b00e719e459a548829fb513d6b3e5f", "surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed", "source_files": 150, "hint_files": 46, diff --git a/internal/cli/schema_catalog.json b/internal/cli/schema_catalog.json index fd700a96..31db012d 100644 --- a/internal/cli/schema_catalog.json +++ b/internal/cli/schema_catalog.json @@ -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" } }, diff --git a/internal/cli/schema_constraints_runtime.go b/internal/cli/schema_constraints_runtime.go index ed99df19..2351f60f 100644 --- a/internal/cli/schema_constraints_runtime.go +++ b/internal/cli/schema_constraints_runtime.go @@ -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") diff --git a/internal/cli/schema_parameters_runtime.go b/internal/cli/schema_parameters_runtime.go index 71c7bbd3..8fe8aeac 100644 --- a/internal/cli/schema_parameters_runtime.go +++ b/internal/cli/schema_parameters_runtime.go @@ -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"}, }) diff --git a/internal/event/consume/formatter_test.go b/internal/event/consume/formatter_test.go index 236417ed..e6049e1e 100644 --- a/internal/event/consume/formatter_test.go +++ b/internal/event/consume/formatter_test.go @@ -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 diff --git a/internal/event/consume/pipeline_test.go b/internal/event/consume/pipeline_test.go index aabb7c8e..a4b7257d 100644 --- a/internal/event/consume/pipeline_test.go +++ b/internal/event/consume/pipeline_test.go @@ -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", diff --git a/internal/event/personal/client_test.go b/internal/event/personal/client_test.go index bc1d619d..d24eae81 100644 --- a/internal/event/personal/client_test.go +++ b/internal/event/personal/client_test.go @@ -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", } { diff --git a/internal/event/personal/output.go b/internal/event/personal/output.go index 5c4296e3..c2a344dc 100644 --- a/internal/event/personal/output.go +++ b/internal/event/personal/output.go @@ -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) } diff --git a/internal/event/personal/output_test.go b/internal/event/personal/output_test.go index 92846953..57433642 100644 --- a/internal/event/personal/output_test.go +++ b/internal/event/personal/output_test.go @@ -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 { diff --git a/internal/event/personal/registry_test.go b/internal/event/personal/registry_test.go index daa58ef0..a5beaa23 100644 --- a/internal/event/personal/registry_test.go +++ b/internal/event/personal/registry_test.go @@ -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 { diff --git a/skills/mono/references/products/event.md b/skills/mono/references/products/event.md index 1f2db4ac..e7653477 100644 --- a/skills/mono/references/products/event.md +++ b/skills/mono/references/products/event.md @@ -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 ` | -| "监听我和 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 ` | -| "监听我和 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 ` | | "监听并自动回复某人的单聊消息" | 先解析对端 userId,再启动 o2o consume;不要写轮询脚本 | | "查看个人消息事件 schema" | `dws event schema ` | @@ -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 -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 -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 -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 -f ndjson ``` diff --git a/skills/multi/dingtalk-event/SKILL.md b/skills/multi/dingtalk-event/SKILL.md index 6fa34430..6e5a5742 100644 --- a/skills/multi/dingtalk-event/SKILL.md +++ b/skills/multi/dingtalk-event/SKILL.md @@ -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 ``` diff --git a/skills/multi/dingtalk-event/references/event-im.md b/skills/multi/dingtalk-event/references/event-im.md index dacd9095..25000f68 100644 --- a/skills/multi/dingtalk-event/references/event-im.md +++ b/skills/multi/dingtalk-event/references/event-im.md @@ -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 # 指定群聊表情回应事件 diff --git a/test/skill_tests.md b/test/skill_tests.md index 2b39826b..62892c4d 100644 --- a/test/skill_tests.md +++ b/test/skill_tests.md @@ -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** diff --git a/test/skill_tests_results.md b/test/skill_tests_results.md index cd00a4f0..7bc7003e 100644 --- a/test/skill_tests_results.md +++ b/test/skill_tests_results.md @@ -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)