assessment-model/src/lib/bulkDocumentDownload/model.test.ts
Khalim Conn-Kowlessar 098a5ccffc fix(live-reporting): bulk download poller no longer hangs after completion
The status poll only declared "ready" when it could parse presigned_url out
of sub_task.outputs, so a finished job whose link used a different key (or
whose completion was only reflected in status) sat on "preparing" forever.

- Derive terminal state from completion/failure *status* (task or sub_task,
  incl. jobCompleted), not just the parsed link.
- Tolerate link-key variants (presigned_url / presignedUrl / download_url /
  url; package_s3_key / s3_key).
- When completed but the link can't be surfaced in-app, show a "ready — check
  your email" card instead of hanging.
- Ease the poll cadence to 8s (assembly takes minutes; email is the real
  channel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 16:34:00 +00:00

193 lines
5.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
MAX_BULK_DOWNLOAD_PROPERTIES,
buildBulkDownloadRecord,
buildSelectionConfig,
bulkDownloadRefetchInterval,
parseDelivery,
} from "./model";
describe("buildSelectionConfig — UI selection → stored config", () => {
const portfolioId = 814;
it("one project + include all → project_codes", () => {
const r = buildSelectionConfig({
scope: { kind: "project", projectCode: "ABRI-2024-01" },
includeAll: true,
landlordPropertyIds: [],
portfolioId,
});
expect(r).toEqual({
ok: true,
config: { project_codes: ["ABRI-2024-01"], portfolio_id: 814 },
});
});
it("one project + ticked subset → landlord_property_ids (scope ignored)", () => {
const r = buildSelectionConfig({
scope: { kind: "project", projectCode: "ABRI-2024-01" },
includeAll: false,
landlordPropertyIds: ["LP1023", "LP1044"],
portfolioId,
});
expect(r).toEqual({
ok: true,
config: { landlord_property_ids: ["LP1023", "LP1044"], portfolio_id: 814 },
});
});
it("all projects + include all → every project code", () => {
const r = buildSelectionConfig({
scope: { kind: "all-projects", projectCodes: ["A-1", "B-2"] },
includeAll: true,
landlordPropertyIds: [],
portfolioId,
});
expect(r).toEqual({
ok: true,
config: { project_codes: ["A-1", "B-2"], portfolio_id: 814 },
});
});
it("all projects + ticked subset → landlord_property_ids", () => {
const r = buildSelectionConfig({
scope: { kind: "all-projects", projectCodes: ["A-1", "B-2"] },
includeAll: false,
landlordPropertyIds: ["LP1"],
portfolioId,
});
expect(r).toEqual({
ok: true,
config: { landlord_property_ids: ["LP1"], portfolio_id: 814 },
});
});
it("de-duplicates ids and codes", () => {
expect(
buildSelectionConfig({
scope: { kind: "all-projects", projectCodes: ["A-1", "A-1"] },
includeAll: true,
landlordPropertyIds: [],
portfolioId,
}),
).toEqual({ ok: true, config: { project_codes: ["A-1"], portfolio_id: 814 } });
expect(
buildSelectionConfig({
scope: { kind: "project", projectCode: "A-1" },
includeAll: false,
landlordPropertyIds: ["LP1", "LP1", " LP1 "],
portfolioId,
}),
).toEqual({
ok: true,
config: { landlord_property_ids: ["LP1"], portfolio_id: 814 },
});
});
it("rejects an empty subset", () => {
const r = buildSelectionConfig({
scope: { kind: "project", projectCode: "A-1" },
includeAll: false,
landlordPropertyIds: [],
portfolioId,
});
expect(r.ok).toBe(false);
});
it("rejects include-all with no usable codes", () => {
const r = buildSelectionConfig({
scope: { kind: "all-projects", projectCodes: ["", " "] },
includeAll: true,
landlordPropertyIds: [],
portfolioId,
});
expect(r.ok).toBe(false);
});
it("rejects a subset over the 500 cap with the detail message", () => {
const ids = Array.from({ length: 501 }, (_, i) => `LP${i}`);
const r = buildSelectionConfig({
scope: { kind: "project", projectCode: "A-1" },
includeAll: false,
landlordPropertyIds: ids,
portfolioId,
});
expect(r.ok).toBe(false);
if (!r.ok) {
expect(r.error).toContain(`over the ${MAX_BULK_DOWNLOAD_PROPERTIES} limit`);
expect(r.error).toContain("501 properties");
}
});
});
describe("buildBulkDownloadRecord", () => {
it("writes the app-owned task marker and the config as JSON inputs", () => {
const record = buildBulkDownloadRecord({
portfolioId: "814",
config: { project_codes: ["A-1"], portfolio_id: 814 },
});
expect(record.taskSource).toBe("app:bulk_document_download");
expect(record.source).toBe("portfolio_id");
expect(record.sourceId).toBe("814");
expect(record.status).toBe("waiting");
expect(JSON.parse(record.inputs)).toEqual({
project_codes: ["A-1"],
portfolio_id: 814,
});
});
});
describe("parseDelivery", () => {
it("reads a finished delivery", () => {
const d = parseDelivery(
JSON.stringify({
presigned_url: "https://x",
package_s3_key: "k",
included: 137,
skipped: [{ landlord_property_id: "LP9", s3_key: "k", reason: "unreadable" }],
}),
);
expect(d).toEqual({
presignedUrl: "https://x",
packageS3Key: "k",
included: 137,
skipped: [{ landlord_property_id: "LP9", s3_key: "k", reason: "unreadable" }],
});
});
it("tolerates link-key naming variants", () => {
expect(parseDelivery(JSON.stringify({ presignedUrl: "https://a" }))?.presignedUrl).toBe(
"https://a",
);
expect(parseDelivery(JSON.stringify({ download_url: "https://b" }))?.presignedUrl).toBe(
"https://b",
);
expect(parseDelivery(JSON.stringify({ url: "https://c", s3_key: "k" }))).toMatchObject({
presignedUrl: "https://c",
packageS3Key: "k",
});
});
it("returns null for in-flight or malformed outputs", () => {
expect(parseDelivery(null)).toBeNull();
expect(parseDelivery("")).toBeNull();
expect(parseDelivery("{not json")).toBeNull();
expect(parseDelivery(JSON.stringify({ included: 0 }))).toBeNull();
});
});
describe("bulkDownloadRefetchInterval", () => {
it("polls while preparing, stops once resolved", () => {
expect(bulkDownloadRefetchInterval(undefined)).toBe(8_000);
expect(bulkDownloadRefetchInterval({ state: "preparing", delivery: null })).toBe(8_000);
expect(
bulkDownloadRefetchInterval({
state: "ready",
delivery: { presignedUrl: "x", packageS3Key: null, included: 1, skipped: [] },
}),
).toBe(false);
expect(bulkDownloadRefetchInterval({ state: "failed", delivery: null })).toBe(false);
});
});