{"version":3,"file":"tool-call.test.d.ts","sourceRoot":"","sources":["../../../src/core/tool-call/tool-call.test.ts"],"names":[],"mappings":"","sourcesContent":["import { describe, expect, it } from \"vitest\";\nimport {\n\tcanonicalStableStringify,\n\tnormalizeToolCall,\n\trecoverTruncatedJson,\n\trunToolCallPipeline,\n\tscavengeToolCall,\n\tsha256Hex,\n\tvalidateAgainstSchema,\n} from \"./index.js\";\nimport type { CanonicalJsonSchema } from \"./types.js\";\n\nconst readSchema: CanonicalJsonSchema = {\n\ttype: \"object\",\n\trequired: [\"path\"],\n\tproperties: {\n\t\tpath: { type: \"string\" },\n\t\tlimit: { type: \"integer\" },\n\t\twhole: { type: \"boolean\" },\n\t},\n};\n\ndescribe(\"tool-call normalization\", () => {\n\tit(\"canonical stable stringify is deterministic\", () => {\n\t\texpect(canonicalStableStringify({ b: 1, a: [2, { y: true, x: 1 }] })).toBe(\n\t\t\tcanonicalStableStringify({ a: [2, { x: 1, y: true }], b: 1 }),\n\t\t);\n\t\texpect(sha256Hex(\"x\")).toHaveLength(64);\n\t});\n\n\tit(\"leaves a valid native call unchanged\", () => {\n\t\tconst normalized = normalizeToolCall({\n\t\t\tname: \"read\",\n\t\t\trawName: \"read\",\n\t\t\ttoolCallId: \"c1\",\n\t\t\tparsedArgs: { path: \"src/index.ts\" },\n\t\t\tschema: readSchema,\n\t\t});\n\t\texpect(normalized.outcome).toBe(\"valid_without_repair\");\n\t\texpect(normalized.name).toBe(\"read\");\n\t\texpect(normalized.args.path).toBe(\"src/index.ts\");\n\t});\n\n\tit(\"coerces string boolean and integer deterministically\", () => {\n\t\tconst normalized = normalizeToolCall({\n\t\t\tname: \"read\",\n\t\t\trawName: \"read\",\n\t\t\ttoolCallId: \"c2\",\n\t\t\tparsedArgs: { path: \"a.ts\", limit: \"42\", whole: \"true\" },\n\t\t\tschema: readSchema,\n\t\t});\n\t\texpect(normalized.outcome).toBe(\"repaired_and_valid\");\n\t\texpect(normalized.args.limit).toBe(42);\n\t\texpect(normalized.args.whole).toBe(true);\n\t\texpect(normalized.repairs.some((r) => r.repairKind === \"coerce-string-integer\")).toBe(true);\n\t\texpect(normalized.repairs.some((r) => r.repairKind === \"coerce-string-boolean\")).toBe(true);\n\t});\n\n\tit(\"collapses a safe singleton into an array\", () => {\n\t\tconst schema: CanonicalJsonSchema = {\n\t\t\ttype: \"object\",\n\t\t\trequired: [\"files\"],\n\t\t\tproperties: { files: { type: \"array\", items: { type: \"string\" } } },\n\t\t};\n\t\tconst normalized = normalizeToolCall({\n\t\t\tname: \"x\",\n\t\t\trawName: \"x\",\n\t\t\ttoolCallId: \"c3\",\n\t\t\tparsedArgs: { files: \"src/a.ts\" },\n\t\t\tschema,\n\t\t});\n\t\texpect(normalized.outcome).toBe(\"repaired_and_valid\");\n\t\texpect(normalized.args.files).toEqual([\"src/a.ts\"]);\n\t\texpect(normalized.repairs.some((r) => r.repairKind === \"singleton-to-array\")).toBe(true);\n\t});\n\n\tit(\"resolves a known tool alias\", () => {\n\t\tconst normalized = normalizeToolCall({\n\t\t\tname: \"ReadFile\",\n\t\t\trawName: \"ReadFile\",\n\t\t\ttoolCallId: \"c4\",\n\t\t\tparsedArgs: { path: \"a.ts\" },\n\t\t\taliasMap: { ReadFile: \"read\" },\n\t\t\tschema: readSchema,\n\t\t});\n\t\texpect(normalized.name).toBe(\"read\");\n\t\texpect(normalized.repairs.some((r) => r.repairKind === \"tool-name-alias\")).toBe(true);\n\t});\n\n\tit(\"fails closed on missing required semantic field\", () => {\n\t\tconst normalized = normalizeToolCall({\n\t\t\tname: \"read\",\n\t\t\trawName: \"read\",\n\t\t\ttoolCallId: \"c5\",\n\t\t\tparsedArgs: {},\n\t\t\tschema: readSchema,\n\t\t});\n\t\t// path is required and absent => invalid_schema (never invented).\n\t\texpect(normalized.outcome).toBe(\"invalid_schema\");\n\t});\n\n\tit(\"never invents a file path\", () => {\n\t\tconst normalized = normalizeToolCall({\n\t\t\tname: \"read\",\n\t\t\trawName: \"read\",\n\t\t\ttoolCallId: \"c6\",\n\t\t\tparsedArgs: { limit: \"3\" },\n\t\t\tschema: readSchema,\n\t\t});\n\t\texpect(normalized.args.path).toBeUndefined();\n\t\texpect(normalized.outcome).toBe(\"invalid_schema\");\n\t});\n});\n\ndescribe(\"truncated JSON recovery\", () => {\n\tit(\"closes an unterminated object\", () => {\n\t\tconst rec = recoverTruncatedJson(`{\"path\":\"src/a.ts\",\"limit\":3`, {});\n\t\texpect(rec.outcome.status).toBe(\"recovered\");\n\t\tconst value = rec.value as { path: string; limit: number };\n\t\texpect(value.path).toBe(\"src/a.ts\");\n\t\texpect(value.limit).toBe(3);\n\t});\n\n\tit(\"closes an unterminated array value string\", () => {\n\t\tconst rec = recoverTruncatedJson(`{\"files\":[\"a\",\"b`, {});\n\t\texpect(rec.outcome.status).toBe(\"recovered\");\n\t\tconst value = rec.value as { files: string[] };\n\t\texpect(value.files).toEqual([\"a\", \"b\"]);\n\t});\n\n\tit(\"reports unrecoverable when structurally impossible\", () => {\n\t\tconst rec = recoverTruncatedJson(`{\"path\": 3, \"limit\": }`, {});\n\t\texpect(rec.outcome.status).toBe(\"unrecoverable\");\n\t});\n\n\tit(\"detects already-valid as not truncated\", () => {\n\t\tconst rec = recoverTruncatedJson(`{\"a\":1}`, {});\n\t\texpect(rec.outcome.status).toBe(\"not_truncated\");\n\t});\n});\n\ndescribe(\"schema validation\", () => {\n\tit(\"enforces required and additionalProperties=false\", () => {\n\t\tconst schema: CanonicalJsonSchema = {\n\t\t\ttype: \"object\",\n\t\t\trequired: [\"path\"],\n\t\t\tproperties: { path: { type: \"string\" } },\n\t\t\tadditionalProperties: false,\n\t\t};\n\t\texpect(validateAgainstSchema({ path: \"a\" }, schema)).toEqual([]);\n\t\texpect(validateAgainstSchema({}, schema).length).toBeGreaterThan(0);\n\t\texpect(validateAgainstSchema({ path: \"a\", extra: 1 }, schema).length).toBeGreaterThan(0);\n\t});\n});\n\ndescribe(\"tool-call scavenging\", () => {\n\tconst schemaById: Record<string, CanonicalJsonSchema> = { read: readSchema };\n\n\tit(\"does NOT scavenge arbitrary prose\", () => {\n\t\tconst result = scavengeToolCall('please run read on src/a.ts using this json: {\"path\":\"a\"}', {\n\t\t\torigin: \"provider_tool_channel\",\n\t\t\tschemaById,\n\t\t\tensureToolExists: () => true,\n\t\t});\n\t\texpect(result).toBeNull();\n\t});\n\n\tit(\"ignores content that is not an unambiguous envelope\", () => {\n\t\tconst result = scavengeToolCall(\n\t\t\t{ text: \"here is some prose\", tone: \"neutral\" },\n\t\t\t{\n\t\t\t\torigin: \"provider_tool_channel\",\n\t\t\t\tschemaById,\n\t\t\t},\n\t\t);\n\t\texpect(result).toBeNull();\n\t});\n\n\tit(\"recoverable documented compat envelope validates\", () => {\n\t\tconst wrapped = { tool_call: { name: \"read\", id: \"z\", arguments: { path: \"src/a.ts\" } } };\n\t\tconst result = scavengeToolCall(wrapped, {\n\t\t\torigin: \"documented_compat_envelope\",\n\t\t\tschemaById,\n\t\t});\n\t\texpect(result).not.toBeNull();\n\t\texpect(result!.normalized.name).toBe(\"read\");\n\t\texpect(result!.normalized.outcome).toBe(\"valid_without_repair\");\n\t});\n\n\tit(\"rejects unknown tool names\", () => {\n\t\tconst result = scavengeToolCall(\n\t\t\t{ tool_call: { name: \"rm_rf\", id: \"q\", arguments: { path: \"/\" } } },\n\t\t\t{\n\t\t\t\torigin: \"documented_compat_envelope\",\n\t\t\t\tschemaById,\n\t\t\t\tensureToolExists: () => false,\n\t\t\t},\n\t\t);\n\t\texpect(result).toBeNull();\n\t});\n});\n\ndescribe(\"pipeline\", () => {\n\tconst schemaById: Record<string, CanonicalJsonSchema> = { read: readSchema };\n\tit(\"normalizes through the pipeline\", () => {\n\t\tconst res = runToolCallPipeline(\n\t\t\t{ name: \"read\", toolCallId: \"c\", arguments: { path: \"a.ts\", limit: \"2\" } },\n\t\t\t{ schemaById },\n\t\t);\n\t\texpect(res.normalized?.outcome).toBe(\"repaired_and_valid\");\n\t\texpect(res.normalized?.args.limit).toBe(2);\n\t});\n\n\tit(\"blocks unknown tools\", () => {\n\t\tconst res = runToolCallPipeline({ name: \"nope\", toolCallId: \"c\", arguments: {} }, { schemaById });\n\t\texpect(res.normalized).toBeNull();\n\t\texpect(res.blockedReason).toContain(\"unknown tool\");\n\t});\n});\n"]}