[object Object]

← 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

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 →