mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-11 02:44:57 +08:00
187 lines
7.2 KiB
JavaScript
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")));
|
|
});
|