This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { beforeEach, describe, expect, test } from 'bun:test';
|
||||
|
||||
import { MessageFreshnessDetector } from './messageFreshness';
|
||||
|
||||
import type { Message } from '@opencode-ai/sdk/v2';
|
||||
|
||||
const makeAssistantMessage = (id: string, created: number): Message =>
|
||||
({
|
||||
id,
|
||||
role: 'assistant',
|
||||
sessionID: 'session-a',
|
||||
time: { created },
|
||||
}) as unknown as Message;
|
||||
|
||||
describe('MessageFreshnessDetector.shouldAnimateMessage', () => {
|
||||
let detector: MessageFreshnessDetector;
|
||||
|
||||
beforeEach(() => {
|
||||
detector = MessageFreshnessDetector.getInstance();
|
||||
detector.clearAll();
|
||||
});
|
||||
|
||||
test('fresh message animates once and is recorded as seen', () => {
|
||||
detector.recordSessionStart('session-a');
|
||||
const message = makeAssistantMessage('msg-fresh', Date.now());
|
||||
|
||||
expect(detector.shouldAnimateMessage(message, 'session-a')).toBe(true);
|
||||
expect(detector.hasBeenAnimated('msg-fresh')).toBe(true);
|
||||
});
|
||||
|
||||
test('regression #2124: fresh message does not re-animate when returning to the session', () => {
|
||||
detector.recordSessionStart('session-a');
|
||||
const message = makeAssistantMessage('msg-fresh', Date.now());
|
||||
|
||||
// First visit: the message is fresh and animates.
|
||||
expect(detector.shouldAnimateMessage(message, 'session-a')).toBe(true);
|
||||
|
||||
// User switches away and back; ChatViewport remounts and re-evaluates
|
||||
// before recordSessionStart runs again, so the old session start time
|
||||
// is still in effect. The message must not animate a second time.
|
||||
expect(detector.shouldAnimateMessage(message, 'session-a')).toBe(false);
|
||||
});
|
||||
|
||||
test('stale history message never animates and is recorded as seen', () => {
|
||||
detector.recordSessionStart('session-a');
|
||||
const message = makeAssistantMessage('msg-old', Date.now() - 60_000);
|
||||
|
||||
expect(detector.shouldAnimateMessage(message, 'session-a')).toBe(false);
|
||||
expect(detector.hasBeenAnimated('msg-old')).toBe(true);
|
||||
expect(detector.shouldAnimateMessage(message, 'session-a')).toBe(false);
|
||||
});
|
||||
|
||||
test('message evaluated without a recorded session start does not animate and is recorded', () => {
|
||||
const message = makeAssistantMessage('msg-no-session', Date.now());
|
||||
|
||||
expect(detector.shouldAnimateMessage(message, 'session-a')).toBe(false);
|
||||
expect(detector.hasBeenAnimated('msg-no-session')).toBe(true);
|
||||
|
||||
// Recording the session start afterwards must not resurrect the animation.
|
||||
detector.recordSessionStart('session-a');
|
||||
expect(detector.shouldAnimateMessage(message, 'session-a')).toBe(false);
|
||||
});
|
||||
|
||||
test('non-assistant messages never animate', () => {
|
||||
detector.recordSessionStart('session-a');
|
||||
const message = {
|
||||
id: 'msg-user',
|
||||
role: 'user',
|
||||
sessionID: 'session-a',
|
||||
time: { created: Date.now() },
|
||||
} as unknown as Message;
|
||||
|
||||
expect(detector.shouldAnimateMessage(message, 'session-a')).toBe(false);
|
||||
});
|
||||
|
||||
test('a new fresh message still animates after older fresh messages were seen', () => {
|
||||
detector.recordSessionStart('session-a');
|
||||
const first = makeAssistantMessage('msg-first', Date.now());
|
||||
const second = makeAssistantMessage('msg-second', Date.now());
|
||||
|
||||
expect(detector.shouldAnimateMessage(first, 'session-a')).toBe(true);
|
||||
expect(detector.shouldAnimateMessage(second, 'session-a')).toBe(true);
|
||||
expect(detector.shouldAnimateMessage(second, 'session-a')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -44,10 +44,13 @@ export class MessageFreshnessDetector {
|
||||
|
||||
const isFresh = message.time.created > (sessionStartTime - 5000);
|
||||
|
||||
if (!isFresh) {
|
||||
this.seenMessageIds.add(message.id);
|
||||
this.messageCreationTimes.set(message.id, message.time.created);
|
||||
}
|
||||
// Record fresh messages too so they animate at most once per detector
|
||||
// lifetime. The detector is a module singleton that outlives ChatViewport
|
||||
// remounts; without this, switching away and back re-evaluates the same
|
||||
// message against the stale session start time (recordSessionStart runs
|
||||
// in an effect after the first render) and replays the entry animation.
|
||||
this.seenMessageIds.add(message.id);
|
||||
this.messageCreationTimes.set(message.id, message.time.created);
|
||||
|
||||
return isFresh;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user