WFDASH-WEBAPP/src/lib/quote-assistant-merge.test.ts
Dave 4e0f7a5c5c fix(quote-assistant): AI fill on Process page keeps unsaved typing
After "Let AI fill this quote" the page re-pulls the quote's lines and
merges them with mergeItemsAfterAiRun: rows unchanged since the last load
take the server copy (so fills show), rows with unsaved changes stay as
typed, with a toast saying how many were kept. Previously onDone ran
fetchData, which replaced every row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:29:04 +02:00

66 lines
2.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { mergeItemsAfterAiRun } from "./quote-assistant-merge";
type Row = { id: string; description: string; cost_price: number; ai_fill?: unknown };
const row = (id: string, over: Partial<Row> = {}): Row => ({
id,
description: `item ${id}`,
cost_price: 0,
...over,
});
describe("mergeItemsAfterAiRun", () => {
it("takes the server row for lines the rep has not touched", () => {
const baseline = [row("a"), row("b")];
const local = [row("a"), row("b")];
const fresh = [row("a", { cost_price: 10, ai_fill: { status: "filled" } }), row("b")];
const out = mergeItemsAfterAiRun(local, baseline, fresh);
expect(out.items).toEqual(fresh);
expect(out.keptDirty).toEqual([]);
});
it("keeps a row with unsaved typing as the rep left it", () => {
const baseline = [row("a"), row("b")];
const local = [row("a", { description: "rep is typing" }), row("b")];
const fresh = [
row("a", { cost_price: 10, ai_fill: { status: "filled" } }),
row("b", { cost_price: 7, ai_fill: { status: "filled" } }),
];
const out = mergeItemsAfterAiRun(local, baseline, fresh);
expect(out.items[0]).toEqual(local[0]);
expect(out.items[1]).toEqual(fresh[1]);
expect(out.keptDirty).toEqual(["a"]);
});
it("follows the server's row set and order: new rows appear, deleted rows go", () => {
const baseline = [row("a"), row("b")];
const local = [row("a"), row("b")];
const fresh = [row("c"), row("a")];
const out = mergeItemsAfterAiRun(local, baseline, fresh);
expect(out.items.map((r) => r.id)).toEqual(["c", "a"]);
});
it("treats a local row with no baseline as dirty (added locally, never loaded)", () => {
const local = [row("a", { description: "x" })];
const fresh = [row("a")];
const out = mergeItemsAfterAiRun(local, [], fresh);
expect(out.items[0]).toEqual(local[0]);
expect(out.keptDirty).toEqual(["a"]);
});
it("compares values, not object identity or key order", () => {
const baseline = [{ id: "a", cost_price: 0, description: "d", ai_fill: { status: "no_match" } }];
const local = [{ description: "d", id: "a", ai_fill: { status: "no_match" }, cost_price: 0 }];
const fresh = [{ id: "a", cost_price: 5, description: "d", ai_fill: { status: "filled" } }];
const out = mergeItemsAfterAiRun(local, baseline, fresh);
expect(out.items).toEqual(fresh);
});
});