const assert = require("node:assert/strict"); const fs = require("node:fs"); const vm = require("node:vm"); const { test } = require("node:test"); const { loadBackground } = require("./support/characterization.cjs"); function setup() { let now = 100000, hours = 24, user = "a@example.invalid"; const factory = new vm.Script(fs.readFileSync("src/features/preferences/request-cache.js", "utf8") + "\ncreateFormRequestCache;").runInNewContext({ Date: { now: () => now } }); const cache = factory({ getHours: () => hours, getUserId: () => user }); return { cache, advance: value => { now += value; }, hours: value => { hours = value; }, user: value => { user = value; } }; } const message = (name, fields = {}) => ({ type: `opportunitiesExtension.${name}`, ...fields }); test("all four form query families reuse successes with parameter-specific keys and live duration", async () => { const h = setup(); let calls = 0; const fetch = async request => { calls++; assert.equal(request.cacheMaxAgeMs, 24 * 3600000); return { ok: true, items: [{ name: "Original" }], debugRequests: [{ name: "network" }] }; }; for (const name of ["requestWeekDetails", "requestActiveServiceRequests", "requestServiceRequestExpand", "requestLookupTaskType", "requestActionTeamMembers", "requestActionFindLookup", "requestSrLookupPrimaryPillar", "requestSrLookupLanguage", "requestSrLookupTechPlatform", "requestSrLookupOpportunityBusiness", "requestLookupTypePlatform", "requestInternalSrServices"]) { const msg = message(name, { id: 1 }); const result = await h.cache.send(msg, fetch); result.items[0].name = "Edited"; const cached = await h.cache.send(msg, fetch); assert.equal(cached.fromCache, true); assert.equal(cached.items[0].name, "Original"); assert.equal(cached.debugRequests.length, 0); await h.cache.send(message(name, { id: 2 }), fetch); } assert.equal(calls, 24); h.advance(2 * 3600000); h.hours(1); await h.cache.send(message("requestWeekDetails", { id: 1 }), async request => { calls++; assert.equal(request.cacheMaxAgeMs, 3600000); return { ok: true }; }); assert.equal(calls, 25); }); test("failed results are never cached and identity changes discard previous responses", async () => { const h = setup(); let calls = 0; const send = async () => { calls++; return { ok: calls > 1 }; }; const msg = message("requestLookupTaskType"); await h.cache.send(msg, send); await h.cache.send(msg, send); await h.cache.send(msg, send); assert.equal(calls, 2); h.user("b@example.invalid"); await h.cache.send(msg, send); assert.equal(calls, 3); h.user(""); await h.cache.send(msg, send); await h.cache.send(msg, send); assert.equal(calls, 5); }); test("writes and token requests always execute; successful saves and Refresh all invalidate queries", async () => { for (const name of ["requestMgmtAction", "requestPostServiceRequest", "requestPostInternalServiceRequest", "invalidateProductsCache"]) { const h = setup(); let queries = 0, writes = 0; const query = message("requestActionFindLookup"); const queryTransport = async () => { queries++; return { ok: true }; }; await h.cache.send(query, queryTransport); for (let i = 0; i < 2; i++) await h.cache.send(message(name, { phase: "confirm" }), async () => { writes++; return { ok: true }; }); await h.cache.send(query, queryTransport); assert.equal(writes, 2); assert.equal(queries, 2); } const h = setup(); let calls = 0; for (let i = 0; i < 2; i++) await h.cache.send(message("requestSrTokenCatalog"), async () => { calls++; return { ok: true }; }); assert.equal(calls, 2); }); test("concurrent queries share work but old responses cannot repopulate an invalidated cache", async () => { const h = setup(); let resolve, calls = 0; const msg = message("requestLookupTypePlatform"); const transport = () => { calls++; return new Promise(done => { resolve = done; }); }; const first = h.cache.send(msg, transport), second = h.cache.send(msg, transport); assert.equal(calls, 1); h.cache.invalidate(); resolve({ ok: true, items: [] }); await Promise.all([first, second]); await h.cache.send(msg, async () => { calls++; return { ok: true }; }); assert.equal(calls, 2); }); test("resource user respects duration and Refresh all invalidation", async () => { let calls = 0; const app = loadBackground({ fetch: async () => { calls++; return new Response(JSON.stringify({ resourcePartyId: 123 })); } }); const msg = message("requestResourceUser", { emailAddress: "a@example.invalid", cacheMaxAgeMs: 3600000 }); await app.dispatch(msg); await app.dispatch(msg); assert.equal(calls, 1); app.advance(3600000); await app.dispatch(msg); assert.equal(calls, 2); await app.dispatch(message("invalidateProductsCache")); await app.dispatch(msg); assert.equal(calls, 3); }); test("Preferences displays generic cache duration while preserving the saved preference key", () => { const source = fs.readFileSync("src/features/preferences/view.js", "utf8"); assert.match(source, /cacheLabel.textContent = "Cache duration"/); assert.doesNotMatch(source, /Product cache duration/); assert.match(source, /cacheSelect.name = "opptyProductsCacheHours"/); });