Skip to content

Commit 9fb0c08

Browse files
fix(chat): gate workflow creation on write access, pin the key in schedule tests
The zero-workflow landing offered "Create workflow" to every member. Creation navigates optimistically, so a read-only member was sent to a workflow the server had already refused to create, with the failure never surfaced. Gate both entry points — the empty state and the sidebar's "New workflow" row — on the same `canEdit` check the rest of the sidebar uses, and tell read-only members who can make one instead of offering an action that cannot succeed. The schedule-execution tests only passed locally because vitest loads the developer's own `.env`, which supplied COPILOT_API_KEY; CI has none, so the prompt-job claim guard skipped the claims those cases assert on. Pin the key through the env mock so the suite states its own preconditions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ErcRgvi7VQBeKDQ3MBMha
1 parent 86a8553 commit 9fb0c08

3 files changed

Lines changed: 40 additions & 6 deletions

File tree

apps/sim/app/api/schedules/execute/route.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
requestUtilsMockFns,
1010
resetDbChainMock,
1111
resetEnvFlagsMock,
12+
resetEnvMock,
13+
setEnv,
1214
setEnvFlags,
1315
} from '@sim/testing'
1416
import { type NextRequest, NextResponse } from 'next/server'
@@ -275,7 +277,10 @@ function createMockRequest(): NextRequest {
275277
} as NextRequest
276278
}
277279

278-
afterAll(resetEnvFlagsMock)
280+
afterAll(() => {
281+
resetEnvFlagsMock()
282+
resetEnvMock()
283+
})
279284

280285
describe('Scheduled Workflow Execution API Route', () => {
281286
beforeEach(() => {
@@ -290,6 +295,9 @@ describe('Scheduled Workflow Execution API Route', () => {
290295
dbChainMockFns.execute.mockResolvedValue([{ acquired: true }] as never)
291296
requestUtilsMockFns.mockGenerateRequestId.mockReturnValue('test-request-id')
292297
setEnvFlags({ isTriggerDevEnabled: false, isHosted: false, isProd: false, isDev: true })
298+
// Prompt-job claims are skipped without the mothership credential; pin it so
299+
// these cases do not depend on whether the runner happens to have a .env.
300+
setEnv({ COPILOT_API_KEY: 'test-api-key' })
293301
mockShouldExecuteInline.mockReturnValue(false)
294302
mockEnqueue.mockReset()
295303
mockEnqueue.mockResolvedValue('job-id-1')

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,9 @@ export const Sidebar = memo(function Sidebar({
747747
icon: isChatEnabled ? Home : Plus,
748748
href: isChatEnabled ? `/workspace/${workspaceId}/home` : undefined,
749749
onClick: isChatEnabled ? undefined : createWorkflow,
750+
// Creation navigates optimistically, so a read-only member would land
751+
// on a workflow the server declined to create.
752+
hidden: !isChatEnabled && !permissionsLoading && !canEdit,
750753
},
751754
{
752755
id: 'search',
@@ -763,7 +766,14 @@ export const Sidebar = memo(function Sidebar({
763766
hidden: permissionConfig.hideIntegrationsTab,
764767
},
765768
].filter((item) => !item.hidden),
766-
[workspaceId, openSearchModal, createWorkflow, permissionConfig.hideIntegrationsTab]
769+
[
770+
workspaceId,
771+
openSearchModal,
772+
createWorkflow,
773+
canEdit,
774+
permissionsLoading,
775+
permissionConfig.hideIntegrationsTab,
776+
]
767777
)
768778

769779
const workspaceNavItems = useMemo(

apps/sim/app/workspace/[workspaceId]/w/page.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Chip } from '@sim/emcn'
55
import { createLogger } from '@sim/logger'
66
import { useParams, useRouter } from 'next/navigation'
77
import { ReactFlowProvider } from 'reactflow'
8+
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
89
import { Panel, Terminal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components'
910
import { useWorkflowOperations } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
1011
import { useWorkflows } from '@/hooks/queries/workflows'
@@ -33,6 +34,7 @@ export default function WorkflowsPage() {
3334

3435
const { data: workflows = [], isLoading, isError, isPlaceholderData } = useWorkflows(workspaceId)
3536
const { handleCreateWorkflow, isCreatingWorkflow } = useWorkflowOperations({ workspaceId })
37+
const { canEdit, isLoading: permissionsLoading } = useUserPermissionsContext()
3638

3739
// An id rather than the filtered array: `data` defaults to a fresh `[]` while
3840
// the query has no data, so an array dependency would re-fire this on every
@@ -60,6 +62,7 @@ export default function WorkflowsPage() {
6062
* longer a landing option, so it has to offer a way out rather than spin.
6163
*/
6264
const isEmpty = !isResolving && !isError && !firstWorkflowId
65+
const canCreate = !permissionsLoading && canEdit
6366

6467
return (
6568
<div className='flex h-full w-full flex-col overflow-hidden bg-[var(--bg)]'>
@@ -81,11 +84,24 @@ export default function WorkflowsPage() {
8184
<div className='flex flex-col items-center gap-3 text-center text-[var(--text-secondary)]'>
8285
<div>
8386
<p className='font-medium text-small'>No workflows yet</p>
84-
<p className='mt-1 text-caption'>Create one to start building.</p>
87+
<p className='mt-1 text-caption'>
88+
{canCreate
89+
? 'Create one to start building.'
90+
: 'Ask a workspace admin to create one.'}
91+
</p>
8592
</div>
86-
<Chip variant='primary' onClick={handleCreateWorkflow} disabled={isCreatingWorkflow}>
87-
{isCreatingWorkflow ? 'Creating…' : 'Create workflow'}
88-
</Chip>
93+
{/* The create mutation navigates optimistically, so offering it
94+
without write access would strand a read-only member on a
95+
workflow the server declined to create. */}
96+
{canCreate && (
97+
<Chip
98+
variant='primary'
99+
onClick={handleCreateWorkflow}
100+
disabled={isCreatingWorkflow}
101+
>
102+
{isCreatingWorkflow ? 'Creating…' : 'Create workflow'}
103+
</Chip>
104+
)}
89105
</div>
90106
) : (
91107
<Spinner />

0 commit comments

Comments
 (0)