From 4f8ba6c190a712c992708116a762890035200598 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Thu, 6 Aug 2026 02:09:27 +0800 Subject: [PATCH] test(tools): pin the U+200C tail/head split; qualify the identifier-equivalence measurement The docstring names ZWNJ/ZWJ as a skew instance but nothing checked the predicate's tail-position accept branch; this is its first test, and it also covers camelCase's Tool-prefix branch for a head that is XID_Continue but not XID_Start. The equivalence sentence pinned its evidence to Node 22.23.1 against CPython 3.9.6 without saying the samples sit inside those two versions' shared tables, next to five named characters where that same pair diverges. --- packages/core/tools/src/code-mode.ts | 8 +++--- packages/core/tools/src/py-types.ts | 9 ++++--- packages/core/tools/tests/py-types.spec.ts | 29 ++++++++++++++++++++++ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/core/tools/src/code-mode.ts b/packages/core/tools/src/code-mode.ts index 4b5cb1fa31..aa4a1f027a 100644 --- a/packages/core/tools/src/code-mode.ts +++ b/packages/core/tools/src/code-mode.ts @@ -300,10 +300,10 @@ export interface RunCodeBridgeOptions { /** Resolves `ctx.codeRuntime` or throws the loud misconfiguration error (shared with the registry's assembly-time checks). */ requireRuntime: () => CodeRuntime /** - * Reads `ctx.codeRuntime` without throwing: `undefined` when none is - * mounted. Lets schema emission tell "no runtime" (degrade to TS; the - * readers that reach it are {@link resolveFlavor}'s) apart from "unknown - * language" (fail loud). + * Reads `ctx.codeRuntime` without throwing: `undefined` when none is mounted. + * Lets schema emission tell "no runtime" (degrade to TS; the readers that + * reach it are {@link resolveFlavor}'s) apart from "unknown language" (fail + * loud). */ peekRuntime: () => CodeRuntime | undefined /** The run's overlap cap for parallel-classified sub-calls (the registry passes its validated `maxParallelSubCalls`). */ diff --git a/packages/core/tools/src/py-types.ts b/packages/core/tools/src/py-types.ts index 69aa63fb2d..d1358124c3 100644 --- a/packages/core/tools/src/py-types.ts +++ b/packages/core/tools/src/py-types.ts @@ -42,10 +42,11 @@ const IDENTIFIER = /^[\p{XID_Start}_]\p{XID_Continue}*$/u * take the subscript path, which carries their exact bytes. * * `IDENTIFIER`'s equivalence to `str.isidentifier()` was measured across 21 - * samples with zero divergence, on Node 22.23.1 against CPython 3.9.6. The - * predicate as a whole is deliberately stricter than `isidentifier()`, which - * does not test NFKC stability: `'field'.isidentifier()` is True and this - * returns false. + * samples with zero divergence, on Node 22.23.1 against CPython 3.9.6 — every + * sample sits inside the two versions' shared tables, and the skew characters + * below are exactly where that pair diverges. The predicate as a whole is + * deliberately stricter than `isidentifier()`, which does not test NFKC + * stability: `'field'.isidentifier()` is True and this returns false. * * Both conditions are evaluated against the ENGINE's Unicode tables, and the * two sides are versioned independently — `\p{XID_Start}`/`\p{XID_Continue}` diff --git a/packages/core/tools/tests/py-types.spec.ts b/packages/core/tools/tests/py-types.spec.ts index 8004a330e7..7a3a573349 100644 --- a/packages/core/tools/tests/py-types.spec.ts +++ b/packages/core/tools/tests/py-types.spec.ts @@ -466,6 +466,35 @@ describe('renderToolsSdkPy', () => { expect(text).not.toContain('field:') }) + it('keeps U+200C in a name tail while rejecting it at a name head, per the two XID properties', () => { + // ZWNJ carries `XID_Continue` and not `XID_Start`, so the predicate splits + // on position: bare in a tail, subscripted at a head. Both verdicts are + // stable across the supported engines — the property arrives in Unicode + // 15.1 and the floor (Node 22.19.0, Unicode 16.0) is past it. + // + // The interpreter side is where this one skews, and it is the same skew the + // 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 `'a‌b'.isidentifier()` + // is False on both, measured), so the field emitted bare here needs an + // interpreter with 15.1 tables or newer. + const of = (name: string): ToolSdkSchema => ({ + name, + description: `Tool ${name}.`, + parameters: { type: 'object', additionalProperties: false, properties: { 'a‌b': { type: 'string' } } }, + output: { type: 'string' }, + }) + const text = renderToolsSdkPy([of('ping'), of('‌b')]) + expect(text).toContain('async def ping(self, args: PingArgs) -> str:') + expect(text).toContain(' a‌b: 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: Tool‌bArgs) -> str') + expect(text).toContain('class Tool‌bArgs(TypedDict):') + expect(text).not.toContain('async def ‌b') + }) + it('subscripts a tool name that NFKC-normalizes to something else, while declaring a plain Unicode one', () => { // Same split at the tool-name site: `路径` becomes an `async def`, the // ligature name cannot, because `async def find` would define `find`. The