@flamingo-stack/openframe-frontend-core 0.0.508 → 0.0.509
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat-protocol/decode.d.ts +58 -0
- package/dist/chat-protocol/decode.d.ts.map +1 -0
- package/dist/chat-protocol/encode.d.ts +44 -0
- package/dist/chat-protocol/encode.d.ts.map +1 -0
- package/dist/chat-protocol/env-flag.d.ts +35 -0
- package/dist/chat-protocol/env-flag.d.ts.map +1 -0
- package/dist/chat-protocol/events.d.ts +216 -0
- package/dist/chat-protocol/events.d.ts.map +1 -0
- package/dist/chat-protocol/frames.d.ts +213 -0
- package/dist/chat-protocol/frames.d.ts.map +1 -0
- package/dist/chat-protocol/index.cjs +506 -0
- package/dist/chat-protocol/index.cjs.map +1 -0
- package/dist/chat-protocol/index.d.ts +18 -0
- package/dist/chat-protocol/index.d.ts.map +1 -0
- package/dist/chat-protocol/index.js +490 -0
- package/dist/chat-protocol/index.js.map +1 -0
- package/dist/chat-protocol/ip-normalize.d.ts +44 -0
- package/dist/chat-protocol/ip-normalize.d.ts.map +1 -0
- package/dist/chat-protocol/nats-decoder.d.ts +26 -0
- package/dist/chat-protocol/nats-decoder.d.ts.map +1 -0
- package/dist/{chunk-3SQ5KXHQ.cjs → chunk-2T4GTV25.cjs} +9 -9
- package/dist/{chunk-3SQ5KXHQ.cjs.map → chunk-2T4GTV25.cjs.map} +1 -1
- package/dist/{chunk-CWAOV2FK.cjs → chunk-3CGZPAGR.cjs} +3 -3
- package/dist/{chunk-CWAOV2FK.cjs.map → chunk-3CGZPAGR.cjs.map} +1 -1
- package/dist/{chunk-TLMJHMXJ.js → chunk-3QSR22IC.js} +12 -12
- package/dist/chunk-3QSR22IC.js.map +1 -0
- package/dist/{chunk-TWYNR4TZ.cjs → chunk-4G5TLHJD.cjs} +7141 -4799
- package/dist/chunk-4G5TLHJD.cjs.map +1 -0
- package/dist/{chunk-ADPMHWOE.js → chunk-72XAID7Y.js} +4 -4
- package/dist/{chunk-PAGKRNWK.js → chunk-7WZHBQ4J.js} +2 -2
- package/dist/{chunk-OCEKO5CW.js → chunk-AEELJC4N.js} +12106 -9764
- package/dist/chunk-AEELJC4N.js.map +1 -0
- package/dist/{chunk-Z42BGM6Q.cjs → chunk-BT2WWDFP.cjs} +5 -5
- package/dist/{chunk-Z42BGM6Q.cjs.map → chunk-BT2WWDFP.cjs.map} +1 -1
- package/dist/{chunk-7LEL3RIX.cjs → chunk-C7OY3PQP.cjs} +11 -11
- package/dist/{chunk-7LEL3RIX.cjs.map → chunk-C7OY3PQP.cjs.map} +1 -1
- package/dist/{chunk-RHGGEKPQ.cjs → chunk-CIOLOQKQ.cjs} +4 -4
- package/dist/{chunk-RHGGEKPQ.cjs.map → chunk-CIOLOQKQ.cjs.map} +1 -1
- package/dist/{chunk-TSZHM74B.js → chunk-DC2TKS7C.js} +2 -2
- package/dist/{chunk-M2GOR3XQ.cjs → chunk-DYPYR7FR.cjs} +87 -87
- package/dist/{chunk-M2GOR3XQ.cjs.map → chunk-DYPYR7FR.cjs.map} +1 -1
- package/dist/{chunk-I64ABCDX.js → chunk-I2B6X77L.js} +2 -2
- package/dist/{chunk-NIZDKTGL.cjs → chunk-K5MSCMDK.cjs} +37 -37
- package/dist/{chunk-NIZDKTGL.cjs.map → chunk-K5MSCMDK.cjs.map} +1 -1
- package/dist/{chunk-DTYRYB2N.js → chunk-K6QTZ7CZ.js} +2 -2
- package/dist/{chunk-4LTMXDUS.js → chunk-KBN7PMFT.js} +2 -2
- package/dist/{chunk-TBMV7I5N.js → chunk-KXZO2WZX.js} +2 -2
- package/dist/{chunk-QTYZMP6D.cjs → chunk-LBAKLT6T.cjs} +62 -62
- package/dist/chunk-LBAKLT6T.cjs.map +1 -0
- package/dist/{chunk-KEBYLU3U.js → chunk-LFUPI7VX.js} +2 -2
- package/dist/{chunk-LDTR4IVZ.cjs → chunk-M37PBG4N.cjs} +31 -31
- package/dist/{chunk-LDTR4IVZ.cjs.map → chunk-M37PBG4N.cjs.map} +1 -1
- package/dist/{chunk-WA7RR64F.js → chunk-PO5O654H.js} +6 -5
- package/dist/chunk-PO5O654H.js.map +1 -0
- package/dist/{chunk-WK4N5VBX.cjs → chunk-QJJISLPA.cjs} +28 -27
- package/dist/chunk-QJJISLPA.cjs.map +1 -0
- package/dist/{chunk-2F44PLTV.cjs → chunk-T7MCSA55.cjs} +26 -26
- package/dist/{chunk-2F44PLTV.cjs.map → chunk-T7MCSA55.cjs.map} +1 -1
- package/dist/{chunk-WJQHJD7J.js → chunk-TWSKTJHW.js} +2 -2
- package/dist/{chunk-ZSQHZYCO.cjs → chunk-UQDMJ6DH.cjs} +13 -13
- package/dist/{chunk-ZSQHZYCO.cjs.map → chunk-UQDMJ6DH.cjs.map} +1 -1
- package/dist/{chunk-TZRUCD56.js → chunk-V3ZAAFE3.js} +5 -5
- package/dist/{chunk-LRGHJPET.js → chunk-XGIFOHKE.js} +2 -2
- package/dist/{chunk-POOMO3PA.cjs → chunk-YQEPRYT5.cjs} +7 -7
- package/dist/{chunk-POOMO3PA.cjs.map → chunk-YQEPRYT5.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/hooks/index.d.ts +0 -1
- package/dist/components/chat/hooks/index.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chunk-catchup.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +15 -48
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +10 -57
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +10 -2
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +21 -13
- package/dist/components/chat/stream/chat-dialog-store.d.ts +184 -0
- package/dist/components/chat/stream/chat-dialog-store.d.ts.map +1 -0
- package/dist/components/chat/stream/chat-stream-reducer.d.ts +380 -0
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -0
- package/dist/components/chat/stream/delta-batcher.d.ts +51 -0
- package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -0
- package/dist/components/chat/stream/index.d.ts +17 -0
- package/dist/components/chat/stream/index.d.ts.map +1 -0
- package/dist/components/chat/stream/message-mutations.d.ts +77 -0
- package/dist/components/chat/stream/message-mutations.d.ts.map +1 -0
- package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +22 -0
- package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +1 -0
- package/dist/components/chat/types/api.types.d.ts +1 -81
- package/dist/components/chat/types/api.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +2 -93
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +15 -0
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +34 -6
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
- package/dist/components/chat/utils/history-merge.d.ts.map +1 -1
- package/dist/components/chat/utils/index.d.ts +2 -3
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts +59 -11
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +174 -132
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +65 -23
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +34 -34
- package/dist/components/meeting-scheduler/index.js +3 -3
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/index.cjs +44 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -2
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +55 -13
- package/dist/components/ui/markdown/base-components.d.ts +51 -0
- package/dist/components/ui/markdown/base-components.d.ts.map +1 -0
- package/dist/components/ui/markdown/engine.d.ts +99 -0
- package/dist/components/ui/markdown/engine.d.ts.map +1 -0
- package/dist/components/ui/markdown/heading-ids.d.ts +65 -0
- package/dist/components/ui/markdown/heading-ids.d.ts.map +1 -0
- package/dist/components/ui/markdown/index.d.ts +18 -0
- package/dist/components/ui/markdown/index.d.ts.map +1 -0
- package/dist/components/ui/markdown/mermaid-diagram.d.ts +79 -0
- package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts +14 -0
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +40 -0
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/shortcodes.d.ts +8 -0
- package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +1 -0
- package/dist/components/ui/markdown/sanitize.d.ts +153 -0
- package/dist/components/ui/markdown/sanitize.d.ts.map +1 -0
- package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +23 -0
- package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +1 -0
- package/dist/components/ui/markdown/streaming.d.ts +78 -0
- package/dist/components/ui/markdown/streaming.d.ts.map +1 -0
- package/dist/components/ui/markdown/text-size.d.ts +34 -0
- package/dist/components/ui/markdown/text-size.d.ts.map +1 -0
- package/dist/index.cjs +60 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +71 -13
- package/dist/utils/index.cjs +169 -40
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +162 -41
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/markdown-fences.d.ts +42 -0
- package/dist/utils/markdown-fences.d.ts.map +1 -0
- package/dist/utils/markdown-heading-id.d.ts +85 -0
- package/dist/utils/markdown-heading-id.d.ts.map +1 -0
- package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
- package/package.json +7 -1
- package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +319 -0
- package/src/chat-protocol/__tests__/chat-protocol.test.ts +516 -0
- package/src/chat-protocol/__tests__/env-flag.test.ts +52 -0
- package/src/chat-protocol/__tests__/ip-normalize.test.ts +136 -0
- package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +237 -0
- package/src/chat-protocol/decode.ts +278 -0
- package/src/chat-protocol/encode.ts +71 -0
- package/src/chat-protocol/env-flag.ts +39 -0
- package/src/chat-protocol/events.ts +254 -0
- package/src/chat-protocol/frames.ts +245 -0
- package/src/chat-protocol/index.ts +21 -0
- package/src/chat-protocol/ip-normalize.ts +146 -0
- package/src/chat-protocol/nats-decoder.ts +264 -0
- package/src/components/chat/.chat-message-list.md +10 -6
- package/src/components/chat/__tests__/chat-message-list.test.tsx +450 -9
- package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +207 -0
- package/src/components/chat/__tests__/chat-pending-turn.test.tsx +250 -0
- package/src/components/chat/chat-message-enhanced.tsx +122 -20
- package/src/components/chat/chat-message-list.tsx +515 -133
- package/src/components/chat/embeddable-chat.tsx +6 -0
- package/src/components/chat/hooks/.index.md +30 -33
- package/src/components/chat/hooks/.use-nats-chat-adapter.md +36 -55
- package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +37 -0
- package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
- package/src/components/chat/hooks/__tests__/chunk-catchup-dialog-staleness.test.ts +92 -0
- package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +438 -0
- package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +318 -0
- package/src/components/chat/hooks/index.ts +0 -1
- package/src/components/chat/hooks/use-chat.ts +8 -0
- package/src/components/chat/hooks/use-chunk-catchup.ts +26 -1
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +254 -825
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +380 -684
- package/src/components/chat/index.ts +5 -0
- package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +944 -0
- package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +806 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +467 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +1330 -0
- package/src/components/chat/stream/__tests__/delta-batcher.test.ts +255 -0
- package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +105 -0
- package/src/components/chat/stream/chat-dialog-store.ts +536 -0
- package/src/components/chat/stream/chat-stream-reducer.ts +1928 -0
- package/src/components/chat/stream/delta-batcher.ts +168 -0
- package/src/components/chat/stream/index.ts +55 -0
- package/src/components/chat/stream/message-mutations.ts +340 -0
- package/src/components/chat/stream/use-chat-stream-reducer.ts +126 -0
- package/src/components/chat/thinking-display.tsx +1 -1
- package/src/components/chat/types/.api.types.md +39 -49
- package/src/components/chat/types/.processing.types.md +29 -58
- package/src/components/chat/types/.unified-chat-state.types.md +2 -2
- package/src/components/chat/types/api.types.ts +6 -73
- package/src/components/chat/types/processing.types.ts +11 -53
- package/src/components/chat/types/unified-chat-state.types.ts +16 -0
- package/src/components/chat/utils/.extract-incomplete-message-state.md +21 -3
- package/src/components/chat/utils/.history-merge.md +44 -3
- package/src/components/chat/utils/.index.md +31 -40
- package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +420 -0
- package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +605 -0
- package/src/components/chat/utils/__tests__/extract-incomplete-message-state.test.ts +106 -0
- package/src/components/chat/utils/__tests__/history-merge.test.ts +160 -0
- package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +61 -0
- package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +394 -0
- package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +270 -0
- package/src/components/chat/utils/extract-incomplete-message-state.ts +93 -6
- package/src/components/chat/utils/history-merge.ts +85 -5
- package/src/components/chat/utils/index.ts +5 -8
- package/src/components/chat/utils/process-historical-messages.ts +361 -385
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +22 -4
- package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
- package/src/components/shared/product-release/release-detail-page.tsx +19 -4
- package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +2420 -0
- package/src/components/ui/__tests__/markdown-parity.test.tsx +2540 -0
- package/src/components/ui/index.ts +1 -2
- package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +176 -0
- package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +151 -0
- package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +354 -0
- package/src/components/ui/markdown/__tests__/sanitize-render.test.tsx +118 -0
- package/src/components/ui/markdown/__tests__/streaming.test.tsx +436 -0
- package/src/components/ui/markdown/base-components.tsx +360 -0
- package/src/components/ui/markdown/engine.tsx +315 -0
- package/src/components/ui/markdown/heading-ids.ts +239 -0
- package/src/components/ui/markdown/index.ts +49 -0
- package/src/components/ui/markdown/mermaid-diagram.tsx +323 -0
- package/src/components/ui/markdown/rich/embed-overrides.tsx +199 -0
- package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +184 -0
- package/src/components/ui/markdown/rich/shortcodes.ts +170 -0
- package/src/components/ui/markdown/sanitize.ts +3584 -0
- package/src/components/ui/markdown/simple-markdown-renderer.tsx +28 -0
- package/src/components/ui/markdown/streaming.ts +390 -0
- package/src/components/ui/markdown/text-size.ts +106 -0
- package/src/components/ui/release-changelog-section.tsx +1 -1
- package/src/components/ui/ticket-info-section.tsx +1 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/markdown-fences.ts +127 -0
- package/src/utils/markdown-heading-id.ts +348 -0
- package/src/utils/markdown-section-extractor.ts +41 -56
- package/dist/chunk-OCEKO5CW.js.map +0 -1
- package/dist/chunk-QTYZMP6D.cjs.map +0 -1
- package/dist/chunk-TLMJHMXJ.js.map +0 -1
- package/dist/chunk-TWYNR4TZ.cjs.map +0 -1
- package/dist/chunk-WA7RR64F.js.map +0 -1
- package/dist/chunk-WK4N5VBX.cjs.map +0 -1
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +0 -6
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +0 -1
- package/dist/components/chat/utils/chunk-parser.d.ts +0 -25
- package/dist/components/chat/utils/chunk-parser.d.ts.map +0 -1
- package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
- package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
- package/dist/components/ui/simple-markdown-renderer.d.ts +0 -73
- package/dist/components/ui/simple-markdown-renderer.d.ts.map +0 -1
- package/src/components/chat/hooks/.use-realtime-chunk-processor.md +0 -77
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +0 -489
- package/src/components/chat/utils/.chunk-parser.md +0 -62
- package/src/components/chat/utils/chunk-parser.ts +0 -262
- package/src/components/ui/.simple-markdown-renderer.md +0 -52
- package/src/components/ui/rich-markdown-renderer.tsx +0 -1223
- package/src/components/ui/simple-markdown-renderer.tsx +0 -964
- /package/dist/{chunk-ADPMHWOE.js.map → chunk-72XAID7Y.js.map} +0 -0
- /package/dist/{chunk-PAGKRNWK.js.map → chunk-7WZHBQ4J.js.map} +0 -0
- /package/dist/{chunk-TSZHM74B.js.map → chunk-DC2TKS7C.js.map} +0 -0
- /package/dist/{chunk-I64ABCDX.js.map → chunk-I2B6X77L.js.map} +0 -0
- /package/dist/{chunk-DTYRYB2N.js.map → chunk-K6QTZ7CZ.js.map} +0 -0
- /package/dist/{chunk-4LTMXDUS.js.map → chunk-KBN7PMFT.js.map} +0 -0
- /package/dist/{chunk-TBMV7I5N.js.map → chunk-KXZO2WZX.js.map} +0 -0
- /package/dist/{chunk-KEBYLU3U.js.map → chunk-LFUPI7VX.js.map} +0 -0
- /package/dist/{chunk-WJQHJD7J.js.map → chunk-TWSKTJHW.js.map} +0 -0
- /package/dist/{chunk-TZRUCD56.js.map → chunk-V3ZAAFE3.js.map} +0 -0
- /package/dist/{chunk-LRGHJPET.js.map → chunk-XGIFOHKE.js.map} +0 -0
|
@@ -770,3 +770,163 @@ describe('page helpers', () => {
|
|
|
770
770
|
expect(maxPersistedStreamSeq(undefined)).toBe(0)
|
|
771
771
|
})
|
|
772
772
|
})
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* The scenario the reducer's `streamSeq` stamp exists for: a history refetch
|
|
776
|
+
* lands MID-STREAM (a reconnect after the window lost focus fires one). History
|
|
777
|
+
* holds only what the backend has persisted so far — often just the user row —
|
|
778
|
+
* while the store holds several live assistant bubbles of the turn in flight.
|
|
779
|
+
*/
|
|
780
|
+
describe('mid-stream refetch (live turn not yet persisted)', () => {
|
|
781
|
+
// Two bubbles of ONE live turn: the backend splits a turn (a preamble, then
|
|
782
|
+
// the tool bubble), so only ONE of them can be `streamingMessageId`.
|
|
783
|
+
const LIVE_A: TestMessage = {
|
|
784
|
+
id: 'assistant-3000-a',
|
|
785
|
+
role: 'assistant',
|
|
786
|
+
content: txt('let me check that'),
|
|
787
|
+
timestamp: t(3000),
|
|
788
|
+
streamSeq: 41,
|
|
789
|
+
}
|
|
790
|
+
const LIVE_B: TestMessage = {
|
|
791
|
+
id: 'assistant-3100-b',
|
|
792
|
+
role: 'assistant',
|
|
793
|
+
content: [tool('exec-7', 'EXECUTING_TOOL')],
|
|
794
|
+
timestamp: t(3100),
|
|
795
|
+
streamSeq: 42,
|
|
796
|
+
}
|
|
797
|
+
// What history returns mid-turn: the user row only. It carries no
|
|
798
|
+
// `lastChunkStreamSeq` (the backend does not stamp user rows).
|
|
799
|
+
const U_LIVE: TestMessage = { id: 'aaaa0009', role: 'user', content: 'check the disk', timestamp: t(2900) }
|
|
800
|
+
|
|
801
|
+
it('keeps every live bubble, not just the streaming one', () => {
|
|
802
|
+
const merged = mergeHistoryWithRealtime({
|
|
803
|
+
processedHistory: [U0, A0, U_LIVE],
|
|
804
|
+
existingMessages: [U0, A0, U_LIVE, LIVE_A, LIVE_B],
|
|
805
|
+
// Only the bubble currently being written is exempt by id — the earlier
|
|
806
|
+
// one of the same turn has to survive on its own seq evidence.
|
|
807
|
+
streamingMessageId: LIVE_B.id,
|
|
808
|
+
// The refetch instant is AFTER both bubbles were created, which is
|
|
809
|
+
// exactly what the wall-clock fallback misreads as "history has them".
|
|
810
|
+
historyFetchedAt: 3500,
|
|
811
|
+
historyMaxStreamSeq: 0,
|
|
812
|
+
realtimeSeenStreamSeq: 42,
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
expect(ids(merged)).toEqual([U0.id, A0.id, U_LIVE.id, LIVE_A.id, LIVE_B.id])
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
it('drops a live bubble once a persisted row of its role reaches its seq', () => {
|
|
819
|
+
// Self-healing side of the same rule: when the turn IS persisted, the
|
|
820
|
+
// synthetic must go or it renders twice.
|
|
821
|
+
const persisted: TestMessage = {
|
|
822
|
+
id: 'aaaa0010',
|
|
823
|
+
role: 'assistant',
|
|
824
|
+
content: txt('let me check that'),
|
|
825
|
+
timestamp: t(3000),
|
|
826
|
+
streamSeq: 41,
|
|
827
|
+
}
|
|
828
|
+
const merged = mergeHistoryWithRealtime({
|
|
829
|
+
processedHistory: [U0, A0, U_LIVE, persisted],
|
|
830
|
+
existingMessages: [U0, A0, U_LIVE, LIVE_A],
|
|
831
|
+
streamingMessageId: null,
|
|
832
|
+
historyFetchedAt: 3500,
|
|
833
|
+
historyMaxStreamSeq: 41,
|
|
834
|
+
realtimeSeenStreamSeq: 41,
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
expect(ids(merged)).toEqual([U0.id, A0.id, U_LIVE.id, persisted.id])
|
|
838
|
+
})
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* The other half of the mid-stream refetch: history has begun persisting the
|
|
843
|
+
* turn that is STILL STREAMING. Its snapshot stops at whatever chunk the
|
|
844
|
+
* backend had written, so the two copies disagree — the same tool row reads
|
|
845
|
+
* `pending` in one and `failed` in the other. Ids cannot match here: adoption
|
|
846
|
+
* only happens on a replay, and a refetch mid-stream has none.
|
|
847
|
+
*/
|
|
848
|
+
describe('mid-stream refetch (turn partially persisted)', () => {
|
|
849
|
+
const LIVE: TestMessage = {
|
|
850
|
+
id: 'assistant-4000-live',
|
|
851
|
+
role: 'assistant',
|
|
852
|
+
content: [tool('exec-1', 'EXECUTED_TOOL'), tool('exec-2', 'EXECUTED_TOOL'), tool('exec-3', 'EXECUTING_TOOL')],
|
|
853
|
+
timestamp: t(4000),
|
|
854
|
+
streamSeq: 90,
|
|
855
|
+
}
|
|
856
|
+
// History's snapshot of the SAME turn, under its Mongo id, two chunks behind.
|
|
857
|
+
const PERSISTED_PARTIAL: TestMessage = {
|
|
858
|
+
id: 'aaaa0020',
|
|
859
|
+
role: 'assistant',
|
|
860
|
+
content: [tool('exec-1', 'EXECUTED_TOOL'), tool('exec-2', 'EXECUTING_TOOL')],
|
|
861
|
+
timestamp: t(3900),
|
|
862
|
+
streamSeq: 70,
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
it('keeps ONE copy — the live one — when the turn is still streaming', () => {
|
|
866
|
+
const merged = mergeHistoryWithRealtime({
|
|
867
|
+
processedHistory: [U0, A0, PERSISTED_PARTIAL],
|
|
868
|
+
existingMessages: [U0, A0, LIVE],
|
|
869
|
+
streamingMessageId: LIVE.id,
|
|
870
|
+
historyFetchedAt: 4200,
|
|
871
|
+
historyMaxStreamSeq: 70,
|
|
872
|
+
realtimeSeenStreamSeq: 90,
|
|
873
|
+
})
|
|
874
|
+
|
|
875
|
+
expect(ids(merged)).toEqual([U0.id, A0.id, LIVE.id])
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
it('keeps the live copy when it is richer even after the stream ends', () => {
|
|
879
|
+
// No `streamingMessageId` any more, but the turn's own request ids still
|
|
880
|
+
// identify the twin and the live copy still holds more of it.
|
|
881
|
+
const merged = mergeHistoryWithRealtime({
|
|
882
|
+
processedHistory: [U0, A0, PERSISTED_PARTIAL],
|
|
883
|
+
existingMessages: [U0, A0, LIVE],
|
|
884
|
+
streamingMessageId: null,
|
|
885
|
+
historyFetchedAt: 4200,
|
|
886
|
+
historyMaxStreamSeq: 70,
|
|
887
|
+
realtimeSeenStreamSeq: 90,
|
|
888
|
+
})
|
|
889
|
+
|
|
890
|
+
expect(ids(merged)).toEqual([U0.id, A0.id, LIVE.id])
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
it('lets the persisted copy win once it has caught up (self-heals)', () => {
|
|
894
|
+
const persistedFull: TestMessage = {
|
|
895
|
+
...PERSISTED_PARTIAL,
|
|
896
|
+
content: [tool('exec-1', 'EXECUTED_TOOL'), tool('exec-2', 'EXECUTED_TOOL'), tool('exec-3', 'EXECUTED_TOOL')],
|
|
897
|
+
streamSeq: 95,
|
|
898
|
+
}
|
|
899
|
+
const merged = mergeHistoryWithRealtime({
|
|
900
|
+
processedHistory: [U0, A0, persistedFull],
|
|
901
|
+
existingMessages: [U0, A0, LIVE],
|
|
902
|
+
streamingMessageId: null,
|
|
903
|
+
historyFetchedAt: 4500,
|
|
904
|
+
historyMaxStreamSeq: 95,
|
|
905
|
+
realtimeSeenStreamSeq: 90,
|
|
906
|
+
})
|
|
907
|
+
|
|
908
|
+
expect(ids(merged)).toEqual([U0.id, A0.id, persistedFull.id])
|
|
909
|
+
})
|
|
910
|
+
|
|
911
|
+
it('does not pair unrelated turns that merely sit next to each other', () => {
|
|
912
|
+
// No shared request id → no turn identity → both stand. Guards the widened
|
|
913
|
+
// match from swallowing a genuinely separate turn.
|
|
914
|
+
const unrelatedLive: TestMessage = {
|
|
915
|
+
id: 'assistant-4100-other',
|
|
916
|
+
role: 'assistant',
|
|
917
|
+
content: [tool('exec-77', 'EXECUTING_TOOL')],
|
|
918
|
+
timestamp: t(4100),
|
|
919
|
+
streamSeq: 91,
|
|
920
|
+
}
|
|
921
|
+
const merged = mergeHistoryWithRealtime({
|
|
922
|
+
processedHistory: [U0, A0, PERSISTED_PARTIAL],
|
|
923
|
+
existingMessages: [U0, A0, unrelatedLive],
|
|
924
|
+
streamingMessageId: unrelatedLive.id,
|
|
925
|
+
historyFetchedAt: 4200,
|
|
926
|
+
historyMaxStreamSeq: 70,
|
|
927
|
+
realtimeSeenStreamSeq: 91,
|
|
928
|
+
})
|
|
929
|
+
|
|
930
|
+
expect(ids(merged)).toEqual([U0.id, A0.id, PERSISTED_PARTIAL.id, unrelatedLive.id])
|
|
931
|
+
})
|
|
932
|
+
})
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { describe, it, expect } from 'vitest'
|
|
12
12
|
import { processHistoricalMessagesWithErrors } from '../process-historical-messages'
|
|
13
|
+
import { createChatStreamReducer } from '../../stream/chat-stream-reducer'
|
|
13
14
|
import type { HistoricalMessage, MessageSegment } from '../../types'
|
|
14
15
|
|
|
15
16
|
const adminApprovalMessage = (id: string): HistoricalMessage => ({
|
|
@@ -95,3 +96,63 @@ describe('processHistoricalMessagesWithErrors — displayApprovalTypes', () => {
|
|
|
95
96
|
expect(asst?.streamSeq).toBe(50)
|
|
96
97
|
})
|
|
97
98
|
})
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* PAIRED-PATH contract. The history processor and the realtime reducer are the
|
|
102
|
+
* two halves of one pipeline, and they DISAGREE by default: history renders
|
|
103
|
+
* every approval type (the deliberate behaviour this file's header argues for),
|
|
104
|
+
* the reducer defaults to `['CLIENT']`. Each half is pinned on its own — above
|
|
105
|
+
* for history, in the reducer's own suite for realtime — but NOTHING asserted
|
|
106
|
+
* the pair, so aligning one default "for consistency" (or a host wiring the
|
|
107
|
+
* option into one call site only) changed observable behaviour with every test
|
|
108
|
+
* still green.
|
|
109
|
+
*
|
|
110
|
+
* These two fixtures state the divergence and its remedy in one place: omitted
|
|
111
|
+
* → the SAME approval is an inline card after a reload and invisible live;
|
|
112
|
+
* explicit list → both paths agree. Read the header for why the asymmetric
|
|
113
|
+
* defaults are the intended trade-off (a `['CLIENT']` history default made
|
|
114
|
+
* pending ADMIN cards vanish on every refetch).
|
|
115
|
+
*/
|
|
116
|
+
describe('displayApprovalTypes — history vs realtime default divergence', () => {
|
|
117
|
+
const liveApprovalSegments = (displayApprovalTypes?: string[]) => {
|
|
118
|
+
const effects: Array<{ name: string; args: unknown[] }> = []
|
|
119
|
+
const r = createChatStreamReducer({
|
|
120
|
+
transport: 'nats',
|
|
121
|
+
onEffect: (e) => effects.push(e),
|
|
122
|
+
...(displayApprovalTypes ? { displayApprovalTypes } : {}),
|
|
123
|
+
})
|
|
124
|
+
r.apply({ type: 'turn-start' })
|
|
125
|
+
r.apply({
|
|
126
|
+
type: 'approval-request',
|
|
127
|
+
requestId: 'req-m1',
|
|
128
|
+
approvalType: 'ADMIN',
|
|
129
|
+
command: 'systemctl restart nats',
|
|
130
|
+
})
|
|
131
|
+
return {
|
|
132
|
+
rendered: r.state.messages.flatMap((m) => approvalSegments(m.segments)),
|
|
133
|
+
escalated: effects.filter((e) => e.name === 'onEscalatedApproval'),
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
it('OMITTED: history renders the ADMIN card inline, realtime escalates it', () => {
|
|
138
|
+
const history = processHistoricalMessagesWithErrors([adminApprovalMessage('m1')])
|
|
139
|
+
expect(history.messages.flatMap((m) => approvalSegments(m.content))).toHaveLength(1)
|
|
140
|
+
expect(history.escalatedApprovals.size).toBe(0)
|
|
141
|
+
|
|
142
|
+
const live = liveApprovalSegments()
|
|
143
|
+
expect(live.rendered).toHaveLength(0)
|
|
144
|
+
expect(live.escalated).toHaveLength(1)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('EXPLICIT list: both paths render the same ADMIN card', () => {
|
|
148
|
+
const types = ['CLIENT', 'ADMIN']
|
|
149
|
+
const history = processHistoricalMessagesWithErrors([adminApprovalMessage('m1')], {
|
|
150
|
+
displayApprovalTypes: types,
|
|
151
|
+
})
|
|
152
|
+
expect(history.messages.flatMap((m) => approvalSegments(m.content))).toHaveLength(1)
|
|
153
|
+
|
|
154
|
+
const live = liveApprovalSegments(types)
|
|
155
|
+
expect(live.rendered).toHaveLength(1)
|
|
156
|
+
expect(live.escalated).toHaveLength(0)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase-0 GOLDEN CHARACTERIZATION TESTS — `processHistoricalMessages`.
|
|
3
|
+
*
|
|
4
|
+
* Pins the CURRENT GraphQL-history → ProcessedMessage[] transformation
|
|
5
|
+
* before the stream-reader unification. A recorded corpus of messageData
|
|
6
|
+
* rows is processed and the FULL output array is snapshotted.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: `process-historical-messages-approvals.test.ts` already guards
|
|
9
|
+
* displayApprovalTypes semantics — this file deliberately does not
|
|
10
|
+
* duplicate it and instead records the full-output shape.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
14
|
+
import { processHistoricalMessages, extractErrorMessages, processHistoricalMessagesWithErrors } from '../process-historical-messages'
|
|
15
|
+
import type { HistoricalMessage } from '../../types'
|
|
16
|
+
|
|
17
|
+
// Deterministic Date.now for the `pending-approvals-${Date.now()}` synthetic
|
|
18
|
+
// message id + its `new Date()` timestamp.
|
|
19
|
+
const FIXED_NOW = new Date('2026-07-20T10:00:00.000Z')
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.useFakeTimers()
|
|
23
|
+
vi.setSystemTime(FIXED_NOW)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
vi.useRealTimers()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
/** Recorded corpus — one realistic dialog covering every row shape. */
|
|
31
|
+
const CORPUS: HistoricalMessage[] = [
|
|
32
|
+
// 1. User message with contextItems (CLIENT owner).
|
|
33
|
+
{
|
|
34
|
+
id: 'u1',
|
|
35
|
+
chatType: 'CLIENT_CHAT',
|
|
36
|
+
createdAt: '2026-07-19T09:00:00Z',
|
|
37
|
+
owner: { type: 'CLIENT', userId: 'user-42' },
|
|
38
|
+
lastChunkStreamSeq: 10,
|
|
39
|
+
messageData: [
|
|
40
|
+
{
|
|
41
|
+
type: 'TEXT',
|
|
42
|
+
text: 'diagnose this device please',
|
|
43
|
+
// wire shape: [{ type, id }] — label falls back to id
|
|
44
|
+
contextItems: [
|
|
45
|
+
{ type: 'device', id: 'dev-1' },
|
|
46
|
+
{ type: 'bad' }, // missing id → filtered
|
|
47
|
+
],
|
|
48
|
+
} as any,
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
// 2. Assistant: thinking + text (grouped into one bubble).
|
|
52
|
+
{
|
|
53
|
+
id: 'a1',
|
|
54
|
+
chatType: 'CLIENT_CHAT',
|
|
55
|
+
createdAt: '2026-07-19T09:00:05Z',
|
|
56
|
+
owner: { type: 'ASSISTANT', model: 'claude-sonnet-x' },
|
|
57
|
+
lastChunkStreamSeq: 20,
|
|
58
|
+
messageData: [
|
|
59
|
+
{ type: 'THINKING', text: 'Checking device state… ' },
|
|
60
|
+
{ type: 'THINKING', text: 'looks like disk pressure.' },
|
|
61
|
+
{ type: 'TEXT', text: 'The device is low on disk. ' },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
// 3. Assistant continued: EXECUTING + EXECUTED tool pair merges in place.
|
|
65
|
+
{
|
|
66
|
+
id: 'a2',
|
|
67
|
+
chatType: 'CLIENT_CHAT',
|
|
68
|
+
createdAt: '2026-07-19T09:00:10Z',
|
|
69
|
+
owner: { type: 'ASSISTANT' },
|
|
70
|
+
lastChunkStreamSeq: 15, // lower than a1's — MAX (20) must win on the flushed turn
|
|
71
|
+
messageData: [
|
|
72
|
+
{
|
|
73
|
+
type: 'EXECUTING_TOOL',
|
|
74
|
+
integratedToolType: 'TACTICAL_RMM',
|
|
75
|
+
toolFunction: 'run_script',
|
|
76
|
+
title: 'Run cleanup script',
|
|
77
|
+
parameters: { script: 'cleanup.sh' },
|
|
78
|
+
toolExecutionRequestId: 'exec-1',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: 'EXECUTED_TOOL',
|
|
82
|
+
integratedToolType: 'TACTICAL_RMM',
|
|
83
|
+
toolFunction: 'run_script',
|
|
84
|
+
parameters: { script: 'cleanup.sh' },
|
|
85
|
+
result: 'Freed 2.3 GB',
|
|
86
|
+
success: true,
|
|
87
|
+
toolExecutionRequestId: 'exec-1',
|
|
88
|
+
},
|
|
89
|
+
{ type: 'TEXT', text: 'Cleanup done.' },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
// 4. ADMIN-owned user message (owner-type author resolution: display name from user).
|
|
93
|
+
{
|
|
94
|
+
id: 'u2',
|
|
95
|
+
chatType: 'CLIENT_CHAT',
|
|
96
|
+
createdAt: '2026-07-19T09:01:00Z',
|
|
97
|
+
owner: {
|
|
98
|
+
type: 'ADMIN',
|
|
99
|
+
user: {
|
|
100
|
+
id: 'admin-7',
|
|
101
|
+
firstName: 'Jane',
|
|
102
|
+
lastName: 'Admin',
|
|
103
|
+
image: { imageUrl: '/avatars/jane.png' },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
messageData: [{ type: 'TEXT', text: 'please restart the broker' }],
|
|
107
|
+
},
|
|
108
|
+
// 5. Assistant: single approval request + result (resolved in-history).
|
|
109
|
+
{
|
|
110
|
+
id: 'a3',
|
|
111
|
+
chatType: 'CLIENT_CHAT',
|
|
112
|
+
createdAt: '2026-07-19T09:01:05Z',
|
|
113
|
+
owner: { type: 'ASSISTANT' },
|
|
114
|
+
lastChunkStreamSeq: 30,
|
|
115
|
+
messageData: [
|
|
116
|
+
{ type: 'TEXT', text: 'I need approval to restart it.' },
|
|
117
|
+
{
|
|
118
|
+
type: 'APPROVAL_REQUEST',
|
|
119
|
+
approvalRequestId: 'req-1',
|
|
120
|
+
approvalType: 'CLIENT',
|
|
121
|
+
command: 'systemctl restart nats',
|
|
122
|
+
explanation: 'Restart the message broker',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
type: 'APPROVAL_RESULT',
|
|
126
|
+
approvalRequestId: 'req-1',
|
|
127
|
+
approved: true,
|
|
128
|
+
approvalType: 'CLIENT',
|
|
129
|
+
resolvedByName: 'Jane Admin',
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
// 6. Assistant: batch approval + per-tool executions merged into the batch.
|
|
134
|
+
{
|
|
135
|
+
id: 'a4',
|
|
136
|
+
chatType: 'CLIENT_CHAT',
|
|
137
|
+
createdAt: '2026-07-19T09:02:00Z',
|
|
138
|
+
owner: { type: 'ASSISTANT' },
|
|
139
|
+
messageData: [
|
|
140
|
+
{
|
|
141
|
+
type: 'APPROVAL_REQUEST',
|
|
142
|
+
approvalRequestId: 'batch-1',
|
|
143
|
+
approvalType: 'CLIENT',
|
|
144
|
+
toolCalls: [
|
|
145
|
+
{
|
|
146
|
+
toolExecutionRequestId: 'exec-a',
|
|
147
|
+
toolName: 'create_ticket',
|
|
148
|
+
toolTitle: 'Create ticket',
|
|
149
|
+
requiresApproval: true,
|
|
150
|
+
approvalType: 'CLIENT',
|
|
151
|
+
toolCallArguments: { subject: 'Printer down' },
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
toolExecutionRequestId: 'exec-b',
|
|
155
|
+
toolName: 'read_kb',
|
|
156
|
+
requiresApproval: false,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
type: 'APPROVAL_RESULT',
|
|
162
|
+
approvalRequestId: 'batch-1',
|
|
163
|
+
approved: true,
|
|
164
|
+
resolvedByName: 'Alice Client',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: 'EXECUTING_TOOL',
|
|
168
|
+
integratedToolType: 'PSA',
|
|
169
|
+
toolFunction: 'create_ticket',
|
|
170
|
+
toolExecutionRequestId: 'exec-a',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: 'EXECUTED_TOOL',
|
|
174
|
+
integratedToolType: 'PSA',
|
|
175
|
+
toolFunction: 'create_ticket',
|
|
176
|
+
result: 'Ticket #77 created',
|
|
177
|
+
success: true,
|
|
178
|
+
toolExecutionRequestId: 'exec-a',
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
// 7. Standalone SYSTEM message row — flushes the assistant turn and
|
|
183
|
+
// renders as a system-authored row.
|
|
184
|
+
{
|
|
185
|
+
id: 's1',
|
|
186
|
+
chatType: 'CLIENT_CHAT',
|
|
187
|
+
createdAt: '2026-07-19T09:03:00Z',
|
|
188
|
+
owner: { type: 'ASSISTANT' },
|
|
189
|
+
lastChunkStreamSeq: 40,
|
|
190
|
+
messageData: [{ type: 'SYSTEM', text: 'Technician joined the chat' } as any],
|
|
191
|
+
},
|
|
192
|
+
// 8. DIRECT_MESSAGE row. CHARACTERIZATION: DIRECT_MESSAGE is a
|
|
193
|
+
// realtime-only concept — the history processors have NO branch for
|
|
194
|
+
// it. This ADMIN-owned row takes the user-message path, which only
|
|
195
|
+
// renders TEXT data, so it contributes NOTHING to the output (the
|
|
196
|
+
// bubble is lost on reload). Recording current behavior as-is; see
|
|
197
|
+
// unification plan.
|
|
198
|
+
{
|
|
199
|
+
id: 'd1',
|
|
200
|
+
chatType: 'CLIENT_CHAT',
|
|
201
|
+
createdAt: '2026-07-19T09:03:30Z',
|
|
202
|
+
owner: { type: 'ADMIN', user: { id: 'admin-7', firstName: 'Jane' } },
|
|
203
|
+
messageData: [{ type: 'DIRECT_MESSAGE', text: 'Human here, taking over' } as any],
|
|
204
|
+
},
|
|
205
|
+
// 9. Error row inside an assistant turn.
|
|
206
|
+
{
|
|
207
|
+
id: 'a5',
|
|
208
|
+
chatType: 'CLIENT_CHAT',
|
|
209
|
+
createdAt: '2026-07-19T09:04:00Z',
|
|
210
|
+
owner: { type: 'ASSISTANT' },
|
|
211
|
+
messageData: [
|
|
212
|
+
{ type: 'TEXT', text: 'Attempting one more thing… ' },
|
|
213
|
+
{ type: 'ERROR', error: 'Agent crashed', details: '{"error":{"message":"boom"}}' },
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
describe('processHistoricalMessages — golden corpus', () => {
|
|
219
|
+
it('processes the full recorded dialog (snapshot)', () => {
|
|
220
|
+
const result = processHistoricalMessages(CORPUS, {
|
|
221
|
+
assistantName: 'Mingo AI',
|
|
222
|
+
assistantType: 'mingo',
|
|
223
|
+
assistantAvatar: '/avatars/mingo.png',
|
|
224
|
+
})
|
|
225
|
+
expect({
|
|
226
|
+
messages: result.messages,
|
|
227
|
+
escalatedApprovals: Array.from(result.escalatedApprovals.entries()),
|
|
228
|
+
}).toMatchSnapshot()
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('unresolved single approval is resurrected as a synthetic pending-approvals bubble', () => {
|
|
232
|
+
const result = processHistoricalMessages([
|
|
233
|
+
{
|
|
234
|
+
id: 'a1',
|
|
235
|
+
createdAt: '2026-07-19T09:00:00Z',
|
|
236
|
+
owner: { type: 'ASSISTANT' },
|
|
237
|
+
messageData: [
|
|
238
|
+
{
|
|
239
|
+
type: 'APPROVAL_REQUEST',
|
|
240
|
+
approvalRequestId: 'req-pending',
|
|
241
|
+
approvalType: 'CLIENT',
|
|
242
|
+
command: 'reboot server',
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
])
|
|
247
|
+
// Date.now is frozen → deterministic synthetic id.
|
|
248
|
+
expect(result.messages).toMatchSnapshot()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('owner-type author resolution: CLIENT vs ADMIN vs ADMIN-without-name display names/avatars', () => {
|
|
252
|
+
const rows: HistoricalMessage[] = [
|
|
253
|
+
{
|
|
254
|
+
id: 'u-client',
|
|
255
|
+
createdAt: '2026-07-19T09:00:00Z',
|
|
256
|
+
owner: { type: 'CLIENT', userId: 'user-1' },
|
|
257
|
+
messageData: [{ type: 'TEXT', text: 'from client' }],
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
id: 'u-admin-named',
|
|
261
|
+
createdAt: '2026-07-19T09:00:01Z',
|
|
262
|
+
owner: {
|
|
263
|
+
type: 'ADMIN',
|
|
264
|
+
user: { id: 'adm', firstName: 'Jane', lastName: 'Admin', image: { imageUrl: '/a.png' } },
|
|
265
|
+
},
|
|
266
|
+
messageData: [{ type: 'TEXT', text: 'from named admin' }],
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
id: 'u-admin-anon',
|
|
270
|
+
createdAt: '2026-07-19T09:00:02Z',
|
|
271
|
+
owner: { type: 'ADMIN' },
|
|
272
|
+
messageData: [{ type: 'TEXT', text: 'from anonymous admin' }],
|
|
273
|
+
},
|
|
274
|
+
]
|
|
275
|
+
expect(processHistoricalMessages(rows).messages).toMatchSnapshot()
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('chatTypeFilter drops rows of other chat types', () => {
|
|
279
|
+
const rows: HistoricalMessage[] = [
|
|
280
|
+
{
|
|
281
|
+
id: 'u1',
|
|
282
|
+
chatType: 'CLIENT_CHAT',
|
|
283
|
+
createdAt: '2026-07-19T09:00:00Z',
|
|
284
|
+
owner: { type: 'CLIENT' },
|
|
285
|
+
messageData: [{ type: 'TEXT', text: 'visible' }],
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: 'u2',
|
|
289
|
+
chatType: 'ADMIN_AI_CHAT',
|
|
290
|
+
createdAt: '2026-07-19T09:00:01Z',
|
|
291
|
+
owner: { type: 'CLIENT' },
|
|
292
|
+
messageData: [{ type: 'TEXT', text: 'filtered out' }],
|
|
293
|
+
},
|
|
294
|
+
]
|
|
295
|
+
expect(
|
|
296
|
+
processHistoricalMessages(rows, { chatTypeFilter: 'CLIENT_CHAT' }).messages,
|
|
297
|
+
).toMatchSnapshot()
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
describe('extractErrorMessages / processHistoricalMessagesWithErrors — golden', () => {
|
|
302
|
+
it('extractErrorMessages surfaces ERROR rows as standalone error-role messages', () => {
|
|
303
|
+
expect(extractErrorMessages(CORPUS, { assistantName: 'Mingo AI', assistantType: 'mingo' })).toMatchSnapshot()
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('processHistoricalMessagesWithErrors output parity on the same corpus (snapshot)', () => {
|
|
307
|
+
const result = processHistoricalMessagesWithErrors(CORPUS, {
|
|
308
|
+
assistantName: 'Mingo AI',
|
|
309
|
+
assistantType: 'mingo',
|
|
310
|
+
assistantAvatar: '/avatars/mingo.png',
|
|
311
|
+
})
|
|
312
|
+
expect({
|
|
313
|
+
messages: result.messages,
|
|
314
|
+
escalatedApprovals: Array.from(result.escalatedApprovals.entries()),
|
|
315
|
+
}).toMatchSnapshot()
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
// ─── GUIDE rows (#1583) ────────────────────────────────────────────────────
|
|
320
|
+
//
|
|
321
|
+
// The realtime path decodes a `GUIDE` chunk into a `guide-delta` event; the
|
|
322
|
+
// history path must decode the persisted `GUIDE` row into the SAME event so a
|
|
323
|
+
// reloaded dialog renders the guide card instead of silently dropping it.
|
|
324
|
+
// Explicit assertions rather than a corpus snapshot — the point is the exact
|
|
325
|
+
// coalescing shape, not the whole-dialog record.
|
|
326
|
+
|
|
327
|
+
describe('processHistoricalMessages — GUIDE rows', () => {
|
|
328
|
+
const guideDialog: HistoricalMessage[] = [
|
|
329
|
+
{
|
|
330
|
+
id: 'u1',
|
|
331
|
+
chatType: 'CLIENT_CHAT',
|
|
332
|
+
createdAt: '2026-07-19T09:00:00Z',
|
|
333
|
+
owner: { type: 'CLIENT', userId: 'user-42' },
|
|
334
|
+
messageData: [{ type: 'TEXT', text: 'how do I enroll a device?' }],
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
id: 'a1',
|
|
338
|
+
chatType: 'CLIENT_CHAT',
|
|
339
|
+
createdAt: '2026-07-19T09:00:05Z',
|
|
340
|
+
owner: { type: 'ASSISTANT', model: 'claude-sonnet-x' },
|
|
341
|
+
messageData: [
|
|
342
|
+
{ type: 'GUIDE', text: '## Enroll a device\n' },
|
|
343
|
+
{ type: 'GUIDE', text: '1. Open Settings' },
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
]
|
|
347
|
+
|
|
348
|
+
it('replays consecutive GUIDE rows into ONE coalesced guide segment', () => {
|
|
349
|
+
const { messages } = processHistoricalMessages(guideDialog, {
|
|
350
|
+
assistantName: 'Mingo AI',
|
|
351
|
+
assistantType: 'mingo',
|
|
352
|
+
})
|
|
353
|
+
expect(messages).toHaveLength(2)
|
|
354
|
+
expect(messages[1]).toMatchObject({
|
|
355
|
+
role: 'assistant',
|
|
356
|
+
content: [{ type: 'guide', text: '## Enroll a device\n1. Open Settings' }],
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('keeps GUIDE and TEXT as separate segments in wire order', () => {
|
|
361
|
+
const mixed: HistoricalMessage[] = [
|
|
362
|
+
{
|
|
363
|
+
id: 'a1',
|
|
364
|
+
chatType: 'CLIENT_CHAT',
|
|
365
|
+
createdAt: '2026-07-19T09:00:05Z',
|
|
366
|
+
owner: { type: 'ASSISTANT' },
|
|
367
|
+
messageData: [
|
|
368
|
+
{ type: 'TEXT', text: 'Here is the walkthrough.' },
|
|
369
|
+
{ type: 'GUIDE', text: '## Steps' },
|
|
370
|
+
{ type: 'TEXT', text: 'Anything else?' },
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
]
|
|
374
|
+
const { messages } = processHistoricalMessages(mixed)
|
|
375
|
+
expect(messages[0].content).toEqual([
|
|
376
|
+
{ type: 'text', text: 'Here is the walkthrough.' },
|
|
377
|
+
{ type: 'guide', text: '## Steps' },
|
|
378
|
+
{ type: 'text', text: 'Anything else?' },
|
|
379
|
+
])
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it('a GUIDE row with no text decodes to nothing (no empty card)', () => {
|
|
383
|
+
const empty: HistoricalMessage[] = [
|
|
384
|
+
{
|
|
385
|
+
id: 'a1',
|
|
386
|
+
chatType: 'CLIENT_CHAT',
|
|
387
|
+
createdAt: '2026-07-19T09:00:05Z',
|
|
388
|
+
owner: { type: 'ASSISTANT' },
|
|
389
|
+
messageData: [{ type: 'GUIDE' } as any],
|
|
390
|
+
},
|
|
391
|
+
]
|
|
392
|
+
expect(processHistoricalMessages(empty).messages).toEqual([])
|
|
393
|
+
})
|
|
394
|
+
})
|