Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions apps/sim/lib/mothership/chat/payload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
billingSubscriptionMock,
billingSubscriptionMockFns,
} from '@sim/testing/mocks/billing-subscription.mock'
import { featureFlagsMock, featureFlagsMockFns } from '@sim/testing/mocks/feature-flags.mock'
import {
integrationsAvailabilityMock,
integrationsAvailabilityMockFns,
Expand Down Expand Up @@ -51,6 +52,7 @@ vi.mock('@/lib/mothership/chat/workspace-inventory', () => ({
truncated: [],
})),
}))
vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock)
vi.mock('@/lib/dashboards/application/availability', () => ({
readDashboardAvailability: { execute: mockDashboardAvailability },
}))
Expand Down Expand Up @@ -375,19 +377,34 @@ describe('buildCopilotRequestPayload', () => {
}
)

it('never grants the workspace-only dashboards entitlement to an organization chat', async () => {
mockDashboardAvailability.mockResolvedValue(true)
const payload = await buildCopilotRequestPayload({
message: 'Show my dashboard',
userId: 'actor',
userMessageId: 'message-1',
organizationId: 'org-1',
principal: { kind: 'session' as const, userId: 'actor' },
mode: 'agent',
model: '',
})
expect(payload.entitlements).toEqual([])
})
it.each([
['dashboards', 'dashboards', true],
['dashboards', 'dashboards', false],
['tests', 'workflow-tests', true],
['tests', 'workflow-tests', false],
['changelog', 'changelog', true],
['changelog', 'changelog', false],
] as const)(
'grants an organization Build chat the %s entitlement from the %s rollout: %s',
async (entitlement, rolloutFlag, enabled) => {
featureFlagsMockFns.mockIsFeatureEnabled.mockImplementation(
async (flag) => enabled && flag === rolloutFlag
)
const payload = await buildCopilotRequestPayload({
message: 'Build it',
userId: 'actor',
userMessageId: 'message-1',
organizationId: 'org-1',
principal: { kind: 'session' as const, userId: 'actor' },
mode: 'agent',
model: '',
})
expect(payload.entitlements).toEqual(enabled ? [entitlement] : [])
expect(featureFlagsMockFns.mockIsFeatureEnabled).toHaveBeenCalledWith(rolloutFlag, {
orgId: 'org-1',
})
}
)

beforeEach(() => {
mockTrackChatUpload.mockResolvedValue({ displayName: 'payroll.xlsx' })
Expand Down
13 changes: 10 additions & 3 deletions apps/sim/lib/mothership/entitlements.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { Principal } from '@sim/auth/principal'
import { readChangelogAvailability } from '@/lib/changelog/application/availability'
import { isChangelogEnabled } from '@/lib/changelog/feature-flag'
import { readDashboardAvailability } from '@/lib/dashboards/application/availability'
import { isDashboardsEnabled } from '@/lib/dashboards/feature-flag'
import { ENTITLEMENTS, type Entitlement } from '@/lib/mothership/generated/protocol'
import { readWorkflowTestAvailability } from '@/lib/workflow-tests/application/availability'
import { isWorkflowTestsEnabled } from '@/lib/workflow-tests/feature-flag'

/** The owner of one chat turn: exactly one of a workspace or an organization. */
export interface EntitlementOwner {
Expand All @@ -22,9 +25,10 @@ interface OrganizationOwner {
}

/**
* Each entitlement declares the chat scopes it exists in. A scope it does not
* declare is never granted, so a workspace-only capability cannot leak into an
* organization chat that has no workspace to run it against.
* Each entitlement declares the chat scopes it exists in; a scope it does not declare is never
* granted. An organization chat in Build or Plan works in the organization's workspaces, so a
* workspace capability exists there whenever the organization has it rolled out. The operation
* behind it still checks the workspace it runs against.
*/
interface EntitlementEvaluator {
workspace?: (owner: WorkspaceOwner) => Promise<boolean>
Expand All @@ -49,14 +53,17 @@ const EVALUATORS: Record<Entitlement, EntitlementEvaluator> = {
[ENTITLEMENTS.dashboards]: {
workspace: ({ principal, workspaceId }) =>
readDashboardAvailability.execute({ principal, input: { workspaceId } }),
organization: ({ organizationId }) => isDashboardsEnabled(organizationId),
},
[ENTITLEMENTS.tests]: {
workspace: ({ principal, workspaceId }) =>
readWorkflowTestAvailability.execute({ principal, input: { workspaceId } }),
organization: ({ organizationId }) => isWorkflowTestsEnabled(organizationId),
},
[ENTITLEMENTS.changelog]: {
workspace: ({ principal, workspaceId }) =>
readChangelogAvailability.execute({ principal, input: { workspaceId } }),
organization: ({ organizationId }) => isChangelogEnabled(organizationId),
},
}

Expand Down
Loading