import { campaignJsonForAttachmentPreview } from "../src/features/campaigns/utils/templatePreviewDraft"; import { checkContentLibraryCompatibility } from "../src/features/campaigns/utils/contentLibraryCompatibility"; import { campaignVersionUpdateForRequest, clientCampaignEditorState } from "../src/features/campaigns/utils/editorState"; function assert(condition: unknown, message = "assertion failed"): void { if (!condition) throw new Error(message); } function asRecord(value: unknown): Record { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; } const draft = { recipients: { from: { name: "", email: "" }, to: [{ name: "Bob", email: " bob@example.org " }, { name: "", email: "" }] }, entries: { inline: [{ id: "recipient-1", active: true, name: "", email: "", from: [], to: [{ name: "", email: "" }, { name: "Alice", email: " alice@example.org " }], cc: [], bcc: [], merge_to: false }], defaults: { name: "", email: "", to: [{ name: "", email: "" }], attachments: [] } } }; const normalized = campaignJsonForAttachmentPreview(draft); const entries = asRecord(normalized.entries); const inline = entries.inline as Record[]; const entry = inline[0]; const to = entry.to as Record[]; const defaults = asRecord(entries.defaults); const recipients = asRecord(normalized.recipients); const globalTo = recipients.to as Record[]; assert(recipients.from === undefined, "empty global from object is stripped"); assert(globalTo.length === 1 && globalTo[0].email === "bob@example.org", "global recipient arrays are sanitized"); assert(entry.email === undefined, "empty legacy entry email is stripped"); assert(entry.name === undefined, "empty legacy entry name is stripped"); assert(Array.isArray(entry.from) && (entry.from as unknown[]).length === 0, "empty address arrays remain arrays"); assert(to.length === 1 && to[0].email === "alice@example.org", "empty recipient objects are removed and valid addresses are trimmed"); assert(defaults.email === undefined, "empty defaults email is stripped"); assert(Array.isArray(defaults.to) && (defaults.to as unknown[]).length === 0, "empty defaults recipients are removed"); assert((asRecord(draft.entries).inline as Record[])[0].email === "", "original draft is not mutated"); const compatibility = checkContentLibraryCompatibility([ { path: "local::display_name", value_type: "string", label: "Display name", required: true }, { path: "global.case_reference", value_type: "string", label: "Case reference", required: true }, { path: "count", value_type: "number", label: "Count", required: true }, { path: "local.to.2.email", value_type: "string", label: "Second recipient", required: true }, { path: "optional", value_type: "string", required: false } ], new Map([ ["local:display_name", "string"], ["local:to", "string"], ["count", "boolean"] ])); assert(!compatibility.compatible, "missing or wrong-typed fields are incompatible"); assert(compatibility.missing.length === 1 && compatibility.missing[0].path === "global:case_reference", "namespaced missing fields are normalized"); assert(compatibility.incompatible.length === 1 && compatibility.incompatible[0].actual === "boolean", "type mismatches are reported"); // Ordinary saves must not echo the server-owned evidence returned by reads. // This is the state that previously broke saves when leaving the Template page. const loadedEditorState = { created_from: "minimal_campaign", field_overrides: { department: false }, opt_ins: { inline_guidance: true }, review_send: { inspection_complete: true, reviewed_message_keys: ["gabriele.fode", "fruzsina.molnar-gabor"], issue_decisions: [], updated_at: "2026-07-15T11:23:31.569257+00:00", updated_by_user_id: "064fa105-9e69-4f29-8558-a7665eb219df" }, approval_gate: { request_id: "approval-1", requested_by_user_id: "approver-1" }, smtp: { password: "legacy-secret" } }; const clientState = clientCampaignEditorState(loadedEditorState); assert(JSON.stringify(clientState) === JSON.stringify({ created_from: "minimal_campaign", field_overrides: { department: false }, opt_ins: { inline_guidance: true } }), "draft and merge metadata contains only client-owned fields"); const savePayload = { editor_state: loadedEditorState, base_revision: 7, campaign_json: { template: { subject: "Updated template" } }, migrate_legacy_mail_settings: true }; const requestPayload = campaignVersionUpdateForRequest(savePayload); assert(JSON.stringify(requestPayload.editor_state) === JSON.stringify(clientState), "save, autosave, and fork requests omit review and approval evidence"); assert(!JSON.stringify(requestPayload).includes("legacy-secret"), "unknown legacy metadata is not resubmitted"); assert(requestPayload.base_revision === 7 && requestPayload.migrate_legacy_mail_settings, "concurrency and explicit migration controls are preserved"); assert(requestPayload.campaign_json === savePayload.campaign_json, "campaign content is not rewritten by metadata serialization"); assert(savePayload.editor_state.review_send.inspection_complete, "read response evidence is not mutated"); assert("approval_gate" in savePayload.editor_state, "read response approval evidence is preserved"); assert(Object.keys(clientCampaignEditorState(undefined)).length === 0, "new drafts need no metadata"); const omittedEditorState = { base_revision: 2, editor_state: undefined }; assert(campaignVersionUpdateForRequest(omittedEditorState) === omittedEditorState, "omitted metadata remains omitted"); const nullEditorState = { editor_state: null }; assert(campaignVersionUpdateForRequest(nullEditorState) === nullEditorState, "null metadata retains its unchanged-state semantics");