Implement custom reliability flags#41
Conversation
…does not return them
richford
left a comment
There was a problem hiding this comment.
Thanks @amyyeung17. A few change requests below. Also, please run npm run format (i.e. prettier/eslint).
| }); | ||
|
|
||
| // Mock date to avoid timezone-related flakiness | ||
| jest.useFakeTimers(); |
There was a problem hiding this comment.
jest.useFakeTimers() sits at the top level, so it activates at module load and applies to every test in the file until jest.useRealTimers() is called at the end of the getAgeData test body. This causes two issues: (1) unrelated tests registered earlier in the file run under fake timers unintentionally; (2) if that test throws before the restore line, fake timers leak into subsequent tests. Can you move setup/teardown into beforeAll/afterAll (or beforeEach/afterEach) scoped to that test/describe, restoring real timers in afterAll so a failure can't leak. The suite passes today, but it's a fragility trap.
There was a problem hiding this comment.
Hm, what are your thoughts about moving the data and fakeTimers into the test case? Or do we prefer defining a top-level variable so we can reuse it for other future tests?
| } | ||
|
|
||
| isReliable = flags.filter((x) => includedReliabilityFlags.includes(x)).length === 0; | ||
| flags = flags.filter((x) => includedReliabilityFlags.includes(x)); |
There was a problem hiding this comment.
This is pre-existing but it looks like no "informational-only" flag is possible. The closing flags = flags.filter(x => includedReliabilityFlags.includes(x)) removes every flag not in includedReliabilityFlags from the returned output and from the reliability decision. So a custom flag you compute but don't include is invisible and you can't surface a flag purely for reporting without also making it count toward unreliability. This is pre-existing behavior but can you add a comment or something to the JsDoc documenting the known limitation. And maybe also a test for this.
| if (logicalOperation.toLowerCase() === 'and') { | ||
| conditionMet = conditions.every((condition) => condition(data)); | ||
| } else if (logicalOperation.toLowerCase() === 'or') { | ||
| conditionMet = conditions.some((condition) => condition(data)); | ||
| } |
There was a problem hiding this comment.
Unsupported logicalOperation values silently no-op here. Only 'and'/'or' are handled. Anything else ('only', a typo, 'xor') leaves conditionMet = false and the flag silently never fires, with no warning. Please add an else { throw } with console.warn.
There was a problem hiding this comment.
Gotcha. Now that you've brought up 'only' and 'xor', should we just implement them now?
There was a problem hiding this comment.
Implemented 'xor'. 'Only' can be handled by just passing a single condition.
| if (logicalOperation.toLowerCase() === 'and') { | ||
| conditionMet = conditions.every((condition) => condition(data)); | ||
| } else if (logicalOperation.toLowerCase() === 'or') { | ||
| conditionMet = conditions.some((condition) => condition(data)); | ||
| } |
There was a problem hiding this comment.
Also, a malformed rule throws and aborts the whole evaluation here. logicalOperation.toLowerCase() has no guard, so a rule missing logicalOperation (or with a non-string value) throws a TypeError, which propagates out of baseEvaluateValidity and takes the engagement-flag handling down with it. Real-world likelihood is low (rules are app-defined constants), but can you add a default value (logicalOperation = 'and') and also an explicit guard against non-string values?
There was a problem hiding this comment.
Added error handling during setup. I thought it might make sense to allow one-off custom rules that don't require logicalOperations. As a result, the validation only checks for valid logicalOperations when there are two or more conditions.
|
|
||
| // Evaluate custom validations alongside default checks | ||
| if (customValidations.length > 0) { | ||
| const data = { responseTimes, responses, correct, completed, existingFlags: flags }; |
There was a problem hiding this comment.
existingFlags is a live reference shared across rules leading to order dependence. data is constructed once and existingFlags: flags aliases the same array that the forEach mutates later. So a later rule's condition can observe flags pushed by earlier custom rules, not just the default flags, which makes rule ordering important. Can you either snapshot it (existingFlags: [...flags]) so every rule sees the same baseline, or document that custom rules chain to each other. This also isn't exercised by tests (the multi-rule test only references base flags).
There was a problem hiding this comment.
Documented. I explored allowing customValidations rules to depend on one another, but the implementation became too unwieldy and introduced too many edge cases (e.g., circular dependencies).
…ct custom validations
…sures' into enh/allow-custom-reliability-measures
- Snapshot built-in flags once before the custom-validation loop so a custom rule cannot observe another custom rule's flag (enforces the documented independence contract) - Only evaluate custom validations when responseTimes.length >= minResponsesRequired, so they are skipped on insufficient-response runs - Clarify JSDoc: logicalOperation semantics (and=all, or=any, xor=exactly one), note the min-responses gating, and give the example reduce an initial value - Add tests for custom-flag independence and skip-on-insufficient-responses
This PR introduces the new argument
customValidationstocreateEvaluateValidity.customValidationsallows apps to pass in new flags using existing or new conditions defined in theconditionsarray. EachcustomValidationsitem must contain the newflagname,conditionsarray, andlogicalOperationto use withconditions.conditionscallback function parameters:{ responseTimes, responses, correct, completed, existingFlags }.responseTimes,responses,correct, andcompletedare existing values provided by the parent functionbaseEvaluateValidity(which is the function returned when callingcreateEvaluateValidity).data.existingFlagsincludedReliabilityFlagsUnrelated edit: Set fixed date for
getAgeDatatest. The test was being flaky because the function relied onnew Date().