business-as-code
Version:
Primitives for expressing business logic and processes as code
547 lines (462 loc) • 17.3 kB
text/typescript
/**
* Tests for roles.ts - Business roles and task assignment
*/
import { describe, it, expect } from 'vitest'
import {
StandardBusinessRoles,
createBusinessRole,
hasPermission,
canHandleTask,
canApproveRequest,
canDelegateTask,
findRoleForTask,
createTaskAssignment,
transitionTaskStatus,
} from '../src/roles.js'
import type {
BusinessRole,
TaskRoutingRule,
TaskAssignment,
AssigneeRef,
WorkflowRole,
} from '../src/roles.js'
describe('Roles', () => {
describe('StandardBusinessRoles', () => {
it('should have executive roles', () => {
expect(StandardBusinessRoles.ceo).toBeDefined()
expect(StandardBusinessRoles.cto).toBeDefined()
expect(StandardBusinessRoles.cfo).toBeDefined()
})
it('should have management roles', () => {
expect(StandardBusinessRoles.director).toBeDefined()
expect(StandardBusinessRoles.manager).toBeDefined()
expect(StandardBusinessRoles.lead).toBeDefined()
})
it('should have individual contributor roles', () => {
expect(StandardBusinessRoles.engineer).toBeDefined()
expect(StandardBusinessRoles.analyst).toBeDefined()
})
it('should have operations roles', () => {
expect(StandardBusinessRoles.agent).toBeDefined()
expect(StandardBusinessRoles.assistant).toBeDefined()
})
it('CEO should have manage all permissions', () => {
const ceo = StandardBusinessRoles.ceo
expect(ceo?.permissions?.['*']).toContain('manage')
})
it('CTO should have technology permissions', () => {
const cto = StandardBusinessRoles.cto
expect(cto?.permissions?.technology).toContain('manage')
expect(cto?.permissions?.repository).toContain('manage')
})
it('engineer should have repository permissions', () => {
const engineer = StandardBusinessRoles.engineer
expect(engineer?.permissions?.repository).toContain('read')
expect(engineer?.permissions?.repository).toContain('edit')
})
it('agent should be AI-first', () => {
const agent = StandardBusinessRoles.agent
expect(agent?.workerType).toBe('ai')
})
it('assistant should be AI-first', () => {
const assistant = StandardBusinessRoles.assistant
expect(assistant?.workerType).toBe('ai')
})
})
describe('createBusinessRole()', () => {
it('should create role from standard template', () => {
const role = createBusinessRole('role_cto_jane', 'cto')
expect(role.id).toBe('role_cto_jane')
expect(role.name).toBe('Chief Technology Officer')
expect(role.type).toBe('cto')
expect(role.level).toBe(10)
})
it('should allow overrides', () => {
const role = createBusinessRole('role_engineer_custom', 'engineer', {
name: 'Full Stack Engineer',
level: 4,
skills: ['TypeScript', 'React', 'Node.js'],
})
expect(role.name).toBe('Full Stack Engineer')
expect(role.level).toBe(4)
expect(role.skills).toContain('TypeScript')
})
it('should throw error for unknown template', () => {
expect(() =>
createBusinessRole('role_test', 'unknown_role' as keyof typeof StandardBusinessRoles)
).toThrow('Unknown role template: unknown_role')
})
})
describe('hasPermission()', () => {
const role: BusinessRole = {
id: 'role_test',
name: 'Test Role',
type: 'engineer',
permissions: {
repository: ['read', 'edit', 'act:merge'],
project: ['read'],
document: ['manage'],
},
}
it('should return true for exact permission match', () => {
expect(hasPermission(role, 'repository', 'read')).toBe(true)
expect(hasPermission(role, 'repository', 'edit')).toBe(true)
})
it('should return true for action verb permissions', () => {
expect(hasPermission(role, 'repository', 'act:merge')).toBe(true)
})
it('should return true for manage permission (covers all)', () => {
expect(hasPermission(role, 'document', 'read')).toBe(true)
expect(hasPermission(role, 'document', 'edit')).toBe(true)
expect(hasPermission(role, 'document', 'delete')).toBe(true)
})
it('should return false for permission not granted', () => {
expect(hasPermission(role, 'repository', 'delete')).toBe(false)
expect(hasPermission(role, 'project', 'edit')).toBe(false)
})
it('should return false for non-existent resource type', () => {
expect(hasPermission(role, 'invoice', 'read')).toBe(false)
})
it('should return false for role without permissions', () => {
const emptyRole: BusinessRole = { id: 'empty', name: 'Empty', type: 'engineer' }
expect(hasPermission(emptyRole, 'repository', 'read')).toBe(false)
})
it('should handle wildcard permissions', () => {
const adminRole: BusinessRole = {
id: 'admin',
name: 'Admin',
type: 'ceo',
permissions: { '*': ['manage'] },
}
expect(hasPermission(adminRole, 'repository', 'read')).toBe(true)
expect(hasPermission(adminRole, 'anyResource', 'anyAction')).toBe(true)
})
it('should handle act:* wildcard', () => {
const roleWithActWildcard: BusinessRole = {
id: 'test',
name: 'Test',
type: 'lead',
permissions: {
repository: ['read', 'act:*'],
},
}
expect(hasPermission(roleWithActWildcard, 'repository', 'act:merge')).toBe(true)
expect(hasPermission(roleWithActWildcard, 'repository', 'act:deploy')).toBe(true)
})
})
describe('canHandleTask()', () => {
const role: BusinessRole = {
id: 'role_test',
name: 'Test Role',
type: 'engineer',
canHandle: ['coding', 'code-review', 'testing'],
}
it('should return true for task in canHandle list', () => {
expect(canHandleTask(role, 'coding')).toBe(true)
expect(canHandleTask(role, 'code-review')).toBe(true)
})
it('should return false for task not in canHandle list', () => {
expect(canHandleTask(role, 'architecture-review')).toBe(false)
})
it('should return false for role without canHandle', () => {
const emptyRole: BusinessRole = { id: 'empty', name: 'Empty', type: 'engineer' }
expect(canHandleTask(emptyRole, 'coding')).toBe(false)
})
it('should handle wildcard in canHandle', () => {
const wildcardRole: BusinessRole = {
id: 'admin',
name: 'Admin',
type: 'ceo',
canHandle: ['*'],
}
expect(canHandleTask(wildcardRole, 'anything')).toBe(true)
})
})
describe('canApproveRequest()', () => {
const role: BusinessRole = {
id: 'role_test',
name: 'Test Role',
type: 'manager',
canApprove: ['expense-under-5k', 'time-off', 'code-review'],
}
it('should return true for request in canApprove list', () => {
expect(canApproveRequest(role, 'expense-under-5k')).toBe(true)
expect(canApproveRequest(role, 'time-off')).toBe(true)
})
it('should return false for request not in canApprove list', () => {
expect(canApproveRequest(role, 'hiring')).toBe(false)
})
it('should return false for role without canApprove', () => {
const emptyRole: BusinessRole = { id: 'empty', name: 'Empty', type: 'engineer' }
expect(canApproveRequest(emptyRole, 'time-off')).toBe(false)
})
it('should handle wildcard in canApprove', () => {
const wildcardRole: BusinessRole = {
id: 'admin',
name: 'Admin',
type: 'ceo',
canApprove: ['*'],
}
expect(canApproveRequest(wildcardRole, 'anything')).toBe(true)
})
})
describe('canDelegateTask()', () => {
const role: BusinessRole = {
id: 'role_test',
name: 'Test Role',
type: 'lead',
canDelegate: ['code-review', 'testing'],
}
it('should return true for task in canDelegate list', () => {
expect(canDelegateTask(role, 'code-review')).toBe(true)
expect(canDelegateTask(role, 'testing')).toBe(true)
})
it('should return false for task not in canDelegate list', () => {
expect(canDelegateTask(role, 'architecture-review')).toBe(false)
})
it('should return false for role without canDelegate', () => {
const emptyRole: BusinessRole = { id: 'empty', name: 'Empty', type: 'engineer' }
expect(canDelegateTask(emptyRole, 'code-review')).toBe(false)
})
it('should handle wildcard in canDelegate', () => {
const wildcardRole: BusinessRole = {
id: 'admin',
name: 'Admin',
type: 'ceo',
canDelegate: ['*'],
}
expect(canDelegateTask(wildcardRole, 'anything')).toBe(true)
})
})
describe('findRoleForTask()', () => {
const rules: TaskRoutingRule[] = [
{
taskType: 'code-review',
requiredRole: 'engineer',
requiredLevel: 2,
preferWorkerType: 'human',
},
{
taskType: 'expense-approval',
requiredRole: 'manager',
amountThreshold: 1000,
},
{
taskType: 'expense-approval',
requiredRole: 'director',
escalateAbove: 5000,
escalateTo: 'director',
},
{
taskType: 'customer-inquiry',
requiredRole: 'agent',
preferWorkerType: 'ai',
fallbackTo: 'human',
},
]
it('should find matching rule for task type', () => {
const rule = findRoleForTask('code-review', rules)
expect(rule).toBeDefined()
expect(rule?.requiredRole).toBe('engineer')
expect(rule?.requiredLevel).toBe(2)
})
it('should return undefined for non-existent task type', () => {
const rule = findRoleForTask('non-existent', rules)
expect(rule).toBeUndefined()
})
it('should return first matching rule when multiple exist', () => {
const rule = findRoleForTask('expense-approval', rules)
expect(rule).toBeDefined()
expect(rule?.requiredRole).toBe('manager')
})
it('should consider amount for escalation', () => {
const rule = findRoleForTask('expense-approval', rules, { amount: 10000 })
// Should escalate to director for amount > 5000
expect(rule).toBeDefined()
expect(rule?.requiredRole).toBe('director')
})
})
describe('createTaskAssignment()', () => {
const assignee: AssigneeRef = {
type: 'worker',
id: 'worker_alice',
name: 'Alice',
}
it('should create task assignment with defaults', () => {
const assignment = createTaskAssignment('task_123', 'code-review', assignee)
expect(assignment.id).toMatch(/^assign_/)
expect(assignment.taskId).toBe('task_123')
expect(assignment.taskType).toBe('code-review')
expect(assignment.assignee.id).toBe('worker_alice')
expect(assignment.status).toBe('assigned')
expect(assignment.priority).toBe('normal')
expect(assignment.assignedAt).toBeInstanceOf(Date)
})
it('should accept options', () => {
const assignment = createTaskAssignment('task_123', 'code-review', assignee, {
priority: 'high',
role: 'role_engineer',
description: 'Review PR #456',
context: { pullRequestId: 'pr_456' },
})
expect(assignment.priority).toBe('high')
expect(assignment.role).toBe('role_engineer')
expect(assignment.description).toBe('Review PR #456')
expect(assignment.context?.pullRequestId).toBe('pr_456')
})
it('should support team assignee', () => {
const teamAssignee: AssigneeRef = {
type: 'team',
id: 'team_platform',
name: 'Platform Team',
}
const assignment = createTaskAssignment('task_123', 'support', teamAssignee)
expect(assignment.assignee.type).toBe('team')
expect(assignment.assignee.id).toBe('team_platform')
})
it('should support role assignee', () => {
const roleAssignee: AssigneeRef = {
type: 'role',
id: 'role_engineer',
name: 'Engineer',
}
const assignment = createTaskAssignment('task_123', 'support', roleAssignee)
expect(assignment.assignee.type).toBe('role')
expect(assignment.assignee.id).toBe('role_engineer')
})
})
describe('transitionTaskStatus()', () => {
const baseAssignment: TaskAssignment = {
id: 'assign_123',
taskId: 'task_123',
taskType: 'code-review',
assignee: { type: 'worker', id: 'worker_alice' },
status: 'assigned',
assignedAt: new Date('2024-01-01'),
}
it('should transition to in_progress', () => {
const updated = transitionTaskStatus(baseAssignment, 'in_progress')
expect(updated.status).toBe('in_progress')
expect(updated.startedAt).toBeInstanceOf(Date)
})
it('should transition to completed', () => {
const inProgress: TaskAssignment = {
...baseAssignment,
status: 'in_progress',
startedAt: new Date('2024-01-01'),
}
const updated = transitionTaskStatus(inProgress, 'completed', {
result: { approved: true },
notes: 'LGTM',
})
expect(updated.status).toBe('completed')
expect(updated.completedAt).toBeInstanceOf(Date)
expect(updated.result).toEqual({ approved: true })
expect(updated.notes).toBe('LGTM')
})
it('should transition to failed', () => {
const updated = transitionTaskStatus(baseAssignment, 'failed', {
notes: 'Task failed due to error',
})
expect(updated.status).toBe('failed')
expect(updated.completedAt).toBeInstanceOf(Date)
expect(updated.notes).toBe('Task failed due to error')
})
it('should preserve existing startedAt when completing', () => {
const inProgress: TaskAssignment = {
...baseAssignment,
status: 'in_progress',
startedAt: new Date('2024-01-01T10:00:00'),
}
const updated = transitionTaskStatus(inProgress, 'completed')
expect(updated.startedAt?.getTime()).toBe(new Date('2024-01-01T10:00:00').getTime())
})
it('should handle blocked status', () => {
const updated = transitionTaskStatus(baseAssignment, 'blocked', {
notes: 'Waiting for dependencies',
})
expect(updated.status).toBe('blocked')
expect(updated.notes).toBe('Waiting for dependencies')
})
it('should handle cancelled status', () => {
const updated = transitionTaskStatus(baseAssignment, 'cancelled', {
notes: 'No longer needed',
})
expect(updated.status).toBe('cancelled')
expect(updated.notes).toBe('No longer needed')
})
})
describe('BusinessRole interface', () => {
it('should support full business role structure', () => {
const role: BusinessRole = {
id: 'role_senior_engineer',
name: 'Senior Software Engineer',
type: 'engineer',
department: 'Engineering',
description: 'Builds and maintains software systems',
responsibilities: [
'Design and implement features',
'Code review',
'Mentor junior engineers',
],
skills: ['TypeScript', 'React', 'Node.js', 'PostgreSQL'],
permissions: {
repository: ['read', 'edit', 'act:merge'],
project: ['read', 'edit'],
document: ['read'],
},
canHandle: ['coding', 'code-review', 'mentoring'],
canDelegate: ['testing'],
canApprove: ['code-review'],
escalateTo: 'role_lead',
reportsTo: 'role_lead',
workerType: 'hybrid',
level: 4,
compensationBand: 'L4',
metadata: { track: 'IC' },
}
expect(role.name).toBe('Senior Software Engineer')
expect(role.skills).toContain('TypeScript')
expect(role.canHandle).toContain('coding')
})
})
describe('TaskRoutingRule interface', () => {
it('should support full routing rule structure', () => {
const rule: TaskRoutingRule = {
taskType: 'complex-support-ticket',
requiredRole: 'agent',
requiredLevel: 2,
requiredSkills: ['technical-support', 'troubleshooting'],
requiredPermissions: ['ticket:read', 'ticket:respond'],
preferWorkerType: 'ai',
amountThreshold: 1000,
escalateAbove: 5000,
escalateTo: 'manager',
fallbackTo: 'human',
defaultPriority: 'high',
slaMinutes: 120,
conditions: { severity: 'critical' },
}
expect(rule.taskType).toBe('complex-support-ticket')
expect(rule.requiredSkills).toContain('technical-support')
expect(rule.slaMinutes).toBe(120)
})
})
describe('WorkflowRole interface', () => {
it('should support full workflow role structure', () => {
const workflowRole: WorkflowRole = {
name: 'Approver',
description: 'Reviews and approves requests',
canInitiate: false,
tasks: ['review', 'approve', 'reject'],
canView: ['details', 'history', 'comments'],
requiredBusinessRole: 'manager',
requiredPermissions: ['request:read', 'request:approve'],
minLevel: 3,
}
expect(workflowRole.name).toBe('Approver')
expect(workflowRole.tasks).toContain('approve')
expect(workflowRole.minLevel).toBe(3)
})
})
})