Browse Source

Proposal voting & status tests

Leszek Wiesner 3 years ago
parent
commit
548b7fd8e4

+ 38 - 38
tests/integration-tests/proposal-parameters.json

@@ -1,104 +1,104 @@
 {
   "set_max_validator_count_proposal": {
-      "voting_period": 10,
+      "voting_period": 20,
       "grace_period": 0
   },
   "runtime_upgrade_proposal": {
-    "voting_period": 10,
-    "grace_period": 10,
+    "voting_period": 20,
+    "grace_period": 20,
     "constitutionality": 2
   },
   "signal_proposal": {
-    "voting_period": 10,
+    "voting_period": 20,
     "grace_period": 0
   },
   "funding_request_proposal": {
-      "voting_period": 10,
-      "grace_period": 5
+      "voting_period": 20,
+      "grace_period": 10
   },
   "create_working_group_lead_opening_proposal": {
-      "voting_period": 10,
+      "voting_period": 20,
       "grace_period": 0
   },
   "fill_working_group_lead_opening_proposal": {
-      "voting_period": 10,
+      "voting_period": 20,
       "grace_period": 0
   },
   "update_working_group_budget_proposal": {
-      "voting_period": 10,
+      "voting_period": 20,
       "grace_period": 0
   },
   "decrease_working_group_lead_stake_proposal": {
-      "voting_period": 10,
+      "voting_period": 20,
       "grace_period": 0
   },
   "slash_working_group_lead_proposal": {
-      "voting_period": 10,
+      "voting_period": 20,
       "grace_period": 0
   },
   "set_working_group_lead_reward_proposal": {
-      "voting_period": 10,
+      "voting_period": 20,
       "grace_period": 0
   },
   "terminate_working_group_lead_proposal": {
-      "voting_period": 10,
+      "voting_period": 20,
       "grace_period": 0
   },
   "amend_constitution_proposal": {
-      "voting_period": 10,
-      "grace_period": 10,
+      "voting_period": 20,
+      "grace_period": 20,
       "constitutionality": 2
   },
   "cancel_working_group_lead_opening_proposal": {
-      "voting_period": 10,
+      "voting_period": 20,
       "grace_period": 0
   },
   "set_membership_price_proposal": {
-      "voting_period": 10,
-      "grace_period": 5
+      "voting_period": 20,
+      "grace_period": 10
   },
   "set_council_budget_increment_proposal": {
-      "voting_period": 10,
-      "grace_period": 5
+      "voting_period": 20,
+      "grace_period": 10
   },
   "set_councilor_reward_proposal": {
-      "voting_period": 10,
+      "voting_period": 20,
       "grace_period": 20
   },
   "set_initial_invitation_balance_proposal": {
-      "voting_period": 10,
-      "grace_period": 5
+      "voting_period": 20,
+      "grace_period": 10
   },
   "set_membership_lead_invitation_quota_proposal": {
-      "voting_period": 10,
-      "grace_period": 5
+      "voting_period": 20,
+      "grace_period": 10
   },
   "set_referral_cut_proposal": {
-      "voting_period": 10,
-      "grace_period": 5
+      "voting_period": 20,
+      "grace_period": 10
   },
   "set_invitation_count_proposal": {
-      "voting_period": 10,
-      "grace_period": 5
+      "voting_period": 20,
+      "grace_period": 10
   },
   "create_blog_post_proposal": {
-      "voting_period": 10,
-      "grace_period": 5
+      "voting_period": 20,
+      "grace_period": 10
   },
   "edit_blog_post_proposal": {
-      "voting_period": 10,
-      "grace_period": 5
+      "voting_period": 20,
+      "grace_period": 10
   },
   "lock_blog_post_proposal": {
-      "voting_period": 10,
-      "grace_period": 5
+      "voting_period": 20,
+      "grace_period": 10
   },
   "unlock_blog_post_proposal": {
-      "voting_period": 10,
-      "grace_period": 5
+      "voting_period": 20,
+      "grace_period": 10
   },
   "veto_proposal_proposal": {
-      "voting_period": 10,
+      "voting_period": 20,
       "grace_period": 0
   }
 }

+ 45 - 30
tests/integration-tests/src/Api.ts

@@ -26,6 +26,7 @@ import {
   OpeningFilledEventDetails,
   ProposalsEngineEventName,
   ProposalCreatedEventDetails,
+  ProposalType,
 } from './types'
 import {
   ApplicationId,
@@ -37,7 +38,8 @@ import {
 } from '@joystream/types/working-group'
 import { DeriveAllSections } from '@polkadot/api/util/decorate'
 import { ExactDerive } from '@polkadot/api-derive'
-import { ProposalId } from '@joystream/types/proposals'
+import { ProposalId, ProposalParameters } from '@joystream/types/proposals'
+import { BLOCKTIME, proposalTypeToProposalParamsKey } from './consts'
 
 export class ApiFactory {
   private readonly api: ApiPromise
@@ -452,14 +454,18 @@ export class Api {
     )
   }
 
-  public async untilProposalsCanBeCreated(numberOfProposals = 0, intervalMs = 6000, timeoutMs = 180000): Promise<void> {
+  public async untilProposalsCanBeCreated(
+    numberOfProposals = 1,
+    intervalMs = BLOCKTIME,
+    timeoutMs = 180000
+  ): Promise<void> {
     await Utils.until(
       `${numberOfProposals} proposals can be created`,
       async ({ debug }) => {
         const { maxActiveProposalLimit } = this.consts.proposalsEngine
         const activeProposalsN = await this.query.proposalsEngine.activeProposalCount()
         debug(`Currently active proposals: ${activeProposalsN.toNumber()}/${maxActiveProposalLimit.toNumber()}`)
-        return activeProposalsN.lt(maxActiveProposalLimit)
+        return maxActiveProposalLimit.sub(activeProposalsN).toNumber() >= numberOfProposals
       },
       intervalMs,
       timeoutMs
@@ -468,33 +474,42 @@ export class Api {
 
   public async untilCouncilStage(
     targetStage: 'Announcing' | 'Voting' | 'Revealing' | 'Idle',
-    blocksReserve = 3
+    blocksReserve = 3,
+    intervalMs = BLOCKTIME
   ): Promise<void> {
-    await Utils.until(`council stage ${targetStage} (+${blocksReserve} blocks reserve)`, async ({ debug }) => {
-      const currentCouncilStage = await this.query.council.stage()
-      const currentElectionStage = await this.query.referendum.stage()
-      const currentStage = currentCouncilStage.stage.isOfType('Election')
-        ? (currentElectionStage.type as 'Voting' | 'Revealing')
-        : (currentCouncilStage.stage.type as 'Announcing' | 'Idle')
-      const currentStageStartedAt = currentCouncilStage.stage.isOfType('Election')
-        ? currentElectionStage.asType(currentElectionStage.type as 'Voting' | 'Revealing').started
-        : currentCouncilStage.changed_at
-
-      const currentBlock = await this.getBestBlock()
-      const { announcingPeriodDuration, idlePeriodDuration } = this.consts.council
-      const { voteStageDuration, revealStageDuration } = this.consts.referendum
-      const durationByStage = {
-        'Announcing': announcingPeriodDuration,
-        'Voting': voteStageDuration,
-        'Revealing': revealStageDuration,
-        'Idle': idlePeriodDuration,
-      } as const
-
-      const currentStageEndsIn = currentStageStartedAt.add(durationByStage[currentStage]).sub(currentBlock)
-
-      debug(`Current stage: ${currentStage}, blocks left: ${currentStageEndsIn.toNumber()}`)
-
-      return currentStage === targetStage && currentStageEndsIn.gten(blocksReserve)
-    })
+    await Utils.until(
+      `council stage ${targetStage} (+${blocksReserve} blocks reserve)`,
+      async ({ debug }) => {
+        const currentCouncilStage = await this.query.council.stage()
+        const currentElectionStage = await this.query.referendum.stage()
+        const currentStage = currentCouncilStage.stage.isOfType('Election')
+          ? (currentElectionStage.type as 'Voting' | 'Revealing')
+          : (currentCouncilStage.stage.type as 'Announcing' | 'Idle')
+        const currentStageStartedAt = currentCouncilStage.stage.isOfType('Election')
+          ? currentElectionStage.asType(currentElectionStage.type as 'Voting' | 'Revealing').started
+          : currentCouncilStage.changed_at
+
+        const currentBlock = await this.getBestBlock()
+        const { announcingPeriodDuration, idlePeriodDuration } = this.consts.council
+        const { voteStageDuration, revealStageDuration } = this.consts.referendum
+        const durationByStage = {
+          'Announcing': announcingPeriodDuration,
+          'Voting': voteStageDuration,
+          'Revealing': revealStageDuration,
+          'Idle': idlePeriodDuration,
+        } as const
+
+        const currentStageEndsIn = currentStageStartedAt.add(durationByStage[currentStage]).sub(currentBlock)
+
+        debug(`Current stage: ${currentStage}, blocks left: ${currentStageEndsIn.toNumber()}`)
+
+        return currentStage === targetStage && currentStageEndsIn.gten(blocksReserve)
+      },
+      intervalMs
+    )
+  }
+
+  public proposalParametersByType(type: ProposalType): ProposalParameters {
+    return this.api.consts.proposalsCodex[proposalTypeToProposalParamsKey[type]]
   }
 }

+ 2 - 2
tests/integration-tests/src/Fixture.ts

@@ -9,12 +9,14 @@ import { AnyQueryNodeEvent, EventDetails } from './types'
 
 export abstract class BaseFixture {
   protected readonly api: Api
+  protected debug: Debugger.Debugger
   private _executed = false
   // The reason of the "Unexpected" failure of running the fixture
   private _err: Error | undefined = undefined
 
   constructor(api: Api) {
     this.api = api
+    this.debug = Debugger(`fixture:${this.constructor.name}`)
   }
 
   // Derviced classes must not override this
@@ -89,12 +91,10 @@ export abstract class BaseFixture {
 
 export abstract class BaseQueryNodeFixture extends BaseFixture {
   protected readonly query: QueryNodeApi
-  protected debug: Debugger.Debugger
 
   constructor(api: Api, query: QueryNodeApi) {
     super(api)
     this.query = query
-    this.debug = Debugger(`fixture:${this.constructor.name}`)
   }
 
   public async runQueryNodeChecks(): Promise<void> {

+ 14 - 1
tests/integration-tests/src/QueryNodeApi.ts

@@ -179,10 +179,15 @@ import {
   GetStakingAccountAddedEventsByEventIdsQuery,
   GetStakingAccountAddedEventsByEventIdsQueryVariables,
   GetStakingAccountAddedEventsByEventIds,
+  ProposalVotedEventFieldsFragment,
+  GetProposalVotedEventsByEventIdsQuery,
+  GetProposalVotedEventsByEventIdsQueryVariables,
+  GetProposalVotedEventsByEventIds,
 } from './graphql/generated/queries'
 import { Maybe } from './graphql/generated/schema'
 import { OperationDefinitionNode } from 'graphql'
 import { ProposalId } from '@joystream/types/proposals'
+import { BLOCKTIME } from './consts'
 export class QueryNodeApi {
   private readonly queryNodeProvider: ApolloClient<NormalizedCacheObject>
   private readonly debug: Debugger.Debugger
@@ -200,7 +205,7 @@ export class QueryNodeApi {
     query: () => Promise<QueryResultT>,
     assertResultIsValid: (res: QueryResultT) => void,
     timeoutMs = 60000,
-    retryTimeMs = 15000
+    retryTimeMs = BLOCKTIME * 3
   ): Promise<QueryResultT> {
     const label = query.toString().replace(/^.*\.([A-za-z0-9]+\(.*\))$/g, '$1')
     const retryDebug = this.tryDebug.extend(label).extend('retry')
@@ -691,4 +696,12 @@ export class QueryNodeApi {
       'proposals'
     )
   }
+
+  public async getProposalVotedEvents(events: EventDetails[]): Promise<ProposalVotedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetProposalVotedEventsByEventIdsQuery,
+      GetProposalVotedEventsByEventIdsQueryVariables
+    >(GetProposalVotedEventsByEventIds, { eventIds }, 'proposalVotedEvents')
+  }
 }

+ 41 - 1
tests/integration-tests/src/consts.ts

@@ -1,6 +1,13 @@
 import { WorkingGroup } from '@joystream/types/common'
+import { AugmentedConsts } from '@polkadot/api/types'
 import BN from 'bn.js'
-import { WorkingGroupModuleName } from './types'
+import { ProposalType, WorkingGroupModuleName } from './types'
+
+// Dummy const type validation function (see: https://stackoverflow.com/questions/57069802/as-const-is-ignored-when-there-is-a-type-definition)
+export const validateType = <T>(obj: T) => obj
+
+// Test chain blocktime
+export const BLOCKTIME = 6000
 
 export const MINIMUM_STAKING_ACCOUNT_BALANCE = 200
 export const MIN_APPLICATION_STAKE = new BN(2000)
@@ -34,3 +41,36 @@ export function getWorkingGroupModuleName(group: WorkingGroup): WorkingGroupModu
 
   throw new Error(`Unsupported working group: ${group}`)
 }
+
+// Proposals
+
+export const proposalTypeToProposalParamsKey = {
+  'AmendConstitution': 'amendConstitutionProposalParameters',
+  'CancelWorkingGroupLeadOpening': 'cancelWorkingGroupLeadOpeningProposalParameters',
+  'CreateBlogPost': 'createBlogPostProposalParameters',
+  'CreateWorkingGroupLeadOpening': 'createWorkingGroupLeadOpeningProposalParameters',
+  'DecreaseWorkingGroupLeadStake': 'decreaseWorkingGroupLeadStakeProposalParameters',
+  'EditBlogPost': 'editBlogPostProoposalParamters',
+  'FillWorkingGroupLeadOpening': 'fillWorkingGroupOpeningProposalParameters',
+  'FundingRequest': 'fundingRequestProposalParameters',
+  'LockBlogPost': 'lockBlogPostProposalParameters',
+  'RuntimeUpgrade': 'runtimeUpgradeProposalParameters',
+  'SetCouncilBudgetIncrement': 'setCouncilBudgetIncrementProposalParameters',
+  'SetCouncilorReward': 'setCouncilorRewardProposalParameters',
+  'SetInitialInvitationBalance': 'setInitialInvitationBalanceProposalParameters',
+  'SetInitialInvitationCount': 'setInvitationCountProposalParameters',
+  'SetMaxValidatorCount': 'setMaxValidatorCountProposalParameters',
+  'SetMembershipLeadInvitationQuota': 'setMembershipLeadInvitationQuotaProposalParameters',
+  'SetMembershipPrice': 'setMembershipPriceProposalParameters',
+  'SetReferralCut': 'setReferralCutProposalParameters',
+  'SetWorkingGroupLeadReward': 'setWorkingGroupLeadRewardProposalParameters',
+  'Signal': 'signalProposalParameters',
+  'SlashWorkingGroupLead': 'slashWorkingGroupLeadProposalParameters',
+  'TerminateWorkingGroupLead': 'terminateWorkingGroupLeadProposalParameters',
+  'UnlockBlogPost': 'unlockBlogPostProposalParameters',
+  'UpdateWorkingGroupBudget': 'updateWorkingGroupBudgetProposalParameters',
+  'VetoProposal': 'vetoProposalProposalParameters',
+} as const
+
+type ProposalTypeToProposalParamsKeyMap = { [K in ProposalType]: keyof AugmentedConsts<'promise'>['proposalsCodex'] }
+validateType<ProposalTypeToProposalParamsKeyMap>(proposalTypeToProposalParamsKey)

+ 5 - 7
tests/integration-tests/src/fixtures/council/InitializeCouncilFixture.ts → tests/integration-tests/src/fixtures/council/ElectCouncilFixture.ts

@@ -4,13 +4,8 @@ import { blake2AsHex } from '@polkadot/util-crypto'
 import { MINIMUM_STAKING_ACCOUNT_BALANCE } from '../../consts'
 import { assert } from 'chai'
 
-export class InitializeCouncilFixture extends BaseQueryNodeFixture {
+export class ElectCouncilFixture extends BaseQueryNodeFixture {
   public async execute(): Promise<void> {
-    // Assert no council exists
-    if ((await this.api.query.council.councilMembers()).length) {
-      return this.error(new Error('Council election fixture expects no council seats to be filled'))
-    }
-
     const { api, query } = this
     const { councilSize, minNumberOfExtraCandidates } = api.consts.council
     const numberOfCandidates = councilSize.add(minNumberOfExtraCandidates).toNumber()
@@ -84,6 +79,9 @@ export class InitializeCouncilFixture extends BaseQueryNodeFixture {
     await this.api.untilCouncilStage('Idle')
 
     const councilMembers = await api.query.council.councilMembers()
-    assert(councilMembers.length, 'Council initialization failed!')
+    assert.sameMembers(
+      councilMembers.map((m) => m.membership_id.toString()),
+      candidatesMemberIds.slice(0, councilSize.toNumber()).map((id) => id.toString())
+    )
   }
 }

+ 121 - 0
tests/integration-tests/src/fixtures/proposals/AllProposalsOutcomesFixture.ts

@@ -0,0 +1,121 @@
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { ProposalDetails } from '@joystream/types/proposals'
+import { BaseFixture, FixtureRunner } from '../../Fixture'
+import { CreateInterface } from '@joystream/types'
+import {
+  DecideOnProposalStatusFixture,
+  DecideOnProposalStatusParams,
+  DecisionStatus,
+} from './DecideOnProposalStatusFixture'
+import { ProposalDetailsJsonByType, ProposalType } from '../../types'
+import { BuyMembershipHappyCaseFixture } from '../membership'
+import { CreateProposalsFixture, ProposalCreationParams } from './CreateProposalsFixture'
+import { ElectCouncilFixture } from '../council/ElectCouncilFixture'
+import _ from 'lodash'
+
+export type TestedProposal = { details: CreateInterface<ProposalDetails>; expectExecutionFailure?: boolean }
+export type ProposalTestCase = {
+  type: ProposalType
+  details: ProposalDetailsJsonByType
+  decisionStatus: DecisionStatus
+  expectExecutionFailure?: boolean
+}
+
+export class AllProposalsOutcomesFixture extends BaseFixture {
+  protected testedProposals: TestedProposal[]
+  protected query: QueryNodeApi
+
+  public constructor(api: Api, query: QueryNodeApi, testedProposals: TestedProposal[]) {
+    super(api)
+    this.query = query
+    this.testedProposals = testedProposals
+  }
+
+  public async execute(): Promise<void> {
+    const { api, query, testedProposals } = this
+
+    const decisionStatuses: DecisionStatus[] = ['Approved', 'Rejected', 'Slashed']
+
+    const testCases: ProposalTestCase[] = []
+
+    for (const { details, expectExecutionFailure } of testedProposals) {
+      for (const decisionStatus of decisionStatuses) {
+        const proposalDetails = api.createType('ProposalDetails', details)
+        testCases.push({
+          type: proposalDetails.type,
+          details: proposalDetails.value,
+          decisionStatus,
+          expectExecutionFailure,
+        })
+      }
+    }
+
+    const memberKeys = (await api.createKeyPairs(testCases.length)).map((key) => key.address)
+    const membersFixture = new BuyMembershipHappyCaseFixture(api, query, memberKeys)
+    await new FixtureRunner(membersFixture).run()
+    const memberIds = membersFixture.getCreatedMembers()
+
+    const { maxActiveProposalLimit } = api.consts.proposalsEngine
+    const proposalsPerBatch = maxActiveProposalLimit.toNumber()
+
+    let batch: ProposalTestCase[]
+    let n = 0
+    while ((batch = testCases.slice(n * proposalsPerBatch, (n + 1) * proposalsPerBatch)).length) {
+      await api.untilProposalsCanBeCreated(proposalsPerBatch)
+      const createProposalsParams: ProposalCreationParams[] = batch.map(({ type, details, decisionStatus }, i) => ({
+        asMember: memberIds[i],
+        title: `${_.startCase(type)}`,
+        description: `Test ${type} proposal to be ${decisionStatus}`,
+        type,
+        details: details,
+      }))
+      this.debug(
+        'Creating proposals:',
+        createProposalsParams.map((p) => p.type)
+      )
+      const createProposalsFixure = new CreateProposalsFixture(api, query, createProposalsParams)
+      await new FixtureRunner(createProposalsFixure).runWithQueryNodeChecks()
+      const proposalIds = createProposalsFixure.getCreatedProposalsIds()
+
+      const decideOnProposalStatusBatch: DecideOnProposalStatusParams[] = batch.map(
+        ({ decisionStatus, expectExecutionFailure }, i) => ({
+          proposalId: proposalIds[i],
+          status: decisionStatus,
+          expectExecutionFailure,
+        })
+      )
+      const decideOnProposalsStatusFixture = new DecideOnProposalStatusFixture(api, query, decideOnProposalStatusBatch)
+      this.debug(
+        'Deciding on proposals:',
+        decideOnProposalStatusBatch.map((p) => ({ porposalId: p.proposalId.toString(), status: p.status }))
+      )
+      await new FixtureRunner(decideOnProposalsStatusFixture).runWithQueryNodeChecks()
+
+      let dormantProposals = decideOnProposalsStatusFixture.getDormantProposalsIds()
+      while (dormantProposals.length) {
+        // Reelect council
+        this.debug('Re-electing council...')
+        const electCouncilFixture = new ElectCouncilFixture(api, query)
+        await new FixtureRunner(electCouncilFixture).run()
+        this.debug('Council re-elected')
+
+        const approveProposalsFixture = new DecideOnProposalStatusFixture(
+          api,
+          query,
+          dormantProposals.map((proposalId) => ({
+            proposalId,
+            status: 'Approved',
+          }))
+        )
+        this.debug(
+          'Deciding on proposals:',
+          dormantProposals.map((proposalId) => ({ porposalId: proposalId.toString(), status: 'Approved' }))
+        )
+        await new FixtureRunner(approveProposalsFixture).runWithQueryNodeChecks()
+        dormantProposals = approveProposalsFixture.getDormantProposalsIds()
+      }
+      ++n
+    }
+  }
+}

+ 2 - 37
tests/integration-tests/src/fixtures/proposals/CreateProposalsFixture.ts

@@ -1,7 +1,7 @@
 import { Api } from '../../Api'
 import { QueryNodeApi } from '../../QueryNodeApi'
 import { ProposalCreatedEventDetails, ProposalDetailsJsonByType, ProposalType } from '../../types'
-import { AugmentedConsts, SubmittableExtrinsic } from '@polkadot/api/types'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { Utils } from '../../utils'
 import { ISubmittableResult } from '@polkadot/types/types/'
 import { ProposalCreatedEventFieldsFragment, ProposalFieldsFragment } from '../../graphql/generated/queries'
@@ -23,40 +23,6 @@ export type ProposalCreationParams<T extends ProposalType = ProposalType> = {
   details: ProposalDetailsJsonByType<T>
 }
 
-// Dummy const type validation function (see: https://stackoverflow.com/questions/57069802/as-const-is-ignored-when-there-is-a-type-definition)
-const validateType = <T>(obj: T) => obj
-
-const proposalTypeToProposalParamsKey = {
-  'AmendConstitution': 'amendConstitutionProposalParameters',
-  'CancelWorkingGroupLeadOpening': 'cancelWorkingGroupLeadOpeningProposalParameters',
-  'CreateBlogPost': 'createBlogPostProposalParameters',
-  'CreateWorkingGroupLeadOpening': 'createWorkingGroupLeadOpeningProposalParameters',
-  'DecreaseWorkingGroupLeadStake': 'decreaseWorkingGroupLeadStakeProposalParameters',
-  'EditBlogPost': 'editBlogPostProoposalParamters',
-  'FillWorkingGroupLeadOpening': 'fillWorkingGroupOpeningProposalParameters',
-  'FundingRequest': 'fundingRequestProposalParameters',
-  'LockBlogPost': 'lockBlogPostProposalParameters',
-  'RuntimeUpgrade': 'runtimeUpgradeProposalParameters',
-  'SetCouncilBudgetIncrement': 'setCouncilBudgetIncrementProposalParameters',
-  'SetCouncilorReward': 'setCouncilorRewardProposalParameters',
-  'SetInitialInvitationBalance': 'setInitialInvitationBalanceProposalParameters',
-  'SetInitialInvitationCount': 'setInvitationCountProposalParameters',
-  'SetMaxValidatorCount': 'setMaxValidatorCountProposalParameters',
-  'SetMembershipLeadInvitationQuota': 'setMembershipLeadInvitationQuotaProposalParameters',
-  'SetMembershipPrice': 'setMembershipPriceProposalParameters',
-  'SetReferralCut': 'setReferralCutProposalParameters',
-  'SetWorkingGroupLeadReward': 'setWorkingGroupLeadRewardProposalParameters',
-  'Signal': 'signalProposalParameters',
-  'SlashWorkingGroupLead': 'slashWorkingGroupLeadProposalParameters',
-  'TerminateWorkingGroupLead': 'terminateWorkingGroupLeadProposalParameters',
-  'UnlockBlogPost': 'unlockBlogPostProposalParameters',
-  'UpdateWorkingGroupBudget': 'updateWorkingGroupBudgetProposalParameters',
-  'VetoProposal': 'vetoProposalProposalParameters',
-} as const
-
-type ProposalTypeToProposalParamsKeyMap = { [K in ProposalType]: keyof AugmentedConsts<'promise'>['proposalsCodex'] }
-validateType<ProposalTypeToProposalParamsKeyMap>(proposalTypeToProposalParamsKey)
-
 export class CreateProposalsFixture extends StandardizedFixture {
   protected events: ProposalCreatedEventDetails[] = []
 
@@ -77,8 +43,7 @@ export class CreateProposalsFixture extends StandardizedFixture {
 
   protected proposalParams(i: number): ProposalParameters {
     const proposalType = this.proposalsParams[i].type
-    const paramsKey = proposalTypeToProposalParamsKey[proposalType]
-    return this.api.consts.proposalsCodex[paramsKey]
+    return this.api.proposalParametersByType(proposalType)
   }
 
   protected async getSignerAccountOrAccounts(): Promise<string[]> {

+ 144 - 0
tests/integration-tests/src/fixtures/proposals/DecideOnProposalStatusFixture.ts

@@ -0,0 +1,144 @@
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { Utils } from '../../utils'
+import { Proposal, ProposalId, VoteKind } from '@joystream/types/proposals'
+import { BaseQueryNodeFixture, FixtureRunner } from '../../Fixture'
+import { ProposalVote } from './index'
+import { CouncilMember } from '@joystream/types/council'
+import { VoteOnProposalsFixture } from './VoteOnProposalsFixture'
+import { ProposalFieldsFragment } from '../../graphql/generated/queries'
+import { assert } from 'chai'
+
+export type DecisionStatus = 'Approved' | 'Rejected' | 'Slashed'
+
+type ResultingProposalStatus =
+  | 'ProposalStatusDormant'
+  | 'ProposalStatusGracing'
+  | 'ProposalStatusExecuted'
+  | 'ProposalStatusExecutionFailed'
+  | 'ProposalStatusSlashed'
+  | 'ProposalStatusRejected'
+
+export type DecideOnProposalStatusParams = {
+  proposalId: ProposalId
+  status: DecisionStatus
+  expectExecutionFailure?: boolean
+}
+
+export class DecideOnProposalStatusFixture extends BaseQueryNodeFixture {
+  protected params: DecideOnProposalStatusParams[]
+  protected voteOnProposalsRunner?: FixtureRunner
+  protected proposals: Proposal[] = []
+
+  public constructor(api: Api, query: QueryNodeApi, params: DecideOnProposalStatusParams[]) {
+    super(api, query)
+    this.params = params
+  }
+
+  public getDormantProposalsIds(): ProposalId[] {
+    if (!this.executed) {
+      throw new Error('Trying to get dormant proposal ids before the fixture is executed')
+    }
+    return this.params
+      .filter((p, i) => this.getExpectedProposalStatus(i) === 'ProposalStatusDormant')
+      .map((p) => p.proposalId)
+  }
+
+  protected getVotes(
+    proposalId: ProposalId,
+    proposal: Proposal,
+    targetStatus: DecisionStatus,
+    councilMembers: CouncilMember[]
+  ): ProposalVote[] {
+    const councilSize = councilMembers.length
+    const {
+      approvalQuorumPercentage,
+      approvalThresholdPercentage,
+      slashingQuorumPercentage,
+      slashingThresholdPercentage,
+    } = proposal.parameters
+    const vote = (vote: keyof typeof VoteKind['typeDefinitions'], i: number): ProposalVote => ({
+      asMember: councilMembers[i].membership_id,
+      proposalId,
+      rationale: `Vote ${vote} by member ${i}`,
+      vote,
+    })
+    if (targetStatus === 'Approved') {
+      const minVotesN = Math.ceil((councilSize * approvalQuorumPercentage.toNumber()) / 100)
+      const minApproveVotesN = Math.ceil((minVotesN * approvalThresholdPercentage.toNumber()) / 100)
+      return Array.from({ length: minVotesN }, (v, i) =>
+        i < minApproveVotesN ? vote('Approve', i) : vote('Abstain', i)
+      )
+    } else if (targetStatus === 'Slashed') {
+      const minVotesN = Math.ceil((councilSize * slashingQuorumPercentage.toNumber()) / 100)
+      const minSlashVotesN = Math.ceil((minVotesN * slashingThresholdPercentage.toNumber()) / 100)
+      return Array.from({ length: minVotesN }, (v, i) => (i < minSlashVotesN ? vote('Slash', i) : vote('Abstain', i)))
+    } else {
+      const otherResultMinThreshold = Math.min(
+        approvalThresholdPercentage.toNumber(),
+        approvalQuorumPercentage.toNumber()
+      )
+      const minRejectOrAbstainVotesN = Math.ceil((councilSize * (100 - otherResultMinThreshold)) / 100)
+      return Array.from({ length: minRejectOrAbstainVotesN }, (v, i) => vote('Reject', i))
+    }
+  }
+
+  protected getExpectedProposalStatus(i: number): ResultingProposalStatus {
+    const params = this.params[i]
+    const proposal = this.proposals[i]
+    if (params.status === 'Approved') {
+      if (proposal.parameters.constitutionality.toNumber() > proposal.nrOfCouncilConfirmations.toNumber() + 1) {
+        return 'ProposalStatusDormant'
+      } else if (proposal.parameters.gracePeriod.toNumber()) {
+        return 'ProposalStatusGracing'
+      } else {
+        return params.expectExecutionFailure ? 'ProposalStatusExecutionFailed' : 'ProposalStatusExecuted'
+      }
+    } else if (params.status === 'Slashed') {
+      return 'ProposalStatusSlashed'
+    } else {
+      return 'ProposalStatusRejected'
+    }
+  }
+
+  protected assertProposalStatusesAreValid(qProposals: ProposalFieldsFragment[]): void {
+    this.params.forEach((params, i) => {
+      const qProposal = qProposals.find((p) => p.id === params.proposalId.toString())
+      Utils.assert(qProposal, 'Query node: Proposal not found')
+      assert.equal(qProposal.status.__typename, this.getExpectedProposalStatus(i))
+    })
+  }
+
+  public async execute(): Promise<void> {
+    const { api, query } = this
+    this.proposals = await this.api.query.proposalsEngine.proposals.multi<Proposal>(
+      this.params.map((p) => p.proposalId)
+    )
+    const councilMembers = await this.api.query.council.councilMembers()
+    Utils.assert(councilMembers.length, 'Council must be elected in order to cast proposal votes')
+    let votes: ProposalVote[] = []
+    this.params.forEach(({ proposalId, status }, i) => {
+      const proposal = this.proposals[i]
+      votes = votes.concat(this.getVotes(proposalId, proposal, status, councilMembers))
+    })
+    this.debug(
+      'Casting votes:',
+      votes.map((v) => ({ proposalId: v.proposalId.toString(), vote: v.vote.toString() }))
+    )
+    const voteOnProposalsFixture = new VoteOnProposalsFixture(api, query, votes)
+    this.voteOnProposalsRunner = new FixtureRunner(voteOnProposalsFixture)
+    await this.voteOnProposalsRunner.run()
+  }
+
+  public async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    Utils.assert(this.voteOnProposalsRunner)
+    await this.voteOnProposalsRunner.runQueryNodeChecks()
+
+    // TODO: ProposalDecisionStatus / ProposalExecutionStatus events
+    const qProposals = await this.query.tryQueryWithTimeout(
+      () => this.query.getProposalsByIds(this.params.map((p) => p.proposalId)),
+      (res) => this.assertProposalStatusesAreValid(res)
+    )
+  }
+}

+ 92 - 0
tests/integration-tests/src/fixtures/proposals/VoteOnProposalsFixture.ts

@@ -0,0 +1,92 @@
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails } from '../../types'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { Utils } from '../../utils'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { ProposalFieldsFragment, ProposalVotedEventFieldsFragment } from '../../graphql/generated/queries'
+import { assert } from 'chai'
+import { Proposal, ProposalId, VoteKind } from '@joystream/types/proposals'
+import { MemberId } from '@joystream/types/common'
+import { StandardizedFixture } from '../../Fixture'
+import { ProposalVoteKind } from '../../graphql/generated/schema'
+
+export type ProposalVote = {
+  asMember: MemberId
+  proposalId: ProposalId
+  vote: keyof typeof VoteKind['typeDefinitions']
+  rationale: string
+}
+
+const voteKindToQueryNodeVoteKind: { [K in keyof typeof VoteKind['typeDefinitions']]: ProposalVoteKind } = {
+  'Abstain': ProposalVoteKind.Abstain,
+  'Approve': ProposalVoteKind.Approve,
+  'Reject': ProposalVoteKind.Reject,
+  'Slash': ProposalVoteKind.Slash,
+}
+
+export class VoteOnProposalsFixture extends StandardizedFixture {
+  protected votes: ProposalVote[]
+  protected proposals: Proposal[] = []
+
+  public constructor(api: Api, query: QueryNodeApi, votes: ProposalVote[]) {
+    super(api, query)
+    this.votes = votes
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string[]> {
+    return this.api.getMemberSigners(this.votes)
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.votes.map(({ asMember, proposalId, vote, rationale }) =>
+      this.api.tx.proposalsEngine.vote(asMember, proposalId, vote, rationale)
+    )
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveProposalsEngineEventDetails(result, 'Voted')
+  }
+
+  protected assertQueriedProposalsAreValid(
+    qProposals: ProposalFieldsFragment[],
+    qEvents: ProposalVotedEventFieldsFragment[]
+  ): void {
+    this.events.map((e, i) => {
+      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
+      const vote = this.votes[i]
+      const qProposal = qProposals.find((p) => p.id === vote.proposalId.toString())
+      Utils.assert(qProposal, 'Query node: Proposal not found')
+      assert.include(
+        qProposal.votes.map((v) => v.id),
+        qEvent.id
+      )
+    })
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: ProposalVotedEventFieldsFragment, i: number): void {
+    const vote = this.votes[i]
+    assert.equal(qEvent.proposal.id, vote.proposalId.toString())
+    assert.equal(qEvent.voter.id, vote.asMember.toString())
+    assert.equal(qEvent.votingRound, this.proposals[i].nrOfCouncilConfirmations.toNumber() + 1)
+    assert.equal(qEvent.voteKind, voteKindToQueryNodeVoteKind[vote.vote])
+    assert.equal(qEvent.rationale, vote.rationale)
+  }
+
+  public async execute(): Promise<void> {
+    this.proposals = await this.api.query.proposalsEngine.proposals.multi<Proposal>(this.votes.map((v) => v.proposalId))
+    await super.execute()
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getProposalVotedEvents(this.events),
+      (result) => this.assertQueryNodeEventsAreValid(result)
+    )
+
+    // Query the proposals
+    const qProposals = await this.query.getProposalsByIds(this.votes.map((v) => v.proposalId))
+    this.assertQueriedProposalsAreValid(qProposals, qEvents)
+  }
+}

+ 6 - 0
tests/integration-tests/src/fixtures/proposals/index.ts

@@ -1 +1,7 @@
 export { CreateProposalsFixture, ProposalCreationParams } from './CreateProposalsFixture'
+export { VoteOnProposalsFixture, ProposalVote } from './VoteOnProposalsFixture'
+export {
+  DecideOnProposalStatusFixture,
+  DecideOnProposalStatusParams,
+  DecisionStatus,
+} from './DecideOnProposalStatusFixture'

+ 104 - 81
tests/integration-tests/src/flows/proposals/index.ts

@@ -1,35 +1,25 @@
 import { FlowProps } from '../../Flow'
-import { CreateProposalsFixture } from '../../fixtures/proposals'
-
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'
-import { BuyMembershipHappyCaseFixture } from '../../fixtures/membership'
-import { ProposalDetailsJsonByType, ProposalType } from '../../types'
+import { AddStakingAccountsHappyCaseFixture, BuyMembershipHappyCaseFixture } from '../../fixtures/membership'
 import { Utils } from '../../utils'
-import { DEFAULT_OPENING_PARAMS } from '../../fixtures/workingGroups'
+import {
+  ApplyOnOpeningsHappyCaseFixture,
+  CreateOpeningsFixture,
+  DEFAULT_OPENING_PARAMS,
+} from '../../fixtures/workingGroups'
 import { OpeningMetadata } from '@joystream/metadata-protobuf'
-import _ from 'lodash'
-import { InitializeCouncilFixture } from '../../fixtures/council'
+import { AllProposalsOutcomesFixture, TestedProposal } from '../../fixtures/proposals/AllProposalsOutcomesFixture'
+import { ElectCouncilFixture } from '../../fixtures/council/ElectCouncilFixture'
 
-// const testProposalDetails = {
 //   // TODO:
-//   // RuntimeUpgrade: [],
-//   // CreateBlogPost: [],
-//   // // Requires a proposal:
-//   // VetoProposal: [],
-//   // // Requires an opening:
-//   // CancelWorkingGroupLeadOpening: [],
-//   // FillWorkingGroupLeadOpening: [],
-//   // // Requires a lead:
-//   // DecreaseWorkingGroupLeadStake: [],
-//   // SetWorkingGroupLeadReward: [],
-//   // SlashWorkingGroupLead: [],
-//   // TerminateWorkingGroupLead: [],
-//   // // Requires a blog post
-//   // EditBlogPost: [],
-//   // LockBlogPost: [],
-//   // UnlockBlogPost: [],
-// }
+//   // RuntimeUpgradeProposal
+//   // VetoProposal
+//   // TODO: Blog-related proposals:
+//   // CreateBlogPost
+//   // EditBlogPostProposal
+//   // LockBlogPostProposal
+//   // UnlockBlogPostProposal
 
 export default async function creatingProposals({ api, query }: FlowProps): Promise<void> {
   const debug = Debugger('flow:creating-proposals')
@@ -37,70 +27,103 @@ export default async function creatingProposals({ api, query }: FlowProps): Prom
   api.enableDebugTxLogs()
 
   debug('Initializing council...')
-  const initializeCouncilFixture = new InitializeCouncilFixture(api, query)
-  await new FixtureRunner(initializeCouncilFixture).run()
+  const electCouncilFixture = new ElectCouncilFixture(api, query)
+  await new FixtureRunner(electCouncilFixture).run()
   debug('Council initialized')
 
+  debug('Creating test lead openings and applications...')
+  const createLeadOpeningsFixture = new CreateOpeningsFixture(
+    api,
+    query,
+    'storageWorkingGroup',
+    [DEFAULT_OPENING_PARAMS, DEFAULT_OPENING_PARAMS],
+    true
+  )
+  await new FixtureRunner(createLeadOpeningsFixture).run()
+  const [openingToCancelId, openingToFillId] = createLeadOpeningsFixture.getCreatedOpeningIds()
+
+  const [applicantControllerAcc, applicantStakingAcc] = (await api.createKeyPairs(2)).map((kp) => kp.address)
+  const buyMembershipFixture = new BuyMembershipHappyCaseFixture(api, query, [applicantControllerAcc])
+  await new FixtureRunner(buyMembershipFixture).run()
+  const [applicantMemberId] = buyMembershipFixture.getCreatedMembers()
+
+  const addStakingAccountsFixture = new AddStakingAccountsHappyCaseFixture(api, query, [
+    { asMember: applicantMemberId, account: applicantStakingAcc, stakeAmount: DEFAULT_OPENING_PARAMS.stake },
+  ])
+  await new FixtureRunner(addStakingAccountsFixture).run()
+
+  const applyOnOpeningFixture = new ApplyOnOpeningsHappyCaseFixture(api, query, 'storageWorkingGroup', [
+    {
+      openingId: openingToFillId,
+      applicants: [
+        {
+          memberId: applicantMemberId,
+          stakingAccount: applicantStakingAcc,
+          roleAccount: applicantControllerAcc,
+          rewardAccount: applicantControllerAcc,
+        },
+      ],
+      openingMetadata: DEFAULT_OPENING_PARAMS.metadata,
+    },
+  ])
+
+  await new FixtureRunner(applyOnOpeningFixture).run()
+  const [applicationId] = applyOnOpeningFixture.getCreatedApplicationsByOpeningId(openingToFillId)
+  debug('Openings and applicantions created')
+
   const accountsToFund = (await api.createKeyPairs(5)).map((key) => key.address)
-  const proposalsDetails: { [K in ProposalType]?: ProposalDetailsJsonByType<K> } = {
-    AmendConstitution: 'New constitution',
-    FundingRequest: accountsToFund.map((a, i) => ({ account: a, amount: (i + 1) * 1000 })),
-    Signal: 'Text',
-    CreateWorkingGroupLeadOpening: {
-      description: Utils.metadataToBytes(OpeningMetadata, DEFAULT_OPENING_PARAMS.metadata),
-      reward_per_block: 100,
-      stake_policy: {
-        leaving_unstaking_period: 10,
-        stake_amount: 10,
+  const proposalsToTest: TestedProposal[] = [
+    { details: { AmendConstitution: 'New constitution' } },
+    { details: { FundingRequest: accountsToFund.map((a, i) => ({ account: a, amount: (i + 1) * 1000 })) } },
+    { details: { Signal: 'Text' } },
+    { details: { SetCouncilBudgetIncrement: 1_000_000 } },
+    { details: { SetCouncilorReward: 100 } },
+    { details: { SetInitialInvitationBalance: 10 } },
+    { details: { SetInitialInvitationCount: 5 } },
+    { details: { SetMaxValidatorCount: 100 } },
+    { details: { SetMembershipLeadInvitationQuota: 50 } },
+    { details: { SetMembershipPrice: 500 } },
+    { details: { SetReferralCut: 25 } },
+    { details: { UpdateWorkingGroupBudget: [10_000_000, 'Content', 'Negative'] }, expectExecutionFailure: true },
+    {
+      details: {
+        CreateWorkingGroupLeadOpening: {
+          description: Utils.metadataToBytes(OpeningMetadata, DEFAULT_OPENING_PARAMS.metadata),
+          reward_per_block: DEFAULT_OPENING_PARAMS.reward,
+          stake_policy: {
+            leaving_unstaking_period: DEFAULT_OPENING_PARAMS.unstakingPeriod,
+            stake_amount: DEFAULT_OPENING_PARAMS.stake,
+          },
+          working_group: 'Storage',
+        },
       },
-      working_group: 'Content',
     },
-    SetCouncilBudgetIncrement: 1_000_000,
-    SetCouncilorReward: 100,
-    SetInitialInvitationBalance: 10,
-    SetInitialInvitationCount: 5,
-    SetMaxValidatorCount: 100,
-    SetMembershipLeadInvitationQuota: 50,
-    SetMembershipPrice: 500,
-    SetReferralCut: 25,
-    UpdateWorkingGroupBudget: [1_000_000, 'Content', 'Negative'],
-  }
+    { details: { CancelWorkingGroupLeadOpening: [openingToCancelId, 'Storage'] } },
+    {
+      details: {
+        FillWorkingGroupLeadOpening: {
+          opening_id: openingToFillId,
+          successful_application_id: applicationId,
+          working_group: 'Storage',
+        },
+      },
+    },
+  ]
 
-  const proposalsN = Object.keys(proposalsDetails).length
+  const testAllOutcomesFixture = new AllProposalsOutcomesFixture(api, query, proposalsToTest)
+  await new FixtureRunner(testAllOutcomesFixture).run()
 
-  const memberKeys = (await api.createKeyPairs(proposalsN)).map((key) => key.address)
-  const membersFixture = new BuyMembershipHappyCaseFixture(api, query, memberKeys)
-  await new FixtureRunner(membersFixture).run()
-  const memberIds = membersFixture.getCreatedMembers()
+  // The storage lead should be hired at this point, so we can test lead-related proposals
 
-  const { maxActiveProposalLimit } = api.consts.proposalsEngine
-  const proposalsPerBatch = maxActiveProposalLimit.toNumber()
-  let i = 0
-  let batch: [ProposalType, ProposalDetailsJsonByType][]
-  while (
-    (batch = (Object.entries(proposalsDetails) as [ProposalType, ProposalDetailsJsonByType][]).slice(
-      i * proposalsPerBatch,
-      (i + 1) * proposalsPerBatch
-    )).length
-  ) {
-    await api.untilProposalsCanBeCreated(proposalsPerBatch)
-    await Promise.all(
-      batch.map(async ([proposalType, proposalDetails], j) => {
-        debug(`Creating ${proposalType} proposal...`)
-        const createProposalFixture = new CreateProposalsFixture(api, query, [
-          {
-            asMember: memberIds[i * proposalsPerBatch + j],
-            title: `${_.startCase(proposalType)}`,
-            description: `Test ${proposalType} proposal`,
-            type: proposalType as ProposalType,
-            details: proposalDetails,
-          },
-        ])
-        await new FixtureRunner(createProposalFixture).runWithQueryNodeChecks()
-      })
-    )
-    ++i
-  }
+  const leadId = (await api.query.storageWorkingGroup.currentLead()).unwrap()
+  const leadProposalsToTest: TestedProposal[] = [
+    { details: { DecreaseWorkingGroupLeadStake: [leadId, 100, 'Storage'] } },
+    { details: { SetWorkingGroupLeadReward: [leadId, 50, 'Storage'] } },
+    { details: { SlashWorkingGroupLead: [leadId, 100, 'Storage'] } },
+    { details: { TerminateWorkingGroupLead: { worker_id: leadId, slashing_amount: null, working_group: 'Storage' } } },
+  ]
+  const testAllLeadProposalsOutcomes = new AllProposalsOutcomesFixture(api, query, leadProposalsToTest)
+  await new FixtureRunner(testAllLeadProposalsOutcomes).run()
 
   debug('Done')
 }

+ 36 - 3
tests/integration-tests/src/scenarios/full.ts

@@ -1,3 +1,36 @@
-import './memberships'
-import './workingGroups'
-import './proposals'
+import creatingMemberships from '../flows/membership/creatingMemberships'
+import updatingMemberProfile from '../flows/membership/updatingProfile'
+import updatingMemberAccounts from '../flows/membership/updatingAccounts'
+import invitingMebers from '../flows/membership/invitingMembers'
+import transferringInvites from '../flows/membership/transferringInvites'
+import managingStakingAccounts from '../flows/membership/managingStakingAccounts'
+import membershipSystem from '../flows/membership/membershipSystem'
+import leadOpening from '../flows/working-groups/leadOpening'
+import openingsAndApplications from '../flows/working-groups/openingsAndApplications'
+import upcomingOpenings from '../flows/working-groups/upcomingOpenings'
+import groupStatus from '../flows/working-groups/groupStatus'
+import workerActions from '../flows/working-groups/workerActions'
+import groupBudget from '../flows/working-groups/groupBudget'
+import proposals from '../flows/proposals'
+import { scenario } from '../Scenario'
+
+scenario(async ({ job }) => {
+  const membershipSystemJob = job('membership system', membershipSystem)
+  // All other membership jobs should be executed after membershipSystemJob,
+  // otherwise changing membershipPrice etc. may break them
+  job('creating members', creatingMemberships).after(membershipSystemJob)
+  job('updating member profile', updatingMemberProfile).after(membershipSystemJob)
+  job('updating member accounts', updatingMemberAccounts).after(membershipSystemJob)
+  job('inviting members', invitingMebers).after(membershipSystemJob)
+  job('transferring invites', transferringInvites).after(membershipSystemJob)
+  job('managing staking accounts', managingStakingAccounts).after(membershipSystemJob)
+
+  const proposalsJob = job('proposals', proposals)
+
+  const sudoHireLead = job('sudo lead opening', leadOpening).after(proposalsJob)
+  job('openings and applications', openingsAndApplications).requires(sudoHireLead)
+  job('upcoming openings', upcomingOpenings).requires(sudoHireLead)
+  job('group status', groupStatus).requires(sudoHireLead)
+  job('worker actions', workerActions).requires(sudoHireLead)
+  job('group budget', groupBudget).requires(sudoHireLead)
+})

+ 2 - 1
tests/integration-tests/src/utils.ts

@@ -7,6 +7,7 @@ import { decodeAddress } from '@polkadot/keyring'
 import { Bytes } from '@polkadot/types'
 import { createType } from '@joystream/types'
 import Debugger from 'debug'
+import { BLOCKTIME } from './consts'
 
 export type AnyMessage<T> = T & {
   toJSON(): Record<string, unknown>
@@ -76,7 +77,7 @@ export class Utils {
   public static async until(
     name: string,
     conditionFunc: (props: { debug: Debugger.Debugger }) => Promise<boolean>,
-    intervalMs = 6000,
+    intervalMs = BLOCKTIME,
     timeoutMs = 10 * 60 * 1000
   ): Promise<void> {
     const debug = Debugger(`awaiting:${name}`)