From 2a849c0fef5d803cec83655b6a2729db15d2cfb3 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 28 Aug 2026 23:20:20 -0400 Subject: [PATCH] fix(codemode): close symbol cycle gaps --- .../codemode/src/interpreter/references.ts | 2 +- packages/codemode/src/stdlib/object.ts | 1 + packages/codemode/test/stdlib.test.ts | 25 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/codemode/src/interpreter/references.ts b/packages/codemode/src/interpreter/references.ts index 77928e0e638..2682d81585b 100644 --- a/packages/codemode/src/interpreter/references.ts +++ b/packages/codemode/src/interpreter/references.ts @@ -109,7 +109,7 @@ export const rejectCircularInsertion = (container: object, value: unknown, label throw new InterpreterRuntimeError(`${label} contains a circular value.`, node, "InvalidDataValue") if (current === null || typeof current !== "object" || isRuntimeReference(current) || seen.has(current)) continue seen.add(current) - pending.push(Array.isArray(current) ? current[Symbol.iterator]() : childValues(current)) + pending.push(childValues(current)) } } diff --git a/packages/codemode/src/stdlib/object.ts b/packages/codemode/src/stdlib/object.ts index 6d54b1ca34e..bb7c91f7dc1 100644 --- a/packages/codemode/src/stdlib/object.ts +++ b/packages/codemode/src/stdlib/object.ts @@ -70,6 +70,7 @@ export const invokeObjectMethod = (name: string, args: Array, node: Ast continue } if (key !== AsyncIteratorSymbol && key !== IteratorSymbol) continue + if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue const item = Reflect.get(source, key) rejectCircularInsertion(out, item, "Object.assign result", node) Reflect.set(out, key, item) diff --git a/packages/codemode/test/stdlib.test.ts b/packages/codemode/test/stdlib.test.ts index f3b455eba2d..65208511d74 100644 --- a/packages/codemode/test/stdlib.test.ts +++ b/packages/codemode/test/stdlib.test.ts @@ -17,6 +17,8 @@ import { describe, expect, test } from "bun:test" import { Effect, Schema } from "effect" import { CodeMode, Tool } from "../src/index.js" +import { IteratorSymbol } from "../src/interpreter/model.js" +import { invokeObjectMethod } from "../src/stdlib/object.js" // Standard-library value types: Date, RegExp, Map, Set. Programs use them as ordinary JS; // intra-CodeMode checkpoints (Object.* helpers, spread, coercion inputs) preserve the live @@ -824,6 +826,29 @@ describe("stdlib integration", () => { expect(await value(`try { Object.assign(null, { a: 1 }); return false } catch { return true }`)).toBe(true) }) + test("Object.assign ignores non-enumerable supported symbols without reading them", () => { + const target = {} + const reads: Array = [] + const source = Object.defineProperty({}, IteratorSymbol, { + get() { + reads.push(true) + return target + }, + }) + expect(invokeObjectMethod("assign", [target, source], { type: "CallExpression" })).toBe(target) + expect(reads).toEqual([]) + expect(Object.hasOwn(target, IteratorSymbol)).toBe(false) + }) + + test("Object.assign rejects cycles through supported symbols on nested arrays", () => { + const target = {} + const nested = Object.defineProperty([], IteratorSymbol, { enumerable: true, value: target }) + expect(() => invokeObjectMethod("assign", [target, { nested }], { type: "CallExpression" })).toThrow( + "Object.assign result contains a circular value.", + ) + expect(Object.hasOwn(target, "nested")).toBe(false) + }) + test("Object.assign rejects direct and nested cycles", async () => { expect( await value(`