212 lines
10 KiB
JavaScript
212 lines
10 KiB
JavaScript
#!/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); });
|