forked from cardosofelipe/pragma-stack
Compare commits
6 Commits
15d747eb28
...
f0b04d53af
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0b04d53af | ||
|
|
35af7daf90 | ||
|
|
5fab15a11e | ||
|
|
ab913575e1 | ||
|
|
82cb6386a6 | ||
|
|
2d05035c1d |
@@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
Revision ID: 0004
|
Revision ID: 0004
|
||||||
Revises: 0003
|
Revises: 0003
|
||||||
Create Date: 2025-12-30
|
Create Date: 2025-12-31
|
||||||
|
|
||||||
This migration creates the core Syndarix domain tables:
|
This migration creates the core Syndarix domain tables:
|
||||||
- projects: Client engagement projects
|
- projects: Client engagement projects
|
||||||
- agent_types: Agent template configurations
|
- agent_types: Agent template configurations
|
||||||
- agent_instances: Spawned agent instances assigned to projects
|
- agent_instances: Spawned agent instances assigned to projects
|
||||||
- issues: Work items (stories, tasks, bugs)
|
|
||||||
- sprints: Sprint containers for issues
|
- sprints: Sprint containers for issues
|
||||||
|
- issues: Work items (epics, stories, tasks, bugs)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
@@ -28,7 +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 first
|
# =========================================================================
|
||||||
|
# Create ENUM types
|
||||||
|
# =========================================================================
|
||||||
op.execute(
|
op.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TYPE autonomy_level AS ENUM (
|
CREATE TYPE autonomy_level AS ENUM (
|
||||||
@@ -64,24 +66,24 @@ def upgrade() -> None:
|
|||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE TYPE issue_type AS ENUM (
|
||||||
|
'epic', 'story', 'task', 'bug'
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
op.execute(
|
op.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TYPE issue_status AS ENUM (
|
CREATE TYPE issue_status AS ENUM (
|
||||||
'open', 'in_progress', 'in_review', 'closed', 'blocked'
|
'open', 'in_progress', 'in_review', 'blocked', 'closed'
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
op.execute(
|
op.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TYPE issue_priority AS ENUM (
|
CREATE TYPE issue_priority AS ENUM (
|
||||||
'critical', 'high', 'medium', 'low'
|
'low', 'medium', 'high', 'critical'
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TYPE external_tracker_type AS ENUM (
|
|
||||||
'gitea', 'github', 'gitlab', 'jira'
|
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@@ -95,12 +97,14 @@ def upgrade() -> None:
|
|||||||
op.execute(
|
op.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TYPE sprint_status AS ENUM (
|
CREATE TYPE sprint_status AS ENUM (
|
||||||
'planned', 'active', 'completed', 'cancelled'
|
'planned', 'active', 'in_review', 'completed', 'cancelled'
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
# Create projects table
|
# Create projects table
|
||||||
|
# =========================================================================
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"projects",
|
"projects",
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
@@ -152,7 +156,10 @@ def upgrade() -> None:
|
|||||||
server_default="auto",
|
server_default="auto",
|
||||||
),
|
),
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"settings", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="{}"
|
"settings",
|
||||||
|
postgresql.JSONB(astext_type=sa.Text()),
|
||||||
|
nullable=False,
|
||||||
|
server_default="{}",
|
||||||
),
|
),
|
||||||
sa.Column("owner_id", postgresql.UUID(as_uuid=True), nullable=True),
|
sa.Column("owner_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
sa.Column(
|
sa.Column(
|
||||||
@@ -168,11 +175,10 @@ def upgrade() -> None:
|
|||||||
server_default=sa.text("now()"),
|
server_default=sa.text("now()"),
|
||||||
),
|
),
|
||||||
sa.PrimaryKeyConstraint("id"),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
sa.ForeignKeyConstraint(
|
sa.ForeignKeyConstraint(["owner_id"], ["users.id"], ondelete="SET NULL"),
|
||||||
["owner_id"], ["users.id"], ondelete="SET NULL"
|
|
||||||
),
|
|
||||||
sa.UniqueConstraint("slug"),
|
sa.UniqueConstraint("slug"),
|
||||||
)
|
)
|
||||||
|
# Single column indexes
|
||||||
op.create_index("ix_projects_name", "projects", ["name"])
|
op.create_index("ix_projects_name", "projects", ["name"])
|
||||||
op.create_index("ix_projects_slug", "projects", ["slug"])
|
op.create_index("ix_projects_slug", "projects", ["slug"])
|
||||||
op.create_index("ix_projects_status", "projects", ["status"])
|
op.create_index("ix_projects_status", "projects", ["status"])
|
||||||
@@ -180,6 +186,7 @@ def upgrade() -> None:
|
|||||||
op.create_index("ix_projects_complexity", "projects", ["complexity"])
|
op.create_index("ix_projects_complexity", "projects", ["complexity"])
|
||||||
op.create_index("ix_projects_client_mode", "projects", ["client_mode"])
|
op.create_index("ix_projects_client_mode", "projects", ["client_mode"])
|
||||||
op.create_index("ix_projects_owner_id", "projects", ["owner_id"])
|
op.create_index("ix_projects_owner_id", "projects", ["owner_id"])
|
||||||
|
# Composite indexes
|
||||||
op.create_index("ix_projects_slug_status", "projects", ["slug", "status"])
|
op.create_index("ix_projects_slug_status", "projects", ["slug", "status"])
|
||||||
op.create_index("ix_projects_owner_status", "projects", ["owner_id", "status"])
|
op.create_index("ix_projects_owner_status", "projects", ["owner_id", "status"])
|
||||||
op.create_index(
|
op.create_index(
|
||||||
@@ -189,13 +196,25 @@ def upgrade() -> None:
|
|||||||
"ix_projects_complexity_status", "projects", ["complexity", "status"]
|
"ix_projects_complexity_status", "projects", ["complexity", "status"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
# Create agent_types table
|
# Create agent_types table
|
||||||
|
# =========================================================================
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"agent_types",
|
"agent_types",
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
sa.Column("name", sa.String(100), nullable=False),
|
sa.Column("name", sa.String(255), nullable=False),
|
||||||
sa.Column("slug", sa.String(100), nullable=False),
|
sa.Column("slug", sa.String(255), nullable=False),
|
||||||
sa.Column("description", sa.Text(), nullable=True),
|
sa.Column("description", sa.Text(), nullable=True),
|
||||||
|
# Areas of expertise (e.g., ["python", "fastapi", "databases"])
|
||||||
|
sa.Column(
|
||||||
|
"expertise",
|
||||||
|
postgresql.JSONB(astext_type=sa.Text()),
|
||||||
|
nullable=False,
|
||||||
|
server_default="[]",
|
||||||
|
),
|
||||||
|
# System prompt defining personality and behavior (required)
|
||||||
|
sa.Column("personality_prompt", sa.Text(), nullable=False),
|
||||||
|
# LLM model configuration
|
||||||
sa.Column("primary_model", sa.String(100), nullable=False),
|
sa.Column("primary_model", sa.String(100), nullable=False),
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"fallback_models",
|
"fallback_models",
|
||||||
@@ -203,16 +222,23 @@ def upgrade() -> None:
|
|||||||
nullable=False,
|
nullable=False,
|
||||||
server_default="[]",
|
server_default="[]",
|
||||||
),
|
),
|
||||||
sa.Column("system_prompt", sa.Text(), nullable=True),
|
# Model parameters (temperature, max_tokens, etc.)
|
||||||
sa.Column("personality_prompt", sa.Text(), nullable=True),
|
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"capabilities",
|
"model_params",
|
||||||
|
postgresql.JSONB(astext_type=sa.Text()),
|
||||||
|
nullable=False,
|
||||||
|
server_default="{}",
|
||||||
|
),
|
||||||
|
# MCP servers this agent can connect to
|
||||||
|
sa.Column(
|
||||||
|
"mcp_servers",
|
||||||
postgresql.JSONB(astext_type=sa.Text()),
|
postgresql.JSONB(astext_type=sa.Text()),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default="[]",
|
server_default="[]",
|
||||||
),
|
),
|
||||||
|
# Tool permissions configuration
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"default_config",
|
"tool_permissions",
|
||||||
postgresql.JSONB(astext_type=sa.Text()),
|
postgresql.JSONB(astext_type=sa.Text()),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default="{}",
|
server_default="{}",
|
||||||
@@ -233,12 +259,17 @@ def upgrade() -> None:
|
|||||||
sa.PrimaryKeyConstraint("id"),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
sa.UniqueConstraint("slug"),
|
sa.UniqueConstraint("slug"),
|
||||||
)
|
)
|
||||||
|
# Single column indexes
|
||||||
op.create_index("ix_agent_types_name", "agent_types", ["name"])
|
op.create_index("ix_agent_types_name", "agent_types", ["name"])
|
||||||
op.create_index("ix_agent_types_slug", "agent_types", ["slug"])
|
op.create_index("ix_agent_types_slug", "agent_types", ["slug"])
|
||||||
op.create_index("ix_agent_types_is_active", "agent_types", ["is_active"])
|
op.create_index("ix_agent_types_is_active", "agent_types", ["is_active"])
|
||||||
op.create_index("ix_agent_types_primary_model", "agent_types", ["primary_model"])
|
# Composite indexes
|
||||||
|
op.create_index("ix_agent_types_slug_active", "agent_types", ["slug", "is_active"])
|
||||||
|
op.create_index("ix_agent_types_name_active", "agent_types", ["name", "is_active"])
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
# Create agent_instances table
|
# Create agent_instances table
|
||||||
|
# =========================================================================
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"agent_instances",
|
"agent_instances",
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
@@ -260,17 +291,28 @@ def upgrade() -> None:
|
|||||||
server_default="idle",
|
server_default="idle",
|
||||||
),
|
),
|
||||||
sa.Column("current_task", sa.Text(), nullable=True),
|
sa.Column("current_task", sa.Text(), nullable=True),
|
||||||
|
# Short-term memory (conversation context, recent decisions)
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"config_overrides",
|
"short_term_memory",
|
||||||
postgresql.JSONB(astext_type=sa.Text()),
|
postgresql.JSONB(astext_type=sa.Text()),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default="{}",
|
server_default="{}",
|
||||||
),
|
),
|
||||||
|
# Reference to long-term memory in vector store
|
||||||
|
sa.Column("long_term_memory_ref", sa.String(500), nullable=True),
|
||||||
|
# Session ID for active MCP connections
|
||||||
|
sa.Column("session_id", sa.String(255), nullable=True),
|
||||||
|
# Activity tracking
|
||||||
|
sa.Column("last_activity_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column("terminated_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
# Usage metrics
|
||||||
|
sa.Column("tasks_completed", sa.Integer(), nullable=False, server_default="0"),
|
||||||
|
sa.Column("tokens_used", sa.BigInteger(), nullable=False, server_default="0"),
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"metadata",
|
"cost_incurred",
|
||||||
postgresql.JSONB(astext_type=sa.Text()),
|
sa.Numeric(precision=10, scale=4),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default="{}",
|
server_default="0",
|
||||||
),
|
),
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"created_at",
|
"created_at",
|
||||||
@@ -290,12 +332,21 @@ def upgrade() -> None:
|
|||||||
),
|
),
|
||||||
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
|
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
|
||||||
)
|
)
|
||||||
|
# Single column indexes
|
||||||
op.create_index("ix_agent_instances_name", "agent_instances", ["name"])
|
op.create_index("ix_agent_instances_name", "agent_instances", ["name"])
|
||||||
op.create_index("ix_agent_instances_status", "agent_instances", ["status"])
|
op.create_index("ix_agent_instances_status", "agent_instances", ["status"])
|
||||||
op.create_index(
|
op.create_index(
|
||||||
"ix_agent_instances_agent_type_id", "agent_instances", ["agent_type_id"]
|
"ix_agent_instances_agent_type_id", "agent_instances", ["agent_type_id"]
|
||||||
)
|
)
|
||||||
op.create_index("ix_agent_instances_project_id", "agent_instances", ["project_id"])
|
op.create_index("ix_agent_instances_project_id", "agent_instances", ["project_id"])
|
||||||
|
op.create_index("ix_agent_instances_session_id", "agent_instances", ["session_id"])
|
||||||
|
op.create_index(
|
||||||
|
"ix_agent_instances_last_activity_at", "agent_instances", ["last_activity_at"]
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_agent_instances_terminated_at", "agent_instances", ["terminated_at"]
|
||||||
|
)
|
||||||
|
# Composite indexes
|
||||||
op.create_index(
|
op.create_index(
|
||||||
"ix_agent_instances_project_status",
|
"ix_agent_instances_project_status",
|
||||||
"agent_instances",
|
"agent_instances",
|
||||||
@@ -306,22 +357,30 @@ def upgrade() -> None:
|
|||||||
"agent_instances",
|
"agent_instances",
|
||||||
["agent_type_id", "status"],
|
["agent_type_id", "status"],
|
||||||
)
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_agent_instances_project_type",
|
||||||
|
"agent_instances",
|
||||||
|
["project_id", "agent_type_id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
# Create sprints table (before issues for FK reference)
|
# Create sprints table (before issues for FK reference)
|
||||||
|
# =========================================================================
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"sprints",
|
"sprints",
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False),
|
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
sa.Column("name", sa.String(100), nullable=False),
|
sa.Column("name", sa.String(255), nullable=False),
|
||||||
sa.Column("number", sa.Integer(), nullable=False),
|
sa.Column("number", sa.Integer(), nullable=False),
|
||||||
sa.Column("goal", sa.Text(), nullable=True),
|
sa.Column("goal", sa.Text(), nullable=True),
|
||||||
sa.Column("start_date", sa.Date(), nullable=True),
|
sa.Column("start_date", sa.Date(), nullable=False),
|
||||||
sa.Column("end_date", sa.Date(), nullable=True),
|
sa.Column("end_date", sa.Date(), nullable=False),
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"status",
|
"status",
|
||||||
sa.Enum(
|
sa.Enum(
|
||||||
"planned",
|
"planned",
|
||||||
"active",
|
"active",
|
||||||
|
"in_review",
|
||||||
"completed",
|
"completed",
|
||||||
"cancelled",
|
"cancelled",
|
||||||
name="sprint_status",
|
name="sprint_status",
|
||||||
@@ -348,29 +407,53 @@ def upgrade() -> None:
|
|||||||
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
|
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
|
||||||
sa.UniqueConstraint("project_id", "number", name="uq_sprint_project_number"),
|
sa.UniqueConstraint("project_id", "number", name="uq_sprint_project_number"),
|
||||||
)
|
)
|
||||||
op.create_index("ix_sprints_name", "sprints", ["name"])
|
# Single column indexes
|
||||||
op.create_index("ix_sprints_number", "sprints", ["number"])
|
|
||||||
op.create_index("ix_sprints_status", "sprints", ["status"])
|
|
||||||
op.create_index("ix_sprints_project_id", "sprints", ["project_id"])
|
op.create_index("ix_sprints_project_id", "sprints", ["project_id"])
|
||||||
|
op.create_index("ix_sprints_status", "sprints", ["status"])
|
||||||
|
op.create_index("ix_sprints_start_date", "sprints", ["start_date"])
|
||||||
|
op.create_index("ix_sprints_end_date", "sprints", ["end_date"])
|
||||||
|
# Composite indexes
|
||||||
op.create_index("ix_sprints_project_status", "sprints", ["project_id", "status"])
|
op.create_index("ix_sprints_project_status", "sprints", ["project_id", "status"])
|
||||||
|
op.create_index("ix_sprints_project_number", "sprints", ["project_id", "number"])
|
||||||
|
op.create_index("ix_sprints_date_range", "sprints", ["start_date", "end_date"])
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
# Create issues table
|
# Create issues table
|
||||||
|
# =========================================================================
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"issues",
|
"issues",
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False),
|
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
sa.Column("sprint_id", postgresql.UUID(as_uuid=True), nullable=True),
|
# Parent issue for hierarchy (Epic -> Story -> Task)
|
||||||
sa.Column("assigned_agent_id", postgresql.UUID(as_uuid=True), nullable=True),
|
sa.Column("parent_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
# Issue type (epic, story, task, bug)
|
||||||
|
sa.Column(
|
||||||
|
"type",
|
||||||
|
sa.Enum(
|
||||||
|
"epic",
|
||||||
|
"story",
|
||||||
|
"task",
|
||||||
|
"bug",
|
||||||
|
name="issue_type",
|
||||||
|
create_type=False,
|
||||||
|
),
|
||||||
|
nullable=False,
|
||||||
|
server_default="task",
|
||||||
|
),
|
||||||
|
# Reporter (who created this issue)
|
||||||
|
sa.Column("reporter_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
# Issue content
|
||||||
sa.Column("title", sa.String(500), nullable=False),
|
sa.Column("title", sa.String(500), nullable=False),
|
||||||
sa.Column("description", sa.Text(), nullable=True),
|
sa.Column("body", sa.Text(), nullable=False, server_default=""),
|
||||||
|
# Status and priority
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"status",
|
"status",
|
||||||
sa.Enum(
|
sa.Enum(
|
||||||
"open",
|
"open",
|
||||||
"in_progress",
|
"in_progress",
|
||||||
"in_review",
|
"in_review",
|
||||||
"closed",
|
|
||||||
"blocked",
|
"blocked",
|
||||||
|
"closed",
|
||||||
name="issue_status",
|
name="issue_status",
|
||||||
create_type=False,
|
create_type=False,
|
||||||
),
|
),
|
||||||
@@ -380,33 +463,37 @@ def upgrade() -> None:
|
|||||||
sa.Column(
|
sa.Column(
|
||||||
"priority",
|
"priority",
|
||||||
sa.Enum(
|
sa.Enum(
|
||||||
"critical", "high", "medium", "low", name="issue_priority", create_type=False
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high",
|
||||||
|
"critical",
|
||||||
|
name="issue_priority",
|
||||||
|
create_type=False,
|
||||||
),
|
),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default="medium",
|
server_default="medium",
|
||||||
),
|
),
|
||||||
sa.Column("story_points", sa.Integer(), nullable=True),
|
# Labels for categorization
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"labels",
|
"labels",
|
||||||
postgresql.JSONB(astext_type=sa.Text()),
|
postgresql.JSONB(astext_type=sa.Text()),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default="[]",
|
server_default="[]",
|
||||||
),
|
),
|
||||||
sa.Column(
|
# Assignment - agent or human (mutually exclusive)
|
||||||
"external_tracker",
|
sa.Column("assigned_agent_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
sa.Enum(
|
sa.Column("human_assignee", sa.String(255), nullable=True),
|
||||||
"gitea",
|
# Sprint association
|
||||||
"github",
|
sa.Column("sprint_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
"gitlab",
|
# Estimation
|
||||||
"jira",
|
sa.Column("story_points", sa.Integer(), nullable=True),
|
||||||
name="external_tracker_type",
|
sa.Column("due_date", sa.Date(), nullable=True),
|
||||||
create_type=False,
|
# External tracker integration (String for flexibility)
|
||||||
),
|
sa.Column("external_tracker_type", sa.String(50), nullable=True),
|
||||||
nullable=True,
|
sa.Column("external_issue_id", sa.String(255), nullable=True),
|
||||||
),
|
sa.Column("remote_url", sa.String(1000), nullable=True),
|
||||||
sa.Column("external_id", sa.String(255), nullable=True),
|
sa.Column("external_issue_number", sa.Integer(), nullable=True),
|
||||||
sa.Column("external_url", sa.String(2048), nullable=True),
|
# Sync status
|
||||||
sa.Column("external_number", sa.Integer(), nullable=True),
|
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"sync_status",
|
"sync_status",
|
||||||
sa.Enum(
|
sa.Enum(
|
||||||
@@ -417,15 +504,13 @@ def upgrade() -> None:
|
|||||||
name="sync_status",
|
name="sync_status",
|
||||||
create_type=False,
|
create_type=False,
|
||||||
),
|
),
|
||||||
nullable=True,
|
nullable=False,
|
||||||
|
server_default="synced",
|
||||||
),
|
),
|
||||||
sa.Column("last_synced_at", sa.DateTime(timezone=True), nullable=True),
|
sa.Column("last_synced_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
sa.Column(
|
sa.Column("external_updated_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
"metadata",
|
# Lifecycle
|
||||||
postgresql.JSONB(astext_type=sa.Text()),
|
sa.Column("closed_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
nullable=False,
|
|
||||||
server_default="{}",
|
|
||||||
),
|
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"created_at",
|
"created_at",
|
||||||
sa.DateTime(timezone=True),
|
sa.DateTime(timezone=True),
|
||||||
@@ -440,29 +525,43 @@ def upgrade() -> None:
|
|||||||
),
|
),
|
||||||
sa.PrimaryKeyConstraint("id"),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
|
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["parent_id"], ["issues.id"], ondelete="CASCADE"
|
||||||
|
),
|
||||||
sa.ForeignKeyConstraint(["sprint_id"], ["sprints.id"], ondelete="SET NULL"),
|
sa.ForeignKeyConstraint(["sprint_id"], ["sprints.id"], ondelete="SET NULL"),
|
||||||
sa.ForeignKeyConstraint(
|
sa.ForeignKeyConstraint(
|
||||||
["assigned_agent_id"], ["agent_instances.id"], ondelete="SET NULL"
|
["assigned_agent_id"], ["agent_instances.id"], ondelete="SET NULL"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
op.create_index("ix_issues_title", "issues", ["title"])
|
# Single column indexes
|
||||||
|
op.create_index("ix_issues_project_id", "issues", ["project_id"])
|
||||||
|
op.create_index("ix_issues_parent_id", "issues", ["parent_id"])
|
||||||
|
op.create_index("ix_issues_type", "issues", ["type"])
|
||||||
|
op.create_index("ix_issues_reporter_id", "issues", ["reporter_id"])
|
||||||
op.create_index("ix_issues_status", "issues", ["status"])
|
op.create_index("ix_issues_status", "issues", ["status"])
|
||||||
op.create_index("ix_issues_priority", "issues", ["priority"])
|
op.create_index("ix_issues_priority", "issues", ["priority"])
|
||||||
op.create_index("ix_issues_project_id", "issues", ["project_id"])
|
|
||||||
op.create_index("ix_issues_sprint_id", "issues", ["sprint_id"])
|
|
||||||
op.create_index("ix_issues_assigned_agent_id", "issues", ["assigned_agent_id"])
|
op.create_index("ix_issues_assigned_agent_id", "issues", ["assigned_agent_id"])
|
||||||
op.create_index("ix_issues_external_tracker", "issues", ["external_tracker"])
|
op.create_index("ix_issues_human_assignee", "issues", ["human_assignee"])
|
||||||
|
op.create_index("ix_issues_sprint_id", "issues", ["sprint_id"])
|
||||||
|
op.create_index("ix_issues_due_date", "issues", ["due_date"])
|
||||||
|
op.create_index("ix_issues_external_tracker_type", "issues", ["external_tracker_type"])
|
||||||
op.create_index("ix_issues_sync_status", "issues", ["sync_status"])
|
op.create_index("ix_issues_sync_status", "issues", ["sync_status"])
|
||||||
|
op.create_index("ix_issues_closed_at", "issues", ["closed_at"])
|
||||||
|
# Composite indexes
|
||||||
op.create_index("ix_issues_project_status", "issues", ["project_id", "status"])
|
op.create_index("ix_issues_project_status", "issues", ["project_id", "status"])
|
||||||
|
op.create_index("ix_issues_project_priority", "issues", ["project_id", "priority"])
|
||||||
|
op.create_index("ix_issues_project_sprint", "issues", ["project_id", "sprint_id"])
|
||||||
|
op.create_index("ix_issues_project_type", "issues", ["project_id", "type"])
|
||||||
|
op.create_index("ix_issues_project_agent", "issues", ["project_id", "assigned_agent_id"])
|
||||||
op.create_index(
|
op.create_index(
|
||||||
"ix_issues_project_status_priority",
|
"ix_issues_project_status_priority",
|
||||||
"issues",
|
"issues",
|
||||||
["project_id", "status", "priority"],
|
["project_id", "status", "priority"],
|
||||||
)
|
)
|
||||||
op.create_index(
|
op.create_index(
|
||||||
"ix_issues_external",
|
"ix_issues_external_tracker_id",
|
||||||
"issues",
|
"issues",
|
||||||
["project_id", "external_tracker", "external_id"],
|
["external_tracker_type", "external_issue_id"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -478,9 +577,9 @@ def downgrade() -> None:
|
|||||||
# Drop ENUM types
|
# Drop ENUM types
|
||||||
op.execute("DROP TYPE IF EXISTS sprint_status")
|
op.execute("DROP TYPE IF EXISTS sprint_status")
|
||||||
op.execute("DROP TYPE IF EXISTS sync_status")
|
op.execute("DROP TYPE IF EXISTS sync_status")
|
||||||
op.execute("DROP TYPE IF EXISTS external_tracker_type")
|
|
||||||
op.execute("DROP TYPE IF EXISTS issue_priority")
|
op.execute("DROP TYPE IF EXISTS issue_priority")
|
||||||
op.execute("DROP TYPE IF EXISTS issue_status")
|
op.execute("DROP TYPE IF EXISTS issue_status")
|
||||||
|
op.execute("DROP TYPE IF EXISTS issue_type")
|
||||||
op.execute("DROP TYPE IF EXISTS agent_status")
|
op.execute("DROP TYPE IF EXISTS agent_status")
|
||||||
op.execute("DROP TYPE IF EXISTS client_mode")
|
op.execute("DROP TYPE IF EXISTS client_mode")
|
||||||
op.execute("DROP TYPE IF EXISTS project_complexity")
|
op.execute("DROP TYPE IF EXISTS project_complexity")
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Sprint model for Syndarix AI consulting platform.
|
|||||||
A Sprint represents a time-boxed iteration for organizing and delivering work.
|
A Sprint represents a time-boxed iteration for organizing and delivering work.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sqlalchemy import Column, Date, Enum, ForeignKey, Index, Integer, String, Text
|
from sqlalchemy import Column, Date, Enum, ForeignKey, Index, Integer, String, Text, UniqueConstraint
|
||||||
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
@@ -65,6 +65,8 @@ class Sprint(Base, UUIDMixin, TimestampMixin):
|
|||||||
Index("ix_sprints_project_status", "project_id", "status"),
|
Index("ix_sprints_project_status", "project_id", "status"),
|
||||||
Index("ix_sprints_project_number", "project_id", "number"),
|
Index("ix_sprints_project_number", "project_id", "number"),
|
||||||
Index("ix_sprints_date_range", "start_date", "end_date"),
|
Index("ix_sprints_date_range", "start_date", "end_date"),
|
||||||
|
# Ensure sprint numbers are unique within a project
|
||||||
|
UniqueConstraint("project_id", "number", name="uq_sprint_project_number"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
|||||||
158
frontend/src/components/common/ErrorBoundary.tsx
Normal file
158
frontend/src/components/common/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
/**
|
||||||
|
* Error Boundary Component
|
||||||
|
*
|
||||||
|
* Catches JavaScript errors in child component tree and displays fallback UI.
|
||||||
|
* Prevents the entire app from crashing due to component-level errors.
|
||||||
|
*
|
||||||
|
* @see https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Component, type ReactNode } from 'react';
|
||||||
|
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface ErrorBoundaryProps {
|
||||||
|
/** Child components to render */
|
||||||
|
children: ReactNode;
|
||||||
|
/** Optional fallback component to render on error */
|
||||||
|
fallback?: ReactNode;
|
||||||
|
/** Optional callback when error occurs */
|
||||||
|
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
||||||
|
/** Whether to show reset button (default: true) */
|
||||||
|
showReset?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorBoundaryState {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Default Fallback Component
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface DefaultFallbackProps {
|
||||||
|
error: Error | null;
|
||||||
|
onReset: () => void;
|
||||||
|
showReset: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DefaultFallback({ error, onReset, showReset }: DefaultFallbackProps) {
|
||||||
|
return (
|
||||||
|
<Card className="m-4 border-destructive/50 bg-destructive/5">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-destructive">
|
||||||
|
<AlertTriangle className="h-5 w-5" aria-hidden="true" />
|
||||||
|
Something went wrong
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
An unexpected error occurred. Please try again or contact support if
|
||||||
|
the problem persists.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 rounded-md bg-muted p-3">
|
||||||
|
<p className="font-mono text-sm text-muted-foreground">
|
||||||
|
{error.message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showReset && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={onReset}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4" aria-hidden="true" />
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Error Boundary Component
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error boundary for catching and handling render errors in React components.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* <ErrorBoundary onError={(error) => logError(error)}>
|
||||||
|
* <MyComponent />
|
||||||
|
* </ErrorBoundary>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example With custom fallback
|
||||||
|
* ```tsx
|
||||||
|
* <ErrorBoundary fallback={<CustomErrorUI />}>
|
||||||
|
* <MyComponent />
|
||||||
|
* </ErrorBoundary>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class ErrorBoundary extends Component<
|
||||||
|
ErrorBoundaryProps,
|
||||||
|
ErrorBoundaryState
|
||||||
|
> {
|
||||||
|
constructor(props: ErrorBoundaryProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false, error: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
||||||
|
// Log error to console in development
|
||||||
|
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
||||||
|
|
||||||
|
// Call optional error callback
|
||||||
|
this.props.onError?.(error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReset = (): void => {
|
||||||
|
this.setState({ hasError: false, error: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): ReactNode {
|
||||||
|
const { hasError, error } = this.state;
|
||||||
|
const { children, fallback, showReset = true } = this.props;
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
if (fallback) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaultFallback
|
||||||
|
error={error}
|
||||||
|
onReset={this.handleReset}
|
||||||
|
showReset={showReset}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Common reusable components
|
// Common reusable components
|
||||||
// Examples: LoadingSpinner, ErrorBoundary, ConfirmDialog, etc.
|
// Examples: LoadingSpinner, ErrorBoundary, ConfirmDialog, etc.
|
||||||
|
|
||||||
export {};
|
export { ErrorBoundary } from './ErrorBoundary';
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ function AgentListItem({
|
|||||||
<DropdownMenuItem onClick={() => onAction(agent.id, 'view')}>
|
<DropdownMenuItem onClick={() => onAction(agent.id, 'view')}>
|
||||||
View Details
|
View Details
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{agent.status === 'active' || agent.status === 'working' ? (
|
{agent.status === 'working' || agent.status === 'waiting' ? (
|
||||||
<DropdownMenuItem onClick={() => onAction(agent.id, 'pause')}>
|
<DropdownMenuItem onClick={() => onAction(agent.id, 'pause')}>
|
||||||
Pause Agent
|
Pause Agent
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -196,7 +196,7 @@ export function AgentPanel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const activeAgentCount = agents.filter(
|
const activeAgentCount = agents.filter(
|
||||||
(a) => a.status === 'active' || a.status === 'working'
|
(a) => a.status === 'working' || a.status === 'waiting'
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,21 +14,17 @@ const statusConfig: Record<AgentStatus, { color: string; label: string }> = {
|
|||||||
color: 'bg-yellow-500',
|
color: 'bg-yellow-500',
|
||||||
label: 'Idle',
|
label: 'Idle',
|
||||||
},
|
},
|
||||||
active: {
|
|
||||||
color: 'bg-green-500',
|
|
||||||
label: 'Active',
|
|
||||||
},
|
|
||||||
working: {
|
working: {
|
||||||
color: 'bg-green-500 animate-pulse',
|
color: 'bg-green-500 animate-pulse',
|
||||||
label: 'Working',
|
label: 'Working',
|
||||||
},
|
},
|
||||||
pending: {
|
waiting: {
|
||||||
color: 'bg-gray-400',
|
color: 'bg-blue-500',
|
||||||
label: 'Pending',
|
label: 'Waiting',
|
||||||
},
|
},
|
||||||
error: {
|
paused: {
|
||||||
color: 'bg-red-500',
|
color: 'bg-gray-400',
|
||||||
label: 'Error',
|
label: 'Paused',
|
||||||
},
|
},
|
||||||
terminated: {
|
terminated: {
|
||||||
color: 'bg-gray-600',
|
color: 'bg-gray-600',
|
||||||
@@ -49,7 +45,7 @@ export function AgentStatusIndicator({
|
|||||||
showLabel = false,
|
showLabel = false,
|
||||||
className,
|
className,
|
||||||
}: AgentStatusIndicatorProps) {
|
}: AgentStatusIndicatorProps) {
|
||||||
const config = statusConfig[status] || statusConfig.pending;
|
const config = statusConfig[status] || statusConfig.idle;
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: 'h-2 w-2',
|
sm: 'h-2 w-2',
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import type { IssueSummary as IssueSummaryType } from './types';
|
import type { IssueCountSummary } from './types';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
@@ -32,7 +32,7 @@ import type { IssueSummary as IssueSummaryType } from './types';
|
|||||||
|
|
||||||
interface IssueSummaryProps {
|
interface IssueSummaryProps {
|
||||||
/** Issue summary data */
|
/** Issue summary data */
|
||||||
summary: IssueSummaryType | null;
|
summary: IssueCountSummary | null;
|
||||||
/** Whether data is loading */
|
/** Whether data is loading */
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
/** Callback when "View All Issues" is clicked */
|
/** Callback when "View All Issues" is clicked */
|
||||||
@@ -171,8 +171,8 @@ export function IssueSummary({
|
|||||||
<StatusRow
|
<StatusRow
|
||||||
icon={CheckCircle2}
|
icon={CheckCircle2}
|
||||||
iconColor="text-green-500"
|
iconColor="text-green-500"
|
||||||
label="Completed"
|
label="Closed"
|
||||||
count={summary.done}
|
count={summary.closed}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{onViewAllIssues && (
|
{onViewAllIssues && (
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import type {
|
|||||||
AgentInstance,
|
AgentInstance,
|
||||||
Sprint,
|
Sprint,
|
||||||
BurndownDataPoint,
|
BurndownDataPoint,
|
||||||
IssueSummary as IssueSummaryType,
|
IssueCountSummary,
|
||||||
ActivityItem,
|
ActivityItem,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ const mockProject: Project = {
|
|||||||
id: 'proj-001',
|
id: 'proj-001',
|
||||||
name: 'E-Commerce Platform Redesign',
|
name: 'E-Commerce Platform Redesign',
|
||||||
description: 'Complete redesign of the e-commerce platform with modern UI/UX',
|
description: 'Complete redesign of the e-commerce platform with modern UI/UX',
|
||||||
status: 'in_progress',
|
status: 'active',
|
||||||
autonomy_level: 'milestone',
|
autonomy_level: 'milestone',
|
||||||
current_sprint_id: 'sprint-003',
|
current_sprint_id: 'sprint-003',
|
||||||
created_at: '2025-01-15T00:00:00Z',
|
created_at: '2025-01-15T00:00:00Z',
|
||||||
@@ -66,7 +66,7 @@ const mockAgents: AgentInstance[] = [
|
|||||||
project_id: 'proj-001',
|
project_id: 'proj-001',
|
||||||
name: 'Product Owner',
|
name: 'Product Owner',
|
||||||
role: 'product_owner',
|
role: 'product_owner',
|
||||||
status: 'active',
|
status: 'working',
|
||||||
current_task: 'Reviewing user story acceptance criteria',
|
current_task: 'Reviewing user story acceptance criteria',
|
||||||
last_activity_at: new Date(Date.now() - 2 * 60 * 1000).toISOString(),
|
last_activity_at: new Date(Date.now() - 2 * 60 * 1000).toISOString(),
|
||||||
spawned_at: '2025-01-15T00:00:00Z',
|
spawned_at: '2025-01-15T00:00:00Z',
|
||||||
@@ -102,7 +102,7 @@ const mockAgents: AgentInstance[] = [
|
|||||||
project_id: 'proj-001',
|
project_id: 'proj-001',
|
||||||
name: 'Frontend Engineer',
|
name: 'Frontend Engineer',
|
||||||
role: 'frontend_engineer',
|
role: 'frontend_engineer',
|
||||||
status: 'active',
|
status: 'working',
|
||||||
current_task: 'Implementing product catalog component',
|
current_task: 'Implementing product catalog component',
|
||||||
last_activity_at: new Date(Date.now() - 1 * 60 * 1000).toISOString(),
|
last_activity_at: new Date(Date.now() - 1 * 60 * 1000).toISOString(),
|
||||||
spawned_at: '2025-01-15T00:00:00Z',
|
spawned_at: '2025-01-15T00:00:00Z',
|
||||||
@@ -114,7 +114,7 @@ const mockAgents: AgentInstance[] = [
|
|||||||
project_id: 'proj-001',
|
project_id: 'proj-001',
|
||||||
name: 'QA Engineer',
|
name: 'QA Engineer',
|
||||||
role: 'qa_engineer',
|
role: 'qa_engineer',
|
||||||
status: 'pending',
|
status: 'waiting',
|
||||||
current_task: 'Preparing test cases for Sprint 3',
|
current_task: 'Preparing test cases for Sprint 3',
|
||||||
last_activity_at: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
|
last_activity_at: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
|
||||||
spawned_at: '2025-01-15T00:00:00Z',
|
spawned_at: '2025-01-15T00:00:00Z',
|
||||||
@@ -148,12 +148,12 @@ const mockBurndownData: BurndownDataPoint[] = [
|
|||||||
{ day: 8, remaining: 20, ideal: 24 },
|
{ day: 8, remaining: 20, ideal: 24 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const mockIssueSummary: IssueSummaryType = {
|
const mockIssueSummary: IssueCountSummary = {
|
||||||
open: 12,
|
open: 12,
|
||||||
in_progress: 8,
|
in_progress: 8,
|
||||||
in_review: 3,
|
in_review: 3,
|
||||||
blocked: 2,
|
blocked: 2,
|
||||||
done: 45,
|
closed: 45,
|
||||||
total: 70,
|
total: 70,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -392,7 +392,7 @@ export function ProjectDashboard({ projectId, className }: ProjectDashboardProps
|
|||||||
<ProjectHeader
|
<ProjectHeader
|
||||||
project={project}
|
project={project}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
canPause={project.status === 'in_progress'}
|
canPause={project.status === 'active'}
|
||||||
canStart={true}
|
canStart={true}
|
||||||
onStartSprint={handleStartSprint}
|
onStartSprint={handleStartSprint}
|
||||||
onPauseProject={handlePauseProject}
|
onPauseProject={handlePauseProject}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export function ProjectHeader({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showPauseButton = canPause && project.status === 'in_progress';
|
const showPauseButton = canPause && project.status === 'active';
|
||||||
const showStartButton = canStart && project.status !== 'completed' && project.status !== 'archived';
|
const showStartButton = canStart && project.status !== 'completed' && project.status !== 'archived';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -16,12 +16,8 @@ import type { ProjectStatus, AutonomyLevel } from './types';
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const projectStatusConfig: Record<ProjectStatus, { label: string; className: string }> = {
|
const projectStatusConfig: Record<ProjectStatus, { label: string; className: string }> = {
|
||||||
draft: {
|
active: {
|
||||||
label: 'Draft',
|
label: 'Active',
|
||||||
className: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200',
|
|
||||||
},
|
|
||||||
in_progress: {
|
|
||||||
label: 'In Progress',
|
|
||||||
className: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
className: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||||||
},
|
},
|
||||||
paused: {
|
paused: {
|
||||||
@@ -32,10 +28,6 @@ const projectStatusConfig: Record<ProjectStatus, { label: string; className: str
|
|||||||
label: 'Completed',
|
label: 'Completed',
|
||||||
className: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
className: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||||
},
|
},
|
||||||
blocked: {
|
|
||||||
label: 'Blocked',
|
|
||||||
className: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
|
||||||
},
|
|
||||||
archived: {
|
archived: {
|
||||||
label: 'Archived',
|
label: 'Archived',
|
||||||
className: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400',
|
className: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400',
|
||||||
@@ -48,7 +40,7 @@ interface ProjectStatusBadgeProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ProjectStatusBadge({ status, className }: ProjectStatusBadgeProps) {
|
export function ProjectStatusBadge({ status, className }: ProjectStatusBadgeProps) {
|
||||||
const config = projectStatusConfig[status] || projectStatusConfig.draft;
|
const config = projectStatusConfig[status] || projectStatusConfig.active;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge variant="outline" className={cn(config.className, className)}>
|
<Badge variant="outline" className={cn(config.className, className)}>
|
||||||
|
|||||||
@@ -11,7 +11,10 @@
|
|||||||
// Project Types
|
// Project Types
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export type ProjectStatus = 'draft' | 'in_progress' | 'paused' | 'completed' | 'blocked' | 'archived';
|
/**
|
||||||
|
* Matches backend: ProjectStatus enum in app/models/syndarix/enums.py
|
||||||
|
*/
|
||||||
|
export type ProjectStatus = 'active' | 'paused' | 'completed' | 'archived';
|
||||||
|
|
||||||
export type AutonomyLevel = 'full_control' | 'milestone' | 'autonomous';
|
export type AutonomyLevel = 'full_control' | 'milestone' | 'autonomous';
|
||||||
|
|
||||||
@@ -31,7 +34,10 @@ export interface Project {
|
|||||||
// Agent Types
|
// Agent Types
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export type AgentStatus = 'idle' | 'active' | 'working' | 'pending' | 'error' | 'terminated';
|
/**
|
||||||
|
* Matches backend: AgentStatus enum in app/models/syndarix/enums.py
|
||||||
|
*/
|
||||||
|
export type AgentStatus = 'idle' | 'working' | 'waiting' | 'paused' | 'terminated';
|
||||||
|
|
||||||
export interface AgentInstance {
|
export interface AgentInstance {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -50,7 +56,10 @@ export interface AgentInstance {
|
|||||||
// Sprint Types
|
// Sprint Types
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export type SprintStatus = 'planning' | 'active' | 'review' | 'completed';
|
/**
|
||||||
|
* Matches backend: SprintStatus enum in app/models/syndarix/enums.py
|
||||||
|
*/
|
||||||
|
export type SprintStatus = 'planned' | 'active' | 'in_review' | 'completed' | 'cancelled';
|
||||||
|
|
||||||
export interface Sprint {
|
export interface Sprint {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -78,7 +87,10 @@ export interface BurndownDataPoint {
|
|||||||
// Issue Types
|
// Issue Types
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export type IssueStatus = 'open' | 'in_progress' | 'in_review' | 'blocked' | 'done' | 'closed';
|
/**
|
||||||
|
* Matches backend: IssueStatus enum in app/models/syndarix/enums.py
|
||||||
|
*/
|
||||||
|
export type IssueStatus = 'open' | 'in_progress' | 'in_review' | 'blocked' | 'closed';
|
||||||
|
|
||||||
export type IssuePriority = 'low' | 'medium' | 'high' | 'critical';
|
export type IssuePriority = 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
|
||||||
@@ -96,12 +108,12 @@ export interface Issue {
|
|||||||
labels?: string[];
|
labels?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IssueSummary {
|
export interface IssueCountSummary {
|
||||||
open: number;
|
open: number;
|
||||||
in_progress: number;
|
in_progress: number;
|
||||||
in_review: number;
|
in_review: number;
|
||||||
blocked: number;
|
blocked: number;
|
||||||
done: number;
|
closed: number;
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ const STATUS_ICONS = {
|
|||||||
in_progress: PlayCircle,
|
in_progress: PlayCircle,
|
||||||
in_review: Clock,
|
in_review: Clock,
|
||||||
blocked: AlertCircle,
|
blocked: AlertCircle,
|
||||||
done: CheckCircle2,
|
|
||||||
closed: XCircle,
|
closed: XCircle,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -22,14 +22,17 @@ export const STATUS_CONFIG: Record<IssueStatus, StatusConfig> = {
|
|||||||
in_progress: { label: 'In Progress', color: 'text-yellow-500' },
|
in_progress: { label: 'In Progress', color: 'text-yellow-500' },
|
||||||
in_review: { label: 'In Review', color: 'text-purple-500' },
|
in_review: { label: 'In Review', color: 'text-purple-500' },
|
||||||
blocked: { label: 'Blocked', color: 'text-red-500' },
|
blocked: { label: 'Blocked', color: 'text-red-500' },
|
||||||
done: { label: 'Done', color: 'text-green-500' },
|
closed: { label: 'Closed', color: 'text-green-500' },
|
||||||
closed: { label: 'Closed', color: 'text-muted-foreground' },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Priority configuration with labels and colors
|
* Priority configuration with labels and colors
|
||||||
*/
|
*/
|
||||||
export const PRIORITY_CONFIG: Record<IssuePriority, PriorityConfig> = {
|
export const PRIORITY_CONFIG: Record<IssuePriority, PriorityConfig> = {
|
||||||
|
critical: {
|
||||||
|
label: 'Critical',
|
||||||
|
color: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
|
||||||
|
},
|
||||||
high: { label: 'High', color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' },
|
high: { label: 'High', color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' },
|
||||||
medium: {
|
medium: {
|
||||||
label: 'Medium',
|
label: 'Medium',
|
||||||
@@ -46,10 +49,9 @@ export const STATUS_TRANSITIONS: StatusTransition[] = [
|
|||||||
{ from: 'open', to: 'in_progress', label: 'Start Work' },
|
{ from: 'open', to: 'in_progress', label: 'Start Work' },
|
||||||
{ from: 'in_progress', to: 'in_review', label: 'Submit for Review' },
|
{ from: 'in_progress', to: 'in_review', label: 'Submit for Review' },
|
||||||
{ from: 'in_progress', to: 'blocked', label: 'Mark Blocked' },
|
{ from: 'in_progress', to: 'blocked', label: 'Mark Blocked' },
|
||||||
{ from: 'in_review', to: 'done', label: 'Mark Done' },
|
{ from: 'in_review', to: 'closed', label: 'Complete' },
|
||||||
{ from: 'in_review', to: 'in_progress', label: 'Request Changes' },
|
{ from: 'in_review', to: 'in_progress', label: 'Request Changes' },
|
||||||
{ from: 'blocked', to: 'in_progress', label: 'Unblock' },
|
{ from: 'blocked', to: 'in_progress', label: 'Unblock' },
|
||||||
{ from: 'done', to: 'closed', label: 'Close Issue' },
|
|
||||||
{ from: 'closed', to: 'open', label: 'Reopen' },
|
{ from: 'closed', to: 'open', label: 'Reopen' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -76,14 +78,13 @@ export const STATUS_ORDER: IssueStatus[] = [
|
|||||||
'in_progress',
|
'in_progress',
|
||||||
'in_review',
|
'in_review',
|
||||||
'blocked',
|
'blocked',
|
||||||
'done',
|
|
||||||
'closed',
|
'closed',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All possible priorities in order
|
* All possible priorities in order
|
||||||
*/
|
*/
|
||||||
export const PRIORITY_ORDER: IssuePriority[] = ['high', 'medium', 'low'];
|
export const PRIORITY_ORDER: IssuePriority[] = ['critical', 'high', 'medium', 'low'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync status configuration
|
* Sync status configuration
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ function filterAndSortIssues(
|
|||||||
case 'number':
|
case 'number':
|
||||||
return (a.number - b.number) * direction;
|
return (a.number - b.number) * direction;
|
||||||
case 'priority': {
|
case 'priority': {
|
||||||
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
const priorityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
|
||||||
return (priorityOrder[a.priority] - priorityOrder[b.priority]) * direction;
|
return (priorityOrder[a.priority] - priorityOrder[b.priority]) * direction;
|
||||||
}
|
}
|
||||||
case 'updated_at':
|
case 'updated_at':
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const mockIssues: IssueSummary[] = [
|
|||||||
{
|
{
|
||||||
id: 'ISS-001',
|
id: 'ISS-001',
|
||||||
number: 42,
|
number: 42,
|
||||||
|
type: 'story',
|
||||||
title: 'Implement user authentication flow',
|
title: 'Implement user authentication flow',
|
||||||
description:
|
description:
|
||||||
'Create complete authentication flow with login, register, and password reset.',
|
'Create complete authentication flow with login, register, and password reset.',
|
||||||
@@ -31,6 +32,7 @@ export const mockIssues: IssueSummary[] = [
|
|||||||
{
|
{
|
||||||
id: 'ISS-002',
|
id: 'ISS-002',
|
||||||
number: 43,
|
number: 43,
|
||||||
|
type: 'task',
|
||||||
title: 'Design product catalog component',
|
title: 'Design product catalog component',
|
||||||
description: 'Create reusable product card and catalog grid components.',
|
description: 'Create reusable product card and catalog grid components.',
|
||||||
status: 'in_review',
|
status: 'in_review',
|
||||||
@@ -45,6 +47,7 @@ export const mockIssues: IssueSummary[] = [
|
|||||||
{
|
{
|
||||||
id: 'ISS-003',
|
id: 'ISS-003',
|
||||||
number: 44,
|
number: 44,
|
||||||
|
type: 'bug',
|
||||||
title: 'Fix cart total calculation bug',
|
title: 'Fix cart total calculation bug',
|
||||||
description: 'Cart total shows incorrect amount when discount is applied.',
|
description: 'Cart total shows incorrect amount when discount is applied.',
|
||||||
status: 'blocked',
|
status: 'blocked',
|
||||||
@@ -60,6 +63,7 @@ export const mockIssues: IssueSummary[] = [
|
|||||||
{
|
{
|
||||||
id: 'ISS-004',
|
id: 'ISS-004',
|
||||||
number: 45,
|
number: 45,
|
||||||
|
type: 'story',
|
||||||
title: 'Add product search functionality',
|
title: 'Add product search functionality',
|
||||||
description: 'Implement full-text search with filters for the product catalog.',
|
description: 'Implement full-text search with filters for the product catalog.',
|
||||||
status: 'open',
|
status: 'open',
|
||||||
@@ -74,9 +78,10 @@ export const mockIssues: IssueSummary[] = [
|
|||||||
{
|
{
|
||||||
id: 'ISS-005',
|
id: 'ISS-005',
|
||||||
number: 46,
|
number: 46,
|
||||||
|
type: 'task',
|
||||||
title: 'Optimize database queries for product listing',
|
title: 'Optimize database queries for product listing',
|
||||||
description: 'Performance optimization for product queries with pagination.',
|
description: 'Performance optimization for product queries with pagination.',
|
||||||
status: 'done',
|
status: 'closed',
|
||||||
priority: 'low',
|
priority: 'low',
|
||||||
labels: ['performance', 'backend', 'database'],
|
labels: ['performance', 'backend', 'database'],
|
||||||
sprint: 'Sprint 2',
|
sprint: 'Sprint 2',
|
||||||
@@ -88,9 +93,10 @@ export const mockIssues: IssueSummary[] = [
|
|||||||
{
|
{
|
||||||
id: 'ISS-006',
|
id: 'ISS-006',
|
||||||
number: 47,
|
number: 47,
|
||||||
|
type: 'task',
|
||||||
title: 'Create checkout page wireframes',
|
title: 'Create checkout page wireframes',
|
||||||
description: 'Design wireframes for the checkout flow including payment selection.',
|
description: 'Design wireframes for the checkout flow including payment selection.',
|
||||||
status: 'done',
|
status: 'closed',
|
||||||
priority: 'high',
|
priority: 'high',
|
||||||
labels: ['design', 'checkout', 'ui'],
|
labels: ['design', 'checkout', 'ui'],
|
||||||
sprint: 'Sprint 2',
|
sprint: 'Sprint 2',
|
||||||
@@ -102,6 +108,7 @@ export const mockIssues: IssueSummary[] = [
|
|||||||
{
|
{
|
||||||
id: 'ISS-007',
|
id: 'ISS-007',
|
||||||
number: 48,
|
number: 48,
|
||||||
|
type: 'story',
|
||||||
title: 'Implement responsive navigation',
|
title: 'Implement responsive navigation',
|
||||||
description: 'Create mobile-friendly navigation with hamburger menu.',
|
description: 'Create mobile-friendly navigation with hamburger menu.',
|
||||||
status: 'open',
|
status: 'open',
|
||||||
@@ -116,6 +123,7 @@ export const mockIssues: IssueSummary[] = [
|
|||||||
{
|
{
|
||||||
id: 'ISS-008',
|
id: 'ISS-008',
|
||||||
number: 49,
|
number: 49,
|
||||||
|
type: 'task',
|
||||||
title: 'Set up E2E test framework',
|
title: 'Set up E2E test framework',
|
||||||
description: 'Configure Playwright for end-to-end testing.',
|
description: 'Configure Playwright for end-to-end testing.',
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
@@ -135,6 +143,7 @@ export const mockIssues: IssueSummary[] = [
|
|||||||
export const mockIssueDetail: IssueDetail = {
|
export const mockIssueDetail: IssueDetail = {
|
||||||
id: 'ISS-001',
|
id: 'ISS-001',
|
||||||
number: 42,
|
number: 42,
|
||||||
|
type: 'story',
|
||||||
title: 'Implement user authentication flow',
|
title: 'Implement user authentication flow',
|
||||||
description: `## Overview
|
description: `## Overview
|
||||||
Create a complete authentication flow for the e-commerce platform.
|
Create a complete authentication flow for the e-commerce platform.
|
||||||
|
|||||||
@@ -8,14 +8,22 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Issue status values
|
* Issue type values (for hierarchy: Epic -> Story -> Task)
|
||||||
|
* Matches backend: IssueType enum in app/models/syndarix/enums.py
|
||||||
*/
|
*/
|
||||||
export type IssueStatus = 'open' | 'in_progress' | 'in_review' | 'blocked' | 'done' | 'closed';
|
export type IssueType = 'epic' | 'story' | 'task' | 'bug';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Issue status values
|
||||||
|
* Matches backend: IssueStatus enum in app/models/syndarix/enums.py
|
||||||
|
*/
|
||||||
|
export type IssueStatus = 'open' | 'in_progress' | 'in_review' | 'blocked' | 'closed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Issue priority values
|
* Issue priority values
|
||||||
|
* Matches backend: IssuePriority enum in app/models/syndarix/enums.py
|
||||||
*/
|
*/
|
||||||
export type IssuePriority = 'high' | 'medium' | 'low';
|
export type IssuePriority = 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync status with external trackers
|
* Sync status with external trackers
|
||||||
@@ -64,6 +72,7 @@ export interface IssueActivity {
|
|||||||
export interface IssueSummary {
|
export interface IssueSummary {
|
||||||
id: string;
|
id: string;
|
||||||
number: number;
|
number: number;
|
||||||
|
type: IssueType;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
status: IssueStatus;
|
status: IssueStatus;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const mockAgents: AgentInstance[] = [
|
|||||||
project_id: 'proj-001',
|
project_id: 'proj-001',
|
||||||
name: 'Product Owner',
|
name: 'Product Owner',
|
||||||
role: 'product_owner',
|
role: 'product_owner',
|
||||||
status: 'active',
|
status: 'working',
|
||||||
current_task: 'Reviewing user stories',
|
current_task: 'Reviewing user stories',
|
||||||
last_activity_at: new Date().toISOString(),
|
last_activity_at: new Date().toISOString(),
|
||||||
spawned_at: '2025-01-15T00:00:00Z',
|
spawned_at: '2025-01-15T00:00:00Z',
|
||||||
|
|||||||
@@ -8,12 +8,6 @@ describe('AgentStatusIndicator', () => {
|
|||||||
expect(indicator).toHaveClass('bg-yellow-500');
|
expect(indicator).toHaveClass('bg-yellow-500');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders active status with correct color', () => {
|
|
||||||
const { container } = render(<AgentStatusIndicator status="active" />);
|
|
||||||
const indicator = container.querySelector('span > span');
|
|
||||||
expect(indicator).toHaveClass('bg-green-500');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders working status with animation', () => {
|
it('renders working status with animation', () => {
|
||||||
const { container } = render(<AgentStatusIndicator status="working" />);
|
const { container } = render(<AgentStatusIndicator status="working" />);
|
||||||
const indicator = container.querySelector('span > span');
|
const indicator = container.querySelector('span > span');
|
||||||
@@ -21,16 +15,16 @@ describe('AgentStatusIndicator', () => {
|
|||||||
expect(indicator).toHaveClass('animate-pulse');
|
expect(indicator).toHaveClass('animate-pulse');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders pending status with correct color', () => {
|
it('renders waiting status with correct color', () => {
|
||||||
const { container } = render(<AgentStatusIndicator status="pending" />);
|
const { container } = render(<AgentStatusIndicator status="waiting" />);
|
||||||
const indicator = container.querySelector('span > span');
|
const indicator = container.querySelector('span > span');
|
||||||
expect(indicator).toHaveClass('bg-gray-400');
|
expect(indicator).toHaveClass('bg-blue-500');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders error status with correct color', () => {
|
it('renders paused status with correct color', () => {
|
||||||
const { container } = render(<AgentStatusIndicator status="error" />);
|
const { container } = render(<AgentStatusIndicator status="paused" />);
|
||||||
const indicator = container.querySelector('span > span');
|
const indicator = container.querySelector('span > span');
|
||||||
expect(indicator).toHaveClass('bg-red-500');
|
expect(indicator).toHaveClass('bg-gray-400');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders terminated status with correct color', () => {
|
it('renders terminated status with correct color', () => {
|
||||||
@@ -40,41 +34,41 @@ describe('AgentStatusIndicator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('applies small size by default', () => {
|
it('applies small size by default', () => {
|
||||||
const { container } = render(<AgentStatusIndicator status="active" />);
|
const { container } = render(<AgentStatusIndicator status="working" />);
|
||||||
const indicator = container.querySelector('span > span');
|
const indicator = container.querySelector('span > span');
|
||||||
expect(indicator).toHaveClass('h-2', 'w-2');
|
expect(indicator).toHaveClass('h-2', 'w-2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies medium size when specified', () => {
|
it('applies medium size when specified', () => {
|
||||||
const { container } = render(<AgentStatusIndicator status="active" size="md" />);
|
const { container } = render(<AgentStatusIndicator status="working" size="md" />);
|
||||||
const indicator = container.querySelector('span > span');
|
const indicator = container.querySelector('span > span');
|
||||||
expect(indicator).toHaveClass('h-3', 'w-3');
|
expect(indicator).toHaveClass('h-3', 'w-3');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies large size when specified', () => {
|
it('applies large size when specified', () => {
|
||||||
const { container } = render(<AgentStatusIndicator status="active" size="lg" />);
|
const { container } = render(<AgentStatusIndicator status="working" size="lg" />);
|
||||||
const indicator = container.querySelector('span > span');
|
const indicator = container.querySelector('span > span');
|
||||||
expect(indicator).toHaveClass('h-4', 'w-4');
|
expect(indicator).toHaveClass('h-4', 'w-4');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows label when showLabel is true', () => {
|
it('shows label when showLabel is true', () => {
|
||||||
render(<AgentStatusIndicator status="active" showLabel />);
|
render(<AgentStatusIndicator status="working" showLabel />);
|
||||||
expect(screen.getByText('Active')).toBeInTheDocument();
|
expect(screen.getByText('Working')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not show label by default', () => {
|
it('does not show label by default', () => {
|
||||||
render(<AgentStatusIndicator status="active" />);
|
render(<AgentStatusIndicator status="working" />);
|
||||||
expect(screen.queryByText('Active')).not.toBeInTheDocument();
|
expect(screen.queryByText('Working')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has accessible status role and label', () => {
|
it('has accessible status role and label', () => {
|
||||||
render(<AgentStatusIndicator status="active" />);
|
render(<AgentStatusIndicator status="working" />);
|
||||||
expect(screen.getByRole('status')).toHaveAttribute('aria-label', 'Status: Active');
|
expect(screen.getByRole('status')).toHaveAttribute('aria-label', 'Status: Working');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies custom className', () => {
|
it('applies custom className', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<AgentStatusIndicator status="active" className="custom-class" />
|
<AgentStatusIndicator status="working" className="custom-class" />
|
||||||
);
|
);
|
||||||
expect(container.firstChild).toHaveClass('custom-class');
|
expect(container.firstChild).toHaveClass('custom-class');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { IssueSummary } from '@/components/projects/IssueSummary';
|
import { IssueSummary } from '@/components/projects/IssueSummary';
|
||||||
import type { IssueSummary as IssueSummaryType } from '@/components/projects/types';
|
import type { IssueCountSummary } from '@/components/projects/types';
|
||||||
|
|
||||||
const mockSummary: IssueSummaryType = {
|
const mockSummary: IssueCountSummary = {
|
||||||
open: 12,
|
open: 12,
|
||||||
in_progress: 8,
|
in_progress: 8,
|
||||||
in_review: 3,
|
in_review: 3,
|
||||||
blocked: 2,
|
blocked: 2,
|
||||||
done: 45,
|
closed: 45,
|
||||||
total: 70,
|
total: 70,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ describe('IssueSummary', () => {
|
|||||||
expect(screen.getByText('Blocked')).toBeInTheDocument();
|
expect(screen.getByText('Blocked')).toBeInTheDocument();
|
||||||
expect(screen.getByText('2')).toBeInTheDocument();
|
expect(screen.getByText('2')).toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.getByText('Completed')).toBeInTheDocument();
|
expect(screen.getByText('Closed')).toBeInTheDocument();
|
||||||
expect(screen.getByText('45')).toBeInTheDocument();
|
expect(screen.getByText('45')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const mockProject: Project = {
|
|||||||
id: 'proj-001',
|
id: 'proj-001',
|
||||||
name: 'Test Project',
|
name: 'Test Project',
|
||||||
description: 'A test project for unit testing',
|
description: 'A test project for unit testing',
|
||||||
status: 'in_progress',
|
status: 'active',
|
||||||
autonomy_level: 'milestone',
|
autonomy_level: 'milestone',
|
||||||
created_at: '2025-01-15T00:00:00Z',
|
created_at: '2025-01-15T00:00:00Z',
|
||||||
owner_id: 'user-001',
|
owner_id: 'user-001',
|
||||||
@@ -26,7 +26,7 @@ describe('ProjectHeader', () => {
|
|||||||
|
|
||||||
it('renders project status badge', () => {
|
it('renders project status badge', () => {
|
||||||
render(<ProjectHeader project={mockProject} />);
|
render(<ProjectHeader project={mockProject} />);
|
||||||
expect(screen.getByText('In Progress')).toBeInTheDocument();
|
expect(screen.getByText('Active')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders autonomy level badge', () => {
|
it('renders autonomy level badge', () => {
|
||||||
@@ -44,7 +44,7 @@ describe('ProjectHeader', () => {
|
|||||||
expect(container.querySelectorAll('.animate-pulse').length).toBeGreaterThan(0);
|
expect(container.querySelectorAll('.animate-pulse').length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows pause button when canPause is true and project is in_progress', () => {
|
it('shows pause button when canPause is true and project is active', () => {
|
||||||
const onPauseProject = jest.fn();
|
const onPauseProject = jest.fn();
|
||||||
render(
|
render(
|
||||||
<ProjectHeader
|
<ProjectHeader
|
||||||
@@ -56,7 +56,7 @@ describe('ProjectHeader', () => {
|
|||||||
expect(screen.getByRole('button', { name: /pause project/i })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /pause project/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not show pause button when project is not in_progress', () => {
|
it('does not show pause button when project is not active', () => {
|
||||||
const completedProject = { ...mockProject, status: 'completed' as const };
|
const completedProject = { ...mockProject, status: 'completed' as const };
|
||||||
render(<ProjectHeader project={completedProject} canPause={true} />);
|
render(<ProjectHeader project={completedProject} canPause={true} />);
|
||||||
expect(screen.queryByRole('button', { name: /pause project/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: /pause project/i })).not.toBeInTheDocument();
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { render, screen } from '@testing-library/react';
|
|||||||
import { ProjectStatusBadge, AutonomyBadge } from '@/components/projects/StatusBadge';
|
import { ProjectStatusBadge, AutonomyBadge } from '@/components/projects/StatusBadge';
|
||||||
|
|
||||||
describe('ProjectStatusBadge', () => {
|
describe('ProjectStatusBadge', () => {
|
||||||
it('renders in_progress status correctly', () => {
|
it('renders active status correctly', () => {
|
||||||
render(<ProjectStatusBadge status="in_progress" />);
|
render(<ProjectStatusBadge status="active" />);
|
||||||
expect(screen.getByText('In Progress')).toBeInTheDocument();
|
expect(screen.getByText('Active')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders completed status correctly', () => {
|
it('renders completed status correctly', () => {
|
||||||
@@ -12,21 +12,11 @@ describe('ProjectStatusBadge', () => {
|
|||||||
expect(screen.getByText('Completed')).toBeInTheDocument();
|
expect(screen.getByText('Completed')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders blocked status correctly', () => {
|
|
||||||
render(<ProjectStatusBadge status="blocked" />);
|
|
||||||
expect(screen.getByText('Blocked')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders paused status correctly', () => {
|
it('renders paused status correctly', () => {
|
||||||
render(<ProjectStatusBadge status="paused" />);
|
render(<ProjectStatusBadge status="paused" />);
|
||||||
expect(screen.getByText('Paused')).toBeInTheDocument();
|
expect(screen.getByText('Paused')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders draft status correctly', () => {
|
|
||||||
render(<ProjectStatusBadge status="draft" />);
|
|
||||||
expect(screen.getByText('Draft')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders archived status correctly', () => {
|
it('renders archived status correctly', () => {
|
||||||
render(<ProjectStatusBadge status="archived" />);
|
render(<ProjectStatusBadge status="archived" />);
|
||||||
expect(screen.getByText('Archived')).toBeInTheDocument();
|
expect(screen.getByText('Archived')).toBeInTheDocument();
|
||||||
@@ -34,7 +24,7 @@ describe('ProjectStatusBadge', () => {
|
|||||||
|
|
||||||
it('applies custom className', () => {
|
it('applies custom className', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<ProjectStatusBadge status="in_progress" className="custom-class" />
|
<ProjectStatusBadge status="active" className="custom-class" />
|
||||||
);
|
);
|
||||||
expect(container.firstChild).toHaveClass('custom-class');
|
expect(container.firstChild).toHaveClass('custom-class');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const mockIssues: IssueSummary[] = [
|
|||||||
{
|
{
|
||||||
id: 'issue-1',
|
id: 'issue-1',
|
||||||
number: 42,
|
number: 42,
|
||||||
|
type: 'bug',
|
||||||
title: 'Test Issue 1',
|
title: 'Test Issue 1',
|
||||||
description: 'Description 1',
|
description: 'Description 1',
|
||||||
status: 'open',
|
status: 'open',
|
||||||
@@ -25,6 +26,7 @@ const mockIssues: IssueSummary[] = [
|
|||||||
{
|
{
|
||||||
id: 'issue-2',
|
id: 'issue-2',
|
||||||
number: 43,
|
number: 43,
|
||||||
|
type: 'story',
|
||||||
title: 'Test Issue 2',
|
title: 'Test Issue 2',
|
||||||
description: 'Description 2',
|
description: 'Description 2',
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
|
|||||||
@@ -11,12 +11,11 @@ const statusLabels: Record<IssueStatus, string> = {
|
|||||||
in_progress: 'In Progress',
|
in_progress: 'In Progress',
|
||||||
in_review: 'In Review',
|
in_review: 'In Review',
|
||||||
blocked: 'Blocked',
|
blocked: 'Blocked',
|
||||||
done: 'Done',
|
|
||||||
closed: 'Closed',
|
closed: 'Closed',
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('StatusBadge', () => {
|
describe('StatusBadge', () => {
|
||||||
const statuses: IssueStatus[] = ['open', 'in_progress', 'in_review', 'blocked', 'done', 'closed'];
|
const statuses: IssueStatus[] = ['open', 'in_progress', 'in_review', 'blocked', 'closed'];
|
||||||
|
|
||||||
it.each(statuses)('renders %s status correctly', (status) => {
|
it.each(statuses)('renders %s status correctly', (status) => {
|
||||||
render(<StatusBadge status={status} />);
|
render(<StatusBadge status={status} />);
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ describe('StatusWorkflow', () => {
|
|||||||
expect(screen.getByText('In Progress')).toBeInTheDocument();
|
expect(screen.getByText('In Progress')).toBeInTheDocument();
|
||||||
expect(screen.getByText('In Review')).toBeInTheDocument();
|
expect(screen.getByText('In Review')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Blocked')).toBeInTheDocument();
|
expect(screen.getByText('Blocked')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Done')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Closed')).toBeInTheDocument();
|
expect(screen.getByText('Closed')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user