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
5 changes: 5 additions & 0 deletions .changeset/silent-feet-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@boostv/process-optimizer-frontend-core': patch
---

Fix a 'race condition' in reducer where changes were not detected when a datapoint changes from invalid to valid
131 changes: 131 additions & 0 deletions packages/core/src/context/experiment/calculate-change-reducer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { describe, expect, it } from 'vitest'
import { ExperimentType } from 'common'
import { calculateChangeReducer } from './calculate-change-reducer'
import { createFetchExperimentResultRequest } from './api'
import { emptyExperiment } from './store'
import md5 from 'md5'

describe('calculateChangeReducer', () => {
it('should set changedSinceLastEvaluation to false when hash matches', () => {
const experiment: ExperimentType = {
...emptyExperiment,
id: 'test-id',
}
const hash = md5(
JSON.stringify(createFetchExperimentResultRequest(experiment))
)
const experimentWithHash: ExperimentType = {
...experiment,
lastEvaluationHash: hash,
}

const result = calculateChangeReducer(experimentWithHash)

expect(result.changedSinceLastEvaluation).toBe(false)
})

it('should set changedSinceLastEvaluation to true when hash does not match', () => {
const experiment: ExperimentType = {
...emptyExperiment,
id: 'test-id',
lastEvaluationHash: 'outdated-hash',
}

const result = calculateChangeReducer(experiment)

expect(result.changedSinceLastEvaluation).toBe(true)
})

it('should set changedSinceLastEvaluation to true when lastEvaluationHash is empty', () => {
const experiment: ExperimentType = {
...emptyExperiment,
id: 'test-id',
lastEvaluationHash: '',
}

const result = calculateChangeReducer(experiment)

expect(result.changedSinceLastEvaluation).toBe(true)
})

it('should detect change when optimizer config changes', () => {
const baseExperiment: ExperimentType = {
...emptyExperiment,
id: 'test-id',
}
const hash = md5(
JSON.stringify(createFetchExperimentResultRequest(baseExperiment))
)
const experimentWithHash: ExperimentType = {
...baseExperiment,
lastEvaluationHash: hash,
}

expect(
calculateChangeReducer(experimentWithHash).changedSinceLastEvaluation
).toBe(false)

const modifiedExperiment: ExperimentType = {
...experimentWithHash,
optimizerConfig: {
...experimentWithHash.optimizerConfig,
initialPoints: 10,
},
}

const result = calculateChangeReducer(modifiedExperiment)

expect(result.changedSinceLastEvaluation).toBe(true)
})

it('should detect change when variables are added', () => {
const baseExperiment: ExperimentType = {
...emptyExperiment,
id: 'test-id',
}
const hash = md5(
JSON.stringify(createFetchExperimentResultRequest(baseExperiment))
)
const experimentWithHash: ExperimentType = {
...baseExperiment,
lastEvaluationHash: hash,
}

expect(
calculateChangeReducer(experimentWithHash).changedSinceLastEvaluation
).toBe(false)

const modifiedExperiment: ExperimentType = {
...experimentWithHash,
valueVariables: [
{
type: 'continuous',
name: 'TestVar',
description: 'Test variable',
min: 0,
max: 100,
enabled: true,
},
],
}

const result = calculateChangeReducer(modifiedExperiment)

expect(result.changedSinceLastEvaluation).toBe(true)
})

it('should not mutate the original experiment object', () => {
const experiment: ExperimentType = {
...emptyExperiment,
id: 'test-id',
changedSinceLastEvaluation: false,
lastEvaluationHash: 'some-hash',
}

const result = calculateChangeReducer(experiment)

expect(result).not.toBe(experiment)
expect(experiment.changedSinceLastEvaluation).toBe(false)
expect(result.changedSinceLastEvaluation).toBe(true)
})
})
10 changes: 10 additions & 0 deletions packages/core/src/context/experiment/calculate-change-reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ExperimentType } from 'common'
import { produce } from 'immer'
import md5 from 'md5'
import { createFetchExperimentResultRequest } from './api'

export const calculateChangeReducer = produce((state: ExperimentType): void => {
state.changedSinceLastEvaluation =
state.lastEvaluationHash !==
md5(JSON.stringify(createFetchExperimentResultRequest(state)))
})
3 changes: 0 additions & 3 deletions packages/core/src/context/experiment/experiment-reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,9 +458,6 @@ export const experimentReducer = produce(
default:
assertUnreachable(action)
}
state.changedSinceLastEvaluation =
state.lastEvaluationHash !==
md5(JSON.stringify(createFetchExperimentResultRequest(state)))
state.info.version = state.info.version + 1
}
)
Expand Down
12 changes: 11 additions & 1 deletion packages/core/src/context/experiment/reducers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import {
scoreNames,
ValueVariableType,
} from '@core/common/types'
import { emptyExperiment, State } from '@core/context/experiment'
import {
createFetchExperimentResultRequest,
emptyExperiment,
State,
} from '@core/context/experiment'
import { versionInfo } from '@core/common'
import _ from 'lodash'
import { produce } from 'immer'
Expand All @@ -21,6 +25,7 @@ import {
createValueVariable,
dummyPayloads,
} from '@core/context/experiment/test-utils'
import md5 from 'md5'

describe('experiment reducer', () => {
const initState: State = {
Expand Down Expand Up @@ -116,6 +121,7 @@ describe('experiment reducer', () => {
const payload: ExperimentType = {
id: '5678',
changedSinceLastEvaluation: false,
lastEvaluationHash: '5b4247caaa4e9a5a0c519a9017ccc547',
info: {
swVersion: versionInfo.version,
name: 'Not cake',
Expand Down Expand Up @@ -169,6 +175,10 @@ describe('experiment reducer', () => {
type: 'updateExperiment',
payload,
}
const hash = md5(
JSON.stringify(createFetchExperimentResultRequest(payload))
)
expect(hash).toEqual(payload.lastEvaluationHash)

const actual = rootReducer(initState, action)
expect(actual).toEqual({
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/context/experiment/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { State } from './store'
import { ExperimentAction, experimentReducer } from './experiment-reducers'
import { validateExperiment, ValidationViolations } from './validation'
import { validationReducer } from './validation-reducer'
import { calculateChangeReducer } from './calculate-change-reducer'

export type Action = ExperimentAction

Expand Down Expand Up @@ -36,7 +37,9 @@ export const rootReducer = (state: State, action: Action) => {
validateExperiment(experiment)
return {
...state,
experiment: validationReducer(experiment, validationViolations),
experiment: calculateChangeReducer(
validationReducer(experiment, validationViolations)
),
}
}
default:
Expand Down
Loading