mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-19 10:43:08 +00:00
Fix agent output summary for pipeline steps (#812)
* Changes from fix/agent-output-summary-for-pipeline-steps * feat: Optimize pipeline summary extraction and fix regex vulnerability * fix: Use fallback summary for pipeline steps when extraction fails * fix: Strip follow-up session scaffold from pipeline step fallback summaries
This commit is contained in:
238
apps/server/tests/unit/ui/summary-auto-scroll.test.ts
Normal file
238
apps/server/tests/unit/ui/summary-auto-scroll.test.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Unit tests for the summary auto-scroll detection logic.
|
||||
*
|
||||
* These tests verify the behavior of the scroll detection function used in
|
||||
* AgentOutputModal to determine if auto-scroll should be enabled.
|
||||
*
|
||||
* The logic mirrors the handleSummaryScroll function in:
|
||||
* apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx
|
||||
*
|
||||
* Auto-scroll behavior:
|
||||
* - When user is at or near the bottom (< 50px from bottom), auto-scroll is enabled
|
||||
* - When user scrolls up to view older content, auto-scroll is disabled
|
||||
* - Scrolling back to bottom re-enables auto-scroll
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
/**
|
||||
* Determines if the scroll position is at the bottom of the container.
|
||||
* This is the core logic from handleSummaryScroll in AgentOutputModal.
|
||||
*
|
||||
* @param scrollTop - Current scroll position from top
|
||||
* @param scrollHeight - Total scrollable height
|
||||
* @param clientHeight - Visible height of the container
|
||||
* @param threshold - Distance from bottom to consider "at bottom" (default: 50px)
|
||||
* @returns true if at bottom, false otherwise
|
||||
*/
|
||||
function isScrollAtBottom(
|
||||
scrollTop: number,
|
||||
scrollHeight: number,
|
||||
clientHeight: number,
|
||||
threshold = 50
|
||||
): boolean {
|
||||
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
||||
return distanceFromBottom < threshold;
|
||||
}
|
||||
|
||||
describe('Summary Auto-Scroll Detection Logic', () => {
|
||||
describe('basic scroll position detection', () => {
|
||||
it('should return true when scrolled to exact bottom', () => {
|
||||
// Container: 500px tall, content: 1000px tall
|
||||
// ScrollTop: 500 (scrolled to bottom)
|
||||
const result = isScrollAtBottom(500, 1000, 500);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when near bottom (within threshold)', () => {
|
||||
// 49px from bottom - within 50px threshold
|
||||
const result = isScrollAtBottom(451, 1000, 500);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when exactly at threshold boundary (49px)', () => {
|
||||
// 49px from bottom
|
||||
const result = isScrollAtBottom(451, 1000, 500);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when just outside threshold (51px)', () => {
|
||||
// 51px from bottom - outside 50px threshold
|
||||
const result = isScrollAtBottom(449, 1000, 500);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when scrolled to top', () => {
|
||||
const result = isScrollAtBottom(0, 1000, 500);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when scrolled to middle', () => {
|
||||
const result = isScrollAtBottom(250, 1000, 500);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases with small content', () => {
|
||||
it('should return true when content fits in viewport (no scroll needed)', () => {
|
||||
// Content is smaller than container - no scrolling possible
|
||||
const result = isScrollAtBottom(0, 300, 500);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when content exactly fits viewport', () => {
|
||||
const result = isScrollAtBottom(0, 500, 500);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when content slightly exceeds viewport (within threshold)', () => {
|
||||
// Content: 540px, Viewport: 500px, can scroll 40px
|
||||
// At scroll 0, we're 40px from bottom - within threshold
|
||||
const result = isScrollAtBottom(0, 540, 500);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('large content scenarios', () => {
|
||||
it('should correctly detect bottom in very long content', () => {
|
||||
// Simulate accumulated summary from many pipeline steps
|
||||
// Content: 10000px, Viewport: 500px
|
||||
const result = isScrollAtBottom(9500, 10000, 500);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should correctly detect non-bottom in very long content', () => {
|
||||
// User scrolled up to read earlier summaries
|
||||
const result = isScrollAtBottom(5000, 10000, 500);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should detect when user scrolls up from bottom', () => {
|
||||
// Started at bottom (scroll: 9500), then scrolled up 100px
|
||||
const result = isScrollAtBottom(9400, 10000, 500);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom threshold values', () => {
|
||||
it('should work with larger threshold (100px)', () => {
|
||||
// 75px from bottom - within 100px threshold
|
||||
const result = isScrollAtBottom(425, 1000, 500, 100);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should work with smaller threshold (10px)', () => {
|
||||
// 15px from bottom - outside 10px threshold
|
||||
const result = isScrollAtBottom(485, 1000, 500, 10);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should work with zero threshold (exact match only)', () => {
|
||||
// At exact bottom - distanceFromBottom = 0, which is NOT < 0 with strict comparison
|
||||
// This is an edge case: the implementation uses < not <=
|
||||
const result = isScrollAtBottom(500, 1000, 500, 0);
|
||||
expect(result).toBe(false); // 0 < 0 is false
|
||||
|
||||
// 1px from bottom - also fails
|
||||
const result2 = isScrollAtBottom(499, 1000, 500, 0);
|
||||
expect(result2).toBe(false);
|
||||
|
||||
// For exact match with 0 threshold, we need negative distanceFromBottom
|
||||
// which happens when scrollTop > scrollHeight - clientHeight (overscroll)
|
||||
const result3 = isScrollAtBottom(501, 1000, 500, 0);
|
||||
expect(result3).toBe(true); // -1 < 0 is true
|
||||
});
|
||||
});
|
||||
|
||||
describe('pipeline summary scrolling scenarios', () => {
|
||||
it('should enable auto-scroll when new content arrives while at bottom', () => {
|
||||
// User is at bottom viewing step 2 summary
|
||||
// Step 3 summary is added, increasing scrollHeight from 1000 to 1500
|
||||
// ScrollTop stays at 950 (was at bottom), but now user needs to scroll
|
||||
|
||||
// Before new content: isScrollAtBottom(950, 1000, 500) = true
|
||||
// After new content: auto-scroll should kick in to scroll to new bottom
|
||||
|
||||
// Simulating the auto-scroll effect setting scrollTop to new bottom
|
||||
const newScrollTop = 1500 - 500; // scrollHeight - clientHeight
|
||||
const result = isScrollAtBottom(newScrollTop, 1500, 500);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should not auto-scroll when user is reading earlier summaries', () => {
|
||||
// User scrolled up to read step 1 summary while step 3 is added
|
||||
// scrollHeight increases, but scrollTop stays same
|
||||
// User is now further from bottom
|
||||
|
||||
// User was at scroll position 200 (reading early content)
|
||||
// New content increases scrollHeight from 1000 to 1500
|
||||
// Distance from bottom goes from 300 to 800
|
||||
const result = isScrollAtBottom(200, 1500, 500);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should re-enable auto-scroll when user scrolls back to bottom', () => {
|
||||
// User was reading step 1 (scrollTop: 200)
|
||||
// User scrolls back to bottom to see latest content
|
||||
const result = isScrollAtBottom(1450, 1500, 500);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('decimal scroll values', () => {
|
||||
it('should handle fractional scroll positions', () => {
|
||||
// Browsers can report fractional scroll values
|
||||
const result = isScrollAtBottom(499.5, 1000, 500);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle fractional scroll heights', () => {
|
||||
const result = isScrollAtBottom(450.7, 1000.3, 500);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('negative and invalid inputs', () => {
|
||||
it('should handle negative scrollTop (bounce scroll)', () => {
|
||||
// iOS can report negative scrollTop during bounce
|
||||
const result = isScrollAtBottom(-10, 1000, 500);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle zero scrollHeight', () => {
|
||||
// Empty content
|
||||
const result = isScrollAtBottom(0, 0, 500);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle zero clientHeight', () => {
|
||||
// Hidden container - distanceFromBottom = 1000 - 0 - 0 = 1000
|
||||
// This is not < threshold, so returns false
|
||||
// This edge case represents a broken/invisible container
|
||||
const result = isScrollAtBottom(0, 1000, 0);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('real-world accumulated summary dimensions', () => {
|
||||
it('should handle typical 3-step pipeline summary dimensions', () => {
|
||||
// Approximate: 3 steps x ~800px each = ~2400px
|
||||
// Viewport: 400px (modal height)
|
||||
const result = isScrollAtBottom(2000, 2400, 400);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle large 10-step pipeline summary dimensions', () => {
|
||||
// Approximate: 10 steps x ~800px each = ~8000px
|
||||
// Viewport: 400px
|
||||
const result = isScrollAtBottom(7600, 8000, 400);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect scroll to top of large summary', () => {
|
||||
// User at top of 10-step summary
|
||||
const result = isScrollAtBottom(0, 8000, 400);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user