src/features/account/planned-pages.test.tsx
Rama main · réplica de lectura del repositorio original.
import type { AccessRequestApproval } from '@shared/api/agent-api'
import { cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
import { delay, type HttpResponseResolver } from 'msw'
import type { ReactNode } from 'react'
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
import {
base,
createAccountServer,
createAccountStore,
http,
json,
renderWithClient,
} from '@/features/account/account.test-utils'
import {
AccountAgentsPage,
AccountApplicationsPage,
AccountOrganizationDetailPage,
AccountOrganizationsPage,
AccountOverviewPage,
} from '@/features/account/planned-pages'
const navigate = vi.hoisted(() => vi.fn())
vi.mock('@tanstack/react-router', () => ({
Link: ({ children, className, to }: { children: ReactNode; className?: string; to: string }) => (
<a className={className} href={to}>
{children}
</a>
),
useNavigate: () => navigate,
}))
const store = createAccountStore()
const server = createAccountServer(store)
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => {
cleanup()
server.resetHandlers()
navigate.mockReset()
store.activeOrganizationId = null
store.access = {
canCreateOrganization: true,
showOrganizations: false,
platformOperator: false,
consoleOrganizations: [],
}
store.security.mfa.enabled = false
store.security.passkeys.count = 0
store.sessions = []
store.applications = []
store.agentIdentities = []
vi.restoreAllMocks()
})
afterAll(() => server.close())
describe('planned Account Center journeys', () => {
it('reviews overview security, sessions, invitations, and Agent access decisions', async () => {
vi.spyOn(Date.prototype, 'getHours').mockReturnValue(9)
store.security.mfa.enabled = true
store.sessions = [
{
id: 'session-current',
userAgent: 'Mozilla/5.0 (Mac OS X) Chrome/120',
ipAddress: '192.0.2.10',
expiresAt: '2099-01-01T00:00:00.000Z',
current: true,
},
{
id: 'session-other',
userAgent: null,
ipAddress: null,
expiresAt: '2099-01-02T00:00:00.000Z',
current: false,
},
]
store.applications = [
{
id: 'consent-1',
applicationName: 'Project board',
scopes: ['openid'],
grantedAt: '2026-08-01T00:00:00.000Z',
},
]
store.agentIdentities = [agent('agent-active', 'Build Agent', 'active')]
const requestedAuthorizationDetails = [{ type: 'project_access', project_id: 'project-1', actions: ['read'] }]
let decision: unknown = null
server.use(
http.get(`${base}/api/account/access-requests`, () =>
json({
items: [
{ ...accessRequest(), authorizationDetail: null, authorizationDetails: requestedAuthorizationDetails },
],
pagination: pagination(1),
}),
),
http.put(`${base}/api/account/access-requests/request-1/decision`, async ({ request }) => {
decision = await request.json()
return json({ id: 'request-1', status: 'approved' })
}),
http.get(`${base}/api/auth/organization/list-user-invitations`, () =>
json([
{
id: 'invitation-1',
organizationName: 'Acme',
role: 'developer',
status: 'pending',
createdAt: '2026-08-01T00:00:00.000Z',
expiresAt: '2099-08-08T00:00:00.000Z',
},
{
id: 'invitation-old',
organizationName: 'Old org',
role: 'member',
status: 'accepted',
createdAt: '2026-07-01T00:00:00.000Z',
expiresAt: '2026-07-08T00:00:00.000Z',
},
]),
),
)
renderWithClient(<AccountOverviewPage />)
expect(await screen.findByRole('heading', { name: 'Workbench' })).toBeTruthy()
expect(await screen.findByRole('heading', { name: 'Workbench' })).toBeTruthy()
expect(screen.getByText('Join Acme')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Review request' }))
expect(await screen.findByRole('heading', { name: 'Review Agent access request' })).toBeTruthy()
expect(screen.getByText('{"type":"project_access","project_id":"project-1","actions":["read"]}')).toBeTruthy()
closeDialogWithEscape()
fireEvent.click(screen.getByRole('button', { name: 'Review request' }))
fireEvent.change(screen.getByLabelText('Access duration'), { target: { value: 'until' } })
const expiry = screen.getByLabelText('Expiry date and time')
expect((screen.getByRole('button', { name: 'Approve' }) as HTMLButtonElement).disabled).toBe(true)
fireEvent.change(expiry, { target: { value: '2099-01-01T12:00' } })
fireEvent.click(screen.getByRole('button', { name: 'Approve' }))
await waitFor(() =>
expect(decision).toEqual({
decision: 'approve',
mode: 'until',
authorizationDetails: requestedAuthorizationDetails,
expiresAt: expect.any(String),
}),
)
})
it.each([
[14, 'Workbench'],
[20, 'Workbench'],
])('renders empty overview states at hour %s', async (hour, heading) => {
vi.spyOn(Date.prototype, 'getHours').mockReturnValue(hour)
server.use(http.get(`${base}/api/account/access-requests`, () => json({ items: [], pagination: pagination(0) })))
renderWithClient(<AccountOverviewPage />)
expect(await screen.findByRole('heading', { name: heading })).toBeTruthy()
expect(await screen.findByText("You're all caught up")).toBeTruthy()
expect(screen.getByText('Start with your first Agent')).toBeTruthy()
})
it('keeps overview metrics pending until their resources load', async () => {
const delayedJson = async (payload: Record<string, unknown>) => {
await delay(100)
return json(payload)
}
server.use(
http.get(`${base}/api/account/security`, () => delayedJson({ security: store.security })),
http.get(`${base}/api/account/agents`, () => delayedJson({ items: [], pagination: pagination(0) })),
http.get(`${base}/api/auth/organization/list`, async () => {
await delay(100)
return json([])
}),
http.get(`${base}/api/account/access-requests`, () => json({ items: [], pagination: pagination(0) })),
)
renderWithClient(<AccountOverviewPage />)
expect(await screen.findByRole('heading', { name: 'Workbench' })).toBeTruthy()
expect(screen.getByText('Loading Agents…')).toBeTruthy()
})
it('keeps an overview access request open when denial fails', async () => {
server.use(
http.get(`${base}/api/account/access-requests`, () =>
json({ items: [accessRequest()], pagination: pagination(1) }),
),
http.put(`${base}/api/account/access-requests/request-1/decision`, () =>
json({ message: 'Decision unavailable.' }, { status: 500 }),
),
)
renderWithClient(<AccountOverviewPage />)
fireEvent.click(await screen.findByRole('button', { name: 'Review request' }))
fireEvent.click(screen.getByRole('button', { name: 'Deny' }))
await waitFor(() => expect(screen.getByRole('heading', { name: 'Review Agent access request' })).toBeTruthy())
})
it('renders collection loading, failure, and empty Agent states', async () => {
server.use(http.get(`${base}/api/account/access-requests`, () => json({ items: [], pagination: pagination(0) })))
const overview = renderWithClient(<AccountOverviewPage />)
expect(screen.getByText('Loading account center')).toBeTruthy()
expect(await screen.findByRole('heading', { name: 'Workbench' })).toBeTruthy()
overview.unmount()
renderWithClient(<AccountAgentsPage />)
expect(await screen.findByText('No Agent identities')).toBeTruthy()
openTab('Requests · 0')
expect(await screen.findByText('No pending requests')).toBeTruthy()
})
it('[spec: account-center/authorized-app-separation] reviews and revokes only Realmroot application grants', async () => {
store.applications = [
{
id: 'consent-1',
applicationName: 'Project board',
scopes: ['openid', 'profile'],
grantedAt: '2026-08-01T00:00:00.000Z',
expiresAt: null,
},
{
id: 'consent-2',
applicationName: 'Reports',
scopes: ['openid'],
grantedAt: '2026-08-02T00:00:00.000Z',
expiresAt: '2099-01-01T00:00:00.000Z',
},
]
let revoked = false
server.use(
http.delete(`${base}/api/account/application-authorizations/consent-1`, () => {
revoked = true
return new Response(null, { status: 204 })
}),
)
renderWithClient(<AccountApplicationsPage />)
const reviewButtons = await screen.findAllByRole('button', { name: 'Review' })
fireEvent.click(reviewButtons[0]!)
expect(await screen.findByRole('heading', { name: 'Project board' })).toBeTruthy()
expect(screen.getByText('Never')).toBeTruthy()
closeDialogWithEscape()
fireEvent.click((await screen.findAllByRole('button', { name: 'Review' }))[1]!)
expect(await screen.findByRole('heading', { name: 'Reports' })).toBeTruthy()
fireEvent.click(screen.getAllByRole('button', { name: 'Close' })[0]!)
fireEvent.click((await screen.findAllByRole('button', { name: 'Review' }))[0]!)
fireEvent.click(screen.getByRole('button', { name: 'Revoke access' }))
fireEvent.click(screen.getAllByRole('button', { name: 'Revoke access' }).at(-1)!)
await waitFor(() => expect(revoked).toBe(true))
})
it('shows an empty authorized application collection', async () => {
renderWithClient(<AccountApplicationsPage />)
expect(await screen.findByText('No authorized applications')).toBeTruthy()
})
it('keeps application and Agent decisions open when their mutations fail', async () => {
store.applications = [
{
id: 'consent-failure',
applicationName: 'Unreliable app',
scopes: ['openid'],
grantedAt: '2026-08-01T00:00:00.000Z',
},
]
server.use(
http.delete(`${base}/api/account/application-authorizations/consent-failure`, () =>
json({ message: 'Revoke failed.' }, { status: 500 }),
),
)
const applications = renderWithClient(<AccountApplicationsPage />)
fireEvent.click(await screen.findByRole('button', { name: 'Review' }))
fireEvent.click(screen.getByRole('button', { name: 'Revoke access' }))
fireEvent.click(screen.getAllByRole('button', { name: 'Revoke access' }).at(-1)!)
await waitFor(() => expect(screen.getByRole('heading', { name: 'Unreliable app' })).toBeTruthy())
applications.unmount()
store.agentIdentities = [agent('agent-active', 'Build Agent', 'active')]
server.use(
http.get(`${base}/api/account/access-requests`, () =>
json({ items: [accessRequest()], pagination: pagination(1) }),
),
http.delete(`${base}/api/account/agents/agent-active`, () =>
json({ message: 'Deletion failed.' }, { status: 500 }),
),
http.put(`${base}/api/account/access-requests/request-1/decision`, () =>
json({ message: 'Decision failed.' }, { status: 500 }),
),
)
renderWithClient(<AccountAgentsPage />)
fireEvent.click(await screen.findByRole('button', { name: 'Manage' }))
fireEvent.click(screen.getByRole('button', { name: 'Delete Agent' }))
fireEvent.click(screen.getAllByRole('button', { name: 'Delete Agent' }).at(-1)!)
await waitFor(() => expect(screen.getByRole('heading', { name: 'Build Agent' })).toBeTruthy())
fireEvent.click(screen.getAllByRole('button', { name: 'Close' })[0]!)
fireEvent.mouseDown(screen.getByRole('tab', { name: 'Requests · 1' }), { button: 0, ctrlKey: false })
fireEvent.click(await screen.findByRole('button', { name: 'Review request' }))
fireEvent.click(screen.getByRole('button', { name: 'Deny' }))
await waitFor(() => expect(screen.getByRole('heading', { name: 'Review Agent access request' })).toBeTruthy())
})
it('manages Agent identities and denies pending access requests', async () => {
store.agentIdentities = [
agent('agent-active', 'Build Agent', 'active'),
agent('agent-inactive', 'Old Agent', 'inactive'),
]
let deleted = false
let activated = false
let deactivated = false
let decision: unknown = null
server.use(
http.get(`${base}/api/account/access-requests`, () =>
json({ items: [accessRequest()], pagination: pagination(1) }),
),
http.delete(`${base}/api/account/agents/agent-active`, () => {
deleted = true
return new Response(null, { status: 204 })
}),
http.delete(`${base}/api/account/agents/agent-active/activation`, () => {
deactivated = true
return new Response(null, { status: 204 })
}),
http.put(`${base}/api/account/agents/agent-inactive/activation`, () => {
activated = true
return new Response(null, { status: 204 })
}),
http.put(`${base}/api/account/access-requests/request-1/decision`, async ({ request }) => {
decision = await request.json()
return json({ id: 'request-1', status: 'denied' })
}),
)
renderWithClient(<AccountAgentsPage />)
fireEvent.click((await screen.findAllByRole('button', { name: 'Manage' }))[0]!)
expect(await screen.findByRole('heading', { name: 'Build Agent' })).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Deactivate Agent' }))
await waitFor(() => expect(deactivated).toBe(true))
closeDialogWithEscape()
fireEvent.click((await screen.findAllByRole('button', { name: 'Manage' }))[1]!)
fireEvent.click(screen.getByRole('button', { name: 'Activate Agent' }))
await waitFor(() => expect(activated).toBe(true))
fireEvent.click(screen.getAllByRole('button', { name: 'Close' })[0]!)
fireEvent.click((await screen.findAllByRole('button', { name: 'Manage' }))[0]!)
fireEvent.click(screen.getByRole('button', { name: 'Delete Agent' }))
fireEvent.click(screen.getAllByRole('button', { name: 'Delete Agent' }).at(-1)!)
await waitFor(() => expect(deleted).toBe(true))
fireEvent.mouseDown(screen.getByRole('tab', { name: 'Requests · 1' }), { button: 0, ctrlKey: false })
fireEvent.click(await screen.findByRole('button', { name: 'Review request' }))
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
fireEvent.click(await screen.findByRole('button', { name: 'Review request' }))
fireEvent.click(screen.getByRole('button', { name: 'Deny' }))
await waitFor(() => expect(decision).toEqual({ decision: 'deny' }))
})
it('creates, switches, accepts, and declines Organizations', async () => {
let created: unknown = null
const invitationActions: string[] = []
server.use(
http.get(`${base}/api/auth/organization/list`, () =>
json([
{ id: 'org-active', name: 'Active org', slug: 'active-org', createdAt: '2026-01-01T00:00:00.000Z' },
{ id: 'org-other', name: 'Other org', slug: 'other-org', createdAt: '2026-02-01T00:00:00.000Z' },
]),
),
http.get(`${base}/api/auth/organization/list-user-invitations`, () =>
json([
{
id: 'invitation-accept',
organizationName: 'Join us',
role: 'admin',
status: 'pending',
createdAt: '2026-08-01T00:00:00.000Z',
expiresAt: '2099-08-08T00:00:00.000Z',
},
{
id: 'invitation-decline',
organizationName: 'No thanks',
role: 'member',
status: 'pending',
createdAt: '2026-08-01T00:00:00.000Z',
expiresAt: '2099-08-08T00:00:00.000Z',
},
]),
),
http.post(`${base}/api/auth/organization/create`, async ({ request }) => {
created = await request.json()
return json({ id: 'org-new', ...(created as object) })
}),
http.post(`${base}/api/auth/organization/accept-invitation`, async ({ request }) => {
invitationActions.push(`accept:${JSON.stringify(await request.json())}`)
return json({ invitation: { id: 'invitation-accept' } })
}),
http.post(`${base}/api/auth/organization/reject-invitation`, async ({ request }) => {
invitationActions.push(`reject:${JSON.stringify(await request.json())}`)
return json({ invitation: { id: 'invitation-decline' } })
}),
)
renderWithClient(<AccountOrganizationsPage />)
expect(await screen.findByText('Active org')).toBeTruthy()
expect(screen.queryByText('Current')).toBeNull()
expect(screen.queryByRole('button', { name: 'Switch' })).toBeNull()
expect(screen.getAllByRole('link', { name: 'Manage' })).toHaveLength(2)
fireEvent.click(screen.getAllByRole('button', { name: 'Accept' })[0]!)
fireEvent.click(screen.getAllByRole('button', { name: 'Decline' })[1]!)
await waitFor(() => expect(invitationActions).toHaveLength(2))
fireEvent.click(screen.getByRole('button', { name: 'New organization' }))
closeDialogWithEscape()
fireEvent.click(screen.getByRole('button', { name: 'New organization' }))
fireEvent.submit(screen.getByRole('button', { name: 'Create organization' }).closest('form')!)
fireEvent.change(screen.getByLabelText('Organization name'), { target: { value: 'Payments Team' } })
expect((screen.getByLabelText('Slug') as HTMLInputElement).value).toBe('payments-team')
fireEvent.change(screen.getByLabelText('Slug'), { target: { value: 'payments-platform' } })
fireEvent.change(screen.getByLabelText('Organization name'), { target: { value: 'Payments Platform' } })
expect((screen.getByLabelText('Slug') as HTMLInputElement).value).toBe('payments-platform')
fireEvent.submit(screen.getByRole('button', { name: 'Create organization' }).closest('form')!)
await waitFor(() => expect(created).toMatchObject({ name: 'Payments Platform', slug: 'payments-platform' }))
})
it('hides Organization creation and renders an empty collection when policy disallows it', async () => {
store.access.canCreateOrganization = false
renderWithClient(<AccountOrganizationsPage />)
expect(await screen.findByText('No organizations yet')).toBeTruthy()
expect(screen.queryByRole('button', { name: 'New organization' })).toBeNull()
})
it('keeps Organization creation open when provisioning fails', async () => {
server.use(
http.post(`${base}/api/auth/organization/create`, () =>
json({ message: 'Organization creation failed.' }, { status: 500 }),
),
)
renderWithClient(<AccountOrganizationsPage />)
fireEvent.click(await screen.findByRole('button', { name: 'New organization' }))
fireEvent.change(screen.getByLabelText('Organization name'), { target: { value: 'Failure team' } })
fireEvent.click(screen.getByRole('button', { name: 'Create organization' }))
await waitFor(() => expect(screen.getByLabelText('Organization name')).toBeTruthy())
})
it('[spec: admin-console/admin-govern-organization] manages Organization members, invitations, profile, and deletion', async () => {
const actions: Array<{ path: string; body: unknown }> = []
store.access.platformOperator = true
server.use(
...organizationDetailHandlers('owner'),
http.put(`${base}/api/organizations/org-family/members/:memberId/roles`, async ({ request }) => {
actions.push({ path: '/api/auth/organization/update-member-role', body: await request.json() })
return json({ roles: ['admin'] })
}),
http.get(`${base}/api/organizations/org-family/roles`, () =>
json({
items: ['owner', 'admin', 'developer', 'member'].map((key) => ({ key, displayName: key, predefined: true })),
pagination: pagination(4),
}),
),
http.post(`${base}/api/organizations/org-family/invitations`, async ({ request }) => {
actions.push({ path: '/api/organizations/org-family/invitations', body: await request.json() })
return json({ id: 'inv-new' }, { status: 201 })
}),
organizationMutationHandler(actions),
)
renderWithClient(<AccountOrganizationDetailPage organizationId="org-family" />)
expect(await screen.findByRole('heading', { name: 'Family' })).toBeTruthy()
expect(screen.queryByRole('link', { name: 'Open Console' })).toBeNull()
await openOrganizationSection('members')
fireEvent.click(await screen.findByRole('button', { name: 'Invite member' }))
closeDialogWithEscape()
fireEvent.click(await screen.findByRole('button', { name: 'Invite member' }))
fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'new@example.com' } })
fireEvent.click(screen.getByLabelText('member'))
fireEvent.click(screen.getByLabelText('developer'))
fireEvent.click(screen.getByRole('button', { name: 'Send invitation' }))
await waitFor(() =>
expect(actions).toContainEqual({
path: '/api/organizations/org-family/invitations',
body: { email: 'new@example.com', roles: ['developer'] },
}),
)
fireEvent.click(screen.getByRole('button', { name: 'Manage' }))
closeDialogWithEscape()
fireEvent.click(screen.getByRole('button', { name: 'Manage' }))
fireEvent.change(screen.getByLabelText('Access level'), { target: { value: 'admin' } })
fireEvent.click(screen.getByRole('button', { name: 'Save access level' }))
await waitFor(() => expect(actions.some((action) => action.path.endsWith('/update-member-role'))).toBe(true))
fireEvent.click(screen.getByRole('button', { name: 'Manage' }))
fireEvent.click(screen.getByRole('button', { name: 'Remove member' }))
fireEvent.click(screen.getAllByRole('button', { name: 'Remove member' }).at(-1)!)
await waitFor(() => expect(actions.some((action) => action.path.endsWith('/remove-member'))).toBe(true))
fireEvent.click(screen.getByRole('button', { name: 'Review' }))
closeDialogWithEscape()
fireEvent.click(screen.getByRole('button', { name: 'Review' }))
fireEvent.click(screen.getByRole('button', { name: 'Cancel invitation' }))
fireEvent.click(screen.getAllByRole('button', { name: 'Cancel invitation' }).at(-1)!)
await waitFor(() => expect(actions.some((action) => action.path.endsWith('/cancel-invitation'))).toBe(true))
await openOrganizationSection('settings')
fireEvent.click(screen.getByRole('button', { name: 'Edit' }))
closeDialogWithEscape()
fireEvent.click(screen.getByRole('button', { name: 'Edit' }))
fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'Family workspace' } })
fireEvent.click(screen.getByRole('button', { name: 'Save changes' }))
await waitFor(() => expect(actions.some((action) => action.path.endsWith('/update'))).toBe(true))
fireEvent.click(screen.getByRole('button', { name: 'Delete' }))
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
fireEvent.click(screen.getByRole('button', { name: 'Delete' }))
fireEvent.click(screen.getByRole('button', { name: 'Delete organization' }))
await waitFor(() => expect(actions.some((action) => action.path.endsWith('/delete'))).toBe(true))
expect(navigate).toHaveBeenCalledWith({ to: '/organizations' })
})
it('lets a non-owner leave and shows empty Organization authority surfaces', async () => {
const actions: Array<{ path: string; body: unknown }> = []
store.access.consoleOrganizations = [{ organizationId: 'org-family', level: 'developer' }]
server.use(...organizationDetailHandlers('member', { empty: true }), organizationMutationHandler(actions))
renderWithClient(<AccountOrganizationDetailPage organizationId="org-family" />)
expect(await screen.findByText('Member')).toBeTruthy()
await openOrganizationSection('roles')
expect(await screen.findByText('Your Organization Roles')).toBeTruthy()
expect(screen.getByText('Assigned Roles')).toBeTruthy()
await openOrganizationSection('settings')
fireEvent.click(screen.getByRole('button', { name: 'Leave' }))
fireEvent.click(screen.getByRole('button', { name: 'Leave organization' }))
await waitFor(() => expect(actions.some((action) => action.path.endsWith('/leave'))).toBe(true))
})
it('composes capability content into its Organization section', async () => {
server.use(...organizationDetailHandlers('member'))
renderWithClient(
<AccountOrganizationDetailPage
content={<p>Organization applications inventory</p>}
organizationId="org-family"
section="applications"
/>,
)
expect(await screen.findByText('Organization applications inventory')).toBeTruthy()
})
it('keeps Organization management editors open when server mutations fail', async () => {
store.access.platformOperator = true
server.use(
...organizationDetailHandlers('owner'),
http.get(`${base}/api/organizations/org-family/roles`, () =>
json({
items: ['owner', 'admin', 'developer', 'member'].map((key) => ({ key, displayName: key, predefined: true })),
pagination: pagination(4),
}),
),
http.post(`${base}/api/organizations/org-family/invitations`, () =>
json({ message: 'Organization mutation failed.' }, { status: 500 }),
),
http.delete(`${base}/api/organizations/org-family/members/:memberId`, () =>
json({ message: 'Organization mutation failed.' }, { status: 500 }),
),
http.delete(`${base}/api/organizations/org-family/invitations/:invitationId`, () =>
json({ message: 'Organization mutation failed.' }, { status: 500 }),
),
http.post(`${base}/api/auth/organization/:action`, () =>
json({ message: 'Organization mutation failed.' }, { status: 500 }),
),
)
renderWithClient(<AccountOrganizationDetailPage organizationId="org-family" />)
expect(await screen.findByRole('heading', { name: 'Family' })).toBeTruthy()
await openOrganizationSection('members')
fireEvent.click(await screen.findByRole('button', { name: 'Invite member' }))
fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'failed@example.com' } })
fireEvent.click(await screen.findByLabelText('member'))
fireEvent.click(screen.getByRole('button', { name: 'Send invitation' }))
expect(await screen.findByText('Select at least one Role.')).toBeTruthy()
fireEvent.click(screen.getByLabelText('member'))
fireEvent.click(screen.getByRole('button', { name: 'Send invitation' }))
await waitFor(() => expect(screen.getByLabelText('Email')).toBeTruthy())
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
fireEvent.click(screen.getByRole('button', { name: 'Manage' }))
fireEvent.change(screen.getByLabelText('Access level'), { target: { value: 'admin' } })
fireEvent.click(screen.getByRole('button', { name: 'Save access level' }))
await waitFor(() => expect(screen.getByLabelText('Access level')).toBeTruthy())
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
fireEvent.click(screen.getByRole('button', { name: 'Manage' }))
fireEvent.click(screen.getByRole('button', { name: 'Remove member' }))
fireEvent.click(screen.getAllByRole('button', { name: 'Remove member' }).at(-1)!)
await waitFor(() => expect(screen.getByRole('alertdialog')).toBeTruthy())
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
fireEvent.click(screen.getByRole('button', { name: 'Review' }))
fireEvent.click(screen.getByRole('button', { name: 'Cancel invitation' }))
fireEvent.click(screen.getAllByRole('button', { name: 'Cancel invitation' }).at(-1)!)
await waitFor(() => expect(screen.getByRole('alertdialog')).toBeTruthy())
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
await openOrganizationSection('settings')
fireEvent.click(screen.getByRole('button', { name: 'Edit' }))
fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'Failed update' } })
fireEvent.click(screen.getByRole('button', { name: 'Save changes' }))
await waitFor(() => expect(screen.getByLabelText('Name')).toBeTruthy())
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
fireEvent.click(screen.getByRole('button', { name: 'Delete' }))
fireEvent.click(screen.getByRole('button', { name: 'Delete organization' }))
await waitFor(() => expect(screen.getByRole('alertdialog')).toBeTruthy())
})
it('renders Organization load failures', async () => {
server.use(
http.get(`${base}/api/auth/organization/get-full-organization`, () =>
json({ message: 'Organization unavailable' }, { status: 500 }),
),
http.get(`${base}/api/account/organizations/org-family/agents`, () =>
json({ items: [], pagination: pagination(0) }),
),
)
renderWithClient(<AccountOrganizationDetailPage organizationId="org-family" />)
expect((await screen.findByRole('alert')).textContent).toContain('Organization unavailable')
})
it('[spec: account-center/account-organization-teams] creates explicit kebab-case Teams', async () => {
let created: unknown = null
server.use(
...organizationDetailHandlers('owner'),
http.get(`${base}/api/auth/organization/list-teams`, () => json([])),
http.post(`${base}/api/auth/organization/create-team`, async ({ request }) => {
created = await request.json()
return json({ id: 'team-1', name: 'platform-admins', organizationId: 'org-family' })
}),
)
await openOrganizationSection('teams')
expect(await screen.findByText('No Teams')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Create Team' }))
fireEvent.change(screen.getByLabelText('Team name'), { target: { value: 'Platform Admins' } })
fireEvent.submit(screen.getByRole('button', { name: 'Create Team' }).closest('form')!)
expect((await screen.findByRole('alert')).textContent).toContain('Use lowercase letters')
fireEvent.change(screen.getByLabelText('Team name'), { target: { value: 'platform-admins' } })
fireEvent.submit(screen.getByRole('button', { name: 'Create Team' }).closest('form')!)
await waitFor(() => expect(created).toEqual({ organizationId: 'org-family', name: 'platform-admins' }))
})
it('[spec: account-center/account-organization-teams] renames, deletes, and manages Team members', async () => {
const actions: Array<{ action: string; body: unknown }> = []
server.use(
...organizationDetailHandlers('owner'),
http.get(`${base}/api/auth/organization/list-teams`, () =>
json([{ id: 'team-1', name: 'platform-admins', organizationId: 'org-family' }]),
),
http.get(`${base}/api/account/organizations/org-family/teams/team-1/members`, () =>
json({
items: [
{
id: 'team-member-1',
teamId: 'team-1',
userId: store.profile.id,
createdAt: '2026-08-01T00:00:00.000Z',
},
],
pagination: { page: Math.floor(0 / 20) + 1, pageSize: 20, totalItems: 1, totalPages: Math.ceil(1 / 20) },
}),
),
http.post(`${base}/api/auth/organization/:action`, async ({ params, request }) => {
actions.push({ action: String(params.action), body: await request.json() })
return json({ success: true })
}),
)
await openOrganizationSection('teams')
expect((await screen.findAllByText('platform-admins')).length).toBeGreaterThan(0)
fireEvent.click(screen.getByRole('button', { name: 'Rename' }))
expect(await screen.findByText(/synchronize downstream RBAC first/)).toBeTruthy()
fireEvent.change(screen.getByLabelText('Team name'), { target: { value: 'cluster-admins' } })
fireEvent.submit(screen.getByRole('button', { name: 'Rename Team' }).closest('form')!)
await waitFor(() => expect(actions.some(({ action }) => action === 'update-team')).toBe(true))
fireEvent.click(screen.getByRole('button', { name: 'Members' }))
expect(await screen.findByRole('heading', { name: 'Manage platform-admins members' })).toBeTruthy()
fireEvent.change(await screen.findByLabelText('Organization member'), { target: { value: 'user-2' } })
fireEvent.click(screen.getByRole('button', { name: 'Add member' }))
await waitFor(() => expect(actions.some(({ action }) => action === 'add-team-member')).toBe(true))
fireEvent.click(screen.getByRole('button', { name: 'Remove' }))
await waitFor(() => expect(actions.some(({ action }) => action === 'remove-team-member')).toBe(true))
fireEvent.click(screen.getByRole('button', { name: 'Done' }))
fireEvent.click(screen.getByRole('button', { name: 'Delete' }))
const confirmation = await screen.findByRole('alertdialog')
expect(within(confirmation).getByText(/Update downstream RBAC mappings/)).toBeTruthy()
fireEvent.click(within(confirmation).getByRole('button', { name: 'Delete Team' }))
await waitFor(() => expect(actions.some(({ action }) => action === 'remove-team')).toBe(true))
})
it('[spec: account-center/account-organization-teams] navigates Team members one server page at a time', async () => {
const requestedPages: number[] = []
server.use(
...organizationDetailHandlers('owner'),
http.get(`${base}/api/auth/organization/list-teams`, () =>
json([{ id: 'team-1', name: 'platform-admins', organizationId: 'org-family' }]),
),
http.get(`${base}/api/account/organizations/org-family/teams/team-1/members`, ({ request }) => {
const page = Number(new URL(request.url).searchParams.get('page'))
requestedPages.push(page)
const offset = (page - 1) * 20
const count = page === 1 ? 20 : 1
return json({
items: Array.from({ length: count }, (_, index) => ({
id: `team-member-${offset + index + 1}`,
teamId: 'team-1',
userId: `team-user-${offset + index + 1}`,
createdAt: '2026-08-01T00:00:00.000Z',
})),
pagination: { page, pageSize: 20, totalItems: 21, totalPages: 2 },
})
}),
http.post(`${base}/api/auth/organization/remove-team-member`, () => json({ success: true })),
)
await openOrganizationSection('teams')
fireEvent.click(await screen.findByRole('button', { name: 'Members' }))
expect(await screen.findByText('1–20 of 21')).toBeTruthy()
expect(requestedPages).toEqual([1])
fireEvent.click(screen.getByRole('button', { name: 'Next' }))
expect(await screen.findByText('21–21 of 21')).toBeTruthy()
expect(requestedPages).toEqual([1, 2])
fireEvent.click(screen.getByRole('button', { name: 'Previous' }))
expect(await screen.findByText('1–20 of 21')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Next' }))
expect(await screen.findByText('21–21 of 21')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Remove' }))
expect(await screen.findByText('1–20 of 21')).toBeTruthy()
})
it('[spec: account-center/account-organization-teams] renders an empty Team membership page', async () => {
server.use(
...organizationDetailHandlers('owner'),
http.get(`${base}/api/auth/organization/list-teams`, () =>
json([{ id: 'team-1', name: 'platform-admins', organizationId: 'org-family' }]),
),
http.get(`${base}/api/account/organizations/org-family/teams/team-1/members`, () =>
json({
items: [],
pagination: { page: Math.floor(0 / 20) + 1, pageSize: 20, totalItems: 0, totalPages: Math.ceil(0 / 20) },
}),
),
)
await openOrganizationSection('teams')
fireEvent.click(await screen.findByRole('button', { name: 'Members' }))
expect(await screen.findByText('This Team has no members.')).toBeTruthy()
expect(screen.queryByRole('button', { name: 'Previous' })).toBeNull()
expect(screen.queryByRole('button', { name: 'Next' })).toBeNull()
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
await waitFor(() => expect(screen.queryByRole('heading', { name: 'Manage platform-admins members' })).toBeNull())
})
it('[spec: account-center/account-organization-teams] surfaces Team membership page failures', async () => {
server.use(
...organizationDetailHandlers('owner'),
http.get(`${base}/api/auth/organization/list-teams`, () =>
json([{ id: 'team-1', name: 'platform-admins', organizationId: 'org-family' }]),
),
http.get(`${base}/api/account/organizations/org-family/teams/team-1/members`, () =>
json({ message: 'Team members unavailable.' }, { status: 500 }),
),
)
await openOrganizationSection('teams')
fireEvent.click(await screen.findByRole('button', { name: 'Members' }))
expect((await screen.findByRole('alert')).textContent).toContain('Team members unavailable.')
})
it('[spec: account-center/account-organization-teams] keeps Team management read-only for members and surfaces errors', async () => {
server.use(
...organizationDetailHandlers('member'),
http.get(`${base}/api/auth/organization/list-teams`, () =>
json({ message: 'Teams unavailable.' }, { status: 500 }),
),
)
await openOrganizationSection('teams')
expect((await screen.findByRole('alert')).textContent).toContain('Teams unavailable.')
expect(screen.queryByRole('button', { name: 'Create Team' })).toBeNull()
expect(screen.queryByRole('button', { name: 'Rename' })).toBeNull()
server.use(
http.get(`${base}/api/auth/organization/list-teams`, () =>
json([{ id: 'team-1', name: 'read-only-team', organizationId: 'org-family' }]),
),
)
await openOrganizationSection('teams')
expect((await screen.findAllByText('read-only-team')).length).toBeGreaterThan(0)
expect(screen.queryByRole('button', { name: 'Rename' })).toBeNull()
})
})
describe('Workbench actions and failures', () => {
it.each(['active', 'inactive'] as const)('changes an %s Agent status from its card', async (status) => {
store.agentIdentities = [agent('agent-active', 'Build Agent', status)]
const change = vi.fn(() => {
store.agentIdentities[0]!.status = status === 'active' ? 'inactive' : 'active'
return new Response(null, { status: 204 })
})
server.use(
http.get(`${base}/api/account/access-requests`, () => json({ items: [], pagination: pagination(0) })),
http.delete(`${base}/api/account/agents/agent-active/activation`, change),
http.put(`${base}/api/account/agents/agent-active/activation`, change),
)
renderWithClient(<AccountOverviewPage />)
fireEvent.click(await screen.findByRole('button', { name: 'Manage Build Agent' }))
fireEvent.click(screen.getByRole('button', { name: status === 'active' ? 'Deactivate Agent' : 'Activate Agent' }))
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull())
expect(change).toHaveBeenCalledOnce()
expect(await screen.findByText(status === 'active' ? 'Disabled' : 'Enabled')).toBeTruthy()
})
it('keeps an Agent drawer open after a failed status change', async () => {
store.agentIdentities = [agent('agent-active', 'Build Agent', 'active')]
const change = vi.fn(() => json({ message: 'Status change unavailable.' }, { status: 503 }))
server.use(
http.get(`${base}/api/account/access-requests`, () => json({ items: [], pagination: pagination(0) })),
http.delete(`${base}/api/account/agents/agent-active/activation`, change),
)
renderWithClient(<AccountOverviewPage />)
fireEvent.click(await screen.findByRole('button', { name: 'Manage Build Agent' }))
fireEvent.click(screen.getByRole('button', { name: 'Deactivate Agent' }))
await waitFor(() => expect(change).toHaveBeenCalledOnce())
expect(screen.getByRole('dialog', { name: 'Build Agent' })).toBeTruthy()
closeDialogWithEscape()
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull())
})
it('preserves a failed deletion and removes the Agent only after a successful retry', async () => {
store.agentIdentities = [agent('agent-active', 'Build Agent', 'active')]
let fail = true
server.use(
http.get(`${base}/api/account/access-requests`, () => json({ items: [], pagination: pagination(0) })),
http.delete(`${base}/api/account/agents/agent-active`, () => {
if (fail) return json({ message: 'Deletion unavailable.' }, { status: 503 })
store.agentIdentities = []
return new Response(null, { status: 204 })
}),
)
renderWithClient(<AccountOverviewPage />)
fireEvent.click(await screen.findByRole('button', { name: 'Manage Build Agent' }))
fireEvent.click(screen.getByRole('button', { name: 'Delete Agent' }))
fireEvent.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Delete Agent' }))
await waitFor(() => expect(screen.queryByRole('alertdialog')).toBeNull())
expect(screen.getByRole('dialog', { name: 'Build Agent' })).toBeTruthy()
fail = false
fireEvent.click(screen.getByRole('button', { name: 'Delete Agent' }))
fireEvent.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Delete Agent' }))
expect(await screen.findByText('Start with your first Agent')).toBeTruthy()
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull())
})
it('opens a card request and removes it after denial', async () => {
store.agentIdentities = [agent('agent-active', 'Build Agent', 'active')]
let pending = true
server.use(
http.get(`${base}/api/account/access-requests`, () =>
json({ items: pending ? [accessRequest()] : [], pagination: pagination(pending ? 1 : 0) }),
),
http.put(`${base}/api/account/access-requests/request-1/decision`, () => {
pending = false
return json({ id: 'request-1', status: 'denied' })
}),
)
renderWithClient(<AccountOverviewPage />)
fireEvent.click(await screen.findByRole('button', { name: '1 requests to review' }))
fireEvent.click(screen.getByRole('button', { name: 'Deny' }))
expect(await screen.findByText("You're all caught up")).toBeTruthy()
expect(screen.queryByRole('dialog')).toBeNull()
})
it.each(['accept', 'reject'] as const)('can retry a failed invitation %s without losing it', async (action) => {
let pending = true
let fail = true
const invitation = {
id: 'invitation-1',
organizationName: 'Acme',
role: 'member',
status: 'pending',
createdAt: '2026-08-01T00:00:00.000Z',
expiresAt: '2099-01-01T00:00:00.000Z',
}
const decision = vi.fn<HttpResponseResolver>(async ({ request }) => {
expect(await request.json()).toEqual({ invitationId: 'invitation-1' })
if (fail) return json({ message: 'Invitation unavailable.' }, { status: 503 })
pending = false
return json({ invitation: { ...invitation, status: action === 'accept' ? 'accepted' : 'rejected' } })
})
server.use(
http.get(`${base}/api/account/access-requests`, () => json({ items: [], pagination: pagination(0) })),
http.get(`${base}/api/auth/organization/list-user-invitations`, () => json(pending ? [invitation] : [])),
http.post(`${base}/api/auth/organization/${action}-invitation`, decision),
)
renderWithClient(<AccountOverviewPage />)
fireEvent.click(await screen.findByRole('button', { name: 'Review invitation' }))
closeDialogWithEscape()
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull())
fireEvent.click(screen.getByRole('button', { name: 'Review invitation' }))
const label = action === 'accept' ? 'Accept' : 'Decline'
fireEvent.click(screen.getByRole('button', { name: label }))
await waitFor(() => expect(decision).toHaveBeenCalledOnce())
expect(screen.getByRole('dialog', { name: 'Organization invitation' })).toBeTruthy()
fail = false
fireEvent.click(screen.getByRole('button', { name: label }))
expect(await screen.findByText("You're all caught up")).toBeTruthy()
expect(screen.queryByRole('dialog')).toBeNull()
})
it.each([
'agents',
'requests',
'invitations',
] as const)('shows a failed %s load without claiming the inbox is clear', async (resource) => {
store.agentIdentities = [agent('agent-active', 'Build Agent', 'active')]
const endpoints = {
agents: '/api/account/agents',
requests: '/api/account/access-requests',
invitations: '/api/auth/organization/list-user-invitations',
}
server.use(
http.get(`${base}${endpoints[resource]}`, () =>
json({ message: 'Workbench data unavailable.' }, { status: 503 }),
),
http.get(`${base}/api/account/access-requests`, () => json({ items: [], pagination: pagination(0) })),
)
renderWithClient(<AccountOverviewPage />)
expect(await screen.findByText('Workbench data unavailable.')).toBeTruthy()
if (resource === 'agents') expect(screen.queryByText('Start with your first Agent')).toBeNull()
else expect(screen.queryByText("You're all caught up")).toBeNull()
if (resource === 'requests') expect(screen.getByText('Unable to load requests')).toBeTruthy()
})
it('does not label a card clear while its requests are loading', async () => {
store.agentIdentities = [agent('agent-active', 'Build Agent', 'active')]
server.use(
http.get(`${base}/api/account/access-requests`, async () => {
await delay(150)
return json({ items: [], pagination: pagination(0) })
}),
)
renderWithClient(<AccountOverviewPage />)
await screen.findByRole('button', { name: 'Manage Build Agent' })
expect(screen.getAllByText('Loading requests…').length).toBeGreaterThan(0)
expect(screen.queryByText('No pending requests')).toBeNull()
expect(await screen.findByText('No pending requests')).toBeTruthy()
})
})
async function openOrganizationSection(section: 'members' | 'teams' | 'roles' | 'settings') {
cleanup()
renderWithClient(<AccountOrganizationDetailPage organizationId="org-family" section={section} />)
await screen.findByRole('heading', { name: 'Family' })
}
function openTab(name: string) {
fireEvent.mouseDown(screen.getByRole('tab', { name }), { button: 0, ctrlKey: false })
}
function closeDialogWithEscape() {
fireEvent.keyDown(document, { key: 'Escape' })
}
function agent(id: string, name: string, status: 'active' | 'inactive') {
return {
id,
issuer: 'https://identity.example.com/api/auth',
subject: `${id}-subject`,
name,
homeSpace: { type: 'personal' as const, userId: store.profile.id },
status,
createdAt: '2026-08-01T00:00:00.000Z',
updatedAt: '2026-08-02T00:00:00.000Z',
bindings: [],
}
}
function accessRequest(): AccessRequestApproval {
return {
id: 'request-1',
agentId: 'agent-active',
agent: { id: 'agent-active', name: 'Build Agent' },
resourceServerId: 'resource-server-1',
resourceServer: { id: 'resource-server-1', name: 'Projects API' },
authorizationDetail: {
name: 'Projects',
description: null,
metadata: {},
authorizationDetailTemplates: [],
},
scopes: ['projects:read'],
authorizationDetails: [],
requiresAccountConnection: true,
reason: 'Read projects',
status: 'pending',
interaction: {
type: 'user-approval',
status: 'pending',
url: 'https://identity.example.com/approve',
expiresAt: '2099-01-01T00:00:00.000Z',
},
links: { self: '/api/access-requests/request-1', credentials: null },
credentialOffer: null,
expiresAt: '2099-01-01T00:00:00.000Z',
decidedAt: null,
createdAt: '2026-08-01T00:00:00.000Z',
updatedAt: '2026-08-01T00:00:00.000Z',
}
}
function pagination(total: number) {
return { page: 1, pageSize: 100, totalItems: total, totalPages: Math.ceil(total / 100) }
}
function organizationDetailHandlers(role: string, options: { empty?: boolean } = {}) {
const agents = options.empty ? [] : [agent('agent-family', 'Family assistant', 'active')]
return [
http.get(`${base}/api/auth/organization/get-full-organization`, () =>
json({
id: 'org-family',
name: 'Family',
slug: 'family',
createdAt: '2026-08-01T00:00:00.000Z',
members: [
{
id: 'member-current',
userId: store.profile.id,
role,
user: { id: store.profile.id, name: store.profile.displayName, email: store.profile.email },
createdAt: '2026-08-01T00:00:00.000Z',
},
{
id: 'member-other',
userId: 'user-2',
role: 'developer',
user: { id: 'user-2', name: 'Alex Doe', email: 'alex@example.com' },
createdAt: '2026-08-02T00:00:00.000Z',
},
],
invitations: options.empty
? []
: [
{
id: 'invitation-1',
email: 'invitee@example.com',
role: 'member',
status: 'pending',
createdAt: '2026-08-03T00:00:00.000Z',
expiresAt: '2099-08-10T00:00:00.000Z',
},
{
id: 'invitation-old',
email: 'old@example.com',
role: 'member',
status: 'accepted',
createdAt: '2026-07-03T00:00:00.000Z',
expiresAt: '2026-07-10T00:00:00.000Z',
},
],
}),
),
http.get(`${base}/api/account/organizations/org-family/agents`, () =>
json({ items: agents, pagination: pagination(agents.length) }),
),
]
}
function organizationMutationHandler(actions: Array<{ path: string; body: unknown }>) {
return http.post(`${base}/api/auth/organization/:action`, async ({ params, request }) => {
const body = await request.json()
actions.push({ path: `/api/auth/organization/${String(params.action)}`, body })
return json({ success: true })
})
}