Skip to content

Comments

Resolve conditional steps in benchmark wizard#1961

Draft
ravikiranvm wants to merge 5 commits intomainfrom
ops-3650
Draft

Resolve conditional steps in benchmark wizard#1961
ravikiranvm wants to merge 5 commits intomainfrom
ops-3650

Conversation

@ravikiranvm
Copy link
Contributor

Part of OPS-3650.

Additional Notes

@linear
Copy link

linear bot commented Feb 18, 2026

@ravikiranvm ravikiranvm marked this pull request as ready for review February 18, 2026 13:35
Copilot AI review requested due to automatic review settings February 18, 2026 13:35
}

async function hasMultipleAccounts(_context: WizardContext): Promise<boolean> {
// TODO: Evaluate from connection (e.g. roles.length > 1) and return false when no connection in context. For now always show the accounts step.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will implement this in the follow up PR

method: string,
context: WizardContext,
): Promise<BenchmarkWizardOption[]>;
evaluateConditional(when: string, context: WizardContext): Promise<boolean>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im indecisive between evaluateConditional and evaluateConditionalStep

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

evaluateCondition or resolveCondition

if (!skipToStepId) {
return nextStepIdFromConfig;
}
return resolveNextStepId(skipToStepId, config, providerAdapter, context);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is recursive because what if the resolved step is a conditional step?
But I don't think it is a common scenario; I can change it after inputs.

step: WizardConfigStep,
request: BenchmarkWizardRequest,
projectId: string,
context: WizardContext,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead we build the context and pass it as a whole

projectId,
provider,
});
// Returns true until we implement real evaluation (see hasMultipleAccounts in aws-conditional-step-resolver).
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will modify it once the real evaluation functions is available in the next PR

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements support for conditional wizard steps in the benchmark flow (OPS-3650), allowing provider-specific logic to decide whether a step should be shown and enabling conditional option sources / skip behavior.

Changes:

  • Add ProviderAdapter.evaluateConditional() and implement AWS conditional evaluation (hasMultipleAccounts).
  • Update wizard navigation resolution to asynchronously skip conditional steps based on evaluated conditions.
  • Update AWS wizard config to use conditional.onSuccess.optionsSource and conditional.onFailure.skipToStep, and add unit tests for AWS conditional resolver.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/server/api/src/app/benchmark/wizard.service.ts Adds async conditional-step resolution and conditional optionsSource selection.
packages/server/api/src/app/benchmark/provider-adapter.ts Extends conditional schema and ProviderAdapter with evaluateConditional.
packages/server/api/src/app/benchmark/providers/aws/aws-conditional-step-resolver.ts Adds AWS conditional evaluation entrypoint (stubbed for now).
packages/server/api/src/app/benchmark/providers/aws/index.ts Wires AWS evaluateConditional into the provider adapter.
packages/server/api/src/app/benchmark/providers/aws/aws.json Updates AWS wizard step conditionals and options source behavior.
packages/server/api/test/unit/benchmark/wizard.service.test.ts Updates wizard service test adapter mock to include evaluateConditional.
packages/server/api/test/unit/benchmark/providers/aws/aws-conditional-step-resolver.test.ts Adds unit tests for AWS conditional evaluation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 76 to 80
beforeEach(() => {
jest.clearAllMocks();
mockResolveOptions.mockResolvedValue([]);
mockEvaluateConditional.mockResolvedValue(true);
});
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated conditional behavior isn’t covered by unit tests: mockEvaluateConditional is always set to resolve true, so there’s no assertion that a conditional step is actually skipped (e.g., when it resolves false and onFailure.skipToStep is set). Add a test case where mockEvaluateConditional returns false and verify that navigation skips step2 to step3 and that evaluateConditional is called with the expected when and context.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to 28
const stepsWithOptions = config.steps.filter(
(s) => s.optionsSource || s.conditional?.onSuccess?.optionsSource,
);
const totalSteps = stepsWithOptions.length;
const stepIndex =
stepsWithOptions.findIndex((s) => s.id === stepToReturn.id) + 1;
return { totalSteps, stepIndex };
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getStepProgress counts steps based on presence of an optionsSource (including conditional.onSuccess.optionsSource), but it doesn’t account for conditionals that are evaluated false and skipped. This can make totalSteps/stepIndex incorrect when a conditional step is not shown. Consider computing progress based on the resolved navigation path for the current context (i.e., evaluate conditionals and exclude skipped steps from the count/index).

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +74
const skipToStepId = step.conditional.onFailure?.skipToStep;
if (!skipToStepId) {
return nextStepIdFromConfig;
}
return resolveNextStepId(skipToStepId, config, providerAdapter, context);
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveNextStepId recursively follows conditional.onFailure.skipToStep without any cycle/depth protection. A misconfigured wizard (e.g., A skips to B and B skips back to A) will cause infinite recursion/stack overflow. Add cycle detection (visited set) or a max-depth guard and throw a validation error when a loop is detected.

Copilot uses AI. Check for mistakes.
Comment on lines +137 to +139
const optionsSource =
step.conditional?.onSuccess?.optionsSource ??
(step.optionsSource as WizardStepOptionsSource | undefined);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveOptions always prefers step.conditional.onSuccess.optionsSource even though the conditional may evaluate to false (and the step may still be shown if there’s no onFailure.skipToStep). This makes the meaning of onSuccess inconsistent and can return the wrong options source. Consider passing the evaluated conditional result into resolveOptions (or evaluating the conditional there) so the onSuccess options source is only used when the condition actually succeeds.

Suggested change
const optionsSource =
step.conditional?.onSuccess?.optionsSource ??
(step.optionsSource as WizardStepOptionsSource | undefined);
const optionsSource = step.optionsSource as
| WizardStepOptionsSource
| undefined;

Copilot uses AI. Check for mistakes.
"title": "Which accounts should we include in the report?",
"selectionType": "multi-select",
"optionsSource": { "type": "dynamic", "method": "getConnectionAccounts" },
"optionsSource": "",
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optionsSource is set to an empty string, but optionsSource is expected to be an object (or omitted). This relies on falsy behavior and type assertions and may break any runtime validation/parsing of the wizard config. Prefer removing optionsSource entirely for this step (and relying on conditional.onSuccess.optionsSource), or set it to null and explicitly handle null in the loader/types.

Suggested change
"optionsSource": "",

Copilot uses AI. Check for mistakes.
context,
);
const nextStepDef = steps.find((s) => s.id === resolvedNextStepId);
if (!nextStepDef) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you already throw in getNextStepIdFromConfig

"title": "Which accounts should we include in the report?",
"selectionType": "multi-select",
"optionsSource": { "type": "dynamic", "method": "getConnectionAccounts" },
"optionsSource": "",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this, you should only have it on steps without the conditionals

Copy link
Contributor Author

@ravikiranvm ravikiranvm Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we decided keep it empty string for conditional steps? But I can remove it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, for conditional steps you have it inside the condition, for non conditional steps you have it outside

providerAdapter: ProviderAdapter,
context: WizardContext,
): Promise<string | null> {
if (!nextStepIdFromConfig) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why even allow to add in no next stepId? just make nextStepIdFromConfig: string mandatory


const nextStep = resolveNextStepId(stepToShow, config);
const nextStepIdFromConfig = getNextStepIdFromConfig(stepToShow, config);
const nextStep = await resolveNextStepId(
Copy link
Contributor

@bigfluffycookie bigfluffycookie Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now wrong.

Imagine this:
User is on step1 and clicks next
you look up step1s nextStep which is → step2

step2 has a conditional → evaluates it → condition is FALSE → step2 should not be shown, you recursively check if step3 should be shown (this is correct in theory but honestly the recursion will just confuse us all more)
stepToShow= step3
nextStepId = step4

step2 has a conditional → evaluates it → condition is TRUE → step2 should be shown with its options
stepToShow = step2
nextStepId = step3

BUT what happens now is you go in resolveNextStepId() with step3.id, and you go through the whole loop. no matter whats the case if step2 conditon evaluates to false it means you HAVE to skip to the next step.

Copy link
Contributor Author

@ravikiranvm ravikiranvm Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the condition is TRUE, we show the step.
I've named the method as hasMultipleAccounts and the success path would be to resolve options for the step.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are right, i wrote my comment the wrong we around, i edited to fix it. The point I made is still correct though

Like maybe write a test case with 4 steps, where step 2 evaluates to TRUE and step 3 evaluates to FALSE. See what you get back from this method matches what you expect.

I suspect you get {stepToShow: step2, nextStepId: step4 }

};

const { stepToShow, nextStep } = computeWizardStepResponse(
const { stepToShow, nextStep } = await computeWizardStepResponse(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im sorry i missed it earlier, but it led me to be confused in some places now.

in here nextStep seems to be nextStepID
whereas stepToShow is an actual step.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, here nextStep is the ID. I'm using nextStep here to match the usage in json file. I will see if I can make is more clear to read.

return resolveNextStepId(skipToStepId, config, providerAdapter, context);
}

async function computeWizardStepResponse(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think the problem is that we are doing 2 things here.

  1. we navigate to the next step
  2. we look ahead at whats the next step id for the response to the frontend

in one case nextStep is the next step to navigate to and then later the next step is the nextsteps.nextstep to return

Comment on lines +54 to +55
}
const step = config.steps.find((s) => s.id === nextStepIdFromConfig);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a bit funny, its needed bc of the recursion probably but in essence what you do is

  1. you get a stepId
  2. you call getNextStepIdFromConfig - which gets the steps[stepId]
  3. you throw if there is no step
  4. you return the stepId
  5. you call this function with the ID to then get the step AGAIN from steps
  6. you throw again if there is no step

Copy link
Contributor

@bigfluffycookie bigfluffycookie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong approve

Comment on lines +22 to +27
onSuccess?: {
optionsSource?: WizardStepOptionsSource;
};
onFailure?: {
skipToStep?: string;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't these fields mandatory?

method: string,
context: WizardContext,
): Promise<BenchmarkWizardOption[]>;
evaluateConditional(when: string, context: WizardContext): Promise<boolean>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

evaluateCondition or resolveCondition

@sonarqubecloud
Copy link

@ravikiranvm ravikiranvm marked this pull request as draft February 20, 2026 06:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants