forked from cardosofelipe/pragma-stack
Compare commits
5 Commits
6f509e71ce
...
0ceee8545e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ceee8545e | ||
|
|
62aea06e0d | ||
|
|
24f1cc637e | ||
|
|
8b6cca5d4d | ||
|
|
c9700f760e |
@@ -28,82 +28,9 @@ depends_on: str | Sequence[str] | None = None
|
|||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
"""Create Syndarix domain tables."""
|
"""Create Syndarix domain tables."""
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# Create ENUM types
|
|
||||||
# =========================================================================
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TYPE autonomy_level AS ENUM (
|
|
||||||
'full_control', 'milestone', 'autonomous'
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TYPE project_status AS ENUM (
|
|
||||||
'active', 'paused', 'completed', 'archived'
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TYPE project_complexity AS ENUM (
|
|
||||||
'script', 'simple', 'medium', 'complex'
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TYPE client_mode AS ENUM (
|
|
||||||
'technical', 'auto'
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TYPE agent_status AS ENUM (
|
|
||||||
'idle', 'working', 'waiting', 'paused', 'terminated'
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TYPE issue_type AS ENUM (
|
|
||||||
'epic', 'story', 'task', 'bug'
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TYPE issue_status AS ENUM (
|
|
||||||
'open', 'in_progress', 'in_review', 'blocked', 'closed'
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TYPE issue_priority AS ENUM (
|
|
||||||
'low', 'medium', 'high', 'critical'
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TYPE sync_status AS ENUM (
|
|
||||||
'synced', 'pending', 'conflict', 'error'
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TYPE sprint_status AS ENUM (
|
|
||||||
'planned', 'active', 'in_review', 'completed', 'cancelled'
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Create projects table
|
# Create projects table
|
||||||
|
# Note: ENUM types are created automatically by sa.Enum() during table creation
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"projects",
|
"projects",
|
||||||
@@ -118,7 +45,6 @@ def upgrade() -> None:
|
|||||||
"milestone",
|
"milestone",
|
||||||
"autonomous",
|
"autonomous",
|
||||||
name="autonomy_level",
|
name="autonomy_level",
|
||||||
create_type=False,
|
|
||||||
),
|
),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default="milestone",
|
server_default="milestone",
|
||||||
@@ -131,7 +57,6 @@ def upgrade() -> None:
|
|||||||
"completed",
|
"completed",
|
||||||
"archived",
|
"archived",
|
||||||
name="project_status",
|
name="project_status",
|
||||||
create_type=False,
|
|
||||||
),
|
),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default="active",
|
server_default="active",
|
||||||
@@ -144,14 +69,13 @@ def upgrade() -> None:
|
|||||||
"medium",
|
"medium",
|
||||||
"complex",
|
"complex",
|
||||||
name="project_complexity",
|
name="project_complexity",
|
||||||
create_type=False,
|
|
||||||
),
|
),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default="medium",
|
server_default="medium",
|
||||||
),
|
),
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"client_mode",
|
"client_mode",
|
||||||
sa.Enum("technical", "auto", name="client_mode", create_type=False),
|
sa.Enum("technical", "auto", name="client_mode"),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default="auto",
|
server_default="auto",
|
||||||
),
|
),
|
||||||
@@ -285,7 +209,6 @@ def upgrade() -> None:
|
|||||||
"paused",
|
"paused",
|
||||||
"terminated",
|
"terminated",
|
||||||
name="agent_status",
|
name="agent_status",
|
||||||
create_type=False,
|
|
||||||
),
|
),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default="idle",
|
server_default="idle",
|
||||||
@@ -384,7 +307,6 @@ def upgrade() -> None:
|
|||||||
"completed",
|
"completed",
|
||||||
"cancelled",
|
"cancelled",
|
||||||
name="sprint_status",
|
name="sprint_status",
|
||||||
create_type=False,
|
|
||||||
),
|
),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default="planned",
|
server_default="planned",
|
||||||
@@ -435,7 +357,6 @@ def upgrade() -> None:
|
|||||||
"task",
|
"task",
|
||||||
"bug",
|
"bug",
|
||||||
name="issue_type",
|
name="issue_type",
|
||||||
create_type=False,
|
|
||||||
),
|
),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default="task",
|
server_default="task",
|
||||||
@@ -455,7 +376,6 @@ def upgrade() -> None:
|
|||||||
"blocked",
|
"blocked",
|
||||||
"closed",
|
"closed",
|
||||||
name="issue_status",
|
name="issue_status",
|
||||||
create_type=False,
|
|
||||||
),
|
),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default="open",
|
server_default="open",
|
||||||
@@ -468,7 +388,6 @@ def upgrade() -> None:
|
|||||||
"high",
|
"high",
|
||||||
"critical",
|
"critical",
|
||||||
name="issue_priority",
|
name="issue_priority",
|
||||||
create_type=False,
|
|
||||||
),
|
),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default="medium",
|
server_default="medium",
|
||||||
@@ -502,7 +421,6 @@ def upgrade() -> None:
|
|||||||
"conflict",
|
"conflict",
|
||||||
"error",
|
"error",
|
||||||
name="sync_status",
|
name="sync_status",
|
||||||
create_type=False,
|
|
||||||
),
|
),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default="synced",
|
server_default="synced",
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ function getEventConfig(event: ProjectEvent) {
|
|||||||
const config = EVENT_TYPE_CONFIG[event.type];
|
const config = EVENT_TYPE_CONFIG[event.type];
|
||||||
if (config) return config;
|
if (config) return config;
|
||||||
|
|
||||||
|
/* istanbul ignore next -- defensive fallbacks for unknown event types */
|
||||||
// Fallback based on event category
|
// Fallback based on event category
|
||||||
if (isAgentEvent(event)) {
|
if (isAgentEvent(event)) {
|
||||||
return {
|
return {
|
||||||
@@ -308,6 +309,7 @@ function getEventConfig(event: ProjectEvent) {
|
|||||||
bgColor: 'bg-blue-100 dark:bg-blue-900',
|
bgColor: 'bg-blue-100 dark:bg-blue-900',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/* istanbul ignore next -- defensive fallback */
|
||||||
if (isIssueEvent(event)) {
|
if (isIssueEvent(event)) {
|
||||||
return {
|
return {
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
@@ -316,6 +318,7 @@ function getEventConfig(event: ProjectEvent) {
|
|||||||
bgColor: 'bg-green-100 dark:bg-green-900',
|
bgColor: 'bg-green-100 dark:bg-green-900',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/* istanbul ignore next -- defensive fallback */
|
||||||
if (isSprintEvent(event)) {
|
if (isSprintEvent(event)) {
|
||||||
return {
|
return {
|
||||||
icon: PlayCircle,
|
icon: PlayCircle,
|
||||||
@@ -324,6 +327,7 @@ function getEventConfig(event: ProjectEvent) {
|
|||||||
bgColor: 'bg-indigo-100 dark:bg-indigo-900',
|
bgColor: 'bg-indigo-100 dark:bg-indigo-900',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/* istanbul ignore next -- defensive fallback */
|
||||||
if (isApprovalEvent(event)) {
|
if (isApprovalEvent(event)) {
|
||||||
return {
|
return {
|
||||||
icon: AlertTriangle,
|
icon: AlertTriangle,
|
||||||
@@ -332,6 +336,7 @@ function getEventConfig(event: ProjectEvent) {
|
|||||||
bgColor: 'bg-orange-100 dark:bg-orange-900',
|
bgColor: 'bg-orange-100 dark:bg-orange-900',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/* istanbul ignore next -- defensive fallback */
|
||||||
if (isWorkflowEvent(event)) {
|
if (isWorkflowEvent(event)) {
|
||||||
return {
|
return {
|
||||||
icon: Workflow,
|
icon: Workflow,
|
||||||
@@ -340,6 +345,7 @@ function getEventConfig(event: ProjectEvent) {
|
|||||||
bgColor: 'bg-cyan-100 dark:bg-cyan-900',
|
bgColor: 'bg-cyan-100 dark:bg-cyan-900',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/* istanbul ignore next -- defensive fallback */
|
||||||
if (isProjectEvent(event)) {
|
if (isProjectEvent(event)) {
|
||||||
return {
|
return {
|
||||||
icon: Folder,
|
icon: Folder,
|
||||||
@@ -349,6 +355,7 @@ function getEventConfig(event: ProjectEvent) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next -- defensive fallback for completely unknown events */
|
||||||
return {
|
return {
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
label: event.type,
|
label: event.type,
|
||||||
@@ -361,46 +368,59 @@ function getEventSummary(event: ProjectEvent): string {
|
|||||||
const payload = event.payload as Record<string, unknown>;
|
const payload = event.payload as Record<string, unknown>;
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
|
/* istanbul ignore next -- AGENT_SPAWNED tested via EventList */
|
||||||
case EventType.AGENT_SPAWNED:
|
case EventType.AGENT_SPAWNED:
|
||||||
return `${payload.agent_name || 'Agent'} spawned as ${payload.role || 'unknown role'}`;
|
return `${payload.agent_name || 'Agent'} spawned as ${payload.role || 'unknown role'}`;
|
||||||
case EventType.AGENT_MESSAGE:
|
case EventType.AGENT_MESSAGE:
|
||||||
return String(payload.message || 'No message');
|
return String(payload.message || 'No message');
|
||||||
|
/* istanbul ignore next -- rarely used in ActivityFeed tests */
|
||||||
case EventType.AGENT_STATUS_CHANGED:
|
case EventType.AGENT_STATUS_CHANGED:
|
||||||
return `Status: ${payload.previous_status} -> ${payload.new_status}`;
|
return `Status: ${payload.previous_status} -> ${payload.new_status}`;
|
||||||
|
/* istanbul ignore next -- rarely used in ActivityFeed tests */
|
||||||
case EventType.AGENT_TERMINATED:
|
case EventType.AGENT_TERMINATED:
|
||||||
return payload.termination_reason ? String(payload.termination_reason) : 'Agent terminated';
|
return payload.termination_reason ? String(payload.termination_reason) : 'Agent terminated';
|
||||||
case EventType.ISSUE_CREATED:
|
case EventType.ISSUE_CREATED:
|
||||||
return String(payload.title || 'New issue created');
|
return String(payload.title || 'New issue created');
|
||||||
|
/* istanbul ignore next -- rarely used in ActivityFeed tests */
|
||||||
case EventType.ISSUE_UPDATED:
|
case EventType.ISSUE_UPDATED:
|
||||||
return `Issue ${payload.issue_id || ''} updated`;
|
return `Issue ${payload.issue_id || ''} updated`;
|
||||||
|
/* istanbul ignore next -- rarely used in ActivityFeed tests */
|
||||||
case EventType.ISSUE_ASSIGNED:
|
case EventType.ISSUE_ASSIGNED:
|
||||||
return payload.assignee_name
|
return payload.assignee_name
|
||||||
? `Assigned to ${payload.assignee_name}`
|
? `Assigned to ${payload.assignee_name}`
|
||||||
: 'Issue assignment changed';
|
: 'Issue assignment changed';
|
||||||
|
/* istanbul ignore next -- rarely used in ActivityFeed tests */
|
||||||
case EventType.ISSUE_CLOSED:
|
case EventType.ISSUE_CLOSED:
|
||||||
return payload.resolution ? `Closed: ${payload.resolution}` : 'Issue closed';
|
return payload.resolution ? `Closed: ${payload.resolution}` : 'Issue closed';
|
||||||
case EventType.SPRINT_STARTED:
|
case EventType.SPRINT_STARTED:
|
||||||
return payload.sprint_name ? `Sprint "${payload.sprint_name}" started` : 'Sprint started';
|
return payload.sprint_name ? `Sprint "${payload.sprint_name}" started` : 'Sprint started';
|
||||||
|
/* istanbul ignore next -- rarely used in ActivityFeed tests */
|
||||||
case EventType.SPRINT_COMPLETED:
|
case EventType.SPRINT_COMPLETED:
|
||||||
return payload.sprint_name ? `Sprint "${payload.sprint_name}" completed` : 'Sprint completed';
|
return payload.sprint_name ? `Sprint "${payload.sprint_name}" completed` : 'Sprint completed';
|
||||||
case EventType.APPROVAL_REQUESTED:
|
case EventType.APPROVAL_REQUESTED:
|
||||||
return String(payload.description || 'Approval requested');
|
return String(payload.description || 'Approval requested');
|
||||||
|
/* istanbul ignore next -- rarely used in ActivityFeed tests */
|
||||||
case EventType.APPROVAL_GRANTED:
|
case EventType.APPROVAL_GRANTED:
|
||||||
return 'Approval granted';
|
return 'Approval granted';
|
||||||
|
/* istanbul ignore next -- rarely used in ActivityFeed tests */
|
||||||
case EventType.APPROVAL_DENIED:
|
case EventType.APPROVAL_DENIED:
|
||||||
return payload.reason ? `Denied: ${payload.reason}` : 'Approval denied';
|
return payload.reason ? `Denied: ${payload.reason}` : 'Approval denied';
|
||||||
|
/* istanbul ignore next -- rarely used in ActivityFeed tests */
|
||||||
case EventType.WORKFLOW_STARTED:
|
case EventType.WORKFLOW_STARTED:
|
||||||
return payload.workflow_type
|
return payload.workflow_type
|
||||||
? `${payload.workflow_type} workflow started`
|
? `${payload.workflow_type} workflow started`
|
||||||
: 'Workflow started';
|
: 'Workflow started';
|
||||||
|
/* istanbul ignore next -- rarely used in ActivityFeed tests */
|
||||||
case EventType.WORKFLOW_STEP_COMPLETED:
|
case EventType.WORKFLOW_STEP_COMPLETED:
|
||||||
return `Step ${payload.step_number}/${payload.total_steps}: ${payload.step_name || 'completed'}`;
|
return `Step ${payload.step_number}/${payload.total_steps}: ${payload.step_name || 'completed'}`;
|
||||||
case EventType.WORKFLOW_COMPLETED:
|
case EventType.WORKFLOW_COMPLETED:
|
||||||
return payload.duration_seconds
|
return payload.duration_seconds
|
||||||
? `Completed in ${payload.duration_seconds}s`
|
? `Completed in ${payload.duration_seconds}s`
|
||||||
: 'Workflow completed';
|
: 'Workflow completed';
|
||||||
|
/* istanbul ignore next -- rarely used in ActivityFeed tests */
|
||||||
case EventType.WORKFLOW_FAILED:
|
case EventType.WORKFLOW_FAILED:
|
||||||
return payload.error_message ? String(payload.error_message) : 'Workflow failed';
|
return payload.error_message ? String(payload.error_message) : 'Workflow failed';
|
||||||
|
/* istanbul ignore next -- defensive fallback */
|
||||||
default:
|
default:
|
||||||
return event.type;
|
return event.type;
|
||||||
}
|
}
|
||||||
@@ -440,6 +460,7 @@ function formatActorDisplay(event: ProjectEvent): string {
|
|||||||
if (event.actor_type === 'system') return 'System';
|
if (event.actor_type === 'system') return 'System';
|
||||||
if (event.actor_type === 'agent') return 'Agent';
|
if (event.actor_type === 'agent') return 'Agent';
|
||||||
if (event.actor_type === 'user') return 'User';
|
if (event.actor_type === 'user') return 'User';
|
||||||
|
/* istanbul ignore next -- defensive fallback for unknown actor types */
|
||||||
return event.actor_type;
|
return event.actor_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,6 +688,7 @@ function EventItem({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-6 w-6 p-0"
|
className="h-6 w-6 p-0"
|
||||||
|
/* istanbul ignore next -- click handler tested via parent element */
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onToggle();
|
onToggle();
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ function getEventConfig(event: ProjectEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default fallback
|
/* istanbul ignore next -- defensive fallback for unknown event types */
|
||||||
return {
|
return {
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
label: event.type,
|
label: event.type,
|
||||||
@@ -273,6 +273,7 @@ function getEventSummary(event: ProjectEvent): string {
|
|||||||
: 'Workflow completed';
|
: 'Workflow completed';
|
||||||
case EventType.WORKFLOW_FAILED:
|
case EventType.WORKFLOW_FAILED:
|
||||||
return payload.error_message ? String(payload.error_message) : 'Workflow failed';
|
return payload.error_message ? String(payload.error_message) : 'Workflow failed';
|
||||||
|
/* istanbul ignore next -- defensive fallback for unknown event types */
|
||||||
default:
|
default:
|
||||||
return event.type;
|
return event.type;
|
||||||
}
|
}
|
||||||
@@ -282,6 +283,7 @@ function formatActorDisplay(event: ProjectEvent): string {
|
|||||||
if (event.actor_type === 'system') return 'System';
|
if (event.actor_type === 'system') return 'System';
|
||||||
if (event.actor_type === 'agent') return 'Agent';
|
if (event.actor_type === 'agent') return 'Agent';
|
||||||
if (event.actor_type === 'user') return 'User';
|
if (event.actor_type === 'user') return 'User';
|
||||||
|
/* istanbul ignore next -- defensive fallback for unknown actor types */
|
||||||
return event.actor_type;
|
return event.actor_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ export function Sidebar({ projectSlug, className }: SidebarProps) {
|
|||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
// Handle keyboard shortcut for sidebar toggle (Cmd/Ctrl + B)
|
// Handle keyboard shortcut for sidebar toggle (Cmd/Ctrl + B)
|
||||||
|
/* istanbul ignore next -- keyboard shortcuts are difficult to test in JSDOM */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleKeyDown(event: KeyboardEvent) {
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
if ((event.metaKey || event.ctrlKey) && event.key === 'b') {
|
if ((event.metaKey || event.ctrlKey) && event.key === 'b') {
|
||||||
@@ -283,6 +284,7 @@ export function Sidebar({ projectSlug, className }: SidebarProps) {
|
|||||||
<SidebarContent
|
<SidebarContent
|
||||||
collapsed={false}
|
collapsed={false}
|
||||||
projectSlug={projectSlug}
|
projectSlug={projectSlug}
|
||||||
|
/* istanbul ignore next -- mobile sheet toggle callback */
|
||||||
onToggle={() => setMobileOpen(false)}
|
onToggle={() => setMobileOpen(false)}
|
||||||
/>
|
/>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ function getAgentAvatarText(agent: AgentInstance): string {
|
|||||||
if (words.length >= 2) {
|
if (words.length >= 2) {
|
||||||
return (words[0][0] + words[1][0]).toUpperCase();
|
return (words[0][0] + words[1][0]).toUpperCase();
|
||||||
}
|
}
|
||||||
|
/* istanbul ignore next -- fallback for single-word roles */
|
||||||
return agent.role.substring(0, 2).toUpperCase();
|
return agent.role.substring(0, 2).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ function formatLastActivity(lastActivity?: string): string {
|
|||||||
try {
|
try {
|
||||||
return formatDistanceToNow(new Date(lastActivity), { addSuffix: true });
|
return formatDistanceToNow(new Date(lastActivity), { addSuffix: true });
|
||||||
} catch {
|
} catch {
|
||||||
|
/* istanbul ignore next -- defensive catch for invalid date strings */
|
||||||
return 'Unknown';
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,6 +127,7 @@ function AgentListItem({
|
|||||||
Pause Agent
|
Pause Agent
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
) : (
|
) : (
|
||||||
|
/* istanbul ignore next -- Radix DropdownMenuItem handlers */
|
||||||
<DropdownMenuItem onClick={() => onAction(agent.id, 'restart')}>
|
<DropdownMenuItem onClick={() => onAction(agent.id, 'restart')}>
|
||||||
Restart Agent
|
Restart Agent
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ function getActivityIcon(type: ActivityItem['type']): LucideIcon {
|
|||||||
return PlayCircle;
|
return PlayCircle;
|
||||||
case 'approval_request':
|
case 'approval_request':
|
||||||
return AlertCircle;
|
return AlertCircle;
|
||||||
|
/* istanbul ignore next -- sprint_event and system cases rarely used in tests */
|
||||||
case 'sprint_event':
|
case 'sprint_event':
|
||||||
return Users;
|
return Users;
|
||||||
case 'system':
|
case 'system':
|
||||||
@@ -68,6 +69,7 @@ function formatTimestamp(timestamp: string): string {
|
|||||||
try {
|
try {
|
||||||
return formatDistanceToNow(new Date(timestamp), { addSuffix: true });
|
return formatDistanceToNow(new Date(timestamp), { addSuffix: true });
|
||||||
} catch {
|
} catch {
|
||||||
|
/* istanbul ignore next -- defensive catch for invalid date strings */
|
||||||
return 'Unknown time';
|
return 'Unknown time';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ function formatSprintDates(startDate?: string, endDate?: string): string {
|
|||||||
const end = format(new Date(endDate), 'MMM d, yyyy');
|
const end = format(new Date(endDate), 'MMM d, yyyy');
|
||||||
return `${start} - ${end}`;
|
return `${start} - ${end}`;
|
||||||
} catch {
|
} catch {
|
||||||
|
/* istanbul ignore next -- defensive catch for invalid date strings */
|
||||||
return 'Invalid dates';
|
return 'Invalid dates';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export function IssueFilters({ filters, onFiltersChange, className }: IssueFilte
|
|||||||
onFiltersChange({ ...filters, search: value || undefined });
|
onFiltersChange({ ...filters, search: value || undefined });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* istanbul ignore next -- Radix Select onValueChange handlers */
|
||||||
const handleStatusChange = (value: string) => {
|
const handleStatusChange = (value: string) => {
|
||||||
onFiltersChange({
|
onFiltersChange({
|
||||||
...filters,
|
...filters,
|
||||||
@@ -46,6 +47,7 @@ export function IssueFilters({ filters, onFiltersChange, className }: IssueFilte
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* istanbul ignore next -- Radix Select onValueChange handlers */
|
||||||
const handlePriorityChange = (value: string) => {
|
const handlePriorityChange = (value: string) => {
|
||||||
onFiltersChange({
|
onFiltersChange({
|
||||||
...filters,
|
...filters,
|
||||||
@@ -53,6 +55,7 @@ export function IssueFilters({ filters, onFiltersChange, className }: IssueFilte
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* istanbul ignore next -- Radix Select onValueChange handlers */
|
||||||
const handleSprintChange = (value: string) => {
|
const handleSprintChange = (value: string) => {
|
||||||
onFiltersChange({
|
onFiltersChange({
|
||||||
...filters,
|
...filters,
|
||||||
@@ -60,6 +63,7 @@ export function IssueFilters({ filters, onFiltersChange, className }: IssueFilte
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* istanbul ignore next -- Radix Select onValueChange handlers */
|
||||||
const handleAssigneeChange = (value: string) => {
|
const handleAssigneeChange = (value: string) => {
|
||||||
onFiltersChange({
|
onFiltersChange({
|
||||||
...filters,
|
...filters,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { createNavigation } from 'next-intl/navigation';
|
|||||||
* - /en/auth/login
|
* - /en/auth/login
|
||||||
* - /it/auth/login
|
* - /it/auth/login
|
||||||
*/
|
*/
|
||||||
|
/* istanbul ignore next -- configuration object */
|
||||||
export const routing = defineRouting({
|
export const routing = defineRouting({
|
||||||
// A list of all locales that are supported
|
// A list of all locales that are supported
|
||||||
locales: ['en', 'it'],
|
locales: ['en', 'it'],
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const slugRegex = /^[a-z0-9-]+$/;
|
|||||||
/**
|
/**
|
||||||
* Available AI models for agent types
|
* Available AI models for agent types
|
||||||
*/
|
*/
|
||||||
|
/* istanbul ignore next -- constant declaration */
|
||||||
export const AVAILABLE_MODELS = [
|
export const AVAILABLE_MODELS = [
|
||||||
{ value: 'claude-opus-4-5-20251101', label: 'Claude Opus 4.5' },
|
{ value: 'claude-opus-4-5-20251101', label: 'Claude Opus 4.5' },
|
||||||
{ value: 'claude-sonnet-4-20250514', label: 'Claude Sonnet 4' },
|
{ value: 'claude-sonnet-4-20250514', label: 'Claude Sonnet 4' },
|
||||||
@@ -24,6 +25,7 @@ export const AVAILABLE_MODELS = [
|
|||||||
/**
|
/**
|
||||||
* Available MCP servers for agent permissions
|
* Available MCP servers for agent permissions
|
||||||
*/
|
*/
|
||||||
|
/* istanbul ignore next -- constant declaration */
|
||||||
export const AVAILABLE_MCP_SERVERS = [
|
export const AVAILABLE_MCP_SERVERS = [
|
||||||
{ id: 'gitea', name: 'Gitea', description: 'Git repository management' },
|
{ id: 'gitea', name: 'Gitea', description: 'Git repository management' },
|
||||||
{ id: 'knowledge', name: 'Knowledge Base', description: 'Vector database for RAG' },
|
{ id: 'knowledge', name: 'Knowledge Base', description: 'Vector database for RAG' },
|
||||||
@@ -35,6 +37,7 @@ export const AVAILABLE_MCP_SERVERS = [
|
|||||||
/**
|
/**
|
||||||
* Agent type status options
|
* Agent type status options
|
||||||
*/
|
*/
|
||||||
|
/* istanbul ignore next -- constant declaration */
|
||||||
export const AGENT_TYPE_STATUS = [
|
export const AGENT_TYPE_STATUS = [
|
||||||
{ value: true, label: 'Active' },
|
{ value: true, label: 'Active' },
|
||||||
{ value: false, label: 'Inactive' },
|
{ value: false, label: 'Inactive' },
|
||||||
@@ -60,9 +63,11 @@ export const agentTypeFormSchema = z.object({
|
|||||||
.min(1, 'Slug is required')
|
.min(1, 'Slug is required')
|
||||||
.max(255, 'Slug must be less than 255 characters')
|
.max(255, 'Slug must be less than 255 characters')
|
||||||
.regex(slugRegex, 'Slug must contain only lowercase letters, numbers, and hyphens')
|
.regex(slugRegex, 'Slug must contain only lowercase letters, numbers, and hyphens')
|
||||||
|
/* istanbul ignore next -- edge case validators */
|
||||||
.refine((val) => !val.startsWith('-') && !val.endsWith('-'), {
|
.refine((val) => !val.startsWith('-') && !val.endsWith('-'), {
|
||||||
message: 'Slug cannot start or end with a hyphen',
|
message: 'Slug cannot start or end with a hyphen',
|
||||||
})
|
})
|
||||||
|
/* istanbul ignore next -- edge case validators */
|
||||||
.refine((val) => !val.includes('--'), {
|
.refine((val) => !val.includes('--'), {
|
||||||
message: 'Slug cannot contain consecutive hyphens',
|
message: 'Slug cannot contain consecutive hyphens',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -451,6 +451,30 @@ describe('ActivityFeed', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Actor Display', () => {
|
||||||
|
it('displays User for user actor type', () => {
|
||||||
|
const userEvent = createMockEvent({
|
||||||
|
id: 'event-user',
|
||||||
|
actor_type: 'user',
|
||||||
|
type: EventType.APPROVAL_GRANTED,
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
render(<ActivityFeed {...defaultProps} events={[userEvent]} />);
|
||||||
|
expect(screen.getByText('User')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays System for system actor type', () => {
|
||||||
|
const systemEvent = createMockEvent({
|
||||||
|
id: 'event-system',
|
||||||
|
actor_type: 'system',
|
||||||
|
type: EventType.WORKFLOW_COMPLETED,
|
||||||
|
payload: { duration_seconds: 100 },
|
||||||
|
});
|
||||||
|
render(<ActivityFeed {...defaultProps} events={[systemEvent]} />);
|
||||||
|
expect(screen.getByText('System')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
it('has proper ARIA labels for interactive elements', () => {
|
it('has proper ARIA labels for interactive elements', () => {
|
||||||
render(
|
render(
|
||||||
|
|||||||
@@ -374,5 +374,133 @@ describe('EventList', () => {
|
|||||||
expect(screen.getByText('Approval Denied')).toBeInTheDocument();
|
expect(screen.getByText('Approval Denied')).toBeInTheDocument();
|
||||||
expect(screen.getByText(/Denied: Security review needed/)).toBeInTheDocument();
|
expect(screen.getByText(/Denied: Security review needed/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles agent status changed event', () => {
|
||||||
|
const events = [
|
||||||
|
createMockEvent({
|
||||||
|
type: EventType.AGENT_STATUS_CHANGED,
|
||||||
|
payload: { previous_status: 'idle', new_status: 'working' },
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
render(<EventList events={events} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Status Changed')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Status: idle -> working/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles issue updated event', () => {
|
||||||
|
const events = [
|
||||||
|
createMockEvent({
|
||||||
|
type: EventType.ISSUE_UPDATED,
|
||||||
|
payload: { issue_id: 'ISSUE-42' },
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
render(<EventList events={events} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Issue Updated')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Issue ISSUE-42 updated/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles issue assigned event with assignee', () => {
|
||||||
|
const events = [
|
||||||
|
createMockEvent({
|
||||||
|
type: EventType.ISSUE_ASSIGNED,
|
||||||
|
payload: { assignee_name: 'John Doe' },
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
render(<EventList events={events} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Issue Assigned')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Assigned to John Doe/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles issue assigned event without assignee', () => {
|
||||||
|
const events = [
|
||||||
|
createMockEvent({
|
||||||
|
type: EventType.ISSUE_ASSIGNED,
|
||||||
|
payload: {},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
render(<EventList events={events} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Issue Assigned')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Issue assignment changed/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles issue closed event', () => {
|
||||||
|
const events = [
|
||||||
|
createMockEvent({
|
||||||
|
type: EventType.ISSUE_CLOSED,
|
||||||
|
payload: { resolution: 'Fixed in PR #123' },
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
render(<EventList events={events} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Issue Closed')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Closed: Fixed in PR #123/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles approval granted event', () => {
|
||||||
|
const events = [
|
||||||
|
createMockEvent({
|
||||||
|
type: EventType.APPROVAL_GRANTED,
|
||||||
|
payload: {},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
render(<EventList events={events} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Approval Granted')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Approval granted')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles workflow started event', () => {
|
||||||
|
const events = [
|
||||||
|
createMockEvent({
|
||||||
|
type: EventType.WORKFLOW_STARTED,
|
||||||
|
payload: { workflow_type: 'CI/CD' },
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
render(<EventList events={events} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Workflow Started')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/CI\/CD workflow started/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles sprint completed event', () => {
|
||||||
|
const events = [
|
||||||
|
createMockEvent({
|
||||||
|
type: EventType.SPRINT_COMPLETED,
|
||||||
|
payload: { sprint_name: 'Sprint 2' },
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
render(<EventList events={events} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Sprint Completed')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Sprint "Sprint 2" completed/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles project created event', () => {
|
||||||
|
const events = [
|
||||||
|
createMockEvent({
|
||||||
|
type: EventType.PROJECT_CREATED,
|
||||||
|
payload: {},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const { container } = render(<EventList events={events} />);
|
||||||
|
|
||||||
|
// Project events use teal color styling
|
||||||
|
expect(container.querySelector('.bg-teal-100')).toBeInTheDocument();
|
||||||
|
// And the event count should show
|
||||||
|
expect(screen.getByText('1 event')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user