feat: Add comprehensive tests for contentPattern and glob pattern matching

- Implemented final verification tests for contentPattern to validate behavior with empty strings, dangerous patterns, and normal patterns.
- Created glob pattern matching tests to verify regex conversion and matching functionality.
- Developed infinite loop risk tests using Worker threads to isolate potential blocking operations.
- Introduced optimized contentPattern tests to validate improvements in the findMatches function.
- Added verification tests to assess the effectiveness of contentPattern optimizations.
- Conducted safety tests for contentPattern to identify edge cases and potential vulnerabilities.
- Implemented unrestricted loop tests to analyze infinite loop risks without match limits.
- Developed tests for zero-width pattern detection logic to ensure proper handling of dangerous regex patterns.
This commit is contained in:
catlog22
2026-02-09 11:13:01 +08:00
parent dfe153778c
commit 964292ebdb
62 changed files with 7588 additions and 374 deletions

View File

@@ -26,6 +26,7 @@ import {
Layers,
Wrench,
Cog,
Users,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button';
@@ -80,6 +81,7 @@ const navGroupDefinitions: NavGroupDef[] = [
{ path: '/orchestrator', labelKey: 'navigation.main.orchestrator', icon: Workflow },
{ path: '/history', labelKey: 'navigation.main.history', icon: Clock },
{ path: '/issues', labelKey: 'navigation.main.issues', icon: AlertCircle },
{ path: '/teams', labelKey: 'navigation.main.teams', icon: Users },
],
},
{

View File

@@ -14,8 +14,10 @@ import {
Shield,
Database,
FileText,
Files,
HardDrive,
MessageCircleQuestion,
MessagesSquare,
SearchCode,
ChevronDown,
ChevronRight,
@@ -93,10 +95,12 @@ export interface CcwToolsMcpCardProps {
export const CCW_MCP_TOOLS: CcwTool[] = [
{ name: 'write_file', desc: 'Write/create files', core: true },
{ name: 'edit_file', desc: 'Edit/replace content', core: true },
{ name: 'read_file', desc: 'Read file contents', core: true },
{ name: 'read_file', desc: 'Read single file', core: true },
{ name: 'read_many_files', desc: 'Read multiple files/dirs', core: true },
{ name: 'core_memory', desc: 'Core memory management', core: true },
{ name: 'ask_question', desc: 'Interactive questions (A2UI)', core: false },
{ name: 'smart_search', desc: 'Intelligent code search', core: true },
{ name: 'team_msg', desc: 'Agent team message bus', core: false },
];
// ========== Component ==========
@@ -507,12 +511,16 @@ function getToolIcon(toolName: string): React.ReactElement {
return <Check {...iconProps} />;
case 'read_file':
return <Database {...iconProps} />;
case 'read_many_files':
return <Files {...iconProps} />;
case 'core_memory':
return <Settings {...iconProps} />;
case 'ask_question':
return <MessageCircleQuestion {...iconProps} />;
case 'smart_search':
return <SearchCode {...iconProps} />;
case 'team_msg':
return <MessagesSquare {...iconProps} />;
default:
return <Settings {...iconProps} />;
}

View File

@@ -0,0 +1,35 @@
// ========================================
// TeamEmptyState Component
// ========================================
// Empty state displayed when no teams are available
import { useIntl } from 'react-intl';
import { Users } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/Card';
export function TeamEmptyState() {
const { formatMessage } = useIntl();
return (
<div className="flex items-center justify-center min-h-[60vh]">
<Card className="max-w-md w-full">
<CardContent className="flex flex-col items-center gap-4 py-12">
<div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center">
<Users className="w-8 h-8 text-muted-foreground" />
</div>
<div className="text-center space-y-2">
<h3 className="text-lg font-semibold">
{formatMessage({ id: 'team.empty.title' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'team.empty.description' })}
</p>
</div>
<code className="px-3 py-1.5 bg-muted rounded text-xs font-mono">
/team:coordinate
</code>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,94 @@
// ========================================
// TeamHeader Component
// ========================================
// Team selector, stats chips, and controls
import { useIntl } from 'react-intl';
import { Users, MessageSquare, Clock, RefreshCw } from 'lucide-react';
import { Badge } from '@/components/ui/Badge';
import { Switch } from '@/components/ui/Switch';
import { Label } from '@/components/ui/Label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/Select';
import type { TeamSummary, TeamMember } from '@/types/team';
interface TeamHeaderProps {
teams: TeamSummary[];
selectedTeam: string | null;
onSelectTeam: (name: string | null) => void;
members: TeamMember[];
totalMessages: number;
autoRefresh: boolean;
onToggleAutoRefresh: () => void;
}
export function TeamHeader({
teams,
selectedTeam,
onSelectTeam,
members,
totalMessages,
autoRefresh,
onToggleAutoRefresh,
}: TeamHeaderProps) {
const { formatMessage } = useIntl();
return (
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3 flex-wrap">
{/* Team Selector */}
<Select
value={selectedTeam ?? ''}
onValueChange={(v) => onSelectTeam(v || null)}
>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder={formatMessage({ id: 'team.selectTeam' })} />
</SelectTrigger>
<SelectContent>
{teams.map((t) => (
<SelectItem key={t.name} value={t.name}>
{t.name}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Stats chips */}
{selectedTeam && (
<div className="flex items-center gap-2">
<Badge variant="secondary" className="gap-1">
<Users className="w-3 h-3" />
{formatMessage({ id: 'team.members' })}: {members.length}
</Badge>
<Badge variant="secondary" className="gap-1">
<MessageSquare className="w-3 h-3" />
{formatMessage({ id: 'team.messages' })}: {totalMessages}
</Badge>
</div>
)}
</div>
{/* Controls */}
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<Switch
id="auto-refresh"
checked={autoRefresh}
onCheckedChange={onToggleAutoRefresh}
/>
<Label htmlFor="auto-refresh" className="text-sm text-muted-foreground cursor-pointer">
{formatMessage({ id: 'team.autoRefresh' })}
</Label>
{autoRefresh && (
<RefreshCw className="w-3.5 h-3.5 text-primary animate-spin" style={{ animationDuration: '3s' }} />
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,114 @@
// ========================================
// TeamMembersPanel Component
// ========================================
// Card-based member status display
import { useIntl } from 'react-intl';
import { Card, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { cn } from '@/lib/utils';
import type { TeamMember } from '@/types/team';
interface TeamMembersPanelProps {
members: TeamMember[];
}
function formatRelativeTime(isoString: string): string {
if (!isoString) return '';
const now = Date.now();
const then = new Date(isoString).getTime();
const diffMs = now - then;
if (diffMs < 0) return 'now';
const seconds = Math.floor(diffMs / 1000);
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h`;
const days = Math.floor(hours / 24);
return `${days}d`;
}
function getMemberStatus(member: TeamMember): 'active' | 'idle' {
if (!member.lastSeen) return 'idle';
const diffMs = Date.now() - new Date(member.lastSeen).getTime();
// Active if seen in last 2 minutes
return diffMs < 2 * 60 * 1000 ? 'active' : 'idle';
}
export function TeamMembersPanel({ members }: TeamMembersPanelProps) {
const { formatMessage } = useIntl();
return (
<div className="space-y-3">
<h3 className="text-sm font-medium text-muted-foreground">
{formatMessage({ id: 'team.membersPanel.title' })}
</h3>
<div className="space-y-2">
{members.map((m) => {
const status = getMemberStatus(m);
const isActive = status === 'active';
return (
<Card key={m.member} className="overflow-hidden">
<CardContent className="p-3">
<div className="flex items-start gap-3">
{/* Status indicator */}
<div className="pt-0.5">
<div
className={cn(
'w-2.5 h-2.5 rounded-full',
isActive
? 'bg-green-500 shadow-[0_0_6px_rgba(34,197,94,0.5)]'
: 'bg-muted-foreground/40'
)}
/>
</div>
<div className="flex-1 min-w-0 space-y-1">
{/* Name + status badge */}
<div className="flex items-center gap-2">
<span className="text-sm font-medium truncate">{m.member}</span>
<Badge
variant={isActive ? 'success' : 'secondary'}
className="text-[10px] px-1.5 py-0"
>
{formatMessage({ id: `team.membersPanel.${status}` })}
</Badge>
</div>
{/* Last action */}
{m.lastAction && (
<p className="text-xs text-muted-foreground truncate">
{m.lastAction}
</p>
)}
{/* Stats row */}
<div className="flex items-center gap-3 text-[10px] text-muted-foreground">
<span>
{m.messageCount} {formatMessage({ id: 'team.messages' }).toLowerCase()}
</span>
{m.lastSeen && (
<span>
{formatRelativeTime(m.lastSeen)} {formatMessage({ id: 'team.membersPanel.ago' })}
</span>
)}
</div>
</div>
</div>
</CardContent>
</Card>
);
})}
{members.length === 0 && (
<p className="text-xs text-muted-foreground text-center py-4">
{formatMessage({ id: 'team.empty.noMessages' })}
</p>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,262 @@
// ========================================
// TeamMessageFeed Component
// ========================================
// Message timeline with filtering and pagination
import { useState, useMemo } from 'react';
import { useIntl } from 'react-intl';
import { ChevronDown, ChevronUp, FileText, Filter, X } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/Select';
import { cn } from '@/lib/utils';
import type { TeamMessage, TeamMessageType, TeamMessageFilter } from '@/types/team';
interface TeamMessageFeedProps {
messages: TeamMessage[];
total: number;
filter: TeamMessageFilter;
onFilterChange: (filter: Partial<TeamMessageFilter>) => void;
onClearFilter: () => void;
expanded: boolean;
onExpandedChange: (expanded: boolean) => void;
}
// Message type → color mapping
const typeColorMap: Record<string, string> = {
plan_ready: 'bg-blue-500/15 text-blue-600 dark:text-blue-400 border-blue-500/30',
plan_approved: 'bg-blue-500/15 text-blue-600 dark:text-blue-400 border-blue-500/30',
plan_revision: 'bg-amber-500/15 text-amber-600 dark:text-amber-400 border-amber-500/30',
task_unblocked: 'bg-cyan-500/15 text-cyan-600 dark:text-cyan-400 border-cyan-500/30',
impl_complete: 'bg-primary/15 text-primary border-primary/30',
impl_progress: 'bg-primary/15 text-primary border-primary/30',
test_result: 'bg-green-500/15 text-green-600 dark:text-green-400 border-green-500/30',
review_result: 'bg-purple-500/15 text-purple-600 dark:text-purple-400 border-purple-500/30',
fix_required: 'bg-red-500/15 text-red-600 dark:text-red-400 border-red-500/30',
error: 'bg-red-500/15 text-red-600 dark:text-red-400 border-red-500/30',
shutdown: 'bg-muted text-muted-foreground border-border',
message: 'bg-muted text-muted-foreground border-border',
};
function MessageTypeBadge({ type }: { type: string }) {
const { formatMessage } = useIntl();
const color = typeColorMap[type] || typeColorMap.message;
const labelKey = `team.messageType.${type}`;
let label: string;
try {
label = formatMessage({ id: labelKey });
} catch {
label = type;
}
return (
<span className={cn('text-[10px] px-1.5 py-0.5 rounded border font-medium', color)}>
{label}
</span>
);
}
function MessageRow({ msg }: { msg: TeamMessage }) {
const [dataExpanded, setDataExpanded] = useState(false);
const time = msg.ts ? msg.ts.substring(11, 19) : '';
return (
<div className="flex gap-3 py-2.5 border-b border-border last:border-b-0 animate-in fade-in slide-in-from-top-1 duration-300">
{/* Timestamp */}
<span className="text-[10px] font-mono text-muted-foreground w-16 shrink-0 pt-0.5">
{time}
</span>
{/* Content */}
<div className="flex-1 min-w-0 space-y-1">
{/* Header: from → to + type */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs font-medium">{msg.from}</span>
<span className="text-[10px] text-muted-foreground">&rarr;</span>
<span className="text-xs font-medium">{msg.to}</span>
<MessageTypeBadge type={msg.type} />
{msg.id && (
<span className="text-[10px] text-muted-foreground">{msg.id}</span>
)}
</div>
{/* Summary */}
<p className="text-xs text-foreground/80">{msg.summary}</p>
{/* Ref link */}
{msg.ref && (
<div className="flex items-center gap-1 text-[10px] text-muted-foreground">
<FileText className="w-3 h-3" />
<span className="font-mono truncate">{msg.ref}</span>
</div>
)}
{/* Data toggle */}
{msg.data && Object.keys(msg.data).length > 0 && (
<div>
<button
onClick={() => setDataExpanded(!dataExpanded)}
className="text-[10px] text-primary hover:underline flex items-center gap-0.5"
>
{dataExpanded ? (
<>
<ChevronUp className="w-3 h-3" /> collapse
</>
) : (
<>
<ChevronDown className="w-3 h-3" /> data
</>
)}
</button>
{dataExpanded && (
<pre className="text-[10px] bg-muted p-2 rounded mt-1 overflow-x-auto max-h-40">
{JSON.stringify(msg.data, null, 2)}
</pre>
)}
</div>
)}
</div>
</div>
);
}
export function TeamMessageFeed({
messages,
total,
filter,
onFilterChange,
onClearFilter,
expanded,
onExpandedChange,
}: TeamMessageFeedProps) {
const { formatMessage } = useIntl();
const hasFilter = !!(filter.from || filter.to || filter.type);
// Extract unique senders/receivers for filter dropdowns
const { senders, receivers, types } = useMemo(() => {
const s = new Set<string>();
const r = new Set<string>();
const t = new Set<string>();
for (const m of messages) {
s.add(m.from);
r.add(m.to);
t.add(m.type);
}
return {
senders: Array.from(s).sort(),
receivers: Array.from(r).sort(),
types: Array.from(t).sort(),
};
}, [messages]);
// Reverse for newest-first display
const displayMessages = [...messages].reverse();
return (
<div className="space-y-3">
{/* Header */}
<div className="flex items-center justify-between">
<button
onClick={() => onExpandedChange(!expanded)}
className="flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
{expanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
{formatMessage({ id: 'team.timeline.title' })}
<span className="text-xs font-normal">
({formatMessage({ id: 'team.timeline.showing' }, { showing: messages.length, total })})
</span>
</button>
{hasFilter && (
<Button variant="ghost" size="sm" onClick={onClearFilter} className="h-6 text-xs gap-1">
<X className="w-3 h-3" />
{formatMessage({ id: 'team.timeline.clearFilters' })}
</Button>
)}
</div>
{expanded && (
<>
{/* Filters */}
<div className="flex flex-wrap gap-2">
<Select
value={filter.from ?? '__all__'}
onValueChange={(v) => onFilterChange({ from: v === '__all__' ? undefined : v })}
>
<SelectTrigger className="w-[130px] h-7 text-xs">
<Filter className="w-3 h-3 mr-1" />
<SelectValue placeholder={formatMessage({ id: 'team.timeline.filterFrom' })} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">{formatMessage({ id: 'team.filterAll' })}</SelectItem>
{senders.map((s) => (
<SelectItem key={s} value={s}>{s}</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={filter.to ?? '__all__'}
onValueChange={(v) => onFilterChange({ to: v === '__all__' ? undefined : v })}
>
<SelectTrigger className="w-[130px] h-7 text-xs">
<SelectValue placeholder={formatMessage({ id: 'team.timeline.filterTo' })} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">{formatMessage({ id: 'team.filterAll' })}</SelectItem>
{receivers.map((r) => (
<SelectItem key={r} value={r}>{r}</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={filter.type ?? '__all__'}
onValueChange={(v) => onFilterChange({ type: v === '__all__' ? undefined : v })}
>
<SelectTrigger className="w-[150px] h-7 text-xs">
<SelectValue placeholder={formatMessage({ id: 'team.timeline.filterType' })} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">{formatMessage({ id: 'team.filterAll' })}</SelectItem>
{types.map((t) => (
<SelectItem key={t} value={t}>{t}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Messages list */}
<Card>
<CardContent className="p-3">
{displayMessages.length > 0 ? (
<div className="divide-y-0">
{displayMessages.map((msg) => (
<MessageRow key={msg.id} msg={msg} />
))}
</div>
) : (
<div className="text-center py-8">
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'team.empty.noMessages' })}
</p>
<p className="text-xs text-muted-foreground mt-1">
{formatMessage({ id: 'team.empty.noMessagesHint' })}
</p>
</div>
)}
</CardContent>
</Card>
</>
)}
</div>
);
}

View File

@@ -0,0 +1,163 @@
// ========================================
// TeamPipeline Component
// ========================================
// CSS-based pipeline stage visualization: PLAN → IMPL → TEST + REVIEW
import { useIntl } from 'react-intl';
import { CheckCircle2, Circle, Loader2, Ban } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { TeamMessage, PipelineStage, PipelineStageStatus } from '@/types/team';
interface TeamPipelineProps {
messages: TeamMessage[];
}
const STAGES: PipelineStage[] = ['plan', 'impl', 'test', 'review'];
/** Derive pipeline stage status from message history */
function derivePipelineStatus(messages: TeamMessage[]): Record<PipelineStage, PipelineStageStatus> {
const status: Record<PipelineStage, PipelineStageStatus> = {
plan: 'pending',
impl: 'pending',
test: 'pending',
review: 'pending',
};
for (const msg of messages) {
const t = msg.type;
// Plan stage
if (t === 'plan_ready') status.plan = 'in_progress';
if (t === 'plan_approved') {
status.plan = 'completed';
if (status.impl === 'pending') status.impl = 'in_progress';
}
if (t === 'plan_revision') status.plan = 'in_progress';
// Impl stage
if (t === 'impl_progress') status.impl = 'in_progress';
if (t === 'impl_complete') {
status.impl = 'completed';
if (status.test === 'pending') status.test = 'in_progress';
if (status.review === 'pending') status.review = 'in_progress';
}
// Test stage
if (t === 'test_result') {
const passed = msg.data?.passed ?? msg.summary?.toLowerCase().includes('pass');
status.test = passed ? 'completed' : 'in_progress';
}
// Review stage
if (t === 'review_result') {
const approved = msg.data?.approved ?? msg.summary?.toLowerCase().includes('approv');
status.review = approved ? 'completed' : 'in_progress';
}
// Fix required resets impl
if (t === 'fix_required') {
status.impl = 'in_progress';
}
// Error blocks stages
if (t === 'error') {
// Keep current status, don't override to blocked
}
}
return status;
}
const statusConfig: Record<PipelineStageStatus, { icon: typeof CheckCircle2; color: string; bg: string; animate?: boolean }> = {
completed: { icon: CheckCircle2, color: 'text-green-500', bg: 'bg-green-500/10 border-green-500/30' },
in_progress: { icon: Loader2, color: 'text-blue-500', bg: 'bg-blue-500/10 border-blue-500/30', animate: true },
pending: { icon: Circle, color: 'text-muted-foreground', bg: 'bg-muted border-border' },
blocked: { icon: Ban, color: 'text-red-500', bg: 'bg-red-500/10 border-red-500/30' },
};
function StageNode({ stage, status }: { stage: PipelineStage; status: PipelineStageStatus }) {
const { formatMessage } = useIntl();
const config = statusConfig[status];
const Icon = config.icon;
return (
<div
className={cn(
'flex flex-col items-center gap-1.5 px-4 py-3 rounded-lg border-2 min-w-[90px] transition-all',
config.bg
)}
>
<Icon
className={cn('w-5 h-5', config.color, config.animate && 'animate-spin')}
style={config.animate ? { animationDuration: '2s' } : undefined}
/>
<span className="text-xs font-medium">
{formatMessage({ id: `team.pipeline.${stage}` })}
</span>
<span className={cn('text-[10px]', config.color)}>
{formatMessage({ id: `team.pipeline.${status === 'in_progress' ? 'inProgress' : status}` })}
</span>
</div>
);
}
function Arrow() {
return (
<div className="flex items-center px-1">
<div className="w-6 h-0.5 bg-border" />
<div className="w-0 h-0 border-t-[4px] border-t-transparent border-b-[4px] border-b-transparent border-l-[6px] border-l-border" />
</div>
);
}
function ForkArrow() {
return (
<div className="flex items-center px-1">
<div className="w-4 h-0.5 bg-border" />
<div className="flex flex-col gap-1">
<div className="w-3 h-0.5 bg-border -rotate-20" />
<div className="w-3 h-0.5 bg-border rotate-20" />
</div>
</div>
);
}
export function TeamPipeline({ messages }: TeamPipelineProps) {
const { formatMessage } = useIntl();
const stageStatus = derivePipelineStatus(messages);
return (
<div className="space-y-3">
<h3 className="text-sm font-medium text-muted-foreground">
{formatMessage({ id: 'team.pipeline.title' })}
</h3>
{/* Desktop: horizontal layout */}
<div className="hidden sm:flex items-center gap-0">
<StageNode stage="plan" status={stageStatus.plan} />
<Arrow />
<StageNode stage="impl" status={stageStatus.impl} />
<Arrow />
<div className="flex flex-col gap-2">
<StageNode stage="test" status={stageStatus.test} />
<StageNode stage="review" status={stageStatus.review} />
</div>
</div>
{/* Mobile: vertical layout */}
<div className="flex sm:hidden flex-col items-center gap-2">
{STAGES.map((stage) => (
<StageNode key={stage} stage={stage} status={stageStatus[stage]} />
))}
</div>
{/* Legend */}
<div className="flex flex-wrap gap-3 text-[10px] text-muted-foreground pt-1">
{(['completed', 'in_progress', 'pending', 'blocked'] as PipelineStageStatus[]).map((s) => {
const cfg = statusConfig[s];
const Icon = cfg.icon;
return (
<span key={s} className="flex items-center gap-1">
<Icon className={cn('w-3 h-3', cfg.color)} />
{formatMessage({ id: `team.pipeline.${s === 'in_progress' ? 'inProgress' : s}` })}
</span>
);
})}
</div>
</div>
);
}