#!/usr/bin/env node /** * Dirigent feature test script * Tests: no-reply gate, end-symbol enforcement, turn management * * Usage: * node scripts/test-features.mjs [channelId] * * Env: * PROXY_TOKEN - path to PROXY_BOT_TOKEN file (default: ./PROXY_BOT_TOKEN) * GUILD_ID - guild id (default: 1480860737902743686) * * Reads token from PROXY_BOT_TOKEN file. */ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const TOKEN_FILE = process.env.PROXY_TOKEN || path.resolve(__dirname, "../PROXY_BOT_TOKEN"); const GUILD_ID = process.env.GUILD_ID || "1480860737902743686"; const C = { reset: "\x1b[0m", red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m", blue: "\x1b[34m", cyan: "\x1b[36m", bold: "\x1b[1m", }; const c = (t, col) => `${C[col] || ""}${t}${C.reset}`; const TOKEN = fs.readFileSync(TOKEN_FILE, "utf8").trim().split(/\s/)[0]; async function discord(method, path_, body) { const r = await fetch(`https://discord.com/api/v10${path_}`, { method, headers: { Authorization: `Bot ${TOKEN}`, "Content-Type": "application/json", }, body: body !== undefined ? JSON.stringify(body) : undefined, }); const text = await r.text(); let json = null; try { json = JSON.parse(text); } catch { json = { raw: text }; } return { ok: r.ok, status: r.status, json }; } async function sendMessage(channelId, content) { const r = await discord("POST", `/channels/${channelId}/messages`, { content }); if (!r.ok) throw new Error(`send failed ${r.status}: ${JSON.stringify(r.json)}`); return r.json; } async function getMessages(channelId, limit = 10) { const r = await discord("GET", `/channels/${channelId}/messages?limit=${limit}`); if (!r.ok) throw new Error(`fetch messages failed ${r.status}`); return r.json; // newest first } async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } // Wait for agent responses: poll until we see `expectedCount` new messages from bots async function waitForBotMessages(channelId, afterMsgId, expectedCount = 1, timeoutMs = 15000) { const start = Date.now(); while (Date.now() - start < timeoutMs) { await sleep(1500); const msgs = await getMessages(channelId, 20); const newBotMsgs = msgs.filter(m => BigInt(m.id) > BigInt(afterMsgId) && m.author?.bot === true && m.author?.id !== "1481189346097758298" // exclude our proxy bot ); if (newBotMsgs.length >= expectedCount) return newBotMsgs; } return []; } function printMsg(m) { const who = `${m.author?.username}(${m.author?.id})`; const preview = (m.content || "").slice(0, 120).replace(/\n/g, "\\n"); console.log(` ${c(who, "cyan")}: ${preview}`); } // ───────────────────────────────────────────────────────── // Test helpers // ───────────────────────────────────────────────────────── let passed = 0, failed = 0; function check(label, cond, detail = "") { if (cond) { console.log(` ${c("✓", "green")} ${label}`); passed++; } else { console.log(` ${c("✗", "red")} ${label}${detail ? ` — ${detail}` : ""}`); failed++; } } // ───────────────────────────────────────────────────────── // Main tests // ───────────────────────────────────────────────────────── async function main() { // Resolve channel let channelId = process.argv[2]; if (!channelId) { // Create a fresh test channel console.log(c("\n[setup] Creating private test channel...", "blue")); const meR = await discord("GET", "/users/@me"); if (!meR.ok) { console.error("Cannot auth:", meR.json); process.exit(1); } console.log(` proxy bot: ${meR.json.username} (${meR.json.id})`); // Get guild roles to find @everyone const guildR = await discord("GET", `/guilds/${GUILD_ID}`); const guildEveryoneId = guildR.json?.id || GUILD_ID; // Get guild members to find agent bots const membersR = await discord("GET", `/guilds/${GUILD_ID}/members?limit=50`); const bots = (membersR.json || []) .filter(m => m.user?.bot && m.user?.id !== meR.json.id) .map(m => m.user); console.log(` agent bots in guild: ${bots.map(b => `${b.username}(${b.id})`).join(", ")}`); const allowedUserIds = [meR.json.id, ...bots.map(b => b.id)]; const overwrites = [ { id: guildEveryoneId, type: 0, allow: "0", deny: "1024" }, ...allowedUserIds.map(id => ({ id, type: 1, allow: "1024", deny: "0" })), ]; const chR = await discord("POST", `/guilds/${GUILD_ID}/channels`, { name: `dirigent-test-${Date.now().toString(36)}`, type: 0, permission_overwrites: overwrites, }); if (!chR.ok) { console.error("Cannot create channel:", chR.json); process.exit(1); } channelId = chR.json.id; console.log(` created channel: #${chR.json.name} (${channelId})`); } else { console.log(c(`\n[setup] Using channel ${channelId}`, "blue")); } await sleep(1000); // ───────────────────────────────────────────────────────── // Test 1: Human sends message → agents should respond with end-symbol // ───────────────────────────────────────────────────────── console.log(c("\n[Test 1] Human message → agent response must end with 🔚", "bold")); const msg1 = await sendMessage(channelId, "Hello from human proxy! Please introduce yourself briefly. 🔚"); console.log(` sent: "${msg1.content}"`); console.log(" waiting up to 20s for bot responses..."); const botMsgs1 = await waitForBotMessages(channelId, msg1.id, 1, 20000); if (botMsgs1.length === 0) { check("Agent responded", false, "no bot messages received within 20s"); } else { for (const m of botMsgs1) printMsg(m); check( "Agent response ends with 🔚", botMsgs1.some(m => m.content?.trim().endsWith("🔚")), `got: ${botMsgs1.map(m => m.content?.slice(-10)).join(" | ")}` ); } // ───────────────────────────────────────────────────────── // Test 2: Turn order — only one agent per round // After first agent replies, second agent should be next // ───────────────────────────────────────────────────────── console.log(c("\n[Test 2] Turn order — check /dirigent turn-status", "bold")); console.log(" (Observational — check Discord channel for /dirigent turn-status output)"); console.log(c(" → Manually run /dirigent turn-status in the test channel to verify", "yellow")); // ───────────────────────────────────────────────────────── // Test 3: Bot message without end-symbol → no-reply gate // We send a message that looks like a bot (not in humanList) — observe logs // ───────────────────────────────────────────────────────── console.log(c("\n[Test 3] Second round — agent should reply after human follow-up", "bold")); await sleep(3000); const msg3 = await sendMessage(channelId, "What is 2+2? Answer briefly. 🔚"); console.log(` sent: "${msg3.content}"`); const botMsgs3 = await waitForBotMessages(channelId, msg3.id, 1, 20000); if (botMsgs3.length === 0) { check("Agent responded to follow-up", false, "no response within 20s"); } else { for (const m of botMsgs3) printMsg(m); check( "Follow-up response ends with 🔚", botMsgs3.some(m => m.content?.trim().endsWith("🔚")), ); } // ───────────────────────────────────────────────────────── // Test 4: NO_REPLY behavior — ask something irrelevant to trigger NO_REPLY // ───────────────────────────────────────────────────────── console.log(c("\n[Test 4] NO_REPLY — agents with nothing to say should be silent", "bold")); console.log(" (This is hard to assert automatically — check gateway logs for NO_REPLY routing)"); console.log(c(" → Watch `openclaw logs` for 'dirigent: before_model_resolve blocking out-of-turn'", "yellow")); // ───────────────────────────────────────────────────────── // Summary // ───────────────────────────────────────────────────────── console.log(c(`\n─────────────────────────────────────────────`, "blue")); console.log(`Results: ${c(String(passed), "green")} passed, ${c(String(failed), "red")} failed`); console.log(`Channel: https://discord.com/channels/${GUILD_ID}/${channelId}`); console.log(c("─────────────────────────────────────────────\n", "blue")); } main().catch(e => { console.error(e); process.exit(1); });