const assert = require("node:assert/strict"); const { test } = require("node:test"); const { loadApplication } = require("./support/application.cjs"); const flush = () => new Promise(resolve => setImmediate(resolve)); function setup(family, saved = {}, principal = "user@example.invalid") { const local = { get: async key => key === null ? { ...saved } : ({ [key]: saved[key] }), set: async values => Object.assign(saved, JSON.parse(JSON.stringify(values))), remove: async keys => keys.forEach(key => delete saved[key]) }; const api = family === "firefox" ? { browser: { storage: { local } } } : { chrome: { runtime: {}, storage: { local: { get: (key, done) => local.get(key).then(done), set: (values, done) => local.set(values).then(done), remove: (keys, done) => local.remove(keys).then(done) } } } }; const document = { createElement: () => ({ append() {}, setAttribute() {}, addEventListener() {} }) }; document.createElementNS = (_, tag) => document.createElement(tag); const app = loadApplication("content", { ...api, document }); app.authSessionState.principal = principal; app.currentUser.allowsCrmActivities = () => true; app.preferencesState.opptyProductsCacheHours = 24; app.opportunitiesQueryState.periodRequestVersion = 1; app.activityProgress.update = () => {}; app.productsContentJobs.queueProductsTableRender = () => {}; app.debugContentLog.appendBackgroundRequestLog = () => {}; app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic-token" } }); const calls = []; app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => { calls.push("sr"); return [{ SrNumber: "SR1", ReportedHours_c: 4, OpportunityName_Id_c: { OptyNumber: "OP1" } }]; }; app.platformContentMessages.sendRuntimeMessage = async () => { calls.push("actions"); return { ok: true, actions: [{ owner: principal, name: "Test action" }], hasNext: false }; }; async function load(cachedAt, cacheOnly) { app.opportunityCrmServiceRequests.reset(); await app.opportunityCrmServiceRequests.load("synthetic-token", 1, { cachedAt, cacheOnly }); const products = { items: [{ RevnId: "123" }], cachedAt, fromCache: cacheOnly }; app.productsContentState.opptyProductsByNumber.set("OP1", products); app.opportunityWorkloadActions.loadProducts(products, true, { cachedAt, cacheOnly }); await flush(); } return { app, calls, load, saved, local }; } test("all cached opportunities load missing Actions despite filters, then reuse their completed cache", async () => { const h = setup("firefox"); const stamp = Date.now() - 1000; const app = h.app; app.opportunitiesFilterState.tableSearch = "hide every opportunity"; app.productsContentJobs.renderProductsProgress = () => {}; const products = Array.from({ length: 7 }, (_, index) => [String(index + 1), { items: [{ RevnId: String(index + 1) }], cachedAt: stamp, fromCache: true }]); app.productsContentState.opptyProductsByNumber = new Map(products); await app.activityCache.write("actions", "1", stamp, []); await app.activityCache.write("actions", "2", stamp - 1, []); const queried = []; app.platformContentMessages.sendRuntimeMessage = async message => { queried.push(message.revnId); return { ok: true, actions: message.revnId === "3" ? [{ owner: app.authSessionState.principal }] : [], hasNext: false }; }; async function cycle(id) { app.productsContentState.currentProductsRequestId = id; app.productActivities.begin(id, "synthetic-token"); await app.productActivities.complete(id); } await cycle("first"); assert.deepEqual(queried.sort(), ["2", "3", "4", "5", "6", "7"]); assert.deepEqual(h.calls, ["sr"], "a missing SR cache must not block the Actions queue"); for (const [number] of products) { assert.equal(app.opportunityWorkloadActions.getSummary(number).state, number === "3" ? "matched" : "missing"); } app.opportunityWorkloadActions.reset(); app.opportunityCrmServiceRequests.reset(); await cycle("second"); assert.equal(queried.length, 6, "valid matching action cache avoids all repeat requests"); assert.deepEqual(h.calls, ["sr"]); }); for (const family of ["chrome", "firefox"]) { test(`${family}: Current Quarter -> Next Quarter -> Current Quarter restores SR hours and filters without requests`, async () => { const h = setup(family); const currentStamp = Date.now() - 2000; const nextStamp = currentStamp + 1000; let hours = 2; h.app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => { h.calls.push("sr"); return [{ SrNumber: "SR1", ReportedHours_c: hours, OpportunityName_Id_c: { OptyNumber: "OP1" } }]; }; await h.load(currentStamp, false); hours = 8; await h.load(nextStamp, false); assert.equal(h.app.opportunityCrmServiceRequests.getSummary("OP1").reportedHours, 8); const count = h.calls.length; await h.load(currentStamp, true); assert.equal(h.calls.length, count); assert.equal(h.app.opportunityCrmServiceRequests.createBadge("OP1").textContent, "2h"); h.app.opportunitiesTable.renderOpportunitiesTable = () => {}; h.app.activityFilters.toggle("sr", "under4"); assert.equal(h.app.activityFilters.matches({ OptyNumber: "OP1" }), true); await h.load(nextStamp, true); assert.equal(h.calls.length, count); assert.equal(h.app.activityFilters.matches({ OptyNumber: "OP1" }), false); const reopened = setup(family, h.saved); await reopened.load(currentStamp, true); assert.deepEqual(reopened.calls, []); assert.equal(reopened.app.opportunityCrmServiceRequests.getSummary("OP1").reportedHours, 2); }); test(`${family}: activities persist across instances with product timestamps and no credential storage`, async () => { const h = setup(family); const stamp = Date.now() - 1000; await h.load(stamp, false); assert.deepEqual(h.calls, ["sr", "actions"]); assert.equal(Object.keys(h.saved).length, 2); Object.values(h.saved).forEach(entry => assert.equal(entry.fetchedAt, stamp)); assert.doesNotMatch(JSON.stringify(h.saved), /synthetic-token|debugRequests|authorization/i); const restored = setup(family, h.saved); await restored.load(stamp, true); assert.deepEqual(restored.calls, []); assert.equal(restored.app.opportunityCrmServiceRequests.createBadge("OP1").textContent, "4h"); assert.equal(restored.app.opportunityWorkloadActions.createBadge("OP1").textContent, "1"); await restored.load(stamp, false); assert.deepEqual(restored.calls, [], "valid matching cache also deduplicates repeated fresh-product notifications"); const other = setup(family, h.saved, "other@example.invalid"); await other.load(stamp, true); assert.deepEqual(other.calls, []); assert.equal(other.app.opportunityCrmServiceRequests.createBadge("OP1"), null); assert.equal(other.app.opportunityWorkloadActions.createBadge("OP1").textContent, "?"); await restored.load(stamp + 1, false); assert.deepEqual(restored.calls, ["sr", "actions"]); }); } test("TTL follows product preferences, rejects stale/mismatched cache and stores empty success", async () => { const h = setup("firefox"); const stamp = Date.now() - 2 * 3600000; await h.app.activityCache.write("sr", "all", stamp, []); assert.ok(await h.app.activityCache.read("sr", "all", stamp)); h.app.preferencesState.opptyProductsCacheHours = 1; assert.equal(await h.app.activityCache.read("sr", "all", stamp), null); h.app.preferencesState.opptyProductsCacheHours = 24; assert.equal(await h.app.activityCache.read("sr", "all", stamp + 1), null); assert.equal(await h.app.activityCache.read("sr", "all", Date.now() + 100000), null); await h.load(stamp + 1, true); assert.deepEqual(h.calls, []); }); test("versioned SR cache reads matching legacy entries and prunes expired snapshots only", async () => { const h = setup("firefox"); const legacyKey = `opportunitiesExtension.activities.v1:${JSON.stringify(["user@example.invalid", "sr", "all"])}`; const stamp = Date.now() - 1000; h.saved[legacyKey] = { fetchedAt: stamp, items: [], hasNext: false }; assert.ok(await h.app.activityCache.read("sr", "all", stamp)); assert.equal(await h.app.activityCache.read("sr", "all", stamp + 1), null); const expiredStamp = Date.now() - 25 * 3600000; const expiredKey = `opportunitiesExtension.activities.v1:${JSON.stringify(["user@example.invalid", "sr", "all", expiredStamp])}`; h.saved[expiredKey] = { fetchedAt: expiredStamp, items: [], hasNext: false }; h.saved.preferences = { keep: true }; await h.app.activityCache.write("sr", "all", stamp + 1, []); assert.equal(h.saved[expiredKey], undefined); assert.ok(h.saved[legacyKey]); assert.equal(h.saved.preferences.keep, true); await h.app.activityCache.invalidate(); assert.equal(await h.app.activityCache.read("sr", "all", stamp + 1), null); assert.equal(h.saved.preferences.keep, true); }); test("storage failure does not lose live activity results and failures are never cached", async () => { const h = setup("firefox"); h.local.get = async () => { throw new Error("Synthetic storage read failure"); }; h.local.set = async () => { throw new Error("Synthetic storage write failure"); }; await h.load(Date.now() - 1000, false); assert.equal(h.app.opportunityCrmServiceRequests.createBadge("OP1").textContent, "4h"); const failed = setup("firefox"); failed.app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => { throw new Error("Synthetic failure"); }; failed.app.platformContentMessages.sendRuntimeMessage = async () => ({ ok: false }); await failed.load(Date.now() - 1000, false); assert.deepEqual(failed.saved, {}); });