pi-mono/packages/schema/test
robopipi[bot] 2bd2c2d0a6
fix(schema): apply a relative root $id only once when resolving references (#11726)
A schema whose root `$id` was a relative path with a directory part,
such as `schemas/tool.json`, could not resolve its local `$ref`s. The
`Resolver` constructor applied the root `$id` to `DEFAULT_BASE` once to
compute `rootBase`. The compiler, the error explainer and the coercer
start from `rootBase` and call `baseFor(root, rootBase)`, which applied
the `$id` a second time. Local references therefore resolved against
`https://json-schema.invalid/schemas/schemas/tool.json`.

The effect was that `compile(schema)` threw `Unresolved $ref
#/$defs/count`. With `unresolvedRefs: "reject"`, which is how tool
arguments are validated (pi-ai's `validateToolArguments`, used by the
agent loop and durable, and durable's `structuredOutput` check), valid
arguments were rejected and the tool never ran. Absolute `$id`s and bare
names like `tool.json` were not affected, because resolving them twice
gives the same URI.

The fix:
- `rootBase` is now `DEFAULT_BASE`; walkers apply the root's own `$id`
through `baseFor`.
- `baseFor` indexes the schema before applying an `$id` and then returns
the indexed base, so a caller whose base already includes the `$id`
cannot apply it again. Without this,
`unevaluatedProperties`/`unevaluatedItems` still failed: `build()`
passes the already-based node to `annotating()`, which applied the `$id`
once more.
- The coercer resolves bases through `baseFor` instead of reading
`baseOf` directly. Otherwise the `rootBase` change broke the first
`parse`/`coerce` of a schema with `$dynamicRef`: the restarted dynamic
compiler is not indexed yet, so a relative `$ref` resolved against
`DEFAULT_BASE` and conversion was silently skipped.
- The explainer drops `$id` from the copy it makes for unevaluated
keywords, and resolves `anyOf`/`oneOf` branch `$ref`s against the
branch's own base (both threw in `errors()` on main for nested resources
with a relative `$id`).

Testing:
- Regression tests in `validator.test.ts` (relative root `$id` with
local `$ref`, with `unevaluatedProperties`/`unevaluatedItems`, error
reporting under nested relative resources) and `coerce.test.ts` (first
coercion of a `$dynamicRef` schema with a relative external `$ref`).
They fail without the change and pass with it.
- Full pi-schema suite: 8712 passed.
- Independently reviewed against edge cases: draft 7 (inert `$id` beside
`$ref`, `#anchor` ids), fragment ids, nested relative chains, anchors,
registered schemas referring back to the root, fresh
`parse`/`coerce`/`errors` after a dynamic restart.

Fixes #11697

Requested by @badlogic

---------

Co-authored-by: triagent <triagent@localhost>
Co-authored-by: Petr Houška <houskape@gmail.com>
2026-11-03 23:14:11 +01:00
..
bench feat(schema): benchmark against ata-validator, Ajv and Zod 2026-11-01 10:22:20 +00:00
fixtures feat(schema): add pi-schema, JSON Schema builders and validation without eval 2026-10-31 21:39:34 +00:00
builders.test.ts feat(schema): add pi-schema, JSON Schema builders and validation without eval 2026-10-31 21:39:34 +00:00
coerce-typebox.test.ts feat(ai,agent,coding-agent): validate tool arguments with pi-schema, drop TypeBox 2026-11-01 10:08:42 +00:00
coerce.test.ts fix(schema): apply a relative root $id only once when resolving references (#11726) 2026-11-03 23:14:11 +01:00
conformance.test.ts feat(schema): add pi-schema, JSON Schema builders and validation without eval 2026-10-31 21:39:34 +00:00
shared.test.ts feat(schema): faster object checks and compile, less memory 2026-10-31 21:59:21 +00:00
suite.ts feat(schema): add pi-schema, JSON Schema builders and validation without eval 2026-10-31 21:39:34 +00:00
typebox-static.test.ts feat(agent): pi-schema's Static types TypeBox-built schemas too, without depending on TypeBox 2026-11-01 11:12:44 +00:00
validator.test.ts fix(schema): apply a relative root $id only once when resolving references (#11726) 2026-11-03 23:14:11 +01:00