Skip to content

Commit ff3a3f7

Browse files
committed
fix(cli): refuse a fraction the integer parse silently drops
Above 2^52 a double's spacing is 1, so Number('4503599627370496.5') is an integer and the safe-integer guard passed it — the API received a value the caller never typed. Read the raw text alongside the parsed number. Digits that are all zero are not a fraction, so 1.0 stays a whole number. Also corrects a chat test comment that described a UUID check the command does not perform; it refuses only a blank -c.
1 parent 834e157 commit ff3a3f7

3 files changed

Lines changed: 21 additions & 4 deletions

File tree

packages/sim-cli/src/commands/protocol/chat.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,9 @@ describe('sim chat', () => {
144144
})
145145

146146
/**
147-
* A conversation id as the command prints it: the route requires a UUID and
148-
* the CLI now says so before the request, so a stand-in like `conv-1` is
149-
* refused rather than sent.
147+
* A conversation id as the command prints it. The shape is the route's rule
148+
* to enforce — the CLI refuses only a blank `-c`, which is falsy and would
149+
* otherwise be dropped from the body and start a new conversation.
150150
*/
151151
it('passes -c through as the conversation to continue', async () => {
152152
requestRaw.mockResolvedValue(ndjson([FINAL]))

packages/sim-cli/src/runtime/build.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1689,6 +1689,9 @@ describe('bodies and fields the generator cannot flatten', () => {
16891689
['0', '--limit must be 1 or more'],
16901690
['-1', '--limit must be 1 or more'],
16911691
['1.5', '--limit must be a whole number'],
1692+
// Above 2^52 the parse itself drops the fraction, so `Number.isInteger`
1693+
// alone would pass this and send a value the caller never typed.
1694+
['4503599627370496.5', '--limit must be a whole number'],
16921695
] as const) {
16931696
for (const command of ['batch-delete', 'batch-update'] as const) {
16941697
const argv = ['tables', 'rows', command, 'tbl_1', '--filter', '{"all":[]}']

packages/sim-cli/src/runtime/request.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,17 @@ export function encodeFolderPath(value: string): string {
318318
.join('/')
319319
}
320320

321+
/**
322+
* A fractional part `Number` cannot keep.
323+
*
324+
* Above 2^52 a double's spacing is 1, so `Number('4503599627370496.5')` is an
325+
* integer — `Number.isInteger` passes and the API receives a value the caller
326+
* did not type. Read the text as well as the parsed number so the refusal
327+
* covers the range where the parse itself loses the fraction. Digits that are
328+
* all zero are not a fraction, so `1.0` stays a whole number.
329+
*/
330+
const FRACTIONAL_DIGITS = /\.\d*[1-9]/
331+
321332
/**
322333
* Points at `@` when a value that failed to parse looks like a filename.
323334
*
@@ -390,7 +401,10 @@ export function coerce(raw: unknown, field: FieldSpec, flag: FlagSpec, flagName:
390401
* nor anything the caller typed, on the one flag whose blank, zero and
391402
* non-numeric cases all had a sentence written for them.
392403
*/
393-
if (field.kind === 'integer' && !Number.isInteger(value)) {
404+
if (
405+
field.kind === 'integer' &&
406+
(!Number.isInteger(value) || FRACTIONAL_DIGITS.test(String(raw)))
407+
) {
394408
throw new SimApiError(`--${flagName} must be a whole number`, 0)
395409
}
396410
if (field.kind === 'integer' && !Number.isSafeInteger(value)) {

0 commit comments

Comments
 (0)