Skip to content

Commit 9a53b69

Browse files
dmealingclaude
andcommitted
fix(integration-tests): R6 — canonicalFloat fails loudly on out-of-band (exponential) floats
Strips trailing zeros only for plain-decimal strings; throws on exponential notation so an out-of-band REAL/DOUBLE fixture value surfaces immediately rather than silently corrupting. Adds unit tests for the in-band/integer/bigint/throw cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7d2a4fc commit 9a53b69

2 files changed

Lines changed: 69 additions & 6 deletions

File tree

server/typescript/packages/integration-tests/src/normalization.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,22 @@ function canonicalDecimal(s: string): string {
6464
return out;
6565
}
6666

67-
function canonicalFloat(n: number): string {
67+
export function canonicalFloat(n: number): string {
6868
// In-band dyadic values (per normalization.md) render plain + shortest via String().
69-
let out = String(n);
70-
if (out.includes(".")) {
71-
out = out.replace(/0+$/, "");
72-
if (out.endsWith(".")) out = out.slice(0, -1);
69+
// Out-of-band values (exponential notation) are forbidden by the wire contract;
70+
// throw immediately so an authoring error surfaces loudly rather than silently
71+
// corrupting the exponent digits during the trailing-zero strip.
72+
const out = String(n);
73+
if (/[eE]/.test(out)) {
74+
throw new Error(
75+
`canonicalFloat: ${n} is outside the plain-decimal band (exponential notation); ` +
76+
`REAL/DOUBLE fixture values must be in-band dyadic rationals — ` +
77+
`see fixtures/persistence-conformance/normalization.md`,
78+
);
7379
}
74-
return out;
80+
if (!out.includes(".")) return out;
81+
const stripped = out.replace(/0+$/, "");
82+
return stripped.endsWith(".") ? stripped.slice(0, -1) : stripped;
7583
}
7684

7785
// Format a Date as YYYY-MM-DDTHH:MM:SS[.fff] using UTC getters so the output
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Unit tests for normalization helpers — no DB required.
2+
3+
import { describe, test, expect } from "bun:test";
4+
import { normalizeValue, canonicalFloat } from "../src/normalization.ts";
5+
6+
describe("normalizeValue — floats (REAL/DOUBLE)", () => {
7+
test("1.5 → '1.5'", () => {
8+
expect(normalizeValue(1.5)).toBe("1.5");
9+
});
10+
11+
test("0.125 → '0.125'", () => {
12+
expect(normalizeValue(0.125)).toBe("0.125");
13+
});
14+
15+
test("-3.25 → '-3.25'", () => {
16+
expect(normalizeValue(-3.25)).toBe("-3.25");
17+
});
18+
19+
test("1234.5 → '1234.5'", () => {
20+
expect(normalizeValue(1234.5)).toBe("1234.5");
21+
});
22+
23+
test("trailing zeros are stripped: 1.500 → '1.5'", () => {
24+
// JS String() already produces '1.5' for 1.5, but canonicalFloat handles the strip
25+
expect(canonicalFloat(1.5)).toBe("1.5");
26+
});
27+
});
28+
29+
describe("normalizeValue — integers", () => {
30+
test("100 stays as number 100", () => {
31+
expect(normalizeValue(100)).toBe(100);
32+
});
33+
34+
test("0 stays as number 0", () => {
35+
expect(normalizeValue(0)).toBe(0);
36+
});
37+
});
38+
39+
describe("normalizeValue — bigint", () => {
40+
test("123n → '123' (string)", () => {
41+
expect(normalizeValue(123n)).toBe("123");
42+
});
43+
});
44+
45+
describe("canonicalFloat — out-of-band guard", () => {
46+
test("1.5e-10 throws (exponential notation)", () => {
47+
expect(() => canonicalFloat(1.5e-10)).toThrow(
48+
"canonicalFloat: 1.5e-10 is outside the plain-decimal band (exponential notation)",
49+
);
50+
});
51+
52+
test("normalizeValue(1.5e-10) propagates the throw", () => {
53+
expect(() => normalizeValue(1.5e-10)).toThrow();
54+
});
55+
});

0 commit comments

Comments
 (0)