Skip to main content
Glama
trajectory-tracker.test.ts19.5 kB
/** * Tests for EmotionalTrajectoryTracker */ import { beforeEach, describe, expect, it } from "vitest"; import { EmotionalTrajectoryTracker } from "../../../emotion/trajectory-tracker"; import type { CircumplexState } from "../../../emotion/types"; describe("EmotionalTrajectoryTracker", () => { let tracker: EmotionalTrajectoryTracker; beforeEach(() => { tracker = new EmotionalTrajectoryTracker(); }); const createState = ( valence: number, arousal: number, dominance: number, timestamp?: Date ): CircumplexState => ({ valence, arousal, dominance, confidence: 0.8, timestamp: timestamp || new Date(), }); describe("trackEmotionalState", () => { it("should track a new emotional state", () => { const state = createState(0.5, 0.6, 0.3); tracker.trackEmotionalState(state); const history = tracker.getHistory(); expect(history).toHaveLength(1); expect(history[0]).toEqual(state); }); it("should track multiple states", () => { tracker.trackEmotionalState(createState(0.5, 0.6, 0.3)); tracker.trackEmotionalState(createState(0.3, 0.4, 0.2)); tracker.trackEmotionalState(createState(0.7, 0.8, 0.5)); expect(tracker.getHistory()).toHaveLength(3); }); it("should track trigger when significant shift occurs", () => { tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(0.8, 0.9, 0.7), "good_news"); const triggers = tracker.identifyTriggers(); expect(triggers).toHaveLength(1); expect(triggers[0].trigger).toBe("good_news"); }); it("should not track trigger when shift is below threshold", () => { tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(0.1, 0.1, 0.1), "minor_event"); const triggers = tracker.identifyTriggers(); expect(triggers).toHaveLength(0); }); it("should validate state values", () => { expect(() => { tracker.trackEmotionalState(createState(2.0, 0.5, 0.0)); }).toThrow("Valence must be between -1 and 1"); expect(() => { tracker.trackEmotionalState(createState(0.0, 2.0, 0.0)); }).toThrow("Arousal must be between 0 and 1"); expect(() => { tracker.trackEmotionalState(createState(0.0, 0.5, 2.0)); }).toThrow("Dominance must be between -1 and 1"); }); it("should reject NaN values", () => { expect(() => { tracker.trackEmotionalState(createState(NaN, 0.5, 0.0)); }).toThrow("State values cannot be NaN"); }); }); describe("getHistory", () => { it("should return empty array when no states tracked", () => { expect(tracker.getHistory()).toEqual([]); }); it("should return all states when no limit specified", () => { tracker.trackEmotionalState(createState(0.5, 0.6, 0.3)); tracker.trackEmotionalState(createState(0.3, 0.4, 0.2)); tracker.trackEmotionalState(createState(0.7, 0.8, 0.5)); expect(tracker.getHistory()).toHaveLength(3); }); it("should return limited states when limit specified", () => { tracker.trackEmotionalState(createState(0.5, 0.6, 0.3)); tracker.trackEmotionalState(createState(0.3, 0.4, 0.2)); tracker.trackEmotionalState(createState(0.7, 0.8, 0.5)); const limited = tracker.getHistory(2); expect(limited).toHaveLength(2); expect(limited[0].valence).toBe(0.3); expect(limited[1].valence).toBe(0.7); }); }); describe("clearHistory", () => { it("should clear all tracked states", () => { tracker.trackEmotionalState(createState(0.5, 0.6, 0.3)); tracker.trackEmotionalState(createState(0.3, 0.4, 0.2)); tracker.clearHistory(); expect(tracker.getHistory()).toHaveLength(0); }); it("should clear all triggers", () => { tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(0.8, 0.9, 0.7), "trigger1"); tracker.clearHistory(); expect(tracker.identifyTriggers()).toHaveLength(0); }); }); describe("getStatistics", () => { it("should return zero statistics for empty history", () => { const stats = tracker.getStatistics(); expect(stats.totalStates).toBe(0); expect(stats.averageValence).toBe(0); expect(stats.averageArousal).toBe(0); expect(stats.averageDominance).toBe(0); expect(stats.volatility).toBe(0); expect(stats.timeSpan).toBe(0); }); it("should calculate correct averages", () => { tracker.trackEmotionalState(createState(0.5, 0.6, 0.3)); tracker.trackEmotionalState(createState(0.3, 0.4, 0.2)); tracker.trackEmotionalState(createState(0.7, 0.8, 0.5)); const stats = tracker.getStatistics(); expect(stats.totalStates).toBe(3); expect(stats.averageValence).toBeCloseTo(0.5, 1); expect(stats.averageArousal).toBeCloseTo(0.6, 1); expect(stats.averageDominance).toBeCloseTo(0.333, 1); }); it("should calculate volatility", () => { tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(1.0, 1.0, 1.0)); tracker.trackEmotionalState(createState(-1.0, 0.0, -1.0)); const stats = tracker.getStatistics(); expect(stats.volatility).toBeGreaterThan(0); }); it("should calculate time span", () => { const now = new Date(); const later = new Date(now.getTime() + 5000); tracker.trackEmotionalState(createState(0.5, 0.6, 0.3, now)); tracker.trackEmotionalState(createState(0.3, 0.4, 0.2, later)); const stats = tracker.getStatistics(); expect(stats.timeSpan).toBe(5000); }); }); describe("detectEmotionalShift", () => { it("should detect shift above threshold", () => { tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(0.8, 0.9, 0.7)); const shift = tracker.detectEmotionalShift(0.3); expect(shift).not.toBeNull(); expect(shift?.magnitude).toBeGreaterThan(0.3); }); it("should not detect shift below threshold", () => { tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(0.1, 0.1, 0.1)); const shift = tracker.detectEmotionalShift(0.5); expect(shift).toBeNull(); }); it("should return null with insufficient history", () => { tracker.trackEmotionalState(createState(0.5, 0.6, 0.3)); const shift = tracker.detectEmotionalShift(0.3); expect(shift).toBeNull(); }); it("should validate threshold", () => { tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(0.8, 0.9, 0.7)); expect(() => { tracker.detectEmotionalShift(-0.1); }).toThrow("Threshold must be between 0 and 1"); expect(() => { tracker.detectEmotionalShift(1.5); }).toThrow("Threshold must be between 0 and 1"); }); }); describe("recognizePatterns", () => { it("should return empty array with insufficient history", () => { tracker.trackEmotionalState(createState(0.5, 0.6, 0.3)); tracker.trackEmotionalState(createState(0.3, 0.4, 0.2)); const patterns = tracker.recognizePatterns(); expect(patterns).toEqual([]); }); it("should detect recurring pattern", () => { // Create similar states multiple times tracker.trackEmotionalState(createState(0.5, 0.6, 0.3)); tracker.trackEmotionalState(createState(0.52, 0.58, 0.32)); tracker.trackEmotionalState(createState(0.48, 0.62, 0.28)); const patterns = tracker.recognizePatterns(); const recurring = patterns.find((p) => p.type === "recurring"); expect(recurring).toBeDefined(); expect(recurring?.frequency).toBeGreaterThanOrEqual(3); }); it("should detect progressive pattern", () => { // Create gradually improving states tracker.trackEmotionalState(createState(-0.5, 0.3, -0.2)); tracker.trackEmotionalState(createState(0.0, 0.5, 0.1)); tracker.trackEmotionalState(createState(0.5, 0.7, 0.4)); const patterns = tracker.recognizePatterns(); const progressive = patterns.find((p) => p.type === "progressive"); expect(progressive).toBeDefined(); expect(progressive?.description).toContain("improving"); }); it("should detect declining progressive pattern", () => { // Create gradually declining states tracker.trackEmotionalState(createState(0.5, 0.7, 0.4)); tracker.trackEmotionalState(createState(0.0, 0.5, 0.1)); tracker.trackEmotionalState(createState(-0.5, 0.3, -0.2)); const patterns = tracker.recognizePatterns(); const progressive = patterns.find((p) => p.type === "progressive"); expect(progressive).toBeDefined(); expect(progressive?.description).toContain("declining"); }); it("should detect reactive pattern", () => { // Create rapid changes tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(1.0, 1.0, 1.0)); tracker.trackEmotionalState(createState(-1.0, 0.0, -1.0)); const patterns = tracker.recognizePatterns(); const reactive = patterns.find((p) => p.type === "reactive"); expect(reactive).toBeDefined(); expect(reactive?.description).toContain("rapid changes"); }); }); describe("identifyTriggers", () => { it("should return empty array when no triggers tracked", () => { tracker.trackEmotionalState(createState(0.5, 0.6, 0.3)); const triggers = tracker.identifyTriggers(); expect(triggers).toEqual([]); }); it("should identify trigger with correct properties", () => { tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(0.8, 0.9, 0.7), "good_news"); const triggers = tracker.identifyTriggers(); expect(triggers).toHaveLength(1); expect(triggers[0].trigger).toBe("good_news"); expect(triggers[0].frequency).toBe(1); expect(triggers[0].averageIntensity).toBeGreaterThan(0); expect(triggers[0].lastOccurrence).toBeInstanceOf(Date); }); it("should track multiple occurrences of same trigger", () => { tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(0.8, 0.9, 0.7), "trigger1"); tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(0.7, 0.8, 0.6), "trigger1"); const triggers = tracker.identifyTriggers(); expect(triggers).toHaveLength(1); expect(triggers[0].frequency).toBe(2); }); it("should track different emotion types for different triggers", () => { // Joy trigger (high valence, high arousal) tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(0.8, 0.9, 0.5), "joy_trigger"); // Anger trigger (low valence, high arousal) tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(-0.8, 0.9, 0.5), "anger_trigger"); const triggers = tracker.identifyTriggers(); expect(triggers).toHaveLength(2); expect(triggers.find((t) => t.trigger === "joy_trigger")?.emotionType).toBe("joy"); expect(triggers.find((t) => t.trigger === "anger_trigger")?.emotionType).toBe("anger"); }); }); describe("generateTrajectoryInsights", () => { it("should return empty array with insufficient history", () => { tracker.trackEmotionalState(createState(0.5, 0.6, 0.3)); tracker.trackEmotionalState(createState(0.3, 0.4, 0.2)); const insights = tracker.generateTrajectoryInsights(); expect(insights).toEqual([]); }); it("should generate trend insight for improving emotions", () => { tracker.trackEmotionalState(createState(-0.5, 0.3, -0.2)); tracker.trackEmotionalState(createState(0.0, 0.5, 0.1)); tracker.trackEmotionalState(createState(0.5, 0.7, 0.4)); const insights = tracker.generateTrajectoryInsights(); const trendInsight = insights.find((i) => i.type === "trend"); expect(trendInsight).toBeDefined(); expect(trendInsight?.description).toContain("improving"); }); it("should generate trend insight for declining emotions with recommendation", () => { tracker.trackEmotionalState(createState(0.5, 0.7, 0.4)); tracker.trackEmotionalState(createState(0.0, 0.5, 0.1)); tracker.trackEmotionalState(createState(-0.5, 0.3, -0.2)); const insights = tracker.generateTrajectoryInsights(); const trendInsight = insights.find((i) => i.type === "trend"); expect(trendInsight).toBeDefined(); expect(trendInsight?.description).toContain("declining"); expect(trendInsight?.recommendation).toBeDefined(); }); it("should generate volatility insight", () => { tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(1.0, 1.0, 1.0)); tracker.trackEmotionalState(createState(-1.0, 0.0, -1.0)); const insights = tracker.generateTrajectoryInsights(); const volatilityInsight = insights.find((i) => i.type === "volatility"); expect(volatilityInsight).toBeDefined(); expect(volatilityInsight?.description).toContain("volatile"); }); it("should generate stability insight", () => { tracker.trackEmotionalState(createState(0.5, 0.6, 0.3)); tracker.trackEmotionalState(createState(0.52, 0.58, 0.32)); tracker.trackEmotionalState(createState(0.48, 0.62, 0.28)); const insights = tracker.generateTrajectoryInsights(); const stabilityInsight = insights.find((i) => i.type === "stability"); expect(stabilityInsight).toBeDefined(); expect(stabilityInsight?.description).toContain("stable"); }); it("should generate recovery insight", () => { tracker.trackEmotionalState(createState(-0.5, 0.3, -0.2)); tracker.trackEmotionalState(createState(-0.2, 0.4, 0.0)); tracker.trackEmotionalState(createState(0.2, 0.6, 0.3)); tracker.trackEmotionalState(createState(0.5, 0.7, 0.5)); const insights = tracker.generateTrajectoryInsights(); const recoveryInsight = insights.find((i) => i.type === "recovery"); expect(recoveryInsight).toBeDefined(); expect(recoveryInsight?.description).toContain("recovering"); expect(recoveryInsight?.recommendation).toBeDefined(); }); it("should not generate recovery insight for inconsistent recovery", () => { tracker.trackEmotionalState(createState(-0.5, 0.3, -0.2)); tracker.trackEmotionalState(createState(0.0, 0.4, 0.0)); tracker.trackEmotionalState(createState(-0.3, 0.5, -0.1)); // Dip tracker.trackEmotionalState(createState(0.5, 0.7, 0.5)); const insights = tracker.generateTrajectoryInsights(); const recoveryInsight = insights.find((i) => i.type === "recovery"); expect(recoveryInsight).toBeUndefined(); }); }); describe("predictEmotionalTrend", () => { it("should throw error with empty history", () => { expect(() => { tracker.predictEmotionalTrend(); }).toThrow("Cannot predict trend with empty history"); }); it("should return same state with single state history", () => { const state = createState(0.5, 0.6, 0.3); tracker.trackEmotionalState(state); const prediction = tracker.predictEmotionalTrend(); expect(prediction.valence).toBe(state.valence); expect(prediction.arousal).toBe(state.arousal); expect(prediction.dominance).toBe(state.dominance); }); it("should predict upward trend", () => { tracker.trackEmotionalState(createState(-0.5, 0.3, -0.2)); tracker.trackEmotionalState(createState(0.0, 0.5, 0.1)); tracker.trackEmotionalState(createState(0.5, 0.7, 0.4)); const prediction = tracker.predictEmotionalTrend(); expect(prediction.valence).toBeGreaterThan(0.5); }); it("should predict downward trend", () => { tracker.trackEmotionalState(createState(0.5, 0.7, 0.4)); tracker.trackEmotionalState(createState(0.0, 0.5, 0.1)); tracker.trackEmotionalState(createState(-0.5, 0.3, -0.2)); const prediction = tracker.predictEmotionalTrend(); expect(prediction.valence).toBeLessThan(-0.5); }); it("should clamp predictions to valid ranges", () => { // Create extreme upward trend tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(0.5, 0.5, 0.5)); tracker.trackEmotionalState(createState(1.0, 1.0, 1.0)); const prediction = tracker.predictEmotionalTrend(); expect(prediction.valence).toBeLessThanOrEqual(1.0); expect(prediction.arousal).toBeLessThanOrEqual(1.0); expect(prediction.dominance).toBeLessThanOrEqual(1.0); }); it("should calculate confidence based on volatility", () => { // Stable trend tracker.trackEmotionalState(createState(0.0, 0.5, 0.0)); tracker.trackEmotionalState(createState(0.1, 0.5, 0.1)); tracker.trackEmotionalState(createState(0.2, 0.5, 0.2)); const prediction = tracker.predictEmotionalTrend(); expect(prediction.confidence).toBeGreaterThan(0.5); }); }); describe("inferEmotionType", () => { it("should infer joy for high valence and high arousal", () => { tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(0.8, 0.9, 0.5), "joy_trigger"); const triggers = tracker.identifyTriggers(); expect(triggers[0].emotionType).toBe("joy"); }); it("should infer anger for low valence and high arousal", () => { tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(-0.8, 0.9, 0.5), "anger_trigger"); const triggers = tracker.identifyTriggers(); expect(triggers[0].emotionType).toBe("anger"); }); it("should infer sadness for low valence and low arousal", () => { tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(-0.8, 0.3, -0.5), "sadness_trigger"); const triggers = tracker.identifyTriggers(); expect(triggers[0].emotionType).toBe("sadness"); }); it("should infer gratitude for high valence and low arousal", () => { tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(0.8, 0.3, 0.5), "gratitude_trigger"); const triggers = tracker.identifyTriggers(); expect(triggers[0].emotionType).toBe("gratitude"); }); it("should infer surprise for very high arousal", () => { tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(0.0, 0.9, 0.0), "surprise_trigger"); const triggers = tracker.identifyTriggers(); expect(triggers[0].emotionType).toBe("surprise"); }); it("should infer fear as default for neutral states", () => { tracker.trackEmotionalState(createState(0.0, 0.0, 0.0)); tracker.trackEmotionalState(createState(0.1, 0.4, 0.1), "fear_trigger"); const triggers = tracker.identifyTriggers(); expect(triggers[0].emotionType).toBe("fear"); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keyurgolani/ThoughtMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server