Compare commits

...
5 changed files with 774 additions and 0 deletions
+2
View File
@@ -88,6 +88,7 @@ import { ProviderPlugins } from "./provider.js"
import { WebSearchPlugins } from "./websearch/index.js"
import { SkillPlugin } from "./skill.js"
import { VcsHgPlugin } from "./vcs/hg.js"
import { ToolInputRepairPlugin } from "./tool-input-repair.js"
import { OptimizePlugin } from "./optimize.js"
import { VariantPlugin } from "./variant.js"
import { VcsGitPlugin } from "./vcs/git.js"
@@ -193,6 +194,7 @@ export const requirements = LayerNode.group([
export type InternalPlugin = Plugin<Requirements | Scope.Scope>
const pre = [
ToolInputRepairPlugin.Plugin,
BrowserPlugin,
ConfigMcpPlugin.Plugin,
McpCodeModeExclusionPlugin.Plugin,
@@ -0,0 +1,191 @@
export * as ToolInputRepairPlugin from "./tool-input-repair.js"
import { define } from "@opencode/plugin/effect/plugin"
import type { ToolEditor } from "@opencode/plugin/effect/tool"
import { CodeMode } from "@opencode/codemode"
import { Effect, JsonSchema, Option, Predicate, Schema } from "effect"
import { definition } from "../tool/runtime.js"
// Repairs apply only when the input schema unambiguously supports them:
// - Stringified root or nested object: '{"limit":"20"}' -> { limit: 20 }
// - Closed object: { limit: "20", extra: true } -> { limit: 20 }
// - Optional null or empty-object placeholder: { limit: null } -> {}
// - Numeric or boolean string: { limit: "20", enabled: "false" } -> { limit: 20, enabled: false }
// - Nullable field: { count: "2" } -> { count: 2 }
// - Stringified array or compatible item: { tags: '["a"]', count: "2" } -> { tags: ["a"], count: [2] }
// - Positional tuple: { pair: ["2", "false"] } -> { pair: [2, false] }
// - Typed dictionary: { counts: { first: "2" } } -> { counts: { first: 2 } }
// - Nested fields and local references: { items: [{ count: "2" }] } -> { items: [{ count: 2 }] }
const decodeJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown))
const maxDepth = 6
const executeSchema = Schema.toJsonSchemaDocument(CodeMode.Input).schema
export const Plugin = define({
id: "opencode.tool.input.repair",
effect: Effect.fn(function* (ctx) {
let get: ToolEditor["get"] = () => undefined
yield* ctx.tool.transform((draft) => {
// The draft sees later tool transforms too; reload replaces this lookup.
get = draft.get
})
yield* ctx.tool.hook("execute.before", (event) =>
Effect.sync(() => {
const tool = get(event.tool)
// The outer Code Mode tool is synthesized by snapshots, not registered in the draft.
const schema = tool ? definition(tool).inputSchema : event.tool === "execute" ? executeSchema : undefined
if (schema?.type !== "object") return
event.input = repair(event.input, schema, schema, 0)
}),
)
}),
})
function repair(value: unknown, schema: JsonSchema.JsonSchema, root: JsonSchema.JsonSchema, depth: number): unknown {
if (depth > maxDepth) return value
if (typeof schema.$ref === "string") {
const definitions = /^#\/\$defs\/[^/]+$/.test(schema.$ref)
? root.$defs
: /^#\/definitions\/[^/]+$/.test(schema.$ref)
? root.definitions
: undefined
if (!Predicate.isObject(definitions)) return value
const target = Object.fromEntries(
Object.entries(definitions).filter((entry): entry is [string, JsonSchema.JsonSchema] =>
Predicate.isObject(entry[1]),
),
)[
schema.$ref
.slice(schema.$ref.lastIndexOf("/") + 1)
.replaceAll("~1", "/")
.replaceAll("~0", "~")
]
return target ? repair(value, target, root, depth + 1) : value
}
if (Array.isArray(schema.type)) {
if (value === null && schema.type.includes("null")) return value
if (schema.type.includes(typeof value)) return value
const types = schema.type.filter((type) => type !== "null")
return types.length === 1 ? repair(value, { ...schema, type: types[0] }, root, depth + 1) : value
}
if (schema.type === undefined) {
if (Array.isArray(schema.anyOf) && Array.isArray(schema.oneOf)) return value
const branches = Array.isArray(schema.anyOf) ? schema.anyOf : schema.oneOf
if (!Array.isArray(branches) || value === null) return value
if (branches.some((branch) => !Predicate.isObject(branch) || branch.type === typeof value)) return value
const candidates = branches.filter((branch) => Predicate.isObject(branch) && branch.type !== "null")
return candidates.length === 1 ? repair(value, candidates[0], root, depth + 1) : value
}
if (schema.type === "number" || schema.type === "integer") {
if (typeof value !== "string" || value.trim() === "") return value
const parsed = Number(value)
return Number.isFinite(parsed) && (schema.type !== "integer" || Number.isSafeInteger(parsed)) ? parsed : value
}
if (schema.type === "boolean") return value === "true" ? true : value === "false" ? false : value
if (schema.type === "object") return repairObject(value, schema, root, depth)
if (schema.type === "array") return repairArray(value, schema, root, depth)
return value
}
function repairObject(
value: unknown,
schema: JsonSchema.JsonSchema,
root: JsonSchema.JsonSchema,
depth: number,
): unknown {
const parsed = typeof value === "string" ? Option.getOrUndefined(decodeJson(value)) : value
if (!Predicate.isObject(parsed)) return value
const properties = Predicate.isObject(schema.properties) ? schema.properties : {}
const required = Array.isArray(schema.required) ? schema.required : []
const patterned = Predicate.isObject(schema.patternProperties)
const composed = Array.isArray(schema.allOf) || Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)
return Object.keys(parsed).reduce<Record<string, unknown>>((result, key) => {
const current = result[key]
const declared = Object.hasOwn(properties, key)
const property = declared ? properties[key] : !patterned ? schema.additionalProperties : undefined
if (!declared && schema.additionalProperties === false && !patterned && !composed) {
const next = { ...result }
delete next[key]
return next
}
if (!Predicate.isObject(property)) return result
if (declared && !required.includes(key)) {
const branches = Array.isArray(property.anyOf)
? property.anyOf
: Array.isArray(property.oneOf)
? property.oneOf
: []
const nullable =
property.nullable === true ||
property.type === "null" ||
(Array.isArray(property.type) && property.type.includes("null")) ||
branches.some(
(branch) =>
branch === true ||
(Predicate.isObject(branch) &&
(branch.type === "null" || branch.const === null || typeof branch.$ref === "string")),
)
const placeholder =
Predicate.isObject(current) &&
Object.keys(current).length === 0 &&
typeof property.type === "string" &&
property.type !== "object" &&
branches.length === 0
if ((current === null && !nullable && (property.type !== undefined || branches.length > 0)) || placeholder) {
const next = { ...result }
delete next[key]
return next
}
}
const repaired = repair(current, property, root, depth + 1)
return repaired === current ? result : { ...result, [key]: repaired }
}, parsed)
}
function repairArray(
value: unknown,
schema: JsonSchema.JsonSchema,
root: JsonSchema.JsonSchema,
depth: number,
): unknown {
const parsed = typeof value === "string" ? Option.getOrUndefined(decodeJson(value)) : value
const tuple = Array.isArray(schema.prefixItems)
? schema.prefixItems
: Array.isArray(schema.items)
? schema.items
: undefined
if (Array.isArray(parsed)) {
const repaired = parsed.map((item, index) => {
const member = tuple
? (tuple[index] ?? (Array.isArray(schema.prefixItems) ? schema.items : schema.additionalItems))
: schema.items
return Predicate.isObject(member) ? repair(item, member, root, depth + 1) : item
})
return repaired.every((item, index) => item === parsed[index]) ? parsed : repaired
}
if (tuple || !Predicate.isObject(schema.items)) return value
const repaired = repair(value, schema.items, root, depth + 1)
const type = schema.items.type
const compatible =
type === "object"
? Predicate.isObject(repaired)
: type === "array"
? Array.isArray(repaired)
: type === "integer"
? typeof repaired === "number" && Number.isSafeInteger(repaired)
: type === "number"
? typeof repaired === "number" && Number.isFinite(repaired)
: (type === "string" || type === "boolean") && typeof repaired === type
return compatible ? [repaired] : value
}
+28
View File
@@ -702,6 +702,34 @@ it.effect("does not treat SSE comment heartbeats as model progress", () =>
}),
)
it.effect("preserves valid stringified object tool arguments for execution hooks", () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
const input = '{"count":"2"}'
const raw = JSON.stringify(input)
yield* aisdk.hook.sdk((event) => {
event.sdk = {
languageModel: () =>
streamModel([
{ type: "tool-input-start", id: "call_1", toolName: "lookup" },
{ type: "tool-input-delta", id: "call_1", delta: raw },
{ type: "tool-input-end", id: "call_1" },
{ type: "tool-call", toolCallId: "call_1", toolName: "lookup", input: raw },
{ type: "finish", finishReason: { unified: "tool-calls", raw: "tool_calls" }, usage },
]),
}
})
const resolved = yield* aisdk.model(model("test-ai-sdk"))
const response = yield* LLMClient.generate(LLM.request({ model: resolved, prompt: "Lookup" })).pipe(
Effect.provide(client),
)
expect(response.events.find(LLMEvent.is.toolCall)).toMatchObject({ id: "call_1", name: "lookup", input })
expect(response.events.some(LLMEvent.is.toolInputError)).toBeFalse()
}),
)
it.effect("emits malformed AI SDK tool input without executing it", () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
@@ -0,0 +1,114 @@
import { expect } from "bun:test"
import { Effect, Schema } from "effect"
import { Agent } from "@opencode/core/agent"
import { Plugin } from "@opencode/core/plugin"
import { ToolInputRepairPlugin } from "@opencode/core/plugin/tool-input-repair"
import { Session } from "@opencode/core/session"
import { SessionMessage } from "@opencode/core/session/message"
import { Tool } from "@opencode/core/tool"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const it = testEffect(PluginTestLayer)
const identity = {
sessionID: Session.ID.make("ses_repair"),
agent: Agent.ID.make("build"),
messageID: SessionMessage.ID.make("msg_repair"),
}
it.effect("repairs tool input before validating its original schema", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
const registry = yield* Tool.Service
const executed: unknown[] = []
yield* plugins.activate([
{ ...ToolInputRepairPlugin.Plugin, revision: "1" },
{
id: "repairable-tool",
revision: "1",
effect: (ctx) =>
ctx.tool.transform((draft) =>
draft.add({
name: "repairable",
options: { codemode: false },
description: "Repairable",
input: Schema.Struct({ count: Schema.Int, enabled: Schema.Boolean }),
execute: (input) => Effect.sync(() => executed.push(input)).pipe(Effect.as({ content: "ok" })),
}),
),
},
])
const snapshot = yield* registry.snapshot()
yield* snapshot.execute({
...identity,
call: {
type: "tool-call",
id: "call-repair",
name: "repairable",
input: '{"count":"2","enabled":"true","extra":true}',
},
})
expect(executed).toEqual([{ count: 2, enabled: true }])
yield* registry.transform((draft) => {
draft.update("repairable", (tool) => {
tool.input = Schema.Struct({ count: Schema.Boolean, enabled: Schema.Boolean })
})
})
const updated = yield* registry.snapshot()
yield* updated.execute({
...identity,
call: {
type: "tool-call",
id: "call-updated",
name: "repairable",
input: { count: "false", enabled: "true" },
},
})
expect(executed).toEqual([
{ count: 2, enabled: true },
{ count: false, enabled: true },
])
yield* registry.transform((draft) => draft.remove("repairable"))
const removed = yield* registry.snapshot()
expect(
(yield* removed
.execute({
...identity,
call: { type: "tool-call", id: "call-removed", name: "repairable", input: {} },
})
.pipe(Effect.flip)).message,
).toBe("Unknown tool: repairable")
}),
)
it.effect("repairs outer Code Mode input and looks up namespaced inner tool schemas", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
const registry = yield* Tool.Service
const executed: unknown[] = []
yield* plugins.activate([{ ...ToolInputRepairPlugin.Plugin, revision: "1" }])
yield* registry.transform((draft) =>
draft.add({
name: "count",
options: { namespace: "example" },
description: "Record a count",
input: Schema.Struct({ count: Schema.Int }),
execute: (input) => Effect.sync(() => executed.push(input)).pipe(Effect.as({ content: "ok" })),
}),
)
const snapshot = yield* registry.snapshot()
yield* snapshot.execute({
...identity,
call: {
type: "tool-call",
id: "call-codemode-repair",
name: "execute",
input: JSON.stringify({ code: 'return await tools.example.count({ count: "3" })' }),
},
})
expect(executed).toEqual([{ count: 3 }])
}),
)
@@ -0,0 +1,439 @@
import { describe, expect } from "bun:test"
import { Agent } from "@opencode/core/agent"
import { ToolInputRepairPlugin } from "@opencode/core/plugin/tool-input-repair"
import { Session } from "@opencode/core/session"
import { SessionMessage } from "@opencode/core/session/message"
import type { ToolHooks } from "@opencode/plugin/effect/tool"
import { Tool } from "@opencode/schema/tool"
import { Effect, type JsonSchema } from "effect"
import { it } from "../lib/effect"
import { host } from "./host"
function run(input: unknown, inputSchema: JsonSchema.JsonSchema) {
const event: ToolHooks["execute.before"] = {
tool: "test",
input,
sessionID: Session.ID.make("ses_repair"),
agent: Agent.ID.make("build"),
messageID: SessionMessage.ID.make("msg_repair"),
id: Tool.CallID.make("call_repair"),
}
const events: ToolHooks = {
"execute.before": event,
"execute.after": { ...event, status: "error", error: new Tool.Error({ message: "unused" }) },
}
const base = host()
return ToolInputRepairPlugin.Plugin.effect(
host({
tool: {
...base.tool,
transform: (callback) =>
Effect.sync(() => {
const tool = {
id: "test",
name: "test",
description: "Test repair",
input: inputSchema,
execute: () => Effect.succeed({ content: "unused" }),
}
callback({
list: () => [tool],
get: (id) => (id === tool.id ? tool : undefined),
add: () => {},
namespace: () => {},
update: () => {},
remove: () => {},
})
return { dispose: Effect.void }
}),
hook: (name, callback) => callback(events[name]).pipe(Effect.orDie, Effect.as({ dispose: Effect.void })),
},
}),
).pipe(Effect.as(event))
}
const object = (properties: Record<string, unknown>, required?: string[]) => ({
type: "object" as const,
properties,
...(required ? { required } : {}),
})
describe("tool input repair plugin", () => {
it.effect("preserves valid input identity, nested containers, and unknown properties", () =>
Effect.gen(function* () {
const nested = { enabled: true }
const items = [2, 3]
const input = { count: 2, nested, items, extra: "keep" }
const event = yield* run(
input,
object({
count: { type: "integer" },
nested: object({ enabled: { type: "boolean" } }),
items: { type: "array", items: { type: "number" } },
}),
)
expect(event.input).toBe(input)
expect((event.input as typeof input).nested).toBe(nested)
expect((event.input as typeof input).items).toBe(items)
}),
)
it.effect("parses root objects and repairs nested stringified containers", () =>
Effect.gen(function* () {
const schema = object({
count: { type: "integer" },
item: object({ enabled: { type: "boolean" } }),
list: { type: "array", items: { type: "integer" } },
})
expect(
(yield* run('{"count":"2","item":"{\\"enabled\\":\\"false\\"}","list":"[\\"3\\"]"}', schema)).input,
).toEqual({
count: 2,
item: { enabled: false },
list: [3],
})
expect((yield* run("{broken", schema)).input).toBe("{broken")
expect((yield* run("[]", schema)).input).toBe("[]")
expect((yield* run(null, schema)).input).toBeNull()
}),
)
it.effect("removes extras only from explicitly closed objects without mutating inputs", () =>
Effect.gen(function* () {
const input = {
known: "2",
extra: true,
closed: { keep: "3", extra: true },
open: { keep: "4", extra: true },
items: [{ keep: "5", extra: true }],
}
const event = yield* run(input, {
...object({
known: { type: "integer" },
closed: { ...object({ keep: { type: "integer" } }), additionalProperties: false },
open: object({ keep: { type: "integer" } }),
items: {
type: "array",
items: { ...object({ keep: { type: "integer" } }), additionalProperties: false },
},
}),
additionalProperties: false,
})
expect(event.input).toEqual({
known: 2,
closed: { keep: 3 },
open: { keep: 4, extra: true },
items: [{ keep: 5 }],
})
expect(input.extra).toBeTrue()
expect(input.closed.extra).toBeTrue()
expect(input.items[0]?.extra).toBeTrue()
expect((yield* run({ extra: true }, { ...object({}), additionalProperties: false })).input).toEqual({})
}),
)
it.effect("preserves unknown keys when patterned ownership cannot be determined", () =>
Effect.gen(function* () {
const input = { known: 1, match: "2", extra: true }
const event = yield* run(input, {
...object({ known: { type: "integer" } }),
additionalProperties: false,
patternProperties: { "^match$": { type: "integer" } },
})
expect(event.input).toBe(input)
expect((event.input as typeof input).match).toBe("2")
expect((event.input as typeof input).extra).toBeTrue()
}),
)
it.effect("preserves properties that may belong to composed object schemas", () =>
Effect.gen(function* () {
const input = { name: "example", extra: true }
for (const keyword of ["allOf", "anyOf", "oneOf"]) {
const event = yield* run(input, {
type: "object",
[keyword]: [object({ name: { type: "string" } })],
additionalProperties: false,
})
expect(event.input).toBe(input)
}
}),
)
it.effect("removes only optional nonnullable nulls and non-object empty placeholders", () =>
Effect.gen(function* () {
const input = {
optional: null,
required: null,
nullable: null,
union: null,
constant: null,
permissive: null,
referenced: null,
placeholder: {},
array: {},
requiredPlaceholder: {},
object: {},
unknown: null,
}
const event = yield* run(
input,
object(
{
optional: { type: "string" },
required: { type: "string" },
nullable: { type: "string", nullable: true },
union: { anyOf: [{ type: "integer" }, { type: "null" }] },
constant: { anyOf: [{ type: "integer" }, { const: null }] },
permissive: { anyOf: [{ type: "integer" }, true] },
referenced: { anyOf: [{ type: "integer" }, { $ref: "#/$defs/nullable" }] },
placeholder: { type: "integer" },
array: { type: "array", items: { type: "string" } },
requiredPlaceholder: { type: "boolean" },
object: { type: "object" },
unknown: {},
},
["required", "requiredPlaceholder"],
),
)
expect(event.input).toEqual({
required: null,
nullable: null,
union: null,
constant: null,
permissive: null,
referenced: null,
requiredPlaceholder: {},
object: {},
unknown: null,
})
expect(input.optional).toBeNull()
expect(input.placeholder).toEqual({})
}),
)
it.effect("coerces numeric and boolean strings while preserving invalid and existing values", () =>
Effect.gen(function* () {
const input = {
number: "1.5",
integer: "42",
enabled: "true",
disabled: "false",
valid: 3,
empty: " ",
infinite: "Infinity",
fractional: "1.5",
unsafe: "9007199254740992",
uppercase: "TRUE",
}
const event = yield* run(
input,
object({
number: { type: "number" },
integer: { type: "integer" },
enabled: { type: "boolean" },
disabled: { type: "boolean" },
valid: { type: "integer" },
empty: { type: "number" },
infinite: { type: "number" },
fractional: { type: "integer" },
unsafe: { type: "integer" },
uppercase: { type: "boolean" },
}),
)
expect(event.input).toEqual({ ...input, number: 1.5, integer: 42, enabled: true, disabled: false })
}),
)
it.effect("wraps compatible scalars after repairing array items", () =>
Effect.gen(function* () {
const event = yield* run(
{
text: "one",
integer: "42",
boolean: "false",
item: '{"count":"2"}',
incompatible: 2,
fractional: 1.5,
unconstrained: "4",
},
object({
text: { type: "array", items: { type: "string" } },
integer: { type: "array", items: { type: "integer" } },
boolean: { type: "array", items: { type: "boolean" } },
item: { type: "array", items: object({ count: { type: "integer" } }) },
incompatible: { type: "array", items: { type: "string" } },
fractional: { type: "array", items: { type: "integer" } },
unconstrained: { type: "array" },
}),
)
expect(event.input).toEqual({
text: ["one"],
integer: [42],
boolean: [false],
item: [{ count: 2 }],
incompatible: 2,
fractional: 1.5,
unconstrained: "4",
})
}),
)
it.effect("repairs nested question-like inputs without mutating original containers", () =>
Effect.gen(function* () {
const question = { question: "Pick one", multiple: "false", options: { label: "First", description: null } }
const input = { questions: [question] }
const event = yield* run(
input,
object({
questions: {
type: "array",
items: object({
question: { type: "string" },
multiple: { type: "boolean" },
options: {
type: "array",
items: object({ label: { type: "string" }, description: { type: "string" } }, ["label"]),
},
}),
},
}),
)
expect(event.input).toEqual({
questions: [{ question: "Pick one", multiple: false, options: [{ label: "First" }] }],
})
expect(input).toEqual({ questions: [question] })
expect(question.options.description).toBeNull()
}),
)
it.effect("repairs unique nullable alternatives while preserving accepted union values", () =>
Effect.gen(function* () {
const input = {
number: "2",
boolean: "false",
nullable: null,
typed: "3",
typedBoolean: "true",
accepted: "4",
valid: 5,
}
const event = yield* run(
input,
object({
number: { anyOf: [{ type: "number" }, { type: "null" }] },
boolean: { oneOf: [{ type: "boolean" }, { type: "null" }] },
nullable: { anyOf: [{ type: "number" }, { type: "null" }] },
typed: { type: ["integer", "null"] },
typedBoolean: { type: ["boolean", "null"] },
accepted: { anyOf: [{ type: "string" }, { type: "number" }] },
valid: { type: ["number", "null"] },
}),
)
expect(event.input).toEqual({ ...input, number: 2, boolean: false, typed: 3, typedBoolean: true })
}),
)
it.effect("repairs tuple positions and rest items while preserving valid array identity", () =>
Effect.gen(function* () {
const valid = [2, false]
const input = { prefix: ["2", "false", "3"], draft: '["4","true"]', valid, scalar: "5" }
const event = yield* run(
input,
object({
prefix: {
type: "array",
prefixItems: [{ type: "integer" }, { type: "boolean" }],
items: { type: "number" },
},
draft: { type: "array", items: [{ type: "integer" }, { type: "boolean" }] },
valid: { type: "array", prefixItems: [{ type: "integer" }, { type: "boolean" }] },
scalar: { type: "array", prefixItems: [{ type: "integer" }] },
}),
)
expect(event.input).toEqual({ prefix: [2, false, 3], draft: [4, true], valid, scalar: "5" })
expect((event.input as typeof input).valid).toBe(valid)
expect(input.prefix).toEqual(["2", "false", "3"])
}),
)
it.effect("repairs typed dictionaries and straightforward local references", () =>
Effect.gen(function* () {
const input = {
modern: "2",
legacy: "false",
nested: { count: "3" },
dictionary: { first: "4" },
missing: "5",
pointer: "6",
escaped: "7",
}
const event = yield* run(input, {
...object({
modern: { $ref: "#/$defs/integer" },
legacy: { $ref: "#/definitions/boolean" },
nested: { $ref: "#/$defs/nested" },
dictionary: { type: "object", additionalProperties: { $ref: "#/$defs/integer" } },
missing: { $ref: "#/$defs/missing" },
pointer: { $ref: "#/$defs/nested/properties/count" },
escaped: { $ref: "#/$defs/a~1b~0c" },
}),
$defs: {
integer: { type: "integer" },
"a/b~c": { type: "integer" },
nested: object({ count: { $ref: "#/$defs/integer" } }),
},
definitions: { boolean: { type: "boolean" } },
})
expect(event.input).toEqual({
modern: 2,
legacy: false,
nested: { count: 3 },
dictionary: { first: 4 },
missing: "5",
pointer: "6",
escaped: 7,
})
expect(input.nested.count).toBe("3")
expect(input.dictionary.first).toBe("4")
}),
)
it.effect("leaves ambiguous unions, compositions, and unsupported roots unchanged", () =>
Effect.gen(function* () {
const input = { numeric: "2", objects: { value: "3" }, both: "4", composed: "5", unknown: "6" }
const event = yield* run(
input,
object({
numeric: { anyOf: [{ type: "number" }, { type: "integer" }] },
objects: {
oneOf: [
object({ value: { type: "integer" } }, ["value"]),
object({ value: { type: "number" } }, ["value"]),
],
},
both: { anyOf: [{ type: "integer" }], oneOf: [{ type: "integer" }] },
composed: { allOf: [{ type: "integer" }] },
unknown: {},
}),
)
expect(event.input).toBe(input)
expect((yield* run(input, { properties: { numeric: { type: "integer" } } })).input).toBe(input)
expect((yield* run(input, { allOf: [object({ numeric: { type: "integer" } })] })).input).toBe(input)
}),
)
})