fix(eoc-push): mappa feedback-tidsstämpel till "skapad" per EOC-kontraktet
This commit is contained in:
@@ -14,6 +14,44 @@ function ingestUrl(base: string): string {
|
|||||||
return `${trimmed}/api/v1/apps/cibello/ingest`;
|
return `${trimmed}/api/v1/apps/cibello/ingest`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface EocFeedbackPost {
|
||||||
|
typ?: string;
|
||||||
|
rubrik?: string;
|
||||||
|
status?: string;
|
||||||
|
created_at?: string;
|
||||||
|
skapad?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EocFeedback {
|
||||||
|
oppna?: number;
|
||||||
|
senaste?: EocFeedbackPost[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a payload that matches EOC's authoritative contract:
|
||||||
|
* feedback.senaste[].created_at must be sent as feedback.senaste[].skapad.
|
||||||
|
* All other keys are forwarded unchanged.
|
||||||
|
*/
|
||||||
|
function mapPayloadForEoc(parsed: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const mapped: Record<string, unknown> = { ...parsed };
|
||||||
|
const feedback = mapped.feedback as EocFeedback | undefined;
|
||||||
|
if (feedback && Array.isArray(feedback.senaste)) {
|
||||||
|
mapped.feedback = {
|
||||||
|
...feedback,
|
||||||
|
senaste: feedback.senaste.map((post) => {
|
||||||
|
const { created_at, ...rest } = post;
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
...(created_at !== undefined ? { skapad: created_at } : {}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Push the cached ops summary to EOC. Never throws; failures are returned
|
* Push the cached ops summary to EOC. Never throws; failures are returned
|
||||||
* as `ok: false` and must be logged by the caller.
|
* as `ok: false` and must be logged by the caller.
|
||||||
@@ -42,6 +80,8 @@ export async function pushOpsSummaryToEoc(redis: Redis): Promise<EocPushResult>
|
|||||||
return { ok: false, status: null, error: "ops summary cache is not a JSON object" };
|
return { ok: false, status: null, error: "ops summary cache is not a JSON object" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const body = JSON.stringify(mapPayloadForEoc(parsed as Record<string, unknown>));
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), EOC_PUSH_TIMEOUT_MS);
|
const timeout = setTimeout(() => controller.abort(), EOC_PUSH_TIMEOUT_MS);
|
||||||
|
|
||||||
@@ -52,7 +92,7 @@ export async function pushOpsSummaryToEoc(redis: Redis): Promise<EocPushResult>
|
|||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: json,
|
body,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, it, beforeAll, afterAll } from "vitest";
|
import { describe, expect, it, beforeAll, afterAll, vi } from "vitest";
|
||||||
import IORedis from "ioredis";
|
import IORedis from "ioredis";
|
||||||
import { pushOpsSummaryToEoc } from "../src/processors/eoc-push.js";
|
import { pushOpsSummaryToEoc } from "../src/processors/eoc-push.js";
|
||||||
|
|
||||||
@@ -21,6 +21,43 @@ describe("pushOpsSummaryToEoc", () => {
|
|||||||
await redis.quit();
|
await redis.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("maps feedback.created_at to skapad in the POST body", async () => {
|
||||||
|
process.env.EOC_URL = "https://eoc.example.com";
|
||||||
|
process.env.EOC_PUSH_TOKEN = "tok";
|
||||||
|
const ts = "2026-08-11T10:00:00.000Z";
|
||||||
|
await redis.set(
|
||||||
|
"ops:summary:cache",
|
||||||
|
JSON.stringify({
|
||||||
|
as_of: ts,
|
||||||
|
app: { app: "cibello" },
|
||||||
|
feedback: {
|
||||||
|
oppna: 1,
|
||||||
|
senaste: [{ typ: "bug", rubrik: "exempel", status: "oppen", created_at: ts }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
"EX",
|
||||||
|
60,
|
||||||
|
);
|
||||||
|
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 204 }));
|
||||||
|
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await pushOpsSummaryToEoc(redis);
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const init = fetchMock.mock.calls[0][1] as RequestInit;
|
||||||
|
const body = JSON.parse(init.body as string);
|
||||||
|
expect(body.feedback.senaste[0].skapad).toBe(ts);
|
||||||
|
expect(body.feedback.senaste[0]).not.toHaveProperty("created_at");
|
||||||
|
expect(body.as_of).toBe(ts);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("returns disabled result when EOC_URL is missing", async () => {
|
it("returns disabled result when EOC_URL is missing", async () => {
|
||||||
delete process.env.EOC_URL;
|
delete process.env.EOC_URL;
|
||||||
process.env.EOC_PUSH_TOKEN = "tok";
|
process.env.EOC_PUSH_TOKEN = "tok";
|
||||||
|
|||||||
Reference in New Issue
Block a user