import { describe, expect, it } from "vitest"; import type { ClickEvent, TimelineMarker } from "../../lib/api"; import { buildCaptureInsights, extractAppNameFromLabel, generateStepsFromActivity, markerTypeLabel, normalizeTimelineMarkers, } from "./activityHeuristics"; function marker( id: string, type: TimelineMarker["type"], timestamp_ms: number, label: string, meta?: Record ): TimelineMarker { return { id, type, timestamp_ms, label, meta }; } function click(timestamp_ms: number): ClickEvent { return { timestamp_ms, x: 10, y: 20, button: "left" }; } describe("activityHeuristics", () => { it("summarizes captured activity insights", () => { const markers: TimelineMarker[] = [ marker("m1", "app_focus", 0, "Google Chrome: Inbox"), marker("m2", "shortcut", 1200, "cmd+l"), marker("m3", "keypress", 1400, "key press"), marker("m4", "mouse_scroll", 1800, "scroll down"), marker("m5", "app_focus", 3000, "Terminal: zsh"), ]; const clicks: ClickEvent[] = [click(1000), click(2000)]; const insights = buildCaptureInsights(markers, clicks); expect(insights.appCount).toBe(2); expect(insights.shortcutCount).toBe(1); expect(insights.keypressCount).toBe(1); expect(insights.scrollCount).toBe(1); expect(insights.clickCount).toBe(2); expect(insights.topApps).toContain("Google Chrome"); }); it("generates deterministic steps from mixed markers and clicks", () => { const markers: TimelineMarker[] = [ marker("m1", "app_focus", 0, "Google Chrome: Gmail"), marker("m2", "click", 1500, "click"), marker("m3", "app_focus", 5200, "Terminal: zsh"), marker("m4", "shortcut", 7200, "cmd+shift+p"), ]; const clicks: ClickEvent[] = [click(1300), click(1600), click(8100)]; const steps = generateStepsFromActivity(markers, clicks, 12_000); expect(steps.length).toBeGreaterThanOrEqual(2); expect(steps[0].start_ms).toBe(0); expect(steps[0].title).toMatch(/Work in/); expect(steps.some((step) => step.title.includes("shortcut"))).toBe(true); expect(steps.every((step) => step.end_ms > step.start_ms)).toBe(true); }); it("merges low-novelty adjacent segments from repetitive click activity", () => { const markers: TimelineMarker[] = [ marker("m1", "app_focus", 0, "Google Chrome: Dashboard"), marker("m2", "click", 1300, "click"), marker("m3", "click", 2600, "click"), marker("m4", "click", 3900, "click"), marker("m5", "click", 5200, "click"), marker("m6", "click", 6500, "click"), ]; const clicks: ClickEvent[] = [ click(1300), click(2600), click(3900), click(5200), click(6500), ]; const steps = generateStepsFromActivity(markers, clicks, 10_000); expect(steps.length).toBeLessThanOrEqual(2); expect(steps[0].start_ms).toBe(0); expect(steps[steps.length - 1].end_ms).toBe(10_000); }); it("preserves steps when a strong transition signal appears", () => { const markers: TimelineMarker[] = [ marker("m1", "app_focus", 0, "Google Chrome: Dashboard"), marker("m2", "click", 1200, "click"), marker("m3", "app_focus", 3200, "Terminal: zsh"), marker("m4", "command", 5100, "npm run test"), ]; const clicks: ClickEvent[] = [click(1200)]; const steps = generateStepsFromActivity(markers, clicks, 9000); const titles = steps.map((step) => step.title); expect(steps.length).toBeGreaterThanOrEqual(3); expect(titles.some((title) => title.includes("Terminal"))).toBe(true); expect(titles).toContain("Run command"); }); it("returns empty generated steps when duration is invalid", () => { expect(generateStepsFromActivity([], [], 0)).toEqual([]); }); it("derives web workflow titles for Gmail navigation and message open", () => { const markers: TimelineMarker[] = [ marker("a1", "app_focus", 0, "Google Chrome: New Tab"), marker("k1", "keypress", 220, "key press"), marker("k2", "keypress", 350, "key press"), marker("k3", "keypress", 500, "key enter"), marker("a2", "app_focus", 1400, "Google Chrome: Gmail"), marker("a3", "app_focus", 2600, "Google Chrome: Inbox (99) - user@gmail.com - Gmail"), marker("c1", "click", 3600, "click"), marker( "a4", "app_focus", 4600, "Google Chrome: Re: Interview loop details - user@gmail.com - Gmail" ), ]; const clicks: ClickEvent[] = [click(3600)]; const steps = generateStepsFromActivity(markers, clicks, 8000); const titles = steps.map((step) => step.title); expect( titles.some((title) => title === "Navigate in browser" || title === "Go to gmail.com") ).toBe(true); expect(titles).toContain("Open email from Gmail inbox"); expect( titles.some((title) => title.startsWith("Open email in Gmail: Re: Interview loop details") ) ).toBe(true); expect(steps.some((step) => step.tags.includes("site:gmail"))).toBe(true); }); it("derives ecommerce search and product step titles from browser window patterns", () => { const markers: TimelineMarker[] = [ marker( "a1", "app_focus", 0, "Google Chrome: Best Buy | Official Online Store | Shop Now & Save" ), marker("c1", "click", 900, "click"), marker("k1", "keypress", 1100, "key press"), marker("k2", "keypress", 1220, "key press"), marker("k3", "keypress", 1340, "key press"), marker("k4", "keypress", 1480, "key enter"), marker("a2", "app_focus", 2300, "Google Chrome: iphone - Best Buy"), marker("c2", "click", 3400, "click"), marker( "a3", "app_focus", 4600, "Google Chrome: Apple iPhone 17 256GB Sage (AT&T) MG4A4LL/A - Best Buy" ), ]; const clicks: ClickEvent[] = [click(900), click(3400)]; const steps = generateStepsFromActivity(markers, clicks, 9000); const titles = steps.map((step) => step.title); expect( titles.some( (title) => title.includes('Search for "iphone" on Best Buy') || title.includes('Review "iphone" results on Best Buy') ) ).toBe(true); expect( titles.some( (title) => title.includes("Open product page on Best Buy") || title.includes( 'Review "Apple iPhone 17 256GB Sage (AT&T) MG4A4LL/A" results on Best Buy' ) ) ).toBe(true); expect(steps.some((step) => step.tags.includes("site:best_buy"))).toBe(true); }); it("derives search intent from dom navigation URLs (less hardcoded)", () => { const markers: TimelineMarker[] = [ marker("n1", "dom_navigation", 500, "Navigate: www.bestbuy.com", { url: "https://www.bestbuy.com/site/searchpage.jsp?st=iphone", title: "iphone - Best Buy", transition: "load", }), marker("c1", "dom_action", 1200, "Click: Apple iPhone", { url: "https://www.bestbuy.com/site/searchpage.jsp?st=iphone", title: "iphone - Best Buy", action: "click", target: { role: "link", name: "Apple iPhone" }, }), ]; const steps = generateStepsFromActivity(markers, [], 5000); expect(steps.some((step) => step.title.includes('Search for "iphone"'))).toBe(true); expect(steps.some((step) => step.tags.includes("site:best_buy"))).toBe(true); }); it("derives Gmail email open titles from dom click targets", () => { const markers: TimelineMarker[] = [ marker("n1", "dom_navigation", 200, "Navigate: mail.google.com", { url: "https://mail.google.com/mail/u/0/#inbox", title: "Inbox (12) - user@gmail.com - Gmail", transition: "hashchange", }), marker("a1", "dom_action", 800, "Click: Re: Interview loop details", { url: "https://mail.google.com/mail/u/0/#inbox", title: "Inbox (12) - user@gmail.com - Gmail", action: "click", target: { role: "link", name: "Re: Interview loop details" }, }), ]; const steps = generateStepsFromActivity(markers, [], 4000); expect( steps.some((step) => step.title.startsWith("Open email in Gmail: Re: Interview loop details") ) ).toBe(true); expect(steps.some((step) => step.tags.includes("site:gmail"))).toBe(true); }); it("normalizes scroll bursts into fewer timeline markers", () => { const markers: TimelineMarker[] = [ marker("s1", "mouse_scroll", 1000, "scroll up"), marker("s2", "mouse_scroll", 1100, "scroll up"), marker("s3", "mouse_scroll", 1225, "scroll up"), marker("c1", "click", 1600, "click"), marker("s4", "mouse_scroll", 2400, "scroll down"), marker("s5", "mouse_scroll", 2500, "scroll down"), ]; const normalized = normalizeTimelineMarkers(markers); expect(normalized).toHaveLength(3); expect(normalized[0].type).toBe("mouse_scroll"); expect(normalized[0].label).toBe("scroll up x3"); expect(normalized[1].type).toBe("click"); expect(normalized[2].label).toBe("scroll down x2"); }); it("counts normalized scroll burst labels in capture insights", () => { const markers: TimelineMarker[] = [ marker("s1", "mouse_scroll", 1000, "scroll up x4"), marker("s2", "mouse_scroll", 1700, "scroll down x3"), ]; const insights = buildCaptureInsights(markers, []); expect(insights.scrollCount).toBe(7); }); it("provides utility label helpers", () => { expect(extractAppNameFromLabel("Google Chrome: Gmail")).toBe("Google Chrome"); expect(markerTypeLabel("app_focus")).toBe("App Focus"); expect(markerTypeLabel("mouse_scroll")).toBe("Scroll"); }); });