fix(oracle-r4): validate selectedStepId across live refreshes
Oracle r4 'Watch Out For' caught a real edge case: if a backend graph changes shape while the scenarioId stays the same, the prior selectedStepId could survive the merge and point at a step that no longer exists. Two new vitest regressions in state/store.test.ts now pin the contract: - refreshLive() resets selectedStepId to the scenario's defaultStepId when the prior step no longer exists in the merged catalog - refreshLive() preserves a still-valid selectedStepId runLiveFetch() now derives stepStillThere from the merged scenario's own steps (not the old store) and falls back to defaultStepId when stale. Same single-set call as before; no extra renders. Confidence: high Scope-risk: narrow Not-tested: real backend definition with step IDs that disappear mid-session (covered by stub + assertion above)
This commit is contained in:
parent
2e0e4c08e8
commit
49639a0857
@ -74,4 +74,28 @@ describe("store live-mode + refresh", () => {
|
|||||||
await useApp.getState().setMode("snapshot");
|
await useApp.getState().setMode("snapshot");
|
||||||
expect(useApp.getState().toasts.length).toBe(before);
|
expect(useApp.getState().toasts.length).toBe(before);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("refreshLive() resets selectedStepId to defaultStepId when the prior step no longer exists in the merged catalog", async () => {
|
||||||
|
stubFetch();
|
||||||
|
await useApp.getState().setMode("live");
|
||||||
|
const scenario = useApp.getState().scenarios[0];
|
||||||
|
if (!scenario) throw new Error("no scenario");
|
||||||
|
useApp.setState({ scenarioId: scenario.id, selectedStepId: "definitely-not-a-real-step-id" });
|
||||||
|
await useApp.getState().refreshLive();
|
||||||
|
const after = useApp.getState();
|
||||||
|
const sc = after.scenarios.find((s) => s.id === after.scenarioId);
|
||||||
|
expect(sc).toBeDefined();
|
||||||
|
expect(sc!.steps.some((st) => st.id === after.selectedStepId)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refreshLive() preserves a still-valid selectedStepId", async () => {
|
||||||
|
stubFetch();
|
||||||
|
await useApp.getState().setMode("live");
|
||||||
|
const scenario = useApp.getState().scenarios[0];
|
||||||
|
if (!scenario) throw new Error("no scenario");
|
||||||
|
const validStep = scenario.steps[scenario.steps.length - 1];
|
||||||
|
useApp.setState({ scenarioId: scenario.id, selectedStepId: validStep.id });
|
||||||
|
await useApp.getState().refreshLive();
|
||||||
|
expect(useApp.getState().selectedStepId).toBe(validStep.id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -84,14 +84,17 @@ async function runLiveFetch(
|
|||||||
const merged = [...scenarios, ...syntheticScenarios];
|
const merged = [...scenarios, ...syntheticScenarios];
|
||||||
const first = merged[0];
|
const first = merged[0];
|
||||||
const currentId = get().scenarioId;
|
const currentId = get().scenarioId;
|
||||||
|
const currentStepId = get().selectedStepId;
|
||||||
const stillThere = merged.find((s) => s.id === currentId);
|
const stillThere = merged.find((s) => s.id === currentId);
|
||||||
|
const stepStillThere = stillThere?.steps.some((st) => st.id === currentStepId);
|
||||||
|
const nextScenario = stillThere ?? first;
|
||||||
set({
|
set({
|
||||||
scenarios: merged,
|
scenarios: merged,
|
||||||
liveTotals: { workItems: workItems.length, distinctDefs },
|
liveTotals: { workItems: workItems.length, distinctDefs },
|
||||||
liveFetchedAt: Date.now(),
|
liveFetchedAt: Date.now(),
|
||||||
liveLoading: false,
|
liveLoading: false,
|
||||||
scenarioId: stillThere ? currentId : first?.id ?? currentId,
|
scenarioId: nextScenario?.id ?? currentId,
|
||||||
selectedStepId: stillThere ? get().selectedStepId : first?.defaultStepId ?? null,
|
selectedStepId: stepStillThere ? currentStepId : nextScenario?.defaultStepId ?? null,
|
||||||
});
|
});
|
||||||
get().pushToast(
|
get().pushToast(
|
||||||
"ok",
|
"ok",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user