// Phase 5 tests — access tracking. // Run: node test/test_access_log.js import { promises as fs } from "node:fs"; import path from "node:path"; import { AccessLog } from "../src/access_log.js"; let passed = 0; let failed = 0; function assert(name, ok, extra = "") { if (ok) { passed++; console.log(`PASS ${name}`); } else { failed++; console.log(`FAIL ${name}${extra ? ` (${extra})` : ""}`); } } const TMP = `/tmp/access_log_test_${process.pid}.json`; async function fresh() { // wipe file try { await fs.unlink(TMP); } catch {} // simple: pass filePath directly to constructor return new AccessLog(TMP); } // ---- constructor ---- { const al = await fresh(); assert("new AccessLog: empty map", al.byUrn.size === 0); assert("new AccessLog: not loaded", al._loaded === false); assert("new AccessLog: schema_version=1", al.meta.schema_version === 1); } // ---- bump ---- { const al = await fresh(); const r1 = al.bump("urn:ump:a"); assert("bump new urn: returns object", r1 && r1.count === 1); assert("bump new urn: last_accessed_at is ISO", typeof r1.last_accessed_at === "string" && r1.last_accessed_at.endsWith("Z")); const r2 = al.bump("urn:ump:a"); assert("bump same urn: count++", r2.count === 2, `got ${r2.count}`); const r3 = al.bump("urn:ump:b"); assert("bump different urn: separate counter", r3.count === 1 && al.byUrn.size === 2); } { const al = await fresh(); const r = al.bump(null); assert("bump null: returns null", r === null); const r2 = al.bump(undefined); assert("bump undefined: returns null", r2 === null); const r3 = al.bump(""); assert("bump empty string: returns null", r3 === null); } // ---- bumpMany ---- { const al = await fresh(); const results = al.bumpMany(["urn:ump:a", "urn:ump:b", "urn:ump:c", null, ""]); assert("bumpMany: skips null/empty entries", Object.keys(results).length === 3); al.bumpMany(["urn:ump:a", "urn:ump:a"]); assert("bumpMany: cumulative counts", al.byUrn.get("urn:ump:a").count === 3, `got ${al.byUrn.get("urn:ump:a").count}`); } // ---- get ---- { const al = await fresh(); al.bump("urn:ump:x"); const got = al.get("urn:ump:x"); assert("get: returns copy", got && got.count === 1); // Mutating returned object shouldn't affect internal state got.count = 999; assert("get: returns shallow copy", al.byUrn.get("urn:ump:x").count === 1, `internal was mutated`); assert("get: null urn → null", al.get(null) === null); assert("get: unknown urn → null", al.get("urn:ump:never-bumped") === null); } // ---- snapshot ---- { const al = await fresh(); al.bump("urn:ump:a"); al.bump("urn:ump:b"); al.bump("urn:ump:b"); const snap = al.snapshot(); assert("snapshot: includes all URNs", Object.keys(snap).length === 2); assert("snapshot: counts preserved", snap["urn:ump:a"].count === 1 && snap["urn:ump:b"].count === 2); } // ---- load ---- { // Write a log file manually, then load const al1 = await fresh(); al1.bump("urn:ump:existing"); al1.bump("urn:ump:existing"); await al1.flush(); const al2 = new AccessLog(TMP); await al2.load(); assert("load: existing data loaded", al2.byUrn.has("urn:ump:existing") && al2.byUrn.get("urn:ump:existing").count === 2); } { // Load from non-existent file → no error, empty map const al = new AccessLog("/tmp/does-not-exist-" + Date.now() + ".json"); await al.load(); assert("load: missing file → empty (no error)", al.byUrn.size === 0); } // ---- flush + persistence roundtrip ---- { const al = await fresh(); al.bump("urn:ump:flush-test"); al.bump("urn:ump:flush-test"); al.bump("urn:ump:flush-test"); await al.flush(); // File exists const raw = await fs.readFile(TMP, "utf8"); const data = JSON.parse(raw); assert("flush: file written", data.by_urn && data.by_urn["urn:ump:flush-test"].count === 3); assert("flush: schema_version present", data.schema_version === 1); assert("flush: meta.total_writes incremented", data.meta.total_writes >= 1); // No .tmp left behind const tmpFiles = (await fs.readdir(path.dirname(TMP))).filter(f => f.includes("access_log_test") && f.endsWith(".tmp-" + process.pid)); assert("flush: no .tmp file left behind", tmpFiles.length === 0, `found ${tmpFiles.join(",")}`); } // ---- stats ---- { const al = await fresh(); al.bump("urn:ump:s1"); al.bump("urn:ump:s1"); al.bump("urn:ump:s2"); al.bump("urn:ump:s3"); const s = al.stats(); assert("stats: unique_urns=3", s.unique_urns === 3, `got ${s.unique_urns}`); assert("stats: total_accesses=4", s.total_accesses === 4, `got ${s.total_accesses}`); assert("stats: accessed_within_7d=3 (all just bumped)", s.accessed_within_7d === 3); assert("stats: file path set", s.file && s.file.length > 0); } console.log(`\n${passed}/${passed + failed} passed`); process.exit(failed > 0 ? 1 : 0);