fix(tools): complete the Literal parseability attribution and the soft-keyword positions

pyScalar's docstring named only the two code points CPython refuses
anywhere in source. A bare quote, a trailing odd backslash, and a bare
LF/CR break the Literal line just as fatally, and JSON.stringify is what
covers those too. The argument also leaned on an unstated coincidence:
every escape JSON.stringify can emit is a Python escape for the same
character, which is why the emitted text both parses and decodes back to
the declared value. Say both, and assert the second class.

"statement head" does not describe `case`, whose clause block is not a
statement. Split the positions three ways.

Add the mode 'both' by python assembly, pinning the mode-by-language
matrix rather than leaving it to the shared code path.
This commit is contained in:
Chinesezjc
2026-08-05 19:01:18 +08:00
parent 5afcf36ae1
commit dbefb2fa90
3 changed files with 43 additions and 13 deletions

View File

@@ -350,6 +350,21 @@ describe('mode-aware wire contribution', () => {
expect(sdk?.text).toContain('top-level `await`')
})
it("assembles under a python runtime in mode 'both' as well, SDK and schema together", async () => {
// `both` reaches the same wireSchemas/requireCodeRuntime/SDK-section code
// as `code`, so this pins the mode-by-language matrix rather than a
// separate path — including that `schemas()` under `both` projects the
// Python flavor instead of hitting the flavor-table guard.
const { ctx, systemPrompt } = await setup({ mode: 'both', runtime: { language: 'python' } })
registerEcho(ctx)
const assembly = await systemPrompt.assemble()
expect(assembly.sections.find(section => section.name === 'tools:sdk')?.text).toContain('class Tools(Protocol):')
const runCodeSchema = assembly.tools.find(tool => tool.name === RUN_CODE_NAME)
expect(runCodeSchema?.description).toContain('Execute a Python program')
// `both` keeps the native tools alongside run_code; `code` does not.
expect(assembly.tools.map(tool => tool.name)).toContain('echo')
})
it('emits a TypeScript-flavored run_code schema under a typescript runtime', async () => {
const { ctx, systemPrompt } = await setup({ mode: 'code', runtime: { language: 'typescript' } })
registerEcho(ctx)

View File

@@ -52,12 +52,18 @@ describe('jsonSchemaToPy', () => {
})
it('leans on JSON.stringify to keep a Literal parseable', () => {
// The two code points CPython refuses in source reach this path as well,
// and nothing here escapes them itself — `JSON.stringify` does, NUL as a
// C0 control and a lone surrogate under ES2019 well-formed stringification.
// Python decodes both escapes back to the value the schema declared.
// Nothing here escapes anything itself; `JSON.stringify` carries both
// classes of hazard. The two code points CPython refuses anywhere in
// source: NUL, and a lone surrogate under ES2019 well-formed
// stringification.
expect(jsonSchemaToPy({ type: 'string', const: 'a\u0000b' })).toBe(String.raw`Literal["a\u0000b"]`)
expect(jsonSchemaToPy({ type: 'string', enum: ['a\ud800b'] })).toBe(String.raw`Literal["a\ud800b"]`)
// And the ones that break this line in particular: a bare quote closing
// the literal early, a trailing backslash eating the closing quote, a bare
// newline ending it before its terminator. Every escape it emits is also a
// Python escape for the same character, so the value round-trips.
expect(jsonSchemaToPy({ type: 'string', const: 'say "hi"\n' })).toBe(String.raw`Literal["say \"hi\"\n"]`)
expect(jsonSchemaToPy({ type: 'string', const: 'ends\\' })).toBe(String.raw`Literal["ends\\"]`)
})
it('emits exact digits for a beyond-safe-range integer literal', () => {