@telnyx/voice-agent-tester 0.4.0 → 0.4.3
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/.github/workflows/draft-release.yml +39 -4
- package/.github/workflows/publish-release.yml +2 -2
- package/CHANGELOG.md +32 -0
- package/README.md +28 -1
- package/applications/telnyx.yaml +4 -3
- package/applications/vapi.yaml +0 -4
- package/package.json +1 -1
- package/src/index.js +91 -3
- package/src/provider-import.js +1 -1
- package/src/voice-agent-tester.js +189 -1
|
@@ -46,8 +46,8 @@ jobs:
|
|
|
46
46
|
|
|
47
47
|
- name: Setup Git user
|
|
48
48
|
run: |
|
|
49
|
-
git config user.name
|
|
50
|
-
git config user.email
|
|
49
|
+
git config user.name "github-actions[bot]"
|
|
50
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
51
51
|
|
|
52
52
|
- name: Use Node.js 20.x
|
|
53
53
|
uses: actions/setup-node@v4
|
|
@@ -64,9 +64,44 @@ jobs:
|
|
|
64
64
|
env:
|
|
65
65
|
CI: true
|
|
66
66
|
|
|
67
|
-
- name:
|
|
67
|
+
- name: Determine next version
|
|
68
|
+
id: version
|
|
68
69
|
run: |
|
|
69
|
-
npx release-it --ci --
|
|
70
|
+
NEXT=$(npx release-it --ci --release-version${{ env.INCREMENT_ARG }}${{ env.PRERELEASE_ARGS }} 2>/dev/null)
|
|
71
|
+
echo "next=$NEXT" >> "$GITHUB_OUTPUT"
|
|
72
|
+
echo "branch=release/v$NEXT" >> "$GITHUB_OUTPUT"
|
|
73
|
+
env:
|
|
74
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
75
|
+
|
|
76
|
+
- name: Create release branch
|
|
77
|
+
run: |
|
|
78
|
+
git checkout -b "${{ steps.version.outputs.branch }}"
|
|
79
|
+
|
|
80
|
+
- name: Create draft release on branch
|
|
81
|
+
run: |
|
|
82
|
+
npx release-it --ci --github.draft --no-npm.publish --no-git.push --no-git.requireUpstream${{ env.INCREMENT_ARG }}${{ env.PRERELEASE_ARGS }}
|
|
70
83
|
env:
|
|
71
84
|
NPM_TOKEN: ${{ secrets.NPM_CI_TOKEN }}
|
|
72
85
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
86
|
+
|
|
87
|
+
- name: Push release branch
|
|
88
|
+
run: |
|
|
89
|
+
git push origin "${{ steps.version.outputs.branch }}"
|
|
90
|
+
|
|
91
|
+
- name: Create pull request
|
|
92
|
+
run: |
|
|
93
|
+
gh pr create \
|
|
94
|
+
--title "chore: release v${{ steps.version.outputs.next }}" \
|
|
95
|
+
--body "## Release v${{ steps.version.outputs.next }}
|
|
96
|
+
|
|
97
|
+
Automated release PR created by the draft-release workflow.
|
|
98
|
+
|
|
99
|
+
- Version bump in \`package.json\`
|
|
100
|
+
- Updated \`CHANGELOG.md\`
|
|
101
|
+
- Draft GitHub release created
|
|
102
|
+
|
|
103
|
+
**After merging**, publish the release from the [releases page](https://github.com/${{ github.repository }}/releases)." \
|
|
104
|
+
--base "${{ env.TARGET_REF }}" \
|
|
105
|
+
--head "${{ steps.version.outputs.branch }}"
|
|
106
|
+
env:
|
|
107
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -19,8 +19,8 @@ jobs:
|
|
|
19
19
|
|
|
20
20
|
- name: Setup Git user
|
|
21
21
|
run: |
|
|
22
|
-
git config user.name
|
|
23
|
-
git config user.email
|
|
22
|
+
git config user.name "github-actions[bot]"
|
|
23
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
24
24
|
|
|
25
25
|
- name: Use Node.js 20.x
|
|
26
26
|
uses: actions/setup-node@v4
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.3](https://github.com/team-telnyx/voice-agent-tester/compare/v0.4.2...v0.4.3) (2026-03-11)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* add click_with_retry action and fix audio event race conditions ([#19](https://github.com/team-telnyx/voice-agent-tester/issues/19)) ([#19](https://github.com/team-telnyx/voice-agent-tester/issues/19)) ([13e2009](https://github.com/team-telnyx/voice-agent-tester/commit/13e2009a94b4e2f7e05972f01a47c9b31758bf58))
|
|
8
|
+
|
|
9
|
+
### Chores
|
|
10
|
+
|
|
11
|
+
* release v0.4.2 ([#18](https://github.com/team-telnyx/voice-agent-tester/issues/18)) ([1cf64ef](https://github.com/team-telnyx/voice-agent-tester/commit/1cf64ef563e813c2f06b2b655bfcc414637594cb))
|
|
12
|
+
|
|
13
|
+
## [0.4.2](https://github.com/team-telnyx/voice-agent-tester/compare/v0.4.1...v0.4.2) (2026-02-23)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* add dashboard hints for Vapi and ElevenLabs comparison mode params ([#16](https://github.com/team-telnyx/voice-agent-tester/issues/16)) ([7fda40b](https://github.com/team-telnyx/voice-agent-tester/commit/7fda40b6971a968dde1fc1c3466662227a3bc77e))
|
|
18
|
+
|
|
19
|
+
### Chores
|
|
20
|
+
|
|
21
|
+
* improve event logs and comparison mode docs ([#17](https://github.com/team-telnyx/voice-agent-tester/issues/17)) ([24a9683](https://github.com/team-telnyx/voice-agent-tester/commit/24a968337a0b4a6c2d6baddd0aa507d5a87c9488))
|
|
22
|
+
|
|
23
|
+
## [0.4.1](https://github.com/team-telnyx/voice-agent-tester/compare/v0.4.0...v0.4.1) (2026-02-18)
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* require provider-specific params for comparison mode ([#10](https://github.com/team-telnyx/voice-agent-tester/issues/10)) ([db9eb27](https://github.com/team-telnyx/voice-agent-tester/commit/db9eb273c139374a9f6358126113cab92f8f5b32))
|
|
28
|
+
* use Qwen/Qwen3-235B-A22B as model for imported assistants ([#11](https://github.com/team-telnyx/voice-agent-tester/issues/11)) ([3c4ed0a](https://github.com/team-telnyx/voice-agent-tester/commit/3c4ed0a14498833544f1797426b234585adcb49b))
|
|
29
|
+
|
|
30
|
+
### Bug Fixes
|
|
31
|
+
|
|
32
|
+
* add --no-git.requireUpstream to release-it in draft workflow ([#14](https://github.com/team-telnyx/voice-agent-tester/issues/14)) ([9553e65](https://github.com/team-telnyx/voice-agent-tester/commit/9553e65bdc6f0094853895da6b806befc5a898f6))
|
|
33
|
+
* use triggering user as git author and create PR for releases ([#13](https://github.com/team-telnyx/voice-agent-tester/issues/13)) ([8ebecba](https://github.com/team-telnyx/voice-agent-tester/commit/8ebecba1839985949e46bec457f327711f89138d))
|
|
34
|
+
|
|
3
35
|
## [0.4.0](https://github.com/team-telnyx/voice-agent-tester/compare/v0.3.0...v0.4.0) (2026-01-26)
|
|
4
36
|
|
|
5
37
|
### Features
|
package/README.md
CHANGED
|
@@ -31,6 +31,8 @@ voice-agent-tester -a applications/telnyx.yaml -s scenarios/appointment.yaml --a
|
|
|
31
31
|
| `--provider` | | Import from provider (`vapi`, `elevenlabs`, `retell`) |
|
|
32
32
|
| `--provider-api-key` | | External provider API key (required with `--provider`) |
|
|
33
33
|
| `--provider-import-id` | | Provider assistant ID to import (required with `--provider`) |
|
|
34
|
+
| `--share-key` | | Vapi share key for comparison mode (prompted if missing) |
|
|
35
|
+
| `--branch-id` | | ElevenLabs branch ID for comparison mode (prompted if missing) |
|
|
34
36
|
| `--compare` | `true` | Run both provider direct and Telnyx import benchmarks |
|
|
35
37
|
| `--no-compare` | | Disable comparison (run only Telnyx import) |
|
|
36
38
|
| `-d, --debug` | `false` | Enable detailed timeout diagnostics |
|
|
@@ -190,20 +192,45 @@ When importing from an external provider, the tool automatically runs both bench
|
|
|
190
192
|
1. **Provider Direct** - Benchmarks the assistant on the original provider's widget
|
|
191
193
|
2. **Telnyx Import** - Benchmarks the same assistant after importing to Telnyx
|
|
192
194
|
|
|
195
|
+
### Provider-Specific Keys
|
|
196
|
+
|
|
197
|
+
Comparison mode requires a provider-specific key to load the provider's direct widget. If not passed via CLI, the tool will prompt you with instructions on how to find it.
|
|
198
|
+
|
|
199
|
+
| Provider | Flag | How to find it |
|
|
200
|
+
|----------|------|----------------|
|
|
201
|
+
| Vapi | `--share-key` | In the Vapi Dashboard, select your assistant, then click the link icon (🔗) next to the assistant ID at the top. This copies the demo link containing your share key. |
|
|
202
|
+
| ElevenLabs | `--branch-id` | In the ElevenLabs Dashboard, go to Agents, select your target agent, then click the dropdown next to Publish and select "Copy shareable link". This copies the demo link containing your branch ID. |
|
|
203
|
+
|
|
193
204
|
### Import and Compare (Default)
|
|
194
205
|
|
|
206
|
+
**Vapi:**
|
|
207
|
+
|
|
195
208
|
```bash
|
|
196
209
|
npx @telnyx/voice-agent-tester@latest \
|
|
197
210
|
-a applications/telnyx.yaml \
|
|
198
211
|
-s scenarios/appointment.yaml \
|
|
199
212
|
--provider vapi \
|
|
213
|
+
--share-key <VAPI_SHARE_KEY> \
|
|
200
214
|
--api-key <TELNYX_KEY> \
|
|
201
215
|
--provider-api-key <VAPI_KEY> \
|
|
202
216
|
--provider-import-id <VAPI_ASSISTANT_ID>
|
|
203
217
|
```
|
|
204
218
|
|
|
219
|
+
**ElevenLabs:**
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
npx @telnyx/voice-agent-tester@latest \
|
|
223
|
+
-a applications/telnyx.yaml \
|
|
224
|
+
-s scenarios/appointment.yaml \
|
|
225
|
+
--provider elevenlabs \
|
|
226
|
+
--branch-id <ELEVENLABS_BRANCH_ID> \
|
|
227
|
+
--api-key <TELNYX_KEY> \
|
|
228
|
+
--provider-api-key <ELEVENLABS_KEY> \
|
|
229
|
+
--provider-import-id <ELEVENLABS_AGENT_ID>
|
|
230
|
+
```
|
|
231
|
+
|
|
205
232
|
This will:
|
|
206
|
-
- Run Phase 1:
|
|
233
|
+
- Run Phase 1: Provider direct benchmark
|
|
207
234
|
- Run Phase 2: Telnyx import benchmark
|
|
208
235
|
- Generate a side-by-side latency comparison report
|
|
209
236
|
|
package/applications/telnyx.yaml
CHANGED
package/applications/vapi.yaml
CHANGED
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -87,6 +87,60 @@ function substituteUrlParams(url, params) {
|
|
|
87
87
|
return result;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Get the list of missing provider-specific parameters required for comparison mode.
|
|
92
|
+
* Each provider has its own set of required params for the direct widget benchmark.
|
|
93
|
+
*
|
|
94
|
+
* @param {Object} argv - Parsed CLI arguments
|
|
95
|
+
* @returns {Array<{key: string, flag: string, description: string}>} Missing params
|
|
96
|
+
*/
|
|
97
|
+
function getCompareRequiredParams(argv) {
|
|
98
|
+
const missing = [];
|
|
99
|
+
|
|
100
|
+
switch (argv.provider) {
|
|
101
|
+
case 'vapi':
|
|
102
|
+
if (!argv.shareKey) {
|
|
103
|
+
missing.push({
|
|
104
|
+
key: 'shareKey',
|
|
105
|
+
flag: '--share-key',
|
|
106
|
+
description: 'Vapi share key',
|
|
107
|
+
hint: 'In the Vapi Dashboard, select your assistant, then click the link icon (🔗) next to the assistant ID at the top. This copies the demo link containing your share key.'
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
case 'elevenlabs':
|
|
112
|
+
if (!argv.branchId) {
|
|
113
|
+
missing.push({
|
|
114
|
+
key: 'branchId',
|
|
115
|
+
flag: '--branch-id',
|
|
116
|
+
description: 'ElevenLabs branch ID',
|
|
117
|
+
hint: 'In the ElevenLabs Dashboard, go to Agents, select your target agent, then click the dropdown next to Publish and select "Copy shareable link". This copies the demo link containing your branch ID.'
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
// retell and others: no extra params needed yet
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return missing;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get provider-specific template parameters for comparison mode URL/HTML substitution.
|
|
129
|
+
*
|
|
130
|
+
* @param {Object} argv - Parsed CLI arguments
|
|
131
|
+
* @returns {Object} Template params to merge into provider params
|
|
132
|
+
*/
|
|
133
|
+
function getCompareTemplateParams(argv) {
|
|
134
|
+
switch (argv.provider) {
|
|
135
|
+
case 'vapi':
|
|
136
|
+
return { shareKey: argv.shareKey };
|
|
137
|
+
case 'elevenlabs':
|
|
138
|
+
return { branchId: argv.branchId };
|
|
139
|
+
default:
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
90
144
|
// Helper function to load and validate application config
|
|
91
145
|
function loadApplicationConfig(configPath, params = {}) {
|
|
92
146
|
const configFile = fs.readFileSync(configPath, 'utf8');
|
|
@@ -235,6 +289,14 @@ const argv = yargs(hideBin(process.argv))
|
|
|
235
289
|
type: 'string',
|
|
236
290
|
description: 'Provider assistant/agent ID to import (required with --provider)'
|
|
237
291
|
})
|
|
292
|
+
.option('share-key', {
|
|
293
|
+
type: 'string',
|
|
294
|
+
description: 'Vapi share key for direct widget testing (required for comparison mode with --provider vapi)'
|
|
295
|
+
})
|
|
296
|
+
.option('branch-id', {
|
|
297
|
+
type: 'string',
|
|
298
|
+
description: 'ElevenLabs branch ID for direct widget testing (required for comparison mode with --provider elevenlabs)'
|
|
299
|
+
})
|
|
238
300
|
.option('assistant-id', {
|
|
239
301
|
type: 'string',
|
|
240
302
|
description: 'Assistant/agent ID for direct benchmarking (works with all providers)'
|
|
@@ -456,8 +518,8 @@ async function main() {
|
|
|
456
518
|
// Parse URL parameters for template substitution
|
|
457
519
|
const params = parseParams(argv.params);
|
|
458
520
|
|
|
459
|
-
// Determine if we should run comparison benchmark
|
|
460
|
-
|
|
521
|
+
// Determine if we should run comparison benchmark (may be updated later if public key is missing)
|
|
522
|
+
let shouldCompare = argv.provider && argv.compare && !argv.noCompare;
|
|
461
523
|
|
|
462
524
|
// Store credentials for potential comparison run
|
|
463
525
|
let telnyxApiKey = argv.apiKey;
|
|
@@ -492,6 +554,32 @@ async function main() {
|
|
|
492
554
|
}
|
|
493
555
|
}
|
|
494
556
|
|
|
557
|
+
// Require provider-specific params when comparison mode is enabled
|
|
558
|
+
if (shouldCompare) {
|
|
559
|
+
const missingParams = getCompareRequiredParams(argv);
|
|
560
|
+
if (missingParams.length > 0) {
|
|
561
|
+
for (const param of missingParams) {
|
|
562
|
+
console.log(`\n🔑 ${param.description} is required for comparison mode`);
|
|
563
|
+
if (param.hint) {
|
|
564
|
+
console.log(` ${param.hint}`);
|
|
565
|
+
}
|
|
566
|
+
const inputVal = await promptUserInput(`Enter ${param.description} (or press Enter to skip comparison): `);
|
|
567
|
+
if (inputVal) {
|
|
568
|
+
argv[param.key] = inputVal;
|
|
569
|
+
} else {
|
|
570
|
+
console.warn(`⚠️ Missing ${param.flag}. Disabling comparison mode (--no-compare).`);
|
|
571
|
+
console.warn(` To run comparison benchmarks, pass ${param.flag} <value>\n`);
|
|
572
|
+
argv.compare = false;
|
|
573
|
+
argv.noCompare = true;
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Re-evaluate shouldCompare after potential public key prompt
|
|
581
|
+
shouldCompare = argv.provider && argv.compare && !argv.noCompare;
|
|
582
|
+
|
|
495
583
|
const importResult = await importAssistantsFromProvider({
|
|
496
584
|
provider: argv.provider,
|
|
497
585
|
providerApiKey: providerApiKey,
|
|
@@ -583,7 +671,7 @@ async function main() {
|
|
|
583
671
|
|
|
584
672
|
// Phase 1: Provider Direct Benchmark
|
|
585
673
|
// Load provider-specific application config with provider assistant ID
|
|
586
|
-
const providerParams = { ...params, assistantId: providerImportId };
|
|
674
|
+
const providerParams = { ...params, assistantId: providerImportId, ...getCompareTemplateParams(argv) };
|
|
587
675
|
const providerAppPath = path.resolve(__packageDir, 'applications', `${argv.provider}.yaml`);
|
|
588
676
|
|
|
589
677
|
if (!fs.existsSync(providerAppPath)) {
|
package/src/provider-import.js
CHANGED
|
@@ -200,7 +200,7 @@ async function configureImportedAssistant({ assistantId, assistantName, telnyxAp
|
|
|
200
200
|
},
|
|
201
201
|
body: JSON.stringify({
|
|
202
202
|
name: newName,
|
|
203
|
-
model: 'Qwen/Qwen3-235B-
|
|
203
|
+
model: 'Qwen/Qwen3-235B-A22B',
|
|
204
204
|
telephony_settings: {
|
|
205
205
|
supports_unauthenticated_web_calls: true
|
|
206
206
|
},
|
|
@@ -330,7 +330,8 @@ export class VoiceAgentTester {
|
|
|
330
330
|
await this.page.exposeFunction('__publishEvent', (eventType, data) => {
|
|
331
331
|
const event = { eventType, data, timestamp: Date.now() };
|
|
332
332
|
|
|
333
|
-
|
|
333
|
+
const elementSuffix = data && data.elementId ? ` (audio element: ${data.elementId})` : '';
|
|
334
|
+
console.log(`\t📢 ${eventType}${elementSuffix}`);
|
|
334
335
|
|
|
335
336
|
// Check if there are any pending promises waiting for this event type
|
|
336
337
|
const pendingPromises = this.pendingPromises.get(eventType);
|
|
@@ -534,6 +535,9 @@ export class VoiceAgentTester {
|
|
|
534
535
|
case 'screenshot':
|
|
535
536
|
handlerResult = await this.handleScreenshot(step);
|
|
536
537
|
break;
|
|
538
|
+
case 'click_with_retry':
|
|
539
|
+
handlerResult = await this.handleClickWithRetry(step);
|
|
540
|
+
break;
|
|
537
541
|
|
|
538
542
|
default:
|
|
539
543
|
console.log(`Unknown action: ${action}`);
|
|
@@ -576,10 +580,173 @@ export class VoiceAgentTester {
|
|
|
576
580
|
await this.page.click(selector);
|
|
577
581
|
}
|
|
578
582
|
|
|
583
|
+
async handleClickWithRetry(step) {
|
|
584
|
+
const selector = step.selector;
|
|
585
|
+
if (!selector) {
|
|
586
|
+
throw new Error('No selector specified for click_with_retry action');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const maxRetries = step.retries || 2;
|
|
590
|
+
const retryDelay = step.retryDelay || 3000;
|
|
591
|
+
const checkDelay = step.checkDelay || 4000;
|
|
592
|
+
|
|
593
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
594
|
+
let clicked = false;
|
|
595
|
+
try {
|
|
596
|
+
await this.page.waitForSelector(selector, { timeout: attempt === 1 ? 30000 : 5000 });
|
|
597
|
+
await this.page.click(selector);
|
|
598
|
+
clicked = true;
|
|
599
|
+
} catch {
|
|
600
|
+
// Selector not found — will check for widget config errors below
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (!clicked) {
|
|
604
|
+
// Check if the widget is showing a configuration error
|
|
605
|
+
const widgetState = await this._getWidgetErrorState(selector);
|
|
606
|
+
|
|
607
|
+
if (widgetState.isConfigError) {
|
|
608
|
+
// Widget is showing "unauthenticated web calls" or similar config error.
|
|
609
|
+
// This means the API config hasn't propagated to the widget yet.
|
|
610
|
+
if (attempt < maxRetries) {
|
|
611
|
+
console.log(`\t⚠️ Click attempt ${attempt}/${maxRetries}: widget not ready — "${widgetState.errorText}"`);
|
|
612
|
+
console.log(`\t⏳ Waiting for configuration to propagate (reloading in ${retryDelay}ms)...`);
|
|
613
|
+
await this.sleep(retryDelay);
|
|
614
|
+
await this.page.reload({ waitUntil: 'networkidle0', timeout: 30000 });
|
|
615
|
+
await this.sleep(2000); // extra time after reload
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
throw new Error(
|
|
619
|
+
`Widget configuration not ready after ${maxRetries} attempts: "${widgetState.errorText}"\n` +
|
|
620
|
+
`The "Supports Unauthenticated Web Calls" setting may not have propagated yet.\n` +
|
|
621
|
+
`Try running again in a few seconds, or verify the setting in the Telnyx portal.`
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Not a config error — genuinely missing selector
|
|
626
|
+
if (attempt < maxRetries) {
|
|
627
|
+
console.log(`\t⚠️ Click attempt ${attempt}/${maxRetries}: selector not found, retrying in ${retryDelay}ms...`);
|
|
628
|
+
await this.sleep(retryDelay);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
throw new Error(`Selector "${selector}" not found after ${maxRetries} attempts`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
console.log(`\t🖱️ Click attempt ${attempt}/${maxRetries}`);
|
|
635
|
+
|
|
636
|
+
// Wait for connection to establish
|
|
637
|
+
await this.sleep(checkDelay);
|
|
638
|
+
|
|
639
|
+
// Check if audio elements are monitored or WebRTC connections exist
|
|
640
|
+
const status = await this._checkConnectionStatus();
|
|
641
|
+
|
|
642
|
+
if (status.isConnected) {
|
|
643
|
+
console.log(`\t✅ Connection established (monitored: ${status.monitoredElements}, rtc: ${status.rtcConnections})`);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (attempt < maxRetries) {
|
|
648
|
+
console.log(`\t⚠️ No connection detected (monitored: ${status.monitoredElements}, rtc: ${status.rtcConnections}), retrying in ${retryDelay}ms...`);
|
|
649
|
+
await this.sleep(retryDelay);
|
|
650
|
+
} else {
|
|
651
|
+
console.log(`\t⚠️ No connection detected after ${maxRetries} attempts, proceeding anyway`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Check if a widget is showing a configuration error (e.g., "unauthenticated web calls" not enabled).
|
|
658
|
+
* Inspects the shadow DOM for error indicators.
|
|
659
|
+
*/
|
|
660
|
+
async _getWidgetErrorState(selector) {
|
|
661
|
+
const parts = selector.split('>>>').map(s => s.trim());
|
|
662
|
+
const hostSelector = parts[0];
|
|
663
|
+
|
|
664
|
+
return await this.page.evaluate((host) => {
|
|
665
|
+
const el = document.querySelector(host);
|
|
666
|
+
if (!el || !el.shadowRoot) return { isConfigError: false };
|
|
667
|
+
|
|
668
|
+
const text = el.shadowRoot.textContent || '';
|
|
669
|
+
|
|
670
|
+
// Check for known configuration error messages
|
|
671
|
+
const configErrors = [
|
|
672
|
+
'unauthenticated web calls',
|
|
673
|
+
'support unauthenticated',
|
|
674
|
+
'not configured',
|
|
675
|
+
'configuration required'
|
|
676
|
+
];
|
|
677
|
+
|
|
678
|
+
const lowerText = text.toLowerCase();
|
|
679
|
+
for (const pattern of configErrors) {
|
|
680
|
+
if (lowerText.includes(pattern)) {
|
|
681
|
+
// Extract a readable error message
|
|
682
|
+
const errorText = text.trim().replace(/\s+/g, ' ').substring(0, 200);
|
|
683
|
+
return { isConfigError: true, errorText };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return { isConfigError: false };
|
|
688
|
+
}, hostSelector);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async _checkConnectionStatus() {
|
|
692
|
+
const status = await this.page.evaluate(() => {
|
|
693
|
+
const info = { monitoredElements: 0, hasActiveConnection: false };
|
|
694
|
+
|
|
695
|
+
if (window.audioMonitor && window.audioMonitor.monitoredElements) {
|
|
696
|
+
info.monitoredElements = window.audioMonitor.monitoredElements.size;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
document.querySelectorAll('audio').forEach(el => {
|
|
700
|
+
if (el.srcObject) info.hasActiveConnection = true;
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
return info;
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
let rtcConnections = 0;
|
|
707
|
+
try {
|
|
708
|
+
const rtpStats = await this.page.evaluate(async () => {
|
|
709
|
+
if (typeof window.__getRtpStats === 'function') {
|
|
710
|
+
return await window.__getRtpStats();
|
|
711
|
+
}
|
|
712
|
+
return null;
|
|
713
|
+
});
|
|
714
|
+
if (rtpStats) rtcConnections = rtpStats.connectionCount || 0;
|
|
715
|
+
} catch {
|
|
716
|
+
// Ignore RTP stats errors
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
monitoredElements: status.monitoredElements,
|
|
721
|
+
rtcConnections,
|
|
722
|
+
isConnected: status.monitoredElements > 0 || status.hasActiveConnection || rtcConnections > 0
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
579
726
|
async handleWaitForVoice() {
|
|
580
727
|
if (this.debug) {
|
|
581
728
|
console.log('\t⏳ Waiting for audio to start (AI agent response)...');
|
|
582
729
|
}
|
|
730
|
+
|
|
731
|
+
// Check if audio is already playing before waiting for a new event.
|
|
732
|
+
// This handles the case where audiostart fired before we started listening
|
|
733
|
+
// (e.g., during click_with_retry or between steps).
|
|
734
|
+
const alreadyPlaying = await this.page.evaluate(() => {
|
|
735
|
+
if (window.audioMonitor && window.audioMonitor.monitoredElements) {
|
|
736
|
+
for (const [, data] of window.audioMonitor.monitoredElements) {
|
|
737
|
+
if (data.isPlaying) return true;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return false;
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
if (alreadyPlaying) {
|
|
744
|
+
if (this.debug) {
|
|
745
|
+
console.log('\t✅ Audio already playing');
|
|
746
|
+
}
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
583
750
|
await this.waitForAudioEvent('audiostart');
|
|
584
751
|
if (this.debug) {
|
|
585
752
|
console.log('\t✅ Audio detected');
|
|
@@ -590,6 +757,27 @@ export class VoiceAgentTester {
|
|
|
590
757
|
if (this.debug) {
|
|
591
758
|
console.log('\t⏳ Waiting for audio to stop (silence)...');
|
|
592
759
|
}
|
|
760
|
+
|
|
761
|
+
// Check if all monitored elements are already silent.
|
|
762
|
+
// This handles the case where audiostop fired before we started listening.
|
|
763
|
+
const allSilent = await this.page.evaluate(() => {
|
|
764
|
+
if (window.audioMonitor && window.audioMonitor.monitoredElements) {
|
|
765
|
+
if (window.audioMonitor.monitoredElements.size === 0) return false; // no elements yet
|
|
766
|
+
for (const [, data] of window.audioMonitor.monitoredElements) {
|
|
767
|
+
if (data.isPlaying) return false;
|
|
768
|
+
}
|
|
769
|
+
return true; // all elements exist and are silent
|
|
770
|
+
}
|
|
771
|
+
return false;
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
if (allSilent) {
|
|
775
|
+
if (this.debug) {
|
|
776
|
+
console.log('\t✅ Already silent');
|
|
777
|
+
}
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
593
781
|
await this.waitForAudioEvent('audiostop');
|
|
594
782
|
if (this.debug) {
|
|
595
783
|
console.log('\t✅ Silence detected');
|