import { describe, expect, it } from "vitest"; import { appendNote, buildBackToPendingNote, formatNotesForDisplay, parseStoredNotes, type NoteEntry, } from "./note-feed"; const entry = (over: Partial = {}): NoteEntry => ({ ts: "2026-10-01T09:00:00.000Z", actorId: "u1", actorName: "Dana", category: "note", body: "something", ...over, }); describe("parseStoredNotes", () => { it("returns an empty feed for empty / null / undefined input", () => { expect(parseStoredNotes("")).toEqual([]); expect(parseStoredNotes(" ")).toEqual([]); expect(parseStoredNotes(null)).toEqual([]); expect(parseStoredNotes(undefined)).toEqual([]); }); it("returns an empty feed for legacy plain text instead of throwing", () => { // The old assign route wrote a bare string. Parsing must degrade. expect(parseStoredNotes("spoke to David")).toEqual([]); }); it("returns an empty feed for a JSON object", () => { expect(parseStoredNotes('{"body":"x"}')).toEqual([]); }); it("drops malformed entries but keeps the good ones", () => { const stored = JSON.stringify([entry(), { ts: "nope" }, null, "junk"]); const parsed = parseStoredNotes(stored); expect(parsed).toHaveLength(1); expect(parsed[0].actorName).toBe("Dana"); }); it("drops an entry whose category is unknown", () => { // Unknown category => the next read would silently lose the entry. const stored = JSON.stringify([entry({ category: "not-a-category" as never })]); expect(parseStoredNotes(stored)).toEqual([]); }); it("keeps the new pending category", () => { const stored = JSON.stringify([entry({ category: "pending" })]); expect(parseStoredNotes(stored)).toHaveLength(1); }); }); describe("appendNote", () => { it("appends after the existing entries, preserving them", () => { const first = appendNote("", { ts: "2026-10-01T09:00:00.000Z", actor: { id: "u1", name: "Dana" }, category: "note", body: "first" }); const second = appendNote(first, { ts: "2026-10-02T09:00:00.000Z", actor: { id: "u2", name: "Ruan" }, category: "pending", body: "second" }); const parsed = parseStoredNotes(second); expect(parsed).toHaveLength(2); expect(parsed[0].body).toBe("first"); expect(parsed[1].body).toBe("second"); expect(parsed[1].actorName).toBe("Ruan"); }); it("does not mutate or alias the caller's string", () => { const before = appendNote("", { ts: "2026-10-01T09:00:00.000Z", actor: { id: "u1", name: "Dana" }, category: "note", body: "first" }); const after = appendNote(before, { ts: "2026-10-02T09:00:00.000Z", actor: { id: "u1", name: "Dana" }, category: "note", body: "second" }); expect(parseStoredNotes(before)).toHaveLength(1); expect(parseStoredNotes(after)).toHaveLength(2); }); }); describe("buildBackToPendingNote", () => { // A pre-existing entry, so the tests prove the new one is APPENDED // rather than written in place of the history. const seed = JSON.stringify([ { ts: "2026-09-01T09:00:00.000Z", actorId: "u0", actorName: "Dana", category: "collection", body: "Collected" }, ]); const last = (value: string) => { const entries = parseStoredNotes(value); return entries[entries.length - 1]; }; it("appends after the existing feed instead of replacing it", () => { const out = buildBackToPendingNote({ currentNotes: seed, note: "supplier pushed it a week", actor: { id: "u1", name: "Dana" }, unassigned: true, unready: true, timestamp: "2026-10-15T08:00:00.000Z", }); const entries = parseStoredNotes(out); expect(entries).toHaveLength(2); expect(entries[0].body).toBe("Collected"); expect(last(out).category).toBe("pending"); expect(last(out).ts).toBe("2026-10-15T08:00:00.000Z"); expect(last(out).actorName).toBe("Dana"); }); it("names both things it cleared", () => { const out = buildBackToPendingNote({ currentNotes: "", note: "supplier pushed it a week", actor: { id: "u1", name: "Dana" }, unassigned: true, unready: true, timestamp: "2026-10-15T08:00:00.000Z", }); expect(last(out).body).toContain("driver unassigned"); expect(last(out).body).toContain("no longer ready for collection"); expect(last(out).body).toContain("supplier pushed it a week"); }); it("only claims what actually changed", () => { const out = buildBackToPendingNote({ currentNotes: "", note: "was never assigned", actor: { id: "u1", name: "Dana" }, unassigned: false, unready: true, timestamp: "2026-10-15T08:00:00.000Z", }); expect(last(out).body).not.toContain("driver unassigned"); expect(last(out).body).toContain("no longer ready"); }); it("still writes an entry when there was nothing to clear", () => { const out = buildBackToPendingNote({ currentNotes: "", note: "operator nudged it", actor: { id: "u1", name: "Dana" }, unassigned: false, unready: false, timestamp: "2026-10-15T08:00:00.000Z", }); expect(last(out).body).toContain("no state change needed"); expect(last(out).body).toContain("operator nudged it"); }); it("starts a fresh feed from a legacy plain-string value", () => { const out = buildBackToPendingNote({ currentNotes: "spoke to David", note: "pushed back", actor: { id: "u1", name: "Dana" }, unassigned: false, unready: true, timestamp: "2026-10-15T08:00:00.000Z", }); const entries = parseStoredNotes(out); expect(entries).toHaveLength(1); expect(entries[0].category).toBe("pending"); expect(entries[0].body).toContain("pushed back"); }); }); describe("formatNotesForDisplay", () => { it("renders the feed as readable lines, newest first", () => { const stored = appendNote("", { ts: "2026-09-01T09:00:00.000Z", actor: { id: "u1", name: "Dana" }, category: "collection", body: "Collected", }); const withSecond = appendNote(stored, { ts: "2026-10-15T21:17:58.000Z", actor: { id: "operator", name: "Operator" }, category: "status", body: "Set the expected-ready date to 21 Oct 2026", }); const lines = formatNotesForDisplay(withSecond); expect(lines).toHaveLength(2); // Newest first. expect(lines[0]).toContain("Operator"); expect(lines[0]).toContain("Set the expected-ready date to 21 Oct 2026"); expect(lines[1]).toContain("Dana"); expect(lines[1]).toContain("Collected"); }); it("never leaks raw JSON to the operator", () => { const stored = JSON.stringify([ { ts: "2026-10-15T21:17:58.000Z", actorId: "operator", actorName: "Operator", category: "status", body: "Marked ready" }, ]); const [line] = formatNotesForDisplay(stored); expect(line).not.toContain('"ts"'); expect(line).not.toContain("actorId"); expect(line).not.toContain("{"); }); it("flattens a multi-line note so each entry stays one line", () => { const stored = JSON.stringify([ { ts: "2026-10-15T21:17:58.000Z", actorId: "operator", actorName: "Operator", category: "status", body: "Set the date — Delivery Date:\n1 for est. delivery\n2", }, ]); const lines = formatNotesForDisplay(stored); expect(lines).toHaveLength(1); expect(lines[0]).not.toContain("\n"); }); it("renders a non-string body object without crashing", () => { const stored = JSON.stringify([ { ts: "2026-10-15T21:17:58.000Z", actorId: "u1", actorName: "Dana", category: "cost", body: { prevCost: 100, newCost: 120 }, }, ]); const [line] = formatNotesForDisplay(stored); expect(line).toContain("120"); }); it("falls back to the raw string for a legacy plain-text value", () => { expect(formatNotesForDisplay("spoke to David")).toEqual(["spoke to David"]); }); it("returns nothing for empty / missing input", () => { expect(formatNotesForDisplay("")).toEqual([]); expect(formatNotesForDisplay(null)).toEqual([]); expect(formatNotesForDisplay(undefined)).toEqual([]); }); });