Skip to content

Commit eb7c1f2

Browse files
authored
Merge pull request #886 from joelpeace48-cell/fix/issues-533-541-543-559
Fix chat hydration mismatch and improve telemetry/state docs
2 parents 5dafccc + da30797 commit eb7c1f2

8 files changed

Lines changed: 173 additions & 3 deletions

File tree

dex_with_fiat_frontend/src/hooks/chatStateMachine.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
ChatGuards,
66
ChatMachineContext,
77
ChatState,
8+
copyChatStateSnapshot,
89
createChatStateMachine,
10+
formatChatStateSnapshot,
911
} from './chatStateMachine';
1012

1113
describe('ChatStateMachine', () => {
@@ -664,3 +666,42 @@ describe('ChatStateMachine', () => {
664666
});
665667
});
666668
});
669+
670+
describe('chatStateMachine clipboard snapshot helpers', () => {
671+
it('formats a stable snapshot string', () => {
672+
const context: ChatMachineContext = {
673+
messageCount: 2,
674+
hasUserCancelled: false,
675+
pendingTransactionData: { tokenIn: 'XLM' },
676+
needsClarification: false,
677+
clarificationQuestion: null,
678+
errorMessage: null,
679+
lastEventTime: Date.now(),
680+
previousState: null,
681+
};
682+
expect(formatChatStateSnapshot(ChatState.ANALYZING, context)).toContain(
683+
'state=ANALYZING',
684+
);
685+
});
686+
687+
it('copies snapshot to clipboard when available', async () => {
688+
const writeText = vi.fn().mockResolvedValue(undefined);
689+
Object.defineProperty(navigator, 'clipboard', {
690+
configurable: true,
691+
value: { writeText },
692+
});
693+
const context: ChatMachineContext = {
694+
messageCount: 1,
695+
hasUserCancelled: false,
696+
pendingTransactionData: null,
697+
needsClarification: false,
698+
clarificationQuestion: null,
699+
errorMessage: null,
700+
lastEventTime: Date.now(),
701+
previousState: null,
702+
};
703+
const copied = await copyChatStateSnapshot(ChatState.SENDING_MESSAGE, context);
704+
expect(copied).toBe(true);
705+
expect(writeText).toHaveBeenCalledTimes(1);
706+
});
707+
});

dex_with_fiat_frontend/src/hooks/chatStateMachine.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,4 +289,39 @@ export function createChatStateMachine(): StateMachine<ChatState, ChatEvent, Cha
289289
return new StateMachine<ChatState, ChatEvent, ChatMachineContext>(config);
290290
}
291291

292+
/**
293+
* Formats a compact snapshot string that can be copied from debug tools/UI.
294+
*/
295+
export function formatChatStateSnapshot(
296+
state: ChatState,
297+
context: ChatMachineContext,
298+
): string {
299+
return [
300+
`state=${state}`,
301+
`messageCount=${context.messageCount}`,
302+
`hasUserCancelled=${context.hasUserCancelled}`,
303+
`needsClarification=${context.needsClarification}`,
304+
`hasPendingTx=${Boolean(context.pendingTransactionData)}`,
305+
].join(' | ');
306+
}
307+
308+
/**
309+
* Copies the provided state-machine snapshot to clipboard.
310+
* Returns true on success and false when clipboard is unavailable/fails.
311+
*/
312+
export async function copyChatStateSnapshot(
313+
state: ChatState,
314+
context: ChatMachineContext,
315+
): Promise<boolean> {
316+
if (typeof navigator === 'undefined' || !navigator.clipboard?.writeText) {
317+
return false;
318+
}
319+
try {
320+
await navigator.clipboard.writeText(formatChatStateSnapshot(state, context));
321+
return true;
322+
} catch {
323+
return false;
324+
}
325+
}
326+
292327
export { ChatGuards };

dex_with_fiat_frontend/src/hooks/useChat.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type AnalyzeResult = {
1818
};
1919

2020
let analyzeQueue: Array<AnalyzeResult | Promise<AnalyzeResult>> = [];
21+
const createNewSessionSpy = vi.fn(() => 'session-1');
2122

2223
class MockAIAssistant {
2324
static readonly LOW_CONFIDENCE_THRESHOLD = 0.7;
@@ -80,7 +81,7 @@ async function flushEffects(ticks: number = 1) {
8081

8182
vi.mock('./useChatHistory', () => ({
8283
useChatHistory: () => ({
83-
createNewSession: vi.fn(() => 'session-1'),
84+
createNewSession: createNewSessionSpy,
8485
updateCurrentSession: vi.fn(),
8586
loadSession: vi.fn(() => null),
8687
currentSessionId: 'session-1',
@@ -643,6 +644,7 @@ describe('Message Retry UX', () => {
643644
describe('useChat flow state transitions', () => {
644645
beforeEach(() => {
645646
analyzeQueue = [];
647+
createNewSessionSpy.mockClear();
646648
});
647649

648650
afterEach(() => {
@@ -692,6 +694,13 @@ describe('useChat flow state transitions', () => {
692694
harness.cleanup();
693695
});
694696

697+
it('hydration-safe init: does not throw and eventually initializes session', async () => {
698+
const harness = await setupHook();
699+
await flushEffects(3);
700+
expect(createNewSessionSpy).toHaveBeenCalled();
701+
harness.cleanup();
702+
});
703+
695704
it('pending data merge: extractedData is accumulated into pendingTransactionData', async () => {
696705
analyzeQueue = [
697706
{

dex_with_fiat_frontend/src/hooks/useChat.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const useChat = () => {
4949
currentSessionId,
5050
currentSession,
5151
} = useChatHistory();
52+
const [hasHydrated, setHasHydrated] = useState(false);
5253

5354
// State machine for chat lifecycle
5455
const machineRef = useRef<ReturnType<typeof createChatStateMachine>>(createChatStateMachine());
@@ -123,6 +124,10 @@ What would you like to do today? I'm here to make your XLM-to-fiat journey smoot
123124
const queuedSendsRef = useRef<QueuedSend[]>([]);
124125
const replayingQueueRef = useRef(false);
125126

127+
useEffect(() => {
128+
setHasHydrated(true);
129+
}, []);
130+
126131
useEffect(() => {
127132
messagesRef.current = messages;
128133
}, [messages]);
@@ -162,6 +167,7 @@ What would you like to do today? I'm here to make your XLM-to-fiat journey smoot
162167

163168
// Initialize chat session
164169
useEffect(() => {
170+
if (!hasHydrated) return;
165171
const machine = machineRef.current;
166172
const machineState = machine.getState();
167173

@@ -175,7 +181,7 @@ What would you like to do today? I'm here to make your XLM-to-fiat journey smoot
175181
machine.transition(ChatEvent.INITIALIZE_SESSION);
176182
}
177183
}
178-
}, [currentSession, currentSessionId, createNewSession]);
184+
}, [currentSession, currentSessionId, createNewSession, hasHydrated]);
179185

180186
// Persist messages to session
181187
useEffect(() => {
@@ -645,6 +651,7 @@ What would you like to do today? I'm here to make your XLM-to-fiat journey smoot
645651

646652
// Update suggested actions when wallet connection changes
647653
useEffect(() => {
654+
if (!hasHydrated) return;
648655
const machine = machineRef.current;
649656
if (machine.getState().state !== ChatState.UNINITIALIZED) {
650657
setMessages((prevMessages: ChatMessage[]) => {
@@ -662,7 +669,7 @@ What would you like to do today? I'm here to make your XLM-to-fiat journey smoot
662669
return prevMessages;
663670
});
664671
}
665-
}, [connection.isConnected, getInitialSuggestedActions]);
672+
}, [connection.isConnected, getInitialSuggestedActions, hasHydrated]);
666673

667674
// Derive conversationState from machine for backward compatibility
668675
const conversationState = useMemo((): ConversationState => {

dex_with_fiat_frontend/src/lib/chatTelemetry.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
getTelemetryConsent,
77
setTelemetryConsent,
88
TELEMETRY_SCHEMA_VERSION,
9+
telemetryMotionVariants,
910
type ChatEvent,
1011
type AccessibleAvatarColorTelemetryPayload,
1112
type MessageSendPayload,
@@ -187,6 +188,14 @@ describe('Avatar contrast helpers', () => {
187188
});
188189
});
189190

191+
describe('Telemetry motion variants', () => {
192+
it('exposes framer-motion variants for hidden/visible/exit', () => {
193+
expect(telemetryMotionVariants.hidden).toBeDefined();
194+
expect(telemetryMotionVariants.visible).toBeDefined();
195+
expect(telemetryMotionVariants.exit).toBeDefined();
196+
});
197+
});
198+
190199
// ── Regression test for issue #539 ────────────────────────────────────────
191200

192201
describe('Rendering overflow fix (issue #539)', () => {

dex_with_fiat_frontend/src/lib/chatTelemetry.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use client';
2+
import type { Variants } from 'framer-motion';
23

34
// ── Schema ────────────────────────────────────────────────────────────────
45

@@ -83,6 +84,27 @@ export interface AccessibleAvatarColorTelemetryPayload
8384
avatarContrastCompliant: boolean;
8485
}
8586

87+
/**
88+
* Shared animation variants for telemetry chips/toasts in chat UI.
89+
* Keeping this in telemetry allows consumers to animate state changes
90+
* consistently when telemetry event status changes.
91+
*/
92+
export const telemetryMotionVariants: Variants = {
93+
hidden: { opacity: 0, y: 6, scale: 0.98 },
94+
visible: {
95+
opacity: 1,
96+
y: 0,
97+
scale: 1,
98+
transition: { duration: 0.2, ease: 'easeOut' },
99+
},
100+
exit: {
101+
opacity: 0,
102+
y: -4,
103+
scale: 0.98,
104+
transition: { duration: 0.15, ease: 'easeIn' },
105+
},
106+
};
107+
86108
// ── Consent key ───────────────────────────────────────────────────────────
87109

88110
const CONSENT_KEY = 'nova_telemetry_consent';

docs/fuzz-test-boundary.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Fuzz Test Boundary Guide
2+
3+
This guide explains the boundary assumptions used by fuzz/property-style tests in this repository.
4+
5+
## Why boundaries matter
6+
7+
Most arithmetic and state-machine fuzz tests are only meaningful when generated inputs stay in the
8+
same domain as production calls. The goal is not "all possible i128 values", but "all realistic
9+
values plus edge-adjacent values that can reveal overflow or state bugs."
10+
11+
## Contract arithmetic boundaries
12+
13+
For fixed-point math in `stellar-contracts/src/math.rs`:
14+
15+
- `FIXED_POINT = 10_000_000`
16+
- Primary risk boundary: intermediate `a * b` before division.
17+
- Safe operational envelope should keep `a * b` well below `i128::MAX`.
18+
- Include targeted edge probes near:
19+
- `0`
20+
- `1`
21+
- `FIXED_POINT`
22+
- `i128::MAX / FIXED_POINT` (overflow-adjacent upper edge)
23+
24+
## State-machine boundaries
25+
26+
For chat lifecycle logic in `dex_with_fiat_frontend/src/hooks/chatStateMachine.ts`:
27+
28+
- message threshold edges (`2 -> 3` messages) are critical
29+
- cancellation and error recovery transitions should be sampled from all non-terminal states
30+
- transaction-trigger guards should be fuzzed with sparse transaction payloads
31+
(token only, amount only, fiat only) to verify minimum-data semantics
32+
33+
## Minimal checklist for new fuzz tests
34+
35+
- Document the accepted input range in a docstring or test comment.
36+
- Add at least one "just below / at / just above" boundary assertion.
37+
- Keep generated data deterministic with a seed when possible.
38+
- Record expected failure mode (panic, explicit error, rejected transition).

stellar-contracts/docs/OVERFLOW_PREVENTION.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,15 @@ fn propose_upgrade_overflow_prevention() {
164164

165165
---
166166

167+
## Fuzz boundary notes
168+
169+
When adding fuzz/property tests for arithmetic, prefer bounded domains around the contract's
170+
operational ranges rather than unrestricted full-range `i128` generation. See
171+
[`docs/fuzz-test-boundary.md`](../../docs/fuzz-test-boundary.md) for recommended ranges and
172+
"just below / at / just above" edge strategy.
173+
174+
---
175+
167176
## Checklist for New Arithmetic
168177

169178
When adding new arithmetic to the contract, verify:

0 commit comments

Comments
 (0)