Files
op-packages/luci-theme-aurora/.dev/tests/log-patch.test.js
T

187 lines
7.2 KiB
JavaScript

import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { existsSync, readFileSync, statSync } from "node:fs";
import { resolve } from "node:path";
import { test } from "node:test";
/* The patch ships as a plain deferred script, not a module. Evaluate it with
an injected `module` to reach its parsing exports; `document` stays
undefined so the DOM bootstrap is skipped. */
const source = await readFile(
new URL("../src/resource/patches/admin-status-logs.js", import.meta.url),
"utf8",
);
const mod = { exports: {} };
new Function("module", "exports", "document", "window", source)(
mod,
mod.exports,
undefined,
undefined,
);
const { parseSyslogLine, parseDmesgLine, evaluate, PARSE_GATE } = mod.exports;
test("classic logread format (23.05 syslog, 24.10 syslog-wrapper) parses", () => {
const r = parseSyslogLine(
"Sat Jul 26 03:14:15 2026 daemon.info dnsmasq-dhcp[1]: DHCPACK(br-lan) 192.168.1.140",
);
assert.deepEqual(
{ time: r.time, fac: r.fac, sev: r.sev, sevTok: r.sevTok, tag: r.tag },
{
time: "Sat Jul 26 03:14:15 2026",
fac: "daemon",
sev: "info",
sevTok: "daemon.info",
tag: "dnsmasq-dhcp[1]",
},
);
assert.equal(r.msg, "DHCPACK(br-lan) 192.168.1.140");
// ctime() pads single-digit days with a space — kept verbatim.
const padded = parseSyslogLine(
"Sun Jul 6 09:05:07 2026 daemon.warn odhcpd[2029]: No default route present",
);
assert.equal(padded.time, "Sun Jul 6 09:05:07 2026");
// Kernel lines routed through logread; hostapd logs without a pid.
assert.equal(
parseSyslogLine(
"Sat Jul 26 03:14:16 2026 kern.info kernel: [73412.882110] br-lan: port 3(lan3) entered forwarding state",
).tag,
"kernel",
);
assert.equal(
parseSyslogLine(
"Sat Jul 26 03:14:17 2026 daemon.notice hostapd: wlan1: AP-STA-CONNECTED 3c:22:fb:8a:12:9d",
).tag,
"hostapd",
);
});
test("master RPC bracket format parses across browser locales", () => {
// The datestr renders verbatim, brackets and timezone included — the
// stamp is UTC-formatted upstream and hiding the zone read as local time.
const en = parseSyslogLine(
"[Jul 24, 2026, 10:59:50 PM UTC] daemon.info: dnsmasq-dhcp[1]: DHCPREQUEST(br-lan) 192.168.1.140",
);
assert.equal(en.time, "[Jul 24, 2026, 10:59:50 PM UTC]");
assert.equal(en.sevTok, "daemon.info:");
assert.equal(
parseSyslogLine(
"[Jul 24, 2026, 10:59:50 PM Coordinated Universal Time] daemon.warn: odhcpd[2029]: No default route present",
).time,
"[Jul 24, 2026, 10:59:50 PM Coordinated Universal Time]",
);
// Non-en datestrs parse fine and display untouched — never guessed at.
const zh = parseSyslogLine(
"[2026年7月24日 GMT 22:59:50] daemon.err: odhcpd[2029]: Failed to send RA",
);
assert.equal(zh.sev, "err");
assert.equal(zh.time, "[2026年7月24日 GMT 22:59:50]");
});
test("rendered tokens rejoin into the source line byte for byte", () => {
const lines = [
"[Jul 24, 2026, 10:59:50 PM UTC] daemon.info: dnsmasq-dhcp[1]: DHCPREQUEST(br-lan) 192.168.1.140",
"[Jul 25, 2026, 4:30:55 PM UTC] kern.info: [ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x51af8014]",
"Sun Jul 6 09:05:07 2026 daemon.warn odhcpd[2029]: No default route present",
"Sat Jul 26 03:14:16 2026 kern.info kernel: [73412.882110] br-lan: port 3(lan3) entered forwarding state",
];
for (const line of lines) {
const p = parseSyslogLine(line);
const parts = [p.time, p.sevTok];
if (p.tag) parts.push(p.tag + ":");
parts.push(p.msg);
assert.equal(parts.join(" "), line, line);
}
const dm = "[ 12.345678] br-lan: port 3(lan3) entered forwarding state";
const d = parseDmesgLine(dm);
assert.equal([d.time, d.msg].join(" "), dm);
});
test("severity comes from the field, never from message content", () => {
const r = parseSyslogLine(
"[Jul 24, 2026, 11:00:00 PM UTC] daemon.info: logger: user said daemon.err: fake",
);
assert.equal(r.sev, "info");
assert.equal(r.msg, "user said daemon.err: fake");
});
test("unknown facility/severity and foreign formats are rejected", () => {
assert.equal(
parseSyslogLine("[Jul 24, 2026, 11:00:00 PM UTC] bogus.info: x: y"),
null,
);
assert.equal(
parseSyslogLine("[Jul 24, 2026, 11:00:00 PM UTC] daemon.bogus: x: y"),
null,
);
// syslog-ng's classic template has no facility.severity field at all.
assert.equal(
parseSyslogLine("Jul 26 10:00:00 OpenWrt dnsmasq[123]: query answered"),
null,
);
assert.equal(parseSyslogLine(""), null);
});
test("dmesg lines: uptime stamp only, message verbatim, no severity", () => {
// Kernel lines have no uniform tag grammar (upstream renders them as plain
// text for the same reason) — a partial split styled only some lines and
// ate only their colons, so no tag is ever extracted and the message keeps
// its punctuation untouched.
const r = parseDmesgLine(
"[ 12.345678] br-lan: port 3(lan3) entered forwarding state",
);
assert.equal(r.time, "[ 12.345678]");
assert.equal(r.tag, "");
assert.equal(r.sev, "");
assert.equal(r.msg, "br-lan: port 3(lan3) entered forwarding state");
const pci = parseDmesgLine(
"[75821.004518] mt7921e 0000:01:00.0: Message 00000010 timeout",
);
assert.equal(pci.tag, "");
assert.equal(pci.msg, "mt7921e 0000:01:00.0: Message 00000010 timeout");
assert.equal(parseDmesgLine("no bracket prefix here"), null);
});
test("parse gate: mixed content falls back, empty value stays calm", () => {
const good = "Sat Jul 26 03:14:15 2026 daemon.info dnsmasq[1]: ok";
const bad = "Jul 26 10:00:00 OpenWrt dnsmasq[123]: syslog-ng style";
const healthy = evaluate(
[good, good, good, good, bad].join("\n"),
parseSyslogLine,
);
assert.ok(healthy.ratio >= PARSE_GATE, "4/5 parsed must stay enhanced");
const foreign = evaluate([bad, bad, bad].join("\n"), parseSyslogLine);
assert.ok(foreign.ratio < PARSE_GATE, "foreign log must fall back");
const empty = evaluate("", parseSyslogLine);
assert.equal(empty.rows.length, 0);
assert.equal(empty.ratio, 1, "empty log must not trip the gate");
});
test("built log payloads land in their homes", () => {
const out = resolve(import.meta.dirname, "../../htdocs/luci-static/aurora");
// The JS ships on-demand under one name — every supported release
// (23.05/24.10/master) mounts both log pages under admin/status/logs/*.
const js = resolve(out, "patches/admin-status-logs.js");
assert.ok(existsSync(js), `${js} missing — run pnpm build first`);
assert.ok(statSync(js).size <= 6_000, `${js} exceeds 6 KB`);
// The CSS is core-page styling and lives in the main bundle, where the
// Tailwind token pipeline (radius-base chain, app shadows) applies — a
// stray patches/ copy would mean the component migration regressed.
const main = readFileSync(resolve(out, "main.css"), "utf8");
assert.ok(main.includes(".syslog-view"), "main.css lost the log viewer");
assert.ok(
main.includes("calc(var(--radius-base)"),
"radius utilities no longer resolve through the reactive base chain",
);
assert.ok(!existsSync(resolve(out, "patches/admin-status-logs.css")));
assert.ok(!existsSync(resolve(out, "patches/_log-shared.css")));
});