import assert from "node:assert/strict"; import test from "node:test"; import type { SchedulingRequest } from "../src/api/scheduling.ts"; import { applySchedulingAvailabilityChoice, groupSchedulingRequests, participantDraftFromResponse, participantDraftsFromPicker, participantPayload, schedulingSortPhase, type SchedulingActor } from "../src/features/scheduling/schedulingViewModel.ts"; test("maps visible account and contact selections into bounded scheduling participant payloads", () => { let sequence = 0; const selected = participantDraftsFromPicker([ { selection_key: "account:account-2", kind: "account", reference_id: "account-2", display_name: "Ada Account", email: "ADA@EXAMPLE.TEST", source_module: "access", source_label: "Accounts", provenance: { tenant_id: "must-not-be-persisted" }, metadata: { group_ids: ["must-not-be-persisted"] } }, { selection_key: "contact:contact-3:contact@example.test", kind: "contact", reference_id: "contact-3", display_name: "Contact Person", email: "contact@example.test", source_module: "addresses", source_label: "Contacts", source_revision: "revision-3", provenance: { address_book_id: "must-not-be-persisted" } } ], [], () => `participant-${++sequence}`); assert.equal(selected[0].respondent_id, "account-2"); assert.equal(selected[0].participant_type, "internal"); assert.equal(selected[0].email, "ada@example.test"); assert.deepEqual(selected[0].metadata, { directory_selection: { selection_key: "account:account-2", kind: "account", reference_id: "account-2", source_module: "access", source_label: "Accounts", source_revision: null } }); assert.equal(selected[1].respondent_id, null); assert.equal(selected[1].participant_type, "external"); assert.equal( (selected[1].metadata?.directory_selection as { source_revision: string }).source_revision, "revision-3" ); assert.deepEqual(participantPayload(selected[1]), { respondent_id: null, display_name: "Contact Person", email: "contact@example.test", participant_type: "external", required: true, metadata: selected[1].metadata }); }); test("reconstructs saved directory selections and preserves existing reconciliation identity", () => { const responseParticipant = { id: "stored-participant", is_current_participant: false, respondent_id: "account-2", display_name: "Ada Account", email: "ada@example.test", participant_type: "internal", required: true, status: "invited", poll_invitation_id: "invitation-1", metadata: { directory_selection: { selection_key: "account:account-2", kind: "account", reference_id: "account-2", source_module: "access", source_label: "Accounts" } } }; const draft = participantDraftFromResponse(responseParticipant, "draft-1"); const remapped = participantDraftsFromPicker([draft], [draft], () => "unexpected"); assert.equal(remapped[0], draft); assert.equal(draft.sourceId, "stored-participant"); assert.equal(draft.identityLocked, true); assert.equal(participantPayload(draft).id, "stored-participant"); }); const now = new Date("2026-07-20T10:00:00Z"); const actor: SchedulingActor = { accountId: "account-1", membershipId: "membership-1", email: "person@example.test" }; function request( id: string, options: { organizer?: string; participantStatus?: string; status?: SchedulingRequest["status"]; start?: string; end?: string; } = {} ): SchedulingRequest { return { id, tenant_id: "tenant-1", title: id, timezone: "UTC", status: options.status ?? "collecting", organizer_user_id: options.organizer ?? "organizer-elsewhere", allow_external_participants: true, allow_participant_updates: true, result_visibility: "after_close", participant_visibility: "aggregates_only", effective_participant_visibility: "aggregates_only", participant_aggregate: { total: options.participantStatus ? 1 : 0, status_counts: options.participantStatus ? { [options.participantStatus]: 1 } : {} }, participant_visibility_decision: { requested_visibility: "aggregates_only", effective_visibility: "aggregates_only", policy_applied: false, source_path: [], details: {} }, notify_on_answers: true, single_choice: false, max_participants_per_option: null, allow_maybe: true, allow_comments: false, participant_email_required: false, anonymous_password_protection_enabled: false, public_participation_policy_enforcement_available: false, public_participation_policy_enforcement_reason: "Public participation gateway not installed", calendar_integration_enabled: false, calendar_freebusy_enabled: false, calendar_hold_enabled: false, create_calendar_event_on_decision: false, created_at: "2026-07-01T00:00:00Z", updated_at: "2026-07-01T00:00:00Z", metadata: {}, slots: [{ id: `slot-${id}`, label: id, start_at: options.start ?? "2026-07-21T09:00:00Z", end_at: options.end ?? "2026-07-21T10:00:00Z", timezone: "UTC", position: 0, revision: "0".repeat(64), freebusy_conflicts: [], metadata: {} }], participants: options.participantStatus ? [{ id: `participant-${id}`, is_current_participant: true, respondent_id: "membership-1", email: "person@example.test", participant_type: "internal", required: true, status: options.participantStatus, metadata: {} }] : [] }; } test("groups owned, invited, and administrator-only requests without hiding any", () => { const groups = groupSchedulingRequests([ request("managed"), request("mine", { organizer: "account-1" }), request("invited", { participantStatus: "invited" }) ], actor, now); assert.deepEqual(groups.owned.map((item) => item.id), ["mine"]); assert.deepEqual(groups.invited.map((item) => item.id), ["invited"]); assert.deepEqual(groups.other.map((item) => item.id), ["managed"]); }); test("matches email-only invitations without case sensitivity", () => { const invited = request("email-case", { participantStatus: "invited" }); invited.participants[0].is_current_participant = false; invited.participants[0].respondent_id = null; invited.participants[0].email = "Person@Example.Test"; const groups = groupSchedulingRequests([invited], actor, now); assert.deepEqual(groups.invited.map((item) => item.id), ["email-case"]); assert.deepEqual(groups.other, []); }); test("recognizes the current participant after identity bindings are redacted", () => { const invited = request("safe-projection", { participantStatus: "invited" }); invited.participants[0].respondent_id = null; invited.participants[0].email = null; const groups = groupSchedulingRequests([invited], actor, now); assert.deepEqual(groups.invited.map((item) => item.id), ["safe-projection"]); assert.deepEqual(groups.other, []); }); test("keeps an organizer's active request in the open phase even if they also responded", () => { const owned = request("mine-and-invited", { organizer: "account-1", participantStatus: "responded" }); assert.equal(schedulingSortPhase(owned, actor, now), "unanswered"); }); test("orders unanswered by nearest slot before answered, closed, determined, and past", () => { const groups = groupSchedulingRequests([ request("past", { participantStatus: "invited", start: "2026-07-18T09:00:00Z", end: "2026-07-18T10:00:00Z" }), request("determined", { participantStatus: "invited", status: "decided" }), request("closed", { participantStatus: "invited", status: "closed" }), request("answered", { participantStatus: "responded" }), request("later", { participantStatus: "invited", start: "2026-07-23T09:00:00Z" }), request("nearer", { participantStatus: "invited", start: "2026-07-21T09:00:00Z" }) ], actor, now); assert.deepEqual(groups.invited.map((item) => item.id), [ "nearer", "later", "answered", "closed", "determined", "past" ]); assert.equal(schedulingSortPhase(groups.invited[0], actor, now), "unanswered"); assert.equal(schedulingSortPhase(groups.invited.at(-1)!, actor, now), "past"); }); test("single-choice availability keeps one positive choice while preserving explicit no answers", () => { const next = applySchedulingAvailabilityChoice( ["slot-a", "slot-b", "slot-c"], { "slot-a": "available", "slot-b": "maybe", "slot-c": "unavailable" }, "slot-b", "available", true ); assert.deepEqual(next, { "slot-a": "unavailable", "slot-b": "available", "slot-c": "unavailable" }); }); test("multi-choice availability changes only the selected slot", () => { const next = applySchedulingAvailabilityChoice( ["slot-a", "slot-b"], { "slot-a": "available" }, "slot-b", "maybe", false ); assert.deepEqual(next, { "slot-a": "available", "slot-b": "maybe" }); });