const test = require("node:test"); const assert = require("node:assert/strict"); const { loadBackground, response, plain } = require("./support/characterization.cjs"); const { loadApplication } = require("./support/application.cjs"); const type = "opportunitiesExtension.requestWorkloadForecastUpdate"; const payload = [{ year: 2026, month: 11, amount: 303.03, updatedBy: "test@example.invalid" }]; const message = { type, workloadId: "1234", authorization: { value: "synthetic-wwb" }, payload }; for (const family of ["chrome", "firefox"]) { test(`${family}: forecast PATCH uses dynamic workload, JSON, WWB authorization and invalidates cache on 204`, async () => { const calls = []; const storage = { "opportunitiesExtension.workloadsCache.v1": { old: { items: [{ id: 1234 }] } } }; const h = loadBackground({ family, storage, fetch: async (url, options) => { calls.push({ url, options }); return new Response(null, { status: 204 }); } }); const result = await h.dispatch(message); assert.equal(result.ok, true); assert.equal(calls.length, 1); assert.ok(calls[0].url.endsWith("/workloads/1234/forecast")); assert.equal(calls[0].options.method, "PATCH"); assert.equal(calls[0].options.credentials, "include"); assert.equal(calls[0].options.headers["wwb-provider-authorization"], "synthetic-wwb"); assert.equal(calls[0].options.headers["Content-Type"], "application/json"); assert.deepEqual(JSON.parse(calls[0].options.body), payload); assert.deepEqual(storage["opportunitiesExtension.workloadsCache.v1"], {}); assert.equal(result.debugRequests[0].name, "requestWorkloadForecastUpdate"); assert.equal(result.debugRequests[0].httpStatus, 204); }); test(`${family}: denied PATCH preserves cache, returns debug and is not automatically retried`, async () => { let calls = 0; const storage = { "opportunitiesExtension.workloadsCache.v1": { old: { items: [{ id: 1234 }] } } }; const before = JSON.stringify(storage); const h = loadBackground({ family, storage, fetch: async () => { calls++; return response({ error: "Denied" }, 403); } }); const result = await h.dispatch(message); assert.equal(result.ok, false); assert.equal(result.httpStatus, 403); assert.equal(calls, 1); assert.equal(JSON.stringify(storage), before); assert.equal(result.debugRequests[0].httpStatus, 403); }); } test("forecast update rejects invalid identity, invalid amounts and duplicate months before fetch", async () => { let calls = 0; const h = loadBackground({ fetch: async () => { calls++; return response({}); } }); for (const change of [{ workloadId: "../other" }, { authorization: {} }, { payload: [] }, { payload: [{ ...payload[0], amount: -1 }] }, { payload: [{ ...payload[0], month: 13 }] }, { payload: [{ ...payload[0], updatedBy: "" }] }, { payload: [payload[0], payload[0]] }]) { assert.equal((await h.dispatch({ ...message, ...change })).ok, false); } assert.equal(calls, 0); }); function setup() { const app = loadApplication("content"); const item = { RevnId: 1234, RampMonths_c: 2, workload: { id: 1234, adjustedACR: 50000, consumptionStartDate: "2026-11-25", forecast: [ { year: 2026, month: 10, adjustedConsumptionAmount: 0 }, { year: 2026, month: 11, adjustedConsumptionAmount: 100 }, { year: 2026, month: 12, adjustedConsumptionAmount: 100 } ] } }; app.currentUser.ensure = async () => {}; app.currentUser.getUserId = () => "test@example.invalid"; app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: message.authorization }); app.debugContentLog.appendBackgroundRequestLog = () => {}; app.productsContentJobs.queueProductsTableRender = () => {}; app.productsContentState.opptyProductsByNumber.set("ONE", { items: [item] }); app.productsContentState.opptyProductsByNumber.set("TWO", { items: [{ ...item }] }); app.productsContentState.workloadsByCustomer.set("99", { status: "ready", workloads: [item.workload], byId: new Map([["1234", item.workload]]) }); return { app, item }; } test("content constructs calculated months and updates both joins only after successful PATCH", async () => { const { app, item } = setup(); let release, sent; app.platformContentMessages.sendRuntimeMessage = value => { sent = value; return new Promise(resolve => { release = resolve; }); }; const saving = app.workloadForecastUpdate.save(item); await new Promise(resolve => setImmediate(resolve)); assert.equal(item.workload.forecast[1].adjustedConsumptionAmount, 100); assert.equal(sent.type, type); assert.equal(sent.workloadId, "1234"); assert.deepEqual(plain(sent.payload), [payload[0], { year: 2026, month: 12, amount: 3030.30, updatedBy: "test@example.invalid" }]); await assert.rejects(app.workloadForecastUpdate.save(item), /already being updated/); release({ ok: true }); await saving; assert.equal(app.rampProjection.compare(item).status, "matched"); assert.equal(app.productsContentState.workloadsByCustomer.get("99").byId.get("1234").forecast[1].adjustedConsumptionAmount, 303.03); for (const record of app.productsContentState.opptyProductsByNumber.values()) assert.equal(app.rampProjection.compare(record.items[0]).status, "matched"); }); test("failed forecast update leaves values unchanged and allows a deliberate retry", async () => { const { app, item } = setup(); const before = JSON.stringify(item); app.platformContentMessages.sendRuntimeMessage = async () => ({ ok: false, error: "Forbidden" }); await assert.rejects(app.workloadForecastUpdate.save(item), /Forbidden/); assert.equal(JSON.stringify(item), before); app.platformContentMessages.sendRuntimeMessage = async () => ({ ok: true }); await app.workloadForecastUpdate.save(item); assert.equal(app.rampProjection.compare(item).status, "matched"); });