From e4dddb4ad52eedae44d8f2984be9042f8ddcbe81 Mon Sep 17 00:00:00 2001 From: PJ Date: Sat, 6 Jun 2026 12:49:28 +0530 Subject: [PATCH] test(web-runtime): guard sanitize cycle, function, and depth limits --- pkg/spec/test/web-runtime.test.ts | 48 +++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/pkg/spec/test/web-runtime.test.ts b/pkg/spec/test/web-runtime.test.ts index 2ee1e3b..7c176eb 100644 --- a/pkg/spec/test/web-runtime.test.ts +++ b/pkg/spec/test/web-runtime.test.ts @@ -183,3 +183,51 @@ test("an uncaught cross-extractor read aborts evaluateExtractors", () => { /inside another extractor is not allowed/, ); }); + +// sanitize runs over every extractor's return value before it leaves the +// runtime. A user extractor that returns a page object reachable from +// document/window can be self-referential, carry functions, or nest deeply; +// without cycle, function, and depth guards extraction overflows the stack or +// emits non-serializable values. These exercise sanitize via the real path. +function sanitizeViaExtract(value: unknown): unknown { + __testing__.extractors.length = 0; + __testing__.runtime.extract(() => value); + let out: Record = {}; + withState(() => { + out = __testing__.evaluateExtractors(); + }); + return out[0]; +} + +test("sanitize breaks a self-referential cycle instead of overflowing", () => { + const cyclic: Record = { name: "root" }; + cyclic.self = cyclic; + const result = sanitizeViaExtract(cyclic) as Record; + assert.equal(result.name, "root"); + assert.equal(result.self, null); +}); + +test("sanitize drops function-valued properties", () => { + const result = sanitizeViaExtract({ keep: 1, fn: () => 7 }) as Record; + assert.deepEqual(result, { keep: 1 }); +}); + +test("sanitize drops a top-level function to undefined", () => { + assert.equal(sanitizeViaExtract(() => 7), undefined); +}); + +test("sanitize bounds recursion past its depth limit", () => { + let deep: Record = { leaf: true }; + for (let i = 0; i < 40; i++) deep = { next: deep }; + // Walk to the depth cap; beyond it sanitize must yield null, not recurse on. + let node: unknown = sanitizeViaExtract(deep); + for (let i = 0; i < 32 && node && typeof node === "object"; i++) { + node = (node as Record).next; + } + assert.equal(node, null); +}); + +test("sanitize preserves arrays and nested plain values", () => { + const result = sanitizeViaExtract({ items: [1, "two", { ok: true }] }); + assert.deepEqual(result, { items: [1, "two", { ok: true }] }); +});