fix(code-runtime-python): stop overclaiming O(cap) object metering

checkDoneValue cannot bound object width sublinearly: JS has no lazy own-key
iterator (for...in materializes the key set), and done.value is already
JSON.parse'd before the check runs, so the frame's width is paid upstream. The
genuine width bound is the host's fixed 256 MiB fd-3 receive buffer (a later
stack layer). Reword the JSDoc and branch comments to claim only what holds —
the traversal caps the INCREMENTAL allocation the check would add (escaped
strings, enqueued children, per-key stringify) and refuses over-budget before
those secondary allocations — and drop the mid-count micro-check that JS cannot
honor. Replace the Proxy test (whose ownKeys allocated a 2M array, proving
nothing) with assertions that an over-budget string/array/object is refused
before its escaped copy or child enqueue.
This commit is contained in:
Chinesezjc
2026-08-07 13:27:54 +08:00
parent 9dc9113ed7
commit ae8070d799
2 changed files with 37 additions and 52 deletions
@@ -194,34 +194,23 @@ describe('checkDoneValue', () => {
}
})
it('stops early on a huge value instead of measuring it whole', () => {
it('rejects an over-budget value before its secondary allocations', () => {
// A huge string is refused on the cheap length lower bound, before its
// escaped copy is built.
const huge = { data: 'x'.repeat(1_000_000), tail: 'y' }
expect(checkDoneValue(huge, 1024)).toEqual({ ok: false, reason: 'over-budget' })
// A forged flat array below the frame ceiling must fail BEFORE its
// elements are enqueued — the pre-enqueue bound keeps the walk O(cap).
// A flat array far above the budget fails on the brackets+length bound,
// before its elements are pushed onto the traversal stack. (The array is
// already materialized by the upstream parse; this only avoids the extra
// per-element stack growth.)
const flat = new Array(10_000_000).fill(0)
expect(checkDoneValue(flat, 1024)).toEqual({ ok: false, reason: 'over-budget' })
// Same bound for a wide object: braces+commas fit the cap, but the
// per-entry lower bound (quoted key + colon + value) does not, so it fails
// before any key is metered or any value enqueued.
// A wide object: braces+commas fit the cap, but the per-entry lower bound
// (quoted key + colon + value = count*4) does not, so it fails before any
// key is escaped or any value enqueued.
const wide: Record<string, number> = {}
for (let i = 0; i < 10; i++) wide[`k${i}`] = i
expect(checkDoneValue(wide, 12)).toEqual({ ok: false, reason: 'over-budget' })
// A forged object with millions of keys and a small cap must reject in
// O(cap): the key COUNT loop itself bails once the running minimum encoding
// (braces + 4 bytes/entry + commas) crosses the budget, rather than walking
// the whole breadth before checking. Observable as a bounded key subset:
// build a Proxy whose ownKeys would yield far more than the cap admits and
// assert the metered walk never enumerates past it.
let enumerated = 0
const millionKeys = new Proxy({}, {
ownKeys() { return Array.from({ length: 2_000_000 }, (_unused, i) => `k${i}`) },
getOwnPropertyDescriptor() { enumerated += 1; return { enumerable: true, configurable: true, value: 0 } },
})
expect(checkDoneValue(millionKeys, 64)).toEqual({ ok: false, reason: 'over-budget' })
// With cap 64, at most ~16 entries (4 bytes each) can fit before the bound
// trips, so the walk enumerates far fewer than the 2,000,000 declared keys.
expect(enumerated).toBeLessThan(1000)
})
it('rejects an over-budget string on its length before escaping it', () => {