Skip to content
Open
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
51 changes: 51 additions & 0 deletions packages/core/src/visibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,57 @@ describe("evaluateVisibility", () => {
),
).toBe(false);
});

it("compares numeric strings with numbers", () => {
expect(
evaluateVisibility(
{ $state: "/count", gt: 3 },
{ stateModel: { count: "5" } },
),
).toBe(true);
expect(
evaluateVisibility(
{ $state: "/count", gte: 5 },
{ stateModel: { count: "5" } },
),
).toBe(true);
expect(
evaluateVisibility(
{ $state: "/count", lt: 5 },
{ stateModel: { count: "3" } },
),
).toBe(true);
expect(
evaluateVisibility(
{ $state: "/count", lte: 5 },
{ stateModel: { count: "5" } },
),
).toBe(true);
});

it("compares two numeric strings", () => {
expect(
evaluateVisibility(
{ $state: "/count", gt: { $state: "/limit" } },
{ stateModel: { count: "10", limit: "3" } },
),
).toBe(true);
});

it("still returns false for empty and non-numeric strings", () => {
expect(
evaluateVisibility(
{ $state: "/count", gt: 0 },
{ stateModel: { count: "" } },
),
).toBe(false);
expect(
evaluateVisibility(
{ $state: "/count", gt: 0 },
{ stateModel: { count: "10px" } },
),
).toBe(false);
});
});

describe("dynamic path references in comparison", () => {
Expand Down
47 changes: 31 additions & 16 deletions packages/core/src/visibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,33 @@ function resolveConditionValue(
return getByPath(ctx.stateModel, (cond as StateCondition).$state);
}

/**
* Coerce a comparison operand to a finite number.
* Accepts numbers and numeric strings (`"10"`) so LLM/YAML quoted counts
* still work with gt/gte/lt/lte. Empty strings, booleans, and `"10px"`
* are not numbers.
*/
function toComparableNumber(value: unknown): number | undefined {
if (typeof value === "number") {
return Number.isFinite(value) ? value : undefined;
}
if (typeof value === "string" && value.trim() !== "") {
const n = Number(value);
return Number.isFinite(n) ? n : undefined;
}
return undefined;
}

function compareNumbers(
value: unknown,
rhs: unknown,
op: (a: number, b: number) => boolean,
): boolean {
const left = toComparableNumber(value);
const right = toComparableNumber(rhs);
return left !== undefined && right !== undefined ? op(left, right) : false;
}

/**
* Evaluate a single condition against the context.
*
Expand Down Expand Up @@ -254,34 +281,22 @@ function evaluateCondition(
// Greater than
else if (cond.gt !== undefined) {
const rhs = resolveComparisonValue(cond.gt, ctx);
result =
typeof value === "number" && typeof rhs === "number"
? value > rhs
: false;
result = compareNumbers(value, rhs, (a, b) => a > b);
}
// Greater than or equal
else if (cond.gte !== undefined) {
const rhs = resolveComparisonValue(cond.gte, ctx);
result =
typeof value === "number" && typeof rhs === "number"
? value >= rhs
: false;
result = compareNumbers(value, rhs, (a, b) => a >= b);
}
// Less than
else if (cond.lt !== undefined) {
const rhs = resolveComparisonValue(cond.lt, ctx);
result =
typeof value === "number" && typeof rhs === "number"
? value < rhs
: false;
result = compareNumbers(value, rhs, (a, b) => a < b);
}
// Less than or equal
else if (cond.lte !== undefined) {
const rhs = resolveComparisonValue(cond.lte, ctx);
result =
typeof value === "number" && typeof rhs === "number"
? value <= rhs
: false;
result = compareNumbers(value, rhs, (a, b) => a <= b);
}
// Truthiness (no operator)
else {
Expand Down