← back to Butlr
scripts: validate-twiml.js — 11-assertion smoke test for prod TwiML
27d8207fcfddce2b64680bd625181b3406660fa3 · 2026-05-12 21:08:27 -0700 · SteveStudio2
POSTs /twilio/twiml/<call_id> against prod and asserts:
- track="inbound_track" on <Stream>
- no <Redirect> (would tear down <Stream>)
- exactly one <Gather> with actionOnEmptyResult=true
- gather action URL matches /twilio/gather/<id>
- announce present (Play or Say fallback)
Used in YOLO tick 3 to confirm cd10f95 deploy was correct on prod —
returned 11/11 green. Exit code = 0 on all-pass for CI use.
Files touched
A scripts/validate-twiml.js
Diff
commit 27d8207fcfddce2b64680bd625181b3406660fa3
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Tue May 12 21:08:27 2026 -0700
scripts: validate-twiml.js — 11-assertion smoke test for prod TwiML
POSTs /twilio/twiml/<call_id> against prod and asserts:
- track="inbound_track" on <Stream>
- no <Redirect> (would tear down <Stream>)
- exactly one <Gather> with actionOnEmptyResult=true
- gather action URL matches /twilio/gather/<id>
- announce present (Play or Say fallback)
Used in YOLO tick 3 to confirm cd10f95 deploy was correct on prod —
returned 11/11 green. Exit code = 0 on all-pass for CI use.
---
scripts/validate-twiml.js | 63 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 63 insertions(+)
diff --git a/scripts/validate-twiml.js b/scripts/validate-twiml.js
new file mode 100755
index 0000000..52ac73f
--- /dev/null
+++ b/scripts/validate-twiml.js
@@ -0,0 +1,63 @@
+#!/usr/bin/env node
+// Validate the TwiML emitted by /twilio/twiml/:call_id against the
+// inline-Gather + inbound_track contract.
+//
+// Asserts:
+// • response is well-formed XML wrapped in <Response>…</Response>
+// • contains exactly one <Start><Stream … track="inbound_track" …/></Start>
+// • Stream url uses wss:// and matches the wsBase host pattern
+// • contains exactly one <Gather> with action=…/twilio/gather/<id>
+// • does NOT contain <Redirect> (would tear down <Stream>)
+// • Gather has actionOnEmptyResult="true"
+//
+// Usage:
+// node scripts/validate-twiml.js <call_id> [--base https://butlr.agentabrams.com]
+//
+// Exits non-zero on first assertion failure with a clear message.
+
+const callId = process.argv[2];
+const baseIdx = process.argv.indexOf('--base');
+const base = baseIdx > 0 ? process.argv[baseIdx + 1] : 'https://butlr.agentabrams.com';
+
+if (!callId) {
+ console.error('usage: node scripts/validate-twiml.js <call_id> [--base URL]');
+ process.exit(64);
+}
+if (!/^[A-Za-z0-9_-]{6,16}$/.test(callId)) {
+ console.error('bad call_id format'); process.exit(64);
+}
+
+const url = `${base}/twilio/twiml/${callId}`;
+console.log(`→ POST ${url}`);
+
+async function main() {
+ const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: '' });
+ const xml = await r.text();
+ console.log(` status: ${r.status} content-type: ${r.headers.get('content-type')}`);
+ console.log(' body:'); console.log(xml.split('\n').map(l => ' ' + l).join('\n'));
+
+ const assertions = [
+ { name: 'http 200', ok: r.status === 200 },
+ { name: 'xml prolog', ok: /^<\?xml/.test(xml.trim()) },
+ { name: '<Response> wrapper', ok: /<Response>[\s\S]*<\/Response>/.test(xml) },
+ { name: '<Start><Stream> present', ok: /<Start>\s*<Stream\b/.test(xml) },
+ { name: 'track="inbound_track"', ok: /<Stream[^>]*\btrack="inbound_track"/.test(xml) },
+ { name: 'stream url is wss://', ok: /<Stream[^>]*\burl="wss:\/\//.test(xml) },
+ { name: 'one <Gather> only', ok: (xml.match(/<Gather\b/g) || []).length === 1 },
+ { name: 'gather action → /twilio/gather', ok: new RegExp(`action="[^"]*\\/twilio\\/gather\\/${callId}"`).test(xml) },
+ { name: 'actionOnEmptyResult="true"', ok: /actionOnEmptyResult="true"/.test(xml) },
+ { name: 'NO <Redirect>', ok: !/<Redirect\b/.test(xml) },
+ { name: 'announce present (Play or Say)', ok: /<Play>|<Say\b/.test(xml) },
+ ];
+
+ let pass = 0, fail = 0;
+ console.log('\nassertions:');
+ for (const a of assertions) {
+ console.log(` ${a.ok ? '✓' : '✗'} ${a.name}`);
+ if (a.ok) pass++; else fail++;
+ }
+ console.log(`\n${pass}/${pass + fail} passed${fail ? ` — ${fail} FAILED` : ''}`);
+ process.exit(fail ? 1 : 0);
+}
+
+main().catch(e => { console.error('error:', e.message); process.exit(2); });
← ead4134 calls: inline MP3 player per row + has-recording probe + LLM
·
back to Butlr
·
admin: /admin/orphan-recordings route + UA regex fix for /st 2196d1f →