Skip to content

Commit 1b71d76

Browse files
committed
test(a2ui-toolkit): de-flake deep child-chain cycle tests
The two deep-chain cycle tests in validate.test.ts intermittently tripped vitest's 5000ms default timeout on CI (~5358ms), reddening the unit workflow's typescript job repo-wide. Root cause is not the validator: validateA2UIComponents on the 50k chain runs in ~59ms (build 6ms + validate 53ms). findChildCycles is already a linear, Set/Map-backed iterative DFS — no quadratic scan to fix. The flake is GC/scheduling jitter: each test allocates ~150k objects (50k components + 50k DFS frames + path/adjacency), and two such tests under parallel-fork contention on a 2-vCPU runner stall on GC, inflating wall-clock far past CPU time. Fix (test-only — validator left untouched, it is optimal): - N 50000 -> 20000 in both deep tests. V8's recursive overflow depth is ~8807 (trivial frame; lower for a real DFS frame), so 20k is still >2x past it and proves the walk is iterative, while cutting allocations 2.5x to shrink the GC-stall window. - Add an explicit 30000ms timeout to decouple the provably-fast assertion from CI runner scheduling jitter. Toolkit suite: 84/84 pass, validate.test.ts ~103ms.
1 parent 7248d71 commit 1b71d76

1 file changed

Lines changed: 33 additions & 19 deletions

File tree

sdks/typescript/packages/a2ui-toolkit/src/__tests__/validate.test.ts

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -179,25 +179,39 @@ describe("validateA2UIComponents — child cycles", () => {
179179
expect(r.errors.some((e) => e.code === "child_cycle")).toBe(false);
180180
});
181181

182-
it("handles a pathologically deep child chain without overflowing the stack", () => {
183-
// The cycle check runs on untrusted model output; a deep linear chain that
184-
// would blow a recursive DFS's call stack must validate iteratively. 50k deep
185-
// is well past V8's recursion limit but a no-op for the explicit-stack walk.
186-
const N = 50000;
187-
const comps: Array<Record<string, unknown>> = [{ id: "root", component: "Row", children: ["n0"] }];
188-
for (let i = 0; i < N; i++) comps.push({ id: `n${i}`, component: "Row", children: i + 1 < N ? [`n${i + 1}`] : [] });
189-
const r = validateA2UIComponents({ components: comps });
190-
expect(r.errors.some((e) => e.code === "child_cycle")).toBe(false);
191-
});
192-
193-
it("detects a cycle that closes at the end of a deep chain", () => {
194-
// Same deep chain, but the tail points back at root — one cycle, no overflow.
195-
const N = 50000;
196-
const comps: Array<Record<string, unknown>> = [{ id: "root", component: "Row", children: ["n0"] }];
197-
for (let i = 0; i < N; i++) comps.push({ id: `n${i}`, component: "Row", children: [i + 1 < N ? `n${i + 1}` : "root"] });
198-
const r = validateA2UIComponents({ components: comps });
199-
expect(r.errors.filter((e) => e.code === "child_cycle").length).toBe(1);
200-
});
182+
it(
183+
"handles a pathologically deep child chain without overflowing the stack",
184+
() => {
185+
// The cycle check runs on untrusted model output; a deep linear chain that
186+
// would blow a recursive DFS's call stack must validate iteratively. 20k deep
187+
// is >2x V8's recursion overflow depth (~8.8k for a trivial frame, lower for
188+
// a real DFS frame), so it still proves the walk is iterative — but allocates
189+
// far less than 50k, keeping GC pressure (and thus the chance of a CI-runner
190+
// stall) low. The explicit-stack walk itself is linear (~25ms for this N).
191+
const N = 20000;
192+
const comps: Array<Record<string, unknown>> = [{ id: "root", component: "Row", children: ["n0"] }];
193+
for (let i = 0; i < N; i++) comps.push({ id: `n${i}`, component: "Row", children: i + 1 < N ? [`n${i + 1}`] : [] });
194+
const r = validateA2UIComponents({ components: comps });
195+
expect(r.errors.some((e) => e.code === "child_cycle")).toBe(false);
196+
},
197+
// The work is ~25ms; the default 5s timeout flaked under parallel-fork GC
198+
// contention on shared CI runners. A generous explicit budget decouples the
199+
// (provably fast) assertion from runner scheduling jitter.
200+
30000,
201+
);
202+
203+
it(
204+
"detects a cycle that closes at the end of a deep chain",
205+
() => {
206+
// Same deep chain, but the tail points back at root — one cycle, no overflow.
207+
const N = 20000;
208+
const comps: Array<Record<string, unknown>> = [{ id: "root", component: "Row", children: ["n0"] }];
209+
for (let i = 0; i < N; i++) comps.push({ id: `n${i}`, component: "Row", children: [i + 1 < N ? `n${i + 1}` : "root"] });
210+
const r = validateA2UIComponents({ components: comps });
211+
expect(r.errors.filter((e) => e.code === "child_cycle").length).toBe(1);
212+
},
213+
30000,
214+
);
201215
});
202216

203217
describe("validateA2UIComponents — data bindings", () => {

0 commit comments

Comments
 (0)