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.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/assets/agora/SKILL.md +113 -0
- package/assets/agora/references/cli/README.md +161 -0
- package/assets/agora/references/cli/automation.md +189 -0
- package/assets/agora/references/cli/doctor.md +129 -0
- package/assets/agora/references/cli/env.md +158 -0
- package/assets/agora/references/cli/install-auth.md +152 -0
- package/assets/agora/references/cli/projects.md +116 -0
- package/assets/agora/references/cli/quickstarts.md +117 -0
- package/assets/agora/references/cloud-recording/README.md +86 -0
- package/assets/agora/references/conversational-ai/README.md +285 -0
- package/assets/agora/references/conversational-ai/agent-client-toolkit-react.md +182 -0
- package/assets/agora/references/conversational-ai/agent-samples.md +101 -0
- package/assets/agora/references/conversational-ai/agent-toolkit-android.md +209 -0
- package/assets/agora/references/conversational-ai/agent-toolkit-ios.md +208 -0
- package/assets/agora/references/conversational-ai/agent-toolkit.md +201 -0
- package/assets/agora/references/conversational-ai/agent-ui-kit.md +63 -0
- package/assets/agora/references/conversational-ai/architecture.md +221 -0
- package/assets/agora/references/conversational-ai/auth-flow.md +154 -0
- package/assets/agora/references/conversational-ai/conversational-ai-studio.md +173 -0
- package/assets/agora/references/conversational-ai/go-sdk.md +184 -0
- package/assets/agora/references/conversational-ai/integration-from-quickstart.md +203 -0
- package/assets/agora/references/conversational-ai/python-sdk.md +122 -0
- package/assets/agora/references/conversational-ai/quickstarts.md +710 -0
- package/assets/agora/references/conversational-ai/server-custom-llm.md +45 -0
- package/assets/agora/references/conversational-ai/server-mcp.md +40 -0
- package/assets/agora/references/conversational-ai/server-sdk-rename.md +78 -0
- package/assets/agora/references/conversational-ai/server-sdks.md +128 -0
- package/assets/agora/references/doc-fetching.md +67 -0
- package/assets/agora/references/integration-patterns.md +201 -0
- package/assets/agora/references/mcp-tools.md +49 -0
- package/assets/agora/references/rtc/README.md +104 -0
- package/assets/agora/references/rtc/android.md +344 -0
- package/assets/agora/references/rtc/cross-platform-coordination.md +61 -0
- package/assets/agora/references/rtc/flutter.md +282 -0
- package/assets/agora/references/rtc/ios.md +306 -0
- package/assets/agora/references/rtc/nextjs.md +87 -0
- package/assets/agora/references/rtc/react-native.md +266 -0
- package/assets/agora/references/rtc/react.md +186 -0
- package/assets/agora/references/rtc/web.md +506 -0
- package/assets/agora/references/rtm/README.md +80 -0
- package/assets/agora/references/rtm/android.md +277 -0
- package/assets/agora/references/rtm/ios.md +231 -0
- package/assets/agora/references/rtm/web.md +348 -0
- package/assets/agora/references/server/README.md +22 -0
- package/assets/agora/references/server/tokens.md +74 -0
- package/assets/agora/references/server-gateway/README.md +80 -0
- package/assets/agora/references/server-gateway/linux-cpp.md +251 -0
- package/assets/agora/references/testing-guidance/SKILL.md +65 -0
- package/assets/agora/references/testing-guidance/completeness-gate.md +28 -0
- package/assets/agora/references/testing-guidance/convoai-rest.md +83 -0
- package/assets/agora/references/testing-guidance/mobile-rtm-and-renewal.md +109 -0
- package/assets/agora/references/testing-guidance/rtc-android.md +70 -0
- package/assets/agora/references/testing-guidance/rtc-ios.md +73 -0
- package/assets/agora/references/testing-guidance/rtc-react.md +51 -0
- package/assets/agora/references/testing-guidance/rtc-web.md +94 -0
- package/cordis.patch.yml +5 -0
- package/index.js +53 -0
- 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
|