import { describe, it, expect } from "vitest"; import { assertValidDispatchPriorityRows, buildDispatchPrioritiesPageQuery, buildDispatchPrioritiesWhere, buildDispatchPriorityScopesPageQuery, buildRemoveDispatchPriorityScopeMutation, buildReplaceDispatchPriorityScopeMutation, dedupeDispatchPriorityRows, dispatchPriorityIdentityKey, normalizeDispatchSequenceNumber, parseDispatchPriorityRow, ranksCollapsedByStorage, readAffectedRows, sortByRank, } from "./dispatch-priorities.js"; import { chunkList, collectNonBlankStrings, normalizePositiveInteger, toNullableFiniteNumber, } from "./graphql-helpers.js"; import { buildWorkOrderOperationsByWorkOrderIdsQuery, normalizeWorkOrderIdList, } from "./work-order-operations-graphql.js"; import { MMSendDispatchPriority } from "./types/send-types.js"; import { MMReceiveDispatchPriority } from "./types/receive-types.js"; const row = ( overrides: Partial = {} ): MMSendDispatchPriority => new MMSendDispatchPriority( overrides.authorityOperationId ?? "PFM-1", overrides.scopeResourceId ?? "CELL-A", overrides.workOrderId ?? "JR-1", overrides.sequenceNumber !== undefined ? overrides.sequenceNumber : 10, overrides.lot ?? "0", overrides.split ?? "0", overrides.sub ?? "0", overrides.priority ?? 1, overrides.preallocatedMachineResourceId ?? null, overrides.customFieldsSchemaRef, overrides.customFields ); describe("normalizeDispatchSequenceNumber", () => { it("renders numeric forms identically, like MM's real column", () => { expect(normalizeDispatchSequenceNumber("010")).toBe("10"); expect(normalizeDispatchSequenceNumber(10)).toBe("10"); expect(normalizeDispatchSequenceNumber(" 10 ")).toBe("10"); expect(normalizeDispatchSequenceNumber("10.5")).toBe("10.5"); }); it("keeps non-numeric text and maps null/blank to empty", () => { expect(normalizeDispatchSequenceNumber("A10")).toBe("A10"); expect(normalizeDispatchSequenceNumber(null)).toBe(""); expect(normalizeDispatchSequenceNumber(" ")).toBe(""); }); }); describe("dispatchPriorityIdentityKey / dedupeDispatchPriorityRows", () => { it("keys on MM's unique-constraint columns, not the authority id", () => { const a = row({ authorityOperationId: "A" }); const b = row({ authorityOperationId: "B", sequenceNumber: "010" }); expect(dispatchPriorityIdentityKey(a)).toBe("JR-1|10|0|0|0"); expect(dispatchPriorityIdentityKey(a)).toBe(dispatchPriorityIdentityKey(b)); }); it("encodes parts unambiguously so a literal separator cannot shift the boundaries", () => { // Without escaping both of these join to "A|1|2|3|4|5" and one valid row is dropped. const a = dispatchPriorityIdentityKey({ workOrderId: "A|1", sequenceNumber: "2", lot: "3", split: "4", sub: "5", }); const b = dispatchPriorityIdentityKey({ workOrderId: "A", sequenceNumber: "1", lot: "2", split: "3", sub: "4|5", }); expect(a).not.toBe(b); // The escape character itself is escaped, so "X\\" and "X" stay distinct too. expect( dispatchPriorityIdentityKey({ workOrderId: "X\\", sequenceNumber: "1", lot: "", split: "", sub: "", }) ).not.toBe( dispatchPriorityIdentityKey({ workOrderId: "X", sequenceNumber: "1", lot: "", split: "", sub: "", }) ); }); it("keys a sequence number in its canonical numeric form", () => { const base = { workOrderId: "JR-1", lot: "0", split: "0", sub: "0" }; expect( dispatchPriorityIdentityKey({ ...base, sequenceNumber: 10 }) ).not.toBe(dispatchPriorityIdentityKey({ ...base, sequenceNumber: 20 })); // Non-numeric text is keyed verbatim rather than becoming NaN. expect( dispatchPriorityIdentityKey({ ...base, sequenceNumber: "A10" }) ).toBe("JR-1|A10|0|0|0"); }); it("shares a key exactly when MM would store the same sequence number", () => { const base = { workOrderId: "JR-1", lot: "0", split: "0", sub: "0" }; // Copilot's case: distinct doubles, one float32 value. Must be ONE key so dedupe // catches it instead of the insert failing the unique constraint. expect( dispatchPriorityIdentityKey({ ...base, sequenceNumber: 9999999.5 }) ).toBe(dispatchPriorityIdentityKey({ ...base, sequenceNumber: 10000000 })); // Ordinary steps stay distinct, and "010" / 10 / "10.0" are one step. expect( dispatchPriorityIdentityKey({ ...base, sequenceNumber: 10.1 }) ).not.toBe(dispatchPriorityIdentityKey({ ...base, sequenceNumber: 10.2 })); expect( dispatchPriorityIdentityKey({ ...base, sequenceNumber: "010" }) ).toBe(dispatchPriorityIdentityKey({ ...base, sequenceNumber: "10.0" })); }); it("is exact rather than pretty — display goes through normalizeDispatchSequenceNumber", () => { const base = { workOrderId: "JR-1", lot: "0", split: "0", sub: "0" }; expect(dispatchPriorityIdentityKey({ ...base, sequenceNumber: 10.1 })).toBe( `JR-1|${Math.fround(10.1)}|0|0|0` ); expect(normalizeDispatchSequenceNumber(10.1)).toBe("10.1"); }); it("leaves ordinary ERP values in the readable pipe-joined form", () => { expect( dispatchPriorityIdentityKey({ workOrderId: "JR-1", sequenceNumber: "010", lot: "0", split: "0", sub: "1", }) ).toBe("JR-1|10|0|0|1"); }); it("does not case-fold (MM compares text exactly)", () => { expect(dispatchPriorityIdentityKey(row({ workOrderId: "jr-1" }))).not.toBe( dispatchPriorityIdentityKey(row({ workOrderId: "JR-1" })) ); }); it("keeps the better-ranked row, whatever order the caller passed them in", () => { const worse = row({ authorityOperationId: "LATE", priority: 9 }); const better = row({ authorityOperationId: "BEST", priority: 1 }); const other = row({ authorityOperationId: "OTHER", sub: "1", priority: 2 }); // The better row is second in the list: array position must not decide this. const { kept, dropped } = dedupeDispatchPriorityRows([ worse, other, better, ]); expect(kept).toEqual([other, better]); expect(dropped).toHaveLength(1); expect(dropped[0]).toMatchObject({ row: worse, index: 0, duplicateOf: better, identityKey: "JR-1|10|0|0|0", }); }); it("ranks numerically even when a caller outside TypeScript passes priorities as text", () => { const ten = row({ authorityOperationId: "TEN", priority: "10" as unknown as number, }); const nine = row({ authorityOperationId: "NINE", priority: "9" as unknown as number, }); // As text, "10" < "9"; as ranks, 9 wins. expect(dedupeDispatchPriorityRows([ten, nine]).kept).toEqual([nine]); }); it("treats ranks MM would store identically as a tie, keeping the earlier row", () => { const first = row({ authorityOperationId: "FIRST", priority: 1700000000001, }); const second = row({ authorityOperationId: "SECOND", priority: 1700000000000, }); // As doubles the second is lower; as float32 they are one value, so position decides. expect(dedupeDispatchPriorityRows([first, second]).kept).toEqual([first]); }); it("keeps the earlier row on an exact tie", () => { const first = row({ authorityOperationId: "FIRST", priority: 5 }); const second = row({ authorityOperationId: "SECOND", priority: 5 }); const { kept, dropped } = dedupeDispatchPriorityRows([first, second]); expect(kept).toEqual([first]); expect(dropped[0]).toMatchObject({ row: second, duplicateOf: first }); }); it("keeps the caller's ordering among the survivors", () => { const a = row({ authorityOperationId: "A", sub: "1", priority: 3 }); const b = row({ authorityOperationId: "B", sub: "2", priority: 1 }); const c = row({ authorityOperationId: "C", sub: "3", priority: 2 }); expect(dedupeDispatchPriorityRows([a, b, c]).kept).toEqual([a, b, c]); }); }); describe("assertValidDispatchPriorityRows", () => { it("accepts well-formed rows, including rows with an empty scopeResourceId", () => { expect(() => assertValidDispatchPriorityRows("CELL-A", [ row(), row({ scopeResourceId: "" }), ]) ).not.toThrow(); }); it("names the row even when the element is not an object", () => { expect(() => assertValidDispatchPriorityRows("CELL-A", [ row(), undefined as unknown as MMSendDispatchPriority, ]) ).toThrow(/row 1 \(undefined\)/); expect(() => assertValidDispatchPriorityRows("CELL-A", [ null as unknown as MMSendDispatchPriority, ]) ).toThrow(/row 0 \(null\)/); }); it.each([ [ "authorityOperationId", row({ authorityOperationId: " " }), /authorityOperationId is required/, ], ["workOrderId", row({ workOrderId: "" }), /workOrderId is required/], [ "sequenceNumber", row({ sequenceNumber: "ten" }), /sequenceNumber must be a finite number/, ], [ "blank sequenceNumber", row({ sequenceNumber: "" }), /sequenceNumber must be a finite number/, ], [ "null sequenceNumber", row({ sequenceNumber: null as unknown as number }), /sequenceNumber must be a finite number/, ], [ "boolean sequenceNumber", row({ sequenceNumber: true as unknown as number }), /sequenceNumber must be a finite number/, ], [ "negative sequenceNumber", row({ sequenceNumber: -5 }), /sequenceNumber cannot be negative/, ], [ "priority", row({ priority: Number.NaN }), /priority must be a finite number/, ], [ "foreign scope", row({ scopeResourceId: "CELL-B" }), /belongs to scope "CELL-B", not "CELL-A"/, ], ])( "rejects a row with a bad %s naming the row and the field", (_label, bad, message) => { expect(() => assertValidDispatchPriorityRows("CELL-A", [row(), bad]) ).toThrow(message); expect(() => assertValidDispatchPriorityRows("CELL-A", [row(), bad]) ).toThrow(/row 1 /); } ); }); describe("numeric text must be decimal", () => { it.each(["0x10", "0b11", "0o17", "1_000", "Infinity", "١٢"])( "rejects %p, which Number() would otherwise read as a step", (sequenceNumber) => { expect(() => assertValidDispatchPriorityRows("CELL-A", [row({ sequenceNumber })]) ).toThrow(/sequenceNumber must be a finite number/); } ); it("still accepts decimal forms, including an exponent", () => { expect(() => assertValidDispatchPriorityRows("CELL-A", [ row({ sequenceNumber: "010" }), row({ sequenceNumber: "1e3", sub: "1" }), row({ sequenceNumber: " 12.5 ", sub: "2" }), row({ sequenceNumber: "+7", sub: "3" }), ]) ).not.toThrow(); }); }); describe("assertValidDispatchPriorityRows — routing-step bound", () => { // A routing step is a small number. The bound is what lets everything downstream ignore // the fact that MM stores these in a float32 column. it("rejects a sequenceNumber far larger than any routing step", () => { expect(() => assertValidDispatchPriorityRows("CELL-A", [ row({ sequenceNumber: 1e100 }), ]) ).toThrow(/sequenceNumber is larger than any routing step/); }); it("rejects a nonzero sequenceNumber far smaller than any routing step", () => { expect(() => assertValidDispatchPriorityRows("CELL-A", [ row({ sequenceNumber: 1e-50 }), ]) ).toThrow(/sequenceNumber is smaller than any routing step/); }); it("still accepts a step of zero, which is what the column defaults to", () => { expect(() => assertValidDispatchPriorityRows("CELL-A", [row({ sequenceNumber: 0 })]) ).not.toThrow(); }); it("accepts every ordinary routing step and rank", () => { expect(() => assertValidDispatchPriorityRows("CELL-A", [ row({ sequenceNumber: 10, priority: 1 }), row({ sequenceNumber: "20", priority: 0, sub: "1" }), row({ sequenceNumber: 10.1, priority: 1.5, sub: "2" }), row({ sequenceNumber: 0, priority: -1, sub: "3" }), ]) ).not.toThrow(); }); it("accepts a large rank on its own", () => { expect(() => assertValidDispatchPriorityRows("CELL-A", [row({ priority: 1.7e12 })]) ).not.toThrow(); }); it("rejects a priority too large for MM to store", () => { expect(() => assertValidDispatchPriorityRows("CELL-A", [row({ priority: 1e100 })]) ).toThrow(/priority is too large for MM to store/); }); it("reports ranks that are different but would be stored as one", () => { // Epoch-millisecond ranks: at 1.7e12 the column's granularity is ~131072, so these two // collapse. Reported, not fatal: rows go out in rank order and MM tie-breaks by ref. const collisions = ranksCollapsedByStorage([ row({ authorityOperationId: "A", priority: 1700000000000 }), row({ authorityOperationId: "B", sub: "1", priority: 1700000000001 }), ]); expect(collisions).toHaveLength(1); expect(collisions[0]).toMatch( /different ranks but MM stores both as 1700000038912/ ); }); it("sorts rows for sending by rank, then by the caller's position", () => { const a = row({ authorityOperationId: "A", priority: 3 }); const b = row({ authorityOperationId: "B", sub: "1", priority: 1 }); const c = row({ authorityOperationId: "C", sub: "2", priority: 3 }); expect(sortByRank([a, b, c])).toEqual([b, a, c]); }); it("orders ranks the column cannot separate, which is the whole point of sending in order", () => { // Both collapse to one float32 value. Rounding before comparing would leave the caller's // array order to decide and MM would hand the App the authority's list backwards. const later = row({ authorityOperationId: "LATER", priority: 1700000000001, }); const earlier = row({ authorityOperationId: "EARLIER", sub: "1", priority: 1700000000000, }); expect( sortByRank([later, earlier]).map((r) => r.authorityOperationId) ).toEqual(["EARLIER", "LATER"]); }); it('treats "10" and 10 as the same rank, not as a storage collision', () => { expect( ranksCollapsedByStorage([ row({ authorityOperationId: "A", priority: "10" as unknown as number }), row({ authorityOperationId: "B", sub: "1", priority: 10 }), ]) ).toEqual([]); }); it("leaves genuinely equal ranks alone — ties are supported", () => { expect( ranksCollapsedByStorage([ row({ authorityOperationId: "A", priority: 5 }), row({ authorityOperationId: "B", sub: "1", priority: 5 }), ]) ).toEqual([]); }); }); describe("mutation builders", () => { it("replace: one document with delete-by-scope then insert, values as variables", () => { const objects = [row().toGraphqlInsertInput()]; const op = buildReplaceDispatchPriorityScopeMutation('CELL "A"', objects); expect(op.operationName).toBe("replaceDispatchPriorityScope"); expect(op.query).toMatch( /^mutation replaceDispatchPriorityScope\(\$scope: String!, \$objects: \[erpWorkOrderOperationDispatchPriorities_insert_input!\]!\)/ ); const deleteIdx = op.query.indexOf( "deleted: deleteErpWorkOrderOperationDispatchPriorities(where: { scopeResourceId: { _eq: $scope } })" ); const insertIdx = op.query.indexOf( "inserted: insertErpWorkOrderOperationDispatchPriorities(objects: $objects)" ); expect(deleteIdx).toBeGreaterThan(-1); expect(insertIdx).toBeGreaterThan(deleteIdx); expect(op.variables).toEqual({ scope: 'CELL "A"', objects }); expect(op.query).not.toContain('CELL "A"'); }); it("remove: delete-by-scope only", () => { const op = buildRemoveDispatchPriorityScopeMutation("CELL-A"); expect(op.query).toContain( "deleted: deleteErpWorkOrderOperationDispatchPriorities(where: { scopeResourceId: { _eq: $scope } })" ); expect(op.query).not.toContain("insert"); expect(op.variables).toEqual({ scope: "CELL-A" }); }); }); describe("buildDispatchPrioritiesWhere", () => { it("matches everything for an empty filter", () => { expect(buildDispatchPrioritiesWhere({})).toEqual({}); }); it("merges single and list filters into one trimmed, deduped _in", () => { expect( buildDispatchPrioritiesWhere({ scopeResourceId: " CELL-A ", scopeResourceIds: ["CELL-A", "CELL-B", ""], workOrderIds: ["JR-1"], unresolvedOnly: true, }) ).toEqual({ scopeResourceId: { _in: ["CELL-A", "CELL-B"] }, workOrderId: { _in: ["JR-1"] }, _not: { workOrderOperation: {} }, }); }); it("returns null when a list filter is given but empty, so callers return [] instead of matching all", () => { expect(buildDispatchPrioritiesWhere({ scopeResourceIds: [] })).toBeNull(); expect(buildDispatchPrioritiesWhere({ workOrderId: " " })).toBeNull(); expect( buildDispatchPrioritiesWhere({ scopeResourceId: "CELL-A", workOrderIds: [""], }) ).toBeNull(); }); it("accepts numeric ids in a filter the way the send model does", () => { expect( buildDispatchPrioritiesWhere({ workOrderIds: [12345, "JR-1"] as unknown as string[], }) ).toEqual({ workOrderId: { _in: ["12345", "JR-1"] } }); expect(normalizeWorkOrderIdList([12345] as unknown as string[])).toEqual([ "12345", ]); }); it("treats a bare string passed as the list as one value, not as its characters", () => { expect( buildDispatchPrioritiesWhere({ scopeResourceIds: "CELL-A" as unknown as string[], }) ).toEqual({ scopeResourceId: { _in: ["CELL-A"] } }); }); it("treats a null single value from an untyped caller as given-but-empty instead of throwing", () => { expect( buildDispatchPrioritiesWhere({ scopeResourceId: null as unknown as string, }) ).toBeNull(); expect( buildDispatchPrioritiesWhere({ workOrderIds: [null as unknown as string, "JR-1"], }) ).toEqual({ workOrderId: { _in: ["JR-1"] } }); }); }); describe("page query builders", () => { it("dispatch page query orders by scope, priority, ref and passes where/limit/offset as variables", () => { const op = buildDispatchPrioritiesPageQuery( { scopeResourceId: { _in: ["A"] } }, 500, 1000 ); expect(op.query).toContain( "rows: erpWorkOrderOperationDispatchPriorities(where: $where, limit: $limit, offset: $offset, order_by: [{ scopeResourceId: asc }, { priority: asc }, { dispatchPriorityRef: asc }])" ); expect(op.query).toContain( "authorityOperationId scopeResourceId workOrderId sequenceNumber lot split sub preallocatedMachineResourceId priority" ); expect(op.variables).toEqual({ where: { scopeResourceId: { _in: ["A"] } }, limit: 500, offset: 1000, }); }); it("operations-by-ids query uses _in with the fetch-all ordering and field list", () => { const op = buildWorkOrderOperationsByWorkOrderIdsQuery( ["JR-1", "JR-2"], 500, 0 ); expect(op.query).toContain( "workOrderOperations: erpWorkOrderOperations(where: { workOrderId: { _in: $ids } }, limit: $limit, offset: $offset, order_by: [{ workOrderId: asc }, { lot: asc }, { split: asc }, { sub: asc }, { sequenceNumber: asc }]) { companyId workOrderId lot split sub sequenceNumber resourceId" ); expect(op.variables).toEqual({ ids: ["JR-1", "JR-2"], limit: 500, offset: 0, }); }); }); describe("buildDispatchPriorityScopesPageQuery", () => { it("reads one column with distinct_on, ordered by the distinct column", () => { const op = buildDispatchPriorityScopesPageQuery(500, 0); expect(op.operationName).toBe("getErpDispatchPriorityScopes"); // Hasura rejects the query if order_by does not lead with the distinct_on column. expect(op.query).toContain( "rows: erpWorkOrderOperationDispatchPriorities(distinct_on: scopeResourceId, order_by: { scopeResourceId: asc }, limit: $limit, offset: $offset) { scopeResourceId }" ); expect(op.variables).toEqual({ limit: 500, offset: 0 }); }); }); describe("list helpers", () => { it("normalizeWorkOrderIdList trims, drops blanks and dedupes exactly, keeping order", () => { expect( normalizeWorkOrderIdList([" JR-2", "JR-1", "", "JR-2", "jr-1"]) ).toEqual(["JR-2", "JR-1", "jr-1"]); }); it("chunkList splits by size and rejects a non-positive size", () => { expect(chunkList([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]); expect(chunkList([1, 2, 3], 2.9)).toEqual([[1, 2], [3]]); expect(chunkList([], 3)).toEqual([]); expect(() => chunkList([1, 2], 0)).toThrow(RangeError); expect(() => chunkList([1, 2], Number.NaN)).toThrow(RangeError); }); it("normalizePositiveInteger floors positives and falls back on anything else", () => { expect(normalizePositiveInteger(250, 500)).toBe(250); expect(normalizePositiveInteger(2.9, 500)).toBe(2); expect(normalizePositiveInteger(undefined, 500)).toBe(500); expect(normalizePositiveInteger(0, 500)).toBe(500); // A fractional size floors to 0, which would make a paged read loop forever. expect(normalizePositiveInteger(0.5, 500)).toBe(500); expect(normalizePositiveInteger(0.999, 500)).toBe(500); expect(normalizePositiveInteger(1, 500)).toBe(1); expect(normalizePositiveInteger(-5, 500)).toBe(500); expect(normalizePositiveInteger(Number.NaN, 500)).toBe(500); expect(normalizePositiveInteger(Number.POSITIVE_INFINITY, 500)).toBe(500); }); it("collectNonBlankStrings merges a single value and a list; strings and finite numbers count, the rest is dropped", () => { expect( collectNonBlankStrings(" a ", [ "b", " ", "a", null as unknown as string, 7 as unknown as string, true as unknown as string, ]) ).toEqual(["a", "b", "7"]); expect(collectNonBlankStrings(undefined, undefined)).toEqual([]); expect(collectNonBlankStrings(null, [])).toEqual([]); }); it("toNullableFiniteNumber keeps null distinct from 0 and rejects non-numeric types", () => { expect(toNullableFiniteNumber(0)).toBe(0); expect(toNullableFiniteNumber("5")).toBe(5); expect(toNullableFiniteNumber("")).toBeNull(); expect(toNullableFiniteNumber(null)).toBeNull(); expect(toNullableFiniteNumber(true)).toBeNull(); expect(toNullableFiniteNumber("abc")).toBeNull(); expect(toNullableFiniteNumber(Number.POSITIVE_INFINITY)).toBeNull(); }); }); describe("parseDispatchPriorityRow", () => { const raw = { dispatchPriorityRef: "17", companyId: "c1", authorityOperationId: "PFM-1", scopeResourceId: "CELL-A", workOrderId: "JR-1", sequenceNumber: 10, lot: "0", split: "0", sub: "1", preallocatedMachineResourceId: null, priority: "2.5", createdAt: "2026-09-14T00:00:00+00:00", updatedAt: "2026-09-14T00:00:00+00:00", customFieldsSchemaRef: null, customFields: null, }; it("exposes identity text exactly as MM holds it, never trimmed", () => { const parsed = parseDispatchPriorityRow({ ...raw, lot: " 0 ", sub: "1 " }); expect({ lot: parsed.lot, sub: parsed.sub }).toEqual({ lot: " 0 ", sub: "1 ", }); }); it("reads back a non-finite value another writer stored, instead of failing the page", () => { const parsed = parseDispatchPriorityRow({ ...raw, priority: "Infinity", sequenceNumber: "NaN", }); expect(parsed.priority).toBe(Number.POSITIVE_INFINITY); expect(parsed.sequenceNumber).toBe("NaN"); // The surrogate key is the one number that must be finite. expect(() => parseDispatchPriorityRow({ ...raw, dispatchPriorityRef: "Infinity" }) ).toThrow(/dispatchPriorityRef/); }); it("throws on a missing identity column instead of reading it as empty", () => { expect(() => parseDispatchPriorityRow({ ...raw, lot: undefined })).toThrow( /lot/ ); expect(() => parseDispatchPriorityRow({ ...raw, createdAt: null })).toThrow( /createdAt/ ); }); it("coerces bigint/Float renderings and renders sequenceNumber as a string", () => { const parsed = parseDispatchPriorityRow(raw); expect(parsed).toBeInstanceOf(MMReceiveDispatchPriority); expect(parsed).toMatchObject({ dispatchPriorityRef: 17, authorityOperationId: "PFM-1", sequenceNumber: "10", sub: "1", priority: 2.5, preallocatedMachineResourceId: null, customFieldsSchemaRef: null, customFields: null, }); }); it("keeps a null authorityOperationId (MM allows it) and parses customFields", () => { const parsed = parseDispatchPriorityRow({ ...raw, authorityOperationId: null, customFieldsSchemaRef: "3", customFields: { a: 1 }, }); expect(parsed.authorityOperationId).toBeNull(); expect(parsed.customFieldsSchemaRef).toBe(3); expect(parsed.customFields).toEqual({ a: 1 }); }); it("accepts a large ref without ceremony — the sequence is nowhere near 2^53", () => { expect( parseDispatchPriorityRow({ ...raw, dispatchPriorityRef: "4503599627370496", }).dispatchPriorityRef ).toBe(4503599627370496); }); it("throws naming the field when a row is not the documented shape", () => { expect(() => parseDispatchPriorityRow(null)).toThrow(/Expected object/); expect(() => parseDispatchPriorityRow({ ...raw, dispatchPriorityRef: undefined }) ).toThrow(/dispatchPriorityRef must be a finite number/); expect(() => parseDispatchPriorityRow({ ...raw, scopeResourceId: undefined }) ).toThrow(/scopeResourceId/); expect(() => parseDispatchPriorityRow({ ...raw, priority: "high" }) ).toThrow(/priority must be a number/); }); }); describe("readAffectedRows", () => { it("reads numeric or string counts and rejects a missing field", () => { expect( readAffectedRows( { data: { deleted: { affected_rows: 3 } } }, "deleted", "op" ) ).toBe(3); expect( readAffectedRows( { data: { deleted: { affected_rows: "3" } } }, "deleted", "op" ) ).toBe(3); expect( readAffectedRows( { data: { deleted: { affected_rows: 0 } } }, "deleted", "op" ) ).toBe(0); expect(() => readAffectedRows({ data: {} }, "deleted", "op")).toThrow( /data\.deleted\.affected_rows is missing or not a number/ ); expect(() => readAffectedRows(undefined, "deleted", "op")).toThrow( /malformed/ ); }); it("does not read null, blank or boolean counts as zero rows", () => { for (const bad of [null, "", " ", false, true]) { expect(() => readAffectedRows( { data: { deleted: { affected_rows: bad } } }, "deleted", "op" ) ).toThrow(/malformed/); } }); }); describe("MMSendDispatchPriority.toGraphqlInsertInput", () => { it("canonicalizes a directly-constructed row the same way fromPlainObject would", () => { // Before, validation passed on a coerced copy and this threw `this.lot.trim is not a function`. const raw = new MMSendDispatchPriority( 99 as unknown as string, "CELL-A", 12345 as unknown as string, "010", 0 as unknown as string, "0", 1 as unknown as string, "2" as unknown as number, null ); expect(() => assertValidDispatchPriorityRows("CELL-A", [raw]) ).not.toThrow(); expect(raw.toGraphqlInsertInput()).toMatchObject({ authorityOperationId: "99", workOrderId: "12345", sequenceNumber: 10, lot: "0", sub: "1", priority: 2, }); }); it("refuses to build a payload for a row validation would refuse", () => { expect(() => row({ authorityOperationId: " " }).toGraphqlInsertInput() ).toThrow(); }); it("trims text columns so the stored row matches the dedupe key", () => { const input = row({ authorityOperationId: " A ", workOrderId: " JR-1 ", lot: "0 ", split: " 0", sub: " 1 ", preallocatedMachineResourceId: " M1 ", }).toGraphqlInsertInput(); expect(input).toMatchObject({ authorityOperationId: "A", workOrderId: "JR-1", lot: "0", split: "0", sub: "1", preallocatedMachineResourceId: "M1", }); expect( row({ preallocatedMachineResourceId: " " }).toGraphqlInsertInput() .preallocatedMachineResourceId ).toBeNull(); }); it("sends numeric sequenceNumber, explicit nulls, and never companyId", () => { const input = row({ sequenceNumber: " 010 " }).toGraphqlInsertInput(); expect(input).toEqual({ authorityOperationId: "PFM-1", scopeResourceId: "CELL-A", workOrderId: "JR-1", sequenceNumber: 10, lot: "0", split: "0", sub: "0", priority: 1, preallocatedMachineResourceId: null, }); expect(input).not.toHaveProperty("companyId"); }); it("does not send a schema ref that has no values to describe", () => { const input = row({ customFieldsSchemaRef: 7 }).toGraphqlInsertInput(); expect(input).not.toHaveProperty("customFieldsSchemaRef"); expect(input).not.toHaveProperty("customFields"); }); it("sends numbers already rounded to float32 — exactly what the real column will hold", () => { const input = row({ sequenceNumber: 10.1, priority: 0.1, }).toGraphqlInsertInput(); expect(input.sequenceNumber).toBe(Math.fround(10.1)); expect(input.priority).toBe(Math.fround(0.1)); }); it("includes custom fields only when non-empty", () => { expect(row({ customFields: {} }).toGraphqlInsertInput()).not.toHaveProperty( "customFields" ); const withCf = row({ customFieldsSchemaRef: 7, customFields: { fetchedAt: "x" }, }).toGraphqlInsertInput(); expect(withCf).toMatchObject({ customFieldsSchemaRef: 7, customFields: { fetchedAt: "x" }, }); }); it("fromPlainObject round-trips a cache-style row", () => { const built = MMSendDispatchPriority.fromPlainObject({ authorityOperationId: "A", scopeResourceId: "S", workOrderId: "W", sequenceNumber: "20", lot: "", split: "", sub: "1", priority: "3", preallocatedMachineResourceId: "M1", customFieldsSchemaRef: "7", }); expect(built).toMatchObject({ authorityOperationId: "A", sequenceNumber: 20, sub: "1", priority: 3, preallocatedMachineResourceId: "M1", customFieldsSchemaRef: 7, }); }); it.each([true, false, [], [5], {}, "abc", null, undefined, ""])( "refuses to build a row with a priority of %p", (priority) => { expect(() => MMSendDispatchPriority.fromPlainObject({ authorityOperationId: "A", scopeResourceId: "S", workOrderId: "W", sequenceNumber: 1, priority, }) ).toThrow(); } ); it("keeps a numeric identity component instead of blanking or crashing on it", () => { // lot/split/sub are numeric in many ERPs; MM stores them as text ("0" at Straits). const row = MMSendDispatchPriority.fromPlainObject({ authorityOperationId: "A", scopeResourceId: "CELL", workOrderId: "JR-1", sequenceNumber: 10, lot: 0, split: 1, sub: 2, priority: 1, }); expect({ lot: row.lot, split: row.split, sub: row.sub }).toEqual({ lot: "0", split: "1", sub: "2", }); // A number used to survive to the insert path and throw there on .trim(). expect(row.toGraphqlInsertInput()).toMatchObject({ lot: "0", split: "1", sub: "2", }); }); it("stringifies a numeric work order id rather than reporting it as missing", () => { const row = MMSendDispatchPriority.fromPlainObject({ authorityOperationId: 99, scopeResourceId: "CELL", workOrderId: 12345, sequenceNumber: 10, priority: 1, }); expect(row.workOrderId).toBe("12345"); expect(row.authorityOperationId).toBe("99"); expect(() => assertValidDispatchPriorityRows("CELL", [row])).not.toThrow(); }); it("refuses a boolean or object in an identity column", () => { for (const lot of [false, {}, []]) { expect(() => MMSendDispatchPriority.fromPlainObject({ authorityOperationId: "A", scopeResourceId: "S", workOrderId: "W", sequenceNumber: 1, priority: 1, lot, }) ).toThrow(); } }); it("treats a blank preallocated machine as absent, but keeps a numeric one", () => { const base = { authorityOperationId: "A", scopeResourceId: "CELL", workOrderId: "JR-1", sequenceNumber: 10, priority: 1, }; expect( MMSendDispatchPriority.fromPlainObject({ ...base, preallocatedMachineResourceId: " ", }).preallocatedMachineResourceId ).toBeNull(); expect( MMSendDispatchPriority.fromPlainObject({ ...base, preallocatedMachineResourceId: 7, }).preallocatedMachineResourceId ).toBe("7"); }); it("refuses a fractional customFieldsSchemaRef — the column is a bigint and MM would reject the whole scope", () => { expect(() => MMSendDispatchPriority.fromPlainObject({ authorityOperationId: "A", scopeResourceId: "S", workOrderId: "W", sequenceNumber: 1, priority: 1, customFieldsSchemaRef: 1.5, }) ).toThrow(/must be a whole number/); }); it("refuses a boolean customFieldsSchemaRef, and treats absent as none", () => { const base = { authorityOperationId: "A", scopeResourceId: "S", workOrderId: "W", sequenceNumber: 1, priority: 1, }; expect(() => MMSendDispatchPriority.fromPlainObject({ ...base, customFieldsSchemaRef: true, }) ).toThrow(); expect( MMSendDispatchPriority.fromPlainObject(base).customFieldsSchemaRef ).toBeNull(); }); it("accepts a legitimate rank of zero", () => { expect( MMSendDispatchPriority.fromPlainObject({ authorityOperationId: "A", scopeResourceId: "S", workOrderId: "W", sequenceNumber: 1, priority: 0, }).priority ).toBe(0); }); });