Add emoji-bot enhancements: agent router, math utils, project service, and tests

Co-authored-by: blackboxprogramming <118287761+blackboxprogramming@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-11-24 22:59:11 +00:00
parent 36c253c298
commit d777faedd4
7 changed files with 1316 additions and 0 deletions

233
bot/agent-math-utils.js Normal file
View File

@@ -0,0 +1,233 @@
// bot/agent-math-utils.js
// Utility functions for tracking agent activity and emoji heatmaps
/**
* Track agent trigger counts
*/
class AgentTracker {
constructor() {
this.triggers = {};
}
/**
* Record an agent trigger
* @param {string} agentName - The name of the agent triggered
*/
recordTrigger(agentName) {
if (!this.triggers[agentName]) {
this.triggers[agentName] = 0;
}
this.triggers[agentName]++;
}
/**
* Get trigger count for a specific agent
* @param {string} agentName - The name of the agent
* @returns {number} - The trigger count
*/
getTriggerCount(agentName) {
return this.triggers[agentName] || 0;
}
/**
* Get all trigger counts
* @returns {Object} - All agent trigger counts
*/
getAllTriggers() {
return { ...this.triggers };
}
/**
* Reset all trigger counts
*/
reset() {
this.triggers = {};
}
}
/**
* Track emoji usage and generate heatmaps
*/
class EmojiHeatmap {
constructor() {
this.counts = {};
this.total = 0;
}
/**
* Record an emoji occurrence
* @param {string} emoji - The emoji character
* @param {number} [count=1] - Number of occurrences to add
*/
record(emoji, count = 1) {
if (!this.counts[emoji]) {
this.counts[emoji] = 0;
}
this.counts[emoji] += count;
this.total += count;
}
/**
* Get count for a specific emoji
* @param {string} emoji - The emoji character
* @returns {number} - The count
*/
getCount(emoji) {
return this.counts[emoji] || 0;
}
/**
* Get percentage for a specific emoji
* @param {string} emoji - The emoji character
* @returns {number} - The percentage (0-100)
*/
getPercentage(emoji) {
if (this.total === 0) return 0;
return ((this.counts[emoji] || 0) / this.total) * 100;
}
/**
* Get all emoji counts
* @returns {Object} - All emoji counts
*/
getAllCounts() {
return { ...this.counts };
}
/**
* Get percentage breakdown for all emojis
* @returns {Object} - Emoji to percentage mapping
*/
getPercentageBreakdown() {
const breakdown = {};
for (const emoji of Object.keys(this.counts)) {
breakdown[emoji] = this.getPercentage(emoji);
}
return breakdown;
}
/**
* Get total count
* @returns {number} - Total emoji count
*/
getTotal() {
return this.total;
}
/**
* Reset heatmap
*/
reset() {
this.counts = {};
this.total = 0;
}
}
/**
* Generate a visual progress bar using emojis
* @param {Object} options - Progress bar options
* @param {number} options.completed - Number of completed items
* @param {number} options.inProgress - Number of in-progress items
* @param {number} options.total - Total number of items
* @param {number} [options.width=10] - Width of progress bar in characters
* @returns {string} - Visual progress bar
*/
function generateProgressBar({ completed, inProgress, total, width = 10 }) {
if (total === 0) return "⬜".repeat(width);
const completedRatio = completed / total;
const inProgressRatio = inProgress / total;
const completedSlots = Math.round(completedRatio * width);
const inProgressSlots = Math.round(inProgressRatio * width);
const remainingSlots = width - completedSlots - inProgressSlots;
return (
"✅".repeat(completedSlots) +
"🟡".repeat(inProgressSlots) +
"⬜".repeat(Math.max(0, remainingSlots))
);
}
/**
* Calculate sprint progress statistics
* @param {Array<Object>} items - Array of sprint items with status
* @returns {Object} - Sprint statistics
*/
function calculateSprintProgress(items) {
const stats = {
total: items.length,
done: 0,
inProgress: 0,
notStarted: 0,
blocked: 0,
needsReview: 0,
escalation: 0
};
for (const item of items) {
const status = item.status?.toLowerCase();
switch (status) {
case "done":
stats.done++;
break;
case "in progress":
stats.inProgress++;
break;
case "not started":
stats.notStarted++;
break;
case "blocked":
stats.blocked++;
break;
case "needs review":
stats.needsReview++;
break;
case "escalation":
stats.escalation++;
break;
default:
stats.notStarted++;
}
}
stats.completionPercentage =
stats.total > 0 ? (stats.done / stats.total) * 100 : 0;
stats.progressBar = generateProgressBar({
completed: stats.done,
inProgress: stats.inProgress,
total: stats.total
});
return stats;
}
/**
* Format emoji statistics as a human-readable report
* @param {EmojiHeatmap} heatmap - The emoji heatmap instance
* @returns {string} - Formatted report
*/
function formatHeatmapReport(heatmap) {
const breakdown = heatmap.getPercentageBreakdown();
const lines = ["📊 Emoji Heatmap Report", ""];
const entries = Object.entries(breakdown).sort((a, b) => b[1] - a[1]);
for (const [emoji, percentage] of entries) {
const count = heatmap.getCount(emoji);
lines.push(`${emoji} ${percentage.toFixed(1)}% (${count} occurrences)`);
}
lines.push("");
lines.push(`Total: ${heatmap.getTotal()} emojis tracked`);
return lines.join("\n");
}
module.exports = {
AgentTracker,
EmojiHeatmap,
generateProgressBar,
calculateSprintProgress,
formatHeatmapReport
};

122
bot/emoji-agent-router.js Normal file
View File

@@ -0,0 +1,122 @@
// bot/emoji-agent-router.js
// Routes emoji reactions to appropriate agent handlers
const AGENT_ROUTES = {
// Status change reactions
"✅": { agent: "status-agent", action: "mark_done" },
"🟡": { agent: "status-agent", action: "mark_in_progress" },
"⬜": { agent: "status-agent", action: "mark_not_started" },
"❌": { agent: "status-agent", action: "mark_blocked" },
"🔁": { agent: "status-agent", action: "mark_rework" },
// Special agent triggers
"🤔": { agent: "review-agent", action: "request_review" },
"🛟": { agent: "guardian-agent", action: "escalate" },
"🤖": { agent: "auto-assign-agent", action: "auto_assign" },
"🧍‍♀️": { agent: "assignment-agent", action: "assign_human" },
"👥": { agent: "team-agent", action: "tag_team" }
};
// Reaction name to emoji mapping for GitHub reactions
const REACTION_TO_EMOJI = {
"+1": "👍",
"-1": "👎",
laugh: "😄",
hooray: "🎉",
confused: "😕",
heart: "❤️",
rocket: "🚀",
eyes: "👀"
};
// Reaction name to agent routing
const REACTION_ROUTES = {
hooray: { agent: "status-agent", action: "mark_done" },
rocket: { agent: "status-agent", action: "mark_done" },
eyes: { agent: "status-agent", action: "mark_in_progress" },
"+1": { agent: "status-agent", action: "approve" },
"-1": { agent: "status-agent", action: "mark_blocked" },
confused: { agent: "status-agent", action: "mark_blocked" }
};
/**
* Route an emoji to the appropriate agent
* @param {string} emoji - The emoji character
* @returns {Object|null} - Agent routing info or null if not found
*/
function routeEmoji(emoji) {
return AGENT_ROUTES[emoji] || null;
}
/**
* Route a reaction name to the appropriate agent
* @param {string} reaction - The reaction name (e.g., "rocket")
* @returns {Object|null} - Agent routing info or null if not found
*/
function routeReaction(reaction) {
return REACTION_ROUTES[reaction] || null;
}
/**
* Get all registered emoji routes
* @returns {Object} - All emoji routes
*/
function getEmojiRoutes() {
return { ...AGENT_ROUTES };
}
/**
* Get all registered reaction routes
* @returns {Object} - All reaction routes
*/
function getReactionRoutes() {
return { ...REACTION_ROUTES };
}
/**
* Convert a reaction name to its emoji representation
* @param {string} reaction - The reaction name
* @returns {string|null} - The emoji or null if not found
*/
function reactionToEmoji(reaction) {
return REACTION_TO_EMOJI[reaction] || null;
}
/**
* Process an incoming reaction and return handling instructions
* @param {Object} options - Processing options
* @param {string} options.reaction - The reaction name
* @param {Object} options.payload - The event payload
* @returns {Object} - Processing result with routing info
*/
function processReaction({ reaction, payload }) {
const route = routeReaction(reaction);
if (!route) {
return {
handled: false,
reason: `No route for reaction: ${reaction}`
};
}
return {
handled: true,
agent: route.agent,
action: route.action,
reaction,
emoji: reactionToEmoji(reaction),
issueNumber: payload?.issue?.number || payload?.pull_request?.number,
repository: payload?.repository?.full_name
};
}
module.exports = {
routeEmoji,
routeReaction,
getEmojiRoutes,
getReactionRoutes,
reactionToEmoji,
processReaction,
AGENT_ROUTES,
REACTION_ROUTES
};

View File

@@ -0,0 +1,85 @@
// bot/handlers/project-status-sync.js
// Handler for syncing project statuses based on emoji reactions
const STATUS_MAP = {
"✅": "Done",
"🟡": "In Progress",
"⬜": "Not Started",
"❌": "Blocked",
"🔁": "Rework",
"🤔": "Needs Review",
"🛟": "Escalation"
};
/**
* Map emoji to project status
* @param {string} emoji - The emoji character
* @returns {string|null} - The corresponding status or null if not found
*/
function mapEmojiToStatus(emoji) {
return STATUS_MAP[emoji] || null;
}
/**
* Map reaction name to project status
* @param {string} reaction - The reaction name (e.g., "rocket", "eyes")
* @returns {string|null} - The corresponding status or null if not found
*/
function mapReactionToStatus(reaction) {
const REACTION_MAP = {
hooray: "Done",
rocket: "Done",
eyes: "In Progress",
"+1": "Done",
"-1": "Blocked",
confused: "Blocked",
thinking_face: "Needs Review",
rotating_light: "Escalation"
};
return REACTION_MAP[reaction] || null;
}
/**
* Get all status mappings
* @returns {Object} - All emoji to status mappings
*/
function getStatusMappings() {
return { ...STATUS_MAP };
}
/**
* Sync project status based on reaction
* @param {Object} options - Sync options
* @param {string} options.reaction - The reaction name
* @param {string|number} options.issueId - The issue or PR ID
* @param {string|number} options.projectId - The project ID
* @returns {Object} - Sync result
*/
async function syncProjectStatus({ reaction, issueId, projectId }) {
const status = mapReactionToStatus(reaction);
if (!status) {
return {
success: false,
message: `No status mapping for reaction: ${reaction}`
};
}
// This would be implemented with actual GitHub GraphQL API calls
// For now, return the intended status change
return {
success: true,
issueId,
projectId,
newStatus: status,
reaction,
message: `Status would be updated to: ${status}`
};
}
module.exports = {
mapEmojiToStatus,
mapReactionToStatus,
getStatusMappings,
syncProjectStatus
};

View File

@@ -0,0 +1,248 @@
// bot/project-status-service.js
// Service for interacting with GitHub Projects via GraphQL API
/**
* GitHub GraphQL API client for Project status management
*/
class ProjectStatusService {
/**
* Create a new ProjectStatusService
* @param {Object} options - Service options
* @param {string} options.token - GitHub API token
* @param {string} [options.apiUrl] - GitHub GraphQL API URL
*/
constructor({ token, apiUrl = "https://api.github.com/graphql" }) {
this.token = token;
this.apiUrl = apiUrl;
}
/**
* Execute a GraphQL query
* @param {string} query - The GraphQL query
* @param {Object} [variables] - Query variables
* @returns {Promise<Object>} - Query result
*/
async graphql(query, variables = {}) {
const response = await fetch(this.apiUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${this.token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ query, variables })
});
if (!response.ok) {
throw new Error(`GraphQL request failed: ${response.status}`);
}
const result = await response.json();
if (result.errors) {
throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);
}
return result.data;
}
/**
* Get project by ID
* @param {string} projectId - The project node ID
* @returns {Promise<Object>} - Project data
*/
async getProject(projectId) {
const query = `
query GetProject($projectId: ID!) {
node(id: $projectId) {
... on ProjectV2 {
id
title
number
fields(first: 20) {
nodes {
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
}
}
}
`;
return this.graphql(query, { projectId });
}
/**
* Get the Status field and its options from a project
* @param {string} projectId - The project node ID
* @returns {Promise<Object|null>} - Status field data or null
*/
async getStatusField(projectId) {
const data = await this.getProject(projectId);
const project = data?.node;
if (!project?.fields?.nodes) {
return null;
}
const statusField = project.fields.nodes.find(
(field) => field.name === "Status"
);
return statusField || null;
}
/**
* Get project item by issue/PR node ID
* @param {string} projectId - The project node ID
* @param {string} contentId - The issue or PR node ID
* @returns {Promise<Object|null>} - Project item data or null
*/
async getProjectItem(projectId, contentId) {
const query = `
query GetProjectItem($projectId: ID!) {
node(id: $projectId) {
... on ProjectV2 {
items(first: 100) {
nodes {
id
content {
... on Issue {
id
number
title
}
... on PullRequest {
id
number
title
}
}
fieldValues(first: 10) {
nodes {
... on ProjectV2ItemFieldSingleSelectValue {
name
field {
... on ProjectV2SingleSelectField {
name
}
}
}
}
}
}
}
}
}
}
`;
const data = await this.graphql(query, { projectId });
const items = data?.node?.items?.nodes || [];
return items.find((item) => item.content?.id === contentId) || null;
}
/**
* Update project item status
* @param {Object} options - Update options
* @param {string} options.projectId - The project node ID
* @param {string} options.itemId - The project item ID
* @param {string} options.fieldId - The status field ID
* @param {string} options.optionId - The status option ID
* @returns {Promise<Object>} - Update result
*/
async updateItemStatus({ projectId, itemId, fieldId, optionId }) {
const mutation = `
mutation UpdateProjectItemField($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: ProjectV2FieldValue!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: $value
}) {
projectV2Item {
id
}
}
}
`;
return this.graphql(mutation, {
projectId,
itemId,
fieldId,
value: { singleSelectOptionId: optionId }
});
}
/**
* Set status for an issue/PR by finding and updating its project card
* @param {Object} options - Options
* @param {string} options.projectId - The project node ID
* @param {string} options.contentId - The issue or PR node ID
* @param {string} options.statusName - The status name to set
* @returns {Promise<Object>} - Result
*/
async setStatus({ projectId, contentId, statusName }) {
// Get status field and options
const statusField = await this.getStatusField(projectId);
if (!statusField) {
return { success: false, error: "Status field not found" };
}
// Find the option ID for the requested status
const option = statusField.options.find(
(opt) => opt.name.toLowerCase() === statusName.toLowerCase()
);
if (!option) {
return {
success: false,
error: `Status option "${statusName}" not found`
};
}
// Get the project item for this content
const item = await this.getProjectItem(projectId, contentId);
if (!item) {
return { success: false, error: "Project item not found" };
}
// Update the status
await this.updateItemStatus({
projectId,
itemId: item.id,
fieldId: statusField.id,
optionId: option.id
});
return {
success: true,
itemId: item.id,
newStatus: statusName
};
}
}
/**
* Create a ProjectStatusService from environment variables
* @returns {ProjectStatusService|null} - Service instance or null if token not available
*/
function createFromEnv() {
const token = process.env.GITHUB_TOKEN;
if (!token) {
return null;
}
return new ProjectStatusService({ token });
}
module.exports = {
ProjectStatusService,
createFromEnv
};

144
docs/emoji-bot-spec.txt Normal file
View File

@@ -0,0 +1,144 @@
================================================================================
EMOJI-BOT SPECIFICATION
BlackRoad OS Intelligence Suite
================================================================================
OVERVIEW
--------
Emoji-Bot is an intelligent automation bot that uses emoji reactions and
patterns to orchestrate GitHub project management workflows. It bridges
human-readable emoji cues with automated agent actions and project status
updates.
ARCHITECTURE
------------
The bot consists of the following core components:
1. index.js
- Entry point for the GitHub Actions workflow
- Parses event payloads and extracts reaction data
- Logs processing activity
2. emoji-agent-router.js
- Routes emoji/reactions to appropriate agent handlers
- Maintains mapping tables for emoji → agent routing
- Supports both Unicode emoji and GitHub reaction names
3. handlers/project-status-sync.js
- Maps reactions to project status values
- Synchronizes status changes with GitHub Projects
- Provides status mapping utilities
4. project-status-service.js
- GitHub GraphQL API client for Projects V2
- Reads project structure (fields, options)
- Updates project item statuses
5. agent-math-utils.js
- Tracks agent trigger statistics
- Generates emoji heatmaps
- Calculates sprint progress metrics
- Renders visual progress bars
EMOJI MAPPINGS
--------------
Status Emojis:
✅ → Done
🟡 → In Progress
⬜ → Not Started
❌ → Blocked
🔁 → Rework
🤔 → Needs Review
🛟 → Escalation
Agent Triggers:
🤖 → Auto-assign to agent
🧍‍♀️ → Assign to human
👥 → Tag team
GitHub Reaction Mappings:
:rocket: / :hooray: → Done
:eyes: → In Progress
:-1: / :confused: → Blocked
:thinking_face: → Needs Review
:rotating_light: → Escalation
WORKFLOW
--------
1. User reacts to issue/PR comment with an emoji
2. GitHub triggers the emoji-bot workflow
3. Bot parses the reaction from the event payload
4. emoji-agent-router determines the appropriate handler
5. project-status-sync maps reaction to status
6. project-status-service updates the GitHub Project card
7. Bot posts a comment using the appropriate template
COMMENT TEMPLATES
-----------------
Location: bot/comments/
- in-progress.md : Posted when work begins
- completed.md : Posted when task is done
- blocked.md : Posted when progress is halted
- review-requested.md : Posted when review is needed
- escalation.md : Posted for critical issues
CONFIGURATION FILES
-------------------
- emoji-bot-config.yml : Emoji mappings and agent triggers
- project-config.yml : GitHub Project settings and automation rules
ANALYTICS
---------
The agent-math-utils module provides:
1. Agent Tracker
- Records how many times each agent is triggered
- Supports per-agent and aggregate reporting
2. Emoji Heatmap
- Tracks emoji usage frequency
- Calculates percentage distributions
- Example output:
✅ 45.0% (45 occurrences)
❌ 30.0% (30 occurrences)
🟡 25.0% (25 occurrences)
3. Sprint Progress Bar
- Visual representation of sprint status
- Format: ✅✅✅🟡🟡⬜⬜⬜⬜⬜
- Configurable width
TESTING
-------
Tests are located in tests/reaction.test.ts and cover:
- Reaction to status mapping
- Emoji routing
- Agent tracking
- Heatmap calculations
- Progress bar generation
GITHUB ACTIONS WORKFLOW
-----------------------
Location: .github/workflows/emoji-bot.yml
Triggers on:
- issue_comment created events
Steps:
1. Checkout repository
2. Set up Node.js 18
3. Run bot/index.js
FUTURE ENHANCEMENTS
-------------------
- Real-time Slack notifications
- Cross-org project synchronization
- LUCIDIA AI integration for intelligent routing
- Custom reaction vocabulary per repository
- Sprint velocity predictions
================================================================================
© BlackRoad OS - All Rights Reserved
================================================================================

111
project-config.yml Normal file
View File

@@ -0,0 +1,111 @@
# project-config.yml
# Configuration for GitHub Projects integration
# Default project settings
default_project:
# Organization and project number
organization: "blackroad-os"
project_number: 1
# Node ID for GraphQL API (set after project creation)
# project_id: "PVT_kwDOxxxxxx"
# Status field configuration
status_field:
name: "Status"
options:
- id: "not_started"
name: "Not Started"
emoji: "⬜"
- id: "in_progress"
name: "In Progress"
emoji: "🟡"
- id: "done"
name: "Done"
emoji: "✅"
- id: "blocked"
name: "Blocked"
emoji: "❌"
- id: "needs_review"
name: "Needs Review"
emoji: "🤔"
- id: "rework"
name: "Rework"
emoji: "🔁"
- id: "escalation"
name: "Escalation"
emoji: "🛟"
# Custom fields for sprint tracking
custom_fields:
- name: "Sprint"
type: "iteration"
- name: "Priority"
type: "single_select"
options:
- "🔴 Critical"
- "🟠 High"
- "🟡 Medium"
- "🟢 Low"
- name: "Assigned Agent"
type: "single_select"
options:
- "🤖 builder-agent"
- "📝 scribe-agent"
- "👁️ guardian-agent"
- "📋 planner-agent"
- "🧍‍♀️ Human"
# Automation rules
automation:
# Auto-assign agent based on labels
auto_assign:
enabled: true
rules:
- label: "documentation"
agent: "scribe-agent"
- label: "bug"
agent: "builder-agent"
- label: "security"
agent: "guardian-agent"
- label: "planning"
agent: "planner-agent"
# Auto-status transitions
status_transitions:
enabled: true
rules:
- trigger: "pr_opened"
from: "Not Started"
to: "In Progress"
- trigger: "review_requested"
from: "In Progress"
to: "Needs Review"
- trigger: "pr_merged"
from: "*"
to: "Done"
- trigger: "issue_closed"
from: "*"
to: "Done"
# Notification settings
notifications:
# Slack integration (if configured)
slack:
enabled: false
channel: "#blackroad-status"
# Comment notifications
comments:
on_status_change: true
on_escalation: true
on_assignment: true
# Sprint tracking configuration
sprint:
# Sprint duration in weeks
duration_weeks: 2
# Goal calculation
velocity_tracking: true
# Progress bar width
progress_bar_width: 10

373
tests/reaction.test.ts Normal file
View File

@@ -0,0 +1,373 @@
import { describe, it, expect, beforeEach } from "vitest";
// Import bot modules - using require since they are CommonJS
const projectStatusSync = require("../bot/handlers/project-status-sync.js");
const emojiAgentRouter = require("../bot/emoji-agent-router.js");
const agentMathUtils = require("../bot/agent-math-utils.js");
describe("project-status-sync", () => {
describe("mapEmojiToStatus", () => {
it("maps ✅ to Done", () => {
expect(projectStatusSync.mapEmojiToStatus("✅")).toBe("Done");
});
it("maps 🟡 to In Progress", () => {
expect(projectStatusSync.mapEmojiToStatus("🟡")).toBe("In Progress");
});
it("maps ⬜ to Not Started", () => {
expect(projectStatusSync.mapEmojiToStatus("⬜")).toBe("Not Started");
});
it("maps ❌ to Blocked", () => {
expect(projectStatusSync.mapEmojiToStatus("❌")).toBe("Blocked");
});
it("maps 🔁 to Rework", () => {
expect(projectStatusSync.mapEmojiToStatus("🔁")).toBe("Rework");
});
it("maps 🤔 to Needs Review", () => {
expect(projectStatusSync.mapEmojiToStatus("🤔")).toBe("Needs Review");
});
it("maps 🛟 to Escalation", () => {
expect(projectStatusSync.mapEmojiToStatus("🛟")).toBe("Escalation");
});
it("returns null for unknown emoji", () => {
expect(projectStatusSync.mapEmojiToStatus("🎉")).toBeNull();
});
});
describe("mapReactionToStatus", () => {
it("maps hooray to Done", () => {
expect(projectStatusSync.mapReactionToStatus("hooray")).toBe("Done");
});
it("maps rocket to Done", () => {
expect(projectStatusSync.mapReactionToStatus("rocket")).toBe("Done");
});
it("maps eyes to In Progress", () => {
expect(projectStatusSync.mapReactionToStatus("eyes")).toBe("In Progress");
});
it("maps -1 to Blocked", () => {
expect(projectStatusSync.mapReactionToStatus("-1")).toBe("Blocked");
});
it("maps confused to Blocked", () => {
expect(projectStatusSync.mapReactionToStatus("confused")).toBe("Blocked");
});
it("maps thinking_face to Needs Review", () => {
expect(projectStatusSync.mapReactionToStatus("thinking_face")).toBe(
"Needs Review"
);
});
it("maps rotating_light to Escalation", () => {
expect(projectStatusSync.mapReactionToStatus("rotating_light")).toBe(
"Escalation"
);
});
it("returns null for unknown reaction", () => {
expect(projectStatusSync.mapReactionToStatus("heart")).toBeNull();
});
});
describe("syncProjectStatus", () => {
it("returns success for valid reaction", async () => {
const result = await projectStatusSync.syncProjectStatus({
reaction: "rocket",
issueId: 123,
projectId: 456
});
expect(result.success).toBe(true);
expect(result.newStatus).toBe("Done");
expect(result.issueId).toBe(123);
expect(result.projectId).toBe(456);
});
it("returns failure for unknown reaction", async () => {
const result = await projectStatusSync.syncProjectStatus({
reaction: "unknown",
issueId: 123,
projectId: 456
});
expect(result.success).toBe(false);
expect(result.message).toContain("No status mapping");
});
});
});
describe("emoji-agent-router", () => {
describe("routeEmoji", () => {
it("routes ✅ to status-agent with mark_done action", () => {
const route = emojiAgentRouter.routeEmoji("✅");
expect(route).toEqual({ agent: "status-agent", action: "mark_done" });
});
it("routes 🛟 to guardian-agent with escalate action", () => {
const route = emojiAgentRouter.routeEmoji("🛟");
expect(route).toEqual({ agent: "guardian-agent", action: "escalate" });
});
it("routes 🤖 to auto-assign-agent", () => {
const route = emojiAgentRouter.routeEmoji("🤖");
expect(route).toEqual({ agent: "auto-assign-agent", action: "auto_assign" });
});
it("returns null for unrouted emoji", () => {
expect(emojiAgentRouter.routeEmoji("🎉")).toBeNull();
});
});
describe("routeReaction", () => {
it("routes rocket to status-agent with mark_done action", () => {
const route = emojiAgentRouter.routeReaction("rocket");
expect(route).toEqual({ agent: "status-agent", action: "mark_done" });
});
it("routes eyes to status-agent with mark_in_progress action", () => {
const route = emojiAgentRouter.routeReaction("eyes");
expect(route).toEqual({
agent: "status-agent",
action: "mark_in_progress"
});
});
it("routes confused to status-agent with mark_blocked action", () => {
const route = emojiAgentRouter.routeReaction("confused");
expect(route).toEqual({ agent: "status-agent", action: "mark_blocked" });
});
it("returns null for unrouted reaction", () => {
expect(emojiAgentRouter.routeReaction("heart")).toBeNull();
});
});
describe("reactionToEmoji", () => {
it("converts +1 to 👍", () => {
expect(emojiAgentRouter.reactionToEmoji("+1")).toBe("👍");
});
it("converts rocket to 🚀", () => {
expect(emojiAgentRouter.reactionToEmoji("rocket")).toBe("🚀");
});
it("returns null for unknown reaction", () => {
expect(emojiAgentRouter.reactionToEmoji("unknown")).toBeNull();
});
});
describe("processReaction", () => {
it("processes valid reaction with payload", () => {
const result = emojiAgentRouter.processReaction({
reaction: "rocket",
payload: {
issue: { number: 42 },
repository: { full_name: "org/repo" }
}
});
expect(result.handled).toBe(true);
expect(result.agent).toBe("status-agent");
expect(result.action).toBe("mark_done");
expect(result.issueNumber).toBe(42);
expect(result.repository).toBe("org/repo");
});
it("returns unhandled for unknown reaction", () => {
const result = emojiAgentRouter.processReaction({
reaction: "unknown",
payload: {}
});
expect(result.handled).toBe(false);
expect(result.reason).toContain("No route");
});
});
});
describe("agent-math-utils", () => {
describe("AgentTracker", () => {
let tracker: InstanceType<typeof agentMathUtils.AgentTracker>;
beforeEach(() => {
tracker = new agentMathUtils.AgentTracker();
});
it("starts with zero triggers", () => {
expect(tracker.getTriggerCount("test-agent")).toBe(0);
});
it("records and retrieves triggers", () => {
tracker.recordTrigger("test-agent");
tracker.recordTrigger("test-agent");
tracker.recordTrigger("other-agent");
expect(tracker.getTriggerCount("test-agent")).toBe(2);
expect(tracker.getTriggerCount("other-agent")).toBe(1);
});
it("returns all triggers", () => {
tracker.recordTrigger("agent-a");
tracker.recordTrigger("agent-b");
const all = tracker.getAllTriggers();
expect(all).toEqual({ "agent-a": 1, "agent-b": 1 });
});
it("resets triggers", () => {
tracker.recordTrigger("test-agent");
tracker.reset();
expect(tracker.getTriggerCount("test-agent")).toBe(0);
});
});
describe("EmojiHeatmap", () => {
let heatmap: InstanceType<typeof agentMathUtils.EmojiHeatmap>;
beforeEach(() => {
heatmap = new agentMathUtils.EmojiHeatmap();
});
it("starts with zero total", () => {
expect(heatmap.getTotal()).toBe(0);
});
it("records emoji occurrences", () => {
heatmap.record("✅");
heatmap.record("✅", 2);
heatmap.record("❌");
expect(heatmap.getCount("✅")).toBe(3);
expect(heatmap.getCount("❌")).toBe(1);
expect(heatmap.getTotal()).toBe(4);
});
it("calculates percentages", () => {
heatmap.record("✅", 75);
heatmap.record("❌", 25);
expect(heatmap.getPercentage("✅")).toBe(75);
expect(heatmap.getPercentage("❌")).toBe(25);
});
it("returns 0 percentage when empty", () => {
expect(heatmap.getPercentage("✅")).toBe(0);
});
it("returns percentage breakdown", () => {
heatmap.record("✅", 50);
heatmap.record("❌", 50);
const breakdown = heatmap.getPercentageBreakdown();
expect(breakdown["✅"]).toBe(50);
expect(breakdown["❌"]).toBe(50);
});
it("resets heatmap", () => {
heatmap.record("✅", 10);
heatmap.reset();
expect(heatmap.getTotal()).toBe(0);
expect(heatmap.getCount("✅")).toBe(0);
});
});
describe("generateProgressBar", () => {
it("generates empty bar for zero total", () => {
const bar = agentMathUtils.generateProgressBar({
completed: 0,
inProgress: 0,
total: 0
});
expect(bar).toBe("⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜");
});
it("generates full completed bar", () => {
const bar = agentMathUtils.generateProgressBar({
completed: 10,
inProgress: 0,
total: 10
});
expect(bar).toBe("✅✅✅✅✅✅✅✅✅✅");
});
it("generates mixed progress bar", () => {
const bar = agentMathUtils.generateProgressBar({
completed: 5,
inProgress: 2,
total: 10
});
expect(bar).toBe("✅✅✅✅✅🟡🟡⬜⬜⬜");
});
it("respects custom width", () => {
const bar = agentMathUtils.generateProgressBar({
completed: 4,
inProgress: 0,
total: 8,
width: 8
});
expect(bar).toBe("✅✅✅✅⬜⬜⬜⬜");
});
});
describe("calculateSprintProgress", () => {
it("calculates progress for sprint items", () => {
const items = [
{ status: "Done" },
{ status: "Done" },
{ status: "In Progress" },
{ status: "Blocked" },
{ status: "Not Started" }
];
const stats = agentMathUtils.calculateSprintProgress(items);
expect(stats.total).toBe(5);
expect(stats.done).toBe(2);
expect(stats.inProgress).toBe(1);
expect(stats.blocked).toBe(1);
expect(stats.notStarted).toBe(1);
expect(stats.completionPercentage).toBe(40);
});
it("returns zero completion for empty sprint", () => {
const stats = agentMathUtils.calculateSprintProgress([]);
expect(stats.total).toBe(0);
expect(stats.completionPercentage).toBe(0);
});
it("includes progress bar", () => {
const items = [{ status: "Done" }, { status: "Done" }];
const stats = agentMathUtils.calculateSprintProgress(items);
expect(stats.progressBar).toContain("✅");
});
});
describe("formatHeatmapReport", () => {
it("formats heatmap as report", () => {
const heatmap = new agentMathUtils.EmojiHeatmap();
heatmap.record("✅", 45);
heatmap.record("❌", 30);
heatmap.record("🟡", 25);
const report = agentMathUtils.formatHeatmapReport(heatmap);
expect(report).toContain("📊 Emoji Heatmap Report");
expect(report).toContain("✅ 45.0%");
expect(report).toContain("Total: 100 emojis tracked");
});
});
});