137 lines
5.4 KiB
Python
137 lines
5.4 KiB
Python
#!/usr/bin/env python
|
|
"""selftest for errorlog-dream: run the analyzer against a synthetic errorlog
|
|
in a temp dir and assert each detector fires, then assert --apply-archive
|
|
moves exactly the old entries and leaves the marker, recent entries, and
|
|
unparsed legacy blocks intact."""
|
|
|
|
import io
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import errorlog_dream as ed
|
|
|
|
|
|
def build_fixture(root, today):
|
|
d = lambda n: (today - timedelta(days=n)).strftime("%Y-%m-%d")
|
|
old1 = d(95)
|
|
old2 = d(65)
|
|
recent = d(3)
|
|
lines = [
|
|
"# Error Log",
|
|
"",
|
|
ed.MARKER,
|
|
"",
|
|
# noise cluster: same machine+skill+shape on 3 distinct days
|
|
"%s | BOX-A | widget/api | HTTP 500 on id 12345 [ctx: cmd=list]" % d(1),
|
|
"",
|
|
"%s | BOX-A | widget/api | HTTP 500 on id 99881 [ctx: cmd=list] (x3)" % d(2),
|
|
"",
|
|
"%s | BOX-A | widget/api | HTTP 500 on id 40404 [ctx: cmd=list]" % recent,
|
|
"",
|
|
# repeat ref (2x) citing an existing memory
|
|
"%s | BOX-B | bash/env | [friction] used /tmp again [ctx: ref=fix_tmp_rule]" % d(4),
|
|
"",
|
|
"%s | BOX-B | bash/env | [friction] used /tmp AGAIN again [ctx: ref=fix_tmp_rule]" % recent,
|
|
"",
|
|
# machine-name case drift
|
|
"%s | box-b | coord | HTTP 0 talking to coord" % d(5),
|
|
"",
|
|
# resolved entry
|
|
"%s | BOX-A | sync/submodules | checkout aborted. [RESOLVED %s] fixed in sync.sh" % (d(6), d(5)),
|
|
"",
|
|
# correction
|
|
"%s | BOX-A | client/foo | [correction] assumed X; correct is Y" % d(7),
|
|
"",
|
|
# old entries -> archive candidates (two months)
|
|
"%s | BOX-A | oldskill | ancient failure one" % old1,
|
|
"",
|
|
"%s | BOX-B | oldskill | ancient failure two" % old2,
|
|
"",
|
|
# legacy multi-line unparsed block (no pipes on first line)
|
|
"Some legacy hand-written note",
|
|
"spanning two lines with no format.",
|
|
"",
|
|
]
|
|
log = os.path.join(root, "errorlog.md")
|
|
with io.open(log, "w", encoding="utf-8", newline="\n") as fh:
|
|
fh.write("\n".join(lines))
|
|
mem = os.path.join(root, ".claude", "memory")
|
|
os.makedirs(mem)
|
|
with io.open(os.path.join(mem, "fix_tmp_rule.md"), "w", encoding="utf-8") as fh:
|
|
fh.write("---\nname: fix_tmp_rule\n---\nrule body\n")
|
|
return log
|
|
|
|
|
|
def main():
|
|
tmp = tempfile.mkdtemp(prefix="eldream-test-")
|
|
failures = []
|
|
ok = lambda cond, name: failures.append(name) if not cond else None
|
|
try:
|
|
today = datetime.now(timezone.utc)
|
|
log = build_fixture(tmp, today)
|
|
header, entries, unparsed = ed.parse_log(log)
|
|
|
|
ok(len(entries) == 10, "parse: 10 entries (got %d)" % len(entries))
|
|
ok(len(unparsed) == 1, "parse: 1 unparsed block")
|
|
ok(any(e.count == 3 for e in entries), "parse: (x3) counter read")
|
|
|
|
r = ed.analyze(entries, unparsed, tmp, 60, today)
|
|
ok(r["weighted"] == 12, "weighted count incl x3 (got %d)" % r["weighted"])
|
|
ok(r["by_type"].get("friction") == 2 and r["by_type"].get("correction") == 1,
|
|
"type tally")
|
|
ok(any(ref == "fix_tmp_rule" and n == 2 and exists
|
|
for ref, n, exists, _ in r["repeat_refs"]),
|
|
"repeat-ref detector (existing memory)")
|
|
ok(any(skill == "widget/api" and w == 5 for _, skill, _, w, _ in r["noise"]),
|
|
"noise-cluster detector")
|
|
ok(len(r["resolved"]) == 1, "resolved detector")
|
|
ok("box-b" in r["machine_drift"], "machine-drift detector")
|
|
ok(len(r["archive"]) == 2, "archive candidates (got %d)" % len(r["archive"]))
|
|
|
|
report = ed.render(r, 60)
|
|
for token in ("STRENGTHEN?", "SUPPRESS?", "ARCHIVE?", "MACHINE-NAME DRIFT"):
|
|
ok(token in report, "report contains %s" % token)
|
|
|
|
# --apply-archive: moves the 2 old entries, keeps everything else
|
|
rc = ed.main(["--log", log, "--root", tmp, "--no-file", "--apply-archive"])
|
|
ok(rc == 0, "apply-archive exit 0")
|
|
h2, e2, u2 = ed.parse_log(log)
|
|
ok(len(e2) == 8, "post-archive: 8 entries kept (got %d)" % len(e2))
|
|
ok(len(u2) == 1, "post-archive: unparsed block untouched")
|
|
with io.open(log, encoding="utf-8") as fh:
|
|
body = fh.read()
|
|
ok(ed.MARKER in body, "post-archive: marker intact")
|
|
ok("ancient failure" not in body, "post-archive: old entries removed")
|
|
arch = os.path.join(tmp, "errorlog-archive")
|
|
months = sorted(f for f in os.listdir(arch) if f.endswith(".md"))
|
|
ok(len(months) == 2, "archive split by month (got %s)" % months)
|
|
joined = ""
|
|
for f in months:
|
|
with io.open(os.path.join(arch, f), encoding="utf-8") as fh:
|
|
joined += fh.read()
|
|
ok("ancient failure one" in joined and "ancient failure two" in joined,
|
|
"archived content present")
|
|
|
|
# idempotent: second run archives nothing
|
|
rc = ed.main(["--log", log, "--root", tmp, "--no-file", "--apply-archive"])
|
|
_, e3, _ = ed.parse_log(log)
|
|
ok(rc == 0 and len(e3) == 8, "second apply-archive is a no-op")
|
|
finally:
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
|
|
if failures:
|
|
print("[ERROR] selftest FAILED:")
|
|
for f in failures:
|
|
print(" - " + f)
|
|
return 1
|
|
print("[OK] errorlog-dream selftest: all assertions passed")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|