Lumi/scripts/verify-command-preview-confirmations.js
2026-07-18 02:56:03 +02:00

299 lines
12 KiB
JavaScript

const assert = require("assert");
const fs = require("fs");
const path = require("path");
const {
generateCommandPreview,
findDynamicSegments,
normalizePreviewText,
previewParts
} = require("../src/services/command-preview");
const { runAdvancedCommand } = require("../src/services/commands");
const {
conditionMatches,
selectRandomReply,
validateRandomConfig
} = require("../src/services/command-random");
const {
DELAY_MS,
issueConfirmation,
consumeConfirmation,
isDestructivePath
} = require("../src/services/destructive-confirm");
const { db, migrate } = require("../src/services/db");
async function run() {
migrate();
const columns = db.prepare("PRAGMA table_info(custom_commands)").all().map((row) => row.name);
for (const column of [
"preview_text",
"preview_status",
"preview_error",
"preview_generated_at",
"preview_dynamic_segments",
"description",
"random_replies_json",
"rng_enabled",
"rng_min",
"rng_max"
]) {
assert(columns.includes(column), `Missing custom command preview column: ${column}`);
}
db.exec("SAVEPOINT verify_command_description");
try {
const now = Date.now();
const trigger = `verify-description-${now}`;
const inserted = db.prepare(
"INSERT INTO custom_commands (trigger, description, response, created_at, updated_at) VALUES (?, ?, ?, ?, ?)"
).run(trigger, "Initial description", "Test reply", now, now);
db.prepare("UPDATE custom_commands SET description = ? WHERE id = ?")
.run("Updated description", inserted.lastInsertRowid);
assert.equal(
db.prepare("SELECT description FROM custom_commands WHERE id = ?")
.get(inserted.lastInsertRowid).description,
"Updated description"
);
} finally {
db.exec("ROLLBACK TO verify_command_description; RELEASE verify_command_description");
}
const preview = await generateCommandPreview({
language: "js",
code: `
function run(ctx) {
ctx.reply("Hello " + ctx.user.username);
return "Balance 1234, id " + ctx.user.id + ", date " + new Date().toISOString();
}
`
});
assert.equal(preview.preview_status, "ready");
assert.match(preview.preview_text, /Hello some_user/);
assert.equal(preview.preview_text.includes("Some User"), false);
const segments = JSON.parse(preview.preview_dynamic_segments);
for (const type of ["username", "number", "id", "date"]) {
assert(segments.some((segment) => segment.type === type), `Missing ${type} dynamic segment`);
}
assert(previewParts(preview.preview_text, segments).some((part) => part.dynamic));
assert.equal(normalizePreviewText("Some User and some-user"), "some_user and some_user");
assert(findDynamicSegments("some_user 42 user_123 2026-01-02T12:34:56.000Z").length, 4);
const punctuationPreview = "Fun fact for some_user:\nExample";
assert.equal(
previewParts(punctuationPreview, findDynamicSegments(punctuationPreview)).map((part) => part.text).join(""),
punctuationPreview
);
const snippetPreview = await generateCommandPreview({
language: "js",
code: 'return "Snippet for " + ctx.user.username;'
});
assert.equal(snippetPreview.preview_status, "ready");
assert.equal(snippetPreview.preview_text, "Snippet for some_user");
const exportedPreview = await generateCommandPreview({
language: "js",
code: 'module.exports = (ctx) => "Export for " + ctx.user.username;'
});
assert.equal(exportedPreview.preview_status, "ready");
assert.equal(exportedPreview.preview_text, "Export for some_user");
const safeBuiltInsPreview = await generateCommandPreview({
language: "js",
code: `
const facts = ["using a process called respiration"];
const selected = facts[Math.floor(Math.random() * facts.length)];
const url = new URL("https://example.com/facts?count=1");
return JSON.stringify({ selected, host: url.hostname, count: url.searchParams.get("count") });
`
});
assert.equal(safeBuiltInsPreview.preview_status, "ready");
assert.deepEqual(JSON.parse(safeBuiltInsPreview.preview_text), {
selected: "using a process called respiration",
host: "example.com",
count: "1"
});
for (const code of [
'return Math.constructor.constructor("return pro" + "cess")().cwd();',
'return ctx.constructor.constructor("return pro" + "cess")().cwd();',
'return URL.constructor.constructor("return pro" + "cess")().cwd();'
]) {
const constructorEscape = await generateCommandPreview({ language: "js", code });
assert.equal(constructorEscape.preview_status, "unavailable");
assert.match(constructorEscape.preview_error, /code generation from strings disallowed/i);
}
const pythonSnippetPreview = await generateCommandPreview({
language: "python",
code: 'return "Python snippet"'
});
assert.equal(pythonSnippetPreview.preview_status, "ready");
assert.equal(pythonSnippetPreview.preview_text, "Python snippet");
assert.equal(await runAdvancedCommand({ language: "js", code: 'return "wrapped";' }, {}), "wrapped");
const randomConfig = {
replies: [
{ text: "Low {{core.command.rng}}", weight: 1, condition: "<20" },
{ text: "High", weight: 2, condition: ">=20" }
],
rngEnabled: true,
rngMin: 1,
rngMax: 100
};
const rolls = [0.1, 0.9];
const selectedRandom = selectRandomReply(randomConfig, () => rolls.shift());
assert.equal(selectedRandom.rng, 11);
assert.equal(selectedRandom.reply.text, "Low {{core.command.rng}}");
assert.equal(conditionMatches(">=10", 10), true);
assert.equal(conditionMatches("<20", 20), false);
assert.equal(validateRandomConfig({ replies: [{ text: "Hi", weight: 1000 }], rngMin: 1, rngMax: 100 }).ok, false);
assert.equal(validateRandomConfig({ replies: [{ text: "Hi", weight: 1, condition: "maybe" }], rngEnabled: true }).ok, false);
const noSideEffects = await generateCommandPreview({
language: "js",
code: `
async function run(ctx) {
await ctx.economy.add(ctx.user.id, 999999);
await ctx.inventory.remove(ctx.user.id, "all");
await ctx.moderation.ban(ctx.user.id);
await ctx.files.write("outside.txt", "blocked");
ctx.db.prepare("DELETE FROM users").run();
return "Balance " + await ctx.economy.getBalance(ctx.user.id);
}
`
});
assert.equal(noSideEffects.preview_status, "ready");
assert.equal(noSideEffects.preview_text, "Balance 1234");
const readOnlyFetch = await generateCommandPreview({
language: "js",
code: `
async function run() {
const response = await fetch("data:application/json,%7B%22message%22%3A%22preview%22%7D");
const data = await response.json();
return data.message;
}
`
});
assert.equal(readOnlyFetch.preview_status, "ready");
assert.equal(readOnlyFetch.preview_text, "preview");
const blockedNetworkFetch = await generateCommandPreview({
language: "js",
code: `
async function run() {
await fetch("http://127.0.0.1:3000/admin/settings");
return "no";
}
`
});
assert.equal(blockedNetworkFetch.preview_status, "unavailable");
assert.match(blockedNetworkFetch.preview_error, /does not send external network requests/i);
const blockedWriteFetch = await generateCommandPreview({
language: "js",
code: `
async function run() {
await fetch("data:text/plain,no", { method: "POST", body: "write" });
return "no";
}
`
});
assert.equal(blockedWriteFetch.preview_status, "unavailable");
assert.match(blockedWriteFetch.preview_error, /read-only GET and HEAD/i);
const blocked = await generateCommandPreview({
language: "js",
code: "function run() { return process.cwd(); }"
});
assert.equal(blocked.preview_status, "unavailable");
assert.match(blocked.preview_error, /blocks filesystem, process/i);
const blockedImport = await generateCommandPreview({
language: "python",
code: "import os\ndef run(ctx):\n return os.getcwd()"
});
assert.equal(blockedImport.preview_status, "unavailable");
assert.match(blockedImport.preview_error, /blocks imports/i);
const timedOut = await generateCommandPreview({
language: "js",
code: "function run() { while (true) {} }"
});
assert.equal(timedOut.preview_status, "unavailable");
assert.match(timedOut.preview_error, /timed out/i);
const req = fakeRequest();
assert.equal(isDestructivePath("/admin/commands/1/delete"), true);
assert.equal(isDestructivePath("/admin/commands/1/update"), false);
const first = issueConfirmation(req, "/admin/commands/1/delete");
assert.equal(first.delay_seconds, DELAY_MS / 1000);
assert.equal(consumeConfirmation(req, "/admin/commands/1/delete", first.token).reason, "too_early");
const second = issueConfirmation(req, "/admin/commands/1/delete");
req.session.destructive_confirmations[second.token].not_before = Date.now() - 1;
assert.equal(consumeConfirmation(req, "/admin/pages/1/delete", second.token).reason, "action_mismatch");
const third = issueConfirmation(req, "/admin/commands/1/delete");
req.session.destructive_confirmations[third.token].not_before = Date.now() - 1;
assert.equal(consumeConfirmation(req, "/admin/commands/1/delete", third.token).valid, true);
assert.equal(consumeConfirmation(req, "/admin/commands/1/delete", third.token).valid, false);
const appScript = fs.readFileSync(path.join(__dirname, "..", "src", "web", "public", "app.js"), "utf8");
const layout = fs.readFileSync(path.join(__dirname, "..", "src", "web", "views", "partials", "layout-bottom.ejs"), "utf8");
const commandView = fs.readFileSync(path.join(__dirname, "..", "src", "web", "views", "admin-commands.ejs"), "utf8");
const publicCommandView = fs.readFileSync(path.join(__dirname, "..", "src", "web", "views", "commands.ejs"), "utf8");
const publicCommandContent = fs.readFileSync(path.join(__dirname, "..", "src", "web", "views", "partials", "command-list-content.ejs"), "utf8");
assert(appScript.includes("${confirmLabel(form, submitter)} in ${remaining}"));
assert(appScript.includes('button.disabled = remaining > 0'));
assert(appScript.includes('fetch("/api/destructive-confirmations"'));
assert(appScript.includes("destructiveToken: timedDestructiveToken"));
assert(appScript.includes("destructiveFetch(action"));
assert(appScript.includes('headers.set("X-Confirmation-Token", token)'));
assert(appScript.includes("event.preventDefault();"));
assert(appScript.includes("expiryTimer"));
assert(appScript.includes("resetDestructive"));
assert(layout.includes("data-destructive-modal"));
assert(layout.includes("data-destructive-confirm disabled"));
assert(commandView.includes("Preview unavailable"));
assert(commandView.includes("preview-dynamic"));
assert(commandView.includes('data-collapsed-label="Read more"'));
assert(commandView.includes('data-expanded-label="Read less"'));
assert.equal(commandView.includes("Full preview"), false);
assert.equal(commandView.includes("command-preview-full"), false);
assert(appScript.includes('[data-expand-text]'));
assert(commandView.includes(">Static<"));
assert(commandView.includes(">Random Reply<"));
assert(commandView.includes(">Dynamic<"));
assert(commandView.includes("random_weight"));
assert(commandView.includes("{{core.command.rng}}"));
assert(commandView.includes('name="description"'));
assert(publicCommandView.includes('data-command-content-toggle'));
assert(publicCommandView.includes('partials/command-list-content'));
assert(publicCommandContent.includes("command.preview.parts"));
assert(publicCommandContent.includes("data-command-content-description"));
assert.equal(publicCommandContent.includes("Refresh preview"), false);
assert(appScript.includes('row.style.display = expanded ? "table-row" : "none"'));
assert(appScript.includes("[data-command-list-content]"));
assert.equal(commandView.includes('"Advanced (" + command.language + ")"'), false);
console.log("Command preview and destructive confirmation verification passed.");
}
function fakeRequest() {
return {
session: {
user: { id: "admin-user", isAdmin: true }
},
body: {},
get() {
return null;
}
};
}
run().catch((error) => {
console.error(error);
process.exitCode = 1;
});