fix(usage-bar): validate meter width with strict integer parsing (#110410)

* fix(usage-bar): validate meter width with strict integer parsing

* fix(usage-bar): restore canonical meter width range [0, 100]

* fix(usage-bar): preserve bounded meter defaults

Co-authored-by: sheyanmin <she.yanmin@xydigit.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
sheyanmin
2026-07-18 21:57:11 +01:00
committed by GitHub
co-authored by Peter Steinberger
parent 242093fb00
commit ff3ed99cde
3 changed files with 33 additions and 7 deletions
+3
View File
@@ -256,6 +256,9 @@ Pipe a value through verbs left to right; a non-verb segment is the fallback.
`fixed:N` accepts only a complete decimal integer from 0 through 100. Invalid
precision arguments make that interpolation empty.
`meter:W:SCALE` accepts only a complete decimal integer width from 1 through 100. Leave the width blank to use the default 5 (`meter::braille`); invalid
widths make that interpolation empty.
### Piece forms
- `{ "text": "📚 {context.max_tokens|num}" }`: literal + interpolation.
@@ -72,6 +72,21 @@ describe("usage-bar verbs", () => {
expect(render([{ text: "{x|meter:1:moon}" }], { x: 100 })).toBe("🌕");
});
it("meter — preserves default and supported explicit widths", () => {
expect(render([{ text: "{x|meter::braille}" }], { x: 75 })).toBe("⣿⣿⣿⣧⠐");
expect(render([{ text: "{x|meter: :braille}" }], { x: 75 })).toBe("⣿⣿⣿⣧⠐");
expect(render([{ text: "{x|meter:+5:braille}" }], { x: 75 })).toBe("⣿⣿⣿⣧⠐");
expect(render([{ text: "{x|meter: 5 :braille}" }], { x: 75 })).toBe("⣿⣿⣿⣧⠐");
expect(render([{ text: "{x|meter:100:braille}" }], { x: 50 })).toHaveLength(100);
});
it.each(["0", "-1", "2.5", "101", "1e2", "2junk", "abc", "9007199254740992"])(
"meter — rejects invalid width %s",
(width) => {
expect(render([{ text: `{x|meter:${width}:braille}` }], { x: 75 })).toBe("");
},
);
it("alias — listed shortens, unlisted echoes through", () => {
expect(render([{ text: "{m|alias:models}" }], { m: "claude-opus-4-6" })).toBe("opus46");
expect(render([{ text: "{m|alias:models}" }], { m: "some-new-model" })).toBe("some-new-model");
+15 -7
View File
@@ -1,4 +1,8 @@
import { expectDefined, parseStrictInteger } from "@openclaw/normalization-core";
import {
asSafeIntegerInRange,
expectDefined,
parseStrictInteger,
} from "@openclaw/normalization-core";
export type UsageBarTemplate = Record<string, unknown>;
export type UsageContract = Record<string, unknown>;
type Vocab = Record<string, unknown>;
@@ -117,9 +121,12 @@ function meter(value: unknown, width: number, scale: unknown): string {
const VERB_NAMES = new Set(["num", "fixed", "dur", "pct", "inv", "alias", "meter"]);
function parseFixedDigits(raw: string | undefined): number | undefined {
const digits = raw === undefined ? 2 : parseStrictInteger(raw);
return digits !== undefined && digits >= 0 && digits <= 100 ? digits : undefined;
function parseBoundedIntegerArg(
raw: string | undefined,
options: { defaultValue: number; min: number; max: number },
): number | undefined {
const value = raw === undefined ? options.defaultValue : parseStrictInteger(raw);
return asSafeIntegerInRange(value, options);
}
function applyVerb(name: string, args: string[], value: unknown, vocab: Vocab): unknown {
@@ -127,7 +134,7 @@ function applyVerb(name: string, args: string[], value: unknown, vocab: Vocab):
case "num":
return num(value);
case "fixed": {
const digits = parseFixedDigits(args[0]);
const digits = parseBoundedIntegerArg(args[0], { defaultValue: 2, min: 0, max: 100 });
return digits === undefined ? "" : fixed(value, digits);
}
case "dur":
@@ -148,9 +155,10 @@ function applyVerb(name: string, args: string[], value: unknown, vocab: Vocab):
return Object.hasOwn(table, lower) ? table[lower] : value;
}
case "meter": {
const width = args[0] ? Number.parseInt(args[0], 10) || 5 : 5;
const rawWidth = args[0]?.trim() ? args[0] : undefined;
const width = parseBoundedIntegerArg(rawWidth, { defaultValue: 5, min: 1, max: 100 });
const scale = args.length > 1 ? vocab[expectDefined(args[1], "args entry at 1")] : undefined;
return meter(value, width, scale);
return width === undefined ? "" : meter(value, width, scale);
}
default:
return String(value);