mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 16:45:22 +00:00
254 lines
7.7 KiB
TypeScript
254 lines
7.7 KiB
TypeScript
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<SchemaPermissionState>().toEqualTypeOf<PermissionState>();
|
|
});
|
|
|
|
it("DenyWithReason is equivalent", () => {
|
|
expectTypeOf<SchemaDenyWithReason>().toEqualTypeOf<DenyWithReason>();
|
|
});
|
|
|
|
it("PatternValue is equivalent", () => {
|
|
expectTypeOf<SchemaPatternValue>().toEqualTypeOf<PatternValue>();
|
|
});
|
|
|
|
it("FlatPermissionConfig is equivalent", () => {
|
|
expectTypeOf<SchemaFlatPermissionConfig>().toEqualTypeOf<FlatPermissionConfig>();
|
|
});
|
|
});
|
|
|
|
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<string, unknown>;
|
|
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<string, unknown>
|
|
>;
|
|
expect(typeof properties.yoloMode.markdownDescription).toBe("string");
|
|
});
|
|
|
|
it("preserves the permission examples", () => {
|
|
const properties = schema.properties as Record<
|
|
string,
|
|
Record<string, unknown>
|
|
>;
|
|
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());
|
|
});
|
|
});
|