dsh-agora 0.2.0

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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/assets/agora/SKILL.md +113 -0
  4. package/assets/agora/references/cli/README.md +161 -0
  5. package/assets/agora/references/cli/automation.md +189 -0
  6. package/assets/agora/references/cli/doctor.md +129 -0
  7. package/assets/agora/references/cli/env.md +158 -0
  8. package/assets/agora/references/cli/install-auth.md +152 -0
  9. package/assets/agora/references/cli/projects.md +116 -0
  10. package/assets/agora/references/cli/quickstarts.md +117 -0
  11. package/assets/agora/references/cloud-recording/README.md +86 -0
  12. package/assets/agora/references/conversational-ai/README.md +285 -0
  13. package/assets/agora/references/conversational-ai/agent-client-toolkit-react.md +182 -0
  14. package/assets/agora/references/conversational-ai/agent-samples.md +101 -0
  15. package/assets/agora/references/conversational-ai/agent-toolkit-android.md +209 -0
  16. package/assets/agora/references/conversational-ai/agent-toolkit-ios.md +208 -0
  17. package/assets/agora/references/conversational-ai/agent-toolkit.md +201 -0
  18. package/assets/agora/references/conversational-ai/agent-ui-kit.md +63 -0
  19. package/assets/agora/references/conversational-ai/architecture.md +221 -0
  20. package/assets/agora/references/conversational-ai/auth-flow.md +154 -0
  21. package/assets/agora/references/conversational-ai/conversational-ai-studio.md +173 -0
  22. package/assets/agora/references/conversational-ai/go-sdk.md +184 -0
  23. package/assets/agora/references/conversational-ai/integration-from-quickstart.md +203 -0
  24. package/assets/agora/references/conversational-ai/python-sdk.md +122 -0
  25. package/assets/agora/references/conversational-ai/quickstarts.md +710 -0
  26. package/assets/agora/references/conversational-ai/server-custom-llm.md +45 -0
  27. package/assets/agora/references/conversational-ai/server-mcp.md +40 -0
  28. package/assets/agora/references/conversational-ai/server-sdk-rename.md +78 -0
  29. package/assets/agora/references/conversational-ai/server-sdks.md +128 -0
  30. package/assets/agora/references/doc-fetching.md +67 -0
  31. package/assets/agora/references/integration-patterns.md +201 -0
  32. package/assets/agora/references/mcp-tools.md +49 -0
  33. package/assets/agora/references/rtc/README.md +104 -0
  34. package/assets/agora/references/rtc/android.md +344 -0
  35. package/assets/agora/references/rtc/cross-platform-coordination.md +61 -0
  36. package/assets/agora/references/rtc/flutter.md +282 -0
  37. package/assets/agora/references/rtc/ios.md +306 -0
  38. package/assets/agora/references/rtc/nextjs.md +87 -0
  39. package/assets/agora/references/rtc/react-native.md +266 -0
  40. package/assets/agora/references/rtc/react.md +186 -0
  41. package/assets/agora/references/rtc/web.md +506 -0
  42. package/assets/agora/references/rtm/README.md +80 -0
  43. package/assets/agora/references/rtm/android.md +277 -0
  44. package/assets/agora/references/rtm/ios.md +231 -0
  45. package/assets/agora/references/rtm/web.md +348 -0
  46. package/assets/agora/references/server/README.md +22 -0
  47. package/assets/agora/references/server/tokens.md +74 -0
  48. package/assets/agora/references/server-gateway/README.md +80 -0
  49. package/assets/agora/references/server-gateway/linux-cpp.md +251 -0
  50. package/assets/agora/references/testing-guidance/SKILL.md +65 -0
  51. package/assets/agora/references/testing-guidance/completeness-gate.md +28 -0
  52. package/assets/agora/references/testing-guidance/convoai-rest.md +83 -0
  53. package/assets/agora/references/testing-guidance/mobile-rtm-and-renewal.md +109 -0
  54. package/assets/agora/references/testing-guidance/rtc-android.md +70 -0
  55. package/assets/agora/references/testing-guidance/rtc-ios.md +73 -0
  56. package/assets/agora/references/testing-guidance/rtc-react.md +51 -0
  57. package/assets/agora/references/testing-guidance/rtc-web.md +94 -0
  58. package/cordis.patch.yml +5 -0
  59. package/index.js +53 -0
  60. package/package.json +49 -0
@@ -0,0 +1,251 @@
1
+ # Server Gateway SDK — Linux C++
2
+
3
+ ## Initialization
4
+
5
+ ```cpp
6
+ // Create and initialize IAgoraService — one instance per process
7
+ auto service = createAgoraService();
8
+ agora::base::AgoraServiceConfiguration scfg;
9
+ scfg.appId = appid;
10
+ scfg.enableAudioProcessor = true;
11
+ scfg.enableAudioDevice = false; // servers don't use audio devices
12
+ scfg.enableVideo = true;
13
+ scfg.useStringUid = false; // set true to use string UIDs
14
+ if (service->initialize(scfg) != agora::ERR_OK) {
15
+ return nullptr;
16
+ }
17
+ ```
18
+
19
+ ## Create and Connect
20
+
21
+ ```cpp
22
+ // Create connection — autoSubscribe must be false; set BROADCASTER role
23
+ agora::rtc::RtcConnectionConfiguration ccfg;
24
+ ccfg.autoSubscribeAudio = false;
25
+ ccfg.autoSubscribeVideo = false;
26
+ ccfg.clientRoleType = agora::rtc::CLIENT_ROLE_BROADCASTER;
27
+ agora::agora_refptr<agora::rtc::IRtcConnection> connection =
28
+ service->createRtcConnection(ccfg);
29
+
30
+ // Register connection observer before connecting
31
+ auto connObserver = std::make_shared<SampleConnectionObserver>();
32
+ connection->registerObserver(connObserver.get());
33
+
34
+ // Connect to channel
35
+ if (connection->connect(appId, channelId, userId)) {
36
+ AG_LOG(ERROR, "Failed to connect to Agora channel!");
37
+ return -1;
38
+ }
39
+ ```
40
+
41
+ ## Sending Media
42
+
43
+ ### Create senders
44
+
45
+ ```cpp
46
+ agora::agora_refptr<agora::rtc::IMediaNodeFactory> factory =
47
+ service->createMediaNodeFactory();
48
+
49
+ // PCM audio sender
50
+ agora::agora_refptr<agora::rtc::IAudioPcmDataSender> audioPcmDataSender =
51
+ factory->createAudioPcmDataSender();
52
+
53
+ // YUV video sender
54
+ agora::agora_refptr<agora::rtc::IVideoFrameSender> videoFrameSender =
55
+ factory->createVideoFrameSender();
56
+
57
+ // Encoded audio sender (AAC, Opus, etc.)
58
+ agora::agora_refptr<agora::rtc::IAudioEncodedFrameSender> audioFrameSender =
59
+ factory->createAudioEncodedFrameSender();
60
+
61
+ // Encoded video sender (H.264, etc.)
62
+ agora::agora_refptr<agora::rtc::IVideoEncodedImageSender> videoEncodedFrameSender =
63
+ factory->createVideoEncodedImageSender();
64
+ ```
65
+
66
+ ### Create and publish tracks
67
+
68
+ ```cpp
69
+ // PCM audio track
70
+ agora::agora_refptr<agora::rtc::ILocalAudioTrack> customAudioTrack =
71
+ service->createCustomAudioTrack(audioPcmDataSender);
72
+
73
+ // Encoded audio track
74
+ agora::agora_refptr<agora::rtc::ILocalAudioTrack> customAudioTrack =
75
+ service->createCustomAudioTrack(audioFrameSender, agora::base::MIX_DISABLED);
76
+
77
+ // YUV video track
78
+ agora::agora_refptr<agora::rtc::ILocalVideoTrack> customVideoTrack =
79
+ service->createCustomVideoTrack(videoFrameSender);
80
+
81
+ // Encoded video track
82
+ agora::agora_refptr<agora::rtc::ILocalVideoTrack> customVideoTrack =
83
+ service->createCustomVideoTrack(videoEncodedFrameSender);
84
+
85
+ // Enable and publish
86
+ customAudioTrack->setEnabled(true);
87
+ connection->getLocalUser()->publishAudio(customAudioTrack);
88
+ customVideoTrack->setEnabled(true);
89
+ connection->getLocalUser()->publishVideo(customVideoTrack);
90
+ ```
91
+
92
+ ### Send PCM audio (10 ms intervals — required)
93
+
94
+ ```cpp
95
+ // PCM must be sent in exactly 10 ms frames — use a pacer thread
96
+ static void SampleSendAudioTask(
97
+ agora::agora_refptr<agora::rtc::IAudioPcmDataSender> audioFrameSender,
98
+ bool& exitFlag) {
99
+ PacerInfo pacer = {0, 10, std::chrono::steady_clock::now()};
100
+ while (!exitFlag) {
101
+ // samplesPer10ms = sampleRate / 100
102
+ audioFrameSender->sendAudioPcmData(
103
+ frameBuf, 0, samplesPer10ms,
104
+ agora::rtc::TWO_BYTES_PER_SAMPLE,
105
+ numOfChannels, sampleRate);
106
+ waitBeforeNextSend(pacer);
107
+ }
108
+ }
109
+ ```
110
+
111
+ ### Send encoded H.264 video
112
+
113
+ ```cpp
114
+ static void sendOneH264Frame(
115
+ int frameRate,
116
+ std::unique_ptr<HelperH264Frame> h264Frame,
117
+ agora::agora_refptr<agora::rtc::IVideoEncodedImageSender> videoH264FrameSender) {
118
+ agora::rtc::EncodedVideoFrameInfo videoEncodedFrameInfo;
119
+ videoEncodedFrameInfo.rotation = agora::rtc::VIDEO_ORIENTATION_0;
120
+ videoEncodedFrameInfo.codecType = agora::rtc::VIDEO_CODEC_H264;
121
+ videoEncodedFrameInfo.framesPerSecond = frameRate;
122
+ videoEncodedFrameInfo.frameType =
123
+ h264Frame->isKeyFrame
124
+ ? agora::rtc::VIDEO_FRAME_TYPE_KEY_FRAME
125
+ : agora::rtc::VIDEO_FRAME_TYPE_DELTA_FRAME;
126
+
127
+ videoH264FrameSender->sendEncodedVideoImage(
128
+ reinterpret_cast<uint8_t*>(h264Frame->buffer.get()),
129
+ h264Frame->bufferLen,
130
+ videoEncodedFrameInfo);
131
+ }
132
+ ```
133
+
134
+ ## Receiving Media
135
+
136
+ ```cpp
137
+ // Register observers via SampleLocalUserObserver (wraps ILocalUserObserver)
138
+ auto localUserObserver =
139
+ std::make_shared<SampleLocalUserObserver>(connection->getLocalUser());
140
+
141
+ // Audio: set PCM params before registering observer
142
+ connection->getLocalUser()->setPlaybackAudioFrameBeforeMixingParameters(
143
+ numOfChannels, sampleRate);
144
+
145
+ auto pcmFrameObserver = std::make_shared<PcmFrameObserver>(outputFile);
146
+ localUserObserver->setAudioFrameObserver(pcmFrameObserver.get());
147
+
148
+ // Video (encoded H.264)
149
+ auto h264FrameReceiver = std::make_shared<H264FrameReceiver>(outputFile);
150
+ localUserObserver->setVideoEncodedImageReceiver(h264FrameReceiver.get());
151
+ ```
152
+
153
+ ### PCM receive callback
154
+
155
+ ```cpp
156
+ bool PcmFrameObserver::onPlaybackAudioFrameBeforeMixing(
157
+ const char* channelId,
158
+ agora::media::base::user_id_t userId,
159
+ AudioFrame& audioFrame) {
160
+ size_t writeBytes =
161
+ audioFrame.samplesPerChannel * audioFrame.channels * sizeof(int16_t);
162
+ fwrite(audioFrame.buffer, 1, writeBytes, pcmFile_);
163
+ return true;
164
+ }
165
+ ```
166
+
167
+ ### Encoded video receive callback
168
+
169
+ ```cpp
170
+ class H264FrameReceiver : public agora::rtc::IVideoEncodedImageReceiver {
171
+ public:
172
+ bool OnEncodedVideoImageReceived(
173
+ const uint8_t* imageBuffer,
174
+ size_t length,
175
+ const agora::rtc::EncodedVideoFrameInfo& videoEncodedFrameInfo) override;
176
+ };
177
+ ```
178
+
179
+ ### YUV video receive callback
180
+
181
+ ```cpp
182
+ class YuvFrameObserver : public agora::rtc::IVideoFrameObserver2 {
183
+ public:
184
+ void onFrame(
185
+ const char* channelId,
186
+ agora::user_id_t remoteUid,
187
+ const agora::media::base::VideoFrame* frame) override;
188
+ };
189
+ ```
190
+
191
+ ## Video Mixing
192
+
193
+ ```cpp
194
+ // Create mixer and mixed video track
195
+ agora::agora_refptr<agora::rtc::IVideoMixerSource> videoMixer =
196
+ factory->createVideoMixer();
197
+ agora::agora_refptr<agora::rtc::ILocalVideoTrack> mixVideoTrack =
198
+ service->createMixedVideoTrack(videoMixer);
199
+
200
+ // Configure encoder
201
+ agora::rtc::VideoEncoderConfiguration encoderConfig;
202
+ encoderConfig.codecType = agora::rtc::VIDEO_CODEC_H264;
203
+ encoderConfig.dimensions.width = 1920;
204
+ encoderConfig.dimensions.height = 1080;
205
+ encoderConfig.frameRate = 15;
206
+ mixVideoTrack->setVideoEncoderConfiguration(encoderConfig);
207
+
208
+ // Add remote video tracks and set layout
209
+ videoMixer->addVideoTrack(userId, remote_video_track_);
210
+ videoMixer->setStreamLayout(userId, layout); // position/size per user
211
+ videoMixer->setBackground(1920, 1080, 15);
212
+ videoMixer->refresh(); // apply layout changes
213
+ ```
214
+
215
+ ## Shutdown Sequence
216
+
217
+ Order matters — follow exactly:
218
+
219
+ ```cpp
220
+ // 1. Unpublish tracks
221
+ connection->getLocalUser()->unpublishAudio(customAudioTrack);
222
+ connection->getLocalUser()->unpublishVideo(customVideoTrack);
223
+
224
+ // 2. Unregister observers
225
+ connection->unregisterObserver(connObserver.get());
226
+ local_user_->unregisterAudioFrameObserver(audio_frame_observer_);
227
+ local_user_->unregisterVideoFrameObserver(video_frame_observer_);
228
+
229
+ // 3. Disconnect
230
+ connection->disconnect();
231
+
232
+ // 4. Release objects (nullptr in this order)
233
+ connObserver.reset();
234
+ localUserObserver.reset();
235
+ audioPcmDataSender = nullptr;
236
+ videoFrameSender = nullptr;
237
+ customAudioTrack = nullptr;
238
+ customVideoTrack = nullptr;
239
+ factory = nullptr;
240
+ connection = nullptr;
241
+
242
+ // 5. Release service last
243
+ service->release();
244
+ service = nullptr;
245
+ ```
246
+
247
+ ## Official Documentation
248
+
249
+ - **[Send and Receive Media Streams](https://docs.agora.io/en/server-gateway/develop/send-receive-media-streams?platform=linux-cpp)**
250
+ - **[Video Mixing](https://docs.agora.io/en/server-gateway/develop/video-mixing?platform=linux-cpp)**
251
+ - **[API Reference](https://api-ref.agora.io/en/server-gateway-sdk/linux-cpp/4.x/index.html)**
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: agora-testing-guidance
3
+ description: |
4
+ Mocking patterns and testing requirements for Agora SDK integration code.
5
+ Covers RTC Web, RTC React, RTC iOS, RTC Android, RTC React Native, RTC Flutter,
6
+ RTM Web, RTM iOS, RTM Android, and ConvoAI REST API.
7
+ Use when generating tests for any Agora integration, or when reminding the user
8
+ to add tests to an implementation.
9
+ license: MIT
10
+ metadata:
11
+ author: agora
12
+ version: '1.0.0'
13
+ ---
14
+
15
+ # Agora Testing Guidance
16
+
17
+ Mocking patterns and completeness requirements for Agora SDK integration code.
18
+
19
+ ## When to Generate Tests
20
+
21
+ Every code generation task that produces implementation code must include test stubs.
22
+ If the user asks to "implement" something, remind them to generate tests before the
23
+ task is complete. Do not mark an implementation task as done until tests are addressed.
24
+
25
+ For the reminder template, see [completeness-gate.md](completeness-gate.md).
26
+
27
+ ## Route by Stack
28
+
29
+ Use the file that matches the user's integration:
30
+
31
+ | Stack | File |
32
+ |---|---|
33
+ | RTC Web (`agora-rtc-sdk-ng`) | [rtc-web.md](rtc-web.md) |
34
+ | RTC React (`agora-rtc-react`) | [rtc-react.md](rtc-react.md) |
35
+ | RTC iOS (Swift) | [rtc-ios.md](rtc-ios.md) |
36
+ | RTC Android (Kotlin/Java) | [rtc-android.md](rtc-android.md) |
37
+ | RTC React Native / Flutter / RTM Web / RTM iOS / RTM Android / token renewal | [mobile-rtm-and-renewal.md](mobile-rtm-and-renewal.md) |
38
+ | ConvoAI REST API / backend HTTP integrations | [convoai-rest.md](convoai-rest.md) |
39
+ | Completeness reminder and behavior-specific test prompts | [completeness-gate.md](completeness-gate.md) |
40
+
41
+ ## Core Rule
42
+
43
+ Mock at the boundary your code owns:
44
+
45
+ - SDK wrapper code: mock the SDK module or engine/client abstraction
46
+ - UI hooks/components: mock the hooks or provider layer
47
+ - REST integrations: mock the HTTP client, not the SDK
48
+ - Native singletons/factories: wrap behind protocols/interfaces first, then inject mocks
49
+
50
+ ## Platform Notes
51
+
52
+ - Prefer unit tests for join/publish/subscribe/token-renewal behavior.
53
+ - For UI tests, assert rendered state after simulated SDK callbacks rather than trying to render the actual native/media views.
54
+ - For ConvoAI, assert both request shape and auth headers.
55
+
56
+ ## Required Behaviors to Cover
57
+
58
+ At minimum, generated tests should verify the behaviors most likely to break:
59
+
60
+ - join/login is called with the correct channel and identity
61
+ - tokens are renewed before expiry
62
+ - cleanup happens in the right order
63
+ - ConvoAI payload types are correct (`agent_rtc_uid` string, `remote_rtc_uids` array)
64
+ - auth headers are present and sourced correctly
65
+ - failure paths do not continue with partial initialization
@@ -0,0 +1,28 @@
1
+ # Testing Guidance — Completeness Gate
2
+
3
+ When generating an implementation, append the following reminder after the code block:
4
+
5
+ ```text
6
+ > **Testing:** The above implementation is not complete without tests.
7
+ > Generate unit tests that verify: [list specific behaviors from the implementation].
8
+ > See `references/testing-guidance/SKILL.md` for mocking patterns.
9
+ ```
10
+
11
+ Substitute `[list specific behaviors]` with the concrete behaviors the tests should
12
+ cover. Do not leave the completeness gate as a generic reminder.
13
+
14
+ Examples:
15
+
16
+ - "join is called with the correct channel name and UID"
17
+ - "agent_rtc_uid is passed as string, not integer"
18
+ - "remote_rtc_uids is an array of strings"
19
+ - "acquire is called before start; start is not called if acquire fails"
20
+ - "RTM login resolves before subscribe is attempted"
21
+ - "token renewal fetches a fresh token before expiry"
22
+
23
+ ## Rule
24
+
25
+ Do not mark an implementation task as done until tests are either:
26
+
27
+ - generated, or
28
+ - explicitly called out as still required with concrete behaviors to cover
@@ -0,0 +1,83 @@
1
+ # Testing Guidance — ConvoAI REST API
2
+
3
+ Mock at the HTTP client layer. ConvoAI integration generates REST calls — mock the
4
+ HTTP client, not the Agora SDK.
5
+
6
+ ## Python
7
+
8
+ Use `unittest.mock.patch`:
9
+
10
+ ```python
11
+ from unittest.mock import patch, MagicMock
12
+
13
+ @patch('requests.post')
14
+ def test_create_agent(mock_post):
15
+ mock_post.return_value = MagicMock(
16
+ status_code=200,
17
+ json=lambda: {"agent_id": "agent_abc123", "status": "STARTING"}
18
+ )
19
+
20
+ from your_module import create_agent
21
+ result = create_agent(channel="test-channel", uid="42")
22
+
23
+ assert result["agent_id"] == "agent_abc123"
24
+ mock_post.assert_called_once()
25
+ call_kwargs = mock_post.call_args
26
+ assert "Authorization" in call_kwargs.kwargs.get("headers", {})
27
+ ```
28
+
29
+ ## JavaScript / TypeScript
30
+
31
+ Use `jest.spyOn` on `global.fetch`:
32
+
33
+ ```javascript
34
+ beforeEach(() => {
35
+ jest.spyOn(global, 'fetch').mockResolvedValue({
36
+ ok: true,
37
+ json: async () => ({ agent_id: 'agent_abc123', status: 'STARTING' }),
38
+ })
39
+ })
40
+
41
+ afterEach(() => jest.restoreAllMocks())
42
+
43
+ test('createAgent sends correct request', async () => {
44
+ await createAgent({ channel: 'test-channel', uid: '42' })
45
+ expect(fetch).toHaveBeenCalledWith(
46
+ expect.stringContaining('/join'),
47
+ expect.objectContaining({ method: 'POST' }),
48
+ )
49
+ })
50
+ ```
51
+
52
+ If using `axios`, use `axios-mock-adapter` or `jest.spyOn(axios, 'post')`.
53
+
54
+ ## Required Assertions
55
+
56
+ Every ConvoAI REST integration test should verify:
57
+
58
+ - `Authorization` header is present
59
+ - the request body uses `agent_rtc_uid` as a string, not an integer
60
+ - `remote_rtc_uids` is an array of strings
61
+ - `name` is unique or generated dynamically
62
+ - auth credentials/tokens come from environment or server config, not hardcoded literals
63
+ - `/update` sends the full `params` object rather than only the changed field
64
+
65
+ Example payload assertions:
66
+
67
+ ```javascript
68
+ expect(fetch).toHaveBeenCalledWith(
69
+ expect.any(String),
70
+ expect.objectContaining({
71
+ headers: expect.objectContaining({
72
+ Authorization: expect.stringContaining('agora token='),
73
+ }),
74
+ body: expect.stringContaining('"agent_rtc_uid":"0"'),
75
+ }),
76
+ )
77
+ ```
78
+
79
+ Failure-path tests to include:
80
+
81
+ - non-200 response surfaces `detail` / `reason`
82
+ - 409 agent-name collision retries with a new name
83
+ - request failure does not continue with partial local state
@@ -0,0 +1,109 @@
1
+ # Testing Guidance — React Native, Flutter, RTM, and Token Renewal
2
+
3
+ ## RTC React Native (`react-native-agora`)
4
+
5
+ Mock at the module boundary using `jest.mock`. The engine is created via `createAgoraRtcEngine()` — mock the factory and capture the registered event handler so tests can fire callbacks.
6
+
7
+ ```javascript
8
+ // __mocks__/react-native-agora.js
9
+ const mockEngine = {
10
+ initialize: jest.fn().mockResolvedValue(undefined),
11
+ enableVideo: jest.fn().mockResolvedValue(undefined),
12
+ startPreview: jest.fn().mockResolvedValue(undefined),
13
+ joinChannel: jest.fn().mockResolvedValue(undefined),
14
+ leaveChannel: jest.fn().mockResolvedValue(undefined),
15
+ registerEventHandler: jest.fn(),
16
+ unregisterEventHandler: jest.fn(),
17
+ release: jest.fn().mockResolvedValue(undefined),
18
+ }
19
+
20
+ module.exports = {
21
+ createAgoraRtcEngine: jest.fn().mockReturnValue(mockEngine),
22
+ ChannelProfileType: { ChannelProfileCommunication: 1 },
23
+ ClientRoleType: { ClientRoleBroadcaster: 1, ClientRoleAudience: 2 },
24
+ RtcSurfaceView: 'RtcSurfaceView',
25
+ }
26
+ ```
27
+
28
+ Simulate callbacks by capturing the registered handler and calling methods directly.
29
+
30
+ ## RTC Flutter (`agora_rtc_engine`)
31
+
32
+ Use the `mockito` package with `build_runner` to generate mocks. Inject the engine via constructor rather than calling `createAgoraRtcEngine()` directly.
33
+
34
+ ```yaml
35
+ dev_dependencies:
36
+ mockito: ^5.4.0
37
+ build_runner: ^2.4.0
38
+ ```
39
+
40
+ ```dart
41
+ @GenerateMocks([RtcEngine])
42
+ void main() {}
43
+ ```
44
+
45
+ Test join and leave behavior by stubbing async engine methods and verifying the correct parameters.
46
+
47
+ ## RTM Web (`agora-rtm`)
48
+
49
+ Mock at the module boundary. The `RTM` constructor can throw — the mock should reflect that. Capture `addEventListener` calls so tests can simulate incoming events.
50
+
51
+ ```javascript
52
+ const mockRtmClient = {
53
+ login: jest.fn().mockResolvedValue({}),
54
+ logout: jest.fn().mockResolvedValue({}),
55
+ subscribe: jest.fn().mockResolvedValue({}),
56
+ unsubscribe: jest.fn().mockResolvedValue({}),
57
+ publish: jest.fn().mockResolvedValue({}),
58
+ addEventListener: jest.fn(),
59
+ }
60
+ ```
61
+
62
+ Primary assertions:
63
+
64
+ - `login` resolves before `subscribe`
65
+ - incoming `message` events update UI/state
66
+ - presence handling only works when subscribed with `withPresence: true`
67
+
68
+ ## RTM iOS / Android
69
+
70
+ Use the same native pattern as RTC:
71
+
72
+ - iOS: protocol-based injection around the RTM client
73
+ - Android: interface extraction with Mockito
74
+
75
+ Capture async completion callbacks so tests can simulate success/failure for login,
76
+ subscribe, unsubscribe, and publish.
77
+
78
+ Primary assertions:
79
+
80
+ - login happens before subscribe
81
+ - publish sends to the correct channel/topic
82
+ - message callbacks update state correctly
83
+
84
+ ## Token Renewal Across Platforms
85
+
86
+ Token renewal is a required production behavior. Cover both RTC and RTM paths:
87
+
88
+ - RTC Web: `token-privilege-will-expire`
89
+ - RTC iOS: `onTokenPrivilegeWillExpire`
90
+ - RTC Android: `onTokenPrivilegeWillExpire`
91
+ - RTM Web / native: token-expiry or status callbacks for RTM re-login
92
+
93
+ Web example:
94
+
95
+ ```javascript
96
+ test('handles RTM token expiry', async () => {
97
+ global.fetch = jest.fn().mockResolvedValue({
98
+ ok: true,
99
+ json: async () => ({ rtmToken: 'new-rtm-token' }),
100
+ })
101
+
102
+ const statusHandler = rtmClient.addEventListener.mock.calls
103
+ .find(([e]) => e === 'status')[1]
104
+
105
+ await statusHandler({ state: 'TOKEN_EXPIRED', reason: 'token expired' })
106
+
107
+ expect(rtmClient.login).toHaveBeenCalledWith({ token: 'new-rtm-token' })
108
+ })
109
+ ```
@@ -0,0 +1,70 @@
1
+ # Testing Guidance — RTC Android (Kotlin)
2
+
3
+ Use interface extraction with Mockito. `RtcEngine.create(context, appId, handler)`
4
+ is a factory method — wrap it behind an interface to enable mocking.
5
+
6
+ Pattern:
7
+
8
+ ```kotlin
9
+ interface RtcEngineInterface {
10
+ fun joinChannel(token: String?, channelName: String, uid: Int,
11
+ options: ChannelMediaOptions): Int
12
+ fun leaveChannel(): Int
13
+ fun enableVideo(): Int
14
+ fun muteLocalAudioStream(mute: Boolean): Int
15
+ fun renewToken(token: String): Int
16
+ }
17
+
18
+ class RtcEngineAdapter(private val engine: RtcEngine) : RtcEngineInterface {
19
+ override fun joinChannel(token: String?, channelName: String, uid: Int,
20
+ options: ChannelMediaOptions) =
21
+ engine.joinChannel(token, channelName, uid, options)
22
+ override fun leaveChannel() = engine.leaveChannel()
23
+ override fun enableVideo() = engine.enableVideo()
24
+ override fun muteLocalAudioStream(mute: Boolean) = engine.muteLocalAudioStream(mute)
25
+ override fun renewToken(token: String) = engine.renewToken(token)
26
+ }
27
+ ```
28
+
29
+ Example:
30
+
31
+ ```kotlin
32
+ @RunWith(MockitoJUnitRunner::class)
33
+ class RtcManagerTest {
34
+ @Mock lateinit var mockEngine: RtcEngineInterface
35
+
36
+ @Test
37
+ fun `joins channel with correct parameters`() {
38
+ whenever(mockEngine.joinChannel(anyOrNull(), any(), any(), any())).thenReturn(0)
39
+
40
+ val manager = RtcManager(engine = mockEngine)
41
+ manager.join(channel = "test", token = null, uid = 0)
42
+
43
+ verify(mockEngine).joinChannel(eq(null), eq("test"), eq(0), any())
44
+ }
45
+
46
+ @Test
47
+ fun `renews token when privilege will expire`() {
48
+ val manager = RtcManager(engine = mockEngine)
49
+ val handler = manager.getRtcEventHandler()
50
+
51
+ handler.onTokenPrivilegeWillExpire("expiring-token")
52
+
53
+ verify(mockEngine, timeout(500)).renewToken(any())
54
+ }
55
+ }
56
+ ```
57
+
58
+ Add to `build.gradle`:
59
+
60
+ ```groovy
61
+ testImplementation 'org.mockito:mockito-kotlin:5.+'
62
+ testImplementation 'org.mockito:mockito-core:5.+'
63
+ ```
64
+
65
+ Primary assertions:
66
+
67
+ - join called with correct token/channel/UID
68
+ - engine setup methods invoked in order
69
+ - token renewal path is wired
70
+ - cleanup/leave path runs on disconnect
@@ -0,0 +1,73 @@
1
+ # Testing Guidance — RTC iOS (Swift)
2
+
3
+ Use protocol-based injection. `AgoraRtcEngineKit.sharedEngine(withAppId:delegate:)`
4
+ is a singleton — wrap it behind a protocol to enable mocking.
5
+
6
+ Pattern:
7
+
8
+ ```swift
9
+ protocol RtcEngineProtocol: AnyObject {
10
+ func joinChannel(byToken token: String?,
11
+ channelId: String,
12
+ uid: UInt,
13
+ mediaOptions: AgoraRtcChannelMediaOptions) -> Int32
14
+ func leaveChannel(_ leaveChannelBlock: ((AgoraChannelStats) -> Void)?) -> Int32
15
+ func enableVideo() -> Int32
16
+ func muteLocalAudioStream(_ mute: Bool) -> Int32
17
+ func renewToken(_ token: String) -> Int32
18
+ }
19
+
20
+ extension AgoraRtcEngineKit: RtcEngineProtocol {}
21
+
22
+ class RtcManager {
23
+ private let engine: RtcEngineProtocol
24
+ init(engine: RtcEngineProtocol = AgoraRtcEngineKit.sharedEngine(
25
+ withAppId: Config.appId, delegate: nil)) {
26
+ self.engine = engine
27
+ }
28
+ }
29
+ ```
30
+
31
+ Mock:
32
+
33
+ ```swift
34
+ class MockRtcEngine: RtcEngineProtocol {
35
+ var joinChannelCallCount = 0
36
+ var renewedToken: String?
37
+
38
+ func joinChannel(byToken token: String?, channelId: String,
39
+ uid: UInt,
40
+ mediaOptions: AgoraRtcChannelMediaOptions) -> Int32 {
41
+ joinChannelCallCount += 1
42
+ return 0
43
+ }
44
+
45
+ func leaveChannel(_ leaveChannelBlock: ((AgoraChannelStats) -> Void)?) -> Int32 { 0 }
46
+ func enableVideo() -> Int32 { 0 }
47
+ func muteLocalAudioStream(_ mute: Bool) -> Int32 { 0 }
48
+ func renewToken(_ token: String) -> Int32 {
49
+ renewedToken = token
50
+ return 0
51
+ }
52
+ }
53
+ ```
54
+
55
+ Token renewal:
56
+
57
+ ```swift
58
+ func testRenewsTokenBeforeExpiry() {
59
+ let mockEngine = MockRtcEngine()
60
+ let manager = RtcManager(engine: mockEngine)
61
+
62
+ manager.rtcEngine(mockEngine as! AgoraRtcEngineKit, tokenPrivilegeWillExpire: "expiring-token")
63
+
64
+ XCTAssertNotNil(mockEngine.renewedToken)
65
+ }
66
+ ```
67
+
68
+ Primary assertions:
69
+
70
+ - join called with correct channel/token/UID
71
+ - video/audio setup methods are invoked
72
+ - token renewal path runs before expiry
73
+ - leave/cleanup happens once