import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, expectTypeOf, it } from "vitest"; import type { DenyWithReason as SchemaDenyWithReason, FlatPermissionConfig as SchemaFlatPermissionConfig, PatternValue as SchemaPatternValue, PermissionState as SchemaPermissionState, } from "#src/config-schema"; import { buildPermissionsJsonSchema, PERMISSIONS_SCHEMA_URL, unifiedConfigSchema, } from "#src/config-schema"; import type { DenyWithReason, FlatPermissionConfig, PatternValue, PermissionState, } from "#src/types"; describe("unifiedConfigSchema", () => { describe("valid configs", () => { it("accepts a full config with runtime knobs and flat permission", () => { const result = unifiedConfigSchema.safeParse({ debugLog: true, permissionReviewLog: false, yoloMode: true, toolInputPreviewMaxLength: 1000, toolTextSummaryMaxLength: 120, piInfrastructureReadPaths: ["/extra/path"], permission: { "*": "ask", read: "allow", bash: { "*": "ask", "git status": "allow", "npm *": { action: "deny", reason: "Use pnpm instead" }, }, }, }); expect(result.success).toBe(true); }); it("accepts an empty config", () => { expect(unifiedConfigSchema.safeParse({}).success).toBe(true); }); it("accepts a $schema field", () => { expect( unifiedConfigSchema.safeParse({ $schema: "https://example.com/s.json" }) .success, ).toBe(true); }); }); describe("invalid configs are rejected", () => { it("rejects an unknown top-level key", () => { const result = unifiedConfigSchema.safeParse({ unknownField: "x" }); expect(result.success).toBe(false); }); it("rejects a non-boolean debugLog", () => { const result = unifiedConfigSchema.safeParse({ debugLog: "yes" }); expect(result.success).toBe(false); if (!result.success) { expect(result.error.issues[0]?.path).toEqual(["debugLog"]); } }); it("rejects a non-integer toolInputPreviewMaxLength", () => { expect( unifiedConfigSchema.safeParse({ toolInputPreviewMaxLength: 1.5 }) .success, ).toBe(false); }); it("rejects a zero toolInputPreviewMaxLength", () => { expect( unifiedConfigSchema.safeParse({ toolInputPreviewMaxLength: 0 }).success, ).toBe(false); }); it("rejects a non-string entry in piInfrastructureReadPaths", () => { expect( unifiedConfigSchema.safeParse({ piInfrastructureReadPaths: ["a", 1] }) .success, ).toBe(false); }); it("rejects a string permission value", () => { expect( unifiedConfigSchema.safeParse({ permission: "allow" }).success, ).toBe(false); }); it("rejects an invalid PermissionState inside a permission map", () => { const result = unifiedConfigSchema.safeParse({ permission: { write: "invalid" }, }); expect(result.success).toBe(false); }); it("rejects a deny-with-reason with a non-string reason", () => { const result = unifiedConfigSchema.safeParse({ permission: { bash: { "npm *": { action: "deny", reason: 42 } } }, }); expect(result.success).toBe(false); }); }); describe("shellTools field", () => { it("accepts a shellTools map with a full alias", () => { const result = unifiedConfigSchema.safeParse({ shellTools: { exec_command: { commandArgument: "cmd", workdirArgument: "workdir" }, }, }); expect(result.success).toBe(true); }); it("accepts an alias with only commandArgument", () => { const result = unifiedConfigSchema.safeParse({ shellTools: { exec_command: { commandArgument: "cmd" } }, }); expect(result.success).toBe(true); }); it("accepts ask as a shell alias decision floor", () => { const result = unifiedConfigSchema.safeParse({ shellTools: { ssh_bash: { commandArgument: "command", decisionFloor: "ask" }, }, }); expect(result.success).toBe(true); }); it.each(["allow", "deny", "invalid"] as const)( "rejects %s as a shell alias decision floor", (decisionFloor) => { const result = unifiedConfigSchema.safeParse({ shellTools: { ssh_bash: { commandArgument: "command", decisionFloor } }, }); expect(result.success).toBe(false); }, ); it("rejects an alias missing commandArgument", () => { const result = unifiedConfigSchema.safeParse({ shellTools: { exec_command: { workdirArgument: "workdir" } }, }); expect(result.success).toBe(false); }); it("rejects an unknown field inside an alias", () => { const result = unifiedConfigSchema.safeParse({ shellTools: { exec_command: { commandArgument: "cmd", extra: "x" } }, }); expect(result.success).toBe(false); }); it("rejects a non-string commandArgument", () => { const result = unifiedConfigSchema.safeParse({ shellTools: { exec_command: { commandArgument: 42 } }, }); expect(result.success).toBe(false); }); it("rejects an empty-string commandArgument", () => { const result = unifiedConfigSchema.safeParse({ shellTools: { exec_command: { commandArgument: "" } }, }); expect(result.success).toBe(false); }); }); }); describe("inferred types match the hand-written domain types", () => { it("PermissionState is equivalent", () => { expectTypeOf().toEqualTypeOf(); }); it("DenyWithReason is equivalent", () => { expectTypeOf().toEqualTypeOf(); }); it("PatternValue is equivalent", () => { expectTypeOf().toEqualTypeOf(); }); it("FlatPermissionConfig is equivalent", () => { expectTypeOf().toEqualTypeOf(); }); }); describe("buildPermissionsJsonSchema", () => { const schema = buildPermissionsJsonSchema(); it("targets Draft 2020-12", () => { expect(schema.$schema).toBe("https://json-schema.org/draft/2020-12/schema"); }); it("sets the root $id to the monorepo raw URL", () => { expect(schema.$id).toBe(PERMISSIONS_SCHEMA_URL); expect(schema.$id).toContain("gotgenes/pi-packages"); }); it("forbids additional top-level properties", () => { expect(schema.additionalProperties).toBe(false); }); it("extracts the shared sub-schemas into $defs", () => { const defs = schema.$defs as Record; expect(Object.keys(defs).sort()).toEqual([ "denyWithReason", "permissionMap", "permissionState", ]); }); it("preserves markdownDescription for editor hovers", () => { expect(typeof schema.markdownDescription).toBe("string"); const properties = schema.properties as Record< string, Record >; expect(typeof properties.yoloMode.markdownDescription).toBe("string"); }); it("preserves the permission examples", () => { const properties = schema.properties as Record< string, Record >; expect(Array.isArray(properties.permission.examples)).toBe(true); }); }); describe("committed schemas/permissions.schema.json is in sync", () => { it("equals the generated schema (run `pnpm run gen:schema` if this fails)", () => { const committedPath = join( import.meta.dirname, "..", "schemas", "permissions.schema.json", ); const committed = JSON.parse( readFileSync(committedPath, "utf-8"), ) as unknown; expect(committed).toEqual(buildPermissionsJsonSchema()); }); });