|
| 1 | +/** |
| 2 | + * #338: graft rendered its savings footer with the machine's locale and read it |
| 3 | + * back with a regex that knows only `,`, so a `de-DE`/`tr-TR` box wrote `1.990` |
| 4 | + * and parsed it as `1`. |
| 5 | + * |
| 6 | + * A process cannot change its own ambient locale — Node takes it from the |
| 7 | + * environment on posix and from the OS on Windows — so these tests move the |
| 8 | + * *default* instead: `toLocaleString()` with no argument answers the way a |
| 9 | + * `.`-grouping locale would, while calls that name a locale are left alone. |
| 10 | + * That is exactly the difference the fix turns on, and it behaves identically on |
| 11 | + * an en-US CI runner, which is why CI never caught the original. |
| 12 | + */ |
| 13 | +import { test } from 'node:test'; |
| 14 | +import assert from 'node:assert/strict'; |
| 15 | +import { savingsLine, sumSavingsFooters } from '../src/context/savings.js'; |
| 16 | +import { formatSessionStats } from '../src/claude/session-metrics.js'; |
| 17 | + |
| 18 | +/** Run `fn` on a machine whose default locale groups thousands with `locale`'s |
| 19 | + * separator. Restores the real method even if `fn` throws. */ |
| 20 | +function withDefaultLocale<T>(locale: string, fn: () => T): T { |
| 21 | + const real = Number.prototype.toLocaleString; |
| 22 | + Number.prototype.toLocaleString = function ( |
| 23 | + this: number, |
| 24 | + locales?: string | string[], |
| 25 | + options?: Intl.NumberFormatOptions, |
| 26 | + ): string { |
| 27 | + return real.call(this, locales ?? locale, options); |
| 28 | + }; |
| 29 | + try { |
| 30 | + return fn(); |
| 31 | + } finally { |
| 32 | + Number.prototype.toLocaleString = real; |
| 33 | + } |
| 34 | +} |
| 35 | + |
| 36 | +test('#338: the savings footer round-trips on a machine that is not en-US', () => { |
| 37 | + // Sanity: the stand-in really does change what an unqualified call renders, |
| 38 | + // or the rest of this file would pass for the wrong reason. |
| 39 | + assert.equal(withDefaultLocale('de-DE', () => (1990).toLocaleString()), '1.990'); |
| 40 | + |
| 41 | + const line = withDefaultLocale('de-DE', () => savingsLine('x'.repeat(40), { files: 2, baselineChars: 8000 })); |
| 42 | + assert.match(line, /tokens saved ≈ 1,990/, 'grouped for the reader, not for the machine'); |
| 43 | + assert.equal(sumSavingsFooters(line), 1990, 'and read back whole, not as its first group'); |
| 44 | +}); |
| 45 | + |
| 46 | +test('#338: what graft displays groups the same way as what it parses', () => { |
| 47 | + const out = withDefaultLocale('de-DE', () => |
| 48 | + formatSessionStats({ id: 'abc', perAgentQuery: {}, graftReads: 8, sourceReads: 2, savedTokens: 12345 }), |
| 49 | + ); |
| 50 | + assert.match(out, /tokens saved:\s+~12,345/); |
| 51 | +}); |
0 commit comments