/* Unit tests for the Lagebild ingress HTTP client. Verifies payload construction, idempotency key generation, and network error handling. Do not make real HTTP calls — fetch is mocked. RFC-1311: Compass header sweep — declares the lagebild suite's contract for test-file scaffold coverage. */ import { test, expect, vi } from "vitest"; import { submitIngress, buildIdempotencyKey, LAGEBILD_INGRESS_CONTRACT_VERSION, type LagebildIngressConfig, } from "../lagebild-ingress.ts"; import { lagebildDestinationAdapter, funnelPayloadToProcessHint, } from "../lagebild-destination-adapter.ts"; import type { IntegrationEvent } from "../port.ts"; const baseConfig: LagebildIngressConfig = { apiUrl: "https://lagebild-api.example.workers.dev", apiKey: "lbk_test_key_0001", tenantId: "ten_warpgogol000001", sourceSystemId: "site_warpgogol_com", }; test("buildIdempotencyKey is stable and combines tenant + submission", () => { const key = buildIdempotencyKey("ten_abc", "sub_123"); expect(key).toBe("ten_abc:sub_123"); expect(buildIdempotencyKey("ten_abc", "sub_123")).toBe(key); }); test("submitIngress sends POST with correct headers and payload", async () => { const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response( JSON.stringify({ submission_id: "sub_test00000000000001", accepted: true, is_replay: false, correlation_id: null, }), { status: 200, headers: { "Content-Type": "application/json" } }, ), ); const result = await submitIngress(baseConfig, { submissionId: "sub_test00000000000001", idempotencyKey: "ten_warpgogol000001:sub_test00000000000001", interactionKind: "contact_message", identityClaims: [ { claim_type: "email", value: "test@example.com" }, { claim_type: "person_name", value: "Test User" }, ], message: "Hello from the test", origin: { page_uri: "https://example.com/contact", locale: "de" }, }); expect(result.accepted).toBe(true); expect(result.is_replay).toBe(false); expect(fetchSpy).toHaveBeenCalledOnce(); const [url, init] = fetchSpy.mock.calls[0] as [string, RequestInit]; expect(url).toBe("https://lagebild-api.example.workers.dev/v1/ingress"); expect(init.method).toBe("POST"); expect((init.headers as Record)["Authorization"]).toBe( "Bearer lbk_test_key_0001", ); const body = JSON.parse(init.body as string) as Record; expect(body["contract_version"]).toBe(LAGEBILD_INGRESS_CONTRACT_VERSION); expect(body["tenant_id"]).toBe("ten_warpgogol000001"); expect(body["source_system_id"]).toBe("site_warpgogol_com"); expect(body["interaction_kind"]).toBe("contact_message"); fetchSpy.mockRestore(); }); test("submitIngress serializes processHint verbatim as process_hint (RFC-1239)", async () => { const fetchSpy = vi .spyOn(globalThis, "fetch") .mockResolvedValue( new Response(JSON.stringify({ accepted: true, is_replay: false }), { status: 201 }), ); await submitIngress(baseConfig, { submissionId: "sub_hint0000000000001", idempotencyKey: "ten:hint1", interactionKind: "other", identityClaims: [{ claim_type: "email", value: "t@example.com" }], processHint: { schema: "process-hint/v1", system: "werkstatt", catalog_ref: "werkstatt:visitor-funnel/v1", phase: "offer_presented", previous_phase: "qualification_region", changed_at: "2026-10-08T10:00:00.000Z", reason: "qualification.answered", }, }); const body = JSON.parse( (fetchSpy.mock.calls[0] as [string, RequestInit])[1].body as string, ) as Record; expect(body["process_hint"]).toEqual({ schema: "process-hint/v1", system: "werkstatt", catalog_ref: "werkstatt:visitor-funnel/v1", phase: "offer_presented", previous_phase: "qualification_region", changed_at: "2026-10-08T10:00:00.000Z", reason: "qualification.answered", }); expect(body["contract_version"]).toBe(1); vi.restoreAllMocks(); }); // --------------------------------------------------------------------------- // RFC-1239 / spec v0.1.1 V04 (handoffs.yaml H02): funnel transitions → // process_hint on the wire, built by the destination adapter. // --------------------------------------------------------------------------- const secrets = { LAGEBILD_API_URL: "https://lagebild-api.example.workers.dev", LAGEBILD_API_KEY: "lbk_test_key_0001", LAGEBILD_TENANT_ID: "ten_warpgogol000001", LAGEBILD_SOURCE_SYSTEM_ID: "site_warpgogol_com", }; function funnelEvent(payload: Record): IntegrationEvent { return { eventId: "evt-funnel-0000000000000001", kind: "message", source: "uchat", locale: "de", occurredAt: "2026-10-08T10:00:00.000Z", payload, }; } async function captureIngressBody(event: IntegrationEvent): Promise> { const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response( JSON.stringify({ submission_id: "sub_evt00000000000000001", accepted: true, is_replay: false, }), { status: 201 }, ), ); await lagebildDestinationAdapter.route(event, secrets); const [, init] = fetchSpy.mock.calls[0] as [string, RequestInit]; vi.restoreAllMocks(); return JSON.parse(init.body as string) as Record; } test("funnel transition emits process_hint with reason=eventKind (AC-1, AC-5)", async () => { const body = await captureIngressBody( funnelEvent({ funnelVersion: "1.0.0", eventKind: "qualification.answered", stage: "qualification_company", previousStage: "qualification_priority", contact: { email: "visitor@example.com" }, }), ); expect(body["process_hint"]).toEqual({ schema: "process-hint/v1", system: "werkstatt", catalog_ref: "werkstatt:visitor-funnel/v1", phase: "qualification_company", previous_phase: "qualification_priority", changed_at: "2026-10-08T10:00:00.000Z", reason: "qualification.answered", }); // Fallback: claims came from payload.contact, not event.contact. expect(body["identity_claims"]).toEqual([{ claim_type: "email", value: "visitor@example.com" }]); // Funnel-only submission (no message) — honest kind, not contact_message. expect(body["interaction_kind"]).toBe("other"); }); test("first observed stage emits hint without previous_phase", async () => { const body = await captureIngressBody( funnelEvent({ funnelVersion: "1.0.0", eventKind: "privacy.acknowledged", stage: "privacy_acknowledged", contact: { email: "v@example.com" }, }), ); const hint = body["process_hint"] as Record; expect(hint["phase"]).toBe("privacy_acknowledged"); expect("previous_phase" in hint).toBe(false); }); test("no hint when stage equals previousStage, stage absent, or event is not a funnel payload (AC-3)", async () => { const noTransition = await captureIngressBody( funnelEvent({ funnelVersion: "1.0.0", eventKind: "operator.note.added", stage: "operator_review", previousStage: "operator_review", }), ); expect("process_hint" in noTransition).toBe(false); const noStage = await captureIngressBody( funnelEvent({ funnelVersion: "1.0.0", eventKind: "language.selected" }), ); expect("process_hint" in noStage).toBe(false); const plainLead = await captureIngressBody({ eventId: "evt-lead-00000000000000001", kind: "lead", source: "send-message", locale: "de", occurredAt: "2026-10-08T10:00:00.000Z", contact: { email: "lead@example.com" }, payload: { message: "plain contact form" }, }); expect("process_hint" in plainLead).toBe(false); expect(plainLead["interaction_kind"]).toBe("contact_message"); }); // --------------------------------------------------------------------------- // RFC-1241 / spec v0.1.4 V05: acquisition-context on the wire — adapter // pass-through with structural validation; malformed degrades to absent. // --------------------------------------------------------------------------- test("adapter passes a valid payload.acquisition through as the acquisition wire field (RFC-1241, AC-3)", async () => { const body = await captureIngressBody( funnelEvent({ funnelVersion: "1.0.0", eventKind: "material.submitted", stage: "materials_requested", contact: { email: "v@example.com" }, acquisition: { schema: "acquisition-context/v1", utm_source: "meta", utm_medium: "paid_social", utm_campaign: "sit001-bw-v1", utm_term: "-", utm_content: "cre_b3d8e2f1", utm_id: "exp_7f2a91c4", }, }), ); expect(body["acquisition"]).toEqual({ schema: "acquisition-context/v1", utm_source: "meta", utm_medium: "paid_social", utm_campaign: "sit001-bw-v1", utm_term: "-", utm_content: "cre_b3d8e2f1", utm_id: "exp_7f2a91c4", }); }); test("adapter omits malformed payload.acquisition without throwing (RFC-1241, AC-3)", async () => { const missingKey = await captureIngressBody( funnelEvent({ contact: { email: "v@example.com" }, acquisition: { schema: "acquisition-context/v1", utm_source: "meta" }, }), ); expect("acquisition" in missingKey).toBe(false); const extraKey = await captureIngressBody( funnelEvent({ contact: { email: "v@example.com" }, acquisition: { schema: "acquisition-context/v1", utm_source: "meta", utm_medium: "cpc", utm_campaign: "c", utm_term: "-", utm_content: "-", utm_id: "-", fbclid: "IwAR123", // platform click ids are inexpressible by contract }, }), ); expect("acquisition" in extraKey).toBe(false); const badCharset = await captureIngressBody( funnelEvent({ contact: { email: "v@example.com" }, acquisition: { schema: "acquisition-context/v1", utm_source: "meta ads", // space violates the contract charset utm_medium: "cpc", utm_campaign: "c", utm_term: "-", utm_content: "-", utm_id: "-", }, }), ); expect("acquisition" in badCharset).toBe(false); }); test("submitIngress serializes input.acquisition verbatim as acquisition (RFC-1241, AC-3)", async () => { const fetchSpy = vi .spyOn(globalThis, "fetch") .mockResolvedValue( new Response(JSON.stringify({ accepted: true, is_replay: false }), { status: 201 }), ); await submitIngress(baseConfig, { submissionId: "sub_acq00000000000001", idempotencyKey: "ten:acq1", interactionKind: "contact_message", identityClaims: [{ claim_type: "email", value: "t@example.com" }], acquisition: { schema: "acquisition-context/v1", utm_source: "newsletter", utm_medium: "email", utm_campaign: "launch-2026", utm_term: "-", utm_content: "-", utm_id: "-", }, }); const body = JSON.parse( (fetchSpy.mock.calls[0] as [string, RequestInit])[1].body as string, ) as Record; expect(body["acquisition"]).toEqual({ schema: "acquisition-context/v1", utm_source: "newsletter", utm_medium: "email", utm_campaign: "launch-2026", utm_term: "-", utm_content: "-", utm_id: "-", }); expect(body["contract_version"]).toBe(1); vi.restoreAllMocks(); }); test("system is the literal werkstatt — personal_agent can never be emitted (AC-3)", () => { const hint = funnelPayloadToProcessHint( { funnelVersion: "1.0.0", eventKind: "operator.won", stage: "won", previousStage: "operator_review", }, "2026-10-08T11:00:00.000Z", ); expect(hint?.system).toBe("werkstatt"); expect(hint?.phase).toBe("won"); }); test("submitIngress returns accepted=false on network error", async () => { vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("connection refused")); const result = await submitIngress(baseConfig, { submissionId: "sub_test00000000000002", idempotencyKey: "ten:test2", interactionKind: "inquiry", identityClaims: [{ claim_type: "email", value: "test@example.com" }], }); expect(result.accepted).toBe(false); expect(result.rejection_class).toBe("network_error"); vi.restoreAllMocks(); }); test("submitIngress returns accepted=false on 400 response", async () => { vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response( JSON.stringify({ submission_id: "sub_test00000000000003", accepted: false, is_replay: false, rejection_class: "schema_invalid", correlation_id: null, }), { status: 400, headers: { "Content-Type": "application/json" } }, ), ); const result = await submitIngress(baseConfig, { submissionId: "sub_test00000000000003", idempotencyKey: "ten:test3", interactionKind: "inquiry", identityClaims: [{ claim_type: "email", value: "bad" }], }); expect(result.accepted).toBe(false); expect(result.rejection_class).toBe("schema_invalid"); vi.restoreAllMocks(); });