import { describe, expect, it } from "bun:test"; import { appEventFlowCompletedUnknownVersionSchema, appEventFlowCompletedV1Schema, appEventFlowDismissedUnknownVersionSchema, appEventFlowDismissedV1Schema, appEventFlowViewedV1Schema, appEventPayloadSchema, appEventTrackSchema, buildFlowCompletedV1, buildFlowCompletedV1AppEvent, buildFlowDismissedV1, buildFlowDismissedV1AppEvent, buildFlowViewedV1, buildFlowViewedV1AppEvent, FLOW_COMPLETED_EVENT_NAME, FLOW_COMPLETED_EVENT_SCHEMA_ID, FLOW_COMPLETED_EVENT_SCHEMA_VERSION, FLOW_DISMISSED_EVENT_NAME, FLOW_DISMISSED_EVENT_SCHEMA_ID, FLOW_DISMISSED_EVENT_SCHEMA_VERSION, FLOW_VIEWED_EVENT_NAME, FLOW_VIEWED_EVENT_SCHEMA_ID, FLOW_VIEWED_EVENT_SCHEMA_VERSION, flowCompletedV1ContractSchema, flowCompletedV1InputSchema, flowDismissedV1ContractSchema, flowDismissedV1InputSchema, flowViewedV1ContractSchema, flowViewedV1InputSchema, } from "./index.js"; const commonInput = { flowId: "flow_1", flowVersionId: "flow_version_1", flowVersionNumber: 3, flowRunId: "flow_run_1", flowSurface: "widget" as const, }; const commonProperties = { gx_flow_id: "flow_1", gx_flow_version_id: "flow_version_1", gx_flow_version_number: 3, gx_flow_run_id: "flow_run_1", gx_flow_surface: "widget", }; describe("Flow lifecycle v1 app-event contracts", () => { it("builds exact v1 lifecycle envelopes with the common Flow coordinates", () => { const viewed = buildFlowViewedV1(commonInput); const completed = buildFlowCompletedV1({ ...commonInput, flowCompletionId: "flcmp_1", }); const dismissed = buildFlowDismissedV1(commonInput); expect(viewed).toEqual({ event: FLOW_VIEWED_EVENT_NAME, eventSchema: FLOW_VIEWED_EVENT_SCHEMA_ID, eventSchemaVersion: FLOW_VIEWED_EVENT_SCHEMA_VERSION, properties: { gx_event_schema: "gx_flow_viewed", gx_event_schema_version: 1, ...commonProperties, }, }); expect(completed).toEqual({ event: FLOW_COMPLETED_EVENT_NAME, eventSchema: FLOW_COMPLETED_EVENT_SCHEMA_ID, eventSchemaVersion: FLOW_COMPLETED_EVENT_SCHEMA_VERSION, properties: { gx_event_schema: "gx_flow_completed", gx_event_schema_version: 1, ...commonProperties, gx_flow_completion_id: "flcmp_1", }, }); expect(dismissed).toEqual({ event: FLOW_DISMISSED_EVENT_NAME, eventSchema: FLOW_DISMISSED_EVENT_SCHEMA_ID, eventSchemaVersion: FLOW_DISMISSED_EVENT_SCHEMA_VERSION, properties: { gx_event_schema: "gx_flow_dismissed", gx_event_schema_version: 1, ...commonProperties, }, }); expect( buildFlowViewedV1({ ...commonInput, flowSurface: "web_page", }).properties.gx_flow_surface, ).toBe("web_page"); expect( flowViewedV1InputSchema.safeParse({ ...commonInput, flowSurface: "email", }).success, ).toBe(false); }); it("keeps completion identity exclusive to Flow Completed", () => { const viewed = buildFlowViewedV1(commonInput); const dismissed = buildFlowDismissedV1(commonInput); expect("gx_flow_completion_id" in viewed.properties).toBe(false); expect("gx_flow_completion_id" in dismissed.properties).toBe(false); expect( flowViewedV1ContractSchema.safeParse({ ...viewed, properties: { ...viewed.properties, gx_flow_completion_id: "flcmp_forbidden", }, }).success, ).toBe(false); expect( flowDismissedV1ContractSchema.safeParse({ ...dismissed, properties: { ...dismissed.properties, gx_flow_completion_id: "flcmp_forbidden", }, }).success, ).toBe(false); expect(flowCompletedV1InputSchema.safeParse(commonInput).success).toBe( false, ); }); it("accepts unknown Flow Completed versions only through the internal schema", () => { const unknown = { origin: "system" as const, event: FLOW_COMPLETED_EVENT_NAME, eventSchema: FLOW_COMPLETED_EVENT_SCHEMA_ID, eventSchemaVersion: 2, messageId: "gx_flow_completed:v2:future", timestamp: 1_786_000_000_000, properties: { gx_event_schema: FLOW_COMPLETED_EVENT_SCHEMA_ID, gx_event_schema_version: 2, gx_flow_completion_id: "future_completion", gx_future_fact: true, }, }; expect(appEventFlowCompletedUnknownVersionSchema.parse(unknown)).toEqual( unknown, ); expect(appEventTrackSchema.safeParse(unknown).success).toBe(false); expect(appEventPayloadSchema.safeParse(unknown).success).toBe(false); expect( appEventFlowCompletedUnknownVersionSchema.safeParse({ ...unknown, properties: { ...unknown.properties, gx_event_schema_version: 3 }, }).success, ).toBe(false); expect( appEventFlowCompletedUnknownVersionSchema.parse({ ...unknown, properties: { ...unknown.properties, gx_response_id: "future_meaning", future_fact: true, }, }).properties, ).toMatchObject({ gx_response_id: "future_meaning", future_fact: true, }); }); it("reserves occurrence identity outside all lifecycle properties", () => { const viewedContract = buildFlowViewedV1(commonInput); const completedContract = buildFlowCompletedV1({ ...commonInput, flowCompletionId: "flcmp_1", }); const dismissedContract = buildFlowDismissedV1(commonInput); const viewed = buildFlowViewedV1AppEvent({ event: viewedContract, flowOccurrenceId: "view_1", timestamp: 1_786_000_000_000, }); const dismissed = buildFlowDismissedV1AppEvent({ event: dismissedContract, flowOccurrenceId: "view_1", timestamp: 1_786_000_000_001, }); const completed = buildFlowCompletedV1AppEvent({ event: completedContract, timestamp: 1_786_000_000_002, }); for (const { schema, event } of [ { schema: flowViewedV1ContractSchema, event: viewedContract }, { schema: flowCompletedV1ContractSchema, event: completedContract }, { schema: flowDismissedV1ContractSchema, event: dismissedContract }, ]) { expect( schema.safeParse({ ...event, properties: { ...event.properties, gx_flow_occurrence_id: "view_1", }, }).success, ).toBe(false); } for (const { schema, event } of [ { schema: appEventFlowViewedV1Schema, event: viewed }, { schema: appEventFlowCompletedV1Schema, event: completed }, { schema: appEventFlowDismissedV1Schema, event: dismissed }, ]) { expect( schema.safeParse({ ...event, properties: { ...event.properties, gx_flow_occurrence_id: "view_1", }, }).success, ).toBe(false); } }); it("uses strict trusted inputs and rejects response, Ending, and arbitrary aliases", () => { for (const schema of [ flowViewedV1InputSchema, flowDismissedV1InputSchema, ]) { const input = commonInput; for (const extra of [ { responseId: "response_1" }, { endingId: "ending_1" }, { flowOccurrenceId: "view_1" }, { arbitrary: true }, ]) { expect(schema.safeParse({ ...input, ...extra }).success).toBe(false); } } for (const extra of [ { responseId: "response_1" }, { endingId: "ending_1" }, { arbitrary: true }, ]) { expect( flowCompletedV1InputSchema.safeParse({ ...commonInput, flowCompletionId: "flcmp_1", ...extra, }).success, ).toBe(false); } }); it("allows additive GX properties but rejects response, Ending, and non-GX properties", () => { const contracts = [ { schema: flowViewedV1ContractSchema, event: buildFlowViewedV1(commonInput), }, { schema: flowCompletedV1ContractSchema, event: buildFlowCompletedV1({ ...commonInput, flowCompletionId: "flcmp_1", }), }, { schema: flowDismissedV1ContractSchema, event: buildFlowDismissedV1(commonInput), }, ]; for (const { schema, event } of contracts) { expect( schema.safeParse({ ...event, properties: { ...event.properties, gx_future_fact: "future" }, }).success, ).toBe(true); for (const properties of [ { response_id: "response_1" }, { gx_response_id: "response_1" }, { gx_flow_ending_id: "ending_1" }, ]) { expect( schema.safeParse({ ...event, properties: { ...event.properties, ...properties }, }).success, ).toBe(false); } } }); it("derives stable message IDs without exposing occurrence identity", () => { const viewedContract = buildFlowViewedV1(commonInput); const completedContract = buildFlowCompletedV1({ ...commonInput, flowCompletionId: "flcmp_1", }); const dismissedContract = buildFlowDismissedV1(commonInput); const viewed = buildFlowViewedV1AppEvent({ event: viewedContract, flowOccurrenceId: "view_1", timestamp: 1_786_000_000_000, }); const completed = buildFlowCompletedV1AppEvent({ event: completedContract, timestamp: 1_786_000_000_001, }); const dismissed = buildFlowDismissedV1AppEvent({ event: dismissedContract, flowOccurrenceId: "view_1", timestamp: 1_786_000_000_002, }); expect(viewed.messageId).toBe("gx_flow_viewed:v1:view_1"); expect(completed.messageId).toBe("gx_flow_completed:v1:flcmp_1"); expect(dismissed.messageId).toBe("gx_flow_dismissed:v1:view_1"); expect(viewed.origin).toBe("system"); expect(completed.origin).toBe("system"); expect(dismissed.origin).toBe("system"); expect("gx_flow_occurrence_id" in viewed.properties).toBe(false); expect("gx_flow_occurrence_id" in dismissed.properties).toBe(false); expect("references" in viewed).toBe(false); expect("references" in completed).toBe(false); expect("references" in dismissed).toBe(false); expect( appEventFlowViewedV1Schema.safeParse({ ...viewed, messageId: "wrong:view_1", }).success, ).toBe(false); expect( appEventFlowCompletedV1Schema.safeParse({ ...completed, messageId: "gx_flow_completed:v1:flcmp_other", }).success, ).toBe(false); expect( appEventFlowDismissedV1Schema.safeParse({ ...dismissed, messageId: "gx_flow_dismissed:v1:", }).success, ).toBe(false); }); it("revalidates contract inputs before app-event construction", () => { const viewed = buildFlowViewedV1(commonInput); const completed = buildFlowCompletedV1({ ...commonInput, flowCompletionId: "flcmp_1", }); const dismissed = buildFlowDismissedV1(commonInput); const widenedFields = [ { traits: { plan: "growth" } }, { identities: [{ type: "userId", value: "user_1" }] }, { context: { locale: "en" } }, { arbitrary: true }, ]; for (const widened of widenedFields) { expect(() => buildFlowViewedV1AppEvent({ event: { ...viewed, ...widened } as typeof viewed, flowOccurrenceId: "view_1", timestamp: 1_786_000_000_000, }), ).toThrow(); expect(() => buildFlowCompletedV1AppEvent({ event: { ...completed, ...widened } as typeof completed, timestamp: 1_786_000_000_001, }), ).toThrow(); expect(() => buildFlowDismissedV1AppEvent({ event: { ...dismissed, ...widened } as typeof dismissed, flowOccurrenceId: "view_1", timestamp: 1_786_000_000_002, }), ).toThrow(); } }); it("rejects legacy references and unknown top-level lifecycle fields", () => { const events = [ { event: buildFlowViewedV1AppEvent({ event: buildFlowViewedV1(commonInput), flowOccurrenceId: "view_1", timestamp: 1_786_000_000_000, }), schema: appEventFlowViewedV1Schema, }, { event: buildFlowCompletedV1AppEvent({ event: buildFlowCompletedV1({ ...commonInput, flowCompletionId: "flcmp_1", }), timestamp: 1_786_000_000_001, }), schema: appEventFlowCompletedV1Schema, }, { event: buildFlowDismissedV1AppEvent({ event: buildFlowDismissedV1(commonInput), flowOccurrenceId: "view_1", timestamp: 1_786_000_000_002, }), schema: appEventFlowDismissedV1Schema, }, ]; const forbiddenTopLevelFields = [ { responseId: "response_1" }, { endingId: "ending_1" }, { gx_response_id: "response_1" }, { gx_flow_ending_id: "ending_1" }, { references: [{ response: { id: "response_1" } }] }, { arbitrary: true }, ]; for (const { schema, event } of events) { for (const forbidden of forbiddenTopLevelFields) { expect(schema.safeParse({ ...event, ...forbidden }).success).toBe( false, ); } } }); it("bounds timestamps and preserves exact and future lifecycle versions", () => { const viewed = buildFlowViewedV1AppEvent({ event: buildFlowViewedV1(commonInput), flowOccurrenceId: "view_1", timestamp: 1_786_000_000_000, }); const lifecycleEvents = [ { event: viewed, schema: appEventFlowViewedV1Schema }, { event: buildFlowCompletedV1AppEvent({ event: buildFlowCompletedV1({ ...commonInput, flowCompletionId: "flcmp_1", }), timestamp: 1_786_000_000_001, }), schema: appEventFlowCompletedV1Schema, }, { event: buildFlowDismissedV1AppEvent({ event: buildFlowDismissedV1(commonInput), flowOccurrenceId: "view_1", timestamp: 1_786_000_000_002, }), schema: appEventFlowDismissedV1Schema, }, ]; for (const { event, schema } of lifecycleEvents) { for (const timestamp of [-1, 8_640_000_000_000_001]) { expect(schema.safeParse({ ...event, timestamp }).success).toBe(false); } } expect(appEventTrackSchema.safeParse(viewed).success).toBe(true); expect(appEventPayloadSchema.safeParse(viewed).success).toBe(true); const dismissed = lifecycleEvents[2]?.event; expect(appEventTrackSchema.safeParse(dismissed).success).toBe(true); expect(appEventPayloadSchema.safeParse(dismissed).success).toBe(true); for (const { event } of lifecycleEvents.slice(1, 2)) { expect(appEventTrackSchema.safeParse(event).success).toBe(false); expect(appEventPayloadSchema.safeParse(event).success).toBe(false); } const unknown = { ...dismissed, eventSchemaVersion: 2, messageId: "gx_flow_dismissed:v2:future", properties: { gx_event_schema: "gx_flow_dismissed" as const, gx_event_schema_version: 2, gx_future_coordinate: "future", }, }; expect( appEventFlowDismissedUnknownVersionSchema.safeParse(unknown).success, ).toBe(true); expect(appEventPayloadSchema.safeParse(unknown).success).toBe(true); expect(appEventTrackSchema.safeParse(unknown).success).toBe(true); expect( appEventTrackSchema.safeParse({ ...unknown, properties: { ...unknown.properties, gx_flow_completion_id: "browser_spoof", }, }).success, ).toBe(false); expect( appEventFlowDismissedUnknownVersionSchema.safeParse({ ...unknown, properties: { ...unknown.properties, gx_response_id: "future_meaning", future_fact: true, }, }).success, ).toBe(true); }); });