test(tools): escape U+200C in the new case and name both carriers of the 15.1 requirement

The file's convention is a \uXXXX escape for a character with no visible
width (\u0301, \u1100, \u1161, \ud800 are all written that way) and a
literal only for a visible one; the new case wrote nine raw ZWNJ.

The comment also named only the field as needing 15.1 tables. Two emitted
code positions do: the bare field, once in each class, and the
Tool\u200CbArgs class name. The subscript comment is not one.
This commit is contained in:
Chinesezjc
2026-08-06 02:22:51 +08:00
parent 4f8ba6c190
commit 631d3f930e

View File

@@ -476,23 +476,25 @@ describe('renderToolsSdkPy', () => {
// docstring's four other characters record, reached in a tail position
// instead of at a head: CPython reads XID_Continue out of the
// `DerivedCoreProperties.txt` of the UCD it was built against (13.0.0 on
// 3.9.6 and 15.0.0 on 3.12.13 both lack the row, and `'ab'.isidentifier()`
// is False on both, measured), so the field emitted bare here needs an
// interpreter with 15.1 tables or newer.
// 3.9.6 and 15.0.0 on 3.12.13 both lack the row, and
// `'a\u200Cb'.isidentifier()` is False on both, measured). Two emitted
// positions then need 15.1 tables or newer: the bare field, once in each
// class, and the `Tool\u200CbArgs` class name. The subscript comment
// quoting the tool name is not one: it is not parsed as an identifier.
const of = (name: string): ToolSdkSchema => ({
name,
description: `Tool ${name}.`,
parameters: { type: 'object', additionalProperties: false, properties: { 'ab': { type: 'string' } } },
parameters: { type: 'object', additionalProperties: false, properties: { 'a\u200Cb': { type: 'string' } } },
output: { type: 'string' },
})
const text = renderToolsSdkPy([of('ping'), of('b')])
const text = renderToolsSdkPy([of('ping'), of('\u200Cb')])
expect(text).toContain('async def ping(self, args: PingArgs) -> str:')
expect(text).toContain(' ab: NotRequired[str]')
expect(text).toContain(' a\u200Cb: NotRequired[str]')
// A head that is XID_Continue but not XID_Start takes the subscript path,
// and `camelCase` prefixes `Tool` to make the class name start legally.
expect(text).toContain('# tools["b"](args: ToolbArgs) -> str')
expect(text).toContain('class ToolbArgs(TypedDict):')
expect(text).not.toContain('async def b')
expect(text).toContain('# tools["\u200Cb"](args: Tool\u200CbArgs) -> str')
expect(text).toContain('class Tool\u200CbArgs(TypedDict):')
expect(text).not.toContain('async def \u200Cb')
})
it('subscripts a tool name that NFKC-normalizes to something else, while declaring a plain Unicode one', () => {