|
1 | 1 | import { beforeAll, describe, expect, it } from "vitest"; |
2 | | -import { buildConfigSchema } from "./schema.js"; |
| 2 | +import { buildConfigSchema, lookupConfigSchema } from "./schema.js"; |
3 | 3 | import { applyDerivedTags, CONFIG_TAGS, deriveTagsForPath } from "./schema.tags.js"; |
4 | 4 |
|
5 | 5 | describe("config schema", () => { |
@@ -202,4 +202,101 @@ describe("config schema", () => { |
202 | 202 | } |
203 | 203 | } |
204 | 204 | }); |
| 205 | + |
| 206 | + it("looks up a config schema path with immediate child summaries", () => { |
| 207 | + const lookup = lookupConfigSchema(baseSchema, "gateway.auth"); |
| 208 | + expect(lookup?.path).toBe("gateway.auth"); |
| 209 | + expect(lookup?.hintPath).toBe("gateway.auth"); |
| 210 | + expect(lookup?.children.some((child) => child.key === "token")).toBe(true); |
| 211 | + const tokenChild = lookup?.children.find((child) => child.key === "token"); |
| 212 | + expect(tokenChild?.path).toBe("gateway.auth.token"); |
| 213 | + expect(tokenChild?.hint?.sensitive).toBe(true); |
| 214 | + expect(tokenChild?.hintPath).toBe("gateway.auth.token"); |
| 215 | + const schema = lookup?.schema as { properties?: unknown } | undefined; |
| 216 | + expect(schema?.properties).toBeUndefined(); |
| 217 | + }); |
| 218 | + |
| 219 | + it("returns a shallow lookup schema without nested composition keywords", () => { |
| 220 | + const lookup = lookupConfigSchema(baseSchema, "agents.list.0.runtime"); |
| 221 | + expect(lookup?.path).toBe("agents.list.0.runtime"); |
| 222 | + expect(lookup?.hintPath).toBe("agents.list[].runtime"); |
| 223 | + expect(lookup?.schema).toEqual({}); |
| 224 | + }); |
| 225 | + |
| 226 | + it("matches wildcard ui hints for concrete lookup paths", () => { |
| 227 | + const lookup = lookupConfigSchema(baseSchema, "agents.list.0.identity.avatar"); |
| 228 | + expect(lookup?.path).toBe("agents.list.0.identity.avatar"); |
| 229 | + expect(lookup?.hintPath).toBe("agents.list.*.identity.avatar"); |
| 230 | + expect(lookup?.hint?.help).toContain("workspace-relative path"); |
| 231 | + }); |
| 232 | + |
| 233 | + it("normalizes bracketed lookup paths", () => { |
| 234 | + const lookup = lookupConfigSchema(baseSchema, "agents.list[0].identity.avatar"); |
| 235 | + expect(lookup?.path).toBe("agents.list.0.identity.avatar"); |
| 236 | + expect(lookup?.hintPath).toBe("agents.list.*.identity.avatar"); |
| 237 | + }); |
| 238 | + |
| 239 | + it("matches ui hints that use empty array brackets", () => { |
| 240 | + const lookup = lookupConfigSchema(baseSchema, "agents.list.0.runtime"); |
| 241 | + expect(lookup?.path).toBe("agents.list.0.runtime"); |
| 242 | + expect(lookup?.hintPath).toBe("agents.list[].runtime"); |
| 243 | + expect(lookup?.hint?.label).toBe("Agent Runtime"); |
| 244 | + }); |
| 245 | + |
| 246 | + it("uses the indexed tuple item schema for positional array lookups", () => { |
| 247 | + const tupleSchema = { |
| 248 | + schema: { |
| 249 | + type: "object", |
| 250 | + properties: { |
| 251 | + pair: { |
| 252 | + type: "array", |
| 253 | + items: [{ type: "string" }, { type: "number" }], |
| 254 | + }, |
| 255 | + }, |
| 256 | + }, |
| 257 | + uiHints: {}, |
| 258 | + version: "test", |
| 259 | + generatedAt: "test", |
| 260 | + } as unknown as Parameters<typeof lookupConfigSchema>[0]; |
| 261 | + |
| 262 | + const lookup = lookupConfigSchema(tupleSchema, "pair.1"); |
| 263 | + expect(lookup?.path).toBe("pair.1"); |
| 264 | + expect(lookup?.schema).toMatchObject({ type: "number" }); |
| 265 | + expect((lookup?.schema as { items?: unknown } | undefined)?.items).toBeUndefined(); |
| 266 | + }); |
| 267 | + |
| 268 | + it("rejects prototype-chain lookup segments", () => { |
| 269 | + expect(() => lookupConfigSchema(baseSchema, "constructor")).not.toThrow(); |
| 270 | + expect(lookupConfigSchema(baseSchema, "constructor")).toBeNull(); |
| 271 | + expect(lookupConfigSchema(baseSchema, "__proto__.polluted")).toBeNull(); |
| 272 | + }); |
| 273 | + |
| 274 | + it("rejects overly deep lookup paths", () => { |
| 275 | + const buildNestedObjectSchema = (segments: string[]) => { |
| 276 | + const [head, ...rest] = segments; |
| 277 | + if (!head) { |
| 278 | + return { type: "string" }; |
| 279 | + } |
| 280 | + return { |
| 281 | + type: "object", |
| 282 | + properties: { |
| 283 | + [head]: buildNestedObjectSchema(rest), |
| 284 | + }, |
| 285 | + }; |
| 286 | + }; |
| 287 | + |
| 288 | + const deepPathSegments = Array.from({ length: 33 }, (_, index) => `a${index}`); |
| 289 | + const deepSchema = { |
| 290 | + schema: buildNestedObjectSchema(deepPathSegments), |
| 291 | + uiHints: {}, |
| 292 | + version: "test", |
| 293 | + generatedAt: "test", |
| 294 | + } as unknown as Parameters<typeof lookupConfigSchema>[0]; |
| 295 | + |
| 296 | + expect(lookupConfigSchema(deepSchema, deepPathSegments.join("."))).toBeNull(); |
| 297 | + }); |
| 298 | + |
| 299 | + it("returns null for missing config schema paths", () => { |
| 300 | + expect(lookupConfigSchema(baseSchema, "gateway.notReal.path")).toBeNull(); |
| 301 | + }); |
205 | 302 | }); |
0 commit comments