diff --git a/docs/rule-cases.json b/docs/rule-cases.json new file mode 100644 index 0000000..500969f --- /dev/null +++ b/docs/rule-cases.json @@ -0,0 +1,54 @@ +{ + "config": { + "enabled": true, + "discordOnly": true, + "bypassUserIds": ["561921120408698910"], + "endSymbols": ["。", "!", "?", ".", "!", "?"] + }, + "cases": [ + { + "name": "non-discord skips gate", + "channel": "telegram", + "senderId": "u1", + "content": "hello", + "expect": { + "shouldUseNoReply": false, + "reason": "non_discord", + "injectEndMarker": false + } + }, + { + "name": "bypass sender injects end marker", + "channel": "discord", + "senderId": "561921120408698910", + "content": "hello", + "expect": { + "shouldUseNoReply": false, + "reason": "bypass_sender", + "injectEndMarker": true + } + }, + { + "name": "ending punctuation injects end marker", + "channel": "discord", + "senderId": "u2", + "content": "你好!", + "expect": { + "shouldUseNoReply": false, + "reason": "end_symbol:!", + "injectEndMarker": true + } + }, + { + "name": "no ending punctuation triggers no-reply override", + "channel": "discord", + "senderId": "u2", + "content": "继续", + "expect": { + "shouldUseNoReply": true, + "reason": "rule_match_no_end_symbol", + "injectEndMarker": false + } + } + ] +} diff --git a/plugin/package.json b/plugin/package.json index 9095670..35b00ba 100644 --- a/plugin/package.json +++ b/plugin/package.json @@ -5,6 +5,7 @@ "type": "module", "description": "WhisperGate OpenClaw plugin", "scripts": { - "check": "node ../scripts/check-plugin-files.mjs" + "check": "node ../scripts/check-plugin-files.mjs", + "check:rules": "node ../scripts/validate-rules.mjs" } } diff --git a/scripts/validate-rules.mjs b/scripts/validate-rules.mjs new file mode 100644 index 0000000..4c74bc6 --- /dev/null +++ b/scripts/validate-rules.mjs @@ -0,0 +1,64 @@ +import fs from "node:fs"; +import path from "node:path"; + +function getLastChar(input) { + const t = (input || "").trim(); + return t.length ? t[t.length - 1] : ""; +} + +function evaluateDecision({ config, channel, senderId, content }) { + if (config.enabled === false) { + return { shouldUseNoReply: false, reason: "disabled" }; + } + + const ch = (channel || "").toLowerCase(); + if (config.discordOnly !== false && ch !== "discord") { + return { shouldUseNoReply: false, reason: "non_discord" }; + } + + if (senderId && (config.bypassUserIds || []).includes(senderId)) { + return { shouldUseNoReply: false, reason: "bypass_sender" }; + } + + const last = getLastChar(content || ""); + if (last && (config.endSymbols || []).includes(last)) { + return { shouldUseNoReply: false, reason: `end_symbol:${last}` }; + } + + return { shouldUseNoReply: true, reason: "rule_match_no_end_symbol" }; +} + +function shouldInjectEndMarker(reason) { + return reason === "bypass_sender" || String(reason).startsWith("end_symbol:"); +} + +const fixturePath = path.join(process.cwd(), "docs", "rule-cases.json"); +const payload = JSON.parse(fs.readFileSync(fixturePath, "utf8")); + +let ok = true; +for (const c of payload.cases || []) { + const d = evaluateDecision({ + config: payload.config, + channel: c.channel, + senderId: c.senderId, + content: c.content, + }); + + const inject = shouldInjectEndMarker(d.reason); + const pass = + d.shouldUseNoReply === c.expect.shouldUseNoReply && + d.reason === c.expect.reason && + inject === c.expect.injectEndMarker; + + if (!pass) { + ok = false; + console.error(`FAIL ${c.name}`); + console.error(` got: ${JSON.stringify({ ...d, injectEndMarker: inject })}`); + console.error(` expect: ${JSON.stringify(c.expect)}`); + } else { + console.log(`OK ${c.name}`); + } +} + +if (!ok) process.exit(1); +console.log("all rule cases passed");