mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-07-12 13:28:55 +00:00
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>
193 lines
5.8 KiB
TypeScript
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);
|
|
});
|
|
});
|