@juzi/wechaty 1.0.160 → 1.0.162

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/cjs/src/package-json.js +2 -2
  2. package/dist/cjs/src/user-modules/call.d.ts +24 -0
  3. package/dist/cjs/src/user-modules/call.d.ts.map +1 -1
  4. package/dist/cjs/src/user-modules/call.js +65 -0
  5. package/dist/cjs/src/user-modules/call.js.map +1 -1
  6. package/dist/cjs/src/user-modules/call.spec.js +171 -0
  7. package/dist/cjs/src/user-modules/call.spec.js.map +1 -1
  8. package/dist/cjs/src/wechaty-mixins/misc-mixin.d.ts +0 -6
  9. package/dist/cjs/src/wechaty-mixins/misc-mixin.d.ts.map +1 -1
  10. package/dist/cjs/src/wechaty-mixins/puppet-mixin.d.ts +4 -4
  11. package/dist/cjs/src/wechaty-mixins/puppet-mixin.d.ts.map +1 -1
  12. package/dist/cjs/src/wechaty-mixins/puppet-mixin.js +15 -41
  13. package/dist/cjs/src/wechaty-mixins/puppet-mixin.js.map +1 -1
  14. package/dist/esm/src/package-json.js +2 -2
  15. package/dist/esm/src/user-modules/call.d.ts +24 -0
  16. package/dist/esm/src/user-modules/call.d.ts.map +1 -1
  17. package/dist/esm/src/user-modules/call.js +65 -0
  18. package/dist/esm/src/user-modules/call.js.map +1 -1
  19. package/dist/esm/src/user-modules/call.spec.js +171 -0
  20. package/dist/esm/src/user-modules/call.spec.js.map +1 -1
  21. package/dist/esm/src/wechaty-mixins/misc-mixin.d.ts +0 -6
  22. package/dist/esm/src/wechaty-mixins/misc-mixin.d.ts.map +1 -1
  23. package/dist/esm/src/wechaty-mixins/puppet-mixin.d.ts +4 -4
  24. package/dist/esm/src/wechaty-mixins/puppet-mixin.d.ts.map +1 -1
  25. package/dist/esm/src/wechaty-mixins/puppet-mixin.js +15 -41
  26. package/dist/esm/src/wechaty-mixins/puppet-mixin.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/package-json.ts +2 -2
  29. package/src/user-modules/call.spec.ts +203 -0
  30. package/src/user-modules/call.ts +71 -0
  31. package/src/wechaty-mixins/puppet-mixin.ts +19 -36
  32. package/dist/cjs/src/wechaty-mixins/puppet-mixin-dirty-pool.spec.d.ts +0 -3
  33. package/dist/cjs/src/wechaty-mixins/puppet-mixin-dirty-pool.spec.d.ts.map +0 -1
  34. package/dist/cjs/src/wechaty-mixins/puppet-mixin-dirty-pool.spec.js +0 -121
  35. package/dist/cjs/src/wechaty-mixins/puppet-mixin-dirty-pool.spec.js.map +0 -1
  36. package/dist/esm/src/wechaty-mixins/puppet-mixin-dirty-pool.spec.d.ts +0 -3
  37. package/dist/esm/src/wechaty-mixins/puppet-mixin-dirty-pool.spec.d.ts.map +0 -1
  38. package/dist/esm/src/wechaty-mixins/puppet-mixin-dirty-pool.spec.js +0 -96
  39. package/dist/esm/src/wechaty-mixins/puppet-mixin-dirty-pool.spec.js.map +0 -1
  40. package/src/wechaty-mixins/puppet-mixin-dirty-pool.spec.ts +0 -144
@@ -931,3 +931,206 @@ test('dirty(Call) refreshes user-layer payload so getters see new value', async
931
931
  await wechaty.stop()
932
932
  sandbox.restore()
933
933
  })
934
+
935
+ // ---------------------------------------------------------------------------
936
+ // 12. end() — universal terminator dispatches by state
937
+ // ---------------------------------------------------------------------------
938
+
939
+ test('call.end() on outgoing calling invokes puppet.callCancel and ends', async t => {
940
+ const { puppet, wechaty } = buildWechaty()
941
+ await startAndLogin(puppet, wechaty)
942
+
943
+ puppet.callInvite = sandbox.stub().resolves('call-id-end-cancel')
944
+ stubCallPayload(puppet, (id: string) => ({
945
+ id,
946
+ starter : 'bot-self',
947
+ participants : [ 'bot-self', 'peer' ],
948
+ media : PUPPET.types.CallMediaType.Audio,
949
+ startTime : 1,
950
+ }))
951
+ const cancelStub = sandbox.stub().resolves(undefined)
952
+ const hangupStub = sandbox.stub().resolves(undefined)
953
+ puppet.callCancel = cancelStub
954
+ puppet.callHangup = hangupStub
955
+
956
+ const contact = (wechaty.Contact as typeof ContactImpl).load('peer')
957
+ const call = await contact.call()
958
+ t.equal(call.status(), 'calling', 'prerequisite: outgoing call starts as calling')
959
+
960
+ await call.end('demo')
961
+ t.same(cancelStub.firstCall.args, [ call.id ], 'callCancel should be invoked with callId')
962
+ t.equal(hangupStub.callCount, 0, 'callHangup should not be touched')
963
+ t.equal(call.status(), 'ended', 'status should be ended')
964
+ t.equal((wechaty as any).__callPool.has(call.id), false, 'pool should be evicted')
965
+
966
+ await call.end('again')
967
+ t.equal(cancelStub.callCount, 1, 'second end() should be a no-op')
968
+
969
+ await wechaty.stop()
970
+ sandbox.restore()
971
+ })
972
+
973
+ test('call.end() on connected call invokes puppet.callHangup with reason', async t => {
974
+ const { puppet, wechaty } = buildWechaty()
975
+ await startAndLogin(puppet, wechaty)
976
+
977
+ puppet.callInvite = sandbox.stub().resolves('call-id-end-hangup')
978
+ stubCallPayload(puppet, (id: string) => ({
979
+ id,
980
+ starter : 'bot-self',
981
+ participants : [ 'bot-self', 'peer' ],
982
+ media : PUPPET.types.CallMediaType.Audio,
983
+ startTime : 1,
984
+ }))
985
+ const cancelStub = sandbox.stub().resolves(undefined)
986
+ const hangupStub = sandbox.stub().resolves(undefined)
987
+ puppet.callCancel = cancelStub
988
+ puppet.callHangup = hangupStub
989
+
990
+ const contact = (wechaty.Contact as typeof ContactImpl).load('peer')
991
+ const call = await contact.call()
992
+
993
+ ;(puppet as any).emit('call', {
994
+ callId : call.id,
995
+ signal : PUPPET.types.CallSignal.Accept,
996
+ contactId : 'peer',
997
+ timestamp : 2,
998
+ } as PUPPET.payloads.EventCall)
999
+ await flush()
1000
+ t.equal(call.status(), 'connected', 'prerequisite: call should be connected')
1001
+
1002
+ await call.end('bye')
1003
+ t.same(hangupStub.firstCall.args, [ call.id, 'bye' ], 'callHangup args should be [callId, reason]')
1004
+ t.equal(cancelStub.callCount, 0, 'callCancel should not be touched')
1005
+ t.equal(call.status(), 'ended', 'status should be ended')
1006
+
1007
+ await wechaty.stop()
1008
+ sandbox.restore()
1009
+ })
1010
+
1011
+ test('call.end() on incoming ringing invokes puppet.callReject with reason', async t => {
1012
+ const { puppet, wechaty } = buildWechaty()
1013
+ await startAndLogin(puppet, wechaty)
1014
+
1015
+ const CALL_ID = 'call-id-end-reject'
1016
+ stubCallPayload(puppet, (id: string) => ({
1017
+ id,
1018
+ starter : 'peer',
1019
+ participants : [ 'peer', 'bot-self' ],
1020
+ media : PUPPET.types.CallMediaType.Audio,
1021
+ startTime : 1,
1022
+ }))
1023
+ const rejectStub = sandbox.stub().resolves(undefined)
1024
+ puppet.callReject = rejectStub
1025
+
1026
+ let incoming: CallInterface | undefined
1027
+ ;(wechaty as any).on('call', (c: CallInterface) => { incoming = c })
1028
+ ;(puppet as any).emit('call', {
1029
+ callId : CALL_ID,
1030
+ signal : PUPPET.types.CallSignal.Invite,
1031
+ contactId : 'peer',
1032
+ timestamp : 1,
1033
+ } as PUPPET.payloads.EventCall)
1034
+ await flush()
1035
+ t.ok(incoming, 'prerequisite: incoming call should surface via bot.on(call)')
1036
+ t.equal(incoming!.status(), 'ringing', 'prerequisite: incoming call should be ringing')
1037
+
1038
+ await incoming!.end('busy')
1039
+ t.same(rejectStub.firstCall.args, [ CALL_ID, 'busy' ], 'callReject args should be [callId, reason]')
1040
+ t.equal(incoming!.status(), 'ended', 'status should be ended')
1041
+
1042
+ await wechaty.stop()
1043
+ sandbox.restore()
1044
+ })
1045
+
1046
+ test('call.end() falls back to callHangup when callCancel is UNIMPLEMENTED', async t => {
1047
+ const { puppet, wechaty } = buildWechaty()
1048
+ await startAndLogin(puppet, wechaty)
1049
+
1050
+ puppet.callInvite = sandbox.stub().resolves('call-id-end-fallback')
1051
+ stubCallPayload(puppet, (id: string) => ({
1052
+ id,
1053
+ starter : 'bot-self',
1054
+ participants : [ 'bot-self', 'peer' ],
1055
+ media : PUPPET.types.CallMediaType.Audio,
1056
+ startTime : 1,
1057
+ }))
1058
+ const cancelStub = sandbox.stub().rejects(new Error('12 UNIMPLEMENTED: CallCancel is not supported'))
1059
+ const hangupStub = sandbox.stub().resolves(undefined)
1060
+ puppet.callCancel = cancelStub
1061
+ puppet.callHangup = hangupStub
1062
+
1063
+ const contact = (wechaty.Contact as typeof ContactImpl).load('peer')
1064
+ const call = await contact.call()
1065
+
1066
+ await call.end('timeout')
1067
+ t.equal(cancelStub.callCount, 1, 'callCancel should be attempted first')
1068
+ t.same(hangupStub.firstCall.args, [ call.id, 'timeout' ], 'should retry via callHangup')
1069
+ t.equal(call.status(), 'ended', 'status should be ended')
1070
+
1071
+ await wechaty.stop()
1072
+ sandbox.restore()
1073
+ })
1074
+
1075
+ test('call.end() retries via callHangup when peer accepts during in-flight cancel', async t => {
1076
+ const { puppet, wechaty } = buildWechaty()
1077
+ await startAndLogin(puppet, wechaty)
1078
+
1079
+ puppet.callInvite = sandbox.stub().resolves('call-id-end-race')
1080
+ stubCallPayload(puppet, (id: string) => ({
1081
+ id,
1082
+ starter : 'bot-self',
1083
+ participants : [ 'bot-self', 'peer' ],
1084
+ media : PUPPET.types.CallMediaType.Audio,
1085
+ startTime : 1,
1086
+ }))
1087
+ const hangupStub = sandbox.stub().resolves(undefined)
1088
+ puppet.callHangup = hangupStub
1089
+ puppet.callCancel = sandbox.stub().callsFake(async (callId: string) => {
1090
+ ;(puppet as any).emit('call', {
1091
+ callId,
1092
+ signal : PUPPET.types.CallSignal.Accept,
1093
+ contactId : 'peer',
1094
+ timestamp : 2,
1095
+ } as PUPPET.payloads.EventCall)
1096
+ await flush()
1097
+ throw new Error('call already answered')
1098
+ })
1099
+
1100
+ const contact = (wechaty.Contact as typeof ContactImpl).load('peer')
1101
+ const call = await contact.call()
1102
+
1103
+ await call.end('too late to cancel')
1104
+ t.same(hangupStub.firstCall.args, [ call.id, 'too late to cancel' ], 'should retry via callHangup after accept race')
1105
+ t.equal(call.status(), 'ended', 'status should be ended')
1106
+
1107
+ await wechaty.stop()
1108
+ sandbox.restore()
1109
+ })
1110
+
1111
+ test('call.end() rethrows non-recoverable pre-connect failures but still finalizes', async t => {
1112
+ const { puppet, wechaty } = buildWechaty()
1113
+ await startAndLogin(puppet, wechaty)
1114
+
1115
+ puppet.callInvite = sandbox.stub().resolves('call-id-end-fatal')
1116
+ stubCallPayload(puppet, (id: string) => ({
1117
+ id,
1118
+ starter : 'bot-self',
1119
+ participants : [ 'bot-self', 'peer' ],
1120
+ media : PUPPET.types.CallMediaType.Audio,
1121
+ startTime : 1,
1122
+ }))
1123
+ const hangupStub = sandbox.stub().resolves(undefined)
1124
+ puppet.callCancel = sandbox.stub().rejects(new Error('network error'))
1125
+ puppet.callHangup = hangupStub
1126
+
1127
+ const contact = (wechaty.Contact as typeof ContactImpl).load('peer')
1128
+ const call = await contact.call()
1129
+
1130
+ await t.rejects(call.end(), /network error/, 'end() should re-throw transport failures')
1131
+ t.equal(hangupStub.callCount, 0, 'no hangup retry for transport failures')
1132
+ t.equal(call.status(), 'ended', 'status should still finalize')
1133
+
1134
+ await wechaty.stop()
1135
+ sandbox.restore()
1136
+ })
@@ -29,6 +29,18 @@ interface CallConstructorOptions {
29
29
 
30
30
  const CallMixinBase = wechatifyMixin(CallEventEmitter)
31
31
 
32
+ /**
33
+ * Whether a puppet rejection means "this verb is not supported by the
34
+ * implementation" (gRPC UNIMPLEMENTED, or an explicit not-supported error),
35
+ * as opposed to a transport/business failure. Message-based on purpose:
36
+ * puppet errors cross the service boundary as GError and no structured
37
+ * status code survives the trip.
38
+ */
39
+ function isVerbUnsupportedError (e: unknown): boolean {
40
+ const message = e instanceof Error ? e.message : String(e)
41
+ return /UNIMPLEMENTED|not\s+(?:supported|implemented)|unsupported/i.test(message)
42
+ }
43
+
32
44
  /**
33
45
  * Call – a live, stateful call-control session abstraction.
34
46
  *
@@ -224,6 +236,65 @@ class CallMixin extends CallMixinBase {
224
236
  }
225
237
  }
226
238
 
239
+ /**
240
+ * Universal terminator (the "red button"): end the call from whatever
241
+ * state it is in, dispatching to the state-appropriate verb —
242
+ *
243
+ * connected → puppet.callHangup(reason)
244
+ * incoming + ringing → puppet.callReject(reason)
245
+ * outgoing + calling / ringing → puppet.callCancel()
246
+ * ended → no-op (idempotent)
247
+ *
248
+ * Unlike the precise verbs (cancel/reject/hangup), which throw on any state
249
+ * mismatch, end() absorbs the two failure modes inherent to check-then-act
250
+ * termination, retrying ONCE via callHangup when:
251
+ *
252
+ * - the peer accepts while our pre-connect verb is in flight (the verb
253
+ * comes back rejected but the session is now connected, so hangup is
254
+ * the correct verb by the time we retry), or
255
+ * - the puppet does not implement the chosen verb (protocols whose
256
+ * transport folds cancel/reject into a single hangup action).
257
+ *
258
+ * Local state always finalizes, matching cancel/reject/hangup semantics:
259
+ * whichever side actually terminated the session, the protocol-side record
260
+ * is the arbiter and its verdict arrives via the terminal signal path.
261
+ */
262
+ async end (reason?: string): Promise<void> {
263
+ if (this.__status === 'ended') {
264
+ this.log.verbose('Call', 'end() no-op: already ended, callId=%s', this.id)
265
+ return
266
+ }
267
+
268
+ try {
269
+ if (this.__status === 'connected') {
270
+ await this.wechaty.puppet.callHangup(this.id, reason)
271
+ return
272
+ }
273
+
274
+ try {
275
+ if (this.__direction === 'incoming') {
276
+ await this.wechaty.puppet.callReject(this.id, reason)
277
+ } else {
278
+ await this.wechaty.puppet.callCancel(this.id)
279
+ }
280
+ } catch (e) {
281
+ const acceptedMeanwhile = this.status() === 'connected'
282
+ if (!acceptedMeanwhile && !isVerbUnsupportedError(e)) {
283
+ throw e
284
+ }
285
+ this.log.verbose(
286
+ 'Call',
287
+ 'end() pre-connect verb failed (%s), retrying via callHangup, callId=%s',
288
+ acceptedMeanwhile ? 'accepted meanwhile' : 'verb unsupported',
289
+ this.id,
290
+ )
291
+ await this.wechaty.puppet.callHangup(this.id, reason)
292
+ }
293
+ } finally {
294
+ this.__finalize()
295
+ }
296
+ }
297
+
227
298
  /**
228
299
  * Invite additional contacts to a live call (group-call growth).
229
300
  */
@@ -18,8 +18,12 @@ import type {
18
18
  CallImpl,
19
19
  ContactImpl,
20
20
  ContactInterface,
21
+ MessageImpl,
22
+ RoomImpl,
21
23
  TagGroupInterface,
22
24
  TagInterface,
25
+ WxxdOrderImpl,
26
+ WxxdProductImpl,
23
27
  } from '../user-modules/mod.js'
24
28
 
25
29
  import type {
@@ -724,46 +728,26 @@ const puppetMixin = <MixinBase extends WechatifyUserModuleMixin & GErrorMixin &
724
728
  case 'dirty':
725
729
  /**
726
730
  * https://github.com/wechaty/wechaty-puppet-service/issues/43
727
- *
728
- * `dirty` semantics: the puppet-service told us the cached payload
729
- * for this id is stale. It is NOT a request to load the payload.
730
- *
731
- * The old handler used `this.Xxx.find({ id })` which, on pool miss,
732
- * constructs a fresh user-layer instance AND calls `.ready()` — a
733
- * puppet-side gRPC round-trip. For a `dirty` event about an id
734
- * that no business code has ever touched, this turned a passive
735
- * "invalidate if cached" signal into an active fetch, amplifying
736
- * dirty-event bursts (e.g. during message storms) into extra
737
- * puppet-service QPS.
738
- *
739
- * Fix: for pooled user-modules, look the instance up in the pool
740
- * directly and only call `.ready(true)` when it is already there.
741
- * The Call branch (below) already follows this shape via
742
- * `__callPool.get(...)`. Message has no pool, so its branch is a
743
- * no-op break (any caller wanting a Message must load it itself).
744
731
  */
745
732
  puppet.on('dirty', async ({ payloadType, payloadId }) => {
746
733
  try {
747
734
  switch (payloadType) {
748
735
  case PUPPET.types.Payload.Contact: {
749
- const cached = this.Contact.pool.get(payloadId)
750
- if (cached) await cached.ready(true)
736
+ const contact = await this.Contact.find({ id: payloadId }) as unknown as undefined | ContactImpl
737
+ await contact?.ready(true)
751
738
  break
752
739
  }
753
740
  case PUPPET.types.Payload.Room: {
754
- const cached = this.Room.pool.get(payloadId)
755
- if (cached) await cached.ready(true)
741
+ const room = await this.Room.find({ id: payloadId }) as unknown as undefined | RoomImpl
742
+ await room?.ready(true)
756
743
  break
757
744
  }
758
745
  case PUPPET.types.Payload.RoomMember: {
759
746
  if (payloadId.includes(PUPPET.STRING_SPLITTER)) {
760
747
  break
761
748
  }
762
- // A bare (non-composite) RoomMember dirty id addresses the
763
- // whole room, mirroring the Room branch — but never load
764
- // a new Room here.
765
- const cached = this.Room.pool.get(payloadId)
766
- if (cached) await cached.ready()
749
+ const room = await this.Room.find({ id: payloadId }) as unknown as undefined | RoomImpl
750
+ await room?.ready()
767
751
  break
768
752
  }
769
753
 
@@ -773,13 +757,12 @@ const puppetMixin = <MixinBase extends WechatifyUserModuleMixin & GErrorMixin &
773
757
  case PUPPET.types.Payload.Friendship:
774
758
  // Friendship has no payload
775
759
  break
776
- case PUPPET.types.Payload.Message:
777
- // Message has no user-layer pool (see message.ts: `load`
778
- // constructs a fresh instance every call). Dirty for a
779
- // Message id is therefore a no-op at this layer — the
780
- // cache-mixin listener still invalidates the puppet-side
781
- // payload cache alongside this handler.
760
+ case PUPPET.types.Payload.Message: {
761
+ // Message does not need to dirty (?)
762
+ const message = await this.Message.find({ id: payloadId }) as unknown as undefined | MessageImpl
763
+ await message?.ready(true)
782
764
  break
765
+ }
783
766
  case PUPPET.types.Payload.Tag:
784
767
  break
785
768
  case PUPPET.types.Payload.TagGroup:
@@ -787,13 +770,13 @@ const puppetMixin = <MixinBase extends WechatifyUserModuleMixin & GErrorMixin &
787
770
  case PUPPET.types.Payload.Post:
788
771
  break
789
772
  case PUPPET.types.Payload.WxxdProduct: {
790
- const cached = this.WxxdProduct.pool.get(payloadId)
791
- if (cached) await cached.ready(true)
773
+ const product = await this.WxxdProduct.find({ id: payloadId }) as unknown as undefined | WxxdProductImpl
774
+ await product?.ready(true)
792
775
  break
793
776
  }
794
777
  case PUPPET.types.Payload.WxxdOrder: {
795
- const cached = this.WxxdOrder.pool.get(payloadId)
796
- if (cached) await cached.ready(true)
778
+ const order = await this.WxxdOrder.find({ id: payloadId }) as unknown as undefined | WxxdOrderImpl
779
+ await order?.ready(true)
797
780
  break
798
781
  }
799
782
  case PUPPET.types.Payload.Call: {
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env -S node --no-warnings --loader ts-node/esm
2
- export {};
3
- //# sourceMappingURL=puppet-mixin-dirty-pool.spec.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"puppet-mixin-dirty-pool.spec.d.ts","sourceRoot":"","sources":["../../../../src/wechaty-mixins/puppet-mixin-dirty-pool.spec.ts"],"names":[],"mappings":""}
@@ -1,121 +0,0 @@
1
- #!/usr/bin/env -S node --no-warnings --loader ts-node/esm
2
- "use strict";
3
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
- if (k2 === undefined) k2 = k;
5
- var desc = Object.getOwnPropertyDescriptor(m, k);
6
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
- desc = { enumerable: true, get: function() { return m[k]; } };
8
- }
9
- Object.defineProperty(o, k2, desc);
10
- }) : (function(o, m, k, k2) {
11
- if (k2 === undefined) k2 = k;
12
- o[k2] = m[k];
13
- }));
14
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
- Object.defineProperty(o, "default", { enumerable: true, value: v });
16
- }) : function(o, v) {
17
- o["default"] = v;
18
- });
19
- var __importStar = (this && this.__importStar) || function (mod) {
20
- if (mod && mod.__esModule) return mod;
21
- var result = {};
22
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
23
- __setModuleDefault(result, mod);
24
- return result;
25
- };
26
- Object.defineProperty(exports, "__esModule", { value: true });
27
- const tstest_1 = require("tstest");
28
- const PUPPET = __importStar(require("@juzi/wechaty-puppet"));
29
- const wechaty_puppet_mock_1 = require("@juzi/wechaty-puppet-mock");
30
- const wechaty_builder_js_1 = require("../wechaty-builder.js");
31
- /**
32
- * Regression: the `dirty` event handler for pooled user-modules
33
- * (Contact / Room / WxxdProduct / WxxdOrder) used to call
34
- * `this.Xxx.find({ id }).ready(true)`. `find({ id })` on a pool miss
35
- * constructs a fresh instance and calls `ready()` — a puppet-side gRPC
36
- * round-trip — turning a passive "invalidate if cached" signal into an
37
- * active fetch for ids no business code has touched.
38
- *
39
- * The fix looks the id up in the pool directly and only calls
40
- * `.ready(true)` when the instance is already pooled.
41
- */
42
- async function delay(ms) {
43
- await new Promise(resolve => setTimeout(resolve, ms));
44
- }
45
- (0, tstest_1.test)('dirty handler does not fetch payload for un-pooled ids, but still refreshes pooled ones', async (t) => {
46
- const puppet = new wechaty_puppet_mock_1.PuppetMock();
47
- const wechaty = wechaty_builder_js_1.WechatyBuilder.build({ puppet });
48
- const botContact = puppet.mocker.createContact({ name: 'test-bot' });
49
- await wechaty.start();
50
- await puppet.mocker.login(botContact);
51
- // --- Case 1: un-pooled Contact id must not trigger contactRawPayload ---
52
- let contactRawPayloadCalls = 0;
53
- const originalContactRawPayload = puppet.contactRawPayload.bind(puppet);
54
- puppet.contactRawPayload = async (id) => {
55
- contactRawPayloadCalls++;
56
- return originalContactRawPayload(id);
57
- };
58
- puppet.emit('dirty', {
59
- payloadType: PUPPET.types.Payload.Contact,
60
- payloadId: 'never-loaded-contact-id',
61
- });
62
- await delay(50);
63
- t.equal(contactRawPayloadCalls, 0, `dirty(Contact) for un-pooled id must not trigger contactRawPayload; got ${contactRawPayloadCalls} call(s)`);
64
- // --- Case 2: un-pooled Room id must not trigger roomRawPayload ---
65
- let roomRawPayloadCalls = 0;
66
- const originalRoomRawPayload = puppet.roomRawPayload.bind(puppet);
67
- puppet.roomRawPayload = async (id) => {
68
- roomRawPayloadCalls++;
69
- return originalRoomRawPayload(id);
70
- };
71
- puppet.emit('dirty', {
72
- payloadType: PUPPET.types.Payload.Room,
73
- payloadId: 'never-loaded-room-id',
74
- });
75
- await delay(50);
76
- t.equal(roomRawPayloadCalls, 0, `dirty(Room) for un-pooled id must not trigger roomRawPayload; got ${roomRawPayloadCalls} call(s)`);
77
- // --- Case 3: Message dirty must not trigger messageRawPayload (Message has no user-layer pool) ---
78
- let messageRawPayloadCalls = 0;
79
- const originalMessageRawPayload = puppet.messageRawPayload.bind(puppet);
80
- puppet.messageRawPayload = async (id) => {
81
- messageRawPayloadCalls++;
82
- return originalMessageRawPayload(id);
83
- };
84
- puppet.emit('dirty', {
85
- payloadType: PUPPET.types.Payload.Message,
86
- payloadId: 'some-message-id',
87
- });
88
- await delay(50);
89
- t.equal(messageRawPayloadCalls, 0, `dirty(Message) must not trigger messageRawPayload at the user layer; got ${messageRawPayloadCalls} call(s)`);
90
- // --- Case 4: un-pooled Contact still emits the user-facing dirty event ---
91
- let wechatyDirtyEmits = 0;
92
- wechaty.on('dirty', () => {
93
- wechatyDirtyEmits++;
94
- });
95
- puppet.emit('dirty', {
96
- payloadType: PUPPET.types.Payload.Contact,
97
- payloadId: 'yet-another-un-pooled-id',
98
- });
99
- await delay(50);
100
- t.equal(wechatyDirtyEmits, 1, `wechaty.emit('dirty', ...) must fire once even when the id is not pooled; got ${wechatyDirtyEmits}`);
101
- // --- Case 5: pooled Contact MUST re-fetch on dirty ---
102
- const pooledContact = puppet.mocker.createContact({ name: 'pooled-contact' });
103
- // Force the contact into the wechatified pool via find(). Payload gets
104
- // loaded once as part of find()'s ready() call — subsequent counts we
105
- // measure from AFTER this seed.
106
- await wechaty.Contact.find({ id: pooledContact.id });
107
- let contactPayloadCalls = 0;
108
- const originalContactPayload = puppet.contactPayload.bind(puppet);
109
- puppet.contactPayload = async (id) => {
110
- contactPayloadCalls++;
111
- return originalContactPayload(id);
112
- };
113
- puppet.emit('dirty', {
114
- payloadType: PUPPET.types.Payload.Contact,
115
- payloadId: pooledContact.id,
116
- });
117
- await delay(50);
118
- t.ok(contactPayloadCalls >= 1, `dirty(Contact) for pooled id must trigger contactPayload via ready(true); got ${contactPayloadCalls} call(s)`);
119
- await wechaty.stop();
120
- });
121
- //# sourceMappingURL=puppet-mixin-dirty-pool.spec.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"puppet-mixin-dirty-pool.spec.js","sourceRoot":"","sources":["../../../../src/wechaty-mixins/puppet-mixin-dirty-pool.spec.ts"],"names":[],"mappings":"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"}
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env -S node --no-warnings --loader ts-node/esm
2
- export {};
3
- //# sourceMappingURL=puppet-mixin-dirty-pool.spec.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"puppet-mixin-dirty-pool.spec.d.ts","sourceRoot":"","sources":["../../../../src/wechaty-mixins/puppet-mixin-dirty-pool.spec.ts"],"names":[],"mappings":""}
@@ -1,96 +0,0 @@
1
- #!/usr/bin/env -S node --no-warnings --loader ts-node/esm
2
- import { test } from 'tstest';
3
- import * as PUPPET from '@juzi/wechaty-puppet';
4
- import { PuppetMock } from '@juzi/wechaty-puppet-mock';
5
- import { WechatyBuilder } from '../wechaty-builder.js';
6
- /**
7
- * Regression: the `dirty` event handler for pooled user-modules
8
- * (Contact / Room / WxxdProduct / WxxdOrder) used to call
9
- * `this.Xxx.find({ id }).ready(true)`. `find({ id })` on a pool miss
10
- * constructs a fresh instance and calls `ready()` — a puppet-side gRPC
11
- * round-trip — turning a passive "invalidate if cached" signal into an
12
- * active fetch for ids no business code has touched.
13
- *
14
- * The fix looks the id up in the pool directly and only calls
15
- * `.ready(true)` when the instance is already pooled.
16
- */
17
- async function delay(ms) {
18
- await new Promise(resolve => setTimeout(resolve, ms));
19
- }
20
- test('dirty handler does not fetch payload for un-pooled ids, but still refreshes pooled ones', async (t) => {
21
- const puppet = new PuppetMock();
22
- const wechaty = WechatyBuilder.build({ puppet });
23
- const botContact = puppet.mocker.createContact({ name: 'test-bot' });
24
- await wechaty.start();
25
- await puppet.mocker.login(botContact);
26
- // --- Case 1: un-pooled Contact id must not trigger contactRawPayload ---
27
- let contactRawPayloadCalls = 0;
28
- const originalContactRawPayload = puppet.contactRawPayload.bind(puppet);
29
- puppet.contactRawPayload = async (id) => {
30
- contactRawPayloadCalls++;
31
- return originalContactRawPayload(id);
32
- };
33
- puppet.emit('dirty', {
34
- payloadType: PUPPET.types.Payload.Contact,
35
- payloadId: 'never-loaded-contact-id',
36
- });
37
- await delay(50);
38
- t.equal(contactRawPayloadCalls, 0, `dirty(Contact) for un-pooled id must not trigger contactRawPayload; got ${contactRawPayloadCalls} call(s)`);
39
- // --- Case 2: un-pooled Room id must not trigger roomRawPayload ---
40
- let roomRawPayloadCalls = 0;
41
- const originalRoomRawPayload = puppet.roomRawPayload.bind(puppet);
42
- puppet.roomRawPayload = async (id) => {
43
- roomRawPayloadCalls++;
44
- return originalRoomRawPayload(id);
45
- };
46
- puppet.emit('dirty', {
47
- payloadType: PUPPET.types.Payload.Room,
48
- payloadId: 'never-loaded-room-id',
49
- });
50
- await delay(50);
51
- t.equal(roomRawPayloadCalls, 0, `dirty(Room) for un-pooled id must not trigger roomRawPayload; got ${roomRawPayloadCalls} call(s)`);
52
- // --- Case 3: Message dirty must not trigger messageRawPayload (Message has no user-layer pool) ---
53
- let messageRawPayloadCalls = 0;
54
- const originalMessageRawPayload = puppet.messageRawPayload.bind(puppet);
55
- puppet.messageRawPayload = async (id) => {
56
- messageRawPayloadCalls++;
57
- return originalMessageRawPayload(id);
58
- };
59
- puppet.emit('dirty', {
60
- payloadType: PUPPET.types.Payload.Message,
61
- payloadId: 'some-message-id',
62
- });
63
- await delay(50);
64
- t.equal(messageRawPayloadCalls, 0, `dirty(Message) must not trigger messageRawPayload at the user layer; got ${messageRawPayloadCalls} call(s)`);
65
- // --- Case 4: un-pooled Contact still emits the user-facing dirty event ---
66
- let wechatyDirtyEmits = 0;
67
- wechaty.on('dirty', () => {
68
- wechatyDirtyEmits++;
69
- });
70
- puppet.emit('dirty', {
71
- payloadType: PUPPET.types.Payload.Contact,
72
- payloadId: 'yet-another-un-pooled-id',
73
- });
74
- await delay(50);
75
- t.equal(wechatyDirtyEmits, 1, `wechaty.emit('dirty', ...) must fire once even when the id is not pooled; got ${wechatyDirtyEmits}`);
76
- // --- Case 5: pooled Contact MUST re-fetch on dirty ---
77
- const pooledContact = puppet.mocker.createContact({ name: 'pooled-contact' });
78
- // Force the contact into the wechatified pool via find(). Payload gets
79
- // loaded once as part of find()'s ready() call — subsequent counts we
80
- // measure from AFTER this seed.
81
- await wechaty.Contact.find({ id: pooledContact.id });
82
- let contactPayloadCalls = 0;
83
- const originalContactPayload = puppet.contactPayload.bind(puppet);
84
- puppet.contactPayload = async (id) => {
85
- contactPayloadCalls++;
86
- return originalContactPayload(id);
87
- };
88
- puppet.emit('dirty', {
89
- payloadType: PUPPET.types.Payload.Contact,
90
- payloadId: pooledContact.id,
91
- });
92
- await delay(50);
93
- t.ok(contactPayloadCalls >= 1, `dirty(Contact) for pooled id must trigger contactPayload via ready(true); got ${contactPayloadCalls} call(s)`);
94
- await wechaty.stop();
95
- });
96
- //# sourceMappingURL=puppet-mixin-dirty-pool.spec.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"puppet-mixin-dirty-pool.spec.js","sourceRoot":"","sources":["../../../../src/wechaty-mixins/puppet-mixin-dirty-pool.spec.ts"],"names":[],"mappings":"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"}