Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions packages/core/src/utils/contextLengthError.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,45 @@ describe('contextLengthError', () => {

expect(info.isExceeded).toBe(false);
});

it('skips accessor properties that throw while collecting error text', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The existing throwing-accessor test covers Error instances, which exercise the safeReadProperty path. Non-Error plain objects with throwing accessors go through a completely different code path — enumerableValues directly — which has no test coverage. Consider adding a test for that path:

it('skips accessor properties that throw on non-Error objects', () => {
  const obj: Record<string, unknown> = {};
  Object.defineProperty(obj, 'detail', {
    enumerable: true,
    get() { throw new TypeError('accessor refused'); },
  });
  Object.defineProperty(obj, 'message', {
    enumerable: true,
    value: 'context_length_exceeded: too many tokens',
  });
  const info = getContextLengthExceededInfo(obj);
  expect(info.isExceeded).toBe(true);
  expect(info.message).toContain('context_length_exceeded');
});

— qwen3.7-max via Qwen Code /review

const error = new Error('Connection error.');

Object.defineProperty(error, 'name', {
enumerable: true,
get() {
throw new TypeError('Value of "this" must be of DOMException');
},
});
Object.defineProperty(error, 'details', {
enumerable: true,
get() {
throw new TypeError('Value of "this" must be of DOMException');
},
});

const info = getContextLengthExceededInfo(error);

expect(info.isExceeded).toBe(false);
expect(info.message).toContain('Connection error.');
});

it('skips throwing accessors on plain objects', () => {
const errorLike: Record<string, unknown> = {};
Object.defineProperty(errorLike, 'detail', {
enumerable: true,
get() {
throw new TypeError('accessor refused');
},
});
Object.defineProperty(errorLike, 'message', {
enumerable: true,
value: 'context_length_exceeded: too many tokens',
});

const info = getContextLengthExceededInfo(errorLike);

expect(info.isExceeded).toBe(true);
expect(info.message).toContain('context_length_exceeded');
});
});
42 changes: 39 additions & 3 deletions packages/core/src/utils/contextLengthError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,32 @@ function tryParseEmbeddedJson(text: string): unknown | undefined {
}
}

function safeReadProperty(value: object, key: string): unknown {
try {
return (value as Record<string, unknown>)[key];
} catch {
return undefined;
}
}

function enumerableValues(value: object): unknown[] {
try {
return Object.values(value);
} catch {
try {
const descriptors = Object.getOwnPropertyDescriptors(value);
return Object.values(descriptors)
.filter(
(descriptor): descriptor is PropertyDescriptor & { value: unknown } =>
'value' in descriptor && descriptor.enumerable === true,
)
.map((descriptor) => descriptor.value);
} catch {
return [];
}
}
}

function collectStrings(
value: unknown,
seen: Set<object>,
Expand Down Expand Up @@ -135,11 +161,21 @@ function collectStrings(

const strings: string[] = [];
if (value instanceof Error) {
strings.push(value.name, value.message);
strings.push(...collectStrings(value.cause, seen, depth + 1));
const name = safeReadProperty(value, 'name');
const message = safeReadProperty(value, 'message');

if (typeof name === 'string') {
strings.push(name);
}
if (typeof message === 'string') {
strings.push(message);
}
strings.push(
...collectStrings(safeReadProperty(value, 'cause'), seen, depth + 1),
);
}

for (const [, nested] of Object.entries(value)) {
for (const nested of enumerableValues(value)) {
strings.push(...collectStrings(nested, seen, depth + 1));
}

Expand Down
Loading